BOARD — next validation candidates, each rooted
Measured 2026-07-27 by running the
-testspipeline over every unbanked candidate the shared-fixture fix structurally unblocked, plus the packages a prior scout left build-blocked. Every entry carries the first and most informative diagnostic, so the next arc starts from a root cause rather than an exploration. Revised 2026-07-27 (later) by the reference-closure arc: the closure family is closed,internal/zstdis banked, and two claims in the original revision are retracted as measurement errors — see the sections below. Corpus state after that arc, plus the 2026-07-29hash/maphashbank and the 2026-07-31image/draw,image/gif,crypto/md5,compress/flate,image/jpeg,image/pngandindex/suffixarraybanks: 69 validated / 215 (32.1%).Revised again 2026-07-31 by the build-blocker arc:
path/filepathandnet— the last two unrooted build blockers — are both fixed at the converter, and both rows moved down into their own sections with what stood behind them. Neither package banks, and the roster is unchanged at 66; a build blocker closing is worth recording precisely because the next wall is now measurable.Revised again 2026-07-31 (r28-net): six of the seven semantic roots the previous revision bucketed for
netare fixed —netis down to 2 errors from one root, and that root is a ruling (testing.T.Deadlineneeds a type the one-testing-package cannot name), not a defect. See thenetsection. The roster is unchanged at 69 — the “66” in the paragraph above was already stale when it was written, and no package banks from this arc.Revised again 2026-08-02 (r35-context):
contextgets its own section below — five converter roots closed,T.Deadlineun-blocked, 36 of 38 verdicts match, and two rooted failures left, one owned by the reflection-bridge arc and one a measured disclosure. The roster is unchanged at 71: nothing banks from that arc. Its most valuable measurement is a negative — the sharpest select/cancellation suite in the standard library finds no channel defect at all, which is independent confirmation of the wave3 landing.Revised again 2026-08-02 (r35-os):
osgets its own section below — it builds with 0 errors and reaches 158 of 178 top-level tests matching + 1 disclosed, up from 48 at the start of the arc. Two converter roots and one host-killer closed; the residual is rooted row by row, and the largest single item (12 unreached) is heap corruption whose crash SITE moves between runs, not a defect at any of the three sites it has been credited to.osdoes not bank and the roster is unchanged at 71.Revised again 2026-08-02 (r37-gob):
encoding/gobgets its own section below — the build blocker that made all 106 of its verdicts read empty is closed at the test-project-model record-anchoring root, and gob is measured for the first time: 86 of 106 match, every mismatch bucketed to one of seven roots. A second converter defect found through it — a dead deref alias that took downunique’s andnet/netip’s package initializers corpus-wide — is fixed in the same arc, though it movesTestNetIP’s site rather than greening it. The roster is unchanged at 71: gob does not bank.Revised again 2026-08-03 (r38-gob-fin): gob moves 86 → 88 of 106 on a converter fix,
uniqueBUILDS for the first time and gets its first census, and four of r37’s seven gob roots turn out to be mis-attributed — see the rewritten gob section. The roster is still 71: neither package banks.Revised again 2026-08-03 (r39-nilcomplex): both converter items r38 handed on ROOTED — the typed-nil BOUNDARY and the
complex()element-width pin — land, and gob moves 88 → 91 of 106. Two of r38’s seven roots close outright; the third typed-nil row does NOT, and its residual is now rooted one layer down in the reflection bridge (see r39-nilcomplex at the end of the gob section). The roster is still 71/72: gob does not bank.Revised again 2026-08-07 (r41c-cloneseq):
unique’smakeCloneSeqroot closes and the package moves 1 → 4 of 19, on aninternal/abihand-own plus a converter fix — but the row’s own DESCRIPTION below was wrong in both halves (it is not aslice<T>enumerator edge, and it is reflection-bridge territory), so read ThemakeCloneSeqroot, CLOSED rather than the table cell. The roster is unchanged at 73:uniquedoes not bank.Revised again 2026-08-07 (r43c-candidates): the first pure measure-first breadth pass — 47 never-measured candidates run back to back through the pipeline. Twenty-three validated on the first run with no converter change of any kind, taking the roster 73 → 96 (44.7%); every one of the twenty-four that did not is rooted in the new section at the end of this file. The finding worth carrying forward is the negative one: the corpus had already grown past those packages and nothing was watching, so the roster’s denominator is limited by who has looked, not by what is broken. Re-scout the tail after any capability lands, not only the packages that capability names.
Revised again 2026-08-07 (r44a-rescout): r43c’s own instruction executed — 108 pipeline runs over BOTH r43c’s rooted non-validators and the 76 never-measured tail packages. Twelve bank, taking the roster 97 → 109 (50.7%) and past the campaign’s 50 % mark; eleven needed nothing, and the twelfth (
internal/cpu) took a one-declaration hand-own. The re-scout of r43c’s OWN roots yielded exactly one package (expvar) and every other rooting re-measured verbatim, which sharpens the instruction rather than repealing it: a rooted non-validator has been looked at; the yield is in what nobody has run. Eighteen packages are now ONE OR TWO ROWS from banking, and the tail’s build blockers are named with their verdict counts — see the r44a section at the end of this file.A note the arc earned: a first diagnostic is a starting point, not a diagnosis.
io’s first error is CS0012 and reads as a missing reference; it is not one. Two of the three claims below that were stated as “measured” did not survive re-measurement on a freshly built converter. r41c is the same lesson at one more remove: the exception TYPE and the frame it is thrown in (IndexOutOfRangeExceptioningo.slice<T>.Enumerator) named a component that had nothing wrong with it, because a garbage slice HEADER two frames up makes a correct enumerator throw.Re-validate everything after any change here with
./src/run-validated-sweep.ps1— it reads the roster and the expected counts fromValidatedTestPackages.mdand fails on a count mismatch, so a package that still passes but asserts something different is caught rather than assumed.
✅ BANKED (2026-08-17, lane claude/connraw-hang) — database/sql validates 137 of 139 as roster row 148; TestConnRaw was never a lock, and the guilty layer was GoFrame.Run
The hypothesis this lane was handed was wrong in every particular, and one dotnet-stack report
against the hung host said so before any code was read — which is the sixth consecutive brief this
week to name the wrong layer, and the reason the instruction to measure first is worth its cost.
The brief’s leading suspicion was a deadlock or lost wakeup in golib’s hand-owned sync.RWMutex
shim, from release(ErrBadConn) → closemuRUnlockCondReleaseConn → Conn.close taking
closemu.Lock() against the test’s own recover handler. No thread was blocked on closemu, or on
any lock. The only test thread was in time.Sleep, inside waitCondition, inside closeDB — the
package’s ordinary teardown poll. sync’s shim is innocent and unchanged; so is the Conn.Raw
emission the previous entry had already cleared.
The hang was never a hang — it is waitCondition sized by t.Deadline()
closeDB ends with waitCondition(t, func() bool { return db.numOpenConns() == 0 }), and
waitCondition does not poll for five seconds when the test has a deadline: it polls for
time.Until(deadline) minus 10 % headroom. So an assertion failure that leaves ONE connection open
does not fail fast — it consumes 90 % of whatever -test-timeout was passed, then reports. That is
the whole of the 3,418 s: at -test-timeout 60m, 0.9 × 60 min. The two previous entries’ 1,712 s and
3,423 s package figures are the same arithmetic at their own deadlines, not machine load and not lane
contention.
Consequence for the board’s reading habits: a Phase-4 row whose C# elapsed lands suspiciously
near 0.9 × -test-timeout is a failing assertion inside a deadline-sized wait, not a deadlock. Run
it again with a small -test-timeout (20 s was enough here) and it prints its real failure in
seconds. Do not reach for dotnet-stack first — but if you do, it answers immediately too.
The root: GoFrame.Run re-raised a panic the frame never caught
Probes on the converted sql.cs caught the panic being re-thrown from a frame that had nothing to do
with it. Conn.Raw’s deferred cleanup reaches Conn.close, which sets c.dc = nil only AFTER
dc.releaseConn → db.putConn → dc.Close → finalClose → withLock. withLock is two lines, it
holds one defer, and it panics nothing — but its finally’s ᒐ.Run() ended with
if (GoFuncRoot.CapturedPanicValue is not null)
throw GoFuncRoot.CapturedPanicValue;
and that slot is the THREAD’s, non-empty for the whole of the panicking frame’s deferred sequence. So
withLock threw Conn.Raw’s panic on the way out, close never reached c.dc = nil, conn.dc
stayed non-nil (the failure the previous entry recorded), the later deferred conn.Close() found
done already true and returned ErrConnDone without releasing (the “1 connections still open”),
and waitCondition sat on it for the rest of the deadline. One root, both recorded symptoms, and
the hang.
The rule is now stated instead of inferred: the re-raise belongs to the frame whose own catch
caught the panic. GoFrame.Capture arms a claim, the next Run on the thread claims it — always
that same frame’s, because nothing runs between an emitted catch body and its finally — and a frame
that caught nothing claims null. The emission is untouched; the change is 33 lines inside
golib/GoFrame.cs + GoFuncRoot.cs, and no converted file moved.
This is a general class, not a database/sql fix: EVERY converted function with a defer was a
spurious re-raise site whenever it was called during another frame’s deferred sequence. Cleanup paths
that call helpers are the common shape, so expect other Phase-4 rows in the “cleanup didn’t finish”
family to move without being touched.
Doctrine: ConversionStrategies-Reference.md — The re-raise of an unrecovered panic belongs to the
frame that CAUGHT it, not to the thread, which also records the one adjacent hole left deliberately
open (a nested frame’s recover() clears the outer frame’s parked panic — same shared slot, predates
this change, no measured consumer).
database/sql — 137 of 139, and the owed $longTimeouts floor is repriced away
139 rows, 137 agree, 2 disclosed, 0 skipped, 27 excluded (the standard Benchmark/Example
deferrals). The two are TestGrabConnAllocs and TestRawBytesAllocs, the standing alloc-profile
class, pinned by signature in a hand-owned go2cs_test_disclosures.json.
No $longTimeouts entry is owed, and the two previous entries’ “'database/sql' = '60m' if it ever
banks” is formally repriced to zero. The converted suite now runs in 3.5 s (Go: 46 s); the whole
-test-action all round trip, both builds included, is 33 s. The sweep’s default deadline clears it
by three orders of magnitude.
Guards, and the corpus footprint
PanicDeferCalleeFrame(new behavioral test) — output-compared againstgo run: the reducedConn.Rawshape (acquire → panic in the callback → cleanup that must still reachopen = false), three deferring callees stacked below one deferred call, and the two negatives (a panic raised by a frame’s own deferred call still escapes it; one raised inside a deferred cleanup still replaces the panic already unwinding). Neuter-verified: reverted, the run diverges from Go.GolibTests.GoFrameTests— four rows pinning the rule at the frame itself, including them_count == 0path that skips the deferred sequence and reaches the tail directly. Neuter-verified: the two that pin the fix fail with the exact truncated-cleanup collection; the other two are the over-suppression negatives and pass either way, by design.- Corpus footprint: zero regenerated files.
database/sql’ssql.csanddatabase.sql.csprojcame back byte-identical from a seeded scoped reconvert, whose only diff was the README’s Tests badge (137/139).
✅ CLOSED (2026-08-16, lane claude/assignableto-arc) — the AssignableTo deferral is retired, with the struct/func/interface identity arms and the chan direction fixed in the same change
The reflect-bridge lane recorded the retirement SEQUENCE rather than the fix, and the sequence was
right in every particular: HasName() had to become truthful first, and the struct and chan arms of
haveIdenticalUnderlyingType had to be fixed with the hand-own’s retirement rather than after it.
Following it turned up one arm the sequence did not name — func — and one it did name as
undecidable turned out to be decidable after all, once the question was asked about the right object.
database/sql moves 135 → 136 of 139. It does not bank: TestConnRaw is still open, and
this lane measured something about it that changes what “open” costs (below).
What each of the four pieces actually was
| Piece | Before | Now |
|---|---|---|
abi.Type.HasName() |
false for EVERY synthesized descriptor — TFlagNamed was never carried |
synthesizeDescriptor stamps the bit from GoReflect.HasGoName, the SAME gate reflect’s own rtype.Name() already stood on |
implements (the free function) |
reinterprets the descriptor as an interfaceType and reads .Methods off a default promoted-embed box — throws for any non-empty interface |
bridged over GoReflect.GoImplements, the probe rtype.Implements and the emitted _<T> asserts already share |
haveIdenticalUnderlyingType struct / func / interface arms |
prefix-downcast; read ZERO fields / ZERO in-out counts / ZERO methods and returned true | Go’s own clauses over GoReflect.GoFields + TryFuncShape, at the reflect level |
ChanDir() (both abi.Type and rtype) |
reads a direction out of the memory FOLLOWING the descriptor’s value slot — non-deterministically | BothDir, the real direction of the only channel type the bridge can describe |
rtype.AssignableTo |
hand-owned as identity-on-the-managed-type + implements | RETIRED — Go’s directlyAssignable(uu.t, t.t) \|\| implements(uu.t, t.t) runs |
The struct arm was worse than recorded, and the recorded shape understated it. The board said “any
two structs of EQUAL FIELD COUNT compare identical”. Measured: the count check itself reads
len(Fields) off the same dead downcast, so it is 0 == 0 for both operands and even a differing
field COUNT compares identical. struct{B []byte; M map[string]int} was reported convertible to the
same struct with M map[string]int64, to one whose second field is merely RENAMED, and to
struct{B []byte}.
The FUNC arm is the one the sequence did not name, and it had to be fixed here. It fails the same
way through InCount/OutCount, so any two func types compared identical. It was already live
through ConvertibleTo, and it would have widened to assignment the moment AssignableTo started
routing through the walk — the exact trade the recorded sequence exists to prevent. It is answered
from GoReflect.TryFuncShape, the same shape rtype.NumIn/In/NumOut/Out read.
The CHAN ruling — decidable, because the question was being asked about the wrong object
The scout recorded ChanDir as “the only one of the family with no synthesis waiting for it:
<-chan int and chan int are both channel<nint>, so a direction is not recoverable from the
managed type for an unnamed directional channel. This one needs a ruling, not a fix.” Both halves of
that are true, and the conclusion still does not follow — because a directional channel type is
never an OPERAND here. The bridge cannot build a descriptor for <-chan int at all; it builds one
for channel<T>, whose direction genuinely is BothDir, and whose Type.String() has said chan T
since the beginning. Answering BothDir is therefore not a guess about something unknown — it is the
correct answer about the descriptor actually being asked, and it makes the descriptor’s kind, name and
direction agree where the downcast made one of the three disagree at random.
The limit is real but it lives one layer up, in the converter’s channel emission: reflect.TypeOf
over a <-chan int reports chan int. That is stated in
ConversionStrategies-Reference.md rather than hidden, and it is not disclosable and needs no
gate, for a specific reason worth recording: no package on the validated roster observes it. The
one corpus consumer that branches on direction is text/template’s walkRange (rejecting a range
over a send-only channel), and text/template is not on the roster — only text/template/parse is.
Recovering the direction would mean carrying it as descriptor cargo the way array dims are carried,
which no measured consumer asks for (the r39d rule). If text/template is ever taken up, this is
the row to expect, and the remedy is cargo, not a disclosure.
Two residuals stated the same way: the interface arm proves “methodless” only for object (Go’s
any), so a defined empty interface with a managed type of its own answers not identical — the
conservative direction, since a false negative degrades a caller to “needs a conversion” while a false
positive is a silent wrong assignment; and a defined methodless func type has no managed identity at
all (the converter renders it inline as its base delegate), so the named/unnamed pairs every other
kind asserts cannot be produced for funcs.
database/sql — 136 of 139, and TestConnRaw is a HANG, which reprices the owed floor
TestUserDefinedBytes flips exactly as predicted, and TestConversions stays passed. 139 rows, 136
agree, 3 disagree, 0 skipped, 0 disclosed, 27 excluded (the standard Benchmark/Example
deferrals). The three: TestGrabConnAllocs and TestRawBytesAllocs, the standing alloc-profile
class, and TestConnRaw.
⚠ The brief for this lane predicted 137 of 139; the arithmetic was one high (135 + the one flip = 136). Recording it so the next planner starts from the measured number.
The new measurement. TestConnRaw does not merely assert wrong — it hangs, and it is the
entire runtime of the package:
| C# | Go | |
|---|---|---|
| whole-package terminal elapsed | 3,423.3 s | 46 s |
TestConnRaw alone |
3,418.2 s | 0.005 s |
| every other test COMBINED | 5.1 s | — |
So the 'database/sql' = '60m' $longTimeouts floor two previous entries record as owed is not
“this package is slow” — it is this one test. Fix TestConnRaw and database/sql runs in about
five seconds and needs no floor entry at all. Nothing is added to $longTimeouts today (the
package still has no roster row, and a floor for a package the sweep never visits is dead
configuration) — but the entry that banks it should re-measure before assuming it owes one. It also
retro-explains the previous two lanes’ 1,712 s and 3,423 s figures, which were read as machine load
and lane contention: both were this test blocking until whatever bound applied.
Where the root is NOT. The converted Conn.Raw emission is structurally correct: fPanic and the
named result err are captured by REFERENCE while dc/release are snapshotted, the deferred lambda
runs from finally { ᒐ.Run(); }, and ᒐdone: return err sits after the try/finally so the deferred
write to err is observed. The suspect path is what the defer CALLS —
release(driver.ErrBadConn) → closemuRUnlockCondReleaseConn (closemu.RUnlock(), then
errors.Is, then c.close(err)) → Conn.close, which takes closemu.Lock(), a WRITE lock, before
setting c.dc = nil, while the test’s own recover handler independently takes conn.closemu.Lock().
A deadlock or lost wakeup in the hand-owned sync.RWMutex shim under that ordering is the leading
hypothesis; errors.Is answering false for an identity comparison is the cheap one to falsify first.
Not chased further here — it is a sync/defer-during-panic question, not an assignability one.
Guards, and the corpus footprint
ReflectConvertAssignable(extended, 14 → 34 compared rows) — Go’s assignability rule clause by clause: both gates of the unnamed↔named rule INCLUDING the two-defined-types negative thatHasName()alone decides, the interface clause both ways, the struct arm against a differing field type / a renamed field / a differing field count / a tag that conversion ignores and assignment honors, the func arm’s parameter and result discrimination, and the chan rows the bridge can truthfully produce. Measured failing-first: the struct and func arms answeredtruewhere Go answersfalse, and theassignrows answeredfalsewhere Go answerstrue.GolibTests.GoStructLayoutTests.EmbeddedField_IsDistinguishableFromADeclaredFieldOfTheSameNameAndType— pins the newGoFieldInfo.Embeddedprojection the struct arm stands on;struct{T}andstruct{T T}agree on field count, name, type, tag and offset and are separated by nothing else. Neuter-verified (removing the flag fails it with the exact assertion).- Corpus footprint: two regenerated files,
internal/abi/type.csandreflect/type.cs(four Go bodies replaced by placeholders, one placeholder replaced by Go’s own restoredAssignableTo).
Doctrine: ConversionStrategies-Reference.md — Go’s ASSIGNABILITY rule, and the identity walk
underneath it (new), which also CLOSES the follow-on recorded at the end of reflect.Type.Name() —
a DEFINED type HAS a name even when its underlying type is a composite: reflectlite.rtype.Name()
becomes truthful for free, since it gates on the bit this lane started carrying.
OPEN — -recurse emission is covered by NO standing gate, and issue #35 proves what that costs (2026-08-08)
Every standing gate measures the behavioral corpus or the standard library. Neither can see a
-recurse-only defect, so an end-user conversion is guarded solely by hand-authored synthetic fixtures —
one per past issue. Issue #35 is the demonstration: a truncated project name put 175 duplicate
.csproj names into a user’s 1,727-project solution (Visual Studio then refuses to open it and says
nothing), and every gate stayed green through it. check-no-regression reported byte-identical C# and
.csproj across all 574 behavioral packages, and -stdlib cannot reach the code at all — it returns on
the GOROOT branch before the module walk. See ConversionStrategies-Reference.md,
A project name is the package’s FULL import path.
That is structural, not bad luck. -recurse is one of the two end-user use cases
(DESIGN-recursive-enduser-conversion.md), and
the only thing exercising it is the nine fixtures in moduleConverter_integration_test.go — three that
cover a mode (TestRecurseSyntheticModule, …NuGetReferences, …ModuleOnly) and six written after the
defect they cover (TestModuleCachePoisonedGoWorkLoad, TestModuleCacheVestigialReplaceLoad,
TestRecurseQuotedModulePath, …KeywordNamespaceSegment, …ChannelOfHyphenatedModulePath,
…GoFileFreeContainerDirsKeepDistinctProjectNames). That is precisely the “enumerate the shapes we have
SEEN rather than state the property we need” tell this file already names as the recurring signature of a
point repair: the six shapes are the ones issues #32, #33 (×3) and #35 happened to hit.
Two increments, the first nearly free:
- State the property. After any
-recurserun the emitted project names must be distinct and each must equal its package’s import path, dotted. That is one assertion overconvertedCsproj, it closes the whole class rather than one shape, and it costs nothing to add to the existing fixtures. (Deliberately as a TEST assertion, not a converter runtime check — post-fix the name IS the import path by construction, so a runtime guard would be machinery for an unreachable state. The value is in pinning the invariant, which is a test’s job.) - One adversarial fixture instead of seven incidental ones. A checked-in, network-free module whose
layout is the union of every shape that has bitten: go-file-free container directories,
internal/, a/vNsubmodule, a quotedmoduledirective, a C#-keyword path element, a hyphenated path, same-named leaf packages, a co-locatedreplace, ago.work. Convert it and golden-compare the emitted.csproj/.slnx/.csthe way the behavioral corpus is compared — which gives-recursethe drift detection it has never had, and makes the next shape a few lines of fixture rather than a new test.
Worth doing before the next end-user report rather than after it: the class has now produced four issues (#33 ×2, #35, and #32’s loader shape), and each arrived from a user rather than from a gate. The derivation’s own recurring-defect row is in the Recurring classes section below.
CLOSED — the ARGUMENT-path exponential is fixed, and the corpus paid its 29-file debt in the same change (2026-08-07, r43a-argexp)
Same bug class as the chained-call exponential closed directly below, one code path over, and closed the same way: stop paying for a traversal whose answer the type system already holds. Nesting depth 22 went from 13.7s to 0.54s, and the whole 302-package standard library still compiles.
What it was. After rendering a call, convCallExpr re-walked every argument for its recording side
effects — the loop at the end of convCallExpr, whose own comment said it “re-converts each arg purely for
its side-effects (recording implicit conversions); the result is discarded” — and
checkForImplicitConversion opened with a full expr := v.convExpr(arg, nil). So every argument subtree
was converted twice: once by convExprList for the emitted text, once again here for the recording. On
NESTED calls — f(f(f(…))), where each argument IS the next call — that compounds to 2^depth.
Fix (convCallExpr.go), exactly the split the rooting designed,
because the premise held on inspection: expr is pure text that flows only to the return value (two
pointer cases wrap it), while every recording decision comes from funcType, argType,
targetTypeName/argTypeName and packageTypeSpecRHS.
applyImplicitConversion(funcType, arg, targetTypeName, expr)— the recording half, type-driven, takes the rendered text as a parameter instead of producing it;checkForImplicitConversion=convExpr+ that, unchanged for its one caller that USES the return (the explicit type-conversion branch);- the discard-the-result loop calls
applyImplicitConversiondirectly with""and converts nothing.
Removing the traversal also retires the hoistedDecls save/restore that bracketed the loop: its only job
was to stop a func-literal argument’s capture decls being written into the hoist buffer a second time by
the very conversion that is now gone.
Measured A/B on the DESKTOP (Windows, this repo’s box — the rooting’s table was a laptop, so both columns are re-measured here). Paired runs, same seeded scratch module, single-package conversion, best of two:
| argument nesting depth | before | after |
|---|---|---|
| 10 | 0.56s | 0.53s |
| 14 | 0.59s | 0.55s |
| 18 | 1.22s | 0.56s |
| 22 | 13.66s | 0.54s |
| 26 | killed at 416s, unfinished | 0.54s |
| 30 | (not attempted — extrapolates past half an hour) | 0.55s |
After is FLAT at the ~0.55s go/packages load floor through depth 30, i.e. the conversion component is
gone, not merely reduced. Before, subtracting that floor leaves a conversion component that doubles per
level: 0.67s at depth 18 → 13.11s at depth 22, a factor of 19.6 over four levels ≈ 2.1× per level. The
excess over a clean 2× is GC of what the doubled traversal allocates, and it compounds — which is what put
depth 26 past 416s of wall at ~1.9 cores without finishing.
Full-stdlib conversion wall. go2cs -stdlib -comments over all 302 packages: 378.9s before →
221.2s after. Read that as directional only — the two runs saw different sibling-lane load on a shared
box (the same before-converter measured 251.2s on an earlier, quieter run), and CLAUDE.md’s own baseline
for this command is ~195–225s, which the after run sits inside. The honest claim is that the argument tax
is real but small against go/packages load time on ordinary code; the fixture is where it is dramatic.
Gates — all green, and the arc is NOT emission-neutral in the way that mattered.
- CNR: 4 of 571 changed (550s) —
FuncLitCaptureInCondition,NilPointerPanic,NilPointerParamMethods,NilReceiverMethods, allmain.cs, all a pure capture-variable RENUMBERING (lookupʗ3/5/7→lookupʗ2/3/4), declaration and every use renamed together. Same mechanism the sibling arc hit: the discarded conversion had been consuming values fromgetCapturedVarName’s monotonic per-prefix counter, so removing it closes the gaps. Verified collision-free (every generated name declared exactly once per file) and then verified where it counts — all four Compile pass and Output pass againstgo run— before re-baselining. Goldens updated with the runner’s--update-targets; only those four.cs.targetmoved, no test-method churn. - Full behavioral suite: 546/546 Transpile, 546/546 Target, C# Compile 0 failed — but its Output
phase was never reached, and that is a machine story, not a result. Three consecutive full runs were
killed externally, each truncated mid-run with no diagnostic (the signature CLAUDE.md documents for a
sibling lane’s name-matched cleanup); one of them also hit
CS8104/CS0016“not enough space on the disk” in the one-shot batch while C: sat at 2.67 GB free, which the runner’s own per-project re-attribution then cleared to 0 failed. What the completed phases DO establish is the part that matters here: all 546 goldens byte-match, so emission is stable across two independent full re-transpiles (CNR’s and the suite’s). Output is covered where it can differ, by filtered runs that completed: the 27 projects spanning all four changed goldens —--filter Nil(25) and--filter FuncLitCapture(2) — pass all four phases, 25 Output-compared againstgo run, 0 failed. For the other 542 the generated C# is byte-identical to HEAD, and byte-identical generated C# ⟹ identical compile+run ⟹ identical results — the same reasoning that makes CNR the authoritative drift instrument. go test ./...: ok, exit 0 (44.9s), including the new guard and the projitems gate.- Full
go2cs-stdlib.slnxbuild: 0 errors (302 projects, 199s) on the overlaid corpus.
The one thing the rooting said to prove, PROVEN — and CNR alone could not have proven it. The recorded
SET is identical: a paired seeded full-stdlib A/B (both roots seeded per the measurement-loop rules, single
run each, seed gate clean at 39 marked files) puts 8,356 of 8,386 files byte-identical, and normalizing
away the numeric suffix of the counter-driven generated identifiers makes the other 30 identical too —
zero residual differences. Not one package_info.cs, .csproj or README.md moved anywhere in the
corpus, which is exactly where a divergent recording would have surfaced (recorded conversions land in
package_info.cs and drive ImplicitConvGenerator).
The 30 split two ways, both counter renumbering: 19 capture (ʗN, getCapturedVarName’s per-prefix
counter), 10 type-switch temp (ᴛN, getGlobalTempVarName("switch")), one file both. The ᴛN
counter is package-global, which is why all ten of its files are in go/types: ONE extra hoist in the
discarded traversal shifts every later switchᴛN in the whole package. That half is a family CNR
structurally cannot see — no behavioral project puts a side-effecting type-switch tag inside an argument
subtree — so the stdlib A/B, not CNR, is the instrument that closes this class. Worth remembering for
the next converter change that touches a traversal: CNR’s 571 small packages and the corpus’s 302 real
ones fail in different places.
Corpus levelled in the same change: 29 files overlaid into src/core (+116/−116 lines, mechanically
verified to be counter renumbering and nothing else), and the full stdlib solution rebuilt clean on top of
them. runtime/mfinal.cs.auto also renumbered but is deliberately NOT overlaid — the standard overlay rule
excludes *.cs.auto, and those siblings are levelled together as CleanupBacklog item 18.
Guard: TestNestedArgumentConversionIsNotExponential (nestedArgScaling_test.go) converts a 30-deep
nested call under a 90s budget in a CHILD PROCESS — same plumbing as the chained-call guard, and for the
same reason (the conversion cannot be cancelled, so an in-process regression would keep go test alive
until the harness killed it) — then asserts every nesting level survived into the emitted C#, so it cannot
pass by dropping the expression. Negative control against the pre-fix source: FAIL at 90.02s; with the
fix, PASS at 0.85s. runWithinBudget’s timeout message is now generic, with each guard naming its own
defect in the Fatalf that wraps it.
Reproduction fixture (depth N nested calls; N=22 is the row above):
package main
func f(x int) int { return x + 1 }
func main() {
y := f(f(f(/* … N deep … */ 1)))
println(y)
}
How to profile the converter — the recipe, and three traps that cost the sibling arc an hour
The chained-call arc was diagnosed by profiling a live, still-spinning converter. That is now a one-liner, and this section exists so the next session does not rebuild it:
GO2CS_PPROF=localhost:6060 go2cs -recurse ./app ./out # off unless the var is set
go tool pprof -top -nodecount=35 http://localhost:6060/debug/pprof/profile?seconds=20
curl http://localhost:6060/debug/pprof/goroutine?debug=2 # every goroutine's stack
The endpoint is loopback-only by design (it serves goroutine stacks and heap contents); a bare
:6060 is read as localhost and an explicitly non-loopback host is refused. See
diagnosticProfiling.go.
Trap 1 — a -cpuprofile-style flag cannot see this class of bug at all. Those write on exit, and
the failure mode is a run that never exits. Same blind spot for -memprofile. The live endpoint (or a
stack dump on a timer) is the only thing that observes it.
Trap 2 — dlv attach KILLS its target. On a non-terminal stdin it exits with
Stdin is not a terminal, use '-r' … and takes the process down with it, destroying a reproduction
that took minutes to reach. If a debugger is genuinely wanted, pass
--allow-non-terminal-interactive=true or run headless (--headless --listen) and connect
separately — but the pprof endpoint above is the cheaper answer.
Trap 3 — Ctrl+Break does NOT dump goroutines on Windows. The Go runtime’s ctrlHandler maps
CTRL_BREAK_EVENT to SIGINT and calls exit(2) when nothing is handling it; there is no SIGQUIT-style
traceback the way there is on Unix. Do not plan a diagnosis around it.
Read the process before the source. The single most useful step in the sibling arc took seconds and no tooling: sample CPU and working set. CPU pegged + heap flat ⇒ exhaustive re-work (this family). 0% CPU ⇒ deadlock. Heap climbing ⇒ runaway allocation/recursion. Stack depth stable while CPU burns is the signature of re-walking a bounded tree, which is what separates an exponential from runaway recursion — and it rules out most of the field before a profiler is even attached.
Per-package wall time is now printed at the end of every -recurse run (Slowest N of M packages), so
a superlinearity that has not yet become fatal is visible as an outlier rather than needing a
reproduction to find.
CLOSED — the issue-#33 follow-up: the bsoncodec “hang” is an EXPONENTIAL, and it is fixed (2026-08-07)
The reporter re-ran with the three fixes in, cleared the crash, and hit a different wall: a -recurse
run “hanging indefinitely” at [1440/1726] Converting go.mongodb.org/mongo-driver/bson/bsoncodec, over
half an hour on one package. It is not a hang. It is (p+1)^N work, and the whole arc is measured.
Diagnosed from the process, not from the source. Reproduced locally in a 7-package closure (a scratch
module importing bson/bsoncodec from go.mongodb.org/mongo-driver@v1.17.9): the other six packages
convert in seconds, bsoncodec never finishes. The process is CPU-bound at ~1.5 cores with a FLAT 345 MB
working set — which is what rules the field down to one answer before any code is read: not a deadlock
(that is 0% CPU), not a leak (that grows). A CPU profile puts convCallExpr/convExpr at 66%
cumulative, mutually recursive, the balance being GC of what they allocate; goroutine dumps show a
stable ~40-deep convCallExpr → convExpr → convSelectorExpr → convExpr cycle that does not grow.
Bounded depth with unbounded work is re-walking, not runaway recursion.
Root cause. A fluent chain nests LEFT, so each link’s callee IS the rest of the chain.
convCallExpr’s argument classifier ran funcName := v.convExpr(callExpr.Fun, nil) inside
for i := range params.Len() — a full conversion of the entire callee subtree on every iteration — purely
to test whether the callee TEXT spelled print/println, and Phase 7 then converted it once more for
real. A call with p parameters walked its callee p+1 times, which on a chain compounds to (p+1)^N.
bsoncodec registers its default codecs as 42-link (encoders) and 63-link (decoders)
rb.RegisterTypeEncoder(t, codec).… chains over a 2-parameter method: 3^42 ≈ 1.2e19 callee walks for
one function.
The fix is to stop asking the question in text. callFunIsUniversePrint reads the name from the AST
and is O(1). It agrees with the old form by construction: identIsUniverseBuiltin
(ObjectOf(ident).(*types.Builtin)) already required a bare identifier resolving to Universe, and such an
identifier’s name IS the built-in’s name — a shadowing declaration makes both forms false.
Rule this establishes, and it generalizes past this bug: never derive a predicate from CONVERTED TEXT when the AST or the type system answers it. Conversion is not a pure function of a node — it is a full subtree walk with side effects — so a text probe inside a loop is a hidden complexity multiplier, and on any LEFT-NESTING construct it is exponential rather than merely quadratic.
Paired A/B, idle machine, single-package conversion of a synthetic chain over a 2-parameter method (the
bsoncodec shape); after is flat at the go/packages load floor:
| links | before | after |
|---|---|---|
| 12 | 4,375ms | 1,902ms |
| 16 | >120s (killed) | 2,014ms |
| 20 | >120s (killed) | 2,342ms |
| 24 | >120s (killed) | 1,912ms |
| 42 | >120s (killed) | 1,974ms |
And the reporter’s real shape: the bsoncodec closure converts 7/7 in 36.7s, the package’s 42-link
chain emitted faithfully (all links, interface adapters and ж<T> boxes intact).
Gates — all green, and the arc is NOT emission-neutral, which CNR caught rather than argued.
- CNR: 2 of 569 changed —
DeferArgEnclosingCapture/main.csandGoStmtValueReturn/main.cs, both a pure capture-variable RENUMBERING (doneʗ3→doneʗ2,oʗ2→oʗ1), declaration and uses renamed together. The discarded callee conversion had been bumping the capture counter as a side effect, so removing it closes a gap in the sequence. Verified collision-free (every…ʗNoccurs exactly twice, properly nested) and then verified where it counts: both projects Compile pass and Output pass againstgo run. Goldens re-baselined withUpdateTestTargets --createTargetFiles; only those two.cs.targetmoved, no test-method churn. - Full behavioral suite: 544/544 Transpile+Compile+Target, 514/514 Output, 0 failed (1,792.5s).
go test ./...: ok, exit 0 (106s), including the new guard and the sibling lane’s projitems gate (the new source file is registered ingo2cs-src.projitems, BOM and CRLF preserved).
Guard: TestChainedCallConversionIsNotExponential (chainedCallScaling_test.go) converts a 40-link
chain over a 2-parameter method under a 90s budget in a CHILD PROCESS — the conversion cannot be
cancelled, so a regression would otherwise leave a goroutine spinning and keep go test alive until the
harness killed it minutes later — then asserts every link survived into the emitted C#, so it cannot pass
by dropping the chain. Negative control against the pre-fix source: FAIL at 90.05s; with the fix,
PASS at 1.6s.
Finding handed on — a SECOND exponential of the same class, on the ARGUMENT path
Not owned by this arc and not what the reporter hit. After rendering a call, convCallExpr re-walks every
argument through checkForImplicitConversion — its own comment says it “re-converts each arg purely for
its side-effects (recording implicit conversions); the result is discarded” — which is a second full
conversion of each argument subtree, compounding to 2^depth on NESTED calls (f(f(f(…)))). Measured
with the callee fix already in: nesting depth 18 → 3.4s, depth 22 → 24.9s. It did not block the reporter
(argument nesting that deep is rare where 42-link fluent chains are not), and the recording is
entirely type-driven — expr flows only to the return value — so the durable fix is to split the
recording from the rendering and let the discard-the-result call site skip convExpr entirely.
Deliberately NOT folded in here: it is an independent change with its own emission-regression surface (this
arc already moved two goldens), and entangling it with a one-line fix would cost the clean A/B. Banked as
its own arc by user ruling (2026-08-07), and CLOSED the same day by r43a-argexp — see the section at the
top of this board for the fix, the desktop A/B and the gates. Holding it back was the right call: it moved
four behavioral goldens and 29 corpus files, none of which would have been separable inside the one-line
callee fix.
OWED DISCHARGED — the issue-#33 arc is measured on Windows (2026-08-06, same day)
Every owed gate ran green, the 3a probe validated findings (b) and (c) end to end, and the probe paid for itself with a new finding — (d), below, FIXED and CLOSED the same day.
- CNR: NO REGRESSION — byte-identical across all 569, exit 0 (1,088s).
go2cs.exewas rebuilt immediately before the run per this entry’s own route-#2 warning. This also discharges item 5 corpus-wide: no bare-LF line boundary surfaced anywhere under F3’s normalized split. - Full behavioral suite: 544/544 Transpile+Compile+Target, 514/514 Output, 0 failed
(3,508.5s under machine load). Honesty proven, not assumed: the exe was rebuilt after CNR’s
transpile (20:17:47 > every CNR-refreshed
.csat 20:02:56), the suite re-transpiled all 544 (DeepEqual/main.cs→ 20:21:13), and the tree was CLEAN after — emission unchanged, in agreement with CNR. go test ./...: ok, exit 0 (84.3s). The three new guards’ first Windows run: 3/3 PASS —TestModuleCacheVestigialReplaceLoad(1.17s, both sides of its fixture, so the control still reproduces on Windows),TestUntypedPackageConvertsWithoutPanic(1.73s),TestEscapeAnalysisPanicReachesCaller. The container’s nine Linux failures are absent here, as predicted. 3a. The otel probe ran, with finding (c) honored first: this box’s native toolchain is go1.23.2 — below (c)’s floor — so the probe converter was built withGOTOOLCHAIN=go1.25.0into a scratch location (the repo’s binary stays the native build the gates measured). A module importinggo.opentelemetry.io/otel@v1.44.0(vestigial./trace/./metricreplaces confirmed present in the cachedgo.mod): closure 209 discovered, 25/25 converted (1 app + 24 third-party, 48.2s), zeroinvalid package name, zeronewer Go version— (b)’s remedy and (c)’s guidance both hold on Windows, where the reporter hit them.- Sweep waived by this entry’s own condition — 1–3 clean and byte-identical emission leaves no path into the banked suites.
The stray remote branch claude/recurse-option-diagnosis-cb1ins (fully contained in master) is
deleted. The container’s original record follows.
(d) CLOSED 2026-08-06 — the build-constraint evaluator could not parse a Go release tag, and the
!go1.21 “asymmetry” was never an asymmetry. Observed in the same otel probe: five
github.com/go-logr/logr@v1.4.3 files gated //go:build go1.21 each warned failed to parse build
constraint: 1:4: expected 'EOF', found .21, while the paired context_noslog.go (!go1.21) warned not at
all and was correctly excluded — with an identical dual-line header, which made the two look like they were
handled by different code paths.
Root cause. EvaluateConstraint ran the constraint through parser.ParseExpr, a Go expression
parser, for which go1.21 is the identifier go1 followed by an illegal .21 selector. It fails on
!go1.21 too — at 1:5 — so nothing diverged here. context_noslog.go produced no warning because
go/packages had already excluded it upstream: it is absent from pkg.GoFiles and never reaches this
code at all. Verified directly, both halves: ParseExpr errors on both forms, and a two-file probe module
loaded through packages.Load returns only the go1.21 file.
Why it was not cosmetic. conversionDriver.go warns on a constraint error and falls THROUGH to
including the file, so on those five files the wrong machinery reached the right answer. It is wrong the
moment a constraint mixes a release tag with a platform: //go:build go1.21 && windows converted for linux
lost its platform half along with the rest of the expression and was included. Two further defects fell out
of the same layer — the regex scanner matched only //go:-prefixed lines, so a legacy // +build-only file
(the norm in pre-1.17 third-party modules, which is exactly what -recurse meets) converted as
unconstrained; and it scanned the WHOLE file, so a //go:build quoted in documentation below the
package clause gated the file.
Fix. The hand-rolled parse/eval layer is gone, replaced by go/build/constraint — the package the
toolchain itself uses. constraint.IsGoBuild/IsPlusBuild recognize the lines (column zero, header only,
//go:build winning over +build as go/build orders them), constraint.Parse parses both syntaxes, and
Expr.Eval drives a single matchTag callback that owns every tag class. Tag matching is now
case-sensitive, as the toolchain matches; the old evaluator lowercased the whole expression, which quietly
made a mixed-case -tags MyTag unsatisfiable.
One hazard this fix creates and closes in the same change, and it is finding (c) wearing a different
hat. Release-tag evaluation was previously inert — it always errored — so activating it puts the
compiled-in build.Default.ReleaseTags in charge of go1.N. Under GOTOOLCHAIN=auto that list is not the
loader’s: go2cs.exe built with Go 1.23 converting a module that declares go 1.25 would call go1.24 false
while go/packages called it true, dropping every file gated between the two along with the !go1.24
sibling the loader had already excluded — leaving the package with neither half. That configuration is not
hypothetical; it is what this machine had (converter built go1.23.2, otel probe loading under go1.25.0).
Over-exclusion is this evaluator’s recurring failure mode — the purego seeding and the goexperiment
ToolTags branch both exist to undo one — and it is the dangerous direction, because the loader has already
applied the full constraint for the target platform, so anything this pass subtracts is real code. Release
tags are therefore resolved by asking the go command (go env GOVERSION from the same directory
packages.Load uses), cached per module root so a -stdlib run pays one ~300 ms lookup rather than 302.
Note this does not retire (c): the linked-in type checker is still whatever release compiled go2cs,
and no toolchain switch reaches it. Build go2cs with a toolchain at least as new as the closure’s newest
go directive regardless.
Guarded by src/go2cs/buildConstraints_test.go — release tags bare/negated/compound, the legacy +build
grammar, extraction precedence, and the loader-toolchain resolution. Verified against the pre-fix converter
rather than assumed: every new assertion fails on it, including the two the fix was not looking for (the
legacy-only file and the documentation-gated file).
For the next local (Windows) session: master carries the issue-#33 arc in three commits — fe9bec0
(the package_info.cs EOL-agnostic read-back, Linux finding F3), 6ca9565 (the panic fix itself), and the
main-module load shape for module-cache packages that closes finding (b) below — posted directly to master
under the same standing ruling as the issue-#32 entry, and for the same reason: a remote Linux container
where the .NET/PowerShell gates cannot run, so the arc ships with converter-level evidence only. All three
are emission-neutral for the corpus, and that is measured rather than argued: 569/569 behavioral
packages transpile byte-identically to the converter that predates each change.
What the reporter hit. A -recurse conversion of renart died
at [736/1726] on a nil dereference at escapeAnalysisOperations.go:739, discarding ~1,000 packages of
queued work; reported again under -recurse=module, where it lands at [33/44] on the app’s own packages.
Root cause, two independent halves — the second is the one that mattered.
- The dereference.
go/typesrecords no type at all for an expression whose operand went invalid (Checker.recordreturns early formode == invalid), sotypes.Info.TypeOfreturns a nil interface — notTyp[Invalid]. The reported crash isTypeOf(call.Fun).Underlying()for an address-taken argument of a call to an undefined function. Theaddr=0x20in the pasted trace is the itab’sfun[1]slot, which is what distinguishes nil-interface from typed-nil. Reproduced in six lines of Go, same file, same line, same fault address. - The containment hole.
ModuleConverter.convertAllandStdLibConverter.convertPackageeach already wrap a conversion inrecoverso one unconvertible package fails alone — andperformEscapeAnalysisruns its files in goroutines, where a panic unwinds only its own stack. Every fault raised on that side of thegostatement was unrecoverable by anyone. That is what turned a one-package defect into a dead run. Workers now capture the first panic withdebug.Stack()(before the frame is lost, so the report still names the faulting converter line rather than the re-raise site) and re-panic afterWait.
Rule this establishes, and it generalizes past this bug: any pass that spawns goroutines must re-raise a
worker panic on the caller’s goroutine, or the per-package containment both batch drivers depend on is
silently void. Written up under Packages That Do Not Type-Check in
ConversionStrategies-Reference.md, with the underlyingOf()
convention for any type reached through TypeOf/getType on an arbitrary source expression.
What the container DID establish. All 569 behavioral packages re-transpiled twice — once with the
converter that predates the arc, once with the fix — and the output is byte-identical everywhere except
two Windows-only packages, UnsafeStringEmpty and FindFirstFileData, which do not type-check on Linux
(syscall.UTF16ToString). Those are the in-repo proof rather than an exception: the old converter dropped
UnsafeStringEmpty/main.go entirely through the per-file recover, and the fixed converter emits a
main.cs matching the committed Windows golden byte-for-byte modulo CRLF. The converter’s own
go test ./... failure set is identical with and without the arc (isolated by re-running with only the
F3 commit applied) — nine failures, all pre-existing Linux path-separator/CRLF findings, none in these paths.
Owed, in order (budgets from the CLAUDE.md table):
./src/tests/Behavioral/check-no-regression.ps1— timeout 700s. Expect byte-identical. Both commits are no-ops on Windows by construction: F3’s read path only differs on an LF file (autocrlf gives CRLF working trees), and the #33 guards only fire on a package that does not type-check — the behavioral corpus has none on Windows. ⚠ Re-rungo build -o bin\go2cs.exefirst: agit checkoutrestore refreshes every.csmtime and re-arms false-green route #2, exactly as the issue-#32 entry records../src/tests/Behavioral/run-behavioral.ps1(full) — timeout 2100s. Expect 544/544 + 514/514.go test ./...fromsrc/go2cs— expectok, exit 0, including the three new guards (TestUntypedPackageConvertsWithoutPanic,TestEscapeAnalysisPanicReachesCaller,TestModuleCacheVestigialReplaceLoad) and the seven pre-existing recurse tests that the Linux container cannot pass. The third guard asserts both sides from one fixture, so a Windows pass also confirms the control still reproduces there. 3a. Worth doing once, and it is not a gate: a real-recurserun against a module that depends ongo.opentelemetry.io/otel@v1.44.0(or any monorepo-layout module with relative replaces) — the container measured 2 → 0invalid package namefailures, and Windows is where the reporter hit it. Build the converter with Go 1.25 or newer first, per finding (c)../src/run-validated-sweep.ps1only if 1–3 surface anything — byte-identical emission leaves no path into the banked suites otherwise.- One Windows-specific risk worth a look, not a gate: F3 now splits a read-back
package_info.cson normalized\n. A file containing a bare LF inside a line was previously kept as part of that line and is now a line boundary. Converter-written files are CRLF throughout and autocrlf normalizes on checkout, so this should be unreachable — CNR clean in step 1 confirms it across all 569.
Findings for follow-up, neither owned by this arc
(a) F3 was masking the Linux F5 failures. With the read-back seam fixed, the converter’s go test ./...
on Linux runs to completion for the first time and surfaces nine failures. That is not a regression: the
old binary log.Fataled inside the first processConversion and ended the whole test binary, so most
of the suite never ran and the truncated output read as two failures. Seven of the nine are F5 (Linux
filepath.Join does not normalize the \ the code injects — $(go2csPath)core\fmt/\fmt.csproj) and two
are the CRLF-template tests. All nine are unchanged with the #33 arc removed. Recorded here because the
count of Linux failures moved for a benign reason, and the next Linux session should not read it as drift.
F5 remains Arc 2 of PLAN-linux-operation.md, untouched.
(b) ROOTED, REPRODUCED, and FIXED — the reporter’s invalid package name: "" was the issue-#32 family,
one directive over. Reproduced end-to-end the same session against the reporter’s own dependency
(go.opentelemetry.io/otel@v1.44.0), so this is measured, not argued. The hypothesis first written here —
“the standalone module-cache load is a weaker context” — is confirmed in mechanism and wrong in detail:
it has nothing to do with MVS version selection or the app’s own replace directives.
The mechanism. otel@v1.44.0/go.mod carries the monorepo’s own relative replaces:
replace go.opentelemetry.io/otel/trace => ./trace
replace go.opentelemetry.io/otel/metric => ./metric
Valid in the otel source repo, where those are sibling directories. The published module zip excludes
them — trace and metric are separate modules — so in the cache ./trace does not exist. A replace is
honored only in the main module, and processConversion loading a package with Dir inside the cache is
exactly what promotes that dependency’s go.mod to main-module status. The go command then says
replacement directory ./trace does not exist, otel/trace never loads, its types.Package stays
empty-named, and go/types reports could not import go.opentelemetry.io/otel/trace (invalid package
name: "") at every use site. Same root as issue #32 — a module-cache directory is not a main module —
and GOWORK=off cannot reach it, because replace is not a workspace feature.
The three-way probe (packages.Load, LoadAllSyntax, run under go1.25 so the language-version noise
below is out of the picture):
| Load shape | Result |
|---|---|
A — Dir = the cache dir, pattern = that dir (what processConversion does) |
could not import go.opentelemetry.io/otel/trace (invalid package name: "") — the reporter’s error verbatim |
B — Dir = the app module, pattern = the import path |
0 errors. The dependency’s replaces are ignored, as a non-main module’s must be |
C — Dir = otel/trace@v1.44.0’s own cache dir |
3 further failures from its vestigial replace go.opentelemetry.io/otel => ../ |
Blast radius, measured: 189 of the 244 packages in the otel module zip import otel/trace or
otel/metric, so all 189 lose their types under load shape A. This is not an otel quirk — it is every
monorepo-layout module that carries relative replaces, which is the common shape for a multi-module Go repo.
The remedy is validated, not sketched: for a package under GOMODCACHE, load it from the main
module’s directory by import path (shape B) instead of standalone by directory. That also makes the
issue-#32 GOWORK=off gate redundant for third-party packages — the go command never enters the
dependency’s directory, so a vestigial go.work is not read either — though the gate should stay for the
non-recurse paths. ModuleConverter has both inputs already (pkgPath and the main module dir);
processConversion takes a directory, so the import path needs plumbing through. Worth weighing at the same
time: this is also 1,726 separate packages.Load invocations, the dominant cost of a recurse run, against a
closure loadClosure already type-checked correctly in one pass.
LANDED 2026-08-07 (commit on master; design detail in
DESIGN-recursive-enduser-conversion.md, The same seam,
one directive over). A module-cache package is loaded from the main module’s directory by import path
whenever the run is -recurse and both inputs are known; every other load keeps the directory shape and the
GOWORK=off gate with it. Measured: the otel -recurse run goes 2 → 0 invalid package name failures,
and with the converter rebuilt under Go 1.25 the closure converts 14/14 with no warning of any kind. All
569 behavioral packages transpile byte-identically to the converter that predates the change —
expected, since no behavioral package is under GOMODCACHE, and measured rather than assumed. Guarded by
TestModuleCacheVestigialReplaceLoad, which asserts BOTH sides from one fixture so the guard cannot pass
vacuously. The converter’s own suite has the same nine pre-existing Linux failures, unchanged.
Deliberately LEFT UNDONE — the one item this arc hands forward. Closure reuse: loadClosure already
type-checks the whole graph in one pass in the main module’s context, and now that each package is loaded
from that same context anyway, the per-package reload is re-deriving in 1,726 separate packages.Load
invocations what one pass already had. That is the dominant cost of a recurse run. It was not folded into
this fix because it is a pipeline-shape decision rather than a bug fix, and because it has to respect
-recurse=module, which deliberately skips the full-closure type-check precisely so an unconvertible
dependency graph cannot block the app’s own code (issue #32’s mode). Rooted and ready; wants a measurement of
the real saving before it is worth the risk.
(c) A second, independent finding from the same reproduction — the converter cannot type-check a module
whose go directive exceeds the Go release go2cs was BUILT with. otel@v1.44.0 declares go 1.25.0; a
go2cs built with go1.24 reports package requires newer Go version go1.25 (application built with go1.24)
and every downstream expression goes untyped. The go command switches toolchains automatically
(GOTOOLCHAIN=auto), but the type checker go2cs links in is whatever release compiled it, and no toolchain
switch reaches that. This is invisible until a dependency adopts a new language version, then it silently
degrades whole packages. Two things follow: build go2cs with a toolchain at least as new as the newest go
directive in any closure it is asked to convert, and consider making the converter say so by name rather
than letting it read as an ordinary type error. Independent of (b) — it reproduced on both load shapes and
disappeared on both when the probe was re-run under go1.25, and confirmed a third time end to end: rebuilding
the converter itself with Go 1.25 is what took the otel -recurse run from “14/14 with 13 best-effort
warnings” to “14/14, silent”. Left as guidance, not code — the honest fix is a build requirement, and the
optional refinement (naming this condition in the diagnostic instead of letting it read as an ordinary type
error) is a small, separate change nobody is blocked on.
OWED DISCHARGED — the issue-#32 go.work fix is measured on Windows (2026-08-06, same day)
Every owed gate ran; the change is clean, and its emission-neutrality is proved against the converter that predates it rather than argued from the diff.
- CNR — the gate could not use the committed corpus as its reference, so it was run in a stronger
form. A plain CNR reported drift under both candidate roots, in opposite directions and on
disjoint file sets (4 files vs 12) — a pre-existing condition of the committed
package_info.cscorpus that has nothing to do with this change; it gets its own entry below. The gate’s actual question was therefore answered converter-vs-converter: every one of the 569 behavioral packages transpiled twice in one environment, once withmaster’s converter and once with a converter built fromc57f1a878(the commit before this arc), hashing all 1,176 generated.cs. Manifests byte-identical (A8E0B75B…C15EC80both sides), 0 transpile failures. The change is emission-neutral across the whole corpus, which is what “expect byte-identical” was for. - Full behavioral suite: 544/544 Transpile+Compile+Target, 514/514 output comparisons, 0
failed, 30 skipped (2,124.3s). ⚠ The FIRST attempt was a false green by documented route #2 and
is recorded because the trap is easy to re-enter: restoring the tree with
git checkoutrefreshes every.csmtime, soUpToDate’scsTime <= exeguard sees fresh output, Transpile is skipped for all 544, and the suite validates the committed.csinstead of the converter’s. The guard is sound — a checkout defeats it, not a converter rebuild. Re-runninggo build -o bin\go2cs.exebefore the suite makes the exe newest again and forces the real pass; confirm it ran by checking that the transpile left the tree dirty. - The recurse guards’ first Windows run: all 7 PASS (14.7s) —
TestModuleCachePoisonedGoWorkLoad0.65s,TestRecurseModuleOnly,TestRecurseSyntheticModule,TestRecurseNuGetReferences,TestRecurseNuGetResolvesForeignImplements,TestRecurseLinknameForwarder,TestRecurseModeFlag. Fullgo test ./...isok, exit 0 — the container’s 7 “failures” were Linux-path artifacts, as it predicted. Additionally measured, because the committed guard pinsgoModCachedirectly and so never exercises the real Windows resolution:goModCacheDir()resolves throughgo envtoC:\Users\rcarroll\go\pkg\mod(theGOMODCACHEenv var is unset here, so the second fallback is the live path), andisPathUnderclassifies correctly against a real cache path — case-insensitive in both directions, separator-agnostic, root-inclusive, and not fooled by the sibling-prefix trap…\pkg\mod-notthecache. The gate fires on Windows. - Sweep waived by this entry’s own condition. Items 1–3 surfaced nothing attributable to the
change, and byte-identical emission leaves no path into the banked suites. The corpus finding below
is confined to behavioral
package_info.csfiles and touches neithersrc/corenor any banked suite. - The
eol=crlfpin is invisible on this clone, positively.git check-attr eolreportscrlffor all three templates, all three are fully CRLF on disk, andgit statusstayed clean across the pull — the expected outcome, verified rather than assumed.
The container’s original record follows, kept for its diagnosis.
For the next local (Windows) session: master carries the second issue-#32 arc — commit
121c61d (the GOWORK=off fix + its guard) and 0267629 (the template eol=crlf pin), the diagnosis
of the reporter’s pasted -recurse failure log (the Renart project) and its fix, posted directly to
master per user ruling 2026-08-06. Same posture as the d00cac5 entry below, same reason: a remote Linux
container where the standing gates cannot run, so the change ships with unit-level evidence only.
What was found (full write-up: DESIGN-recursive-enduser-conversion.md,
Module-cache loads and the vestigial go.work): the reporter’s abort was their pre-d00cac5 binary
(the fatal load path this board’s discharged entry below already measured), but underneath it sits a real,
still-current loss — cloud.google.com/go’s module zip ships the monorepo’s go.work, and
processConversion’s reload, running the go command from inside the module cache, enters workspace mode
and fails every package of that root module (“cannot load module ../accessapproval listed in go.work
file”). The fix appends GOWORK=off to the loader env only when the input dir is under GOMODCACHE;
ambient workspace behavior is preserved everywhere else. A second commit pins the three embedded converter
templates eol=crlf in .gitattributes — the checkout-level discharge of the CRLF seam the entry below
recorded as recorded-not-owed (an LF checkout’s converter log.Fataled on every conversion; the
"\r\n"-splitting code seam itself is unchanged).
What the container DID establish: TestModuleCachePoisonedGoWorkLoad (new, network-free, both sides of
the gate) passes; the full go test ./... failure set is identical to baseline (the same 7
pre-existing Windows-path tests, nothing new — measured with-fix vs. master on the same box); an
end-to-end repro (a module importing cloud.google.com/go/civil@v0.123.0) goes from 1/2 converted
(civil failed) to 2/2 converted with the emitted civil.cs/csproj/slnx spot-checked.
Owed, in order (budgets from the CLAUDE.md table) — the d00cac5 pattern verbatim:
./src/tests/Behavioral/check-no-regression.ps1— timeout 700s. Expect byte-identical: the change is an env-var gate on a-recurse-only load path plus a checkout attribute; no emission logic moved../src/tests/Behavioral/run-behavioral.ps1(full) — timeout 2100s. Expect 544/544 + 514/514.go test -run 'TestRecurse|TestModuleCachePoisonedGoWorkLoad' ./fromsrc/go2cs— the new guard’s first Windows run../src/run-validated-sweep.ps1only if 1–3 surface anything (no path into the banked suites otherwise).- ⚠ The
eol=crlfpin takes effect on checkout — existing Windows clones already have CRLF working trees via autocrlf, so expect no visible change there; agit statusafter pulling the attribute commit should stay clean for the three templates. If it does not, that is a finding.
Open CLOSED — CNR’s verdict no longer moves with an ambient variable, and the split package_info.cs corpus is normalized (2026-08-06, same day)
All three steps landed, in the order this entry insisted on — pin the root, make an unusable one
loud, then normalize — because normalizing against a root no gate enforces would only have re-split the
corpus on the next machine. Commits 826b7e486 (the mechanism) and 9859dd993 (the data), kept apart so
each is reviewable on its own.
- Every seam names its root, computed from its own location. Five invocation sites, from a
tree-wide sweep for
go2cs.exe:check-no-regression.ps1(a$PSScriptRootwalk),BehavioralRunnerandPerformanceRunner(anAppContext.BaseDirectorywalk,s_srcRoot), MSTestBehavioralTestBase(a newGo2csRoot, resolved inInitbefore its up-to-date early return and with the trailing separator trimmed — a backslash before a closing quote escapes it on a Windows command line), andrun-validated-sweep.ps1. The sweep was not on the list above and is pinned deliberately: a-testsrun self-locates only when the ambient root is invalid, so aGO2CSPATHaimed at some other real go2cs tree — adeploy-corestaging root — would still have been honored, building a suite against one tree’s metadata while compiling the other’s sources. Confirmed first that-go2cspathcannot move WHERE single-package output is written (it feeds onlygetImportPackageInfo’sTargetDirsubstitution;outputFilePathis untouched):DeepEqualtranspiled with the flag lands in place and leaves the tree clean, without it the same command drops its fivereflectaliases. - The converter recovers, and says so when it cannot.
resolveGo2CSPath(commandLineOptions.go) now runs for every single-package conversion, not just-tests: when the configured root is not a go2cs root (nocore\golib\golib.csproj) it walks the ancestors of the conversion’s OUTPUT path for one. Output, not input, is the anchor — the emittedpackage_info.cs/.csprojand their$(go2csPath)corereferences live there, so the tree that must satisfy them is the tree the output is written into; where the two differ (converting GOROOT sources into a repository tree) the input walks the wrong chain entirely, and where they are the same directory — the barego2cs <pkg-dir>— it is exactly what makes an unconfigured run inside a clone resolve against that clone. An explicitly configured working root still wins. Found nothing, the run proceeds (standalone conversion with no deployed runtime is legitimate) but emits ONE prominent stderr warning naming the resolved path and both consequences.-recursewarns but never self-locates (its root doubles as the output root without a second positional, so moving it would move the generated tree);-recurse=nugetdoes neither;-stdlibdoes neither, because there the root is the output root the run itself populates and an absentgolibis the normal state of a first conversion. Guarded byTestResolveGo2CSPathSelfLocation/TestResolveGo2CSPathUnusableRootWarns— network-free, both sides, the real stderr captured through anos.Stderrswap rather than a stand-in, with the precedence rule and both suppressions pinned; the once-per-run warning latch is package-level and test-pinnable in thegoModCachemanner. - Exactly the twelve, exactly the predicted direction. The now-deterministic CNR reported precisely
the twelve files this entry named, all pure additions — 46 lines, 0 removals: 28
time, 10os(theio/fsre-exportsFileInfo/FileMode/DirEntry/PathErrorplusos.Signal), 5syscall, 2encoding/json, 1runtime. Banked alone in9859dd993.
Gates, all green. go test ./... from src/go2cs: ok, exit 0 (103.3s cold), including the two
new guards. check-no-regression after the normalization commit: NO REGRESSION — byte-identical
across all 569 behavioral packages, exit 0 (917.5s; the pre-normalization run was 864.2s and reported
the twelve). Full behavioral suite: 544/544 Transpile, Compile and Target, 514/514 output
comparisons, 0 failed, 30 skipped — PASS in 2,453.3s. Both long runs sat above the CLAUDE.md budgets
because a sibling worktree was active; forward progress was confirmed by watching the transpile advance
alphabetically, not assumed. The MSTest seam was spot-checked separately (--filter DeepEqual, 4/4).
⚠ The false-green trap was avoided by construction, and the avoidance was measured. CNR’s own
transpile leaves every .cs newer than go2cs.exe, which is precisely the state that makes
UpToDate’s csTime <= exe guard skip Transpile for all 544. go2cs.exe was rebuilt immediately before
the suite and the skip was disproved with mtimes, not assumed: DeepEqual/main.cs moved 15:14:26Z →
15:27:19Z, strictly newer than the exe at 15:24:43Z. A clean tree after a CONFIRMED transpile is the
pass condition, and that is what this run produced.
DECISION (the coordinator’s, recorded and not relitigated): package_info.cs gets NO .cs.target
golden. CNR is deterministic now and is the standing converter gate; 569 new golden files is
disproportionate footprint for a line CNR already holds.
Left alone, recorded rather than fixed: deploy-core.ps1 still stages to %GOPATH%\src\go2cs while
the converter’s -go2cspath defaults to ~/go2cs, so running the documented deploy does not populate
the root a flagless converter run reads. That divergence no longer costs anything — every gate names its
root, and a bare run self-locates or says why it cannot — so the two roots stay as they are rather than
being unified in this arc.
OWED DISCHARGED — the issue-#32 -recurse change is now measured on Windows (2026-08-05, same day)
All four gates ran or were legitimately waived; the change is clean. (1) check-no-regression:
byte-identical across all 569 behavioral packages — the entry’s highest-stakes expectation held
exactly. (2) Full behavioral suite: 544/544 Transpile+Compile+Target, 514/514 output
comparisons, 0 failed (1,092.5s). (3) The recurse tests’ first real Windows run: TestRecurseModuleOnly
PASS (0.81s — the Windows-path assertion that could only fail-on-Linux now actually exercises),
TestRecurseSyntheticModule PASS, TestModuleConverterPartitionScope both scopes PASS; full
go test ./... ok with nothing new failing. (4) The sweep was waived by this entry’s own condition —
1–3 clean and byte-identical emission leaves no path into the banked suites. The stray remote branch
claude/go2cs-issue-32-5osg4q is deleted. The container’s original record follows, kept for its
observations (the CRLF-coupled packageInfoWriter seam remains recorded-not-owed).
For the next local (Windows) session: commit d00cac5 — issue #32,
-recurse=module plus the load-failure fix — was authored and pushed from a remote Linux container,
where the standing gates cannot run. It is on master with unit-level evidence only. Nothing about it
is suspected; it is simply unmeasured against the corpus, and that is the whole point of this entry.
What the container could not do, and why it is not a converter defect:
- No
pwsh, nodotnet—check-no-regression.ps1,run-behavioral.ps1andrun-validated-sweep.ps1are all Windows/pwsh instruments; none of the three ran. - The converter cannot write
package_info.cson an LF checkout at all.packageInfoWritersplits the template on"\r\n"(packageInfoWriter.go:52,57), so on acore.autocrlf-less clone the<ImportedTypeAliases>section is never found and every package conversionlog.Fatalfs. The recurse integration tests therefore fail identically before and after the change there — a checkout artifact, not a regression. (Recorded as its own observation: the converter is Windows-line-ending coupled at that one seam. Not owed as work; noted so the next reader does not re-diagnose it.)
What WAS established, so the re-check knows what to expect:
go test -short ./failure set is identical to baseline — the same 6 pre-existing Windows-path tests (TestParseCoreProjectRefs,TestCollectConvertedProjects*,TestIsSelfProjectReference,TestValidationPack*), nothing new.- New guards pass:
TestRecurseModeFlag(extended),TestModuleConverterPartitionScope; the newTestRecurseModuleOnlyfails on Linux at exactly the one Windows-path assertion its siblingTestRecurseSyntheticModulefails on ($(go2csPath)core\fmt\fmt.csprojemitted ascore\fmt/\fmt.csproj). - Smoke-run end to end against a CRLF’d template:
-recurse=moduleconverts an app plus its sub-package in dependency order and writes nopkg\tree, and a later plain-recursefills exactly the referencedpkg\path.diff -rof the two runs’src\trees:.cs/.csprojbyte-identical, only the.slnx/pkg/folder differs.
Owed, in order (budgets from the CLAUDE.md table):
./src/tests/Behavioral/check-no-regression.ps1— timeout 700s. Expect byte-identical: the change touches only error paths and-recurse-scoped branches, and no emission logic. A non-emptygit statushere is a real finding and outranks everything else in this entry../src/tests/Behavioral/run-behavioral.ps1(full, 4 phases) — timeout 2100s. Expect 544/544 + 514/514 output comparisons.- The three recurse integration tests on Windows —
go test -run 'TestRecurse' ./fromsrc/go2cs— which is the FIRST real runTestRecurseModuleOnlywill get. ./src/run-validated-sweep.ps1(backgrounded, 46–53 min) only if 1–3 surface anything; a converter change confined to the recurse driver has no path to the banked suites, so a clean 1–2 discharges this item without it.
Also owed, trivially: delete the remote branch claude/go2cs-issue-32-5osg4q. It is fully
contained in master (both point at d00cac5) and the local copy is gone, but the remote one could
not be deleted from the container — the session’s git proxy rejects ref-deletion pushes
(send-pack: unexpected disconnect, twice, for both --delete and :branch forms), and the GitHub
MCP surface here has no delete-branch tool. One git push origin --delete claude/go2cs-issue-32-5osg4q
locally, or the button on GitHub.
LANDED — the GoFrame arc (2026-08-05), and what it leaves behind
The frame is built. DESIGN-closure-emission.md §4 is now the AS-BUILT record; §4.10 carries the
findings and §4.11 the bang verdict. Landed in five gated checkpoints along §4.8’s path — golib frame,
declarations with unnamed results, recover + named results, function literals, then the rename and the
deletion of the machinery it replaced — each with its own full behavioral gate.
Verdicts and findings, in one place:
- The bang is DROPPED.
deferǃisdefer.deferis a Go keyword, so no Go identifier can ever be spelled that way; it is not a C# keyword; and the one binder that ever put the bare name in scope was the execution context’s lambda parameter, which is gone.goǃandmakeǃkeep theirs, for reasons of their own (gois the root namespace;makeis a predeclared Go identifier a package may shadow). Full analysis: design §4.11. - §4.5 (open-coding the static defers) is NOT in the arc, and the measurement says why. A defer whose
target is a cached static method group already costs 0 B under the frame; the residue §4.5 would
remove is entirely the display class + delegate of a defer that genuinely CLOSES OVER something —
measured at 96 B for one and 192 B for two. There is therefore no cheap subset to take: the eligible
shapes are exactly the ones needing argument and receiver temps hoisted OUT of the
try(afinallycannot see a variable declared inside it), which is §4.5’s own fiddly half. It is a separately reviewable increment worth ~192 B on a two-capturing-defer function and nothing on the rest. - A shape §4 did not anticipate: a DEFERRED literal that defers on its own account. Go scopes that inner defer to the literal; the old emission registered it into the enclosing function instead. Zero instances in the corpus, so it was a latent hole rather than a live defect, and the frame closed it.
- Two C# scoping facts, verified by compiling rather than reasoning. A lambda or local function MAY declare a local spelled like one in the enclosing method (the pre-C#-8 CS0136 rule does not fire), so every frame reads under the same name; a LABEL may not (CS0158), so the named-result exit label alone is depth-numbered.
bodyWrappedInDeferContextis now OPTIONAL and deliberately kept. It forced the direct-жreceiver because aref Treceiver cannot be referenced from inside a lambda; an inline body removes that constraint. Kept because the direct-жform is also the alloc-free, race-free one and changing receiver shapes corpus-wide is its own change. Open simplification.- Pre-existing, NOT this arc: the auto-sibling visit panic. A
-stdlibreconvert reportsvisit file error: … nil pointer dereferenceforinternal/godebug/godebug.goandinternal/concurrent/hashtriemap.go, skipping their.cs.autoREVIEW siblings only (production emission and package-wide state are unaffected — it is a separate re-visit pass). A/B’d against the master converter: identical. Belongs with CleanupBacklog item 18, which already owns.cs.autostaleness.
The measured result. os.File.WriteString — the row that named the 440 B term in the first place —
goes 2,736 → 2,368 B/call, the same 368 B coming off os.File.Write and off the wrapper band that
contains internal/poll.FD.Write’s two defers. Per-shape: the execution context cost 160 B with no
defers and 248 B with one or two non-capturing ones; the frame costs 0.
The arc’s original commission (user rulings 2026-08-05), and two tasks it queues
The closure-emission frame design is APPROVED (DESIGN-closure-emission.md
§4–§5): the execution-context lambda gives way to the ref struct frame with the body emitted inline in
try/catch/finally. The user’s context, recorded because it shapes the work: the lambda form was
chosen for visual parity and was long suspected of a capture-semantics divergence class (the lambda
captures variables the original Go never captured); the frame form removes that class by construction and
the allocation cost was never weighed. One ruling amends the design:
- Evaluate the NEED for the
deferǃbang-suffixed name and DROP the bang if possible. It exists solely to disambiguate calls against thedefer-named delegate parameter of the GoFunc lambda — a parameter the frame design eliminates. Go source can never declare identifiers nameddefer/recover(keyword/builtin), so with the lambda gone the collision source should be gone too; the arc verifies there is no other collision (golib surface, generated code) and documents the verdict either way. Same evaluation for any sibling bang-named member of the defer/recover family. (Symbols.cs constants, never the literal glyph.)
Arc mechanics: lands with its OWN corpus regen (post-r40 doctrine — the corpus stays level with its converter; no new standing-drift era), full gate battery including the sweep (the banked alloc rows are the design’s own motivation), and per-stage checkpoint commits along §4.8’s migration path.
Queued task 1 — the documentation-reality pass (dedicated sub-agent, AFTER the arc lands). The frame
changes every deferred function’s emitted shape: ConversionStrategies.md and
ConversionStrategies-Reference.md examples, and any doc quoting the lambda form, must be brought to
match reality. Style ruling: present tense, educating a new reader — no history in the teaching docs;
posterity lives in the design doc.
Queued task 2 — the [GoTestMatchingConsoleOutput] audit — CLOSED (r41b-outputattr, 2026-08-07).
Before core/fmt was real, some behavioral tests skipped output-matching because the stub could not
format their output. Measured 2026-08-05: 14 projects had package main but no attribute. Each was
run via go run . (5+ repetitions per project, comparing stdout/exit-code across runs) to classify as
GRADUATE (deterministic stdout, exit 0), DELIBERATE-SKIP (nondeterministic or panic/deadlock by design),
or FIXABLE-MISMATCH (deterministic Go output, but the transpiled C# currently diverges). 4 of 14
graduated and are now output-compared (run-behavioral.ps1 --filter <Name>, all four phases green);
one attempted graduate uncovered a genuine converter bug and was left un-annotated, reported below as a
new board candidate:
| Project | Verdict | Reason |
|---|---|---|
| ChannelReceiveFromNil | DELIBERATE-SKIP | <- on a nil channel — Go’s deadlock detector fires (fatal error: all goroutines are asleep - deadlock!), zero stdout, exit code 2. The message carries a goroutine stack trace (addresses/line offsets); a managed re-implementation cannot be expected to reproduce it byte-for-byte, and there is nothing on stdout to compare regardless. |
| ChannelSendToClosed | DELIBERATE-SKIP | Ten goroutines race to send on / close the same buffered channel with no synchronization — a deliberately racy program. Repeated go run showed both the count of values printed before the panic (0 vs 10 observed) and which goroutine panics vary between runs; output is provably nondeterministic. |
| ChannelSendToNil | DELIBERATE-SKIP | c <- v on a nil channel — same deadlock-detector shape as ChannelReceiveFromNil (zero stdout, exit code 2, non-reproducible stack trace). |
| DeferSimple | GRADUATED | Deterministic 3-line stdout (Open file / Write data to file / Close file), exit 0, confirmed across 5 runs. run-behavioral.ps1 --filter DeferSimple — 4/4 phases PASS (48.2s). |
| ForVariants | DELIBERATE-SKIP | Spawns unsynchronized goroutines (go fmt.Println(...)) whose print ordering interleaves with the main goroutine’s loop output. Two consecutive go run invocations produced different line orderings/content, confirming scheduler-dependent nondeterminism. |
| GoCallVariations | DELIBERATE-SKIP | Exercises ~8 different go-statement call shapes (bare func, closure, method value, function-returning-function, etc.) with no synchronization between them; two consecutive runs printed the same lines in different relative order — nondeterministic by design (that’s the point of the test). |
| InferredForeignTypeNoImport | GRADUATED | Deterministic 2-line stdout (true / 5), exit 0, confirmed across 5 runs. run-behavioral.ps1 --filter InferredForeignTypeNoImport — 4/4 phases PASS (19.6s). |
| InterfaceInheritance | GRADUATED | Deterministic 2-line stdout (two map[:N :M] lines — Go’s fmt sorts map keys since 1.12, so the single-key-per-map output is stable), exit 0, confirmed across 5 runs. run-behavioral.ps1 --filter InterfaceInheritance — 4/4 phases PASS (15.5s), proving the transpiled map-print ordering matches too. |
| PointerCastSliceRange | GRADUATED | Deterministic single-line stdout (6 100 11), exit 0, confirmed across 5 runs. run-behavioral.ps1 --filter PointerCastSliceRange — 4/4 phases PASS (17.9s). |
| RangePointerArrayConversion | FIXABLE-MISMATCH (new board candidate) | Go’s stdout is deterministic (63, exit 0, confirmed across 5 runs) — a graduate by the audit’s own criterion — but the transpiled C# prints 0. Root cause is visible in the emitted code: for i, x := range (*[3]int)(p) (p := unsafe.Pointer(&a)) converts to foreach (var (i, x) in ((ж<array<nint>>)(uintptr)(p)).Value) — the round-trip through uintptr cannot recover the original managed box Ꮡa, so the cast yields a fresh/default array<nint> and the loop sums over zero elements instead of {10,20,30}. This is the same “unsafe.Pointer reinterpret via raw address” limitation already load-bearing in the neighboring UnsafePointerReinterpret test’s own design comment (that test deliberately stays Compile+Target-only for exactly this reason). The attribute was NOT added — adding it would redden the Output phase — so this project is left exactly as measured (no diff). Candidate fix belongs with whichever arc next touches unsafe.Pointer reinterpret-cast codegen (see ж<T>/Ꮡ boxing notes); until then this stays a known, deliberate non-graduate for a different reason than the other nine (a real bug, not an inherent nondeterminism). |
| SelectStatement | DELIBERATE-SKIP | Go’s select deliberately pseudo-randomizes among multiple ready cases. Two consecutive runs showed different orderings/values (OK: true -- got: 12 at a different line position; final tuple 17 -5 12 3 vs 3 17 20 -5) — confirmed nondeterministic. |
| StructWithPointer | DELIBERATE-SKIP | stdout embeds a raw pointer address (Value of red = {2 red 0xc0...}); 5 repeated runs showed two distinct addresses (0xc000028180, 0xc00010a150) recurring at random. A memory address can never be expected to match between the Go runtime’s allocator and the CLR’s, so this can never be a stable golden regardless of transpile correctness. |
| TypeConversionReturnType | DELIBERATE-SKIP | Same shape as StructWithPointer — stdout embeds two raw pointer addresses ({Go 0xc0... 0xc0... map[]}) that varied across all 5 runs. Not stable in Go itself, so not a candidate for a byte-exact golden. |
| UnsafePointerReinterpret | DELIBERATE-SKIP | Explicitly documented in its own source comment as “a Compile + Target (golden byte-comparison) test, NOT an output-comparison test” — it uses println (Go builtin, writes to stderr) rather than fmt.Println, and exercises the same raw-address unsafe.Pointer reinterpret limitation that RangePointerArrayConversion’s mismatch surfaces at runtime. |
Net: 4 graduated (DeferSimple, InferredForeignTypeNoImport, InterfaceInheritance,
PointerCastSliceRange), 9 deliberate-skips (documented above, each for a distinct concrete reason —
deadlock detection, goroutine-scheduling nondeterminism, select randomization, or raw pointer-address
non-reproducibility), 1 new FIXABLE-MISMATCH board candidate (RangePointerArrayConversion — unsafe.Pointer
reinterpret-cast through a raw uintptr round-trip loses the original managed box). Change footprint:
4 one-line [GoTestMatchingConsoleOutput] additions to package_info.cs + the UpdateTestTargets-generated
OutputComparisonTests.cs block (4 new Check<Name>() methods) — no golden re-baselining needed (no
emission changed), no go2cs.slnx registration changes (all 14 projects were already registered).
The -tests reference-closure family — CLOSED (2026-07-27)
DisableTransitiveProjectReferences=true means the generated test project lists only the imports
the converter computed, so any package named by a type the test code merely touches is missing
and the build fails with CS0012. crypto/hmac was the first case solved (interface embedding);
the closure is now generalized to the declaration edges of the types the compilation names
(declarationClosureImports), covering both an interface’s bases and a struct’s field types. Full
rule, minimality gates and guards: docs/ConversionStrategies-Reference.md, Reference closure (the
declaration-edge rule).
| Package | Missing type | Outcome |
|---|---|---|
image/draw |
rand_package.Rand |
build unblocked — a struct field of quick.Config reached at an element-bearing composite literal. Now validated 9/9 (2026-07-31), once the two runtime defects below were fixed. |
io |
io_package.Writer |
NOT a closure defect — see the next section. Adding the reference cannot fix it. |
Minimality is the hard part, and it is measured, not asserted. Regenerating every banked
package’s .tests.csproj and diffing is the instrument, and it rejected three looser rules before
the landed one. Seeding from every file rather than the compiled ones drifted compress/gzip
(context, crypto/tls, mime/multipart, net/http, net/url — reached through http.Request’s fields,
from a Phase-4D-excluded example_test.go that is never compiled) and go/token (go/ast); firing
the struct edge on any value use drifted eleven more (sync.Once, sync.Map, reflect.Value);
firing it on an unscoped empty literal still drifted three (mime, testing/quick,
encoding/binary), because an empty Go literal converts to new Δsync.Once(nil) — go2cs-gen’s nil
constructor, which names no field, and whose FIELDWISE overload is internal and so not even a
candidate outside the declaring assembly. Each of those gates drifts zero banked packages. The
one edge that is deliberately not zero is the root-scoped empty literal, re-measured at the
63-package roster on 2026-07-31: it changes exactly one project by exactly one line
(math/rand/v2 gains internal.chacha8rand.csproj) — the root set itself, with all three
foreign-struct negatives byte-identical.
⚠ Run that probe with the converter’s exit status checked. A conversion that fails writes no
csproj, so an ignored failure reads exactly like “no drift” — a false-clean of the same family as
charter §9’s false-green traps. That is how a real defect in the first cut hid through three
measurement rounds: a struct literal declared in the EXTERNAL test variant reached
reach(<pkg>_test), a synthetic path that resolves to no importable package, and every affected
package died with F14b’s resolve test project dependency "bytes_test": package bytes_test is not
in std — silently, until the validated sweep failed on bytes at the second package.
io — duplicate-type build blocker CLOSED (landed on master 2026-07-31); runtime blockers remain
The diagnosis was correct: recompiling io into its mixed internal/external test assembly created a second io_package.Writer, distinct from the one named by hash.Hash, bytes, fmt, and the rest of the referenced closure. The general fix is the new whitebox-reference test-project model. A production package with build-selected same-package tests conditionally grants friend access to <assembly>.tests; internal _test.go declarations emit into <name>_internal_test_package; external references to those declarations route to the bridge by go/types.Object identity; and test-contributed adapters live in the test metadata anchor. Production remains the only identity for its types. Records that truly require a production-type mutation still fall back to recompile.
Fresh io conversion now emits testProjectModel: whitebox-reference, references io.csproj, compiles no production .cs into io.tests, and builds with 0 errors. The host runs all 54 included test functions.
⚠ 2026-07-31 (reflection chip): the 0-errors claim had silently regressed on landed master — a
fresh conversion produced CS1503 ×20: emittedAdapterPair’s bare-cast fallback resolved io_test’s
own Buffer to the first same-simple-name record in order, the FOREIGN bytes_package.Buffer
(bytes_BufferжReader(rb)), while the generator names the anchor-local record’s adapter bare
(BufferжReader). Both A/B binaries (8d55344cc landing, f73d62d71) emit the same broken
pairing, so the recorded 45/54 was measured with an intermediate, not the final, binary — the §9
mixed-vintage lesson in the wild. Fixed in the chip’s landing (anchor-local records win the
dotless fallback; exact-key matching is a full first pass; anchoredAdapterMemberName composes
bare for anchor-local records — guard TestBareCastPrefersAnchorLocalRecordOverForeignSimpleNameMatch).
With that repaired and the chip’s runtime.Callers/Frames.Next managed traceback landed, the
host reached 47 pass / 54 (superseded 2026-08-01 — see the closing paragraph of this section);
the remaining seven top-level verdicts were separate runtime/semantic roots:
— CLOSED 2026-07-31 by the reflection Phase-3 chip (increment 4):TestMultiReaderFlattenandTestMultiWriterSingleChainFlatten:runtime.getcallerspruntime.Callers+Frames.Nexthand-owned over a Go-logical managed stack projection;getcallerspstays an honest stub (see DESIGN-reflection-bridge.md and the ConversionStrategies-Reference section).TestOffsetWriter_Seek,TestOffsetWriter_WriteAt,TestWriteAt_PositionPriorToBase, plusTestOffsetWriter_Writesubtests:os.runtime_randis unimplemented in the tempfile path — owned by theosoperational arc.— CLOSED 2026-08-01, and it was NOT a forwarding bug: the emittedTestMultiWriter_StringCheckCall:WriteStringforwarding behavior mismatchmultiWriter.WriteStringperformsw._<StringWriter>(ᐧ)exactly as Go does. The assertion MISSED because golib’s Go-method-set probe compares EMITTED C# names, and-testsB9 Δ-renames the test-file declaratorfunc (c *writeStringChecker) WriteStringtoΔWriteString(the bare name would hijack the dot-importedio.WriteStringat every unqualified call site — C# resolves the enclosing class’s method group ahead ofusing static). NoGoImplementrecord exists for the pair either, by design since the structural recorders were retired, so the runtime shell tier was the only resolver and its gate said MISS. Fixed ingolib—TypeExtensions.GoMethodNameMatchesprojects a leadingShadowVarMarkeraway as a SECOND pass, after an exact-name pass finds nothing, andAdapterBinder.ResolveReceiverMethodsapplies the same rule so binder and probe cannot disagree. Proven by A/B before the fix: renaming only the emitted method (and qualifying the three call sites the bare name would hijack) turns the test green with no other change. Full rule:docs/ConversionStrategies-Reference.md, A candidate’s EMITTED name is not always its GO name. ⚠ The CLASS is open, not just this instance: any-testsΔ-renamed method that is also an interface member asserted at run time failed the same silent way, and the failure mode is valid-but-degraded (MultiWriterfell through toWrite, which returns the same(n, err)).— RULED 2026-08-01: both areTestMultiWriter_WriteStringSingleAllocandTestPipeAllocations: exact allocation-profile assertions; no disclosure ruling has been madealloc-count-semanticsdisclosures in io’s hand-ownedgo2cs_test_disclosures.json, the classstringsalready established. Neither is an allocation-profile divergence; both are the UNIT mismatch the shim discloses by design —testing.AllocsPerRuncounts mallocs in Go and allocated BYTES on the CLR, so a nonzero-count assert can never agree whatever the allocation behavior. Measured before disclosing, which is the point of the order:num allocations = 406-407; want 1andtoo many allocations for io.Pipe() call: 1184.000000(want ≤ 4) — bytes in both cases. Signature-pinned on"num allocations = "and"too many allocations for io.Pipe() call: ", so any OTHER failure of either test stays a strict mismatch.
With the two above settled, the host reaches 48 pass / 54 · 2 disclosed · 4 os-blocked, and the
os runtime_rand row is the whole of what stands between io and a bank. Every remaining verdict
has a named owner and must be handled by that arc rather than folded into this item.
BANKED 2026-08-01 (r32 train): io validates — 59 matching · 2 disclosed (alloc-count-semantics).
The os.runtime_rand hand-own landed with the os-roots lane and the four OffsetWriter tests pass; the
probe fix and the disclosures above did the rest. One standing footprint note: the satisfies-but-never-
witnessed recorder (r32’s converter increment) adds 2 GoImplement records to io’s production
package_info.cs on every -tests regeneration; the committed file predates the recorder and is
deliberately NOT rebanked (charter: no partial rebanks), so sweeps show that +2 as expected drift —
restore, don’t chase — until the whole-corpus regen levels it, along with the rest of the increment’s
measured 34-file footprint.
context — five converter roots closed; 36 of 38 match; two rooted failures remain (2026-08-02)
Attempted after the wave3 channel semantics were ground-truthed. The channels are not the problem
and never appear in this census — context’s suite is the stdlib’s sharpest select/cancellation
exerciser (100-node cancellation trees, interlocked cancels, closed-channel Done() broadcast,
AfterFunc registration races) and every one of those tests passes. That is a strong independent
confirmation of the wave3 landing, and the single most useful thing this arc measured.
Five converter roots stood between the package and a run; all five are fixed and documented in
ConversionStrategies-Reference.md:
| # | First diagnostic | Root | Layer |
|---|---|---|---|
| 1 | CS1003/CS1026/CS1513 ×195 in x_test.cs |
a func literal inside a for … range composite literal emits its capture snapshot — a STATEMENT — into the element position; visitRangeStmt provided no pre-statement hoist sink (the fourth statement kind to need one) |
converter |
| 2 | CS0051 ×4 — testingT less accessible than XTestParentFinishesChild |
visitTypeSpec asked the testInlineTypeAccess arm FIRST, so it decided the modifier’s VALUE from the name and discarded the publicization signal |
converter |
| 3 | CS8030 — anonymous function converted to a void-returning delegate |
a returned FUNC LITERAL is typeless in C#, which allExecWrapperReturnsAreTypeless (written for nil/constants) did not count |
converter |
| 4 | CS1929 — timerCtx has no Done, best overload wants ж<afterFuncContext> |
the internal bridge re-recorded a production↔production pointer pair production already implements, minting a DUPLICATE adapter whose members resolved in the test class’s scope (and whose cancel was an EMPTY body) |
converter |
| 5 | CS8917 + CS8130 in example_test.cs |
a func literal returned inside another literal has no natural type, so the enclosing lambda has none either — the sibling of lambdaConstReturnCastType |
converter |
Root 1 is guarded by the RangeExprFuncLitCapture behavioral test (its A/B reproduces the cascade);
roots 2–5 are -tests-only shapes with no behavioral-corpus expression, so context’s own banked
suite is their guard when it banks.
T.Deadline was ALSO still capability-blocked, and that was pure staleness. The member landed with
the one-tree consolidation (core/testing/testing.cs Deadline + TestHost.PackageDeadlineUtc) but
supportedTestCapabilities() was never widened, so six of context’s tests — TestDeadline,
TestTimeout, TestSimultaneousCancels, TestInterlockedCancels, TestLayersCancel,
TestLayersTimeout, i.e. the whole tree-cancellation family — were excluded rather than run. Widened,
with the charter §9 roster scan done first (positive control context/x_test.go:50 + net/net_test.go:78
both fire): the only validated package whose _test.go calls it is os/signal, and both of its call
sites are in //go:build unix files this platform never builds. All six now run and pass.
Census after all six changes: 38 top-level verdicts, 36 pass, 2 fail. The two failures are rooted and owned elsewhere:
| Test | Root | Owner |
|---|---|---|
TestValues |
internal/reflectlite’s rtype.String() is the literal Go conversion — t.nameOff(t.Str).Name() over a type-descriptor name offset the managed bridge never populates — so it returns "". reflect’s equivalent is hand-owned over GoReflect.GoTypeName (type.cs:517 placeholder); reflectlite’s mini-bridge only ever landed Len/Swapper. Symptom: context.Background.WithValue(, c1k1) where Go prints WithValue(context_test.key1, c1k1) — the stringify fallback arm for a key with no String() method. |
reflection-bridge arc |
TestAllocs |
testing.AllocsPerRun unit mismatch, the established alloc-count-semantics class (io, strings, bytes). MEASURED before ruling: Background() allocs = 128.000000 want 0, WithValue = 754 want 3, WithTimeout(1ns) = 3744 want 12, WithCancel = 2104 want 5, WithTimeout(5ms) = 4876 want 8 — bytes in every case, so no allocation behavior can satisfy a count assert. A signature-pinned disclosure is warranted; it is deliberately NOT written here, since a disclosure manifest belongs with the banking commit that verifies it end to end. |
context’s banking arc |
So context is one reflectlite member plus one disclosure away from banking, with nothing
context-local left. Note the reflectlite gap is not context-specific: any package whose code path
reaches reflectlite.TypeOf(x).String() gets an empty string today, silently.
Build-blocked, each its own root
| Package | First diagnostic | Note |
|---|---|---|
image/jpeg |
CS0111: … already defines a member called 'init' |
DONE 2026-07-31 — 14/14, banked. NO converter change was needed — the diagnostic was stale by the time the row was written. The converter has always uniquified multiple package inits from a package-scoped counter (init, initΔ1, … in visitFuncDecl.go), and jpeg’s production pair (reader.go + writer.go) emits correctly. The collision was between PRODUCTION’s init and INTERNAL test file dct_test.go’s, which the recompile model put in the same jpeg_package; the whitebox-reference model emits internal test declarations into <pkg>_internal_test_package, so it cannot form. A corpus scan finds 12 packages with both a production and a test init (flag, net, os, runtime, sync, testing, time, crypto/x509, image/jpeg, net/http, os/signal, os/user); every one takes a reference model. The recompile FALLBACK (recordsRequireProductionMutation) would still collide — latent, reachable by no package today, deliberately not fixed speculatively. Cross-file multi-init is now guarded by the MultiFileInitOrder behavioral test (five inits across three files, order-compared vs go run); Solitaire already covered two in one file. |
index/suffixarray |
CS0206: A non ref-returning property or indexer may not be used as an out or ref value |
DONE 2026-07-31 — 12/12, banked. TWO go2cs-gen defects, stacked, both general. suffixarray_test.go declares type index Index — a defined type over the production struct — and Go gives it Index’s field set. (1) GetStructDeclaration resolves an underlying struct only from SOURCE, and a real MSBuild <ProjectReference> arrives as compiled METADATA, so under the white-box model NO members were forwarded and every x.sa/x.data was CS1061; a symbol-based fallback now resolves it, forwarding what IsSymbolAccessibleWithin permits — Go’s exported/unexported rule projected into C#. (2) The forward was a get/set property, i.e. a VALUE, so x.sa.len() (a this ref receiver) and &x.sa could not bind — this row’s original CS0206. It is now an [UnscopedRef] REF-returning property, a strict superset. Fixing (1) alone collapsed the CS1061 wall onto exactly the CS0206 recorded here: root-cause layering, the first diagnostic moving rather than clearing. Full rule: docs/ConversionStrategies-Reference.md, The forwarded member must be a VARIABLE, and the underlying may be METADATA-ONLY; guarded by the DefinedTypeOverForeignStruct behavioral test (whose A/B reproduces CS1061 and CS0206 separately). ⚠ TestNew{32,64}/exhaustive3 run ~35 min in C# vs 12.4 s in Go — a performance gap, not a correctness one; run-validated-sweep.ps1 gives the package a 60m deadline. |
internal/zstd |
CS1929: 'testing_package.B' … 'Cleanup' |
DONE 2026-07-27 — 534/534, banked. The common members are on core/testing’s B; see the retraction below. |
crypto/md5 |
CS0030: Cannot convert type 'System.Type' to 'uint' |
DONE 2026-07-31 — 11/11 (1 alloc-profile disclosure), banked. TWO defects, both general. unsafe.Alignof/Offsetof built their System.Type argument by splitting the CONVERTED C# text on . as though it were a Go field selector, so unsafe.Alignof(uint32(0)) emitted (uint32)0.GetType() — which C# parses as (uint32)(0.GetType()). Both now resolve the operand through go/types and emit typeof(T). Behind it stood a second: buf := buf in benchmarkSize reads a package-level buf declared in md5_test.go, and the shadowed-global qualifier named the PRODUCTION class (md5_package.buf, CS0117) rather than the white-box bridge class that actually declares it. |
path/filepath |
CS0103: The name 'Ꮡlstat' does not exist |
Build blocker CLOSED 2026-07-31; FindFirstFile host-killer CLOSED 2026-08-01; BANKED 2026-08-01 (r32 train) at 61 matching — see below. |
net |
CS1031: Type expected |
Syntax cascade CLOSED 2026-07-31 — see below. Still does not compile: 94 SEMANTIC errors stood behind it. |
path/filepath — build blocker closed; the FindFirstFile root closed; 46 of 61 match; two runtime roots remain
The name was never mangled. ßÅæ is the bytes E1 8F 91 rendered in cp437 — the UTF-8 encoding
of U+13D1 Ꮡ, the AddressPrefix marker. The missing symbol is Ꮡlstat, the heap box for
path.go’s var lstat = os.Lstat // for testing, whose address export_test.go takes
(var LstatP = &lstat, the hook that lets a test swap the implementation Walk calls).
go/packages excludes _test.go from a production package, so the production emission never saw the
address-taking and left lstat a plain field, while the test variant emitted Ꮡlstat. Fixed
generally: the converter now scans the build-selected in-package _test.go half for addressed
globals and folds them into packageAddressedGlobals — in ordinary and -tests conversion alike, so
production storage shape stays mode-stable. Rule, the three properties that keep it safe, and the
SiblingTestAddressedGlobal guard: ConversionStrategies-Reference.md,
A global addressed only by the package’s own _test.go is still heap-boxed.
Its reach is wider than filepath. A whole-stdlib A/B put the footprint at 13 globals in 13 files,
every one a Go “for testing” hook and no false positives: os’s lstat /
testingForceReadDirLstat / allowReadDirFileID, runtime’s readRandomFailed / useAeshash /
doubleCheckReadMemStats / casgstatusAlwaysTrack / forcegcperiod / timeBeginPeriodRetValue,
reflect’s callGC, internal/poll’s logInitFD, net/http’s maxWriteWaitBeforeConnReuse and
testHookEnterRoundTrip, and time’s usPacific. Those are exactly the hooks os, runtime,
reflect, net/http, internal/poll and time need aliasing real storage before their own suites
can pass — so this is prerequisite work already banked for six future arcs, not filepath-local cost.
filepath now builds with 0 errors and the host runs. Root 2 below is closed (2026-08-01), and
closing it is what lets the host survive a whole-suite run — so the numbers no longer have to be
gathered per test. Measured in ONE -tests -test-action all -test-timeout 10m run: 46 of 61
match (C# 40 pass + 6 skip against Go’s 41 pass + 20 skip), with zero empty verdicts.
Every one of the 15 remaining mismatches reaches one of the two roots that are left — 14 the
symlink-privilege one, 1 the gogetenv one — and none is a marshalling failure:
| Root | Reached via | Note |
|---|---|---|
os.runtime_rand unimplemented |
os.MkdirTemp → nextRandom → testenv.MustHaveSymlink / initWinHasSymlink |
The same root the io row names — owned by the os operational arc. Go skips these tests for want of symlink privilege; C# infrastructure-errors before testenv can decide, so clearing this likely converts most of them to matching skips rather than passes. |
FindFirstFile struct marshalling |
EvalSymlinks → toNorm → normBase → syscall.FindFirstFile |
CLOSED 2026-08-01. findFirstFile1 handed (uintptr)new @unsafe.Pointer(Ꮡdata) to the raw Syscall, and the kernel wrote a 592-byte WIN32_FIND_DATAW over a C# struct whose [MAX_PATH]uint16 field is an array<uint16> — an 8-byte managed reference, not inline storage. The write clobbered that reference, so the next read was an IndexOutOfRangeException in PinnedBuffer or a hard AccessViolation (0xC0000005) that killed the host. Fixed as the third member of the struct-passing class below: findFirstFile1/findNextFile1 are hand-owned against a blittable mirror in syscall/zsyscall_windows_impl.cs, guarded value-level by the FindFirstFileData behavioral output test. TestDriveLetterInEvalSymlinks — the crash site — and TestEvalSymlinksCanonicalNames, TestToNorm, TestGlob/TestWindowsGlob/TestGlobUNC, TestWalk/TestWalkDir all now match Go. |
runtime.gogetenv — fatal error: getenv before env init |
testenv.GOROOT → runtime.GOROOT |
runtime.envs is never populated (Go fills it in goenvs during scheduler init); throw then re-faults on the unimplemented getcallerpc. Only TestBug3486 here, but it gates every testenv.GOROOT consumer. |
⚠ Resolved with root 2, and worth remembering as a shape. While that AccessViolation stood, one
full-suite run under-reported badly: the host died mid-TestDriveLetterInEvalSymlinks and every
later verdict read C#="", which presents as a mass infrastructure wall rather than as one crash —
so the package had to be bucketed per test. A single host-killing defect will do this to any package;
the tell is a run whose empty verdicts all fall AFTER one particular test. filepath’s whole-suite
run now has zero empty verdicts, so per-test bucketing is no longer needed here.
The remaining 15 split cleanly by root. Fourteen are the symlink-privilege family — Go’s
testenv.MustHaveSymlink skips them for want of SeCreateSymbolicLinkPrivilege, while C# never
reaches that decision: 3 die in os.MkdirTemp → runtime_rand first, 9 go on to attempt the symlink
and fail on the privilege, and 2 infrastructure-error on the consequences of having attempted it
(TestNTNamespaceSymlink’s mklink, TestWalkDirectoryJunction’s cleanup UnauthorizedAccessException
over the junction it created): TestEvalSymlinks, TestEvalSymlinksAboveRoot,
TestEvalSymlinksAboveRootChdir, TestEvalSymlinksIsNotExist, TestEvalSymlinksTooManyLinks,
TestGlobSymlink, TestIssue13582, TestNTNamespaceSymlink, TestRelativeSymlinkToAbsolute,
TestWalkDirectoryJunction, TestWalkDirectorySymlink, TestWalkSymlink, TestWalkSymlinkRoot,
TestWindowsEvalSymlinks. The fifteenth is TestBug3486 (getcallerpc after the gogetenv
throw). Clearing root 1 should convert most of the fourteen to matching skips, exactly as
predicted — so filepath’s remaining distance is entirely os/runtime work, with nothing
filepath-local left.
BANKED 2026-08-01 (r32 train): path/filepath validates — 61 matching, 20 of them
privilege-gated skips agreeing with Go’s. The os-roots lane landed both remaining roots
(os.runtime_rand → the fourteen become matching skips; runtime.envs → gogetenv works), and
TestBug3486 took one ruling on top: runtime.GOROOT() has no linker-baked defaultGOROOT in a
converted assembly, so the pipeline now exports GOROOT to both children (go test and the C#
host — user-ruled 2026-08-01, the run-time-export option over baking a machine path into committed
host metadata; testConversion.go’s runCommandWithTimeout). One FOURTH root surfaced only on the
merged tree — charter §9 layering: with the tempfile and mirror fixes in, TestNTNamespaceSymlink
got far enough to create its junction-to-a-volume-root and then t.TempDir() cleanup died
(UnauthorizedAccessException), because the host delegated to .NET’s Directory.Delete(recursive),
which opens some junction targets during its walk. Go’s cleanup is os.RemoveAll, which removes a
reparse point AS THE LINK. core/testing’s TempDir now walks with exactly those semantics
(reparse points deleted as links, never traversed; read-only cleared and retried) — general for
every future junction/symlink-creating suite, os’s own first among them.
net — syntax cascade closed; 94 semantic errors remain
CS1031 was one defect with a ~90-error blast radius, and it was not about net at all: the
anonymous-struct lift probe descended exactly one level of the declared type, so []struct{…}
lifted and []*struct{…} did not. ip_test.go’s var ipStringTests = []*struct{ in IP; str string;
byt []byte; error }{…} therefore emitted raw Go type text into the C# declaration. (The shape had
stayed hidden because a composed occurrence still resolves when some other declaration registered
the identical signature first; the embedded error makes this signature unique.) The probe is now a
recursive descent over the type-composing syntax — pointer, array/slice element, ...T, parens, map
value then key, channel element — shared by the struct and interface extractors, and the separate
one-off map-value probe it subsumes was deleted. Rule + the AnonStructComposedTypes guard:
ConversionStrategies-Reference.md, An anonymous struct
lifts from ANY depth of its declared type. Zero syntax errors remain in net — no
CS1031/CS1003/CS1519/CS1002/CS1513.
net still does not compile. What the cascade was hiding, bucketed — charter §9’s layering lesson in
its purest form, since Roslyn skips method-body binding while declaration errors stand:
| Count | Code | Root |
|---|---|---|
| 52 | CS0426 | The type name 'ConnᴠReader' does not exist in the type 'net_test_package'. The ᴠ value-adapter for a production↔production pair (net.Conn → io.Reader) is generated into the PRODUCTION class, but an external-test use site qualifies it with the TEST class. One root, in test-project-model record anchoring (splitExternalVariantRecords); 55% of all remaining errors. |
| 14 | CS1929 | Two shapes: core/testing’s T declares no Deadline (so a same-named contextWithNonZeroDeadline extension is offered instead), and socktest.Switch methods want a ж<Switch> receiver where a value is supplied. |
| 6 | CS8130 | deconstruction of a result whose type did not bind |
| 4 each | CS1061 / CS8183 / CS8917 | member lookup, var-in-deconstruction inference, delegate-type inference |
| 2 each | CS1501 / CS1503 / CS0029 / CS8934 | arity; ж<AddrError> → error; a (ctx, cancel) tuple assigned to Context; lambda return type |
Rooting those is the next net increment. Note net’s own init gap (the sync.OnceFunc nil panic at
fd_windows.cs:27) sits behind all of it, and the Tier-0 channel/rendezvous frog behind that — so
compiling is the realistic near-term goal, not validating.
Revised 2026-07-31 — six of those seven roots are fixed; ONE architectural blocker remains
Re-measured on a converter carrying the r27 adapter-resolver chip: 46 unique errors (the “94” above counts each twice — MSBuild reports every error once per pass). Six roots landed, each a general fix at its own layer; the count after each, in order:
| # | Root | Layer | Errors after |
|---|---|---|---|
| — | (start) | 46 | |
| 1 | A white-box production type is FOREIGN to go2cs-gen, so the interface-sourced adapter name must carry the package prefix — the carve-out the value arm already had (whiteboxProductionTarget) |
converter | 17 |
| 2 | A pointer-receiver method value binds the address in assignment context too (poll.CloseFunc = sw.Closesocket) — the value-context arm already did |
converter | 11 |
| 3 | &x.(*T).field — a type-assertion base is a pointer rvalue, so it field-refs the box instead of copy-boxing |
converter | 11 |
| 4 | A literal whose every return arm is untyped nil states its return type (the single-result twin of the multi-result rule) |
converter | 11 |
| 5 | goǃ gains the Func<…, TResult> twins deferǃ always had — go f(…) discards results for any f, including a func literal with a named result |
golib | 11 |
| 6 | var a, b = f() gates on identHasHeapBox, not the blanket identEscapesHeap flag — every tuple with an interface or func result was falling back to the broken per-name path |
converter | 3 |
| 7 | The elided pointer element composite ([]*struct{…}{{…}}) routes its interface fields, like both sibling composite paths |
converter | 2 |
Rows 3–5 cleared together on the same measurement (2–4 were independent roots whose sites overlapped
in the same three files). Every one is documented in
ConversionStrategies-Reference.md; behavioral CNR is
byte-identical across all 517 projects for the whole set, which is the expected shape — five of the six
converter roots are reachable only from Go that the behavioral corpus does not contain, and two only
under -tests.
The original bucketing held up well with one correction worth recording: row 1’s mechanism was not
splitExternalVariantRecords and not an anchor split. Both sides agreed on the anchor all along — the
record lands in package_test_info.cs and the class is generated into the test metadata class — and
only the simple name disagreed, because the converter asks “is the source type in another Go
package?” where the generator asks “is it in another assembly?” Under the white-box model those
differ for exactly one set of types. The board’s guess named the right file and the wrong seam; the
diagnostic (does not exist in the type 'net_test_package') reads like an anchor problem and is not
one.
The remaining blocker: testing.T.Deadline needs a type core/testing cannot name — CLOSED 2026-08-01, option (d)
testing.T.Deadline needs a type core/testing cannot nameCLOSED. The blocker was never about
Deadline; it was about there being twogo.time_packagedeclarations on disk. On 2026-08-01 the stub baseline retired and the converted standard library moved intosrc/core(commit2e8066da6), socore/testingsimply referencescore\timelike any other consumer — the answer none of (a), (b) or (c) below could be, because it removes the premise rather than working around it. Call it option (d): there is onetime.
testing.T.Deadline()now returns a real(time.Time, bool), reporting the instant the package deadline (-timeout) expires — seesrc/core/testing/testing.csandTestHost.PackageDeadlineUtc.DisableTransitiveProjectReferencesis not a problem here after all: the host is a FIXED reference of every generated test project, sotimearrives through it directly.Everything below is the record of the blocker as it stood. The footprint table still says which packages the member unblocks.
Both remaining errors are t.Deadline() (net_test.go:78, dial_test.go:391). Go’s signature is
func (t *T) Deadline() (deadline time.Time, ok bool), and net uses the result as a real time.Time
(deadline.Add(-time.Until(deadline)/10), td.Add(-arbitraryCleanupMargin)) — so no primitive or
golib stand-in can satisfy it.
core/testing is hand-owned and, per the F15b one-testing-package ruling, is bound by every test
host by path ($(go2csPath)core\testing\testing.csproj). It references only golib and the analyzer
today, and that is not an oversight — its whole public surface (TB, T, B, F) is expressible in
primitives and golib types. Deadline is the first member that needs a converted stdlib type, and
neither candidate works:
core/time— collides. Every.tests.csprojalready referencesgo-src-converted\time, and both assemblies declarego.time_package, so a project seeing both gets CS0433 on every use.go-src-converted/time— inverts the layeringcore↔go-src-convertedis built on, and drags the converted tree intogo2cs.slnx(which registerscore/testing).
Note DisableTransitiveProjectReferences=true on the test projects makes this worse, not better: the
reference would not flow, and a core/testing API mentioning time.Time would then be CS0012 at
every consumer — the reference-closure family again.
Three ways out, none of them a converter fix, all of them a decision above a single package’s arc:
(a) parameterize core/testing’s time reference per consumer (MSBuild AdditionalProperties on the
ProjectReference — works, but makes the one testing package polymorphic in its dependency and touches
every generated .tests.csproj); (b) promote time to a position both trees share, the way golib
already is; (c) rule that testing’s time-typed surface is out of scope and accept that packages using
it cannot compile their suites. Owed to a ruling, not to this arc.
The ruling came as (d): retire the second tree entirely (2026-08-01). (b) was the closest guess —
it just turned out the position time needed to share was the one golib already had, and moving ONE
package there would have left the same seam for the next member that needed a converted type.
Footprint, so the ruling is sized rather than guessed. Scanning GOROOT _test.go for a testing
receiver (\b(t|b|tb)\.Deadline\(\), positive control net/net_test.go:78) and dropping what this
platform and this campaign never build:
| Package | Note |
|---|---|
net |
this row |
net/http, net/http/httputil |
4 sites |
os/exec |
1 site |
runtime/pprof |
1 site |
context (x_test.go) |
1 site |
os/signal |
7 sites, all in //go:build unix files — never built on Windows, which is how os/signal banks at 1 today while carrying the call |
internal/poll |
splice_linux_test.go only |
cmd/go, cmd/cgo/... |
not stdlib validation targets |
So six packages, not the wider set a naive .Deadline() grep suggests (that one also catches
context.Context.Deadline). The os/signal row is worth keeping visible: it is exactly the shape of
counterexample that would look like it disproves this blocker, and does not.
net state: 2 errors, one root, no converter work left in it. Everything the r27 lane bucketed is
closed. When the ruling lands, net should compile on the next run — and the init gap (sync.OnceFunc
nil panic at fd_windows.cs:27) plus the Tier-0 channel frog are what stand between compiling and
validating, exactly as this section said.
Updated 2026-08-01: the ruling landed (option (d) above) and net builds — see the Deadline banner.
The init gap and the channel frog are what remain, exactly as predicted.
Ground-truthed 2026-08-02 (r37-poll scout) — the census, and the sync.OnceFunc row is STALE
Measured on the post-r37-poll tree, one pipeline invocation
(-tests -test-action all -test-timeout 20m). The wall is exactly one root, and it is not the one
recorded above.
| production + test build | 0 errors (warnings only) |
| Go side | 138 top-level tests |
| C# side | 0 reached — status: conversion-blocked, every row C#="" |
| excluded declarations | 129 (unsupported capabilities) |
The sync.OnceFunc nil panic at net/fd_windows.cs:27 does not reproduce. That line is
poll.InitWSA(), and nothing gets far enough to execute it — InitWSA appears nowhere in the run.
Whatever closed it closed it uncredited, exactly the staleness charter §9 warns about; probe, don’t
inherit.
Today’s blocker is the OPEN pointer-PARAMETER nil-deref row, the one the os nil-receiver arc
named as still outstanding (“the same defect is still open for pointer PARAMETERS … the complete fix
is to give parameters the same unconditional DerefOrNull”). The chain is identical whether net is
entered through a program or through its test host:
go.net_package..cctor() net/addrselect.cs
→ netip.AddrFrom16 net/netip/netip.cs
→ go.net.netip_package..cctor()
→ unique.Make → go.unique_package..cctor() unique/handle.cs
→ concurrent.NewHashTrieMap
→ concurrent.newIndirectNode(nil) internal/concurrent/hashtriemap.cs:372
→ PanicException: runtime error: invalid memory address or nil pointer dereference
func newIndirectNode[K, V comparable](parent *indirect[K, V]) *indirect[K, V] {
return &indirect[K, V]{node: node[K, V]{isEntry: false}, parent: parent} // parent is nil here
}
internal static ж<Δindirect<K, V>> newIndirectNode<K, V>(ж<Δindirect<K, V>> Ꮡparent) {
ref var parent = ref Ꮡparent.Value; // ← eager entry alias; the body never dereferences it
return Ꮡ(new Δindirect<K, V>(node: new node<K, V>(isEntry: false), parent: Ꮡparent));
}
The body only ever uses Ꮡparent; the alias exists and panics. Neither nilSafePtrParamNames
heuristic fires (the parameter is not nil-compared in the body and no same-package call site passes a
literal nil — NewHashTrieMap’s does, but through a generic instantiation). So net is a
one-root wall, and that root is already designed: the parameter arm of DerefOrNull. It is a
much larger emission footprint than the receiver arm (3167 entry aliases) and wants its own
measurement and ruling — but it now has a second package demanding it, and unique and
internal/concurrent are blocked by the same line.
Nothing beyond it is measurable yet: with zero tests reached there is no second bucket to report. Re-run this census the moment the parameter arm lands.
time — builds and RUNS (2026-08-02, r35): 139 pass / 17 fail / 2 skip / 1 infra-error of 159
time was opened the day the channels frog was confirmed closed. It went from 260 build errors to
0, and the host now runs the whole suite in ~60 s with zero empty verdicts — the timer
machinery in time_impl.cs (one global heap on a Windows high-resolution waitable timer) holds up:
TestTicker, TestTickTimes, TestAfterTimes, TestAfterTick, TestTimerStopStress,
TestTimerModifiedEarlier, TestAdjustTimers, TestLongAdjustTimers, TestAfterFuncStarvation
and the sleep family all pass against real rendezvous. No channel-semantics defect was found; the
one channel-shaped failure is a documented model divergence, not a wave3 regression (below).
Seven roots stood between the package and a build; all seven are fixed and none was time-specific.
Six are in the converter or go2cs-gen, one is a hand-owned reach:
| Errors | Root | Layer |
|---|---|---|
| 1 (blocking all) | A mixed-accessibility GoImplicitConv pair whose less-accessible side is in ANOTHER assembly has no legal operator — skip it (export_test.go’s type RuleKind int over production ruleKind) |
go2cs-gen |
| 176 | A DOT-imported collision-renamed CONST/VAR emitted its raw Go name (Second, UTC, Hour, …) |
converter |
| 44 | A collision-renamed member kept the RAW package qualifier where the file’s using is Δ-renamed (time.ΔNanosecond vs Δtime.ΔNanosecond) |
converter |
| 33 | A local/parameter that SHADOWS a package name was resolved as the package — getAliasedTypeName applied to a rendered expression; time.Year() → Δtime.Year(), time.Month() → timeꓸMonth(), time.Hour() → time.ΔHour() |
converter |
| 3 | A nested func literal’s captures hoisted to the ENCLOSING statement’s buffer, above the declaration they name | converter |
| 2 | A folded constant of a NAMED type lost its type (8 * time.Hour → a bare long) — the loud half is CS1929, the silent half is d printing as digits |
converter |
| 1 | A concat of two SLICED string literals has no C# operator (span + span is literal-only) |
converter |
Plus the runtime blocker behind the build: time/tzdata’s init() pulls
time.registerLoadFromEmbeddedTZData by //go:linkname, which was a throwing stub — inside a MODULE
INITIALIZER, so a blank import _ "time/tzdata" took the host down before main. Now a real
forwarder (see A whitelisted target may be ORDINARY CONVERTED GO in the reference). That fix pays
for itself twice: with tzdata registered, loadLocation falls back to the embedded database, which is
how the suite’s initTestingZone reaches America/Los_Angeles at all — its hard-coded
../../lib/time/zoneinfo.zip cannot resolve from the C# host’s working directory.
Guard for the six general converter/generator roots: the PackageNameShadowing behavioral test
(a describe(time time.Time) parameter, a time := local, Δ-qualified renamed members, a
dot-importing sibling file, the named-type fold in both positions, and the sliced-literal concat —
output-compared vs go run) plus FuncLitArgCapture case 15 for the hoist.
The 17 remaining failures, rooted, none of them time-local machinery:
| Count | Tests | Root | Owner |
|---|---|---|---|
| 6 | TestChan and its five subtests |
Documented model divergence, not a defect. Go 1.23 made a chan-based Timer/Ticker channel SYNCHRONOUS (#37196) by coupling the channel’s receive path to the timer inside the runtime; time_impl.cs reproduces Go’s own GODEBUG=asynctimerchan=1 mode instead, so tim.Stop() = false, want true and “extra tick” are exactly what that mode produces. ⚠ The asynctimerchan=1 SUBTEST also fails, which the divergence does NOT explain — either t.Setenv("GODEBUG", …) does not reach the converted godebug, or the async model has its own bug. That subtest is the honest next probe here. ⚠ HISTORICAL — superseded twice: the mode-1 failure was the one-firing-per-pass burst (r39-timer), and the mode-0 “divergence” is IMPLEMENTED (r39b); TestChan passes in all three modes. See RESOLVED — r39b lands the synchronous timer channel below. |
time / godebug |
| 9 | TestDefaultLoc, TestNanosecondsToUTC, TestSecondsToUTC, TestParse, TestTimeGob, TestTimeIsDST, TestTimeJSON, TestUnmarshalInvalidTimes, TestZoneBounds |
All die with the same nil pointer dereference inside GoFunc.HandleFinally. Every one of them formats a Time through fmt on its FAILURE path (%#v, %+v, %v of a struct with a *Location), so the NRE is plausibly SECONDARY to a comparison that already failed — the reflect/fmt bridge, not the clock. Not rooted; the next increment should print the pre-format comparison rather than reason about the stack. |
reflect/fmt bridge |
| 1 | TestParseErrors |
A REAL parse divergence: Go reports extra text: "07:00" where C# reports cannot parse "Z07:00" as "Z07:00" — the Z07:00 layout element consumes differently. format.go conversion defect, time-local. |
time |
| 1 | TestTruncateRound |
math/big.mulAddVWW is an unimplemented asm stub (NotImplementedException), reached through big.Int.Mul.math/big ARC — it was a build-tag selection. math/big predates the purego convention and gates its portable fallbacks on its own math_big_pure_go, which the default tag set did not carry, so all EIGHT of arith_decl.go’s bodyless declarations became throwing stubs. The scope was not one test: the whole package compiled clean and could not do arithmetic — a direct probe dies inside big.Int.SetString, i.e. parsing a decimal string, because that is already a mulAddVWW. See purego is not the only spelling of this decision in ConversionStrategies-Reference.md. |
|
| 1 | TestUnmarshalTextAllocations |
got 3784 allocs, want 0 — the established alloc-count-semantics unit mismatch (AllocsPerRun counts mallocs in Go, BYTES on the CLR). A disclosure candidate by the class strings/io already established; not self-ruled here. |
ruling |
So time’s distance is: one time-local parse bug, one probe (asynctimerchan=1), one shared
reflect/fmt-bridge NRE family worth 9 verdicts, and two rows owned elsewhere. Nothing about timers,
sleeps, tickers or channel rendezvous is in the way.
Re-measured 2026-08-02 (r37-time-os-fin): 146 pass / 11 fail / 2 skip / 0 infra-error of 159
Measured as a same-session A/B, both arms on this tree, only src/core/math/big differing:
| Arm | Split of 159 verdict rows (137 top-level + 22 subtests) |
|---|---|
| math/big asm stubs (the r36 state) | 145 pass · 11 fail · 1 infrastructure-error · 2 skip — reproducing the r36 record exactly |
| math/big pure-Go arith | 146 pass · 11 fail · 0 infrastructure-error · 2 skip |
Exactly one row moved — TestTruncateRound, infrastructure-error → pass — which is what the
math_big_pure_go build tag was expected to do and nothing else. The infrastructure-error column
is now empty, so every remaining row is a real verdict disagreement rather than a host casualty.
The 11 failing rows, exhaustively, in three buckets:
| Rows | Tests | Bucket |
|---|---|---|
| 8 | TestChan + asynctimerchan={0,1,2} + their Timer/Ticker children |
The timer-model item, recorded and deliberately not taken: time_impl.cs §”⚠ OPEN — a periodic timer can fire an UNBOUNDED BURST in one service pass”. The Timer half under asynctimerchan=0 is the accepted sync-mode divergence; the Ticker half fails in all three modes and is the burst. The faithful fix (“fire each timer at most once per pass”) changes the heart of the model and wants its own lane. The t.Setenv("GODEBUG", …) half of the old ⚠ is closed — r36 proved the converted godebug sees it. ⚠ HISTORICAL — both halves are now closed: the burst by r39-timer, the sync-mode divergence by r39b (see RESOLVED — r39b lands the synchronous timer channel below). |
| 2 | TestTimeJSON, TestUnmarshalInvalidTimes |
The reflect-bridge chip’s rows — the last two survivors of the old 9-verdict NRE family (r36’s honest traceback rooted the other seven at Location.lookup, and they pass). Untouched here by fence. |
| 1 | TestUnmarshalTextAllocations |
Alloc-count-semantics, awaiting the coordinator’s disclosure ruling — unchanged in status, but the number moved: got 3544 → got 2728, an exactly-predicted −816 B/run (6 × 136, the six parseUint range loops in parseRFC3339’s UTC path) from the allocation-free slice<T> enumerator. Also measured as an A/B on this tree; the board’s older 3784 predates other r36 fixes. Nonzero remains, so a ruling is still what settles this row — see docs/CleanupBacklog.md item 7 (IByteSeq<T> interface boxing) for the next lever. |
TestParseErrors is gone from the failing set (r36’s fallthrough-placement fix), as are the
seven Location.lookup rows. time’s distance to a bank is now: the timer-model item, the
reflect-bridge chip, and one ruling — three owners, none of them the converter, and nothing
time-local outside the timer model.
Re-measured 2026-08-03 (r39-timer): 152 pass / 5 fail / 2 skip of 159 — the timer model is CLOSED and every residual row is a RULING
The reflect-bridge chip’s two rows (TestTimeJSON, TestUnmarshalInvalidTimes) closed on their own
between r37 and this lane — increment 5 landed, and the base commit 832f0960d already measured
148 pass / 9 fail / 2 skip: the eight TestChan rows plus the one alloc row and nothing else.
This lane took the timer-model item and rooted the alloc row.
The timer-model item is fixed, and the faithful fix was one statement. The burst was never a
“fire at most once per pass” heuristic waiting to be invented — it is what Go gets for free by
sampling the clock ONCE per service pass. timers.check reads nanotime() once and threads that
value through timers.run(now) into timer.unlockAndRun(now); the clock is never re-read inside a
pass. serviceTimers was re-reading it on every drain iteration, so the theorem that bounds Go did
not hold here. Moving int64 now = runtimeNano(); above the drain loop restores it, and the bound is
then provable rather than enforced: for a periodic timer next = when + period*(1 + delay/period)
with delay = now - when = q*period + r, 0 <= r < period, so next = now + (period - r) > now
strictly — the re-peek always breaks. One-shots clear when. Hence every timer fires at most once
per pass, for every period including the 1 ns testTimerChan resets to. It does not rate-limit: the
pass then waits until the new head deadline, which for a fast ticker is already past, so the next pass
begins at once — exactly Go’s scheduler calling check again. Recorded in
ConversionStrategies-Reference.md, ONE firing per timer per pass.
Measured on this tree, same command both arms (go2cs -tests -test-action all -test-timeout 10m):
| Row | Base 832f0960d |
After the fix |
|---|---|---|
TestChan/asynctimerchan=0/Timer |
fail — tim.Stop() = false, want true + extra tick |
fail — identical message |
TestChan/asynctimerchan=0/Ticker |
fail — extra tick + early done |
fail — extra tick ×4, early done gone |
TestChan/asynctimerchan=1/Ticker |
fail — extra tick ×2 + early done |
pass |
TestChan/asynctimerchan=2/Ticker |
fail — extra tick |
pass |
TestChan/asynctimerchan={1,2} parents |
fail | pass |
TestChan/asynctimerchan=0 parent, TestChan root |
fail | fail (mode 0 only) |
TestUnmarshalTextAllocations |
fail — got 216 allocs |
fail — got 216 allocs (untouched) |
+4 rows, and early done — the burst’s signature — is gone from every mode.
What the mode-0 ruling now decides over: exactly 4 rows, and they are the documented divergence,
row for row. With the burst gone, asynctimerchan=1 and =2 pass completely — Timer and
Ticker. Those are the modes where testTimerChan sets synctimerchan=false and therefore drains
stale values explicitly. Only mode 0 fails, and each of its failures sits either inside a block the
test guards with if synctimerchan (the tim.Stop() = false, want true pair, which is #37196’s
Stop-blocks-old-values semantics) or on a noTick() whose preceding drainAsync() is a deliberate
no-op in sync mode (the four extra ticks). The same implementation passes the identical test body
wherever the test expects asynchronous semantics and fails only where it switches to expecting
synchronous ones. That is the accepted GODEBUG=asynctimerchan=1 divergence and nothing else — no
residual burst, no channel-rendezvous defect. Closing it for real means implementing Go 1.23’s
synchronous timer channel (the ignored syncTimer(c) argument), which lives inside golib’s channel
implementation — a Tier-0 golib capability, not a time fix.
TestUnmarshalTextAllocations — rooted, and the board’s previous attribution was WRONG. The r38
train recorded “the FINAL 216 B live above parseRFC3339 in the Time.UnmarshalText wrapper chain”.
Measured directly (a probe project borrowing InternalsVisibleTo("time.tests"),
GC.GetAllocatedBytesForCurrentThread() over 2,000 runs), zero bytes are above parseRFC3339:
| Frame | B/run |
|---|---|
Time.UnmarshalText(data) |
88 |
parseStrictRFC3339(b) |
88 |
parseRFC3339<slice<byte>>(b, Local) |
88 |
Date(...), daysIn(...), isDigit(...) |
0 |
the same parseRFC3339 body with the closure replaced by a static local function |
0 |
| a bare capturing lambda, isolated control | 88 |
the converted TEST body: heap(new Time(), out var Ꮡt) alone |
128 |
the converted TEST body: heap(...) + UnmarshalText |
216 |
216 = 88 + 128, and both halves are converter emission, not time:
- 88 B —
parseRFC3339’sparseUintfunc literal. It capturesok, so C# hoistsokinto a display class and allocates that class plus aFunc<>delegate on every call (24 + 64 = 88, matched exactly by the isolated control). Go stack-allocates both, because escape analysis proves the closure does not escape. The general converter fix is real and valuable — a func literal bound to a local that is only ever CALLED should be emitted as a C# local function, which captures without allocating — but it is a new emission mode inconvFuncLit.go/captureModeOperations.go(847 + 1,136 lines) reaching every closure in the corpus. - 128 B — the converter heaps the test’s own
var t Time. The emission isref var tΔ1 = ref heap(new Δtime.Time(), out var ᏑtΔ1);becauset’s address is taken by the pointer-receiver callt.UnmarshalText(in). Go keeps it on the stack (that is why the assert says zero). NoteᏑtΔ1is never referenced in the emitted body — the box is minted dead — so a narrow rule (“don’t heap when the emittedᏑxis unused, because a C#refparameter provably cannot escape its callee”) looks sound and would be a headline win. It is still an escape-analysis change, which charter §7 puts behind an adversarially-reviewed design.
Consequence for the ruling: this row is NOT a clean disclosure candidate. The established
alloc-profile class covers asserts the managed CLR provably cannot satisfy; both halves here are
fixable converter gaps, and §5 says a real bug is never a disclosure candidate. Equally, neither half
alone flips the row (216 → 128 still fails want 0), so it cannot be cleared incrementally either.
The honest options are: (a) land both converter fixes as their own gated arcs and green the row
outright, (b) hold the row open until they land, or (c) disclose it knowingly as a converter-gap
rather than a CLR-semantics divergence — which would be a new disclosure class and should be decided
as one. Not self-ruled here.
time’s distance to a bank is now two RULINGS and zero open engineering: the mode-0
sync-timer-channel divergence (4 rows, needs a golib channel capability to close for real) and
TestUnmarshalTextAllocations (1 row, needs two converter arcs to close for real).
RESOLVED — r39b lands the synchronous timer channel; the 4 mode-0 rows close (2026-08-03)
Ruling #1 below commissioned the arc; it is implemented. The change is small because the guarantee is small, once stated as a guarantee rather than as plumbing:
A
StoporResetprevents any tick generated before the call from being received after it.
Two mechanisms carry it, at Go’s own two layers. golib gains the hchan.timer hook the wave3
design deliberately left out — IChannelTimer installed by channel<T>.AttachTimer,
Capacity/Length masked to 0 while the owner answers HidesBuffer (asked LIVE, because
GODEBUG=asynctimerchan selects the model at every observation), and DrainBuffer() =
runtime.timerchandrain, the only sanctioned way to un-send. time_impl.cs gains Go’s
timer.sendLock + timer.seq: a service pass now only offers a tick — it captures seq with the
firing decision and re-checks it under sendLock before sending, so an offer a Stop/Reset
overtook is ABANDONED. seq is deliberately not gen (a firing bumps gen, so a delivery check
must not key off it). Stop/Reset bump seq and drain inside ONE sendLock hold — stronger than
Go’s ordering, and necessarily so: Go can drain outside the lock because a sync-mode chan timer is
heaped only while a receiver blocks on it, and this model’s service thread is always eager.
A third mechanism has no Go counterpart and is the arc’s real lesson. The adversarial round
measured that mechanisms 1 and 2 revoke correctly but cannot between them ANSWER correctly: in the
window mechanism 1 exists to cover, a tick is in neither place a Stop looks — when cleared at
commit, buffer not yet filled — so Stop revoked the tick and reported that there had been none.
Hundreds of one-shots per run where Go answers true for every one. Go never reaches that state
because a sync-mode chan timer nobody is receiving from is not heaped at all and therefore never
fires; eager firing opens the window, so eager firing has to close it — runtimeTimer.offered
records the in-flight firing and Stop/Reset count it as pending. The general form of the lesson:
a divergence in WHEN work happens is not free just because the observable end states match — check
the states in between. Two more review findings landed with it: the mode selector no longer routes
through the punned unsafe.Pointer cp (its non-nil-ness was an accident of two type layouts, and
this very change added a field to ChanCore), and asyncTimerChan now reproduces
runtime.atoi32’s parse, so asynctimerchan=00 is synchronous as in Go rather than asynchronous.
Measured on this tree, both arms with the same command, the fixed arm run twice with identical verdicts:
| Row | After r39-timer (df3da05d1) |
After r39b |
|---|---|---|
TestChan/asynctimerchan=0/Timer |
fail — tim.Stop() = false, want true + extra tick |
pass |
TestChan/asynctimerchan=0/Ticker |
fail — extra tick ×2 + early done |
pass |
TestChan/asynctimerchan=0 parent, TestChan root |
fail | pass |
TestChan/asynctimerchan={1,2} × Timer/Ticker |
pass | pass (async model untouched) |
TestUnmarshalTextAllocations |
fail — got 216 allocs |
fail — unchanged (ruling #2’s arc) |
| package | 152 pass / 5 fail / 2 skip | 156 pass / 1 fail / 2 skip of 159 |
time is therefore down to ONE row, and it is the alloc row ruling #2 already commissioned an arc
for. Nothing here is time-specific: the hook is on ChanCore, so any future owner-fed channel gets
the same revocation primitive. ⚠ DrainBuffer revokes values the channel already accepted and is
sound only for a channel whose producer owns it exclusively — it is not a general “clear the
channel” utility. Guard: the SyncTimerChannel behavioral project (stdout byte-compared against
go run), which asserts the pending answers, the absence of stale ticks, len/cap 0, that
AfterFunc is untouched, 200 Reset-to-imminent timers that must still DELIVER (the
counter-property that keeps the drain honest), 600 ticker Stop/Reset-vs-firing races that must
revoke exactly nothing, and two 600-timer batches armed against ONE absolute deadline and
stopped/reset at that instant. ⚠ That last shape is load-bearing and fragile in a way worth
recording: it only samples the window because the batch and the caller’s sleep share an absolute
deadline. The first draft gave each timer its own relative duration, so the caller woke milliseconds
after the flush and the neutered control PASSED — a guard that proved nothing. Neutered controls now
fire for all three mechanisms (drop offered: 315–483 of 600; drop the drain: 477 stale of 600;
drop the seq check: stale ticks in all four race sections).
math/big — 224 of 226 (re-measured 2026-08-09, r58b); root 3 CLOSED, the two left are both the alloc model
r58b (2026-08-09): root 3 below is FIXED and both gob rows pass. The reflection bridge now packs the typed nil —
reflect.Value.Interface()re-encodes a null read out of a POINTER-kinded slot as that slot’s canonical typed nil (ж<T>.NilBox, the same instancereflect.Zeroand every emittednil→*Tconversion already produce), sov.Interface().(GobEncoder)succeeds andbig.Int.GobEncode’sif x == nilarm is reached.TestGobEncodingNilIntInSliceandTestGobEncodingNilRatInSliceboth pass: 222 → 224 of 226, exactly those two rows. Guarded by theReflectTypedNilInterfacebehavioral test.
math/bigstill does NOT bank, for the reason root 3’s own paragraph predicted: rows 1 and 2 are the alloc model and neither is disclosable. (Superseded 2026-08-13 — the ж-box A3 section at the end of this board carries the post-A2 pinned pipeline measure: 224/226, TestMulUnbalanced 20,499,128 B (+0.06 %, unmoved), TestNewIntAllocs exactly 1 obj/run.) Both re-measured on this tree —TestNewIntAllocs: “measured 81,600 allocated BYTES over 100 run(s) … got 816.000000”;TestMulUnbalanced: “multiplication uses too much memory (20487208 > 51 times the size of inputs)” (inputs = (50000+40)×8 = 400,320 B, so the convertednat.mulallocates ~51× where Go bounds at 10×). Row 1 waits on the AllocsPerRun ownership decision; row 2 is a truthful, comparable measurement of the box model, which a disclosure would launder rather than explain.⚠ Attribution correction, measured as an A/B rather than reasoned. The paragraph below says this root “also blocks part of
encoding/gob(99 of 106)”. The bridge half does not. r58b ran gob’s full pipeline with and without the fix on the same tree: 99 of 106 both ways, the same seven divergent rows. The typed-nil root has TWO halves that pay DIFFERENT packages — the reflection READ path (fixed here; it pays math/big’s gob rows, because gob reaches math/big’s types throughreflect) and the EMISSION path (var ip *intboxed into an interface by ordinary converted code), which is what gob’s ownTestNilPointerInsideInterfaceand themustPanicfamily need. The emission half remains chip-class / design-with-user and is untouched.
Historical — the r57a state (superseded above)
math/big — 222 of 226 (measured 2026-08-09, r57a); three roots left, two of them the alloc model
Supersedes the 2026-08-02 state below and the board’s
9 of 226census. Both were taken with r56f’s named-numeric shift-masking defect live — the defect whose corrupted Lehmer cosequences madeGCD’sfor len(B.abs) > 1loop stop converging, i.e. an infinite loop inside this very package. With it fixed the suite runs to completion: 226 verdicts, 222 matching, 83 excluded (examples + benchmarks, Phase-4D). The “nilx/yGCD panic” recorded below did not reproduce;lehmerGCD’s converted entry guards its extended outputs correctly (if (Ꮡx != nil) { x = Ua.Value; … },int.cs:970) andbig.Rat’sSetFrac→norm→GCDpath runs, so that root is closed too.The four remaining rows are three roots, and only ONE is a defect:
TestNewIntAllocs— the AllocsPerRun-reports-BYTES shim, fifth member. Measured 81,600 B over 100 runs; the assert wants0allocations fromx.Add(x, NewInt(0))and is handed816.000000“allocations” that are really bytes per run. Report-never-disclose, per the standing rule.TestMulUnbalanced— the same alloc model, measured honestly in bytes on both sides. “multiplication uses too much memory (20487200 > 51 times the size of inputs)”. Go readsruntime.MemStats.TotalAllocaround the multiply and bounds it at 51× the input words, so unlike row 1 the units ARE comparable — the convertednatsimply allocates far more per word, which is theж<T>/slicebox model r56d decomposed to the byte on nistec. Not a disclosure and not a correctness failure: it is the allocation-model overhead stated as a budget, and it will move when that model does, not before.TestGobEncodingNilIntInSlice/TestGobEncodingNilRatInSlice— a REAL defect, and a general one: Go’s TYPED-NIL interface does not survive the conversion. Both panic identically with “interface conversion: interface {} is nil, not gob.GobEncoder” insidegob.EncodeValue(encoder.cs:303). In Go, an element ofmake([]*Int, 1)is a nil*Int, sov.Interface()yields a non-nil interface carrying(type=*Int, value=nil); the assertion.(GobEncoder)therefore SUCCEEDS andGobEncodeis dispatched on a nil receiver, whichmath/bighandles explicitly —func (x *Int) GobEncode() { if x == nil { return nil, nil } }(intmarsh.go:18). The whole test exists to exercise that contract. In the conversion the nilж<ΔInt>reaches the interface as a plainnull, losing its type identity, so the assertion fails and gob’scatchErrorre-panics (correctly — Go re-panics on a non-gobErrortoo). Scope is corpus-wide, notmath/big’s: anyx.(I)on a typed-nil pointer takes the wrong arm, and this is one of Go’s most load-bearing interface behaviors. Worth noting golib already has the vocabulary —ж<T>distinguishesIsNilStandardPointerfrom a null reference (DerefOrNull,ж.PointerExtensions.cs:359), so a typed nil is representable; what is missing is producing one where a nil pointer is boxed into an interface (the reflection bridge’sValue.Interface()knows the static type and is the narrow place to start). Chip-class / design-with-user, not a lane fix — it changes what== nilmeans for every converted interface. Also blocks part ofencoding/gob(99 of 106).Consequence for banking:
math/bigcannot bank on roots 1 and 2 regardless of root 3, so fixing the typed-nil defect paysencoding/goband the corpus, not this row. The package is nonetheless now one of the most thoroughly exercised in the corpus — 222 verdicts acrossInt,Rat,Float,nat, decimal/float conversion, primality, GCD and the marshalling surfaces.
Historical — the 2026-08-02 state (superseded above)
Until r37-time-os-fin math/big was in the 302-package clean compile and could not perform a single
operation: the math_big_pure_go build tag was missing from the default set, so all eight of
arith_decl.go’s assembly-backed declarations converted to throwing partial stubs (detail in the
TestTruncateRound row above and in ConversionStrategies-Reference.md). With the tag applied, a
direct Go-vs-C# probe — SetString, Mul, Add, Sub, Lsh, Rsh, Quo, Rem, Exp,
big.Float.Mul, and a 64-deep Mul chain — is byte-identical to go run. Before the fix the
same probe died on its first line, inside big.Int.SetString.
One root remains before the package’s own suite is worth running: big.Int.GCD with nil x/y
panics with a nil pointer dereference. Repro is three lines —
new(big.Int).GCD(nil, nil, a, b) — and big.Rat reaches it on the ordinary path
(SetFrac → norm → GCD), so all of big.Rat is behind it. Go documents nil x/y as the
normal non-extended call, so this is a real conversion defect, not an unsupported shape.
Measured on BOTH the committed corpus and a fresh whole-stdlib reconvert, so it is not the
pending deref-accessor rebank: lehmerGCD’s entry aliases already take the current
DerefOrNull/DerefOrNil accessors in the reconverted emission and it panics identically. Not
rooted further — it was found in passing while verifying the build-tag fix and is out of that
lane’s scope.
Runtime failures
| Package | State |
|---|---|
hash/maphash |
DONE 2026-07-29 — 22/22, banked. Computed float constants that directly use a named untyped integer wrapper now materialize once at the destination’s float width; TestSmhasherAvalanche’s mean is 50000 and the full SMHasher matrix matches Go. |
compress/flate |
DONE 2026-07-31 — 64/64, banked. TestWriterReset was NOT a state difference: deepValueEqual’s Func arm returned false unconditionally, on the reasoning that two nil funcs would already have matched the invalid == invalid rule at the top. That holds only for a nil func boxed as any; a nil func reached as a struct FIELD is typed by its static func type and is a VALID nil Value, so the arm declared every pair of nil func fields unequal — and the test nils fill/step/bulkHasher/bestSpeed precisely so DeepEqual can compare the rest. Go’s rule is “equal iff both nil”; the arm now asks it. The tell was that every field compared equal individually while the enclosing struct did not. |
image/gif |
DONE 2026-07-31 — 28/28, banked. TestWriter was the blank-import module-initializer gap and nothing else: with _ "image/png"’s init() forced, the PNG decoder registers and image.Decode reads ../testdata/video-001.png. No image/gif defect existed. |
image/png |
DONE 2026-07-31 — 28/28, banked. The old “does not validate” probe was stale by weeks: a fresh run split 15 of 17 top-level tests passing, and the remainder was ONE defect with a second stacked on top of it. The real root is that Go’s slice-to-array-pointer conversion (*[N]T)(s) was emitted as a copy. png’s cbTCA8 row loop writes every un-premultiplied pixel through d := (*[4]byte)(dst), so a non-opaque RGBA source encoded as an all-zero image — and the two TestWriteRGBA subtests that did pass passed by luck (the opaque one takes cbTC8 entirely; the fully-transparent one wants all-zero output, which is also what a lost write produces). array<T> now carries a (low, length) window and the pointer form takes array<T>.Alias; the value form [N]T(s) still copies, because Go’s does. Above it sat a redundant value adapter — see the row below — which only ever surfaced on diff’s failure path, so fixing the aliasing greened the package on its own. |
image/draw |
DONE 2026-07-31 — 9/9, banked. All four failures were two defects, both fixed at the root. TestDraw was the address-taken value parameter box-copy: DrawMask’s clip(dst, &r, src, &sp, mask, &mp) narrows all three in place, and Ꮡ(r) boxed a COPY, so the draw loop ran on the unclipped rectangle. (The empty-Pix panic above was that same unclipped geometry, not an assertion defect — the guess in this row was wrong.) The other three were value adapters carrying no Go dynamic type, so image.Image type switches took the wrong arm. |
RETRACTED — the encoding/base32/base64 “mode-unstable production emission” was STALE BANKED OUTPUT
This section previously recorded the receiver-box drift on encoding/base32/base32.cs (3/3 lines) and
encoding/base64/base64.cs (6/6) as a mode disagreement — the receiver-box analysis reaching a
different answer under -tests than under -stdlib — and ruled the drift “expected sweep output, and
must be restored, never banked”. Both halves of that are wrong. Re-measured 2026-07-31 on master:
| Emission | base32.cs / base64.cs |
|---|---|
whole-stdlib -stdlib -comments reconvert, master converter |
boxed (encʗp + ref var enc = ref heap(…) + return Ꮡenc) |
the -tests pipeline’s regenerated production .cs |
byte-identical to the above |
| the committed files | unboxed — the pre-c23caf4f9 form |
The two modes agree exactly. What actually drifted is the corpus: c23caf4f9 (an address-taken
value RECEIVER heap-boxes) landed before this row was written and moved these two files, and they were
never rebanked — so every sweep since compared a current emission against a stale bank and restored it
again, three times over. The prior “three measurements” attribution is charter §9’s false-alarm trap (a)
in its textbook form: a bin/go2cs.exe built before c23caf4f9 reproduces the reported result exactly,
including the claim that -stdlib “equals the committed file”. Same origin as the internal/zstd /
crypto/hmac retraction above — force go build -o bin/go2cs.exe before recording a coupling.
There is no mode-instability to close here, and there cannot be: a method’s receiver is
function-scoped, so its address can only be taken inside its own method body. A production method’s body
is production source; a _test.go file cannot add a statement to it. The receiver-box analysis therefore
reads an input -tests mode cannot widen — structurally unlike the package-level-var case the
sibling-scan fix above exists for, where a _test.go &g genuinely does address production storage.
Recorded as a property of the rule in
ConversionStrategies-Reference.md, An address-taken VALUE
PARAMETER heap-boxes too.
Both files are banked (2026-07-31) at the boxed emission, and both packages re-validate at their exact counts (base32 26, base64 17). The standing sweep drift is closed.
Open CLOSED — the REDUNDANT adapter was a key mismatch (value 2026-07-31, pointer 2026-08-02)
DONE. Both halves landed at the converter: ONE key spelling shared by the record loader and the
cast site (implementRecordKey / canonicalImplementRecordIfaceName, named valueImplementKey /
canonicalValueRecordIfaceName until the pointer set joined them), and the func-type exclusion this
row demanded (valueRecordRealizesAsPartialStruct, gating on the target’s Go underlying being a
non-*types.Signature). Whole-stdlib A/B, both roots seeded, 302/302 converted per side: 13 files,
497 constructions removed, every changed line the same edit, plus the 16 records that existed only
to generate those adapters; the rest of the corpus adapter census is identical count for count,
HandlerFuncᴠΔHandler included.
Two corrections to the row as filed below, both measured rather than reasoned:
- A SECOND divergence sat underneath the reported one. Besides the interface side, the record
carries the EMITTED C# type name while the use side named the GO type — image/color’s
RGBAisΔRGBAin its own metadata (collision-renamed against itsRGBA()method). That divergence alone gates the 478-site group; fixing the interface side by itself would have recovered only 19. - The 79
binary_*ᴠByteOrderare NOT this defect.encoding/binary/package_info.csholds noGoImplementlines at all — the package never converts one of its own values toByteOrder(Go’svar BigEndian bigEndiancarries novar _ ByteOrder = …witness), so there is no record to match and the consumer’s local adapter is the only realization.color.Palette→color.Model(5) survives for the same reason. A pair a package satisfies but never records is its own root — the one place where “the declaring assembly implements it” is true in Go and false in the emitted C#. That increment is now DONE (recordSamePackageImplements,samePackageImplements.go): the declaring side records the VALUE pairs it satisfies, behind five gates — exported interface, underlying not a*types.Signature, neither side generic, both sides declared in a file the run converts, and every interface method reachable within ONE embed hop (ImplementGenerator forwards a promoted member exactly that far) — and a whole-stdlib A/B landed the prediction below exactly, 89 constructions across 34 files (43 + 36 + 5 + 5), alongside 33 records added and 31 removed (3 prune-subsumed, 28 consumer-local) across 16 declaring packages; 68 files total,go2cs-stdlib.slnx0 errors.HandlerFunc→ΔHandleris absent, as the delegate gate requires. Owed, and the reason the depth gate is conservative: extending ImplementGenerator’s promoted-member forwarding past one hop would recovernet’s twotcpConnWithout*→Connrecords. The POINTER half of the SAME-PACKAGE recorder is now DONE too (2026-08-14, lane D) — distinct from the foreign-record key increment two paragraphs below, which fixed how a record is LOOKED UP; this fixes which records EXIST.recordSamePackageValueImplementsbecamerecordSamePackageImplementsand askstypes.Implements(*T, Iface)as well, behind the five value gates with TWO changes, each forced by the different trust rule. Added: BOTH sides EXPORTED (pointerRecordIsPubliclyRealizable), because a(Pointer = true)record is consumed by NAMING the generated<T>ж<Iface>class and ImplementGenerator scopes itpubliconly when both participants are — an unexported participant would advertise a class no consumer can reference (CS0122). Tightened: realizability requires DIRECT resolution (index length 1), no promotion, where the value bound allows one embed hop — the ж adapter’s promoted-member arms are keyed on embedded POINTER fields and, with exactly one present, take every unbound member unconditionally, which is sound for a DEMANDED record and not for a speculative one.StructPointerPromotionWithInterface’sMyCustomError(embeds both theAbserinterface and*MyError;Abscomes from the interface) is the corpus instance, and thego2cs.slnxbuild caught it as CS1929 bindingAbstotime.Abs— a reminder that the solution build is the only gate compiling generated adapters tree-wide, and that a speculative record must be bounded by what the GENERATOR can realize, not by what Go’s method set says. Whole-stdlib A/B, both roots seeded, 304/304 per side: 75 files, 184 records added / 117 removed (net +67, 1,071 → 1,138), 318 adapter constructions repointed, 0.csproj, 0README.md, marker gate 0 violations. Every removal is a consumer-local duplicate the declaring assembly now owns —go/parser49 andgo/types28 ofgo/ast’s node types, the fivedebug/*readers’io.SectionReaderpairs,net/http’sio.Pipe*andsync.Mutex→Locker. The 548-pair figure the deferral quoted was the raw same-package pair count, not the delta: most were already recorded from cast sites. This retires the L10 second-identity trap at the root.syscall’s threeSockaddrpairs were witnessed by one method body, so hand-owningRawSockaddrAny.Sockaddrdropped all three records andnetminted duplicates. Re-running that exact probe on both converters: pre-increment the records are absent andnetemitssyscall_SockaddrInet4жΔSockaddr; post-increment all three are present andnetemitssyscall.SockaddrInet4жΔSockaddr. The netpoll arc’s §7 blocker is unblocked at the record level (the port-alias decode defect itself still stands — see that doc). Guards:SamePackageImplementNoWitness(*Tally→Metricmoved negative→positive; new unexported-target negativetick),ForeignPointerImplementSuppression(Lonelikewise;shade.Levelnegative byte-identical). Still owed: the-testsclosure emits these records too, so the committed test sources of the validated roster want a refresh at the next milestone rebank. Guard:SamePackageImplementNoWitness. Rule:ConversionStrategies-Reference.md, A package records the pairs it SATISFIES, not only the ones it witnesses. Its whole corpus footprint, measured on the post-fix census by classifying every remaining<pkg>_<T>ᴠ<Iface>construction (is<Iface>declared in<T>’s own package, and is<T>apartial structrather than an interface or a delegate?), is 89 constructions in 3 packages:encoding/binarybigEndian/littleEndian→ByteOrder(79),image/colorPalette→Model(5),cryptoHash→SignerOpts(5). Everything else remaining is either interface-sourced (io.ReadWriteCloser,flate.Reader,net.Conn,ast.Expr— a different adapter kind entirely) or genuinely cross-package (syscall.Signal→os.Signal), or is the deliberately-excluded delegate (net/httpHandlerFunc→ΔHandler, 8+4).
The deferred POINTER increment is now DONE too (2026-08-02). importedPointerImplements carried the
same two divergences, and both sides now compose through the same shared implementRecordKey — no second
naming path, and canonicalRecordIfaceName retired with its last caller. The trust rule really is
different, and it turned out to be weaker, not stronger: (Pointer = true) is precisely the shape
ImplementGenerator realizes as the adapter class <T>ж<Iface>, so the record’s existence IS the
answer and no valueRecordRealizesAsPartialStruct analogue is needed (the delegate hazard cannot arise
on a set whose every member already took the adapter route). Measured before deciding, per the row’s own
discipline: an instrumented whole-stdlib run classified all 1,224 pointer lookups as 289 hits, 868
genuine no-records, and 67 near-misses — every one a true pair, no candidate-key regressions.
Whole-stdlib A/B, both roots seeded, 304/304 per side: 31 files, 66 constructions rewritten from the
consumer’s local <pkg>_<T>ж<Iface> to the declaring package’s own <pkg>.<T>ж<Iface>, plus the 37
(Pointer = true) records that existed only to generate those local classes. Zero additions; the total
adapter-construction census is unchanged at 4348, so this is a one-for-one redirection rather than a
removal — the pointer form’s dead machinery is a duplicate class, not an extra allocation. By declaring
package: text/template/parse 33, go/types 20, image 4, net/http 4, net/url 2, net/textproto 1,
go/internal/srcimporter 1, go/build/constraint 1. go2cs-stdlib.slnx 0 errors on the overlaid tree;
CNR byte-identical across all 544 behavioral packages.
Two findings worth carrying forward:
- A dependent EMISSION defect that only the collision-renamed types reach. A Δ-renamed foreign type
resolves through a whole-TYPE
global usingalias (imageꓸRGBA), which is an identifier and not a path, so composing the adapter onto it names nothing —imageꓸRGBAжImage, CS0246 ×11 (confirmed by building, not predicted). The foreign-adapter arm now rebuilds a dotless base as the package qualifier plus the type’s EMITTED simple name. The same latent composition sits in the neighbouring same-assembly (-tests) arm; nothing reaches it today and it was deliberately left alone. - No observable failure was reproduced, and that is the honest finding. The generated pointer
adapter’s
EqualscomparesIжAdapter.Boxby reference, so a redundant local adapter and the declaring assembly’s own one compare equal and alias the same object — unlike the value form, which really did breakimage/png’s%v. What is wrong is duplication plus a load-order-dependent dynamic type:AdapterRegistry.Registeris first-wins, so which assembly’s class a type-assert re-wraps into depends on which module initializer ran first.
Rule, both compositions, and the trust gates:
ConversionStrategies-Reference.md, A foreign implement record
is keyed in ONE spelling, and a VALUE one is trusted only for a partial struct. Guarded by the
ForeignValueImplementSuppression behavioral test (a multi-segment sibling that DOES convert its own
values, a collision-renamed implementer, and a named FUNC type as the live negative — the pre-fix
converter emits five adapters where the fixed one emits the func’s alone), with
ValueAdapterDynamicType as its byte-identical complement, and by the pointer sibling
ForeignPointerImplementSuppression (a self-converting sibling with a collision-renamed *Tone and an
ordinary *Plain as the positives, against two live negatives: *Lone, a pair the sibling satisfies but
never records, and shade.Level, an interface with the same SIMPLE name — pre-fix emits four local
adapters, fixed emits the two negatives’ alone).
The row as originally filed follows.
Converting a foreign package’s value into an interface that package itself declares emits a
local <pkg>_<T>ᴠ<Iface> adapter class even though the declaring assembly already implements the
pair. The converter already knows not to (convCallExpr’s both-foreign value arm consults
importedValueImplements, recorded from the dependency’s package_info.cs [assembly:
GoImplement<T, Iface>] lines) — the lookup simply never matches for a multi-segment import
path, because the two sides compose the interface key differently. Measured, not reasoned
(canonicalRecordIfaceName called directly):
| import path | load side (from the package NAME) | use side (the rendered C# name) | |
|---|---|---|---|
bufio |
bufio_package.Reader |
bufio_package.Reader |
match |
image/color |
color_package.Color |
image.color_package.Color |
miss |
encoding/binary |
binary_package.ByteOrder |
encoding.binary_package.ByteOrder |
miss |
Corpus footprint of the redundant constructions: 478 color_ΔRGBAᴠColor, 79
binary_{big,little}Endianᴠ ByteOrder, plus the rest of image/color’s models — every same-package
value-form foreign record in the corpus is a nested path, and not one is single-segment.
It is not merely dead machinery. The adapter is a second identity for one Go value: reflect
and fmt see the adapter object where the Value’s own type says the wrapped struct, which is how
it surfaced — image/png’s diff printing %v of a color.Color died with
System.ArgumentException: Field 'R' … is not a field on the target object which is of type
'go.image_package+color_NRGBAᴠColor'. (It masked the aliasing defect above: fixing the aliasing
removed the failure that reached the print.) A direct-boxed NRGBA and an adapter-wrapped one also
compare unequal in one direction.
Any fix must clear one hazard first. The record says nothing about how the DECLARING assembly
realized the pair, and a named FUNC type cannot be realized as a partial struct — net/http’s
[assembly: GoImplement<HandlerFunc, ΔHandler>] is realized as an adapter class there, so trusting
the record for it would emit a bare delegate into an interface slot (CS0029) in expvar,
net/http/cgi and three more. The usable gate is the target’s Go underlying: trust the record only
when it is not a *types.Signature.
Two live consumers are named by the reflection arc (§6.1’s adapter-type Kind/Elem follow-up),
and they do NOT overlap: this row removes adapters that were never needed, while the reflection
chip must still unwrap the ones that genuinely are (color_PaletteᴠModel, syscall_ΔSignalᴠΔSignal,
net_Connᴠ*). Both are real; neither subsumes the other.
Open — intermittent, on an already-banked package
| Package | State |
|---|---|
hash/maphash |
INTERMITTENT (filed 2026-07-31, not rooted). Banked and validating at 22/22, but ONE validated sweep died mid-TestSmhasher* with a .NET FailFast on a worker thread, the fault attributed to go.UntypedInt.CastTo<ulong>(Int64) with RhThrowHwEx on the stack. Two sibling sweeps in the same wave ran maphash to its exact banked count, and so did the r26 integration train’s own 66-package sweep over the three lanes combined (66 pass / 0 fail, 2,454 s), which ran maphash to its exact 22. The attribution is almost certainly misleading: CastTo is a raw reinterpret and cannot raise a hardware exception, so the likely fault is an NRE/AV in an inlined caller credited to the frame it was inlined into — e.g. unboxing a null any into UntypedInt on the worker path. SMHasher seeds randomly, which is what makes it probabilistic and why it reproduces on no fixed input. Rooted enough to file, not enough to fix: the next sighting should capture the full FailFast stack and the seed. |
The blank-import module-initializer gap — CLOSED (2026-07-31)
Go’s _ "image/png" imports a package purely for the side effect of its init(), and the
language guarantees that initializer runs before main. The converter maps a Go init() onto
[GoInit], which csproj-template.xml aliases to .NET’s [ModuleInitializer] — the right shape,
and a weaker guarantee: a module initializer fires at first access to something in its module,
so an assembly nothing in the program ever names is never loaded and its initializer never runs.
A blank import is by definition the case that names nothing, and the observable form was a registry
that stays empty: image/gif’s writer_test.go blank-imports _ "image/png" so png’s init()
calls image.RegisterFormat (image/png/reader.cs), it never ran, and TestWriter failed with
../testdata/video-001.png image: unknown format at 27 of 28.
The converter now emits, at the top of the importing file’s class body, a hook that forces it:
// blank import: go.image.png_package (side effects only; no using emitted — a `using _` alias hijacks C# discards)
[GoInit] internal static void initᴛᴛblankImportꓸimageꓸpng() { builtin.initPackage(typeof(go.image.png_package)); }
builtin.initPackage is RuntimeHelpers.RunModuleConstructor, which the runtime guarantees runs a
module constructor at most once (so several blank importers of one package are no-ops) and which
is measured AOT-safe — under Native AOT the gap does not arise at all, since a single native image
has no lazy assembly load. One hook per (assembly, imported package), named from the import path so
two blank imports in one file cannot collide; Go’s pseudo-packages (unsafe, builtin, C) are
skipped because the language gives them no initialization, which holds the corpus blast radius to
three files — crypto/x509 (sha1/sha256/sha512), runtime/metrics (runtime), runtime/race
(amd64v1) — rather than the seventy that carry import _ "unsafe" for //go:linkname. Full rule,
the ordering reasoning, and the deliberately-deferred alternative (forcing every import eagerly in
dependency order — the only way to reproduce Go’s init ordering in full, at the cost of loading the
whole transitive assembly closure at startup): docs/ConversionStrategies-Reference.md, A blank
import forces the imported package’s init to run. Guarded by the BlankImportSideEffects
behavioral test (a registry two blank-imported siblings fill from their inits, read back by an
importer that never names either) plus the TestBlankImportInitName / TestNoInitPseudoPackages
converter unit tests.
The other consumers this unblocks are all registration-by-blank-import: database/sql drivers
(_ "github.com/…/mysql" → sql.Register), net/http/pprof (its init() installs the
/debug/pprof handlers), image/png/image/jpeg as decoders for anything that calls
image.Decode, and time/tzdata. A blank import was never invisible to the build — it is in
go/packages’ import list, so the project reference already existed; only the load did not happen.
os — 681 of 683 rows agree + 1 disclosed; ONE residual, now ROOTED (r35-os → r39-osalloc, 2026-08-03)
⚠ CURRENT STATE, 2026-09-02 (lane
claude/sub-os-row), measured on the i7 at Debug/tiered against ago1.23.12oracle:osis 683 of 685 agreeing + 1 disclosed (TestUTF16Alloc), with exactly ONE real divergence left —TestWriteStringAlloc. Go: 665 pass / 20 skip. C#: 663 pass / 20 skip / 2 fail. 39 capability-excluded declarations, 4 gated capability entries. TheNetShareAdd/SHARE_INFO_2host-killer is CLOSED (see the ✅ block inos’s “REGRESSION” is a HOST CAPABILITY at the end of this file); the row moved 682 → 683 agreeing, two real divergences → one.osdoes not bank, and now for exactly one reason: ruling #1 holds a want-ZERO alloc assert to be satisfiable in principle, soTestWriteStringAllocis a real divergence rather than a disclosure. It currently readsexpected 0 allocs for File.WriteString, got 17(1,700 golib allocations / 132,000 bytes over 100 runs). The sub-sections below are the arc that got it here.Header as it stood before 2026-09-02: Current state is the r39-osalloc sub-section at the END of this block — 681 of 683 rows agreeing (173 of 175 top-level), 34 matching skips, 4 capability-excluded, and exactly one real divergence (
TestWriteStringAlloc). r39 decomposed that divergence to the byte and closed 65.6 % of it in two golib fixes; the remainder is architectural and is recorded there as an arc, soosdoes NOT bank on this row. Everything between here and there is the arc that got it there, kept for its roots and its retractions. The header below is the r36 state.Header as it stood before r38-os-fin:
os— 164 of 178 match + 1 disclosed; the unreached block is gone (r35-os → r36-os-tail, 2026-08-02)
Measured with go2cs -tests -test-action all -test-timeout 35m "<GOROOT>/src/os" src/core/os.
os builds with 0 errors and the host runs. Progression across the arc, all from one pipeline
command: 48 agreeing → 141 → 158 → 164; the first jump from the build blockers, the second from
the readReparseLink host-killer, the third from the element-alias arm and the run-directory shape
below. ⚠ Give it 35 m, not 15 — at 15 m under sibling-worktree load the host self-terminated at
900 s and reported the tail as unreached.
| Go | C# (r35) | C# (r36) | |
|---|---|---|---|
| top-level tests | 178 (143 pass · 34 skip · 1 fail) | 166 reached (123 pass · 34 skip · 8 fail · 1 infra-error) | 177 reached (129 pass · 34 skip · 12 fail · 2 infra-error) |
| agreeing | 158 | 164 | |
| disclosed | 1 | 1 (TestUTF16Alloc, alloc-count-semantics) |
|
| real mismatches | 7 | 13 | |
| unreached (host died) | 12 | 1 (TestPipeEOF) |
The mismatch count RISES while agreement rises because the r35 host died at test ~50: eleven of the thirteen rows below were never reached before, so they were counted as unreached rather than as failures. Six of them are load-sensitive (they pass standalone), and of the genuinely stable ones, every row is now rooted.
⚠ TestReadStdin’s 462 subtests still fill the errors list, and it is a NAME-ENCODING artifact,
not a failure. Two of its inputs contain \x1a (SUB). go test -json renders that rune in the
subtest name as the ESCAPED text \x1a; the C# host emits the raw rune, so the oracle pairs each
subtest as Go="pass" C#="" plus Go="" C#="pass" — 924 lines that read like a mass failure and are
not one. The top-level TestReadStdin AGREES. Fixing it means escaping non-printable runes in
TestReporter’s reported names the way Go does; that changes every package’s reported subtest names,
so it wants the full sweep as its gate and is recorded here rather than done in passing.
Closed in this arc
- Build blocker 1 — a production type ALIAS is invisible to its own test assembly. Under the
white-box reference model the production sources are not compiled into the test assembly, so the
global using FileInfo = go.io.fs_package.FileInfo;thatos/types.csdeclares is out of scope for a converted_test.go.export_test.go’svar Atime = atimenamesFileInfounqualified → CS0246 ×2, the whole build. Fixed at the same seam the foreign-alias arm already states: a same-package alias DECLARED IN A PRODUCTION FILE renders as its TARGET undertestWhiteboxReference(an alias declared by a_test.goemits its ownglobal usingand is left alone).typeNameResolution.go; CNR byte-identical. - Build blocker 2 — a
GoImplicitConvrecord with NO local operand.os_windows_test.go’s privilege helper convertssyscall.Handle(t)over asyscall.Token; both operands are foreign, soImplicitConvGeneratorhad nothing to extend and minted a phantompartial struct ΔHandleinsideos_test_package(CS1061 on.Value). Both arms ofcheckForImplicitConversionnow requireconversionRecordHasLocalOperand. GuardForeignPairNumericConv; CNR byte-identical. Rule:ConversionStrategies-Reference.md, A GoImplicitConv record needs at least one LOCAL operand. - Runtime root — a keyed element inherited the LHS variable’s interface.
TestCopyFS’sfsys = fstest.MapFS{"william": {Data: …}}(withfsysanfs.FS) ran every*MapFileelement through a spurious*T → Ifacecast whose deref-copy collapse turned the elidedᏑ(new MapFile(…))into the bare struct — CS0029 ×5. The element’s target is now the composite’s own value slot. GuardElidedPtrElemIfaceAssign; CNR byte-identical. Rule: A keyed element’s interface target is the composite’s own SLOT, never the LHS variable’s type. - Host-killer —
os.readReparseLink, hand-owned. A fourth member of the raw-metal-on-non-native- types fork, and the first to take the host down inos: the reparse-buffer structs end inPathBuffer [1]uint16, a Go inline array standing in for the variable-length name the kernel wrote after it and an 8-byte MANAGED REFERENCE in the conversion. golib correctly refuses to alias managed storage for a reference-bearing struct, so the reinterpret took the raw-address route and&rb.PathBuffer[0]resolved an object reference synthesized out of path bytes: ACCESS_VIOLATION inarray<uint16>.get_Item, at test 50 of 178.src/core/os/file_windows_impl.csdecodes the record out of the byte slice at its documented offsets (same remedy asdir_windows_impl.cs);manualConversionFuncsgainsos.readReparseLink. ⚠syscall.Readlinkcarries the SAME defect over its own privatereparseDataBuffer/symbolicLinkReparseBuffer/mountPointReparseBuffercopies — LATENT (nothing in the validated corpus reaches it), recorded rather than fixed speculatively.
Closed in the r36-os-tail follow-up (2026-08-02)
- Converter — an element pointer reinterpreted as an array pointer now ALIASES.
(*[N]T)(unsafe.Pointer(p))wherepis a*Temitsarray<T>.AliasPointer(p, N)— a window over the storagepis an element of — instead of the raw-address route, whose two lowerings were both wrong for it: dereferenced it read anarray<T>struct out of the pointed-at DATA, and underconvSliceExpr’s[:n]fusion it produced aslice<T>COPY whose writes went nowhere. That copy is what made all 462TestReadStdinsubtests read zeros. Same element type is the gate (aT[]view over differently-typed storage has no managed spelling, so every genuine reinterpret keeps the address route), golib decides at RUNTIME whether real element storage is behind the pointer, and Go’sNis clamped to the extent that exists — in this idiomNis a promise (10000,1<<16,0xffff) and the result is always re-sliced to the real count. One latent sibling defect fell out with it:SliceExtensions.slice(this array<T>, …)sliced the RAW backing, so explicit bounds over ANY window (Alias’s too) addressed the source’s elements rather than the array’s. GuardArrayPointerElementAlias; rule inConversionStrategies-Reference.md. Corpus emission footprint: 3 production sites, all in the same raw-metal family, none of them reached (§A/B footprint below). - Pipeline — the isolated run directory reproduces the package’s SHAPE, not just its files. The
converter enumerates the package directory’s immediate subdirectory NAMES into the manifest and the
input digest; the host creates them empty before staging fixtures. That is what
TestReadDirneeded (execbesideread_test.go) and it is the last of the environment-fidelity gaps inos.
A/B footprint of the element-alias arm — 13 files, all classified
Measured as a two-temp-root reconvert (base converter vs this one, same seed) rather than against
the committed tree: src/core at af5df9e16 carries ~132 files of pre-existing drift from other
lanes’ converter changes, which a diff-vs-HEAD would have mixed in. No file is in a validated
package. The corpus builds with 0 errors on the overlaid reconvert (304 projects).
| Sites | Files | Classification |
|---|---|---|
(*[4]byte)(unsafe.Pointer(n.Data(off))) in abi.Name.pkgPath |
reflect/type.cs, internal/reflectlite/type.cs, runtime/type.cs |
Strict improvement. The name blob IS managed byte storage, so the window is real where the address route punned an array<byte> struct (a reference + bounds) out of four name bytes. |
reflect.rtype.gcSlice over t.t.GCData |
reflect/type.cs |
Read-only GC-bitmap view. Was a ReadOnlySpan copy of raw memory, now a window (or the identical address fallback when GCData is not managed storage). |
reparse PathBuffer decode |
internal/syscall/windows/reparse_windows.cs ×2, syscall/syscall_windows.cs ×2 |
Same raw-metal family as readReparseLink, unchanged in outcome. PathBuffer [1]uint16 is a variable-length tail standing in for kernel bytes, so a managed window over it is one element and the old span read GC heap past that one element. Neither can work; the new form fails LOUDLY (a Go-style slice-bounds panic) instead of returning garbage. os does not reach these — its own decode is hand-owned (os/file_windows_impl.cs). |
| Win32 DNS record strings | net/lookup_windows.cs ×3 |
Native pointers, so golib takes the address fallback: byte-identical behavior. |
| runtime internals | runtime/{select,heapdump,mbitmap,string}.cs |
Paths the managed runtime does not execute (selectgo is superseded by ChanCore). |
AllowUnsafeBlocks true→false |
internal.syscall.windows.csproj, net.csproj, reflect.csproj |
Consequence, and a welcome one: the span fusion was those packages’ ONLY unsafe usage. |
⚠ The huge sentinel length is why the emission casts. runtime’s findnull/findnullw/
gostringw convert to *[1<<47-1]byte / *[1<<46-1]uint16; such a literal types as long in C# and
has no implicit conversion to nint (CS1503 ×3, caught by the corpus build, fixed by csNintLiteral).
It is also why AliasPointer CLAMPS: an unclamped (int) of that length would overflow.
The residual, every row rooted
| Row | Cost | Root |
|---|---|---|
| host-killer: an ExecutionEngineException whose SITE MOVES between runs | 12–29 | Not a defect at the crash site. Three runs died in three different places (TestReadlink’s AV, then syscall.Environ, then syscall.encodeWTF16 under os.MkdirAll), and each site runs CLEAN standalone — syscall.Environ() was probed end-to-end in its own converted program and returns the real block. That is accumulated heap corruption, and the strongest candidate is os_windows_test.go’s own createMountPoint: it reinterprets a managed []byte as a windows.MountPointReparseBuffer and WRITES four uint16 fields through it. golib’s Reinterpret cannot alias a reference-bearing struct, so the fallback hands back (ж<TDst>)(uintptr)box — a transient pinned address of a managed slice, written through after its pin expired. Remedy candidates, both bigger than a package arc: make the non-representable fallback PIN the source for the derived box’s lifetime, or make it fail loudly instead of returning a stale address. A blanket “fail loudly” is NOT available — reflect’s prefix-downcast idiom ((*structType)(unsafe.Pointer(t))) deliberately depends on the address route. |
TestDirectoryJunction |
1 | The same createMountPoint reinterpret, this time surfacing as a contained IndexOutOfRangeException at &buf.PathBuffer[0]. Raw metal on a non-native type, in TEST code that cannot be hand-owned — no converter or golib change can lay a managed array reference over inline OS bytes. |
TestReadStdin (462 subtests) |
CLOSED 2026-08-02 (r36-os-tail). The remedy this row named was the right one: (*[N]T)(unsafe.Pointer(p)) over a *T now emits array<T>.AliasPointer(p, N), a real window over the storage p is an element of, instead of the raw-address route whose [:n] fusion produced a slice<T> COPY. All 462 subtests pass. Guard ArrayPointerElementAlias; behavioral footprint one justified re-baseline (PointerCastSliceReinterpret’s same-element-type arm). |
|
TestNilFileMethods |
1 | CLOSED 2026-08-02 (r36-nilrecv) — the alternative this row named is the one that works. See A nil RECEIVER is nil-deferring, not nil-safe below. |
TestReadDir |
CLOSED 2026-08-02 (r36-os-tail). The remedy this row named, implemented: the converter enumerates the package directory’s immediate subdirectory NAMES (testFixtureDirectories, part of the manifest and the input digest) and the host creates them EMPTY in its run root before staging fixtures (TestHost.CreateFixtureDirectories). ReadDir(".") now sees the same shape go test does. Blast radius is far smaller than feared: across the validated roster only os, io and math/rand have any subdirectory beyond the testdata already staged with contents. |
|
TestCmdArgs |
1 | Newly REACHED 2026-08-02 (it was inside r35’s unreached block). Raw metal, pre-existing: syscall.CommandLineToArgv returns a NATIVE pointer, so (ж<array<ж<array<uint16>>>>)(uintptr)(r0) reads an array<T> STRUCT — a backing reference plus bounds — out of the pointer array’s own bytes, and (*argv)[:argc] then slices with fabricated bounds: ArgumentException: Indices low, high and max represent a range outside bounds of the array reference. Untouched by the element-alias arm, which requires a Go POINTER source; a uintptr source keeps the address route by design. Same family as TestDirectoryJunction and the createMountPoint reinterpret. |
TestGetppid |
1 | Newly REACHED 2026-08-02. The child runs and answers, but syscall.Getppid() reports 0 where the parent’s pid is expected — getProcessEntry’s Process32First/Next walk finds no entry. A real, contained syscall gap (it does NOT fault, which is what the struct-passing census below already recorded for this wrapper). |
TestReadlink |
0–1 | Newly REACHED 2026-08-02, and it is the symlink-privilege row (the os.runtime_rand → testenv.MustHaveSymlink row above) surfacing at last: standalone, its six symlink_* subtests fail with “A required privilege is not held by the client” while the three junction_* subtests PASS. Go SKIPS the symlink arms for want of the privilege; C# runs and fails them. Confirms that row’s prediction — clearing MustHaveSymlink converts these to matching skips rather than passes. (It agreed in the final full run, so it is privilege/timing-sensitive as well.) |
TestRootDirAsTemp |
1 | Newly REACHED 2026-08-02. The test re-execs the host with TMP/TEMP pointed at a drive ROOT to check TempDir(); the CHILD host then cannot create its own isolated run directory there — DirectoryNotFoundException: Could not find a part of the path 'Z:\go2cs-tests\os\…' out of TestHost.Run’s Directory.CreateDirectory(workingDirectory). The isolation model and the test’s premise collide: Go’s test binary needs no scratch directory of its own. Pre-existing (same line before and after this lane’s host change). |
TestWriteStringAlloc |
1 | AllocsPerRun bounded at ZERO. Deliberately not disclosed: the byte-derived shim CAN report 0, so the io/strings unit-mismatch ruling does not cover it. Go’s WriteString avoids the copy with unsafe.Slice over the string’s own bytes; a go2cs @string is its own storage, so the write path allocates (measured 9088 bytes). A real divergence — an sstring-shaped optimization, not a disclosure. |
TestRemoveAllWithExecutedProcess |
1 | ROOTED 2026-08-02 (r36-os-tail), and it is the .NET deployment model, not a conversion defect. The test copies os.Executable() — one file — into a fresh t.TempDir() 100 times and runs each copy, to make Windows hold an image handle. os.Executable() is CORRECT: it returns the test host’s apphost (os.tests.exe). But an apphost is a stub bound at build time to a managed assembly of the same base name that must sit BESIDE it, so a single-file copy can never run. Reproduced standalone by copying any converted project’s apphost alone into a temp directory: exit 0x8000809a = hostfxr LibHostAppRootFindFailure, message “The application to execute does not exist: ‘…<AssemblyName>.dll’” — byte-for-byte the code the test reports. Go’s test binary is statically linked, which is the only reason its premise holds there. The sole fix that would satisfy it is publishing every converted test host self-contained single-file (≈70 MB and a publish instead of a build, per package) — disproportionate to one test. Environment divergence; leave failing. |
TestStartProcess/relative |
1 | RE-MEASURED 2026-08-02 (r36-os-tail): PASSES — 3/3 standalone and in the full run, with nothing in this lane touching joinExeDirAndFName/FullPath/StartProcess. It belongs to the load-sensitive child-output class below, not to a code defect. |
| load-sensitive child-process flakes | ~6 | New classification 2026-08-02 (r36-os-tail). A set of tests that pass standalone and fail only in the full parallel run, all with one signature: the child process produced NO output (system hostname of "", Child returned "[]", reports stdin is not pipe '') or a t.TempDir() that had vanished. The membership MOVES between runs, which is the tell: across the two full runs measured, TestFileReaddir/TempDir, TestStatLxSymLink and TestReadlink failed in one and passed in the other, while TestStartProcess and TestLongPath did the reverse; TestHostname, TestExecutable and TestStatStdin failed in both yet pass 3/3 standalone. Measured while three sibling worktrees ran their own pipelines. Treat any single-run failure in this set as unconfirmed until it is reproduced standalone — that is how TestStartProcess/relative came to be recorded as a rooted mismatch when it is not one. |
TestStatLxSymLink |
intermittent | t.TempDir() cleanup hit a file “used by another process” — a handle the host had not released yet. Same load-sensitive family as the row above. |
A nil RECEIVER is nil-DEFERRING, not nil-safe — TestNilFileMethods closed (r36-nilrecv, 2026-08-02)
The row above asked for a ruling and named the alternative in its last clause. That alternative is the
right one, and it is now built and gated: golib’s DerefOrNull binds Unsafe.NullRef<T>() for a nil
box, and every pointer-receiver entry alias uses it unconditionally. A null ref is legal to HOLD and
to pass on as ref T; it faults on USE. So the receiver panic is not raised at entry (today’s defect)
and not discarded (the naive widening’s defect) — it lands where Go’s does, after any side effect the
body performed first, as NullReferenceException → TryAsPanic → Go’s own
runtime error: invalid memory address or nil pointer dereference, recoverable and printed verbatim.
Because it is faithful whether or not the body guards, there is no predicate:
isComparedDirectBoxReceiverIdent is subsumed and deleted. The alias is emitted in TWO places and
go2cs-gen’s ReceiverMethodTemplate — the bridge reaching a ref T receiver through a box — deref’d
eagerly too, one call frame EARLIER than Go; both now take the accessor.
Measured. os pipeline: TestNilFileMethods → pass (all fifteen methods return ErrInvalid).
Footprint against a control reconvert with the base converter (so pre-existing corpus staleness is
subtracted): 378 stdlib files in 132 packages, and every changed line is the alias — 1858 .Value
- 159
.DerefOrNilbecome 2017.DerefOrNull, nothing else. CNR: 27 behavioral projects, 56 lines, one shape. That is one project MORE than the reverted widening’s 26, becauseDerefOrNullalso subsumes theisInherentlyHeapAllocatedType→.ValueSlotreceiver arm. Full corpus builds 304/304 clean; behavioral suite 528/528 + 498/498 output. Guard:NilReceiverMethods. No null-page cliff: a synthetic field 200 KB past address zero still faults as a clean NRE, and a converted Go struct cannot reach that offset anyway (inline[N]T→ an 8-bytearray<T>reference).
⚠ The same defect is still open for pointer PARAMETERS — 3167 entry aliases in the corpus keep the
eager .Value, mitigated only by the two heuristics (nilSafePtrParamNames: nil-COMPARED in the body, or
passed nil at a same-package call site), and those two route to the nil-SAFE DerefOrNil, which is the
silent-default(T) accessor. Go’s rule is identical for a parameter and a receiver, so the complete fix is
to give parameters the same unconditional DerefOrNull — mechanically trivial now, but a much larger
emission footprint that wants its own measurement and its own ruling.
⚠ The os run that closed this row reached FARTHER than the banked one (177 top-level tests vs 166),
because the moving-site ExecutionEngineException above did not fire. The newly-reached tests bring their
own failures (TestCmdArgs slice-bounds in test code, plus TestExecutable/TestGetppid/TestStatStdin/
TestRootDirAsTemp/TestHostname/TestUserConfigDir/TestLongPathAbs), none of them receiver-shaped.
Read the arc’s residual table as measured against 166 reached; a fresh baseline needs a quiet machine.
⚠ A NEW member of the -tests-closure production-file family, found by this arc’s canaries and owed
to the next rebank. Since the validation-badge work (2026-08-02) every package’s .csproj carries an
eight-line “Ship this package’s versioned validation proof sheet” block, emitted by the -stdlib
driver, which has the roster. A single-package -tests run does not, so it regenerates the .csproj
without those eight lines — 0 8 on git diff --numstat, in EVERY banked package a sweep touches.
Confirmed on both canaries below (path/filepath, io) and on os itself; it predates this arc and is
caused by no change in it. Classify it with the other -tests-closure files: restore, never bank, and
let the whole-corpus regen level it.
Spot-canaries on the post-change tree, both at their banked counts: path/filepath →
status: validated, matched: true, 55 top-level (37 pass · 18 skip), 0 errors. io →
status: validated, matched: true, 54 top-level, 2 disclosed, 0 errors (its production
package_info.cs shows the documented +2 satisfies-not-witnesses records — restore, don’t chase).
Re-run after the r36-os-tail changes, both still at their roster counts: path/filepath →
61 validated (20 skips agreeing), io → 59 validated, 2 disclosed. Their -tests-closure
churn is the documented set and nothing else: the 0 8 validation-proof block on every .csproj,
io’s +2 package_info.cs records, and — pre-existing, from converter changes landed since the last
whole-corpus regen — io’s package_test_info.cs implicit-conv record set, io_test.cs and
multi_test.cs. The committed go2cs_test_host.cs does NOT churn: the run-directory list is
omitted entirely when a package has no subdirectories, which is 56 of the 71 banked packages, so only
the 15 that genuinely have one differ (and only by the lines that describe it).
r36-pin, same day — the moving-crash row RETRACTED, and the real top row named. The r35
attribution of the moving-site ExecutionEngineException to createMountPoint’s transient-pin
write was wrong: a pre-fix control run of the whole suite at base af5df9e16 produced ZERO
ExecutionEngine/AccessViolation faults — whatever closed that crash closed it inside the r35 train
itself, uncredited. (The transient-pin defect is nonetheless REAL and fixed — Reinterpret’s
fallback pinned for one statement while the derived pointer lived on; deterministic guard
ReinterpretPinLifetime, rule in ConversionStrategies-Reference — it just was not os’s crash.)
os’s dominant remaining cost is the blocking-pipe family: internal/poll on Windows does not
unblock an in-progress read on Close, hanging TestPipeEOF/TestPipeIOCloseRace + two siblings
and starving six more tests of child stdout (TestHostname, TestExecutable, TestGetppid,
TestStatStdin, both TestStartProcess arms, TestRootDirAsTemp) — its own future arc, and the
reason pipeline invocations leak os.tests.exe. (Closed 2026-08-02 by r37-poll, below — with the
diagnosis half right: the hang was real and is fixed, but it was not in internal/poll, and the
six child-stdout rows did not follow it.) ⚠ Scheduling: never run two lanes against ONE
package’s pipeline — the host is named per package, so the rename defence cannot apply; the tell
for a sibling-killed run is go2cs_test_results.json carrying the PREVIOUS run’s mtime.
Attribution was measured, not asserted — FIVE runs, and only one test is converter-determined
(r36-nilrecv). Three with the base converter, two with the fix. TestNilFileMethods: fail 3/3
on base, pass 2/2 with the fix. Every other test that moved, moved in BOTH arms —
TestHostname (2 base, 2 fix), TestStatLxSymLink (2, 1), TestFileReaddir (1, 2),
TestReaddirnamesOneAtATime (1, 1), TestProgWideChdir (1, 0), TestCopyFS (0, 1),
TestLongPathAbs/TestUserConfigDir (0, 1 each). The same-converter run-to-run spread is 3–4
tests and the outcome distributions coincide (base run 2 landed on 125 pass · 14 fail · 3 infra —
identical to the fix’s run 1). The lesson for the next arc: a single os run cannot attribute a
one-test delta. Pair every claim with a control run of the unchanged converter.
The blocking-pipe family — CLOSED 2026-08-02 (r37-poll), and it was never internal/poll
Measured, -test-action all -test-timeout 35m, three runs on the fixed tree: 165 agreeing of 178
(twice, identically) against the banked 164, with 177 reached. The whole blocking-pipe family flips
from HANG to PASS — TestPipeCloseRace, TestPipeIOCloseRace, TestFdRace, TestFdReadRace,
TestCloseWithBlockingReadByFd, TestCloseWithBlockingReadByNewFile, TestClosedPipeRaceRead,
TestClosedPipeRaceWrite — and no run leaks an os.tests.exe.
The conversion of internal/poll was faithful all along, and so was everything under it. Probed
bottom-up rather than reasoned about: syscall.CancelIoEx really does abort a blocking ReadFile on
a CreatePipe handle through the converted trampoline (a syscall-only program reproduces Go’s
ERROR_OPERATION_ABORTED exactly), and FD.Read really does return Go’s read |0: file already
closed. What never returned was FD.Close, parked forever in runtime_Semacquire(&fd.csema)
after the reader had already finished — the stack says so directly.
The root is Go pointer identity, in two layers, both now fixed and both corpus-wide:
- A field promoted through an embedded POINTER was rooted at the OUTER allocation.
os.Fileembeds*file, so&f.pfdreached throughж<File>and&file.pfdreached throughж<file>were different pointers where Go has one address.internal/poll’s semaphores are keyed by pointer identity, soos.read’s release andos.close’s acquire landed in different buckets. go2cs-gen now emits the pointer-crossing promoted accessor in a re-rooting shape (instance.@file.of(file.Ꮡpfd)), golib gainsFieldPtrFunc<T,TElem>plus the matchingof/atoverloads, and no call site changes — the overload is chosen by the accessor’s return type. 340 accessors corpus-wide take the new form; a cross-package embed keeps the oldrefform by design (its member list comes from metadata and can name fields the inner declaration never had —abi.Type.sysType, promoted intoruntime.rtype, has no generated accessor to re-root through), and that fallback is fail-loud (CS0117 at the corpus build). - A field reference’s SOURCE was compared by object reference, so a two-level
of()chain broke.Ꮡo.of(Outer.Ꮡin).of(Inner.Ꮡv)mints a fresh intermediate box per access, so&o.in.v == &o.in.vwas false at depth two (true at depth one) and amap[*T]Vgrew one entry per access.ж<T>.Equals/GetHashCode/PointerOrderTokennow resolve the source through the chain, the wayReferentObjectalready did.
Guards: PipeCloseUnblocksRead (goroutine blocked on a pipe read, closer, output-compared) and
EmbeddedPointerFieldIdentity (depth-2 equality, map[*T]V keying, both spellings of a
pointer-embed-promoted field). Both are deterministic neutered-fix controls — on the base tree the
first HANGS outright and the second prints depth2: false. Gates: full behavioral suite 535/535 +
505/505 output; CNR byte-identical across all 560 behavioral packages except the two new projects;
go2cs-stdlib.slnx 304 projects, 0 errors; go test ./... in src/go2cs green.
The control run answered in twenty seconds, and it is worth knowing that it can. The five-run
lesson above is about attributing a one-test delta; when the delta is a hang, the control does not
need to finish — it needs a stack. With the change stashed and go2cs-gen rebuilt at base, os’s host
was sampled 20 s in and had three threads already parked in
internal.poll.Close → runtime_Semacquire — testClosedPipeRace twice and TestPipeIOCloseRace
once, the very tests that pass on the fixed tree — plus testPipeEOF in the channel row below. Same
call site, same run, before and after: that is the attribution, at a cost of one build and one sample
rather than another 35-minute measurement.
What the fix did NOT do — two board predictions corrected
- The six child-stdout rows do not follow.
TestExecutable,TestGetppid,TestStatStdin,TestRootDirAsTempandTestStartProcessstill fail with an empty child result, so their root is not pipe blocking;TestHostnamepassed in one of the three runs and failed in two, which puts it in the load-sensitive class rather than either.TestExecutableis the sharpest specimen and worth rooting next: it re-execs the host with a relativecmd.Path,cmd.Dirset to the parent directory, and a forgedargv[0]of"-", then readsCombinedOutput. (Its failure message also exposes a second, independent gap: Go renders%qof an empty[]byteas"", the convertedfmtrenders[].) - The new top row is
TestPipeEOF, and it is a CHANNEL row, not a pipe row. With the pipe close unblocked the test now runs to its end and hangs there — reproducibly, at the identical site in both runs that hung (the third run instead died withFatal error. Internal CLR error. (0x80131506)). Captured stacks: the test’s deferred<-writerDonewaits while the writer goroutine is parked inChanCore<nint>.Recvinsidechannel<T>.GetEnumerator.MoveNext()— afor rangeover a channel the main goroutine has already CLOSED and drained. A lost wakeup (or a closed-and-empty receive that parks), ingolib/channel.cs, which this lane is fenced from. It does not reproduce in isolation: a standalone probe of the same shape — buffered channel, ranging goroutine that sleeps between receives, sender that closes — terminated 10,000 times out of 10,000, so it needs the suite’s parallel load. Deliver it to the channels lane with the stacks; closing it should takeosto 166.
TestCmdArgs — the blittable-mirror remedy does NOT apply, and the reason is specific
syscall.CommandLineToArgv returns *[8192]*[8192]uint16 over a block the OS allocated, and the
caller frees it: defer syscall.LocalFree(syscall.Handle(uintptr(unsafe.Pointer(argv)))). The
converted wrapper makes a native-address box, so ~argv reads an array<ж<array<uint16>>> struct
— a managed backing reference plus bounds — out of the pointer block’s own bytes, and (*argv)[:argc]
then slices with fabricated bounds (ArgumentException). Hand-owning it to return a MANAGED
materialization of the block fixes the walk and breaks the free: for a ж<T> whose pointee is a
Go fixed array, uintptr(unsafe.Pointer(p)) takes ж.cs’s pinnedArrayData path and hands back the
real GC-heap data address, so LocalFree would be asked to free GC memory — the exact
STATUS_HEAP_CORRUPTION failure mode ж.cs’s own banner records for the
GetEnvironmentStringsW/FreeEnvironmentStringsW pair. That trades a contained ArgumentException
for a process kill, so it was not done.
What would close it is a pointer flavor golib does not have: a box that answers ADDRESS questions with
the real native address while answering VALUE questions with a managed materialization — a snapshot
pointer, sound precisely for read-only native output blocks. net/lookup_windows.cs’s DNS-record walks
are the same shape, so it wants designing with them rather than minting for one test. Scope today is
exactly one test: nothing in the converted stdlib calls syscall.CommandLineToArgv (the only other
caller in GOROOT is the vendored x/sys/windows copy, which is not converted).
TestDirectoryJunction was characterized alongside it and is not the same family — no native
block, no free. Its createMountPoint helper is TEST code that reinterprets a managed []byte as
windows.MountPointReparseBuffer and writes four uint16 fields through it, then indexes
&buf.PathBuffer[0] — a [1]uint16 inline tail standing in for kernel bytes. That is the raw-metal
fork’s stub arm, in code that cannot be hand-owned, exactly as the residual table already recorded.
FOUND while attributing the above — t.TempDir() collides two tests that differ only by CASE
TestExecution.TempDir() (hand-owned src/core/testing/TestExecution.cs) builds
<work>/.tmp/<SanitizeName(TestName)>/<seq>. On a case-insensitive filesystem — the Windows default —
TestFileReaddir and TestFileReadDir resolve to one directory, and os’s suite runs both
t.Parallel(). Whichever finishes first runs its Cleanup(() => RemoveAll(path)) and deletes the
other’s temp dir mid-test; the loser fails open …\.tmp\TestFileReaddir\1: The system cannot find
the file specified. Proven directly: creating TestFileReaddir\1 makes Test-Path
TestFileReadDir\1 true and leaves ONE directory, and every leftover run root under
%TEMP%\go2cs-tests\os\ contains exactly one of the two names, never both — while the passing
test in each run is always the one that is present. Pre-existing, in the test HOST rather than in
conversion, and independent of the nil-receiver change (it fired in a base-converter run too); the
fix is to disambiguate the sanitized name (a case-marker suffix, or a per-execution sequence)
rather than trust the test name to be a unique path component. TestFileReadDir vs
TestFileReaddir is the only collision in os; the same generator will collide anywhere Go names
two tests with case-only differences.
r38-os-fin (2026-08-03) — the premature-EOF root was the SYSCALL SEAM, and os lands on ONE residual
Measured twice, identically, -test-action all -test-timeout 35m: 681 of 683 rows agree
(173 of 175 top-level), 1 disclosed, 34 matching skips, 4 capability-excluded, 1 residual. The
run takes about five minutes now, where the base tree’s timed out at 35. Progression across the
whole os arc: 48 agreeing → 141 → 158 → 164 → 681-of-683.
base (85ce6744c) |
r38-os-fin | |
|---|---|---|
| host run | timed out at 35 m, wedged in TestPipeEOF |
completes in ~5 m |
| oracle error list | 937 lines (462 name-encoding PAIRS + 13 real rows) | 1 |
| real top-level mismatches | 13 | 1 (TestWriteStringAlloc) |
| rows agreeing (all levels) | not measurable — the run never finished | 681 of 683 |
| top-level agreeing | 173 of 175 | |
| disclosed | 1 | 1 (TestUTF16Alloc) |
| capability-excluded | 1 | 4 |
The root: every managed address handed to native code was a FORMER address
bufio.Reader.ReadBytes over a converted os.Pipe returning a premature io.EOF only under
parallel load — the r37-chanrace handoff, with its probability gradient (-parallel 1: 0/4 · 2:
0/4 · 4: 1/3 · 8: 5/5 · 16: 2/2 · default: 100%) and its two surviving suspects — is
neither a handle double-close nor a spurious zero-byte read. It is the ж<T> → uintptr
conversion, and the file that performs it had the defect written on its own front door:
syscall/dll_windows.cs’s soundness note said the argument uintptrs the zsyscall wrappers capture
are TRANSIENT addresses that “golib’s ж→uintptr conversion cannot pin across the call”, and judged
the window “short and allocation-free”.
It is neither, for a BLOCKING syscall. Both operators end in a fixed block — a pin that lasts for
one statement — and then RETURN the address as an integer, so the window is not capture→calli, it
is capture→the kernel’s write. testPipeEOF parks in ReadFile on a pipe for 10 ms per read
while the rest of a parallel suite allocates around it. Measured directly rather than argued: a
heap(new uint32(), out var Ꮡdone) box and a Ꮡ(buf, 0) element pointer BOTH report a different
address after one forced collection. The kernel then writes to neither — done stays 0,
syscall.Read returns (0, nil), internal/poll’s FD.eofError turns that into io.EOF.
Every measured property of the row follows: monotone in parallelism (more threads ⇒ more allocation
⇒ more collections inside the same 10 ms window); indifferent to the finalizer bridge (a control had
already ruled that out); and the buffer’s half of the same defect — 4 KB written into freed heap —
is the moving-site ExecutionEngineException and the Fatal error. Internal CLR error. recorded
beside it.
The fix is golib-only, and it makes the ADDRESS MODEL sound rather than patching a caller.
ж<T>’s uintptr/void* operators now pin before they read (EnsureStableAddress), taking a
lifetime GCHandle on the ROOT storage the pointer names — a heap box pins its own value slot, an
element reference the canonical backing array, a field reference recurses to the containing
allocation — on exactly the terms pinnedArrayData already used for the fixed-array case. The
enabler is that a standard heap box’s value storage is now a one-element array for a T that
contains no references (ж<T>.m_slot): a box is a class with reference fields and GCHandle refuses
to pin anything containing pointers, so the value had nowhere pinnable to live. It is allocated
EAGERLY and never migrated — heap<T>(out ж<T>) hands out a ref alias before any address is taken,
so moving the storage on first address-take would strand that alias on the abandoned copy, which is
this very bug one level down. A reference-bearing T gets no slot and keeps the old transient
address; its C# layout is not a native layout either, so nothing can meaningfully be handed its
address. RuntimeHelpers.IsReferenceOrContainsReferences<T>() is a JIT constant, so a managed-T
box pays neither the branch nor the allocation. This also makes Go’s unsafe.Pointer rule 3
(pointer arithmetic through uintptr) sound, which it silently was not.
Guard: src/tests/GolibTests/NativeAddressStabilityTests.cs — a neutered-fix control across all
four box kinds plus the reference-bearing negative case; with EnsureStableAddress removed every
address assertion fails on the first forced collection. Rule in
ConversionStrategies-Reference.md.
The gradient closes at every point it was measured at. Same matrix, three host runs per point,
TestPipeEOF counted as a pass verdict rather than as the absence of an abort:
-parallel |
before (aborts) | after (passes) |
|---|---|---|
| 4 | 1 of 3 | 3 of 3 |
| 8 | 5 of 5 | 3 of 3 |
| 16 | 2 of 2 | 3 of 3 |
| default (24) | 6 of 6, plus 2 of 2 through the pipeline | 3 of 3, plus 2 of 2 through the pipeline |
What it closed, in one change: 13 residual rows → 3. TestPipeEOF and the whole
child-stdout family — TestExecutable, TestStatStdin, TestHostname, both TestStartProcess
arms, TestRootDirAsTemp’s spawn — because an empty child result WAS the same premature EOF, read
through os/exec’s pipe. The r37-poll prediction that those six “do not follow” was right about the
pipe-close fix and wrong about the family: they had one root after all, one layer down.
The other four rows, each rooted and closed
TestGetppid— the syscall STRUCT-PASSING seam’s third member, and the one that fails SILENTLY.PROCESSENTRY32Wis 568 bytes ending inszExeFile[260]INLINE; the convertedProcessEntry32holds that as onearray<uint16>reference, so the record is ~56 bytes and every field pastth32DefaultHeapIDreads from the wrong offset. Nothing faults — the kernel writes 568 bytes over a 56-byte object and the caller reads whatever lands — sosyscall.Getppidanswered 0. Same remedy asGetTimeZoneInformationandfindFirstFile1/findNextFile1: a blittable mirror + direct P/Invoke + field-for-field copy back (syscall/zsyscall_windows_impl.cs), withProcess32First/Process32Nextadded tomanualConversionFuncs.dwSizeis an INPUT the mirror owns too — Go sets it fromunsafe.Sizeof(procEntry), which is the MANAGED size here. The seam is now 6 wrappers, not 8. ⚠ A quiet wrong ANSWER is the worst shape this class takes; the crash cases at least announce themselves.TestRootDirAsTemp— the host’s isolation must not depend on an environment variable the suite can rewrite. The test re-execs the binary with TMP/TEMP pointed at a deliberately UNMOUNTED drive root (findUnusedDriveLetterpicks a letter becauseos.Statsays it is not there). Go’s test binary needs no scratch space; this host does, and it died in startup withDirectoryNotFoundExceptionbefore running a test, which the parent read as a child that produced nothing.TestHost.CreateRunDirectorynow tries the temp path first and falls back toAppContext.BaseDirectory, which exists by construction because the host is running out of it.TestStatLxSymLink— NOT load-sensitive; Go retries Windows sharing violations and we did not. Recorded on this board as an intermittent member of the load-sensitive family; on the fixed tree it reproduced 3 runs of 3 (ERROR_SHARING_VIOLATIONon thet.TempDir()directory, which a WSL child had been run inside). Go’s owntesting.removeAllretriesERROR_ACCESS_DENIEDandERROR_SHARING_VIOLATIONfor ~2 s with jittered backoff (go.dev/issue/50051, /51442); the shim did not, making it reproducibly less tolerant than the runtime it stands in for. Now it does, with Go’s timeout and backoff. 2 runs of 2 clean afterwards. The general lesson: “intermittent” is a hypothesis, not a classification — this one was deterministic once the rows in front of it cleared.TestReadStdin’s 462 subtests — the NAME-ENCODING artifact is gone.TestExecution.SanitizeNamefolded every non-printable rune to U+FFFD where Go’stesting.rewriteemits thestrconv.QuoteRunebody (\x1a), and a subtest’s NAME is what the oracle pairs by — so each of the 462 became a matched pair of one-sided rows, 924 lines that read like a mass failure on a top-level test that AGREED.SanitizeNameis now Go’s rewrite:isSpace→_,unicode.IsPrintdecides, non-printable takes the Go escape.TempDirNamefolds the backslash the escape introduces, since a name is also a path component. Guarded byTestingRuntimeTests.SubtestNamesEscapeNonPrintableRunesTheWayGoDoes. (TheTestFileReaddir/TestFileReadDircase-collision this board also records was already closed byTempDirName’s per-name hash.)
Capability-exclusions — the three sanctioned by the 2026-08-02 ruling, implemented
unsupportedRuntimeCapabilities now maps a SYMBOL to the NAME of the capability it requires, so the
manifest, the comparison and the proof page show “relocatable single-file test executable” rather
than a bare symbol. A key may name the test DECLARATION itself, which requiredFor honors by gating a
listed function on its own account — the shape a HOST capability takes, since nothing NAMES a test and
the caller-side arm can therefore never record it.
| Test | Capability | Key |
|---|---|---|
TestCmdArgs |
native output block with caller-side LocalFree |
syscall.CommandLineToArgv |
TestDirectoryJunction |
raw-metal struct overlay on managed bytes | os_test.createMountPoint |
TestRemoveAllWithExecutedProcess |
relocatable single-file test executable | os_test.TestRemoveAllWithExecutedProcess |
§9 roster scan, with positive control. All 72 validated packages’ _test.go files scanned for the
three keys: zero hits. Controls fired: AllocsPerRun finds 18 of the same 72 (so the loop and the
paths resolve), and os itself — deliberately off the roster — hits all three. Guarded by
TestUnsupportedRuntimeCapabilityGate (the lookup answers with the capability, stays package-scope,
and every entry must name one) and TestUnsupportedRuntimeCapabilityGatesTheDeclarationItself (the
self-gating arm, with an unlisted sibling as the negative control).
The ONE residual — TestWriteStringAlloc, and it is honestly a residual
AllocsPerRun bounded at ZERO, measured 9184 bytes per f.WriteString(…). Not a disclosure
candidate — ruling #1 of 2026-08-02 stands, a want-zero assert is satisfiable and disclosing it would
soften the doctrine the badges depend on — and not a capability exclusion either, since nothing here is
unownable. It is a real divergence with a known shape and no cheap fix: Go’s WriteString avoids the
copy with unsafe.Slice(unsafe.StringData(s), len(s)), while the converted path allocates a
PinnedBuffer + box for StringData, then pays the func<T>((defer, recover) => …) closure and defer
context of os.File.Write and internal/poll.FD.Write, then the syscall’s own boxes. The defer
machinery dominates, so this is the It moved from
9088 to 8856-9184 bytes across the arc — noise, not regression.sstring/GoFunc performance arc, not an os row.
⚠ RETRACTED by r39-osalloc (2026-08-03): the defer machinery does NOT dominate — it is 440 of 9,208 bytes, under 5 %. The sentence above was an attribution, never a decomposition, and it named a component costing a twentieth of the bill. 62 % was two silent allocations inside
ж<T>:IsNullboxing the whole pointee on every dereference (4,760 B) andof(…)minting its untyped accessor wrapper per call (968 B). Both are fixed; see the r39-osalloc sub-section below for the byte-exact decomposition and the arc that owns the rest.
os is therefore an honest NEAR-BANK: every row accounted — 681 agreeing, 1 disclosed, 34 matching
skips, 4 capability-excluded — with exactly one real divergence, stable across two identical pipeline
runs and five direct host runs.
⚠ Owed to the rebank: every proof page’s Excluded declarations preamble is one sentence out of
date. The generator now says a declaration may need “a capability the managed runtime does not
provide — a testing member the host has not implemented, or a platform behavior it provably cannot
reproduce”, because a runtime capability is no longer hypothetical. The 72 committed pages still carry
the old “a testing capability the host does not yet provide”. Regenerating them here would mean
banking 72 pages that also carry a fresh date/converter stamp — a partial rebank by another name — so
they are RESTORED with the rest of the sweep’s drift and will level at the scheduled whole-corpus
regen (ruling #6). The per-entry text, which is the substance, is already correct in the pages that
have such an entry.
r39-osalloc (2026-08-03) — the 9,184 decomposes, and it was NOT the defer machinery
r38 attributed the residual to the func((defer, recover) => …) closure and defer context of
os.File.Write / internal/poll.FD.Write, and filed it against “the sstring/GoFunc performance
arc”. That attribution was plausible and wrong. Decomposed to the byte, the defer machinery is
440 of 9,208 bytes — under 5 %; 62 % was two silent allocations inside ж<T> itself, both of
which are now gone. The lesson generalizes: an attribution that was never decomposed is a
hypothesis, and this one sent the fix at a component costing a twentieth of the bill.
Method (reproducible). A console probe references core/os, core/syscall, core/internal/poll
and golib and measures GC.GetAllocatedBytesForCurrentThread deltas across N calls — the same
instrument the AllocsPerRun shim uses, so the numbers ARE the ones the test sees. A temporary
AllocMark slot table (begin/end pairs with depth suppression, so nesting is charged once) was
threaded through every frame of WriteString → File.Write → file.write → FD.Write → syscall.Write →
WriteFile, plus per-TElem buckets inside ж<T>.of and ж<T>.Value. The instrumentation is
temporary by construction and was reverted; what survives is the arithmetic, which closes exactly
at every level — the ibyteseq standard. (The probe reads 9,208 where the pipeline read 9,184: the
probe writes to its own file rather than the host’s t.TempDir() one, a 24-byte difference in the
path taken above File.Write. The two agree to the byte AFTER the fix, both at 3,168 — the
figure the pipeline now prints in expected 0 allocs for File.WriteString, got 3168.)
| Cost | B/op | Share | Root |
|---|---|---|---|
ж<T>.IsNull boxing the pointee on every standard-box deref |
4,760 | 51.7 % | m_val is null on an unconstrained T compiles to box !T — 8 × a 592-byte os.file copy, + 24 for one os.File |
of(…) minting the untyped accessor wrapper per call |
968 | 10.5 % | display class + delegate, 88 B × 11 field pointers |
the ж<T> boxes themselves |
1,488 | 16.2 % | 11 boxes; ж<FD> alone is 608 B because a field-ref box still carries an inline m_val of the pointee type |
| syscall seam | 1,048 | 11.4 % | heap(new uint32()) 136 · Ꮡ(buf,0) 152 · 3 × new unsafe.Pointer 664 · procWriteFile.Addr() 96 |
GoFunc + defer machinery |
440 | 4.8 % | func<> object + closure + delegates 224 · defer delegates 128 · the Stack<Action> 88 |
unsafe.Slice(unsafe.StringData(s), len(s)) |
136 | 1.5 % | PinnedBuffer + ж<byte>; free in Go |
| loop/slice residues | 368 | 4.0 % |
The two fixes, both golib-only, both pure defect removal (detail and the emitted-form rule:
ConversionStrategies-Reference.md Reading a pointer and
taking a field pointer allocate NOTHING). IsNull’s value-peeking term is now guarded by a per-T
s_valueCanBeNull — the question is only answerable for a reference type or a Nullable<>, and for
everything else evaluating it boxed the whole pointee for a constant-false answer; the guard also
made the peek read the right slot, correcting ж<Nullable<T>> (latent — Go has no Nullable).
of(…)’s untyped wrapper is a pure function of the accessor, and the accessor is a compiler-cached
static method group, so the wrapper is now memoized per accessor in a weak-keyed table.
| probe measurement | before | after |
|---|---|---|
os.File.WriteString(s) |
9,208 B/op | 3,168 B/op (−65.6 %) |
os.File.Write(b) |
9,072 | 3,032 |
syscall.Write(h, b) |
1,072 | 784 |
ж<Mutex>.Value (a field-pointer deref) |
592 | 0 |
Guard: GolibTests.PointerDereferenceAllocationTests, a neutered-fix control — with the fixes removed
it reports 528 B/deref for a 512-byte pointee, 288 for a reference-bearing one, 32 through a
field-pointer chain, and 200-vs-112 B/call for of(…) against a bare box of the same type.
TestWriteStringAlloc still does not reach zero — and the reason is architectural, not a missing fix
3,168 bytes remain and none of them is waste; each is the current model charging for something Go
gets from its compiler. os therefore does not bank on this row, ruling #1 still stands (a
want-zero assert is satisfiable in principle, so it is not a disclosure), and the honest statement is
that the row is an ARC, not a defect. The arc, in descending value, with what each item would cost:
ж<T>serves four box kinds from one class (1,488 B, 47 % of the remainder). A field-reference box, an element box and a native-address box all carry an inlinem_valslot of the pointee type that they never read —ж<FD>is 608 bytes for a pointer. They also each carry BOTHm_structFieldRef(aNullable<(object, Delegate, Delegate)>, 32 B) andm_arrayIndexRef(Nullable<(IArray,int)>, 24 B) although the kinds are mutually exclusive. Two independent moves: flattening the two nullable tuples into four plain fields is contained and worth ~28 B per box (~308 B here); removing the inlinem_valfrom the three non-standard kinds needs the class split into per-kind subclasses, orm_valmoved intom_slotunconditionally — which would ADD an allocation to every standard box, so it is a real trade and wants the whole-corpus measurement before it is taken. Blast radius: every converted package. Chip-class, design-WITH-user.uintptr(unsafe.Pointer(x))materializes a deadPointerobject (496 B here, 15.7 %). The converter emits(uintptr)new @unsafe.Pointer(x)for Go’s most common syscall idiom; the object is provably dead — the ctor takes(uintptr)xand the cast reads it straight back. A converter peephole would remove three allocations from EVERY zsyscall wrapper in the corpus. This is the cheapest remaining increment and the one with the widest reach outsideos; it was deliberately NOT taken in this lane because it is a different change class (converter → CNR + corpus build + goldens) and would have made the A/B footprint non-minimal for a row that cannot bank either way.GoFuncis a heap frame (440 B, 13.9 %). Thefunc<T>((defer, recover) => …)shape costs aGoFunc<T>, a display class, the body delegate, one delegate perdefer, and aStack<Action>on the first registration. Go’sdeferrecord is stack-allocated and, since Go 1.14, usually open-coded into the frame. The managed analogue is aref structframe with the defers in inline fields — which cannot hold the body as a lambda, so it is an EMISSION change (the converter would have to emit the body as a local function takingrefto the frame). Chip-class; do not attempt it as a golib-local edit.- The syscall seam boxes the arguments (288 B beyond item 2).
heap(new uint32(), out Ꮡdone)is Go’svar done uint32; &done— a stack variable in Go, a heap box here — andᏑ(buf, 0)is&buf[0]. Both fall out of item 1 if a pointer stops being a class. unsafe.StringDatapins eagerly (136 B). It builds aPinnedBufferview over the string’s bytes so the pointer has a stable address. Since r38,ж<T>’s address operators pin on demand (EnsureStableAddress), so the eager pin is no longer load-bearing: returning an element reference into the string’s own backing array would drop thePinnedBuffer, makeunsafe.Slice(StringData(s), len(s))a true aliasing window (which is what Go’s does), and giveStringData(s) == StringData(s)for free. Small, principled, and touching a hand-owned file with subtle empty-string history — worth doing WITH the item-1 work rather than alone.AMENDED 2026-09-01 (the +136 attribution run): this item is DONE and the entry above is history —
e1ef6ca85deleted the eager pin on 2026-08-30 (it was a leak class: the pinned handle strong-rooted every outliving string’s backing, and a non-zero-offset window materialized a COPY, breaking Go’s sub-string aliasing;StringDataIdentityguards the repair). Its replacement — a boxedslice<byte>window handed toElemRefBox— costs +24 B/op, and the “136 B” this item recorded turned out to be a MAGNITUDE COINCIDENCE with the later os TestWriteStringAlloc +136.00 regression, whose dominant +112.00 is the element-aliasing merge’s 8-bytem_publishedArrayBackingfield on the abstract baseж<T>(× 14 boxes on that path; correctness-load-bearing, the per-box publish gate — not revertible). Full A/B decomposition, reconciled to the byte with two cross-controls (TestUTF16Alloc +8.00 = one box; utf16 TestAllocationsDecode +0.00 = zero boxes): the census record’s 2026-09-01 amendment. The surviving surgical item: anElemRefBox<T>internal(T[] backing, nint absoluteIndex)ctor removes one object + ~56 B per StringData call (row lands BELOW pre-regression, 17 → 16 allocs) — queued as the WriteStringAlloc arc’s first increment.
What this lane changes about os’s accounting: nothing. The row still diverges, so os stays at
681 of 683 agreeing + 1 disclosed + 34 matching skips + 4 capability-excluded, with one real
divergence — now measured at 3,168 bytes instead of 9,184, and rooted rather than attributed.
encoding/gob — build blocker CLOSED; first real census: 86 of 106 match (2026-08-02, r37-gob)
gob had never been measured. package_info_internal_test.cs emitted
[assembly: GoImplement<gob_internal_test_package.Point, Pythagoras>] — the EXTERNAL suite’s pair
anchored at the BRIDGE, where Pythagoras (declared only in example_interface_test.go) is not in
scope. One CS0246, therefore no test host, therefore all 106 verdicts read empty: a missing host
masquerading as mass runtime failure, and the reason DESIGN-reflection-bridge.md’s “gob 79/98”
residue list could not be re-measured.
Root — test-project-model record anchoring (the splitWhiteboxVariantRecords family), not
reflection. The bridge’s declared-name set is a set of SIMPLE names, and the two -tests variants
are separate Go packages free to declare the same one: gob declares Point in codec_test.go
(package gob, implementing the internal Squarer) and again in example_interface_test.go
(package gob_test, implementing Pythagoras). Each variant’s records are split as that variant
converts, and every cross-variant reference is routed by go/types.Object identity to a
CLASS-QUALIFIED spelling — so a BARE name recorded by the external suite is external-declared by
construction. The set is now consulted only while splitting the BRIDGE variant’s own records, and
the emission mirror that names an adapter through its record’s anchor carries the identical gate, so
the two cannot disagree. Write-time qualification could not have repaired it: it roots an ambiguous
bare name at the file it is ALREADY being written into, so a mis-anchored record merely comes out
qualified to the wrong variant. Rule:
ConversionStrategies-Reference.md, A BARE record name
resolves in the variant that RECORDED it; guard
TestSplitWhiteboxVariantRecordsResolvesBareNamesInTheRecordingVariant (a fixture module declaring
Point in both variants, asserting the collision through the real go/types scan before exercising
either split). The fix is test-model-only — verified, not asserted: CNR is byte-identical across all
558 behavioral packages, and the whole-stdlib A/B reconvert shows it changing no production file.
First measurement (go2cs -tests -test-action all -test-timeout 20m, one run, zero empty
verdicts): 86 of 106 match — C# 81 pass + 5 skip against Go’s 101 pass + 5 skip; 19
declarations capability-excluded, 0 disclosed. The 20 mismatches reach seven roots, none of them
new-and-unrooted:
| Root | Tests | Note |
|---|---|---|
| A pointer REINTERPRET used as a VALUE boxes a copy | TestGobEncoderField, TestGobEncoderNonStructSingleton, TestGobEncoderPointerThenValue, TestGobEncoderValueThenPointer, TestGobEncoderValueEncoder (5) |
The largest single root, and precisely located. Gobber.GobDecode writes back through a reinterpreted named-type pointer — fmt.Sscanf(string(data), "VALUE=%d", (*int)(g)) — which emits fmt.Sscanf(…, Ꮡ((nint)(g))): the POINTEE is converted to a value and that temporary is boxed, so Sscanf’s write lands in a throwaway box and g never changes (“expected ‘23 got 0”; TestGobEncoderValueEncoder NREs on the unwritten value instead of mismatching). The managed-reinterpret route (Reinterpret<U>(), which aliases the source box) exists and is correct — but reinterpretManagedEmission is reached only when context.isPointerCast (the conversion is the operand of a deref) or the source is a RAW address. A (*U)(p) whose result is used as a VALUE — passed as an argument — satisfies neither and falls through to the ordinary value-conversion path. Reinterpret area ⇒ chip-owned; recorded, not fixed here. Fifth sighting of the address-of-copy-boxing shape, one base shape per fix. |
GobDecode write-back for a named-ARRAY pointer receiver |
TestGobEncodeIsZero (1) |
isZeroBugArray [2]uint8’s GobDecode writes a[0]/a[1] through the pointer receiver, and the embedded time.Time decodes the same way; the round-trip returns [0 0] and a zero Time where Go returns [1 2] and time.Unix(1e9,0). The direct-field-write case (ByteStruct) passes, so Value.Addr’s write-back path is sound — this is the element/receiver storage shape, adjacent to the root above. |
| Reflection bridge | TestSingletons, TestIndirectSliceMapArray, TestIgnoreDepthLimit (3) |
Already recorded in DESIGN-reflection-bridge.md and now confirmed by measurement rather than inference. array<T> does not carry its LENGTH, so a type-only walk sees a slice where the wire says [7]int (gob: decoding into local type *[]int, received remote type [7]int) and a [3]int mismatch for a field declared [3]int; TestIgnoreDepthLimit is reflect.ArrayOf → the typelinks stub (a NotImplementedException, so it reports infrastructure-error, not fail). Chip-owned. |
Typed-nil pointer identity through any |
TestTopLevelNilPointer, TestNilPointerPanics, TestNilPointerInsideInterface (3) |
var ip *int emits ж<nint> ip = default!, so encodeAndRecover(ip) hands gob a plain null and gob answers gob: cannot encode nil value where Go sees a typed *int nil and panics “nil pointer”. Same shape for the four mustPanic cases and for a nil pointer inside an interface (“expected error, got none”). The canonical typed-nil boxing (ж<T>.NilBox) exists; a nil pointer VARIABLE’s zero value does not reach it. One root, three tests. |
A nil deref inside the engine, re-panicked through catchError |
TestEndToEnd, TestLargeSlice + /byte + /struct (4) |
The stack ends at error.cs:45 — catchError’s throw panic(e) re-raising a value that is NOT a gobError, i.e. a genuine NullReferenceException from inside Encode/DecodeValue, with the original site consumed by recover(). Differential worth keeping: TestLargeSlice’s int8 and string subtests PASS while byte and struct fault, so it is shape-dependent, not size-dependent. Unrooted below the recover boundary; the next visit should print before recovering rather than reason about the stack. |
| Wire-level error-path divergences | TestBadData, TestIgnoreRecursiveType, TestOverflow (3) |
TestBadData case #8 gets gob: bad data: field numbers out of bounds where Go reports exceeds input size; TestIgnoreRecursiveType gets that same message on a stream Go accepts; TestOverflow produces no range error for complex64 only (every int/uint/float width matches). Small, separable, and each names its own expected string. |
unique’s package initializer |
TestNetIP (1) |
Two roots stacked in internal/concurrent.NewHashTrieMap. The FIRST — a dead deref alias, described below — is fixed this arc, and it was neither net nor reflection (the r18-era claim that this is net’s sync.OnceFunc in fd_windows is retracted; that is not on the stack). Fixing it MOVED the error site rather than greening the test: NewHashTrieMap now fails one line later with ArgumentException: Delegate to an instance method cannot have null 'this' at keyHash: new Func<…>((~mapType).Hasher), i.e. abi.TypeOf(m).MapType() over a zero map yields a descriptor with no hasher. That second root is the descriptor surface — chip-owned. |
The first TestNetIP root, fixed: a dead deref alias kept alive by a NAMED-ARGUMENT LABEL.
TestNetIP reported TypeInitializationException for go.net.netip_package → go.unique_package
→ a nil deref in internal/concurrent.newIndirectNode. Go’s
newIndirectNode(parent *indirect) { return &indirect{node: …, parent: parent} } never dereferences
parent, but the converter’s alias-liveness scan is a whole-word TEXT match over the converted body
and the composite literal’s field key emits as the C# named argument parent: Ꮡparent — so the
LABEL matched, the alias survived as a dead local, and ref var parent = ref Ꮡparent.Value
dereferenced the box at entry. NewHashTrieMap builds its ROOT node with newIndirectNode(nil), so
unique’s package initializer threw and took net/netip and every dependent with it. The scan now
excludes a named-argument label (isNamedArgumentLabel); rule and A/B in
ConversionStrategies-Reference.md, A pointer parameter
used only through its box gets no deref VALUE alias, guarded by the extended
NilPointerParamUnsafePointer behavioral test (the composite-literal shape plus a dereferencing
positive control). Whole-stdlib A/B: 39 files, every hunk one removed dead ref var line and
nothing else; the reconverted corpus builds 304/304, 0 errors.
⚠ It moved the site, it did not green the test — the charter’s root-cause-layering warning, in the
wild again. Proving even that much needed the dependency regenerated: a -tests run regenerates only
the package under test, so gob’s first re-measurement still linked the COMMITTED
internal/concurrent/hashtriemap.cs and reproduced the original stack verbatim. Overlaying that one
file from the reconvert is what showed the newIndirectNode frame gone and the next root exposed. gob’s
verdict split is identical before and after (86/106) for exactly that reason; the value banked here
is the general converter defect and its 39-file corpus footprint, not a verdict.
gob does not bank (86 of 106), so the roster is unchanged and no gob artifact is committed.
encoding/gob re-measured: 88 of 106, and four of the seven roots above were mis-attributed (2026-08-03, r38-gob-fin)
Re-run on the same command (-tests -test-action all -test-timeout 20m, zero empty verdicts): 88 of
106 — C# 83 pass + 5 skip against Go’s 101 pass + 5 skip; 19 capability-excluded, 0 disclosed.
The +2 is TestGobEncoderField and TestGobEncoderNonStructSingleton, greened by the
aliasing-reinterpret converter fix below. The other 18 rows re-bucket to seven roots, and the
re-bucketing matters more than the +2: three separate rows above were one root, and it is not the row any
of them named.
| Root | Tests | Owner |
|---|---|---|
reflect.Value.IsZero is wrong for a named STRING and for an ARRAY |
TestGobEncoderPointerThenValue, TestGobEncoderValueThenPointer, TestGobEncoderValueEncoder, TestGobEncodeIsZero (4) |
reflect bridge — chip, LANDED |
reflect.Value.Grow nil-derefs |
TestLargeSlice + /byte + /struct (3 rows) |
reflect bridge — chip, LANDED |
Typed-nil identity through any |
TestTopLevelNilPointer, TestNilPointerPanics, TestNilPointerInsideInterface (3) |
converter — LANDED r39 (r39-nilcomplex); 2 of 3 closed, the third re-rooted to the bridge |
array<T> carries no LENGTH |
TestSingletons, TestIndirectSliceMapArray, TestEndToEnd (3) |
reflect bridge — chip |
reflect.ArrayOf → the typelinks stub |
TestIgnoreDepthLimit (1) |
reflect bridge — chip (reports infrastructure-error) |
| The decoder’s IGNORE path rejects a valid field number | TestBadData #8, TestIgnoreRecursiveType (2) |
gob decode path — unrooted |
MapType().Hasher over a zero map |
TestNetIP (1) |
reflect bridge — CLOSED 2026-08-03 by the ruled internal/concurrent hand-own (see the r39d section at the end of this file); TestNetIP still fails, on the linkname-PUSH root now behind it |
TestOverflow (1) |
converter — LANDED |
Root 1 — four tests, one root, and it is an ENCODE-side skip, not a decode write-back. r37 read the
three TestGobEncoder*Value* failures as residue of the reinterpret row and TestGobEncodeIsZero as a
separate “GobDecode write-back for a named-ARRAY pointer receiver”, reasoning from ByteStruct passing
that “Value.Addr’s write-back path is sound”. ByteStruct is reached through a POINTER field
(GobTest0{17, &ByteStruct{'A'}}), so it never exercised Value.Addr at all — and a direct probe shows
reflect.Value.Field(i).Addr(), including a reinterpret through it, writes back correctly in C#. The
actual root is one line up, on the ENCODE side: gobEncodeOpFor’s if !state.sendZero && v.IsZero() {
return }. Probed directly against go run:
| value | Go | C# |
|---|---|---|
NS("val") (type NS string) |
IsZero=false Len=3 |
IsZero=true Len=0 |
"val" (plain string) |
IsZero=false Len=3 |
IsZero=false Len=3 |
[2]uint8{1,2} |
IsZero=false |
IsZero=true |
NA{1,2} (type NA [2]uint8) |
IsZero=false |
IsZero=true |
NI(3), NB("ab") (named int / named slice) |
correct | correct |
So gob omits the field from the wire entirely and the decoder leaves the zero value — visible as
v = "", want "forty-two" for the VALUE fields while the POINTER fields of the same type pass, and as
TestGobEncodeIsZero’s [0 0] where Go has [1 2]. A minimal gob.Encode probe confirms it at the
byte level: Go’s wire carries \x01\tVALUE=val\x01\tVALUE=ptr, C#’s only \x02\tVALUE=ptr. In the
converted reflect/value.cs the String arm delegates to v.Len() (broken for the [GoType("str")]
wrapper — it sees the wrapper struct, not the underlying @string) and the Array/Struct arms take
raw-memory shortcuts (typ.Equal(…) against zeroVal, isZero(unsafe.Slice(v.ptr, size))) that cannot
mean anything in the managed model. Chip-owned; recorded, not touched.
Root 2 — reflect.Value.Grow, and it is SIZE-dependent, not shape-dependent. r37 kept the
int8-passes/byte-faults differential as evidence of shape-dependence. It is a threshold: []byte
round-trips fine at 1 MiB and faults at ≥ 10 MiB, which is internal/saferio’s chunk = 10 << 20. Above
it gob only partially allocates and grows incrementally — decUint8Slice (decode.go:387) and
decodeArrayHelper (decode.go:553) both call value.Grow(1) — and reflect.Value.Grow nil-derefs. The
four-line probe is decisive on its own: reflect.ValueOf(&s).Elem().Grow(1) on a []byte prints
len/cap 4 8 in Go and panics in C#. int8 and string pass only because their decHelper fast paths
(decInt8Slice, decStringSlice) return before the Grow loop. The stack that “ends at catchError’s
throw panic(e)” is genuine but says nothing; the probe is what roots it. Chip-owned.
Root 3 — typed nil, rooted precisely, and deliberately NOT landed here. var ip *int emits
ж<nint> ip = default! — a plain C# null — so boxing it into any yields interface-nil, and
encodeAndRecover(ip) gets gob: cannot encode nil value where Go sees a typed *int nil. The control
that names the root exactly: ip2 := (*int)(nil) emits ((ж<nint>)nil), goes through golib’s canonical
ж<T>.NilBox, and probes IDENTICAL to Go (kind=ptr isnil=true type=*int). A nil pointer FIELD has the
same defect (st.P → interface-nil); a nil MAP is already correct. So the canonical typed-nil
representation exists and works, and the gap is only that a pointer VARIABLE’s (and field’s) zero value
never reaches it. Two candidate remedies — emit ж<T>.NilBox for a pointer variable’s zero value, or
box at the interface-conversion boundary (box ?? ж<T>.NilBox) — and both change emission at every
pointer declaration or every pointer→interface conversion in the corpus, i.e. a change whose gate is
the full 71-package validated sweep plus a corpus rebuild, not something to land at the tail of an arc
for three tests. Handed on rooted rather than half-gated (charter §2/§5).
Root 6 — the two IGNORE-path rows share a symptom and a reproducer. TestBadData #8 (expected
exceeds input size) and TestIgnoreRecursiveType (a stream Go accepts) both die with
gob: bad data: field numbers out of bounds, and both decode into nil — the ignore path. The
converted ignoreStruct is faithful line-for-line, so the divergence is upstream, in how the ignore
ENGINE is compiled for a self-referential type: fieldnum >= len(engine.instr) rejects a field number Go
accepts. TestIgnoreRecursiveType’s 36-byte data literal is a complete standalone reproducer. Not
reflect; unrooted below the engine compile.
TestEndToEnd moved rather than greened — the charter’s root-cause-layering warning again. It was an
NRE below catchError; it now reports gob: length mismatch in decodeArray, i.e. the array-length row,
which the crash had been masking. Counted under root 4, not as a fix.
Root 7 — TestOverflow’s complex64, rooted precisely, and an attempted fix REJECTED by the gate.
Not a decode-path divergence at all: complex(math.MaxFloat32*2, math.MaxFloat32*2) produces a
complex64 of +Inf in C# and 6.8e38 in Go. UntypedFloat converts implicitly to BOTH float32
and float64, so both golib complex overloads are applicable and C# prefers the better conversion
target — the NARROWER one. gob’s float32FromBits treats +Inf as legal in both widths, so the decode
produced no range error at all while every int/uint/float width matched. The general class is worth
more than the row: any golib builtin overloaded on float width silently narrows an UntypedFloat
operand.
The obvious remedy — name the untyped pair explicitly (complex(UntypedFloat, UntypedFloat) =>
complex128, Go’s default type) — does not work, and the full behavioral suite is what proved it.
It made every MIXED call ambiguous: complex(0D, gHalfPi) has the float64 overload better on the first
operand and the untyped one better on the second, so neither wins (CS0121 in the
ComplexConstContext guard). Completing the set with all four width pairings does not rescue it
either: UntypedFloat converts implicitly in both directions with float32 and float64, so for
an operand that is neither — complex(7/2, 0D), an int — no candidate is strictly better and the
ambiguity simply moves. Overload resolution cannot express this rule; the change was reverted rather
than banked.
The remedy that can work is CONVERTER-side and deterministic: emit each complex() argument at the
element width Go’s typing gives the call — complex((float64)(x), (float64)(y)) for a complex128
result, float32 for complex64 — which is the rule assignUntypedConstContext already computes for
literal rendering but cannot apply to a named untyped const (Δmath.MaxFloat32) or a constant
expression over one. Its footprint is every complex() site in the corpus (math/cmplx above all), so
it wants its own A/B, corpus build and re-validation of the math packages — deliberately not squeezed
in at the tail of this arc.
encoding/gob fixes landed this arc
- A pointer REINTERPRET used as a VALUE boxed a copy — CONVERTER, fixed. r37 located this precisely
and routed it to the chip as “Reinterpret area”. It is not: the shape never reaches
reinterpretManagedEmission’s gate at all, because thenamedToNamed || namedToBasic || basicToNamedre-box arm returns first — which is also whycontext.isPointerCastwas a red herring (a deref of the same conversion took the copy route too). The arm now tries the aliasing emission first. Rule and the 14-file / 41-hunk A/B inConversionStrategies-Reference.md, These three arms now ALIAS instead of boxing a copy; guard = the extendedNamedNumericPointerReinterpretbehavioral output test (neuter-proven). The blast radius is far larger than gob: the copy silently broke write-through inflag(a parsed flag never reached the caller’s variable),crypto/tlskey-share/signature-scheme parsing,crypto/cipher’s CBC IV,image/png’s pooled encoder buffer andgo/types. Reconverted corpus builds 304/304, 0 errors. - The reference closure’s MEMBER-ACCESS edge — CONVERTER (test model), fixed. Landed for
unique(below); it changes nothing for gob, whose host already linked.
gob still does not bank (88 of 106) and no gob artifact is committed.
encoding/gob re-measured: 91 of 106 — both deferred converter items land (2026-08-03, r39-nilcomplex)
Same command, zero empty verdicts: 91 of 106, 15 mismatches. The +3 is TestTopLevelNilPointer
and TestNilPointerPanics (the typed-nil boundary) and TestOverflow (the complex width pin). The
remaining 15 re-bucket to seven roots, and not one of them is the converter’s any more — six are
the reflection bridge (the chip) and the seventh is gob’s own decode path:
| Root | Tests | Owner |
|---|---|---|
reflect.Value.IsZero is wrong for a named STRING and for an ARRAY |
TestGobEncoderPointerThenValue, TestGobEncoderValueThenPointer, TestGobEncoderValueEncoder, TestGobEncodeIsZero (4) |
reflect bridge — chip |
reflect.Value.Grow nil-derefs |
TestLargeSlice + /byte + /struct (3) |
reflect bridge — chip |
array<T> carries no LENGTH |
TestSingletons, TestIndirectSliceMapArray, TestEndToEnd (3) |
reflect bridge — chip |
reflect.Value.IsNil on an INTERFACE asks the POINTEE |
TestNilPointerInsideInterface (1) |
reflect bridge — chip, NEW, rooted below |
reflect.ArrayOf → the typelinks stub |
TestIgnoreDepthLimit (1) |
reflect bridge — chip (infrastructure-error) |
MapType().Hasher over a zero map |
TestNetIP (1) |
reflect bridge — chip (infrastructure-error) |
| The decoder’s IGNORE path rejects a valid field number | TestBadData #8, TestIgnoreRecursiveType (2) |
gob decode path — unrooted |
The two that closed. TestTopLevelNilPointer needed only the boundary: encodeAndRecover(ip)
now hands gob a typed nil, reflect.ValueOf sees kind ptr with IsNil true, and gob panics
“nil pointer” exactly as Go does. TestNilPointerPanics needed one slot more — its table is
[]struct{ value any; mustPanic bool }{{nilStringPtr, true}, …}, a POSITIONAL element of a struct
literal whose field is any, which the first cut of the boundary did not cover; the rule and its
(zero-site) corpus footprint are in
ConversionStrategies-Reference.md, A pointer crossing
into an interface carries its static type. TestOverflow closed on the complex() element-width
pin (same reference, complex() over a NAMED untyped constant pins the element width).
The one that did NOT, and why — a NEW chip root, rooted with a five-line probe.
TestNilPointerInsideInterface builds struct{ I any }{I: ip} and expects
Encode to fail with “nil pointer … interface”. The converter’s half is done and visible in the
emission (I: ip.OrTypedNil()), but the C# still reports expected error, got none. The reason is
one layer down: reflect.Value.IsNil on an INTERFACE-kind value answers about the POINTEE, not
about the interface. Probed directly against go run:
| Go | C# | |
|---|---|---|
reflect.ValueOf(si).Field(0).Kind() |
interface |
interface |
…Field(0).IsNil() |
false |
true |
…Field(0).IsZero() |
false |
true |
…Field(0).Elem().Kind() |
ptr |
ptr |
…Field(0).Elem().IsNil() |
true |
true |
IsZero for an interface IS IsNil (reflect/value.cs’s Chan/Func/Interface/Map/Pointer/Slice/
UnsafePointer arm), so the wrong answer makes gob’s if !state.sendZero && v.IsZero() { return }
skip the field outright — encodeInterface, which is where the expected error lives, is never
reached. It is the same encode-side skip as root 1, from a different wrong predicate, and it is the
bridge’s to fix: an interface value’s nilness is a property of the interface, not of whatever
pointer it happens to carry. Chip-owned; recorded, not touched (the boundary fence).
gob still does not bank (91 of 106) and no gob artifact is committed.
unique builds and RUNS for the first time — 0 of 19, one chip-owned wall (2026-08-03, r38-gob-fin)
unique had never linked a test host. handle_test.cs calls cleanupMu.Lock() on the production
package’s var cleanupMu sync.Mutex, and the -tests csproj emitter did not reference sync —
CS0012 … 'sync_package.Mutex' ×2. Root: the reference closure was missing its MEMBER-ACCESS edge
(declarationClosureImports covered a named type’s interface bases and struct fields, but not the type
of a RECEIVER — resolving x.M requires binding x’s type, and when x is declared elsewhere that type is
spelled nowhere in the compilation); rule, minimality probe and the recompile-model no-op argument in
ConversionStrategies-Reference.md, The third closure edge — a
MEMBER ACCESS. Test-model only, and zero-drift: regenerating all 73 banked .tests.csproj changes
exactly one line, unique’s own sync reference.
⚠ The minimality probe is not ceremony — it rejected two successive forms of this rule that a reading of C#’s binding rules would have justified. “The type of every var/const/func the compilation NAMES” drifts 23 of 73; narrowing to receivers but still seeding from the production sources drifts 13. Both were caught only by running it. (And the per-file scoping is load-bearing: go/packages loads the INTERNAL test variant with the production files alongside its own, so a per-package gate lets every production receiver straight back in — the 13-drift form, wearing the fix’s clothes.)
First census, with internal/concurrent/hashtriemap.cs overlaid from a fresh reconvert (the committed
corpus predates r37’s dead-alias fix, and a -tests run regenerates only the package under test —
without the overlay all 15 rows report r37’s already-fixed newIndirectNode stack, which reads exactly
like a live defect): 0 of 19 — Go 19 pass; C# 4 fail + 15 infrastructure-error. Every one of the
15 is the same TypeInitializationException, and it is the second root r37 already named as
chip-owned: NewHashTrieMap → keyHash: new Func<…>((~mapType).Hasher) →
ArgumentException: Delegate to an instance method cannot have null 'this', because
abi.TypeOf(m).MapType() over a zero map yields a descriptor with no hasher. unique is therefore a
one-root wall, and that root is the chip’s; nothing else about the package is measurable until it
clears. ⚠ Increment 8 rooted that wall and reported it NOT landable in the bridge — the hasher’s
contract is “hash the value at this address” and a managed address names no value (two boxes holding
equal strings have different addresses; a reference-containing pointee’s address moves across a GC).
The recommended remedy is a hand-owned internal/concurrent/hashtriemap.cs, which needs an ownership
ruling; see Increment 8 below. (The 4 fail rows are TestMakeCloneSeq subtests whose names Go takes from
reflect.TypeFor[T]().String(); C# reports that as "", so Go’s testString becomes C#’s #00 — the
rtype.String/TypeFor surface, also chip.) unique does not bank; the overlaid dependency was
restored, not banked.
SUPERSEDED 2026-08-03 — the hand-own landed and this census is re-measured at 1 of 19 with the
single wall replaced by five distinct downstream roots; see internal/concurrent.HashTrieMap HAND-OWNED
at the end of this file. (The rtype.String/TypeFor chip row above is now known to be the SAME defect as
the third root there: abi.TypeFor<T>() returns the descriptor’s Equal delegate for an interface T.)
⚠ Overlaying a dependency .cs is not enough to re-measure it — Copy-Item preserves the source’s
LastWriteTime, so the older-than-the-.dll copy was skipped by MSBuild and the run reproduced the
ORIGINAL stack verbatim, which reads as “the fix did not work”. Touch the file after overlaying.
RETRACTED — TestPipeEOF is NOT a channel row: the channel was never CLOSED (r37-chanrace, 2026-08-02)
The r37-poll handoff recorded TestPipeEOF’s post-pipe-fix hang as “a for range over an already
CLOSED, drained channel that never wakes — a lost wakeup, in golib/channel.cs“, and routed it to
the channels lane as the first real channel-semantics defect since wave3. It is not one. The
“already closed” half was inferred from reading testPipeEOF’s source flow — close(write) sits
above the deferred <-writerDone, so a main goroutine parked in that defer looks like it must have
closed. Measured instead of inferred, it had not: this is §9’s don’t-trust-a-plausible-reading trap,
one hop further in.
The instrument. ChanCore<T>.Recv/Send’s park was env-gated onto a timed wait that reports the
core’s state and the parked thread’s stack once a threshold elapses, plus a line per closechan.
That is the cheap general answer to any future “a channel never woke” sighting: it distinguishes a
lost wakeup from a close that never ran, in one run, without a debugger.
What it captured — identically in both instrumented pipeline runs, and a third time driving the host directly:
STUCK recv core#281 closed=False qcount=0 cap=1 recvqEmpty=False elem=IntPtr
channel<T>.GetEnumerator+MoveNext ← testPipeEOF's `for i := range write` (the writer goroutine)
STUCK recv core#280 closed=False qcount=0 cap=0 recvqEmpty=False elem=EmptyStruct
GoFunc.HandleFinally → builtin.ᐸꟷ ← the deferred `<-writerDone` (the test goroutine)
Both channels open. And the cross-check is absolute: across the whole suite run the close log
contains 125 closes, not one of them a chan int — close(write) never executed on any core.
The real control flow. rbuf.ReadBytes('\n') returned io.EOF, so t.Fatal(err) fired at
pipe_test.go:395. Fatal → FailNow → TestAbortException unwinds → GoFunc.HandleFinally runs
the deferred func → <-writerDone. close(write) on the line below never runs, so the writer
goroutine ranges over a channel that will never close, and writerDone therefore never closes
either. Real Go deadlocks identically here — Go’s own test code is not hang-safe on that branch;
Go simply never takes it, and its binary-level timeout panic would dump it if it did. The channel
runtime did exactly what Go specifies at every step.
So the actual os row is: bufio.Reader.ReadBytes over a converted os.Pipe returns a premature
io.EOF, and only under parallel load. Characterized on the r37-poll tree, driving
os.tests.exe directly:
| configuration | runs where TestPipeEOF aborts |
|---|---|
-run TestPipeEOF alone |
0 of 5 |
-run the whole pipe/fd family |
0 of 3 |
full suite, -parallel 1 |
0 of 4 |
full suite, -parallel 2 |
0 of 4 |
full suite, -parallel 4 |
1 of 5 |
full suite, -parallel 8 |
5 of 5 |
full suite, -parallel 16 |
2 of 2 |
full suite, default (TestOptions.Parallel = Environment.ProcessorCount, 24 here) |
6 of 6, plus 2 of 2 through the pipeline |
Monotone in the concurrency level and not attributable to one interfering test — every test still
runs at -parallel 1, and the abort signature is unmistakable in the host’s own output (the whole
suite reports ~650 results and TestPipeEOF contributes no line at all, because its goroutine
never returns).
⚠ The knee is a gradient, and an earlier revision of this row got that wrong. It claimed a
clean threshold at 8 — 100% either side — on the strength of only two samples at -parallel 4.
A host reboot forced the whole measurement to be re-established from scratch, and on the quiet
machine -parallel 4 aborted 1 of 3. So 4 is not a safe configuration, it is a low-probability
one, and any future bisection of this row must budget more than two runs per point near the knee.
What the reboot did not move is the headline: default parallelism aborts 100% both before and
after (3/3 loaded, 3/3 cold), which is what makes the zero rows at -parallel 1/2 worth trusting
rather than dismissing as luck. One default-parallelism run also died with Fatal error. Internal
CLR error. (0x80131506), the same crash r37-poll saw once; whether that shares the root is open.
Premature finalization is RULED OUT, by control rather than by argument. os.newFile registers
runtime.SetFinalizer((~f).file, close) and runtime/mfinal.cs’s native bridge honors it for real —
instrumented, it runs 21 finalizer-driven close calls per three suite runs, which is exactly
the mechanism Go’s own KeepAlive doc warns about and made a compelling root. It is not this one:
with the bridge disabled outright (SetFinalizer registering nothing), TestPipeEOF still EOFs
3/3. Handle double-close / handle-value reuse across parallel tests, and a spurious zero-byte
read reaching FD.eofError, are the candidates left standing.
The negative control for the channel verdict. 93,000 racing instances across five shapes —
ranging receiver woken by close, direct hand-off racing close, the testPipeEOF choreography
itself, a blocked select woken by close, and select single-fire under contention — under ThreadPool
and GC pressure, zero hangs and zero invariant violations. Separately, testPipeEOF’s exact
choreography over the REAL pipe/bufio/fmt/time stack (transpiled, not synthetic) completed
200/200 rounds in C# and under go run. The select park path was checked as the twin of the
suspected window and is clean on the same evidence. Three of those shapes are now standing guards in
src/tests/GolibTests/ChannelWakeupStrainTests.cs; they are neutered-fix controls (with closechan
not draining Recvq, all three fail as a parked channel operation was never woken).
Owed. The premature-EOF root goes back to the os/internal/poll arc with the table above. And
the wedged host is not reaped: it outlived -test-timeout 6m by minutes and had to be killed by
PID — the leaked-os.tests.exe symptom already on this board is this, and Go’s binary-level timeout
panic is the behavior the host still lacks.
CLOSED 2026-08-03 (r38-os-fin) — and it was neither surviving suspect. Not a handle double-close and not a spurious zero-byte read: the
ж<T>→uintptrconversion returned an address whosefixedpin had already expired, so a gen0 collection during the 10 ms blockingReadFilemoved the*uint32byte-count box out from under the kernel anddonestayed 0. The gradient this table measured is exactly the probability of a collection landing in that window. Full account in theosblock’s r38-os-fin sub-section. The wedged-host / no-timeout-panic half of this Owed is untouched and still open — it simply stopped firing once nothing hangs.
Open — the syscall STRUCT-PASSING seam: 6 wrappers still hand a non-blittable struct to the kernel
Down from 8 on 2026-08-03 (r38-os-fin):
Process32First/Process32Nextjoined the fixed set, and correct a claim this section made — the row below reads “reached-and-working”, which it was not. It failed SILENTLY:syscall.Getppidanswered 0, because the kernel wrote a 568-bytePROCESSENTRY32Wover a ~56-byte managed record and the caller read whatever landed. A quiet wrong ANSWER is the worst shape this class takes — a fault at least announces itself, and “it did not crash” is not evidence a wrapper works.
Named as a class 2026-08-01, after syscall.GetTimeZoneInformation became the second member of it
to be hand-owned (the first was StartProcess/_STARTUPINFOEXW, 2026-07-19). findFirstFile1 /
findNextFile1 followed the same day — the first members a real Go test suite reached rather than
a census predicted, and the reason path/filepath’s EvalSymlinks family took the C# test host down
mid-run.
Amendment 2026-09-02 (C2, from the Sendto cut) — the class has a THIRD outcome, and it is the quietest: not a fault and not a wrong answer, but a wrong DESTINATION that the network forgives. Linux
syscall.Sendtohanded the kernel the address of a managedRawSockaddrInet4; the sixteen bytes it actually received were02 00 | AE 54 | 00 00 00 00 | <a managed reference>— family and port CORRECT, because they are inline scalars, and the destination address0.0.0.0, because the four bytes at that offset are the managed struct’s padding while the reference that should hold the octets sits eight bytes further on. Linux treats0.0.0.0as a destination meaning “this host”, so every loopback send arrived at the right socket and a loopback guard read GREEN over a send to entirely the wrong address. A guard for an address-encoding member of this class needs a destination the kernel must actually honour —SendtoSeambinds127.0.0.2for exactly this reason, and its first draft on127.0.0.1passed against the defective body.And
net.Interfaces()was safe for a reason that does not generalize.NetlinkRIBcallsSendtowith aSockaddrNetlink, whose raw struct isFamily/Pad/Pid/Groups— all scalars, so it is accidentally blittable and has no reference to misplace. Membership in this class is decided per ADDRESS FAMILY, not per wrapper: the same wrapper is correct for netlink and wrong forInet4/Inet6/Unix/Linklayer. That is why nothing had noticed.
The class. A generated wrapper passes uintptr(unsafe.Pointer(&s)) for a converted struct whose
C# layout is not the native one — any struct holding a golib array<T> (Go’s inline [N]T) or a
ж<T> (Go’s pointer field) where Windows expects inline bytes or a raw address. The kernel then
writes the NATIVE-sized record over a smaller managed object: heap corruption past its end, and
fabricated object references in the reference-typed fields. It does not fail at the call; it fails at
the next read of one of those fields, usually as an ACCESS_VIOLATION deep inside golib. That is why
time.Now().Weekday() died in slice<ushort>..ctor and not in GetTimeZoneInformation.
Census (src/core/syscall, positive control = Timezoneinformation): 32 non-blittable structs, 11
wrappers passing one by address (the earlier count of ten collapsed the
findFirstFile1/findNextFile1 pair into a single row). Three are fixed; the other eight are
latent — nothing in the behavioral suite or the 69-package sweep exercises them today:
| Wrapper | Struct | Reached by |
|---|---|---|
findFirstFile1 / findNextFile1 |
win32finddata1 (FileName, AlternateFileName) |
FIXED 2026-08-01 — path/filepath.EvalSymlinks → toNorm → normBase; guarded by the FindFirstFileData behavioral output test |
Process32First / Process32Next |
ProcessEntry32 (ExeFile) |
FIXED 2026-08-03 — os’s TestGetppid → syscall.Getppid → getProcessEntry; the mirror owns dwSize too, since Go computes it from unsafe.Sizeof |
Bind / Connect / ConnectEx / Getsockname / Getpeername (+ the two sockaddr() encoders) |
RawSockaddrInet4 / RawSockaddrInet6 (Addr, Zero) |
FIXED 2026-08-11 (lane L10) — net.Listen → listenStream → syscall.Bind; guarded by the SockaddrRoundTrip behavioral output test. The first member with TWO defects: a (*[2]byte) port alias panicked (index out of range [0] with length 0) before the struct-passing seam was even reached. Mirrors are stack LOCALS, and no new [DllImport] was needed — golib models unsafe.Pointer as an address box, so the package’s generated bind/connect/connectEx already take any address. ⚠ Hand-owning RawSockaddrAny.Sockaddr (the decode) was REJECTED on measurement at L10: its body held the only ΔSockaddr casts in the package, so skipping its emission dropped the GoImplement records and made net mint duplicate adapters. TAKEN 2026-08-14 (netpoll S2b), once the pointer method-set recorder made those records independent of that body — re-measured on the taking lane’s own build (all three records present with the body displaced, net referencing syscall’s adapters at all seven sites and minting none, A/B footprint exactly one file) and guarded at value level by four new SockaddrRoundTrip lines, including an IPv6 address that deliberately crosses the Addr.Data/Pad boundary. It decodes by FLATTENING the managed struct back to its 116-byte native image and reusing readNativeSockaddr, the encoders’ own decode — see the ConversionStrategies-Reference.md entry |
WSARecv / WSASend / AcceptEx / GetAcceptExSockaddrs / CancelIoEx / WSAGetOverlappedResult (+ LoadConnectEx, and ConnectEx extended) |
WSABuf (Buf), Overlapped (as the operation’s kernel-side IDENTITY), the AcceptEx sockaddr block, GUID (Data4) |
FIXED 2026-08-15 (netpoll S2b) — every TCP read/write/accept/dial; guarded at VALUE level by the TcpLoopbackRoundTrip and NetDeadlineMatrix behavioral output tests. The first ASYNC members of this class, and async breaks the mirror-is-a-local rule twice: the kernel keeps the OVERLAPPED and the buffers until COMPLETION (unbounded), and CancelIoEx matches BY ADDRESS, so a fresh copy per call would break cancellation. Remedy is a per-operation RECORD keyed by the ж<Overlapped> (whose field-reference equality resolves an of() chain recursively, so all of execIO’s call sites AND separate FD.Read calls hit one record), holding the PreAllocatedOverlapped, the native staging and the ж<byte> boxes whose pins hold the caller’s buffers. The completion→waiter signal is pushed through a new platform-neutral golib rendezvous (GoAsyncIO), because internal/poll references syscall and Go’s own trick — reading the enclosing operation back out of the OVERLAPPED — has no go2cs equivalent. ⚠ LoadConnectEx was NOT on this census and the netpoll design recorded it as “already working”: it is not. syscall.GUID’s Data4 [8]byte makes the struct reference-bearing, so WSAIoctl compared a CLR auto-layout image and answered WSAEINVAL on every host — failed to find ConnectEx: An invalid argument was supplied, the shape crypto/tls banked nine times. “Synchronous, therefore fine” is not a safe inference; only a census is |
WSAStartup / WSAEnumProtocols |
WSAData (Description, SystemStatus, VendorInfo), WSAProtocolInfo (ProtocolName, and the nested GUID.Data4 / WSAProtocolChain.ChainEntries) |
FIXED 2026-08-16 (lane claude/wsaenum-mirror) — internal/poll’s InitWSA, once per process that imports net; guarded at VALUE level by the WsaProtocolInfo behavioral output test. NEITHER was on this census (see the ⚠ below), and the pair is the class’s largest overwrite by an order of magnitude: WSAPROTOCOL_INFOW is 628 bytes native with three inline arrays the conversion collapses to references, so the managed record is ~120 bytes — and checkSetFileCompletionNotificationModes asks for 32 of them, telling the kernel unsafe.Sizeof(buf) = 20,096 bytes while handing over a ~3.8 KB managed array. WSAStartup is the same shape over WSADATA (408 native vs ~40 managed) and is UPSTREAM of it: reading data.Description after it dies with ACCESS_VIOLATION in slice<byte>..ctor before the enumeration is reached, which is why the two arrived together. ⚠ The WSAStartup overwrite has been live since the corpus first dialled a socket and was SILENT only because net never reads the WSAData it passes — the strongest instance yet of “it did not crash” proving nothing. The enumeration’s answer is load-bearing too: it sets useSetFileCompletionNotificationModes → FD.skipSyncNotif, the IO path the netpoll design’s OQ5 ratified keeping. Both mirrors live in syscall/windows/zsyscall_windows_wsa_impl.cs (the ws2_32 family, beside LoadConnectEx), and WSAEnumProtocols adds a third SIZE-IS-AN-INPUT edge after Process32First’s dwSize: the byte count is also an OUTPUT, rewritten on WSAENOBUFS with a required size expressed in NATIVE strides |
GetIfEntry |
MibIfRow (Name, PhysAddr, Descr) |
net.Interfaces |
getStartupInfo |
StartupInfo (Desktop, Title) |
⚠ NOT os startup — corrected 2026-08-02 by the r35-os arc, which ran the whole suite without reaching it. Nothing in os calls it; in Go 1.23 the only caller is the public syscall.GetStartupInfo, exercised by syscall’s own test. Process32First/Next above ARE reached from os (TestGetppid → syscall.Getppid → getProcessEntry) and did not fault, so that row is reached-and-working rather than latent. |
GetAddrInfoW / FreeAddrInfoW |
AddrinfoW (Canonname, Addr, Next) |
FIXED 2026-08-16 (lane claude/tls-endgame) — net.Dial → resolveAddrList → LookupPort, i.e. every converted program that resolves a name or a service; measured first as a process AV from crypto/tls’s TestVerifyHostname. The class’s first LINKED output, and the first where copying the top-level record is NOT enough: net reads the sockaddr THROUGH the result, so the whole chain and its sockaddrs are transcribed into managed boxes and the managed pointer is carried across the unsafe.Pointer field by ManagedPointerTokens (its second minter ever; the reflection bridge was the first). FreeAddrInfoW becomes a hand-owned NO-OP, because the native chain is freed eagerly at the copy and nothing native escapes the call. Guarded at VALUE level by the LookupServicePort behavioral output test, proven failing-first (exit code mismatch: C# -1073741819 vs Go 0). ⚠ Fixing it does NOT make TestVerifyHostname pass — net.adapterAddresses is the wall behind it; see the entry at the end of this file |
CertEnumCertificatesInStore, CertFreeCertificateChain, CertFreeCertificateContext |
CertContext, CertChainContext |
crypto/x509 on Windows |
⚠ A SECOND class, censused 2026-08-16 and distinct from this one: 13 wrappers take a **T OUT-parameter and receive NULL, silently — ж<T> → uintptr answers 0 for a ж<ж<T>> whose held pointer is null, which is every out-parameter before the call. Measured through CertAddCertificateContextToStore (err == nil, storeCtx == nil, and crypto/x509’s systemVerify then nil-derefs). CLOSED AT THE BOUNDARY 2026-08-17 (lane claude/x509-cryptoapi): the operator has TWO wrong answers here, not one — a live MANAGED address once the held pointer is non-null, which is worse — and neither is fixable in ж.cs, so the remedy is a native out-cell plus a ValueSlot publish in the wrapper. 5 of the 13 taken (the SID pair, NetGetJoinInformation, and the two crypt32 members), guarded by the PointerOutParameter behavioral test; 8 left for stated reasons. ⚠ Attribution correction: NetGetJoinInformation and NetUserGetInfo are in syscall, not internal/syscall/windows — the split is 11 + 2. Full disposition in the entry at the end of this file |
— | — |
Remedy, per member: the established one — a blittable [StructLayout(LayoutKind.Sequential)]
mirror with fixed buffers for the inline arrays, a direct [DllImport], and an explicit
field-for-field copy at the boundary, declared in manualConversionFuncs so the generated wrapper
becomes a placeholder. Worked example: src/core/syscall/zsyscall_windows_impl.cs.
⚠ The census UNDER-COUNTS, twice measured — treat 11 as a floor, not a total. LoadConnectEx
(2026-08-15) and now WSAStartup / WSAEnumProtocols (2026-08-16) were all three defective members
that this wrapper list never held, even though each passes a struct of exactly the censused shape by
address. Two of them are on the net init path, and one of them — WSAStartup — has been
overwriting the heap in every converted program that imports net since the corpus first dialled a
socket. So a wrapper’s ABSENCE from the table below is not evidence it is sound; it is evidence
nobody looked. When a lane reaches a new Winsock/kernel32 wrapper, re-derive the shape from the
struct it passes rather than consulting this list, and add the row.
⚠ This census is scoped to src/core/syscall, and that scope is not the class’s boundary.
internal/syscall/windows holds six more wrappers of the same shape — one of them,
NetShareAdd/SHARE_INFO_2, is FATAL today on any host where the Server service is reachable, and
it is what a real suite reached first. Its table is in os’s “REGRESSION” is a HOST CAPABILITY at
the end of this file, together with the second failure shape the class takes: the kernel READING a
managed record as a native one, dereferencing a value field as a pointer, and faulting AT the call
rather than arbitrarily far away.
Do them when a suite reaches them, not speculatively — each needs its own value-level
verification (a mirror with wrong offsets returns garbage without faulting, so “it no longer
crashes” proves nothing; LocalTimeZone compares real zone abbreviations and offsets against Go, and
FindFirstFileData compares real directory entries — long names ASCII and non-ASCII, 8.3 alternate
names, the directory bit, byte sizes, and a distinct per-entry LastWriteTime).
net and crypto/x509 are the two packages that will surface most of the rest.
Two details of the findFirstFile1 implementation generalize and are worth cribbing for the next
member: the caller’s UTF-16 name buffer is pinned with a fixed block wrapped around the call
rather than handed golib’s TRANSIENT ж→uintptr address, and an inline WCHAR[N] buffer is copied
back whole, NULs included — Go reads it as UTF16ToString(buf[:]), which stops at the first NUL,
and the struct is reused across an enumeration, so a copy that stopped at the terminator would leave
the previous entry’s runes behind it. Full write-up:
ConversionStrategies-Reference.md, A STRUCT handed to the
kernel by address must be blittable.
Recurring classes worth a general fix rather than another point repair
-
The import-path → C#-identity derivation. THREE sightings, each fix covering exactly ONE shape.
getProjectName(importOperations.go) mints four identities from one string — the.csprojfilename, the library<AssemblyName>, the NuGetPackageId, and (minus the last segment) the C# namespace — all of which must be unique across the package graph. It has now been wrong three times: (i) a quotedmodule "gopkg.in/yaml.v3"directive carried its quotes into the csproj filename, which Windows rejects outright (#33); (ii) a path element containing a C# keyword was escaped on the declaration side and not by consumers, so the two sides of one namespace disagreed (#33); (iii) the upward walk forgo.modtreated the first ancestor holding no.gofiles as the module boundary and truncated the name to its leaf segment (#35, 2026-08-08) — 743 of 1,727 names in one user’s conversion, 175 of them colliding, and 531 collapsed into the baregonamespace where 12 landed on converted-stdlib classes (errors,strings,runtime,os, …). Note the escalation: the third one is not merely a naming nuisance, it silently aliases third-party packages onto the standard library’s own classes.The shape to check for the next one: the derivation still reconstructs the import path by walking the filesystem, even though the loader’s canonical path is in hand at every call site —
options.packageImportPathon the declaration side (conversionDriver.go), theimportPathkey on all three reference sides (getLocalModulePackageInfo,getRecurseDependencyInfo, and the stdlib arm ofgetImportPackageInfo, which already does exactly this and has never been wrong). Reconstruction was left standing after #35 because it now provably yields the import path for any module package (module path + relative path is the definition of one), andpkg.PkgPathriskscommand-line-argumentsfor a bare-directory conversion. But a fourth mangling means plumbing the canonical path through is the general fix and the heuristic is the point repair. Full rule:ConversionStrategies-Reference.md, A project name is the package’s FULL import path. ⚠ Every one of these passed all standing gates — see the-recursegate gap at the top of this file. - Zero-value construction for a type that needs one. Fixed four times now in four different
emission paths: a heap-boxed local fixed array,
new([N]T)dropping its length,make([]S, n)whereScarries a fixed-array field, and (2026-07-27)makeof a defined slice type, whose go2cs-gen wrapper has no element-factory constructor —internal/fmtsort’smake(SortedMap, 0, n)emitted a lambda into annintparameter (CS1660). That fourth one was live on master, not latent, and it took 20 of 61 banked packages down in a single sweep:-testsregenerates production.cson every run, so the one package that regenerated a brokensort.csbroke every later package downstream offmtin the same tree. Residue: adefault!zero-var local. Every new emission path re-opens this class, which argues for centralizing zero-value construction instead of patching sites — this is now the fourth data point for that. -
A one-level probe of a COMPOSED type — closed for anonymous-type lifting (2026-07-31), and worth looking for elsewhere. The extractor that finds an anonymous
struct{…}/interface{…}in a declaration inspected the immediate child of each container kind, so it saw*T,[]Tand (after a separate one-off patch)map[K]V, but no composition of them —[]*struct{…}fell straight through to raw Go text and a CS1031 cascade. The tell that this is a class rather than a bug: the map arm had already been added as its own function rather than as a rule, which is the shape a point-repair leaves behind. The fix replaced both extractors’ dispatch with one recursive descent over the type-composing operands. Any other analysis that peels a type expression by hand — rather than throughgo/typesor the shared walk — is a candidate for the same defect. ⚠ And scope any such site with an A/B reconvert, not a source scan: a grep for the shape reported zero production hits and would have called the corpus untouched, but the A/B foundencoding/gob/type.cs, whose(*struct{ r7 int })(nil)reaches its literal through a parenthesized pointer conversion the pattern never looked for. Charter §9’s rule earning its keep in the opposite direction — the scan had a positive control for[]*struct{…}and none for(*struct{…}).The one site this bullet named is now closed too (2026-07-31).
visitStructType.go’s struct-FIELD arm kept its own hand-written peel and lifted[N]struct{…}but not[N]*struct{…},[]*struct{…},map[K]struct{…}orchan struct{…}; it now callsextractStructType/extractInterfaceTypelike every other lift site. (The bullet’s list was one entry too generous — a bare*struct{…}field always had its own arm and always lifted.) A/B’d over all 305 projects: the widening itself has no corpus consumer, exactly as predicted, and the only change is one incidental canonicalization in 4 files / 2 packages — the shared helpers exclude the emptystruct{}and the old arm did not, soruntime.Func’sopaqueanddatabase/sql’s two_NamedFieldsRequiredfields now take golib’sEmptyStructinstead of minting a private empty[GoType("dyn")]type apiece. Corpus builds 302/302 with 0 errors; nothing referenced the removed names. Full rule + the two properties that keep the shared helper faithful (lift naming, sub-struct tracking):ConversionStrategies-Reference.md, An anonymous struct lifts from ANY depth of its declared type; guarded byAnonStructArrayElement.What it did not close, and is the honest next increment here: the cross-context anonymous-lift identity split. Constructing a value of an anonymous struct type lifts a second, function- or file-scoped name for the same Go type (
fill_sbeside the field’sS_One), so a direct struct assignment survives only on go2cs-gen’s dyn-struct implicit conversion and a container of it —slice<ж<A>>toslice<ж<B>>— has nothing to bridge it (CS1503). That is why the new guard reads its composed fields at their zero values, and why the pre-existing one-level guard never indexesStats.BySizeeither. It predates this arm and is unaffected by it.Two more instances of the class landed 2026-07-31, both in
net, and both confirm the diagnosis. (i)convUnaryExpr’s&base.fieldrouting admits a base by an enumerated shape list (ident / selector / call / index / star) that a type assertion is not in, so&c.(*UDPConn).conncopy-boxed. (ii)convCompositeLithas three composite paths, and the elided pointer arm ([]*struct{…}{{…}}) never called the interface-field router its two siblings call. The shared tell is now unmistakable: whenever an analysis enumerates shapes it has SEEN rather than stating the property it needs, the sibling composition is the one missing. Both fixes state the property instead (a postfix rendering chains.of(…); every composite path records its interface fields). ⚠ A related asymmetry is deliberately left standing and is worth a look with its own guard: that elided-pointer arm still does not callmarkStringFieldLits, relying on a blanket per-elementu8StringArgOKinstead of the typed path’s per-field precision. It emits correctly for every corpus site today (net’s"?0123456789abcdef"u8among them) and CNR is byte-identical, so there is no demonstrated consumer — the same reason thevisitStructTypeitem above was held back from the commit that predicted it, and then landed as its own guarded increment. - Untyped constants in a typed slot — CLOSED 2026-07-29. The int-literal case was already fixed;
a computed float constant that directly uses a named untyped integer wrapper now folds once at the
resolved float width.
hash/maphashvalidates 22/22;UntypedConstDefineguards both:=and typed slots. - A conversion that must ALIAS, implemented as a copy — the same silent-wrong-answer shape as
the address-of family, at a different seam (fixed 2026-07-31). Go’s slice-to-array pointer
conversion
(*[N]T)(s)shares the slice’s storage; go2cs boxed a copy of it, so every write through the pointer was discarded. It had been recorded as a known divergence (“aliasing stays faithful for reads back through the same pointer, and the corpus sites are read-only inputs”) — true when written, false the moment a write site appeared, andimage/pngwas that site. The lesson generalizes past this one construct: a documented “faithful for reads” divergence is a latent wrong answer with a timer on it, and the write case arrives without announcing itself. Two more sites the fix silently corrected:net/http’s data-chunk pools now return buffers that really are the pooled storage. Guarded bySliceToArrayPointerAlias. - The address-of box-copy family — CLOSED at all six paths (2026-07-31). The sixth, the value
RECEIVER, is fixed:
markAddressTakenBoxedReceivergives an address-taken value receiver the same entry-timeref var b = ref heap(bʗp, out var Ꮡb)preamble the value parameter takes, gated on emission byrecvBoxReasonHolds(whichparamNeedsHeapBoxconsults viafuncDecl.Recv, since the params walk cannot see a receiver). Both silent-wrong-answer symptoms this row predicted are gone, plus one it did not: an array receiver’s&a[i]was not silent but a hard CS0103 — the emission already spelledᏑa(convUnaryExpr’s array copy-box fallback is keyed onidentIsParameter, which excludes the receiver), naming a box nothing declared. Corpus footprint, from a two-seeded-root A/B over all 305 projects: 3 receiver sites in 2 files (encoding/base64WithPadding/Strict,encoding/base32WithPadding), every one areturn &encafter the last mutation — correct-by-luck before, one storage identity now, no live victim. Closing the family at its root rather than after a sixth broken package is exactly what this row argued for. Full rule, the public-surface argument (the receiver’s C# type never moves, soRecvGenerator/[GoRecv], pointer calls and interface satisfaction are untouched), and the measured note that the inherently-heap restriction rejects zero receiver sites today — unlike the parameter arm’s 48 of 149, whose over-boxing came from also recordingpackageCaptureModeBoxIdents, which the receiver arm never does: seedocs/ConversionStrategies-Reference.md, An address-taken VALUE PARAMETER heap-boxes too. Guarded byAddressOfParamWrite, extended with the receiver arm and its four controls.
RETRACTED — the internal/zstd / testing.B “trap” was a false alarm
internal/zstd is worth 534 verdicts, and the fix is what it looked like: Go’s B and T both
embed common, so a benchmark body may call Cleanup, Error, Log, Name, TempDir and the
rest, while core/testing’s compile-only B surface declared almost none of them. Adding the
missing common members makes internal/zstd validate at 534/534 — banked 2026-07-27.
Two claims previously recorded here are wrong, and both were re-measured on master before the retraction:
- “Completing
B’s surface breakscrypto/hmac.” It does not. With all 14 members added,crypto/hmacregenerates with its<ProjectReference … io.csproj />intact and validates at 172/172. The stated mechanism cannot hold:core/testingis hand-owned C#, the closure is computed in Go fromgo/types, and the converter never reads the shim — no edit totesting.cscan change a byte of converter output. (Adding extension methods would not makeBimplementTBin C# either.) - “
crypto/hmac’s closure is not reproducible from a standalone regeneration.” It is. Deletingcrypto.hmac.tests.csprojoutright and re-running the pipeline on the committed tree regenerates it byte-identically,io.csprojincluded, with and without theBmembers.
The likely origin of both is charter §9’s false-alarm trap (a): a bin/go2cs.exe built before
60f99c505 — the commit that added the interface-base closure, and the one immediately before
hmac’s banking commit — regenerates hmac without the io reference and fails exactly as described.
Lesson to carry forward: when a change in one language appears to alter output produced by
another, force go build -o bin/go2cs.exe and re-measure before recording a coupling.
Rulings — 2026-08-02 (user; all recommended options adopted)
time/TestUnmarshalTextAllocations: NO disclosure. A want-zero alloc assert is satisfiable, so disclosing it would soften the doctrine the badges depend on. TheIByteSeq<T>boxing redesign (CleanupBacklog #7) is PROMOTED ontotime’s critical path. The doctrine paid off twice onos’s instance (r39-osalloc, 2026-08-03). Refusing the disclosure forcedTestWriteStringAlloc’s 9,208 bytes to be DECOMPOSED rather than argued about, and the decomposition found two silent allocations inж<T>—IsNullboxing the whole pointee on every dereference, andof(…)minting its untyped accessor wrapper per call — worth 62 % of the bill and paid by every pointer read and field address in the corpus, not just byos. A disclosure would have banked the package and left both in place. The row still does not reach zero andosstill does not bank; the remainder is an architectural arc, recorded in theosblock’s r39-osalloc sub-section.- Capability-exclusion SANCTIONED for the provably-unownable os class — the hostfxr
apphost-relocation limitation (
TestRemoveAllWithExecutedProcess),TestCmdArgs(a managed materialization would let GoLocalFreeGC memory), andTestDirectoryJunction(raw-metal on non-native types in test code). Implement via the establishedunsupportedRuntimeCapabilitiesmechanism, WITH the mandatory §9 roster scan (positive control) before widening. This plus the fixable rows isos’s path to a bank. IMPLEMENTED 2026-08-03 (r38-os-fin) — all three, with the roster scan clean (zero hits across 72 packages) and both controls firing. The mechanism gained one generalization it needed: an entry now maps a SYMBOL to the NAME of the capability, so the proof page reads “relocatable single-file test executable” instead of a bare symbol, and a key may name the test DECLARATION itself for a capability that belongs to the host rather than to anything the test calls. Detail in theosblock’s r38-os-fin sub-section. The fixable rows all closed too; the path led to one residual, not to a bank — see ruling #1, whichTestWriteStringAllocis now the second instance of. - Timer mode-0 divergence ruling DEFERRED until the recorded one-fire-per-pass timer-model fix lands and reshapes the residual — no ruling on a measurement about to change.
GoUntyped→GoBigConst(see the charter §6.1 math/big row); rides the rebank. LANDED 2026-08-04 (r40-rebank, commit A) — a pure rename of theSystem.Numerics.BigIntegercsproj<Using Alias>: converter emission + templates,golib.csproj, the behavioral goldens that carry it, and the strategy docs. The corpus saidGoUntypeduntil the rebank’s own regen levelled it in commit B. The behavioral projectGoUntypedConstArgkeeps its name — it is named for the Go-language untyped const concept, not for the C# alias.- The native-address+managed-snapshot pointer flavor is DEFERRED until
net’s DNS work demands it; then a design-with-user session — not designed against one test. - Whole-corpus rebank: scheduled immediately after the r37 train lands (carries the
accumulated intended drift + the param-unification footprint + the
GoBigConstrename). - NuGet release: after the rebank, so the first badged release ships a corpus byte-current with the converter.
The r37 train’s sweep catch — reflection increment 6 REVERTED pending its atomic twin (2026-08-03)
The all-ships sweep failed math/rand AND math/rand/v2 on the assembled train:
panic: reflect: Method index out of range in TestRegress — the EXACT successor gap increment
6’s own report recorded (“a NumMethod() > 0 gate lets method-enumeration loops get further; the
first consumer that walks one demonstrates it”). The demonstration arrived one session later, in
two BANKED packages no lane had canaried — which is precisely the coverage the sweep exists to
provide. Reverted from the train (39de5dd77 reverts d75e0afcd); both packages re-validate at
their exact banked counts (43, 36); time returns to 145 (its two JSON rows re-land with the
pair). The durable scoping lesson: NumMethod and Method(i)/Value.Method/Call are one
ATOMIC increment — a count without an enumerator converts silent vacuous passes into hard
panics. Increment 6’s work survives on claude/elated-hodgkin-12581e (d75e0afcd); the chip’s
increment-7 chit carries the pair, with TestRegress’s loop as the primary gate and math/rand ×2
as mandatory canaries.
RESOLVED — increment 7 lands the pair (2026-08-03)
The count and the walk shipped together: rtype.{NumMethod, Method, MethodByName} + Value.Method
over ONE ordered table whose .Count IS NumMethod, with a method value represented as an
ordinary receiver-bound delegate so Type()/NumIn/In/Out/Call are existing surface
unchanged. Measured on this tree:
| Package | Before (master) | After | Note |
|---|---|---|---|
math/rand |
43 (TestRegress passing vacuously — NumMethod 0 ⟹ zero loop iterations) |
43 | TestRegress now genuinely runs its 320 golden comparisons; the bridge reports *rand.Rand NumMethod: 16 in Go’s order |
math/rand/v2 |
36 (same vacuous pass) | 36 | 18-method table, same shape |
time |
146 pass / 11 fail / 2 skip of 159 (the r37 re-measure above) | 148 pass / 9 fail / 2 skip | TestTimeJSON + TestUnmarshalInvalidTimes re-land. Remaining 9 = TestChan ×8 (timer-model item) + TestUnmarshalTextAllocations (disclosure ruling) — neither this arc’s |
Note the board’s “time returns to 145” above was written against the older 145 figure; the
correct successor of the r37 re-measure (146) is 148. The vacuous-pass detail is the part worth
carrying forward: the banked 43/36 were never evidence that TestRegress worked, because with
NumMethod at 0 its loop body never executed — a count of zero is indistinguishable from a type
with no methods, which is the same silent-degradation class as the "" type name (increment 5).
Also fixed here, and it retroactively invalidates increment 6’s numbers: a this object extension
method (golib’s TryCastAsInteger) was entering every type’s method table through the
candidate source’s assignability safety net, and doing so nondeterministically — the same binary
reported NumMethod 4 or 6 for the same type depending on which assemblies had loaded when the
cache was first filled.
Increment 8 — the ZERO test, and the one row that must NOT be landed (2026-08-03)
Two of gob’s chip-owned roots close; the third is rooted and handed back with a recommendation rather than a fix.
Measured on the post-fix tree: encoding/gob 88 → 95 of 106. One -tests -test-action all
-test-timeout 20m run, zero empty verdicts: the mismatch list goes from 18 rows to 11, and the
seven that vanished are exactly the two roots below — TestGobEncoderPointerThenValue,
TestGobEncoderValueThenPointer, TestGobEncoderValueEncoder, TestGobEncodeIsZero,
TestLargeSlice + /byte + /struct. No new mismatch appeared, so this is not the
root-cause-layering case where one row’s fix merely unmasks another. gob still does not bank and no
gob artifact was committed (the measurement tree was restored). The remaining 11 keep their existing
owners: array-length model (3), ArrayOf/typelinks (1), MapType().Hasher (1, below), typed-nil
converter (3), the gob ignore path (2), untyped complex narrowing (1).
Closed — root 1 (Value.IsZero, 4 rows) and root 2 (Value.Grow, 3 rows). The census
understated root 1 considerably. It is not “wrong for a named STRING and for an ARRAY”: both the
Array and the Struct arm fall to v.ptr == nil, which the bridge never populates, so IsZero
answered true for every array and every struct in the corpus whatever it held. Measured against
go run on a purpose-built probe before the fix — [2]uint8{1,2}, NA{1,2}, inner{N:1},
outer{P:&n}, outer{I.S:x} — every one true in C#, false in Go. A fourth read had to land with
it: IsZero’s String arm is Len() == 0, and Len was blind to a [GoType("str")] wrapper (every
other named container answers through its golib interface; a named string implements none), so the
arm could not be right until Len was. Both now hand-owned, plus Grow, which read a
*unsafeheader.Slice off the same absent v.ptr and nil-deref’d for every caller. Guard:
tests/Behavioral/ReflectZeroAndGrow, byte-identical to go run across 33 rows. Design:
ConversionStrategies-Reference A ZERO test is a descriptor read too.
NOT landed, deliberately — MapType().Hasher / Key.Equal (unique’s 15 of 19, net’s last cctor
root, gob’s TestNetIP). This row is not the same shape as the others and populating it would be a
regression, not a partial fix. Hasher(unsafe.Pointer, uintptr) uintptr must hash the value at an
address; the address that call site produces cannot name a managed value. Three measurements settle
it: two boxes holding equal @string values necessarily have different addresses (so no
address-derived hash can make unique.Make("hello") agree with itself — the package’s whole point);
a box whose pointee contains a reference has no pinnable slot and its address moved across a forced
GC; and the unsafe.Pointer the call site builds retains no link to its source box, its
constructor taking a uintptr. Key/elem types are recoverable from the carried System.Type, but
landing only those is strictly worse than today: Key.Equal is the comparability SIGNAL — a
pointer-identity compare — so a half-populated descriptor turns a loud NewHashTrieMap construction
failure into a map that silently mislays every key. The increment-6 lesson inverted: a descriptor
field whose read cannot be honored must not be populated to look truthful.
Recommendation (needs a coordinator ownership ruling). The remedy is one layer down and outside
this arc’s declared files: hand-own internal/concurrent/hashtriemap.cs on the sync.Mutex
precedent. Its CONTRACT — a concurrent map from comparable K to V — is answered natively and
correctly by the CLR; only its MECHANISM (hash the bytes at an address) is raw-metal that the managed
model cannot express. That is exactly the documented S1 fork. It would clear unique’s single wall
(making 19 rows measurable for the first time), net’s last initializer root, and gob’s TestNetIP.
The chip did not take it unilaterally because internal/concurrent belongs to no lane’s declared
ownership and the file is a whole-package hand-own, not a bridge _impl.cs.
Rulings — 2026-08-03 (user; both recommendations adopted)
- The mode-0 timer residual (time’s 4 rows): COMMISSION THE SYNCHRONOUS-TIMER-CHANNEL ARC rather
than ruling a divergence — Go 1.23’s sync timer channel (#37196: Stop/Reset that blocks stale
values; no drain needed) implemented in golib’s channel layer. Wave3’s successor arc, §7
adversarial discipline, r39-timer’s zero-margin drain constraint (at Stop/Reset at most 2 ticks
exist: 1 buffered + 1 committed-unsent) is required reading. time banks when it lands (152 + 4
mode-0 rows + the alloc row below).
IMPLEMENTED 2026-08-03 (r39b-synctimer) — all 4 rows closed,
timeat 156/1/2 of 159; detail in thetimeblock’s RESOLVED — r39b lands the synchronous timer channel sub-section. The bank is now gated solely on ruling #2’s arc. - time’s alloc row (216 B, both halves fixable): NO disclosure — commission the CLOSURE-EMISSION
arc. The 88 B half = the local-function emission mode (a func literal bound to a local that is
only ever CALLED emits a C# local function — captures without allocating; corpus-wide fidelity +
perf win). The 128 B half = escape-analysis refinement (an address-taken local Go stack-allocates
need not heap-box). Sequenced AFTER r39-osalloc’s dock so its defer-closure findings unify with
the local-function mode into ONE reviewed closure-emission design.
IMPLEMENTED 2026-08-03 (r39e-closure) — both halves landed,
timeat 157 pass / 0 fail / 2 skip of 159 and banked as package #73. The unified design the ruling asked for isDESIGN-closure-emission.md: §3 records what landed, §4 is the ref-struct frame (r39-osalloc arc item 3) written up as a proposal for user review, NOT implemented. Detail in the section below.
r39e-closure (2026-08-03) — 216 = 128 + 88, both halves are converter emission, and time banks
Ruling #2 commissioned this arc on r39-timer’s decomposition. That decomposition was exact: each half was re-measured here in isolation, by reverting one emitted form at a time in the built test host and re-running the single row.
time TestUnmarshalTextAllocations |
allocs |
|---|---|
branch base 18423efaf |
216 |
| local-function fix only (test-body box restored by hand) | 128 |
| escape-narrowing fix only (parseUint lambda restored by hand) | 88 |
| both | 0 — passes |
Fix 1 — a func literal that is only ever CALLED emits as a C# local function (88 B). A capturing
lambda allocates a display class AND a delegate on every evaluation of the lambda expression — per
call of the enclosing function, whether the closure runs or not. A local function that is never
converted to a delegate captures through a by-ref STRUCT closure: same single storage location per
captured variable, no heap object. The gate is the proof that keeps that compilation available —
every reference other than the declaration must be a call callee, which also subsumes reassignment
and address-taking. Emission is a new LambdaContext.localFuncName mode in convFuncLit, so the
whole body pipeline (capture hoisting, boxed value params, variadic prologue, array clones, named
results, the single-return collapse) is shared verbatim with the lambda path. A literal that
defers or recovers is deliberately excluded: its 440 B execution context dominates the 88 this
removes, and lifting the exclusion is §4 of the design, not a workaround here.
Fix 2 — a variable DECLARED INSIDE a closure is not captured BY it (128 B). The escape
analysis’s function-literal arm matched any mention of an object lexically inside a literal’s body,
and for a variable declared there that mention is its own declaration. var t Time;
t.UnmarshalText(in) inside a closure heap-boxed t — and the box Ꮡt was never referenced in
the emitted body — while the identical statements outside a closure emitted a plain local. One
containment test fixes it, and the skip keeps descending so a literal NESTED inside still marks the
escape it genuinely causes. The narrowing direction is the dangerous one, so the proof is explicit:
Go scoping puts a literal’s own local out of reach of every other frame, and every route by which
such a local can still escape (&x, &x.f, &x[i], a pointer argument, a capture-mode method, a
pointer-receiver method value, a go/defer use) is decided by an arm that walks the whole
enclosing body, literal bodies included.
Whole-corpus footprint — two-temp-root A/B (both roots seeded per CLAUDE.md §1/§1a; base exe built
from HEAD versions of the four changed converter files):
| files | sites | |
|---|---|---|
| local-function emission | 91 | 152 (133 block-bodied, 19 expression-bodied) |
| heap box removed | 22 | 32 |
| both families in one file | 8 | |
| total changed | 105 |
Every changed line in the 105 files falls in one of the two families — verified by attribution, not
by sampling: each removed line is a lambda declaration, a };→} close, a = ref heap box, or a
statement in a file that has a box removal (the collapse’s second line, and in reflect/iter.cs the
valueᴛ1 for-loop temp that only existed because the variable was a ref local). Marker gate:
39 [module: GoManualConversion] files, line-anchored, 0 clobbered, 16 carrying a .cs.auto.
Behavioral CNR: 41 files, 96 local-function sites + 1 box removal, 168 added / 169 deleted —
arithmetic closes exactly (96 + 71 }; + 2 box lines removed = 169; 96 + 71 + 1 = 168). Guards:
LocalFunctionEmission (10 probes, 5 negative controls — one per disqualifying reason) and
ClosureLocalNoHeapBox (8 probes, 5 of them boxes that must survive, each writing through the
escaping alias and reading it back). Both neuter-proven: with the fix removed each golden
mismatches (24 and 11 changed lines respectively) and restoring it returns them to green.
One incidental finding worth recording. The committed src/core is stale by 685 files
against a seeded reconvert with the BASE converter — the r36 four-deref-accessor change
(Ꮡp.Value → Ꮡp.DerefOrNull() on pointer receivers and parameters) landed as a converter fix
without a corpus regen, which is correct policy but means a plain overlay-then-git diff is NOT a
usable A/B instrument on this branch. The two-temp-root form is, and it is what the numbers above
come from. The same staleness is what makes a time -tests run show DerefOrNull and
fallthrough-placement diffs in its production .cs; those are pre-existing, not -tests-closure
drift, and they are restored rather than banked.
The sweep found a 74th thing: a disclosure that was never a CLR limit. The full 73-package
validated sweep (2,783 s) reported 72 pass / 1 “fail”, and the one flagged row was bytes at
count 82, banked 81 — MORE matching verdicts than the roster claimed. TestEqual had an
alloc-profile disclosure since 2026-07-18 reading “the managed runtime allocates during the
converted Equal comparison loop where Go’s compiler-optimized code does not”. It does not. The
converted test body was
foreach (var (_, vᴛ1) in compareTests) {
ref var tt = ref heap(new compareTestsᴛ1(), out var Ꮡtt); // ← per ITERATION
tt = vᴛ1;
…
}
— the range variable of a loop inside the AllocsPerRun closure, heap-boxed by exactly the arm this
train narrowed, once per iteration of an assert that wants zero. It now emits
foreach (var (_, tt) in compareTests) and the test passes on its own merits. The disclosure is
retired, not re-signed: §5 of the disclosure policy says a real bug is never a disclosure
candidate, and this one had been standing in for a converter defect for two weeks. bytes moves to
82 matched · 6 disclosed (re-run twice, identical), the roster to 2,713 matching · 50
disclosed, and its TestEqual verdict is now earned rather than excused.
That is also the general lesson worth keeping: a want-zero alloc assert is a converter test, and a
disclosure filed against one should be re-examined every time the emission changes. Five
alloc-profile disclosures remain in bytes and one in bufio; nothing here says they are wrong, but
nothing has re-derived them either. The cheap instrument is the one this lane used by accident — run
the sweep and read a count that is HIGHER than banked as a finding, not as noise.
Sweep aftermath, classified: 60 proof pages regenerated (a renderer wording change from an earlier
lane plus provenance — restored, they belong to a rebank), the documented 7-file -tests-closure
emission class, and corpus-wide production .cs churn that is the same 685-file staleness recorded
above. Only bytes’s test sources, its disclosure manifest and its proof page were banked, because
only they are the evidence for a row that changed.
internal/concurrent.HashTrieMap HAND-OWNED — the wall falls, and three walls stand behind it (2026-08-03, r39d-hashtriemap)
The user-ruled hand-own landed: src/core/internal/concurrent/hashtriemap.cs is now a whole-file managed
reimplementation carrying [module: go.GoManualConversion] (corpus marker census 39 → 40). Rationale,
the API map, and the equality-bridge verification live in
ConversionStrategies-Reference.md, internal/concurrent.HashTrieMap;
the hand-own mechanics in Baseline-vs-FullConversion.md. Summary of
what was measured, because the shape of the result matters more than the row count:
Gates. internal/concurrent and unique build clean; go2cs-stdlib.slnx builds 304/304, 0 errors.
A seeded full -stdlib -comments reconvert leaves hashtriemap.cs and package_info.cs MD5-identical;
strip the marker and the same run overwrites hashtriemap.cs with its own 21 KB emission and rewrites
package_info.cs — the protection proven in both directions. The behavioral suite and the 72-package
validated sweep are green/unchanged (internal/concurrent is in no banked package’s closure — the gates
were insurance, not measurement).
The equality bridge is NOT a problem — verified by probe, not by reading. EqualityComparer<K>.Default
is Go’s == for every key shape the corpus interns: ж<T> (pointer identity + matching identity hash, and
abi.TypeFor<T>() interns one descriptor box per System.Type, so a second call finds the first call’s
entry), a [GoType] struct of {bool; @string} — netip’s addrDetail shape — (generated field-wise
Equals + HashCode.Combine, matching for two keys built from distinct string storage), and @string
(content). LoadOrStore was contention-probed: exactly 1 winner in 64 racing callers.
encoding/gob: 95 of 106, unchanged — TestNetIP does NOT flip. Its root MOVED one frame, from
NewHashTrieMap → ArgumentException: Delegate to an instance method cannot have null 'this' to
NotImplementedException: runtime_registerUniqueMapCleanup. No row regressed (TestNetIP is the only gob row
whose closure reaches unique; the other ten failures are gob-internal and untouched).
unique: 0 → 1 of 19 — and it is no longer a ONE-root wall. That is the real deliverable. Its 15
identical TypeInitializationException rows resolve into five distinct downstream roots, each now
separately actionable:
| Root | unique rows | Shape |
|---|---|---|
//go:linkname PUSH never links: unique.runtime_registerUniqueMapCleanup |
1 (+ gob’s TestNetIP, + net’s cctor) |
CLOSED 2026-08-07 (r43b-linkname). runtime/mgc.go PUSHES its body into unique’s bodyless declaration and the converter’s forwarder handled the PULL direction only, so the consuming side was a throwing PartialStubGenerator stub. It now FORWARDS to runtime’s converted body — see the linkname PUSH direction below |
internal/weak.runtime_registerWeakPointer / runtime_makeStrongFromWeak |
4 → 7 → 0 | CLOSED 2026-08-07 (r43e-weak). runtime/mheap.go pushes both; hit inside weak.Make, i.e. unique.Make’s newValue(). r43b took the linkname half only (registered UNHONORABLE, announcing itself by name), because linking was never the remedy — runtime’s converted bodies walk mheap_ span metadata the managed model does not populate. The remedy was the hand-own it announced, and it has landed: internal/weak/pointer.cs on System.WeakReference + a ConditionalWeakTable canonical index. The seven rows it had absorbed now advance INTO the test body — see internal/weak HAND-OWNED at the end of this file for where each one stops instead |
abi.TypeFor<T>() is silently WRONG for an INTERFACE T |
1 | TypeFor’s interface branch is TypeOf((*T)(nil)).Elem(), and Type.Elem() for Kind == Pointer reinterprets the descriptor as a PtrType and reads .Elem — which under the managed layout lands on the descriptor’s Equal field. TypeFor<any>() and TypeFor<error>() return a System.Func<unsafe.Pointer, unsafe.Pointer, bool>, not a ж<abi.Type>. Shared generics store it into ConcurrentDictionary<ж<abi.Type>, any> uncast-checked, and the first key comparison dispatches IEquatable<ж<abi.Type>>.Equals on a delegate → EntryPointNotFoundException. Corpus-wide, and it was invisible until now: the old trie compared raw addresses through keyEqual and never dispatched on a key’s runtime type. Reflection-bridge row |
GCHandle: Object contains references |
1 | abi.Escape pinning a managed pointee on the weak.Make path |
IndexOutOfRangeException in go.slice<T>.Enumerator.get_Current |
6 | CLOSED 2026-08-07 (r41c-cloneseq). Not the enumerator, and not “neither linkname nor reflection” — see the makeCloneSeq root, closed immediately below |
Plus the 3 fail rows the r38 census already recorded (TestMakeCloneSeq/#00, #01, interface_{} — Go
names those subtests from reflect.TypeFor[T]().String(), which C# renders ""; note this is the same
TypeFor surface as the third root above). unique does not bank; its test artifacts were restored,
not committed.
⚠ Two traps this arc paid for. (1) In the PowerShell tool, [System.IO.File] resolves a RELATIVE path
against the process working directory, which is the MAIN checkout — not Set-Location’s. A
read-modify-write with a relative path silently read H:\Projects\go2cs’s copy of the file and wrote it
over the worktree’s, reverting the hand-own. Always use absolute paths with the [System.IO.File] APIs.
(2) emitAutoConversionSiblings — the fully-hand-owned-package branch — runs only six of the whole-package
pre-passes, and panics on a generic file (WARNING: visit file error: … nil pointer dereference in
"hashtriemap.go" (auto-conversion sibling skipped)), so no .cs.auto review sibling is produced for
internal/concurrent. Pre-existing converter defect, harmless to the marker’s protection, not chased.
The makeCloneSeq root, CLOSED — unique 1 → 4 of 19 (2026-08-07, r41c-cloneseq)
The board’s guess about this root was wrong in both halves, and the way it was wrong is the finding.
It is not a slice<T>.Enumerator edge — the enumerator behaves correctly given the header it is handed
— and it is squarely reflection-bridge territory rather than “the only root that is neither linkname nor
reflection”. What made it look otherwise is that the diagnostic names golib and the cause is two
frames up, which is charter §9’s layering lesson in a new dress: a first diagnostic is a starting
point, not a diagnosis.
The root. makeCloneSeq → buildStructCloneSeq opens with styp := typ.StructType(), and Go’s
(*structType)(unsafe.Pointer(t)) is the PREFIX-DOWNCAST idiom — the linker really allocated a
structType behind the Type header. Nothing sits behind a ж<abi.Type>, and golib’s Reinterpret
rightly REFUSES to alias managed storage for a reference-bearing pair (aliasing would fabricate object
references), so it fell through to the raw-address route and read ΔStructType’s fields out of the
memory following the descriptor’s value slot. Probed on abi.TypeFor[testStringStruct]():
Fields.Length 8830452760576 <- an address fragment read as a slice length
Fields.Capacity 16 <- the descriptor's OWN Size_, bleeding through the shifted view
m_array landed on a real heap object, so the first Current threw IndexOutOfRangeException instead
of access-violating: a CLR type-safety break that happened to be caught. internal/reflectlite’s
NumField/Len read the same garbage.
The fix, at the root’s own layer. Type.StructType and Type.ArrayType join TypeOf in
manualConversionFuncs["internal/abi"] and are SYNTHESIZED in type_impl.cs from the descriptor’s
carried System.Type — field types via synthType, Go (amd64) field offsets and array Len/Elem/
Slice via golib. Offsets come from the same walk that stamps a descriptor’s Size_
(GoReflect.GoFieldOffsets, factored out of GoSizeOf’s struct arm), so the two cannot disagree.
Nothing unknowable is invented: no System.Type, or a field whose Go size is unknowable, answers Go’s
nil; StructField.Name/PkgPath stay the zero ΔName (its readers walk addChecked raw addresses —
the same route that produced the garbage — and Go’s own Name() answers "" for a nil Bytes, so the
zero value is a state the format defines). Full rationale:
ConversionStrategies-Reference.md, abi.Type’s
SPECIALIZATIONS are synthesized, not downcast.
A second, independent defect in the same file — a converter one, and silent. buildArrayCloneSeq’s
whole body was emitted as a /* … */ COMMENT. visitRangeStmt recognized range-over-integer only for
types.Int/untyped-int, so for range atyp.Len (a uintptr) fell through to the “unexpected
expression” arm and the loop VANISHED — unique’s cloneSeq for any array-of-string type came back
empty. It was the only such comment in the entire converted stdlib. Fixed generally (any integer kind,
golib range<T> with the operand’s own Go width, explicit type argument at each non-int site) and
guarded by the RangeOverIntegerTypes behavioral test; details in the same reference doc,
Range-over-integer covers EVERY integer type. ⚠ Worth remembering: the first attempt at the golib
overload REGRESSED range(3) to System.Int32, because the generic is an identity match where
range(nint) needs a conversion and C#’s prefer-non-generic tie-break never fires — caught by probe,
not by reading, and closed with a third range(int) overload.
Census, unique, matched rows: 1 → 4 of 19. All six IndexOutOfRangeException rows are gone. The
three TestMakeCloneSeq ones (testStringStruct, testStruct, testStringStructArrayStruct) now
PASS; the three TestHandle ones MOVED to the internal/weak.runtime_registerWeakPointer root that
was always behind them. A fifth subtest, TestMakeCloneSeq/testStringArray, now computes the correct
{[0 16 32]} but still cannot MATCH, because C# names it #01 — that is the TypeFor/Name root,
row three of the table above, untouched. TestHandle/interface_{}/<nil> also moved (from
EntryPointNotFoundException to a null HashTrieMap key); that root’s Type.Elem() reinterpret is
the SAME defect class as this one and simply read different garbage this run — it was not chased, and
neither were MapType()/FuncType()/InterfaceType()/Key()/Len(), which all still reinterpret.
unique still does not bank; its test artifacts were restored, not committed.
Blast radius. StructType()/ArrayType() have exactly two corpus callers (unique,
internal/reflectlite), and the range widening has exactly one corpus site. Gates: behavioral CNR
byte-identical across all 570 packages apart from the new test project; run-behavioral.ps1 full
545/545 transpile+compile+golden and 515/515 stdout (30 skipped, no package main), 1,081 s;
go2cs-stdlib.slnx 304/304, 0 errors; go test ./... in the converter ok; GolibTests 69/69
(60 + 9 new), ChannelTests 24/24.
The linkname PUSH direction, CLOSED as a MECHANISM — one pair links, one announces itself (2026-08-07, r43b-linkname)
The converter’s forwarder handled only the PULL direction (a bodyless declaration naming another
package’s symbol). Go’s other direction — the DEFINING package carries the body and names another
package’s declaration, the consumer being a bodyless func under a one-arg //go:linkname handle
— linked nothing, so every consumer fell to the PartialStubGenerator. Mechanism and rationale:
ConversionStrategies-Reference.md, A cross-package
//go:linkname PUSH resolves per recorded disposition.
| Pair | Disposition | Why |
|---|---|---|
runtime.unique_runtime_registerUniqueMapCleanup → unique.runtime_registerUniqueMapCleanup |
FORWARDED | The pushed body is ordinary converted Go — a chan struct{} plus a goroutine that drains it and calls the callback. The managed model runs the real thing; nothing signals the channel because clearpools() is driven by Go’s GC, which does not run. That is Go’s own behavior for a program whose GC never fires (the intern map keeps its entries), not a fabricated answer |
runtime.internal_weak_runtime_registerWeakPointer → internal/weak.runtime_registerWeakPointer |
LOUD STUB → HAND-OWNED | getOrAddWeakHandle → spanOfHeap → throw("getWeakHandle on invalid pointer"): the body walks mheap_ span metadata the managed model does not populate. Answered 2026-08-07 (r43e-weak) by the internal/weak hand-own; the registry row STAYS, because it is what a conversion into a root without the hand-own must still emit |
runtime.internal_weak_runtime_makeStrongFromWeak → internal/weak.runtime_makeStrongFromWeak |
LOUD STUB → HAND-OWNED | Re-derives an object pointer from a heap address. A forwarder would fault or — worse — return a plausible pointer derived from garbage, the inverse-atomic rule’s exact prohibition. Same disposition, same answer, same reason for keeping the row |
The registry is curated, and the reason is structural, not caution. The converter never sees the
pushing package’s directives while converting the consumer — a package is converted from its own
syntax, dependencies contribute types rather than comments, and the pusher need not even be a
dependency. Go 1.23 carries ~200 pushes outside cmd/; the corpus exposes eleven as bodyless
one-arg-handle declarations, and linking those wholesale would REGRESS working packages: time’s
timer trio is already answered by time_impl.cs and a converter-emitted body would collide with it,
while internal/syscall/windows’s stdcall wrappers and internal/coverage/cfile’s linker-section
walk push bodies the managed model cannot run at all.
Measurement — the honest read is “the root moved”, not “rows flipped”.
unique: 4 of 19, UNCHANGED. The cleanup registration links and no longer throws anywhere; the sevenTestHandlerows that stopped there now stop one frame later, insideweak.Make, on the ANNOUNCED weak pair. The remaining roots are untouched:abi.TypeFor<T>()for an interfaceT(EntryPointNotFoundException, still row three of the table above),GCHandle: Object contains referencesonabi.Escape, and theTypeFor/Namesubtest-naming rows.uniquedoes not bank; its test artifacts were restored, not committed.encoding/gob: 98 of 106;TestNetIPdoes NOT flip. Its root moves fromNotImplementedException: runtime_registerUniqueMapCleanupto the announcedinternal/weak.runtime_registerWeakPointerinsidenet/netip’scctor→unique.Make→newValue(). ⚠ The 98 is not this arc’s delta: the board’s 95 dates from r39d and the other seven failures (TestBadData,TestEndToEnd,TestIgnoreDepthLimit,TestIgnoreRecursiveType,TestIndirectSliceMapArray,TestNilPointerInsideInterface,TestSingletons) are gob-internal, outside anything three files inunique/weak/runtimecan reach. The intervening arcs moved them; re-baseline the row from this number, do not credit it here.
What internal/weak is now waiting on — and it is the ONLY thing. A hand-owned managed weak
reference (System.WeakReference over the ж<T> box) under [module: go.GoManualConversion], the
same shape sync’s Mutex family and internal/concurrent.HashTrieMap took: honor the observable
contract, never emulate the mechanism. Deliberately NOT attempted in this lane — the linkname
mechanism and a semantic hand-own are separate units of work, and the loud stub is what makes the
second one findable. Its single file (internal/weak/pointer.go) makes a whole-file replacement the
natural form. ✅ Landed the same day (r43e-weak), in exactly that shape — see
internal/weak HAND-OWNED at the end of this file.
Gates. go test ./... in the converter ok (new TestRecurseLinknamePush, both arms
neuter-proven); CNR byte-identical across all 571 behavioral packages; a seeded full
-stdlib -comments reconvert is byte-identical to the committed tree across every .cs/.csproj/
README.md (zero unclassified; hand-own clobber gate 0 violations; no DYNTYPE markers);
go2cs-stdlib.slnx 304/304, 0 errors. A/B footprint: 3 corpus files.
⚠ A NEW environmental failure shape worth recognizing: the host DISK FILLED mid-suite. The full
run-behavioral.ps1 reported FAIL (546 projects, 1,413.5s) with 115 Go build failures plus one
Output mismatch — and every one of the 115 reads verbatim compile: writing output: write
$WORK\b001\_pkg_.a: There is not enough space on the disk (C: was at 2.8 GB free of 1.86 TB, three
lanes deep). The C#-side phases, the ones a converter change can actually move, all passed the WHOLE
corpus: Transpile 546/546, Compile 546/546, Target 546/546. Output read 404 pass / 1 fail / 141 skip,
where the skips are the 115 disk-killed Go builds on top of the usual no-package main set. The single
Output failure, FindFirstFileData, re-runs PASS 1/1 across all four phases in isolation once space
is freed — the standing rule for a Go-toolchain-side failure under load (re-run that one project
filtered before believing it) applied to a new cause. Read a wall of identical not enough space on the
disk lines as the machine: check Get-PSDrive C FIRST, and do not go hunting for a converter
regression — Target passing 546/546 already proves no golden moved.
internal/weak HAND-OWNED — the announced pair gets its answer (2026-08-07, r43e-weak)
The third instance of the ruled precedent, after sync’s Mutex family and
internal/concurrent.HashTrieMap, and the easiest fit of the three: src/core/internal/weak/pointer.cs
is now a whole-file hand-own under [module: go.GoManualConversion] built on System.WeakReference
over the ж<T> box, with a ConditionalWeakTable keyed on ж<T>.ReferentObject standing in for the
runtime’s canonical per-address specialWeakHandle. Design, the clause-by-clause contract table, the
ephemeron argument for why the canonical index does not pin what it indexes, and the guarding
measurements: ConversionStrategies-Reference.md,
internal/weak.Pointer. Marker census 39 → 40 (line-anchored; note 39, not the 40 CLAUDE.md
records from r40 — math/unsafe.cs shed its marker in the interim). internal/weak joins
internal/godebug and internal/concurrent as fully hand-owned: internal.weak.csproj,
package_info.cs and README.md stop re-emitting and no .cs.auto sibling is produced — all three
confirmed in place, since a -tests run over the package left every one of them untouched.
A SECOND defect was standing behind the first, and it is not weak’s. The [GoType] generator gates
struct equality on every type parameter carrying an IEqualityOperators constraint, so a Go type
declared [T any] (or [T comparable], which the converter renders new()) emits
Equals(other) => false /* missing equality constraints */ — even when no field’s type mentions the
parameter at all. Both weak.Pointer[T] (field unsafe.Pointer) and unique.Handle[T] (field
ж<T>, which defines == for every T) were victims. Pointer[T]’s copy is fixed here by hand-writing
the struct; unique.Handle’s is a GENERATOR fix, chipped, and it is what six TestHandle rows now
report.
Measurements — every root moved, no row count did
| Package | Before | After | What actually changed |
|---|---|---|---|
internal/weak (own suite, first ever run) |
— | 1 of 3 | TestPointerEquality PASSES vs go test — the canonicalization clause, the hardest one, validated end to end. TestPointer/TestPointerFinalizer fail on the roster’s codegen-liveness class (below). Does NOT bank, and not because the count is short of the bar: TestPointerFinalizer does not fail an assertion that could be disclosed, it BLOCKS forever on <-done awaiting a finalizer a still-rooted object can never queue |
unique |
4 of 19 | 4 of 19 | the announced weak panic is gone from every row; the host stops DEADLOCKING; the seven TestHandle rows resolve into four distinct new roots |
encoding/gob |
98 of 106 | 98 of 106 | TestNetIP no longer throws — net/netip’s package initializer completes for the first time and the test produces a value: decoded to ::ffff:1.2.3.4%, want 1.2.3.4. A netip 4-in-6/zone rendering difference, in nothing this arc touches. The other seven failures are the same gob-internal set |
The unique host used to hang, and closing weak is what exposed it. handle_test.go’s drainMaps
arms a one-shot notification, calls runtime.GC(), then BLOCKS on <-wait until the intern-map cleanup
runs. runtime.GC()’s hand-owned managed body (runtime/managed_impl.cs) wired only the sync.Pool arm
of clearpools(), so the cleanup could never run and every TestHandle subtest deadlocked — taking the
whole test host to its package timeout and erasing the verdicts of the 12 rows that had nothing to do
with it. That deadlock only became reachable once internal/weak stopped panicking one frame earlier.
GC() now also does clearpools’ unique arm — the same non-blocking send on uniqueMapCleanup, inert
until unique.Make has run. Result: a 10-minute timeout with 3 usable verdicts becomes a 2-minute run
with 19.
unique’s four surviving roots, all measured this arc:
| Root | Rows | Shape |
|---|---|---|
[GoType] equality gate — unique.Handle<T>.Equals is false |
6 | every reachable subtest reports v0 != v1 and never v0.Value() != v1.Value(), i.e. both Make calls interned the SAME ж<T>. Generator fix (chipped), not a hand-own |
codegen-liveness — a live C# local roots what Go proves dead |
6 (same rows) | checkMapsFor reports value X still referenced a handle. The cleanup now RUNS and CompareAndDelete is reachable for the first time; v0/v1 are simply still rooted where Go’s per-safepoint liveness maps have already dropped them. The roster’s existing disclosure class (sync carries several) |
abi.TypeFor<T>() for an interface T |
2 | EntryPointNotFoundException at IEquatable<ж<abi.Type>>.Equals — unchanged, reflection-bridge row |
GCHandle: Object contains references |
1 | now reached in clone → ж<T>→uintptr → pinnedArrayData, not on the abi.Escape path the old row named |
array<T>.Equals structural comparer |
1 | ArgumentException: Type of argument is not compatible with the generic comparer for an array OF [GoType] structs. New row, previously masked |
Plus the standing TypeFor().Name() subtest-naming rows, which pair up by content but cannot match by
name.
TestPointer’s failure is GC precision, and that is proven rather than argued. Go’s own
pointer_test.go does st := wt.Strong(), then runtime.GC(), then asserts nil — Go’s compiler proves
st and bt dead; a C# frame reports them live. A dedicated probe separates the two by creating and
dropping the referent inside a [MethodImpl(NoInlining)] helper:
PASS CONTROL plain object collects
PASS CONTROL ж<int> in a self-keyed ConditionalWeakTable collects
PASS CONTROL two-level CWT->ConcurrentDictionary keyed on ж<int> collects
PASS Strong() is nil once the referent is unreachable (never probed)
FAIL Strong() is nil once the referent is unreachable (probed first)
16 of 17 assertions pass, and the single failure is the probe’s own frame holding the ж<int> that an
earlier Strong() returned. The controls make the ephemeron reasoning measured rather than assumed.
Linkname PUSH registry — disposition unchanged, prose updated. The two internal/weak rows STAY in
linknamePushTargets as loud stubs, because they describe what a conversion into a root that does not
already carry the hand-own must emit; the deployed corpus never regenerates the marked file. Their
reason strings now name internal/weak/pointer.cs instead of asking for it.
Gates. internal/weak + unique + runtime build clean; go2cs-stdlib.slnx 304/304, 0 errors;
seeded full -stdlib -comments reconvert with the hand-own clobber gate; go test ./... in the
converter ok; GolibTests / ChannelTests at baseline; full behavioral suite. A/B footprint: 2 corpus
files (internal/weak/pointer.cs, runtime/managed_impl.cs) + the converter’s registry comments.
The WHOLE-CORPUS REBANK — 1,316 files, sixteen families, zero unclassified (2026-08-04, r40-rebank)
User ruling #6’s one deliberate regeneration. The campaign’s standing discipline is that the unit of
work is the CONVERTER FIX and that a corpus regen must never bury it, so arc after arc landed a gated
converter change and left src/core behind. This paid that debt in one session, in commits whose only
job is to BE that diff.
It is a bank, not a repair — every file is the already-gated output of a change that shipped with its own behavioral guard, and the reconverted corpus builds 304/304 with zero errors.
Family census — 1,299 files from the overlay (703 .cs, 298 .csproj, 298 README.md)
A file may carry several families; the count is files touched by that signature.
| Family | Files | What moved |
|---|---|---|
| deref-accessor | 592 | Ꮡx.Value / .ValueSlot → .DerefOrNull() at pointer ENTRY aliases (r36 four-accessor, r37b param unification) |
| dead-param-alias | 541 | the entry alias is dropped outright where nothing reads it |
| GoBigConst | 304 | the rename reaching every emitted .csproj + 6 const sites |
| README-badge | 298 | Go_tests → Tests label (r39) + matched/total refresh; 298 removed / 298 added, so no README lost its badge |
| typed-nil | 145 | Ꮡfd → Ꮡfd.OrTypedNil() (r39-nilcomplex) |
| local-func | 90 | an only-called closure literal becomes a local function (r39e) |
| GoImplement | 44 | satisfies-not-witnesses: encoding/binary now records bigEndian → ByteOrder, which nothing ever cast to witness |
| value-adapter | 40 | …and therefore consumers stop minting binary_bigEndianᴠByteOrder |
| implicit-conv | 23 | importedPointerImplements retirement: text/template stops recording the foreign parse package’s pairs |
| closure-box | 22 | a closure’s own local needs no ref heap<T> box (r39e) |
| import-alias | 20 | the using x = go.y_package those records required, now unused |
| wrapper-qualification | 17 | srcimporter_ImporterжImporter → srcimporter.ImporterжImporter |
| pointer-reinterpret | 15 | Ꮡ((T)(~p)) → p.Reinterpret<F,T>() (ruled, 70cbcad69) |
| named-const-cast | 12 | an untyped const argument takes its named parameter type (time.Sleep((time.Duration)(…))) |
| fallthrough | 12 | the flag moves INSIDE the do{}while(false) so an early break no longer sets it — a real semantic fix |
| alias-pointer | 10 | (ж<array<T>>)(uintptr)(new @unsafe.Pointer(x)) → array<T>.AliasPointer(x, n), which stops copying the run |
Reconciliation with the forecast. 699 .cs carry a genuine drift family against the 695
r39-nilcomplex measured on 2026-08-03 — agreement to within the arcs that landed between. The other 600
files are the rebank’s own two corpus-wide relabels (302 GoBigConst, 298 badges), neither of which
existed when the forecast was taken. The r39c pointer peephole showed no new drift, as predicted:
every @unsafe.Pointer line in the diff belongs to alias-pointer or local-func, none to the peephole.
Plus 17 files the regen structurally cannot reach: the three hand-owned packages whose .csproj is
never re-emitted (unsafe, internal/concurrent, internal/godebug), the 13 Perf*.csproj
(regenerated by transpiling each benchmark — the Perf .cs proved to carry no drift at all), and the
Go comments in BigUntypedConstComparison that name the emitted type.
Restored, not banked — and the third phantom shape
- 28 auto-normalized CRLF phantoms — dirty in
git statuswith no diff hunks at all (they do not even appear in--numstat), each proven content-identical modulo CR, positive control fired. -texttestdata copies — ⚠ the trap:src/core/compress/testdata/*is marked-text, so git does NOT normalize it and a pure CRLF flip shows as a real non-empty numstat (gettysburg.txt29/29). The standing rule “a phantom has an empty numstat” is therefore false for-textpaths — test CR-equality directly instead of trusting the numstat.- The
-tests-closure production re-flip — see the correction in DESIGN-named-interface-wrappers §7: the corpus now RESTS on the-stdlibside, but the asymmetry is intact and every sweep re-flipsusing io = io_package;tousing Δio = io_package;. Restore, never bank.
Confirmations this rebank was the right place to make
- Hand-owned marker gate: 40 marked, 0 clobbered, 16
.cs.auto. Unanchored grep reports 63 — the anchor is load-bearing. CLAUDE.md’s census updated 32 → 40. - ZERO production-
.csprojstrips. Backlog item 16 (a-testsrun stripping the validation-pack block, “a loaded gun for the whole-corpus rebank”) is defused byce82093b0: the pack-block census held at 300 of 303 production csprojs across the full sweep, and no production.csprojchanged. - The
.cs.autoreview siblings are TRACKED and 11 of 16 are STALE — new backlog item 18. The overlay excludes them, and that exclusion is exactly what protects the hand-owned.csbeside them, so levelling them is a separate commit rather than something smuggled into a bank.
The sweep — the policy inversion, and what it proved
A validated sweep is normally a GATE whose dirt is restored. Here the corpus itself had moved, so the
sweep’s OUTPUT was the deliverable: 73 packages, 2,713 expected verdicts, 73 pass / 0 fail in
2,736 s (45.6 min), every package at its exact banked count. 299 files banked — 137 *_test.cs,
73 *.tests.csproj, 59 proof pages, 15 go2cs_test_host.cs, 12 package_test_info.cs,
3 package_init.cs — and 36 restored across four shapes (13 closure re-flips, 10 -text testdata,
8 .cs.auto, 5 CRLF phantoms).
That a 1,316-file corpus bank moved no verdict anywhere the roster reaches is the strongest single statement available that this was a bank and not a repair.
It also closed backlog residual #15 exactly as written — the seven banked DerefOrNil() sites in
container/ring, go/token, index/suffixarray and testing/quick re-emitted as DerefOrNull()
the moment each package’s -tests pipeline ran, with no separate work. Zero remain.
⚠ Two traps this half paid for, both worth carrying.
-textpaths break the phantom rule.src/core/compress/testdata/*is marked-text, so git does not normalize it and a pure CRLF flip shows a REAL non-empty numstat (gettysburg.txt29/29). The standing “a phantom has an empty numstat” test is false there — compare CR-stripped content directly instead of trusting--numstat.- Never amend a commit while a run that stamps its SHA is in flight. The proof pages record the
tree they validated against, so amending the corpus bank’s message mid-sweep left 17 of 59 pages
naming a commit that no longer existed. Recoverable only because the amend preserved the tree
exactly (both SHAs point at tree
15e4eca18), which made the stamp correctable textually rather than by re-running 17 packages.
The r43c breadth pass — 58 candidates measured, 23 bank, every non-bank rooted (2026-08-07)
The charter says the cheapest scout is the pipeline itself, and until now it had only ever been
pointed at packages some other arc had named. This pass pointed it at the long tail as a batch:
58 never-measured candidates run back to back through
go2cs -tests -test-action all -test-timeout 10m, selected only by “its dependency closure is
already validated and it is not behind a known deep wall.”
Twenty-three validated on the first run, with no converter, golib or host change of any kind —
roster 73 → 96 (44.7%), 13,070 matching verdicts. One candidate (image/color/palette) has no
eligible Test declarations at all. The remaining 34 are rooted below.
The finding worth carrying forward is the negative one. None of the twenty-three needed anything; the corpus had already grown past them, package by package, as forty banked packages’ worth of shared machinery landed, and nobody had looked. The roster’s denominator was limited by attention, not by defects. So: re-scout the tail after any capability lands, not just the packages that capability was aimed at — a Tier-0 frog closes silently for packages nobody associated with it.
Two mechanical notes for the next person running a batch like this:
- The batch is cheap. A leaf package costs 9–20 s end to end (convert, build, run both sides, diff). Fifty-eight candidates is under half an hour of wall time — far cheaper than reasoning about which ones might be close.
- The proof-page renderer and
docs/validation/index.mdupdate themselves on every successfulallrun, so a batch of banks costs no per-package documentation work. The roster row, its header arithmetic, and the closure-family restore are the only manual steps.
The twenty-three
crypto · crypto/aes · crypto/des · crypto/rc4 · crypto/internal/alias ·
crypto/internal/bigmod · go/constant · go/doc/comment · go/format · go/printer · hash ·
image · image/color · internal/buildcfg · internal/coverage/cformat ·
internal/coverage/cmerge · internal/coverage/pods · internal/dag · internal/diff ·
mime/quotedprintable · net/url · testing/iotest · text/template/parse
go/doc/comment alone contributes 10,059 verdicts — its TestTestdata walks every doc comment
in the standard library’s own Go sources — and is now the largest single suite banked. hash,
crypto and image are worth noting for a different reason: each is a tiny contract package
whose suite exercises the whole family beneath it (hash’s marshal round-trip runs across all
eighteen stdlib digests; crypto’s out-of-bounds guards run every stream mode).
Four of the twenty-three flip a production .cs on every sweep, per the standing -tests-closure
family (crypto/crypto.cs, hash/hash.cs, image/format.cs — the Δio alias; and
internal/buildcfg/package_init.cs — the init-tests hook, which its test half implements nothing
of). All four are added to run-validated-sweep.ps1’s documented $closureFiles set so the sweep
keeps classifying them rather than reporting them as content drift.
Build-blocked — eight roots
| Package | First diagnostic | Root, as far as it was taken |
|---|---|---|
log, go/scanner |
CLOSED 2026-08-07 (r43f-closure-edge): both edges landed, go/scanner BANKED 11/11, log does NOT bank — two roots stand behind the closure one. Full account in the last section of this file. The rooting below called both mechanisms correctly and was wrong about two details worth carrying: log’s literal is not log.Logger{} but var l Logger (a zero-value DECLARATION, in the INTERNAL white-box half — no composite literal exists, which is exactly why no literal walk could see it), and the implemented-interface gate is not types.Implements but the package’s own emitted VALUE-form GoImplement RECORDS: satisfaction alone drifts 16 of the 96 banked projects. Original rooting: log’s external test half writes log.Logger{}; under the white-box InternalsVisibleTo grant the package-under-test’s internal fieldwise constructor IS a resolution candidate, so binding it needs atomic.Bool’s assembly. go/scanner’s generated ErrorList↔error witness calls m_value.Equals(…), and binding a member on ErrorList needs the assemblies of the interfaces its own declaration implements (sort.Interface, ×13). The existing rule’s minimality gate fires the struct edge on an EMPTY literal only for a ROOT package — log’s case says the white-box grant is the same situation by a different route. Both are one edge each on declarationClosureImports, and both must be measured with that rule’s own instrument: regenerate every banked .tests.csproj and require zero drift. The cheapest two banks left on this list. |
|
slices |
CS0305 / CS0411 | Go infers S ~[]E and E from a single argument; C# cannot infer E from S. Equal/EqualFunc/CompareFunc/Reverse/Insert/CompactFunc emit as two-parameter generics and essentially every call site fails. Needs element-type deduction (or witness parameters) for constrained slice generics — the widest root in the batch, and it blocks the largest unbanked leaf (63 Test funcs). |
archive/tar |
writer_test.cs emits the same using alias twice in one file (testFnc, fileMaker), plus one CS0111. A test-half alias emission that does not dedupe within a file. Shallow.claude/dup-append-emission). The rooting was right about the symptom and wrong about the scope in two ways: the collision is not per-file but per-COMPILATION (global using), and dedupe is not the fix — two functions declaring type testFnc any declare two unrelated Go types, so the alias takes the same enclosing-function lift every other local type-declaration kind already took. CS0121 on a one-field nil constructor stood behind it. See CLOSED for archive/tar at the end of this file. |
|
archive/zip |
ReadCloser→fs.FS witness binds Open against a ж<Reader> receiver while holding a value ReadCloserOpen is a pointer-receiver method promoted from ReadCloser’s exported Reader value embed, and that promotion was not emitted at all (root 1), then emitted internal because the scope heuristic reads a tuple return’s trailing error) as unexported (root 3). Package now BUILDS and RUNS at 95 of 98; the residual is TestZip64LargeDirectory + 2 subtests as a performance row (Go 13.2 s, C# > 45 m), not a defect. See r56g below. — BANKED 98/98 2026-08-09 (r57c): the performance row was @string slicing in O(n); see r57c at the end of this file. |
|
testing/fstest |
CS0030 | Converting the test-local named type shuffledFS to its underlying map[string]*MapFile. |
internal/types/errors |
CS0246 | Error / Info — names the emitted code does not declare for a test-local enumeration. |
crypto/ecdh (CS1001), crypto/ed25519 (CS0030), crypto/internal/mlkem768 (CS0315), runtime/debug (CS0264) |
— | not taken past the first diagnostic. |
Runtime — rooted, not fixed
| Package | Root |
|---|---|
html |
map[K][N]T’s missing-key read yields a zero-LENGTH array, not Go’s zero-VALUE array. unescapeEntity does x := entity2[name] over a map[string][2]rune and then tests x[0]; C#’s default(array<rune>) carries m_length == 0, so the read throws IndexOutOfRange where Go sees 0. The class is wider than maps: anywhere the Go zero value of [N]T (or of a struct containing one) is produced by C#’s default(T), it is wrong the same way. The converter already knows how to render the right thing (arrayZeroValueArgs, visitArrayType.go) — it simply is not consulted at a map read. |
internal/platform, crypto/internal/hpke |
Same shape, two packages: json: cannot unmarshal array into Go value of type []T, where T is a converter-lifted anonymous struct ([]platform_test.listEntry, []hpke.TestRFC9180Vectors_vectors). A JSON array of arrays/objects decoded into a slice of a lifted type — worth one look, since two independent packages reach it. |
net/http/internal |
TestChunkReaderAllocs — an exact allocation-count assert, the established alloc-count-semantics class. Would be a disclosure candidate only after re-deriving the measurement; the rest of the package matched. |
go/ast |
ast.Fprint → reflect.MapKeys → mapType.get_MapType() fails an interface conversion inside go2cs-gen’s promoted-field accessor. Reflection-bridge territory — that chip’s, not a breadth lane’s. |
go/parser |
performance_test.cs’s package initializer reads a testdata file at cctor time and panics, taking every test in the internal variant with it — the -tests init-relocation shape internal/fmtsort already needed a rule for. |
expvar |
Type-initializer failure inside a generated ᴛRegisterAdapter for ΔStringжVar; first divergent verdict TestAppendJSONQuote. |
internal/cpu |
getGOAMD64level is an unimplemented PartialStubGenerator stub; every GODEBUG-driven feature-mask row reaches it. |
testing/slogtest |
✅ BANKED 2026-08-07 (r44b-slog) — 17/17, no disclosures. Both log/slog roots below are closed; see testing/slogtest banks at the end of this document. runtime.Caller → the getcallersp stub, reached from a package initializer, so the whole package infrastructure-errors. Same getcallersp row the reflection arc carries.TestRun 7 of 18 subtests pass. Two log/slog roots stand behind it, neither a slogtest defect: (1) unsafe.SliceData over a reference-bearing element type — slog.GroupValue’s groupptr(unsafe.SliceData(as)) on []Attr reaches slice<T>.buffer → PinnedBuffer → GCHandle.Alloc(…, Pinned), which throws ArgumentException: Object contains references (5 infrastructure-errors: groups, empty-group, inline-group, resolve-groups, resolve-WithAttrs-groups); (2) a WithAttrs attribute-loss (4 fails: WithAttrs, multi-With, empty-group-record, resolve-WithAttrs — all “missing key”), whose likely shape is Value.Kind()/isEmptyGroup misclassifying a non-group value so commonHandler.withAttrs’s countEmptyGroups(as) == len(as) early-return drops the attrs. Both belong to a log/slog operational arc, which is unmeasured (log/slog is on neither the roster nor this board). |
internal/unsafeheader |
TestTypeMatchesReflectType / TestWriteThroughHeader: the converted unsafeheader.Slice/String do not alias the same storage a slice<T> does, so a write through the header is invisible. Structural — a managed slice is not a {Data,Len,Cap} triple. |
io/ioutil |
TestReadDir reads .. and expects the sibling package’s io_test.go. The pipeline stages Go sources only for the package under test, so the parent directory holds none. Environment, not conversion. |
internal/singleflight |
✅ BANKED 2026-08-14 (SCHED-S2) — 5/5, no disclosures. TestDoAndForgetUnsharedRace never returns and the package hits the deadline. |
crypto/cipher (TestGCMAsm), internal/godebugs (TestAll) |
one row each, both Go="pass" C#="skip" — a build-tag/capability gate the C# side answers differently. |
crypto/elliptic (TestInfinity/P224/Params), crypto/internal/edwards25519/field (TestBytesBigEquivalence), crypto/internal/boring/bcache (init in cache_test.cs), internal/chacha8rand (TestBlockGeneric), internal/profile (TestPackedEncoding encodes empty), encoding/asn1 (TestCertificate), go/doc (Test/default/a), net/mail (TestAddressParser), net/http/httptrace (TestCompose), mime/multipart (TestLineContinuation) |
first divergent verdict recorded; not root-attributed. |
⚠ One trap this pass hit, worth writing down: a corrupted GO BUILD CACHE reads exactly like a
package failure. A host reboot mid-run left twelve zero-filled entries in %LOCALAPPDATA%\go-build,
and crypto/internal/alias then failed with could not import crypto/internal/alias (EOF) — reported
by the pipeline as FAIL … [build failed] on the Go side, i.e. the oracle itself. The tell is
that the same go test passes from a different working directory. go clean -cache is the blunt fix
and is machine-global (bad while siblings are running); the surgical one is to delete only cache files
whose first bytes are zero, which is a cache MISS rather than a corruption and is safe concurrently.
The same reboot zero-filled 566 files under src/core/**/{bin,obj} — those read as build failures too.
The gate — 96 of 96, and what the aftermath said
The bank’s gate is the full validated sweep at the NEW roster, and it ran clean: 96 packages, 96
matching at their exact banked counts, zero COUNT mismatches and zero failures. (81 through
run-validated-sweep.ps1, which was killed externally at path/filepath — the machine-global
kill signature §9 warns about, not a verdict — and the remaining 15 driven straight through the
pipeline and cross-checked against the table’s counts by hand.)
Two things in the aftermath are worth recording because neither is drift and both will recur.
src/core/time/package_init.cs was a standing restore that no list named. The time bank
recorded it in prose (“no committed package_init.cs in the corpus carries the hook, and time’s
implements nothing”) but never added it to run-validated-sweep.ps1’s $closureFiles, so every
sweep since has reported it under CONTENT drift — inspect before banking or restoring. It is now
listed, alongside the four this arc’s own banks contribute.
Twelve banked TEST sources are stale against the current converter, and it is pre-existing.
bytes/reader_test.cs, compress/flate/deflate_test.cs, context/benchmark_test.cs,
strings/reader_test.cs, sync/{cond,map,mutex,rwmutex,waitgroup,example}_test.cs,
time/{sleep,time}_test.cs all re-emit differently — almost entirely the capture suffix
renumbering (ʗ2 → ʗ1) that a later converter arc introduced, plus one comment-emission
difference in sync/example_test.cs. This lane changed no converter, golib or generator source
(git diff master..HEAD -- src/go2cs src/core/golib src/gen is empty), so the staleness is master’s:
those packages were banked before the change and their test sources were never refreshed. Restored
here rather than banked — refreshing another package’s test sources is a rebank’s job, not a
breadth lane’s — and owed to the next one, alongside the .cs.auto review siblings (CleanupBacklog
item 18), eight of which drift the same way.
⚠ One more environmental trap, alongside the build-cache one above: a full sweep at 96 packages can
FILL THE DISK. Each package’s test bin holds a copy of its whole closure, so a cold sweep writes
tens of gigabytes; this one exhausted C: mid-run with sibling lanes also building. The failure is
loud but misleading — the converter reports failed to write to output source file … There is not
enough space on the disk for crypto/sha1/sha1.cs and friends, i.e. it truncates TRACKED corpus
files, which then read as corpus corruption. git checkout -- src/core restores all of it, but the
lesson is to check free space before a full sweep and to prune bin/obj between chunks on a
contended box.
The fourth and fifth closure edges — CLOSED; go/scanner banks 11/11, log has two roots behind it (2026-08-07, r43f-closure-edge)
The r43c rooting above named log and go/scanner “the cheapest two banks left on this list.”
One of them was. Both build blockers are fixed by two new edges on declarationClosureImports —
the same family the 2026-07-27 arc closed for interface bases and struct fields and r38 extended to
member-access receivers — but only go/scanner banks. Full technical account, with both edges’
gates and their guards, in
ConversionStrategies-Reference.md, The fourth and fifth
closure edges.
Edge 4 — a ZERO-VALUE DECLARATION is a constructor call. r43c read log’s blocker as
log.Logger{} in the external half. It is var l Logger in the INTERNAL (white-box) half, and the
difference is the whole point: there is no composite literal in the package’s test sources at all,
so no *ast.CompositeLit walk could ever have found it. The converter renders Go’s zero value of a
struct as a constructor call (ref var l = ref heap(new Logger(), out var Ꮡl) when the address
is taken, new Logger() otherwise), C# overload resolution materializes every accessible
constructor’s signature before choosing one, and the white-box InternalsVisibleTo grant makes the
internal fieldwise overload accessible — CS0012 … 'atomic_package.Pointer<>' … 'sync.atomic'.
It is the existing root-scoped empty-literal edge’s exact demand by another route, so it feeds the
same seed under the same gate.
Edge 5 — a concrete type’s bases live in its package’s RECORDS, not in its declaration.
[GoType("[]ж<ΔError>")] partial struct ErrorList; names no interface. sort.Interface reaches it
as a VALUE-form [assembly: GoImplement<ErrorList, sort_package.Interface>] record that go2cs-gen
realizes as partial struct ErrorList : global::go.sort_package.Interface inside go.scanner.dll
— so the metadata type declares the base and binding any member on it must resolve it. Thirteen
sites failed: list.Sort(), len(list), Ꮡlist.RemoveMultiples(), and the generated
ErrorList→error value adapter’s own m_value.Equals(…).
⚠ The correction worth carrying: the gate is the RECORDS, not types.Implements. The natural
go/types statement of edge 5 — “the interfaces the receiver’s type implements, from the declaring
package’s imports”, mirroring interfaceBaseCandidates one type-kind over — passes every unit test,
fixes go/scanner, and drifts 16 of the 96 banked projects. A record exists only where the
converter converted a CAST, so Go satisfaction wildly over-approximates the emitted base list:
os.File satisfies syscall.Conn and hands syscall to thirteen projects, though os records
File only against io/fs.File and io.Writer and both in POINTER form, which generates an
adapter CLASS rather than a base and demands nothing of a member binding; bytes.Buffer satisfies
most of io and hands io to sort and unicode/utf8 though bytes emits no records at all;
internal/buildcfg’s Stringer hands it fmt from an equally empty set. All sixteen compile clean
today with none of it. Gating on the package’s own value-form records — keyed per type, because
os’s one genuine syscall record is for rawConn and not File — is zero-drift across all 96.
Two lessons generalize: (1) satisfying an interface in Go is not carrying it as a base in C#, and
the emitted package_info.cs is the authority on which is which; (2) this family’s instrument keeps
earning its keep — it has now rejected six rules that a reading of C#’s binding rules justifies,
and this one was the most convincing of them.
go/scanner — BANKED, 11 of 11, roster 96 → 97 (45.1%), 13,081 matching verdicts. Whole token
and literal matrix, semicolon insertion, //line directives, ErrorList sort + one-per-line dedup,
CR stripping. No production .cs drift and no closure-family restore — the package contributes
nothing to $closureFiles.
log — builds and RUNS for the first time, and does NOT bank. Seven of its nine test functions
agree with go test; two roots stand behind the closure one, neither of them this family’s:
| Test | Verdict | Root |
|---|---|---|
TestAll |
infrastructure-error |
runtime.Caller → runtime.callers → getcallersp, an unimplemented PartialStubGenerator stub. log.output calls runtime.Caller(calldepth) whenever the logger carries Lshortfile/Llongfile, and TestAll sweeps every flag combination. This is the SAME getcallersp row testing/slogtest carries and the reflection arc tracks — not a log defect, and the one root standing between log and a bank. A real runtime.Caller (managed StackTrace, or a hand-owned extern.cs) would likely bank log and slogtest together, and is worth its own arc. |
TestDiscard |
fail |
got 424 allocs, want at most 1 — an exact allocation-count assert, the established alloc-profile class. A legitimate disclosure candidate once TestAll clears; disclosing it alone banks nothing, so nothing was disclosed and no log artifact is committed. |
Both were reachable only after the closure fix, so the edge paid for itself twice over even where it
did not bank: log’s suite had never linked a host and had never been measured.
runtime.Caller lands — and log still does not bank, for a reason worth naming (2026-08-07, r43g-caller)
The row above predicted “a real runtime.Caller … would likely bank log and slogtest together”. The
Caller half was right and cheap; the prediction was wrong, in both packages, and the reasons are
different and both worth carrying.
The fix is one entry, and it is on the FUNNEL. runtime.Caller’s auto body calls the
lower-case callers, not the exported Callers the 2026-07-31 reflection chip hand-owned — and
callers is the declaration that opens with getcallersp(). Four call sites funnel through it
(Caller, mprof, proc.createstack, tracestack), so "callers": true on
manualConversionFuncs["runtime"] fixes all four and leaves Caller itself auto-converted and
Go-shaped. Corpus A/B footprint: one file, src/core/runtime/traceback.cs (the body becomes the
standard placeholder comment). Mechanism, the skip + 1 / skip + 2 frame budgets, the
NoInlining requirement, and the honesty boundary are in
ConversionStrategies-Reference.md, runtime.Callers /
Frames.Next walk the managed stack. Guarded by the RuntimeCallerFrames behavioral test.
log — 7/9 still, and TestAll is now an honest, measured divergence instead of a crash. With
Caller alive, TestAll runs its whole flag matrix and produces real output. It fails on Go
source geometry, which the fix was never going to supply:
| Go asserts | The converted run reports |
|---|---|
^[A-Za-z0-9_\-]+\.go:(63\|65): hello 23 world$ |
C:\…\src\core\log\log_test.cs:69: hello 23 world |
^.*/[A-Za-z0-9_\-]+\.go:(63\|65): … (Llongfile) |
same, with \ separators |
Three separate mismatches in one assert: the .go extension, the / path separator, and the exact
line numbers of the Printf/Println calls inside log_test.go (the test’s own comment says
“must update if the calls to l.Printf / l.Print below move”). Caller reports the converted .cs
position because that is the source the running program has.
⚠ This is deliberately NOT disclosed. The bar for the disclosed-divergence manifest is an
assertion unsatisfiable at any layer go2cs owns (alloc-profile, codegen-liveness). This one is
satisfiable at a layer go2cs owns — a Go-source position map: either #line directives in the
emitted C# (the CLR’s own transpiler mechanism; the PDB would then carry .go files and lines, and
StackFrame.GetFileName/GetFileLineNumber would answer in Go’s terms for free), or a side-car map
per package consulted by internCallerFrame. Both are whole-corpus emission changes with real
trade-offs — #line noise cuts against the readability goal, a side-car adds a file and a csproj
item to every package — so this is an architectural arc to design with the user, not something
to slip in behind a bank. Until it lands, log stays off the roster; disclosing around it would
launder a missing capability as an unsatisfiable assert.
TestDiscard re-derived under the fix: still got 424 allocs, want at most 1, unchanged by this
arc (flag is 0, so Output never reaches Caller). The figure is go2cs’s AllocsPerRun shim
reporting bytes per run, not mallocs; Go allocates once (the []any{s} variadic pack) for
l.Printf("%s", s) over a 102 400-byte string. It remains a legitimate alloc-profile candidate and
remains undisclosed, because disclosing it alone banks nothing — the same call r43f made.
testing/slogtest — initializes and runs for the first time, 7 of 18 subtests pass, two log/slog
roots behind it. Detail in the runtime-roots table above. The lesson is that slogtest is a thin
wrapper over log/slog: banking it is a log/slog operational arc, and log/slog has never been
measured at all. That is the recorded next candidate out of this lane.
testing/slogtest banks, and log/slog gets its first census (2026-08-07, r44b-slog)
Both roots r43g left behind were real, both were converter/runtime defects with corpus-wide reach
beyond slog, and both closed. testing/slogtest went 7 of 18 -> 17/17 matched, no disclosures
in one pass. log/slog was measured for the first time and does not bank, for reasons that are
now named rather than guessed at.
Root 1 - unsafe.SliceData was a PIN where Go means an INTERIOR POINTER
slog.GroupValue stores a group as groupptr(unsafe.SliceData(as)) plus len(as) and rebuilds it
with unsafe.Slice in Value.group(). That is identity and aliasing, never an address - but golib
answered SliceData with a pinned-buffer box over slice.buffer, and GCHandle.Alloc(..., Pinned)
refuses any storage whose element type carries a managed reference. Every grouping path in the
package infrastructure-errored with ArgumentException: Object contains references.
Go DEFINES unsafe.SliceData(s) as &s[:1][0], so the faithful model is the array-element
reference the converter already emits for &s[0]. Pinning was never SliceData’s job: an address
is needed only at a uintptr/void* conversion, and the pointer box pins there on demand
(EnsureStableAddress), declining gracefully for storage that cannot be held still. Two further
latent defects fell out with it: the pin covered the whole backing array from index 0, so
SliceData(s[2:]) addressed the wrong element and failed Go’s == &s[2] identity; and
PinnedBuffer implements IArray<byte> alone, so the derived pointer was undereferenceable for
every element type but byte. A/B footprint: one hand-owned file, src/core/unsafe/unsafe.cs.
Guarded by the new UnsafeSliceDataAliasing behavioral test. Full mechanism in
ConversionStrategies-Reference.md, unsafe.SliceData is
an INTERIOR POINTER, not a pin.
Root 2 - the named-slice pointer reinterpret boxed a COPY, so out-parameters wrote nowhere
r43g’s guess (a Value.Kind()/isEmptyGroup misclassification driving countEmptyGroups) was
wrong, and the real root is upstream of slog entirely. commonHandler.withAttrs writes its
pre-formatted attributes through (*buffer.Buffer)(&h2.preformattedAttrs) - a pointer conversion
from *[]byte to a named-slice pointer, whose whole purpose is that the bytes land in h2’s own
field. The converter emitted a wrapper box over a copy. Its own comment recorded the assumption
- “aliasing with the original is not preserved … but the reinterpret is used through the returned
pointer, which is the pattern” - and that assumption is false for exactly the sites that matter.
WithAttrsdropped every attribute while still advancinggroupPrefix/nOpenGroups, so the JSON that followed was unbalanced: four slogtest rows (WithAttrs,multi-With,empty-group-record,resolve-WithAttrs).
The fix routes the shape through golib’s existing storage reinterpret -
Reinterpret<slice<byte>, buffer.Buffer>() over the field’s own pointer - which re-views the same
slot as the wrapper. A generated named-slice wrapper is a single-field struct over the slice header,
precisely the correspondence ReinterpretAliasesStorage recognizes, so the managed alias arm engages
and writes reach the addressed storage. The reach is wider than slog: crypto/tls’s
readUint{8,16,24}LengthPrefixed and parseECHConfigList, and cryptobyte’s ReadASN1Bytes, all
take (*cryptobyte.String)(out) on an out-PARAMETER or a struct FIELD - every one of them was
silently discarding what it parsed. Corpus A/B footprint: 5 files, 8 sites. Guarded by the
extended NamedSlicePointerReinterpret behavioral test, whose previous version had written the
defect into its own comments as expected behavior and deliberately never read the source back; it
now does, on all four source shapes.
log/slog - first census: 185 pass, 28 fail, 1 crash. It does not bank.
| Class | Rows | Disposition |
|---|---|---|
Go-source geometry - TestCallDepth, TestConnections (+1 sub), TestJSONAndTextHandlers (+3), TestPanics, TestRecordSource |
9 | The SAME class r43g named on log’s TestAll: runtime.Caller honestly reports logger_test.cs:905 where the assert wants ^logger_test\.go:\d+$. Not disclosable - satisfiable at a layer go2cs owns (a Go-source position map: #line directives, or a per-package side-car). It is the architectural arc to design with the user, and it is what actually gates log/slog and log. |
alloc-profile - TestAlloc (+13 subs), TestAnyLevelAlloc, TestAttrNoAlloc, TestTextHandlerAlloc, TestValueNoAlloc |
18 | The established disclosure class. Nothing disclosed - disclosing them alone banks nothing while the geometry class stands, which is the same call r43f and r43g made on log. |
Package initialization ORDER - TestLogLoggerLevelForDefaultHandler (fail), TestSetDefault (the crash) |
1 + crash | A new, general root; see below. |
Root 3 (found, NOT fixed) - Go initializes an imported package before its importer; C# does not
slog’s init captures log/internal.DefaultOutput, which log’s own init installs. Go’s
spec orders that by the import graph, so the capture is always non-nil. A .NET module initializer
fires at first access to its own module, so whichever of log/log/slog is touched first wins:
touch slog first and defaultHandler.output is captured nil, and the next slog.Info is an
unrecovered nil-pointer panic that kills the process. In the census it aborted the run at
TestSetDefault and hid 34 further rows (re-measured separately: 32 of them pass).
Reduced to a 12-line standalone program that go run handles and the transpiled build crashes on:
func main() {
slog.Info("hello from slog") // touch slog BEFORE anything in log
var buf bytes.Buffer
log.SetOutput(&buf)
slog.Info("second")
fmt.Printf("log buffer: %q\n", buf.String())
}
The mechanism to fix it already exists and is already documented - golib’s
builtin.initPackage(Type) (RuntimeHelpers.RunModuleConstructor), which the converter emits today
for blank imports. Making every package force its DIRECT imports at module-init time reproduces
Go’s ordering exactly and transitively (the import graph is a DAG, so direct-imports-only composes to
the full closure in post-order). That is precisely the extension
ConversionStrategies-Reference.md records as “deliberately
deferred, not overlooked”: it trades eager loading of the whole transitive assembly closure at module
init for fidelity. log/slog is the first case that NEEDS it, and it is a whole-corpus emission
change with a real startup trade-off - so it is left as an architectural arc to design with the
user, alongside the position map, rather than slipped in behind a bank. It buys 2 log/slog rows
on its own and would not bank the package.
Aftermath noticed in passing: 24 README validation badges are stale
A full seeded -stdlib reconvert on this lane’s converter differed from the committed tree in
28 files: 3 are this lane’s fix (above), 1 is testing/slogtest’s own new badge, and the other
24 are src/core/<pkg>/README.md badges still reading not_yet_validated for packages that
validated in 47ec27319 (“bank 23 packages from a measure-first breadth pass”). That bank wrote the
proof pages under docs/validation/current/ but never overlaid the READMEs the converter composes
from them, so those 24 packages currently under-report themselves on nuget.org. Deterministic
converter output, zero risk to refresh - left for an idle-point overlay rather than folded into this
lane’s commits. Affected: crypto, crypto/aes, crypto/des, crypto/rc4, crypto/internal/alias,
crypto/internal/bigmod, go/constant, go/doc/comment, go/format, go/printer, go/scanner,
hash, image, image/color, internal/buildcfg, internal/coverage/cformat,
internal/coverage/cmerge, internal/coverage/pods, internal/dag, internal/diff,
mime/quotedprintable, net/url, testing/iotest, text/template/parse.
Resolved, and the CAUSE is standing (2026-08-08, r45b). Those 24 were leveled by a later regen,
and r45b’s Docs-badge overlay leveled the next batch — 12 of r44a/r44b’s banks, plus
internal/concurrent’s label, which had frozen on the pre-2026-08-03 Go_tests spelling because the
package is hand-owned by consequence. The mechanism that creates them has not changed: a bank writes
docs/validation/current/<dot-id>.md and never re-emits the README the converter composes from it,
so every bank leaves its own badge stale until the next corpus README overlay. Treat a handful of
stale Tests badges as the EXPECTED state between overlays, not as a finding — and level them whenever
a lane is regenerating the corpus anyway.
Recorded next candidates out of this lane. Two architectural arcs, both now with named
beneficiaries: the Go-source position map (unblocks log and log/slog, ~9 rows across the
two) and import-ordered package initialization (correctness, not just verdicts - any converted
program that touches log/slog before log crashes today).
The r44a re-scout — r43c’s own lesson executed; 12 bank, and the tail’s roots are named (2026-08-07)
r43c ended with an instruction rather than a finding: re-scout the tail after ANY capability lands,
not just the packages that capability was aimed at. Five capabilities landed between it and this
pass — managed weak references (internal/weak), per-field [GoType] struct equality,
runtime.Caller over a managed frame walk, range-over-every-integer-kind, and the abi.Type
StructType/ArrayType specializations — plus the linkname PUSH direction. This pass ran the pipeline
over BOTH halves of the tail that instruction names:
- (a) the 32 still-unbanked packages r43c rooted — its 34 minus
go/scanner(banked at r43f) and minuslog, whose position-map root is a board-documented architectural arc; and - (b) 76 never-measured tail packages — everything unbanked and testable that is not a
board-documented deep wall (
net,unique,os) and not sibling-owned (log/slogand its subpackages, andtesting/slogtest, which is alog/slogarc).
108 pipeline runs, -test-action all -test-timeout 4m, serial, on a corpus prewarmed by one
go2cs-stdlib.slnx build (304/304, 0 errors, 113 s), plus a 12-package re-run (below).
Roster 97 → 109 (45.1% → 50.7%), 13,081 → 13,611 matching verdicts, 50 disclosed (unchanged).
The re-scout of r43c’s own roots yielded exactly one package
expvar — r43c’s “type-initializer failure inside a generated ᴛRegisterAdapter for ΔStringжVar”
— now validates 11 of 11, with no change of any kind in this lane. Every other package on
r43c’s list re-measured verbatim, down to the error code: the eight build blockers are unmoved and
the runtime roots reproduce their recorded shape.
That is a result, not a null: the re-scout instruction is right, and its yield on an already-rooted list is small. A rooted non-validator has been looked at. The yield is in the packages nobody has run — eleven of the twelve banks came from there.
The twelve
crypto/internal/boring · crypto/rand (298) · database/sql/driver · debug/buildinfo (197) ·
debug/plan9obj · expvar · go/importer · internal/cpu · internal/sysinfo ·
os/exec/internal/fdtest · plugin · runtime/internal/sys
Eleven needed nothing at all. internal/cpu is the lane’s ONE fix, below. crypto/rand (298) and
debug/buildinfo (197) carry the volume; os/exec/internal/fdtest’s single verdict is a
platform-gated skip on both sides — the converted run reaches Go’s own runtime.GOOS guard and
declines exactly where Go does, which the proof page states plainly rather than dressing up.
The one fix — internal/cpu.getGOAMD64level, and why 1 is a measurement
TestDisableSSE3 opens if GetGOAMD64level() > 1 { t.Skip(…) }. Go reads 1 and walks on to a skip
inside runDebugOptionsTest; the converted run hit an unimplemented PartialStubGenerator stub and
infrastructure-errored, and that one row was the whole gap (7 of 8).
getGOAMD64level is declared in cpu_x86.s and its body is a compile-time constant — the
GOAMD64_vN define the toolchain sets from go env GOAMD64, with #else MOVL $1 as the
fall-through. It answers which microarchitecture level was this BINARY built for, never which
does this CPU support; a v3 machine running a v1 build still reports 1, which is exactly why
doinit keeps the sse3/avx/avx512 GODEBUG knobs switchable at level 1. go2cs emits portable C# with
no GOAMD64 define and no microarchitecture-gated emission, so 1 is the same constant Go’s own
assembly produces for go2cs’s build configuration — a measured property of the emission, not a
placeholder, and probing the CPU here would answer a different question. Registered in
manualConversionFuncs["internal/cpu"] with the body in cpu_x86_impl.cs. A/B footprint: one
corpus file (cpu_x86.cs’s declaration becomes the standard placeholder comment) plus the
hand-own. Marker census +1.
ONE ROW AWAY — the list this pass most wants read
Eighteen packages match every verdict but one or two. Each cell is the whole gap.
| Package | Census | The row, and its root |
|---|---|---|
runtime/internal/math |
BANKED (roster line 143) — re-measured 1/1 by r57b | |
internal/platform |
0 of 1 | json: cannot unmarshal array into a slice of a converter-LIFTED anonymous struct (crypto/internal/hpke is the same shape). ⚠ r57b’s naming arm changed the TEXT to Go’s structural []struct { GOOS string; … }; the row is a Kind question about the lift, not a naming one |
internal/profile |
BANKED (roster line 117) — re-measured 1/1 by r57b | |
internal/godebugs |
0 of 1 | TestAll reads GOROOT-relative ../../../doc/godebug.md; the pipeline’s working dir has none |
html |
2 of 3 | the array<T> unshaped-instance class, producer (1) |
internal/chacha8rand |
3 of 4 | the same class, producer (2) |
internal/singleflight |
BANKED 5/5 by the scheduler arc — the hang was the ThreadPool executor, not the package | |
internal/cpu |
BANKED this arc | |
go/ast |
BANKED by r57b at 9/9 — two roots: the unbridged map read pair, then the lift’s leaked C# name | |
debug/gosym |
8 of 9 | TestPCLine’s child process exits 1 |
debug/pe |
9 of 10 | the array<T> unshaped class — _ [3]uint8 prints [0 0 0 0 0 0 0 0] vs Go’s [0 0 0] (r57b) |
net/http/internal |
9 of 10 | TestChunkReaderAllocs — re-measured r58a as 2 objects/run against Go’s budget of 1, a lower bound; ruling pending, below |
net/http/fcgi |
BANKED (roster line 133) — re-measured 12/12 by r57b; the TestGetValues mismatch is gone |
|
crypto/cipher |
13 of 14 | the oracle’s build tags, below |
crypto/internal/edwards25519/field |
13 of 16 | the array<T> class, producer (3) |
internal/poll |
18 of 19 | runtime_pollServerInit — the netpoller has no managed body |
net/textproto |
BANKED at 26/26 by L11 (2026-08-12) — the want-ZERO row closed honestly: the 3 objects/816 B per canonicalMIMEHeaderKey call decomposed into Once.Do’s two ж field-box mints (fixed: atomic scalar ops → [GoRecv] ref form, once.cs hand-owned), the m[string(b)] probe-key copy (fixed: the converter now emits golib’s zero-alloc tmpstring transient for map-READ keys, Go’s own compiler special case), and 14 dead per-call BigInteger.Parses from the emitted-but-folded 128-bit mask local (fixed: function-local int-kind GoBigConsts hoist their parse to a static readonly field). No disclosure, no test-shaping |
|
io/ioutil |
27 of 28 | TestReadDir looks in .. for the SIBLING package’s io_test.go; also ORDER-DEPENDENT, since a sweep that ran io first leaves that file staged — a reason not to bank it even when it passes |
net/http/cgi |
36 of 39 | three rows |
syscall |
61 of 62 | the pipeline’s own path depth — below |
syscall — 61 of 62, and the one row is a bank the PIPELINE is costing itself
TestGetwd_DoesNotPanicWhenPathIsLong (Go issue 60051) calls t.TempDir(), then os.Chdir, and
skips itself if the Chdir fails. Go’s run succeeds; the converted run’s Chdir fails and the test
skips, because the C# host’s temp root is
%TEMP%\go2cs-tests\syscall\<32-hex-digest>\syscall\.tmp\<TestName> — already deep before a test
whose entire purpose is to build a path past MAX_PATH adds its own. Shorten the staging root (short
prefix, truncated digest) and 62 verdicts should land. Rooted, not fixed: the staging path feeds the
input-digest manifest, so it is its own change with its own gate.
The array<T> UNSHAPED-INSTANCE class — three producers, three packages, five rows
array<T> carries its Go length N in the INSTANCE — golib’s own IGoZeroShaped says so, because
[4]int32 and [8]int32 are the same C# type. So every path that materializes one from TYPE
information alone must supply N, and three such paths do not:
- a map miss —
html.unescapeEntityreadsentity2[name]overmap[string][2]rune; the miss yieldsdefault(array<rune>), length 0, andx[0]throws (html2 of 3). r43c named this one. - an unsafe reinterpret of an array pointer —
internal/chacha8rand.setupreaches(*[16][4]uint32)(unsafe.Pointer(buf))over a*[32]uint64, and the reinterpretedж<array<array<uint32>>>has length 0 (internal/chacha8rand3 of 4). - the reflection bridge generating a value —
testing/quick→reflect.Callhands a zero-lengtharray<byte>to a function taking[32]byte(crypto/internal/edwards25519/field13 of 16, three rows).
IGoZeroShaped cannot serve any of them: it produces a zero value shaped like a value you ALREADY
have, and none of these three has one. Closing the class banks three packages (23 verdicts); each
producer is a separate fix and (3) is the reflection chip’s.
An untyped constant SHIFT computed in C# int32 — a silent wrong answer
runtime/internal/math’s TestMulUintptr reports MulUintptr(1, 1) = 1, false want 1, true. The
row is Go’s {1 << (UintptrSize / 2), 1 << (UintptrSize / 2), true} with UintptrSize == 64; the
converter emitted (uintptr)(1 << (int)((UintptrSize / 2))), and C# masks an int shift count to
five bits, so 1 << 32 is 1. The NEIGHBOURING table row folds correctly —
1<<(UintptrSize/2) - 1 → (uintptr)(4294967296L - 1) — because the shift is then an INNER node
whose recorded type is UntypedInt and overflowingConstLiteral’s SIGNED arm folds anything out of
int32 range. As the OUTERMOST node the shift carries the CONTEXT’s uintptr, takes the UNSIGNED
arm, and that arm folds only values beyond int64. Its stated reason — “a TYPED unsigned constant
shift emits with a width-cast operand from the retype path” — is true for a shift the Go SOURCE
typed and false for a tree the context typed, which is precisely this case.
Corpus reach, measured: 69 1 << (int)(<symbolic>) sites; the counts are constants and nearly all
are below 32, but runtime/mpagealloc_64bit.cs:234 is (uintptr)(1 << (int)(heapAddrBits)) with
heapAddrBits == 48 — 1 << 16 where Go computes 2⁴⁸. Latent, and the same silent-wrong-answer
shape. Deliberately NOT fixed here: overflowingConstLiteral already documents six carefully-scoped
rules and a wrong widening drifts the corpus silently, so this wants its own arc with an A/B.
Three roots that each hold a whole package
iter— 0 of 28.newcoro/coroswitchare unimplemented stubs.iter.Pull/Pull2are built on Go’s coroutine primitive and every one of the package’s tests goes through them. A self-contained arc of exactly the shapesync’s Mutex family andinternal/weaktook: the observable contract (a resumable producer, withstop, panic propagation andGoexitpropagation) has a managed answer; Go’s mechanism — switching stacks — does not.mime/multipart— 7 of 52. A linkname PULL of an UNEXPORTED cross-package symbol.readmimeheader.gois a bodyless//go:linkname readMIMEHeader net/textproto.readMIMEHeader— the PULL direction r43b never had to touch because it already worked, but only for a target the consumer can NAME.net/textproto.readMIMEHeaderis unexported, so across the assembly boundary it is inaccessible and the declaration falls to the throwing stub. Remedy shape is an accessibility bridge, not a hand-own: the white-box test model already mints anInternalsVisibleTogrant for this exact problem. CLOSED by L12 (2026-08-13): banked at 52/52. This census was RIGHT — 7 of 52 reproduced to the verdict a week later, and the root is exactly the symbol named here. Two corrections worth carrying. (1) The remedy was over-designed: noInternalsVisibleTogrant was needed and none was written. The converter already owns both halves —linknameForwardTargetsemits the forwarder body andpackageFuncAccesswidens a forward target its own package opened with a one-arg//go:linknamehandle frominternaltopublic— so the whole fix is ONE allowlist row, and the corpus moved by exactly two files (multipart’s declaration becomes a forwarder; textproto’sreadMIMEHeaderbecomespublic). The lesson generalizes: before designing an accessibility bridge, check whether the linkname machinery already covers the direction. (2) The differential was ENTIRELY this one symbol — all 41 infrastructure-errors carried the identical exception and the fourfailrows were parent-test shadows — so the 45-row gap closed in a single step with no second root behind it.crypto/internal/nistec— 0 of 2,200, build-blocked on four CS0311s, all the same shape:ж<P224Point>(…P256/P384/P521) rejected as the type argument of the generic BENCHMARK helpersbenchmarkScalarMult<P>/benchmarkScalarBaseMult<P>, whose Go constraint isnistPoint[P]— a self-referential interface constraint over a pointer receiver. Nothing EXECUTES those helpers; they merely have to compile.crypto/ecdsa(82) is blocked in the same family. The largest single prize on this list.
Two findings that are NOT disclosures, and refuse for the same reason
The differential oracle is not built with the corpus’s build tags. crypto/cipher matches on
every row but TestGCMAsm, where Go passes and C# skips with Go’s own message, “no assembly
implementation of GCM” — the test’s first act is reflect.TypeOf(asm) == reflect.TypeOf(generic),
and under the standing purego ruling the converted corpus genuinely has one GCM implementation,
not two. The C# side is right. The oracle is what differs: compareGoAndConvertedTests runs
go test -json -count=1 -timeout … . with no -tags, while every conversion applies
defaultStdLibBuildTags = {purego, math_big_pure_go}. Go under the corpus’s own tags would skip
that row too. It is satisfiable at a layer go2cs owns — one argument on one exec.Command — so
disclosing it would launder a comparison defect as an unsatisfiable assert. But it also changes what
EVERY roster row claims (“passes Go’s tests” → “passes Go’s tests as Go builds them for the pure-Go
configuration”), so it is an arc to design with the user and gate on a full sweep, not something to
slip in behind a bank.
AllocsPerRun reports BYTES, and it now blocks a second package. net/http/internal matches 9
of 10; TestChunkReaderAllocs reports mallocs = 640; want 1. r43g root-caused the same shape in
log’s TestDiscard (got 424 allocs, want at most 1): the shim measures allocated BYTES per run,
not allocation COUNT, because the CLR exposes GC.GetAllocatedBytesForCurrentThread and no object
counter. Two packages now stop here, which is the argument for owning it rather than disclosing
around it — until the shim reports a count, no alloc-profile disclosure at these sites can claim
the CLR provably cannot satisfy the assert, because nobody has measured the number the assert is
about.
r56d settled the units question by measurement, and the shim no longer presents bytes as a count. The survey is recorded on the declaration itself (
testing.cs,AllocsPerRun): net9.0/9.0.18 x64 exposes byte totals ONLY —GetAllocatedBytesForCurrentThreadis exact (40.000 B/object over 1, 10, 1e3, 1e5 allocations of a 40-byte type) but cannot separate count from size,GCAllocationTickis a byte-threshold sample (378 events per 1,000,000 allocations, one per ≈105,820 B),GCSampledObjectAllocation— whoseObjectCountForTypeSampleWOULD be a count — raises zero events through an in-processEventListenerin every configuration tried (High0x200000, Low0x2000000, both, all keywords0xFFFFFFFFFFFF, Verbose and Informational) with the GC keyword’s own tick count as the live positive control,System.Runtime’s 27 EventCounters offer onlyalloc-rate(bytes/interval), and runtime events reach an in-process listener asynchronously (zero visible immediately after the loop, settling ≈117 ms later), so no event-derived figure could serve a synchronous call regardless. A nonzero result now notes its unit once on the running test; the zero case is left untouched because there the two units agree exactly, so no passing row’s output moves (verified: 2,195 passing nistec rows carry no note). The disclosure question is now answerable — but it is still the user’s, and it has a third option, below.
r56d-allocdecomp — nistec’s 21,964,011 decomposes, and 100 % of it is the ж<T> box model
The prize was gated on one number, so the number was decomposed the way r39-osalloc decomposed os’s
9,184. Method: a console probe references the converted crypto/internal/nistec + fiat and
measures GC.GetAllocatedBytesForCurrentThread deltas — the same instrument the shim uses, so the
figures ARE the ones the test sees. Positive control: the probe’s P256 body reads 21,963,547
against the pipeline’s 21,964,011, the 464-byte gap being the rand.Read the probe substitutes.
Temporary counters in golib’s ж/array/slice constructors (reverted; instrumentation is
temporary by construction) supplied exact per-class counts.
Phase decomposition, P224 body (per run) — sums to within 156 B of the whole, the ibyteseq standard:
| Phase | B/run | Share |
|---|---|---|
ScalarMult(p, scalar) |
13,042,167 | 55.2 % |
ScalarBaseMult(scalar) |
5,592,992 | 23.7 % |
SetBytes(compressed) |
4,556,755 | 19.3 % |
Bytes() / BytesCompressed() |
203,226 / 203,194 | 0.9 % each |
NewP224Point().SetBytes(out) |
17,681 | 0.1 % |
NewP224Point().SetGenerator() |
8,344 | 0.0 % |
make([]byte, 28) |
104 | 0.0 % |
| whole body (control) | 23,624,307 | 100 % |
Unit costs close the bill to the BYTE — three classes, and every field-element operation is
exactly (number of field pointers × 128) + (number of address-taken locals × 144):
| Operation | Measured | Closes as |
|---|---|---|
P224Element.Sub |
528 | 3 × 128 + 1 × 144 |
P224Element.Mul |
960 | 3 × 128 + 4 × 144 |
P224Element.Add |
960 | 3 × 128 + 4 × 144 |
P224Element.Square |
832 | 2 × 128 + 4 × 144 |
P224Point.Add |
39,464 | ≈43 field ops + 8 @new boxes |
P224Point.Double |
31,552 | same shape |
Allocation COUNTS per run (golib counters; Go’s count for all four is zero):
| Curve | standard ж boxes |
of which pinnable T[1] |
field-ref ж boxes |
array<T> backings |
total objects | bytes |
|---|---|---|---|---|---|---|
| P224 | 106,472 | 86,930 | 66,081 | 3,373 | 263,049 | 23,624,307 |
| P256 | 97,389 | 76,513 | 63,786 | 3,386 | 241,077 | 21,963,547 |
| P384 | 200,133 | 168,947 | 94,993 | 4,992 | 469,068 | 40,754,499 |
| P521 | 386,667 | 343,898 | 129,963 | 6,783 | 867,314 | 72,242,788 |
Ownership, per class — none of it is established-class waste, and that is the finding:
- field-ref boxes, 128 B (
of(…), i.e. Go’s&e.x) — a freshж<array<uint64>>per call. Go’s&e.xis free and yields the same pointer every time, so memoizing the box per (source, accessor) is semantically faithful — but it is r39 item 1’s territory and changes pinning lifetime, so it is chip-class, design-WITH-user, not a lane fix. - address-taken locals, 144 B (
heap(new uint64(), out var Ꮡx)) — Go’svar x uint64; &xhanded top224CmovznzU64, a stack variable there. 144 B = theжbox plus theT[1]pinnable slot its constructor allocates eagerly for an unmanagedT. Removing the eager slot needs the box pinned by handle instead — again theж<T>architecture. @new<T>()boxes, 128 B — Go’s comment inScalarMultsays it outright: “The explicit NewP224Point calls get inlined, letting the allocations live on the stack.” The managed model has no inlining that turns a heap box into a frame slot.array<T>backings, 88 B — Go’s[4]uint64is inline in the struct; golib’sarray<T>is a struct wrapping a heapT[].
The r39-killed classes did NOT reappear — the hot path has zero dead unsafe.Pointer temps,
zero GoFunc/defer frames and zero capture boxes (the only closures are one-time sync.Once
initializers, outside the measured window). Checked explicitly, because a regression there would
have been a lane fix.
So nistec does NOT bank, and the reason is honest: five want-zero rows fail on a real
divergence, ruling #1 stands (a want-zero assert is satisfiable in principle, so it is not a
disclosure), and no established class remains to fix. Roster unchanged at 110/215. The 2,200
verdicts are gated on the ж<T> box arc — the same arc os’s residual named — which makes that
arc’s value 2,200 verdicts larger than it looked.
The third option for the disclosure decision. A true allocation COUNT is obtainable — not from
the CLR, but from go2cs’s own runtime. golib allocates essentially every Go-semantic object, so
counting there mirrors precisely what Go’s Mallocs already is: a runtime-owned counter, not a
platform facility. r56d proved it works (the count column above IS that instrument). It was
deliberately not landed: a count that silently omits allocation sites is worse than an honest byte
figure — the inverse-of-atomic rule — so making golib the counter requires an audited-total census
of its allocation sites and a ruling on what counts as an allocation. Design-with-user.
log and net/http/internal are a DIFFERENT case from nistec — and the difference decides them
Both remaining AllocsPerRun blockers assert a nonzero budget of exactly 1, where nistec asserts
zero. Measured through the shim itself (a temporary object-count readout, since these closures resist
a standalone probe — the pointer-to-interface conversions go2cs-gen mints are scoped to the declaring
assembly, so a hand-written probe cannot obtain them):
| Row | reported | true B/run | golib-tracked objects/run | Go’s budget |
|---|---|---|---|---|
log TestDiscard |
got 424 allocs, want at most 1 |
424 | ≥ 2 | 1 |
net/http/internal TestChunkReaderAllocs |
mallocs = 640; want 1 |
640 | ≥ 2 | 1 |
crypto/internal/nistec TestAllocations/P256 |
got 21964011.0 |
21,964,011 | 241,077 | 0 |
The top two are the same order of magnitude as Go — single-digit objects against a budget of one — so their failures are dominated by the unit mismatch, not by over-allocation. nistec is five orders away. That is the line the disclosure decision should follow.
⚠ But they are still not disclosable today, and the reason is a result this lane produced against
itself. The counter used above covers ж/array/slice only, and 424 bytes cannot be two objects
of ~50 B each — so allocations exist on that path which the instrument did not see (@string,
object[] varargs, delegates, boxing). The partial counter demonstrating its own incompleteness is
the concrete evidence for the caveat above: a golib-derived count is the right mechanism and is NOT
trustworthy until its census of allocation sites is audited-total. Until then no site can claim the
CLR provably cannot satisfy the assert, because the number the assert is about is still not known
exactly — which is precisely the standard r43g set. The lower bound is nonetheless decision-relevant,
and it points the opposite way from nistec.
Build roots found in the never-measured tail
| Package | Verdicts | First diagnostic |
|---|---|---|
crypto/tls |
3,519 | CS0234 'vendor' does not exist — the test half’s vendored import |
crypto/internal/nistec |
2,200 | CS0311, above |
runtime |
870 | build-blocked |
go/types |
557 | CS0839 Argument missing |
encoding/json |
491 | claude/json-measure then claude/json-unlock: 76 → 8 → 0). The package RUNS: 400 of 491 matching, 91 divergent in four named roots — see encoding/json — the wall is down and the package RUNS at the end of this file |
encoding/xml |
386 | CS0426 ΔToken does not exist in xml_package |
crypto/x509 |
335 | CS0102 duplicate definition in x509_package |
net/netip |
266 | CS1525 Invalid expression term '<' |
net/http |
245 | CS1002 ; expected |
html/template |
243 | CS0030 on a test-local named type |
sync/atomic |
108 | CS0103 ᏑᏑX — a DOUBLE address-prefix marker |
runtime/pprof |
— | CS0103 ᏑᏑsalts — the same double-Ꮡ root |
crypto/ecdsa |
82 | the nistec family |
fmt |
63 | fmt_test_package.SE already defines Append14bf20010; claude/fmt-roots): the host builds and 62 of 63 verdicts agree. One verdict (TestSprintf) remains, behind three reflect/fmt-bridge divergences — see fmt’s compile wall is CLOSED at the end of this file |
text/template |
52 | CS0030 on a test-local named type |
debug/elf |
31 | CS8183 cannot infer the type of an implicitly-typed discard |
internal/reflectlite |
30 | CS0016 could not write to output file |
database/sql |
25 | CS0029 |
flag |
24 | CS1929 on ж<flag_test_package.URLValue> |
os/exec |
22 | CS0103 The name 'var' does not exist |
internal/concurrent |
20 | CS0426 node<,> — the hand-owned hashtriemap.cs does not declare the internal type its WHITE-BOX test half references |
internal/runtime/atomic, internal/syscall/windows/registry, net/rpc/jsonrpc, go/internal/srcimporter, testing/fstest, internal/types/errors |
— | build-blocked, first diagnostic recorded |
And one hard CONVERTER failure in 108 packages: reflect. go2cs.exe: Failed to convert package
tests in "…\src\reflect": convert test file "…\reflect\all_test.go": 1e+06 not an Int — a
float-shaped untyped constant reaching a path that demands constant.Int. Every other package in
the batch CONVERTED; only the C# build or the run failed. This one has a one-line repro.
Re-baselines this pass owes the board
encoding/gob: 98 → 99 of 106.TestNetIPnow passes (theinternal/weakhand-own letnet/netip’s initializer complete and the value render correctly). The seven remaining failures are the same gob-internal set.- ⚠ The first two rows below are SUPERSEDED — re-measured 2026-08-09 (r57a) after the r56f shift
fix:
crypto/ellipticis 82 of 82 and BANKED,math/bigis 222 of 226. Both were censused with the named-numeric shift-masking defect live, so they measured the defect rather than the package. Treat every census on this list as carrying a timestamp against the corpus it was taken on. ,crypto/elliptic4 of 82,math/big9 of 226go/doc24 of 85,go/parser6 of 173,mime/multipart7 of 52,(re-measured 34 of 38 by r57b, below),encoding/asn128 of 38net/rpc6 of 15,net/http/httputil16 of 53,net/http/httptest24 of 55,net/http/cookiejar10 of 17,debug/dwarf7 of 40,internal/coverage/cfile4 of 16,go/internal/gcimporter399 of 583 — first censuses, all recorded here rather than in prose. ⚠ The last two were re-measured 2026-08-11 (L9, laptop G) and both reproduce EXACTLY — walled, not stale, with their roots attributed in the L9 laptop-G section below.net/internal/socktest,internal/syscall/unix,log/syslog,runtime/racehave no eligibleTestdeclarations on windows/amd64 — they are in the naive 215 denominator but cannot bank on this target.os/usercannot bank at all: Go’s ownTestGroupIdsFAILS in the oracle.
⚠ Two self-inflicted traps, both worth the next lane’s attention
- The corpus is an INPUT to a running batch. Staging the
cpu_x86_impl.cshand-own while the batch was still running made six unrelated packages reportCS0111: Type 'cpu_package' already defines a member called 'getGOAMD64level'— the impl implements a partial the CURRENTLY-BUILT converter still emits, and the error is reported against the CONSUMER package, not againstinternal/cpu. Never stage a converter-paired corpus file until the batch is idle and the converter is rebuilt. - Clean the batch’s untracked artifacts between passes. The re-run then failed wholesale with
NuGet.targets(1311,5): error MSB4006: circular dependency … "_GenerateRestoreProjectPathWalk"againstinternal.syscall.windows.csproj: a package whose run FAILED still leaves a generated<pkg>.tests.csprojon disk, andinternal/syscall/windows’s test half importsinternal/syscall/windows/registry, which importsinternal/syscall/windows— a cycle NuGet’s restore path walk rejects even though the C# compile would be fine.git addthe banks, thengit clean -fd -- src/core, before re-running anything.
The one-row-away cluster, worked — 3 bank, and syscall’s root is not the one on record (2026-08-09, r56c-onerow)
Worked the ONE ROW AWAY list above in its own order. Three banked (internal/profile 1,
net/http/fcgi 12, runtime/internal/math 1); roster 110 → 113 (51.2% → 52.6%), 13,628 →
13,642 matching verdicts, 50 disclosed (unchanged). Every bank came from a converter or generator
defect that was producing a SILENT wrong answer — none needed a disclosure, and none was a
test-targeted patch.
The three roots
-
A Go package that spans two assemblies lost its unexported interface methods.
internal/profile’sproto_test.goispackage profile— an internal white-box test — and it implements the production package’s unexportedmessageinterface on its ownpackedInts.ImplementGeneratoremitted the adapter’s members as=> default!/{ }: a required member satisfied by a NO-OP.marshal()returned an empty buffer,unmarshaldecoded nothing, and nothing at any layer said so. The stub is a real mechanism (Go’s package-sealing markers —ast.Expr.exprNode()), but its test wasunexported name && declaring assembly != this assembly, a proxy for “there is nothing to forward to” that answers wrongly for the one shape where a single Go package spans two C# assemblies. It now also requires the struct to declare no method of that name in the current compilation. This class is corpus-wide: any white-box test package whose test-local type implements a production unexported interface was silently no-op before this, and the failure mode is invisible — it compiles and it runs. -
C#’s
\xescape is greedy where Go’s is exactly two digits.net/http/fcgi’sconst want = "\x0f\x01" + "FCGI_MPXS_CONNS1" + …folds to one constant with no singleBasicLit, so it bypassedconvBasicLit’s byte-array diversion and the folded arm asked onlyutf8.ValidString. The value is pure ASCII, so that test passed it — and\x01Fre-parsed as U+001F with theFeaten.TestGetValuescompared a correct response against its own corrupted constant. The folded arm now runs the same predicateconvBasicLitdoes. Measured reach: one live site — every other\x-plus-hex-digit run in the emitted corpus is inside a C# verbatim@"…"literal, where\xis two ordinary characters. -
uintptrwas missing fromisWideShiftType. Go’suintrenders as the C# primitivenuint, but Go’suintptrrenders as golib’suintptrSTRUCT — so it was the one wide unsigned type that fell to the narrow arm and got its shift cast on the RESULT, which is exactly what that arm’s own comment says does not help.1 << (4 * goarch.PtrSize)emitted(uintptr)(1 << (int)(32)), C# masked the count to five bits, and the value was 1. Whole-corpus A/B: eight files, one mechanical family, six sub-int32 reshapes and two live wrong answers —MulUintptr’s overflow fast path (guarding at 1, so everyuintptrbelowMaxUint32“overflowed”) andruntime/mpagealloc_64bit.go’s1 << heapAddrBits(2^16 where Go computes 2^48, the latent site this board already recorded). Both banked rather than deferred.
⚠ syscall 61/62 — the recorded root is WRONG, and the recorded remedy cannot work
This board says the row is “the pipeline’s own path depth … shorten the staging root and 62 verdicts should land.” Both halves are false, and the correction matters because the real remedy is cheap and sits in another lane’s file.
TestGetwd_DoesNotPanicWhenPathIsLong skips on Chdir failed: … The filename or extension is too
long. MkdirAll SUCCEEDS — only Chdir fails. The arithmetic refutes the depth story on its own:
the test appends two 200-character segments, so it contributes 401 characters whatever the root
is. The converted run’s path is ~551; Go’s own is ~488. No staging root gets the total under
MAX_PATH (260) — Go is not passing because its path is shorter, it is passing at 488 characters,
which is already 1.9x the limit.
Probed directly — same 446-character path, same machine:
| binary | SetCurrentDirectoryW(plain) |
|---|---|
Go (os.Chdir) |
succeeds |
.NET (dotnet run, no manifest) |
fails, error 206 (ERROR_FILENAME_EXCED_RANGE) |
.NET, <ApplicationManifest> carrying <ws2:longPathAware>true</ws2:longPathAware> |
succeeds |
The root is that converted Windows binaries are not long-path aware and every Go Windows binary
is. MkdirAll worked because Go’s fixLongPath prefixes \\?\ explicitly; Chdir hands
SetCurrentDirectoryW a plain path, and without the opt-in the process is held to MAX_PATH.
(\\?\ is no escape hatch here: SetCurrentDirectory rejects the extended form outright — it fails
206 too.)
CLOSED 2026-08-09 (r56e) — syscall banks at 62/62. The diagnosis above held; the MECHANISM
attributed to Go did not, and the correction changed the remedy. Go’s linker bakes in no manifest.
runtime/os_windows.go’s initLongPathSupport(), called from osinit(), checks for Windows
10.0.15063 and then sets the undocumented IsLongPathAwareProcess bit in the PEB’s bit field
itself — which is why every Go Windows binary is long-path aware.
That distinction is not academic, because the two routes are not equivalent: Windows honors a
manifest’s longPathAware only when the machine-wide policy
HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled is ALSO 1. It is 1 on this
machine — which is exactly why the manifest measured as a fix in the row above — so a manifested
converted binary would still have diverged from the Go binary on a default install, where that value
is 0. Go asks for neither the manifest nor the policy.
So the remedy landed in golib, not the csproj template: builtin.WindowsLongPaths.cs sets the
same PEB bit from InitializeGoLib, golib’s analogue of osinit. Probed both ways in one process —
without golib the PEB reads 0x04 and a 434-character Directory.SetCurrentDirectory fails
0x800700CE; referencing golib it reads 0x84 before the probe’s own code runs and the same call
succeeds. It is also the far smaller footprint: no <ApplicationManifest> property, no per-project
manifest artifact, nothing in the emitted .csproj — so CNR stayed byte-identical across all 576
behavioral packages including their .csproj, and none of the banked <pkg>.tests.csproj went
stale. internal/syscall/windows.CanUseLongPaths is deliberately left false (golib cannot reference
a converted package, and the \\?\ spelling still works with the bit set).
Rooted, not fixed — carried back with evidence
debug/pe9/10 — a byte-level struct pun across surrogate layouts.COFFSymbolAuxFormat5prints_:[0 0 0 0 0 0 0 0]where Go prints_:[0 0 0]. Go reinterprets aCOFFSymbolas the aux record ((*COFFSymbolAuxFormat5)(unsafe.Pointer(&sym))); the two have identical GO layouts but no field correspondence at all (Name [8]uint8vsSize uint32 + NumRelocs uint16 + …). golib’s alias route correctly REFUSES this (6 fields vs 7, not layout-compatible), so it falls to the raw-address route — which reads the aux struct’sarray<uint8>field out of the bytes whereCOFFSymbol.Name’s reference sits: a fabricated managed reference that happens to be type-compatible, so it aliasesName’s 8-element array instead of a fresh 3-element one. The scalars round-trip only because the same wrong mapping is used in both directions. A correct answer needs a Go-LAYOUT marshalling view for the pun, not a shape patch; that is an arc, and the fallback’s “never something newly wrong” claim inж.PointerExtensions.csdeserves revisiting with it — here it fabricates a reference, which is the very thing the alias route refuses to do.- A GOROOT-tree-reproduction class: four packages, one question.
go testruns a package’s tests with cwd = the package’s GOROOT source directory; the converted host runs in the staged copy, and the staging deliberately bounds itself to paths carrying atestdatasegment. Sodebug/gosym8/9 (TestPCLinerunsgo buildintestdataand dies ongo.mod file not found— GOROOT/src has one, the staged tree does not),internal/godebugs0/1 (../../../doc/godebug.md),internal/platform0/1 (readszosarch.gofrom cwd, behind its own JSON root), andio/ioutil27/28 (lists..for a sibling package’s file) are ONE question: how much of the GOROOT tree around a package should the run reproduce? Pointing the host’s cwd at the real GOROOT package directory answers all four and makes both sides see literally the same tree — and would also makeio/ioutilorder-INdependent, retiring the reason this board gives for not banking it. But it lets a test write into GOROOT and it trades away the staged copy’s reproducibility, so it is a pipeline design decision, not a defect fix. net/http/cgi36/39 is TWO roots, not three rows.TestCopyErrorinfrastructure-errors onGetSystemDirectory: external (assembly or cgo) function is not implemented— a//go:linknamePUSH fromruntimethat is not inlinknamePushTargets, and it throws out ofnet_package’s type initializer, so everyhttptestconsumer dies innet’s cctor. Its pushed body readsruntime.sysDirectory, whichinitSysDirectoryfills viastdcall2— nothing the managed model runs — so a bare forwarder would hand back"": a plausible-looking wrong answer, which is exactly what the registry’s own rule forbids. The honorable shape is the oneos.runtime_argsalready took: a hand-owned module initializer populatingsysDirectoryfromEnvironment.GetFolderPath(SpecialFolder.System), with the registry row landing WITH it rather than before it.TestDir/TestEnvOverrideare the staging-cwd class above — the re-exec’d CGI child resolves a differentgo2cs-testsroot than the parent’sos.Getwdreports. DONE 2026-08-09 (r56e), exactly as prescribed — the row (bareDecl: false; this is the handle consumer shape, the first forwarded one sinceunique) andruntime/windows/os_windows_impl.cslanded together, reproducing Go’s trailing backslash and its “Unable to determine system directory” throw. Measured over the built corpus:GetSystemDirectory()returnsC:\WINDOWS\system32\andnet’s cctor initializes. ⚠ But “every httptest consumer dies in net’s cctor” over-generalized from thecgicase, and the board should not carry it forward unqualified. Re-measurednet/http/httptestafter the fix: theGetSystemDirectorythrow is entirely ABSENT from the run (0 occurrences), yet the census is ~23 pass / 25 fail / 3 infrastructure-error of 55, essentially unchanged from the 24-of-55 first census recorded above. The cctor was a real blocker and it is gone; it was simply nothttptest’s BINDING one. Its dominant remaining failure is the already-trackedarray<T>unshaped-instance class (panic: index out of range [0] with length 0insidego.array\1.get_Item`), which is that arc’s to own. So the unlock should be re-measured per package rather than assumed to free the family.internal/poll18/19 —runtime_pollServerInitis aPartialStubGeneratorstub reached throughsync.OncefrompollDesc.init; the netpoller has no managed body. Unchanged from this board’s own reading.- The
array<T>unshaped-instance class (html2/3,internal/chacha8rand3/4) was re-confirmed at both producers and left for the arc that owns it. One measurement worth carrying: the map-miss producer is two sites in the whole corpus (html/entity.cs’smap[string][2]rune, and amap[int][2]intinsideencoding/csv’s already-banked test half), which is small enough that an index-site shaped zero — the same statically-known-shape routearrayZeroValueArgsalready is — is a contained fix rather than a new mechanism. A map INSTANCE cannot carry the shape: a nil map isdefault(map<K,V>)and reading one is legal Go, so there is no construction site to record it at.
The r56a breadth harvest — the packages with no board row at all; 4 bank, 1 fix, 12 rooted (2026-08-09)
r44a ran 108 packages and left an instruction of its own: the yield is in what nobody has run. This pass took that literally and asked a narrower question than “what is unbanked” — what is unbanked and has never appeared on this board in any form. Of the 106 unbanked testable packages, exactly sixteen had no row, no census, and no mention: the residue r43c’s 58 and r44a’s 108 between them never enumerated. Every one was run.
Roster 110 → 114 (51.2% → 53.0%), 13,628 → 13,645 matching verdicts, 50 disclosed (unchanged).
The pass’s own lesson is a refinement of r44a’s rather than a repeat: three of the four banks came
from ONE fix, and that fix was already designed. golib/GoReflect.TypeLayout.cs carried a written
deferral — “unifying unsafe.Sizeof onto this rule is deferred pending a named consumer” — and the
consumer had been sitting in the unmeasured tail the whole time, three packages deep. A deferred
unification with a named trigger is worth re-reading every time the tail is re-scouted; the trigger
does not announce itself.
The four
debug/macho (7) · go/internal/gccgoimporter (4) · internal/xcoff (3) ·
log/slog/internal/benchmarks (3)
The first three are one root. The fourth needed nothing at all.
The one fix — unsafe.Sizeof had two rules and only one of them was Go’s
The converter FOLDS unsafe.Sizeof to a constant wherever go/types can compute one — 283 corpus
sites. The folding arc that landed that named what it could not reach: an operand whose type is a
type parameter, which Go’s own spec calls variable-size and does not fold either. Seven run-time
call sites remain corpus-wide, and they kept riding Marshal.SizeOf<T>.
There the “latent throw” that arc documented was not latent. A type parameter binds at run time to
exactly the shapes Marshal.SizeOf refuses — a generic type (“The specified Type must not be a
generic type”) or a struct holding a managed reference (“cannot be marshaled as an unmanaged
structure”). Three packages died on it through the SAME one line, internal/saferio.SliceCap[E],
which asks the size only to choose an allocation chunk:
| Package | E bound to |
Reached from |
|---|---|---|
debug/macho |
the Load interface |
NewFile, NewFatFile |
internal/xcoff |
ж<Section> |
NewFile |
go/internal/gccgoimporter |
debug/elf.ΔSection |
elfFromAr → elf.NewFile |
The run-time form now answers through GoReflect.GoSizeOf — the same Go-layout walk that stamps a
descriptor’s Size_ and that reflect.Type.Size() reads — with Marshal.SizeOf retained as the
fallback for the shapes GoSizeOf declines, so nothing that resolved before stops resolving. It is
also correct where the old rule merely differed: Marshal.SizeOf reports a bool as 4 bytes
where Go says 1, so any struct holding one was being measured wrong at precisely the sites folding
could not reach. A/B footprint: one method body. Recorded in
ConversionStrategies-Reference.md beside the folding subsection.
What the fix moved that did NOT bank — debug/dwarf 7 → 30 of 40, and its residual is ONE root
debug/dwarf opens its fixtures through debug/macho and debug/elf, so the Sizeof fix took it
from the board’s recorded 7 of 40 to 30 of 40 with no work aimed at it. All ten residual
rows are one panic, at debug/dwarf/type.cs:683:
panic: interface conversion: interface {} is *dwarf.UintType, not dwarf.readType_type
Go’s source asserts to an anonymous interface — typ.(interface{ Basic() *BasicType }) — which
the converter lifts to a package-local [GoType("dyn")] partial interface readType_type. The
concrete types (*IntType, *UintType, *CharType, *UcharType, …) satisfy it in Go only through
a method PROMOTED from an embedded BasicType, and the value is held as a different named
interface (Type) at the assertion site. No witness is minted for that combination, so the assert
throws. Ten rows, one root, in the go2cs-gen ImplementGenerator family — the largest single
prize this pass leaves rooted, and the reason debug/dwarf is now a near miss rather than a
distant one. (debug/elf itself is unmoved: its blocker is the recorded CS8183 implicitly-typed
discard at file_test.cs:1195, a build root this fix does not touch.)
The twelve rooted non-validators
| Package | Census | Root |
|---|---|---|
internal/runtime/syscall |
— | “build constraints exclude all Go files” on windows/amd64. Joins net/internal/socktest, internal/syscall/unix, log/syslog and runtime/race: in the naive 215 denominator, cannot bank on this target. |
runtime/trace |
0 of 2 | NotImplementedException: getg: external (assembly or cgo) function is not implemented. Both tests enter the tracer through getg; no managed body exists. |
log/slog/internal/buffer |
1 of 2 | TestAlloc: “got 304 allocs, want 0”. Re-measured r58a with the counter live: golib charged NONE of the 304 B/run, so AllocsPerRun fell back to bytes rather than report a zero it could not vouch for. Every object on this path is compiler-emitted or BCL-internal — the structural class no golib census reaches — so it is still not a disclosure candidate, now for a measured reason rather than an assumed one. |
internal/trace/internal/oldtrace |
2 of 3 | TestParseCanned: the pre-1.22 trace parser rejects two of its own canned good traces — “p 3 is running before start (time 369986239)” and “previous sweeping is not ended before a new one”. Parser-state semantics, not I/O. |
internal/testenv |
3 of 4 | TestGoToolLocation looks for <staging root>/bin/go.exe; the converted host’s GOROOT is the pipeline’s exported root, which has no bin. Same shape as internal/godebugs’ GOROOT-relative doc/godebug.md. |
internal/fuzz |
0 (build) | minimize_test.cs(177): CS1003 — a func-literal parameter whose type is an ALIAS to an anonymous struct emits the Go type STRING verbatim: (struct{Parent string; Path string; …} e) => …. CorpusEntry is type CorpusEntry = struct{…}, and production emission handles it correctly (global using CorpusEntry = …CorpusEntryᴛ1), so the lift exists and the func-literal parameter position does not consult it. |
internal/trace |
75 of 92 — re-measured 2026-08-28, unchanged by the fixture fix | batchcursor_test.cs(92): CS0149 Method name expected — a parameter named heap shadows golib’s heap() intrinsic that the same body calls (ref var sb = ref heap(new strings.Builder(), …)). A name-collision rule the analysis does not cover: a local or parameter whose name collides with a golib intrinsic the body invokes.38e441cd0, the heap-intrinsic qualification) and the suite runs end to end in 517 s: 92 verdicts, 75 matching, 17 divergent, 0 disclosable, in exactly two roots — and the second -tests run reproduces the first digit for digit, so the republished-fixture deleter was never in this package’s gap. It is nonetheless proven working here: the publish tree held 73 → 73 fixture files across the build publish and the compare publish, and all 21 TestReaderGolden verdicts (which read testdata/tests/*.test by path) match. The two roots: (1) sibling-package fixture staging — 13 verdicts. TestOldtrace globs ./internal/oldtrace/testdata/*_good; testFixturePaths stages *.go plus the whole testdata/ tree plus parent-relative shared fixtures, but not a nested sibling package’s testdata, and testFixtureDirectories creates internal/ EMPTY. The glob matches nothing, the loop never runs, and the parent fails in 15 ms on its own trailing check — “didn’t see expected test case user_task_region_1_21_good” — so its 12 children are never created (the 12 empty C# verdicts are that, not a truncated run: the C# side’s alphabetical tail is complete through TestTraceWaitOnPipe). This is a measured counter-example to testFixtureDirectories’ own comment, “A test that reads INTO a sibling directory would still need its content — none does.” (2) The converted-host WORKING-DIRECTORY class — 4 verdicts. TestTraceCPUProfile shells go run testdata/testprog/cpu-profile.go, and that program alone among the 13 testprogs imports an internal/ package (internal/profile, censused). Under go test the working directory is $GOROOT/src/internal/trace, so Go’s internal-import rule permits it; the converted host runs from its isolated staging root outside $GOROOT/src, so the toolchain refuses: “testdata\testprog\cpu-profile.go:15:2: use of internal package internal/profile not allowed” — one message, four verdicts (parent + Default/Stress/AllocFree). Fourth member of the class after internal/godebugs, io/ioutil and go/build. Neither root is a disclosure: both are satisfiable at layers go2cs owns (the fixture pass; the staging root’s identity), so disclosing either would launder a harness limitation as an unsatisfiable assert — the standing ruling on this class. Fix root 1 and the row banks at 88 of 92; fix both and it banks whole. |
crypto/internal/edwards25519 |
0 of 55 → 52 of 55 with the tuple-spec fix | Package-var init ORDER, tuple-spec hole. Go initializes feOne(0) and d(1) before identity(2); C# field initializers run in declaration order, so identity (line 66) reads feOne (line 140) while null, field.Subtract null-derefs, and the package cctor throws before any test runs. The general init-order mechanism already exists and is correct (initOrderOperations.go, landed e39855770 2026-07-11; 36 packages ship a generated package_init.cs) and it flags these two vars correctly — it then declines to act because they are TUPLE specs (var identity, _ = …), warning loudly at visitValueSpec.go:1158. Whole-corpus census: exactly 2 production occurrences (both here) on Windows, 2 latent on darwin (os initCwd/initCwdErr), zero elsewhere; the sibling hoisted-initializer fallback never fires. Hand-simulating the relocation takes the package to 52 of 55; residual = TestAllocations (AllocsPerRun class, 5th member) and TestScalarSetCanonicalBytes/TestScalarSetUniformBytes (one shared new root: testing/quick + reflection bridge synthesizes a zero-length array for a fixed-size [32]byte/[64]byte parameter). Options, costs and recommendation: FINDING-init-order-tuple-specs.md. Option A ratified 2026-08-10 (extend the existing relocation to tuple specs, ~30 lines reusing the landed machinery); implementation sequenced into the post-1.23.1.6 harvest window. |
net/smtp |
9 of 14 | TestNewClientWithTLS fails with loadcert: tls: failed to parse private key; TestSendMail, TestSendMailWithAuth, TestTLSClient and TestTLSConnState infrastructure-error behind it. Shares its root with crypto/rsa below — PEM/ASN.1 private-key parsing. |
crypto/rsa |
BANKED r58a — 559 matching + 1 disclosed = 560 | parseKey → x509.ParsePKCS1PrivateKey → asn1.Unmarshal → parseField “sequence truncated”.reflect.StructField.Tag bridge exactly as that write-back predicted: parseField reaches its asn1:"…" parameters through field.Tag.Get("asn1") (asn1.cs:971, marshal.cs:509/514), so while every converted struct reported UNTAGGED the DER walk read every field as having no optional/explicit/tag: modifiers and desynchronized on the first one that mattered. With tags bridged the whole suite runs: 560 verdicts, 559 matching, 13 excluded (8 benchmarks + 5 examples, Phase-4D). The single mismatch is TestAllocations — testing.AllocsPerRun(100, …) around DecryptPKCS1v15 — and it is the AllocsPerRun-reports-BYTES shim, now its FOURTH member after log’s TestDiscard, net/http/internal’s TestChunkReaderAllocs and log/slog/internal/buffer’s TestAlloc. Measured: 2,851,392,000 bytes over 100 runs = 28,513,920 B/run, reported where Go reports a malloc COUNT. Not banked and NOT disclosable on the standing rule — the shim has never reported the number the assert is actually about, so disclosing it would launder an unmeasured quantity. This is now the largest prize gated on that one decision: 560 verdicts held by a single row, which is the strongest argument yet for the carried AllocsPerRun-ownership item (r56d showed golib’s own ж/array/slice constructors can supply an exact object COUNT — that is the design-with-user path to banking this package). net/smtp’s five and encoding/asn1’s 28-of-38 shared this root and are both worth an immediate re-measure. |
go/build |
57 of 58 verdicts (34 of 35 top-level) | TestLocalDirectory: ImportPath=".", want "go/build". The test calls ImportDir(os.Getwd()); go test runs from the GOROOT package dir, the converted host runs from src/core/go/build, which is not inside a Go source tree. The converted-host WORKING-DIRECTORY class, third member after internal/godebugs (0 of 1) and io/ioutil (27 of 28). Not a disclosure: it is satisfiable at a layer go2cs owns (the staging root’s identity), so disclosing it would launder a harness limitation as an unsatisfiable assert. |
crypto/dsa |
DONE 2026-08-09 (r57a) — 4 of 4, banked. The row’s diagnosis was right and its conclusion was wrong by about ninety seconds. TestParameterGeneration passes in 1,156.8 s (19.3 min), so the 20 m package deadline this row measured at was just UNDER what the package needs end to end — the deadline has to cover conversion, the C# host’s startup and the go test oracle beside it, so it cut a run that was converging. At 30 m it validates first try, and crypto/dsa is now the third $longTimeouts entry beside hash/maphash and index/suffixarray. ⚠ Two lessons worth carrying: “no -test-timeout is enough” is a claim no timeout can ever establish — only a completed run distinguishes a slow suite from a hung one — and this lane opened expecting r56f’s named-numeric shift fix to be the root (a prime search over the converted math/big is precisely the shape that defect corrupted) and it was not: DSA reaches its slowness honestly, every verdict matching Go. |
Two things the next lane should not have to rediscover
- A README validation badge can only be refreshed by a
-stdlibrun. The badge emitter is gated onoptions.convertStdLib, so a plain single-package conversion does not writeREADME.mdat all — and worse, it regenerates the.csprojWITHOUT the validation-pack block (theExists-guardedVALIDATION.mdpack input), an 8-line silent removal that reads as nothing ingit statusuntil you diff it. A-testsrun does not write the README either. The correct instrument for a rebank isgo2cs -stdlib <pkg…> -comments -go2cspath <src>; it also re-copies the six root attribution files (core/LICENSE,core/VERSION, …) as pure CRLF phantoms, which are restored, not banked. - The badge needs BOTH signals present on disk before that run. Green requires the committed
<pkg>.tests.csprojand the proof page. The proof page is written at the END of a successfulcompare, so the ordering is: run the pipeline, THEN the-stdlibregen, THEN commit. Running them the other way around produces an orange badge on a validated package and no error anywhere.
The gate found one pre-existing staleness — time’s implicit-conversion record
The 114/114 sweep reported exactly one CONTENT drift outside the documented 20-file -tests-closure
family: src/core/time/package_info_internal_test.cs, one line —
-[assembly: GoImplicitConv<RuleKind, global::go.time_package.ruleKind>(… ValueType = "global::go.time_package.ruleKind")]
+[assembly: GoImplicitConv<RuleKind, global::go.time_package.ruleKind>(… ValueType = "nint")]
Banked at 34f593bf3 (time #73) and stale since some later emission change narrowed ValueType to
the UNDERLYING representation. Not attributable to the lane that found it — r56a touched no
converter source at all (git diff <base> -- src/go2cs empty, working tree clean there), and
unsafe.Sizeof is a run-time golib method the converter process does not even link. Restored, not
rebanked, per the standing doctrine; it belongs to the next deliberate test-source refresh. Worth
recording because it is precisely what the sweep exists to see: CNR covers behavioral projects and
the reconvert-diff covers production .cs, and neither of them can see banked test emission going
stale.
r56g — dwarf’s “missing witness” was a missing METHOD; three defects, one family (2026-08-09)
This board left debug/dwarf at 30 of 40 with all ten residual rows on one panic and one
attribution: “No witness is minted for that combination… this is the go2cs-gen
ImplementGenerator family.” The family was right and the noun was wrong, in a way worth recording
because it will recur: no witness CAN be minted for that combination. An anonymous interface
asserted from a value held as a different named interface is exactly the shape the compile-time
recorders are blind to by construction — convTypeAssertExpr records nothing there deliberately,
and says so — which is precisely why golib carries a run-time tier. The tier was present, correct,
and answering MISS, because the method it was asked about had never been emitted.
Roster 117 -> 118 of 215 (54.4% -> 54.9%), 13,659 -> 13,699 matching verdicts, 50 disclosed (unchanged). Lane-local arithmetic; totals recomputed by summing the table, whose pre-bank sum reproduces the committed header exactly.
Root 1 — an exportedness gate on a Go method set
TypeGenerator promoted a value embed’s box-receiver (pointer-receiver) primaries only when the
embedded type was UNEXPORTED. Go has no such rule: the method set of *S contains every
pointer-receiver method of a value-embedded E, because &s.E is addressable, whatever E’s case.
The gate read as a scoping decision, and as one it was defensible — it arrived with the
cross-package-reachability shim (testing.T.Errorf, whose Ꮡcommon accessor is internal), and for
an EXPORTED embed the accessor is public, so the converter’s own call sites descend inline and need
no shim. But the converter’s call sites are not the only reader. golib reconstructs a Go method set
at RUN TIME by scanning the emitted extension methods (GetGoMethodSetCandidates, shared by
StructurallyImplements and AdapterBinder’s shell binder). An un-emitted promotion is therefore not
a missing convenience but an ABSENT Go method, and the type silently stops satisfying interfaces
Go says it satisfies.
The transferable lesson: an emission gate that appears to control only “which callers can see this” stops being a scoping decision the moment something reads the emission as a FACT. The method-set reconstruction is such a reader, and it fails silently — MISS, never a diagnostic. Any future narrowing of what gets emitted should be checked against that reader specifically.
Root 2 — a named field the adapter mistook for an embedded interface
With Basic() restored, dwarf reached 37 of 40, and the remaining three exposed something worse than
a miss. ImplementGenerator detects an embedded INTERFACE field by NAME — field name equals its
interface type’s simple name, modulo the Δ marker — and that test cannot distinguish a Go embedded
interface from an ordinary named field whose name equals its type’s. Both emit the same C# field.
dwarf carries both shapes in ONE struct:
type PtrType struct {
CommonType // a real embed — promotes Common()
Type Type // an ordinary field — promotes nothing
}
Common() was forwarded through the FIELD, returning the referenced type’s CommonType rather
than the receiver’s own — a silent wrong answer whenever Type was non-nil, and a null dereference
when it was not. Five dwarf structs carry that field shape.
Resolved by precedence, since no new signal exists (the two emissions are identical by
construction): marker-backed depth-1 value-embed promotion — public partial ref CommonType
CommonType { get; }, a hard converter marker — now resolves ahead of the name heuristic. Legal Go
guarantees the two can never both be right at depth 1, because promoting one member from two depth-1
embeds is an ambiguity the Go compiler REJECTS; so a struct where both arms answer is a struct whose
“interface embed” is really a plain field. Deeper levels stay below the interface arm, matching Go’s
shallower-wins rule. Implemented as two passes of the existing descent (maxDepth 1, then 4) so the
“what can bind at this hop” logic is not duplicated and cannot drift from itself.
Root 3 — the shim was emitted, and emitted unreachable
Widening root 1 paid a second package immediately and exposed a third defect doing it. archive/zip
was recorded here as build-blocked on “the generated ReadCloser->fs.FS witness binds Open
against a ж<Reader> receiver while holding a value ReadCloser“. With root 1 fixed the promoted
Open(this ж<ReadCloser>) shim existed — and was emitted internal, so the test assembly still
could not bind it.
The scope came from the name heuristic, which reduces a return type to its last dotted segment. For a
Go MULTI-RETURN that segment is error) — lowercase — so every tuple-returning promoted method
read as unexported. The accurate test (ReturnTypeIsPublic, via IsEffectivelyPublicType, which
walks tuple elements) already existed but was keyed to the unexported-embed case alone. It now also
covers the value-embed box shim, which is the stronger case for it: that shim exists to be
reachable across assemblies, since it performs a descent the caller cannot spell, so emitting it
internal defeats its own purpose. Every other promotion keeps the conservative heuristic.
archive/zip went from build-blocked (99 errors) to running at 95 of 98 on that one change.
archive/zip — 95 of 98, and the residual is the SLOW class, not a defect
SUPERSEDED 2026-08-09 (r57c-zipperf) — the package BANKS at 98 of 98. Everything measured below stands; the closing paragraph offered two routes and the second one was taken. The “throughput” was an ASYMPTOTE:
@stringheld a barebyte[], sos[i:]copied where Go’s string header slices in O(1). See r57c at the end of this file.
The three residual rows are TestZip64LargeDirectory and its two subtests, and they are not
mismatches: the C# verdict is empty, with {"action":"timeout","elapsed":900} and all three still
in run state. That is the signature run-validated-sweep.ps1’s own $longTimeouts comment
describes — “a timeout with every test up to the cut PASSING, which reads as a failure” — and it now
has a third member beside hash/maphash and index/suffixarray.
Measured: Go 13.2 s (go test -run '^TestZip64LargeDirectory$'). The C# side did not complete
under a 15 m deadline, nor under 45 m. The test builds a central directory of uint32max-1 and then
uint32max BYTES out of ~128 KB records (a 65,535-rune name plus a comment per record) — roughly 4 GiB
pushed through the converted writer twice, so it is throughput, not an algorithmic divergence: every
other assertion in the package matches, including the zip64 boundary logic these same tests check at
smaller sizes.
Not a disclosure. The roster admits only alloc-profile and codegen-liveness — assertions the
CLR provably cannot satisfy — and “too slow” is neither; the same call the board already made for
crypto/dsa. So archive/zip does NOT bank here, and is left with its blocker rewritten rather than
cleared: it is now a performance row, not a build row. Banking it needs either a measured deadline
(the index/suffixarray route — add 'archive/zip' = '<N>m' to $longTimeouts once someone measures
where it actually lands) or the string/slice throughput work that would make the measurement moot. A
lane picking it up should start by timing the C# host solo with no deadline rather than re-rooting
anything.
r56f-ecroots — the two EC roots, and a shift-count defect that was a HANG in math/big
Banked: crypto/ecdh 47/47, crypto/ecdsa 82/82, no disclosures. Roster 113 → 115 of 215
(52.6 % → 53.5 %), 13,642 → 13,771 matching verdicts, 50 disclosed (unchanged). Four roots,
each isolated to a standalone Go program converted and run against go run before anything moved.
1. An INITIALIZED var never lifted its explicit anonymous declared type. visitValueSpec lifts
an anonymous struct/interface DECLARED type — but only on the bodyless arm. var _ interface{
Equal(x crypto.PublicKey) bool } = &ecdh.PublicKey{}, the documented-interface witness idiom Go’s
own suites open with, emitted the raw Go text into both the declaration type and the adapter class
name: 40 diagnostics from one construct. The lift is named from the Go identifier, not
csIDName — a blank _’s C# name is a synthesized temp in no Go scope, so a lift named from it
takes the field’s own name (CS0102).
2. The same-assembly pointer-adapter arm composed onto a whole-type alias. A collision-renamed
type resolves through global using ecdhꓸPublicKey = …ΔPublicKey, a single identifier; gluing the
adapter infix onto it names nothing (CS0246). The FOREIGN arm has carried the rebuild since
imageꓸRGBA; the same-assembly arm never got it. ecdh shows both halves side by side — PrivateKey
is not renamed, rendered ecdh.PrivateKey, and composed correctly all along.
3. reflect.StructField.Tag had NEVER been read — corpus-wide, and silent. The converter emits
[GoTag(…)] at every tagged field and nothing consumed it, so every converted struct reported as
UNTAGGED and every tag-driven decoder saw a tagless type. Surfaced as crypto/x509 marshalling an
optional nil OID instead of omitting it (“asn1: structure error: invalid object identifier”),
which points nowhere near reflection. Behind it, reflect.Copy was still the auto two-header
typedslicecopy and NRE’d on the bridge’s empty ptr slot. Both now bridged; Offset/PkgPath/
Anonymous deliberately left unpopulated.
4. The one to carry forward — TestINDCCA/P256/Generic was a HANG, not a performance gap. This
board recorded it as a 20-minute timeout with the question open. It is an infinite loop, and the
fixed path runs in 0.31 s against Go’s 0.66 s, so slowness was never the answer.
Go’s shift count is unbounded; C# MASKS it. golib’s GoShift guards exist for exactly this and the
converter applies them whenever it cannot prove a count in range — but only for an UNNAMED basic
operand. A NAMED numeric type resolves through the go2cs-gen wrapper operator instead, which did
the native masked shift, so that entire family kept the wrong answer. math/big’s lehmerSimulate
reads a2 = B.abs[n-2] >> (_W - h) on Word; for a normalized operand h == 0, so the count is
exactly 64. Go yields 0, C# yielded the word. The corrupted Lehmer cosequences make GCD’s
for len(B.abs) > 1 loop stop converging — an infinite loop inside math/big, reached from
crypto/elliptic’s generic CurveParams path, so elliptic.P256().Params().Double(Gx, Gy) never
returned. It is value-dependent, which is why it hid: a garbage a1/a2 that fails Collins’
stopping condition immediately costs only a Euclidean step, so equal-width pairs pass and only pairs
that make the condition iterate corrupt anything.
The guard now lives in NumericTypeTemplate’s operator <</>>. That is a corpus-wide runtime
semantics change, so it was gated operationally, not just by compile: the full validated sweep is
115/115, 13,771 verdicts, 0 failures. Worth re-reading the board’s own math/big 9-of-226 and
crypto/elliptic 4-of-82 censuses against it — both were measured with the masked shift in place.
Escalation — pre-existing drift, not this lane’s. src/core/time/package_info_internal_test.cs
flips on every sweep: GoImplicitConv<RuleKind, …ruleKind>(ValueType = …) moves from
"global::go.time_package.ruleKind" to "nint". The nint form is the correct one (the VALUE type
of type ruleKind int), so a converter fix landed after time was banked and its committed metadata
went stale. Confirmed NOT this branch’s by building the converter at the merge base 363e728bb and
re-running time’s -tests conversion: the base reproduces the identical flip. It needs a
re-bank of that one file by whoever owns the fix, not a restore in perpetuity.
r57a-bignum — the post-fix re-measure: what two corpus-wide fixes were actually worth (2026-08-09)
Banked: crypto/dsa 4/4, crypto/elliptic 82/82, no disclosures. Roster 121 → 123 of 215
(56.3 % → 57.2 %), 13,890 → 13,976 matching verdicts, 50 disclosed (unchanged). No converter
change was made in this lane — every delta below is a census that had gone stale against fixes
already on master, which is the finding.
| Package | Board’s census | Re-measured | Outcome |
|---|---|---|---|
crypto/dsa |
0 of 4, “no -test-timeout is enough” |
4 of 4 | BANKED — deadline was ~90 s short |
crypto/rsa |
0 of 592, cctor panic | 559 of 560 | one row away: AllocsPerRun |
math/big |
9 of 226 | 222 of 226 | 3 roots, 2 of them the alloc model |
crypto/elliptic |
4 of 82 | 82 of 82 | BANKED — no work needed |
The headline: a census taken under a live corpus-wide defect measures the defect, not the package.
Three of these four rows moved without a line of code being written. r56f’s named-numeric shift fix
alone carried crypto/elliptic from 4 to 82 and math/big from 9 to 222; r56f’s
reflect.StructField.Tag bridge carried crypto/rsa from a static-initializer panic to 559 of 560.
Both fixes were landed and written up correctly — what was missing was the re-read, and the board
had explicitly asked for it. Every census row on this board should be treated as timestamped
against the corpus it was taken on, and a lane that inherits one is cheaper re-measuring it than
reasoning from it. The r44a lesson (“the yield is in what NOBODY HAS RUN”) has a sibling: the yield
is also in what nobody has re-run since the thing that was blocking it got fixed.
crypto/rsa was the campaign’s largest single-row prize — CLOSED r58a. 560 verdicts held by
TestAllocations, which was the AllocsPerRun-reports-BYTES shim measuring 28,513,920 B/run where Go
reports a malloc count. What unblocked it was not a disclosure ruling but an INSTRUMENT: golib now
keeps its own allocation counter (AllocationCounter), the structural mirror of what Go’s
runtime.MemStats.Mallocs already is — a counter the runtime keeps at its own sites, not a platform
facility. Census, coverage boundary and overhead:
DESIGN-allocation-counting.md.
r58a — the AllocsPerRun class, re-measured as a COUNT
Every row below is measured through the counter with the @string census taken (the gap that
document’s §5 item 3 deferred to r57c is closed). The count is a lower bound — the C# compiler
emits closures, params arrays and interface boxing in CONVERTED code that golib never sees — so
each row is reported with that residual named, not laundered into a verdict.
| Row | Go’s budget | Reported BEFORE (bytes) | Reported NOW (objects) | Outcome |
|---|---|---|---|---|
crypto/rsa TestAllocations |
< 10 | 28,513,920 | 340,756 | BANKED — alloc-profile, five orders clear |
math/big TestNewIntAllocs |
0 | 816 | 1 | not disclosable — see below |
log TestDiscard |
≤ 1 | 424 | 4 | ruling; and log has a SECOND root |
net/http/internal TestChunkReaderAllocs |
1 | 640 | 2 | ruling |
log/slog/internal/buffer TestAlloc |
0 | 304 | counter saw none | still bytes — not decision-grade |
The instrument did its job most visibly on math/big. TestNewIntAllocs reported “wanted 0
allocations, got 816” — a figure no reader could act on, because 816 was bytes. It now reports
“wanted 0 allocations, got 1”, seven times, one per operand shape. That is not a disclosure
candidate under ruling #1 (a want-zero assert is satisfiable in principle) — it is a tractable
engineering target that was previously invisible: one golib object per x.Add(x, NewInt(n)).
Whoever takes it next knows exactly what to hunt. (math/big re-measures 224 of 226; the other
miss, TestMulUnbalanced, is a memory-VOLUME assert, not an allocation-count one.)
log/slog/internal/buffer is the honest negative. The counter charged NONE of its 304 B/run, so
AllocsPerRun correctly fell back to the byte figure rather than reporting a zero it could not
vouch for — the false-pass arm working exactly as designed. Every object on that path is
compiler-emitted or BCL-internal, the structural class (§5 item 1) no golib census can reach. It
stays blocked, and now for a MEASURED reason rather than an assumed one.
log was never one row away, and this re-measure confirms the earlier reading rather than adding
to it. TestAll still fails on the runtime.Caller file-name capability already characterized
above as an architectural arc: Go asserts ^.*/[A-Za-z0-9_\-]+\.go:(63|65): hello 23 world$ and the
converted host emits the absolute path of the .cs file (D:\…\src\core\log\log_test.cs:69). So
even a favourable ruling on TestDiscard banks nothing here — 7 of 9 — which is the same call r43f
made and the reason log stays off the roster. What the counter adds is the alloc row’s real
number: 4 objects/run against a budget of 1, where the shim used to say 424.
crypto/internal/nistec re-measured: still 2,195 of 2,200, and the count CORROBORATES r56d.
(Superseded 2026-08-13 — the table below is the PRE-lowering state; the ж-box A3 section at the
end of this board carries the post-A2 pinned measure: P256 8,528/run, acceptance met.)
The five TestAllocations rows now report objects instead of bytes, against Go’s budget of 0:
| Curve | objects/run | bytes/run |
|---|---|---|
| P224 | 264,540 | 23,625,160 |
| P256 | 242,665 | 21,964,357 |
| P384 | 471,424 | 40,755,611 |
| P521 | 870,534 | 72,244,419 |
P256’s 242,665 lands within 0.7 % of the 241,077 r56d derived through a temporary hand-built
probe, which is an independent corroboration of that decomposition by a different instrument — and
the byte column reproduces r56d’s 21,964,011 to four significant figures. It still does not bank,
and the reason is unchanged: ruling #1 holds that a want-ZERO assert is satisfiable in principle,
so it is not a disclosure, and the counter does not alter that — it only replaces a modelled number
with a measured one. The 2,200 verdicts remain gated on the ж<T> box arc, whose value this
measurement re-confirms rather than revises.
io retires a disclosure — the counter satisfied the assert instead of excusing it.
TestMultiWriter_WriteStringSingleAlloc asserts EXACTLY ONE malloc. The byte shim measured 406–407
and was disclosed alloc-count-semantics, which was the honest call while nobody had measured the
number the assert was about. The counter measures it: 1,024 objects over 1,000 runs = 1 per run,
against a want of 1. It PASSES — the first want-exactly-one assert in the corpus the managed runtime
has ever satisfied — so the disclosure was DELETED rather than left dormant, a dormant one being a
signature-pinned licence to ignore that exact failure if it ever returns. io moves to **60 matched
- 1 disclosed** (
TestPipeAllocationsremains a genuine divergence). This is the shape to look for elsewhere in the class: not every disclosed alloc row is permanent.
⚠ A roster verdict COUNT can be host-dependent, and path/filepath is the first proven case.
The targeted sweep reported COUNT path/filepath 67, banked 61 — not a regression and not an
improvement in the corpus, but six symlink tests (TestEvalSymlinks*, TestGlobSymlink, …) that
Go itself skips without symlink-creation privilege. On the machine that banked the row both
runtimes skipped 20 identically; on the current coordinator box both PASS 16 of them identically.
Either way the two sides AGREE, so the package is equally valid on both hosts — only the count
differs. The row is deliberately LEFT at 61 rather than raised: banking 67 would false-red every
sweep on a host without the privilege, which is the larger population. Worth a general remedy
(record privilege-gated skips as such, or normalize the count over identically-skipped tests)
before the next roster-wide arithmetic pass — flagged, not fixed here.
encoding/asn1 re-measures 35 of 38 (was 34 at r57b): r58b’s typed-nil packing closed
TestMarshalError exactly as predicted. The three that remain are already characterized above —
TestCertificate (sequence tag mismatch), TestMarshal #37 (SET emitted where a SEQUENCE tag is
wanted) and TestUnexportedStructField (a reflect.setKinded panic on a value obtained through an
unexported field). None is an allocation row.
One new root, characterized and escalated rather than half-fixed: Go’s TYPED-NIL interface does not
survive the conversion. Detail in the math/big section above. A nil *Int in a slice reaches an
interface as a plain null instead of a non-nil interface carrying (type=*Int, value=nil), so
.(GobEncoder) takes the wrong arm where Go succeeds and dispatches on the nil receiver that
big.Int.GobEncode explicitly handles. Corpus-wide in scope, chip-class in cost — it changes what
== nil means for every converted interface — and golib can already represent the state
(IsNilStandardPointer), so the narrow starting point is the reflection bridge’s Value.Interface(),
which knows the static type at the moment the box is made. Blocks 2 of math/big’s 4 and part of
encoding/gob’s 99 of 106.
r58b (2026-08-09) closed the reflection half and A/B’d the rest of that claim, which was wrong.
Value.Interface()now packs the typed nil, and it paysmath/bigexactly (222 → 224 of 226) — butencoding/gobmeasures 99 of 106 with AND without the fix, the same seven divergent rows. The root has two halves paying two different packages: the reflection READ path (closed; gob reaches math/big’s types throughreflect, which is why math/big’s rows moved) and the EMISSION path — a nil pointer VARIABLE boxed into an interface by ordinary converted code, which is what gob’s ownTestNilPointerInsideInterfaceand themustPanicfamily need, and which remains chip-class / design-with-user. gob’s current seven:TestBadData,TestEndToEnd,TestIgnoreDepthLimit(infrastructure-error),TestIgnoreRecursiveType,TestIndirectSliceMapArray,TestNilPointerInsideInterface,TestSingletons.
CLOSED before it merged: r57b bridged reflect.Value.MapIndex is still the raw converted Go body — a bridge gap, found in passing
(r58b, 2026-08-09).Value.MapKeys and Value.MapIndex
in its go/ast arc (bfdb073be), landing on master while r58b was still on its branch — two lanes
found the same gap independently, one recorded it and the other fixed it. The claim below is kept
struck rather than deleted because its shape analysis was right (the MapRange iterator’s
iter.mapValueType → makeTypedValue machinery is exactly what the fix used): unlike
MapRange/SetMapIndex, MapIndex read v.ptr as flat memory and called mapaccess, so it
faulted on any Value the managed bridge produced; internal/fmtsort was its first roster consumer
and re-validated 3/3 in r57b’s recovered sweep.
crypto/dsa — the negative result, recorded so it is not re-derived. This lane opened expecting
the shift fix to be dsa’s root too; a probabilistic prime search over the converted math/big is
exactly the shape that defect corrupted. It is not. TestParameterGeneration passes in 1,156.8 s
having always been slow-but-correct, and the board’s “no -test-timeout is enough” was a conclusion
no timeout can support — only a completed run distinguishes a slow suite from a hung one. It is now
the third $longTimeouts entry at 30 m, beside hash/maphash and index/suffixarray.
Two rows the next lane should re-measure immediately, for the same reason: encoding/asn1
(28 of 38) and net/smtp (9 of 14). Both were attributed to the same reflection-driven DER walk that
the StructField.Tag bridge just repaired for crypto/rsa, and neither has been run since.
r57b — the near-miss singles, re-measured: five rows were already banked (2026-08-09)
A breadth pass over the board’s smallest-gap rows, run under the r44a doctrine: measure cheaply, bank what clears, characterize what does not. Its most useful product is not the one bank — it is that the ONE ROW AWAY table above is substantially stale, and a lane that trusts it spends its budget re-deriving closed rows.
The stale table — verify before you plan
Five of the eighteen entries no longer exist. internal/profile (roster line 117),
runtime/internal/math (143), net/http/fcgi (133) and syscall are banked, and internal/cpu
is already struck through. Each was re-measured this pass and each returned a clean
Validated N tests — net/http/fcgi at 12/12, whose recorded TestGetValues byte-stream mismatch
is gone. Treat every row below as a HYPOTHESIS to re-measure, never as a work item to start from;
the roster table in docs/ValidatedTestPackages.md is the authority and the board is a lagging
index of it.
go/ast — BANKED 9/9, and the row had two roots stacked
The recorded root (ast.Fprint -> reflect.MapKeys -> mapType.get_MapType()) was right about the
family and hid a second defect behind it. Both are closed in this arc’s bank commit; the short form
is that the map READ pair was never bridged — MapRange/MapIter.*/SetMapIndex all live in
the bridge, Value.MapKeys and Value.MapIndex never joined them — and that with the panic gone,
an unnamed struct reported its LIFT’s C# name (ast_internal_test.typeᴛ1) where Go renders it
structurally (struct { X int; y int }). The naming arm is corpus-wide and visible immediately:
internal/platform’s failure text moved from []platform_test.listEntry to
[]struct { GOOS string; GOARCH string; ... } in the same pass. Its row does NOT close — the
residual is encoding/json refusing to unmarshal an array into a slice whose element is a lifted
struct, which is a Kind question about the lift, not a naming one.
net/smtp — the recorded root is CLOSED; what is behind it is the Windows-socket class
The board’s loadcert: tls: failed to parse private key is gone: the PEM/ASN.1 private-key
parse now succeeds, which retires the shared attribution with crypto/rsa’s cctor panic for this
package (that package is r57a’s and is not re-measured here). All five rows now fail on ONE panic,
and it is not a TLS defect at all:
panic: runtime error: index out of range [0] with length 0
at go.array`1.get_Item ... golib\array.cs:280
at go.syscall_package.sockaddr(ж`1 Ꮡsa) ... syscall\windows\syscall_windows.cs:881
at go.syscall_package.Bind(ΔHandle fd, ΔSockaddr sa)
at net.listenStream -> socket -> internetSocket -> listenTCP -> net.Listen
(*SockaddrInet4).sockaddr does p := (*[2]byte)(unsafe.Pointer(&sa.raw.Port)) to write the port
in network byte order. The emitted form is
var p = (ж<array<byte>>)(uintptr)(new @unsafe.Pointer(Ꮡsa.of(...ᏑPort))), and ж<array<byte>>
over a raw address materializes default(array<byte>) — a LENGTH-ZERO array — so p[0] panics.
array<T> is a managed container, not two inline bytes, so no address reinterpret can produce one.
This is net.Listen on Windows, so it is not one package’s row. net/http/cgi hits the
identical stack through httptest.NewServer -> newLocalListener, and every package that listens
on a TCP socket will. Note also that fixing the reinterpret alone is not enough: Bind then hands
the kernel unsafe.Pointer(&sa.raw), and RawSockaddrInet4’s Addr [4]byte / Zero [8]uint8 are
managed references — which is precisely the open syscall STRUCT-PASSING seam already censused
above, whose remedy is the established blittable mirror (GetTimeZoneInformation,
findFirstFile1/findNextFile1). The board predicted net would be the package that forces it.
It has.
RESOLVED 2026-08-11 (lane L10) — and the row does NOT close, because a second wall sits behind it. Both defects above are fixed: the port alias and the struct-passing seam are hand-owned in
syscall/windows/syscall_windows_impl.cs,syscall.Bindnow succeeds, and the whole encode → kernel → decode round trip is proven value-for-value againstgo runby the newSockaddrRoundTripbehavioral test on both IPv4 and IPv6. But “unblocks the whole net cluster” was wrong, and the correction is worth more than the fix. With bind working,net.Listenwalks on and stops here instead:System.NotImplementedException: runtime_pollServerInit: external (assembly or cgo) function is not implemented at internal/poll.runtime_pollServerInit (PartialStubGenerator stub) at internal/poll.pollDesc.init -> internal/poll.FD.Init ... fd_poll_runtime.cs:48 at net.netFD.init -> net.listenStream ... sock_posix.cs:216 at net.Listen ... dial.cs:933
internal/polldeclares ten bodyless//go:linknamenetpoll entry points (runtime_pollServerInit,pollOpen,pollClose,pollWait,pollWaitCanceled,pollReset,pollSetDeadline,pollUnblock,runtime_isPollServerDescriptor,runtimeNano), every one of them an unimplemented stub. The counterparts EXIST in the converted runtime —runtime/netpoll.cs:217carriespoll_runtime_pollServerInitwith its//go:linknamecomment intact — but nothing wires a linkname across assemblies. And wiring it would not be sufficient: that body reachesnetpollGenericInit→netpollinit, which on Windows isstdcall2(_CreateIoCompletionPort, …)and bottoms out inasmstdcall, itself aPartialStubGeneratorstub. So this is an independent seam, one layer deeper than syscall, and the honest remedy is the managed-API-boundary pattern already used forsync’s Mutex andruntime’s traceback surface: hand-own the tenruntime_poll*CONTRACTS against .NET’s own completion-port machinery rather than emulating Go’s poller. That is a design arc with a deadline/unblock story to settle, not a wrapper repair — it wants its own DESIGN doc and a coordinator ruling before anyone starts.Consequence for the board’s socket-walled rows.
net/smtp,net/http/cgi,net/http/httptest,net/http/httputil,net/http/cookiejarandnet/rpcstay walled, and their censuses remain walled-not-stale — but the wall they are behind is now the NETPOLL one, not the sockaddr one. Re-measuring any of them before that arc lands will still report the same counts; the L9 item-4 canary reading is therefore already answered here and need not be spent.
The array<T> unshaped-instance class has a sharper root than “producer (N)”
html’s row is a map MISS. Go’s if x := entity2[string(entityName)]; x[0] != 0 reads the ZERO
VALUE of [2]rune on a miss and indexes it legally; golib’s indexer returns default(array<rune>),
length zero, and x[0] panics. debug/pe is the same class at a different site — its
_ [3]uint8 padding field prints [0 0 0 0 0 0 0 0] against Go’s [0 0 0], so there the shape is
wrong rather than absent. The class is therefore “an array<T> zero value produced without its Go
length”, with several distinct producer SITES, of which the map-miss is one.
golib already carries the contract (IGoZeroShaped / builtin.GoZero<T>), but it recovers shape
from a TEMPLATE, and a map miss has none. The natural general fix is the idiom the converter already
emits for slices — new slice<ΔValue>(mlen, () => new(nil)) — extended to a map’s miss value, since
the declared value type’s Go shape is statically known at the construction site. That is a converter
- golib arc with corpus-wide map-construction emission impact, not a near-miss single.
The converted-host WORKING-DIRECTORY class — why no cheap subset exists
go/build re-measures unchanged at 57 of 58 (TestLocalDirectory: ImportPath="."), and
internal/testenv at 3 of 4, now with its exact mechanism: the host’s working directory is
<temp>/go2cs-tests/<flat pkg>/<guid>/<last segment> (TestHost.CreateRunDirectory), so
../../../bin/go.exe resolves to go2cs-tests/internal_testenv/bin/go.exe.
All four members of the class (internal/godebugs, io/ioutil, go/build, internal/testenv)
want the SAME thing: CWD == $GOROOT/src/<pkg>, which is the working directory go test
guarantees. Reproducing it is honest — it is the harness’s job to reproduce go test’s execution
environment, and CWD is part of that environment exactly as GOROOT and the env are.
But there is no cheap subset, and the reason is worth recording. Deepening the run directory to
<runRoot>/src/<full import path> costs nothing and fixes the SHAPE — and closes none of the four,
because every one of them needs CONTENT at the reconstructed ancestor: bin/go.exe for testenv,
doc/godebug.md for godebugs, the package’s own .go sources for go/build’s ImportDir, the
sibling package’s sources for io/ioutil. So the remedy really is the full synthetic-GOROOT
staging the board suspected, it changes the execution contract for all 122 banked packages at once,
and it interacts with the staging path that feeds the input-digest manifest. Design-with-user, not a
breadth lane’s — and NOT a disclosure, for the reason already recorded: it is satisfiable at a layer
go2cs owns.
RESOLVED 2026-08-13 (lane
claude/synthetic-goroot-class) — four of the six bank, and the class was TWO roots, not one. Design:DESIGN-package-ancestry-view.md. The remedy is an ANCESTRY view, not a synthetic GOROOT, and the difference was measured rather than argued.PackageAncestrystages GOROOT’s content from its top level down to the package — sibling directories as links, files as hard links, the path to the package materialized, the package’s own directory real copies — under a working directory that gains thesrclevel this section correctly predicted it needed. GOROOT itself keeps pointing at the real installation.Why not the synthetic GOROOT this section expected. A linked mirror is not walk-equivalent to the real tree: Go reports a junction from
Lstatas an irregular file, sofilepath.WalkDirsteps over it rather than descending. Measured against Go 1.23.1 on a mirrored root — a walk counting*.gzunder GOROOT finds 0 where the real tree has 4, and a walk ofsrc/unicodereports 1 entry against the real 19. Two ALREADY-BANKED packages walk GOROOT that way (compress/gzip’s issue14937,path/filepath), so repointing GOROOT would have REGRESSED them. Reads through a junction are faithful and every member of this class resolves against its working directory, so leaving GOROOT real costs nothing here. The feared “changes the execution contract for all 122 banked packages at once” did not materialize: nothing about GOROOT resolution moved.Banked:
go/parser173/173,io/ioutil28/28,internal/testenv7/7,internal/godebugs1/1 — +209 verdicts, the arc’s ledger paid in full and then some (the deferral price this board tracked was 167 + 12).Two corrections to this board’s own rows, both from reading the sources rather than the verdict names — the same methodology note §go/parser 6/173 already earned:
go/parser’s initializer reads../printer/nodes.go, notparser.go. That is the whole design question: its own sources would be served by staging the package directory; a SIBLING package’s are not.internal/godebugsneeds more thandoc/godebug.md. Past that read,TestAllrunsgo list -f={{.Dir}} std cmdand reads every.gofile it names — a working toolchain, not a staged file.The residue is a SECOND root — GOROOT-IDENTITY — and it holds the two that did not bank. Both need the importing/asking file to sit under the GOROOT the toolchain itself uses, which no ancestry view can provide and which repointing GOROOT cannot fix either (the child
goresolves its own GOROOT from its executable location — measured:go listreturns real-GOROOT paths withGOROOTset to a mirror).
go/build57/58, unchanged and now precisely attributed.TestLocalDirectory’sImportDir(cwd)derives the import path by relating cwd to the GOROOT the process REPORTS.internal/coverage/cfile4/16 — same count, but a root MOVED, which is why it was re-measured. The module-resolution facet is closed:TestIssue59563TruncatedCoverPkgAllno longer dies ongo.mod file not found, it runsgo test -coverpkg=allto completion and emits a full profile, failing now on CONTENT (wanted 1 found, got 0) — a real coverage question, no longer infrastructure. The internal-import facet stands:use of internal package internal/coverage/slicewriter not allowed, because the stagedharness.gois outside the tree the toolchain resolvesinternal/...within. Read the row as one GOROOT-identity failure plus one content failure, with the nineTestCoverageApissubtests still shadows of their parent.Two host defects surfaced on the way and are fixed here, both general and both previously masked:
- PATH fidelity.
go testPREPENDS$GOROOT/binto the test binary’s PATH (measured:PATH[0]is$GOROOT/bin,exec.LookPath("go")resolves there). The pipeline now does the same, beside its existing GOROOT export. On a machine carrying two installations of the same Go version this is the difference between pass and fail forinternal/testenv.t.TempDir()placement. It sat under the WORKING directory; Go’s sits in the system temp, with nogo.modabove it. The staged ancestry putssrc/go.modabove the package tree, so it is hoisted to the run root. This unmasked a false pass:go/build’sTestImportPackageOutsideModulewants “go.mod file not found in current directory or any parent directory” and was getting it only because the old sandbox had nogo.modANYWHERE.And one wall behind the wall. With its initializer working,
go/parserran and died on an uncatchableStack overflow.—TestParseDepthLimitdrives Go’s ownmaxNestLevof 100,001 levels deliberately, ~400k converted frames, which the host’s 256 MB per-test thread served only if every frame fit in 671 bytes. Raised to Go’s own 1 GB ceiling (reservation is address space; pages commit on demand). Any lane hitting a deep-recursion wall elsewhere should suspect this constant before suspecting the conversion.
net/textproto also re-measures unchanged at 25 of 26 — still the want-ZERO
canonicalMIMEHeaderKey allocs = 816 against the AllocsPerRun-reports-BYTES shim, still not a
disclosure candidate under ruling #1.
Escalation — InterfaceInheritance fails on master, and it is not this lane’s
The full behavioral suite gating this arc came back 554/554 transpile + compile + target, 527 of
528 output, with one failure: InterfaceInheritance, map[:2 :1] against Go’s map[:1 :2].
Confirmed pre-existing, by restoring src/core/golib and src/core/reflect to the merge base
(7c7bc7d69) and re-running the project filtered — it fails identically there. It is also
deterministic, not flaky: six consecutive runs give byte-identical output. The mechanism is
internal/fmtsort.compare’s Interface arm, which orders two keys of differing dynamic type by
comparing their type descriptors as VALUES — in Go a Kind Pointer compare of descriptor
ADDRESSES, which the linker assigns in declaration order. go2cs’s canonical interned
reflect.Type has no such ordering, so the pair sorts by whatever box identity gives. Worth
deciding deliberately rather than patching: this is an ordering Go’s own documentation treats as an
implementation detail, so the guard may be asserting something go2cs can only match by luck.
encoding/asn1 — the fourth charter row, re-measured: 34 of 38, and the tag root DID close
The board carried encoding/asn1 at 28 of 38 with the standing hypothesis that it shared
crypto/rsa’s DER/tag root. Re-measured on this branch it is 34 of 38: six rows closed on
their own, which is the hypothesis confirmed — the repaired tag handling reached here too. It is
still not bankable, and the converted test artifacts were deliberately NOT committed, per the
policy that test sources bank only when a suite validates.
What the hypothesis got WRONG is the shape of the remainder. The four survivors are not one root waiting on one fix; they are four, and three of them belong to areas other lanes already own:
TestMarshalError— the TYPED-NIL class, and it is r58b’s.panic: interface conversion: interface {} is nil, not *big.IntinsidemakeBody. Go asserts a nil*big.Intout of an interface and the assertion SUCCEEDS, yielding a typed nil the marshaller then rejects with its own error; go2cs’s_<T>sees an untyped nil and panics instead. That is exactly the stateclaude/r58b-typednilis bounded to atValue.Interface(). Re-measure this row first when r58b lands — it is a free second witness for that arc, on a package r58b is not otherwise touching.TestUnexportedStructField— a reflection-bridge FIELD-FLAG gap, distinct from the map/naming pair this lane closed. Go expectsUnmarshalto RETURNstructure error: struct contains unexported fields; go2cs returns<nil>and then panics inmustBeAssignable. So the read-only flag is not propagated onto aValuereached through an unexported field:CanSet()answers true where Go answers false, asn1’s own guard never fires, and the write runs on tosetKinded. The guard is asn1’s, but the defect isflagROpropagation inValue.Field, so it will surface anywhere a package probes settability rather than trusting it.TestMarshal#37 — one byte, and it is the tag.300302010aagainst310302010a:0x30SEQUENCE emitted where Go writes0x31SET. Thesetfield parameter is not reaching the emitted tag inmakeField. Narrow and self-contained — the likeliest single-row win of the four.TestCertificate— nested slice-of-slice-of-struct.sequence tag mismatch, and the RDN name comes back EMPTY ([]where Go has the full[[{[2 5 4 6] XX}] …]). The only one still unattributed below the surface message.
L6 (2026-08-11) closes the last two of those bullets with ONE fix, and it is neither the converter nor
makeField. Thesetfield parameter reaches the emitted tag correctly —TestMarshalWithParams, which is theasn1:"set"PARAMETER path, passed throughout.TestMarshal#37 istestSET([]int{10}), the TYPE-NAME path:getUniversalTypeselects SET over SEQUENCE onstrings.HasSuffix(t.Name(), "SET")and nothing else. The bridge’srtype.Name()gated onGoReflect.ElementType(st) is not null— a proxy for “unnamed composite” that is equally true of a DEFINED container — so everytype S []T/[N]T/map[K]V/chan T/*Tin the corpus reported no name.PkgPath(), reading the same managed nesting, answered"main"for the same types, which is a pair Go’s model cannot produce and is what named the defect. Fixed withGoReflect.HasGoName, the managed stand-in for the descriptor’sTFlagNamedbit, mirroringGoTypeNamearm for arm.
TestCertificateis the SAME root, and is hereby attributed: itsRDNSequenceis a[]RelativeDistinguishedNameSET, so the inner elements were emitted as SEQUENCEs and the RDN came back empty — the “unattributed below the surface message” bullet needs no separate investigation. Measured A/B on one machine, same tree, same GOROOT: 35 of 38 before, 37 of 38 after, the remainder beingTestUnexportedStructFieldalone (L7’sflagROgap). The board’s projected “36 of 38 when the tag row closes” was one row low for this reason.⚠ Two follow-ons for whoever plans next. (1)
abi.Type.HasName()is stillfalsefor every synthesized descriptor, sointernal/reflectlite.rtype.Name()— the ordinary converted body, which gates on it — answers""for EVERY type, strictly worse than whatreflecthad. It is dormant (reflectlite’s consumerscontextanderrorsuse onlyString/Kind/Comparable/AssignableTo/Implements), so it was recorded rather than fixed: populating the bit also changesdirectlyAssignable’sT.HasName() && V.HasName()short-circuit, currently over-permissive in both packages, which is a corpus-wide assignability change and not a naming one. (2) The measure was taken on the laptop’s Go 1.23.2 GOROOT against the corpus’s pinned 1.23.1; the denominator was verified as 38 test functions, unchanged between the two patch releases, and both sides of the comparison read the same sources — so the per-test agreement is sound and only the absolute count is developmental. Coordinator re-gates on the pinned machine.
The final sweep, recovered after the hardware failure (2026-08-10)
This lane was parked mid-sweep when the coordinator machine died, so the verdict was lost with it.
Re-run FILTERED over the lane’s own banked and re-measured rows on a replacement box: 8 packages,
137 verdicts, 8 pass / 0 fail — go/ast 9, syscall 62, go/printer 45, net/http/fcgi 12,
go/format 4, internal/fmtsort 3, internal/profile 1, runtime/internal/math 1. The last three
of those are the rows this pass struck through as already-banked, so the strikethroughs are now
gate-backed rather than argued. internal/fmtsort and go/printer were added on purpose beyond the
lane’s own list: the bank’s real blast radius is the reflection bridge, and internal/fmtsort is the
direct consumer of the MapKeys/MapIndex pair this lane moved into it.
⚠ The crash-save wip commit contained NOTHING that belonged. All 22 files classified as
standing aftermath and were dropped: nine production .cs in the -tests-closure restore family
(the Δio alias and the root-qualification escape), four package_init.cs carrying the
initᴛᴛtests hook, three -text-marked compress/testdata fixtures showing a pure CRLF flip —
and six log/slog/internal/benchmarks files that were 100% NUL bytes. That last group is a new
shape worth naming: NTFS committed each file’s SIZE and lost its DATA in the power failure, and the
sizes match the committed content’s CRLF-smudged length exactly, byte for byte, across all six.
So the package had no real drift at all — a crash-save git status can be dirty for reasons that
are neither a converter change nor a documented phantom, and a size-vs-content check separates them.
r57c — archive/zip banks 98/98; the “performance row” was a WRONG ASYMPTOTE in @string (2026-08-09)
Banked: archive/zip 98/98, no disclosures. Roster 121 → 122 of 215 (56.3 % → 56.7 %),
13,890 → 13,988 matching verdicts, 50 disclosed (unchanged). Lane-local arithmetic against this
branch’s base; the coordinator union-recomputes at merge.
The board’s own archive/zip section closed by naming two routes to a bank — “either a measured
deadline (the index/suffixarray route) or the string/slice throughput work that would make the
measurement moot” — and advised a lane to “start by timing the C# host solo with no deadline rather
than re-rooting anything”. That advice was followed exactly, and it is what found the defect: the
host, timed solo with no deadline, still had not finished after 45 minutes against Go’s 13.2 s.
A constant-factor throughput gap does not do that. Profiling it (dotnet-stack, both worker threads,
every sample) put the entire cost in one frame — detectUTF8 → Buffer._Memmove.
detectUTF8 is the ordinary Go rune walk, and the emission is a faithful 1:1 rendering of it:
for i := 0; i < len(s); { r, size := utf8.DecodeRuneInString(s[i:]); i += size }
The defect was underneath, in the REPRESENTATION. A Go string header is a pointer plus length
into shared immutable storage, so s[i:] is O(1) and allocates nothing. @string held a bare
byte[], so its range indexer had to materialize the sub-string: O(n), with an allocation. Over a
65,535-byte file name that makes the loop accidentally quadratic — ~2.1 GB copied per call, two
calls per record, 32,768 records. Not slowness; the wrong asymptote. @string now carries the
header’s real shape (backing array, offset, length) and slices into a window; the backing array is
PRIVATE, so a consumer reading it instead of the window is a compile error rather than a wrong
answer, which is how the last three raw-array readers were found. Detail in the two signed commits
and in ConversionStrategies-Reference.md.
TestZip64LargeDirectory: >45 min (never completed) → 20.2 s, against Go’s 11.3 s.
What this row costs the sweep, and why the deadline entry is still needed
The pipeline builds Debug, where the non-inlined golib window accessors cost ~22x, so the banked
suite is minutes rather than seconds and archive/zip joins hash/maphash and
index/suffixarray in run-validated-sweep.ps1’s $longTimeouts — authored at '20m', raised
to '30m' at merge: the i7-5820K re-measure below left 20m only ~35 % headroom, and a deadline
is a safety net against a hung run, never a performance assumption. The 391 s figure was measured on the
reference desktop (391 s for the whole suite). Re-verified on the replacement box (i7-5820K
6C/12T, ~3x slower, with two sibling lanes building): the suite ran 792.6 s, of which
TestZip64LargeDirectory alone was 774.0 s. Still inside 20 m, but with only ~35 % headroom on a
slow loaded box — so if a future sweep reports archive/zip as an empty verdict, suspect the
deadline before suspecting the package. Two remedies landed 2026-08-10: $longTimeouts is now
a FLOOR rather than an override, so a larger -TestTimeout raises these entries like it raises
every other package (a smaller value still loses to the table) — until that fix the table won
unconditionally and the flag was silently ignored for exactly the four packages that need it (an
i7-5820K sweep reported hash/maphash and crypto/dsa as FAIL … package timeout after 00:30:00
and re-running at 60m died at 30:00 again, while the same package’s pipeline driven by hand at
60m validated its banked 22/22). And the floors themselves were recalibrated to the slow host at
merge (maphash/dsa 60m, suffixarray 120m; archive/zip’s 30m stands on its 774 s measurement),
so a bare sweep passes on this machine class with tight nets kept on the other 121 packages.
The crash-save classification refines r57b’s NUL rule
r57b found the first instance of crash corruption in a wip(...) snapshot — files that are 100 %
NUL bytes, NTFS having committed each file’s SIZE and lost its DATA — and proposed the
size-vs-committed-content check as the test that separates it from real drift. This lane’s wip
carried five more (go/internal/gccgoimporter/{ar,gccgoinstallation,importer,package_info,parser}.cs)
and that test would have MISSED all five: their sizes do not match the committed content, they
match the intended new content, because each was mid-rewrite by a -tests-closure emission when
the machine died. The reliable discriminator is therefore the content itself — a file that is
100 % NUL is corruption, whatever its size — with the size comparison demoted to a corroborating
detail. git diff --stat names them for free: a .cs reported as Bin <old> -> <new> bytes is
never legitimate converter output.
The rest of the wip classified into the standing families with nothing unexplained: the
-tests-closure production restore family (Δio alias in bufio/bytes/crypto, the
global::go.* root escape in crypto/md5, and one initᴛᴛtests package_init hook in
crypto/ecdh), three -text compress/testdata CRLF phantoms, and a stray 16 MB src/go2cs.exe
build artifact at the repository root — which is worth one line of its own: the converter’s
gitignore entry is /src/go2cs/go2cs.exe, so a binary built one directory up is tracked, and a
crash-save picks it up.
Handoffs — neither owned by this lane
ByteSeqAllocationTests’@stringbound is stale-LOOSE. The window makes a sub-string allocation-free, so the test’s asserted upper bound now passes with room to spare rather than measuring anything. It belongs to r58a’s allocation-counting arc, which is the lane that will have a true count to tighten it against.InterfaceInheritance/ValueOf(Type).Pointer(). The one behavioral failure seen while gating this lane was proven pre-existing on master (reproduced at the merge base), and its root is in the reflection bridge — r58b’s area. This lane’s only touch on that file is a comment.
Coordinator ratifications — the alloc-count rulings, user-confirmed (2026-08-10)
The user ratified the r58a merge’s disclosure rulings, with the honest assessment carried here so the decision and its evidence stay together:
crypto/rsaTestAllocations— DISCLOSED, ratified. The true count is 340,756 objects per run against a budget of 10 — five orders of magnitude, dominated by managed big-integer arithmetic no golib optimization can remove. Squarely the provably-cannot-satisfy class the 38 existingalloc-profiledisclosures pin.net/http/internal(2 objects vs budget 1) andmath/bigTestNewIntAllocs(1 vs 0) — NOT disclosED, ratified. These are near-budget lower-bound counts: nothing proves the extra object is unavoidable rather than a golib inefficiency, and disclosing them would launder an optimization target as an impossibility — the exact move r56d refused. They stay characterized and undisclosed until either an optimization closes them honestly (the ж-box arc is the likely instrument) or a proof of unavoidability emerges.log(4 vs 1) is moot for banking regardless: itsTestAllfails on theruntime.Callerarchitectural arc the board already carries.path/filepathstays banked at 61 — ratified. The six symlink rows are host-privilege- dependent and BOTH runtimes agree in both states; a general roster mechanism for host-conditional verdicts is commissioned as a chip rather than a count bump that would false-red unprivileged boxes.
Also ruled in the same pass: the ж-box arc is commissioned chip-class AFTER the near-miss harvest (the counter gives it an exact instrument); the init-ORDER arc starts as a characterization scout; GOROOT-tree-reproduction is DEFERRED past 75% (four packages against a harness-contract change re-validating all 126); r59 runs as the next dedicated lane after the harvest with backlog 24 riding its regen; NuGet 1.23.1.6 is approved after the day’s final consolidated sweep (release push user-owned).
Harvest r60 — the post-1.23.1.6 collection (2026-08-11)
The first release-gated harvest, run across two machines the same day the fixes landed. Every item below supersedes its older census row; the roster is the authority as always.
encoding/asn1 — BANKED 38/38 (roster 127, 74cec76e3). The full arc: 28/38 under one
hypothesized DER root → r57a’s StructField.Tag bridge closed six for free → r57b split the
remainder into four TRUE roots → r58b’s typed-nil packing took one, L6 took two
(TestMarshal #37 AND TestCertificate — via reflect.Type.Name() blanking defined container
types, NOT the hypothesized converter SET-tag defect; converter unmodified), L7 took the last
(StructField.PkgPath unset on the type side — NOT flagRO; the value side was already refusing
writes correctly). The two lanes’ residual sets were exactly complementary and neither could
observe the union; the pinned-machine measurement confirmed 38/38.
crypto/internal/edwards25519 — measured 54/55 on merged L4+L7 (was 0/55, a whole-package
cctor casualty). L4’s tuple-spec relocation lets the package RUN; L7’s array-dims fix greens both
quick.Check rows with real [32]byte/[64]byte values. Sole residual: TestAllocations
(109 objects vs want 0 — the ж-box arc’s row; NOT disclosed per the near-budget ruling). NOT
banked. ⚠ The fix’s production emission (a new ordered package_init.cs) is deliberately
UNCOMMITTED — additive-only drift owed to r59’s queued whole-corpus regen, per the
no-casual-regens rule.
math/big (224/226) and nistec (2,195/2,200) — refresh deliberately SKIPPED. Nothing in
this harvest touches their residual roots (the want-zero counter rows and TestMulUnbalanced’s
truthful performance measurement — all ж-box territory). Their recent measurements stand; a
refresh would have measured the same defect-free packages against the same open arcs.
New open items from the lanes’ re-attributions
rtype.PkgPath()answers “main” for an UNNAMED struct where Go answers ““ — the sibling of L6’s Name() fix, found by L7’s cross-validation, fixed by neither. Latent until a consumer compares package paths of anonymous types.abi.Type.HasName()is false for every synthesized descriptor — dormant, but populating it changesdirectlyAssignable’s short-circuit corpus-wide; wants its own lane, not a drive-by.StructField.Anonymous+ embedded-field ORDER — go2cs-gen emits promoted-embed boxes after declared fields, so bridge walk order differs from Go’s declaration order. One increment, needs a demonstrated consumer.Out(i).Len()for a func returning a fixed-size array — no attribute position exists on a ValueTuple; recorded, unowned.- A bridge-minted method value keeps a dims-less descriptor — adjacent to L7’s fix, same remedy shape, needs a consumer.
Machine traps (both cost real time on laptop-1; both now protocol)
-testsself-location does NOT fire when a deployed root exists: a valid machine-global%USERPROFILE%\go2cspre-empts self-location (“an explicitly configured working root always wins”), and the resulting version-mixed build dies withMSB4006 circular dependency ... unsafe.csproj— which reads exactly like a corpus defect and is not one. EVERY pipeline measurement passes an explicit-go2cspath <checkout>\src.Copy-ItempreservesLastWriteTime, so a copy-aside/restore A/B leaves the restored file OLDER than build output and MSBuild skips the rebuild — surfacing as a phantomCS0117against source that plainly contains the member. Touch restored files;git checkoutstamps fresh.
Process rulings recorded in passing
- Version flips belong to the release ritual; hand-fixes own numbers. The io/rsa badge regeneration pinned 1.23.1.6 pre-release (safe only because Phase 1 had already bumped); the clean rule is mid-cycle regens pin the published version and the ritual does all flipping.
push-nuget.ps1’s badge preflight is blind to a MISSING badge — a banked package with no Tests badge ships silently (crypto/rsa nearly did). Hardening owed: a banked proof page with no corresponding badge claim fails as loudly as a wrong one.- L5’s publish-stamp follow-up stands: the preflight still proxies “published” via the build release; the repo-recorded stamp written by the publish ritual (feed query advisory-only) is the ruled remedy.
- The proof pages are an L8-guarded surface too: a sweep on a mispinned toolchain rewrites
docs/validation/current/*’s Go-version stamp with counts unchanged (observed:encoding.binary.md1.23.1 → 1.23.2, restored not banked). L8’s guard covers the sweep; this is the second thing it protects.
Backlog: the AOT full-trim column, deferred with its reasoning (user query, 2026-08-11)
Full trimming (vs the suite’s TrimMode=partial) would shrink the AOT binary and some of its
startup/memory floor – but it strips exactly the metadata golib reaches reflectively (fmt’s
formatting, sort’s Interface
One sharpening from the user (2026-08-11): hoisted string literals materialize at package init (module initializers run eagerly at assembly load), so the hoist cost – deliberately moved to startup to kill per-use allocations and UTF16->UTF8 conversions, and still the right trade by the StringMatch numbers – COMPOUNDS with the eager-closure cost. The lazy-package-init arc therefore recovers both at once: unused packages skip their init() AND their literal materialization. The two items are one lever.
ж-box arc stage A1 — the census is in, the projection HOLDS (lane L3, 2026-08-11)
The zero-emission census the design gated every golden move behind ran corpus-wide, three GOOS
targets, on laptop-1 (⚠ go1.23.2 — developmental until the pinned-machine re-derivation, which is
ONE command). Full report: CENSUS-zh-box-a1.md. The instrument is
-ref-census over the new classification pass (refLoweringAnalysisOperations.go, wired into all
three drivers, -debug census per package; CNR byte-identical — analysis only). The headline
verdicts, so the next session doesn’t have to open the report to know where the arc stands:
- §3.6’s projection CONFIRMED on the recommended (§10.3 hoisted-temp) branch — fiat lowers
96/96 pointer params and 150/158 address-taken locals; every class the ~7k-P256-residual
projection zeroes is statically covered (row-5 conversion sites measured wider than the
panel’s 16: 20 fiat + 7 edwards25519-scalar, all
conv-of-addressat lowered positions). The branch question stays CLOSED. - The §3.3 emission table covers the corpus: zero
other-vetoargument shapes at lowered positions on all three targets (the caller-side strip never fired outside unit tests). - Per-GOOS delta = 25 positions across 5 packages, ALL already layout-L3 (runtime 14, net 7, os 2, filepathlite 1, syscall 1) — zero new L3 memberships; A2’s merge churn is within-package.
- Hand-own audit closes small: census re-measured 49 marked / 41
*_impl.cs/ 59 distinct (the design’s 44+26 moved, as CLAUDE.md says it does); 17 textual candidate references resolve to 4 real (crc32’scastagnoliShift/slicingUpdate, runtime’sgetLockRank/lockWithRankMayAcquirefrommfinal.cs) + 13 comment collisions. A2 remedy: an X5 declared-in-hand-own arm (mechanical, reuses the marker probe) + a 3-function curated list; zero hand-own edits owed. - A′’s prize measures small (§10.1’s checkpoint input): strict exported candidates 64 funcs / 69 corpus call-site records / +68 lowered positions (564→632). The design’s 347 was a looser screen (census L1 = 354 ≈ 347; buckets and the constructor-shaped count deviate — reported, not reconciled; re-derive on the pinned machine before deciding A′).
- Findings that reprice A2 details, none the branch: the §3.2-vs-§3.3 defer/go tension resolved
toward §3.3 (defer/go never strips the callee; the caller-side param mirror vetoes as
X2-defer-arg);p[:]pointer-slice is a real non-D shape (19 params, tagged, copyFieldElement the fiat instance);SetBytes’sinkeeps its box under the design’s ownx[:]predicate (§7’s probe rows gain a small named residual term);edwards25519/field’sfeMul/feSquarestrip on the trailingcarryPropagate()METHOD call — the second fiat family’s field half is B′ constituency, its scalar half lowers fully.
Pinned-machine re-derivation (coordinator, go1.23.1, 2026-08-11): the instrument reproduces. Exact agreement on every aggregate the census itself prints — 25 positions / 5 packages all-already-L3, hand-own 49/41/17, exported return-shaped 3 — with exported candidates 62 vs the laptop’s 64, exactly the GOROOT patch-release shape the developmental flag existed for. A2 is GO on the go-ahead ratifications recorded with its opening.
L9 wave, desktop share — six hypotheses measured, six honest answers (2026-08-11 night)
The stale-census re-measure wave’s items 1–2 plus the seam canary, run on the pinned machine against post-r59 master. One census was stale in each DIRECTION, and the rest attributed:
debug/dwarf— the ONE-ROW-AWAY row was stale-BANKED: the package has been on the roster at 40 for some time; tonight’s pipeline re-validated it 40/40 on merged master (a free re-validation, not a bank). The r57b lesson re-taught at the coordinator’s own expense: roster first, board second — every census row is a hypothesis, including the hypothesis that the package is still unbanked.net/textproto25/26 unchanged, with its number UPGRADED:TestCommonHeadersnow measures 3 objects per run vs want 0 (was 816 shim-bytes) — the common-header fast path materializes three golib allocations Go’s interning avoids. Near-budget: not disclosed, not banked; a precise interning/ж-box target. CLOSED by L11 (2026-08-12): banked at 26/26 — the counter’s 3 (and the bytes behind them) decomposed measured, not inherited: 2 obj/216 B inOnce.Do’sof()chain, 1 obj/40 B in them[string(b)]probe-key materialization, plus 560 B/call of deadBigInteger.Parsefrom the folded mask local. All three fixed at their own layers (hand-owned atomic/once, convertertmpstringemission, converter big-const hoist); the L9 note’s “likeliest instant bank” hypothesis was wrong in the instructive direction — the counter-shim fix alone changed the NUMBER, not the verdict.mime/multipart~11/52 — real roots, census confirmed:TestMultipartSlowInputcrashes the host mid-suite (multipart_test.cs:172), and theReadFormlimits family (TestReadFormEndlessHeaderLine,TestReadFormLimits,TestReadForm_MetadataTooLarge) plusTestQuotedPrintableEncodingfail on content. Needs its own characterization lane. CLOSED by L12 (2026-08-13): banked at 52/52 — and this bullet was WRONG in every particular, which is the part worth keeping. The lane’s own baseline on137b86552measured 7 of 52, not ~11, reproducing the ORIGINAL census (above) rather than this one. There is no host crash: all 52 verdicts are reported andTestMultipartSlowInputis simply one moreinfrastructure-error. Nothing “fails on content”: the four named rows are the PARENT tests whose subtests errored — theinternal/coverage/cfile“read the row as three failures, not twelve” shape, one bullet further down this very section. And the ~41 tests said to be hidden behind the crash are not hidden; they run, and all 41 carry the IDENTICAL exception (readMIMEHeader: external (assembly or cgo) function is not implemented), with no second root anywhere in the log. Read together with thenet/textprotobullet above — whose “likeliest instant bank” hypothesis was also wrong — this section’s own methodology note earns another entry: a differential summarized from verdict NAMES invents structure that the exception TEXT does not support. Attribute from the roots, and count parent rows as shadows before calling them a family.go/parser6/173 — the CWD class gains its FIFTH member: the test package’s static initializer runsvar src = readFile("parser.go")and panics (performance_test.cs:15), so the host dies before test one. The deferred synthetic-GOROOT arc now holds 167 verdicts for this package alone on top of its original four — its deferral price has materially risen and deserves re-prioritization when the wave settles.go/doc24/85 unchanged — attributed: theTest/AllDecls/*golden-file family RUNS and fails on CONTENT (not infrastructure) — a real doc-extraction root, not the CWD class.net/rpc4/13 — the seam canary reproduces the diagnosis byte-for-byte:index out of range [0] with length 0atarray.cs:280viasockaddr(syscall_windows.cs:881) — the exact r57b stack, on demand, same-day baseline for L10. The three held rows (httputil/httptest/cookiejar) stay held, as ordered.
L9 wave, laptop-G share — item 3’s two toolchain-exec rows, both censuses CONFIRMED (2026-08-11 night)
Item 3 (internal/coverage/cfile, go/internal/gcimporter) measured on laptop G (Ryzen 5 PRO
6650U, 6C/12T, pinned go1.23.1) against dc78eb63c, -tests -test-action all -test-timeout 30m
with an explicit -go2cspath. Wall clock: cfile 199 s, gcimporter ~630 s (its C# suite
alone 235 s) — both cheap, as the wave promised.
Neither census was stale. Both reproduce their first-census number to the verdict — 4 of 16 and 399 of 583. These two rows are walled, not stale, and nothing in the nine landed fixes touches either wall. The item’s stated expectation — “both exec the Go toolchain, so expect the GOROOT-tree/cwd class in the residue” — is right for one and wrong for the other, which is exactly why the row was measured rather than inherited.
internal/coverage/cfile — 4 of 16 confirmed; the WORKING-DIRECTORY class gains its SIXTH member
Go emits 16 verdicts, the host 7; 4 match (TestAuxMetaDataFiles, TestCoverageSnapshot,
TestTestSupport pass/pass; TestIssue56006EmitDataRaceCoverRunningGoroutine skip/skip). Three
top-level tests fail — and nine of the twelve mismatches are one root’s shadow:
TestCoverageApis t.Fatals in its setup before any of its nine t.Run subtests register, so they
compare Go="pass" against C#="" (absent). Read the row as three failures, not twelve.
All three are the established converted-host WORKING-DIRECTORY class (§The converted-host
WORKING-DIRECTORY class — why no cheap subset exists), reached because each shells out to the Go
toolchain with a path relative to CWD whose MEANING depends on sitting under $GOROOT/src. The host
runs in <temp>/go2cs-tests/<pkg>/<guid>/cfile (TestHost.CreateRunDirectory, fixtures staged
beside it); go test runs in $GOROOT/src/internal/coverage/cfile. Two facets:
- Internal-import visibility —
TestCoverageApis,TestApisOnNocoverBinary.buildHarnessrunsgo build … testdata/harness.go(emitdata_test.go:123, RELATIVE) andharness.goimportsinternal/coverage/slicewriter. The toolchain grants internal visibility by the importing FILE’S LOCATION, so the staged copy under temp is refused:testdata\harness.go:10:2: use of internal package internal/coverage/slicewriter not allowed. The staging is not at fault — the error names line 10 column 2, which is that import, so the file was found and read; only its location was wrong. - Module resolution —
TestIssue59563TruncatedCoverPkgAllsetscmd.Dir = filepath.Join("testdata", "issue59563")(emitdata_test.go:510) and runsgo test -coverpkg=all. Neither GOROOT’s copy nor the staged one carries ago.mod; Go’s run succeeds because the parent walk reaches$GOROOT/src/go.mod(module std), while the host’s walk reaches the filesystem root with none →go: go.mod file not found in current directory or any parent directory.
A textbook confirmation of that section’s “no cheap subset” ruling, and it extends the content
list: alongside bin/go.exe, doc/godebug.md and sibling sources, a synthetic GOROOT must also
reconstruct src/go.mod, and must be a location the toolchain accepts for internal imports.
Both facets are satisfied by that one arc and by nothing smaller — a per-test accommodation would be
dishonest here, since these tests are measuring the real toolchain’s behavior. +12 verdicts to
the arc’s ledger, on top of go/parser’s 167.
go/internal/gcimporter — 399 of 583 confirmed; NOT the cwd class — it is go/types generics
583 verdicts both sides, 399 match, 184 mismatch, and every mismatch is inside ONE top-level
test: TestImportTypeparamTests (its parent row plus 183 subtests), all Go="pass" C#="fail".
Everything else agrees, including TestImportStdLib’s 303 matching subtests and 82 of the
typeparam subtests themselves (68 pass, 14 skip).
The predicted GOROOT-tree/cwd class does not appear, for a precise reason worth keeping:
gcimporter’s toolchain execs root their paths at testenv.GOROOT(t) — ABSOLUTE, and the pipeline
exports that GOROOT to both sides deliberately (runCommandWithTimeout) — whereas cfile’s are
cwd-relative. Same “execs the toolchain” premise, opposite outcome.
The real root sits one layer down, in the test’s own ORACLE half. Each subtest runs
compile → importPkg → checkFile in that order (gcimporter_test.go:184-187), and every
failure is in checkFile, i.e. go/types.Check over the source file. So compile and
importPkg both SUCCEEDED for all 183 — the converted gcimporter read the gc export data fine,
and 303 stdlib packages import correctly — and the converted go/types then failed to type-check
the generic source handed to it as the reference. The subject of the test is healthy; its
yardstick is not.
| Shape | Count | Evidence |
|---|---|---|
| Bogus type errors on TYPE PARAMETERS | 92 | absdiff2.go:70:9: cannot use a.Value_ (variable of type T constrained by orderedNumeric) as T value in return statement — T judged not assignable to T. Also cannot assign map[K]V to map[K]V, S does not implement I[byte], cannot infer S, cannot assign []int to [5]int |
| Nil-pointer panic inside the checker | 91 | re-panicked value is runtime error: invalid memory address or nil pointer dereference, surfacing through check.cs:430 |
⚠ The panic stacks are the RE-PANIC, not the fault. check.cs:430 is the default: throw
panic(p) arm of handleBailout — a faithful conversion of Go’s own re-panic for a non-bailout
recover value — so the frames shown are the bailout path (430 → GoFrame.Run → 435 → 414) and the
ORIGINATING frames are gone. Anyone working this must instrument the re-panic or disable the
recover; taking check.cs:430 as the defect site will waste the lane.
Whether the 92 and the 91 are one root or two is not established here and must not be assumed.
The dominant signature — a type parameter judged not identical to itself — would plausibly produce
both, but that is a hypothesis, and this week’s board has a four-for-four record against inherited
ones. go/types is unbanked and carries no board row of its own; on this evidence it has earned
one, and go/internal/gcimporter’s 184 rows are downstream of it — the row moves when
go/types generics do, and not before. Nothing in gcimporter itself is implicated.
The nil-panic is NOT confined to generics — and there is now a minimal reproducer (2026-08-13)
Filed from claude/types-errors-delta-rename, which cleared internal/types/errors’ build blocker
and so ran that package’s suite for the first time. Both its top-level tests die on this exact
signature — runtime error: invalid memory address or nil pointer dereference re-surfacing
through check.cs:430 — and neither is a generics test.
Both fail at the SAME call, codes_test.cs:70 (walkCodes), which is
conf.Check("types", fset, []*ast.File{file}, info) over codes.go itself — reached before
any Example snippet is ever type-checked. That input is about as small as the checker’s surface
gets:
- no imports at all, so
importer.Default()is never invoked — the importer is out of suspicion; - no generics —
codes.godeclares exactlytype Code intplusiotaconst blocks. (Five[T any]greps in the file are all inside doc comments; each was checked line by line.)
So the 91-count nil-panic class cannot be wholly explained by the generics hypothesis, and the “one root or two” question above now has real evidence on the two side. Anyone taking this on should prefer this reproducer over gcimporter’s typeparam corpus: one import-free non-generic file, two failing tests, ~7 s to the panic, versus 583 verdicts behind a compile+import pipeline.
⚠ The re-panic warning above applies here unchanged — check.cs:430 is handleBailout’s faithful
default: panic(p) arm and the originating frames are gone. Instrument the re-panic or disable the
recover first; this lane re-derived that independently before finding the note, which is some
evidence of how naturally the stack misleads.
ROOT FOUND AND FIXED (2026-08-13, claude/go-types-checker-wall) — an interface MAP KEY compared by wrapper identity
The reproducer above did its job: the fault is one golib defect, it is not generics, not the importer, and not the ж-box or scheduler arcs’ territory.
How it was reached. The re-panic warning is correct and the stack really is useless, but the
originating frames were never destroyed — they are snapshotted at the point the .NET exception is
ADOPTED as a Go panic (RuntimeErrorPanic.TryAsPanic → PanicException.CaptureThrowSite). Dumping
PanicTrace there named the site in one run:
at go.ж`1.op_OnesComplement(ж`1 value) in golib\ж.cs:957
at go.go.types_package.dependencyGraph(map`2 objMap) in go\types\initorder.cs:233
at go.go.types_package.initOrder(ж`1 Ꮡcheck) in go\types\initorder.cs:33
at go.go.types_package.checkFiles(...) in go\types\check.cs:515
(Why InheritThrowSite did not already surface this: the re-panic at check.cs:430 is thrown
INSIDE handleBailout’s own try, so the emitted catch … when (GoFrame.IsPanic(…)) snapshots its
own one-frame site first, and InheritThrowSite’s ??= then declines the origin. Worth a separate
diagnostic fix — it is what makes this stack mislead every reader — but it is not the wall.)
The mechanism. initorder.cs:233 is for d := range objMap[obj].deps, and objMap[obj]
missed, returning a nil ж<declInfo> that ~ dereferenced one frame later. In Go the lookup
cannot miss: every key of M came from ranging objMap. The two differ only in the STATIC interface
the key is held in — Object going in, dependency coming back out of obj.(dependency).
Go compares interface values by (dynamic type, dynamic value), and that ONE relation serves both ==
and map-key lookup. In the conversion they had diverged: emitted ==/!= route through
builtin.AreEqual, which unwraps the three adapter tiers, while map<K,V>’s backing Dictionary
used the DEFAULT comparer and compared the WRAPPERS — and an interface value’s wrapper is not stable,
since asserting to a narrower interface yields a different adapter object over the same receiver box.
Equal but unfindable: Object(d) != obj answered correctly (AreEqual unwrapped) while
objMap[d] missed. Only the compile-time ImplementGenerator adapters ever carried the
unwrap-and-hash contract; the runtime shells go2cs-gen builds for a duck-typed assert
(Δ<iface><T>, Δ<iface>ᴛObj) override neither Equals nor GetHashCode.
The fix is golib-only and centralizes rather than duplicates: GoEqualityComparer projects
AreEqual as an IEqualityComparer<TKey> and hashes the UNWRAPPED root (the same rule the
compile-time adapters already used), installed by map<K,V> only when typeof(TKey).IsInterface or
TKey is any — so concrete keys keep EqualityComparer<TKey>.Default’s devirtualized path, the
test being a JIT-time constant per instantiation. Restating the relation in each generated shell was
rejected: AreEqual is golib’s single definition of Go equality and a per-shell copy is exactly the
drift that produced this. Guarded by the InterfaceAssertionMapKey behavioral test; documented in
ConversionStrategies-Reference.md under An INTERFACE map key compares by Go equality.
Measured movement. internal/types/errors BANKS at 155/155 (0 mismatches, status:
validated) — the row’s full 155 verdicts, from 0 before. The pre-fix host produced two
nil-panics and zero verdicts; post-fix it type-checks codes.go and every Example snippet, and the
subtests that pass include the generics family (NotAGenericType, WrongTypeArgCount,
CannotInferTypeArgs, InvalidTypeArg, InvalidInstanceCycle, MisplacedTypeParam).
go/internal/gcimporter moves 399 → 475 of 583 (+76; mismatches 184 → 108), and the split the
row’s census left open — “whether the 92 and the 91 are one root or two is NOT established here and
must not be assumed” — is now answered: TWO. The nil-panic class is entirely gone (zero
invalid memory address and zero check.cs:430 occurrences across the whole 583-verdict run); every
one of the 108 residual mismatches is the OTHER class, the bogus type-parameter errors, unchanged in
signature (absdiff2.go:70:9: cannot use a.Value_ (variable of type T constrained by orderedNumeric)
as T value in return statement). That second root — a type parameter judged not identical to itself —
is still open and is not this fix’s, and gcimporter’s remaining rows stay downstream of it. The
row does NOT bank; test sources deliberately not committed.
go/types itself: NOT measured — one BUILD blocker, and it is not the wall. With the wall down,
go/types’ own suite was taken through -tests -test-action all -test-timeout 90m for the first
time. The conversion fully succeeds — all 34 _test.cs files emit — and the host build produces
exactly one error:
check_test.cs(200,53): error CS0839: Argument missing
defer(ᴛ1 => throw panic(errΔ2), , ref ᒐ); // Go: `defer panic(err)` (check_test.go:170)
Mechanism. visitDeferStmt.go:62-66 forces the temp-param lambda form for a BUILTIN callee, so
paramCount == 1 and lambdaContext.callArgs is sized 1 — but panic is not rendered as a call.
It emits throw panic(<expr>) with the ORIGINAL argument expression inlined in the lambda body, so
the ᴛ1 substitution never happens and callArgs[0] is never filled, leaving the empty argument slot
above. Note the near neighbours are fine: defer delete(w.seen, typ) (infer.go:715,
typestring.go:121) converts and compiles today, which is why the corpus never surfaced this — the
defect is specific to the one builtin that is a throw, not a call.
Remedy shape. Prefer routing panic through the same temp-param substitution as every other
builtin — defer(ᴛ1 => throw panic(ᴛ1), errΔ2, ref ᒐ). The tempting alternative (drop the lambda
param and let the body capture the expression) is WRONG: Go evaluates a deferred call’s arguments at
defer time, so capturing errΔ2 would report whatever the variable held when the frame unwound.
Small and well-scoped, but it is a CONVERTER change and therefore owes its own CNR + full behavioral
gate, which is why this lane characterized it rather than folding it into a golib-only commit.
Adjacent, NOT measured — a plausible sibling worth one probe. go2cs-gen’s struct-equality template
compares an INTERFACE-typed field with C# == and hashes it with HashCode.Combine(field, …) — e.g.
go/types’ own graphNode: this.obj == other.obj. On a C# interface == is reference equality, so
a struct carrying an interface field would compare by ADAPTER identity rather than by Go’s (dynamic
type, dynamic value) — the same class as this fix, one level up. Unlike the map defect it is at least
self-consistent (Equals and GetHashCode are both reference-based), so it produces no equal-but-
unfindable split and no nil-panic; it would show as two structs holding the same dynamic value
comparing unequal. This is read off the generated template, not observed in a failing test — it
may well be masked in practice, and it is stated here as a candidate to measure, not as a finding. The
remedy shape, if it reproduces, is the same one used here: route the field through AreEqual and hash
GoEqualityComparer.RootOf.
⚠ One environmental note for anyone re-running these by hand: four subtests
(InvalidPkgUse, UnusedImport, UndeclaredImportedName, UnexportedName) use
importer.Default() and fail with could not import fmt … ($GOROOT not set) when the host exe is
launched directly. That is the known GOROOT-resolution class, not a checker defect — the pipeline
exports GOROOT to both sides, and under it the package is 155/155. Running the host bare gives
150/156.
Scout batch 1 — twelve never-run packages (2026-08-11)
Twelve packages that had never linked a test host were taken end to end through -tests -test-action all
on the pinned coordinator (i7-5820K, go1.23.1). Nothing banks, and no row is disclosed — the
2026-08-10 ratification stands, and none of these is an alloc row anyway. The value is the census: five
packages produced a verdict map, seven died before producing one, and all twelve now have a named root.
(Six produce one since 2026-08-12 — net/rpc/jsonrpc’s build-blocker was fixed and the package runs;
its row and §”Five converter defects” item 3 below carry the measured result. The “nothing banks”
above has since been overtaken by one row: net/rpc/jsonrpc banked 9/9 on 2026-08-14, three
converter defects after this census first named it.)
| Package | Go verdicts | C# matched | Outcome | Root / attribution |
|---|---|---|---|---|
internal/chacha8rand |
4 | 3 | near-miss, 1 row | array-SHAPE reinterpret (array.cs:280) — the seam L10 works through sockaddr, here with no kernel in it |
runtime/metrics |
2 | 1 | near-miss, 1 row | a //go:linkname PUSHED into a test package is unwired; the implementation exists in converted runtime |
internal/singleflight |
BANKED 5/5 | the scheduler arc retired the wall — TestDoAndForgetUnsharedRace converges in 1.2 s (was 28.7 min), banked inside the DEFAULT deadline with no $longTimeouts entry (SCHED-S2) |
||
net/http/httptrace |
2 | 2 | ✅ BANKED 2026-08-29 (2|0) — reflect.MakeFunc hand-owned |
the funcType reinterpret had no managed record behind it (refined root 2026-08-29: funcLayout panics funcLayout of non-func type <nil> on the zero box the reinterpret recovers); MakeFunc is now Value.Call’s exact inverse over the descriptor’s carried System.Type — reflect/makefunc_impl.cs + golib GoReflect.MakeGoFuncDelegate, guarded by the ReflectMakeFunc behavioral test |
internal/unsafeheader |
6 | 0 | rooted (architectural) | the package’s entire subject is the slice/string HEADER LAYOUT that golib deliberately does not have |
unique |
19 | 0 | ⚠ REGRESSION — flagged, not decided | host dies: Fatal error. Internal CLR error. (0x80131506) in System.GC.Collect ← runtime.GC() ← drainMaps. Board has this package at 4 of 19 (r43e) |
internal/types/errors |
155 | 0 | go/types |
the Δ-renamed-imported-type defect is fixed (claude/types-errors-delta-rename); the package now BUILDS and RUNS, and both tests then die on the go/types checker nil-panic — see the sub-row below |
internal/fuzz |
52 | 52 | ✅ BANKED 2026-08-14 — two blockers deep | alias-to-anonymous-struct (CorpusEntry) cleared the BUILD (79f2b5e62); the run then died at flag provided but not defined: -json because worker_test.go’s TestMain calls flag.Parse(). The host’s TestFlagBridge now declares its own command line on flag.CommandLine the way testing.Init() declares -test.*, and the suite validates whole |
net/rpc/jsonrpc |
9 | 9 | ✅ BANKED 2026-08-14 — three converter defects deep | embedded-pointer promotion (2026-08-12), the non-trailing-default switch lowering (2026-08-13), and a fixed-size array’s LENGTH reaching reflect through a METHOD’s POINTER parameter (2026-08-14). The last was one test, not the three recorded here: TestBuiltinTypes sorts first, its goroutine panic killed the host, and the other eight recorded no verdict — which is also why the host now reports a goroutine panic with its traceback and flushes what it has. Never socket-walled |
testing/fstest |
7 | — | converter defect | a defined type over ANOTHER package’s named map type — the emitted two-hop conversion has only one hop |
internal/syscall/windows/registry |
6 | — | converter defect | the internal-test partial class is emitted non-static, and in this package nothing else declares it |
embed/internal/embedtest |
7 | — | not a candidate | test-only Go package: there is no production package for the host to reference |
⚠ The comparison JSON’s status string is not a diagnosis. Seven of these read
conversion-blocked, but that value only records “no result file appeared”. Five of the seven are C#
COMPILE failures of the converted test sources, one (embed/internal/embedtest) is a package that
cannot have a production assembly at all, and unique built and ran and then killed its own
process. Read the errors array, not the status. (The recorded PS 5.1 ConvertFrom-Json duplicate-key
trap did not fire in this batch — a census of all 24 verdict maps found zero case-insensitive
duplicate keys — but the raw-text parse was used regardless.)
The address-reinterpret seam owns three of the five packages that ran
internal/chacha8rand, internal/unsafeheader and net/http/httptrace fail at one architectural
place: a raw address reinterpreted into a managed shape. They are worth reading together because
each names a different facet of it, and fixing the narrowest one fixes none of the others.
internal/chacha8rand— 3 of 4, and the wall is already written down in this repo.TestBlockGenericpanicsindex out of range [0] with length 0atarray.cs:280, insidesetup(chacha8_generic.cs:66) reached fromblock_generic(:141). The emitted form isvar b = (ж<array<array<uint32>>>)(uintptr)(new @unsafe.Pointer(Ꮡbuf))for Go’s(*[16][4]uint32)(unsafe.Pointer(buf))over a[32]uint64— the reinterpret materializesdefault(array<…>), a LENGTH-ZERO array, exactly as(*SockaddrInet4).sockaddrdoes in r57b/L10. The other three rows pass becauseblockis hand-owned:chacha8_impl.cs’s header comment already states that forwarding to the convertedblock_generic“is also not available — it opens the*[32]uint64output buffer as(*[16][4]uint32)(unsafe.Pointer(buf)), an array-SHAPE reinterpretation that a managed nested-array view cannot reconstruct.” The hand-own routed around the wall for every consumer; the package’s own test walks straight into it. ⚠ This is a STRONGER form than sockaddr’s and may not fall with it: sockaddr needs a 2-byte window onto auint16field, chacha8rand needs a[16][4]uint32view of a[32]uint64— different element type, different rank, same allocation. Its value to L10 is that it is a pure, kernel-free witness: one converted function, no socket, no syscall, reproducible in seconds, so it makes a far cheaper regression test thannet.Listen.internal/unsafeheader— 0 of 6, and this one is architectural rather than a defect. The package exists to describe the memory layout of a slice/string header, andgolib’sslice<T>and@stringare managed containers with no such layout by deliberate design.TestWriteThroughHeaderreportsalias of "Hello, checkptr!" constructed via String = ""andalias of slice(…) with cap 16 has cap 0— a header assembled from aDatapointer aliases nothing.TestTypeMatchesReflectTypefails one layer earlier and for a second reason: it prints0x2839d0e63b0.Data has type 0x2839d0e6878, but …, i.e. the reflect surface renders a type as a raw address (the knownrtype.String/TypeFornaming row). Recommend this package be marked NOT A CANDIDATE rather than left on the queue — banking it would requireslice/@stringto have Go’s memory layout, which is the one thing the runtime design says they will not have.net/http/httptrace— 0 of 2, and the seam is embedded-struct promotion. Both tests die onpanic: runtime error: invalid memory address or nil pointer dereferenceatabi_package.ΔFuncType.get_Type(), throughж.FieldRefWrappers…getFieldRef(golib/ж.cs:545).(*ClientTrace).compose(trace.go:179-202) walks its own struct with reflect and callsreflect.MakeFuncfor every func-typed field; reflect reaches the func type by reinterpreting a type descriptor as*abi.FuncType, and the generatedpublic partial ref abi_package.Type Type => ref ᏑʗType.ValueSlotthen dereferences a promoted embedded reference that the reinterpret never populated. Same family as the two above — a managed shape conjured from an address — with promotion rather than an array as the victim. httptrace is a two-test package sitting entirely behind it. RESOLVED 2026-08-29 — banked 2|0. The httptrace bank lane refined the root one level down: past the promoted-embed read,MakeFunc’sfuncLayout(ftyp, nil)panicsreflect: funcLayout of non-func type <nil>because every managed-backed descriptor is a plainheap<Type>box (abi.synthType) and theReinterpret<abi.Type, funcType>()recovers a zero box. The remedy did not fight the seam — it removed the reinterpret entirely:MakeFuncis hand-owned asValue.Call’s exact inverse (reflect/makefunc_impl.cs, over golibGoReflect.MakeGoFuncDelegate’s expression-compiled per-delegate-type factory), reading the delegate type off the descriptor’ssysTypecargo.reflect/iter.cs’s rangefuncSeq/Seq2become live MakeFunc consumers with it. Variadic func types stay a loud refusal (the Span tail — the reverse ofInvokeVariadic’s family trampolines, unbuilt for want of a consumer), exactly asValue.CallSlicerecords.
runtime/metrics — 1 of 2, and the missing row is a linkname DIRECTION
TestDocs passes. TestNames reports infrastructure-error:
System.NotImplementedException: runtime_readMetricNames: external (assembly or cgo) function is not
implemented, from a PartialStubGenerator stub. The implementation is not missing — converted
runtime/metrics.cs:800 holds internal static slice<@string> readMetricNames() carrying its
//go:linkname readMetricNames runtime/metrics_test.runtime_readMetricNames comment, and
runtime/metrics/description_test.cs:35 holds the matching bodyless
internal static partial slice<@string> runtime_readMetricNames();. The two are never joined, because
this linkname PUSHES a symbol from a production package INTO a test package rather than pulling one in.
That is a narrower job than the getg-style “external (assembly or cgo)” rows it superficially
resembles: both halves already exist in the corpus and the wiring is the whole task, and doing it
takes this package to 2 of 2, i.e. to a bank. Cheapest bank-per-unit-work row in the batch.
BANKED 2026-08-12 at 2 of 2 — but “the wiring is the whole task” was measured FALSE, by three
layers. The push wiring landed general (a linknamePushTargets row whose key simply spells the
_test package path — convertTestVariant already sets currentPackagePath to the external
variant’s own PkgPath, so a production package pushing into its own test package needs no new
machinery; the registry guard learned that an external test package’s source lives in the base
package’s _test.go files). Behind it, in the order the reruns surfaced them:
metricsLock→semacquire1→getg— the pushed body’s first call was the runtime sleeping semaphore. Hand-owned at themetricsLock/metricsUnlockboundary (manualConversionFuncs+managed_impl.cs, aSemaphoreSlim(1,1)); the same fix unblocksreadMetricsfor every future consumer.readMetricsLocked’s slice-header reconstruct —metrics.Readcrosses by raw first-element address and the runtime rebuilds a[]metricSampleover it: the L10 address-reinterpret seam, measured here as garbage@stringnames out of the fabricated slice. NOT chased through L10: the crossing is re-shaped instead —runtime/metrics/sample.csis hand-owned andReadmarshals names in / computed(kind, scalar, pointer)out through the publicruntime.readMetricsManagedshim (theregisterPoolCleanupprecedent), preservingreadMetricsLocked’s batch semantics; theruntime_readMetricspush row is recorded UNHONORABLE with the reason naming the hand-own. The metrics TABLE,initMetrics, and every compute closure stay auto-converted.- Two
allmwalkers in the compute closures —NumCgoCallandtotalMutexWaitTimeNanosboth walk the scheduler’s m-list (nil here where Go always has m0). Hand-owned with their honest managed answers (no cgo calls exist → 0; the two real wait-time counters minus the per-m profile walk);consistentHeapStats.readlikewise (no Ps, no allocator deltas → the zero snapshot, theReadMemStatsprecedent).
The package is therefore no longer an L10 witness — its seam instance is routed around, not fixed — and chacha8rand remains L10’s cheapest kernel-free reproduction.
internal/singleflight — 4 of 5, one test that does not come back
Go passes 5. The host passes 4 and then reports {"action":"timeout","elapsed":1800,"output":"package
timeout after 00:30:00"}; TestDoAndForgetUnsharedRace gets no verdict at all, so the emitted
JUnit reads tests="4" failures="0", which must not be read as a clean run.
The test (singleflight_test.go:145) loops forever with no iteration bound: it launches n=1000
goroutines that each call g.Do on one key whose function sleeps d, and if calls != 1 — i.e. if
the goroutines did not all park inside g.Do before the first call completed — it doubles d and
retries. Whether the converted host converges slowly (each retry costs at least d, and d doubles)
or never converges is not established here and should not be assumed; distinguishing them needs a
run with a much larger -test-timeout and a print per iteration, which this batch did not spend. What
is established: the other four rows agree, and the package is one row from a bank behind a question
about goroutine parking latency, not about singleflight itself.
Convergence measured 2026-08-12 — it CONVERGES: 20 iterations, 28.7 minutes, and the package validates 5/5
The instrumented run the paragraph above asked for was spent (branch claude/singleflight-convergence,
laptop lane — Ryzen 7 PRO 6850U, 16 hardware threads — solo, go1.23.1; NOT the coordinator the scout
used, which matters below). Method: the -tests pipeline split at its action seams —
convert, then a hand edit to the STAGED converted singleflight_test.cs adding a per-iteration
stderr print (iteration, d, calls, shared, ThreadPool.ThreadCount at iteration start/end,
pending-queue count, spawn/wait timings), then build, then compare -test-timeout 90m — the same
phases -test-action all runs, split only so the edit could sit between conversion and build (the
manifest’s input digest covers Go sources, options and runtime sources, not the emitted .cs, so the
staleness gate accepts an instrumented staged copy by design). Everything was reverted after: the
measurement emitted a real proof page — the comparison genuinely printed Validated 5 tests against
go test, 5/5 matched — and that page was deliberately discarded, because a hand-instrumented host
banks nothing.
The verdict: converges. TestDoAndForgetUnsharedRace passes at iteration 20 (d=524s), test
elapsed 1720.8s; the other four rows total ~0.3s, package wall ≈ 1725s — a 75-second margin
under the 30-minute deadline the scout run died at. go test’s whole package on the same machine:
0.040s (the race test itself 0.01s — Go converges on its first iteration; the gap is ~10^5).
shared was 0 at the converging iteration, so the assert the loop protects passed: the 28.7 minutes
is all scheduling, and none of it is singleflight.
The census (run 2; run 1, without the pool columns, matched it iteration-for-iteration through 15):
| iter | d | calls | pool start→end | wait |
|---|---|---|---|---|
| 1-3 | 1-4ms | 20, 8, 6 | 12→258 | ~0.02s |
| 4-10 | 8-512ms | 4-9 | 258→258, flat | ~4×d |
| 11-15 | 1-16.4s | 4-7 | 258→354 (~+0.9/s) | ~3-4×d |
| 16 | 32.8s | 4 | 354→162 | 98s |
| 17 | 65.5s | 3 | 162→221 | 197s |
| 18 | 131s | 2 | 221→567 | 262s (=2×d) |
| 19 | 262s | 2 | 567→75 | 524s (=2×d) |
| 20 | 524s | 1 | 75→1002 | 524s (=1×d) |
Three mechanisms, each owning one phase of the table — and none of them is parking LATENCY:
- The goroutines that miss the window never parked slowly; they never STARTED.
spawn_s=0.00every iteration (queueing 1000 work items is instant), andcalls ≈ ceil(1000 / live pool): the queue drains in WAVES of pool size, and each post-wave batch dispatches only after the previous call completed — which by construction mints a fresh call. Hencewait ≈ calls×dthroughout, and no value ofdhelps while the pool is small: the tail is sitting in the pool queue, not inwg.Wait. - Iterations 1-10 are pinned at golib’s own floor.
Goroutine.StartisThreadPool.QueueUserWorkItem(golib/runtime/Goroutine.cs:64); the min-thread floor of max(4×cores, 256) (golib/builtin.cs:78) is why the pool leaps 12→258 in three iterations and then sits EXACTLY there — below the floor, creation is on demand; above it, only the starvation gate injects (~0.9-1.8/s), and an iteration must hold starvation ≥ ~1s continuously to trip it, which is why growth begins only atd≥1s. - Idle-thread retirement FIGHTS the injection, so capacity cannot accumulate across iterations.
Iteration 16 ends 192 threads BELOW its start — 162 live, under the min floor, which governs
injection aggressiveness, not keep-alive — and iteration 19 ends at 75: once
dexceeds the pool’s ~20s idle timeout, every thread left idle through the final wave’s sleep is culled. Convergence therefore arrives only when a SINGLEdis long enough for in-sleep injection alone to field all 1000: iteration 20 starts at 75 live and injects ~930 during one 524s sleep, every arrival parking into the one in-flight call.
Why the scout run got no verdict: the finish sits at 28.7 min ± the injection rate, and a miss is quantized — the ladder doubles, so slipping one iteration moves the finish to ~55 min (2245s+1049s), two to ~107 min. 1800s lands inside the measured run’s iteration-19/20 window, so any marginally slower gate — the scout’s host was the i7-5820K coordinator, not this laptop, and the rate is a runtime heuristic, not work — pushes convergence one rung up and the deadline eats the test mid-iteration, verdict-less, exactly as recorded. That the same ladder shape produced “4 of 5 + a consumed deadline” on one machine and “5/5 in 28.7 min” on another IS the fragility finding: the two runs differ by one rung, and a rung is a doubling.
Remedy recommendation — two options, priced:
- A
$longTimeoutsfloor of 60m banks the row as measured (28.7 min with 2× margin, and it survives a one-iteration slip at ~55 min by five minutes; only 120m survives two). ⚠ Priced honestly, this floor differs in KIND from the four standing ones: maphash’s 15-min-vs-7.6s is deterministic work on a slow host, while this finish time is a race against the .NET thread pool’s injection-vs-retirement heuristics with 2× penalty steps — under the standing solo-sweep rule it should hold, but it is the first deadline asked to cover a heuristic rather than work. It also adds ~29-55 min to EVERY full sweep for one row. - The durable path is the one golib already names.
builtin.cs:75-76calls the min-thread floor “a mitigation, not a scheduler: programs parking thousands of goroutines remain out of reach until a cooperative scheduler exists (documented divergence)”. n=1000 sits exactly on that line, and this table is the divergence’s first quantified witness — the row is ultimately a scheduler-arc row, and any floor is a bridge across it, not a fix for it.
Per the errand’s charter nothing was chased: measurement only, aftermath reverted, no bank.
RESOLVED 2026-08-14 — the scheduler arc landed and the row banks 5/5 in 1.2 s (SCHED-S1/S2)
The recommendation above (“the durable path is the one golib already names”) was taken. Goroutine.Start
now creates one dedicated background thread per goroutine instead of queueing a ThreadPool work item, and
the min-thread floor retired in the same commit (DESIGN-cooperative-scheduler.md §5.2, OQ1/OQ3). No
$longTimeouts entry was ever landed — OQ9 resolved in the ratifying direction, and the bridge is now
dead as designed.
Re-measured on the same laptop, solo, go1.23.1 — the machine that produced the 1720.8 s table above, so this is a clean A/B and not a hardware difference. Same method, same instrumentation seams:
| iter | d | calls | pool start→end | goroutines live (peak) | spawn | wait |
|---|---|---|---|---|---|---|
| 1 | 1ms | 165 | 3→3 | 4 | 141ms | 141ms |
| 2 | 2ms | 112 | 3→3 | 10 | 141ms | 141ms |
| 3 | 4ms | 83 | 3→3 | 20 | 125ms | 125ms |
| 4 | 8ms | 54 | 3→3 | 41 | 125ms | 140ms |
| 5 | 16ms | 35 | 3→3 | 85 | 125ms | 125ms |
| 6 | 32ms | 30 | 3→3 | 45 | 125ms | 157ms |
| 7 | 64ms | 6 | 3→3 | 392 | 125ms | 140ms |
| 8 | 128ms | 1 | 3→3 | 1001 | 110ms | 172ms |
Converges at iteration 8, d=128ms, test elapsed 1.2318 s (was iteration 20, d=524s, 1720.8 s — a
1,397× reduction). Package wall 1.54 s against go test’s 0.040 s, so the residual gap is ~31×, not ~10⁵.
Read the pool column: it is flat at 3 for every iteration. Every mechanism the original table
identified is now structurally absent rather than merely faster — there is no queue to drain in waves, no
floor to sit at, and no injection-vs-retirement race, because nothing Go-semantic touches the pool at all.
What remains is honest work: spawn ≈ 110-141ms is the cost of creating 1000 real threads (~125 µs each),
and the ladder now climbs only until d exceeds that spawn window — which is why it stops at 128ms. The
live-goroutine count is the new registry’s, and it returns to 2 after every iteration, so 1000 threads are
created and retired eight times over with no leak.
The fragility finding retires with the row: the finish no longer sits on a heuristic’s knife edge, so it cannot slip a rung between machines. Banked per the validated-package ritual (roster line, proof page, committed test sources) inside the DEFAULT deadline with ~3 orders of margin.
unique — a REGRESSION against this board’s own record, flagged for a bisect lane
The r43e-weak entry above records unique at 4 of 19 in a “2-minute run with 19 verdicts”, after
runtime.GC()’s hand-owned body learned clearpools’ unique arm. Today, against origin/master
(5fe713f98), the host produces zero verdicts: every TestHandle subtest starts, and the process
then dies with
Fatal error. Internal CLR error. (0x80131506)
at System.GC.Collect(Int32, System.GCCollectionMode, Boolean, Boolean)
at go.runtime_package.GC()
at go.unique_internal_test_package.drainMaps(go.ж`1<T>)
at go.unique_internal_test_package+<>c__DisplayClass21_0`1[[…testStringStruct…]].<testHandle>b__0
Reproduced twice, per the standing “re-run before believing a crash” rule — identical stack, the
naming subtest differing only because the subtests run in parallel. The scout’s own run of the same
package instead consumed its 30-minute deadline and was recorded by the pipeline as
conversion-blocked/dotnet timed out after 31m0s; a hang and an execution-engine crash are the same
“no verdicts” outcome from the pipeline’s point of view, so the recorded status understates this row
in both runs.
0x80131506 is COR_E_EXECUTIONENGINE — a GC-state fault, not a managed exception, and this board
already ties that exact string to the address model (see the ж<T>/EnsureStableAddress analysis
above, where a 4 KB write into freed heap surfaced as ExecutionEngineException and
Fatal error. Internal CLR error.). golib has moved materially since r43e — the @string window,
the allocation counters, the array-backing materializer — so a plausible story exists in more than one
place, and that is precisely why this is flagged rather than attributed. It wants a bisect between
r43e and 5fe713f98 with unique’s host as the probe, and it should be treated as higher severity
than an ordinary failing row: a memory-safety fault does not stay inside the package that reveals it.
RESOLVED (2026-08-12, unique-bisect lane) — bisected to the @string window; closed by a clone.cs hand-own
The culprit is fc6d8c179 (r57c-zipperf, 2026-08-09): “golib: a Go string is a WINDOW”. Bisect
over the 126-commit first-parent range 57abfe9e1..5fe713f98, unique’s pipeline as the probe:
r43e anchor GOOD (full 19-verdict map, the recorded 4-of-19-era shape), f2b80a766 (#63) GOOD,
90059385b (#80, r57b) GOOD, 515cea127 (#81, r57c) BAD, and fc6d8c179 — the window commit,
probed directly against its own parent’s lineage — BAD. The window is present in every BAD tree and
absent from every GOOD one.
Mechanism — one defect, two eras. Converted unique/clone.cs:28 rewrote every string field of a
value IN PLACE the way Go does — (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&value)) +
offset)) — emitted as a read/write of @string through (uintptr)Ꮡvalue + offset: an interior
address of the movable ж<T> heap box plus a Go ABI offset, against a CLR object whose field
layout is unrelated to Go’s ABI. For any T whose strings sit at nonzero offsets (testStruct’s b
at +8, testStringStructArrayStruct’s s[1].a at +16 — the latter landing entirely OUTSIDE the
8-byte array<T> ref that is the whole CLR value), the access lands on the box’s OWN fields.
- Pre-window era:
@stringwas one 8-byte reference, so every mislaid store was a single aligned pointer-sized slot holding a valid object — type-confusing (part of the “v0 != v1” noise the r43e record shows) but nothing the collector trips over. - Window era:
@stringis 16 bytes (byte[]+ offset + length), so the same store’s INTEGER tail lands in an adjacent GC-scanned reference slot of the box. The next collection walks a garbage pointer and the runtime fail-fasts — anddrainMapsFORCES that collection viaruntime.GC()in everyTestHandlesubtest, which is why the host died with zero verdicts (or hung: same corruption, discovered differently under load).
Mechanism proven without unique: a ~25-line program against golib alone — ж<TT> over
struct { array<SS> s } where SS is struct { @string a }, two @string stores at Go offsets
0/+16 through (ж<@string>)(uintptr), then GC.Collect — dies with the identical
0x80131506-at-GC.Collect stack on window-era golib, and on pre-window golib (7c7bc7d69)
survives both collections and only faults when the program itself reads the type-confused slots
back. The A/B isolates the window as the escalation and proves the writes were corrupting values all
along.
The fix (this lane): src/core/unique/clone.cs is hand-owned ([module: go.GoManualConversion]),
the documented S1 managed-referent remedy. clone<T>’s contract — “MAY update string values found in
value with a cloned version” — is a retention optimization, never a semantic requirement, so the
hand-own clones the T == string case (right-sized copy via stringslite.Clone, no address
arithmetic) and returns aggregates unchanged; the only divergence from Go is retention (an interned
aggregate’s strings keep sharing their original backing). makeCloneSeq and the builders stay in
their converted form — pure descriptor arithmetic, still covered by TestMakeCloneSeq. Post-fix
census on the c33b3a67e base: the host runs to completion — 5 of 19 matched (the five
TestMakeCloneSeq passes; r43e recorded 4), 13 fail / 2 infrastructure-error, all on the
pre-existing roots this section already names (the [GoType] equality gate’s v0 != v1 rows, the
eface subtest-naming pair, the nil-vs-empty cloneSeq DeepEqual rows). unique still does not bank;
the REGRESSION row is closed.
Residual, for the ж-box arc: the corpus has seven more (ж<@string>)(uintptr) sites, all in
converted runtime (map_faststr.cs:487 and iface.cs:461 are the two WRITE sites; alg.cs,
error.cs, arena.cs×3 read) — dead or near-dead under the managed model’s own map/iface, but the
same shape, and any future caller inherits the same two-era hazard. The general
(uintptr)ж<T>-for-managed-T transient-address model is unchanged by this fix; that is the ж-box
arc’s charter, not this lane’s.
Five converter defects, each with a named mechanism
None of these five is a wall; all are ordinary emission bugs, listed with the evidence a fix needs.
-
FIXED 2026-08-13 (internal/types/errors— a Δ-renamed IMPORTED type is spelled with its bare Go name.claude/types-errors-delta-rename).codes_test.csemittederr._<Error>(ᐧ)(38,42) andnew Info(…)(65,22) → CS0246 ×2, againstgo/types’ΔError/ΔInfo, while the test’s ownpackage_test_info.csalready mintedtypesꓸError/typesꓸInfoand left them unused.The diagnosis above was one step coarse, and the correction is the useful part. It is not “the cross-package path” — the QUALIFIED cross-package spelling was always right (
shapelib.Marker{…}→new shapelibꓸMarker(…), verified directly). The loss is the bare ident, which only a DOT import produces (codes_test.gohas. "go/types"). Type-DRIVEN positions — declaration, parameter, conversion, field — resolve fromtypes.TypethroughgetCSharpTypeName/getScopeCheckedTypeName, both of which already consultedforeignAliasedTypeName; that is whyvar mu Mutexthrough a dot import has worked sinceDotImportRenamedPackage. The two AST-IDENT type positions did not: a type-assertion target and a composite-literal type render throughconvIdent’sisTypearm, which returned the bare sanitized Go name and consulted nothing. That arm now routes through the same lookup. Guarded byDotImportRenamedType; CNR byte-identical across 588 packages.Blast radius, for whoever wonders whether a corpus regen is owed: none. The only production (non-test) dot import in the converted corpus is
go/types→internal/types/errors, and that package publishes ZERO collision renames;generrordocs.go’s dot import ofgo/typesis//go:build ignoreand is not converted. The fix therefore cannot move productionsrc/core— it reaches-testsconversions only.The row did not validate, and its remaining blocker is NOT its own. With the build blocker cleared the host builds and runs; both top-level tests then fail with the
go/typeschecker nil-panic re-surfacing throughcheck.cs:430— Go 155 verdicts, C# 0.internal/types/errorsis now a second dependent of the unbankedgo/typesrow, exactly asgo/internal/gcimporter’s 184 rows are. Not banked; test sources and proof page deliberately not committed. -
internal/fuzz— an alias whose RHS is an anonymous struct does not reach the test compilation.minimize_test.cs:26andworker_test.cs:52emitFunc<struct{Parent string; Path string; Data []byte; …}, error>— raw Go syntax in a C# file — producing CS1031/CS1525/CS1003 cascades. Go declarestype CorpusEntry = struct{…}(fuzz.go:463), and the production emission handles it correctly by lifting it:fuzz.cs:8carriesglobal using CorpusEntry = go.@internal.fuzz_package.CorpusEntryᴛ1;.global usingis compilation-scoped, and this is a production-reference test project, so the alias never crosses into the test assembly —package_test_info.cscontains noCorpusEntryline at all. Remedy shape: re-emit the lifted alias into the test compilation, or spellCorpusEntryᴛ1.✅ FIXED (
79f2b5e62, converter) and the package is now BANKED 52/52 (2026-08-14). Clearing the build did not validate it: the host then died before any test onflag provided but not defined: -json, becauseworker_test.go’sTestMaincallsflag.Parse()and nothing had ever declared the host’s own command line on the convertedflag.CommandLine. That second blocker is a whole CLASS, remedied host-only bysrc/core/testing/TestFlagBridge.cs— see the flag-bridge entry above for the class census, the measurements that ruled out atesting→flagproject reference, and where the other three members stand. -
net/rpc/jsonrpc— promotion from embedded POINTER fields is invisible toImplementGenerator.all_test.go:310declarestype pipe struct { *io.PipeReader; *io.PipeWriter }, whoseRead/Write/Closecome entirely by promotion. The generated…pipe-global__go.io_package.ReadWriteCloser-ptr.g.cscalls.Read/.Writeonж<pipe>, finds no member, and binds an unrelated extension method instead — CS1929 namingio_package.Read(ref io_package.LimitedReader, slice<byte>)andio_package.Write(ref io_package.OffsetWriter, …). An error message pointing atLimitedReaderfrom a jsonrpc test is the giveaway that this is an overload-resolution fallthrough, not a missing reference.✅ FIXED 2026-08-12 (
ImplementGenerator+InterfaceImplTemplate, generators only — zero converter-emission change). The hop forwarding was gated to a struct with exactly ONE embedded pointer; with several, no hop was named and every promoted member fell through to the barem_box.M(…)/this.M(…)receiver. It now indexes the hop path per member, routing each to the UNIQUE embed declaring it — Go’s depth-1 promotion rule, with a name two embeds declare dropped rather than guessed (which is why*pipe.Close, declared on the struct, still wins over theCloseboth halves of the pipe declare). Each embed’s method set is read from local syntax where it is declared here and from METADATA where it is not, which is the whole jsonrpc case. Guarded by theMultiPointerEmbedPromotionbehavioral test; detail inConversionStrategies-Reference.md.⚠ This row is NOT socket-walled, contrary to the batch’s expectation. With the host building, the package RUNS end to end: 9 Go verdicts, 6 C# matched, 3 failed, no timeout and no netpoll involvement — because jsonrpc’s own tests never open a socket. They talk over the in-memory
pipeabove (myPipe()from twoio.Pipe()s, “Copied from package net”), which is precisely why that type exists in the file. Being undernet/rpcis not the same as reaching the kernel; the walled-list membership was inherited from the parent path, not measured. The three remaining failures share one root and it is a json defect, not an rpc one —TestBuiltinTypes,TestClientandTestServerall reportjson: cannot unmarshal array into Go value of type [1]interface {}, i.e.encoding/jsoncannot unmarshal a JSON array into a Go fixed-size array type (jsonrpc passes params as[1]any). The other six pass:TestMalformedInput,TestMalformedOutput,TestServerEmptyMessage,TestServerErrorHasNullResult,TestServerNoParams,TestUnexpectedError. Nothing banked and no roster change — the row moves from “build-blocked” to “one named json root from a bank”, and that root is worth checking againstencoding/json’s own suite before anyone spends a lane on it here.UPDATE 2026-08-13 — the json root is fixed, and it was WIDER than recorded here; jsonrpc now stands on a second, unrelated root. The defect was not in the reflection bridge or in
array<T>at all: it was the converter’sswitchlowering for adefaultclause that Go places before some of its cases (claude/json-fixed-array-unmarshal, guarded byJsonFixedArrayUnmarshal; full mechanism inConversionStrategies-Reference.md). Scope correction: convertedencoding/jsoncould not decode a JSON array into any target except a bareinterface{}— every[N]Tand every[]Ttook the error arm, not only fixed-size arrays. The same converter bug independently madeinternal/bisectreject every pattern it was given. jsonrpc’s 3 rows are unchanged in NAME but not in cause:TestBuiltinTypes,TestClientandTestServernow get past json and panicindex out of range [0] with length 0. Two things make this worth a fresh lane rather than a footnote: (1) the panic escapes on a goroutine and takes the host process down, so the package now records 0 verdicts where it used to record 6 — a full-suite run of this package reads worse than before even though the corpus is strictly better; run it filtered (-test.runover the other six) to see the 6 passes. (2) The obvious suspect is ruled OUT: a struct-typed field holding a[1]any, left at its zero value by a keyed composite literal that omits it, was probed directly (codec{name:…, id:…}→c.req.Params[0] = …, plus the bare-varand local-varshapes) and matches Go exactly — the generated constructor chain does run the= new(1)field initializer. No stack trace is available from the host: it printspanic: {message}with an emptyStackTrace, which is itself worth fixing, because a goroutine panic with no frame is the hardest possible diagnostic to act on.✅ CLOSED 2026-08-14 (
claude/jsonrpc-goroutine-panic) — the package VALIDATES 9/9 and is BANKED. Two landings, and the first one is what found the second.(a) The host no longer swallows a goroutine panic’s traceback. The frameless report was not a missing
catch— it was golib’s AppDomain backstop doing exactly its job: it prints the panic VALUE and exits 2, which is Go’s own report for a program and useless to a host running many Go programs in one process.Goroutine.ObserveUnhandledPanicnow lets a host WATCH a panic cross a goroutine root from an exception FILTER that always declines — so the fatal path stays byte-identical (Go fidelity, and the oracle keeps observing it) while the report is written with the stack still standing. The converted-test host installs one: it attributes the panic to the test whose goroutine it was, reports it as that test’s terminal FAIL with the full traceback, and FLUSHES the result files the fatal path used to discard whole. A goroutine panic now costs the tail of a run instead of all of it. Guards:GolibTests/GoroutineRootPanicTests(5 tests over the root’s whole policy — observed-and-still-escapes, the fault site surviving, a runtime-error panic, containment still taking non-panic exceptions, Goexit taking neither).(b) With the trace liberated, the panic rooted in one read — and it was ONE test, not three. The trace named
array.get_Itemunderall_test.cs’s ownBuiltinTypes.Array(i int, reply *[1]int), called throughreflect.Callfromnet/rpc’sservice.call. net/rpc allocates every reply from the method type alone —reflect.New(mtype.ReplyType.Elem())— and the[1]int’s LENGTH did not survive the trip, soNewbuilt a zero-length array and the callee’s first write panicked. Two hops were missing, both now closed: a METHOD’s func type is built from the method TABLE and never passes through a delegate, soGoReflect.MethodParamDimsreads the[GoArrayDims]stamps off theMethodInfodirectly; and the array sits behind a POINTER, so the converter stamps a parameter’s POINTEE dims and a pointer descriptor’s dims pass throughElem()unshifted. The converter half had to go intovisitFuncDecl’s REBUILT signature path, which is the one a*[N]Tparameter always takes — having a pointer parameter is itself what triggers the rebuild — and that same gap had been silently dropping the stamp from VALUE array parameters in heap-boxing functions too. Full mechanism:ConversionStrategies-Reference.md; guarded by the extendedReflectFuncArrayParamDimsbehavioral test (rpc’s shape in miniature) and byTestGoArrayDimsAttribute.The “three failing tests” were one.
TestClientandTestServernever failed on this root at all —TestBuiltinTypessorts first, its panic killed the host, and the other eight recorded no verdict. That is precisely the misreading (a) exists to prevent, and it is worth carrying as a rule: a package that records 0 verdicts has one failure and an unknown remainder, not N failures. Corpus footprint of the converter half, over all 592 behavioral packages: 5 declarations in 5 files, one line each. Gates: behavioral suite 566/566 + 540/540 output, CNR classified, GolibTests 102/102, convertergo test ./..., filtered sweepPASS net/rpc/jsonrpc 9. testing/fstest— a defined type over ANOTHER package’s named map type gets a one-hop conversion. Go hastype shuffledFS MapFSwhereMapFS map[string]*MapFile. The emission declares[GoType("global::go.testing.fstest_package.MapFS")] internal partial struct shuffledFS;and then writes((MapFS)(map<@string, ж<MapFile>>)fsys).Open(name)— CS0030 at (62,62), because the generated conversion offersshuffledFS→MapFSbut notshuffledFS→ the raw map the intermediate cast asks for.internal/syscall/windows/registry— the internal-test partial class is emitted non-static.export_test.cs:9openspartial class registry_internal_test_package {and then declaresinternal static error SetValue(this …registry_package.Key k, …)→ CS1106. Banked packages get away with the identical non-static opener only incidentally:sort/bytes/stringseach have a go2cs-genRecvGeneratorfile that re-declares the class aspublic static partial. Registry’s generated set coversregistry_packageandregistry_test_packageonly, so nothing supplies the modifier. The emitter should not be relying on a generator for it.
embed/internal/embedtest should be struck from the candidate list
CS0234: embedtest_package does not exist in go.embed.@internal. It never will —
$GOROOT/src/embed/internal/embedtest contains embed_test.go, embedx_test.go, testdata/ and
concurrency.txt and no non-test source. There is no production package, hence no production
.csproj in src/core, and the -tests emitter nonetheless writes
global using static global::go.embed.@internal.embedtest_package; plus a production project
reference. The tests here exercise embed, not a package of their own. Two follow-ups, both small:
teach the emitter the test-only-package shape (no production reference, no _package using), and
route the coverage to embed where it belongs. Until then this row is not a measure of anything.
ж-box arc stage A3 — the pinned measure: P256 lands at 8,528, under the wall (lane L3, 2026-08-13)
The §9 A3 row of DESIGN-zh-box-reduction.md, run whole on the pinned
laptop R (Ryzen 7 PRO 6850U, 32 GB, go1.23.1 exactly; ambient load one idle two-day-old MSBuild
node — effectively solo): the real pipeline on nistec and math/big, the r56d-method probe for the
unit targets, the two A2-owed confirmations from the 2026-08-13 sweep’s evidence, and the perf suite
with the AOT column. The measured numbers below supersede the r58a four-curve table and the
r58b/r60 math/big alloc-row measurements; §7’s table carries the same numbers in its A3 column.
The verdict measure. -tests -test-action all -test-timeout 30m on crypto/internal/nistec
(explicit -go2cspath, ~8 min wall): terminal results 2,195 matched / 5 divergent of 2,200 —
the identical arithmetic to the standing measure, the five being exactly the want-zero
TestAllocations rows. The counter’s numbers, against Go’s want of 0:
| Curve | obj/run (was, r58a) | B/run |
|---|---|---|
| P224 | 8,484 (264,540) | 746,678 |
| P256 | 8,528 (242,665) | 733,766 |
| P384 | 12,572 (471,424) | 1,198,070 |
| P521 | 17,090 (870,534) | 1,867,811 |
P256 8,528 ≤ 10,000: the §7 acceptance is MET on the recommended branch — −96.5 % from the bill, and BELOW A2’s approximate mirror (10,105 on the artifact-laden host; L11’s Once/tmpstring/ const-hoist landings plus the clean pinned host account for the direction). The §3.6 branch question stays closed; nothing re-opens.
The residual decomposes to the named classes and nothing else. The r56d-method probe (a
scratchpad console probe over the converted fiat/nistec + the golib AllocationCounter; temporary
by construction, reverted with the session) reproduces the pipeline at 8,524 obj/run — Δ4 obj /
466 B, the rand.Read substitution, the same gap shape r56d recorded (positive control passed).
Phase decomposition of the P256 body:
| Phase | obj/run | B/run |
|---|---|---|
NewP256Point().SetGenerator() |
31 | 2,970 |
make([]byte, 32) + fill |
1 | 56 |
ScalarBaseMult |
1,871 | 160,652 |
ScalarMult |
6,485 | 558,077 |
Bytes |
25 | 2,128 |
NewP256Point().SetBytes(out) |
48 | 4,152 |
BytesCompressed |
25 | 2,096 |
SetBytes(compressed) |
38 | 3,168 |
| whole-window control | 8,524 | 733,299 |
98.0 % of the residual sits in ScalarMult + ScalarBaseMult — the new(fiat.P256Element) /
NewP256Point temporaries of point Add/Double/table construction (class 3b) and their
lazily-materialized array<uint64> backings (class 4), the two Phase-C classes. The remaining
~168 close to the object at the element level: fiat SetBytes = 12 exactly (3 × 3b
minusOneEncoding news + 5 backings + in’s kept box 2 + Bytes-chain out kept box 2), fiat
Bytes = 3 exactly (out keep 2 + tmp backing 1) — the §6.3/A1-named wrapper keeps, small and
named as priced. Classes 1/2/3a measure ZERO: the fiat five unit targets
(Mul/Add/Sub/Square/Select, P224 and the P256 SetBytes/Bytes twins) all read 0 B/op,
0 obj/op (were 960 / 960 / 528 / 832 / ~1,344).
Census note, attributed — NOT the A2 host artifact. go test additionally reports
TestP256OrdInverse and TestP256PrecomputedTable ((amd64||arm64) && !purego files: the real
toolchain exercises its asm implementation’s tests; the converted purego suite doesn’t declare
them). The comparison’s census gate records and excludes them — compared maps have go-only = 0 and
the terminal arithmetic is identical to the standing 2,195/2,200. Zero go.std.* namespaces
anywhere in the run: the A2 std/-prefix artifact did not recur on the repinned GOROOT.
math/big re-measured through the pipeline: 224/226, both alloc rows unmoved.
TestMulUnbalanced reads 20,499,128 B vs the 51× budget of 20,416,320 (51.21× on 400,320 B of
inputs; r58b read 20,487,208 — +0.06 %, noise), and TestNewIntAllocs reads exactly 1 obj/run on
all seven NewInt shapes (want 0 — class 3b, the escape-analysis elision, Phase-C). As §3.6
forecast: nat’s traffic is slice-backed real allocation on both sides; Phase A promised these
rows nothing and moved them nothing.
os probe FINDING. os.File.WriteString measures 2,368 B/op (17 golib objects) against
§7’s “3,168 unchanged (±0)” claim — moved −800 B, favorably. The 3,168 stamp is r39-era and
predates r41’s inline-defer retirement of that decomposition’s 440 B GoFunc/defer term plus
everything since, so the claim’s baseline was stale when written; the per-term re-attribution of
the −800 belongs to the next os re-instrumentation (AllocMark), not to this stage’s arithmetic.
The two A2-owed validation items, confirmed from the 2026-08-13 sweep’s evidence:
- §3.5 func-value adapter — covered, with one precision.
internal/profile(swept clean, its 1 verdictTestPackedEncodingpass/pass) carries the boxed-shape adapter lambdas in its white-box decoder table —(ж<buffer> b, message m) => decodeUint64s(ref (b).DerefOrNull(), …)over the LOWEREDdecodeUint64s(ref buffer b, ж<slice<uint64>> Ꮡx)(proto_test.cs:70-71/proto.cs:344) — and those sources were refreshed BY the sweep-aftermath bank itself (e4d9faa71,proto_test.csamong the eleven), so the sweep validated with the adapters in play and exercised (the decode path runs through the table). The precision: the corpus instance is a func-LITERAL table inproto_test.go, not anexport_test.gobare alias — no swept export_test.go aliases a lowered function (measured: zero adapter shapes in committedexport_test.cscorpus-wide); that exact spelling is guarded at the converter level (refLoweringAnalysis_test.go’s §3.5 classification-equality guard, with positive control) and behaviorally (RefLoweredParams’ func-value X5 arm). Not a gap — the mechanism is swept, the spelling is unit-guarded — but stated so nobody reads “export_test.go” as sweep-evidenced. - io canary — still exactly 1.
ioswept clean at 60 matched / 1 disclosed (its roster row untouched by the aftermath commits, i.e. counts held),TestMultiWriter_WriteStringSingleAllocamong the matched (pass|pass, proof pageio.md); the assert is want-EXACTLY-one, so the pass IS the “still exactly 1” claim.
The perf suite — measured, AOT included, on the dedicated machine (2026-08-13; closes the §9 A3
row’s last obligation via HANDOFF-l3-a3-perf.md). The work laptop’s stand-down transferred the
run to a solo, sleep-proofed machine: AMD Ryzen 5 PRO 6650U (6C/12T, 30.8 GB), Windows 11
10.0.26200, pinned go1.23.1 (gate zero verified), .NET SDK 9.0.316, MSVC 14.44 link.exe
present — the AOT column is measured, not owed. Full run-performance.ps1 --update-readme
(default 5-run medians, MSBUILDDISABLENODEREUSE=1): Verify passed 14/14 — identical
timing-filtered stdout across Go binary / C# JIT / C# Native AOT, the gate Measure sits behind —
total 14,171 s wall, 13,552 s (95.6 %) of it the fourteen sequential ILC publishes. The table
banks in src/tests/Performance/README.md (PERF-RESULTS; mirrored to docs/Performance.md), the
first to carry the ж-bound RefLower row: Go 226.3 ms · JIT 660.6 ms (2.92×) · AOT 1,827.8 ms
(8.08×). The row is the standing README record, not the arc’s gate — the gate was A2’s paired
same-machine A/B per §7’s protocol, and cross-table comparison against the replaced i7-5820K
table (2026-08-11, 13 rows, same toolchain) is machine effect by construction. The §7-item-4
recording, from the run’s ILC ok (NNNs) lines and the published
Perf*\bin\Release\aot\<proj>.exe sizes — every publish succeeded first-try (the work
laptop’s exit-1-then-self-heal quirk did not recur, so no retry-inflated time exists anywhere),
each exe carrying the full converted-stdlib closure:
| Benchmark | ILC wall (s) | AOT publish size (MB) |
|---|---|---|
| PerfStartup | 981 | 296.4 |
| PerfFib | 1,085 | 296.4 |
| PerfSieve | 1,094 | 296.4 |
| PerfMatMul | 1,081 | 296.9 |
| PerfString | 972 | 296.4 |
| PerfStringView | 929 | 296.4 |
| PerfStringMatch | 955 | 302.3 |
| PerfMap | 933 | 296.4 |
| PerfSort | 909 | 297.0 |
| PerfChannel | 923 | 296.4 |
| PerfIfaceCall | 936 | 296.4 |
| PerfIface | 921 | 296.5 |
| PerfIfaceShell | 908 | 296.9 |
| PerfRefLower | 925 | 296.4 |
Two run notes for the record: the JIT one-shot batch build reported errors that per-project attribution resolved to 0 failed — the known parallel-build race, exactly as the handoff predicted, not a corpus defect. And an observation the A′/B′ checkpoint should see: on this machine AOT trails JIT on the compute-bound rows (RefLower 8.08× vs 2.92×, MatMul 5.02× vs 1.66×, Sieve 3.28× vs 1.61×) while beating it on Startup and Map — the ref-heavy hot loop is the widest such gap, so ILC codegen of the lowered emission is worth a look when B′’s dual-emission increment prices its own AOT bill.
Scout batch 2 — the never-measured/stale tail, 50 packages re-run; 3 bank, 8 roots MOVED (2026-08-14, lane B)
The derivation first, because the denominator is the part boards get wrong. 305 package
directories under src/core carry a production .csproj; 216 of them have a func Test in
their Go 1.23.1 GOROOT sources; subtracting hand-owned testing (skip-listed, never queued)
reconciles that to the roster header’s 215 exactly, and 215 − 136 banked = 79 unbanked.
Of the 79, five have no eligible Test declaration on windows/amd64 (internal/runtime/syscall,
internal/syscall/unix, net/internal/socktest, log/syslog, runtime/race) and os/user
cannot bank at all (Go’s own TestGroupIds fails in the oracle); the named walls (netpoll sockets,
scheduler-walled internal/singleflight, go/types, synthetic-GOROOT go/build) and the censuses
taken since 2026-08-09 were left to their owners. Fifty packages were run end to end through
-tests -test-action all.
Three bank, forty-six are rooted, and one is NOT MEASURED. Roster 136 → 139 (63.3% → 64.7%), 15,137 → 15,181 matching verdicts, 47 disclosed (unchanged).
The three
crypto/internal/hpke (19) · crypto/internal/edwards25519/field (16) · debug/gosym (9)
None needed a change of any kind — each is a capability that landed in the preceding week finally
being looked at, which is r43c’s lesson executed for the third time. crypto/internal/hpke was
held by the JSON-into-a-lifted-anonymous-struct root (claude/json-fixed-array-unmarshal);
crypto/internal/edwards25519/field by the array<T> unshaped-instance class it was recorded as
“producer (3)” of; debug/gosym’s TestPCLine by the converted host’s working directory — it
shells out to the real Go toolchain to build testdata/, so it closed when the host learned to
reproduce a package’s directory ancestry (40beb776e), not merely its shape.
⚠ The measurement hazard this pass paid for twice — MSB4006 is NOT a build root
r44a’s trap #2 recorded that a failed run’s leftover <pkg>.tests.csproj can produce
NuGet.targets(1311,5): error MSB4006: circular dependency … "_GenerateRestoreProjectPathWalk".
This pass hit a second, sharper form of the same hazard, and the poisoned file is TRACKED: a
-tests run rewrites the production .csproj of packages in its closure, and the runtime run
rewrites src/core/runtime/runtime.csproj’s windows-conditional ItemGroup to add
internal/syscall/windows — which references syscall, which references runtime. That is a real
project-path cycle, and every package whose closure reaches runtime then fails to restore. Four
packages (runtime, crypto/tls, go/internal/gcimporter, os) were first recorded as
“conversion-blocked” this way, which reads exactly like a build root and is not one.
Two rules follow, and the second cost more than the first:
- Restore tracked
.csprojdrift between pipeline runs, not just untracked artifacts, then re-measure. Proof: withruntime.csprojat HEAD and no priorruntimerun,crypto/tlscompiles and reaches its own diagnostic in 337 s — no cycle. All four were re-measured this way and the three that could be are reported below on their real roots. - Do NOT “clean” by deleting
*.tests.csprojwholesale. 136 of them are committed source under the validated-package commit policy. An over-broad prophylactic delete wiped every banked package’s test project in one command;git checkout -- src/corerestores it, but a lane that did not notice would have banked the deletion. Scope such a clean togit clean, which by construction cannot touch tracked files — never to aGet-ChildItem | Remove-Itemsweep.
runtime itself remains NOT MEASURED: its own -tests run is what writes the cycle, so the
package cannot be measured without first fixing that emission. That is the finding, and it is a
converter defect — a test-closure-only reference reaching a production .csproj.
Eight roots MOVED — the board’s recorded first diagnostic is stale for these
| Package | Census | Board had | Measured 2026-08-14 |
|---|---|---|---|
archive/tar |
CS1537 ×3, duplicate using alias in one file |
CS0111 — tar_internal_test_package.fileOps already defines Append. The alias-dedupe root is CLOSED; behind it is the same defect as fmt — ⚠ and that “CLOSED” was wrong: CS1537 was still live and became this package’s next wall once CS0111 fell. See CLOSED for archive/tar below (2026-08-15) |
|
fmt |
CS0111 fmt_test_package.SE already defines Append |
COMPILE WALL CLOSED (2026-08-16, lane claude/fmt-roots) — two converter roots, not four; the host builds and 62 verdicts agree. TestSprintf is the one open verdict, behind three reflect/fmt-bridge divergences. See fmt’s compile wall is CLOSED below |
|
sync/atomic |
0 of 108 | CS0103 ᏑᏑX, double address-prefix |
CS0841 — cannot use local magic64 before it is declared. The double-Ꮡ is closed |
internal/reflectlite |
0 of 30 | CS0016 could not write to output file | CS0715 — static classes cannot contain user-defined operators: a generic test-local type’s ==/!= emitted into the static reflectlite_test_package |
runtime/debug |
0 of 9 | CS0264, not taken past the first diagnostic | CS0264 + CS0715 — the same static-class-operator defect as internal/reflectlite. Two packages, one root |
database/sql |
0 of 25 | CS0029 | CS0121 ambiguous stubDriverStmt(NilType) constructor + CS0117 sql_package has no Δtable |
crypto/x509 |
0 of 335 | CS0102 duplicate definition in x509_package |
CS0234 cryptobyte_package / CS0246 hash_package unresolved in the production files under the -tests closure |
internal/platform |
0 of 1 | JSON array into a slice of a lifted anonymous struct | panic: nil dereference in text/template’s errRecover — TestGenerated renders its expectation through a template. The lift question may still sit behind it, but it is no longer the failing edge |
crypto/tls — 0 of 3,519, and the vendor root is in GENERATED code
The recorded root (“CS0234 'vendor' does not exist — the test half’s vendored import”) is right
about the token and wrong about the file. The errors are CS0246 vendor could not be found in
go2cs-gen RecvGenerator output — e.g.
go.crypto.tls_package.NonceSize.global__go.crypto.tls_package.prefixNonceAEAD.g.cs(17,7). The
generator emits a receiver overload whose parameter type names the GOROOT-vendored root namespace,
so this is an emission question in the analyzer, not an import list. It is the largest verdict count
on the unbanked list by a factor of six.
Re-baselines this pass owes the board
go/internal/gcimporter: 399 → 475 of 583. The L9 laptop-G census was taken with thego/typeschecker nil-panic live;25108403e(2026-08-13) closed it and moved 76 rows with no work aimed at this package. The residual is 108 rows, allTestImportTypeparamTests/*— still the type-parameter class, so the wall is narrower, not gone. 1,391 s to measure.- ⚠
os: 681 of 683 (board) → 31 of 679 (measured), and this is a REGRESSION to bisect. The converted host dies with exit status0xc0000005(access violation) after recording 32 verdicts, the last four beingTestLookupEnv,TestMkdirAllAtSlash,TestMkdirAllExtendedLengthAtRoot,TestMkdirAllVolumeNameAtRoot. Reproduced on a clean, cycle-free re-measure, so it is not the MSB4006 artifact above. Per the jsonrpc rule, a package that dies mid-run has one failure and an unknown remainder, not 648 — the 31 is a floor, not a census. Flagged for a bisect lane; not this lane’s to root. ⚠ RETRACTED 2026-08-14 — not a regression. The bisect lane reproduced the identical fault at the r39-osalloc commit that recorded the 681, and rooted it in one test whose syscall is reachable only on a host with the Server service running. The flag was right to raise it and right about the floor; the diagnosis “REGRESSION” is withdrawn. Seeos’s “REGRESSION” is a HOST CAPABILITY, and the killer isSHARE_INFO_2at the end of this file. log: theAllocsPerRunshim now reports a COUNT here.TestDiscardmeasures 300 go2cs-runtime object allocations over 100 runs = 3 objects/run against Go’s want-zero (the board’s fourth-member row read “bytes”).net/http/internal’sTestChunkReaderAllocsreproduces r58a exactly at 2 objects/run against a budget of 1.log/slog/internal/bufferis unchanged — still bytes (1,520 B over 5 runs), so r58a’s “counter saw none” stands for that one.logis 7 of 9;TestAllis its second, separate row.log/slog: 153 of 213 matched. Recorded differently by r44b (185 pass / 28 fail / 1 crash), which counted the C# side rather than agreement with Go; the two are not comparable and this row is the differential. Not re-attributed here.
Reproduced verbatim — no re-measure owed until a relevant capability lands
go/doc 24 of 85 · encoding/gob 99 of 106 · crypto/cipher 13 of 14 · debug/pe 9 of 10 ·
html 2 of 3 · internal/trace/internal/oldtrace 2 of 3 · internal/weak 1 of 3 ·
log/slog/internal/buffer 1 of 2 · iter 0 of 28 (newcoro/coroswitch stubs) ·
runtime/trace 0 of 2 (getg stub) · internal/concurrent 0 of 20 (CS0426 node<,>) ·
debug/elf 0 of 31 (CS8183 at file_test.cs(1195,5)) · os/exec 0 of 22 (CS0103 var) ·
text/template 0 of 52 and html/template 0 of 243 (CS0030 S→I) · slices 0 of 122
(CS0305/CS0411) · encoding/xml 0 of 386 (CS0426 ΔToken) · — the compile wall is CLOSED end to end and the package RUNS: 400 of 491
matching, 91 divergent in four named roots (2026-08-16) · encoding/json 0 of 491
(CS0050/CS0053)net/netip 0 of 266 (CS1002/CS1525) · internal/trace 0 of 92 and
runtime/pprof 0 of 174 (CS0149 Method name expected; pprof also still CS0103 ᏑᏑsalts) ·
internal/runtime/atomic 0 of 15 (CS0103 ᏑᏑx) · flag 0 of 24 (CS1929 on
ж<flag_test_package.URLValue>) · crypto/ed25519 0 of 9 (CS0030 PrivateKey→crypto.Signer) ·
crypto/internal/mlkem768 0 of 11 (CS0315, plus CS0841) · reflect — the converter itself still
fails on all_test.go (1e+06 not an Int).
Four packages measured for the FIRST time — no prior board row of any kind
| Package | Census | Root |
|---|---|---|
net/http/pprof |
5 of 15 | TestHandlers fails and seven of its subtests infrastructure-error (/debug/pprof/{heap,mutex,trace,profile,block,goroutine}); TestDeltaProfile skips where Go passes. Profile collection has no managed body — sibling of runtime/pprof’s and runtime/trace’s stubs |
internal/godebug |
3 of 5 | TestCmdBisect and TestMetrics fail; TestBisectTestCase and the rest pass. The package is fully hand-owned, so this is the hand-own’s own contract being measured for the first time |
internal/syscall/windows |
0 of 2 | TestRunAtLowIntegrity, TestSupportUnixSocket — both fail. This is also the package whose -tests run participates in the MSB4006 cycle above |
crypto/internal/boring/bcache |
0 of 1 | NotImplementedException: registerCache: external (assembly or cgo) function is not implemented — a PartialStubGenerator stub reached from Register[K,V]. The board had only “first divergent verdict recorded; not root-attributed” |
net/mail gets its first real census too — 7 of 11, the four TestAddress* rows
infrastructure-erroring on System.ArgumentException: Indices low, high and max represent a range
outside bounds of the array reference — where the board had only “first divergent verdict
recorded”. And go/internal/srcimporter (0 of 7) fails before any test with
flag provided but not defined: -json, i.e. the process the host launches is not the go2cs test
host; that is an infrastructure root, not the recorded build block.
⚠ The last clause of that diagnosis is CORRECTED (2026-08-14, lane claude/defect-batch-1).
The process IS the go2cs test host. internal/fuzz reaches the identical symptom once its build
blocker is cleared, and there the cause is exact: worker_test.go’s TestMain calls
flag.Parse(), and the converted flag.CommandLine has never been told about the host’s own
--json / --result / --junit / -timeout arguments, so parsing rejects them before any test
runs. In Go, testing.M registers those flags on flag.CommandLine before TestMain executes,
which is what makes the same flag.Parse() legal there. The remedy belongs to the hand-owned
src/core/testing host — register its flags on the converted flag.CommandLine — and is a
separate, unclaimed item. Every package whose TestMain calls flag.Parse() sits behind it.
✅ REMEDIED 2026-08-14 (lane claude/testmain-flag-bridge) — src/core/testing/TestFlagBridge.cs,
host-only, zero converter change. The host now DECLARES its own command line on the converted
flag.CommandLine before it invokes a converted TestMain, which is precisely what
testing.Init() does for -test.* and precisely what was missing. Four things the implementation
had to get right, each measured rather than assumed:
- The whole
-test.*set is registered, not just the spellings the host was given — with this run’s real values (test.run←-run,test.parallel←-parallel,test.v,test.short,test.count,test.timeout,test.shuffle; the rest at Go’s defaults), because converted tests READ them:os/exec’sTestMaingates onflag.Lookup("test.run").Value.String() == ""andflag.Lookup("test.list"), andruntime’s gdb tests doflag.Lookup("test.parallel").Value.(flag.Getter).Get().(int). Registering only what appeared on the command line would have traded a parse error for a nil dereference. For the same reason the TYPED registrars are used and notflag.Func/BoolFunc, whosefuncValuehas an emptyString()and is not aGetter. - The
flagpackage is bound LATE, by name, not by project reference. Go’stestingimportsflag, so the reference is the obvious mirror — and it was tried and MEASURED and it does not work here. The generated test csproj setsDisableTransitiveProjectReferences=true(load-bearing: CS0576 against the emittedusingaliases), so atesting→flagreference does not deployflag.dllbeside the 124 of 141 test hosts whose own package does not importflag— an unconditional use wouldFileNotFoundExceptionevery one of them. It also cost every test project’s build +33% (unicode/utf8: 7.5 s warm → 10.2 s). Late binding is the accurate statement of the dependency, not a dodge: the convertedflagpackage is in a test compilation iff the package under test imports it, which is exactly when a convertedflag.Parse()is reachable andflag.CommandLineobservable at all. Only theflag_packageTYPE is resolved by name; every argument type (@string,nint,nuint,time.Duration) is a golib/timetype the host already references. - A name the test package already defined is skipped — the converted
FlagSet.VarPANICS on redefinition, and the host must not turn a package’s own flag into a crash. Only the host’s unprefixed spellings can collide (Go’stest.prefix exists to make collision impossible), and across all of GOROOT’s non-cmdtest sources exactly one such definition exists (-v, incmd/compile/internal/ssa, which is not converted). - Scope boundary, deliberate: Go’s
M.Runalso callsflag.Parse()when it is not yet parsed. That is NOT mirrored — no class member needs it (every one’sTestMainparses explicitly), and an unconditional parse would newly reachExitOnError/os.Exit(2)for the sixteen banked packages that merely referenceflag. Registration alone is the minimal change that closes the class.
Class census (GOROOT non-cmd test sources calling flag.Parse(), per package): internal/fuzz,
go/internal/srcimporter, os/exec, crypto/tls — four members. (runtime and syscall match a
naive grep and are NOT members: runtime/runtime-gdb_unix_test.go’s two hits are inside a Go source
STRING literal for a helper program and the file is unix-only; syscall/syscall_unix_test.go’s is in
a unix-only re-exec helper. flag/example_test.go and database/sql/example_cli_test.go are Example
bodies.) Where the four stand after the bridge:
| Package | Before | After | Note |
|---|---|---|---|
internal/fuzz |
0 (died at flag.Parse) |
BANKED 52/52 | the 141st roster row; its TestMain now parses the host’s command line |
go/internal/srcimporter |
0 of 7 (died at flag.Parse) |
5 of 7 | not banked; the two failures share ONE root and it is not this class — see below |
os/exec |
build-blocked | builds; 22 of 22 match | the unnamed-variadic block is FIXED (lane claude/unnamed-variadic, 2026-08-14) — 40 further tests are excluded for want of TB.*, see that section. That exclusion is CLOSED too (lane claude/tb-surface, same day): the 22 became 101 rows, 48 agreeing, and what was behind it was two HOST roots — see the TB.* section at the end of this file |
crypto/tls |
not measured | builds; blocked at RUN | reached 2026-08-14 by the same lane; two roots, neither this one. Both closed 2026-08-15 (lane claude/crypto-tls) along with a third behind them — the package now compiles and its host reaches the flag bridge, where a different wall stops it: see the section at the end of this file |
go/internal/srcimporter — 5 of 7, one root, and it belongs to go/types. TestIssue20855,
TestIssue23092, TestIssue24392, TestReimport pass and TestCgo skips identically. The two
failures — TestImportStdLib and TestImportedTypes — both die inside the converted go/types
checker on the SAME construct: internal/syscall/windows/version_windows.go:87’s call to
sync.OnceValue(func() bool {…}) reports “in call to sync.OnceValue, cannot infer T” /
“cannot use (func() bool literal) (value of type func() bool) as func() T value”. That is generic
type-parameter inference from a func-literal argument, failing in the converted checker where Go’s
succeeds — a third dependent of the unbanked go/types row, alongside internal/types/errors and
go/internal/gcimporter’s 184. srcimporter type-checks GOROOT from source, so every package whose
import graph reaches os inherits it, which is why both failures name a different top-level package
and the same innermost cause.
os/exec — still build-blocked, and the root is now exact. Three CS0103: The name 'var' does
not exist in the current context, all one shape: an UNNAMED variadic parameter. exec_test.go
declares func cmdPipeTest(...string), func cmdStderrFail(...string) and func cmdStdinClose(...string)
— Go permits a parameter with no name at all — and the converter emits the variadic unpacking with an
empty variable name: internal static void cmdPipeTest(params ꓸꓸꓸstring ʗp) { var = ʗp.slice(); … }.
The parameter itself is named fine (ʗp); it is the unpacked local that inherits the absent Go name.
A one-line converter fix (emit no unpacking, or a discard, when the Go parameter is unnamed) puts
os/exec behind the bridge instead of in front of it.
⚠ FIXED 2026-08-14 (lane claude/unnamed-variadic) — and it was not one line or one site. The
prologue is emitted at TWO sites (visitFuncDecl and convFuncLit), and the blank (_ ...T)
spelling is a third symptom that COMPILED and so was invisible here. os/exec builds clean now.
See the unnamed-variadic build block is fixed below for the fix, the zero-mover census, and where
os/exec actually stands.
The prize left on the table
Two packages, one defect: CS0715 — a generic test-local type’s ==/!= operators are emitted
into the static <pkg>_test_package partial class, which C# forbids. It holds
internal/reflectlite (30 verdicts) and runtime/debug (9). And CS0111 The Append holds fmt
(63) and archive/tar (97) — 160 verdicts on one duplicate-member emission.Append half
is spent — see the two entries below: 14bf20010 closed CS0111 itself, and
claude/dup-append-emission cleared the two walls behind it in archive/tar, which banks 97/97.
fmt did not follow; its five remaining roots are censused below. CS0715 is now the cheapest
remaining pair on this list.
✅ CLOSED for archive/tar (banks 97/97) — two walls behind the Append root; fmt does not follow (2026-08-15, lane claude/dup-append-emission)
The brief was stale, and that is the first finding. The CS0111 Append root this lane was sent
to take had already been fixed by 14bf20010 (2026-08-14) — a named []any slice wrapper’s
non-generic ISlice.Append(object[]) became an EXPLICIT implementation, guarded by
NamedAnySliceType — but that commit did not update this board, so the “eight roots MOVED” table
and The prize left on the table above both still named it. Re-measure before believing a board
row; the row above is now struck through rather than deleted so the same mistake is not made twice.
Behind it, archive/tar had two more walls, both general converter defects, both fixed here:
- CS1537 ×3 — a function-LOCAL type declaration emitted as a COMPILATION-scoped
global using. The board’s “eight roots MOVED” table recorded tar’s alias-dedupe root as CLOSED; theNamedAnySliceTypelane reported it as still live, and it was. Root: every local type-declaration kind (struct, interface, slice, map, channel, pointer, named-ident) takesliftLocalTypeDecl— enclosing-function prefix,ᴛNuniquification,liftedTypeMapregistration — except the branch that emits ausingALIAS, which is what a realtype X = Yand a defined-over-named-interfacetype X anyboth take. Aglobal usingis scoped to the whole compilation, sotype testFnc anyinwriter_test.go’sTestWriterandTestFileWriter, and again inreader_test.go’sTestFileReader(withfileMakeralongside), claimed one alias name. Fix: the naming half ofliftLocalTypeDeclis factored toliftLocalTypeDeclNameand the alias branch calls it in-function, with the reference mapping registered only when the declaration’s own object IS the type (liftedTypeDeclaredBy— keying it on the target would rename everyHeader, or everyint, in the file). Zero production emission impact by construction: an AST scan of Go 1.23.1 finds no function-local alias-or-defined-over-interface declaration in any compiled stdlib file — all 50 hits areinternal/types/testdata. Guard:LocalTypeAliasScope(the unfixed converter emits five duplicateglobal usinglines). - CS0121 ×9 — an ambiguous one-field
nilconstructor.testClose{nil}emittednew TestWriter_testClose(default!); the typelessdefault!converts to both generated one-argument constructors,T(NilType)andT(error field = default!). The argument now carries the field’s type. Narrow by construction — Go requires a positional literal to list every field, so only a one-field struct has the arity, and onlynilrenders typeless.database/sql’s recordedstubDriverStmt(NilType)CS0121 is the same root and should be re-measured.
archive/tar census: 97 rows, 97 agree, 0 disagree, 0 skipped, 0 disclosed, 3 excluded (the
standard Phase-4D Benchmark/Example/Fuzz deferrals). Banked — roster row, proof page,
committed test sources.
A third defect this work found and did NOT fix (chip raised, reported here for the record): an
alias whose target is an unnamed composite emits its type ARGUMENTS unrooted —
type names = []string → global using names = go.slice<@string>;, where @string, a nested
slice, error, complex64, a same-package Header and a foreign io_package.Reader all arrive
bare and do not resolve at compilation scope. It is package-level and pre-existing, unrelated to
the scope fix, and no converted stdlib package declares such an alias — but any converted user
module would. It surfaced only because the LocalTypeAliasScope guard was first written with such
an alias in it.
LANDED 2026-08-16, lane
claude/emission-defects. The alias RHS now renders in a ROOTED-NESTING mode: the target and every name it nests carry full qualification, with four qualifiers that are not interchangeable (go.for golib types,System.for the BCLFunc/Action,go.again for the variadicActionꓸꓸꓸ/Funcꓸꓸꓸfamily, andgo.<ns>.<pkg>_package.for a same-package name), while the csproj-alias names (uint64,any, …) are SUBSTITUTED rather than rooted — they are not members ofgoat all. Two shapes beyond this row’s census turned out to be in the same class and are fixed with it: a func-type alias (Func/ActionneedSystem., which is no more in scope thango) and an alias whose target is itself an alias (a C# using alias may not name another). The census re-verification confirms this row’s “no converted stdlib package declares such an alias” for the type-ARGUMENT arm — the corpus holds exactly four package-level aliases with type arguments (fiat’sp224/p256/p384/p521, each[4]uint64), all four taking a C# keyword as the argument, so that arm moves nothing. The substitution arm did have corpus sites the row did not predict, and they were live CS0234: a csproj-alias name standing as the WHOLE target was rooted (global using _C_int = go.int32;—int32is a<Using Alias=…>forSystem.Int32, not a member ofgo, and the existing safe-name sweep skips dot-qualified names by design). Six sites, all cgo_C_*typedefs in darwin-exclusive files (os/user/darwin/,net/darwin/), latent because the default$(GoTargetOS)iswindowsand nothing compiles them; they now emitint/uint/long. Guard:PackageAliasRootedTypeArgs(25 aliases, output-compared; the unfixed converter fails it with CS0246 and a golden mismatch). Detail:docs/ConversionStrategies-Reference.md§Type Alias Declarations, “The whole RHS is namespace-ROOTED”.
fmt — still 0 of 63, and it is FIVE roots, not one
Measured 2026-08-15 with both fixes above in place. Neither touches fmt: it has no function-local
alias declaration and no one-field nil literal. The Append root is genuinely gone; what is
behind it is a fan-out, not a queue, and three of the five cluster on one test type:
| Diagnostic | Site | Shape |
|---|---|---|
CS1955 non-invocable map<TKey, TValue> |
fmt_test.cs(838) |
map[int]byte(nil) — a CONVERSION to a map type emitted an INVOCATION, map<nint, byte>(default!), not a cast. LANDED 2026-08-16, lane claude/emission-defects: an untyped-nil operand against a map-underlying type LITERAL is now claimed by isTypeConversion (the identical-underlying guard rejected it, since untyped nil’s underlying is itself) and the ordinary renderer casts — ((map<nint, byte>)default!). The row’s “one emission rule, two spellings” reading of the sibling []int(nil) was measured and does not hold: slice<T>(default!) binds builtin.slice<T>(T[]), golib’s REAL conversion helper — the same one []byte("…") is emitted against, documented as such — and yields the nil slice, so it is correct rather than lucky, and (chan T)(nil) already cast. Both are deliberately left on their existing routes; claiming them would rewrite ~25 corpus sites to no effect. Census, by SPELLING: the BARE map[K]V(nil) — the broken one — has 0 stdlib production sites and 13 test-file sites (fmt, reflect, encoding/json, internal/reflectlite), which is why the corpus never showed it; the PARENTHESIZED (map[K]V)(nil) reached the fork via convParenExpr and was already casting, so its single production site (reflect/type.go:1837) compiled all along and now only re-parenthesizes. Total corpus footprint: one line. Guard: UnnamedMapNilConversion. Detail: docs/ConversionStrategies-Reference.md §Canonical typed-nil pointer boxing, “A nil converted to an unnamed MAP type is a cast” |
CS0030 float→renamedComplex64, double→renamedComplex128 ×4 |
fmt_test.cs(941,942,951), scan_test.cs(394) |
a named type over complex64/complex128 does not admit the real→complex conversion Go allows |
CS1729 Scan_type has no 1-arg constructor |
scan_test.cs(576) |
— |
CS0103 Reader does not exist |
ImplementGenerator output for Scan_type→io.Reader |
the generated wrapper names the interface unqualified |
CS0034 == ambiguous on Scan_type |
same generated file | — |
The last three are one type’s story and are likely one root; the first two are independent. This is
a materially different proposition from the 97-verdict single-wall archive/tar half — the “160
verdicts on one defect” framing above never held for fmt.
Root 1 of the five is closed (2026-08-16, lane claude/emission-defects — see the struck row).
fmt’s wall is now FOUR roots; the remaining four are untouched by that lane, so the package’s
verdict count is unchanged until they are worked. Whoever picks fmt up next should re-measure
rather than assume this row’s sites, since the CS1955 no longer masks whatever follows it in
fmt_test.cs.
✅ CLOSED — the unnamed-variadic build block is fixed; os/exec now BUILDS and its next wall is TB.* (2026-08-14, lane claude/unnamed-variadic)
The one-line prediction above was right about the root and wrong about the size: the defect has two emission sites and three symptoms, and the probe found the third.
The fix. visitFuncDecl and convFuncLit each emit a variadic parameter’s unpacking prologue
(var <name> = <name>ʗp.slice();). Both now skip it when the Go parameter is UNNAMED or BLANK, via
one shared predicate, variadicParamIsUnreferenceable. Either spelling is unreferenceable under Go’s
own rules, so the local is dead by construction — and this is the same ruling, for the same reason,
that an unnamed/blank POINTER parameter’s deref alias already takes (it would otherwise emit
ref var = ref Ꮡ.Value;). The A2 rebank note’s precedent chose skip, so this matches it rather
than synthesizing a name. A NAMED variadic still unpacks; the signature is untouched in every case,
keeping its params array under its own ʗp name and simply leaving it unread.
Three symptoms, not one. (1) UNNAMED at a top-level func or method — var = ʗp.slice();, the
empty identifier the board recorded. (2) UNNAMED inside a FUNCTION LITERAL — worse, and previously
unrecorded: the literal’s signature builder normalizes the absent name to _ and declares
params ꓸꓸꓸnint _ʗp, while the prologue kept rendering ʗp from the raw name, so the dead local
carried an empty name AND a name the signature never declared. (3) BLANK (_ ...T) anywhere —
var _ = _ʗp.slice();, which compiles, so it was invisible, but declares a REAL local named _
(a plain var _ = e; declaration is a variable, not a discard) that then hijacks every _ = …
discard in the body — the CS0029 class bodyUsesBlankDiscard exists to prevent for a blank
parameter name.
Corpus movers: zero, and that is measured, not assumed. An AST census of GOROOT (production +
tests, cmd and testdata excluded) finds five sites total. Exactly one is production —
syscall/syscall_linux.go’s func cgocaller(unsafe.Pointer, ...uintptr) uintptr — and it is
bodyless (a //go:uintptrescapes linkname target, emitted
internal static partial uintptr cgocaller(@unsafe.Pointer _Δp0, params ꓸꓸꓸuintptr ʗp);), so it has
no prologue to skip on any target, Linux included. The other four are all os/exec test sources:
exec_test.go’s cmdPipeTest/cmdStdinClose/cmdStderrFail and exec_posix_test.go’s cmdPwd.
CNR agrees: one changed file across 601 behavioral packages, and it is the guard test itself.
Guard: UnnamedParams extended (not a new project — it is already the unnamed/blank parameter
guard, and the pointer precedent this follows lives there). It now pins all three shapes — unnamed,
blank, and a NAMED control that IS read — at declaration, method and function-literal positions,
output-compared vs go run.
os/exec — the build block is GONE; 22 of 22 run, 22 of 22 MATCH, and the next wall is named
The pipeline (-tests -test-action all -test-timeout 30m) now builds os/exec with zero compile
errors, and the three helpers emit exactly as Go declares them — internal static void
cmdPipeTest(params ꓸꓸꓸstring ʗp) { with no prologue, registered as method groups
(["pipetest"u8] = cmdPipeTest,) into the converted map[string]func(...string).
It is not bankable, and the reason is not os/exec and not the host environment:
| Class | Count | What it is |
|---|---|---|
| ran and matched Go exactly | 22 | 19 pass + 3 skip; the go and csharp verdict maps are identical, including the three Windows skips (TestExtraFiles, TestPipeLookPathLeak, TestString) |
excluded — TB.Fatal unsupported |
26 | the roster in supportedTestCapabilities carries the whole *testing.T surface but no TB.* member at all; os/exec’s tests funnel through exePath(t testing.TB), which every helperCommand call reaches |
| excluded — Phase-4D deferral | 14 | 13 ExampleXxx + 1 BenchmarkExecHostname; a standing pipeline policy, not an os/exec fact |
The run still exits 1, and that too is not a divergence: with the 26 TB.Fatal tests excluded, the
helper commands they drive go unused, and os/exec’s own TestMain self-audit fails a run that
leaves a registered helper unused (helper command unused: "echo", …) — on both sides. So the
honest verdict is that os/exec has no measured behavioral divergence at all; it has 40 tests
that never ran. The process-spawn shapes the board expected to wall on host semantics
(TestEcho, TestCatStdin, TestPipes, TestStdinClose, TestExitStatus, TestWaitInterrupt,
TestContextCancel, …) are all in the TB.Fatal bucket — excluded, never executed — so nothing
is yet known about how they behave. Naming TB.* support in the hand-owned core/testing host is
the exact next domino, and it is a host change with no converter component.
crypto/tls — measured for the first time: BUILD-BLOCKED on two roots, neither of them this one
The fourth flag-bridge class member, pipelined once (93 s) to record its outcome class. It does not reach the bridge — it does not build. Four errors, two roots, both new board entries:
- CS0012 ×3 —
crypto/tls/example_test.cs(88, 99, 198): the typetls_package.Configis defined in an assembly that is not referenced, namingcrypto.tls. A fresh instance of the-testsreference-closure family (recorded CLOSED 2026-07-27) arriving through Example declarations in the black-boxtls_testpackage. - CS1503 ×1 —
crypto/tls/tls_test.cs:1893: cannot convertSystem.Func<ushort, ushort, int>toSystem.Func<ushort, ushort, nint>. A comparator lambda whose return rendersintwhere the target signature saysnint.
No deep chase was commissioned and none was done.
✅ BOTH ROOTS CLOSED, and a third behind them — crypto/tls BUILDS (2026-08-15, lane
claude/crypto-tls). Re-measured at master 6dd7547e4 before anything was believed, per the
stale-premise rule: both reproduced verbatim, same codes, same four call sites. This board entry
was accurate. Its attribution of the first root was not — see the section at the end of this file.
RETRACTED — os’s “REGRESSION” is a HOST CAPABILITY, and the killer is SHARE_INFO_2 (2026-08-14, lane os-av-bisect)
Scout batch 2 flagged os as a REGRESSION — 31 of 679 measured against this board’s 681 of
683, the converted host dying with 0xc0000005 after 32 verdicts — and sent it to a bisect lane
under the unique precedent. There is no culprit commit. The access violation reproduces, frame
for frame, at a936c8025 (r39-osalloc) — the very commit whose run recorded the 681. What moved
is the HOST, not the tree.
The killer, named. One test: TestNetworkSymbolicLink. The goroutine-panic containment from the
jsonrpc chip is what makes it nameable — the fault now arrives with a managed stack instead of a bare
exit status:
Fatal error. 0xC0000005
at go.syscall_package.syscalln(UIntPtr, System.ReadOnlySpan`1<go.uintptr>)
at go.syscall_package.SyscallN(go.uintptr, System.Span`1<go.uintptr>)
at go.syscall_package.Syscall6(...)
at go.internal.syscall.windows_package.NetShareAdd(go.ж`1<UInt16>, UInt32, go.ж`1<Byte>, go.ж`1<UInt16>)
at go.os_test_package.TestNetworkSymbolicLink(go.ж`1<T>)
It is the syscall STRUCT-PASSING seam — the open class this file already carries — reached for the first time by a real suite, and reached in the direction that class had not yet shown: not the kernel WRITING a native record over a smaller managed one, but the kernel READING a managed record as a native one and dereferencing a value field as a pointer. That shape faults AT the call, not arbitrarily far away.
The mechanism, measured rather than argued. internal/syscall/windows.SHARE_INFO_2 holds four
ж<uint16> pointer fields and four uint32s. The CLR auto-layouts a struct containing references,
so the references are grouped FIRST — dumped by reflection from the built
internal.syscall.windows.dll, the record is 48 bytes against the native 56:
native SHARE_INFO_2 (x64) |
native off | C# storage actually at that offset | value handed to netapi32 |
|---|---|---|---|
LPWSTR shi2_netname |
0 | Netname (object reference) |
a managed reference, read as runes |
DWORD shi2_type |
8 | low half of Remark (nil) |
0 |
LPWSTR shi2_remark |
16 | Path (object reference) |
a managed reference |
DWORD shi2_permissions |
24 | low half of Passwd (nil) |
0 |
DWORD shi2_max_uses |
28 | high half of Passwd (nil) |
0 |
DWORD shi2_current_uses |
32 | Type |
0x40000000 |
LPWSTR shi2_path |
40 | MaxUses (=1) then CurrentUses (=0) |
0x0000000000000001 |
LPWSTR shi2_passwd |
48 | past the end of the 48-byte record | whatever follows on the heap |
netapi32 dereferences shi2_path — the pointer value 1 — and the process dies. shi2_passwd is
a second, independent defect in the same call: an 8-byte over-read past the managed record.
Proven without go2cs. A standalone C# program calling netapi32!NetShareAdd three ways, on this
host:
| Buffer | Result |
|---|---|
A — blittable [StructLayout(Sequential)] record with real LPWSTRs |
rc=0, the share is genuinely created, NetShareDel rc=0 |
| B — object references at the NATIVE offsets | survives, rc=123 (ERROR_INVALID_NAME) — an object reference is a readable address, so this alone is not fatal |
| C — the MEASURED go2cs layout (refs 0/8/16/24, uints 32/36/40/44) | Fatal error. 0xC0000005, exit -1073741819 |
So the fault is not “a managed reference where LPWSTR belongs”; it is the field REORDERING that
puts an integer 1 under shi2_path. B is the control that makes C mean something.
Why the board’s 681 held and this host’s run does not. Go’s own test treats exactly two
NetShareAdd failures as a skip — ERROR_ACCESS_DENIED and NERR_ServerNotStarted (2114) — and on
a host where either fires, netapi32 never reaches the buffer, both sides skip, and the row AGREES.
On this host neither fires: the session is elevated, LanmanServer is Running, probe A creates a
real share, and Go’s own TestNetworkSymbolicLink PASSES. The buffer is therefore marshalled,
and the layout defect becomes fatal. The 681 was measured on the i9 that died 2026-08-09; it cannot
be re-measured there, and nothing about that record needs to be doubted — it needs a precondition
written down, which is what this entry is.
The record survives — control run, HEAD, one test excluded. With
-run '^(?!TestNetworkSymbolicLink$)' and nothing else changed, the host runs the suite to
completion: 683 verdicts — 659 pass, 21 skip, 2 test failures, those two being TestUTF16Alloc
(the recorded alloc-count-semantics disclosure) and TestWriteStringAlloc (the ONE real residual
r39-osalloc rooted and left as an architectural arc). That is the recorded shape exactly.
TestNetworkSymbolicLink is the sole host-killer; there is nothing behind it. (C#-side census
only — no differential was run, and the skip count differs from the recorded 34 because a
more-capable host skips fewer tests, which is the same host-capability fact stated from the other
side. Do not read 683 here and 683 on the record as the same denominator.)
Verdict for the roster and the board.
- The REGRESSION row is retracted.
31 of 679stands as a floor under the jsonrpc rule, and is not evidence of anything having broken. os’s 681 of 683 + 1 disclosed record stands, now qualified: it is measurable only on a host whereNetShareAddshort-circuits. On a host with the Server service reachable,osmeasures NOTHING — the process dies at test ~32 of 174.osstill does not bank, for the reason it never did:TestWriteStringAlloc. It has never been a roster row; 681 of 683 is a board record.- No commit is implicated, so nothing is reverted and no guard is added.
The census this finding corrects. The struct-passing census above is scoped to
src/core/syscall and therefore could never have listed this member. internal/syscall/windows
is a SECOND package holding the same class, and its own census is:
| Wrapper | Non-blittable struct | Reached by |
|---|---|---|
NetShareAdd |
SHARE_INFO_2 (Netname, Remark, Path, Passwd) |
os’s TestNetworkSymbolicLink — the only caller in all of GOROOT; fatal on a capable host |
GetAdaptersAddresses |
IpAdapterAddresses (nine ж<T>, array<byte> PhysicalAddress, array<uint32> ZoneIndices) |
ROW WITHDRAWN 2026-08-17 — it was never a member of this census. The wrapper is handed a byte BUFFER and fills it, which is what a byte buffer is for; it is correct and stays auto-converted. The defect is entirely in the CALLER, net.adapterAddresses, which reinterprets the filled buffer as the record — the readReparseLink / dir_windows_impl fork, not the mirror-the-wrapper one. Fixed in the net interfaces arc; see the entry at the end of this file |
Module32First / Module32Next |
ModuleEntry32 (array<uint16> Module, array<uint16> ExePath) |
syscall’s own suite |
GetFileInformationByHandleEx |
FILE_ID_BOTH_DIR_INFO / FILE_FULL_DIR_INFO (array<uint16> names) |
os’s readdir — already answered, and it is the worked precedent: src/core/os/windows/dir_windows_impl.cs reads the kernel’s buffer at NATIVE offsets instead of reinterpreting it as the managed surrogate |
WSASendMsg / WSARecvMsg |
WSAMsg (ж<syscall.WSABuf>) |
net’s UDP OOB path |
NetUserGetLocalGroups |
ж<ж<byte>> out-buffer |
os/user |
Why this one is NOT fixed here, and what the candidate remedies cost. The established remedy is a
hand-owned wrapper with a blittable mirror and a field-for-field copy at the boundary. It does not
reach this member, because the wrapper never sees the struct: os_windows_test.go writes
(*byte)(unsafe.Pointer(&p)), which the converter emits as
Ꮡp.Reinterpret<windows.SHARE_INFO_2, byte>(), and Reinterpret correctly REFUSES to alias a
reference-bearing struct as byte — so it falls to (ж<byte>)(uintptr)box and the wrapper receives
a NATIVE-address box with the managed identity already gone. There is nothing left to copy from.
- Recover the struct by reading the raw address (
Unsafe.Read<SHARE_INFO_2>) inside a hand-ownedNetShareAdd. Rejected: it fabricates managed references out of a raw address, whichж.PointerExtensions.csnames as a CLR type-safety break and “strictly worse than the wrong-but-contained read the address route produces”. It would also rest on a pin the address route does not promise. -
Hand-own
NetShareAddto fail by name — amanualConversionFuncsplaceholder returning a declared “non-blittable struct handed to the kernel” error, theregisterCache-style announce-itself stub. Small, zero blast radius (one caller in GOROOT, and it is a test), and it converts a whole-suite process death into ONE loud row. But Go PASSES this test on a capable host, so the row would be a real mismatch rather than a skip, and the stub declares a capability limit — a coordinator ruling, not a lane’s call. Recommended, pending that ruling.✅ RATIFIED and LANDED 2026-08-14 (lane
claude/defect-batch-1). The coordinator ruled for remedy 2; the wrapper is displaced throughmanualConversionFuncsand reimplemented insrc/core/internal/syscall/windows/windows/zsyscall_windows_impl.cs, throwing aNotSupportedExceptionthat names the capability limit, the layout mechanism and this entry. The impl file’s header carries the six same-shape wrappers above as a table, so the boundary of the limit is readable where the limit is declared. The value is measured, on the capable host that produced this entry:osno longer dies at test ~32 of 174 — it runs the suite to completion and the differential is 679 matched of 683, with exactly four divergent rows:TestNetworkSymbolicLink(this declared limit, an honest mismatch — Go passes it),TestUTF16AllocandTestWriteStringAlloc(the recorded alloc-count disclosure and the r39-osalloc architectural residual), andTestDirectorySymbolicLink, which SKIPS where Go passes and is not attributed here. Compare the pre-fix floor on the same host: 31 of 679.osstill does not bank, for the reason it never did. - The durable answer is the ж-box arc’s: have the non-aliasing
Reinterpretfallback retain the source object so a hand-owned wrapper can reach it, at which point remedy 1 becomes an ordinary field-for-field copy with no fabrication.
Whichever lands, verify at VALUE level as the class demands: probe A above is the oracle — the share
must actually be created and NetShareDel must remove it.
✅ CLOSED 2026-09-02 (lane claude/sub-os-row) — remedy 3, and the root was one level deeper than remedy 1’s rejection
NetShareAdd is implemented. TestNetworkSymbolicLink agrees with Go on this host, and the
declared capability limit is retired: os moves from 682 of 685 agreeing + 1 disclosed with TWO
real divergences to 683 of 685 + 1 disclosed with ONE. It still does not bank, for the reason
it never did — TestWriteStringAlloc, ruling #1 — and that row is now the only thing between os
and a bank.
The root, one level under what this entry recorded. The entry above says the wrapper “receives a
NATIVE-address box with the managed identity already gone”, and left it there. But the address route
HAS a recovery seam — the provenance record — and zsyscall_windows_certchain_impl.cs already uses
it to recover a reference-bearing struct at a boundary wrapper
(ManagedPointerTokens.Resolve(scalar) is ж<SSLExtraCertChainPolicyPara>). So the obvious first move
was to reuse it here, and it does not work — for a reason worth writing down, because it is what
separates the two shapes:
ResolveVALIDATES ON READ — alive and still pinned there.RegisterPinnedis called by theuintptroperator either way, butEnsureStableAddresscan only pin whenPinnableStorageis non-null, and a StandardBox of a reference-bearing pointee has nom_slotat all (a reference-containing layout cannot be pinned), soPinnableStorageis null, no pin is taken,IsPinnedAtanswers false andResolveanswers null. The certchain scalar resolves because it came fromMintOpaque, which registers by the box’sPointerOrderToken— a stable identity, not an address. Two different registration routes; only one of them has a key that survives.
Measured, not argued, in a standalone probe against golib with a reference-FREE control on the
same run: a filled reference-bearing StandardBox reports PinnableStorage is null = True,
Resolve(own address) = NULL and Resolve(derived address) = NULL, while the reference-free control
resolves on both. (Probe hygiene note earned the hard way: the first arm used a default-valued
record, whose box reads as a nil pointer, so Reinterpret returned NilBox at address 0 and both
NULLs were vacuous. A reading taken through a nil pointer is not a reading — fill the record.)
The remedy — remedy 3, narrowed. PointerExtensions.Reinterpret’s unpinnable arm now REMEMBERS
its source box against the DERIVED box, in a ConditionalWeakTable beside s_mintedReferents
(ManagedPointerTokens.RememberReinterpretSource / ReinterpretSource). Keyed on the box rather than
on a number, so there is nothing to validate and nothing to go stale. It is purely additive: the
derived pointer’s numeric value and box kind are byte-for-byte what they were, and only a caller that
asks by name sees anything new — asserted as its own guard rather than claimed.
The gate is IsReferenceOrContainsReferences<T>() && !IsReferenceOrContainsReferences<TDst>(), and
the DESTINATION half is the load-bearing one: a reference-bearing destination is Go’s prefix-downcast
idiom — reflect’s (*structType)(unsafe.Pointer(t)) over an abi.Type, which is unpinnable too and
is hot — and it neither needs the source nor should pay for it. A reference-free destination is
the boundary idiom (*byte)(unsafe.Pointer(&record)), whose whole purpose is to cross into a
syscall. Corpus census at the time of writing: ~30 Reinterpret<…, byte> sites, all in syscall/os/
net boundary code, all cold; reflect’s downcast pairs are not among them.
With the source in hand the hand-own is the ORDINARY mirror-and-copy this class has always used —
a [StructLayout(Sequential)] NativeShareInfo2 local, size-asserted against the documented 56,
allocUtf16z for each LPWSTR, freed in a finally because SHARE_INFO_2 is input-only. Nothing is
fabricated out of a raw address, which is what made remedy 1 unacceptable: the real managed record is
in hand before a byte is transcribed. Two paths still THROW rather than guess — a level other than 2
(the buffer’s shape IS the level), and a level-2 buffer with no recoverable source (a genuinely native
record; no such caller exists in the corpus, and passing the scalar through — which is right in the
certchain wrapper, where an unrecognized scalar IS an address — is exactly the fatal path here).
Verified at VALUE level, as this entry demanded. The oracle is Go’s own test, which does not merely
call the wrapper: it Stats the share through its UNC path, requires os.SameFile agreement with the
local directory, creates a symlink INTO the share, reads it back with os.Readlink and resolves it
with filepath.EvalSymlinks — and its deferred NetShareDel is a t.Fatal. A mirror with wrong
offsets cannot pass that; the share must really be created and really be removable.
Gates. No converter source changed, so no CNR and no two-seeded diff is owed. GolibTests
480/480 (474 before, +6 new guards; no Test Run Aborted, and the 485-vs-480 declared gap is the
two Linux-only test files, pre-existing). The retention guard’s negative control was run: deleting
the destination half of the gate fails exactly AReferenceBearingDESTINATIONIsNotRemembered and
nothing else, and the restore is byte-identical (sha256 verified). internal/syscall/windows builds
0 errors. os pipeline re-run at Debug/tiered against a go1.23.12 oracle, on an elevated host with
LanmanServer Running and symlink creation available — i.e. a host where Go’s own
TestNetworkSymbolicLink PASSES, which is the precondition this entry exists to state.
What this leaves for the class. The four sibling wrappers in the table above are unchanged and
still take the ordinary mirror remedy. What has changed is that the byte-reinterpret fork of the
class — a record reaching its wrapper as a *byte with no typed pointer anywhere — is now repairable
at all, where before it had only a declared limit. That is the ж-box arc’s B1 input satisfied for this
shape without the arc: source retention was the missing half, and it turns out to cost a weak table
entry at ~30 cold sites rather than a box-layout change.
A measurement rule this leaves behind. A converted suite that dies with a native fault is not automatically a regression, and a bisect is not automatically the right instrument. Root the fault site FIRST — the containment work makes that cheap now — and ask whether the failing call depends on a host capability the previous measurement did not have. Here one probe at the recording commit settled it in six minutes, where a 230-commit first-parent bisect would have found nothing and said so only after eight probes.
RESOLVED (2026-08-14, lane claude/go-types-av) — go/types’ access violation is a reflect POINTER TOKEN converted back to a pointer; 15 → 202 verdicts
go/types’ converted test host builds and runs now that the CS0839 defer panic(err) blocker is
fixed, and it died with a bare access violation after 15 verdicts:
Fatal error. System.AccessViolationException: Attempted to read or write protected memory.
at go.go.types_test_package.testFilesImpl(...)
at go.go.types_test_package.testFiles(...)
at go.go.types_test_package.testPkg(...)
Exit 0xc0000005, zero verdicts after, 542 behind it. First-contact territory, so the OS playbook
applied rather than a bisect — and it paid the same way: the fault site named the defect outright,
with no probing of history at all.
The fault site. testFilesImpl’s FIRST statement after building the Config is
*boolFieldAddr(&conf, "_Trace") = manual && testing.Verbose() // check_test.go:166
func boolFieldAddr(conf *Config, name string) *bool { // check_test.go:343
v := reflect.Indirect(reflect.ValueOf(conf))
return (*bool)(v.FieldByName(name).Addr().UnsafePointer())
}
There is no frame below testFilesImpl because the faulting store is inlined at the call site;
boolFieldAddr itself returns perfectly well. TestCheck is the first test alphabetically that
reaches testFilesImpl at all — not one of the nine before it (TestAlias_Rhs …
TestBuiltinSignatures) calls testFiles; they type-check through mustTypecheck or not at all, and
neither route uses this idiom — and blank.go is the first file in
testdata/check. So the crash is the FIRST EVER EXECUTION of the idiom, not corruption surfacing
late. blank.go being a five-line package _ file is a coincidence of alphabetical order and means
nothing.
The mechanism. A Go pointer to managed storage has no machine address, so reflect.Value.Pointer
and .UnsafePointer both project through reflectPointerToken (reflect/value_impl.cs) and answer
with a stable order token — INilPointer.PointerOrderToken, whose own remarks say tokens “are
order keys, never an identity substitute”. The hand-own’s header says what it was written for: “fmt
uses it only to test nil-ness and to print an address for %p.” go/types is a new caller with the
other need. Emitted, its line is
return (ж<bool>)(uintptr)(v.FieldByName(name).Addr().UnsafePointer());
and ж<T>’s explicit operator ж<T>(uintptr) builds a native-address box over whatever number it
is handed. The subsequent .Value store writes a bool at the numeric value of an order token — an
access violation where that page is unmapped, silent heap corruption where it is not. Note this is the
unique/clone.cs family stated from the other end: there an interior address was fabricated by
ARITHMETIC on a movable box; here it is fabricated by a projection that never was an address.
Proven without go/types, in 22 lines — a struct with an unexported bool, string and int field,
the boolFieldAddr idiom verbatim, no other package involved. Go prints x true; the converted C#
dies System.AccessViolationException at go.main_package.Main(), the identical frame shape (the
store inlined into its caller). It is the whole defect, with go/types removed.
The fix — golib remembers what the projection drops. The information was never lost:
reflect.Value.Addr surfaces the real aliasing box (addrBox, minted by GoReflect.FieldAliasBox),
and only the scalar projection discards it. golib/ж.PointerTokens.cs adds ManagedPointerTokens, a
weak token→box table that reflectPointerToken registers into and the uintptr → ж<T> operator
consults first; a token that came from there recovers its box and aliases the original storage exactly
as Go’s pointer would, and everything else keeps the native-address route unchanged.
Two properties are deliberate, and both exist to keep the blast radius at zero. The token VALUE does
not change — minting self-identifying handles from a reserved range would also move what %p prints
and what order pointer-keyed maps print in, since fmt and internal/fmtsort read the very same
token — so the association is carried out of band instead. Reads are lock-free with an empty fast
path: Resolve sits on 875 emitted cast sites corpus-wide (54 in the syscall wrappers), and a
program that never asks reflect for a pointer’s scalar form answers from a single volatile load. The
type-descriptor path (typeDescriptorOrderToken) returns before registration and is untouched.
Measured movement: 15 verdicts → 202 (169 pass, 33 fail), zero access violations anywhere in the
run. The 33 failures are entirely the KNOWN-OPEN type-parameter class — TestCheck/{chans,
funcinference,typeinst1,typeparams,map0,map1,slices,issues1}.go, TestExamples/{functions,
inference}.go — the same signature as go/internal/gcimporter’s residual 108.
The next wall, named: the SAME open root, in its non-terminating form. The run now dies at
TestFixedbugs/issue48951.go with 0xc00000fd — STATUS_STACK_OVERFLOW, an unbounded recursion in
validType0:
at go.go.types_package.validType0(...) × until the stack is gone
at go.go.types_package.validType(...)
at go.go.types_package.processDelayed(...)
issue48951.go is the invalid recursive type testdata (A2[P any] [10]A2[*P]), and Go’s cycle guard
is for _, e := range nest { if Identical(e, t) { … } } (validtype.go:104). A converted Identical
that judges a parameterized named type not identical to itself never finds the cycle, so the walk
recurses forever. That is the board’s already-open second root — “a type parameter judged not
identical to itself” — appearing as a hang rather than as a bogus error message. It is a consequence
of that root, not a new defect, and it is not this lane’s.
Roster arithmetic: go/types does NOT bank. 202 of 557 with a live process-killer behind it; test
sources deliberately not committed, and the pipeline’s churn under src/core/go/types restored.
go/internal/gcimporter re-measured on the same tree: 475 of 583 matched, 108 mismatched —
UNCHANGED from its baseline, to the verdict. All 108 are still Go="pass" C#="fail" inside
TestImportTypeparamTests, and nothing else moved in either direction (14m20s, under concurrent
lane load). That is the expected answer and worth recording as a NEGATIVE result: gcimporter’s
residual is the generics root in go/types’ checker, which this fix does not touch, so a package
whose failures all sit there should not move — and did not. The two measurements now agree on the
same open root from opposite directions.
Gates. GolibTests 111/111. Full behavioral suite PASS — 574/574 transpile, compile and
goldens; 548 stdout comparisons, 0 failed, 26 skipped (no package main); 3,769 s (i7-5820K,
solo). check-no-regression NO REGRESSION — generated C# and
.csproj byte-identical across all 601 behavioral packages, 1,025 s (2 advisory converter
warnings, 0 NOT MEASURED).
go2cs.slnx build succeeded, 0 errors, 585 s — owed because golib’s public surface
gained a type. Guarded by the new
ReflectFieldAddrWrite behavioral output test, which faults with an access violation on pre-fix
golib. Doctrine: ConversionStrategies-Reference.md, A pointer reflect handed out as an
unsafe.Pointer must convert BACK.
What this leaves for the ж-box arc. The arc’s charter item — “have the non-aliasing fallback retain
the source object” (remedy 3 of the NetShareAdd entry above) — is the general form of what this table
does for one seam. This fix is deliberately narrower: it restores the round trip for pointers that
reflect itself handed out, and changes nothing about the transient-address model that
(uintptr)ж<T> uses for everything else. A pointer whose scalar form was produced by arithmetic
(uintptr(unsafe.Pointer(&x)) + offset) still cannot come back, and still should not.
✅ CLOSED — the TB.* domino: the whole testing.TB surface is supported, and os/exec is measured for the FIRST time (2026-08-14, lane claude/tb-surface)
The unnamed-variadic lane’s closing prediction — “naming TB.* support in the hand-owned core/testing
host is the exact next domino, and it is a host change with no converter component” — was right about
the domino and inverted about where the work lived. Nothing in the host needed implementing, and the one
line of code that changed is in the CONVERTER: the capability ROSTER was the entire defect.
The mechanism, because it is not obvious. A capability name is keyed on the RECEIVER’s named type
(analyzeTestingCapabilities): a call written t.Fatal(err) records T.Fatal when t is a
*testing.T and TB.Fatal when the enclosing helper declares t testing.TB. Those are two roster
entries over ONE implementation, and supportedTestCapabilities listed only the first. So any test
whose transitive same-package closure touched a testing.TB-typed helper was gated out wholesale —
not for anything it did, but for the type its helper’s parameter happened to name.
Why widening it is honest, and what “supported” mechanically requires. Three things must hold, and
for TB all three already did:
core/testingdeclares the member — itsTBinterface carries Go 1.23’s full public set (the 18, minus the unexportedprivate()).- Something forwards it for the concrete receiver. The converter already emits
[assembly: GoImplement<testing_package.T, testing_package.TB>(Pointer = true)]into every consuming package, and go2cs-gen’sImplementGeneratormints thetesting_TжTBadapter from it — read out of the generated file rather than assumed:void TB.Fatal(Span<object> args) => testing_package.Fatal(m_box, args);, once per member, all 18. - The implementation is real. Each forward lands on the same
TestExecution-backed member theTspelling has always used, so aTB.FailNowthrows the sameTestAbortExceptionand aborts the same way.
So T never needed a base list and no per-suite wiring was ever owed — a standing note on
core/testing’s TB predicted that work, and the adapter had already made it unnecessary. That note
is now corrected in place.
The one declared limit, and it is a property of B, not of TB: an adapter built from a
*testing.B forwards to B’s compile-only no-ops. Benchmarks are never registered or run, so the
only path that puts a live B behind a TB parameter is a Test that calls testing.Benchmark itself
and hands the b onward. No suite does. If one appears its failure reports would be silently
swallowed — a Phase-4D benchmark-execution question, not a reason to withhold the T-backed surface
from every test that has one.
Charter §9 discharged — the roster scan, with its positive control. Widening
supportedTestCapabilities moves excluded tests into the RUN set of packages already banked, so the
rule is to scan every validated package first. Same-package testing.TB helpers exist in 15 GOROOT
packages (cmd excluded); the only two non-test files that declare one are internal/testenv’s, and
those are consumed cross-package, which the same-package reference graph deliberately does not follow.
Four of the 15 are banked, and one of those four — internal/fuzz — does not move, because its
newWorkerForTest(tb testing.TB) is reached only from Benchmark declarations already excluded by
kind. Positive control: the scan finds os/exec’s exePath, the helper this whole entry is about.
The three that DO move were re-measured, and every one moved by COUNT ONLY — the sweep’s sole complaint in each case, with no verdict disagreeing on either side:
| Package | Banked | Now | The tests that were being withheld |
|---|---|---|---|
debug/gosym |
9 | 10 | Test115PclnParsing — pass/pass |
internal/zstd |
534 | 536 | TestAlloc, TestLarge — skip/skip, both gated on a zstd binary this host lacks |
archive/zip |
98 | 100 | TestZip64, TestZip64EdgeCase — pass/pass; the first streams 4 GiB through the writer, and is most of that row’s 775 s |
strings, bytes and context re-validated at their exact banked counts as capability-regression
controls: their exclusion sets are unchanged, which is the property a widening most needs to prove.
One caveat is banked with internal/zstd, because a skip that agrees is not a test that agrees: on a
host WITH zstd installed both rows actually RUN, and TestAlloc asserts an exact zero allocations
— the alloc-profile class the CLR provably cannot satisfy. Expect it to need a disclosure there. The
row COUNT is stable either way, so the sweep’s floor holds; only the verdict would move.
os/exec — measured for the first time: 101 verdict rows, 48 agreeing, and TWO host roots
Before this change the pipeline compared 22 rows and all 22 matched, which read like a nearly-clean
package. It was not a measurement: the 26 excluded tests were every process-spawn shape os/exec has,
and nothing was known about them. With the surface widened the manifest carries 40 included test
declarations and zero capability-blocked ones (13 excluded remain, all Phase-4D: 12 Example, 1
Benchmark), and the comparison covers 101 verdict rows.
| Class | Rows | What it is |
|---|---|---|
| agree | 48 | 40 pass + 8 skip, the skips identical on both sides |
| disagree — root A, helper-command argv | 26 | every one Go="pass" C#="fail" |
| disagree — root B, relocatable single-file test executable | 27 | every one Go="pass" C#="fail" |
No verdict disagrees for a reason belonging to os/exec itself. Both roots are properties of the
converted TEST HOST, and one of the two is an impossibility this file already declared.
Root A — the host throws on its own test binary’s argv. os/exec drives its helpers by re-executing
the test binary with a bare command name (exec.Command(exePath(t), "cat")), and its TestMain
dispatches on os.Args[1]. Go’s flag.Parse() stops at the first non-flag argument and leaves the rest
for the program. The converted host’s TestOptions.Parse instead has no stopping rule: its default:
arm throws ArgumentException("unsupported converted test option: cat"), so every helper child dies at
startup with exit 2 before TestMain is ever entered. The parent then reports the downstream symptom
— echo: want "foo bar baz\n", got "", ExitCode got 2, want 42, Wait: exit status 2, first output
line: EOF — which is why this reads as twenty different failures instead of one. It is a small, closed
fix: os.Args is wired to Environment.GetCommandLineArgs() (runtime/goargs_impl.cs) independently
of the option parser, so the child already CAN see its argv; the host merely has to stop rejecting it,
with Go’s stop-at-first-non-flag rule rather than by ignoring unknown tokens (exe cat -n must leave
-n to the child, not parse it as a host flag). This is the next domino.
Root B — the same 0x8000809a this file already names. TestLookPathWindows and TestCommand
build their fixtures by COPYING the test binary into a temp directory and running the copy. Every such
row fails with exit status 0x8000809a and The application to execute does not exist:
‘…\p1\os.exec.tests.dll’ — byte for byte the LibHostAppRootFindFailure already declared for
os_test.TestRemoveAllWithExecutedProcess under the capability “relocatable single-file test
executable”: an apphost is a stub bound at build time to a managed assembly of the same base name that
must sit beside it. Go’s statically-linked test binary is the only reason the premise holds there.
Deliberately NOT gated. Root B’s 27 rows could be excluded by declaration name the way os’s one
test is, but doing it now would be half-work: root A’s 26 rows would still fail, so os/exec would not
bank either way, and the next lane would have to revisit the same list. Root A is unimplemented, not
impossible, so it must never become a gate entry at all (the standing rule for
unsupportedRuntimeCapabilities). os/exec therefore stays unbanked, with both roots named, and its
converted test sources were removed rather than committed.
The CLASS this domino was holding — every package with a same-package testing.TB helper
internal/testenv’s TB-typed helpers, which most of the corpus calls, never gated anything: the
capability graph closes over SAME-package references only, so a cross-package helper gates through its
own package’s conversion instead. What gated was a package’s own test-file helper. The complete GOROOT
census (cmd and testdata excluded), with the banked rows re-measured here and the rest recorded:
| Package | Banked? | TB-typed helper | Effect of the widening |
|---|---|---|---|
os/exec |
no | exePath |
26 tests unblocked — measured above; the reason this lane exists |
archive/zip |
yes | testZip64 |
re-measured, see the roster |
debug/gosym |
yes | read115Executable |
re-measured, see the roster |
internal/zstd |
yes | findXxhsum, bigData, findZstd, zstdBigData |
re-measured, see the roster |
internal/fuzz |
yes | newWorkerForTest |
none — reached only from Benchmarks, excluded by kind |
database/sql |
no | 36 sites, the widest in the corpus | pending its own measurement |
net |
no | 9 sites | pending |
net/http |
no | 8 sites | pending |
crypto/tls |
no | 3 sites | pending; the two build roots are CLOSED (2026-08-15) and the package compiles, but its run does not reach these sites — see the section at the end of this file |
crypto/internal/mlkem768, crypto/x509, go/types, net/netip, os, runtime |
no | 1 site each | pending |
The nine unbanked rows are not claims — no run was commissioned for them. They are named so the next lane to reach any of them knows this particular exclusion is already gone.
✅ CLOSED — the helper-argv domino: the host stops at the first non-flag argument, and os/exec’s root A is gone. It does NOT bank: root B holds, and a THIRD root appears underneath (2026-08-14, lane claude/host-argv-stop)
The TB.* lane named root A as the next domino and priced it as “a small, closed fix”. It was: one
hand-owned file, src/core/testing/TestOptions.cs, and no converter component at all. 23 of root
A’s 26 rows now agree. The other 3 were never really root A — the helper child died at argv before
it could reach the thing that was ALSO going to kill it, and fixing the first revealed the second.
The fix, and why it is Go’s rule rather than a leniency. flag.(*FlagSet).parseOne stops at the
first token that is not at least two characters long and beginning with -; that token and every one
after it belong to the program. A Go test binary IS a program — its TestMain may take arguments —
and os/exec drives its entire helper protocol that way: exec.Command(exePath(t), "cat"), with
TestMain dispatching on flag.Args()[0]. TestOptions.Parse had no stopping rule; its default:
arm threw unsupported converted test option: cat and the host exited 2 before TestMain was
entered. Nothing else was owed: the converted os package fills os.Args from the real command line
independently of this parser (Windows via syscall.GetCommandLine + commandLineToArgv; unix via
runtime.argslice), so the host’s whole obligation is to stop and leave the rest untouched.
Stopping is not ignoring, in both directions, and the guard pins both: exe cat -n must leave
-n to the child, while an unrecognized -flag BEFORE any non-flag is the host’s own command line
being wrong and still exits 2 — with Go’s wording now (flag provided but not defined: -x), since
this host stands in for a Go test binary and its stderr is read beside one. The rest of parseOne is
mirrored for the same reason: a lone - is a non-flag, -- terminates and is consumed, ---x/-=x
are bad flag syntax, a non-boolean flag takes the next token as its VALUE even when that token looks
like a flag (-run -v filters on -v), and one or two leading dashes name the same flag — the
--json ≡ -json equivalence TestFlagBridge already assumed when it republishes these options
undashed. Nothing records the leftover tokens: the program reads its own argv, and an unread property
would be machinery this host does not need.
os/exec re-measured: 101 rows, 74 agreeing (67 pass + 7 skip), 27 disagreeing
| Class | Rows | Then | Now |
|---|---|---|---|
| agree | 74 | 48 → 71 | 67 pass, 7 skip |
| disagree — root A, helper-command argv | 0 | 26 | ✅ closed by the host-argv-stop lane |
| disagree — root B, relocatable single-file test executable | 27 | 27 | unchanged, and still a DECLARED limit |
disagree — root C, os/signal’s runtime primitives are unimplemented stubs |
0 | 3 | ✅ closed by the os-signal-primitives lane |
Root C is closed (2026-08-14). The six primitives now forward through the //go:linkname push
registry into runtime/sigqueue.cs’s own state machine, and the two dead ends behind the forwarders —
nothing armed the Windows console control handler, and notetsleepg threw on getg() before it could
block — are closed with it. TestWaitInterrupt/{Wait,Exit-hang} and their parent all pass;
cmdHang’s signal.Ignore(os.Interrupt) runs. Detail:
ConversionStrategies-Reference,
“os/signal’s six primitives”.
The disagreeing set is now exactly root B — the 14 TestCommand/*, 11 TestLookPathWindows/*
and their two parents — so os/exec sits at all-agree-except-declared-limit, which is precisely the
shape the coordinator ruling below was reserved for. This lane therefore reports and STOPS: no
bank.
os/signal’s own suite cannot pay this domino back, and the census says why (2026-08-14). It was
re-run end to end (-test-action all) after the primitives landed and it re-validates 1/1,
unchanged — because its Windows-eligible RUN surface is exactly one test. signal_test.go is
//go:build unix, signal_linux_test.go is linux, signal_cgo_test.go needs cgo,
signal_plan9_test.go is plan9, and example_test.go’s two examples carry no // Output: comment,
so go test never executes them (they stay excluded on both sides). That leaves TestCtrlBreak,
which compiles a native Go child with the real go tool and sends it a console control event — so
it exercises syscall.LoadDLL/GenerateConsoleCtrlEvent and os/exec, and never touches a single
converted signal primitive. That is why it passed while all six were throwing stubs, and why it still
passes now: it is not a regression detector for this area in either direction. The guard that does
cover the primitives is the SignalPrimitives behavioral test (Notify/Stop/Ignore/Reset/Ignored
transitions compared against go run, which also proves signal_recv really parks — Stop blocks
in signalWaitUntilIdle until the watcher goroutine reaches sigReceiving).
Real console-event DELIVERY was measured out of band rather than left to inference: a scratch
two-process probe in TestCtrlBreak’s own shape — child started with CREATE_NEW_PROCESS_GROUP,
GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, childPid) from the parent — run against the converted
child and against a native Go child built from the same source. Both print ready then
got: interrupt and exit 0, repeatably. Worth recording from that probe: the FIRST attempt reported
exit status 0xc000013a (STATUS_CONTROL_C_EXIT) with no output at all, and it was a cold-start
race in the probe, not a defect — a cold Debug child carrying the full converted runtime closure had
not yet reached signal.Notify when the 3-second timer fired, so the wanted bit was unset,
sigsend correctly returned false and the default handler killed it (buffered stdout dying with it).
Raising the delay to 8 s made it deterministic. A probe that sends the event on a timer instead of
waiting for the child to announce readiness will keep re-finding this. DELIVERY still has no
SUITE guarding it — that fixture is the honest next item here.
⚠ One row of the split is unreconciled, and is recorded rather than smoothed. The totals move
exactly as root C predicts (101 unchanged; disagree 30 → 27; agree 71 → 74), but the internal split
moved by four passes and one skip (63 pass + 8 skip → 67 pass + 7 skip) where root C accounts for
three. The likely cause is that a subtest of the previously-FAILING TestWaitInterrupt parent was
counted as a skip while its parent failed and now runs and passes; that was not re-derived against
the old binary, so it is a hypothesis, not a finding. The 27 disagreeing rows were enumerated by name
from the run and are all root B.
Two minor host observations, recorded rather than fixed (neither moves a verdict): the host’s
fmt-free TestFormat renders *exec.Cmd and *strings.Builder under %v/%s as raw pointers
(0x1fc631592e0), so t.Log(cmd) and t.Logf("stderr:\n%s", cmd.Stderr) print addresses — which is
what made root C’s diagnostic invisible in the comparison output and cost a direct child run to see.
⛔ STOPPED for a coordinator ruling — and the ruling is no longer load-bearing for os/exec
The lane was chartered to stop, if os/exec landed at all-agree-except-declared-limit, and ask
whether declared-host-limit rows can carry a bank the way alloc-profile disclosures do. It did not
land there — root C is undeclared and unimplemented, so os/exec is unbankable on its own merits
whatever the answer. Its converted test sources were removed rather than committed, exactly as the
TB.* lane left them. The question is still worth answering, so here is the measured shape:
- Can the disclosure machinery pin root B by signature? Mechanically, yes. All 25 leaf rows
(14 under
TestCommand, 11 underTestLookPathWindows) carryexit status 0x8000809aand The application to execute does not exist in their C# failure text — verified, 25 occurrences for 25 leaves.matchTerminalStatusesneeds exactly that: an entry keyed on the test NAME,Go="pass"∧C#="fail", and the declaredsignaturepresent as a substring. The 2 parent rows carry no own output and would ride the existing disclosed-parent aggregation rule (no own text, ≥1 disclosed descendant, no mismatched descendant), so 25 entries would cover all 27. - But the precedent for this exact capability is a GATE, not a disclosure.
os_test.TestRemoveAllWithExecutedProcessis listed inunsupportedRuntimeCapabilitiesunder the samerelocatable single-file test executablename — the test is EXCLUDED from the run set, never run and disclosed. A gate keys on the test DECLARATION, soTestCommandandTestLookPathWindowswould be 2 entries rather than 25. - And no banked package currently carries either form of this limit.
osis not on the roster (onlyos/exec/internal/fdtestandos/signalare), so the 141-package roster contains no precedent for a bank resting on a declared HOST limit. Every existing disclosure class is about measurement the CLR cannot perform (alloc counts), not about a test the host cannot run at all.
That is the whole question, and it is a policy one: whether “the host provably cannot execute this shape” is bankable at all, and if so whether it should read as an excluded capability (2 gate entries, tests never run) or as disclosed divergences (25 signature entries, tests run and fail visibly). Recorded here for the next lane that reaches a package where it IS load-bearing.
RESOLVED (2026-08-14, lane claude/typeparam-identity) — the type-parameter identity wall is EMBEDDED-STRUCT COPY ALIASING, and the fix is one generator field
The campaign’s last mega-wall — one root, three dependents, ~450 verdicts: go/internal/gcimporter’s
108 TestImportTypeparamTests mismatches (the constrained by signature), go/types’ own 33
failures plus the 0xc00000fd stack overflow at TestFixedbugs/issue48951.go, and
go/internal/srcimporter’s 2 cannot infer T failures. None of it is generics, Identical, or
the instance caches. It is a struct value copy that was not a copy.
The hypothesis was wrong, and instrumentation said so in one run. The standing suspicion —
*TypeParam compared by pointer identity through a conversion that mints distinct
wrappers/adapters/boxes, so Identical(t, t) fails reflexivity — is false. Instrumented at
predicates.cs’s identical, the two operands of the failing comparison are TWO DIFFERENT
*TypeParam objects (ids 2 and 3) and AreEqual answers False correctly; a same-object
comparison answers True with the same box on both sides. The Context/instanceHash/lookup
caches were instrumented too and behave exactly as Go’s: distinct hashes (00S[T₂] vs 00S[T₃]),
zero cache hits, no collapse. Everything the board pointed at was healthy.
The 7-line reproducer. Driving the converted go/types.Check directly over a tiny source (the
types/errors lane’s harness shape — build once, then a ~1 s run per probe) minimizes
test/typeparam/absdiff2.go’s 13 errors down to:
package main
type S[T any] struct{ V T }
func (a S[T]) M1() T { return a.V }
func (a S[T]) M2() T { return a.V } // C#: cannot use a.V (variable of type T …) as T value
Go reports 0 errors. The converted checker reports 1, on the SECOND method only — delete M1
and it passes. Spelling the second method’s parameter differently (func (b S[U]) M2() U) prints
the tell outright: b.V has type T, the FIRST method’s type parameter.
The mechanism, printed. Named.expandUnderlying substitutes the origin’s underlying per
instance. Instrumented:
[EXPAND] targs=[TP#2] origUnder=struct{TP#1} smap={TP#1->TP#2} -> newUnder=struct{TP#2}
[EXPAND] targs=[TP#3] origUnder=struct{TP#2} smap={TP#1->TP#3} -> newUnder=struct{TP#2}
^^^^ the ORIGIN was mutated by the first substitution
subst.go’s substVar is copy := *v; copy.typ = typ. go/types.Var EMBEDS object, which
carries typ — and go2cs-gen held a promoted embed in a shared ж<T> box, so the C# struct
assignment copy = v handed both sides one object and copy.typ = typ wrote into the ORIGINAL.
The origin’s struct{V T₁} became struct{V T₂}; the second method then substituted {T₁ → T₃}
over a struct that no longer mentioned T₁, kept T₂, and Identical(T₂, T₃) correctly said no.
cannot infer T and validType0’s unbounded recursion are the same fact in their other costumes.
Proven at golib level in 12 lines, no go/types involved — a struct embedding another, one
plain assignment and one c := *p:
| Go | C# (pre-fix) | |
|---|---|---|
a.v after b := a; b.v = 2 |
1 |
2 |
a.tag (an ORDINARY field) after b.tag = "b" |
a |
a |
orig.v after c := *orig; c.v = 99 |
10 |
99 |
The ordinary field copies correctly; only the embed aliases. This is the gap
GoValueCloneAttribute’s own remarks had already NAMED and set aside — “embedded-struct copy
aliasing is a separate, pre-existing gap” — declared in the array-clone arc and never priced. Its
price was this wall.
The fix is one field in StructTypeTemplate: the embed becomes an INLINE field.
private @object ʗobject; // was: private readonly ж<@object> Ꮡʗobject;
[UnscopedRef] internal partial ref @object @object => ref ʗobject; // was: => ref Ꮡʗobject.ValueSlot;
The box existed only because a struct member cannot ref-return its own instance state (CS8170);
[UnscopedRef] states the ref’s lifetime is the receiver’s — exactly the guarantee Go gives, since
the selection IS the enclosing value’s storage — and the repo already used that technique in
InheritedTypeTemplate. Five emission sites in one file (field, accessor, promoted-field accessor,
two constructor initializers). No converter change, no corpus regen, no golden churn: the
generator’s output is not committed, so the emitted .cs is byte-identical.
Two things improve for free: a default(T) reached where no constructor runs (a missing-key map
read, a freshly maked element) no longer carries a null embed box — the previously documented
residual gap narrows to embedded types that need construction in their own right; and the C# struct
= now means what Go’s = means for every embedding type in the corpus.
The one residue, named and unchanged. A fixed ARRAY reached only THROUGH an embed is still
shared after a copy (array<T> is a struct over a shared T[], and typeNeedsValueClone skips
embedded fields when deciding the [GoValueClone] stamp). It was shared before this fix and is
shared after, by a different mechanism. Widening the walk is now SOUND — the generated
copy.<member> = <member>.ΔClone() lands in the copy’s own inline storage instead of corrupting the
source, which is precisely why it was excluded — but it moves converter EMISSION corpus-wide and
belongs to a change that owns that footprint.
The unlock, MEASURED across all three dependents
| Package | Baseline | After | Verdict |
|---|---|---|---|
go/internal/gcimporter |
475 of 583 (108 mismatches, all TestImportTypeparamTests) |
583 / 583, 0 mismatches | BANKED — status: validated, 14 skipped identically |
go/internal/srcimporter |
5 of 7 (TestImportStdLib, TestImportedTypes died on sync.OnceValue(func() bool {…}) — cannot infer T) |
7 / 7, 0 mismatches | BANKED — status: validated, TestCgo skipped identically |
go/types |
202 verdicts (169 pass / 33 fail), then 0xc00000fd in validType0 at TestFixedbugs/issue48951.go |
513 verdicts, 512 pass + 1 skip, ZERO failures | not banked — 44 verdicts never produced; a NEW wall, below |
go/types is the measurement that settles it: the type-parameter class is entirely gone. Every
TestCheck/*, every TestExamples/*, and every TestFixedbugs/* — issue48951 among them, the
invalid-recursive-generic that used to exhaust the stack because Identical(e, t) never fired —
now passes, and not one produced verdict disagrees with go test. Its 44 absent rows are one
process-killer, and it is a different animal (next entry).
The NEXT wall, named: TestSizeof exhausts the stack in golib’s reflect LAYOUT walk
RESOLVED 2026-08-15 (lane
claude/gosizeof-recursion) — pre-existing golib defect, exactly as this section suspected but declined to assert; the standalone probe it asked for measured the cycle identically on both golibs.go/typesbanks at 557. Entry at the end of this board.
go/types’ run now dies at TestSizeof — the first test alphabetically past the ones that pass —
with an unbounded recursion whose frames alternate exactly two functions:
at go.GoReflect.tryStructLayout(System.Type, IntPtr[] ByRef, IntPtr ByRef)
at go.GoReflect.GoSizeOf(System.Type, IntPtr[]) x until the stack is gone
at go.internal.abi_package.synthesizeDescriptor(...)
at go.reflect_package.TypeOf(System.Object)
at go.go.types_internal_test_package.TestSizeof(...)
It takes TestSizeof and everything alphabetically after it (TestSpec/* and the rest — 44
verdicts) and it is not the type-parameter class: GoSizeOf has no memo and no depth guard,
and the walk is reached the moment reflect.TypeOf is asked to synthesize a descriptor for a
go/types struct. Deliberately NOT attributed to the embed change on the evidence available: the
projection reports the SAME field TYPE for an embed before and after (the old arm unwrapped
ж<T> to T; the new one reads the inline field, whose type is T), so the walk sees the same
graph — but that is an argument, not a measurement, and nobody has run this test on the pre-change
golib because the type-parameter wall stopped the suite ~300 verdicts earlier. Whoever takes it
should settle that first, with a standalone GoSizeOf probe over the same types on both
golibs; if it is pre-existing it is a golib defect in its own right (a memo plus a cycle guard is
the obvious shape), and if it is not, the embed model is implicated and the probe says how.
One coupling the embed change DID break and this lane fixed: GoReflect.collectGoFields
recognized a promoted embed by the old box shape (a field named ᏑʗName whose type is ж<T>),
so an inline ʗName fell through to the generic arm and reported the Go field under its MANGLED
name. The projection now keys on the ʗ marker alone and reports the field’s own type with no
pointer hop. It does NOT change the recursion above (measured: identical stack, identical 44
absent verdicts before and after), which is part of the evidence that the recursion is not the
embed’s.
Guarded by the new EmbeddedStructValueCopy behavioral output test (assignment, by-value
parameter, a two-level c := *p, a slice-element read, and a pointer embed proving both halves of
Go’s rule). Doctrine: ConversionStrategies-Reference.md, An embedded struct is an INLINE field,
so a value copy copies it.
⛔ MEASURED, DOES NOT BANK — the host-limit GATE is self-defeating on os/exec, and it costs 13 agreeing rows before it even gets there (2026-08-15, lane claude/os-exec-gate-bank)
RULING REVISED on this evidence, same day — the gate preference is WITHDRAWN and root B resolves as a DISCLOSURE.
os/execbanks at 74 matched · 27 disclosed (host-limit) — see the disposition at the end of this entry. Everything below stands as measured; it is the evidence the revision rests on, not a live proposal.
The coordinator ruling delivered to this lane was to bank os/exec at 74 agreeing verdicts under the
GATE form: two declaration-keyed entries in unsupportedRuntimeCapabilities, tests excluded from the
run set, no disclosure. The gate itself works exactly as designed — the keys are right, the two
declarations are blocked, the manifest reads 38 included + 2 capability-blocked + 13 Phase-4D
excluded, and every surviving row agrees on both sides. It still does not bank, for two reasons
the ruling’s evidence could not have contained, both measured here rather than argued.
The keys, since the brief asked them to be verified empirically
TestCommand and TestLookPathWindows both live in lp_windows_test.go, which is package
exec_test, so the keys are the EXTERNAL test package’s path:
"os/exec_test.TestCommand": "relocatable single-file test executable",
"os/exec_test.TestLookPathWindows": "relocatable single-file test executable",
os/exec_test, not os/exec and not exec_test. Both fired on the first attempt.
Finding 1 — a declaration-keyed gate withdraws 40 rows, not 27, and os/exec under it is 61
eligibleTerminalTestResults cuts a verdict row at its first /, so gating a declaration takes
every subtest with it — including the ones that were already agreeing. Measured on the same machine,
same toolchain, back to back:
| rows | go | go2cs | agreeing | |
|---|---|---|---|---|
| ungated | 101 | 93 pass, 8 skip | 66 pass, 27 fail, 8 skip | 74 (66 pass + 8 skip) |
| gated (2 entries) | 61 | 53 pass, 8 skip | 53 pass, 8 skip | 61, zero mismatches |
The 40 withdrawn rows are 18 under TestCommand (parent + 17 subtests) and 22 under
TestLookPathWindows (parent + 21). Only 27 of them were disagreeing; the other 13 are passes
that the gate discards along with them — 10 of TestLookPathWindows’ 21 subtests and 3 of
TestCommand’s 17. The reason a table-driven test splits at all is that not every case reaches a
copied executable: for TestLookPathWindows the correlation is exact, every failing case wanting a
.exe (which the ground-truth check then RUNS) and every passing case either expecting
exec.ErrNotFound — nothing is executed — or wanting a .bat, which installBat writes as text
rather than copying. So the choice is not “74 with a gate or 74 with disclosures” — it is 61 with a
gate or 74 with disclosures. The 74 figure in the ruling is reachable only by the arm the ruling
declined.
The ungated re-measurement also reconciles the split the previous lane recorded as unreconciled: it is 66 pass + 8 skip, not 67 + 7. One row differs, and this run agrees with the arithmetic in both directions (61 gated agreeing + 13 withdrawn passes = 74; 8 skips are all outside the gated set).
Finding 2 — the gate ARMS a TestMain census that only the gated tests could have satisfied
This is the decisive one, and it is a property of gating rather than of os/exec. Under the gate the
run reports zero mismatches — and then the converted host exits 1 anyway:
helper command unused: "printpath"
os/exec’s TestMain runs a completeness census after m.Run(): every helper registered with
registerHelperCommand must have been marked used. lp_windows_test.go’s init() registers
printpath, and the ONLY callers of maySkipHelperCommand("printpath") are the two tests the gate
removed. Their init() still runs — a gate excludes declarations, not files — so the helper is
registered and never claimed.
Gating the failures is what arms the census. Go guards it with code == 0, so in the ungated run
the 27 failures made m.Run() non-zero and the census never executed; that is why no earlier
measurement of os/exec ever saw this line. The gate greens the suite, the census fires, and the
package fails at the process level with no failing test to point at. Under the gate os/exec does
not validate at 61 or at any other count.
Why the precedent looked free. os_test.TestRemoveAllWithExecutedProcess is gated under the same
capability name and has never shown this, because os’s TestMain is Exit(m.Run()) with no census
— and because os is not on the roster, so nothing measures it. The precedent is therefore evidence
that the gate MECHANISM works, not that it is free.
The general shape, and the seam where the gate form could be made viable
A capability gate is invisible to the converted host: the converter simply omits the declaration
from the emitted TestRegistry, so nothing at runtime knows a subset is running. Go’s own vocabulary
for “a subset ran” is a non-empty -test.run, which is exactly what its census gates on
(flag.Lookup("test.run").Value.String() == ""), and TestFlagBridge publishes test.run as
options.RunPattern — empty on every pipeline run. So any converted suite whose TestMain asserts
that the whole suite ran will answer wrongly while a gate is active. os/exec is the first package
in the corpus where a gate and such a TestMain coexist; it will not be the last.
Making the gate arm viable therefore means making the host’s test.run truthful: the converter would
have to emit the gated set into the generated host (it currently just drops it), and the bridge would
publish a pattern naming the included set. That is honest rather than lenient — it is Go’s own
statement of the same fact — but it is a converter change plus a hand-owned core/testing change,
so it owes a full validated sweep, and even then the package banks 61, not 74.
The disclosure arm, priced rather than assumed
Measured on the same binary with the gate disabled and a 25-entry signature manifest.
Validated 74 tests against go test (8 skipped identically on both sides,
27 disclosed-divergent (, host-limit), 13 disclosed-unsupported declarations excluded).
It validates, and at exactly the count the ruling named — 74 matched, 27 disclosed, a proof page written. Nothing was banked from it: the artifacts are removed from this branch and the arm is recorded here as a measurement, because admitting the class is the coordinator’s call, not a lane’s.
All 25 leaves carry exit status 0x8000809a (verified: 25 occurrences for 25 leaves) and both
parents carry no failure output of their own, so they ride matchTerminalStatuses’ existing
disclosed-parent aggregation. Because the tests RUN and fail, m.Run() is non-zero and the helper
census never arms — Finding 2 does not exist on this arm. The csErr forgiveness path
(goErr == nil && len(disclosed) > 0 && len(mismatches) == 0 && len(csResults) > 0) covers the
host’s nonzero exit.
The (, host-limit) in that line is quoted verbatim and is a pre-existing cosmetic defect, not a
symptom: the class list is built from disclosures[name].Class over every disclosed row, and the two
rows disclosed by the parent-aggregation rule carry no manifest entry, so the empty class joins the
set. Any package with an aggregated disclosed parent prints it (encoding/binary’s TestSizeAllocs
is the same shape). Recorded, not fixed here.
What it costs is doctrinal, and it is the cost the ruling already weighed: 25 signature entries
rather than 2 declaration keys, and a THIRD disclosure class alongside alloc-profile and
codegen-liveness — one that is not about a measurement the CLR cannot perform but about a test
whose whole premise the host cannot satisfy. docs/ValidatedTestPackages.md’s preamble defines a
disclosure narrowly enough that admitting this class is a decision, not an application.
What landed on this branch regardless of the fork
The proof page now enumerates, per capability-gated declaration, every verdict row go test
reports underneath it, read from the UNFILTERED Go results (they exist nowhere after
eligibleTerminalTestResults) and published under a Gated by a host capability section. This was
the ruling’s third annotation requirement, and it had to be a generator feature rather than a hand
edit, because a proof page is regenerated on every sweep and a hand-added section would be silently
overwritten. It is also what makes Finding 1 visible: without it a gate reads as “2 declarations
excluded” on the page while withdrawing 40 rows.
It is owed by the NEXT gated package to bank rather than by this one: three capability entries stand
today (syscall.CommandLineToArgv, os_test.createMountPoint,
os_test.TestRemoveAllWithExecutedProcess), all in packages not yet on the roster, and os — already
measured at 158/178 — carries two of them. Its page would otherwise claim a matched count while
saying nothing about the rows those gates take.
Guarded by TestCapabilityGatedDeclarationsEnumerateSubtestRows (the row roll-up, the sorted order,
the prefix-vs-segment distinction, and the empty case) and by
TestDeclarationKeyedCapabilityEntries, which pins the standing declaration entry by exact key and
requires any entry naming a Test to be keyed on an EXTERNAL test package path — a mis-keyed gate
fires silently, which is the trap this lane nearly walked into.
⚠ The evidence for that feature is split across two halves rather than one end-to-end run, and it
has to be, today. The producing half ran on real data — the gated os/exec comparison recorded
TestCommand → 18 rows and TestLookPathWindows → 22 — and the rendering half is pinned by the
fixture golden. What has never executed is the composition, because the page is written only for a
comparison that VALIDATES and no gated package validates yet (Finding 2 is why). The first package to
bank with a gate active is the end-to-end proof; until then this is two proven halves and one
function call between them.
Not landed: no roster row, no proof page, no committed test sources, no disclosure manifest —
and not the two gate entries either. os/exec remains unbanked and, more importantly, stays
MEASURABLE exactly as it was: 101 rows, 74 agreeing, 27 rooted failures. Landing the entries would
have traded that for one opaque process-level exit, so they live in this entry (verified, above,
ready to paste) and as a standing note beside unsupportedRuntimeCapabilities rather than in the map.
The branch carries the page feature, the guards, and this record.
What is actually open, in the coordinator’s terms
Three ways forward, each with its measured price:
- Gate, made viable — publish a truthful
test.runwhen declarations are gated (converter emits the gated set into the host;TestFlagBridgepublishes a pattern). Costs a hand-ownedcore/testingchange and therefore a full validated sweep;os/execthen banks 61. - Disclosure — 25 signature entries land today and
os/execbanks 74 matched · 27 disclosed, with no new machinery. Costs a third disclosure class and a preamble that admits host limits alongside CLR-measurement limits. - Neither — drop the gate entries and leave
os/execunbanked but fully MEASURABLE at 101 rows with 27 rooted failures, which is what it is today. The cheapest, and it keeps the 74/27 shape visible for whenever the single-file-publish capability is actually built.
Option 3 is where this branch leaves things, because it is the only one a lane can choose on its own: options 1 and 2 each change doctrine (a hand-owned host contract, or what the word disclosure admits), and those are rulings. The one thing that must not happen is adding the gate entries without option 1’s work — that trades 27 visible, rooted, well-understood failures for a single opaque process-level exit and makes the package measure worse than it does today.
✅ DISPOSITION (2026-08-15, ruling revised, lane claude/os-exec-disclosure-bank) — root B is a DISCLOSURE, os/exec BANKS at 74 + 27
The coordinator withdrew the gate preference on the measurement above and ruled root B a disclosure.
The deciding property is one this lane had priced only as a cost: a disclosure keeps every row
running, so it retires itself. A gate withdraws the 27 rows from the comparison entirely, which
means the day a self-contained single-file test host makes their premise true, nothing happens —
no signal, no failure, the entries simply stay forever. Disclosed, those rows keep executing and keep
being compared, so on that day they start passing, the disclosed arithmetic stops matching, and the
sweep fails loudly until the entries are removed. Add the 13 destroyed passes and the self-defeating
TestMain census, and the fork closes.
Banked: 74 matching (66 pass + 8 skip) + 27 disclosed-divergent. 25 leaf rows pinned by signature
on exit status 0x8000809a; the 2 parents carry no failure text of their own and ride
matchTerminalStatuses’ disclosed-parent aggregation. Roster row 144;
docs/validation/current/os.exec.md names all 27 with class and reason.
The third class, host-limit, is authorized and its bar is recorded in two places —
docs/ValidatedTestPackages.md’s preamble (the reader-facing definition) and
docs/ConversionStrategies-Reference.md’s “host-limit — the third disclosed-divergence class”
(the full mechanism). The bar: an entry must pin a structural property of the current deployment
shape, provable from how the artifact is built, never an unimplemented-but-fixable defect. log
remains the negative control on the other side of that line — a Go-source position map would satisfy
its .go:63 assert exactly, so it stays unbanked rather than disclosed around.
QUEUED, not closed — the gate mechanism’s truthful test.run. The hazard Finding 2 exposed is
real and unfixed: a capability gate is invisible to the running host, so any suite whose TestMain
asserts that the whole suite ran will mis-answer while one is active. The hardening is to emit the
gated set into the generated host and have TestFlagBridge publish a test.run naming the included
set — Go’s own vocabulary for “a subset ran”. It is load-bearing only when a gate meets a
completeness-asserting TestMain, and nothing is broken today: the only gated declarations live in
os and syscall, and os’s TestMain is a bare Exit(m.Run()). It becomes owed the moment
either a gated package with such a TestMain is banked, or os itself banks with its gate entries
intact. Cost when it lands: a converter change plus a hand-owned core/testing change, therefore a
full validated sweep.
One cosmetic defect left standing, deliberately. The summary line prints
27 disclosed-divergent (, host-limit) — the empty class comes from the two aggregation-disclosed
parents, which carry no manifest entry and so contribute "" to the class set. It is pre-existing
(any package with an aggregated disclosed parent prints it) and cosmetic. The generated proof page’s
own preamble is likewise still worded for the two CLR-measurement classes only. Both are one-line
renderer fixes and BOTH were left alone on purpose: touching the renderer restyles all 144 proof
pages, and they only rewrite as each package is next re-validated, so the change would dribble a
whole-corpus docs diff through unrelated lanes. It belongs in a pass that re-validates the roster.
✅ CLOSED (2026-08-15, lane claude/gosizeof-recursion) — TestSizeof’s stack exhaustion is a PRE-EXISTING classification defect in KindOf, and go/types BANKS at 557
The wall named in the entry above is gone, and it was never the embed change’s. go/types now runs
557 verdicts, 557 agreeing with go test, zero mismatches (1 skipped identically on both sides,
6 disclosed-unsupported declarations excluded) — status: validated. That is exactly the 513 the
type-parameter unlock produced plus the 44 the process-killer had been swallowing, and it is the
converted Go type-checker itself: TestCheck/TestSpec/TestFixedbugs over the language’s own
conformance corpora, TestStdlib type-checking all of GOROOT from source, and TestSizeof — the
test that priced this defect — asking reflect for the amd64 size of all 25 type and object nodes.
Attribution, settled FIRST and by measurement — pre-existing, not embed-implicated
The previous entry asked for this explicitly and declined to guess, correctly: the argument that the projection reports the same field type before and after was an argument, not a measurement, and nobody had run the walk on the pre-change golib because the type-parameter wall stopped the suite ~300 verdicts earlier.
A standalone probe settles it. It mirrors GoSizeOf/tryStructLayout exactly — same KindOf
classification, same GoFields projection — but carries a path stack, so instead of exhausting the
stack it NAMES the cycle. Built twice against the same converted go/types corpus, once against
golib at HEAD and once against golib at 48274ae2b (the merge immediately BEFORE the embed change),
both runs print the same 30 lines:
Named CYCLE Named -> Mutex -> SemaphoreSlim -> TaskNode -> TaskNode (REVISIT)
...
SUMMARY cases=25 matched=24 mismatched=0 cycle=1 unknown=0 missing=0
structs=89 cyclic=3 (Named, Context, lazyObject — one distinct cycle tail)
Byte-identical on both golibs. The other 24 of TestSizeof’s 25 cases already produced Go’s
exact size on both. So: a golib defect in its own right, older than the embed model, and the embed
lane’s decision not to attribute it to itself was right.
Root cause — KindOf called a managed REFERENCE a struct, and Struct is the kind that descends
KindOf’s last line answered Struct for any reference type it did not otherwise recognize. Struct
is the one kind whose walks look INSIDE the type, so GoSizeOf/GoAlignOf — and
StructFieldsComparable, which synthesizeDescriptor calls on the very next line — enumerated the
CLR’s own private fields and descended into the BCL object graph. A Go type graph cannot recurse
that way: Go’s layout rule stops at every pointer, slice, map, chan, interface and func, and a Go
struct that contained itself by value would be a type Go itself rejects. The BCL graph has no such
rule, and SemaphoreSlim’s async wait queue is a linked list — TaskNode.Next is a TaskNode —
so the descent alternated tryStructLayout/GoSizeOf frames until the stack was gone.
The corpus entry point is sync.Mutex, hand-owned since 2026-07-11 on a lazily-created
SemaphoreSlim gate because Go’s runtime sleeping semaphore has no managed form. Every struct that
holds one inherits the cycle: in go/types that is Named (via mu sync.Mutex), Context, and
lazyObject (via sync.Once). Nothing about this needed go/types — fmt.Println of any
sync.Mutex-bearing struct was on the same cliff.
Why it was a process-killer and not a failure. A StackOverflowException cannot be caught in
.NET. It took TestSizeof and every test alphabetically after it, and the run reported 44 ABSENT
verdicts rather than one failing test — the worst failure shape the campaign has, because absent
verdicts are unmeasurable rather than wrong.
The fix — one classification line, then one memoized walk behind it
1. The classification (the root cause). go2cs emits every Go struct as a C# VALUE type — the
corpus carries exactly seven [GoType] partial class declarations and all seven are named-POINTER
types (type P *T), already classified Pointer structurally. So a reference type reaching that
final line is never a Go struct: it is an opaque managed handle, the backing object a hand-owned
shim holds in place of Go’s own representation, and in the Go model a handle is one pointer word.
KindOf now says Pointer, the descent stops at the handle, and the answer is Go’s own — a Go
sync.Mutex is 8 bytes and so, now, is the converted one. Named computes to 112, which is
what TestSizeof wants.
2. The durable shape on top. Offsets, size and alignment now come out of ONE memoized pass
(structLayoutOf) instead of two walks that could disagree, and alignment is accumulated over every
field even after a size becomes unknowable, because the two questions are independent. A depth cap
(128) sits underneath as a safety net that answers “size unknown” — the r39d rule, a descriptor
field that cannot be read truthfully stays unpopulated — rather than overflowing. It is unreachable
by construction: only Struct and Array recurse, Struct is now answered for value types alone, and
C# forbids a value type from containing itself transitively (CS0523). Tripping it would mean the
CLASSIFICATION is wrong again, and the point is that the next such defect costs a wrong number
instead of a dead process and 44 unmeasurable verdicts.
A cycle guard proper was deliberately NOT added. The brief’s requirement — a guard must produce
CORRECT sizes for legal self-referential graphs, not merely avoid crashing — is met by the
classification rather than by detection: a struct holding a pointer to itself terminates at the
pointer and answers 24, not “unknown”. Guarded by
GoStructLayoutTests.SelfReferentialThroughPointer_IsFiniteAndCorrect.
Guards
Three new tests in src/tests/GolibTests/GoStructLayoutTests.cs, the golib home of this walk
(114/114, was 111/111): a managed reference is one word and not a struct to descend into
(SemaphoreSlim included, so the real BCL graph is in the assertion); a self-referential managed
class terminates; and Go’s own legal self-reference through a pointer is finite AND correct. The
first two are guards against a stack overflow, which no assertion can catch — reaching the assert at
all is the guard, and the value proves the walk stopped at the handle rather than merely stopping.
Doctrine: ConversionStrategies-Reference.md, A managed reference is a Go POINTER, not a Go
struct — the reflection bridge’s descent rule.
💡 CAPTURED (2026-08-15, user, from the live NuGet page) — README badge layout + a partial-progress badge idea
1. Badge-line wrap on NuGet — ✅ LANDED (lane claude/badge-break, merged 2026-08-15). The four badges emit as one
markdown line (readmeValidationBadge.go), and NuGet’s narrow README pane wraps them raggedly.
User ruling: hard line break between the docs badges and the source badges — line one carries
Tests + Docs (the Tests badge is the variable-width one and gets the room), line two carries
Source·Go + Source·.NET. Sequencing: land AFTER the in-flight second leveling regen merges
(the emitter change re-levels the whole README family at the next regen; published NuGet pages
pick it up at the next version push). Update readmeValidationBadge_test.go’s expectations in
the same change.
2. Partial-progress badge for not-yet-validated packages — DEFERRED, captured verbatim. User:
“does it make sense to carry partial test results, as a badge link to the ‘not yet validated’ so
users can get a sense of how close things sit?” Honest constraints, recorded so the future design
starts from them: the roster’s integrity principle is validated-only rows (a package that almost
passes never appears — that is what keeps the denominator honest); partial counts have no
machine-maintained data source today (the sweep records validated packages only, and this board’s
census entries are prose); and many unvalidated packages do not yet COMPILE their -tests
closure, so “how close” is not a number for them at all. A future shape would need a sweep mode
that records per-package partial verdicts and a badge/proof surface that cannot be mistaken for
validation. Revisit after the 75% terminal marker, possibly with the queued proof-renderer pass.
⛔ MEASURED FOR THE FIRST TIME, DOES NOT BANK — database/sql builds and runs at 133 of 139; the two compile roots are CLOSED and three divergence roots are named (2026-08-15, lane claude/database-sql)
The board carried database/sql as 0 of 25, CS0121 + CS0117 and the TB.* census listed it as
“36 sites, the widest in the corpus — pending its own measurement”. Both compile roots are gone and
the suite now runs end to end: 139 rows, 133 agree, 6 disagree, 0 skipped, 0 disclosed, 8
excluded (the standard Phase-4D Benchmark/Example deferrals). It does not bank — four of the
six are genuine divergences — but it is no longer build-blocked, and every remaining root is named.
The recorded CS0121 was already closed, exactly as the tar lane predicted. stubDriverStmt(NilType)
was the typed-nil constructor argument (08afdd267); it emits new stubDriverStmt(want) today and
never appeared in this lane’s build. The prediction was worth acting on — but the roots BEHIND it
were two, not zero.
Compile census — two roots, both general converter defects, both fixed here
| Diagnostic | Sites | Root |
|---|---|---|
CS0117 'sql_package' does not contain a definition for 'Δtable' |
6 | A box-field accessor (Type.Ꮡfield) qualified its owner with the PRODUCTION package class even when an internal _test.go declares the type. fakedb_test.go’s type table collides with func (db *fakeDB) table(string), so the type is Δ-renamed — and Δ-renamed always qualifies — putting all six t.mu.Lock()/Unlock() sites in a class that has no such member. packageScopeClassName already drew the production/bridge line for package-level VALUE references; the accessor now uses it for the TYPE. Provably inert outside -tests: with no class override the helper returns the production class |
CS0029 cannot convert ж<fakeStmt> to fakeStmt |
1 | func (s *fakeStmt) QueryContext walks s = s.next. Go’s pointer receiver is an ordinary local, so repointing it is legal; the converter deref-aliases the receiver to a value var, which cannot be repointed. visitAssignStmt’s repoint-and-re-alias arm (Ꮡs = s.next; s = ref Ꮡs.DerefOrNull();) has existed as long as the pointer-PARAMETER twin, but it is reachable only through the box — so it needed a direct-ж trigger the pre-pass did not have. bodyReassignsReceiver adds it, matched by object identity so a shadowing inner := does not promote |
The receiver-repoint trigger is corpus-invisible by construction, and that is why it surfaced in a
test file. All ten production sites in the converted scope are also carried by a neighbouring
predicate — container/ring’s Move and go/types’ LookupParent return their receiver;
math/big’s fmtX, net/http’s addBytes and time’s two (*Location) lookups already emit
this ж<T> Ꮡx — verified against the committed corpus before the change and re-verified by the
whole-stdlib A/B after it. (internal/concurrent and testing also match the shape and are
hand-owned, so they never convert.)
Verdict census — 6 disagreeing rows, 4 distinct roots
| Rows | Root | Class |
|---|---|---|
TestGrabConnAllocs, TestRawBytesAllocs |
testing.AllocsPerRun want-zero asserts (grabConn 4 objects/run; RawBytes 15). The existing measured-allocation gap |
alloc-profile — disclosable under the existing class if the other three roots ever close |
TestConversions, TestUserDefinedBytes |
abi.Type.Elem() nil — see below |
real defect, reflect/internal/abi |
TestNullByteSlice |
an untyped nil in a variadic slot vanished — fixed here, see below |
was a converter defect |
TestConnRaw |
conn.Raw(func(…) { panic(…) }) must leave the connection CLOSED: Raw’s deferred cleanup sets err = driver.ErrBadConn while a panic is in flight and calls release(err). The converted run panics correctly (the test’s own recover() assertion passes) but conn.dc is still non-nil afterward, and closeDB then reports 1 connections still open. Not rooted further by this lane — a defer-during-panic + named-result + capture question inside the converted sql.cs |
real defect, unrooted |
TestNullByteSlice was a CONVERTER root and is fixed. exec(t, db, "INSERT|t|id=10,name=?", nil)
emitted exec(…, insertTId10Nameˢ, default!) against params ꓸꓸꓸany argsʗp. A typeless default!
converts to the params ARRAY as readily as to its element, and C# prefers a call’s normal form over
its expanded one — so the argument bound as a null any[], the callee saw len(args) == 0, and the
fake driver answered sql: expected 1 arguments, got 0. A silent behavioral divergence, not a
compile error, which is what makes it worth naming: the emission is valid C# that means something
else, and nothing but a differential run would have caught it. Every trailing argument of an
expanded variadic call that is the predeclared nil now states the element type — (any)(default!).
A spread call is excluded (it passes the slice whole, so describe(none...) still yields 0).
TestConversions/TestUserDefinedBytes are ONE root, and it belongs to reflect, not to
database/sql. Both die in the same stack: convertAssignRows → rtype.ConvertibleTo →
convertOp → haveIdenticalUnderlyingType → haveIdenticalType → nameFor → the hand-owned
reflect.Name, which reads Ꮡt.Value.t.sysType and nil-dereferences. The nil comes from
abi.Type.Elem(): it reaches its element through the prefix-downcast idiom
(Ꮡt.Reinterpret<Type, SliceType>() and the Array/Chan/Map/Pointer siblings) — the exact route
internal/abi’s hand-owned type_impl.cs already documents as unusable under the managed bridge
(“there is nothing behind a ж<abi.Type> to downcast to”), and which it replaced with SYNTHESIS
from the descriptor’s carried System.Type for StructType() and ArrayType(). Elem() and
Key() were never routed through that synthesis, so they answer nil for a slice/pointer/chan/map
descriptor, and any reflect path recursing through them nil-derefs. TestUserDefinedBytes is the
minimal shape: convertAssign(&u, v) with u userDefinedBytes (a local named []byte) and
v []byte reaches sv.Type().ConvertibleTo(dv.Type()), both Kind Slice, and the slice arm
immediately recurses on Elem().
This is a shared root worth its own lane, not a database/sql fix. The remedy is the pattern
type_impl.cs already establishes — synthesize SliceType/PtrType/ChanType/MapType from
sysType and route Elem()/Key() through them, populating nothing that cannot be honored (the
r39d rule). It gates every reflect.ConvertibleTo/AssignableTo recursion in the corpus, not just
these two rows.
Timing — a $longTimeouts floor is owed IF this package ever banks
The converted run took 1,712.7 s (28.5 min) under -test-timeout 30m, i.e. it cleared the
deadline with about 90 seconds to spare on a quiet i7-5820K. At the sweep’s default it would
false-red. Nothing is owed today (the package has no roster row), but the entry that banks it owes
'database/sql' = '60m' in run-validated-sweep.ps1’s $longTimeouts in the same change.
What this lane banked
The two compile fixes and the variadic-nil fix, each with a guard and a reference-doc entry —
bodyReassignsReceiver (guard: PointerReceiverRepoint, a receiver-repointing list walk written to
avoid every other trigger), the bridge-qualified box accessor (guard:
TestTestVariantBoxAccessorNamesBridgeDeclaringClass, asserting both directions), and the variadic
untyped-nil cast (guard: the VariadicSlotInterfaces extension, arity in four positions against
three controls). Both compile-root guards were neuter-verified: reverted, each fails with the exact
diagnostic censused above. database/sql’s converted test sources are NOT committed — the
validated-package commit policy covers packages that validate, and this one does not.
✅ crypto/tls BUILDS — both recorded roots closed, a third behind them, and the RUN wall resolved into four named roots (2026-08-15, lane claude/crypto-tls)
Both recorded roots reproduced verbatim at master 6dd7547e4 before anything was believed —
same codes, same four call sites. That entry was accurate; the campaign’s stale-premise rule cost one
93-second pipeline run to confirm and was worth paying. What was NOT accurate was its attribution
of the first root, and what nobody had seen was a third root standing behind the first two, which
is the reason a build-blocked package is only ever measured one wall at a time.
Four errors → zero. The package compiles for the first time, and its host runs.
Root 1 — CS0012 ×3 is NOT a missing reference; it is the Phase-4D exclusion being too conservative
Recorded as “a fresh instance of the -tests reference-closure family”. It is not: adding the
reference cannot fix it and makes the diagnostic worse. crypto/tls selects the whitebox-reference
model and then FALLS BACK to recompile (recordsRequireProductionMutation), which compiles the
production .cs into the test assembly. example_test.go is the package’s ONLY black-box file, and
its Examples reach http.Transport{TLSClientConfig: …}, http.Server{TLSConfig: …} and
httptest.Server.TLS — fields whose type is tls_package.Config in the production assembly,
while the recompile has just made a second, distinct local copy. The field is therefore unnameable.
Referencing crypto.tls as well would let the compiler name the type and then reject the
assignment between the two copies: CS0012 becomes CS0029. The file simply must not be compiled.
Which is what the Phase-4D file-exclusion ruling exists to do — go/token’s example_test.go is the
recorded precedent, the same failure in the same model. crypto/tls’s file differs in one respect:
its Examples need an io.Reader to hand Config.Rand, so it declares type zeroSource struct{} and
one Read method, and condition (1) disqualified the whole file for it. Condition (1) now admits
pure TYPE declarations and METHODS — they have no run-time behavior, nothing executes at package
init, and any use by a retained file is a reference condition (2) already resolves. The type and
method objects are now recorded in declared, without which the widening would have silently
disarmed condition (2) for exactly the declarations it just admitted. var/const (initializer side
effects) and plain funcs (init) stay disqualifying. Doctrine + the two new unit guards:
ConversionStrategies-Reference.md, An Example/Benchmark-ONLY test file is dropped from the compile
set.
Phase-4D widening confirmed by coordinator ruling 2026-08-15; the 2026-07-24 conservatism’s safety
property is preserved by the declared-object fixpoint. The lane flagged this as a ruled boundary
being moved rather than assuming it. The original ruling’s “conservative by design” existed to
guarantee one property — nothing a test file declares can vanish from the compilation while anything
still references it — and the widening RETAINS it, because the admitted type and method objects are
recorded in declared, so condition (2)’s fixpoint still forces the file back in whenever a
cross-file reference exists. What it removes is a false positive the alternative cannot fix at all
(the recompile model’s duplicate type: adding the reference turns CS0012 into CS0029, never into a
fix). A conservatism whose protective content survives the refinement intact is not weakened by it,
only made precise. The three conditions the ruling attached are met and deliberately kept visible:
the fixpoint protection is stated in the code where the widening lives
(classifyTestFileForExclusion), the two unit guards
(TestSelectCompileExcludedTestFilesDropsExampleWithHelperType,
…KeepsHelperTypeUsedByRetainedTest), and the reference-doc section.
Root 2 — CS1503 is a delegate-VALUED comparator, and the first fix silently did nothing
isBetter := func(a, b uint16) int { …; return -1; …; return +1; …; return 0 } passed to
slices.IsSortedFunc. All-INT-literal arms emit bare, so C# infers Func<ushort, ushort, int>
against a Go int (nint) result. Every call of the variable accepts that (int converts to
nint) — only the delegate-valued use rejects it, delegate types being invariant. convFuncLit now
states the declared return type for that arm set.
Two things this cost, both worth carrying:
numericBasicLitstripped unary-but not unary+. The comparator returns-1AND+1, so the predicate saw a non-literal arm, suppressed itself, and the first fix emitted nothing at all — a green build of a converter that had not changed its output. Go writes an explicitly positive literal precisely where it pairs with a negative one, which is the comparator shape this arm exists for. Both signs are stripped now.- Two successive cuts OVER-applied, and only the emitted C# showed it. The natural rule — “any
numeric literal whose natural C# type differs from the declared result” — is wrong, because the
converter does not emit every literal bare: a declared FLOATING result carries its width into the
literal (
func() float32 { return 0.5 }→0.5F;func() float64 { return 3 }→3D), so those already infer correctly. Only a declared INTEGER width other thanint32leaves the literal bare. A predicate about C# inference must be measured against emitted output, never derived from the Go-side type. All five shapes are pinned side by side in the extendedFuncLitUntypedConstReturnbehavioral guard.
Root 3 — CS7013: the hoisted-literal slug budget never bound the first word
Behind the other two, and previously unseen. key_schedule_test.go carries a 2,176-character hex
test vector; the hoist slug is built from the literal’s own content and truncated at a word boundary
within a 24-character budget — but the truncation only applied once the slug was non-empty, so the
leading word was written whole. A literal that is ONE long word became an identifier of exactly its
own length: CS7013: Name '…' exceeds the maximum length allowed in metadata.
The committed corpus was already past the design’s intent without failing — 33 of 5,928 hoisted
names exceed 24 characters, the longest 256 — so this was luck, not a boundary case. Raising the
number would not close the class; making the budget total does, and len(literalSlug(v)) ≤ 24 is now
an invariant. Those 33 inline instead of hoisting; zero behavioral goldens move.
⚠ The A/B footprint splits 11 / 22, and the second half is LATENT — expect it, do not re-diagnose
it. A seeded whole-corpus reconvert (single run, version.props + docs/validation seeded, marker
gate 52 marked / 0 violations) emitted 1,960 files and changed 352, and the accounting closes
exactly:
- ~297
README.md— thebadge-breakline-break emitter, already merged to master with its corpus re-level still pending. Not this arc’s, and it will vanish at the next leveling regen. - 49
.csCRLF phantoms — the known in-string-LF class; CR-stripped comparison is identical. - 6
.csreal content diffs — every one this fix, every one an over-budget first word now inlined (crypto/elliptic/nistec.cs,go/parser/parser.cs,image/ycbcr.cs,os/exec/windows/lp_windows.cs,runtime/windows/os_windows.cs,syscall/windows/syscall_windows.cs). 0 new files, and ZERO func-literal changes corpus-wide — the root-2 fix has no stdlib footprint at all, which together with CNR’s single-file result is two independent confirmations that its gate is tight rather than merely plausible.
That is 10 of the 33 names. The other 23: one is
crypto/x509/internal/macos/darwin/security.cs, darwin-exclusive and correctly not emitted by a
windows-target run; the remaining 22 live in committed *_test.cs sources across ~14 banked
packages (regexp, bufio, bytes, context, compress/flate, crypto/{dsa,ecdh,rsa},
encoding/base32, go/scanner, internal/cpu, mime/multipart, os/exec, path/filepath,
strings), which a -stdlib run never re-emits. They will re-level at each package’s next -tests
run and show up in a future sweep or leveling regen as unexplained-looking test-source churn. It is
this fix, it is expected, and it is benign.
The host RUNS — 23 of 184 top-level tests before the deadline, and the roots are named
⚠ A live-diagnosis correction, recorded because the method failed, not just the guess. This lane
first reported the host as “blocked from the start — 8.3 s CPU across twenty minutes, flat across
eleven samples, I/O-blocked”. The CPU reading was accurate and the inference from it was wrong.
The host runs the suite alphabetically and got through 23 top-level tests in roughly the first
minute, most of them failing fast; it then blocked inside TestConnCloseBreakingWrite and spent the
remaining ~38 minutes of the deadline parked there. The first CPU sample was taken about nine minutes
in, by which time the interesting part was already over — so a flat sample series was read as “never
started” when it meant “already finished the part that moves”. Flat CPU dates a stall; it cannot
date its beginning. Sample from t=0 or read the verdict stream; do not infer a run’s shape from
its tail.
The measured outcome (converted host, -test-timeout 40m): 26 pass, 12 fail, 1
infrastructure-error, 1 package timeout over 23 top-level tests reached. Go’s own run of the
same suite on the same host executes 184 top-level tests (1,251 pass / 2,381 skip / 12 fail).
Per the jsonrpc rule this board already applies, a package that dies mid-run has one failure and an
unknown remainder — the 26 is a floor, not a census, and no differential is claimable from it.
Four distinct roots, in order of weight:
localPipe: dial tcp 127.0.0.1:…: failed to find ConnectEx: An invalid argument was supplied.×9 — the dominant one, and far more specific than “the net stack”.localPipe()dials a loopback listener, and the converted Windows socket layer cannot resolve theConnectExextension function pointer (WSAIoctl/SIO_GET_EXTENSION_FUNCTION_POINTER). Every handshake-driving test dies on it in ~2 ms. This is S2b netpoll +syscallterritory and is the priority signal this lane hands over.TestConnCloseBreakingWriteblocks indefinitely and consumes the entire remaining deadline — the reason nothing pastTestConn…was reached. A separate defect from #1 (it does not fail fast; it never returns) and the single highest-leverage fix for coverage, since the alphabet beyond it is unexplored.- A golib REFLECT gap, not a net one —
System.InvalidOperationException: Not a pointer box type: go.sync_package+RWStateinGoReflect.slotAccessorShape(golib/GoReflect.FieldAccess.cs:125), reachedreflect.DeepEqual→deepValueEqual→deepValueEqualBoxed→reflect.Elem, fromTestCloneNonFuncFields.sync.RWMutex’sRWStatebox is not recognised as a pointer box, soDeepEqualcannot descend a struct containing one. Independently actionable, unrelated to sockets, and it will bite any package thatDeepEquals a struct holding asyncprimitive. TestCertCache: “timed out waiting for expected ref count” — weak-reference/GC observability, theinternal/weakfamily.TestBogoSuitefails against the external BoGo shim and is not a conversion signal.
One result worth stating: TestCipherSuites PASSES. That is the exact test whose comparator
produced the CS1503 above — so root 2’s fix is validated by execution, not merely by compiling.
⚠ Go itself fails 4 top-level tests on this host — TestResumption, TestVerifyConnection,
TestResumptionKeepsOCSPAndSCT, TestCrossVersionResume (12 verdicts with subtests). Whoever takes
the differential must treat those as host-environmental until proven otherwise, exactly as the os
SHARE_INFO_2 retraction demands.
Not banked, and not bankable on this evidence. Builds-and-partly-runs is precisely the partial
result the roster’s validated-only integrity principle excludes — no roster row, no proof page, no
disclosures. The three converter fixes and their guards are the deliverable; crypto/tls returns to
the board as a package whose build question is answered, whose run question is now four named
roots rather than one vague one, and whose next move is ConnectEx plus the
TestConnCloseBreakingWrite hang.
✅ CLOSED (2026-08-15, lane claude/reflect-bridge-gaps) — both named reflect-bridge gaps, and one root that stood behind them
Two entries on this board named two independent defects. Both reproduced verbatim before anything was believed, both are fixed at the classification/routing principle rather than at the symptom, and a third root of the same family stood behind the first — the pattern this board keeps recording: a wall is only ever measured one layer at a time.
Gap 1 — abi.Type.Elem() / Key() (and Len()) reached their answer by prefix downcast
Recorded as “Elem()/Key() were never routed through the synthesis path, so they return nil and
reflect.Name nil-derefs”. Accurate, and the reproduction is sharper than “returns nil”: a program
whose first statement is reflect.TypeOf([]byte(nil)).ConvertibleTo(reflect.TypeOf(myBytes(nil)))
panics before printing a single line.
That is the whole point of this root. StructType()/ArrayType() also answer nil when a layout is
unknowable, and every Go caller of those tests the nil. Nothing tests Elem(). Go’s
haveIdenticalType recurses straight into nameFor(t), which reads the descriptor’s carried
System.Type — so a nil element is not a wrong answer that propagates, it is a nil dereference that
ends the process. ConvertibleTo/AssignableTo over any operand that is not a scalar was
unreachable corpus-wide.
Both are now hand-owned in internal/abi/type_impl.cs (Type.Elem / Type.Key in
manualConversionFuncs) and synthesized from the carried System.Type over
GoReflect.ElementType / KeyType — the same golib resolution reflect’s own hand-owned
rtype.Elem/rtype.Key already use one layer up, so the descriptor layer and the reflect layer
cannot disagree about what an element type is. The array-dims cargo threads by the rule rtype.Elem
already applies: an array’s element takes the tail of [outer]…[inner], a pointer’s dims are the
pointee’s and pass through unshifted.
The third root, found by the guard rather than by the census. With Elem()/Key() fixed the
new behavioral test still disagreed with go run on the ARRAY rows, and Type.Len() is why: it is
the same downcast, and its failure is the nastiest of the three, because it does not answer nil —
it reads a uintptr out of the memory following the descriptor’s value slot. Two [3]byte
descriptors therefore read two DIFFERENT numbers and haveIdenticalUnderlyingType reported
[3]byte and [3]byte as different types. Type.Len is hand-owned too, over the same carried
dims; a length no source knew still answers Go’s 0, so two dimension-less array descriptors compare
equal rather than randomly unequal.
Gap 2 — slotAccessorShape threw because pointer KIND was being read as pointer BOX
Recorded as a golib descent-rule sibling of the KindOf fix, and that attribution is exactly right.
KindOf classifies every managed REFERENCE it does not otherwise recognize as Pointer — the fix
that stopped the layout walks descending into the BCL object graph. What that settled was the
LAYOUT question. It left a second, different question for the VALUE walks: is there a slot behind
this handle at all?
For an opaque handle there is not — a hand-owned shim’s backing object (sync.Mutex’s
SemaphoreSlim gate, sync.RWMutex’s RWState, sync.WaitGroup’s WaitGroupState) stands in for
a representation Go has and the CLR does not, and nothing behind it has a Go form. So “one word
wide, do not descend into it” is also “no slot, do not read through it” — one rule at two layers,
of which only the first half had been stated.
reflect.Value.Elem asked the wrong one: it resolved a pointee with GoReflect.ElementType and, on
null, fell through to a “detached read” through ReadPointerSlot, which classifies the box shape
itself and threw Not a pointer box type: go.sync_package+RWState. The classification now lives in
ONE place — GoReflect.TryPointerBoxElement, which slotAccessorShape is refactored onto — so “can
I read through this?” and “what will I read?” can never be answered by two different probes.
reflect.Value.Elem and its internal/reflectlite twin ask first and answer the invalid Value
for a handle, which is what they already answer for a nil pointer.
The resulting blindness is Go’s own answer, not a concession. Go’s sync.RWMutex is state
words, and a used-then-released lock is back at its zero state, so two of them are deeply equal —
which is what two handles now compare as, whether or not the shim has lazily created one of them
(crypto/tls’s TestCloneNonFuncFields is the measured consumer). Real Go state beside the handle
is still seen: a sync.Once that has run differs from a fresh one, because done is an ordinary
field and not part of the handle. The DeepEqual behavioral test asserts both directions.
Three adjacent roots this lane measured and deliberately did NOT chase
The new guard was written to cover every element-bearing kind and was then TRIMMED to what the corpus can truthfully produce, because a guard that asserts an answer the corpus cannot give is a standing false red, and one that asserts the current answer pins a defect as a contract. Each row removed is named here instead:
| Surface | Root | Why not here |
|---|---|---|
AssignableTo (all kinds) |
reflect’s rtype.AssignableTo is hand-owned as identity-on-the-managed-type plus interface-implements |
A recorded deferral — and as of this lane it HAS a measured consumer, database/sql’s TestUserDefinedBytes (see below). Still a of Go’s unnamed↔named underlying rule, and retiring it is not the one-liner it looks like: a synthesized descriptor never sets TFlagNamed, so HasName() is false for every type and directlyAssignable’s first gate would call two DISTINCT named types with one underlying type assignable — which Go rejects. Needs HasName before it needs the hand-own removed |
| STRUCT identity | haveIdenticalUnderlyingType’s struct arm downcasts ж<abi.Type> to structType DIRECTLY rather than through the synthesized StructType() |
Reads zero fields, so any two structs of equal field count compare identical — a silent FALSE POSITIVE, measured: struct{B []byte; M map[string]int; …} and the same struct with M map[string]int64 are reported convertible. The fix is a reflect-level hand-own, not an abi one |
| CHAN identity | its arm compares ChanDir() first, the same downcast |
The only one of the family with no synthesis waiting for it: <-chan int and chan int are both channel<nint>, so a direction is not recoverable from the managed type for an unnamed directional channel. This one needs a ruling, not a fix |
MapType(), FuncType() and InterfaceType() remain on the same list for the reason they always
were — each awaits a measured consumer, and a synthesized ΔMapType would have to populate
runtime-map fields (Hasher, KeySize, the indirect-key/elem flags) that have no managed answer.
Guards
ReflectConvertAssignable(new behavioral test) —ConvertibleToacross slice, map, pointer and array, named and unnamed, in both directions, with differing-element and differing-key negatives so an accessor that answered a constant would fail too; plusLen()over equal and differing lengths, and an element/key kind readback through the publicTypesurface. 14 lines, compared togo run. Neuter-verified in the right order: measured as a nil-deref panic before the fix.DeepEqual(extended) — aguardedstruct holdingsync.Mutex,sync.RWMutexandsync.Once, compared through pointers, slices and maps after the locks have been used and released. Also measured failing first, with the exactslotAccessorShapestack.GolibTests.PointerNilPredicateTests(117/117, was 114) —OpaqueManagedHandleIsPointerKindButNotAPointerBoxasserts BOTH halves of the rule in one test (either alone would let the other drift back),PointerBoxShapesResolveTheirPointeepins the positive side so the fix cannot turn real dereferences into nil, andElementAndKeyResolveForEveryKindTheDescriptorMustServepins the golib resolution theabisynthesis now stands on.
Doctrine: ConversionStrategies-Reference.md — abi.Type’s SPECIALIZATIONS are synthesized, not
downcast (extended with Elem/Key/Len) and A managed REFERENCE is a Go pointer, not a Go
struct (extended with a new subsection, The VALUE side of the same rule: pointer KIND is not
pointer BOX).
Corpus footprint, measured
A seeded whole-stdlib reconvert A/Bs to exactly one regenerated file, internal/abi/type.cs
(three Go bodies replaced by placeholders). Two families of difference the same A/B reports are
pre-existing regen debt at master, not this lane’s and deliberately not carried: 300 package
README.md files (the merged badge-line break awaiting its re-level) and seven .cs files
(crypto/elliptic/nistec.cs, go/parser/parser.cs, image/ycbcr.cs, os/exec/windows/lp_windows.cs,
runtime/windows/os_windows.cs, syscall/windows/syscall_windows.cs, testing/slogtest/slogtest.cs
— string-literal hoisting and the variadic untyped-nil cast, both banked without a corpus regen).
database/sql — measured again after the fixes: 135 of 139, and the last non-alloc row is now ROOTED
Re-run end to end (-test-action all -test-timeout 60m; 3,423 s under concurrent lane load, vs the
1,712 s the previous lane measured solo — the machine, not the package). The census moves from
133 agreeing to 135, and the change is exactly what this lane predicted plus one it did not:
| Row | Before | Now |
|---|---|---|
TestConversions |
abi.Type.Elem() nil → reflect.Name nil-deref |
passes |
TestUserDefinedBytes |
the same nil-deref | fails DIFFERENTLY, and the new failure is rooted below |
TestGrabConnAllocs, TestRawBytesAllocs |
AllocsPerRun want-zero |
unchanged — the standing alloc-profile class |
TestConnRaw |
conn.dc non-nil after a callback panic |
unchanged, still unrooted |
It does NOT bank, and the reason is worth more than the row would have been.
TestUserDefinedBytes is now a MEASURED CONSUMER of the AssignableTo deferral. The test does
convertAssign(&u, v) with u userDefinedBytes (a named []byte) and v []byte, then asserts
&u[0] != &v[0] — “got potentially dirty driver memory”. convertAssignRows has two arms in
sequence:
if (sv.IsValid() && sv.Type().AssignableTo(dv.Type())) {
case slice<byte> b: dv.Set(reflect.ValueOf(bytes.Clone(b))); // arm 1 — CLONES
}
if (dv.Kind() == sv.Kind() && sv.Type().ConvertibleTo(dv.Type())) {
dv.Set(sv.Convert(dv.Type())); // arm 2 — SHARES the array
}
Go takes arm 1: []byte is an unnamed type and userDefinedBytes is named with the identical
underlying type, so Go’s assignability rule admits it, and the []byte case clones. The converted
run takes arm 2, because rtype.AssignableTo is hand-owned as identity-on-the-managed-type and
the two managed types are distinct — so Convert hands back a view over the driver’s own array and
the assertion fires.
Two things follow. First, arm 2 is reachable at all only because this lane’s Elem() fix made
ConvertibleTo answer: before it, the same row died in nameFor(nil). The fix moved the test from
a panic to the wrong arm, which is progress and is also how the root became visible. Second, the
AssignableTo entry in the table above should now be read as has a named consumer, not “awaits
one” — and the sequence it needs is fixed: HasName() must become truthful on a synthesized
descriptor before the hand-own can retire, and the struct and chan arms of
haveIdenticalUnderlyingType must be fixed with it, or retiring the hand-own trades one wrong
answer for a wider one (every two structs of equal field count would become assignable). That is a
second arc of this same family, not a tail of this one.
The $longTimeouts floor is still owed if it ever banks — 'database/sql' = '60m', per the
previous lane’s note. Nothing is added today: the package has no roster row, and a floor for a
package the sweep never visits would be dead configuration.
Converted test sources are not committed, per the validated-package commit policy — that policy covers packages that validate, and this one does not.
⛔ RE-MEASURED, DOES NOT BANK — crypto/tls reaches 127 of 180 Go-passing top-level tests; three of the four named roots are CLOSED and the whole remaining divergence is ONE converter defect class in two shapes (2026-08-16, lane claude/tls-remeasure)
The 2026-08-15 entry left four roots and said the next move was ConnectEx plus the
TestConnCloseBreakingWrite hang. Both landed (netpoll S2b), the reflect-bridge lane closed a third,
and this lane re-measured the package on the post-poller tree. The three fixes all hold, and none of
the remaining divergence is a poller defect.
The headline, and the number that went DOWN
| Measure | 2026-08-15 | now |
|---|---|---|
| top-level tests the ONE-PROCESS run reaches before parking | 23 | 7 |
| top-level tests that PASS when each is run on its own | not measured | 127 of 180 |
| C# verdicts agreeing with Go | 26 (floor, uncensused) | 127 top-level / 274 incl. subtests |
| divergences rooted or named | 4 roots, remainder unknown | 53 of 53, zero unrooted |
⚠ The single-process floor got WORSE, and that is progress, not regression — do not read the 23 → 7
as a loss. Under the old ConnectEx failure every handshake-driving test died in ~2 ms, so the
alphabet advanced fast and then parked. Those same tests now do real work, and one of them parks
forever, so a single package deadline buys fewer completed tests than a fast-failing suite did.
Once a suite contains a hang, “how far the one-process run gets” stops measuring capability and
starts measuring where the first hang sits. The per-test census below is the measurement that
answers the question the floor was standing in for; the floor number is kept only because it is what
the previous entry recorded.
Method. Each of the 190 top-level Test* functions in the staged _test.go set was run in its
OWN process (-run '^Name$' -timeout 20s), so one hang costs 20 s instead of the whole package
deadline. 6 of the 190 are //go:build boringcrypto and are absent from Go’s run — 190 − 6 = 184,
which is exactly the count Go executes, so the enumeration is confirmed rather than assumed. Go’s own
run on this host reproduces the previous entry’s numbers exactly: 184 top-level (180 pass, 4 fail);
1,251 pass / 2,381 skip / 12 fail at all levels, 46.5 s.
C# per-test: 127 PASS, 34 FAIL, 13 HANG, 9 infrastructure-error, 1 process CRASH = 184, and the cross-tab against Go closes with no remainder: 127 agreeing passes + 53 real divergences (Go passes, C# does not) + 4 both-fail + 6 build-tag-excluded = 190.
The four roots, re-measured
| # (2026-08-15) | verdict now | evidence |
|---|---|---|
1. failed to find ConnectEx ×9 |
✅ CLOSED | zero occurrences of the string across all 190 test outputs; ESTABLISHED loopback pairs observed live (127.0.0.1:42281 ↔ :42159); full TLS 1.2 handshakes complete end-to-end (TestClientAuth/TLSv12, TestConnectionState/TLSv12, …) |
2. TestConnCloseBreakingWrite blocks forever |
✅ CLOSED | PASSES, 3.5 s. S2b’s second acceptance anchor holds at suite level, not just at golib level |
3. Not a pointer box type: sync_package+RWState |
✅ CLOSED | zero occurrences corpus-wide in the outputs; TestCloneNonFuncFields PASSES |
4. TestCertCache ref-count timing |
⛔ still open | timed out waiting for expected ref count — unchanged, 1 divergence |
The 53 divergences, every one rooted
| count | root |
|---|---|
| 30 | A — ticketKey.aesKey is length 0 → tls: failed to create cipher while encrypting ticket: crypto/aes: invalid key size 0. Every TLS 1.3 session-ticket path |
| 10 | B — net/netip.As16 slices a zero-length array → System.ArgumentException, full stack captured |
| 7 | B-shaped — silent hang, zero verdicts: server parked in pollBlock reading the ClientHello while the client goroutine is gone. Same stack shape as B; the killing exception is NOT captured (see “why it hangs”) |
| 2 | F — hang after partial progress (TestConnectionState, TestHandshakeKyber: TLS 1.2 subtests pass, the 1.3 subtest parks) |
| 1 | D — TestCertCache weak-ref timing (root 4 above) |
| 1 | E — TestBogoSuite against the external BoGo shim; not a conversion signal |
| 1 | G — TestQUICHandshakeError: panic: runtime error: invalid memory address or nil pointer dereference at golib/ж.cs:957 ж<T>.op_OnesComplement |
| 1 | H — TestVerifyHostname: process access violation 0xC0000005 in syscall.GetAddrInfoW |
A and B are the SAME converter defect, and together they are 47 of the 53.
The root: the zero value of an array-bearing type is emitted as default!
C# default produces the all-zero value and does not run field initializers — those run only
through a constructor. go2cs’s array<T> is a struct carrying its own m_length, so a default
one has length 0, not N.
Shape 1 — a [N]T named return. net/netip’s func (ip Addr) As16() (a16 [16]byte) becomes
(src/core/net/netip/netip.cs:718):
public static array<byte> /*a16*/ As16(this ΔAddr ip) {
array<byte> a16 = default!; // ← length 0, not [16]byte
byteorder.BePutUint64(a16[..8], ip.addr.hi); // ← throws here
Shape 2 — a struct whose fields are fixed arrays. crypto/tls’s ticketKey declares
internal array<byte> aesKey = new(16);, and ticketKeyFromBytes opens with
ticketKey key = default!; (src/core/crypto/tls/common.cs:747) — which skips exactly that
initializer, so copy(key.aesKey[..], …) copies nothing and aes.NewCipher is handed 0 bytes.
The converter already knows the right emission — three lines from the broken one,
src/core/net/ip.cs:529 builds the same zero value correctly as new byte[]{}.array(16) in
composite-literal position. Only the zero-value declaration path emits default!.
Census (Windows target, committed corpus — this is a SHIPPED defect, not something this run
emitted; git status on src/core/net/netip is clean).
- 10 sites of the direct
array<T> x = default!;form, in 5 packages:net/netip×2 (As16andAs4),vendor/golang.org/x/crypto/sha3×4,internal/pkgbits×1,syscall×3 (linux/darwin only). - 24 sites declaring
= default!on one of the 269 structs that carryarray<…> … = new(…)field initializers. Not all 24 are live — a site that assigns the whole struct afterwards is harmless — butticketKeyis proven live by execution.
Why it HANGS instead of failing, which is the expensive half. The throw is a plain
ArgumentException from slice<T>’s constructor (golib/slice.cs:227), not the
RuntimeErrorPanic.SliceBoundsOutOfRange that the reslice path raises. A non-panic exception
satisfies Goroutine.CanContain, so the test host contains it and records it on the
TestExecution. If the test then completes, the record flushes and you get the
INFRASTRUCTURE-ERROR line with the full stack (the 10 B rows). If the dying goroutine was the one
another goroutine was waiting on, the test never completes, the record never flushes, and the whole
package deadline burns with no output at all (the 7 B-shaped rows). A Go-visible panic: here
would have failed 17 tests loudly in milliseconds instead.
The one-line reproducer for whoever takes this: TestHostnameInSNI hangs — a pure table test
with no sockets and no TLS, whose table contains IP literals. It reaches
hostnameInSNI → net.ParseIP → parseIP → As16 and dies. As16 is only reached when the string
genuinely parses as an IP, which is why hostname-SNI TLS 1.2 handshakes pass while everything
touching 127.0.0.1 dies. The second reach path is server-side and equally common:
x509.VerifyHostname → net.ParseIP → As16 (captured in TestFallbackSCSV’s stack).
Not fixed here, deliberately. It is a converter emission change with a corpus-wide blast radius
(34 candidate sites across 5+ packages), so it owes the converter gate set plus a seeded reconvert —
its own lane, with a behavioral guard pinning both shapes (a [N]T named return and a
zero-valued struct with an array field) and the slice<T> constructor’s panic-vs-exception question
decided alongside it.
Two singletons worth their own lines
GetAddrInfoWaccess-violates.TestVerifyHostnamekills the process outright:Fatal error. 0xC0000005throughnet.DialContext → resolveAddrList → LookupPort → lookupPort → syscall.GetAddrInfoW → Syscall6. This is the open non-blittable-syscall class CLAUDE.md records after theTimezoneinformationfix — “9 more syscall wrappers pass a non-blittable struct by address … Nothing exercises them today;netandcrypto/x509will.”netjust did, andAddrinfoWis the first member with a measured consumer.ж<T>.op_OnesComplementnil-derefs (golib/ж.cs:957) underTestQUICHandshakeError.
Go’s own 4 failures are EXPIRED TEST FIXTURES, not a host defect
The previous entry flagged TestResumption, TestVerifyConnection, TestResumptionKeepsOCSPAndSCT
and TestCrossVersionResume as “host-environmental until proven otherwise”. They are, and the reason
is now named: the certificates those tests carry expired on 2025-01-01, and this host’s clock is
2026-08-16 —
x509: certificate has expired or is not yet valid:
current time 2026-08-16T02:57:52-05:00 is after 2025-01-01T00:00:00Z
Both languages fail them identically, so they are correctly excluded from the differential (C# turns
them into hangs rather than failures, which is root B’s containment behavior again). This will
worsen with time on any machine, and it caps crypto/tls at 180 of 184 no matter what go2cs does —
whoever eventually banks this package needs that fact before reading a differential, and it is a
Go-toolchain-version property, not a go2cs one.
Not banked, and not bankable
Builds-and-partly-runs, exactly as the previous entry. No roster row, no proof page, no disclosures —
none of the 53 divergences is a disclosed-divergence candidate under alloc-profile,
codegen-liveness or host-limit: every one is either a real defect go2cs owns or an environmental
fixture expiry. Converted test sources are not committed, per the validated-package commit policy.
Next move, in dependency order: the default! zero-value emission (closes 47 of 53 at one
stroke), then GetAddrInfoW, then the two singletons, then TestCertCache. The 7 B-shaped silent
hangs should be re-measured after the emission fix rather than investigated now — the cheapest way
to learn whether they were the same root is to remove the root.
Method notes worth keeping
- A
-testspipeline run cannot be interrupted by killinggo2cs.exealone. Doing so orphans thedotnet runchild and itscrypto.tls.tests.exegrandchild, which keep running, keep the suite’s sockets open, and holdruntime.dlllocked — the next run then dies with MSB3027/MSB3021 and its comparison reportsGo="pass" C#=""for ~180 tests, which reads exactly like a total conversion failure. It is a file lock. Kill the TREE (by verified parentage), not the parent. go2csdoes not relay the test host’s stdout live — it captures and prints at exit, so a log tail shows nothing at all for the entire run. CPU sampling and the process tree are the only live instruments;dotnet-stack report -p <pid>on the parked host is what produced every root above.- Flat CPU dates a stall but not its beginning (the previous entry’s lesson) — sampling from t=0
put this stall at ~30 s into the host run. Both readings were 8.3 s of CPU, which is a coincidence
worth not over-reading: it is where a
crypto/tlshost stops, by two different mechanisms.
⛔ STILL DOES NOT BANK — the default! zero-value emission LANDED and crypto/tls goes 127 → 163 of 184; the remaining wall is ONE pre-existing root worth 10 tests (2026-08-16, lane claude/zero-value-arrays)
The previous entry’s “next move, in dependency order” opened with the default! zero-value emission
and priced it at 47 of 53. It landed (claude/zero-value-arrays), and the re-census on the same
per-test method — one process per top-level Test*, -run '^Name$' -timeout 20s, the same 184
non-boringcrypto functions — measures 163 PASS, 0 FAIL, 4 HANG, 14 CRASH, 3 infrastructure-error
in 691 s.
| Measure | 2026-08-16 (pre-fix) | now |
|---|---|---|
| top-level tests that PASS run on their own | 127 of 184 | 163 of 184 |
| real divergences (Go passes, C# does not) | 53 | 17 |
| distinct roots behind them | 8 | 5 |
Four of the 21 non-passing are Go’s OWN expired-certificate failures (TestResumption,
TestResumptionKeepsOCSPAndSCT, TestVerifyConnection, TestCrossVersionResume) — unchanged, both
languages fail them, still the 180-of-184 ceiling this host cannot beat. 184 − 163 − 4 = 17.
The roots that closed
Roots A (30 tests, ticketKey.aesKey length 0 → aes: invalid key size 0) and B + B-shaped
(17 tests, netip.As16 slicing a zero-length array) were the same defect and are both CLOSED.
Every TLS 1.3 session-ticket path now passes — TestQUICSessionResumption, TestQUICEarlyData,
TestQUICPostHandshakeKeyUpdate and the rest of the QUIC family are green — and the 7 silent
B-shaped hangs are gone, which answers the previous entry’s open question: they were the same root,
and removing it was indeed the cheapest way to learn that. Root F (hang after partial progress)
is also gone as a category.
The root that was HIDING behind them — 10 of the remaining 17
TestHostnameInSNI, the previous entry’s one-line reproducer, no longer hangs: it drives a real
handshake and dies further down, in sha3.copyOut, with a fatal
AccessViolationException reading d.storage[..rate] — i.e. ref state d does not address a valid
managed state. Reach: mlkem768.NewKeyFromSeed → kemKeyGen → sha3.Sum512, which every TLS 1.3
ClientHello performs, so it now claims 10 tests: TestDialTimeout, TestHandshakeKyber,
TestHostnameInSNI, TestKyberDecapsulate, TestKyberEncapsulate, TestSCTHandshake,
TestServerSelectingUnconfiguredApplicationProtocol,
TestServerSelectingUnconfiguredCipherSuite, TestTLS13OnlyClientHelloCipherSuite, TestVersion.
It is PRE-EXISTING, and that was proven rather than assumed. The suspicion is natural — this
lane changed sha3’s four Sum* functions, whose digest [N]byte named results were four of its
14 sites — so the four sites were reverted to default!, the host rebuilt, and the test re-run: the
identical stack reproduces. The in argument plays no part in the faulting path either
(Sum fills a freshly make-d buffer and only touches in in the closing append). It was simply
unreachable behind the netip wall.
The remaining 17, by root
| count | root | status |
|---|---|---|
| 10 | sha3.copyOut AccessViolation via mlkem768 key-gen — ref state not addressing managed storage |
NEW HEAD OF THE QUEUE; pre-existing, newly reached |
| 3 | hangs with no output: TestCipherSuitePreference, TestConnectionState, TestDialer |
unrooted — first census with no output at all to root them |
| 1 | TestVerifyHostname — process AV 0xC0000005 in syscall.GetAddrInfoW |
unchanged; the open non-blittable-syscall class |
| 1 | TestQUICHandshakeError — ж<T>.op_OnesComplement nil-deref |
unchanged singleton |
| 1 | TestCertCache — weak-ref timing |
unchanged (old root 4) |
| 1 | TestBogoSuite — external BoGo shim |
not a conversion signal |
Next move, in dependency order: the sha3/mlkem768 ref state AccessViolation (10 at one
stroke, and it is a POINTER-materialization defect, so it likely reaches well past crypto/tls),
then re-census the 3 unrooted hangs after it — the same “remove the root before investigating what
sits behind it” that just paid off here — then GetAddrInfoW, then the two singletons.
Still builds-and-partly-runs; no roster row, no proof page, no disclosures, converted test sources not committed.
⛔ STILL DOES NOT BANK — the sha3 AccessViolation closes and crypto/tls goes 163 → 176 of 184; real divergences 17 → 4, and every one of them was already on this board (2026-08-16, lane claude/sha3-copyout-av)
The previous entry named the sha3.copyOut AccessViolation the new head of the queue, priced it at
10, and said to re-census the 3 unrooted hangs after removing it rather than investigating them
first. Both calls were right, and the second paid better than the first: the same per-test method —
one process per top-level Test*, -run '^Name$' -timeout 20s, the same 184 non-boringcrypto
functions — now measures 176 PASS, 7 FAIL, 0 HANG, 1 CRASH, 0 infrastructure-error in 728 s.
| Measure | 2026-08-16 (pre-default!) |
after default! |
now |
|---|---|---|---|
| top-level tests that PASS run on their own | 127 of 184 | 163 of 184 | 176 of 184 |
| real divergences (Go passes, C# does not) | 53 | 17 | 4 |
| distinct roots behind them | 8 | 5 | 3 |
| silent HANGs | 13 | 4 | 0 |
184 − 176 = 8 non-passing, and four of those are Go’s OWN expired-certificate failures
(TestResumption, TestResumptionKeepsOCSPAndSCT, TestVerifyConnection, TestCrossVersionResume)
— unchanged, both languages fail them, still the 180-of-184 ceiling this host cannot beat. So the
real divergence count is 4.
What closed
All 10 of the tests the previous entry attributed to this root now PASS: TestDialTimeout,
TestHandshakeKyber, TestHostnameInSNI, TestKyberDecapsulate, TestKyberEncapsulate,
TestSCTHandshake, TestServerSelectingUnconfiguredApplicationProtocol,
TestServerSelectingUnconfiguredCipherSuite, TestTLS13OnlyClientHelloCipherSuite, TestVersion.
And so do all 3 of the unrooted silent hangs — TestCipherSuitePreference,
TestConnectionState, TestDialer. They were the same root, exactly as the “remove the root
before investigating what sits behind it” heuristic predicted for the second time running. The
package now has zero hangs, which is worth recording on its own: a suite that contains a hang
cannot be measured by a single-process run at all (the 23 → 7 floor the 2026-08-16 entry had to
explain away), and this one no longer does.
The root, named exactly — and it was not about sha3, or about crypto, or about the OS
The previous entry read the fault as “ref state not addressing managed storage”. That is the
symptom, not the defect, and the receiver is fine. Reproduced in 25 lines with no crypto in it:
type state struct{ a [4]uint64 }
var d state
ab := (*[32]byte)(unsafe.Pointer(&d.a))
copy(b, ab[:]) // Fatal AccessViolationException
which emits, identically to sha3’s xorIn/copyOut:
var ab = (ж<array<byte>>)(uintptr)(new @unsafe.Pointer(Ꮡd.of(state.Ꮡa)));
copy(b, (~ab)[..]);
A byte[] view over a uint64[] has no managed spelling, so pointerReinterpretManagedSource
correctly excludes pointer-to-ARRAY targets and the site keeps the raw-address route. That box is a
perfectly good address. The defect is the dereference: ~ab reads an array<byte> STRUCT —
a backing-store reference plus bounds — out of the keccak state’s own DATA, fabricating a managed
reference. The stack the previous entry recorded is the fabricated reference being used, one
frame removed from the reinterpret that built it:
System.AccessViolationException
at go.slice`1[Byte]..ctor(Byte[], IntPtr, IntPtr, IntPtr)
at go.array`1[Byte].Slice(Int32, Int32)
at go.array`1[Byte].get_Item(System.Range)
⚠ The previous entry’s “reading d.storage[..rate]” is a misattribution worth correcting: that is
the argument at the call site, and it evaluates fine. The fault is inside copyOut’s own ab[:].
The two fixes, at the two layers that have one
No general fix exists, and that is the finding rather than a shortfall: slice<T>/array<T> are
windows on a real T[], and a U[] view over a V[] cannot be constructed. This is the raw-metal
fork the S1/CS0030 ruling already governs, and crypto/subtle’s xor_generic.cs is the same case
one type-pair over (a uintptr[] view over a byte[]).
The site is src/core/vendor/golang.org/x/crypto/sha3/xor.cs, which takes [module:
GoManualConversion] and crypto/subtle’s remedy: MemoryMarshal.AsBytes over the state array’s own
span is a genuine ALIASING view, so the absorb’s XOR lands in the real state and the squeeze reads
it. Go’s cpu.IsBigEndian branch is left exactly as converted. Marker census 58 → 59.
⚠ The layer fix was BUILT, MEASURED, and REJECTED — and the reasons are the most transferable
thing in this entry. An AccessViolation is the worst available failure mode (uncatchable, no
diagnostic, names the innocent consumer), so ж<T> was made to refuse the read with a contained
panic instead. Two counter-examples killed it, both found by gates rather than by reasoning:
RuntimeHelpers.IsReferenceOrContainsReferences<T>()alone is too WIDE.time.syncTimerisreturn ~Ꮡc.Reinterpret<channel<Time>, unsafe.Pointer>(), andunsafe.Pointeris a CLASS, so it lands on the same address route — but reading it yields the REAL channel object, type-CONFUSED rather than fabricated. That is the managed-referent model the corpus is built on. The wide form took downtime.NewTimer, and with it everycrypto/tlstest that opens a pipe, on the first host run. Adding atypeof(T).IsValueTypeterm fixed that one.- The narrowed form still regressed
ArrayCastDerefClone— the behavioral guard for this very fork, caught by the full suite (Outputexit code mismatch: C# 2 vs Go 0). Its*(*Row)(unsafe.Pointer(&r))over a ZERO-valued array reads anarray<nint>whose fabricated backing reference is null, andarray<T>’s null-safe zero value absorbs it — so the site produced garbage harmlessly, which is exactly the “compiles and does not crash” bar the S1 ruling sets for raw-metal stubs. The refusal converted a tolerated stub into a panic.
The distinction the remedy actually needs is not the pointee’s TYPE but whether the fabricated
reference comes out NULL — benign when it does, fatal when it does not. That cannot be tested
without first materializing a T with a wild reference in a stack slot, which is itself unsafe (a
GC scanning that slot is the same crash). So the class keeps the AccessViolation, and the sketch
above is the starting point for whoever revisits it. Anyone tempted to re-add the refusal should
read this paragraph first: the wide form and the narrow form have both already been tried.
Census of the class, and of the siblings
The emitted shape appears in 43 corpus files. Almost all are either address-only (never
dereferenced) or in runtime raw-metal paths nothing reaches — runtime and its per-GOOS folders
hold 26 of the 43. The live remainder is internal/syscall/windows/registry’s
SetDWordValue/SetQWordValue, reflect/runtime’s name.pkgPath, internal/reflectlite,
internal/poll/windows, syscall/windows and go/types. Each is now a named panic instead of
a process kill, and each gets fixed when a suite reaches it — not speculatively.
Sibling vendored crypto is CLEAN. sha3’s two lines are the only (*[N]T)(unsafe.Pointer(…)) in
all of vendor/golang.org/x/crypto: chacha20, chacha20poly1305, internal/poly1305,
curve25519, hkdf and cryptobyte use none (the only other unsafe.Pointer uses are
internal/alias’s address COMPARISONS, which are not reinterprets), and blake2b is not vendored
into GOROOT at all. The suspicion that “they use similar state tricks” does not survive the census.
The 4 that remain, by root
| count | root | status |
|---|---|---|
| 1 | TestVerifyHostname — process AV 0xC0000005 in syscall.GetAddrInfoW |
unchanged; the open non-blittable-syscall class. Now the head of the queue |
| 1 | TestQUICHandshakeError — ж<T>.op_OnesComplement nil-deref on a goroutine |
unchanged singleton |
| 1 | TestCertCache — weak-ref timing |
unchanged (old root 4) |
| 1 | TestBogoSuite — external BoGo shim |
not a conversion signal |
Next move: GetAddrInfoW (the last root shared with another package — net and crypto/x509
both want it), then the op_OnesComplement singleton. TestCertCache and TestBogoSuite are not
worth an arc: one is a GC-timing assertion the CLR cannot be made to satisfy on demand, the other is
an external binary. At 4 divergences, the question crypto/tls now poses is a banking question,
not a debugging one — what a roster row and a disclosure manifest would have to say about the
four.
Guarding, honestly
No behavioral guard is available for the sha3 fix, and the reason is structural rather than an
omission: the vendored package has no _test.go in GOROOT, and a behavioral test cannot import
golang.org/x/crypto/sha3 (the converter resolves it to core/golang.org/…, not
core/vendor/golang.org/…). What guards it instead:
GolibTests.Sha3ReinterpretVectorTests— known-answer vectors run against the corpus package directly. FIPS-202’s own SHA3-256(“”)/SHA3-256(“abc”)/SHA3-512(“abc”)/SHAKE256(“abc”), plus lengths 135/136/137/200/1000/4096 and an offset-13 sub-slice checked against the OS SHA-3 implementation — an oracle with no dependency on this repository. 136 is SHA3-256’s exact rate, so those three straddle the multi-block boundary wherexorIn’s fast path engages and the state is XORed and permuted repeatedly; the offset sub-slice makes the input’s word-at-a-time read unaligned, which the always-aligned state span never exercises. What they really prove is that the aliasing view WRITES THROUGH — a snapshot instead of an alias gives a wrong digest on the first vector. ⚠ The “no test tier is shaped for this” instinct was wrong and worth un-learning:GenericTestsalready referencescore/sort, so an MSTest tier binding a converted package is established practice, not new infrastructure.- The marker gate — proven by reconvert:
xor.csuntouched, emission redirected toxor.cs.auto.
Neutered-fix control, run rather than asserted. Restoring the auto-converted xor.cs does not
merely fail the four vector tests — it KILLS the test host with an AccessViolationException inside
slice<byte>..ctor. That is the defect’s whole character, and the reason the layer fix above was
attempted at all: expect a dead host, not a red test, if you re-run that control.
⚠ crypto/internal/mlkem768 is the natural operational guard and it is BLOCKED, on two defects
that have nothing to do with any of this. Its suite is the direct consumer of sha3 (TestRoundTrip,
TestPQCrystalsAccumulated and the field/compress vectors all drive SHAKE), and the -tests
pipeline converts it but cannot build it:
mlkem768_test.cs(94): CS0315 —builtin.min<T>overfieldElement, a[GoType("num:uint16")]named numeric, which the generated wrapper does not giveIComparable<T>.mlkem768_test.cs(182,194): CS0841 — “cannot use local variableqbefore it is declared”, an emission-ordering defect in the test file.
Fixing those two would bank mlkem768 as a roster package AND retire this guard gap in one arc; it is the cheapest available roster growth in the crypto tree.
CLOSED 2026-08-16 by lane
claude/mlkem768. Both defects are fixed at their real layer, the package banks 12/12 · 0 disclosed as roster row 147, and the guard gap this paragraph opened is retired. One correction to the bullets above: the second defect is not an emission-ordering defect — nothing is emitted out of order. It is a package-level const taking neither half of the local-shadow defence, and it produces CS0165 as readily as CS0841 depending only on whether the C# declaration saysvar. See the entry at the end of this board.
Method notes worth keeping
- A per-test census script must be written against the CONVERTED host’s verdict format, not
go test’s. The host printsPASS<pad><Name>/FAIL<pad><Name> — reason, not--- PASS: Name. A classifier carrying thego testshapes filed every pass as CRASH and reported 177 CRASH / 7 FAIL for a run that was really 176 PASS. The per-test stdout was saved to disk, so the verdict was recovered by reclassifying offline rather than by re-running — save the raw output, always, and treat a summary that is uniformly bad as a classifier bug before believing it. - A top-level verdict line can carry a trailing message (
PASS TestDialTimeout — with timeout 100000, …retrying), so an end-anchored name match under-counts passes. Anchor on “not followed by a name character or/” instead, or subtests are miscounted too. - A SINGLE-PACKAGE reconvert proves a MARKER, never an emission. Run against sha3 it also
rewrote
sha3.cs(keccakF1600(ref nonnil(ref d).a)→keccakF1600(Ꮡd.of(state.Ꮡa))), emitted akeccakf_amd64.csthe-stdlibdriver excludes, and dropped the csproj’s validation-proof block — three single-package-vs-stdlib artifacts that read exactly like drift. Use the seeded-stdlibreconvert to measure emission.
✅ crypto/internal/mlkem768 BANKS 12/12 — sha3’s only available operational guard, and the two defects that stood in front of it (2026-08-16, lane claude/mlkem768)
The sha3 entry above closed an AccessViolation and then recorded a gap it could not close itself:
crypto/internal/mlkem768 is the only operational guard the hand-owned sha3 fix can have — the
vendored golang.org/x/crypto/sha3 ships no GOROOT test suite and is not importable from a
behavioral test — and the -tests pipeline converted mlkem768 but could not build it, on two
defects that had nothing to do with sha3, with crypto, or with each other. Both are now fixed at
their real layer, both proven failing-first by behavioral guards, and the package banks
12 matched · 0 disclosed as roster row 147 (16,573 verdicts, 68.4%).
Defect 1 — CS0315: a named numeric was ordered by OPERATORS but was not IComparable<T>
min(a-got, got-a, a-got+q, got-a+q) over type fieldElement uint16 (TestDecompressCompress)
did not compile: “no boxing conversion from fieldElement to System.IComparable<fieldElement>“.
Ordering has two surfaces in .NET and the generated [GoType num:] wrapper carried only one.
IComparisonOperators<T,T,bool> — which the wrapper declares — serves a constraint lifted from
cmp.Ordered, and golib’s TWO-argument min/max bind it. The N-argument forms cannot: a
params ReadOnlySpan<T> element has to compare through a member, so they bind IComparable<T>,
which the wrapper did not implement. So a named numeric bound min(a, b) and failed
min(a, b, c, d) — an arity cliff, which is why nothing had tripped it before.
Fixed in go2cs-gen, not at the call site and not by relaxing golib’s constraint:
InheritedTypeTemplate declares IComparable<T> on the same kind-gate as IComparisonOperators
(every numeric kind except complex, which Go orders no more than C# does) and NumericTypeTemplate
emits its one member inside the same gated block, CompareTo forwarding to the UNDERLYING value’s:
public int CompareTo(fieldElement other) => m_value.CompareTo(other.m_value);
Forwarding rather than re-deriving from the wrapper’s own </> is deliberate — it keeps a named
float on the BCL total order (NaN below everything), which is what makes min yield NaN when any
argument is NaN, as Go’s does. The wrapper was already IEquatable<T>; it is ordered now too,
matching the golib uintptr and @string structs, which are both.
Why this layer and not golib. Relaxing the params overload to IComparisonOperators was the
alternative, and it is the wrong general fix: the two overloads differ only in constraint, so it
would have to REPLACE the IComparable<T> one, silently withdrawing N-argument min/max from
every type that is comparable without carrying operators. The wrapper’s missing interface was the
actual incompleteness, and filling it also buys Array/List.Sort, SortedSet<T> and
Comparer<T>.Default over every named numeric in the corpus.
Defect 2 — CS0841/CS0165: a package-level CONST had NEITHER half of the local-shadow defence
q := big.NewInt(q) (TestZetas, TestGammas, over const q = 3329). Legal Go — a short variable
declaration’s scope begins after its own ValueSpec, so the initializer reads the constant — and
illegal C#, where the local owns the name for the whole block and its own initializer binds to it.
The converter already defends this shape twice over, and both defences are keyed to *types.Var:
- the LOCAL-rename half (
usedPackageVarNames, variableAnalysisOperations) pre-scans for idents resolving to a package-level object found inglobalScope— typedmap[string]*types.Var, so a const is not in it and a const-shadowing local is never renamed; - the QUALIFY half (convIdent) rewrites the global’s reference to
<pkg>_package.name, gated onv.info.ObjectOf(ident).(*types.Var)— a const does not match the type assertion.
A *types.Const fell between them. Fixed by giving convIdent a const arm beside the var arm, with
one deliberate difference: it consults funcScopeVarNames (every variable declared ANYWHERE in the
function) rather than funcLevelDecls (declarations directly in the function body), because the
same shape inside an if/for init is not function-level and the const has no rename to fall back
on. Qualifying a reference no local actually shadows costs verbosity and never changes meaning, so
the wider set is the safe side to err on.
The board’s characterization of this one was wrong and is corrected in place above. It reads as
an “emission-ordering defect”; nothing is emitted out of order. It is also not reliably CS0841: that
diagnostic appears only when the C# declaration infers var (which big.NewInt does). Declare the
type and the identical Go source produces CS0165, “use of unassigned local variable” — one root,
two diagnostics, which is why the guard carries both shapes.
Guards, proven failing-first
Both fixes extend an EXISTING behavioral project rather than adding one — each defect is a new member of a family that project already documents.
| Guard | Extension | Pre-fix verdict |
|---|---|---|
MinMaxBuiltin |
min/max at 2 and 4 arguments over named unsigned (fieldElement uint16, including mlkem768’s own four-argument call), floating (ratio float64) and signed (delta int8) underlyings |
CS0315 ×10 across all three kinds |
GlobalShadowedByLocal |
a self-referencing initializer at function level, the same shape in an if init, the var-inferred form that reproduces mlkem768’s exact CS0841, and an unshadowed control that must keep the bare name |
CS0165 ×2 (explicit-type shapes); the var shape adds the CS0841 |
Both were run with the fixes reverted and the binaries rebuilt, not asserted.
Corpus footprint: measured at ZERO
A seeded -stdlib reconvert (5,055 files seeded incl. version.props + docs/validation; 59
[module: GoManualConversion] files, 0 marker-gate violations; 1,664 files emitted) produced
50 CRLF phantoms and exactly ONE real content diff — crypto/internal/mlkem768/README.md, whose
Tests badge flips to 12/12 validated because the package now has a proof page. That is the bank’s
own artifact, not the converter’s. So the const arm changes no stdlib package’s emission: no
converted function both references a package const and declares a same-named local. The generator
change is compile-time and changes no emission at all; its gate is the corpus BUILD, which is clean.
The census, honestly
12 of 12 top-level tests match, 0 disclosed, 0 skipped, 4 declarations excluded on both sides
(BenchmarkKeyGen, BenchmarkEncaps, BenchmarkDecaps, BenchmarkRoundTrip — the standard
Phase-4D benchmark deferral). No third defect appeared behind the two: the suite built and ran
first try once they were fixed, which is worth recording against this week’s pattern.
C# runtime, i7-5820K, solo: 434.7 s, and it is one test —
| Test | C# elapsed |
|---|---|
TestPQCrystalsAccumulated |
417.3 s |
TestFieldReduce |
7.4 s |
TestFieldSub |
3.2 s |
TestFieldMul |
2.8 s |
TestFieldAdd |
2.6 s |
| the other seven | < 1 s each |
TestPQCrystalsAccumulated runs 10,000 full key-gen/encapsulate/decapsulate rounds and accumulates
them into a SHAKE-128 digest. 435 s clears the sweep’s 10 m default by only 1.4x — inside the
spread a loaded sweep produces — so this package is owed a $longTimeouts floor and gets 30m
(4x headroom). That is the sixth entry in that table; the rule it follows is unchanged (a floor, not
an override — a larger -TestTimeout still raises it).
The guard gap closes
mlkem768’s production code drives sha3.Sum512, New512, Sum256, New256, NewShake256 and
NewShake128 — six entry points across key generation, encapsulation, decapsulation and the
sampling PRFs — and every one of them is on the path of the banked verdicts. The sha3 entry’s
hand-owned xor.cs (the array-reinterpret fix) therefore now has a committed suite that exercises
it on every sweep, which is exactly what that entry said it lacked. crypto/tls’s 176-of-184
remains the wider witness; this is the repeatable one.
Run, not asserted — the neutered-fix control. Restoring the auto-converted xor.cs over the
hand-own (its .cs.auto sibling is committed beside it) and rebuilding kills the mlkem768 test host
outright, exactly as the sha3 entry predicted for crypto/tls: a fatal
AccessViolationException, exit 0xC0000005, on the stack
go.slice<byte>..ctor ← array<byte>.Slice ← sha3.copyOut ← padAndPermute ← Read
← sha3.Sum ← Sum512 ← mlkem768.kemKeyGen ← generateKey
← GenerateKey ← TestBadLengths
The host dies at the FIRST test that generates a key, so the comparison reports Go="pass" C#=""
for all 12 rows — the documented dead-host signature, not twelve failures. Restore the hand-own and
all 12 pass again. That is the guard doing its job, measured on the committed suite.
⚠ A restore-from-backup trap, worth one line because it cost a run. Copy-Item PRESERVES the
source file’s LastWriteTime, so copying a backup back over xor.cs gives the restored file an
older timestamp than the neutered build’s assembly — MSBuild judges the project up to date and
keeps the NEUTERED dll. The re-run then reproduces the AV with clean, HEAD-matching source and a
git status that shows nothing at all. Touch the restored file (or build --no-incremental) before
believing any verdict that follows a hand-own swap.
🔎 SCOUT — golib’s name surface leaks into every converted package, and ONE shape of collision compiles and lies (2026-08-16, lane claude/golib-name-scout)
FIX IS QUEUED, NOT PRESSING (user ruling at the time this was raised). Nothing in the corpus is
broken today; the exposure is user-code- and -recurse-facing. This entry is the census, the
measured resolution matrix, the defense recommendation and the guard spec, so the fix lane starts
from measurement rather than from re-derivation.
The question (user-raised)
golib publishes helper names into the scope every converted file compiles under — builtin members
(mapclone, tmpstring, subslice3, …), the runtime types (slice, map, @string, error, …),
extension methods. End-user Go code may legally declare identifiers with those exact names, and Go
even permits shadowing its own predeclared identifiers (func len(...) at package scope is legal Go
— verified, not assumed). What happens in the emitted C#?
The scope converted code compiles under — measured from the emission, not from doctrine
Three channels, and only the first is obvious:
<Using Include="go.builtin" Static="True" />incsproj-template.xml(every converted.csprojcarries it) — everypublic staticmember ofgo.builtinis a bare simple name in every converted file.namespace go;— converted code is emitted inside golib’s own namespace, so every public golib type there (slice<T>,map<K,V>,channel<T>,array<T>,@string,error,sstring,uintptr,comparable<T>,complex64,ж<T>, …) is a bare type name, and the nested namespacego.golibis a bare namespace name.- The alias
<Using>items —any,uint8…uint64,int8…int64,float32/64,complex128,rune,GoBigConst,GoTagAttribute,GoInitAttribute— plususing System;andusing System.Numerics;.package_info.csaddsusing go;+using static go.<pkg>_package;.
⚠ src/core/GlobalUsings.cs is DEAD and misleads a reader into thinking it is the mechanism.
There is no .csproj at src/core/; golib.csproj has no explicit <Compile> and SDK globbing is
rooted at its own directory; every converted csproj does <Compile Remove="**/*.cs" /> then
<Compile Include="*.cs" /> — current folder only. Nothing compiles it, and it has drifted (it still
declares a GoTag alias no live csproj uses). Live global usings come exclusively from
csproj-template.xml. Deleting the file, or wiring it, is a separate small cleanup.
Census of the exposed surface
| Bucket | Count | Names |
|---|---|---|
| (a) all-lowercase ASCII | 46 | append array builtin cap channel clear close comparable complex complex64 copy defer delete error fallthrough fatal heap i imag iota len make map mapclone max min nil nonnil panic print println range real recover select slice sslice sstring str subslice subslice3 tmpstring type uintptr widen zero |
| (a′) lowercase-initial, mixed | +4 | _ errorExtensions initPackage trySelect |
| (b) glyph-bearing | 9 | ᐧ ᐧᐧ ꟷ ꓸꓸꓸ ᐸꟷ ж Ꮡ makeǃ goǃ |
(c) @-escaped |
1 | @string — the collision that matters, see p17 |
| (d) PascalCase | 11 + ~60 types | AreEqual ConvertToType ConvertToUInt64 GetGoTypeName GoZero Implements StackAllocThreshold ToUTF8Bytes TryTypeAssert (+ InitializeGoLib/ZeroIsDefault, internal), plus the public type/attribute/interface surface |
| extension-method names in scope | 16 | _ array DerefOrNil DerefOrNull i Lsh OrTypedNil PrintPointer Reinterpret Rsh slice sslice ToGoString ToSlice ToUTF8Bytes type |
golib’s internal members are not in scope for converted packages — ж.cs:18-19 grants
InternalsVisibleTo only to unsafe and GolibTests. go2cs.Symbols’ 15 constants are not
bare names either (no using static go2cs.Symbols; in any converted csproj); only the namespace
simple name go2cs leaks.
Three C# resolution rules, MEASURED — this is what decides each row
- R1 — a member of
<pkg>_packageHIDES the using-static member of the same name, and C# does not merge the candidate sets. Lookup stops at the first scope that has the name, not the first applicable overload. So a signature-compatible user declaration is silently captured; an incompatible one is a compile error whose message names a helper the user never wrote. - R2 — a call site emitted with EXPLICIT TYPE ARGUMENTS is IMMUNE. Arity participates in lookup,
so
heap<box>(out var Ꮡb)skips a non-generic member and a local variable namedheap. Measured at p03 and p15. This is load-bearing: it removesheap,zero,subslice,subslice3,slice,make,clear,min,maxfrom the dangerous class and shrinks the fix by most of its apparent size. - R3 — a nested type hides a
namespace gotype of the SAME ARITY only.type slice struct{}(arity 0) does not hideslice<T>(arity 1) — buttype string struct{}emits@string(arity 0) and does hidego.@string(arity 0). Measured at p17.
The measured resolution matrix — 21 probes, transpiled, compiled, run and diffed against go run
Probes were behavioral-test-shaped single packages built against the worktree’s live src/
(-p:go2csPath=<worktree>/src/ -p:UseSharedCompilation=false). Every probe’s Go program runs clean
first, so a divergence is the conversion, never the source.
| # | Scenario | C# result | Class |
|---|---|---|---|
| p01 | pkg func len([]int) int (predeclared shadow) + slice exprs, range |
compiles, matches | benign |
| p02 | pkg func append(a, b int) int |
compiles, matches | benign |
| p03 | pkg func heap(int) int + &T{} and an escaping local |
compiles, matches | benign (R2) |
| p04 | pkg func mapclone(int) int + maps.Clone |
compiles, matches | benign (call emits qualified maps.Clone<…>) |
| p05 | pkg func str([]byte) string |
compiles, matches | benign (no builtin.str site emitted here) |
| p06 | pkg func nonnil(int) int, no trigger |
compiles, matches | benign |
| p07 | pkg func tmpstring(int) int + m[string(b)] |
CS1503 cannot convert from 'go.slice<byte>' to 'nint' |
LOUD |
| p08 | type error struct{}, unused |
compiles, matches | benign |
| p09 | type string struct{}, unused |
compiles, matches | benign |
| p10 | type slice struct{} + real slices |
compiles, matches — emitted Δslice |
benign, existing defense |
| p11 | pkg var append = 11 |
compiles, matches | benign |
| p12 | pkg func subslice3(int) int + s[1:3:4] |
compiles, matches (emits s.slice(1,3,4)) |
benign |
| p14 | pkg func tmpstring(b []byte) string + m[string(b)] |
compiles; Go 11 ZZ vs C# 0 ZZ |
SILENT WRONG |
| p15 | LOCAL heap := 5 in a function with a heap-promoted local |
compiles, matches | benign (R2) |
| p16 | pkg func nonnil(int) int + setOne(&e.x) on a pointer param |
CS1615 Argument 1 may not be passed with the 'ref' keyword |
LOUD |
| p17 | type string struct{} + a real string in the package |
CS0029 Cannot implicitly convert 'ReadOnlySpan<byte>' to 'go.main_package.@string' |
LOUD |
| p18 | type error struct{} + a real errors.New value |
compiles, matches | benign |
| p19 | pkg func len + copy/map/range-over-string/3-index/variadic sweep |
compiles, matches | benign |
| p20 | LOCAL vars str, zero, nonnil, subslice, tmpstring |
compiles, matches | benign |
| p21 | type error struct{} + dynamic interface{ Error() string } assert |
compiles, matches | benign |
| p13 | type array + type channel alongside real arrays/channels |
compiles, matches — Δarray/Δchannel |
benign, existing defense |
| p23 | -recurse=module app with a package named golib |
CS0576 Namespace 'go' contains a definition conflicting with alias 'golib' |
LOUD, -recurse-only |
Verdict census: 1 compiles-and-WRONG, 4 compile errors, 16 benign.
p14 is the whole finding
Go source says table[string(b)]. The converter emits table[tmpstring(b)] — builtin.tmpstring
is the zero-copy map-index-key optimization, a name the Go source never spells. A user who happens
to declare func tmpstring(b []byte) string gets @string tmpstring(slice<byte>) in
main_package, which by R1 hides the golib helper and by luck matches its signature. The map
lookup silently routes through the user’s function, misses, and returns the zero value. No
warning, no error, exit 0, wrong answer. The converter’s transpile step exits 0 and prints
nothing.
The same shape is live for every helper the converter emits implicitly and without explicit type
arguments. Derived from the census and the emission sites (convIndexExpr.go:337 tmpstring(%s),
refLoweringEmissionOperations.go:530 nonnil(ref %s), visitFuncDecl.go:1788 maps.clone →
mapclone, and the str/trySelect/fatal/i/widen sites), the dangerous class is 12 names:
_ fatal i initPackage iota mapclone nil nonnil str tmpstring trySelect widen
The other 34 of the 46 fall out for three reasons, and the first is free immunity worth naming:
- Six are Go KEYWORDS and can never be a user identifier at all —
defer,fallthrough,map,range,select,type. golib spells them as C# members precisely because Go reserves them, so that slice of the surface defends itself. - Eighteen are Go universe names (
append,cap,clear,close,complex,copy,delete,imag,len,make,max,min,new,panic,print,println,real,recover) — already defended bygoBuiltinNames/packageBuiltinShadows, below. - The remainder are generic-with-explicit-type-arguments (
heap,zero,subslice,subslice3,slice,sslice,array,channel, …) — immune by R2, or handled on the type side.
Two of the twelve are not function-call shaped and route through different emission paths, so
they need their own attention in the fix: nil (a static readonly field the converter emits in
every == nil / = nil, and var nil int is legal Go) and iota (a const, likewise legal as
a Go package-level name). Neither was probed; both belong in the guard project.
⚠ Extension-form emissions (i, _, type, slice, array, sslice, ToUTF8Bytes) were NOT
probed for silent capture. Extension-method lookup runs only after instance lookup fails and is not
hidden by a same-named static member, so they are low-risk by construction — but that is reasoning,
not measurement, and the fix lane should probe one before dismissing the bucket.
Live corpus near-misses — 5 packages already do this, all benign TODAY
The corpus compiles and 130 packages validate, so nothing here is broken. But the stdlib is already one emission away from p14:
| Package | Declaration | Shadows | Why it survives |
|---|---|---|---|
runtime |
map.cs:1656 internal static any mapclone(any mʗp) |
builtin.mapclone(any) — identical signature |
runtime never emits a maps.Clone call. This is p14’s exact shape, sitting in the tree. |
math |
fma.cs:11 internal static uint64 zero(uint64 x) |
builtin.zero<T>() |
R2 — zero<T>() always carries explicit type args |
runtime, sync |
panic.cs:1103, mutex.cs:35 fatal(@string) |
builtin.fatal(string, nint = 1) |
no builtin.fatal site in either package (mutex.cs is hand-owned, so this one is deliberate) |
log/slog |
value.cs:354 str(this Value v), plus local funcs str in level.cs:65 and json_handler.cs:201 |
builtin.str |
no builtin.str site in slog |
runtime |
debuglog.cs:191 i(this ж<dlogger> Ꮡl, nint x) |
builtin.i (extension) |
extension-vs-extension on different receivers |
That is the honest answer to “does the corpus trip this today”: no — but it holds five loaded
guns, one of them (runtime.mapclone) with a signature that matches exactly.
What defends this today, and the exact gap
Five hand-maintained lists, none of them derived from golib:
| List | File | Size | What it does |
|---|---|---|---|
keywords |
identifierNaming.go:33 |
65 | @-escape — this is what turns Go string into @string, i.e. it creates p17’s collision |
reserved |
identifierNaming.go:85 |
52 | Δ-rename (Δfoo, or Δfooᴛ when also collision-flagged) — holds array, builtin, channel, slice, sstring, GoFrame, NilType, PanicException, AreEqual, GetGoTypeName, ToUTF8Bytes, type, and 4 marker glyphs |
emitterSpelledTypeNames |
nameCollisionAnalysisOperations.go:37 |
4 (any, rune, nint, nuint) |
package-scoped Δ-rename, TYPES only |
goBuiltinNames |
packageGlobalState.go:234 |
18 | a package-level func/method with a Go universe name → the converter’s own builtin calls emit qualified as builtin.X(…) (packageBuiltinShadows). This is the mechanism that covers the universe-name family — p01/p02/p11/p19. |
csharpKeywordCastTypes |
convCallExpr.go:26 |
15 | cast parenthesization, not naming |
The gap, stated exactly: nothing in the converter enumerates golib’s builtin member surface or
its type surface. reserved covers 4 of the ~30 public builtin statics and misses mapclone,
tmpstring, str, subslice, subslice3, nonnil, heap, zero, fatal, range, select,
trySelect, initPackage, widen, i, Implements, TryTypeAssert; on the type side it misses
error, comparable, complex64, uintptr, sslice, and the go.golib namespace.
goBuiltinNames is correct but scoped to the 18 universe names and to func/method declarations only.
The symbol table does not help and was checked because it looked like it should.
src/core/go2cs/symbols.json → gensymbols → symbols.go + Symbols.cs is purely the glyph and
marker vocabulary (27 entries: ж, Ꮡ, Δ, ᴛ, ʗ, ˢ, _package, …). It carries no
reserved-name data, and nameCollisionAnalysisOperations.go does not read it at all — the only seam
is identifierNaming.go:94, which drops four marker glyphs into reserved. check-symbol-sync.ps1
verifies only that the two projections match the JSON, and is wired into no gate (not CNR, not
the behavioral runners, not go test; there is no CI workflow). So the generator precedent exists
and is good — it simply was never pointed at this problem.
No unit test guards any of the five lists. sanitization_test.go covers import-path segments
only. The 46 behavioral projects matching Collision/Shadow/Reserved/Keyword/Builtin cover the
machinery well — ReservedNameShadows is the closest — but every one of them tests names already
in the lists. None tests the gap, by construction.
Recommendation — one generated list, feeding two mechanisms that already exist
The durable fix is not to grow reserved by hand. Two reasons, and the second is a trap:
- Hand-maintenance is exactly what drifted; golib gains members freely and nothing notices.
- ⚠
reservedis the WRONG mechanism for these names. Its own doc comment already warns that names the emitter itself spells must never go in it, because legitimate emissions flow back through the same string-based sanitizers (slice<rune>corpus-wide would corrupt toslice<Δrune>).mapclone,tmpstring,nonnil,strare precisely emitter-spelled names. Adding them toreservedwould corrupt the converter’s own output.
So: generate the list, and feed it to the mechanisms already proven for the universe builtins.
- New:
src/go2cs/internal/gengolibsurface→ committedsrc/go2cs/golibSurface.go, amap[string]golibNameKind(builtinMember/namespaceType/namespaceChild/extensionMethod) produced by scanningsrc/core/golib/*.csforpublic staticmembers ofpartial class builtinand public types innamespace go. Modeled ongensymbols— deterministic,go generate-driven, BOM/line-ending preserving. - Gate it in the plain
go test ./...run, theprojitemsIntegrity_test.goprecedent: regenerate in-memory, compare against the committed file, print the exact missing entry on failure. That is what makes the list unable to drift from golib. (Optionally also wirecheck-symbol-sync.ps1into CNR while in the neighbourhood — it currently gates nothing.) - Consumer 1 — builtin members → qualification, not renaming. Extend
performNameCollisionAnalysisso a package-level declarator (func, type, const or var) whose name is abuiltinMemberregisters inpackageBuiltinShadows. Then threadbuiltin.through the implicit emission sites —convIndexExpr.go:337(tmpstring),refLoweringEmissionOperations.go:530(nonnil),visitFuncDecl.go:1788’s helper map, and thestr/range/select/trySelect/fatal/widensites — via one small helper (golibHelperName("tmpstring")→"builtin.tmpstring"when shadowed). Qualification beats renaming here: it leaves the user’s Go name intact in the emitted C#, which is the project’s stated readability goal, and it is the mechanism already proven for the 18 universe names. R2 means the generic-with-explicit-args sites need no change at all. - Consumer 2 — golib TYPES → the existing package-scoped Δ-rename. Add
namespaceTypenames to whatemitterSpelledTypeNamescovers (error,comparable,complex64,uintptr,sslice, and — the p17 case — the post-@-escape spelling@string). Note the ordering hazard:stringbecomes@stringingetCoreSanitizedIdentifierbefore the reserved check, so the collision test must run against the emitted spelling, not the Go one. - Consumer 3 —
namespaceChild→ the existing import-alias rename.importAliasOperations.goalready renames an alias that collides with a child namespace (using Δunicode = unicode_package;instrings.csis that machinery working). It simply does not knowgo.golibis a child namespace. Addinggolibto its set closes p23 in ~5 lines.
Guard spec
Three behavioral projects, output-compared against go run ([GoTestMatchingConsoleOutput]), all
currently FAILING — so they land with the fix, never before it:
GolibHelperShadows— the p14/p07/p16 family in one package:func tmpstring(b []byte) stringwith am[string(b)]map index;func nonnil(a int) intwith asetOne(&e.x)pointer-field call;func mapclone(...)with amaps.Clone;func str(b []byte) string;func fatal(s string). Each helper’s Go-visible answer must survive AND the converter’s own helper call must still do its job. This is the project that proves the qualification path.GolibTypeShadows—type string struct{}used alongside real strings (p17), plustype error,type comparable,type uintptr,type complex64,type ssliceeach exercised next to the real golib type.ReservedNameShadowsalready ownsany/rune/nint/builtin/sstring/GoFrame; this is its missing half and could reasonably be folded in rather than added beside it.RecursePackageNamedGolib— p23.-recurse-shaped, so it may not fit the behavioral runner’s single-package mold; if not, a converter integration test inmoduleConverter_integration_test.gois the right home.
Plus the drift test above (golibSurfaceIntegrity_test.go), which is the guard that actually
prevents recurrence — the behavioral projects prove today’s fix, the drift test prevents tomorrow’s
golib member from re-opening the hole silently.
Neutered-fix control the lane owes: revert the qualification helper and confirm
GolibHelperShadows reports an OUTPUT divergence (not merely a compile failure) — p14’s whole
character is that it compiles.
Size estimate
One lane, medium — roughly a day of work plus a CNR pass. Generator + generated list + drift test
~250 lines across 3 new files; collision-analyzer extension ~40 lines across two existing files; the
emission-site qualification helper plus ~10–15 call-site swaps; ~5 lines for the import-alias set; 2–3
behavioral projects with goldens and .slnx registration.
⚠ The lane owes a CNR run and must not assume zero corpus drift. Five stdlib packages already
declare shadowing names (table above); the moment packageBuiltinShadows learns about them, any
implicit helper call inside those packages starts emitting builtin.X(…). runtime in particular
declares three of them (mapclone, fatal, i) and is a large package. Expect a small, explainable
emission diff in runtime/math/sync/log/slog, verify each hunk is exactly a qualification, and
budget for a targeted regen rather than assuming none.
Two side findings this scout tripped over, both worth their own chips
- Δ-renamed unexported types are emitted PUBLIC — a live, corpus-wide over-export. The
<TypeAccessibility>block computes exported-ness from the emitted name, andΔ(U+0394, an uppercase Greek letter) reads as exported. Measured in the corpus, not inferred:database/sql/package_info.cs:132public partial struct ΔconnStmt {}(GoconnStmt, unexported),encoding/gob:101public partial interface ΔgobType {}(GogobType),syscall/windows:76,150,151ΔSockaddr/ΔHandle/ΔSignal,debug/{pe,macho,plan9obj}ΔSection,database/sql:130,131ΔConn/ΔStmt,database/sql/driver:118ΔRowsAffected,syscall/linux:149ΔSignal. It is already banked in a golden —ReservedNameShadows/package_info.cscarriespublic partial struct Δbuiltin/Δsstring/Δany/Δrune/Δnint {}.getAccess(identifierNaming.go:260) does stripΔbefore judging exported-ness, so thepackage_infowriter is reaching the answer by a different path; that path is the bug. Not a correctness break — C# over-visibility only — but it leaks unexported Go types into the public API surface of every NuGet-published package, so it matters more than it looks. src/core/GlobalUsings.csis dead and stale (see above). Delete it or wire it; leaving a file that looks like the global-using mechanism next to the real one costs the next reader a full investigation.
What cut against the brief
- The brief expected “silent capture of converter-emitted helper calls” to be the common case for a
user func in the same package. It is the RARE case. R1 stops lookup at the first scope, so an
incompatible signature is a hard compile error — 4 of the 5 non-benign rows are loud. Only an
accidentally signature-compatible declaration goes silent (p14). That is better than feared, and
also worse: the one silent row has no diagnostic whatsoever, and its trigger (
func tmpstring([]byte) string) is a shape a real Go program would plausibly contain. - The brief flagged unicode-prefixed names (
ж,Ꮡ,Δ,ᴛ) as low-probability collisions. They are effectively zero-probability and already defended —identifierNaming.go:94putsPointerPrefix/TrueMarker/OverloadDiscriminator/EllipsisOperatorintoreservedfrom the generated symbol table, the one place the symbol table and the reserved logic already meet. No probe was spent there. - Type-name collisions were expected to be the soft spot; they are mostly the best-defended area.
slice,array,channel,builtin,sstring,any,rune,nint,nuint,GoFrameall Δ-rename correctly today (p10, p13). The type-side gap is narrow:@string(p17) and theerror/comparable/uintptr/complex64/ssliceset, whichReservedNameShadowscurrently treats as pass-through controls — and does so only as locals, never as package-level types. That is the precise blind spot. - R2 was not anticipated by the brief and materially shrinks the fix. Half the scary-looking
names (
heapat 1,858 call sites,zero,subslice3,slice,make,clear,min,max) are immune because their call sites carry explicit type arguments — proven by p03/p15 (a local variable namedheapsitting directly above aheap<box>(out var Ꮡb)call compiles and runs correctly). Do not spend the fix on them. - The corpus check came back richer than “clean”. The brief expected no stdlib package to trip
this. Strictly true — but five packages already declare shadowing names and one of them
(
runtime.mapclone) has a byte-identical signature to the golib helper it hides. The risk is user-code-facing today; it is corpus-facing the momentruntimegains amaps.Clonecall.
Reproducing
The 21 probe packages were built under the session scratchpad and deliberately not committed —
they are all currently-failing or currently-passing-by-accident, so committing them would either
break the behavioral suite or bank the wrong baseline. Every probe’s Go source is reproduced in the
matrix descriptions above and each is 10–25 lines; the guard spec is the durable form. Method:
go2cs.exe -go2cspath <worktree>/src <dir>, then
dotnet build <dir>.csproj -c Debug -p:go2csPath=<worktree>/src/ -p:UseSharedCompilation=false,
then run and diff against go run ..
✅ fmt’s compile wall is CLOSED — two roots, not four; 62 of 63 verdicts agree (2026-08-16, lane claude/fmt-roots)
The census above was stale in the usual direction, and the usual way. It recorded four
remaining roots after the CS1955 map-nil row closed. Re-measured on master (9c64f0a44) the
compile wall is two roots — the census’s last three rows were, as it predicted, “one type’s
story”, and they are indeed ONE root. Both are general converter defects; neither is
fmt-specific; both now carry failing-first behavioral guards.
Root 1 — a cast’s operand asks TWO questions, and the arm asked only one
CS0030 float→renamedComplex64 / double→renamedComplex128 ×4 (8 diagnostics across the internal
and external test variants). The named-numeric identity-constant arm of convCallExpr (reached
because go/types gives a constant operand the target type, so the conversion reads as an identity)
consulted only castOperandNeedsParens — a leading-sign TEXT test that answers the cast-vs-
subtraction parse ambiguity. It never asked the precedence question: a C# cast binds tighter
than every binary operator, so a constant operand rendering as a top-level binary expression has
the cast claim its left operand alone.
((renamedComplex64)3F + 4F.i()) // was; the cast applies to 3F alone, CS0030
((renamedComplex64)(3F + 4F.i())) // now
The compile error is the mild symptom. The same emission is silently value-changing on
every named int/float type, where the [GoType] wrapper supplies an operator for the mis-bound
first leg and the code compiles clean:
| Go | emitted (before) | Go value | C# value |
|---|---|---|---|
rf(3 / 2) |
((rf)3 / 2) |
1 |
1.5 |
rf(7 / 2) |
((rf)7 / 2) |
3 |
3.5 |
rf(1 / 3) |
((rf)1 / 3) |
0 |
0.3333333333333333 |
Go folds the constant expression in exact arbitrary precision — untyped INTEGER division — and
then converts; the mis-bound cast converts first and divides in the target’s own float arithmetic.
Measured end to end, not argued: a four-line probe printed 1 3 0 1 under Go and
1.5 3.5 0.3333333333333333 1.25 under the transpiled C#. A named complex target has no
float→named-complex conversion at all, which is the only reason the class ever surfaced as an
error rather than as a wrong answer.
Keyed on the AST (*ast.BinaryExpr), not the rendered text — only the written expression says
whether a binary operator is left exposed. Unary operands are deliberately excluded: a cast and
a unary operator share precedence and associate right, so (T)~0 already means (T)(~0).
Guard: NamedConstConversionPrecedence (output-compared, so the silent value divergence fails the
gate and not merely the CS0030). Detail: docs/ConversionStrategies-Reference.md §Named Numeric
Types and Constant Contexts, “A cast’s operand asks TWO questions”.
Root 2 — inFunction does not mean “there is a function DECLARATION”
CS1729 + CS0103 + CS0034 — the census’s last three rows, and one root exactly as predicted.
convFuncLit sets inFunction for a literal’s body (correctly — its locals are function scope),
but currentFuncName and currentFuncPrefix, the lift’s name prefix and its declaration
sink, are allocated together by visitFuncDecl. For a literal in a package-level initializer
they therefore held whatever the PREVIOUS function declaration in the file left behind. Every
type-lift site keys on lifted && inFunction and writes into that prefix, so fmt
scan_test.go’s
{"ReaderOnly", func(s string) io.Reader { return struct{ io.Reader }{strings.NewReader(s)} }},
lifted a type named after an unrelated preceding function and wrote its declaration into that
function’s already-flushed buffer. The declaration vanished; only the use site survived —
new Scan_type(…), CS1729 (no one-argument constructor), plus CS0103/CS0034 in the
ImplementGenerator wrapper generated for the phantom type from its surviving
[assembly: GoImplement] record.
And it has a second, worse symptom the census never saw. With NO preceding function
declaration the buffer is not stale but nil, and the lift panics (nil receiver in
strings.Builder.copyCheck). That panic is recovered per file, so the entire FILE is skipped with
only a visit file error warning — a silent, total loss of one file’s conversion. Which symptom
appears depends solely on declaration order within the file, which is why fmt showed the compile
errors while a minimal standalone reproduction showed the panic.
A package-level literal now gets its own sink, flushed at package scope — where a lifted type
belongs anyway, and exactly where the sibling package-level lift (readersᴛ1) already goes — with
its name seeded from the declaration being initialized (readers_type, unique per var as
readersᴛ1 already is). Guard: PackageVarFuncLitTypeLift, whose two files cover both symptoms
(main.go puts a function declaration before the var; varfirst.go declares the var first).
Detail: docs/ConversionStrategies-Reference.md §Struct Types, “A lift inside a PACKAGE-LEVEL func
literal”.
Root 3 (bridge) — reflect.Value.Bytes had no Array arm
Behind the compile wall the first run threw
InvalidCastException: go.array<byte> → go.slice<byte> out of the hand-owned
core/reflect/value_impl.cs. Go’s bytesSlow accepts an addressable byte array as well as a
byte slice, and fmt’s printValue calls Bytes() whenever f.Kind() == Slice || f.CanAddr() — so
Sprintf("%s", &[3]byte{'a','b','c'}), whose pointer deref IS addressable, reached a function that
handled only slice shapes. CanAddr() was right; Bytes was incomplete. It now takes Go’s Array
arm, aliasing the array’s storage through array<T>.Slice exactly as Go’s unsafe.Slice(p, n)
does (a copy would silently break writers), and panics with Go’s own text on an unaddressable byte
array. Zero regression risk by construction: that shape previously always threw.
fmt census — 63 verdicts, 62 agree, 1 open, 0 disclosed
| Go | 62 pass, 1 skip (TestCountMallocs, GOMAXPROCS>1) |
| C# | 61 pass, 1 skip, 1 infrastructure-error |
| Divergences | 1 — TestSprintf |
| Excluded | 42 (standard Phase-4D Benchmark/Example/Fuzz deferrals) |
| Disclosed | 0 |
Not banked, and deliberately: banking wants genuine closure, and TestSprintf is a real
divergence rather than a disclosure candidate (the existing disclosure classes are alloc-count
asserts the CLR provably cannot satisfy; a formatting mismatch is not one).
What TestSprintf is standing on — three co-located bridge divergences
All three live inside that one table-driven test, so none can move the verdict alone. Each is measured, not inferred; the first two are reported by the test itself before the third aborts it.
| # | Entry (fmt_test.go) |
Go | C# | Root |
|---|---|---|---|---|
| 1 | {"%#12.5g", 1230000 - 0i, …} (line 565) |
+0.0000i |
-0.0000i |
The converter emits Go’s constant complex expression as RUNTIME arithmetic (1230000D - 0D.i()), and .NET’s double - Complex computes -right.Imaginary, turning +0 into -0. Go folds the constant exactly, and an exact zero has no sign. Isolated in a probe: only the SUBTRACTION form diverges — complex(1230000.0, 0.0) and 1230000D + 0D.i() are both correct |
| 2 | {"%#v", TestFmtInterface, "(func(*testing.T))(0xPTR)"} (line 717) |
(func(*testing.T))(…) |
(Action`1)(…) |
GoReflect.TypeNaming.cs has no delegate handling at all — rendering a Go func signature from a CLR Action/Func/Actionꓸꓸꓸ/Funcꓸꓸꓸ is an unimplemented bridge feature, not a defect. Sized as its own arc |
| 3 | {"%x", []renamedUint8{…}} and five siblings (lines 734–735, 839–842) |
68656c6c6f |
throws | Bytes() on a slice whose element is a NAMED uint8 type. Go permits it (Elem().Kind() == Uint8) and ALIASES through the slice header; the bridge holds slice<renamedUint8> of wrapper structs, which is not layout-compatible with byte[], so no safe aliasing conversion exists. Deliberately NOT half-fixed here — a copy would silently break writers of a core reflect API. A representation-level question |
Fix 1 was measured and deliberately NOT landed. Folding complex constant expressions is the
right general answer (the established precedent is foldedNamedFloatConstLiteral, which folds
float constant conversions so Go’s exact evaluation rounds only once). But the census says the
subtraction form has zero production sites corpus-wide — all 115 are test files in fmt,
math/cmplx, strconv and internal/fmtsort, three of which are BANKED — so landing it would
churn banked packages’ committed test sources and owe them a re-sweep, while moving no verdict on
its own (2 and 3 would still fail). It belongs with the TestSprintf arc, not ahead of it.
Recommendation for whoever takes TestSprintf: it is one lane, and #2 is its centre of gravity
— #1 and #3 are each contained, #2 is a bridge feature. All three must land together for the
verdict to move, and fmt then banks at 63/63. That bank is worth more than its own row: the
behavioral suite’s ~520 stdout comparisons all run through converted fmt, so fmt’s own
%v/%T table is the deepest test the reflection bridge has, and every Printf-comparing
behavioral test is strengthened by it.
⛔ STILL DOES NOT BANK — crypto/tls holds at 176 of 184, but the four divergences are now four DIFFERENT things, and only one of them is a disclosure (2026-08-16, lane claude/tls-endgame)
The sha3 entry above ended by saying “the question crypto/tls now poses is a banking question,
not a debugging one”, and named the four remaining divergences as GetAddrInfoW, an
op_OnesComplement singleton, TestCertCache and TestBogoSuite. This lane took all four. The
headline number does not move — the same per-test method (one process per top-level Test*,
-test.run '^Name$', 25 s cap, raw stdout saved) measures 176 PASS, 7 FAIL, 1 process-AV in
853 s — and that is the finding rather than a disappointment: GetAddrInfoW is FIXED and the test
that measured it still fails, because a second wall of the same family stood behind it.
| Measure | after default! |
after sha3 |
now |
|---|---|---|---|
| top-level tests that PASS run on their own | 163 of 184 | 176 of 184 | 176 of 184 |
| real divergences (Go passes, C# does not) | 17 | 4 | 4 |
| distinct roots behind them | 5 | 3 | 4 |
| of those, disclosable under an existing class | 0 | — | 1 |
Go on this host, re-measured rather than carried: 184 top-level, 180 pass, 4 fail, 79.9 s. The
four failures are the same expired-fixture set (TestResumption,
TestResumptionKeepsOCSPAndSCT, TestVerifyConnection, TestCrossVersionResume — the test
certificates expired 2025-01-01), and the converted host fails exactly those four, with the same
x509: certificate has expired text, so they are AGREEING rows, not divergences. The cross-tab
closes with no remainder: 176 agreeing passes + 4 agreeing failures + 4 divergences = 184.
1. GetAddrInfoW — FIXED, and the class gained its LINKED-OUTPUT member
The open non-blittable-syscall class CLAUDE.md has carried since the Timezoneinformation fix
(“net and crypto/x509 will” reach it) now has its DNS member closed. Native ADDRINFOW is 48
bytes of scalars and raw pointers where the converted AddrinfoW holds Canonname, Addr and
Next as managed references, so the hints Windows read were garbage and the *ADDRINFOW it wrote
landed in a reference slot — Fatal error. 0xC0000005 inside Syscall6, killing the process.
What makes this member different from every earlier one, and what the next member of the class
should read first: copying the top-level record would not have been enough. net reads the
sockaddr THROUGH the result ((*syscall.RawSockaddrInet4)(unsafe.Pointer(result.Addr))), and
RawSockaddrInet4.Addr [4]byte is an array<byte> — a backing-array REFERENCE plus bounds — so
reading that struct out of a native sockaddr_in fabricates managed references from address bytes.
That is the fork the sha3 entry proved has no general fix. So the hand-own transcribes the WHOLE
chain into managed boxes, sockaddr included, typed by ai_family; frees the native chain eagerly
(which makes FreeAddrInfoW a hand-owned no-op — nothing native escapes the call); and carries the
managed sockaddr pointer across the unsafe.Pointer field through golib’s ManagedPointerTokens,
whose only previous minter was the reflection bridge. Two properties worth carrying:
- the token must be wrapped in a
Pointerbuilt over a native-addressж<EmptyStruct>, because the generated named-pointer wrapper’suintptrconversion returns the address of the storage its box addresses — which for a native box IS the number handed in, so the token survives unchanged; - the token table is weak by design, so the hand-own owns the strong reference — a
ConditionalWeakTablekeyed on the record box, which is exactly the Go lifetime. Without it a live token could name a collected box and the consumer’s cast would fall back to a wild native read, turning a loud defect into a silent one.
Guarded by the new LookupServicePort behavioral output test: net.LookupPort for
tcp/udp/tcp4/tcp6 services is the one reach into this pair needing neither DNS nor a network,
and it exercises the hints mirror, the chain copy, the token handoff and BOTH sockaddr flavors at
VALUE level (a byte-order slip prints a swapped port rather than failing). Proven failing-first: with
the hand-own removed the guard reports exit code mismatch: C# -1073741819 vs Go 0 — the access
violation itself. Marker census: the two entries join manualConversionFuncs["syscall"], and
TestWindowsOnlyEntriesAreScopedToWindows now pins their platform scope.
2. …and TestVerifyHostname STILL fails, one layer further out
It now dies in net.adapterAddresses, which is dnsReadConfig’s only source of DNS servers on
Windows:
Fatal error. System.AccessViolationException
at go.ж`1[IpAdapterAddresses].op_Equality(ж`1<IpAdapterAddresses>, NilType)
at go.net_package.adapterAddresses()
at go.net_package.dnsReadConfig(string) <- via getSystemDNSConfig, from lookupIP
Same family, bigger structure, and NOT a wrapper defect: adapterAddresses asks
GetAdaptersAddresses to fill a managed slice<byte> (that part is legitimate) and then walks it as
Ꮡ(b, 0).Reinterpret<byte, IpAdapterAddresses>(). IP_ADAPTER_ADDRESSES is a linked record with
three raw string pointers, a [8]byte physical address, a [16]uint32 zone-index array and six
nested linked lists; reading it out of a byte buffer fabricates a managed reference on the first
field touched — here the Next comparison itself. The remedy is the same transcription shape this
lane just built for ADDRINFOW, one structure size up, and it belongs to a net interfaces arc
(adapterAddresses also backs interfaceTable, interfaceAddrTable and Interfaces()), not to
crypto/tls. It is what stands between the corpus and any name resolution at all on Windows.
3. The op_OnesComplement singleton is a SECOND syscall class — **T OUT-parameters arrive NULL, censused at 13
The brief for this lane (and the entry above) read TestQUICHandshakeError as “a golib operator on a
nil box … make it a Go-shaped panic, not an NRE”. Both halves are wrong, and the correction is the
most transferable thing here. The panic already IS Go-shaped — golib’s ~ raises
RuntimeErrorPanic.NilPointerDereference, printed as panic: runtime error: invalid memory address
or nil pointer dereference — and golib is the innocent frame. The nil comes from three frames out:
crypto/x509 systemVerify -> (*storeCtx).Store <- storeCtx is nil, and err was nil
createStoreContext -> CertAddCertificateContextToStore(handle, leafCtx, ADD_ALWAYS, &storeCtx)
&storeCtx is a ж<ж<CertContext>>, and golib’s ж<T> → uintptr answers 0 for it: IsNull is
the VALUE-PEEKING question, and a heap-boxed POINTER legitimately holds null before the call. That
answer is deliberate and correct for the case it was written for (syscall.Write hands writeFile a
nil *Overlapped; uintptr(unsafe.Pointer(nil)) is 0 in Go) and silently wrong for the shape every
out-parameter takes: the wrapper tells the kernel “no output wanted”, ppStoreContext is documented
OPTIONAL, the call SUCCEEDS, and the caller reads back the nil it started with. Note the 0 is
accidentally the SAFER outcome — a real address would have had the kernel write a native pointer into
a slot the collector reads as an object reference — so the remedy is a per-wrapper hand-own, never a
change to the operator.
Census of the emitted wrappers taking a **T: 13. CertAddCertificateContextToStore,
CertGetCertificateChain, ConvertSidToStringSid, ConvertStringSidToSid, DnsQuery, _DnsQuery,
GetFullPathName, getQueuedCompletionStatus, GetQueuedCompletionStatus in syscall; plus
CreateEnvironmentBlock, NetGetJoinInformation, NetUserGetInfo, NetUserGetLocalGroups in
internal/syscall/windows. Two more of the shape are already hand-owned for other reasons
(GetAddrInfoW, GetAcceptExSockaddrs). Not fixed speculatively, on the standing
fix-it-when-a-suite-reaches-it rule.
Closing TestQUICHandshakeError is therefore a crypto/x509 Windows system-verifier arc: the
out-parameter is only the first defect, because CertContext, CertChainContext, CertSimpleChain
and CertChainElement are all read back through raw addresses afterwards — the fabricated-reference
fork again, over six CryptoAPI wrappers.
4. TestCertCache IS codegen-liveness, and it is the only disclosable row
The test nils its own local, calls runtime.GC(), and polls for 4 s waiting for a finalizer to
decrement a ref count. Measured, not argued:
- Go passes it in 0.00 s; the converted host fails at the first check (
refs 2 → 1) — 4.3 s of polling on top of a 3.8 s host startup, andt.Fatalaborts before the second check. - It fails identically in an optimized Release build (a separately built Release host), so it is not the non-optimizing JIT’s frame-wide liveness.
- The finalizer bridge itself works:
runtime.SetFinalizeris the hand-ownedConditionalWeakTable+ sentinel bridge,runtime.GC()isCollect → WaitForPendingFinalizers → Collect, andsync’s bankedTestPoolGCmeasures 98 of 100 objects finalizing on the first try.
That is the roster’s existing codegen-liveness class verbatim — “a test asserts, from inside its own
frame, that an object it just stopped referencing is collectible”. The likely rooting slot is the same
one sync’s TestOnceXGC disclosure names: var (certA, err) = cc.newCert(...) materializes an
address-exposed tuple temp, and an address-exposed slot is not lifetime-tracked. That last step is
INFERRED from the class’s known mechanism, not read out of the JIT — the three bullets above are the
measurements. No new class, so no coordinator ruling is owed; the row is disclosable whenever the
package can bank.
5. TestBogoSuite is NOT “external infrastructure” — it is a fixable test-host defect
The previous entry recorded it as “an external binary … not a conversion signal”. Measured, that is
wrong. Go passes it on this host, so BoGo downloads, builds and runs here. It drives the shim as
-shim-path=os.Args[0] -shim-extra-flags=-bogo-mode, i.e. it re-executes the test binary itself.
Run the converted host that way and it answers:
> tlsendHost.exe -bogo-mode
flag provided but not defined: -bogo-mode
bogo-mode IS registered by the converted source (handshake_test.cs:50, a flag.Bool package-var
initializer) — but testing’s host parses args itself in TestOptions.Parse and throws on any
name it does not recognize, before the package’s own flag.Parse() in TestMain ever runs. Go’s
test binary cannot behave that way: testing.Init() merely DEFINES the -test.* flags on
flag.CommandLine, and one flag.Parse() then covers the host’s flags and the package’s together.
TestFlagBridge already does the first half (publishing the host’s flags into flag.CommandLine); the
missing half is tolerating the package’s. So every BoGo case fails instantly, the runner exits 1
without writing results, and the test reports bogo failed: exit status 1.
Sizing it honestly: fixing the pass-through is small and general (any package with its own test flags
benefits), but it lives in the shared hand-owned testing host, so it owes the full validated
sweep as a gate — and it must defer the unknown-flag ERROR to flag.Parse() rather than dropping
it, or the host stops rejecting typos. And even fixed, TestBogoSuite becomes a long external TLS
interop measurement, not a pass: it is its own arc. Deliberately not taken here.
One more thing the bogo failure printed, unrelated and worth a look
t.Fatalf("bogo failed: %s\n%s", err, out) printed the second operand as 0x19c45d7c4f8. out is a
*strings.Builder, whose pointer-receiver String() makes it a fmt.Stringer in Go; the converted
fmt reached its p.arg._<Stringer> assertion and missed. Reproducible on every run of that test.
Whether this is general (any %s/%v on a pointer whose String() was never cast to Stringer in
converted source) or specific to strings.Builder is UNMEASURED — recorded with its exact site so
whoever looks does not have to find one.
Where crypto/tls stands
Four divergences, four different owners, and only one of them is crypto/tls’s to answer. Two
are Windows syscall arcs that other packages want more than this one does (net’s interfaces,
crypto/x509’s system verifier); one is a testing-host gap; one is a disclosure. Nothing here is a
TLS defect — the protocol work has been green since the sha3 fix, TLS 1.2 and 1.3, QUIC, session
tickets, ECH and the whole handshake matrix. The expired-fixture ceiling deserves repeating for
whoever eventually banks it: 180 of 184 is the most this host can score, both languages fail the same
four rows, and it worsens with time — a Go patch release or regenerated fixtures changes the shape,
and a proof page written today must say so.
Still builds-and-partly-runs; no roster row, no proof page, no disclosures, converted test sources not committed.
⛔ encoding/json — the wall is down and the package RUNS: 400 of 491, four roots (2026-08-16, lanes claude/json-measure + claude/json-unlock)
First measurement of the package. The board’s prior rows — encoding/json | 491 | CS0050
inconsistent accessibility on a test-local return type and encoding/json 0 of 491 (CS0050/CS0053)
— reproduce exactly, and the diagnosis behind them was right about the symptom and one word off
about the cause: the offending types are not “test-local”, they are function-local, and that
distinction is the whole fix.
The wall: 76 errors, four codes, one cause — CLOSED
| Code | Count | Shape |
|---|---|---|
| CS0053 | 35 | property type less accessible than property |
| CS0050 | 20 | return type less accessible than method (the generated Ꮡ-accessors) |
| CS0051 | 19 | parameter type less accessible than method (the generated constructors) |
| CS0052 | 2 | field type less accessible than field |
A type declared inside a function body has no Go exportedness. The export convention governs
PACKAGE-LEVEL identifiers; a function-local S8 is exactly as unreachable from outside its function
as embed2 is, and Go draws no distinction between them. go2cs hoists both to package scope as
<Func>_<name>, and at that point an accessibility rule reads a case out of the name — by either of
two routes, which is why the failure looked like several defects:
- the converter’s bridge arm (
visitTypeSpec, undertestInlineTypeAccess) askedgeneratedTypeScopefor the LOCAL name, so the siblings one function declares split public/internal —[GoLocalName("S8")] public partial struct TestUnmarshalEmbeddedUnexported_S8holding a field of[GoLocalName("embed2")] internal partial struct TestUnmarshalEmbeddedUnexported_embed2; - a lifted ANONYMOUS struct carries no modifier at all, so go2cs-gen’s own rule read the
HOISTED name and inherited the case of the enclosing function —
TestEncoderSetEscapeHTML_typeis public because theTest…function is, and its exported fields over the package-level unexportedstrMarshaler/strPtrMarshalerare the two CS0052.
Fix (landed): a function-local type is emitted internal, always — localTypeAccess in
typeAccessibilityOperations.go, consumed at the three finalization points (visitTypeSpec’s bridge
arm, visitStructType’s and visitInterfaceType’s lift defaults). internal is both faithful and
sufficient: no Go consumer outside the function can name the type, and every emitted C# consumer
compiles into the same test assembly. Writing it inline is what makes the generator follow —
measured, not assumed: the generator reproduces a modifier the declaration already carries
(internal partial struct TestUnmarshalEmbeddedUnexported_embed2 appears verbatim in its output) and
falls back to its name rule only for a bare one. Guarded by TestFunctionLocalTypesShareOneAccessibility
(converter go test), which pins all three shapes and fails without the fix (A/B verified —
TestLocals_S8 reverts to public).
Deliberately scoped to the bridge. The production path leaves the modifier empty and lets
recordTypeAccessibility pin generatedTypeScope of the mangled name, which gives every local
type of one function the SAME modifier — uniform, and consistent for that reason rather than by
design. ⚠ The identical latent mixture exists in production (a function-local struct with an
exported field of a package-level unexported type) and no corpus package exhibits it today; flipping
production local types to internal would also move a public value adapter’s operand out from under
it. Measured and left, not changed speculatively — if a production package ever presents the shape,
this is its root.
Behind it: two roots, 8 errors, and the package still does not run
Re-running the pipeline on the fixed converter took the wall from 76 to 8, in two families that
were masked, not caused — renamedByte, strMarshaler and strPtrMarshaler are package-level
types the fix does not touch, and they error now for the first time.
R2 — a hoisted function-local type’s EMBEDDED-field member keeps the hoisted name, while use sites
spell the Go field name (CS1061 ×2, CS1739 ×1). The converter is internally inconsistent about one
name. type S struct{ *myInt } inside TestAnonymousFields emits the member as
TestAnonymousFields_myIntᴛ1 (declaration and go2cs-gen’s promotion and constructor), while the
converted body says s.myInt and the composite literal says
new TestUnmarshalEmbeddedUnexported_S3(embed1: …). The Go field name of an embedded *myInt is
myInt, and [GoLocalName] already records it for %T — the promotion simply does not use it.
Naming the member by the Go local name is collision-free (it lives inside the struct), but the fix
spans the converter’s embedded-field emission and go2cs-gen’s promotion generator, so it is its
own arc.
R3 — a string↔byte-slice conversion loses its footing when either side is a DEFINED type (CS1503 ×5), in two directions of one idea:
| Direction | Emitted | Sites |
|---|---|---|
[]byte(namedString) |
slice<byte>(v) where v is [GoType("@string")] |
marshaledValue, strMarshaler, strPtrMarshaler |
[]NamedByte(string) |
slice<TestSliceOfCustomByte_Uint8>("hello"), slice<renamedByte>((@string)"abc"u8) |
TestSliceOfCustomByte, TestEncodeRenamedByteSlice |
Overload resolution lands on slice<T>(T[]) and reports cannot convert from '…strMarshaler' to
'byte[]'. golib’s string constructor is reachable only for the exact @string → slice<byte>
pairing; a named type on either side is not routed through it. The emitted FORM is not the
problem — positive control: the identical slice<byte>(s) conversion appears in the banked suites
of archive/zip, bytes and compress/zlib and compiles there, because the operand is a plain
@string. What is missing is the unwrap of a DEFINED type to its underlying at the conversion site
(or the participation of the generated implicit conversion in overload resolution) — a
golib/converter boundary question that wants a measurement of its own. The shape is ordinary Go, so
it is very likely NOT confined to this package.
Where encoding/json stands
0 of 491, still compile-blocked — but the block is now 8 errors in two named roots rather than 76
in one, and the converter fix that closed the first is general (any package whose tests declare types
inside a function body). No roster row, no proof page, no disclosures, converted test sources not
committed. The suite converts cleanly end to end — every _test.go file emits, and the production
encoding.json.dll builds — so the remaining distance is exactly R2 and R3, and neither is a
reflection-bridge question. Nothing here touches the descriptor/assignability semantics the
claude/assignableto-arc lane owns; no cross-reference is owed.
UPDATE 2026-08-16 (lane claude/json-unlock) — both roots closed; FIRST measurement: 400 of 491
R2 and R3 are fixed, the test half compiles with 0 errors, and encoding/json ran for the first
time. It does not validate: 400 of 491 verdicts match, 91 diverge. The reflection-heavy body is
now measured rather than guessed at, and the divergences cluster the way this week’s other packages
did — four roots and one loose end, none of them an emission defect.
R2 — an embedded field is named by GO, not by the C# rendering of its type. visitStructType
derived the member name from the rendered type, which coincides with the Go field name for every
ordinary embed and parts from it the moment the converter RENAMES the type: a function-local type
myInt int hoists to TestAnonymousFields_myIntᴛ1, and the declaration (plus go2cs-gen’s generated
constructor and promotion accessor, both read off it) then spelled that while every use site spelled
s.myInt / S3{embed1: …}. The name now comes from the Go OBJECT the embed resolves to — the field
*types.Var for a same-package embed, the embedded type’s TypeName for a selector embed — which is
the Go field name by definition and is already unqualified and type-argument-free, so it REPLACES the
bracket/dot stripping rather than adding to it. It also settles the field’s EXPORTEDNESS, which the
hoisted name silently flipped (embed1 is unexported; the TestUnmarshalEmbeddedUnexported_ prefix
made the member public — the opposite of what that test asserts). The generator followed: its
promoted-struct accessor scoped by TYPE name where every sibling accessor already scoped by MEMBER
name, so the corrected declaration met the opposite modifier (CS8799). Guarded by LiftedLocalTypes
(extended), proven failing-first.
R3 — a string ↔ byte/rune-slice conversion with a DEFINED type on either end. Two ends, two
remedies, one rule: the STRING end spells the (@string) hop explicitly (a [GoType("@string")]
wrapper needs wrapper→@string→byte[], two user-defined conversions, and C# chains at most one);
the ELEMENT end projects element-wise through the wrapper’s own operator with golib’s widen
(slice<byte> and slice<myByte> are unrelated instantiations with NO conversion between them).
Go’s string↔slice conversion always materializes fresh storage, so the element-wise copy is its cost
model, not a concession. Corpus census (type-aware, go/packages over all of std with tests):
FIVE sites in the entire Go 1.23.1 standard library, production and test, all in encoding/json’s
own suite — which is exactly why the corpus compiled clean without them; the string([]myByte)
direction has zero stdlib sites and is emitted by the same rule. Zero committed corpus files move.
A pre-existing sibling hole closed with it: a named byte-slice type converted from a string VARIABLE
(plainByteSlice(s)) was a bare cast, CS0030 — only the LITERAL form had ever been fixed. Guarded by
the new DefinedElemStringConversion, proven failing-first (23 errors without the fix).
The 91 divergences
| # | Root | Verdicts | Evidence |
|---|---|---|---|
| A | An embedded field is invisible to the reflection view as an EMBED — reflect does not report the field as anonymous, so encoding/json never flattens it |
≈31 | every embed marshals as a NAMED object instead of promoting: {"S1":{"X":2},"S2":{"X":4}} for want {}, {"S":"B","BugA":{"S":"A"}} for want {"S":"B"}; DisallowUnknownFields then reports unknown field "Level1b" where Go reports "extra". Covers TestAnonymousFields, TestUnmarshalEmbeddedUnexported, TestEmbeddedBug, TestDuplicatedFieldDisappears, TestTaggedFieldDominates, TestMarshalEmbeds, TestUnmarshal/#56–#63/#89/#91/#93/#139/#140. R2 was the naming PREREQUISITE for this; the anonymous FLAG is a separate seam |
| B | reflect.DeepEqual(ptr, reflect.New(T).Interface()) is false for a fresh zero |
≈41 | TestUnmarshal’s own precondition — unmarshalTest.ptr %#v is not a pointer to a zero value — fires before the subtest’s real assertion runs, so these 40-odd verdicts are UNREACHED rather than wrong. One fix would re-open them all |
| C | The reflection view of a slice whose ELEMENT or whose SLICE TYPE is DEFINED | 3 | reflect.Value.Bytes() (core/reflect/value_impl.cs:616) ends in (slice<byte>)other! and throws InvalidCastException for slice<Uint8> / renamedRenamedByteSlice (TestSliceOfCustomByte, TestEncodeRenamedByteSlice, both surfacing as infrastructure-error); TestByteKind instead marshals a local type byteKind []byte as a generic array, so its Kind is not seen either. ⚠ Note the asymmetry with R3: Go’s Value.Bytes() ALIASES the storage, so this fix must project a view, not a copy |
| D | Cycle detection recurses forever → StackOverflowException kills the host |
13 unrecorded | TestUnsupportedValues marshals a self-referential map; Go returns UnsupportedValueError: encountered a cycle, the converted encoder recurses interfaceEncoder→mapEncoder without bound and the process dies with 0xc00000fd. Per the jsonrpc rule this is ONE failure plus an unmeasured tail: TestValid ×7, TestUnsupportedValues ×4, TestIndentBig, TestMarshalFloat recorded no verdict at all |
| E | A recovered panic value’s boxed type | 2 | TestMarshalPanic/TestUnmarshalPanic panic with 0xdead and compare reflect.DeepEqual(recover(), 0xdead); the C# side reports the right VALUE (57005) and still fails the compare — the untyped-int panic argument boxes at a different width than the comparand |
| — | loose end | 1 | TestLargeByteSlice round-trips a PLAIN 2000-byte []byte and diverges at byte 0. No defined type anywhere in it, so it is not root C; unclassified, and worth one look because it is the only base64 round-trip failure |
Roots A, B, C and E are all reflection-bridge seams; none is a converter emission defect. Root A
is the biggest single unlock (a third of the divergences) and it is the classic Go-embedding contract
that encoding/json, encoding/xml, encoding/gob and text/template all read. ⚠ Root B is
adjacent to the descriptor semantics claude/assignableto-arc owns — cross-referenced, deliberately
not touched here.
No roster row, no proof page, no disclosures, converted test sources NOT committed: 400 of 491 is a measurement, not a validation.
Corpus footprint of both fixes: zero. A seeded stdlib reconvert on the fixed converter (304
packages, 0 failed, 13m32s; marker gate 60 marked / 0 clobbered) emitted 1,681 artifacts, of which
1,629 are byte-identical to the committed tree and 51 of the remaining 52 are the documented CRLF
phantom (identical once CRs are stripped). ⚠ The one REAL difference is not from this lane and is
owed by another: vendor/golang.org/x/text/unicode/bidi/core.cs emits ((level)(maxDepth + 2))
where the committed file has ((level)maxDepth + 2) — the residue of the cast-precedence fix that
landed in claude/fmt-roots (93ef5abaa, guarded by NamedConstConversionPrecedence) without its
corpus regen. It is the arc’s ONLY stdlib site; whoever next levels the corpus should expect exactly
that one file.
✅ fmt BANKS at 63/63 · ⛔ encoding/json reaches 482 of 491 — the REFLECT-BRIDGE CLOSURE arc (2026-08-17, lane claude/reflect-closure)
The union of the roots holding encoding/json (400/491) and fmt (62/63) turned out to be one
subsystem with nine roots, and they came out in dependency order exactly as the brief predicted:
descriptor cargo first, then the value operations, then the consumers. Two of the nine were on no
list at all — each stood behind one that was, which is this board’s recurring shape.
| Package | Before | After |
|---|---|---|
fmt |
62 of 63 | 63 of 63 — BANKED, roster row 149 |
encoding/json |
400 of 491, 13 unrecorded (process death) | 482 of 491, 0 unrecorded |
encoding/json’s whole suite now runs in 24 s, where before it either exhausted a 20-minute
deadline or died of stack exhaustion partway through.
The nine roots
| # | Root | Where | Verdicts |
|---|---|---|---|
| 1 | StructField.Anonymous was never populated, so no embed was an embed to any Go encoder |
reflect/value_impl.cs |
~31 |
| 2 | An embed’s [GoTag] sits on the emitted partial PROPERTY while the backing FIELD is generated — so every embedded field read as untagged |
GoReflect.FieldAccess.cs |
with #1 |
| 3 | new(T) ran golib’s ALLOCATING parameterless constructor, so new([]T) / new(map[K]V) pointed at a non-nil EMPTY container where Go points at nil |
builtin.cs |
~41 |
| 3b | A POINTER descriptor never carried its POINTEE’s array dims, so reflect.New over TypeOf(new([3]int)).Elem() allocated a ZERO-length array |
internal/abi/type_impl.cs, GoReflect.TypeLayout.cs |
with #3 |
| 4 | Three identity rules were each too FINE for a cycle detector: unsafe.Pointer compared by BOX, a map/slice UnsafePointer tokened the HEADER copy, and a struct’s generated Equals compared an interface field with C# == |
unsafe.cs, golib/ж.cs, reflect/value_impl.cs, go2cs-gen |
13 + the process |
| 5 | Value.Bytes threw for a DEFINED byte element, and Value.SetBytes wrote through the never-populated Go data word — silently, for EVERY byte slice |
reflect/value_impl.cs, GoReflect.ValueMarshalling.cs, slice.cs |
3 + 1 (fmt) |
| 6 | An unnamed FUNC type had no Go rendering at all — GoReflect.TypeNaming had no delegate arm |
GoReflect.TypeNaming.cs |
1 (fmt) |
| 7 | A COMPLEX constant expression was emitted as RUNTIME arithmetic, and .NET’s double - Complex negates +0 |
converter, convBinaryExpr.go |
1 (fmt) |
| 8 | (on no list) An unexported EMBED took flagStickyRO where Go gives it flagEmbedRO — only the sticky bit inherits, so every field promoted through an unexported embed was read-only |
reflect/value_impl.cs |
7 |
| 9 | (on no list) rtype.FieldByName searched DIRECT fields only, so a promoted name read as ABSENT — and Value.FieldByName then handed the EMPTY index to FieldByIndex, which answers the struct ITSELF |
reflect/value_impl.cs |
with #8 |
Two more surfaced as consequences of the fixes above and are closed with them: a NaN map key
compared equal to itself (BCL collection semantics, not Go’s ==) — fmt’s own %v of
map[float64]int{NaN:1, NaN:1} reads it back — and a variadic tail was detected by the delegate
FAMILY NAME, which C#’s natural delegate type does not carry, so func(string, ...int) reported
non-variadic and rendered func(string, Span'1).
Root 4 is the one worth reading twice. Go’s cycle detectors are written on identity —
e.ptrSeen[v.Interface()] for a pointer, [v.UnsafePointer()] for a map, and
[struct{ptr any; len int}] for a slice — and each of the three keys took a different route to
being unfindable. The third is a go2cs-gen defect with nothing to do with reflection: the
generated memberwise Equals compared an INTERFACE-typed member with C# ==, i.e. by reference,
where Go compares interface values by dynamic type and value. Since a struct’s Equals is also what
a map LOOKUP calls, such a struct could never be found under a key it had itself stored. The
consequence was not a wrong answer but a dead process: Marshal of a self-referential value recursed
until 0xc00000fd, which is uncatchable and took every verdict the run had not yet produced with it.
Doctrine: ConversionStrategies-Reference.md gained seven sections — An EMBEDDED field is an embed
to reflect, An UNNAMED func type renders STRUCTURALLY, reflect.Value.Bytes/SetBytes are
defined over the element KIND, and they ALIAS, new(T) is Go’s ZERO value, An unsafe.Pointer
is compared BY ADDRESS, A NaN map key is never equal to anything, and A COMPLEX constant
expression must be FOLDED.
Where encoding/json still stands: 482 of 491, ONE root, and it is NOT rooted
TestUnmarshal rows #56–#63 — eight subtests of one table — plus the aggregate row. Each decodes
into the 40-field All fixture and compares the result against the table’s literal with
reflect.DeepEqual. gotJSON and wantJSON are byte-identical, so the decode itself is right;
the only structural difference the failure message shows is one element:
got … [0x… <nil> 0x…] … // SliceP, the decoded side
want … [0x… 0x…] … // SliceP, the table literal — middle element prints EMPTY
allValue.SliceP is []*Small{{Tag:"tag22"}, nil, {Tag:"tag23"}}, emitted as
new ж<Small>[]{Ꮡ(…), default!, Ꮡ(…)}.slice(), so its middle element is a C# null where the
decoder writes the canonical typed-nil box. That is the obvious hypothesis and it is measured
WRONG: a minimal reproduction of exactly that shape — a nil hole in a []*T, built once as a
literal and once through reflect.MakeSlice into a struct field, compared with DeepEqual and
printed with %v — agrees with go run on both counts, and it is committed as the
ReflectBridgeClosure behavioral test’s nil element / decoded vs literal rows. Normalizing a
null pointer read to the canonical nil at Value.live was tried, moved nothing, and was
reverted rather than banked as speculative machinery.
So the root is something in the larger value, not the nil encoding alone. The next lane should start
by finding which of All’s forty fields DeepEqual actually separates — %v of the whole struct is
too lossy to say — and the cheapest instrument is a hand-written probe that walks the two values
field by field with reflect.DeepEqual rather than the test’s single top-level call.
No roster row, no proof page, no disclosures, converted test sources NOT committed: 482 of 491 is a measurement, not a validation.
Adjacent, boarded rather than taken
- A conversion of
nilto a defined METHODLESS func type does not compile.handler(nil)emitsnew Func<nint, error>(default!)— CS8716 + CS0149. It is converter emission work, outside this lane’s boundary by the brief’s own rule; theReflectBridgeClosuretest writes a declared value instead of the conversion so it fails on the naming rule alone. - A defined METHODLESS func type has no managed identity — the converter renders it inline as its
base delegate family — so
reflectreports it as UNNAMED. A defined func type carrying a method does get its own delegate and keeps its name. Same shape as theChanDirruling: the bridge describes the type it can actually build a descriptor for. - Projected field ORDER puts every embed last.
go2cs-genemits the promoted-embed backing field in a generated partial, after the declaring part’s plain fields, soHost{X; y; Inner; inner; Ptr}projects asX, y, Ptr, Inner, inner. No measured consumer observes it — json’s dominance rules read depth and tag, and its one order-sensitive test declares its single plain field first — so it is recorded rather than fixed; the remedy is declaration-order cargo, not a re-sort. The behavioral guard looks its fields up BY NAME so it cannot pin the gap as a contract. - A NAMED float type’s wrapper, and a struct or array CONTAINING a float, still compare a NaN map key by the BCL rule. Covering them would mean routing every struct-keyed map through the reflective relation, and no measured consumer reaches them.
Corpus footprint, measured
A seeded whole-stdlib reconvert (304 packages, 0 failed, 9m00s; marker gate 60 marked / 0
clobbered, full-file line-anchored scan) emitted 1,664 artifacts, of which 1,609 are byte-identical
and 49 of the remaining 55 are the documented CRLF phantom. Six real differences, all owed:
encoding/gob/{encode,enc_helpers}.cs (the complex fold renders 0+0i as the bare 0D.i() —
value-identical, Go’s own spelling), encoding/json/{decode,encode}.cs (restored to the clean
emission after this lane’s own pipeline runs), fmt/README.md (the validation badge fmt earns by
banking), and vendor/golang.org/x/text/unicode/bidi/core.cs — the debt the claude/json-unlock
entry above predicted by name, the residue of claude/fmt-roots’ cast-precedence fix landing
without its corpus regen. It is paid here. reflect/value.cs also moves, by one Go body replaced with
a placeholder (Value.SetBytes joins the hand-owns).
⚠ A caution for the next regen census: scan the WHOLE file for the hand-own marker. A first pass here read only each file’s first 40 lines and reported 35 marked files against the real 60, which would have made the clobber gate vacuous for 25 hand-owns. The rule is already written as line-anchored; it must also be whole-file.
✅ encoding/json BANKS at 491/491 — ONE root, and it was a map ENTRY read without its type (2026-08-17, lane claude/json-lastroot)
The entry above left encoding/json at 482 of 491 with the remainder unrooted: TestUnmarshal
rows #56–#63 plus the aggregate, all comparing a decoded 40-field All fixture against the table
literal with one top-level reflect.DeepEqual, and Marshal of both sides producing byte-identical
JSON. It also left the right next step — find which of All’s forty fields DeepEqual separates —
and that instrument cost about two minutes and answered on the first run.
| Package | Before | After |
|---|---|---|
encoding/json |
482 of 491 | 491 of 491 — BANKED, roster row 150 |
491 matched, 0 disclosed, 0 skipped, 41 Benchmark/Example/Fuzz declarations excluded under the
standard Phase-4D deferral. The whole suite runs in ~24 s, so it needs no $longTimeouts floor.
The leaf, and the root behind it
A field-by-field probe — walk both structs with reflect, DeepEqual each field, recurse, print the
first path that separates — named All.MapP immediately, and named it in the informative way:
DIFF All.MapP: DeepEqual FALSE but every child compares equal (kind map)
All.MapP is map[string]*Small{"19": {Tag:"tag19"}, "20": nil}. Reduced, the divergence is four
lines and does not involve encoding/json at all — it needs only that ONE side was written through
reflect and the other declared as a literal:
| two literals | equal ✓ |
| two decoded | equal ✓ |
| decoded vs literal | false ✗ |
Root: deepValueEqual’s map arm built each entry’s Value from the STORED OBJECT, not from the
map’s declared element type. Every other read in the bridge is slot-derived — Field, Index,
Elem, MapIndex, MapIter.Value all type by the declaring slot — but the DeepEqual map arm walks
the backing Dictionary directly (it must: golib keeps a nil KEY in a side slot no iterator can see)
and used makeReflectValue. A slot physically holding C# null therefore came back as the invalid
zero Value instead of a valid nil pointer. The two sides spell nil differently and always have — a
literal’s nil element is stored as raw null, while a reflective write stores the canonical
ж<T>.NilBox that packInterfaceValue needs so a typed nil survives being handed out as an
interface — so one side read valid and the other invalid, and the map compared unequal. The same
blindness made a nil element compare EQUAL to a missing key (both invalid), which is the more
alarming half and had no consumer.
The fix is mapElemValue: type each entry by Elem() of the map’s own type, exactly as MapIndex
does. Two nil elements then meet at the kind’s nil rule; a missing key still fails on Contains; a
nil element still separates from a present non-nil one. 13 lines in reflect/deepequal_impl.cs,
no converter change, no golib change.
Why the visible-difference hypothesis missed it. The previous lane read the failure text, saw
SliceP’s nil element print differently, reduced exactly that shape, and measured it AGREEING — a
correct measurement of the wrong object. SliceP is a slice, and the slice arm has always used
v1.Index(i), which is typed. Only the map arm was blind. The lesson is the one the entry above
already wrote down and this lane simply executed: when %v is too lossy, bisect with a probe rather
than reason from the rendering.
And the signature is reusable. A container reporting unequal while every one of its elements
compares equal is what a lost slot type looks like from outside, because a walk that re-boxes each
element through Interface() re-enters the dynamic path on both sides and cannot see the difference.
The identical discrepancy named this function’s FUNC arm on 2026-07-31 (compress/flate’s
TestWriterReset, ten levels failing while every field matched). Two arms of one function, four
months apart, found by the same tell.
What the bank proves
encoding/json is not the roster’s biggest suite by verdict count — at 491 it is sixth, behind
go/doc/comment (10,059), go/internal/gcimporter (583), crypto/rsa (559), go/types (557) and
internal/zstd (536) — and the claim worth making is a different one: it is by some distance the
most reflection-dense suite on the roster. Nearly every row of it is a
reflection walk checked against Go’s own answer: struct-tag naming, embedding under Go’s full
dominance rules, Marshaler/Unmarshaler/TextMarshaler dispatch at depth, MakeMap/MakeSlice/
SetMapIndex writes into every container kind, cycle detection keyed on UnsafePointer, and
DeepEqual over the results. The reflect-closure arc alone rooted nine defects, seven of them
credited to encoding/json verdicts, and this lane’s map-element root is the tenth and last.
Banking it means the managed reflect is no longer assumed adequate for the encoding packages —
it is differentially proven against the standard library’s own hardest consumer of it.
Roster: 150 / 215 (69.8%), 17,264 matching verdicts, 76 disclosed — header recomputed from the table itself.
Carried forward, unchanged
Everything the entry above boarded rather than took is still open and still unrooted-by-choice: the
handler(nil) methodless-func conversion (converter emission), a methodless defined func type having
no managed identity, projected field ORDER putting every embed last, and the NaN-in-a-composite map
key. None of them is reachable from any measured consumer, and this arc found no new one behind the
map-element root — the census is honest at one root, not one root plus a remainder.
⛔ The **T OUT-PARAMETER class is CLOSED at the boundary — and crypto/x509 is walled twice more behind it (2026-08-17, lane claude/x509-cryptoapi)
The entry above censused a second syscall class at thirteen wrappers and named its root: ж<T> →
uintptr answers 0 for a ж<ж<T>> whose held pointer is still null, so an out-parameter tells
Windows “no output wanted”. That reading is right and incomplete, and the missing half decides the
fix layer. Measured directly against golib on current master:
| the box | IsNilPointer |
IsNull |
(uintptr) |
|---|---|---|---|
&p before the call (held pointer null) |
false | true | 0 |
&p after anything fills it (held pointer non-null) |
false | false | a live MANAGED address |
So the operator has two wrong answers here, and the second is far worse than the first: a real
address of a slot the collector reads as an OBJECT REFERENCE, handed to a kernel that writes eight
raw bytes into it. The board already suspected as much (“the 0 is accidentally the SAFER outcome”);
this measures it. EnsureStableAddress does not even pin such a slot — GCHandle pins only
reference-free storage — so the address is transient as well as wrong.
The layer, decided by measurement rather than by preference
Nothing golib can return would serve the boundary. The kernel’s representation is eight raw
bytes; the managed one is an object reference; no single address is both. Reconciling them needs a
SYNC POINT — the moment the raw word becomes a pointer box — and that moment is “after the call
returns”, which only the wrapper knows. The remedy is therefore per-wrapper, but the mechanism is
one shape for the whole class: a native cell local to the call, its address handed over, and a
publish through ValueSlot afterwards (never Value, whose nil guard value-peeks and would panic
on the very write that fills the slot in). A zero report publishes the nil pointer with no special
case, because ж<T>’s native constructor already treats address 0 as nil.
ж.cs is unchanged. The operator’s answer is correct for the case it was written for, and the
contract is now pinned from the other side by two GolibTests cases
(AddressOfAPointerToPointerIsNotAKernelWritableSlot,
PublishingANativeAddressThroughValueSlotIsTheBoundaryRemedy) so a future lane cannot “fix” it into
the dangerous answer.
Class disposition: 13 wrappers, 5 taken, 8 left for stated reasons
The census re-measures at 13, and one attribution on it is wrong: NetGetJoinInformation and
NetUserGetInfo are in syscall, not internal/syscall/windows — so the split is 11 + 2, not
9 + 4. Taken (syscall/windows/zsyscall_windows_ptrout_impl.cs, five entries in
manualConversionFuncs):
| Wrapper | Out-param | Why taken |
|---|---|---|
ConvertSidToStringSid |
**uint16 |
SID.String(); half of a round trip |
ConvertStringSidToSid |
**SID |
StringToSid; SID is Go’s EMPTY struct — an opaque handle nothing reads through — so a native box is not merely safe but exactly right |
NetGetJoinInformation |
**uint16 |
a THIRD DLL (netapi32) with a different free routine (NetApiBufferFree) — what makes the guard evidence for a CLASS rather than one advapi32 accident |
CertAddCertificateContextToStore |
**CertContext |
crypto/x509’s measured consumer |
CertGetCertificateChain |
**CertChainContext |
crypto/x509’s measured consumer |
Left, each for a reason rather than for lack of effort: DnsQuery / _DnsQuery (**DNSRecord)
— the pointee is a LINKED native chain whose converted record holds managed references, so
publishing the address alone would replace a silent nil with a fabricated-reference landmine; it
wants the whole-chain transcription zsyscall_windows_addrinfo_impl.cs gives ADDRINFOW, in a net
DNS arc. getQueuedCompletionStatus / GetQueuedCompletionStatus (**Overlapped) — an
OVERLAPPED’s identity belongs to the netpoll arc’s per-operation record, and a bare native box would
mint an identity that arc does not know. GetFullPathName, NetUserGetInfo, and
internal/syscall/windows’ CreateEnvironmentBlock / NetUserGetLocalGroups — the same safe
shape, no corpus consumer, therefore no value-level proof available (Go’s own syscall.FullPath
passes nil for fname, so even its one caller does not exercise it).
The guard, and what failing-first actually printed
New behavioral output test PointerOutParameter: the four well-known SIDs round-tripped
string→*SID→string, a malformed SID that must still be REJECTED, a stability check, and
NetGetJoinInformation whose returned buffer is WALKED to its NUL rather than merely tested for nil
— reading through the published pointer is what proves the address is the one Windows wrote.
Proven failing-first by neutering the cell address. The output is worth recording because it is not a crash:
StringToSid error: The parameter is incorrect. (x4)
malformed SID rejected: true
stable: true false <- silently wrong, and self-consistent
NetGetJoinInformation error: The parameter is incorrect.
exit=0
advapi32 REQUIRES its out-parameter (unlike ppStoreContext, which is documented OPTIONAL), so the
defect surfaces there as a plausible-looking ERROR_INVALID_PARAMETER rather than as silence — and
stable: true false is the quiet-wrong-answer shape this class’s history keeps warning about: two
empty strings agreeing with each other and with nothing else.
crypto/x509 census: the suite cannot be measured, and the verifier is walled twice
The pipeline does not reach a verdict. go2cs -tests -test-action all converts every _test.go
file cleanly and the production assembly builds, but the test host fails to compile with 5 errors
in 3 roots — far smaller than the board’s older CS0102 duplicate definition row implies, and none
of them about CryptoAPI:
| Root | Errors | What it is |
|---|---|---|
| A name minted by the TEST half collides with one the PRODUCTION half already emitted | CS0111 ×2 | x509.go and x509_test.go both blank-import crypto/sha256 and crypto/sha512; each emits initᴛᴛblankImportꓸcryptoꓸsha256 into the same partial class. General to any package whose tests repeat a production blank import |
| The same collision through the BLANK-IDENTIFIER counter | CS0102 ×1 | pem_decrypt.cs has _ᴛ1ʗ (a blank const in an iota block) and oid_test.cs re-mints _ᴛ1ʗ for var _ encoding.BinaryMarshaler = OID{} — the counter restarts for the test half |
hash_package unresolved in the PRODUCTION files under the tests closure |
CS0246 ×2 | the tests csproj sets DisableTransitiveProjectReferences, and hash reaches x509.cs / pem_decrypt.cs only TRANSITIVELY (via crypto/sha256); the production csproj does not reference it either and does not need to |
All three are -tests emission defects, all three are general, and together they are what stands
between crypto/x509 and any operational number at all. Deliberately not taken here — they are a
test-pipeline arc, not a CryptoAPI one.
So the verifier was measured directly instead, with an offline probe: a self-signed ECDSA leaf,
Verify with Roots == nil (which on Windows routes through systemVerify). Go reaches
UnknownAuthorityError immediately. The converted program, with this lane’s fix:
created der: true
parsed cn: go2cs.example
<hangs>
It no longer nil-dereferences at (*storeCtx).Store — that defect is gone — and dotnet-stack
names the new wall exactly:
[Native Frames]
syscall!go.syscall_package.Syscall9(...)
syscall!go.syscall_package.CertGetCertificateChain(...)
crypto.x509!go.crypto.x509_package.systemVerify(...)
Blocked INSIDE the kernel call, ~1.7 s of CPU across minutes of wall time. The mechanism is the
OTHER, established class: CertChainPara is handed to the kernel BY ADDRESS while holding
RequestedUsage.Usage.UsageIdentifiers as ж<ж<byte>> and CacheResync as ж<Filetime> — managed
references — and systemVerify writes para.Size = 80, the NATIVE size, into a much smaller managed
object. Every field past the first therefore reads from the wrong offset, dwUrlRetrievalTimeout
among them, which is a blocking network budget. ((~storeCtx).Store, passed as additionalStore in
the same call, is read at a wrong managed offset for the same reason.)
The full remaining CryptoAPI wall, so the next lane needs no census:
| Direction | Structs | Reference-bearing fields |
|---|---|---|
| PASSED by address (kernel READS) | CertChainPara, CertChainPolicyPara, SSLExtraCertChainPolicyPara, CertChainPolicyStatus |
ж<ж<byte>> UsageIdentifiers, ж<Filetime> CacheResync, Pointer ExtraPolicyPara, ж<uint16> ServerName, Pointer ExtraPolicyStatus |
| READ BACK through raw addresses (kernel WROTE) | CertContext, CertChainContext, CertSimpleChain, CertChainElement, CertRevocationInfo |
ж<byte> EncodedCert, ж<CertInfo>, ж<ж<CertSimpleChain>> Chains, ж<ж<CertChainContext>> LowerQualityChains, ж<ж<CertChainElement>> Elements, ж<CertContext>, ж<CertEnhKeyUsage> ×2, … |
⚠ The read-back half cannot be answered the way GetAddrInfoW was. That hand-own transcribes the
whole chain into managed boxes and makes the free a NO-OP, which works because nothing native has to
survive the call. Here CertVerifyCertificateChainPolicy and CertFreeCertificateChain both need
the ORIGINAL native pointer back, so any remedy needs a DUAL identity — a managed view that still
remembers its native address. That is the arc, and it is not a small one.
Adjacent, found while guarding: (*[N]T)(unsafe.Pointer(p))[:] over a NATIVE pointer is the fabricated-reference fork again
The guard’s first draft read NetGetJoinInformation’s result with Go’s classic pre-unsafe.Slice
idiom and died:
Fatal error. System.AccessViolationException
at go.slice`1[UInt16]..ctor(UInt16[], IntPtr, IntPtr, IntPtr)
at go.array`1[UInt16].Slice(Int32, Int32)
array<T>.AliasPointer windows a real backing array when the pointer HAS managed element storage,
and otherwise falls to (ж<array<T>>)(uintptr)element — a native-address box over array<T>, which
is itself reference-bearing. Dereferencing it fabricates a T[] reference out of address bytes.
Live corpus sites over genuinely native pointers: net/windows/lookup_windows.cs lines 395, 465
and 500 (the DNS answer walk — SRV Target, NS Host, TXT StringArray) and reflect/type.cs:1887
(t.t.GCData). The syscall/internal/syscall/windows uses are over MANAGED element pointers and
are fine. Note where the DNS three sit: directly behind the DnsQuery member this lane deferred, so
that arc owns two walls, not one.
Cross-package effect on crypto/tls, recorded rather than measured
TestQUICHandshakeError — the one crypto/tls divergence rooted in this class — is expected to
still fail, because the nil it dies on is only the first of three defects on that path; the chain
para and the chain walk stand behind it. TestVerifyHostname is likewise still expected to fail on
net.adapterAddresses, which this lane does not touch. The full tls census was deliberately NOT
re-run: a sibling lane is live on shared paths, and re-measuring 184 tests to confirm two
predictions is not worth the contention. Owed at post-merge: one crypto/tls per-test census, whose
honest prediction is 176 of 184, unchanged, with TestQUICHandshakeError’s failure having moved
from a nil-pointer panic to whatever the chain para produces.
Gate verdicts
Converter go test ./... ok (207 s) · GolibTests 148/148 · PointerOutParameter PASS on all
four phases, proven failing-first · syscall package builds clean · solution integrity 622/622 ·
full CNR and the go2cs-stdlib.slnx windows build recorded with the commit.
Still builds-and-partly-runs for crypto/x509: no roster row, no proof page, no disclosures,
converted test sources NOT committed — the package cannot yet produce a verdict to commit.
✅ WINDOWS NAME RESOLUTION WORKS — net.adapterAddresses transcribes the adapter chain, and the wall the tls-endgame lane found was never a wrapper defect (2026-08-17, lane claude/net-interfaces)
The tls-endgame entry above ended by naming net.adapterAddresses as “what stands between the
corpus and any name resolution at all on Windows”, and filed it under the syscall struct-passing
class with GetAdaptersAddresses as the censused wrapper. The wall is down, and the filing was
wrong — that correction is the most transferable thing here.
The row is WITHDRAWN, not marked fixed
internal/syscall/windows’s GetAdaptersAddresses is handed a byte BUFFER and fills it, which is
exactly what a byte buffer is for. The wrapper is correct, it stays auto-converted, and hand-owning
it would have fixed nothing. The defect was entirely in the CALLER — net’s own
adapterAddresses, which walks the filled buffer AS the record:
for aa := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next
IpAdapterAddresses is the corpus’s most reference-dense converted struct (nine ж<T>, an
array<byte> PhysicalAddress, an array<uint32> ZoneIndices) where the native record has raw
pointers and inline storage, so golib rightly declines to alias the byte run as it, the reinterpret
falls to a native-address box, and the loop’s OWN nil test fabricates a managed reference out of
adapter bytes — ACCESS_VIOLATION in ж<IpAdapterAddresses>.op_Equality. So this belongs to the
readReparseLink / dir_windows_impl fork (a byte buffer the kernel filled, reinterpreted by
its consumer), not the mirror-the-wrapper one. The row is struck from the
internal/syscall/windows census above and from that package’s own hand-own table, which had
asserted it was “repairable by the ORDINARY mirror remedy, because each receives the struct as a
typed pointer rather than through a byte reinterpret” — precisely backwards for this member.
The lesson for the rows that remain: which struct is non-blittable does not tell you where the repair goes. Who OWNS the memory it is read out of does.
The remedy, and three properties that generalize
core/net/windows/interface_windows_impl.cs holds the buffer in NATIVE memory that never escapes
the function, transcribes the whole chain into managed boxes, and frees it eagerly in a finally.
- The output is a chain OF CHAINS. Each adapter record carries six nested linked lists (unicast, anycast, multicast, DNS-server, prefix, WINS-server and gateway — five sharing one native shape), and every consumer reaches THROUGH a record into them, so a top-level copy would have moved the fabrication one hop out rather than removing it.
- Whether a managed pointer needs a
ManagedPointerTokenshandoff is decided by the GO DECLARATION, not by the data.AddrinfoW.Addris an untypedsyscall.Pointerthe consumer casts by hand, so it needs a token;SocketAddress.Sockaddris a TYPED*syscall.RawSockaddrAnythat converts to a field carrying a managed box directly — nounsafe.Pointerround trip to survive, so no token, no weak table, no anchor. Reaching for that machinery here would have been ceremony, and the brief for this lane expected it. - The transcription writes the image an EXISTING hand-own reads. Consumers call
.Sockaddr(), which issyscall’s hand-owned decode; it flattens the managedRawSockaddrAnyback to its 116-byte native image, so this is the same inverse-flatteningGetAcceptExSockaddrsperforms (Family at 0,Addr.Data2..15,Pad16..115).
The copy is FAITHFUL rather than minimal: Length, AdapterName, DnsSuffix, Description,
Flags, ZoneIndices, FirstPrefix, the link speeds and the WINS list are carried although no
consumer reads them today, because this record is the public shape behind net.Interfaces and a
declared field left nil is a SILENT divergence for the next consumer rather than a loud one.
The payoff, measured
A probe run under Go and under the converted corpus, output byte-identical:
LookupIP(localhost) err: <nil> count>0: true
LookupHost(localhost) err: <nil> count>0: true
LookupPort(tcp,https): 443 <nil>
LookupHost(dns.google) err: <nil> count>0: true
Dial(dns.google:443): connected
The last two lines are the result: a LIVE DNS resolution through the server list dnsReadConfig
builds from adapterAddresses, then a TCP connection to the resolved address. It is deliberately
NOT promoted to a behavioral guard — it needs a network and an external host — so the suite keeps
the network-free IpAdapterAddresses guard instead and this stays a recorded probe.
crypto/tls’s TestVerifyHostname was NOT re-measured this lane; the probe answers the same
question more directly and an hour-plus census was not spent to restate it. Its verdict stays open.
net’s FIRST pipeline census — it runs, and it does not bank
go2cs -tests -test-action all -test-timeout 60m over net (57 Windows-eligible _test.go files).
The suite CONVERTS and COMPILES, and the host RUNS — where before this arc anything reaching
adapterAddresses killed the process.
| Measure | Value |
|---|---|
| Go on this host | 474 verdicts, 695.6 s, FAIL (network-dependent) |
| converted host | 25 verdicts, then killed at the 61-minute safety net |
| matched / excluded / errors | 1 / 53 (benchmarks) / 461 |
Every one of the 461 “errors” is Go="pass" C#="" — a test the host never REACHED, not a conversion
failure. The 25 it did reach ran TestAcceptError → TestClosingListener, with TestAddrList,
TestAddrListPartition, TestCancelAfterDial, TestCloseError and the accept-path tests passing.
So the root is a severe SLOWDOWN, not a correctness wall, and net needs a poller/performance
arc before its census is even measurable. ⚠ This is exactly the mass-empty shape that reads like
total conversion failure; the 25 real verdicts are what prove it is not one.
Gates
Converter go test ./... ok (216.8 s) · full CNR byte-identical across 620 behavioral packages,
nothing NOT MEASURED · seeded full reconvert 304/304, and the overlay is 0 content differences
/ 0 file-set differences with the marker gate at 61 marked files, 0 clobber violations (18
.cs.auto emitted) — so the hand-own is stable under regeneration, including its L3 routing into
net/windows/ and the AllowUnsafeBlocks flip its [module: GoRequiresUnsafe] causes · full
behavioral suite 593/593 across all four phases, 567 output comparisons, 0 failures (3,077.4 s) ·
solution integrity ok (622 projects). The guard is proven failing-first: with the auto body restored
it reports exit code mismatch: C# -1073741819 vs Go 0, the access violation itself.
⚠ Two process traps paid for here, both already in CLAUDE.md and both re-encountered: PowerShell’s
Start-Process -ArgumentList ARRAY form does not quote a path containing a space, so the first
pipeline launch died with Failed to access input file path "C:\Program" — which reads exactly like
a missing GOROOT and is not (use the single-string form with embedded quotes); and an
until ! powershell -Command "exit (…)" wait-loop reported a still-running go test as finished,
the documented exit $true trap, caught only against a positive process count.
⛔ HARVEST — the pure-compute tail re-measured; nothing banks, net/netip’s SYNTAX wall falls, and three “reproduced verbatim” rows re-earn that phrase (2026-08-17, lane claude/harvest)
A ranked breadth pass over the unbanked pure-compute candidates, run on the r43c/r44a doctrine and returning the least fashionable answer this board records: six candidates measured, zero banked. That is worth writing down precisely because every previous breadth pass here banked something — the cheap rows are spent, and what is left in the pure-compute tail is walls with names. The pass is still worth its cost twice over: two converter defects that emit UNPARSEABLE C# are closed, and a root the board has recommended twice as “the cheapest remaining pair” turns out to be spent.
The enumeration, and what it excludes
305 converted package directories under src/core carry a production .csproj (306 counting the
hand-written golib); 216 have a func Test in their Go 1.23.1 GOROOT sources; minus hand-owned
testing that is the roster header’s 215, and 215 − 150 banked = 65 unbanked. Of those, the following are excluded from a pure-compute
harvest rather than measured: the socket-walled net family (net, net/http, net/http/{cgi,
cookiejar,httptest,httptrace,httputil,internal,pprof}, net/rpc, net/smtp, net/mail,
net/internal/socktest, log/syslog) behind the poller-throughput arc; the runtime/OS internals
(runtime, runtime/{debug,pprof,race,trace}, internal/runtime/*, internal/poll,
internal/syscall/*, internal/trace*, internal/coverage/cfile); the lane-owned crypto/tls,
crypto/x509 and reflect; the packages a ratified ruling already blocks (math/big 224/226 and
net/http/internal 9/10, both held by the undisclosed near-budget alloc counts; log 7/9 behind
the runtime.Caller arc; os/user, whose TestGroupIds fails in Go’s own oracle); the
working-directory class (go/build 57/58, internal/platform, internal/godebugs); and the
architectural rows (internal/unsafeheader 0/6, iter 0/28 on the newcoro/coroswitch stubs,
slices 0/122 on element-type deduction for constrained slice generics).
The six, in the order they were run
| Package | Verdicts | Board had | Measured 2026-08-17 |
|---|---|---|---|
html |
3 | 2 of 3 | 2 of 3, unmoved. TestUnescape — the map-MISS producer of the array<T>-without-its-Go-length class. The default! zero-value emission (claude/zero-value-arrays) does NOT reach it: that fix covers declared zero values, and a map miss has no declaration to read a shape from. Still the converter+golib arc r57b priced |
internal/chacha8rand |
4 | 3 of 4 | 3 of 4, unmoved. TestBlockGeneric panics index out of range [0] with length 0 at chacha8_generic.cs:64 — and the producer is NOT a declaration either: it is the unsafe.Pointer array-SHAPE reinterpret ([32]uint64 → [16][4]uint32) that hands back a length-0 outer array<T>. Same seam as debug/pe’s, with no kernel in it |
debug/pe |
10 | 9 of 10 | 9 of 10, unmoved. TestReadCOFFSymbolAuxInfo — the byte-level struct pun across surrogate layouts, exactly as recorded |
encoding/gob |
106 | 99 of 106 | 100 of 106 — one row, from the reflect-bridge closure. TestSingletons closed. The six that remain are four roots: the array<T> length class (TestEndToEnd, TestIndirectSliceMapArray), gob’s own decoder IGNORE path (TestBadData, TestIgnoreRecursiveType), reflect.Value.IsNil on an INTERFACE asking the pointee (TestNilPointerInsideInterface), plus TestIgnoreDepthLimit infrastructure-erroring on the reflect.ArrayOf→typelinks stub |
net/netip |
266 | 0 of 266, CS1002/CS1525 | The SYNTAX wall is CLOSED — two converter defects, both fixed below — and three semantic roots stand behind it. Still 0 of 266 |
internal/reflectlite |
30 | 0 of 30, CS0715 | 0 of 30, and the root MOVED. CS0715 is gone; two different diagnostics now: CS0030 and CS8130, below |
The two converter defects net/netip was the first thing in the corpus to reach
Both emit C# that does not PARSE, so nothing downstream of the compiler could have caught them, and
both are shapes the production corpus happens not to contain — which is why they survived to be
found by a Phase-4 measurement rather than by a build. Both are fixed, each with a failing-first
guard in src/go2cs/mixedKeyedComposite_test.go.
1. Go’s all-or-nothing keying rule is a STRUCT-literal rule. An ARRAY or SLICE literal may MIX
positional and keyed elements — []byte{0xfe, 0x80, 15: 0x01} is a SIXTEEN-byte value — and every
keyed path in convCompositeLit decided from Elts[0] alone (compositeLitIsKeyed’s comment stated
the wrong rule as its justification). A mixed literal therefore took the plain positional emission
while its keyed elements still rendered through the key/value arm, whose sparse form wants a target
ident that does not exist in an expression position:
new byte[]{0xfe, 0x80, <nil>[15] = 0x01} — CS1525. The fix normalizes the POSITIONAL elements to
the indices Go gives them (first element 0; after a keyed element at k, continue at k+1) so the
literal is all-keyed and the existing SparseArray machinery renders it — new slice<byte>(16){[0] =
0xfe, [1] = 0x80, [15] = 0x01}, which also recovers the length a wrong emission gets silently wrong.
An all-positional or already-all-keyed literal is untouched by construction, which is why the whole
corpus is byte-identical. A literal whose keys will not fold to constants is left exactly as it was:
an index the converter cannot compute is one it must not invent.
2. Rooting a global using RHS was not idempotent. renderCSFullTypeName’s default arm prefixes
the root namespace unconditionally, and a WHITE-BOX test conversion hands it names the test-alias
qualifiers (testAliasShadowOperations) have already rooted with an explicit global:: — so
export_test.go’s type Uint128 = uint128 emitted
global using Uint128 = go.global::go.net.netip_package.uint128;, CS7000 “unexpected use of an
aliased name”. global:: IS the root; prefixing it can only produce a name that is not one.
What stands behind them — net/netip, three roots, all semantic
The package now COMPILES past the parser and stops on eight diagnostics in three families:
| Root | Sites | Note |
|---|---|---|
| An EXPORTED test declaration over an UNEXPORTED production type is emitted more accessible than the type | CS0050 ×2, CS0052 ×3 in export_test.cs |
func MakeAddrDetail(…) addrDetail and var Z0 = …Handle[addrDetail] are exported Go names, so they emit public; addrDetail/uint128 are unexported, so they emit internal. Legal Go, illegal C#. A test assembly’s public has no external consumer, so clamping an internal-test declaration’s accessibility to its type’s is the shape of the remedy |
A global using alias is assembly-scoped, not a namespace member |
CS0426 in netip_test.cs |
netip_package.AddrDetail — the class importOperations.go:894 names, whose CROSS-package two-hop form is already fixed; this is the SAME-package (white-box test) instance of it. encoding/xml’s recorded blocker reads identically (ΔToken does not exist in xml_package, 386 verdicts) and html/template’s CS0030 sits next door — whether one fix serves all three is a hypothesis this pass did NOT measure, but it is the cheapest one to test |
| A generic test helper’s constraint renders non-generically | CS0305/CS0308 in fuzz_test.cs |
comparable<T> used with no type argument, and netipTypeCmp used with one |
internal/reflectlite — the recorded root is spent, and the two behind it are named
CS0715 (a generic test-local type’s operators emitted into the static test partial) no longer
appears, which retires the board’s “cheapest remaining pair” framing — the pair is now one package
(runtime/debug, 9, unmeasured this pass) plus two fresh roots here:
- CS0030 — a Go conversion-to-interface written in CALL syntax takes a raw C# cast.
pinUnexpMeth(EmbedWithUnexpMeth{})emits((pinUnexpMeth)new EmbedWithUnexpMeth(nil)); the converted struct does not declare the interface (that isgo2cs-gen’s adapter’s job), so the cast is illegal.T(x)where T is an interface must route throughconvertToInterfaceType, exactly as an assignment to an interface-typed slot does. - CS8130 — a range over an
append(…)result cannot infer its deconstruction.for i, tt := range append(assignableTests, implementsTests...)emitsforeach (var (i, tt) in append(…))and neitherinortthas an inferable type.
The ranked queue this pass hands on
encoding/xml(386) andnet/netip(266) — the assembly-scoped-alias CS0426 root, measured in both and identical in shape.html/template(243) andtext/template(52) sit behind a CS0030 that may or may not be the same family; measureencoding/xmlfirst, since its diagnostic is the one that matches.sync/atomic(108) — CS0841 (use of a local before its declaration);debug/elf(31) — CS8183 (implicitly-typed discard);flag(24) — CS1929;crypto/ed25519(9) — CS0030;internal/concurrent(20) — CS0426node<,>. Five one-diagnostic build blocks, each unmeasured since scout batch 2 and each therefore a candidate for having MOVED, as reflectlite’s just did.encoding/gob(106) — three roots, of whichValue.IsNilon an interface is a five-line bridge fix the board has carried since r39.internal/reflectlite(30) +runtime/debug(9) — behind the two roots above.crypto/internal/edwards25519(55) — the ratified Option A tuple-spec init-order fix is still unimplemented; the residual after it is one AllocsPerRun row.
Deliberately NOT re-measured: unique (4/19), log/slog (153/213), go/doc (24/85) — each has a
current census and a named owner, and r44a’s lesson is that the yield is in what nobody has run.
The measurement hazard this pass paid for
MSB4166 “Child node exited prematurely” is NOT a build root. debug/pe first measured as a hard
build failure with eleven MSB4166s and no CS diagnostic at all; re-run with
MSBUILDDISABLENODEREUSE=1 it reached its real 9-of-10 in 45 s. The pipeline shells out to
dotnet run, whose MSBuild leaves node-reuse workers alive BETWEEN pipeline runs, and a reused node
that dies takes the next package’s build with it. Set MSBUILDDISABLENODEREUSE=1 for any queue that
runs several -tests conversions back to back — it is also the isolation flag a lane owes its
siblings.
Gates
Converter go test ./... ok (274.3 s) · full CNR byte-identical across all 621 behavioral
packages, nothing NOT MEASURED, 2 advisory warnings (1,440 s) · seeded full corpus reconvert
(307 projects, 549 s) 1,665 emitted artifacts, 1,665 identical, 0 differing, 0 new — the
zero-corpus-churn claim measured rather than argued, and self-validating on the hand-own marker gate
(a failed seeding would have emitted the hand-owns as plain .cs and they would have shown as
differing) · both guards proven failing-first (<nil>[15] = 0x01 and
go.global::go.net.netip_package.uint128 each reproduce with its fix neutered). No roster row, no
proof page, no disclosures, no converted test sources committed: six measurements, zero validations.
⛔ The three -tests roots were SIX, they are all RECOMPILE-MODEL roots, and crypto/x509’s compile wall is now ONE FILE behind a model-selection arc (2026-08-17, lane claude/x509-unlock)
The entry above censused crypto/x509’s test host at 5 errors in 3 roots and called all three
general -tests emission defects. The count was right, the reading was right, and it was half the
depth: closing those three exposed three more of the same family, and closing all six took the
package from 5 errors to 6, in one file — a 97 % reduction that ends at a different class
entirely. Every one of the six is fixed in the converter with a failing-first guard.
One principle, five supplies: under the RECOMPILE model the test half CONTINUES the production pass
Two supplies were already pinned this way (productionLiftedTypeNames,
productionHoistedConstOrdinals). Three more owed it, and the fix collects all five into one
productionSeed struct captured before the first variant’s resetPackageState:
| # | Supply | Emitted name | crypto/x509’s collision |
Errors |
|---|---|---|---|---|
| 1 | blank-import force hooks | initᴛᴛblankImportꓸcryptoꓸsha256 |
x509.go and x509_test.go both blank-import sha256 and sha512 |
CS0111 ×2 |
| 2 | the blank-identifier counter | _ᴛ1ʗ |
pem_decrypt.go’s blank iota const vs oid_test.go’s var _ encoding.BinaryMarshaler |
CS0102 |
| 5 | func init() ordinals |
init / initΔ1 |
windows/root_windows.go’s init vs x509_test.go’s |
CS0111 |
None of these is exotic Go — a test repeating a production blank import is what a test exercising
those registrations does. The blank-import hook is the one whose OWNERSHIP is worth stating and not
merely its uniqueness: exactly one hook per (assembly, imported package), and the production half
owns it whenever its file is in the compilation, because that file is the one a -tests run cannot
rewrite.
Three more: a recompile-model test project compiles the production sources, so it owes their references and their per-GOOS half
| # | Root | crypto/x509’s symptom |
Errors |
|---|---|---|---|
| 3 | the B2c alias scan read only the TEST-emitted files | x509.cs/pem_decrypt.cs emit using hash = hash_package; (crypto.Hash.New() RETURNS hash.Hash, so hash is in no import list and in no production reference) — and DisableTransitiveProjectReferences hides it |
CS0246 ×2 |
| 4 | productionCSFiles was FLAT-only, and layout L3 is not flat |
the whole Windows verifier (windows/verify.cs, windows/root_windows.cs) fell out of the test compilation |
187 |
| 6 | the production static-ctor probe was FLAT-only | a SECOND static x509_package() emitted beside the real one in windows/package_init.cs |
CS0111 |
Root 3’s omission hides in the ordinary case, because a production file’s aliases are usually its own
package’s direct imports, which the import-derived set already carries; it bites only where the alias
names a package reached transitively. Roots 4 and 6 had never been exercised because crypto/x509
is the corpus’s only L3 package on the recompile model — every other L3 suite takes a reference
model, where the production ASSEMBLY carries its per-GOOS half. Note root 4’s shape: 187 errors, every
one of them reported against a TEST file, none of them naming the missing folder.
Unlock breadth: measured, and NARROW — because all six are recompile-model-only
Worth stating plainly, because the honest answer is smaller than the brief expected. All six roots
live on the path taken only when the test half emits into the production class, i.e. the recompile
model, which is a deterministic FALLBACK rather than a selection. Measured on this branch:
- 3 of 151 committed
.tests.csprojare on the recompile model:crypto/ecdh,crypto/x509,text/tabwriter. Every other banked package isreferenceorwhitebox-reference. - Thirteen recorded build-blocked packages sampled, ZERO on recompile —
encoding/xml,net/netip,html/template,flag,debug/elf,os/exec,database/sql,text/template,internal/concurrent,crypto/ecdsa,runtime/pprof,encoding/json(all whitebox-reference) andsync/atomic(reference). None of their walls is one of these six. - The two OTHER banked recompile packages are byte-identical under the fixed converter
(
crypto/ecdh,text/tabwriter: empty-numstat CRLF phantoms plusecdh’s expectedpackage_init-testshook, nothing else) — which is the regression proof that matters most here, since they are the only banked packages the change can reach at all.
So: general in FORM, crypto/x509-specific in PRACTICE today, and free for any future package that
falls back to recompile. That is the whole breadth; there is no larger unlock hiding behind it.
crypto/x509’s census: 5 → 6 errors, ONE file, and the wall is the one the reference model exists to prevent
The remaining six errors are all in hybrid_pool_test.cs, and they are the recompile model’s
signature defect, already written down in this repo’s own doctrine (testProjectReference’s
declaration comment): a referenced assembly whose API mentions a production type names it in the
PRODUCTION assembly, while the test assembly’s recompiled copy is a DISTINCT type.
googChain := c.ConnectionState().PeerCertificates // c is a crypto/tls Conn — a REFERENCED assembly
→ CS0012: The type 'x509_package.Certificate' is defined in an assembly that is not referenced …
'crypto.x509' ×4, plus CS1929 … the best extension method overload 'Verify(ж<Certificate>,
VerifyOptions)' requires a receiver of type 'go.ж<go.crypto.x509_package.Certificate>' ×2. No
compile-set or reference adjustment repairs an identity split; the remedy is to stop needing the
fallback.
Why x509 falls back, censused exactly — ONE record. Instrumenting
recordsRequireProductionMutation on this branch prints exactly one offender:
CENSUS implicit: global::go.crypto.x509_package.Certificate -> ж<global::go.crypto.x509_package.Certificate>
That is the shared Go pointer-boxing route T → ж<T>, and the adjacent
indirectImplicitConversions loop already exempts precisely this shape, with the reasoning
spelled out in its own comment (“the generator intentionally emits no type-owned operator for a
foreign T, so it does not mutate production”). The exemption is simply absent from the DIRECT
implicitConversions/invertedImplicitConversions loop above it. That is the next lane’s move and
this lane deliberately did not take it: it changes MODEL SELECTION, whose blast radius is the
three recompile packages above — two of them banked and validated — so it owes their re-proof plus a
sweep, which is a different arc from six naming/closure fixes and must not be smuggled in behind
them.
The census with that one line applied is MEASURED, not predicted — run on a throwaway binary, with the branch’s source unchanged and the experimental binary deleted afterwards. It is the most useful number this lane produced:
| verdicts agreeing | 264 of 335 |
| divergent | 71 |
— of those: Go=pass C#="" (no verdict produced) |
51 |
— of those: Go=skip C#="" |
16 |
| — of those: real C# failures | 4 |
The host compiles clean and RUNS: zero C# errors, and 264 of the suite’s own verdicts agree with
go test — parsing, marshalling, name constraints, PEM, SEC1, CRL creation, path building, EKU
enforcement. The run then times out at 1h1m (the safety net past -test-timeout 60m), and the
67 empty verdicts are dominated by TestSystemVerify/* and everything sequenced after it — exactly
the stack-proven CertChainPara wall the entry above censused, now visible as a HANG rather than as
a nil dereference. TestHybridPool is among the empty rows.
The four genuine divergences are small, named, and independent of every wall above:
TestParsePKIXPublicKey (and its X25519 subtest), TestOIDMarshal, TestPKCS8.
So the ordering for the next lane is settled by measurement rather than by guess: land the
model-selection exemption (with crypto/ecdh and text/tabwriter re-proved), and crypto/x509
becomes a package with four ordinary divergences and one big wall, not a build-blocked one.
Gate verdicts
Converter go test ./... ok · full CNR byte-identical · the two other banked recompile-model packages
re-converted byte-identical · encoding/json canary re-swept (491) · six guards proven failing-first
by neutering each fix in turn (the neutered runs print the real diagnostics: _ᴛ1ʗ re-minted,
windows/verify.cs missing from the compile set, hash absent from the reference set).
⚠ Process note, both already in CLAUDE.md and both re-paid here: the Start-Process -ArgumentList
array form does not quote C:\Program Files\… (died as Failed to access input file path
"C:\Program"), and the Bash tool’s timeout caps at 600 s regardless of the value passed — a
20-minute pipeline run passed 3000000 and was killed at exactly 10 minutes with exit 143, orphaning
the dotnet run child that then held runtime.dll locked. Long runs go through detached
Start-Process with a PID poll, and the orphan sweep must be scoped by COMMAND LINE
(Win32_Process … CommandLine -like '*<lane>*'), never by process name — three sibling lanes were
live on this machine throughout.
⛔ crypto/tls STILL does not bank — but its endgame collapses from FOUR owners to ONE, and BoGo was never the wall it looked like (2026-08-17, lane claude/tls-finish)
The tls-endgame entry above left four divergences with four different owners and predicted where
each would land once the syscall arcs merged. This lane re-measured all four. Three of the four
predictions were right and the fourth was wrong in the useful direction: TestBogoSuite’s host
defect is fixed, and BoGo does not merely start — a real BoringSSL interop case passes end to
end through the converted host as its TLS shim. What is left is one arc, not four.
The census, re-measured today
Same method as the tls-endgame lane (one process per top-level Test*, -run '^Name$', 25 s cap,
raw stdout saved, classified offline against the CONVERTED host’s PASS<pad><Name> format).
TestBogoSuite is measured separately below, because a 25 s cap cannot hold it on either runtime.
| Measure | after sha3 |
after the syscall arcs | now |
|---|---|---|---|
| top-level tests that PASS run on their own | 176 of 184 | (not re-run) | 176 of 184 |
| real divergences (Go passes, C# does not) | 4 | 4 predicted | 3 measured + BoGo |
| distinct roots behind them | 3 | — | 2 |
| of those, disclosable under an existing class | 1 | — | 1 |
Go on this host, re-measured rather than carried (go test -json -count=1, 56.9 s): 184
top-level, 180 pass, 4 fail, and the four are the same expired-fixture set — TestResumption,
TestResumptionKeepsOCSPAndSCT, TestVerifyConnection, TestCrossVersionResume (the test
certificates expired 2025-01-01). The converted host fails exactly those four, so they are
AGREEING rows. The cross-tab closes with no remainder: 176 agreeing passes + 4 agreeing failures +
3 divergences + TestBogoSuite = 184.
⚠ The expired-fixture ceiling is re-confirmed rather than carried: 180 of 184 is the most this host can score in either language, it worsens with time, and a Go patch release or regenerated fixtures changes the shape. Any proof page written for this package must say so.
1. TestVerifyHostname — the net wall is DOWN, and what it uncovered is not tls’s
The net-interfaces entry above declined to re-measure this test, on the ground that its own probe
answered the question more directly. It did, and the census now confirms it from the consumer side:
Dial("tcp", "www.google.com:https", nil) resolves the name AND the service, connects, and
completes a TLS 1.3 handshake through the server’s Certificate message before failing. Everything
the two syscall arcs bought is exercised on that one line — GetAddrInfoW for LookupPort’s
https, adapterAddresses for the DNS server list, the poller for the connect and the record
reads.
It then dies one layer further out, and NOT in net:
INFRASTRUCTURE-ERROR TestVerifyHostname — System.Runtime.InteropServices.SEHException (0x80004005)
at go.syscall_package.Syscall9(...)
at go.syscall_package.CertGetCertificateChain(...) <- zsyscall_windows_ptrout_impl.cs:222
at go.crypto.x509_package.systemVerify(...) <- root_windows.cs:272
at go.crypto.x509_package.Verify(...)
at go.crypto.tls_package.verifyServerCertificate(...)
at go.crypto.tls_package.readServerCertificate(...) <- handshake_client_tls13.cs:692
That is the wall the x509-cryptoapi entry named: CertChainPara is handed to the kernel BY ADDRESS
while holding RequestedUsage.Usage.UsageIdentifiers as ж<ж<byte>> and CacheResync as
ж<Filetime>, so systemVerify’s para.Size = 80 writes the NATIVE size into a much smaller
managed object and every field past the first is read from the wrong offset. Note where the
frame is: inside the hand-owned zsyscall_windows_ptrout_impl.cs wrapper, i.e. the out-parameter
fix is doing its job and the defect is the other class, exactly as that lane predicted.
2. TestQUICHandshakeError — the prediction was right, and the shape is a HANG
The x509 lane predicted this row would still fail, “with its failure having moved from a nil-pointer
panic to whatever the chain para produces”. Measured: it produces nothing — the process is still
inside CertGetCertificateChain when the census’s 25 s cap fires (TIMEOUT — package timeout after
00:00:25), the same blocked-in-the-kernel signature that lane’s offline probe recorded with
dotnet-stack. The corrupted CertChainPara includes dwUrlRetrievalTimeout, a blocking network
budget, read at the wrong offset.
So TestVerifyHostname and TestQUICHandshakeError are ONE root, not two — the same CryptoAPI
structure-passing wall, reached by two different tls paths (a real server’s chain, and a QUIC
handshake against an untrusted test certificate). Neither is disclosable: this is an
unimplemented-but-fixable defect, which is exactly what the host-limit bar excludes.
3. TestCertCache — disclosed, codegen-liveness, signature-pinned
Re-measured and unchanged: FAIL TestCertCache — timed out waiting for expected ref count, at the
FIRST check (refs 2 → 1), 8.1 s in the per-test census. Argued from the test’s own assertion: it
nils its local, calls runtime.GC() and polls four seconds for a finalizer to decrement a count,
while certA, err := cc.newCert(p.Bytes) is a two-result call whose results materialize an
address-exposed frame temp — a slot the CLR reports live for the whole method, so the activeCert
cannot be collected until TestCertCache RETURNS. The finalizer bridge itself is proven working by
sync’s banked TestPoolGC. Committed as
src/core/crypto/tls/go2cs_test_disclosures.json
ahead of a bank the package cannot yet make, so the row is pinned by exact signature the moment it
can.
4. TestBogoSuite — the host defect is FIXED, and the converted TLS stack talks to BoringSSL
The tls-endgame entry read this as “a fixable test-host defect”, and beyond it, “a long external TLS interop measurement, not a pass: it is its own arc”. The first half was right. The second half is wrong, and the correction is the most encouraging thing in this entry.
The host defect, and its real mechanism. BoGo re-executes the test binary as its shim
(-shim-path=os.Args[0] -shim-extra-flags=-bogo-mode), and -bogo-mode is a package-level
flag.Bool in handshake_test.go. The board read the defect as “the host parses args itself and
throws on any name it does not recognize, before the package’s own flag.Parse() runs” — true, but
the fix is NOT to tolerate unknown flags. Go’s test binary reaches exactly ONE flag.Parse(), by
which time testing.Init() has defined -test.* and the package’s own package-level flag
variables have initialized, so both vocabularies live in one flag set and an unknown name is still
an error there. What the converted host lacked was the ORDER: nothing had run the package’s
initialization at the moment the host had to decide what its command line meant.
The fix, in the shared hand-owned host (three files under src/core/testing/). An unrecognized
flag name no longer throws — TestOptions.Parse records it and STOPS, the way a non-flag token
stops the parse, because nothing there can know a foreign flag’s arity (-port 5000’s value is
indistinguishable from a program argument). TestHost.Run then does what Go does before main: it
runs the package’s own initialization — RuntimeHelpers.RunClassConstructor over the declaring
types of the registry’s delegates, which are exactly the converted package’s classes — registers the
host’s own flags on top, and only then asks the converted flag.CommandLine whether the recorded
name is defined. Undefined — or no flag package in the compilation at all, which is 124 of 141
test projects — is still flag provided but not defined: -x and still exit 2. The rejection
moved; it did not go away. Deliberately NOT done: a host-side flag.Parse() mirroring M.Run’s
if !flag.Parsed(), which the reference already rules out as newly reaching ExitOnError for
packages that merely reference flag.
One measured surprise worth carrying. Creating a delegate over a static method does not run
its declaring type’s static constructor — ldftn+newobj is neither a static-field access nor an
invocation — so the generated test host’s registry.Add("TestX", pkg_test_package.TestX, …) lines
leave the package uninitialized until the first test BODY runs. That is what makes the forcing
load-bearing rather than belt-and-braces, and it is proven failing-first: with the
InitializePackageUnderTest call neutered, the new guard reports flag provided but not defined:
-harness-package-mode and exit 2 — the BoGo failure verbatim.
Guarded by TestingRuntimeTests.APackageRegisteredFlagParticipatesAndAnUndefinedOneIsStillRejected,
which stands a class whose STATIC CONSTRUCTOR declares a flag in for the package under test (written
with an explicit static ctor, not a field initializer, so the CLR’s precise non-beforefieldinit
rules apply and a runtime that chose to initialize early could not fake a pass). It pins all four
claims: the package’s flag participates, a flag BEFORE it is still the host’s, a flag AFTER it
belongs to the program, and an undefined name is still exit 2. This is the one test that needs the
converted flag package present, so BehavioralTests.csproj now references it — testing.csproj
still must not, and does not, which is the whole point of the late binding.
What BoGo then did. Two measurements, both value-level:
> tls.test.exe -bogo-mode -is-handshaker-supported -> No (0.038 s)
> crypto.tls.tests.exe -bogo-mode -is-handshaker-supported -> No (3.30 s)
— byte-identical output from the shim entry point, network-free. And then the real thing, the converted host driven by BoringSSL’s own runner:
> crypto.tls.tests.exe -run "^TestBogoSuite$" -v -timeout 14m -bogo-filter Client-Verify-ECDSA-TLS1
RUN TestBogoSuite
RUN TestBogoSuite/Client-Verify-ECDSA-TLS1
PASS TestBogoSuite/Client-Verify-ECDSA-TLS1
PASS TestBogoSuite
12.1 s, including go mod download of the pinned boringssl module, building the BoGo runner with
the Go toolchain, and a TLS 1.0 client handshake with certificate verification against BoringSSL as
the peer. The converted TLS implementation interoperates with a foreign, adversarial TLS
implementation — which nothing in the corpus had shown before.
Why the full suite was still not run. Go’s own run here is 3,242 BoGo cases (861 pass, 2,381
skip, 0 fail) in 31.8 s; BoGo spawns the shim once per case, and the converted host’s shim
invocation costs 3.30 s against the Go binary’s 0.038 s — ~87x, and ~3 hours of process startup
alone before any TLS work. That is a cost multiplier, not a correctness wall, it is the same
managed-startup number every converted test host pays, and it is not on crypto/tls’s critical
path, because the package cannot bank on the x509 root regardless. Left deliberately
unmeasured-in-full and named for whoever wants it: the honest prediction is that it PASSES, given
roughly a four-hour deadline.
Where crypto/tls stands
One arc — crypto/x509’s Windows system verifier — is now the entire distance between
crypto/tls and a roster row. Everything else is settled: the protocol work has been green since
sha3 (TLS 1.2 and 1.3, QUIC, session tickets, ECH, the whole handshake matrix, and now BoringSSL
interop), the four expired-fixture failures agree with Go, TestCertCache is disclosed and
committed, and TestBogoSuite’s host defect is closed by a general fix the whole roster now
carries. The remaining root has a full census in the x509-cryptoapi entry above (four structures
passed by address, five read back through raw addresses, and the dual-identity problem that makes
the GetAddrInfoW transcription shape inapplicable) — and note that crypto/x509’s own suite still
cannot be measured at all, on three unrelated -tests emission defects, so that arc owes a
test-pipeline arc in front of it.
Still builds-and-partly-runs: no roster row, no proof page, converted test sources not committed (the disclosure manifest is, as the one hand-owned artifact that outlives the measurement).
Gates
Full validated sweep — the gate every core/testing change owes, and the canaries fmt,
database/sql and os/exec are rows inside it · TestingRuntimeTests 26/26 including the new
guard, proven failing-first · solution integrity 623/623 · go2cs.slnx build, the only gate that
compiles BehavioralTests.csproj, whose reference set changed. No converter change, so neither
go test ./... nor CNR is owed — src/go2cs is untouched.
✅ THE CRYPTOAPI CHAIN WALL IS DOWN — and the finding is that its two halves were never separable (2026-08-18, lane claude/x509-verifier)
Three entries above named this wall from three sides: the x509-cryptoapi lane censused it (four
structures passed by address, five read back through raw addresses, “the dual-identity problem that
makes the GetAddrInfoW transcription shape inapplicable”), the x509-unlock lane measured it as a
1-hour hang swallowing 67 verdicts, and the tls-finish lane measured it as the single arc standing
between crypto/tls and a roster row. crypto/x509’s Windows system verifier now runs end to
end, offline, with every value agreeing with Go.
What the brief predicted, and the one prediction that was wrong
The lane was scoped in two phases: fix the INPUT (CertChainPara by address) and measure whether
the call RETURNS; only then consider the read-back. That sequencing turned out to be the most
useful thing measured, because phase 1 alone changes nothing, and the reason is general:
baseline (no fix) SEHException 0x80004005 out of Syscall9
at CertGetCertificateChain <- zsyscall_windows_ptrout_impl.cs:222
at systemVerify <- root_windows.cs:272
parameter mirror ONLY SEHException 0x80004005 out of Syscall9 (identical)
at CertGetCertificateChain <- zsyscall_windows_certchain_impl.cs:183
parameter + read-back all fourteen lines agree with `go run`, 1.44 s
root_windows.go calls CertGetCertificateChain(0, storeCtx, verifyTime, storeCtx.Store, para, …).
storeCtx.Store is a field the CALLER reads out of a native CERT_CONTEXT before the wrapper is
ever entered — at the converted struct’s offset, which under the CLR’s reference-first auto-layout
is where the native record keeps cbCertEncoded. The store handle crypt32 receives is a certificate
LENGTH. When one wrapper’s argument is a field of another wrapper’s result, the input fix and the
read-back fix are one change, and no amount of work on the parameter can show progress alone.
That is worth carrying to the remaining members of both syscall classes.
(The input half is real and does have to be fixed. CERT_CHAIN_PARA is 80 native bytes against a
managed record roughly a third that size, and the field that decides the SYMPTOM is
dwUrlRetrievalTimeout at native offset 56 — a blocking network budget read from arbitrary managed
bytes, which is why the same root shows as a multi-minute hang from crypto/tls and as an SEH from
the offline verifier. RequestedUsage.Usage.UsageIdentifiers is an array of C string pointers into
NUL-terminated managed byte slices — no native form at either level — and is transcribed into one
native block for exactly the duration of the call.)
The dual identity, and why it needed no golib change
The read-back could not take the GetAddrInfoW shape, exactly as the census said: that hand-own
transcribes a native chain and makes the free a NO-OP, which works only because nothing native has
to survive the call. Here the original pointer must go back to crypt32 three more times — as the
next call’s leaf, to CertVerifyCertificateChainPolicy, and to the two CertFree* routines, which
release reference-counted memory. A managed view alone leaks a chain per verification; a native box
alone reads every field from the wrong offset.
So each returned pointer becomes a managed view that remembers its native identity: a real
ж<CertContext> / ж<CertChainContext> whose fields the converted Go code reads as an ordinary
struct, with the address it was built from recorded beside it in a weak ConditionalWeakTable.
Wrappers that must hand a pointer back ask that table first and fall back to the box’s own address —
which is why CertCreateCertificateContext and CertEnumCertificatesInStore stay generated and
keep working: they produce plain native boxes nothing reads a field through.
ж.cs is untouched, and ManagedPointerTokens was not needed either. The table is a syscall-local
seam on purpose: ж<T> has no business knowing that one pointee is reference-counted by crypt32,
and the sync point (“the moment a raw word becomes a pointer box”) is again something only the
wrapper knows — the same argument the **T out-parameter entry made for its own remedy. The corpus
now has THREE answers for a native pointer coming back, and the question that selects between them
is not the struct’s shape but who else needs the pointer: publish a native box (ptrout, for
opaque handles), transcribe and free eagerly (addrinfo, when nothing native survives), or transcribe
and remember (here, when it must).
The guard, and what failing-first printed
New behavioral output test SystemCertVerify. A self-signed ECDSA leaf, generated in-process
(so it never expires and never depends on the host’s certificate stores), verified with
Roots == nil — which on Windows routes to systemVerify — for an untrusted-root verdict every
Windows host agrees on with no network. Then the same CryptoAPI sequence driven DIRECTLY, because a
verdict alone cannot carry this evidence: a misread trust status produces the same “unknown
authority” answer. The round trip is what proves it.
created der: true leaf der round-trips: true
parsed cn: go2cs.example store handle round-trips: true
verify chains: 0 store context der length: true
verify error: x509: certificate signed by unknown authority
unknown authority: true chain count: 1
verify with dnsname error: … unknown authority
hostname mismatch: true chain reports untrusted root: true
simple chain elements: 1
store handle round-trips is the dual identity read back by value (the handle inside the context
equals the handle CertOpenStore returned); leaf der round-trips walks
CertChainContext → Chains → Elements → CertContext → EncodedCert and compares the recovered DER
byte for byte against the original, which exercises the transcribed pointer arrays and
unsafe.Slice over them. Proven failing-first: the pre-fix binary prints the first two lines and
dies with the SEHException above.
Deliberately NOT taken, with the reason: CertVerifyCertificateChainPolicy
Its CERT_CHAIN_POLICY_PARA carries pvExtraPolicyPara as Go’s opaque syscall.Pointer, minted in
crypto/x509 as unsafe.Pointer(sslPara) over an SSLExtraCertChainPolicyPara whose ServerName
is itself a managed reference. What reaches the boundary is therefore a transient managed address
with no recoverable box behind it — golib’s uintptr operator declines to pin reference-bearing
storage, so there is nothing for the wrapper to resolve and nothing to copy from. This is a
MINT-SITE problem, not a boundary one, and it has two priced remedies, neither of them this arc:
| Remedy | Cost | Note |
|---|---|---|
Register a ManagedPointerTokens entry when ж<T> → uintptr cannot pin |
golib, corpus-wide | changes what 875 emitted conversion sites hand out; the token table was written for exactly this round trip, but making the operator a MINTER is a model change and wants its own ruling |
Hand-own crypto/x509’s checkChainSSLServerPolicy |
~80 lines, one package | builds the two policy mirrors itself and calls crypt32 directly; puts a native mirror in a non-syscall package, which no hand-own does today |
It is reached only when a chain is TRUSTED and the caller supplied a DNS name.
What this means for crypto/tls and crypto/x509 — predicted, not measured
Neither package was re-censused here: crypto/x509’s suite still cannot build (three -tests
emission defects the x509-cryptoapi entry names), and a crypto/tls census is 184 processes for a
prediction. Recorded honestly as predictions, for whoever runs them:
TestQUICHandshakeError— an untrusted test certificate, socheckChainTrustStatusanswers before any policy call. Expected to flip. Its failure was the hang this arc removes.TestVerifyHostname— a real server’s TRUSTED chain with a DNS name, so it reachescheckChainSSLServerPolicy. Expected to still fail, now on the one wall named above rather than insideCertGetCertificateChain. So the flagship row is177 of 184, not 178, andcrypto/tlsstill does not bank — but its remaining distance is one named, priced mint-site question rather than an arc.crypto/x509’s own census (264 of 335 agreeing, 67 empty verdicts dominated byTestSystemVerify/*and everything sequenced after the 1h1m timeout) should recover most of that tail: the hang that swallowed it is gone, andTestSystemVerifyitself exercises exactly the offline verdict this lane’s guard pins.TestHybridPoolis in that tail.
Footprint and gates
One new hand-owned file (syscall/windows/zsyscall_windows_certchain_impl.cs), two members MOVED
into it from zsyscall_windows_ptrout_impl.cs with the reason stated at both ends, two new
manualConversionFuncs entries (CertFreeCertificateContext, CertFreeCertificateChain) and their
two generated bodies replaced by placeholders. No golib change, no converter behavior change beyond
the registry, no crypto/x509 change at all.
Converter go test ./... ok (185 s) · full CNR · seeded reconvert with the path-precise marker gate
· run-validated-sweep.ps1 -Filter syscall canary · go2cs-stdlib.slnx windows build ·
SystemCertVerify PASS on all four phases, proven failing-first · solution integrity 625/625.
✅ crypto/tls BANKS at 400 + 2 — the mint-site closes at the EMISSION, not at either priced remedy, and the flagship row is one suite over the whole Windows networking stack (2026-08-18, lane claude/tls-mint-site)
The entry above left one named, priced question between crypto/tls and a roster row:
pvExtraPolicyPara, minted in crypto/x509 as unsafe.Pointer(sslPara) over a record whose
ServerName is itself a managed reference. This lane took that question by MEASUREMENT, and the
measurement rejected BOTH priced remedies for a third shape cheaper to prove than either — after
which the package validated on the first full pipeline run that could hold it.
The remedy decision — why both table rows lost
- Remedy 1 (the operator mints when it cannot pin) is defeated by the token table’s own
integrity check:
ResolveverifiesCurrentToken(box) == token, and the operator hands out transient ADDRESSES for unpinnable storage, not order tokens — so registering what it hands out today can never resolve, and making it hand out tokens instead changes what every unpinnableж→uintptrsite observes. That is the corpus-wide model change the entry above already said wants its own ruling, and nothing here needed it. - Remedy 2 (hand-own
checkChainSSLServerPolicy) cannot satisfy its own guard. The coordinator’s brief asked for a hermetic trusted-chain + DNS-name policy check whose evidence is a value — and a behavioral test’s own Go source mints through the SAME lossy emission, so under a one-function hand-own the guard program’s mint stays broken and no such test is writable. A hand-own also leaves every other author of the Go shape —internal/poll’s sixWSAMsg.Namemints, converted_test.gosuites, user code — minting the same lost pointer. - The census that settled it: the lossy chain
(T)(ж<EmptyStruct>)(uintptr)(new @unsafe.Pointer(p))exists at SEVEN corpus sites in TWO files, all in the already-broken reference-bearing class. Small enough to fix at the source.
What landed (commit e4d4aeb9b)
The converter’s opaquePointerMintEmission (convCallExpr.go): T(unsafe.Pointer(p)) where T’s
underlying type is *struct{} — an opaque pointer BY CONSTRUCTION, nothing to dereference — now
emits golib’s ManagedPointerTokens.MintOpaque(p). The mint keeps the numeric route byte for byte
for every pointee that route already answered exactly (nil → 0, native → its address,
reference-free → pinned stable storage) and diverges only for the reference-bearing class: the
scalar becomes the box’s own pointer-order token, Registered so the boundary wrapper recovers the
box with Resolve — the table’s THIRD minter, the round trip its design doc names — and the
minted box holds the referent reachable for its own lifetime through a ConditionalWeakTable,
because the referent is otherwise reachable only through a local the JIT may retire before the
syscall that consumes the token. CertVerifyCertificateChainPolicy joins the certchain hand-own
as the RESOLVER (native mirrors both directions, nativeIdentityOf for the chain, the server name
transcribed for exactly the call’s duration). crypto/x509 itself is untouched beyond the
regenerated mint line — no new hand-own anywhere, and ж.cs is untouched again.
Guarded failing-first by SystemCertVerify’s three policy rows, all values: with
CERT_CHAIN_POLICY_ALLOW_UNKNOWN_CA_FLAG waiving the fixture’s untrusted root, a MATCHING server
name answers 0 and a MISMATCHED one answers CERT_E_CN_NO_MATCH — an answer crypt32 can only give
if the name crossed the boundary intact — and with nothing waived the same chain answers
CERT_E_UNTRUSTEDROOT. Pre-fix, the first policy call dies 0xC0000005 inside Syscall9’s
successor frame after all 14 existing rows pass. The golib liveness contract is separately pinned
by ManagedPointerTokenMintTests (referent survives a forced collection with only the mint held).
The census, and the two predicted flips — confirmed
Same documented method (one process per top-level Test*, 25 s cap, TestBogoSuite separate):
178 of 183 exit 0, and the five failures are exactly the four expired-fixture rows (same
x509: certificate has expired text as Go) plus TestCertCache’s pinned disclosure signature.
Zero hangs, zero timeouts, zero infrastructure errors. TestVerifyHostname PASSES — a real
server’s TRUSTED chain with a DNS name, i.e. the mint and the resolver working end to end against
live traffic — and TestQUICHandshakeError PASSES (the hang is gone). Go re-measured on this
host: 184 top-level, 180 pass, 4 fail. Cross-tab closes with no remainder:
178 agree-pass + 4 agree-fail + 1 disclosed + TestBogoSuite = 184.
TestBogoSuite — the serial estimate was wrong, and the truth is a different wall
The tls-finish entry priced full BoGo at “~3 hours of process startup”, a SERIAL estimate. Measured:
the runner parallelizes 16 shim workers — and the run still dies at exactly 600 s, twice,
because the runner is itself a go test package carrying Go’s DEFAULT 10-MINUTE test-binary
deadline, which neither crypto/tls’s code nor the host controls (the test passes no -timeout in
either language). At its wall the runner had reached ~267 of 5,481 cases (~3.4 h projected — the
serial estimate was accidentally about right for the wrong reason), then exits without writing its
results JSON, selecting the “bogo failed” arm whose text the disclosure now pins. Go passes inside
the same wall in 32 s. Disclosed host-limit: the ~87x managed shim startup (3.30 s vs
0.038 s) is what a framework-dependent apphost IS — the same self-retiring texture as os/exec’s
single-file entries (publish fast-startup hosts and the row starts passing until the entry is
removed). The interop CAPABILITY is proven by value in the same suite: the filtered
Client-Verify-ECDSA-TLS1 case completes a full handshake with certificate verification against
BoringSSL as the peer, 14 s end to end. Also observed on the way: the host printed a
*strings.Builder %s operand as a raw pointer (0x…) where Go prints its contents — a real
fmt/Stringer-dispatch divergence on the diagnostic path, named here for whoever takes it; it does
not affect any verdict.
Two oracle duals the bank required — both arms that already existed, mirrored
- Go-side exit forgiveness on AGREED failures.
crypto/tlsis the first package whose honestgo testbaseline is red (fixtures expired 2025-01-01), and the compare treated any nonzero Go exit as fatal — so the package could never validate however perfectly the rows matched. The mirror of the existing C#-side arm: both exits forgiven only when BOTH sides exited nonzero, zero mismatches, both runs produced results, and at least one agree-fail row exists to attribute the exits to. A red Go baseline beside a green converted run stays fatal. - Downward disclosure withdrawal. The oracle aggregated disclosed LEAVES up to their parent but
had no arm for a disclosed ROOT’s Go-side children —
TestBogoSuite’s 3,242 case rows exist only on Go’s side because the disclosed failure precedes the case fan-out, and every one read asGo="skip" C#="". The dual rule: a Go-only row under a signature-matched disclosure root is WITHDRAWN — published in the comparison record and on the proof page (count by root), subtracted from the matched count, never silently dropped. It never widens: a root failing with the wrong signature withdraws nothing, and a two-sided divergent child under a disclosed root is still a strict mismatch. Both rules unit-tested beside the existing aggregation guard.
The proof-page generator also gained hand-owned manifest notes (go2cs_test_disclosures.json
"notes"), because the brief requires the expired-fixture ceiling note VERBATIM on the page and
the page is regenerated on every re-validation — a hand edit would not survive one. The note rides
the same hand-owned manifest as the disclosures.
What the flagship row proves
crypto/tls banks at 400 matched + 2 disclosed — and the row is one suite over everything the
Windows arcs built: the managed netpoller and the WSA submit family (every handshake is real
loopback sockets), name resolution (GetAddrInfoW + adapterAddresses), the CryptoAPI chain
verifier end to end (CertGetCertificateChain, the SSL policy check, the opaque-pointer mint),
sha3 key schedules, session tickets/resumption/QUIC/ECH, and BoringSSL interop by value. The four
agree-fail rows carry the expired-fixture ceiling note on the proof page; the sweep floor is 30m
(TestBogoSuite deterministically burns its child runner’s 600 s deadline every run — evidence
comment in $longTimeouts).
Also measured, not taken
The full-reconvert control surfaced SIX files of accumulated intended drift inherited from
master’s escape/box-copy and dyn-struct arcs (encoding/xml ×3, go/constant/value.cs,
runtime/mstats.cs + its windows package_info.cs) — the committed corpus is stale relative to
master’s converter for exactly those files (last regen 489a32e19, converter moved at
0428c3161). Left for the owning arc or the next leveling regen; named so the next full regen
does not read them as its own.
Gates
Converter go test ./... ok twice (211.9 s / 212.4 s, including the new oracle guards proven in
both directions) · full CNR twice — ONE changed file, the intended SystemCertVerify golden ·
full seeded reconvert 304/304, path-precise marker gate 63/63 marked files 0 violations, corpus
footprint exactly syscall + crypto/x509 + internal/poll · go2cs-stdlib.slnx 0 errors ·
go2cs.slnx 0 errors · GolibTests 152/152 · full behavioral 597/597 transpile+compile+target,
571/571 output (2,162 s) · SystemCertVerify 4/4 phases, failing-first · pipeline
-test-action all -test-timeout 60m: Validated 400 tests (2 disclosed-divergent:
codegen-liveness, host-limit), twice, consistent · full validated sweep: 149 of 151 rows PASS
at their exact banked counts in 340 min (laptop R, solo), crypto/tls PASSing at 400 through
the sweep machinery itself — the two not-green rows are archive/zip and go/parser, BOTH
proven full-sweep-load floor shortfalls, not regressions: each timed out at its floor+grace with
one-sided-row truncation signatures under the 151-row sweep’s disk pressure, and each PASSES solo
the same day on the same machine well inside its old floor (zip 100 verdicts in 850 s, parser 173
in 836 s). Floors raised to the loaded case (60m / 90m) with the evidence in $longTimeouts.
⛔ The assembly-scoped-alias CS0426 was ONE root in name only — encoding/xml goes 0 → 353 of 386, net/netip sheds three of four roots, and the lost write behind xml’s last dozen is MEASURED (2026-08-18, lane claude/xml-netip-alias)
The harvest entry handed on encoding/xml (386) and net/netip (266) as one shared root — “the
assembly-scoped-alias CS0426 root, measured in both and identical in shape”. Measured from both ends,
the DIAGNOSTIC is identical and the ROOT is not: they are two different aliases, minted by two
different mechanisms, and neither fix reaches the other package. Both are closed, along with four
more roots the two packages were hiding behind them, and encoding/xml runs for the first time.
Six converter fixes, each with a failing-first guard. Neither package banks — xml is 353 of 386 with five named residual roots, netip is still 0 of 266 behind a structural wall — so there is no roster row, no proof page and no committed test sources.
The two CS0426s are two roots
Both are the same PRINCIPLE — a global using alias is assembly-scoped and is a member of no C#
class, so nothing may qualify it through one — and that is where the resemblance ends.
encoding/xml (36 sites) |
net/netip (1 site) |
|
|---|---|---|
| The alias | ΔToken, from type Token any |
AddrDetail, from type AddrDetail = addrDetail |
| Declared in | a PRODUCTION file (xml.go) |
a TEST file (export_test.go) |
| Why it is an alias | a DEFINED type over an interface has exactly that interface’s method set and can carry no methods of its own, so visitTypeSpec emits global using ΔToken = object; rather than a class member |
an ordinary Go type alias |
| Who qualifies it wrongly | the INTERNAL test half, through the production class it cannot see into (global::go.encoding.xml_package.ΔToken) |
the EXTERNAL test half, through the package’s using alias (netip.AddrDetail), because Go says netip.AddrDetail during a test build |
| Fix | seed the alias from the production package_info.cs into the test compilation — the NAME into productionAliasLiftedTypes so every renderer spells it, the TARGET into importedTypeAliases so the global using is re-emitted |
render it BARE: the internal half’s global using is compilation-scoped and the external half lands in the SAME compilation |
xml’s fix reuses seedProductionAliasLifts’s two-halves-together shape verbatim and follows the same
TWO-HOP published chain a cross-package consumer follows (GoTypeAlias("Token","ΔToken") then
GoTypeAlias("ΔToken","object")), so the name seeded is the one the production compilation actually
declares. It is gated OFF for the recompile model, and for the reason that matters rather than for
safety: there the production .cs are compile items, the alias is already declared, and re-declaring
it would be the defect. The predicate needs the production files’ AST and cannot be recovered from
go/types — type X any and type X interface{} are the same *types.Named over the same empty
*types.Interface, and only the right-hand SYNTAX says which one emits a class member.
encoding/xml: 0 → 353 of 386, and the second root was a residual its own comment had named
Closing the CS0426 left ONE error, six times: CS1503, cannot convert from
'TestUnmarshalEmptyValues_type' to 'Child_G'. read_test.go declares
type Child struct{ G struct{ I int } } — package-level, lifted Child_G — and then writes the very
same anonymous type as a composite literal inside a function, which minted a SECOND C# type. Go says
those are one type and assigns one to the other.
visitStructType already deduped structurally identical anonymous structs, and its own comment
recorded the wider case as open: “Function-scoped: the cross-function/package-level anonymous split
is a recorded residual.” The package-level registry registerDynamicTypeName is the authority for
closing it — package-scoped, so an unexported field name can only mean this package’s, and keyed by
the full types.String() including field tags, which is exactly what Go’s struct identity
compares. Reuse is one-directional: a function-local lift adopts a package-level name, never the
reverse.
Corpus effect, measured by CNR: two behavioral packages, both intended, both the same collapse.
TypeConversionInterfaceParam had three C# types for one Go type (DataProcessor_data,
Process_data, main_data) — and its own Go source comments them as “Define the SAME anonymous
struct inline”, which is the invariant the emission now honors; NewAnonStructIfaceEmbed had two
(reservedᴛ1, main_type). The residual that remains is ORDERING, not scope: the package-level
declaration must already have been visited, which holds within one file and not across files.
xml’s differential: 353 of 386, five roots, and one of them is a SHIPPED LOST WRITE worth 12
386 verdicts, 8 excluded (3 benchmarks, 5 examples), 353 agree, 33 diverge.
| Root | Verdicts | Note |
|---|---|---|
| A type-switch binding whose address is taken is not heap-boxed | 12 | measured, below |
reflect.canonType assertion — “abi.Type has no System.Type (synthType was bypassed)” |
15 | structType.FieldByIndex reaches toType(Ꮡt.of(structType.ᏑType)) on a Reinterpret<rtype, structType> result; the process TERMINATES, so every later test in read_test.go is absent rather than failed. A reflect root, reached from xml.getTypeInfo |
CopyToken(CharData) / (Comment) “uses same buffer” |
2 | the copy is not copying its backing bytes |
reflect.Type.Name() on a GENERIC type returns the CLR arity-mangled name |
1 | TestMarshal/47: marshals a Generic type under its Generic + backtick-1 CLR name where Go writes Generic |
| embedded-struct field ORDER in marshal output | 1 | TestMarshal/64: promoted fields emit in a different order than Go’s |
(plus TestToken and the two parent verdicts TestMarshal/TestUnmarshal.)
The lost write, measured — and the two wrong turns on the way to it
Token()’s case StartElement: arm is emitted
d.pushElement(t1.Name);
d.translate(Ꮡ(t1).of(StartElement.ᏑName), true); // element name
foreach (var (i, _) in t1.Attr) {
d.translate(Ꮡ(t1.Attr, i).of(Attr.ᏑName), false); // attribute names
}
t = t1;
Ꮡ(t1) is golib’s Ꮡ<T>(in T) — its own doc says “Pointer to a new heap allocated COPY” — so
translate writes the resolved namespace into a copy and t = t1 re-boxes the untouched local. The
ATTRIBUTE line is fine: Ꮡ(t1.Attr, i) aliases the slice’s real backing array, which is the
element-field-address form a previous arc already fixed. Go writes through &t1.Name and then
assigns t = t1, so Go’s semantics require the local to BE the storage — i.e. escape analysis must
heap-box the type-switch binding, and does not.
Measured A/B, hand-boxing the local and reading it back: encoding/xml 353 → 365 of 386,
+12 verdicts, every namespace row (TestUnmarshal/46,61,62,70,71,87,106–109, TestIssue7113).
Census of the emitted shape across the whole production corpus: a bare-local address-of followed
by a field select occurs at 6 sites in 4 files — encoding/xml/xml.cs:325,
net/netip/netip.cs:385 and :391, net/rpc/debug.cs:99, runtime/symtab.cs:1102 and :1146.
Only xml’s is a WRITE; the other five read. So exactly ONE shipped lost write today, and a
correspondingly small blast radius for the fix.
Two wrong turns worth recording, because both are traps this repo’s own doctrine warns about and both were paid anyway:
-test-action comparesilently reported a CACHED comparison. Three A/B runs returned an identical353/386because the manifest was stale (test manifest is stale: input digest changed) and the comparison JSON on disk was the previous run’s. A hand-edited corpus file cannot be measured throughcompare; run the host directly (dotnet run --project <pkg>.tests.csproj --no-build -- --json) and diff its stdout against the Go map the last valid comparison recorded. Before that, a FIRST run measured a stale.dll(built 02:26, source edited 02:51) —comparedoes not rebuild.- The minimal fixture does not reproduce it. Every reduction attempted — a plain type switch, a
slice-element sibling call, a pointer-parameter sibling call — had the ref-lowering pass resolve
fix(n *Name)toref Name, which aliases correctly. What forces xml’stranslateto theж<Name>box form instead is UNIDENTIFIED, and identifying it is the first step of the fix, not a detail of it. Do not assume “type switch” is the whole predicate.
net/netip: 13 errors in 4 roots → 7 in 2, and the wall behind it is the x509 split seen from the other side
Three roots closed, all general:
| Root | Errors | Fix |
|---|---|---|
| An EXPORTED test declaration over an UNEXPORTED production type | CS0050 ×2, CS0051 ×2, CS0052 ×3 | the downgrade-to-internal machinery already existed; its predicate looked through pointer/slice/array/map/chan and not through a generic type ARGUMENT (unique.Handle[addrDetail]), an ALIAS (AddrDetail), or a SIGNATURE. All three are positions C# accessibility-consistency looks through exactly as it looks through a pointer |
| The same-package white-box CS0426 | 1 | above |
A constraint interface embedding comparable |
CS0305, CS0308 | Go’s built-in comparable is not expressible in C# — the bare-constraint arm has emitted nothing for it since golib’s comparable<T> CRTP turned out to be implemented by nothing. An EMBEDDED one inherits that fact, and the two sides disagreed: the declaration dropped to arity-0 while the constraint took the generic CRTP form netipTypeCmp<P>. Now comparable is dropped from the C# base list AND discounted when deciding whether an interface is a method set. Zero production-corpus sites — censused before the change |
A fourth root, an [assembly: GoImplement<AddrPort, P>] record naming a type PARAMETER (CS0246 ×3,
plus CS8785 taking the whole generated adapter set down with it), splits into two halves that must
move in opposite directions: the RECORD belongs against P’s CONSTRAINT (the only one of the two with
a C# spelling, and the one C# checks), while the EMISSION must pass the value UNCHANGED (C# infers P
from the argument, so an adapter wrap there is CS1503 ×5 — measured, in that order, one fix at a
time).
What remains — and the first of the two is structural.
- CS0315 ×5 — a generic function constrained by a TEST-DECLARED interface cannot be instantiated
with a PRODUCTION type under the white-box REFERENCE model.
checkStringParseRoundTrip[P netipTypeCmp]is called withAddr,AddrPortandPrefix, all declared in the referenced production assembly;netipTypeCmpis declared infuzz_test.go. C# enforceswhere P : netipTypeCmpNOMINALLY, andgo2cs-gencan only make a production type satisfy a test-declared interface by generating an ADAPTER class — never the partial declaration a nominal constraint needs. This iscrypto/x509’s identity split seen from the other side: x509 needs the REFERENCE model to avoid a split identity, and netip’sfuzz_test.goneeds the RECOMPILE model to satisfy a nominal constraint. One package, one model, and no model serves both files. - CS0019 ×2 — an untyped-constant complement keeps C#’s
int.uint128_test.cs:84,87:^uint64(0) & ^1emits a complementeduint64zero anded with a complemented bare1, and that1is a C#intwhile Go types the whole constantuint64→ “Operator ‘&’ cannot be applied to ‘ulong’ and ‘int’”. Bounded: the shape is a complement of a BasicLit whose go/types type is a basic other thanint, andconvUnaryExpr’s XOR arm already imposes the type for a NAMED operand and foruint8/uint16. Deliberately NOT taken here — it is a production-reachable emission change and belongs in an arc with room to measure its corpus churn, not tacked onto six other fixes.
What this lane did NOT do
No roster row, no proof page, no disclosures, no committed test sources: encoding/xml does not
validate at 353 of 386 and net/netip still produces no verdicts. The corpus is restored — the two
packages’ -tests artifacts are removed and the initᴛᴛtests() package_init.cs hook (the fourth
-tests-closure phantom shape) restored — so the only tracked changes are the converter, its guards,
two intended behavioral goldens, and this entry.
The corpus effect, isolated by a CONTROL reconvert
Two of the six fixes reach PRODUCTION emission (the anonymous-struct lift and the type-parameter slot), and CNR only samples the shapes the behavioral corpus happens to contain — so the stdlib was measured too, twice, under identical seeding:
| Seeded full-corpus reconvert (304 packages) | identical | differing | new |
|---|---|---|---|
| this branch | 1,611 | 53 | 0 |
| CONTROL, master’s converter | 1,613 | 51 | 0 |
Marker gate on both runs: 62 marked files, 0 violations — so neither seeding failed, and the differing sets are comparable. The set difference is exactly TWO files, both only in this branch and nothing only in the control:
runtime/mstats.csandruntime/windows/package_info.cs—readmemstats_m’s localstruct{Size uint32; Mallocs, Frees uint64}is the SAME Go type asMemStats.BySize’s, and now adopts that lift (MemStats_BySize) instead of declaringreadmemstats_m_bySizebeside it. The same collapse the behavioral goldens show, in the package whereReadMemStatscopies between the two — the identical latent hazardencoding/xmlactually hit. Applied into the real tree,runtimebuilds 0 errors; the corpus was then restored, so this commit carries no corpus change and the next full regen will pick these two up.
The other 51 are the documented CRLF phantoms — CR-stripped identical to the committed tree —
plus one that is not, and is not this lane’s either: crypto/internal/edwards25519/field’s
README emits the not_yet_validated badge where the committed file carries 16/16 validated. It
differs identically in the control, so it is a pre-existing badge-emission anomaly (every other
banked package’s README reproduced byte-for-byte under the same seeding). Worth someone’s attention;
not this one’s.
Gates
Converter go test ./... ok (138.5 s), with all six guards, each proven failing-first by neutering
its own fix · full CNR byte-identical across all 621 behavioral packages except the two intended
ones, 0 NOT MEASURED, 0 advisory warnings — and both intended packages re-baselined and green on
all four phases (Transpile/Compile/Target/Output), so the new emission matches go run as well as
its golden · seeded full-corpus reconvert plus a control reconvert against master’s converter,
above · runtime built clean with the one corpus change applied. No golib change, so no
GolibTests/behavioral-suite/go2cs.slnx gate is owed; no corpus regen banked, so no go generate
is owed.
Guards
All six fixes carry a guard in src/go2cs/assemblyScopedAlias_test.go, each proven failing-first by
neutering its own fix:
| Guard | Pins |
|---|---|
TestSeedProductionInterfaceAliasesFollowsPublishedChain |
the AST predicate (exactly the named-RHS declarations, never the inline interface{…} beside them), both seeded halves, the two-hop chain resolving to the alias the production compilation DECLARES, the recompile-model gate, and the _test.go-declared negative control |
TestFunctionLocalAnonStructAdoptsPackageLift |
the literal constructs the field’s lifted type, the package-level lift is declared exactly once, no second lift is minted — and a DIFFERENT signature keeps its own, so the reuse is identity-driven and not a blanket collapse |
TestPublishedAliasChainTerminates |
the chain’s four ends, including a self-referential published set (read from a file this run did not necessarily write) |
TestUnexportedProductionTypeReachedThroughWrappers |
the three new positions (generic type ARGUMENT, ALIAS, SIGNATURE) alongside the pointer that already worked, plus both negative controls |
TestComparableConstraintInterfaceEmitsMethodSetForm |
both sides of the embedded comparable agreeing, and the type-parameter slot’s record-vs-emission split in the same fixture |
TestTestDeclaredAliasSpelledBare |
the external-variant rule and each of the four clauses that must hold it back |
One of those is a PREDICATE guard rather than an emission guard, and the reason is worth carrying: the
qualified spelling netip.AddrDetail depends on which file-local package aliases a real conversion
happens to register, and three successive fixtures (same-package generic, foreign generic, bare var,
each with a dot import) all emitted the bare name for reasons unrelated to the fix — the guard passed
with the fix neutered every time. The arm’s decision was extracted to
testDeclaredAliasSpelledBare so the rule itself could be pinned; the EMISSION is covered by the
measured net/netip run. A guard that passes while its fix is neutered guards nothing, and noticing
that is the only reason this one exists in the shape it does.
MEASURED — the five one-diagnostic build blocks re-measured: ONE moved and its wall FELL, four stood, and crypto/ed25519 turns out to be internal/reflectlite (2026-08-18, lane claude/one-diagnostic-remeasure)
The harvest entry’s queue item 2 listed five packages “each unmeasured since scout batch 2 and each
therefore a candidate for having MOVED, as reflectlite’s just did.” All five are now re-measured on
current master (eda03743e). One moved; four are reproduced verbatim. The one that moved,
sync/atomic, moved because its recorded diagnostic was never its only one — and closing the two
defects behind it takes the package from build-blocked to running, 65 of 108 verdicts agreeing,
stopped by a single named behavioral root rather than by the compiler.
Nothing banks. The honest summary is one build wall closed, two general converter defects fixed with failing-first guards, and four walls re-priced with their current owners named.
The five, in the order they were run
| Package | Verdicts | Board had | Measured 2026-08-18 |
|---|---|---|---|
sync/atomic |
108 | CS0841 | MOVED, and the wall FELL. CS0841 is gone entirely; the build stopped instead on CS0103 ×12 + CS0201 ×2, both fixed below. The package now COMPILES and RUNS: 65 of 108 agree, 43 diverge on ONE root |
debug/elf |
31 | CS8183 | CS8183 ×1, unmoved. file_test.cs(1195): _ = net.ResolveIPAddr — a Go blank assignment from a bare func reference. C# cannot infer the type of an implicitly-typed discard from a method group |
flag |
24 | CS1929 | CS1929 ×10, unmoved — and the owner is NOT the converter. Every one is inside a go2cs-gen GENERATED adapter (Generated/go2cs-gen/go2cs.ImplementGenerator/go.flag_test_package.<T>-global__go.flag_package.Value-ptr.g.cs) |
internal/concurrent |
20 | CS0426 node<,> |
CS0426 ×1, unmoved. Plus a finding the board did not have: the converter CRASHES on this package’s hand-owned file |
crypto/ed25519 |
9 | CS0030 | CS0030 ×2, unmoved — and it is internal/reflectlite’s root, exactly. crypto.Signer(private) emits ((crypto.Signer)@private) |
The two converter defects sync/atomic was the first thing in the corpus to reach
Both are shapes the production corpus happens not to contain, which is why each survived to be found
by a Phase-4 measurement. Both are fixed, with failing-first guards in
src/go2cs/escapedArrayElementAddress_test.go.
1. The address of an element of a heap-escaped array LOCAL composed two box spellings. An array
local that a closure captures escapes to the heap and owns an identity box — ref var X = ref
heap<array<int32>>(out var ᏑX). C# cannot capture the ref alias that names its value, so every
reference inside the closure renders through the box instead. The element-address arm then prefixed
the address operator onto THAT already-boxed render, naming a box of a box that was never declared:
&X[me] inside a goroutine literal emitted ᏑᏑX.Value.at<int32>(me) — CS0103 — and &ack[me][i%3]
the same one hop deeper, because the nested-index router recognised only a SELECTOR inner base, never
a bare escaped ident. This is the same naive-prefix hazard the slice branch and the pointer-to-array
branch above it each already record, arriving at the one base class neither covered. The fix renders
the base in POINTER context to get the box and aliases the element through it — ᏑX.at<int32>(me),
Ꮡack.at<array<int32>>(me).at<int32>(k) — which is also what keeps writes through the returned
pointer landing in the escaped storage rather than in a copy: golib’s at materializes the backing on
the real storage and returns a pointer over the SHARED array.
2. A DISCARDED unsafe.Pointer result kept the cast that types a CONSUMED one. Any call whose
result type is unsafe.Pointer takes a (uintptr) construct prefix so the value converts. In an
expression STATEMENT nothing consumes the value, and a statement slot is the one place C# admits a
call but not a cast: Go’s func() { SwapPointer(nil, nil) } — a row in sync/atomic’s nil-deref table
— emitted (uintptr)SwapPointer(nil, nil);, CS0201. The suppression is keyed on AST-NODE IDENTITY,
not a boolean, so a call nested inside the same statement whose value IS consumed keeps its
conversion; the guard asserts both directions.
What stands behind them in sync/atomic — ONE root, and it is the copy-box
The 43 divergences are not 43 problems. Every one is a lost write through a boxed COPY of a non-escaped struct local:
delta=1 i=0 j=1 k=1 // TestAddInt32Method
x.i.Add(delta) returns the right value (k=1) and then x.i reads back 0. The emission is
Ꮡ(x).of(TestAddInt32Method_x.Ꮡi).Add(delta) — the one-arg Ꮡ(in T) overload boxes a COPY of the
local struct x, so the atomic mutates a temporary and the next occurrence makes a fresh copy. x
has no identity box because escape analysis never marked it, even though its field’s address is
taken. That accounts for all 35 *Method rows, TestAutoAligned64, TestHammer32/64,
TestLoadPointer/TestStorePointer/TestSwapPointer and TestValue_CompareAndSwap. The
seq-consistency tests the two fixes above were found in — TestStoreLoadSeqCst32/64 — PASS, which is
the positive control that the element-address fix aliases rather than copies.
This is the documented Ꮡ(value) limitation, and the remedy is an ESCAPE-ANALYSIS change (mark a
local whose field address is taken through a method call), whose blast radius is the whole corpus. It
is not a bounded single-package fix and this lane deliberately did not take it.
crypto/ed25519 and internal/reflectlite are ONE root, and the remedy is already written down
The harvest entry named reflectlite’s CS0030: “a Go conversion-to-interface written in CALL syntax
takes a raw C# cast … T(x) where T is an interface must route through convertToInterfaceType.”
crypto/ed25519’s two errors are that root verbatim — crypto.Signer(private) on
type PrivateKey []byte.
The gap is precisely locatable. convCallExpr.go’s interface-target conversion branch already routes
two source shapes through convertToInterfaceType: a POINTER source, and a FOREIGN named VALUE source
(pkg != v.pkg, the crypto.SignerOpts(sigHash) precedent). A LOCAL named value source falls
through to the plain cast, on the stated reasoning that a local type can be partial’d to declare the
interface. That reasoning holds for a local interface and fails for a FOREIGN one: nothing records the
[assembly: GoImplement<PrivateKey, crypto.Signer>] pair, so no adapter exists and the cast has
nothing to bind to.
Deliberately NOT taken here, for the same reason the x509-unlock lane declined model selection: the “no churn” boundary on local value sources is load-bearing and the change owes a corpus-wide re-proof, which is a different arc from two emission fixes. Two lanes have now converged on it — it is the cheapest unbanked root on this board that nobody owns.
flag’s owner is go2cs-gen, not the converter
Worth stating because it re-prices the row. All ten CS1929 are inside GENERATED adapter sources: the
ImplementGenerator adapter for a TEST-package type implementing the PRODUCTION flag.Value
interface cannot find the type’s own String method on the box, and binds bytes_package.String
instead. No converter emission is at fault, so the row belongs to whoever owns the generator’s
handling of test-package types — a different arc from every other row in this batch.
internal/concurrent — the recorded diagnostic stands, and there is a crash behind it
CS0426 node<,> reproduces exactly (hashtriemap_test.cs(406,145)). The finding the board did not
have is in the same run’s stderr:
WARNING: visit file error: runtime error: invalid memory address or nil pointer dereference
in "hashtriemap.go" (auto-conversion sibling skipped)
internal/concurrent is one of the three hand-owned-by-consequence packages (its entire single Go
file is hand-owned), so the crash is confined to the .cs.auto review sibling and cannot reach the
corpus — which is exactly why it has gone unnoticed. It is a converter nil-deref on a real stdlib
file and should be root-caused before the .cs.auto backlog (CleanupBacklog item 18) is levelled,
since that item’s whole purpose is refreshing the siblings this crash silently skips.
The measurement hazard this lane paid for
A -tests batch that runs CONCURRENTLY reports CS2012 as if it were a package diagnostic. The
first pass of this batch launched all four remaining packages at once (a PowerShell capture bug: the
launcher’s PID= line goes to the INFORMATION stream, so 2>&1 missed it, the wait was skipped and
the loop ran on). debug/elf and flag came back with CS2012 “cannot open file … being used by
another process” and no other error — which reads exactly like a real one-diagnostic build block,
in a batch whose entire premise is one-diagnostic build blocks. Re-run sequentially they report
CS8183 ×1 and CS1929 ×10. Two lessons, one already in CLAUDE.md and one not: capture repo-script
output with *>&1, never 2>&1; and treat CS2012 as contention, never as a root — it is the
build-lock analogue of the MSB4166 hazard the harvest entry recorded, and it fabricates precisely the
finding this kind of batch is looking for. All five results above are from sequential runs.
Gates
Converter go test ./... ok, 205.2 s, zero failures (includes projitemsIntegrity_test.go over
the newly registered guard file) · full check-no-regression.ps1 byte-identical across all 621 behavioral packages, nothing NOT MEASURED, 2 advisory converter warnings (1,060 s); solution integrity 623/623 and path casing 4,478/4,478 in its preflight · both guards proven failing-first by
neutering each fix in turn: the doubled prefix reproduces and (uintptr)swapPtr( reappears in a
statement slot. No golib change, so GolibTests/behavioral/go2cs.slnx are not owed. No roster row,
no proof page, no disclosures, no converted test sources committed: five measurements, zero
validations.
✅ THE ESCAPE/BOX-COPY ROOT IS CLOSED — an observed address always aliases; sync/atomic 65 → 99 of 108, encoding/xml 353 → 366 of 386, and the residuals belong to other owners (2026-08-18, lane claude/escape-box-copy)
Three entries named this root from three sides: the one-diagnostic-remeasure entry measured
sync/atomic’s 43-divergence residual (Ꮡ(x).of(…Ꮡi).Add(delta) mutating a temporary), the
xml-netip-alias entry measured the shipped Token() lost write (Ꮡ(t1).of(StartElement.ᏑName),
hand-boxed A/B +12) and censused the emitted shape at 6 sites in 4 files. The root is one
principle with two blind spots. Go’s answer to “when must a local’s address alias its storage
rather than copy” is ALWAYS — and go2cs already has both conforming mechanisms (Phase-A
ref-lowering aliases in place when every consumer lowers; the identity box serves everything
else), but two address-taking forms were invisible to the promotion analysis, so emission fell to
the documented last-resort Ꮡ(value) copy-box, which reads right and drops writes.
The predicate the xml lane could not identify, identified
That lane recorded three failed reductions and left “what forces the ж
The two blind spots, closed at the analysis
-
A capture-mode method called on a value-field CHAIN rooted at a local or value parameter —
x.i.Add(delta)is Go’s implicit(&x.i).Add(delta), and the escape trigger recognized the capture-mode call only on the var ITSELF (i.Store(10)).bodyCallsCaptureModeMethodOnObjectnow accepts a chain viaselectorChainRootsAtIdent— the explicit-&arm’s root walk, whoseSelection.Indirect()gate keeps pointer-crossing chains excluded — with the pointer-typed- operand exclusion mirrored fromselectsPointerMethodOn, the method-VALUE analogue of this call form. The analysis trigger and the emission-side re-verification (paramBoxReasonHolds) read the SAME function, so value parameters take the widening in the same motion. A non-capture-mode pointer-receiver call stays untouched: it bindsthis refon the field in place, and promoting for it would heap-box every local that calls any pointer-receiver method. -
Type-switch bindings join both analyses — the object-keyed
performEscapeAnalysisForObject(whichperformEscapeAnalysisnow delegates to) runs the standard walk for each case clause’s implicit var, andcensusFuncLocalstracks the same category, so a binding whose every address-connected use feeds a lowered position still REVERTS to a plain stack local — the lane’s reduction shapes emit byte-identically. Narrowed to non-inherently-heap bound types: the multi-type anddefaultarms always bind at the guard’s interface type, already a reference, whose no-entry state is load-bearing for the capture analysis. On the emission side a C# pattern variable cannot be a ref local, so an escaping binding binds the pattern to a uniquely-numbered temp and opens the clause with the entry-time box pattern proven by the escaping-parameter preamble andselectCommBinding—case StartElement t1ᴛ1: { ref var t1 = ref heap(t1ᴛ1, out var Ꮡt1);— gated onidentHasHeapBox, the exact predicate the body’s&-emission consults, so the box is declared iff it is referenced.
Measured
- sync/atomic: 65 → 99 of 108 (+34 — every one a
*Methodbox-copy row). The residual 9 sit in THREE roots, none of them escape-family, which is a correction to the one-diagnostic-remeasure entry: the 43 were not one root. (a) unsafe.Pointer machinery ×6 — the hammer*Methodvariants go throughᏑuaddr.Reinterpret<uint32, atomic.Int32>()and the view’s writes are lost (TestHammer32/64: AddInt32Method: val=0 want 400000); Go’s pointer-IDENTITY comparek != pis emitted ask.Value != p.Value, a deref-compare that nil-derefs on the nil probe (TestLoad/Store/SwapPointer— pre-fix it compared pointers-into-copies and failed “orderly”, so this fix EXPOSED it to full severity); and the pointer-hammer’s check now fires itsFatalffrom a goroutine after the test window, killing the host (TestHammerStoreLoad, “Log called after … completed”). Because of that death the full-pipeline compare records only 36 C# verdicts; the 99 figure comes from running the host directly with the hammer family excluded and diffing against the recorded go map — the direct- host method the xml lane’s wrong-turn note prescribes. (b) atomic.Value CAS type-identity ×2 —TestValue_CompareAndSwap/7+ parent panic “inconsistently typed values” where Go accepts. (c) reflect-alignment ×1 —TestAutoAligned64. No closure, no bank: no roster row, no proof page, artifacts removed. - encoding/xml: 353 → 366 of 386 (+13 — the twelve namespace rows the hand-box A/B predicted,
plus one, because the fix also boxes the SIBLING case bindings the A/B did not patch:
EndElement’s
d.popElement(Ꮡt1)and marshal/read’s whole-binding copy-boxes). The 20 residual divergences are EXACTLY that lane’s other four roots:reflect.canonTypeterminates the process (15 verdicts EMPTY, not failed),CopyTokensame-buffer ×2,TestMarshal/47arity-mangled generic name,TestMarshal/64embedded-field order, plus theTestMarshalparent. No closure, no bank; artifacts removed and the tracked-testsdirt restored (the three production movers plus theinitᴛᴛtestspackage_init.cshook, the fourth-tests-closure shape). - Seeded full-corpus reconvert: 1,656 identical / 6 movers / 0 new; marker gate 63 marked
files, 0 violations; converter stderr 0 type-check failures. The movers:
encoding/xml/{marshal,read,xml}.cs+go/constant/value.csare this fix — xml.cs:325 is THE shipped lost write, nowcase StartElement t1ᴛ1: { ref var t1 = ref heap(t1ᴛ1, out var Ꮡt1); d.translate(Ꮡt1.of(StartElement.ᏑName), true);; marshal/read move whole-bindingᏑ(t)/Ꮡ(tΔ1)copy-boxes to aliases (reads, now correct by construction); go/constant’scase string: return &stringVal{s: x}joins the composite-literal gray-area rule ordinary locals already have (one extra box alloc, correctness unchanged) — andruntime/mstats.cs+runtime/windows/package_info.csare the xml-netip-alias lane’s pre-documented anon-struct-lift adoptions. All six verified compiling (go2cs-stdlib.slnx0 errors, 6:35) and then RESTORED per corpus-regen policy; the next leveling regen picks them up. The 6-site census held exactly: only xml’s site was a write and only the xml/go-constant sites move —netip.cs:385/391andrpc/debug.cs:99are VALUE-RECEIVER rooted (the receiver path is deliberately untouched; Go itself reads a receiver copy there, so no observable divergence), andruntime/symtab.cs:1102/1146cross a POINTER EMBED (Selection.Indirect()exclusion — the address correctly lands in the shared pointee).
Guards
TypeSwitchBindingAddress — the xml shape through a ж-parameter METHOD, a held p := &t1.n
pointer, the direct &t1 form, and the already-correct slice-element control &t1.attr[i] —
and CaptureModeFieldAddress — local, value parameter, type-switch composition, lifted anonymous
struct, and the non-capture-mode control w.c.inc(). Both proven failing-first against the
pre-fix converter: a x/z w where Go says urn:a x/reset w; local: 5 0 0 0 / param: 0 /
switch: 0 / anon: 1 where Go says 5 7 0 0 / 7 / 9 / 8; controls identical on both
sides. Doctrine recorded in ConversionStrategies-Reference as two new sections beside the
explicit-& selector arm’s.
Gates
Converter go test ./... ok twice — 144.0 s pre-merge with the fix, 142.8 s after both feeder
merges, all guards included · full CNR byte-identical across all 624 behavioral packages except
ONE intended mover, StructPointerPromotionWithInterface — itself the param-arm of this fix
catching a latent copy-box (probeRig(r rig) calling capture-mode Tag() on r.dev; reads
only, so its output matched by luck) — re-baselined and re-verified on all four phases · seeded
reconvert-and-build above · full behavioral suite 597/597 Transpile+Compile+Target, 571/571
Output (26 skipped, no package main), 0 failures, 4,070 s. No golib change, so
GolibTests/go2cs.slnx are not owed. This branch carries both feeder lanes as merges
(8f63c1dde, dd11e1e35) — their entries above land with it.
✅ BANKED — crypto/ed25519 validates 8/8 (+1 disclosed) behind the local-value cast fix; internal/reflectlite’s compile wall was THREE roots, all closed, and the package now RUNS into the reflection mini-bridge (2026-08-18, lane claude/local-iface-cast)
The cheapest unowned root two lanes converged on — a LOCAL named value source with an interface
target falling through convCallExpr’s conversion branch to a plain cast that records nothing —
is closed the way the one-diagnostic-remeasure entry priced it, and the boundary it retired turned
out to be misreasoned rather than load-bearing: a local type CAN be partial’d to declare the
interface, and that is exactly why the route matters, because the partial is go2cs-gen’s, minted
from the [assembly: GoImplement<T, Iface>] record the plain cast never wrote. Framed by SYNTAX
it is one rule: Iface(x) and var i Iface = x are the same Go conversion and now take the same
route. For a local non-func value source the route is RECORD-ONLY — the corpus re-proof measured
the claim rather than arguing it (seeded reconvert, 1,668 emitted, 0 real differences, 0 new; 63
marked hand-owns, 0 clobbered) — and the pairs it newly records are precisely the two shapes the
speculative recorder declines: a FOREIGN interface (recordSamePackageImplements pairs two locals
only) and an UNEXPORTED local one (its exported gate). Guards failing-first at both levels
(TestLocalValueIfaceCallConversion; behavioral LocalValueIfaceCallConversion, all four phases).
crypto/ed25519 — banked, 151/215 (70.2%)
First attempt at the wall: the suite converts, builds and runs 8 of 9 verdicts matching go test
-json — sign/verify plain/ph/ctx, crypto.Signer through the interface surface (the cast that
named the defect), RFC 8032 golden vectors, the extended edge-case set, equality, malleability.
The ninth is TestAllocations, want-zero AllocsPerRun over a loop that derives keys and signs —
disclosed alloc-profile, the bytes/strings precedent verbatim. Roster header recomputed from the
table: 151/215, 17,272 matching, 77 disclosed; proof page converter-generated at 0f3495688.
internal/reflectlite — the wall was three deep; the compiler is no longer the owner
Closing CS0030 exposed the harvest entry’s CS8130 as TWO defects, and a third behind both. All
three are general, all guarded failing-first (packageLevelAnonStructLift_test.go):
- Package-level anonymous-struct lifts now dedupe by structural identity within a file — the
function-scoped dedup (TestSizeStructCache) extended to package scope with an explicit scope
discriminator (
currentFuncNameis never reset after a FuncDecl and would have leaked). Two package vars over one written anonymous struct are one Go type; splitting them madeappend(assignableTests, implementsTests...)un-unifiable (CS9244 + CS8130 ×2). Corpus footprint: exactly ONE production site —reflect’sfuncLookupCache/structLookupCachepair, the same shape in production clothing, adopted with the fix (reflect builds clean; behavioral corpus byte-identical 623/623; stdlib-metadata in sync). The recorded residual narrows to cross-FILE and cross-SCOPE splits. - A whitebox-production type is not a LOCAL operand for a
GoImplicitConvrecord.obj.Pkg() == v.pkgread productionflagas local (go/packages merges the variants), but its C# is the closed referenced assembly, so the record minted the phantompartial struct flagwhose.Valuedoes not exist — CS1061, the exact shapeconversionRecordHasLocalOperandalready declines for the both-foreign pair.whiteboxProductionObjectnow excludes it; declining costs nothing (the cast site’s explicit chain needs no operator, and production’s own package_info.cs carriesGoImplicitConv<flag, abiꓸKind>). Option-gated: non--testsconversions untouched by construction.
With the wall down the package RUNS: 5 of 30 agree (TestImplicitSetConversion,
TestNilPtrValueSub, TestPtrSetNil, TestTypeOf, TestUnaddressableField), 2 skip-parity, and
all 23 remaining failures stop in the reflectlite reflection CORE — packEface nil-deref
(value.cs:125), descriptor field walks (Field index out of bounds/out of range through
export_test’s Field/TField), bad indir (value.cs:116). That is the mini-bridge the board
already records as “only ever landed Len/Swapper” — the reflection-bridge arc owns this
package now, not a converter lane. Nothing banks; no test artifacts committed.
A third call-syntax gap, measured and deliberately not taken
The behavioral guard’s negative case measured the INTERFACE-source twin of the same family:
valued(d) with d an interface emits a plain cast that throws InvalidCastException at runtime
where var dv valued = d builds the describedᴠvalued adapter — same
call-syntax-skips-the-route class, but the route is recordableInterface’s adapter-wrapping
emission, a wider change owing its own re-proof. The guard pins the position UNTOUCHED so a later
widening cannot move it silently; whoever takes it inherits a measured repro.
The measurement hazard this lane paid for
A stale machine-global ~/go2cs deploy root hijacks the canonical two-arg -tests form.
Self-location defers to a CONFIGURED root that exists, and the default -go2cspath (~/go2cs) IS
one wherever deploy-core ever ran — on this laptop that root carries its own circular-restore
defect (MSB4006 in its unsafe.csproj), and the pipeline reported it as the mass-empty
Go="pass" C#="" verdict, reading exactly like total conversion failure. The harness rule
generalizes: pass an EXPLICIT -go2cspath <repo>\src on every pipeline invocation from a repo
with work in flight; the two-arg form is only canonical on a machine with no deploy root.
The ranked queue, updated
debug/elf(31) — CS8183 ×1 (_ = net.ResolveIPAddr, implicitly-typed discard of a method group), reproduced verbatim 2026-08-18 by the sibling lane and untouched here: the next bounded one-diagnostic item.encoding/xml(386) /net/netip(266) — the assembly-scoped-alias CS0426 root, ranked ahead on verdict mass by the harvest entry, unmoved.internal/reflectlite(30) — behind the reflection-bridge arc now;runtime/debug(9) still unmeasured since the CS0715 retirement.
Gates: converter go test ./... ok twice (219.3 s / 193.2 s) · full CNR byte-identical across all
623 behavioral packages twice (0 NOT MEASURED both runs) · TWO seeded whole-stdlib reconverts, 63
marked / 0 clobbered both, every real mover named · reflect single-package build 0 errors ·
solution integrity 625/625, path casing 4,485→4,492. Banked: 0f3495688 (cast fix + guards),
ed25519 bank, e61758549 (reflectlite’s two remaining roots + reflect adoption).
✅ internal/reflectlite VALIDATES 27+3/30 — the mini-bridge was seven roots, five of them SHARED machinery; encoding/gob 100 → 103 of 106 and its census halves (2026-08-18, lane claude/reflect-minibridge)
The reflect mini-bridge pair, taken as one lane so no parallel work touched the bridge. Both prompt targets measured; one banks.
Base note — the lane merged its own dependency
The prompt expected master > d1ed1f7c1 (the local-iface-cast merge). That commit was never
pushed: remote master stood at b5a82df19 and the reflectlite unlock lived only on
origin/claude/local-iface-cast-747795. This lane merged that branch onto master itself
(36d06ab20) — and the merge surfaced a REAL two-lane conflict: the local-iface-cast lane’s
anon-struct scope-dedup (“scopes never unify”) and the xml-netip-alias lane’s package-registry
ADOPTION path compose in git but contradict in one case, the function-local literal of a
package-lifted anonymous struct. The composed behavior (adoption wins) is the more Go-faithful
one — anonymous-struct identity is scopeless — so the guard’s scope-separation assertion was
amended to assert adoption (d8f71750e). Coordinator: when merging local-iface-cast, take this
lane’s reconciliation with it (or merge this lane, which contains it).
internal/reflectlite — banked, 152/215 (70.7%)
5 of 30 at handoff; 27 matched + 2 skip-parity + 3 disclosed (chan-direction) = 30, status
validated, proof page converter-generated. Roster header recomputed from the table:
152/215, 17,299 matching, 80 disclosed. The 23 mini-bridge failures decomposed into SEVEN
roots — five landed in shared machinery both reflection layers (and every bridge consumer)
inherit:
| Root | Fix home | Guard |
|---|---|---|
GoFields walked CLR METADATA order — an embed’s generated backing field lands LAST where Go declared it first; offsets pair by index and reorder with it |
golib GoReflect.FieldAccess (reorder by the generator’s all-fields ctor, exactly when an embed is present) |
GoStructLayoutTests.FieldOrder_IsGoDeclarationOrder_NotMetadataOrder, failing-first |
The structural implements probe lacked Go’s UNEXPORTED-METHOD package rule — *reflectlite_test.notAnExpr “implemented” ast.Expr through its own private exprNode |
golib TypeExtensions.GoMethodSets + new GoReflect.GoPackageClassPath |
GoUnexportedMethodPackageTests (3 rows), failing-first |
| A nil FUNC into interface space erased its type (delegate nil IS null) | golib CanonicalNilFunc/NilFuncValue, minted only at the eface boundary, resolved away on every read-back |
PointerNilPredicateTests.CanonicalNilFuncCarriesItsTypeAndResolvesAwayOnEveryReadBack |
reflectlite’s IsNil lacked the generated == nil operator probe (nil slice/chan fields answered NOT nil) |
isNilGoValue moved to golib as GoReflect.IsNilGoValue; one nilness, three readers |
reflectlite TestIsNil (operational) |
| TryTypeAssert’s dynamic-struct copy arm: public-only GetField NRE’d on unexported fields, and SetValue on the unboxed struct wrote into a transient box (zeros) | golib builtin.cs |
reflectlite TestBigUnnamedStruct (operational) |
Two converter emission defects: a PARENTHESIZED array field type dropped = new(N) (missing ast.Unparen), and the delegate lowering ate a bare chan parameter as a NAME (func(chan *integer, *int8) → Action<ж<integer>, ж<int8>>) |
converter visitStructType.go / typeSignatureParsing.go (the leading token is a name only when it is a plain identifier that is not a type-leading keyword — now shared with convertToCSResultList) |
structFieldEmission_test.go |
The reflectlite MIRRORS: valueInterface (mirrors packInterfaceValue), rtype.PkgPath (over GoPackagePath), the AssignableTo retirement to Go’s literal body over bridged implements+haveIdenticalUnderlyingType, and the export_test surface Field/TField/Zero |
reflectlite type_impl.cs/value_impl.cs + export_impl_test.cs — the first TEST-file hand-own, carried by the new *_impl_test.cs convention (_test.cs suffix rides the existing production exclusion; testConversion globs it into the tests project and digest) |
the banked suite itself |
Naming gains shared by both layers (GoTypeDefinednessTests rows, failing-first): generic
instantiations render Go’s bracket form with IMPORT-PATH-qualified args
(B[internal/reflectlite_test.A]), an anonymous-INTERFACE lift renders structurally, and a
pointer descriptor’s dims thread to the pointee (*[10]int).
The gob IsNil root closed on the way (it is the same bridge): reflect.Value.IsNil on an
INTERFACE-kind value answered about the POINTEE — IsZero for an interface IS IsNil, so gob’s
!sendZero && v.IsZero() skipped the field and its “nil pointer inside interface” error path
was unreachable (carried since r39). The interface arm now answers the interface itself; guarded
by the ReflectTypedNilInterface behavioral shape (both directions + nil-interface control),
proven failing-first.
The three disclosed rows are ONE representational limit, and it now has measured repros
Chan DIRECTION is not in the managed type (channel<T> whatever the direction — the reference
doc’s standing ruling). TestAssignableTo’s <-chan int → chan int row and
TestTypes/TestSetValue’s chan<- string stringification are pinned as class chan-direction.
Whoever takes the direction-as-descriptor-cargo arc the doctrine names inherits three measured
repros and retires the class.
encoding/gob — 100 → 103 of 106; the census HALVES, and it does not bank
Re-measured with everything above in (-test-timeout 12m; the first attempt’s default-deadline
run timed out under concurrent load and reported the tail as mass-empty verdicts — reread the
CLAUDE.md hazard before believing one). Of the four recorded roots, TWO closed:
TestNilPointerInsideInterface (the IsNil arm) and — unowned on the board, closed by this arc’s
shared fixes — BOTH decoder-IGNORE rows (TestBadData #8, TestIgnoreRecursiveType). The two
that survive are the already-recorded owners:
| Root | Tests | State |
|---|---|---|
the array<T> LENGTH class (“gob: length mismatch in decodeArray”; “wrong type ([3]int) for received field Indirect.A”) |
TestEndToEnd, TestIndirectSliceMapArray |
reflect bridge — chip, standing |
reflect.ArrayOf → the typelinks stub |
TestIgnoreDepthLimit |
infrastructure-error, standing |
No gob artifact is committed (100→103 is measurement, not a bank).
Gates
Converter go test ./... ok, 145.9 s, zero failures (post-merge guard subset re-proven
against the coordinator’s reconciliation) · full check-no-regression.ps1 byte-identical
across all 625 behavioral packages, 0 NOT MEASURED, 2 advisory warnings, exit 0 · seeded
whole-stdlib reconvert: 63 marked / 0 clobbered, SIX movers — all intended consequences of
the two merged converter arcs composing (xml ×3 + go/constant take the escape-box heap form at
case-var addresses the cast recorder now observes; runtime retires readmemstats_m_bySize for
the package-level MemStats_BySize under the reconciled adoption rule), zero movers from this
lane’s own emission fixes, all three packages build clean · GolibTests 155/155 ·
stdlib-metadata in sync · solution integrity 627/627, path casing 4,506 · full behavioral suite
598/598 Transpile+Compile+Target, 572/572 Output (26 skipped, no package main), 0
failures, 4,473 s — run AFTER the master merge, so it re-proves the union tree · go2cs.slnx
Build succeeded, 0 errors (the golib-change gate) · sweeps, all -SkipBuild against the
merged tree’s binary: internal/reflectlite PASS 27 (37 s — the bank’s closure), fmt PASS
63 + internal/fmtsort PASS 3, encoding/json PASS 491, go/types PASS 557 — every banked
bridge consumer named as a canary re-validates. Guards proven failing-first
where stated (field order, unexported-method rule, both naming arms, the behavioral IsNil shape,
both emission guards).
The ranked queue, updated
encoding/gob(106) — 103 of 106 behind thearray<T>-length class (2 tests) + thetypelinksstub (1); the closest unbanked package on this board.- The chan-direction cargo arc — three pinned repros in reflectlite’s disclosure manifest; retires a disclosure class.
debug/elf(31) — CS8183 ×1, unmoved, still the next bounded one-diagnostic item.
MEASURED — both template packages’ CS0030 wall is GONE; text/template RUNS at 38 of 52 behind ONE named bridge stub, html/template’s new wall is two roots and this lane closes one (2026-08-19, lane claude/row-harvest-2)
The harvest entry’s queue listed html/template (243) and text/template (52) behind “a CS0030 that
may or may not be the same family” as encoding/xml’s. It was the same family: the local-iface-cast
merge closed it in both, unmeasured, exactly as this lane’s brief predicted. Both are re-measured on
current master (e01780c28). Neither banks, and the honest summary is one compile wall gone in
two packages, one general converter defect found and fixed with a failing-first guard, and the
residual in each priced with its owner named.
text/template — the wall fell, and what was behind it was a ONE-LINE datum loss
52 verdicts. CS0030 is gone entirely; the package compiles and RUNS. First measurement:
36 of 52 matching, 16 divergent — and all 16 were ONE panic, at funcs.cs:95:
panic: 0x211163e3340
at go.text.template_package.addValueFuncs(map`2 out, FuncMap in)
That is addValueFuncs’s throw panic(err) — goodFunc REJECTING a function Go accepts. The
message could not be read because the panic value rendered as an ADDRESS (see the second finding
below); instrumented, it says:
GOODFUNC-DIAG name=die msg=function die has 0 return values; should be 1 or 2
GOODFUNC-DIAG name=doPanic msg=function doPanic has 0 return values; should be 1 or 2
Go’s die is func() bool { panic("die") } — one result. The emission was
["die"u8] = () => { throw panic("die"); }, and a parameterless C# lambda with no reachable
return NATURAL-TYPES to Action. The Go result type was gone, so the reflection bridge answered
NumOut() == 0 — truthfully; the datum was missing from the emission, not from the bridge — and
text/template’s own goodFunc rejected every FuncMap holding such a literal, panicking as the map
was registered.
The defect is general, and the argument position had already been fixed
A func literal in an empty-interface slot has no delegate target, so C# derives the delegate from
the body. CallExprContext.emptyInterfaceArgs → LambdaContext.untypedInterfaceTarget already
stated the declared Go result type explicitly for the ARGUMENT position (testing/quick’s
TestFailure #3). The KEYED COMPOSITE forms — a map[K]any value, an any struct field, a
sparse-[N]any element — are the same slot reached through convKeyValueExpr instead of
convExprList, and were never marked. Two arms, both fixed:
| Arm | Symptom | Fix |
|---|---|---|
| SINGLE result, body never completes normally | no return to infer from → C# infers Action, result type lost outright |
convKeyValueExpr.go marks the slot once valueSlotType is resolved, so all three keyed forms are covered by one predicate |
| MULTI result | every arm a C# tuple with a typeless element ((i - 1, default!), (default!, fmt.Errorf(…))) → no arm fixes the delegate, CS8917 + CS1662/CS8716 per return |
convFuncLit.go states the declared result tuple through generateResultSignature, the helper the generic-inference arm already used |
The multi-result arm retires the reference doc’s standing “no demonstrated consumer” caveat —
html/template’s escape_test is the consumer it was waiting for.
A slot with a CONCRETE func type (map[string]func() bool) HAS a delegate target and is
deliberately untouched; that control is asserted in the guard and stayed green under both
neuterings.
text/template after the fix: 38 of 52, and the residual is ONE stub
36 → 38 of 52. The fix’s own yield is only TWO verdicts, and that is worth stating plainly: the registration panic blocked all 16, but a SECOND wall stands immediately behind it, so thirteen of the sixteen simply advanced from one root to the next. The 14 remaining divergences census to two owners, and thirteen of them are one:
| Root | Verdicts | State |
|---|---|---|
reflect.Value.Call: variadic func values are not implemented |
13 | the stub’s own message names text/template as its “next consumer” — that is now this measurement |
TestIssue43065 |
1 | a different panic, unexamined |
The variadic stub is NOT a bounded member fix and this lane deliberately did not take it. The
obstacle is structural: a converted Go variadic lowers to a params Span<TArg> delegate
(golib variadic.cs, 18 closed shapes across the Funcꓸꓸꓸ/Actionꓸꓸꓸ families), Span<T> is a
ref struct, and a ref struct cannot be boxed into the object?[] that Delegate.DynamicInvoke and
MethodInfo.Invoke both require. Implementing it means typed dispatch — one trampoline per
family arity, plus rebinding a natural-typed variadic delegate onto its family type via
Delegate.CreateDelegate — which is a feature arc with golib gates, not a member fix.
Second finding, recorded and NOT fixed: panic(err) prints an ADDRESS where Go prints the message
PanicException renders its state as state?.ToString(), so a panic carrying a converted Go
error prints panic: 0x211163e3340. Go’s runtime does not: preprintpanics replaces an error
panic value with v.Error() and a Stringer with v.String() BEFORE printing. This cost this
lane a diagnostic round-trip on the only defect it was chasing, and it degrades every panic
traceback in the corpus that carries an error. The remedy is small and has an exact Go precedent,
but it is a golib change (GolibTests + full behavioral + go2cs.slnx) with corpus-wide output
reach, so it is recorded here rather than taken alongside a converter fix.
html/template — CS0030 also gone; the new wall is TWO roots and this lane closes one
243 verdicts, still BUILD-BLOCKED, but on a different and much shorter wall: 9 errors in 2 roots.
| Root | Diagnostics | Owner |
|---|---|---|
A. FuncMap could not be found |
CS0246 ×6 | the board’s ASSEMBLY-SCOPED-ALIAS class. type FuncMap = template.FuncMap emits as a production-file global using FuncMap = go.text.template_package.FuncMap;, which is scoped to ONE compilation; the tests project is reference-model (its <Compile> set is *_test.cs only), so the name does not cross. seedProductionAliasLifts exists for exactly this and is deliberately NARROW — it seeds only aliases whose RHS is an ANONYMOUS struct/interface, on the stated reasoning that “a named RHS already renders through its own qualified name”. This alias has a NAMED RHS and that reasoning does not hold for it: the test half renders the bare FuncMap, not the qualified templateꓸFuncMap the test’s own metadata file does declare. Whether the fix is widening the seed (the doc warns of test-local collisions) or making the renderer honor the stated invariant is a real choice, NOT taken here |
B. the multi-result any-slot literal |
CS8917 ×1, CS1662 ×2, CS8716 ×2 | closed by this lane (the arm above) |
Root A is unrelated to Root B, so html/template stays build-blocked — but the wall is now ONE root
instead of two, and that is re-measured rather than argued: with the fix in, the build reports
6 errors, all of them CS0246 FuncMap (clone_test 277, escape_test 938, exec_test 799/1491/
1519/1949). Root B’s five are gone. The residual behind Root A is unmeasured — but html/template
wraps text/template, so the variadic-Call stub is predicted to stand behind it as well.
encoding/gob’s array<T>-length root, READ but NOT RUN — the zero-value ladder does not reach it
This lane did not get a gob pipeline run in, so nothing below is a measurement. What it is is a located shape, and it corrects the standing assumption that the root is a map-index miss the zero-value ladder can close.
The array-valued map in TestEndToEnd is T1.Marr map[[2]string][2]*float64 — key AND value
are arrays. But gob never INDEXES that map on the decode path: decodeMap calls
allocValue(mtyp.Elem()), i.e. reflect.New over the ELEMENT DESCRIPTOR, and decodeArray
then takes its length from t.Len() (decode.go:856). So the loss is in the descriptor, not in
map<K,V>’s default! miss:
// abi type_impl.cs, Elem() — a MAP descriptor carries no arrayDims, so its element gets none
nint[]? dims = Ꮡt.Value.arrayDims;
nint[]? elemDims = kind == Pointer ? dims : dims is { Length: > 1 } ? dims[1..] : null;
A map descriptor has no dims cargo of its own, so Key()/Elem() hand down null and the
array descriptor answers Len() == 0 — hence “length mismatch in decodeArray”. The
zero-value ladder (GoZero<T>(T template), IGoZeroShaped) cannot help because it recovers
shape FROM a live value, and at decode time the target map is empty: there is no template
anywhere. The durable remedy is the one the func-parameter case already established — carry
the dimension as descriptor CARGO ([GoArrayDims]/funcParamDims’s map twin: key and elem
dims stamped by the converter), which is a converter + golib + abi arc, not a member fix.
The ranked queue, updated
reflect.Value.Callvariadic dispatch — now the single named owner of 13 oftext/template’s 14 residual verdicts, and predicted to stand behindhtml/template’s 243 as well. A feature arc, not a member fix: the tail isparams Span<TArg>,Span<T>is a ref struct, and no reflective invoke path (DynamicInvoke,MethodInfo.Invoke) can box one. Needs typed dispatch across the 18Funcꓸꓸꓸ/Actionꓸꓸꓸshapes plusDelegate.CreateDelegaterebinding for natural-typed variadics. Two packages, ~295 verdicts, one owner.html/template’s Root A — the named-RHS type alias across the reference-model test boundary;seedProductionAliasLifts’s narrowness is the decision to revisit, and it is 6 errors from a 243-verdict measurement.panic(err)renders an address, not the message — golibPanicException; Go’spreprintpanicsis the exact precedent. Small, with corpus-wide traceback reach.encoding/gob(106) — unchanged at 103 of 106; still the closest unbanked package.
Gates
Converter go test ./... ok, 155.2 s, exit 0, zero failures (includes the new
projitemsIntegrity registration and both guard arms) · full check-no-regression.ps1
byte-identical across all 625 behavioral packages, nothing NOT MEASURED, 2 advisory converter
warnings, exit 0, 720 s; preflight solution integrity 627/627 and path casing 4,506/4,506 ·
seeded whole-stdlib reconvert 63 marked / 0 clobbered, 1,661 emitted artifacts identical,
ONE mover — go/internal/gcimporter/gcimporter.cs, the multi-result arm firing on
sync.Map.LoadOrStore(pkgDir, func() (string, error){…}), an any ARGUMENT slot rendering
(@string, error) () => …; verified compiling, 0 errors 0 warnings, then RESTORED per
corpus-regen policy for the next leveling regen. A second reported difference,
html/template/package_init.cs, was an artifact of seeding from this lane’s own -tests-dirty
tree (the initᴛᴛtests hook), not a mover · guards proven failing-first by neutering
each arm in turn — the single-result arm reproduces ["die"u8] = () => and the multi-result arm
reproduces the untyped (params ꓸꓸꓸany aʗp) =>, with the concrete-slot CONTROL staying green under
both · operational proof beyond emission: the converted probe prints 1 1 / 2 1 1, identical
to go run, so NumOut() agrees with Go for the panic-only literal, the normal-return literal, the
multi-result variadic, the any struct field and the concrete-slot control alike.
No golib change, so GolibTests/behavioral/go2cs.slnx are not owed. No roster row, no proof page,
no disclosures, no converted test sources committed: two measurements, one converter fix, zero
validations.
SHIPPED — the variadic reflect.Value.Call stub is GONE; text/template goes 38 → 49 of 52, and a panic value stops printing its address (2026-08-19, lane claude/variadic-call)
The queue’s #1 and #3 rows, taken together because they are the same layer and share one gate set.
Both landed with failing-first guards; neither package banks, and the reasons are measured rather
than assumed. Zero converter changes in this lane — git status src/go2cs is empty — so no
emission moved, no CNR or converter go test is owed, and every diagnostic quoted below is master’s
own behavior.
Row 1 — reflect.Value.Call’s variadic path, via typed dispatch
The board priced this as “a feature arc, not a member fix”, and that reading held: the obstacle is
structural, not a missing descriptor read. A converted Go variadic lowers its tail to
params Span<TArg>; Span<T> is a ref struct; Delegate.DynamicInvoke and MethodBase.Invoke
both marshal through an object?[] a ref struct cannot enter. The third exclusion is the one the
board’s plan did not name and it removes the obvious fallback: System.Linq.Expressions rejects a
byref-like type outright, so increment 3’s Expression.Lambda receiver binder — the machinery that
already builds arbitrary delegates in this bridge — cannot serve here either.
So the call is made in TYPED code (GoReflect.InvokeVariadic, golib GoReflect.TypeLayout.cs, beside
TryFuncShape whose header already claimed Value.Call’s marshalling): eighteen small generic
trampolines, one per family arity, closed over the delegate’s own parameter types by
MakeGenericMethod and cached as ordinary delegates — the elementBoxViaAt idiom
GoReflect.FieldAccess.cs already uses, so this EXTENDS the bridge’s existing machinery rather than
forking a second dispatch tier. Inside a trampoline the tail is a TArg[] and its conversion to
Span<TArg> is ordinary, so nothing is boxed and the tail ALIASES the caller’s array.
The rebind is where the plan needed correcting, and the correction is what makes it total. The
board’s design said to rebind a natural-typed variadic onto its family with
Delegate.CreateDelegate. That is right, but binding the delegate’s own Target/Method — the
obvious form — throws ArgumentException: MethodInfo must be a runtime MethodInfo object for a
delegate the BRIDGE ITSELF built: Value.Method binds a receiver by compiling an expression lambda,
and a compiled lambda’s Method is not a runtime MethodInfo. A variadic method value is exactly
that shape, and it is in the corpus. Retargeting through Invoke
(Delegate.CreateDelegate(familyType, del, "Invoke")) has no such blind spot, needs no special case,
and carries a multicast invocation list intact — so it is the ONLY rebind, not a fallback.
Three delegate identities therefore all work: the golib family type (a declared variadic func type),
C#’s natural type (a variadic literal in an any slot — the FuncMap shape), and an
expression-compiled one (a bridge-built method value). A fixed prefix beyond the family’s eight
throws a named NotImplementedException rather than mis-indexing; a non-variadic delegate arriving
here is refused by name.
text/template: 38 of 52 → 49 of 52. The stub owned 13 verdicts; eleven now match, and the
other two (TestComparison, TestExecute) advanced to roots that were standing behind it. The
residual three verdicts census to four roots, none of them this arc’s:
| Root | Where | State |
|---|---|---|
Value.Index / Value.Slice over a Kind-STRING Value |
TestComparison, part of TestExecute |
Go supports both on a string; the bridge answers reflect: call of reflect.Value.Index on string Value. Bounded and unowned |
| chan DIRECTION lost in the descriptor | TestIssue43065, part of TestExecute |
surfaces as panic: reflect: recv on send-only channel in walkRange — the board’s already-RATIFIED fourth disclosure class, self-retiring on descriptor cargo |
| typed-nil method dispatch | part of TestExecute |
(*W)(nil).Error() renders -<nil>- where Go renders -nilW-; html typed nil renders <no value> for <nil> |
three-index Value.Slice3 |
part of TestExecute |
<slice .SICap 6 10 10> → invalid memory address or nil pointer dereference |
TestIssue43065 is no longer “a different panic, unexamined” — it is the chan-direction class, which
means the board’s own disclosure ruling already owns it.
CallSlice’s named next consumer is retired as measured WRONG. Its stub named text/template;
text/template’s safeCall reaches Go through fun.Call(args) (funcs.go:375) and never
CallSlice, and a GOROOT-wide census finds no other caller at all. The machinery it would need now
exists; it stays a stub for want of a consumer, not for want of a way.
Row 2 — a panic value renders through Go’s preprintpanics rule
Recorded by row-harvest-2, fixed here. PanicException rendered state?.ToString(), so a converted
panic(err) holding a pointer-held error printed its ADDRESS. The guard reproduces the defect
exactly as that lane reported it — neutered, the behavioral test’s first stderr line is
panic: 0x25bf089bd28 against Go’s panic: open final.txt: code 13.
Go’s rule substitutes BEFORE printing: an error panic value becomes its Error(), a Stringer
its String(). Both arms are implemented; the Stringer arm is not redundant with ToString(),
because a Go named type’s generated ToString() forwards to its UNDERLYING value (go2cs-gen’s
InheritedTypeTemplate), so panic(2 * time.Second) would print 2000000000 where Go prints 2s.
The method is found through the extension-method registry — where a converted Go method lives —
the way golib’s error<T> already finds Error.
Computed on FIRST READ, not at construction, because that is when Go computes it: preprintpanics
runs only once a panic has gone unrecovered and is about to print. Eager substitution would call
user Error()/String() on every recovered panic in the corpus (fmt’s catchPanic,
text/template’s errRecover, every defer func(){ recover() }()), which Go does not do.
recover() still hands back the value itself — the substitution is a printing rule, not a value
rewrite. Both readers of a panic value now go through it: the unhandled-exception backstop, and
debug.Stack’s panic line in runtime/managed_impl.cs, which carried its own copy of the old
rendering.
html/template — still build-blocked, and the wall is 7 errors in 2 roots, not 6 in 1
Re-measured on this tree: 243 Go verdicts, 36 excluded, 0 C# verdicts — conversion-blocked.
Root A is unchanged and was not taken here (it is the board’s own “real choice”, and this lane had
banked its two rows). Root C is NEW, and it is a genuine census finding rather than a regression:
| Root | Diagnostics | Owner |
|---|---|---|
A. FuncMap could not be found |
CS0246 ×6 | unchanged — the named-RHS assembly-scoped alias across the reference-model test boundary. [assembly: GoTypeAlias("FuncMap", "go.text.template_package.FuncMap")] IS published, so seedProductionAliasLifts’s “published” precondition is already met; only its anonymous-RHS filter blocks it. Widening that filter vs. making the renderer qualify is still the open choice |
C. defer of a VARIADIC func literal |
CS0411 ×1 | examplefiles_test.cs:113. defer func(dirs ...string){…}(dir1, dir2) emits defer((params ꓸꓸꓸstring dirsʗp) => {…}, dir1, dir2, ref ᒐ), which cannot infer against builtin.defer<T1,T2>(Action<T1,T2>, T1, T2, ref GoFrame) |
Root C is the first demonstrated consumer of a residue ConversionStrategies-Reference already
recorded — “defer/goǃ of a call through a variadic func value would need to capture the Span
tail, which a ref struct cannot be — no stdlib occurrence”. A census of the whole Go 1.23 tree finds
exactly one such site, html/template/examplefiles_test.go:90. The “no stdlib occurrence” claim
was true and is now retired: the original A/B was over PRODUCTION sources, and the shape lives only
in a _test.go, so nothing before the Phase-4 -tests pipeline could see it. The doc is corrected
in place. Remedy (not taken): pack the arguments into a slice at the defer site and defer a
zero-argument closure over it — a visitDeferStmt change, bounded, one site in the entire corpus.
Gates
GolibTests 168/168 · full behavioral suite PASS at 600 projects, 3,877.8 s — Transpile
600/600, Compile 600/600, Target 600/600 byte-identical, Output 574 compared / 0 failed (26
skipped, no package main) · go2cs.slnx Debug --no-incremental 0 errors (282 warnings,
672 s) · canaries all green and unmoved: fmt 63/63,
internal/fmtsort 3/3, encoding/json 491/491, internal/reflectlite 27/27 · marker census
63 marked files / 51 *_impl.cs companions (line-anchored via git grep; the UNANCHORED count
is 101, the false-alarm number the ritual warns about) · a seeded reconvert is not owed — zero
converter changes means zero emission movement, and both corpus files touched
(reflect/value_impl.cs, runtime/managed_impl.cs) are *_impl.cs companions the converter never
re-emits · converter go test and CNR not owed for the same reason. The Target phase is the
independent confirmation of that “zero emission movement” claim: 600 goldens byte-identical, so
nothing the converter writes moved.
Guards proven failing-first by seven separate neuterings, each reproducing its own defect and
nothing else: removing the family rebind (only the natural-typed row fails), defensively copying the
tail (only the aliasing row), throwing from InvokeVariadic (the four call rows, the refusal
control staying green), rebinding via target+method instead of Invoke (only the
expression-compiled row), transposing two entries of the Func family table (only the arity row),
disabling the error arm (the behavioral panic test reproduces panic: 0x25bf089bd28 verbatim,
the exact shape row-harvest-2 reported), and disabling the Stringer arm and the memoization in
turn.
⚠ Arities 3–8 have no consumer in the corpus and are guarded rather than asserted. The
behavioral test reaches 0, 1 and 2 fixed parameters through real Go source; the remaining six
family arities are driven directly through InvokeVariadic in GoReflectBridgeClosureTests, with
fixed argument i set to 1 << i so a dropped or duplicated argument lands on a different sum.
(Two arguments TRANSPOSED still sums the same — addition is commutative — so that is deliberately
not claimed.) A mis-indexed table entry fails loudly whenever it is first used, never silently; the
row exists so “first used” is now, not in some later package.
-tests tree churn classified and RESTORED per the standing rule, nothing unclassified: CRLF
phantoms (text/template/doc.cs, html/template/doc.cs, fmt/doc.cs, five encoding/json
*_test.cs), the initᴛᴛtests hook (html/template/package_init.cs, +7 real lines), the
global::go.* root escape (three internal/reflectlite files) and a seeded ΔToken alias
(encoding/json/package_test_info.cs).
The ranked queue, updated
html/template’s two roots — 243 verdicts behind 7 errors. Root A (the named-RHS alias) is unchanged and coordinator-adjacent; Root C (defer of a variadic func literal) is newly located, one site in the whole corpus, and bounded.reflect.Value.Index/Value.Sliceover a Kind-STRING Value — 2 oftext/template’s 3 residual verdicts, and the only unowned root among the four. Go indexes and slices a string; the bridge refuses both.encoding/gob(106) — unchanged at 103 of 106; still the closest unbanked package.- typed-nil method dispatch —
(*W)(nil).Error(); part oftext/template’sTestExecute, and a shape with reach well beyond templates.
No roster row, no proof page, no disclosures, no converted test sources committed: one golib feature, one golib fix, two behavioral guards, thirteen GolibTests rows (seven variadic-dispatch, six panic-text), two measurements, zero validations.
✅ BANKED — debug/elf validates 31/31; the wall was a DISCARDED method group, and the same fix’s other half was a discard that had been emitting a DECLARATION (2026-08-19, lane claude/row-harvest-3)
The harvest entry’s queue item 3 — “debug/elf (31) — CS8183 ×1, unmoved, still the next bounded
one-diagnostic item” — closes. The diagnostic reproduced exactly as the one-diagnostic-remeasure
entry recorded it, the remedy is one general converter rule, and the package validates on the FIRST
run behind it: 31 matched, 0 disclosed, one skip-parity, two benchmarks excluded. Roster
154/215 (71.6%), header recomputed from the table.
The wall is one line, and it is only ever a COMPILE wall
file_test.go:1159 is _ = net.ResolveIPAddr // force dynamic linkage, inside
TestNoSectionOverlaps — a test that t.Skipfs on Windows four lines earlier and never reaches it.
Go writes the shape to make the linker keep a symbol. C# infers a discard’s type FROM its
right-hand side, and a method group has no type to give: CS8183, “cannot infer the type of
implicitly-typed discard”. Nothing executes the line; it merely has to compile, and until it did,
the whole test assembly did not.
Worth stating plainly, because it prices the row honestly: closing a 31-verdict package here bought
no behavioral change whatsoever in the package’s own code. Everything else in debug/elf — the
section and program-header walk, symbol tables, DynValue, compressed sections in both the GCC
.zdebug and gABI SHF_COMPRESSED spellings, the >65280-section overflow path, and DWARF
relocation application across twenty compiler/architecture objects — already worked.
The fix is two rules, and the second was the worse defect
1. A discarded func value takes a CAST. The two C# forms with no type of their own are the
method group and the lambda (a func literal, or the method VALUE that converts to one). Both are
CS8183 in a discard, and discardTargetTypeName supplies the target: a package named func type
whose underlying signature matches, else the structural Func<…>/Action<…> render
iifeDelegateType already provides. A variadic signature takes the golib family
(Funcꓸꓸꓸ<@string, any, @string> for fmt.Sprintf). The parentheses around the RHS are
load-bearing for the lambda form — (Func<…>)(nint p1) => … does not parse as a cast. A func-typed
VARIABLE is left alone: it already has a C# type.
2. A blank LHS is a discard, never a declaration. The := path one statement form over faces
the same typeless RHS and answers it by DECLARING the local with the matching package named delegate
(stateFn state = lexText;, the self-referential state machine). A blank LHS reached that branch too
and emitted stateFn _ = lexText; — a local literally named _. Everything downstream then
degrades: every other _ = x in the scope becomes an ASSIGNMENT to that local (CS0841 before it,
CS0123/CS0029 after), and a second named-func-type discard collides outright (CS0128). The blank
test is now decided ONCE, above every declaration arm, rather than inside the var arm where it lived.
The asymmetry that makes this a separate rule rather than the := rule again
C# 10 gives a method group a natural type, so var f = pair; compiles and needs nothing. A
discard is specifically excluded from that inference. So two statement forms one line apart in
the same emission block, both facing an untyped func RHS, need OPPOSITE treatments: the declaration
gets a TYPE on the left, the discard gets a CAST on the right. Recorded in
ConversionStrategies-Reference beside the := section it mirrors.
Corpus footprint: zero, measured rather than assumed
Both arms move emission only for a blank LHS. A census of the committed corpus for a declaration
literally named _ returns three hits, all of them Go source inside go/types’ test string
literals (api_test.cs, resolver_test.cs), never an emitted declaration; and a bare
_ = <method group> cannot exist in the compiling corpus, because it would be CS8183. The seeded
whole-stdlib reconvert confirms it: zero movers from this fix.
Guard
BlankIdentifierCollision already owned this family — it guards the _, _, _, _ = a, b, c, d
split-discard CS0128 and the blank-func _() rename — so the shapes were added there rather than in
a new project. blankFuncValues covers all seven RHS forms (unnamed signature, no-results,
no-params, named func type twice in one scope, variadic, cross-package variadic, method value,
func literal) plus two controls: a func-typed VARIABLE discard, which must stay uncast, and
state := lexText; state = lexNumber, which must still declare the named delegate. Proven
failing-first against the neutered converter at 11 diagnostics in 7 classes — CS0841 ×3,
CS0029 ×2, CS0123 ×2, CS0128, CS1661, CS1662, CS1678 — and green on all four phases with the fix in.
The measurement hazard this lane paid for
$ErrorActionPreference = 'Continue' at SCRIPT scope let a failed SEED proceed into a
conversion. The reconvert ritual prescribes 'Continue' around the converter CALL (under
'Stop', the converter’s stderr WARNINGs become a terminating NativeCommandError). Setting it for
the whole script is the easy over-application, and it is not safe: seeded under the session
scratchpad, Copy-Item died mid-tree on MAX_PATH — the source-generator output names under
Generated\ are long enough to exceed it from a deep root — every failure was non-terminating, and
the script converted into a HALF-SEEDED root, which is exactly the state whose false clobber report
the marker gate exists to catch. Two lessons: scope 'Continue' to the converter invocation and
leave seeding under 'Stop'; and seed into a SHORT root with bin/obj/Generated excluded,
which is both immune and far faster (5,297 files). The abandoned run was killed by verified PID, not
by name.
A banking trap nobody had hit yet: .gitignore ate twenty testdata fixtures
TestDWARFRelocations reads twenty compiler/architecture objects from testdata, and Go names a
fixture for what it CONTAINS — so they are *.obj. The root .gitignore’s “Files built by Visual
Studio” block matches by EXTENSION across the whole tree, and *.obj is in it. git add staged
seven of the twenty-seven fixtures and reported nothing wrong; the row would have banked with a
suite that cannot reproduce from a clone, which is the one promise the roster makes.
debug/elf is the first banked package to hit this — debug/dwarf’s equivalents are .elf/.elf4
and were never matched — and the census closes at exactly these twenty (git status
--ignored=matching -- src/core now reports 0 ignored testdata files, was 20). The fix is a
trailing !src/core/**/testdata/** negation, whole-tree rather than per-extension, matching the
scope .gitattributes already gives the same trees with src/core/**/testdata/** -text; a deeper
.gitignore still wins, so src/core/.gitignore’s *.go rule keeps staged Go source copies out.
Check git add’s staged COUNT against the on-disk count when banking a package with testdata —
an ignore rule is silent, and this one had been waiting for the first fixture corpus named after a
build artifact.
A second bank’s badge, corrected in passing
The reconvert’s three movers were debug/elf/README.md (this lane’s — the Tests badge flipping to
31/31 validated), go/internal/gcimporter/gcimporter.cs (row-harvest-2’s pre-documented mover,
still pending for the next leveling regen, left untouched), and crypto/tls/README.md, whose
badge still read not_yet_validated for a package master banked at 400/402 hours earlier. Badge
regeneration is part of banking — debug/dwarf, debug/gosym and crypto/sha256 all carry theirs
— so that one is a missed step, not drift. Adopted here with the fix’s own README, since it is the
converter’s own output for a fact the roster already states.
Gates
Converter go test ./... ok, 240.2 s, zero failures · full check-no-regression.ps1
byte-identical across all 625 behavioral packages except this lane’s own guard artifacts
(BlankIdentifierCollision/main.cs + package_info.cs, both re-baselined and re-verified on all
four phases), 0 NOT MEASURED, 0 advisory warnings, 1,102 s; preflight solution integrity
627/627 and path casing 4,506/4,506 · seeded whole-stdlib reconvert 63 marked / 0
clobbered, 1,665 emitted, 1,662 identical / 3 movers / 0 new, all three classified above ·
sweep closure run-validated-sweep.ps1 -Filter debug/elf PASS 31 in 28 s, comfortably inside
the 10-minute default, so no $longTimeouts floor is owed. No golib and no go2cs-gen change,
so GolibTests/full-behavioral/both-slnx are not owed. Proof page converter-generated at
685aa2170 — the commit that carries the fix, not its parent.
The ranked queue, updated
reflect.Value.Callvariadic dispatch — unchanged at the top; the named owner of 13 oftext/template’s 14 residual verdicts and predicted behindhtml/template’s 243.html/template’s Root A — the named-RHS type alias across the reference-model test boundary.panic(err)renders an address, not the message — golibPanicException.encoding/gob(106) — unchanged at 103 of 106; still the closest unbanked package.
⛔ MEASURED — internal/concurrent does not bank and the reason is STRUCTURAL: its test file is a whitebox consumer of an implementation the hand-own deliberately replaced. The crash behind it is rooted and fixed (2026-08-19, lane claude/row-harvest-3)
The one-diagnostic-remeasure entry left this row with two open items — “CS0426 ×1, unmoved. Plus a finding the board did not have: the converter CRASHES on this package’s hand-owned file.” Both are now resolved, in opposite directions: the crash is a bounded converter defect, fixed and gated; the CS0426 is not a converter defect at all and no converter change can close it.
The CS0426 reproduces exactly, and it is the hand-own’s own consequence
hashtriemap_test.cs(406,145): error CS0426: The type name 'node<,>' does not exist in the type
'concurrent_package' — still the only diagnostic. Line 406 is dumpNode’s signature:
func dumpMap[K, V comparable](ht *HashTrieMap[K, V]) { dumpNode(ht, &ht.root.node, 0) }
func dumpNode[K, V comparable](ht *HashTrieMap[K, V], n *node[K, V], depth int) { … }
Two facts settle it. First, dumpMap is never called by any Test — dumpNode is reached only
from dumpMap and from itself, so this is dead interactive-debugging code that Go nonetheless
compiles as part of the package. Second, src/core/internal/concurrent/hashtriemap.cs is a
[module: GoManualConversion] native replacement: a ConcurrentDictionary-backed mapStore,
with no node, no indirect, no entry, and no trie at all. dumpNode’s body reads n.isEntry,
n.entry(), n.indirect(), i.parent, i.dead, i.children, e.key, e.value, e.overflow
and ht.keyHash(…, ht.seed) — the complete private structure of the implementation the hand-own
exists to not be.
So this is a new shape, and worth naming for the packages behind it: a hand-owned native
replacement is invisible to production consumers, which only ever touch the public API — but a
package’s own _test.go is a WHITEBOX consumer and may reference the replaced implementation’s
internals. Satisfying it means either reimplementing the hash-trie (abandoning the replacement, and
with it the reason it exists) or declaring dead scaffolding for dead code, which is the
fake-but-plausible move hashtriemap.cs’s own header comment forbids. Neither is taken. 20
verdicts stay unmeasured, and this row should be read as closed-by-design rather than pending.
The class is likely small but is not measured: the other two fully-hand-owned packages
(internal/godebug, internal/weak) and the partial hand-owns each carry the same exposure iff
their suites reach past the public API.
The crash: one missing struct field in a duplicated literal
-debug (which suppresses the recover) turns the warning into a stack in four seconds:
panic: runtime error: invalid memory address or nil pointer dereference
strings.(*Builder).String(...)
main.(*Visitor).visitFile(…) visitFile.go:106
main.emitAutoConversionSiblings.func1(…) autoSiblingOperations.go:114
emitAutoConversionSiblings hand-rolled a copy of newFileVisitor’s literal — the constructor
documented as “constructs the per-file Visitor with every eagerly-required field initialized” — and
the copy had drifted by two fields: blankImportInits (nil, so visitFile’s
v.blankImportInits.String() dereferences nil — the panic) and manualConversion (false, though
the field’s whole meaning is “this file’s destination .cs is hand-owned and the emitted text lands
in the .cs.auto review sibling”, which is precisely this pass). It calls the constructor now.
Why it hid for so long, twice over. The emitter runs ONLY in conversionDriver’s
unmarkedFileCount == 0 branch — a FULLY hand-owned package — so exactly the class of three reach
it, and all three are the files the warning names. A PARTIAL hand-own takes the normal per-file loop
with its write target redirected, which is why the ManualConversionSiblingState behavioral guard
never caught this: it is a partial package and has always worked. And the recover phrases the panic
as visit file error … in "hashtriemap.go", which reads as a defect in that file — the hand-owned
file’s own header comment had duly recorded the wrong cause (“panics visiting this generic file”,
“runs only six of the whole-package pre-passes”). Genericity and the pre-pass set had nothing to do
with it; both comments are corrected in place.
Measured by A/B of two seeded whole-stdlib reconverts
| pre-fix | post-fix | |
|---|---|---|
visit file error warnings |
3 (hashtriemap.go, godebug.go, pointer.go) | 0 |
.cs.auto present |
20 | 22 (+internal/concurrent, +internal/weak — neither had EVER been produced) |
| freshly emitted | — | 21 of 22 (math/unsafe.cs.auto is seeded-only) |
| differing among the 20 common | — | 1 — internal/godebug/godebug.cs.auto, and only because it went from not-emitted to emitted |
| marker gate | 63 / 0 | 63 / 0 |
| corpus movers | 3 (both classified, adopted) | 1 (gcimporter.cs, row-harvest-2’s carry) |
The 19 byte-identical siblings are the evidence that the manualConversion switch is output-neutral
for everything that was already being produced, and ManualConversionSiblingState regenerating
state.cs.auto byte-identically is the positive control that the guard still exercises the path.
Two backlog corrections this measurement produced
- CleanupBacklog item 18 is much smaller than recorded. Of 20 tracked corpus
.cs.autofiles, 19 were already byte-fresh against today’s converter; the single stale one (internal/godebug) was stale because of this crash and is adopted here. The recorded “11 of 16 were stale at r40” no longer describes the tree. math/unsafe.cs.autois a tracked ORPHAN. Its principal shed itsGoManualConversionmarker at r41, so no sibling is emitted for it any more, but the file is still tracked. Nothing reads it; it should be deleted when item 18 is levelled.
Gates
Converter go test ./... ok, 240.5 s, zero failures · full check-no-regression.ps1
byte-identical across all 625 behavioral packages, 0 NOT MEASURED · both seeded reconverts above ·
no golib and no go2cs-gen change. No roster row, no proof page, no disclosures, no converted test
sources committed: one structural non-bank, one converter fix.
📉 NARROWED — sync/atomic 99 → 104 of 108: two unsafe.Pointers now compare as BOXES, which is what golib was already built for. The residual is three roots and none of them is the pointer family (2026-08-19, lane claude/row-harvest-3)
The escape/box-copy entry left this row at 99 of 108 with three named residual roots, the second of
which was “Go’s pointer-IDENTITY compare k != p is emitted as k.Value != p.Value, a deref-compare
that nil-derefs on the nil probe … pre-fix it compared pointers-into-copies and failed ‘orderly’, so
that fix EXPOSED it to full severity.” That root is now closed, and closing it did more than its own
three tests: the test host survives the run.
The measurement, before and after, on one tree
| pre-fix | post-fix | |
|---|---|---|
| C# verdicts RECORDED | 35 of 108 | 108 of 108 |
agreeing with go test |
not computable | 104 |
| how the run ended | host died inside TestHammerStoreLoad — OutOfMemoryException in RecordGoroutinePanic’s String.Join, then a nil-deref panic |
clean exit; TestHammerStoreLoad contained as a per-test infrastructure-error |
The escape/box-copy entry’s 99 was obtained by running the host DIRECTLY with the hammer family excluded and diffing against the recorded go map, because the host death made a full-pipeline compare impossible. 104 is a full-pipeline number — every verdict recorded by the differential harness itself — so the two are not composed the same way, and the honest statement of this lane’s own yield is the table above: from 35 recorded verdicts to 108.
The defect: unsafe.Pointer is the one pointer that CARRIES an address rather than being one
golib already models that exactly. Pointer : ж<uintptr> holds the address as its Value, and it
overrides ж<T>.Equals to compare PointerOrderToken (IsNull ? 0 : Value.Value) so that equality,
hashing and ordering are one fact about the address — the override exists because the converter mints
a fresh box on every uintptr → unsafe.Pointer conversion (875 call sites), so two boxes over one
address are ONE Go pointer. The base ==/!= operators route through it and it is nil-safe by
construction.
The emission never reached it. convIdent’s pointer-context arm renders an unsafe.Pointer ident as
x.Value — correct where an address is genuinely wanted — and convBinaryExpr sets that context
for both operands of a comparison, since isPointer() counts unsafe.Pointer (a go/types Basic
of kind UnsafePointer, not a *types.Pointer). So k != p emitted k.Value != p.Value: right by
accident for two non-nil pointers, and a NullReferenceException on a nil one, because a nil
unsafe.Pointer local is default! — a C# null. TestLoadPointer, TestStorePointer and
TestSwapPointer each walk testPointers(), whose first element is nil.
The fix suppresses the pointer context for an equality comparison with unsafe.Pointer on both
sides. The scope is exact rather than conservative: Go admits no other pairing without a conversion
(unsafe.Pointer == *T and == uintptr are type errors), and comparison against untyped nil has
its own arm above and is untouched. It also repairs a shape nobody had filed — a MIXED comparison,
where a selector operand rendered as the box and an ident operand rendered as its address
(x.i != p.Value, (~e).tag != tag.Value); both sides are boxes now.
Corpus reach: seven shipped runtime sites, all verified compiling
runtime/{alg,map,map_fast32,map_fast64,mbarrier,traceback}.cs and runtime/pprof/map.cs — every
one a deref-compare collapsing to a box compare (return x == y;, if (dst == src),
cgoTraceback != traceback). Built clean (runtime.pprof closure, 0 errors, 123 s) and then
RESTORED per corpus-regen policy. Worth flagging for the next leveling regen: these seven are a
CORRECTNESS fix, not cosmetics — each is a latent nil-deref on the shipped path.
sync/atomic/value.cs.auto is adopted here, since it is a review sibling this fix directly re-emits.
The residual: 4 divergences, 3 roots, and a correction to the recorded list
| Root | Verdicts | State |
|---|---|---|
unsafe.Pointer Reinterpret write-back — Ꮡuaddr.Reinterpret<uint32, atomic.Int32>() and the view’s writes are lost (AddInt32Method: val=0 want 400000) |
2 (TestHammer32, TestHammer64) |
unchanged, and now the largest residual |
late-goroutine host death — the pointer hammer fires Fatalf from a goroutine after the test window |
1 (TestHammerStoreLoad) |
contained: was a process kill that cost 73 verdicts, is now one infrastructure-error |
| reflect alignment | 1 (TestAutoAligned64) |
unchanged |
TestUnaligned64 skips identically on both sides. Correction to the escape/box-copy entry’s
residual list: atomic.Value CAS type-identity is NOT among the divergences — TestValue_CompareAndSwap
and its parent both agree. This lane does not claim the credit: sync/atomic/value.cs is a
[module: GoManualConversion] NATIVE reimplementation on Volatile.Read/Interlocked.CompareExchange
and GetType() that never touches unsafe.Pointer, so nothing in this fix can reach it. It is
recorded as measured, cause unattributed.
No bank: 4 divergences stand, so no roster row, no proof page, and the converted test sources are removed.
The fix made an existing guard VACUOUS, which CNR caught and is worth recording
CNR reported a second intended mover this lane did not predict: UnsafePointerKeywordParam. That
project’s whole premise was the arm this fix retires — “an identifier of type unsafe.Pointer used
in a pointer context (e.g. a comparison operand) emits name.Value; built from the RAW Go name, a
parameter named new came out as new.Value, which C# parses as the new operator (CS1526)”. With
comparisons rendering boxes, its emission holds no .Value at all, so it no longer exercised the
keyword sanitization it exists to guard — a live guard quietly reduced to a compile check.
Five candidate shapes were probed for one that still reaches the arm — uintptr(new), *(*int32)(new),
a struct-field store, pointer arithmetic, and a call argument — and none emits <name>.Value; each
takes a (uintptr) cast or the bare box. So rather than guess at a trigger, the project was widened to
those seven arms, every one of which builds its text from the Go name and so carries the identical
sanitization risk ((uintptr)@new, ~(ж<int32>)(uintptr)(@new), h.p = @new, asUintptr(@new)), with
the comparison arm kept as the control that it renders @new and not new. All four phases green, and
the guard is stronger than before.
Left flagged rather than claimed: convIdent’s unsafe.Pointer .Value arm may now be unreachable from
converter output entirely — every <keyword>.Value site in the corpus is in a hand-written _impl.cs,
not an emission. That is an indirect measurement, not a proof, and the arm is harmless where it stands;
confirming it needs converter instrumentation over a corpus run, which is its own small task.
Gates
Converter go test ./... ok, 205.2 s, zero failures · full check-no-regression.ps1
byte-identical across all 625 behavioral packages except the guard’s own intended artifacts,
0 NOT MEASURED · seeded whole-stdlib reconvert 63 marked / 0 clobbered, 1,655 identical / 8
movers / 0 new — the seven above plus go/internal/gcimporter/gcimporter.cs, row-harvest-2’s
pre-documented carry · guard ManagedAtomicPointer proven failing-first on both Target and
Output (exit code 2 — the pre-fix binary crashes on the nil operand), green on all four phases
with the fix in. No golib change: the golib side was already correct, which is the point.
🔎 ROOTED, NOT TAKEN — flag’s CS1929 ×10 is the Δ-RENAME and the adapter disagreeing about one method name; the owner is ImplementGenerator (2026-08-19, lane claude/row-harvest-3)
Re-measured on current master with this lane’s three converter fixes in: CS1929 ×10, unmoved —
five test-package types (boolFlagVar, flagVar, interval, URLValue, zeroPanicker) × two
sites each, every one inside a generated ImplementGenerator adapter. The one-diagnostic-remeasure
entry correctly named the owner as the generator rather than the converter; this narrows it to the
exact disagreement, which is one name.
The disagreement
flag_test.go’s types implement flag.Value with pointer-receiver String/Set. In the whitebox
test compilation those names COLLIDE with the production flag package’s own members (the test
variant carries using static of production), so the converter’s name-collision pass Δ-renames
them, and everything downstream follows it correctly:
// flag_test.cs — the converter's emission
[GoRecv] internal static @string ΔString(this ref boolFlagVar b) { … }
[GoRecv] internal static error ΔSet(this ref boolFlagVar b, @string value) { … }
// RecvGenerator's ж-overload — faithfully renamed too
internal static @string ΔString(this ж<boolFlagVar> Ꮡb) { … }
ImplementGenerator’s adapter does not. It forwards using the Go method name:
global::go.@string global::go.fmt_package.Stringer.String() => m_box.String(); // CS1929
global::go.error global::go.flag_package.Value.Set(@string _) => m_box.Set(_); // CS1929
The interface members it implements are named correctly — Stringer.String, Value.Set are the
INTERFACE’s names and must never be renamed. Only the forwarding target is wrong: it should be
m_box.ΔString() / m_box.ΔSet(_). With no String extension on ж<boolFlagVar>, C# reports the
nearest candidate it can see, which is why the board previously recorded it as “binds
bytes_package.String” — that is the diagnostic’s suggestion, not a real binding.
This is also why the class is test-package-shaped rather than general: production packages rarely produce the collision that triggers the rename, so the adapter’s use of the Go name is normally indistinguishable from the emitted name.
Why it was not taken here, and what the next lane faces
A go2cs-gen change owes the FULL behavioral suite plus BOTH slnx builds, and this lane had no
room for that ledger. The remedy is also a real design choice rather than a one-liner, because the
rename is not currently recorded anywhere the generator can read it:
[GoLocalName]is not the answer — it targetsStruct | Class | Interfaceand carries the Go name of a lifted local TYPE, not a member.- So the options are (a) have the converter record the member rename (a new attribute, or extra
cargo on the existing
[assembly: GoImplement<T, Iface>]record, which the adapter already reads), or (b) have the generator apply the same Δ-rule as a fallback when the plain name does not resolve to a member on the box. (a) is the durable direction — it makes the emitted name a FACT the generator consults rather than a rule it re-derives and can drift from, which is precisely how this defect exists.
24 verdicts sit behind it, unmeasured — the wall is a build block, so nothing about flag’s
runtime behavior is known yet.
📉 NARROWED ×3, banked 0 — text/template 49 → 50 of 52, html/template’s wall falls from 7 errors to 1, and sync/atomic’s alignment row is rooted as structurally unsatisfiable (2026-08-19, lane claude/near-miss-finish)
The near-miss batch: three packages the harvest queue put within a few verdicts of banking. None banks, and the honest summary is six bridge/converter defects rooted and fixed, one written-and-measured fix deliberately REVERTED because shipping it would have cost more verdicts than it bought, and two residuals priced as structural rather than pending.
A correction to the batch’s own premise, before anything else
The brief listed sync/atomic’s residual as “the atomic.Value CAS type-identity pair, the reflect-alignment
row, and one more”. That is the escape/box-copy entry’s list, which row-harvest-3 had already corrected in
place: TestValue_CompareAndSwap and its parent AGREE — sync/atomic/value.cs is a [module:
GoManualConversion] native reimplementation on Volatile.Read/Interlocked.CompareExchange/GetType() and
nothing about the pointer work could reach it. Re-measured here on current master, unchanged. The real four are
TestHammer32, TestHammer64 (one root), TestHammerStoreLoad and TestAutoAligned64.
text/template — 49 → 50 of 52, and the residual is now ONE arc, not four roots
Four of the variadic-call entry’s residual roots were bridge defects; all four are fixed, and TestComparison
banks outright.
| Root | Verdicts | State |
|---|---|---|
Value.Index / Value.Slice over a Kind-STRING Value |
TestComparison + 4 rows of TestExecute |
fixed — Go’s own arms; a string indexes to its i’th BYTE, and slices to its OWN type so a named string stays named |
three-index Value.Slice3 |
1 row of TestExecute |
fixed — it was never bridged at all, and the auto form nil-dereferenced rather than degrading |
typed-nil rendering ((*W)(nil).Error(), html typed nil) |
2 rows of TestExecute |
fixed — two separate defects, both on the one-nil-encoding rule (below) |
| chan direction / chan receive | TestIssue43065 + 1 row of TestExecute |
open, and the whole residual |
TestExecute still fails, so the package sits at 50. But its remaining failure is the SAME arc as
TestIssue43065, which is a real narrowing of the picture: text/template’s entire residual is channels.
The four bridge fixes, and why each was invisible
Each degraded to a value that reads as a real answer — which is what kept them unnoticed under a green compile.
Value.Index/Value.Sliceon a string panickedreflect: call of reflect.Value.Index on string Value— Go’s message for a kind that does not support indexing AT ALL, so it read as “strings aren’t indexable” rather than “this arm is missing”. A string Value has no element type, so it could not share the container route; both arms are Go’s own (a non-addressableuint8byte; a window of the receiver’s own type).Value.Slice3was not in the hand-owned set, so it kept the auto conversion, which reinterprets the never-populatedptrslot as a rawunsafeheader.Sliceand editsData/Len/Capin place. That dereferences nil outright — surfacing asinvalid memory address or nil pointer dereference, which reads like a corpus bug, not a missing member. Bridged over the SAME golib window machinerySliceuses (a new 3-indexGoReflect.SliceWindowoverload), so the two- and three-index forms cannot disagree.Value.Callerased a typed nil crossing into INTERFACE space. Go’s assignment to an interface-typed parameter BUILDS an eface and an eface keeps the type half;marshalCallArgread the slot’s rawnulland handed that across, so the callee’sreflect.ValueOf(arg)answered the INVALID zero Value and text/template’sprintableValuereported<no value>where Go prints<nil>. It packs throughpackInterfaceValue— the same ruleValue.Interface()already used one call away — when and only when the destination is interface space. A concrete parameter type builds no eface and is untouched (asserted as a control).-
A nil RECEIVER threw in the runtime duck-typing SHELL tier. golib’s
error<T>.Error()readm_target_ptr.ValueBEFORE choosing between itsж-receiver and by-value overloads, so a nil pointer threw — for a pointee theжoverload it then selected never needed. Go’s method set belongs to the TYPE, so(*W)(nil).Error()dispatches normally and the method decides what nil means.This one is worth reading twice, because both its invisibility mechanisms are reusable. First, the throw never surfaced:
fmt’shandleMethodswraps everyError()call in Go’s owncatchPanic, which prints<nil>for a nil-pointer argument — so the symptom was a wrong RENDERING, not a crash. Second, it reproduced ONLY where the(type, interface)pair resolved through the runtime shell rather than through a generated nominal adapter, i.e. only when nothing in the program converts that type to that interface explicitly. A probe that addedvar e error = ptrto make the failure clearer made it disappear. The guard therefore carries a type reached exclusively through the shell tier;Blob/Tagbeside it cannot guard this, because their pairs are recorded.
⛔ The one that was written, measured, and REVERTED — and it inverts the queue’s #1 ranking
text/template’s last non-disclosure root is walkRange ranging a channel, and the bridge refused it:
reflect’s auto recv/send open with the very downcast Type.ChanDir retired — reinterpreting the
descriptor onto the linker’s chanType record and reading .Dir out of the memory that follows the value
slot. Behind a synthesized descriptor that reads zero, zero is InvalidDir, and Dir & RecvDir == 0 holds
for EVERY channel — so a plain bidirectional chan string was refused as send-only. (Past that test
neither could have worked either: chanrecv/chansend0 are external stubs the PartialStubGenerator emits as
NotImplementedException.)
Both were hand-owned over golib’s channel<E> with direction asked of abi.ChanDir — the one authority — and
measured: TestExecute and TestComparison both PASS, taking the package to 51 of 52.
Then TestIssue43065 hangs. Its channel is make(chan<- int); the bridge cannot know the direction, so
walkRange’s ChanDir() == SendDir guard does not fire, and the now-working Recv() blocks forever on a
channel nobody will send to. The package deadline expires, 67 of 118 events are recorded, and 51 verdicts are
lost — against the 1 the fix was worth.
So the ranking is wrong, and this is the lane’s most useful finding. The board has carried
reflect.Value.Recv and chan-direction cargo as independent items, with direction ranked as a self-retiring
disclosure class. They are not independent, and the dependency runs the other way:
The chan-direction disclosure class is currently self-limiting only because
recvis broken. Bridgingrecvconverts a fast, attributable failure into an unbounded hang. Direction cargo is therefore a PREREQUISITE for the recv bridge, not a later improvement on it.
Reverted for that reason, and because the hang risk is not confined to this package: any banked row that reflectively ranges a channel it believes bidirectional would acquire the same failure mode, and proving otherwise needs a full sweep this lane could not afford. The two ~25-line implementations and their reasoning are in this entry’s commit message; a lane taking the direction arc can reproduce them in an hour, and should land them together.
Recorded plainly so nobody re-derives it: abi.ChanDir answering BothDir is still the honest answer, and no
descriptor answer helps here. BothDir lets the receive proceed (hang); InvalidDir fails the same guard;
SendDir would reject every range-over-channel in the corpus. The direction has to be carried by the VALUE —
make(chan<- int) is where it is born and where the converter can still see it — which is a channel<T> field
plus every make/narrowing-conversion site plus an abi read. A feature arc, unchanged in size, but now with
a second package waiting behind it.
html/template — Root A is CLOSED; the wall is 7 errors → 1
243 verdicts, still build-blocked, but on a single diagnostic.
Root A (CS0246 ×6, FuncMap could not be found) is fixed. The board had it as “widening the seed vs
making the renderer qualify — a real choice, NOT taken”. Measured, the choice resolves itself, because the
alias is not where the recorded reasoning assumed:
// src/core/html/template/template.cs, line 4 — the PRODUCTION conversion's own declaration
global using FuncMap = go.text.template_package.FuncMap;
seedProductionAliasLifts’s narrowness rests on “a named RHS already renders through its own qualified name”.
That is false for exactly this shape: the alias is declared as a compilation-scoped global using in the FILE
that declares it, the renderer spells the BARE name in production and test alike, and a reference-model test
project compiles *_test.cs only — so the declaring file is absent and the name resolves nowhere. (The test
metadata file does already declare the cross-package two-hop templateꓸFuncMap, which is what makes this a
name-resolution gap rather than a missing import.)
So a named RHS is seeded too — and only the name half. The type half stays anonymous-RHS-only: a named RHS
has its own qualified spelling and is already rendered through it, so recording it in
productionAliasLiftedTypes would re-spell references that already compile. The recorded collision worry is
answered by measurement rather than by argument: across the converted stdlib an EXPORTED type X = <named>
exists in four packages only — html/template (FuncMap), os (DirEntry/PathError/FileInfo/
FileMode), internal/reflectlite (Kind), debug/buildinfo (BuildInfo) — and a _test.go cannot
redeclare such a name in the package’s own scope. The two banked packages in that set (debug/buildinfo 197,
internal/reflectlite 27) are swept as a gate below.
Root C is now the SOLE wall, and it is one site in the entire Go 1.23 tree
(examplefiles_test.go:90) — defer of a VARIADIC func literal:
defer((params ꓸꓸꓸstring dirsʗp) => { … }, dir1, dir2, ref ᒐ); // CS0411
A NON-variadic literal emits explicitly-typed parameters ((@string p, @string q) => …) which convert to
Action<T1,T2> and infer fine; a variadic one has no such conversion. Not taken here — it needs a real
visitDeferStmt transform and this lane had no gate budget left — but it is now scoped rather than sketched.
The two candidate shapes, both preserving Go’s defer-TIME argument evaluation:
- Keep the eager-argument rung (
defer<T1,T2>snapshots the arguments, which IS Go’s rule) and make the callee invocable: forcerenderLambdaParamsfor a variadic literal and wrap the callee in its golib family delegate type, so the emission isdefer((ᴛ1, ᴛ2) => ((Actionꓸꓸꓸ<@string>)(<literal>))(ᴛ1, ᴛ2), dir1, dir2, ref ᒐ).T1/T2infer from the eager arguments, not from the lambda. The obstacle is mechanical: the cast has to be placed around the callee INSIDEconvCallExpr’s rendering, not by surgery on the returned text. - Hoist to the arity-0 rung: a typed local for the literal plus one per argument, then
defer(() => ᴛ1(ᴛ2, ᴛ3), ref ᒐ). More emitted lines, no inference question at all, andlambdaContext.deferredDeclsis already the hoist sink.
iifeDelegateType(sig) already renders Actionꓸꓸꓸ<@string> for the signature, so neither shape needs new
type-naming machinery.
sync/atomic — the alignment row’s OFFSET half is closed; its POINTER half cannot be
TestAutoAligned64 asserted two things and failed on the first:
if o := reflect.TypeOf(&signed).Elem().Field(1).Offset; o != 8 { … } // now PASSES
if p := reflect.ValueOf(&signed).Elem().Field(1).Addr().Pointer(); p&7 != 0 { … } // structurally unsatisfiable
StructField.Offset had stayed unpopulated on the r39d rule, reasoning that a Go byte offset exists only to
be added to a data pointer, and managed storage has no such pointer. True of Offset as an ADDRESS, false of
Offset as layout METADATA — the only way anything has ever read it, including abi.StructType, which
populates it from GoReflect.GoFieldOffsets and has all along. Reading it from that same memoized walk is what
makes the two Go-specific rules come out right where a naive Marshal.OffsetOf would not: a Go zero-size
field occupies nothing (its C# surrogate is one byte) and an align64-bearing field is padded to its 8-byte
boundary. It answered 0 before, which is a real answer for a field at the front of a struct — so an
unpopulated descriptor read as a LAYOUT failure. The r39d rule still bites where it should: one unknowable
field size makes GoFieldOffsets answer null for the whole struct, and every field keeps the zero.
The SECOND assertion reads a pointer’s low bits as a memory-alignment fact. Value.Pointer() is
reflectPointerToken — a deliberate, stable IDENTITY token, explicitly not an address (the whole
FINDING-managed-box-uintptr-lifetime position). Making the token 8-aligned would fabricate a memory property
the model does not have, so this is a structural residual, not a pending fix. It is also a disclosure SHAPE
none of the four ratified classes covers — a test asserting a property of a real machine address — and opening
a fifth class was outside this lane’s brief. Recorded for the coordinator to rule on.
The hammer roots are NOT bounded, and the reason is a layout mismatch worth naming. TestHammer32/
TestHammer64 fail on AddInt32Method: val=0 want 400000, from
addr := (*Int32)(unsafe.Pointer(uaddr)) over a *uint32. In Go that reinterpret is exact: noCopy is a
zero-size struct, so atomic.Int32{_ noCopy; v int32} is 4 bytes. In C# an empty struct is 1 byte and
forces padding, so the surrogate is 8 — and ReinterpretAliasesStorage’s SizeOf<TDst>() <= SizeOf<T>()
test correctly refuses the alias and drops to the raw-address route, where the writes are lost. The size test
is not the defect and must not be relaxed: aliasing an 8-byte view over a 4-byte slot would read and write past
the box’s value slot. Closing it means making the C# struct actually 4 bytes — explicit layout with Go-computed
field offsets, corpus-wide — which is a converter emission arc, not a member fix. TestHammerStoreLoad is the
same family one level out.
⚠ On this machine the host does not merely fail TestHammerStoreLoad, it DIES in it — the late-goroutine
Fatalf storm reaches RecordGoroutinePanic, which tries to serialize a 693 MB JSON string
(The JSON value of length 693174514 is too large) and then nil-derefs, losing 72 of 108 verdicts. row-harvest-3
measured containment on its own machine; the containment is timing-dependent and does not hold here. So
sync/atomic reads as 104 of 108 or 35 recorded of 108 depending on the host, and the difference is
not a regression either way.
A side finding, recorded and NOT rooted
%#x of a uintptr renders %!x(uintptr=252987927579865330) where Go prints 0x…. fmt’s printArg DOES
carry a case uintptr f: arm and it is ordered after case nuint f:, so the arm exists and something upstream
of it is not matching; this lane saw it only as the text of TestAutoAligned64’s failure message and did not
chase it. Every %x/%d of a uintptr in the corpus is affected.
Gates
Converter go test ./... ok, 201.0 s, zero failures · GolibTests 172/172 · full
check-no-regression.ps1 byte-identical across all 628 behavioral packages except this lane’s own guard
artifacts (ReflectStructTagCopy, ReflectTypedNilInterface, the new ReflectStringWindow), 0 NOT
MEASURED, 0 advisory warnings; preflight solution integrity 630/630 and path casing 4,520/4,520 ·
full behavioral suite PASS at 601 projects, 2,148.0 s — Transpile 601/601, Compile 601/601, Target
601/601 byte-identical, Output 575 compared / 0 failed (26 skipped, no package main) · go2cs.slnx Debug
--no-incremental 0 errors (287 warnings, 983 s) · seeded whole-stdlib reconvert 63 marked / 0
clobbered (line-anchored via git grep, path-precise), 1,624 emitted identical / 65 differing / 0 new,
and the 65 classify 56 CRLF phantoms + 9 real movers, none of them this lane’s: gcimporter.cs
(row-harvest-2’s documented carry), the seven runtime unsafe.Pointer box-compare sites row-harvest-3
flagged for the next leveling regen, and runtime/mfinal.cs.auto (a review sibling on its own staleness
schedule) · sweep spot-check of the two banked packages the alias widening can reach: debug/buildinfo
PASS 197 in 33 s and internal/reflectlite 1 pass / 0 fail in 124 s, the latter carrying the widening’s
one intended emission move (package_test_info.cs gains global using Kind = …abi_package.ΔKind;, adopted
here because a banked package’s committed test sources exist to reproduce byte-identically from a clone).
⚠ A byte-for-byte reconvert comparison reports CRLF phantoms AS MOVERS — 56 of the 65 here. The converter
emits LF inside a multi-line string literal while .gitattributes pins the working tree to CRLF, so any
emitted file holding one differs in bytes and matches CR-stripped. Classify with a CR-insensitive compare
before reading a mover count as drift; the raw number is not the finding.
Guards proven failing-first by seven separate neuterings, each reproducing its own defect and nothing else:
the string Index arm (exit 2 — the pre-fix binary panics), the Slice3 capacity bound (stdout), the
Call-into-interface packing (stdout, the call-into-any row alone), the error<T> overload order (exit 2 —
here the assertion succeeds and the throw is unrecovered, unlike the fmt path that swallowed it),
StructField.Offset (stdout), the named-RHS alias seed, and the validation-pack gating. One new behavioral
project (ReflectStringWindow) and two extended (ReflectTypedNilInterface, ReflectStructTagCopy).
-tests tree churn classified and RESTORED per the standing rule, nothing unclassified: two CRLF phantoms
(html/template/doc.cs, text/template/doc.cs — modified with an empty numstat) and the initᴛᴛtests hook
(html/template/package_init.cs, +7 real lines). No converted test sources committed: no package validates.
A banking-hazard fix nobody asked for, found by paying it
Regenerating ONE corpus package after a converter change — go2cs <goroot-pkg-dir> <core-pkg-dir>, the normal
lane move — strips that package’s validation-proof block from its .csproj, silently un-shipping its proof
sheet at the next push-nuget. The block was gated on the invocation MODE (-stdlib, later widened to
-stdlib or -tests), and the single-package form is neither. It is the same defect ce82093b0 fixed for
-tests, reached through a different door, and it is the harder one to catch: only the .csproj moves, and a
lone .csproj diff in a reconvert reads as ordinary emission drift.
Fixed by keying on the OUTPUT LOCATION instead — “under <go2csPath>/core/” — which closes both doors at once
and cannot be re-opened by adding a mode. -recurse, behavioral fixtures and end-user modules all write
elsewhere and keep their historical bytes. CLAUDE.md’s reconvert rule that “any change to a production
.csproj is real drift” is exactly right, and it is what caught this — the diff appeared, was not restored,
and was root-caused instead.
The ranked queue, updated
- Channel direction as descriptor cargo — now a PREREQUISITE, not an improvement. It gates
reflect.Value.Recv(whose bridge is written and measured:text/template50 → 51 of 52) and it retires its own disclosure class. Landing the recv bridge WITHOUT it converts a fast failure into a hang. html/template’s Root C —deferof a variadic func literal, ONE site in the corpus, sole wall on 243 verdicts, with two scoped remedies above.encoding/gob(106) — unchanged at 103 of 106; still the closest unbanked package.sync/atomic’s zero-size-field layout — a Go empty struct is 0 bytes and its C# surrogate is 1, so every(*Named)(unsafe.Pointer(p))over a struct with anoCopy/align64field falls off the alias route. Twosync/atomicverdicts today; the SHAPE is corpus-wide and currently silent.%#xof auintptr— renders%!x(uintptr=…); unrooted, corpus-wide reach.
📉 NARROWED — flag’s CS1929 ×10 build wall is CLOSED and the package runs at 23 of 24; the generator now reads the EMITTED name, and the remedy the board priced as “durable” turned out to record a fact it could already observe (2026-08-19, lane claude/heavy-pair)
The row-harvest-3 entry rooted this precisely — the converter Δ-renames the test types’ String/Set,
RecvGenerator follows, and ImplementGenerator’s adapter forwards to the GO name — and priced two
remedies: (a) have the converter RECORD the member rename for the generator to consult, or (b) have
the generator apply the Δ-rule as a fallback. It called (a) the durable direction, on the reasoning
that it makes the emitted name a fact the generator consults rather than a rule it re-derives.
That reasoning is right and it selects a THIRD option, which is what this lane took. The generator does
not need the rename recorded, because it already enumerates the fact: localImplNames is built
before the member list and holds every method the struct declares in either receiver form. (a) would
have recorded, in a new attribute surface, a fact already sitting in a local variable — and the
generator would STILL have had to consult the declared set for the receiver. Strictly larger footprint
(a golib attribute change, a converter emission, a fourth spelling of the rename to keep in step), same
result.
What was actually wrong — two positions, one name
An interface member is IMPLEMENTED under the interface’s name and FORWARDS under the emitted one. The adapter spelled the interface’s name at both:
global::go.@string global::go.fmt_package.Stringer.String() => m_box.String(); // CS1929
global::go.error global::go.flag_package.Value.Set(@string _) => m_box.Set(_); // CS1929
The left-hand side is correct and must never be renamed. Only the forwarding target was wrong. With no
String extension on ж<boolFlagVar>, C# reported the nearest candidate it could see —
bytes_package.String(ж<bytes_package.Buffer>) — which is why the one-diagnostic re-measure recorded
it as “binds bytes_package.String”. That was the diagnostic’s suggestion, never a real binding.
Common.ResolveForwardMemberName matches the interface member against the declared set, exact name
first and the ShadowVarMarker projection only as a second pass — the identical two-pass shape
TypeExtensions.GoMethodNameMatches and AdapterBinder.ResolveReceiverMethods already run at run
time. So the compile-time adapter, the shell binder and the structural probe now agree by construction
instead of by coincidence; the generator disagreeing with a rule golib had already banked WAS the
defect. Exact-first is load-bearing, not stylistic: Δ is a Unicode letter and so a legal Go
identifier character, and a genuinely ΔX-named Go method must never be displaced by a projection
of X.
The half the diagnosis did not name: the RECEIVER
ForwardReceivers is keyed by DECLARED names too, so fixing only the call target leaves a value
receiver stranded. URLValue’s ΔString(this URLValue v) has no [GoRecv] and therefore no
RecvGenerator ж-twin, so it needs m_box.Value.ΔString(); the Go-name lookup missed and fell through
to the m_box default, which is CS1929 again. Both positions read the resolved name. Emission now:
// boolFlagVar — [GoRecv] ref primary, binds the ж-twin
=> m_box.ΔString();
// URLValue — value receiver, needs the deref'd value
=> m_box.Value.ΔString();
The same resolution is wired into ValueAdapterImplTemplate and InterfaceImplTemplate, which consume
the SAME member list — in the latter it also decides methodOverriden, since a Go-name miss read a
renamed declaration as absent and sent the member down the promotion path. Non-null only when the
struct itself declares the Δ name, which is exactly when the embed hops are skipped, so the hop lookups
keep reading the interface member’s own name — what the EMBEDDED type declares it under.
flag: 0 → 23 of 24, and the residual is not a defect
24 verdicts, 23 agree. The one divergence is TestDefineAfterSet, and it is not a bug in the
conversion:
expected panic("flag myFlag set at .*/flag_test.go:.* before being defined"),
but got panic("flag myFlag set at C:\…\src\core\flag\flag_test.cs:1112 before being defined")
flag.go records the caller with runtime.Caller(2), and the managed traceback picks the correct
logical frame — flag_test.cs:1112 is flags.Set(myFlagˢ, valueˢ), exactly Go’s flag_test.go:852.
What differs is the SOURCE-FILE IDENTITY: the executing source really is the .cs, so runtime.Caller
reports it, and Go’s assertion is a regex over .*/flag_test.go:.* — which the emission misses twice
over, on the extension and on the path separator. Reporting .go would be a lie about what ran.
No disclosure was taken, deliberately. It fits none of the four classes: it is not an allocation
count, not a liveness assertion, not a channel direction, and — the one worth stating — not
host-limit, whose bar is a structural property of the deployment shape that retires itself when the
shape changes. This would not. The durable remedy is a converter one and is priced here rather than
taken: emit #line directives mapping each statement back to its .go origin, which would make BOTH
the file and the line Go’s, natively through the PDB, and would also land debugger stepping in Go
source. It changes every golden in the corpus and is its own arc. So flag does not bank — one
suite, one honest residual, no roster row, no proof page, no committed test sources.
Gates — the full generator ledger
A go2cs-gen change owes all of it, and all of it is green:
| Gate | Result |
|---|---|
converter go test ./... |
ok, 146.5 s, zero failures |
go2cs.slnx (--no-incremental) |
0 errors, 725.8 s |
go2cs-stdlib.slnx (--no-incremental) |
0 errors, 411.2 s |
| full behavioral suite | PASS — 600 projects, 2,285 s; Transpile 600, Compile 600/0/0, Target 600, Output 574 + 26 skip |
full check-no-regression.ps1 |
byte-identical across all 627 packages, 728.9 s, 2 advisory warnings; preflight 629/629 registered, 4,520/4,520 path casing |
GenTests |
24/24 |
Inertness is the claim the corpus gates are testing, and they confirm it: ForwardName is null for
every member the collision pass left alone, so nothing outside a Δ-renamed adapter moves.
Guards, both proven failing-first (src/tests/GenTests/CollisionRenamedForwardTests.cs, 6 tests):
neutering ResolveForwardMemberName to return null fails exactly 3 (the resolution and both template
forward assertions); reverting the RECEIVER lookup alone to the Go name fails exactly 1 — the
value-receiver case — which is what proves that half is independently load-bearing rather than
incidental.
For the next lane
The measurement hazard row-harvest-3 recorded held: every result here is from SEQUENTIAL runs, and the
flag build was purged (bin/obj/Generated) before each measurement, since a generator change is
invisible to an incremental build whose obj/ already holds the previous emission.
📉 NARROWED — encoding/xml 366 → 384 of 386: the canonType process-kill is CLOSED, its root was a reinterpret to a LARGER struct, and the last root is not what the board recorded (2026-08-19, lane claude/heavy-pair)
Re-measured on current master BEFORE changing anything, which is what the handoff asked for and what
paid: the bridge work landed since the census (variadic Call, the reflectlite mini-bridge,
interface-nilness + Go-name-survives-lifting) had already retired three of the five recorded roots.
366 → 369 of 386 for free — TestMarshal/47 (arity-mangled generic name), TestMarshal/64
(embedded-field order) and the TestMarshal parent all agree now. Two roots were left, not five.
Then one fix takes it to 384 of 386.
The canonType kill: the board named the symptom, and the root is one level up
Recorded remedy: “Route the feeding path through abi.synthType.” That treats the missing stamp as
the defect. The stamp is missing because the FEEDER cannot exist in the managed model at all.
rtype.FieldByIndex does Go’s (*structType)(unsafe.Pointer(t)). ReinterpretAliasesStorage takes
the aliasing arm only when the destination FITS in the source, and structType is strictly LARGER
than rtype — it carries PkgPath and Fields past the embedded Type. So the pair falls to the
raw-address route, the derived box names storage that is not a structType, and
Ꮡt.of(structType.ᏑType) hands back an abi.Type whose sysType is null. The auto body’s FIRST
statement, toType(&t.Type), is what trips the assert. Stack, verbatim:
canonType ← toType ← structType.FieldByIndex ← rtype.FieldByIndex
← xml.addFieldInfo ← getTypeInfo ← unmarshal ← DecodeElement ← Decode
And the assert is a Debug.Assert, so the process DIES (0x80131623) rather than failing a test.
That is why 15 verdicts came back EMPTY. An empty verdict reads like a suite that did not run — the
same silent under-reporting findFirstFile1 and readReparseLink produced — so the cost of this root
was never 15 failures anyone could see; it was 15 absences.
A comment in value_impl.cs claimed the branch was dead: “synthType always stamps sysType after its
own nil guard, and every canonType caller feeds a synthType/abi.TypeOf box or nil.” It is not, and
it has not been for as long as anything called FieldByIndex. An assert that documents itself as
unreachable is the one to distrust; this one has been killing xml’s suite since the package first
ran.
The fix, and why it is smaller than the rest of its family
Nothing native is involved and no layout is transcribed. The reinterpret exists ONLY to reach a
descriptor the receiver already holds, so the hand-own reaches it directly: manualConversionFuncs
gains rtype.FieldByIndex, and value_impl.cs seeds f.Type = toType(Ꮡt.common()) — common() IS
the abi.Type Go’s &t.Type names after the reinterpret, synthType-stamped, reached by a route the
managed model can express. No structType is ever synthesized. Go’s index walk is verbatim, and each
hop goes through the already-hand-owned rtype.Field, so the promoted-field projection stays
GoReflect’s.
The transferable rule, added to the reinterpret family in the reference: when a reinterpret’s target
is a struct-kind SPECIALIZATION that only EXTENDS the source, reach the shared prefix directly rather
than re-viewing the whole record — the extra fields are exactly what makes the alias unrepresentable,
and they are usually not what the code wanted. structType.FieldByIndex is left auto and is now
unreachable from rtype.
The last root is NOT “the copy is not copying its backing bytes”
That description is wrong in both halves, and a lane taking it at face value would go looking in the wrong package. Measured:
bytes.Cloneallocates a fresh backing array —append(new byte[]{}.slice(), b.ꓸꓸꓸ). The copy copies.- The
slice<byte>→CharDataconversion ALIASES — the generatedCharData(slice<byte> value) => m_value = value, and the implicit operator is that ctor. The original is not detached either.
The assertion that actually fires is the SECOND one, "CopyToken(CharData) uses same buffer.": after
data[1] = 'o', reflect.DeepEqual(tok1, tok2) is still true. The two are wrongly EQUAL, not
wrongly SHARING — the opposite of the recorded reading.
Narrowed as far as this lane’s budget allowed, and it is a reflect root rather than an xml one:
deepValueEqualBoxed’s []byte fast path is live1 is slice<byte>, which a NAMED byte-slice type
does not satisfy — CharData is a wrapper struct, not a slice<byte> — so the comparison falls to
the element-wise loop over Value.Index. That loop is where the next lane should start. Worth 2
verdicts, and it is the whole of what stands between encoding/xml and a bank.
Measured
| Verdicts | |
|---|---|
| Census (escape-box-copy lane) | 366 of 386, 20 divergences in 5 roots |
| Current master, before this lane | 369 of 386, 17 divergences in 2 roots |
With the FieldByIndex hand-own |
384 of 386, 2 divergences in 1 root |
Zero process terminations, zero asserts, zero EMPTY verdicts. No bank — one root short of a row:
no roster row, no proof page, no committed test sources, -tests artifacts removed and the tracked
-tests-closure dirt restored (xml/package_init.cs’s initᴛᴛtests hook, the CRLF phantoms in
fmt/json, and json/package_test_info.cs’s one added global using ΔToken = object; — that last
one rooted rather than waved through: json’s artifact was banked 2026-08-17 and the xml alias-seeding
chain landed 2026-08-18, so a regeneration picks it up; nothing to do with this change).
Gates
A reflect hand-own plus a converter registry entry owes the full ledger, and the canaries first
because a bridge regression would surface there before anywhere else:
| Gate | Result |
|---|---|
canary fmt |
63/63 — unmoved |
canary encoding/json |
491/491 — unmoved |
converter go test ./... |
ok, 143.7 s, zero failures |
go2cs-stdlib.slnx (--no-incremental) |
0 errors, 358.0 s |
go2cs.slnx (--no-incremental) |
0 errors, 633.6 s |
Two instrument notes this lane paid for
A single-package go2cs <pkg> is the WRONG instrument for refreshing a core package’s .csproj.
It strips the validation-pack block — the standing “0 8” restore family — and that is correct
behavior, not drift: validationPackBlock is gated on -stdlib or a -tests rewrite of a core
package, and a bare single-package run is neither. Restore the .csproj, or use the filtered
-stdlib form. The rule in CLAUDE.md (“any change to a production .csproj is real drift, stop and
root-cause it”) caught this immediately and is worth keeping exactly as written.
A watchdog that reports “EXITED after 0s” is reporting a LAUNCH FAILURE, not a fast gate. A CNR
run in this lane died in 0.16 s on a bad flag (-SkipBuild belongs to run-validated-sweep.ps1, not
check-no-regression.ps1) and the poll dutifully reported completion with exit 0. The tell is the
log’s EXITCODE= being EMPTY rather than 0 — $LASTEXITCODE was never set because the script threw.
Read the .err file before banking any gate that finished implausibly fast; a sawRunning flag in the
poll makes the distinction mechanical.
✅ encoding/xml BANKS at 386/386 — the last root was reflect.DeepEqual unable to see a NAMED slice’s backing array, and it is the named-MAP defect with one word substituted (2026-08-19, lane claude/xml-bank)
Row 155. The largest bank since encoding/json (491), and the whole of what stood between the
heavy-pair lane’s 384 and a row was two verdicts — TestCopyTokenCharData and
TestCopyTokenComment.
A correction to the handoff, and it is the finding
The heavy-pair entry narrowed the residual precisely and named the wrong line for it:
deepValueEqualBoxed’s []byte fast path is
live1 is slice<byte>, which a NAMED byte-slice wrapper never satisfies, so comparison falls to the element-wiseValue.Indexloop.
Both halves of that sentence are true, and neither is why the values compared equal. The fast path is
never reached: sliceData — the probe that answers a Value’s backing array and window offset —
reads m_array/m_low off the boxed object’s own type, and a generated named-slice wrapper has
neither (it holds a slice<E> STRUCT one level down). Both sides therefore resolve to (null, 0),
and the ReferenceEquals(data1, data2) && low1 == low2 identity short-circuit — Go’s
&x[0] == &y[0] rule — fires two lines above the fast path and returns true.
Measured, not argued. A/B over a twenty-row guard, each half neutered alone:
| rows wrong | |
|---|---|
| master (both halves absent) | 8 of 20 |
fast path fixed, sliceData untouched |
8 of 20 — unchanged |
sliceData fixed, fast path untouched |
0 of 20 |
| both | 0 of 20 |
So sliceData is the root and the fast path is an independent, real, silent gap. Both ship; only
the first is load-bearing, and a lane told “fix the fast path” would have measured no movement at all
and gone looking for a second defect that does not exist.
It is the named-MAP defect with SLICE substituted throughout
The fix is a few lines and it was already written, one function away, for maps — mapBacking’s own
comment describes this failure verbatim:
without it BOTH sides of a named-map comparison resolved to null, the
ReferenceEquals(m1, m2)short-circuit above matched them as “the same map object”, and two named maps of equal length were reported deeply equal REGARDLESS of their contents (identityRootwas blind the same way, so a named-map cycle was never detected either).
Every clause holds for slices with the nouns swapped, plus one the map arm does not have: a nil
named slice compared equal to an empty one, because the nil/empty rule (data1 is null != data2 is
null) tests the same two nulls. sliceData now takes the same second step, gated on the type being
slice-KINDED so a struct that merely HAS a slice field can never be mistaken for one, and terminating
because the nested value is a strictly smaller struct that carries the pair itself.
The transferable rule: a container probe that reads golib’s private backing fields must be written
for the WRAPPER as well as the raw container, in the same change. Two of these have now been found
separately, with identical symptoms. The probes of this shape are mapBacking (fixed 2026-07-26),
sliceData (fixed here) and identityRoot’s default: arm; anything new that reaches for
m_array/m_low/a backing Dictionary inherits the obligation.
The symptom named the wrong package, which is why it survived a green compile
TestCopyTokenCharData clones a token’s buffer, mutates the ORIGINAL, and asserts the two are no
longer deeply equal. Its failure text is CopyToken(CharData) uses same buffer. — which reads as
a copy that failed to copy, and sends a reader into encoding/xml’s CopyToken, bytes.Clone, and
the slice<byte>→CharData conversion. All three are correct: Clone allocates a fresh backing
array and the generated conversion aliases exactly as it should. The values were wrongly EQUAL,
never wrongly SHARING. (The heavy-pair lane had already made this correction once, against the
board’s earlier “the copy is not copying its backing bytes” framing; it is recorded again because the
test’s own message will keep asserting the wrong thing.)
The first assertion in each of those tests — the unmutated clone compares equal — passed throughout, which is what kept the defect invisible: half of every such test agrees with a probe that answers “same buffer” for everything.
The []byte fast path, since it ships too
Go selects the bytealg.Equal special case by the element KIND, never by the slice’s or the
element’s name: a raw []byte, a defined slice type over byte (xml.CharData, net.IP,
net.HardwareAddr, tar’s format types) and a slice over a defined byte element all qualify. The
managed arm tested live is slice<byte>, which only the first satisfies, so the other two took the
elementwise Value.Index walk — correct answers, silently off the fast path. It now asks
GoReflect.TryByteSliceView, the element-kind alias Value.Bytes/SetBytes are already built on
(AliasOfElement and its ByteAliasableElement<E> gate), so all three shapes take one route in both
APIs. Both sides are the same Go type by the AreEqual check above it, so one view test settles both.
Guard
The existing DeepEqual behavioral project is extended by twenty rows rather than forked — its
named-MAP block is the direct precedent and now sits beside its named-SLICE twin. Eight of the
twenty printed the wrong answer against the pre-fix binary, every one of them wrongly true; all
63 pre-existing rows agreed before and after. The rows: the CopyToken shape verbatim (clone, mutate
the original, re-compare); a named byte slice through an interface, as a slice element, as a map
value; nil vs empty vs self; length mismatch; a named slice over a DEFINED byte element and one over
string, so the fix cannot be byte-specific; the identity short-circuit where it is genuinely true;
and a self-referential type recur []any cycle, which is accidentally true before the fix (the
short-circuit fires) and terminates after it only because the unwrap reaches the real backing array.
encoding/xml — 386 of 386
386 matched · 0 disclosed · 0 skipped · 0 empty · 0 errors, 8 Example/Benchmark declarations
excluded as everywhere. No disclosure manifest: the package needs none. The suite is one long
reflection walk checked against Go’s own answer — decoder tokenization over the whole grammar,
namespace resolution in both directions, the Marshal/Unmarshal struct-tag grammar,
Marshaler/Unmarshaler at every depth, EncodeToken’s well-formedness rules, and the CVE set —
which is why its last roots were all reflect and none was xml.
Pipeline: 114 s wall for convert + build + run + compare, so no $longTimeouts floor is warranted.
Two emission moves that are NOT this lane’s — ADOPTED rather than restored, and that is a change of practice
The canary sweeps flagged two content movers. Neither can be this lane’s: a hand-owned C# corpus file is skipped by the converter and cannot influence emission at all. Both belong to converter changes already on master whose corpus regen is pending:
| File | Owner | Sweep that proves it |
|---|---|---|
internal/fmtsort/sort_test.cs — ptr.Value == → ptr == |
a8a0b1827, row-harvest-3’s “two unsafe.Pointers compare as BOXES” |
internal/fmtsort PASS 3 |
encoding/json/package_test_info.cs — +global using ΔToken = object; |
the alias-seeding chain — dd11e1e35 (2026-08-18) seeds an anonymous-interface RHS, which type Token any is; b3a5f56e1 later widened the same seed to named RHSs |
encoding/json PASS 491 |
The heavy-pair entry above RESTORED the ΔToken line, having rooted it correctly and identically.
This lane adopts it instead, and the disagreement is worth resolving rather than alternating. The
case for adopting is the one the near-miss lane made when it took internal/reflectlite’s
global using Kind = …abi_package.ΔKind;: a banked package’s committed test sources exist so the
suite is reproducible from a clone, and json’s are not — regenerate them today and you get a line
the repository does not have. Restoring keeps the commit tidy at the cost of a permanent
re-flag on every future sweep and a bank that no longer reproduces. Both movers are proven by this
lane’s own green sweeps of the exact packages they land in, which is the near-miss standard, so they
are adopted here and the practice is stated rather than left to the next lane to re-derive.
internal/fmtsort additionally shows the box-compare change reaches committed TEST sources, not only
the seven runtime production sites row-harvest-3 flagged for the next leveling regen.
Gates
Canaries first, because a bridge regression surfaces there before anywhere else: fmt 63/63
unmoved, internal/fmtsort 3/3, encoding/json 491/491 unmoved. Full behavioral suite PASS at 601 projects, 3,240.9 s — Transpile 601/601, Compile 601/601, Target 601/601 byte-identical, Output 575 compared / 0 failed (26 skipped, no package main), 0 timeouts / 0 NOT MEASURED. Guard
counter-proven failing-first by the A/B table above (both halves neutered separately, then together).
No converter, generator or golib source changed — the fix is one hand-owned corpus file
(reflect/deepequal_impl.cs, [module: GoManualConversion]) whose two changed functions are both
private static, so no public surface moved: no seeded-reconvert marker gate and no converter
go test/CNR are owed, and the compile risk is bounded to the one assembly the behavioral suite
builds 601 projects against.
The -tests tree churn classified per the standing rule: encoding/xml/package_init.cs gained the
initᴛᴛtests() hook (+7 real lines, the fourth -tests-closure shape) and was restored —
zero committed package_init.cs in the corpus carry it, and the tests project compiles
*_test.cs only (EnableDefaultCompileItems=false, explicit items), so the hook is inert to the
bank either way.
The queue, unchanged except for xml leaving it
- Channel direction as descriptor cargo — still the prerequisite for the written-and-measured
reflect.Value.Recvbridge (text/template50 → 51 of 52). html/template’s Root C —deferof a variadic func literal, ONE site in the corpus, sole wall on 243 verdicts.encoding/gob(106) — 103 of 106, now the closest unbanked package.sync/atomic’s zero-size-field layout, 5.%#xof auintptr— unchanged.
📉 html/template’s LAST wall falls — the package RUNS for the first time at 240 of 243, and the variadic-literal cast closes a second door nobody had opened (2026-08-19, lane claude/xml-bank)
Root C, taken as scoped. html/template goes from build-blocked with 0 measured to 240 of 243
matching, 0 empty verdicts, 0 terminations. It does not bank; the three residuals are named below
and none of them is this arc.
The wall was one diagnostic and the board had priced it correctly
Re-measured on this lane’s branch before changing anything: exactly one error, exactly where the near-miss entry left it.
examplefiles_test.cs(113,9): error CS0411: The type arguments for method
'builtin.defer<T1, T2>(Action<T1, T2>, T1, T2, ref GoFrame)' cannot be inferred from the usage.
The board offered two remedies. Remedy 1 was taken — keep the eager-argument rung, force the
temp-parameter form, and make the callee invocable by casting it — and its “mechanical obstacle” is
real but small: the cast goes around funcName immediately after convCallExpr builds it, which is
four lines and needs no new type-naming machinery, because iifeDelegateType has rendered
Actionꓸꓸꓸ<@string> from a variadic signature all along.
defer((ᴛ1, ᴛ2) => ((Actionꓸꓸꓸ<@string>)((params ꓸꓸꓸstring dirsʗp) => { … }))(ᴛ1, ᴛ2), dir1, dir2, ref ᒐ);
Go’s defer-TIME argument evaluation is untouched: dir1/dir2 remain the rung’s eager arguments,
ᴛ1/ᴛ2 are what the thunk receives at unwind. The guard asserts exactly that by reassigning both
arguments after the defer and checking the thunk still prints the originals.
Three things the shape needed that the two priced remedies did not mention
gohas the identical hole.visitGoStmt’s func-literal arm leavesrenderLambdaParamsfalse for the same reasonvisitDeferStmt’s did, sogo func(x ...T){…}(a, b)fails the same way. Both take the same arm, in the same change — the corpus has no such site today, and a fix that covers one and not its twin is the kind of asymmetry that gets rediscovered.- The NULLARY rung fights the fix.
defer f()normally trims the invocation to hand golib a method group, and a variadic literal has none: trimming((Actionꓸꓸꓸ<nint>)(<lit>))()leaves the family delegate in anActionslot (CS1503). The trim is now suppressed for this callee shape and the invocation is wrapped instead. Found by the guard, not by reasoning. - The IIFE interception’s own restriction was stale.
convCallExprphase 1a excludes a variadic literal with the comment “delegate type would need a params array” — untrue sinceiifeDelegateTypelearned the family form. Falling through to the normal path now lands on the same cast, sofunc(parts ...int) int { … }(1, 2, 3)(previously CS0149) compiles as a side effect rather than as separate work.
Two adjacent walls MEASURED here, deliberately not closed, neither caused by this change
The guard was written wider than the fix on purpose, and it paid twice.
| Wall | Repro that proves it independent | Why not here |
|---|---|---|
A SPREAD argument to any deferred variadic call — defer f(nums...) emits nums.ꓸꓸꓸ, a Span<T>, as the type argument of defer<T>; C# forbids a ref struct there (CS9244) |
a NAMED variadic callee, no func literal anywhere, emits the identical defer(ᴛ1 => f(ᴛ1), nums.ꓸꓸꓸ, ref ᒐ) and fails the same way — compiled and observed |
closing it means passing the SLICE and spreading inside the thunk, at every variadic deferred call in the corpus |
An empty variadic call passes an empty slice where Go passes NIL — f() on func f(parts ...int) answers parts == nil true in Go, false here |
a plain DIRECT call, no defer and no literal: Go prints 0 true, the conversion prints 0 false — compiled and observed |
argument CONSTRUCTION, corpus-wide reach, unrelated to delegate conversion |
Both are recorded rather than absorbed, and the guard carries a comment at each excluded row saying which wall it would otherwise be testing — so the next lane inherits the measurement, not the gap.
html/template — 240 of 243, and the residual is three DIFFERENT owners
| Test | Panic | Owner |
|---|---|---|
TestExecute |
reflect: recv on send-only channel, from walkRange |
queue #1, chan-direction as descriptor cargo — the identical root behind text/template’s last verdict. One arc retires both. |
TestErrors |
nil dereference in ж<T>.op_OnesComplement, from text/template/parse.ErrorContext |
a ~-deref defect in the shared parse package, unrooted here |
TestRedundantFuncs |
interface conversion: interface {} is <>f__AnonymousDelegate0, not go.Funcꓸꓸꓸ<object, @string> |
the same missing conversion as Root C, one boundary over: a variadic func in an any slot keeps C#’s SYNTHESIZED anonymous delegate type, so Go’s funcMap[n].(func(...any) string) assertion cannot match. Root C fixed the CALL-callee boundary; this is the assignment/composite-literal one. |
That third row is the useful one: it says the variadic-literal conversion gap is a family with at
least two members, and that the second member is now the cheapest thing standing between
html/template and a two-verdict residual. Whoever takes it should reuse variadicFuncLitCallee and
iifeDelegateType — the seam is already there.
Gates
Converter go test ./... ok, 375.3 s, zero failures (projitemsIntegrity_test included — no new
.go file, so nothing to register). Full check-no-regression.ps1 byte-identical across all 628
behavioral packages except this lane’s own guard artifact (DeferLambdaParam.cs, the intended new
golden), 0 NOT MEASURED, 0 advisory warnings, 1,255 s; preflight solution integrity 630/630,
path casing 4,527/4,527. Full behavioral suite PASS at 601 projects, 1,887.0 s — Transpile 601/601, Compile 601/601, Target 601/601 byte-identical, Output 575 compared / 0 failed (26 skipped), 0 timeouts / 0 NOT MEASURED. go2cs-stdlib.slnx Debug --no-incremental 0 errors, 446.9 s; go2cs.slnx Debug --no-incremental 0 errors, 873.0 s. Canaries re-run AFTER the converter change: fmt 63/63, internal/fmtsort 3/3, encoding/json 491/491, encoding/xml 386/386 — the row this lane banked, still reproducing from the committed roster.
Guard counter-proven failing-first by neutering each half separately: the temp-parameter force alone leaves CS0411 ×4, the delegate cast alone leaves CS0149/CS1503 ×6.
html/template’s -tests artifacts removed and its -tests-closure production dirt restored
(doc.cs, a CRLF phantom; package_init.cs’s initᴛᴛtests hook, +7 real lines) — the package does
not validate, so no roster row, no proof page, no committed test sources.
The queue
- Channel direction as descriptor cargo — unchanged at the top, and now gating THREE things:
the written-and-measured
reflect.Value.Recvbridge,text/template’s last verdict, andhtml/template’sTestExecute. - A variadic func in an
anyslot — new, and the cheapest ofhtml/template’s three; reuses this arc’s seam. encoding/gob(106) — 103 of 106, the closest unbanked package.sync/atomic’s zero-size-field layout, 5.%#xof auintptr— unchanged; joined by the two variadic-argument walls above, which are cheap to state and corpus-wide in reach.
📐 The Caller path is SPELLED Go’s way — this lane opened on two stale premises, and the one live defect it found is the half of flag’s #line remedy nobody priced (2026-08-19, lane claude/edwards25519-a, laptop G)
This lane was briefed to implement crypto/internal/edwards25519’s ratified Option A and, as a stretch,
flag’s #line arc. It re-measured first, per the standing never-trust-a-census-older-than-three-merges
rule, and both premises were stale — in opposite directions. What follows is the re-measurement, the
one live defect it surfaced, and the arc it re-prices with evidence.
Premise 1 — Option A is DONE, and the board still says it is not
81a08a15a (2026-08-11, lane L4) landed the tuple-spec init-order relocation exactly as the ratified
FINDING-init-order-tuple-specs.md §5A specified: both emission
sub-shapes, the refusal warning retired, TestPackageTupleVarSpecInitOrderRelocation and the
InitOrderTupleSpecs behavioral test as guards. 3907a064c banked the production emission
(crypto/internal/edwards25519/package_init.cs, calling initᴛidentity(); initᴛgenerator();), so the
r60 harvest note that it is “deliberately UNCOMMITTED” is spent too.
The ranked queue handed on by row-harvest-3 still reads “the ratified Option A tuple-spec init-order fix is still unimplemented” as its queue item 5. That sentence is what produced this lane. Two board statements about one package disagreed, the older one was right, and nothing in the file said which was which. Recorded here as the correction.
Re-measured on master today (98bff3efc; the converter was rebuilt from HEAD first — the binary in
the worktree was a day stale and 20 KB different, which is route #1 waiting to happen):
| verdicts | 55 |
| matching | 54 |
| divergent | 1 — TestAllocations |
| excluded | 4 benchmarks (Phase-4D) |
| skipped / disclosed | 0 / 0 |
That reproduces r60’s 54/55 exactly. The residual’s number has moved: TestAllocations now reports
98 objects per run (9,800 over 100 runs, 1,324,800 bytes) where r60 measured 109 — the week’s arcs
took eleven off it without anyone claiming them. It stays undisclosed on the standing ruling (a
near-budget lower-bound count is an optimization target, not an impossibility) and the package stays
unbanked, held by that one row. No converter or corpus change was needed or made for edwards25519.
Premise 2 — flag’s 23 of 24 is NOT on master
The stretch was briefed against flag at 23 of 24 with TestDefineAfterSet as the sole residual. That
measurement is real, but it belongs to claude/heavy-pair-7be2d2, which is pushed and unmerged:
7eeeda893 (the ImplementGenerator forward-name fix) and db3bb4b9c (the reflect reinterpret fix)
are not ancestors of master. Measured on master, flag still sits behind its CS1929 ×10 build
wall — this lane re-ran the pipeline and got the same ten diagnostics against the same five test-file
flag.Value implementors, with every verdict empty. So from master there is no 23-of-24 to finish, and
flag’s #line consumer is not reachable until that branch merges.
⚠ Worth generalizing: git log --all lists commits on branches HEAD does not contain, so a brief quoting
a merged-sounding result is not evidence that the result is on master. git merge-base --is-ancestor <sha>
HEAD is the check. It cost this lane one pipeline run to learn.
What DID land: runtime.Caller spells its path Go’s way
Chasing the #line remedy surfaced a live, previously unnamed defect one layer beneath it.
Go records source paths with forward slashes on every platform. Measured directly on this Windows box
with a Go control program: runtime.Caller and runtime.CallersFrames both answer
C:/Program Files/Go/src/runtime/proc.go — forward slashes, drive letter and all. The CLR hands back
whatever the PDB holds, which on Windows is backslash-separated, and internCallerFrame recorded it
verbatim. Every converted program therefore answered C:\…\log_test.cs where Go answers
C:/…/log_test.go: a divergence in the spelling of the path, sitting on top of the already-recorded,
deliberately deferred divergence in what the path points at.
The two are independent, and only the second is the position-map arc. The first is an ordinary fidelity
gap, and it is now closed: goSourcePath (runtime/managed_impl.cs) applies Go’s rule at the two places
a frame’s file reaches a program — internCallerFrame, which every Caller / Callers / CallersFrames
answer is interned through, and appendGoFrames, which renders runtime.Stack’s traceback. Converted
path/filepath accepts either separator on Windows exactly as Go’s does, so no consumer pays for the
normalization, and log’s Lshortfile trim — which looks for the last / — starts working as a side
effect.
Guard, proven failing-first (RuntimeCallerFrames, the project that already guards Caller’s frame
accounting): five assertions across the three surfaces, output-compared against go run. Before the fix
exactly those five lines diverged, and all eleven pre-existing assertions still matched:
| assertion | Go | C# before | C# after |
|---|---|---|---|
| caller file uses forward slash | true | false | true |
| caller file uses host separator | false | true | false |
| frames files use forward slash | true | false | true |
| frames files use host separator | false | true | false |
| traceback uses host separator | false | true | false |
On a forward-slash host all five are trivially true on both sides, so the guard bites on Windows, which is where the two spellings differ — said in the test’s own comment, so a Linux run’s silence is not mistaken for coverage.
The #line arc, re-priced with four measurements
The board prices the remedy for the source-file-identity class as “emit #line directives … which would
make BOTH the file and the line Go’s, natively through the PDB.” Most of that holds; one load-bearing
part does not. Measured in a probe project rather than argued:
#linedoes reachStackFrame. A#line 852 "…/flag_test.go"region makesGetFileName()/GetFileLineNumber()answer with that file and that line. The PDB route needs no golib change — that part of the pricing is confirmed.- The
#lineDIRECTIVE cannot supply the separator. Roslyn resolves and normalizes the directive’s path. Absolute forward-slash (C:/Program Files/Go/…), unix-rooted (/go/src/…) and relative (go/src/…) forms all came back backslash-separated —C:\Program Files\Go\src\flag\flag_test.go. No spelling of the directive survives. So#linealone leavesTestDefineAfterSetfailing, because`.*/flag_test.go:.*`needs a literal/before the filename. The remedy is two halves —#linesupplies the.goextension and the Go line,goSourcePathsupplies the separator — and they compose, because the normalizer runs over whatever the PDB hands back. With the separator half landed, the arc’s remaining distance onflagis one miss, not two. ⚠ One qualifier, measured in the same probe rather than left as a stronger claim than the evidence supports: thePathMapcompiler option does produce a forward-slash path (-p:PathMap=C:\Program Files\Go\src\=/goroot/yielded/goroot/flag/flag_test.go). So the separator is reachable at the compiler as well — just worse there.PathMapis a whole-compilation option that rewrites every source path in the PDB including the emitted.cspositions, it needs the GOROOT prefix as a per-package build property, and it does nothing forruntime.Stackor for any frame reached without a directive. That is why this lane settled the separator at the runtime. - A CS diagnostic inside a
#lineregion reports the GO position — measured:C:\Program Files\Go\src\flag\flag_test.go(854,17): error CS0029. This cost is not on the board and it is not small: every census this board carries is of the shape<file>.cs(NNN): CS####, and the whole compile-wall workflow reads diagnostics against emitted C# line numbers. A corpus-wide#lineemission relocates all of them onto.gofiles that are not in the project. - Per-statement
#lineadds ~28–47% more lines to a converted file — measured onflag/flag.cs(+28%),strings/strings.cs(+42%) andedwards25519/edwards25519.cs(+47%) — interleaved between every statement, across ~3,200 corpus.csfiles and ~600 behavioral goldens.
Costs 3 and 4 are exactly the two the side-car alternative does not pay; it pays a file and a csproj
item per package instead. That trade now has numbers on both sides, which it did not before. This lane
did not take either half of the emission arc: it changes every golden in the corpus and cuts against a
stated project goal, which is coordinator territory, and from master it would not have closed flag
anyway (premise 2).
Blast-radius census for the separator fix
A verdict can only move if something reads the file string. Measured over the 154-row roster:
- 6 roster packages whose own
_test.gocallruntime.Caller/Callers/Stack/debug.Stack—context,encoding/base64,encoding/json,io,log/slog/internal/benchmarks,sync. - 5 more whose converted PRODUCTION code calls them —
database/sql,go/types,internal/fuzz,os/exec,testing/slogtest. (The full production-consumer set is 14 packages; those five are the ones on the roster.) - The hand-owned
testinghost does NOT useruntime.Caller— it reports panics through .NET’s ownex.StackTrace— so no verdict can move through the host itself.
Gates
A src/core/runtime hand-own change plus one behavioral guard; no converter change, so no CNR is owed
(CNR gates converter emission, and the one golden that moved here moved because its .go source did).
| Gate | Result |
|---|---|
go2cs-stdlib.slnx (--no-incremental) |
0 errors, 157 warnings, 369 s |
go2cs.slnx (--no-incremental) |
0 errors, 287 warnings, 728 s |
| full behavioral suite | PASS — 601 projects, 1,862.8 s; Transpile 601, Compile 601/0/0, Target 601/601 byte-identical, Output 575 compared + 26 skip |
full run-validated-sweep.ps1 (154 packages, 17,730 expected verdicts) |
152 pass / 2 fail, 14,729 s (4 h 05 m) — both failures proven pre-existing, below |
RuntimeCallerFrames filtered |
PASS all four phases; failing-first proven by the table above |
CNR is not separately owed: it gates converter emission, there is no converter change, and the behavioral
suite’s Transpile + Target phases re-transpiled all 601 packages and byte-compared every golden —
which is the stronger form of the same check. The one golden that moved moved because its .go source
did, and it was re-baselined through UpdateTestTargets --createTargetFiles (which touched nothing else:
no other .cs.target, no *Tests.cs test-class block).
Both sweep failures are PRE-EXISTING — and one of them is a banked row that has regressed on master
Neither package’s tests call runtime.Caller/Callers/Stack at all, and both were re-run individually
to root them rather than left as sweep noise.
debug/dwarf— a FLAKE. Re-run alone it validates 40 of 40, 0 divergent, statusvalidated. The sweep’s failure was the Go oracle’s own package-levelfailat 0.97 s with every named test passing. Nothing to chase; recorded so the next sweep reader does not chase it either.-
crypto/tls— a REAL, PRE-EXISTING regression against its own banked row. ⚠ It banked at 400 + 2 = 402 on 2026-08-18 (154d5b5ce). Measured today it is 397 of 402, with five real divergences, all Go=pass / C#=fail:TestBogoSuite TestCertCache TestMarshalUnmarshal TestMarshalUnmarshal/*tls.SessionState TestQUICHandshakeErrorA/B’d against master with this lane’s change reverted and the build purged: the same failures, so they are not this lane’s. They arrived with something merged after the tls bank — the two candidates are
c961112ad(row-harvest-3) and98bff3efc(near-miss-finish-batch).TestMarshalUnmarshalis aquick.Checkover*tls.SessionState, i.e. reflection-bridge territory, and the near-miss batch is six bridge fixes plus a recv-bridge revert — the most likely place to look first, stated as a lead and not as a root. The flagship row is red at master and no gate was watching, which is the finding worth acting on regardless of who takes it. One incidental measurement from the same pair of runs: the Go oracle expandedTestBogoSuiteinto 3,243 subtests on one run (3,644 Go verdicts) and did not on the next (402), same machine, same command — so BoGo expansion is nondeterministic here, and a sweep that catches the expanded form compares 402 C# verdicts against 3,644 Go ones. That asymmetry is cosmetic for rooting: the five real divergences are identical in both shapes.
For the next lane
- The position-map arc now has a named first half that is already paid. Whoever takes it inherits
goSourcePathand needs only the.goidentity — and should choose between#lineand the side-car on measurements 3 and 4 above, not on the older two-line pricing. flagcannot be finished from master.claude/heavy-pair-7be2d2merges first; the residual after it is one miss, not two.edwards25519is one row from banking and the row is moving on its own — 109 objects at r60, 98 today. The ж-box arc owns it; nothing else in the package is open.crypto/tlsneeds an owner now. Five verdicts against a banked 402, pre-existing, bisectable across two merges. Nothing banks or re-banks while the flagship row is red.- Nothing banked here. No roster row moved, no proof page was written, no test sources were
committed:
edwards25519stays at 54/55 andflagis not reachable from master. The proof page the sweep regenerated (docs/validation/current/archive.tar.md, which flippedTestFileInfoHeaderSymlinktoskip/skip— both runtimes agreeing, on an unprivileged host) was restored, not banked: it encodes this machine’s symlink privilege, not a validation change.✅ The crypto/tls regression is ROOTED and FIXED — the guilty merge predates the bank’s own merge, the mechanism is the whitebox test model’s two latent defects, and the fix reverts nothing (2026-08-19, lane
claude/tls-regression, laptop G)
The edwards25519-a entry above found the flagship row red at master (397 of 402, five divergences) and named two candidate merges. Both candidates were innocent — and so was every merge in the briefed window. The bisection had to walk PAST the bank itself to converge, and where it landed is the finding that matters beyond this row.
The bisection — seven probes, and the window was wrong
The reproduce (filtered sweep at master, this host) confirmed three REAL divergent tests —
TestMarshalUnmarshal (+ its *tls.SessionState child) and TestQUICHandshakeError — plus
TestBogoSuite’s 3,242 Go-side children flooding as one-sided rows (below), with TestCertCache
matching its pinned disclosure normally. A fast probe replaced the 11-minute sweep for bisection:
convert + build the tls tests closure at each commit (scoped purge first) and run the two failing
tests directly in the built host, ~4 minutes per probe. Verdicts, newest first:
| probe | MarshalUnmarshal / QUIC | emitted test-source shape |
|---|---|---|
98bff3efc (near-miss batch) |
RED | whitebox-reference |
c961112ad (row-harvest-3) |
RED | whitebox-reference |
0390fece4 (variadic-call) |
RED | whitebox-reference |
5a16458ad (the tls bank’s own merge) |
RED | whitebox-reference |
837fd3a01 (reflect-minibridge) |
RED | whitebox-reference |
d1ed1f7c1 (local-iface-cast — GUILTY) |
RED | whitebox-reference |
b5a82df19 (its first-parent) |
GREEN | recompile |
So the regression entered with d1ed1f7c1 — which merged BEFORE the bank. The tls-mint-site lane
forked at b5a82df19, proved 400+2 on its own tip (whose converter still emitted the RECOMPILE
test model for tls), and merged at 5a16458ad — into a master that already carried local-iface-cast.
Each side green alone; the union red; nothing swept the union. A lane’s sweep proof binds its own
tree, never the merge result — the process rule this adds to CLAUDE.md’s gate doctrine, alongside
the derived-never-carried reflect-canary set.
The mechanism — a model FLIP exposing two latent whitebox defects
d1ed1f7c1’s converter change (“a local named value source in call syntax records its pair”) moved
records off the production-anchored partition, so recordsRequireProductionAnchor stopped firing
for crypto/tls and the test conversion stopped FALLING BACK to the recompile model: tls converts
under whitebox-reference from that commit on (production types stay in the referenced
crypto.tls.dll; test sources compile alone against a friend bridge). That flip is INTENDED — it is
the model encoding/xml banked 386/386 under — but tls’s suite exercises two whitebox shapes xml’s
does not, and both had latent defects:
-
ImplementGenerator stubbed bridge-implemented interface members (
TestMarshalUnmarshal).*SessionStatesatisfies the internalhandshakeMessageinterface ONLY through test-declared methods (marshal/unmarshallive in handshake_messages_test.go — Go lets test files add methods to production types). The sealing-marker classification (“a cross-assembly unexported interface method with nothing to forward to is a stub”) gathers its no-local-implementation evidence from the struct’s own declaration syntax — which a PRODUCTION type referenced from a test compilation does not have — so both members classified as markers and the adapter compiled with=> default!bodies: marshal answered an EMPTY buffer with nil error, unmarshal answered false, and the test reported “#15 failed to unmarshal 0x…” with no diagnostic anywhere (the trailing%xprinting nothing was the tell). Fix: the evidence scan now covers the friend bridge’s extensions by receiver simple name in BOTH forms — direct-ж, and[GoRecv] ref(which forwards through its RecvGenerator ж-twin, exactly asIsRefRecvalready routes the local form). Guarded failing-first byGenTests.WhiteboxBridgeAdapterTests(the REAL generator run over a two-assembly model of this shape) and the ref-scan unit rows inFriendBridgeBoxReceiverTests. -
golib’s
error<T>shell never joined the carrier-unwrap protocol (TestQUICHandshakeError). Go interface equality is decided on the dynamic (type, value), never on which CARRIER holds it — and under whitebox the same AlertError value legitimately rides two different carriers: production has noGoImplement<AlertError, error>record (it only ever boxes the value intoany), so fmt’s%wassert minted golib’s runtime shellerror<AlertError>, while the test’serrors.Istarget arrived as the test assembly’s generatedᴠvalue adapter. Every go2cs-gen-generated shell implementsIInterfaceAdaptersoAreEqualcan unwrap it;error<T>— the ONE hand-written shell — did not, so it fell to reference equality and the same alert value never matched itself (quicErrorthen failed to find the chain’salertthe same way, wrappingalertInternalError). Fix:error<T>implementsIInterfaceAdapterwith the identical member every generated shell carries — the ж box when pointer-backed (Go pointer identity), the value otherwise. Guarded failing-first byGolibTests.ErrorShellCarrierEqualityTests(both flavors, plus the protocol membership itself). Under recompile neither defect could fire: production sources recompiled INTO the test assembly, so the struct declaration was local (evidence present) andAlertErrorimplementederrornominally (no carrier on either side).
What this run could and could not verify on this host
Post-fix, the filtered sweep’s real divergence set is empty — TestMarshalUnmarshal,
TestMarshalUnmarshal/*tls.SessionState and TestQUICHandshakeError all pass THROUGH the sweep
machinery, and TestCertCache matches its pinned disclosure. What laptop G cannot reproduce is the
banked run’s TestBogoSuite shape: here the GO ORACLE’s own BoGo run expands to 3,243 subtests and
fails at its child runner’s 600 s deadline (642 s, package-level fail), so its passing Go-side
children flood the comparison as one-sided rows and no disclosure arm can withdraw them — the same
host asymmetry the edwards25519-a entry measured pre-fix, present in every probe including the
GREEN one, and unreachable from any go2cs layer. The row’s 400+2 confirmation on a host whose Go
BoGo behaves as banked (laptop R ran it in 32 s) is owed at merge, per the new post-merge-bank rule.
The banked test sources were deliberately NOT refreshed: the committed tree still carries the
recompile-shape emission from the bank, the reconvert now produces the whitebox shape, and that
refresh belongs to the next milestone rebank, not to a regression fix.
Corrections to the record
- The edwards25519-a entry’s suspect window (
c961112ad/98bff3efc) was wrong in both directions: the guilty merge is EARLIER than the window’s start, and every merge inside the window is clean. - That entry listed five divergences including
TestBogoSuiteandTestCertCache; re-measured here,TestCertCachematches its disclosure every run (its “divergence” reading was the BoGo-flood arithmetic), andTestBogoSuite’s is the host-environmental Go-oracle shape above, not a code regression. The REAL regression was exactly three verdict rows, all fixed. 98bff3efc’s near-miss entry can keep its wins: nothing here reverts the recv-bridge revert, the six bridge fixes, or the discard/alias work — the fix is additive at both layers.
⚠ runtime/debug’s COMPILE WALL IS CLOSED — 0 of 9 → 2 of 9, and the traceback’s RECEIVER half lands; the residual is three capability roots and the deferred position map (2026-08-19, lane claude/runtime-debug)
The board carried this package at 0 of 9 behind CS0264 + CS0715, “the same static-class-operator
defect as internal/reflectlite. Two packages, one root” — and then recorded, when reflectlite’s
CS0715 retired, that runtime/debug was “still unmeasured since the CS0715 retirement”. It is
measured now, and the recorded wall is entirely gone: the host builds with zero CS errors (warnings only, all
pre-existing corpus classes) and every one of the nine verdicts is real.
The re-measure
2 of 9 match — TestSetGCPercent skip-parity (Go skips it on Windows: testenv.SkipFlaky,
issue 20076) and TestSetMaxThreadsOvf pass. Go’s own baseline for the package is 8 pass + 1 skip,
so the denominator is right. The seven failures decompose into four roots, none of them a
converter defect:
| Test(s) | Root | Class |
|---|---|---|
TestWriteHeapDumpNonempty, TestWriteHeapDumpFinalizers, TestWriteHeapDumpTypeName |
WriteHeapDump throws — Go’s heap-dump format is a serialization of the Go heap through Go’s own type descriptors, which do not exist under the CLR |
capability, outside the four disclosure classes |
TestReadGCStats |
no per-GC pause HISTORY. ReadMemStats reports a real NumGC (6) and PauseTotalNs but leaves PauseNs/PauseEnd/LastGC zero; the hand-owned readGCStats reports the same aggregates and an empty history. The test is a self-consistency check between the two surfaces (len(stats.Pause) == min(NumGC, 256)), so it needs no real pause values — it needs the two to agree, which today they do not |
capability, outside the four classes |
TestFreeOSMemory |
MemStats.HeapReleased is deliberately zero, so after.HeapReleased <= before.HeapReleased fires as no memory released: 0 -> 0 |
capability, outside the four classes |
TestStack |
the traceback’s file paths point at the emitted .cs, where Go names the .go — the deferred Go-source position map the board already carries |
deferred arc |
Three of the four would need a disclosure class that does not exist — “a runtime capability the
managed runtime does not provide”, which none of the classes actually in use reaches
(alloc-profile, alloc-count-semantics, codegen-liveness, host-limit, chan-direction; see
the class-count note below) — and the fourth is coordinator-deferred. The lane therefore did not
bank, did not disclose, and did not fake any of the three — a one-byte write would satisfy all
three heapdump assertions (none of them parses the dump), and that is exactly the kind of
technicality the four-class bar exists to refuse. Boarded for a ruling, not taken.
What DID land: a traceback frame is SPELLED Go’s way on the receiver, too
Chasing TestStack surfaced the second half of the fidelity gap 35c806679 opened. That commit
separated which source a frame’s path points at (the deferred position map) from how the path is
written (an ordinary fidelity gap, closed). The same separation applies one field over: which
function a frame names, versus how the NAME is written.
Go names a method frame with its receiver type between the package and the method. Measured on this box with a Go control, on both surfaces a program can read:
| shape | Go’s traceback / Frame.Function |
|---|---|
| pointer receiver | main.(*T).ptrmethod |
| value receiver | main.T.method |
| generic receiver | main.G[...].gmethod — the LITERAL [...], never the instantiated argument |
| package-level func | main.plain — no qualifier |
goFrameName (runtime/managed_impl.cs) emitted the flat <pkg>.<name> for all of them, so a
method frame answered runtime/debug_test.ptrmethod where Go answers
runtime/debug_test.(*T).ptrmethod. A converted Go method is a C# extension method on the
package class whose first parameter is the receiver — this ref T for a pointer receiver (the
[GoRecv] form), this T for a value one, RecvGenerator’s boxed this ж<T> overload for the
pointer form reached through a pointer value — and that this is the whole discriminator: a
package-level func is a plain static method and keeps its bare name, exactly as Go renders one. The
boxed form is detected through IPointer<T> rather than ж<T> itself, so a generated named-pointer
wrapper answers the same way.
Both call sites of goFrameName gain it, which is what Go does: appendGoFrames (the rendered
traceback) and internCallerFrame (every Caller/Callers/CallersFrames answer’s
Frame.Function) — the Go control confirms the two spellings are identical upstream.
Proven against Go’s own test, not only against a guard. TestStack’s assertion failures went
7 → 5 on the same pipeline run: the two function-name assertions
(runtime/debug_test.(*T).ptrmethod, runtime/debug_test.T.method) now agree with Go, and the five
that remain are all the file-path half — four expected prefix "\truntime/debug/stack_test.go" and
one "\ttesting/testing.go". That is the position-map arc and nothing else, so runtime/debug is
now a named consumer of it: closing the position map is worth one more verdict here.
Guard, proven failing-first (RuntimeCallerFrames, the project that already guards Caller’s
frame accounting and the separator): five assertions across the three shapes plus two negative
controls, output-compared against go run. Before the fix exactly those five diverged, all
inverted, and all sixteen pre-existing assertions still matched:
| assertion | Go | C# before | C# after |
|---|---|---|---|
| traceback names pointer receiver | true | false | true |
| traceback names value receiver | true | false | true |
| traceback drops pointer receiver | false | true | false |
| traceback drops value receiver | false | true | false |
| traceback names generic receiver | true | false | true |
| traceback names plain func (control) | true | true | true |
| traceback parenthesizes plain func (control) | false | false | false |
The two controls are the point of the negative half: a package-level func must NOT grow a qualifier, and the guard fails in both directions.
The measurement hazard this lane paid for
BehavioralRunner builds Release; a worktree’s stale bin/Debug is a FALSE reading. The
first failing-first diff was taken against bin\Debug\net9.0\<proj>.exe, which in a reused worktree
was eleven days old — from before 35c806679 landed — so it printed 11 of 23 lines and the diff
read as “the C# side crashes at the separator assertions”, a defect that does not exist. The
runner’s artifact is bin\Release\net9.0; against it the failing-first state was the clean
five-line inversion above. A direct dotnet build <behavioral>.csproj is also NOT a substitute for
reading the runner’s artifact: outside solution context $(go2csPath) does not resolve and the
build dies in CS0234/CS0246 on golib itself, which reads like a corpus break and is not one.
What this lane did NOT do
No roster row, no proof page, no disclosures, no committed test sources: runtime/debug does not
validate at 2 of 9. The package’s -tests artifacts are removed and the corpus restored, so the
tracked changes are the one runtime hand-own, the guard’s Go source, and its three regenerated
emission files.
A doc drift found while ruling the disclosures — the roster says FOUR classes, the manifests use FIVE
docs/ValidatedTestPackages.md states “Four classes exist” and names alloc-profile,
codegen-liveness, host-limit, chan-direction. The 21 committed manifests use five class
strings: those four (41 / 5 / 26 / 3 uses) plus alloc-count-semantics (6 uses, in context,
io, os, strings), which the board itself calls “the established alloc-count-semantics class
(io, strings, bytes)” and ConversionStrategies-Reference.md records as strings’ first use. It is
a real, ruled class — AllocsPerRun counts mallocs in Go and BYTES on the CLR, so a count assert
can never agree — and it is simply missing from the roster’s prose. Not fixed here (it changes
published roster text and the framing its arithmetic hangs off); flagged for a coordinator pass.
It changes nothing about this package: none of the five reaches “a runtime capability the managed
runtime does not provide”, which is what all three of runtime/debug’s capability roots need.
What runtime/debug needs, ranked — three rulings and one arc
- A ruling on the capability class.
WriteHeapDump(3 verdicts), the per-GC pause history (1), andHeapReleased(1) are five of the seven, and all three are the same question: may a package disclose a runtime capability the CLR does not provide? Thehost-limitbar is written narrowly and deliberately — “a structural property of the deployment shape, never an unimplemented-but-fixable defect” — and none of these is a deployment-shape property. Two of the three are also arguably fixable at real cost, which is what makes it a ruling rather than a reading:HeapReleasedhas a managed analogue (the cumulative decrease inGCMemoryInfo.TotalCommittedBytes, i.e. memory this process handed back), but it is a NEW measurement surface everyReadMemStatsconsumer inherits,runtime/metricsincluded.- The pause history is reachable — a gen2 GC callback or an EventPipe listener can record
real
PauseNs/PauseEnd, andNumGCalready counts gen2 collections, so the ring would line up by construction — but it is an always-on recorder every converted program pays for, and finalizer-timing skew would make the count assert flaky. A design, not a defect fix. WriteHeapDumpis the one with no honest managed form at all: Go’s format is defined over Go’s type descriptors. Note the trap for whoever takes it — none of the three tests parses the dump (they checksize >= 1and “does not crash”), so a one-byte write banks all three and proves nothing.
- The position map —
TestStack’s remaining five assertions, and this package is now a named consumer alongsidelog,log/slogandflag. The frame-SPELLING half is fully paid on both axes now (separator35c806679, receiver here); what is left is purely the.goidentity. Two details for whoever takes it, both measured here: the test’sframe()helper checks a code string AND a file prefix per frame, and all five code checks now pass — the five failures are one file-prefix miss each. And the expected prefix is the bare-trimpathform (runtime/debug/stack.go, no GOROOT), because the test spawns a child to read the binary’s BAKED-IN GOROOT and the convertedruntime.GOROOT()answers empty. Incidentally that child round trip —os.Executable()+exec.Commandon the converted host — WORKS; it is only the path identity that is missing. - Nothing else. There is no converter defect in this package. Whoever returns after a ruling inherits a package that builds clean, produces nine honest verdicts, and has each of them rooted.
Gates
A src/core/runtime hand-own change plus one behavioral guard; no converter change, so no CNR is
owed — and the behavioral suite’s Transpile+Target phases are the stronger form of the same check.
| Gate | Result |
|---|---|
| GolibTests | 172/172, 16 s |
go2cs.slnx Debug --no-incremental |
0 errors, 617 s |
| Full behavioral suite | PASS — 601 projects: Transpile 601, Compile 601/0/0, Target 601/601 byte-identical, Output 575 + 26 skip, 0 fail, 2,838 s |
| Targeted validated sweep, the traceback blast radius | 20 packages / 1,702 verdicts / 0 fail, 839 s |
runtime/debug pipeline, before and after |
2 of 9 both runs; TestStack 7 assertion misses → 5 |
The blast radius was re-derived against the CURRENT roster rather than reused: the board’s census for
the separator fix named 11 packages, and it is 12 now — internal/reflectlite joined the roster on
2026-08-18, after that census was taken. Six roster packages’ own _test.go read a traceback
(context, encoding/base64, encoding/json, io, log/slog/internal/benchmarks, sync) and six
more read one from converted PRODUCTION code (database/sql, go/types, internal/fuzz,
internal/reflectlite, os/exec, testing/slogtest); substring filters pulled in eight more for free.
Two facts narrow the risk further, both measured: only RuntimeCallerFrames in the whole 601-project
behavioral corpus reads a traceback at all, and every Go test that pins Frame.Function (log/slog’s
two) pins a package-level name, which this change leaves untouched by construction.
Standing post-sweep dirt, classified — and one PRE-EXISTING staleness worth recording
Every tracked file the sweep moved was restored, not banked. All of it is the documented classes:
CRLF phantoms (base64_test.cs, five encoding/json *_test.cs), the global::go.* root escape
(bufio/{bufio,scan}.cs, internal/reflectlite/{swapper,type,value}.cs), and the initᴛᴛtests()
package_init.cs hook at +7 real lines (go/types, internal/fuzz).
One is NOT on the board and should be: encoding/base64’s committed test-info files are stale
against the current converter. A -tests re-run emits them SMALLER — package_test_info.cs loses
six lines and package_info_internal_test.cs two, all of it the <ImportedTypeAliases> block’s
bytes/strings aliases and four GoImplicitConv<…, ж<…>>(Indirect = true) records. Root: the
converter mints that block from each imported package’s production package_info.cs, and
bytes/strings no longer carry those records there — they live in bytes/package_info_internal_test.cs
now — so the committed files predate that move. encoding/base32 and fmt hold the same records and
would drift the same way. It is benign (base64 validates 17/17 with the block empty) and it is
pre-existing, not this lane’s: A/B’d by stashing the runtime change and re-running the same filter,
which reproduced 0 2 and 0 6 on the same two files with an identical 17/17. Recorded so the next
sweep reader restores it instead of chasing it — or levels all three at a rebank.
✅ net/netip’s CS0019 pair CLOSES, and the churn the deferral was waiting on measures ZERO (2026-08-20, lane claude/runtime-debug)
The board deferred this one explicitly: “Bounded: the shape is a complement of a BasicLit whose
go/types type is a basic other than int … Deliberately NOT taken here — it is a production-reachable
emission change and belongs in an arc with room to measure its corpus churn.” The measurement is the
deliverable, so it was taken first and the fix shaped around it.
The defect, and why exactly ONE width has it
Go’s &^ renders & ~, and C# applies ~ to a promoted operand — so a bare literal complements
as int. Whether that binds is a property of the enclosing result type, and it binds everywhere but
one place:
| result | x &^ 1 emits |
binds? |
|---|---|---|
narrower than int (byte, ushort) |
(byte)(x & ~1) |
yes — promotes to int, truncates under the result cast |
uint32 |
(uint32)(u & ~1) |
yes — uint & int promotes both to long (debug/macho’s Magic32 &^ 1 is the corpus instance) |
int64 |
(int64)(x & ~1) |
yes — the int widens |
uint64 |
(uint64)(x & ~1) |
no — ulong and int share no type C# converts to → CS0019 |
nuint / uintptr |
already cast | yes — this file’s native-int block has imposed the type since NativeIntConstMask |
So the rule is not “cast constants under &^”; it is “uint64 completes the unsigned-64 group
uint64/nuint/uintptr that convBinaryExpr.go already treats together” (the constant-fold arm
groups the same three, for the same reason). The other two carry the cast already; this adds the third.
The predicate is a BasicLit, and that is what makes the churn zero
tv.Value != nil would have been the obvious predicate and would have been wrong. A BasicLit is the
only operand shape that reaches ~ untyped:
- a named untyped-const ref is cast by the
isUntypedNamedConstRefblock directly above; - a Go conversion (
uint64(1)) already renders typed; - a computed constant subtree carries the width cast from the shift-retype path —
hi &^ (1 << 63)emits~(((uint64)1 << 63))today, and re-casting it would be pure churn.
That distinction is why the corpus number below is 0 and not “a few dozen”.
The churn, measured twice under identical seeding
Two seeded whole-stdlib reconverts, each into a FRESH wiped root (6,004 files seeded; bin/obj/
Generated excluded — the converter reads none of them, and the analyzer’s generated filenames blow
past MAX_PATH under any temp root), classified emitted-vs-seeded by sentinel mtime:
| Seeded full-corpus reconvert | emitted | identical | differing | new | marked | clobbered |
|---|---|---|---|---|---|---|
| this branch (549 s) | 1,664 | 1,606 | 58 | 0 | 63 | 0 |
| CONTROL, master’s converter (548 s) | 1,664 | 1,606 | 58 | 0 | 63 | 0 |
The differing sets are identical file for file — diff of the two lists is empty — so the set
difference is ∅ and the fix moves ZERO corpus files. The 58 are master’s own standing pre-regen
drift (the known core/README.md attribution phantom, encoding/xml/README.md, and a spread of
doc.cs), unchanged by this lane and outside its scope. Full CNR agrees from the other side: 628
behavioral packages re-transpiled unconditionally, one mover — the guard itself — 0 NOT MEASURED.
So the deferral’s premise is now answered: the churn is nil, and the fix stands.
Guard, proven failing-first — including the negative half
BitwiseUntypedConst already guards this exact family for a NAMED untyped const
(Float64bits(f) &^ signBit); the bare literal is its sibling and now lives beside it. Before the fix
the project failed to COMPILE with exactly one diagnostic, and it is netip’s verbatim:
main.cs(13,21): error CS0019: Operator '&' cannot be applied to operands of type 'ulong' and 'int'
clearLow32 (uint32) is the negative control and it is the load-bearing half: it compiled before
the fix and emits (uint32)(u & ~1) unchanged after it, which is what demonstrates the change is
exactly as wide as the defect rather than a blanket cast.
Gates
| Gate | Result |
|---|---|
converter go test ./... |
ok, 469 s |
| full CNR (628 behavioral packages, unconditional re-transpile) | 1 mover — the guard’s own main.cs — 0 NOT MEASURED, 0 WARNING, 1,080 s |
| seeded whole-stdlib reconvert, branch and control | 63 marked / 0 clobbered both; differing sets identical; 549 s / 548 s |
BitwiseUntypedConst |
Transpile/Compile/Target/Output all pass; failing-first proven |
The measurement hazard this half paid for
A seeded reconvert cannot run under the scratchpad path. At 161 characters it leaves too little
headroom under Windows MAX_PATH, and the seed copy dies part-way through archive/tar on an
ImplementGenerator output whose filename alone is ~140 characters — reported as
Could not find a part of the path, which reads like a missing source and is not one. Use a SHORT
lane-prefixed root and exclude the build-output directories; the ritual’s inputs (committed
.cs/.csproj/README.md, version.props, docs/validation) are all that is actually seeded for.
net/netip re-censused: 7 build errors → 5, and the residual is ONE root
The pipeline was re-run against the fixed converter. The CS0019 pair is gone and nothing replaced it;
net/netip now stops on five diagnostics in a single family, all fuzz_test.cs:
CS0315 ×5 — 'ΔAddr' / 'AddrPort' ×2 / 'ΔPrefix' ×2 cannot be used as type parameter 'P' in
checkStringParseRoundTrip<P>: no boxing conversion to 'netip_test_package.netipTypeCmp'
That is the board’s recorded structural root, unchanged and unmoved: a generic function constrained by
a TEST-declared interface cannot be instantiated with a PRODUCTION type under the white-box REFERENCE
model. C# enforces where P : netipTypeCmp nominally, and go2cs-gen can only make a production
type satisfy a test-declared interface with an ADAPTER class, never the partial declaration a nominal
constraint needs — crypto/x509’s identity split seen from the other side. No bank was expected and
none was taken: the package still produces zero verdicts. What changed is that its build wall is now
a single named question with a coordinator-level answer (reference vs recompile model), rather than a
structural root plus a bounded emission defect sitting in front of it.
The other three roots the previous lane closed (the exported-over-unexported accessibility clamp, the
same-package CS0426, the embedded comparable constraint) stayed closed across this reconvert.
⛔ NEITHER TEMPLATE PACKAGE BANKS — the chan-direction class covers ONE of their three channel rows, not three; html/template closes its two non-cargo residuals and reaches 242 of 243 (2026-08-20, lane claude/template-banks, laptop G)
This lane was briefed to bank both template packages on the ratified chan-direction disclosure
class — text/template at 50+2, html/template at 240+1 behind two bounded fixes. The two fixes
landed and are the lane’s shipped work. The disclosures did not, and must not: re-measured
against the class’s own retirement test, only ONE of the three channel rows is a chan-direction
shape. The other two are a missing implementation, and disclosing them would launder it.
Three corrections to the brief’s premises, before anything else
- Master is
0c83c34c5(the edwards25519 merge). The tls-regression and runtime-debug merges the brief expected are NOT in it — both exist only asorigin/claude/tls-regression-2997cdandorigin/claude/runtime-debug-5259b5. This lane branched from0c83c34c5and every number below is against it. - The roster prose reads FOUR classes, not five, and the fifth name the brief and the stretch
item both use —
alloc-count-semantics— has never appeared indocs/ValidatedTestPackages.md(git log -Sover the file returns nothing). It is nonetheless a live class: three banked packages pin rows with it (io×2,strings×3,context×1) and their proof pages carry it. So the roster under-declares a class its own rows use — a documentation gap, not a ruling, and left for the coordinator rather than self-ruled here. sync/atomic’sTestAutoAligned64fifth-class question is still open and is unrelated to this lane; noted only because the brief’s “five classes” may have been counting it.
The classification, and the one test that decides it
All four residual rows across the two packages were re-measured on this lane’s branch. Three of them
stop in walkRange’s channel arm with the identical signature:
panic: reflect: recv on send-only channel
at go.reflect_package.recv(ΔValue v, Boolean nb) reflect/value.cs:1773
at go.reflect_package.Recv(ΔValue v) reflect/value.cs:1762
at go.text.template_package.walkRange(…) text/template/exec.cs:530
One signature, one call site, and yet the rows do NOT share a class. The discriminator is the class’s own self-retirement property — “landing it retires this class, and the pinned rows begin passing” — applied mechanically:
| Row | What Go does | With direction carried as descriptor cargo | Class |
|---|---|---|---|
text/template TestIssue43065 |
make(chan<- int) ranged; walkRange’s ChanDir() == SendDir guard fires and Go reports range over send-only channel |
the guard fires, the error is produced, Recv is never called — the row passes |
✅ chan-direction |
text/template TestExecute (row range count) |
ranges count(5), a bidirectional chan string |
the guard correctly does not fire, Recv() is called, and recv reaches chanrecv — a bodyless partial the PartialStubGenerator fills with throw new NotImplementedException("chanrecv: external (assembly or cgo) function is not implemented") — the row still fails |
❌ missing implementation |
html/template TestExecute (row range count) |
identical row, identical helper (func count(n int) chan string); html/template’s tests contain no directional channel at all |
same | ❌ missing implementation |
abi.ChanDir already answers BothDir for every channel (internal/abi/type_impl.cs, hand-owned),
so the direction is not what refuses the two bidirectional rows. reflect.recv is the AUTO
conversion and never asks it: it reinterprets the descriptor onto the linker’s chanType record and
reads .Dir out of the memory after the value slot, which behind a synthesized descriptor is zero —
0 & RecvDir == 0 — so every reflect.Value.Recv() panics as send-only whatever the channel is.
That is the defect the near-miss-finish lane wrote a bridge for and measured (TestExecute and
TestComparison both PASSED), then reverted because bridging recv without direction turns
TestIssue43065 into an unbounded hang. Its ranking finding stands and this lane re-proves it from
the other side: direction is the prerequisite, and it is not the remedy for these two rows.
So a chan-direction disclosure for either TestExecute would pin a row that the class’s own remedy
does not make pass — which is precisely the bar host-limit’s ruling set (“an entry must name a
structural property … never an unimplemented-but-fixable defect”) and the charter’s “a disclosure is
only for asserts the CLR provably cannot satisfy”. Measured, both are satisfiable: one of them has
already been made to pass once.
Consequence: neither package banks today, and the arithmetic is exact.
| Package | Verdicts | Matching now | Residual | Would bank at |
|---|---|---|---|---|
text/template |
52 | 50 | TestExecute (recv), TestIssue43065 (chan-direction) |
51 + 1 disclosed once recv is bridged; 52 + 0 once direction lands too |
html/template |
243 | 242 | TestExecute (recv) |
243 + 0 — no disclosure at all |
The TestIssue43065 disclosure is written out below rather than committed, because a manifest belongs
with the banking commit that verifies it end to end (the standing rule). Whoever lands the direction
arc inherits it ready to paste:
{
"name": "TestIssue43065",
"class": "chan-direction",
"signature": "reflect: recv on send-only channel",
"reason": "the test ranges a `make(chan<- int)` and expects Go's `range over send-only channel` error, which text/template's walkRange produces from `val.Type().ChanDir() == reflect.SendDir`; channel DIRECTION is a representational limit -- a Go channel type emits as golib's channel<T> whatever its direction, so the bridge describes only the bidirectional type and abi.ChanDir answers BothDir, the honest answer for the type it can see. The guard therefore never fires and the range proceeds into Recv. Carrying direction as descriptor cargo the way array dims are carried makes the guard fire and retires this row with the class"
}
⚠ Note the signature it would pin is the SAME string the two non-disclosable rows fail with. That is
not a flaw in the pin — the manifest matches on {test name, signature} together and the other two
rows are different tests — but it is worth stating, because it means a future recv bridge that
changes only the message would silently un-pin this row and the sweep would (correctly) go red.
html/template’s two non-cargo residuals — both rooted, both fixed, 240 → 242 of 243
Neither was the arc the board had them under, and the first is the more valuable finding.
TestErrors — the sealing marker’s stub is a design for the wrong half of the rule
The board carried this as “a ~-deref defect in the shared parse package, unrooted”. It is not a
deref defect; the deref is the symptom two frames downstream of a withheld [assembly: GoImplement]
record.
panic: runtime error: invalid memory address or nil pointer dereference
at go.ж`1.op_OnesComplement(…) golib/ж.cs:967
at …parse_package.ErrorContext(ж<Tree> Ꮡt, Node n) text/template/parse/parse.cs:156
at …html.template_package.Error(ΔError& e) html/template/error.cs:75
The chain, in one pass:
html/template’s escaper hands&n.BranchNodetojoinas aparse.Node— a FOREIGN struct crossing into a foreign interface, so the record for the pair must come from somewhere.parsenever casts a*BranchNodeitself (it casts theIf/Range/Withwrappers), so no cast demanded the record; andrecordSamePackageImplementswithheld the speculative one, becausegeneratorCanForwardPointerMethodSetrequires every interface method to resolve DIRECTLY on the type andBranchNodepromotesType()/Position()from its embeddedNodeType/Pos.- With no exported adapter to reference, html/template minted its own — and
Node’stree()andwriteTo()are Go-UNEXPORTED, henceinternalextensions inparse’s assembly, invisible there. TheImplementGenerator’s inaccessible-marker rule then does what it is designed to do:ж<Tree> Node.tree() => default!; // in html/template's assembly void Node.writeTo(ж<Builder> _) { } parse.ErrorContext(n Node)opens withtree := n.tree(), gets null, takes itsif tree == nilarm and substitutes its own receiver — which aterror.go:234’s(*parse.Tree)(nil).ErrorContext(…)is a typed nil.~treethen dereferences it.
The rule the stub rests on is half a rule. “Go never lets a sealing marker be called from outside its package” is true, and irrelevant: the marker is called from INSIDE, on a value the consumer boxed, which is the entire point of sealing an interface. The stub must therefore be a last resort, and it is always avoidable in one place — Go scopes an implementation of such an interface to the declaring package, so the declaring assembly can always forward the marker natively and export the adapter.
Fix: the speculative POINTER record’s depth gate carves out for a sealed interface. When the
interface carries an unexported method, the pointer record falls back to the VALUE form’s depth-2
bound instead of being withheld — still bounded (deeper than one embed hop is still refused), and
the shape the strict gate was written for (StructPointerPromotionWithInterface’s MyCustomError,
whose promoted member resolves through the wrong embedded POINTER hop) is an all-exported interface
that never reaches the arm. The gate’s own justification — “withholding a speculative record is
always safe, because the consumer keeps the local adapter it had before” — is exactly what is false
for a sealed interface, and that sentence is now corrected in the reference doc.
Corpus footprint: one line, [assembly: GoImplement<BranchNode, Node>(Pointer = true)] in
text/template/parse/package_info.cs. parse’s generated adapter forwards tree()/writeTo()
natively and is public sealed; html/template’s cast site now emits new parse.BranchNodeжNode(…)
through the existing foreign-adapter-exists arm.
What is still stubbed is what should be — and the corpus now holds none of it. A pair the
declaring package genuinely cannot realize (promotion deeper than one hop, a generic, an unexported
target) would still mint a consumer-local adapter with a silently stubbed marker. Censused on the
FRESHLY GENERATED output of the whole-stdlib reconvert below: 1,307 ImplementGenerator adapters,
ZERO with a => default! or empty-body member. Before the fix the same census found exactly one,
and it was this one. ⚠ The census must be taken on freshly generated files only — the seeded root
carries the previous build’s Generated/ folders (they are gitignored, so git clean -fd leaves
them), and a stale crippled adapter sits there looking exactly like a live finding; discriminate by
mtime against the reconvert’s sentinel.
TestRedundantFuncs — the boxing side of a fix whose reading side landed in June
panic: interface conversion: interface {} is <>f__AnonymousDelegate0,
not go.Funcꓸꓸꓸ<object, @string>
html/template’s funcMap is a map[string]any of func(...any) string escapers assigned as method
groups; TestRedundantFuncs reads them back with funcMap[n].(func(...any) string). The ASSERT side
was fixed when convTypeAssertExpr learned to render an anonymous signature through
iifeDelegateType. The BOXING side was not: C# gives a method group at an untyped destination its
NATURAL function type, which for a non-variadic signature is Func<…>/Action<…> — go2cs’s own
lowering, so those already agree — but a params signature has no BCL delegate, so C# synthesizes one
and the box carries <>f__AnonymousDelegate0. The assert was right, the box was wrong, both emitted
by the same converter.
Fix: a variadic func entering EMPTY-INTERFACE space is cast to its Go func type at the boundary —
((Funcꓸꓸꓸ<any, @string>)(attrEscaper)). This is the same carry-your-Go-type rule the pointer box and
the untyped-constant box already apply at that same finite slot set, so it lives with them in
typedNilInterfaceBoxing.go, and both sides now name the type through one renderer
(getCSharpTypeName → iifeDelegateType) and cannot drift. A NON-empty interface target needs
nothing: a bare func type has no methods and satisfies no other Go interface.
The board priced this as “the same missing conversion as Root C, one boundary over” and said to reuse
variadicFuncLitCallee/iifeDelegateType. Half right — iifeDelegateType is reused;
variadicFuncLitCallee is not, because this is not a callee shape and the boundary is the
empty-interface slot set, not the call site.
The alloc-count-semantics gap, and the stretch measurement it was asked for
The brief’s stretch item asked whether the “now-restored” alloc-count-semantics class definition
covers math/big’s and net/http/internal’s near-budget alloc rows, which the 2026-08-10
coordinator ratification left characterized and undisclosed.
First, the class is not in the roster and never has been. docs/ValidatedTestPackages.md says
“Four classes exist” and lists alloc-profile, codegen-liveness, host-limit, chan-direction.
git log -S"alloc-count-semantics" -- docs/ValidatedTestPackages.md returns nothing — the name
has never been in that file. It is nonetheless live: io pins 2 rows with it, strings 3, context
1, and all three proof pages carry it. Proposed bullet, for the coordinator to take or reword:
alloc-count-semantics— a test asserts an allocation COUNT that the managed measurement cannot express in Go’s unit.testing.AllocsPerRuncounts mallocs in Go; the converted shim reports go2cs-runtime object allocations where its counter charges the path and allocated BYTES where it does not, so a nonzero assert against a byte figure can never agree whatever the allocation behavior. Distinct fromalloc-profile, which is a real count the CLR cannot reach.
Second, the measurement — and it says the class does NOT cover either row. Both tests go through
testing.AllocsPerRun, but the shim has not been byte-derived since r58a: it is a hybrid, and which
branch produced a figure decides the question.
Shim branch (r58a hybrid, core/testing/testing.cs) |
What the figure IS | Comparable to Go’s Mallocs? |
|---|---|---|
| zero bytes | exactly zero, in both units | yes |
| nonzero bytes, counter charged ≥1 site | a go2cs-runtime object COUNT, floored at 1, stated as a lower bound | yes |
| nonzero bytes, counter charged nothing | allocated BYTES per run | no — this is the alloc-count-semantics case |
Measured on this branch, through the pipeline:
| Row | Reported | Shim note | Reading |
|---|---|---|---|
net/http/internal TestChunkReaderAllocs |
mallocs = 2; want 1 |
“counted 200 go2cs-runtime object allocations (64,000 bytes) over 100 run(s) — the figure reported above is an allocation COUNT per run … a LOWER BOUND on the true object count” | a COUNT, in Go’s unit. The class does not apply |
math/big TestNewIntAllocs |
wanted 0 allocations, got 1.000000, all seven inputs |
“counted 100 go2cs-runtime object allocations (81,600 bytes) over 100 run(s) — … an allocation COUNT per run … a LOWER BOUND” | a COUNT, in Go’s unit. The class does not apply |
So the class does not cover them, and the 2026-08-10 ratification was right for a reason it did not
have to state: these rows are not a unit mismatch at all. The figure is in Go’s own unit, it is a
LOWER bound (so the true count can only be ≥ the reported one — the assert can never be satisfied by
better measurement, only by allocating less), and one extra golib object per run is an optimization
target. Disclosing them under alloc-count-semantics would be a category error on top of the
laundering the original ruling refused: the class names something the measurement cannot express, and
here it expresses it exactly.
Recommended (coordinator’s call): keep both undisclosed; add the class bullet above to the roster so its own banked rows stop citing a class the document does not define.
What this lane deliberately did NOT do
- Did not bridge
reflect.Value.Recv. It is the single fix that bankshtml/templateoutright, and the brief ring-fenced the bridge’s Value machinery (laptop R’s crypto/tls verification sweep). It is also the fix that, landed alone, is measured HARMFUL:TestIssue43065hangs andtext/templateloses 51 verdicts to the package deadline against the 1 it buys. - Did not open a fifth disclosure class. No new-class need arose: the two
TestExecuterows are not a representational limit of any kind, they arechanrecvthrowingNotImplementedException. - Did not write the
TestIssue43065manifest. Its package does not bank, and a manifest belongs with the banking commit that verifies it end to end. The entry is above, ready to paste. - Did not touch the roster’s class prose. The
alloc-count-semanticsomission is real and the proposed bullet is above, but class definitions are the coordinator’s surface.
Shipped
| Change | File | Footprint | Guard (proven failing-first) |
|---|---|---|---|
| The speculative POINTER record carves out for a SEALED interface: an unexported interface method drops the depth gate to the VALUE form’s depth-2 bound | samePackageImplements.go (+ interfaceHasUnexportedMethod) |
see reconvert census below | CrossPkgLib/CrossPkgUser — Emitter gains a value-returning sealed member nodeTag() and DescribeEmitter(e Emitter), the ErrorContext shape. Neutered, *Branch prints branch/ where Go prints branch/brn; *Leaf is the control and reads leaf/lf either way |
| A VARIADIC func entering EMPTY-INTERFACE space is cast to its Go func type | typedNilInterfaceBoxing.go (the boundary’s two entry points + a new variadicFuncBoxCastType), wired into convCompositeLit’s slice-element and struct-field any arms through the existing castArgToType plumbing |
see reconvert census below | VariadicFuncTypeAssert, extended — the guard whose own comment recorded this as “a separate latent defect, out of this guard’s scope”. Four new shapes (literal direct to any, method group as a map[string]any element, through a plain assignment, as an []any{…} element) plus a non-variadic control. Neutered, all four print no match; the control keeps passing |
Both rows also re-confirm their packages’ standing counts on this branch, unmoved by this lane’s two
converter changes: math/big 224 of 226 (TestNewIntAllocs + TestMulUnbalanced, exactly the
r58b pair) and net/http/internal 9 of 10.
A side finding, measured not chased: encoding/xml’s README still says “not yet validated”
The seeded reconvert’s mover list carried one file this lane does not own and nobody has recorded:
src/core/encoding/xml/README.md. The committed badge line reads
Tests-not_yet_validated-orange; a reconvert emits Tests-386%2F386_validated-brightgreen linking
the package’s proof page. The package BANKED at 386/386 in f83702d6d and its proof page landed with
it, but the README’s badge is composed only at CONVERSION time, so the bank never refreshed it — the
published GitHub and NuGet page for roster row 155 advertises an unvalidated package.
It is exactly one package, not a class. Censused across all 155 packages that have a proof page in
docs/validation/current/ and a README: encoding/xml is the only one carrying
Tests-not_yet_validated. Left for its owner (a one-line README correction, or the next regen), and
recorded here so it is not rediscovered as drift.
Gates
Converter go test ./... ok, 319 s, exit 0 — including projitemsIntegrity_test (no new .go
file, nothing to register) and TestStdLibMetadataInSync. ⚠ Two readings from an earlier,
CONCURRENT run were false and are worth carrying: TestStdLibMetadataInSync reported
“stdlib-metadata.txt is STALE — run go generate .” while src/core was dirty from this lane’s own
-tests pipelines; against the restored tree it passes, and running go generate on that reading
would have banked a metadata file generated from a -tests corpus. Re-run before believing it. The
other failure was real and is fixed: TestUntypedInterfaceFuncLitResultType pins the emitted text of
a func literal in an any slot, and its pred row is the VARIADIC one, so the new boxing cast wraps
it. The guarded property is intact — the literal still states its Go result type INSIDE the cast — and
the expectation now pins both in one string, which is strictly stronger than what it pinned before.
Full check-no-regression.ps1 over all 628 behavioral packages: 7 changed files, 0 NOT
MEASURED, 0 advisory warnings; preflight solution integrity 630/630, path casing
4,527/4,527. Every one classified, and two of the seven are the point:
| File | Why |
|---|---|
CrossPkgLib/{lib.go,lib.cs,package_info.cs}, CrossPkgUser/{main.go,main.cs,package_info.cs}, VariadicFuncTypeAssert/{main.go,main.cs} |
this lane’s guard sources and their intended new emissions |
ReflectBridgeClosure/main.cs |
a variadic func in an []any{…} element read by reflect.TypeOf — now carries (Actionꓸꓸꓸ<@string, nint>) |
ReflectVariadicCall/main.cs |
a map[string]any of three variadic literals — all three now cast |
Seeded whole-stdlib reconvert (fresh root, seeded with core + version.props + docs/validation,
single run): 63 marked / 0 clobbered (line-anchored git grep, path-precise), 1,664 emitted /
1,651 identical / 13 differing / 0 new, compared CR-INSENSITIVELY so no phantom is counted as a
mover. The 13 are 4 this lane’s + 8 documented pre-existing carries + 1 unowned:
- this lane:
text/template/parse/package_info.cs(+1 record),html/template/package_info.cs(−1: the local record it no longer needs),html/template/escape.cs(19/19:funcMap),text/template/funcs.cs(12/12:builtins()— the identical shape, so text/template’s own builtin func values were carrying C#’s synthesized delegate too); - pre-existing:
go/internal/gcimporter/gcimporter.cs(row-harvest-2’s documented carry) and the sevenruntimeunsafe.Pointerbox-compare sites row-harvest-3 flagged for the next leveling regen (alg.cs,map.cs,map_fast32.cs,map_fast64.cs,mbarrier.cs,traceback.cs,pprof/map.cs— eachx.Value == y.Value→x == y); - unowned:
encoding/xml/README.md, the stale validation badge above.
All src/core dirt from the four -tests pipeline runs (text/template ×2, html/template ×4,
math/big, net/http/internal) classified and RESTORED per the standing rule, nothing unclassified:
three CRLF phantoms ({html/template,text/template,math/big}/doc.cs, empty numstat), two
initᴛᴛtests hooks ({html/template,math/big}/package_init.cs, +7 real lines each), math/big/prime.cs
(phantom), and the four real movers above, which belong to a leveling regen rather than to this
converter-fix commit. No package validates, so no roster row, no proof page, no committed test sources.
Full behavioral suite PASS at 601 projects, 2,374.6 s — Transpile 601/601, Compile 601/601,
Target 601/601 byte-identical, Output 575 compared / 0 failed (26 skipped, no package main), 0
timeouts / 0 NOT MEASURED.
Pipeline verdicts on this branch, each re-measured after the fixes: html/template 242 of 243
(was 240), text/template 50 of 52 (unmoved — its residual is the recv defect plus the
chan-direction row), math/big 224 of 226, net/http/internal 9 of 10.
Expected counts for the coordinator’s merge-result sweep (the banking-merge rule): this lane banks no row, so the roster is unchanged at 155 / 215 and no sweep row’s count moves — every banked package is expected at exactly its current number.
The canary question answers itself from the reconvert, which is stronger than a canary list: the
sealed-interface carve-out moved exactly two package_info.cs files in the whole corpus
(text/template/parse gains the record, html/template drops the local one it no longer needs). No
other package in the standard library has a pair the carve-out newly admits, so no banked package’s
records changed and none needs re-validating on that account. The variadic-boxing cast reaches two
more files (html/template/escape.cs, text/template/funcs.cs), neither in a banked package. If a
canary is wanted anyway, go/types (557) is the broadest consumer of a sealed foreign interface and
its emission is byte-identical under the reconvert.
Gates (continued): the reconverted corpus BUILDS
The reconverted temp root (seeded with src/gen so the analyzer resolves) builds its own generated
go2cs-stdlib.slnx: Build succeeded, 0 errors, 157 warnings, 416 s. That is the gate that
matters for the record change — it is what proves the newly recorded pair generates an adapter that
compiles, and that no other package’s adapters moved out from under it.
The queue, re-ranked by what this lane measured
reflect.Value.Recv/chanrecv— promoted, and it is now the ONLY thing betweenhtml/templateand a 243/243 bank with zero disclosures. It is written and measured (near-miss-finish) and must land WITH direction, not before it.- Channel direction as descriptor cargo — unchanged as the prerequisite, now with its exact
value: it retires ONE row (
text/template’sTestIssue43065, disclosure text above) and unblocks #1. Landing both takestext/templateto 52/52 andhtml/templateto 243/243, no disclosures in either. encoding/gob(106) — 103 of 106, still the closest unbanked package that neither of the above touches.sync/atomic’s zero-size-field layout, 5.%#xof auintptr— unchanged.
The sealed-interface stub’s residual leaves the queue: censused after the fix, the corpus has no adapter that silently stubs a member (1,307 generated, zero stubbed). The shape stays worth suspecting, not tracking.
✅ CHANNEL DIRECTION IS DESCRIPTOR CARGO and reflect.Value.Recv/Send land WITH it — the chan-direction class RETIRES, and both template packages bank as rows 156-157 (2026-08-20, lane claude/cargo-recv, laptop G)
The arc the last three entries kept pointing at. internal/reflectlite goes 27+3 → 30+0,
text/template 50 → 52 of 52 with NO disclosure, html/template 242 → 243 of 243. The
roster reaches 157 / 215 (73.0%), 18,414 matching verdicts, 79 disclosed.
Two corrections to the brief’s premises
claude/template-bankswas NOT in master — it existed only as a local branch, andhtml/templatecannot reach 243 without its two fixes. It is MERGED into this lane rather than waited on, which also makes every measurement below bind the UNION of both changes, per the banking-merge doctrine the crypto/tls regression established. Both conflicts were docs and both resolved by keeping both sides.text/template’sTestIssue43065needs no disclosure at all. The template-banks entry wrote its manifest out ready to paste; it is not pasted, because the row PASSES. That entry’s discrimination was exactly right — one of the three channel rows was chan-direction and two were a missingchanrecv— and the consequence of landing both halves is that all three pass.
The design question the brief gated on: does the cargo fit the GoArrayDims precedent?
Yes — but only because a NIL channel can carry a direction, and that is the whole design.
All three internal/reflectlite rows read the direction at a type position, where there is no
live channel to measure: new(<-chan int) and new(chan<- string) describe the zero value behind a
pointer, and struct{ x chan<- string } describes a field nothing ever assigned. A direction that
rode only on a made channel would have retired none of them. Putting it on the channel<T> STRUCT
rather than on its heap ChanCore is what reaches them: direction belongs to the Go TYPE, so two
values of different directions may share one core, and the zero value of a directional type has no
core at all yet still has a direction (channel<T>.SendOnly / .RecvOnly).
With that, the positions map one-for-one onto the array dims’, and the plumbing is the same plumbing:
| Position | Array length | Channel direction |
|---|---|---|
| the constructed value | new(32) |
new channel<nint>(0, GoChanDir.Send) |
| a struct FIELD’s zero | = new(4) initializer |
= channel<@string>.SendOnly initializer |
| behind a POINTER | PointeeArrayDims |
PointeeChanDir |
| the FABRICATED zero | ZeroValueOf(st, dims) |
ZeroValueOf(st, dims, chanDir) |
| a func PARAMETER | [GoArrayDims(32)] |
not carried — no measured consumer |
abi.Type.chanDir joins BOTH interning keys (shared with reflect’s canonType), Type.String()
renders the arrow from the same cargo, and a POINTER hands its pointee’s direction down UNSHIFTED
through Elem(). Unstamped answers BothDir — what the accessor already reported, and the honest
answer for a type nothing narrowed — so only directional sites move.
The fourth position was found by MEASUREMENT, not by reasoning, and it is the useful lesson
The first pipeline run came back 29 + 1 disclosed, not 30. TestAssignableTo and TestSetValue
flipped; TestTypes did not. The guard had already proven the struct-field route working through
reflect, so the shape looked covered — and the discriminator is that reflectlite’s helper does not
ask the TYPE at all:
func TypeString(t Type) string { return fmt.Sprintf("%T", ToInterface(Zero(t))) }
It fabricates a zero FROM the descriptor, boxes it, and re-describes it. The direction survived
Type.String() and died at the box, because GoReflect.ZeroValueOf handed back
default(channel<T>). So the fabricated zero carries the cargo now, for exactly the reason
reflect.Zero of an array type is already sized from the descriptor’s dims: a value the bridge
invents must describe itself the way the descriptor does, or the cargo is lost the first time
anything boxes it. The guard was extended with that shape rather than left as the thing that missed
it.
Three latent defects the cargo exposed, all fixed here
None is this arc’s own work; each was invisible because every ChanDir() answered BothDir.
| Defect | Why it was silent | Consequence once directions are real |
|---|---|---|
internal/reflectlite’s hand-owned haveIdenticalUnderlyingType chan arm dropped Go’s FIRST rule (“x is a bidirectional channel value, T is a channel type, and V and T have identical element types”) |
with one direction, both of Go’s two rules agree for every pair | var r <-chan int = make(chan int) reports unassignable |
reflect.Value.Len() had no IChannel arm at all |
0 is a real length — the same silence the named-string arm had | every channel Value reported len 0 while Cap() answered correctly one method away |
Value.send cannot use Go’s assignTo |
nothing reached send — recv’s guard refused first |
assignTo’s managed form returns a Value carrying only the never-populated raw ptr slot and drops the boxed companion, so the channel received a bare null |
The third is worth carrying: send now marshals through marshalIntoSlot, the rule Value.Call
already used for a call argument, so a channel send and a call argument box a typed nil identically.
That is the third boundary to join the one-nil-encoding rule.
The hang, reproduced and bounded
near-miss-finish measured that bridging recv ALONE turns TestIssue43065 into an unbounded hang
costing 51 verdicts. That coupling is this lane’s acceptance test, and the guard carries it as a
timeout-bounded reproduction: walkRangeShape is text/template’s channel arm in miniature, run in
a goroutine against time.After, so a regression prints a named line instead of wedging the suite.
Proven failing-first by neutering each half SEPARATELY:
- cargo neutered → every
dir=readschan, all four assignability answers flip totrue, and the send-only range printsHUNG -- the direction guard did not fire before Recv; - recv guard neutered to the auto form’s zero →
range count(5)panicsreflect: recv on send-only channel.
Boundaries, stated rather than discovered later
- A NARROWING conversion is not carried.
var s chan<- int = chis a plain struct copy with no construction to hook; stamping it means an explicit call at every assignment, argument and return of a directional channel in the corpus — 89 such positions, measured — for a datum no consumer reads.reflect.TypeOf(s)still reportschan intthere. - A DEFINED channel type is not stamped (
type closeWaiter chan struct{}), for the reason a defined ARRAY type carries no dims: its managed form is a go2cs-gen wrapper, notchannel<T>. An ALIAS for a channel IS its target and is stamped. The generated wrapper still forwards the two newIChanneloperations natively, so a named channel reflects and receives correctly — only its direction reads bidirectional. - A func PARAMETER is not stamped, and a type PARAMETER routes through
ISupportMake, which has no direction-taking form.
The corpus footprint, and the four movers that had to be PROVEN pre-existing
Three-target seeded emission (windows,linux,darwin; fresh root, single run, per the r41 rule):
63 marked / 0 clobbered on every target, path-precise and line-anchored via git grep. The L3
merge wrote 90 artifacts; 33 differ CR-insensitively and every one is classified —
| Class | Count | What |
|---|---|---|
| this arc | 16 | a directional channel FIELD gaining its initializer, in crypto/tls, net/http ×3, net/{windows,linux,darwin} ×6, os/exec, os/signal, runtime/{windows,linux,darwin}/trace ×3, time — 32 field declarations, zero off-shape lines |
| template-banks | 4 | its two fixes’ own emission (parse/package_info.cs +1 record, html/template/package_info.cs −1, escape.cs, funcs.cs) |
| pre-existing carries | 13 | gcimporter; the runtime box-compare family, re-measured at 9 rather than the recorded 7 (alg, map, map_fast32, map_fast64, mbarrier, traceback, pprof/map, plus linux/mem_linux and darwin/mem_darwin — the two per-GOOS members no windows-only reconvert can see); two per-GOOS package_info.cs losing a readmemstats_m_bySize lift record; encoding/xml’s stale README badge |
Zero unclassified. The four that were not obviously pre-existing were proven so rather than
assumed: master’s converter was rebuilt from git archive and re-emitted runtime for linux, and it
reproduces both shapes exactly — while linux/trace.cs is byte-identical under it, which is what
proves that file’s change IS this arc’s. ⚠ Worth keeping as a technique: a single-target reconvert
on the host GOOS cannot see a per-GOOS carry at all, so the runtime box-compare family has been
under-counted at 7 since it was recorded.
Only this arc’s 16 are banked, plus template-banks’ 4 — the latter because they are no longer
optional: html/template validates through them, and restoring parse/package_info.cs is what
made TestErrors fail again on the first html/template run, a measured self-inflicted repro of
exactly the defect that lane rooted. The 13 pre-existing carries are RESTORED and belong to a
leveling regen.
src/core dirt from the five -tests runs classified and restored per the standing rule:
{html,text}/template/doc.cs and reflectlite’s {swapper,type,value}.cs (the global::go.* root
escape), plus html/template/package_init.cs’s initᴛᴛtests hook (+7 real lines).
The stale-validation-badge class is a PIPELINE gap, not four accidents
template-banks censused encoding/xml’s README as the only package advertising
Tests-not_yet_validated while its row was banked, and left it as “a one-line correction, or the
next regen”. It is neither an accident nor a one-off: every bank leaves a stale badge, and the
mechanism is exactly locatable.
The badge is composed at CONVERSION time from docs/validation/current/<dot-id>.md and
src/version.props (readmeBadgeLine), but writeReadmeFile is gated to options.convertStdLib
(projectFileWriter.go) — deliberately, so a behavioral-test or single-package conversion does not
litter its directory. A -tests run writes the proof page and does not write the README; a
single-package conversion does not write the README either. So the badge only ever catches up on the
next whole-stdlib reconvert, which is not part of banking.
This lane hit it three more times at once (internal/reflectlite frozen at 27/30,
{html,text}/template at not_yet_validated) and closed all four by the authoritative route —
a seeded single-target -stdlib reconvert taken AFTER the proof pages updated, overlaying only the
README movers. That is a repeat of the same manual step every future bank will owe.
The durable fix is one line of gating, deliberately NOT taken here because it wants its own
emission gate: let a -tests conversion that WROTE a proof page also refresh that package’s README,
or equivalently drop the convertStdLib gate to “stdlib package” rather than “stdlib run”. Whoever
takes it should keep the litter rule the gate exists for — the test is the PACKAGE’s provenance, not
the run’s mode.
Two measurement hazards this lane paid for
Both are variants of traps CLAUDE.md already names, and both cost a wasted cycle.
A poll that waits for a process to DISAPPEAR fires instantly if it has not yet APPEARED.
until ! tasklist | grep -qi go2cs.exe; do sleep 20; done looks like “wait for the converter to
finish” and is actually “wait for the converter to not be running” — which is TRUE for the seconds
between launching a command and its converter starting (here, a 1.6 GB seeding copy). The waiter
exited immediately and its tail then failed on a log that did not exist yet, which reads exactly
like a crashed run. This is the same class as the recorded exit $true inversion: the poll must be
written on a POSITIVE condition the target actually produces — wait for the log to contain the
completion marker, not for a process table to be empty.
Two conversions raced into ONE temp root, and it was self-inflicted. The r41 rule (“never
convert twice into the same root”) was honored in intent and broken in fact: a nohup … & launch
was replaced with a harness-tracked background launch without killing the first, and both wrote the
same -go2cspath. Caught by tasklist showing two go2cs.exe, not by any output — the r41
corruption is silent until a build fails. The rule needs a companion habit: census the process
table before launching a conversion, because “I meant to replace that run” is not the same as
having replaced it.
Gates
Converter go test ./... ok, 174.1 s, exit 0 (projitemsIntegrity_test included — the one new
.go file is registered; TestStdLibMetadataInSync passes, and no package_info.cs record moved
for this arc, so go generate is not owed).
Full check-no-regression.ps1: byte-identical generated .cs and .csproj across all 629
behavioral packages, 0 NOT MEASURED, 2 advisory converter warnings, 673 s; preflight solution
integrity 631/631, path casing 4,534/4,534. Re-run from scratch after the one late converter
edit (a local-variable rename) so the gate binds the source at HEAD rather than the source it was
launched against.
Full behavioral suite PASS at 602 projects, 1,671.6 s — Transpile 602/602, Compile 602/602,
Target 602/602 byte-identical, Output 576 compared / 0 failed (26 skipped, no package main),
0 timeouts / 0 NOT MEASURED.
Solution builds, all --no-incremental: go2cs.slnx Debug 0 errors, 715 s (the gate no other
harness covers, and this arc changes golib’s IChannel/channel<T> surface);
go2cs-stdlib.slnx -p:GoTargetOS=windows 0 errors, 381 s; -p:GoTargetOS=linux 0 errors,
364 s. The linux build is not optional here — six of this arc’s sixteen corpus movers live in
net/linux/ and runtime/linux/, which the default windows build never compiles. bin/obj purged
between the target switch (916 of 935 directories; the 19 that resisted are long-path Generated
folders in vendor/coverage packages, none platform-varying, and --no-incremental covers them).
Row sweeps, from the committed roster: html/template 243, text/template 52,
text/template/parse 52 — 3 pass / 0 fail, 284 s. internal/reflectlite PASS at its NEW count,
30 — which is the proof the class retirement is real rather than locally observed.
Canaries — the five largest banked reflect consumers BY VERDICT COUNT, recomputed from the roster at
gate time per the doctrine (go/internal/gcimporter 583, go/types 557, encoding/json 491,
crypto/tls 400, encoding/xml 386; the two rows this lane banks are reflect consumers too but at
243 and 52 they do not enter the set):
| Canary | Expected | Result |
|---|---|---|
go/internal/gcimporter |
583 | PASS (492 s) |
go/types |
557 | PASS (256 s) |
encoding/json |
491 | PASS (98 s) |
encoding/xml |
386 | PASS (103 s) |
crypto/tls |
400 | FAIL — rooted to the GO baseline, see below |
Guards proven failing-first by neutering each half separately (both transcripts above).
src/core dirt from the five -tests runs and the sweeps classified and RESTORED per the standing
rule, with ONE exception banked deliberately: text/template/parse/parse_test.cs (2/2), which is
template-banks’ variadic-boxing fix reaching a BANKED row’s committed test sources. It is banked
rather than restored because the package re-validates at its full 52 with the new emission, so
restoring it would leave every future sweep reporting the same drift forever.
⚠ The crypto/tls canary reports FAIL, and the cause is the GO baseline on this host
Recorded in full because a red flagship row is exactly what the banking-merge doctrine exists to catch, and this one is a FALSE red that will recur.
The sweep’s verdict maps say it precisely. The C# side ran its banked 402 rows and every verdict is
identical to the committed proof page — zero differences. Exactly one row diverges between the two
languages, TestCertCache (Go pass, C# fail), and that row is itself a banked codegen-liveness
disclosure. The count still moved:
| Row | Banked model | This run |
|---|---|---|
TestBogoSuite |
Go pass, C# fail → a host-limit DIVERGENCE, counted as disclosed |
Go FAIL, C# fail → the row AGREES, so it is not a divergence at all |
TestCertCache |
Go pass, C# fail → disclosed | unchanged |
Live disclosed = 1 where the roster banks 2, and the sweep’s own disclosed count moved check fires.
The C# side did not move; the GO side did. On this host go test actually executed the BoGo matrix —
3,243 TestBogoSuite/... subtest rows appear on the Go side against the banked model’s absence — and
failed it, which is unsurprising given what that row’s own disclosure documents about the runner’s
10-minute child deadline and 5,481-case matrix.
Nothing in a go2cs branch can influence this. The Go baseline is go test -json over GOROOT’s own
crypto/tls sources with the Go toolchain, and on the Go side the BoGo shim is Go’s own test binary,
not the converted host. The row is environmental — network reachability of the boringssl module, and
how far the runner gets before its own deadline — and it would reproduce at master on this machine.
The finding worth carrying is about the PIN, not about tls. A disclosure pins a divergence, so it
is only stable while the GO side is stable. TestBogoSuite’s Go side is itself load- and
network-dependent, which makes this row brittle in BOTH directions: the sweep goes red when Go starts
failing (here) and would go red again when Go goes back to passing on a quieter host. That is a
property of disclosing a row whose baseline is not deterministic, and it is worth a coordinator ruling
rather than a lane’s patch — the honest options are to annotate the row the way host-conditional
annotates verdict COUNTS, or to accept agreement-on-failure as satisfying a disclosure.
This lane changed nothing here and banked nothing here: the regenerated crypto/tls test sources were
RESTORED, including the pre-existing staleness they exposed (the committed sources still emit
tls_package where the converter now emits tls_internal_test_package, the form 132 other
packages already use — a stale bank, unrelated to this arc, and not something to level inside a red
canary).
The queue, after this
encoding/gob(106) — 103 of 106, now the closest unbanked package and the queue top. Its recorded need is the map key/elem dims cargo, which shares this arc’s plumbing:synthTypealready takes cargo,descriptorDimsKeyalready renders it, andKey()/Elem()are the two accessors that would consume it. Whoever takes it should readchanDirectionCargo.gofirst — the converter-side shape is a third of a page, and the four-position table above is the map for it.sync/atomic’s zero-size-field layout, 3.%#xof auintptr, 4. the two variadic-argument walls (a SPREAD argument to a deferred variadic call; an empty variadic call passing an empty slice where Go passes nil) — all unchanged. The variadic-func-in-an-any-slot item drops off, landed with template-banks.
The chan-direction class leaves the disclosure roster, taking the count from five back to four.
RULING – TestBogoSuite’s pin gains a HOST-CONDITIONAL annotation; agreement-on-failure never satisfies an unannotated disclosure (coordinator, 2026-08-20)
The cargo-recv gate put the choice cleanly (above): a disclosure pins a divergence, and this row’s
divergence has a nondeterministic GO side – network reachability of the boringssl module and the
runner’s own 10-minute child deadline decide whether Go’s TestBogoSuite passes, expands into its
3,243-row matrix, or fails, per host and per run. The C# side was proven unmoved (zero verdict
differences against the committed proof page), so the red is entirely the baseline’s.
Of the two honest options, the narrow one is ruled IN and the broad one is ruled OUT:
-
REFUSED – accepting agreement-on-failure as satisfying a disclosure in general. That would make every pin self-satisfying the moment its Go baseline breaks for any environmental reason, which is exactly the staleness the sweep’s
disclosed count movedcheck exists to catch. A disclosure asserts Go passes, C# provably cannot; if the sweep stops noticing when the first half fails, the assertion quietly decays into C# fails. -
ADOPTED – the per-row annotation, keyed the way host-conditional verdict-COUNT annotations already are. Mechanics, binding on the implementer:
- The signature-pinned manifest entry for a NAMED row may carry a host-conditional marker plus
one sentence naming the environmental dependency. Only rows with coordinator-accepted rooting
evidence receive it;
TestBogoSuite(the rooting above) is the first and only member today. - An annotated row satisfies the sweep in EXACTLY two shapes: Go pass / C# fail (the pinned
divergence) or Go fail / C# fail (agreement, on a host where the Go premise fails). EITHER
shape accounts the row as DISCLOSED – never as matching – so the roster arithmetic is
host-stable:
crypto/tlsreads 400 + 2 on every machine. - Go-side subtest children of an annotated row (the BoGo expansion) are excluded from one-sided accounting; they are the annotated row’s own baseline flapping, not new rows.
- Any movement on the C# side still fires, in both directions. The pin stays strict where it can be strict; the tolerance is confined to the half that was never deterministic.
- The proof page carries the same note the roster’s
internal/zstdrow models: name the dependency, name both accepted shapes.
- The signature-pinned manifest entry for a NAMED row may carry a host-conditional marker plus
one sentence naming the environmental dependency. Only rows with coordinator-accepted rooting
evidence receive it;
Queued as a bounded lane item (compare machinery + manifest schema + the crypto/tls entry + proof
page + a run proving both shapes account as disclosed). Until it lands, a crypto/tls sweep on a
host whose Go BoGo run fails is a KNOWN false red with this section as its rooting – re-read the
verdict maps before believing any other explanation.
NOTE – the hand-leveled .tests.csproj files level themselves at their next sweep; BANK that drift, don’t chase it (coordinator, 2026-08-20)
The preflight-trio metadata wave hand-leveled every banked .tests.csproj because only a -tests
run regenerates them – and the hand-leveling approximated the emission short by five properties
(the conditioned TargetFramework fallback, Authors, the Company chain, Copyright,
RepositoryType, ApplicationIcon). The union gate’s banking sweeps proved it: text/template
and html/template regenerated theirs to the full ratified shape, the sweep reported CONTENT
drift, and both banked at the gate after the sweep itself had built and passed with them.
Standing classification: a .tests.csproj drift of exactly this shape at any future sweep of a
banked package is the SAME catch-up – bank the regenerated file (the converter’s emission is the
authoritative shape), one per package as sweeps touch them. A single scripted leveling pass over
the remaining hand-leveled files is queued as a bounded item for whoever wants the drift gone in
one commit; until then this note is the classification, not a mystery.
✅ SHIPPED — TestBogoSuite’s pin is host-conditional; the second accepted shape is DISCLOSED, and the guard is proven on fixtures because the host cannot be made to produce it (2026-08-20, lane claude/bogo-host-conditional)
Implements the ruling immediately above, nothing broader. The annotation is one optional manifest field, the oracle gains one accepted status pair, and the proof page learns to say so.
The schema: the marker IS the sentence
testDisclosure gains hostConditional — a string whose non-empty value is simultaneously the
marker and the one sentence naming the environmental dependency. Two fields would have allowed a
row marked without saying what it depends on, which is the one thing the ruling requires of an
annotated row; one field makes that unrepresentable. A blank-but-present value is a load-time
error, on the same footing loadTestDisclosures already puts an empty signature: marking a row
widens the oracle by a whole status pair, and a manifest that widens it while naming nothing is
exactly the “a broken disclosure must not widen the oracle” case. schemaVersion stays at 1 — the
field is optional and no reader validates the version.
crypto/tls’s TestBogoSuite is the first and only member.
The oracle: one more accepted pair, and the flood exclusion it drags with it
Two edits in matchTerminalStatuses, and the second is the one nobody would predict from the
ruling’s text.
| Site | Before | After |
|---|---|---|
| the classification loop | Go fail / C# fail never reaches the divergence arm at all — the statuses are EQUAL, so the row falls through to the bottom and counts as an ordinary agreed failure | intercepted BEFORE the equal-status arm: disclosed when the signature still matches, a strict mismatch under the first shape’s own wording when it does not |
disclosureRoots |
admits a root only on Go pass / C# fail | also admits an ANNOTATED root on Go fail / C# fail — the C# signature pin is hoisted out and applies to both |
The second is load-bearing and easy to miss: shape (b) is precisely the shape in which the Go side
DID reach its case fan-out, so an annotated root that does not root leaves TestBogoSuite’s 3,243
Go-only case rows to land as one-sided mismatches — a comparison drowned in rows that say nothing
about the converted code, on a run where the C# side never moved. The existing withdrawal rule
already does the right thing once the root is admitted; nothing else in it changed, and a C#-side row
that EXISTS under an annotated root still compares strictly.
What still fires, both directions. The C# side is pinned by signature in BOTH shapes, so a
failure that moved is a named mismatch, not a quiet reclassification. A converted side that starts
PASSING leaves the disclosed set, the count moves, and the sweep’s disclosed count moved check
fires — which is the self-retirement path this row’s own disclosure text already promises. An
UNANNOTATED row gains no second shape at all.
The proof page: the totals line would have broken at the evidence layer
proofDisclosedNames derives its set from verdict disagreement — which is exactly what shape (b)
does not produce. Left alone, a page regenerated on a shape-(b) host reads 401 matched · 1
disclosed where the roster banks 400 + 2, and renders the row as a plain matched fail | fail: the
host-stable arithmetic the ruling asks for, broken one layer below the sweep. The renderer now reads
the annotation back from the manifest, which is safe precisely because it was reached: a fail/fail
annotated row whose C# output missed the pin is a mismatch, and a mismatch never validates, so no
page is written.
The row’s note is rendered from the manifest, not hand-written into the page — a hand edit does
not survive a regeneration, and the note must never drift from the annotation the oracle actually
applies. It follows the internal/zstd model: name the dependency, then name both accepted shapes.
Proof — and why three of the four guards can only be fixtures
Shape (b) cannot be forced on a host whose Go BoGo run passes, and this host’s does. The fixtures ARE the proof for it, and each was proven failing-first by neutering the half it guards:
| Guard | Neutered | What fired |
|---|---|---|
TestHostConditionalDisclosureAccountsInBothShapes |
the classification arm | disclosed: [TestPinned] — 1 where the row banks 2, the 401 + 1 arithmetic in fixture form |
TestHostConditionalRootWithdrawsGoOnlyDescendantsWhenGoFails |
the root admission | the flood, verbatim: three Go-only children as one-sided mismatches |
TestHostConditionalRowRendersDisclosedWhenBothSidesFail |
the page’s manifest read-back | the totals line lost the disclosed row |
TestHostConditionalMarkerMustNameItsDependency |
the loader’s blank check | a marker naming nothing loaded silently |
Each also carries its own controls, so the tolerance cannot spread: an unannotated agree-fail row stays an ordinary matched agreement, an unannotated root floods (that IS the control), a moved C# signature is a mismatch, a C# side that starts passing leaves the set, and an unannotated disclosure renders no note.
Gates
Converter go test ./... ok, 285.086 s, exit 0 — 387 tests, zero failures, including
projitemsIntegrity_test (the four new guards extend the existing
disclosedParentAggregation_test.go, so there is no new .go file to register) and
TestStdLibMetadataInSync.
Live crypto/tls sweep on this host (laptop R, Ryzen 7 PRO 6850U): PASS 400, 1 pass / 0 fail,
991 s. This host produced shape (a) — TestBogoSuite Go pass / C# fail — which is the
regression half of the proof: the pinned divergence still accounts exactly as banked with the new
arm in the loop. The comparison record reads 402 Go rows / 402 C# rows, 2 disclosed, 3,242
withdrawn, 0 errors, so 402 − 2 = 400 matched, unmoved.
The regenerated proof page’s diff is the provenance line and the note, nothing else — no verdict
row moved, the totals line still reads 400 matched · 2 disclosed, and the withdrawal line still
reads 3,242. Corpus dirt from the sweep classified per the standing rule and restored: six
*_test.cs CRLF phantoms (handshake_client, handshake_server, handshake, key_schedule,
link, tls), each proven CR-stripped-equal to HEAD byte for byte rather than trusted to an
empty --numstat. No production .cs mover, no .csproj change, no initᴛᴛtests hook. The
sweep’s own drift report named exactly one content change, this lane’s manifest edit.
check-no-regression.ps1 over all 629 behavioral packages: 1 changed file, 0 NOT MEASURED,
0 advisory warnings; preflight solution integrity 631/631, path casing 4,534/4,534. The one
changed file is not this lane’s and not a converter change — it is
ReflectChanDirection/ReflectChanDirection.csproj, a UNION CARRY: claude/cargo-recv added that
guard from a fork that predates claude/preflight-trio’s csproj-metadata leveling, so the committed
file still spells <TargetFramework>net9.0</TargetFramework> unconditioned, a literal <Company>
and <Copyright>, and no <RepositoryType>. Censused at HEAD, that is exactly one file of 629 —
the other 628 already carry the leveled form — so it is a stale bank, not an emission change, and it
reproduces at the union base untouched by anything here. RESTORED per the foreign-mover rule and
left for its owner: one re-transpile of that package levels it and takes CNR green.
This lane’s own change cannot reach the emission path at all — loadTestDisclosures,
matchTerminalStatuses, proofDisclosedNames and
renderValidationProofPage are compare/proof-only and no caller of any of them writes a .cs or
.csproj — but the gate is run rather than argued.
What this lane deliberately did NOT do
- Did not touch the roster.
crypto/tlsstill banks400 | 2, which is the whole point: the annotation exists so that number never has to move. The roster’shost-conditionalphrase stays reserved for its verdict-COUNT annotation, and the ruling above is the canonical text. - Did not touch
run-validated-sweep.ps1. Itsdisclosed count movedcheck reads$Comparison.disclosed, which the oracle now holds at 2 in both shapes; the primary path compares 400 against 400. Nothing in the sweep needed to learn a new concept. - Did not annotate a second row. The marker is ruled IN only for a row with coordinator-accepted rooting evidence, and the loader now makes an un-named marker impossible rather than merely discouraged.
- Did not fix the stale
encoding/xmlREADME badge. Coordinator queue, unchanged.
The ruling’s closing caveat — “until it lands, a crypto/tls sweep on a host whose Go BoGo run
fails is a KNOWN false red” — is now historical. Such a host reads 400 + 2 like every other.
CORRECTION – the .tests.csproj leveling-pass queue item retires EMPTY: the census is zero (coordinator, 2026-08-20)
The note two sections up predicted the remaining hand-leveled .tests.csproj would drift one sweep
at a time and queued a scripted leveling pass. Measured, the population does not exist:
git grep -l "<Company>The go2cs Authors</Company>" -- 'src/core/**/*.tests.csproj' returns 0 of
157 at master – independently measured on laptop R at the same base, which is what caught the
over-prediction. The preflight-trio hand-leveling was complete and exact across every file it
touched; the only two short-form files in the corpus were the template pair, created along the
cargo-recv fork line BEFORE the template change existed, and both banked at the union gate. The
standing classification stays (a .tests.csproj emitted by a pre-template fork catches up at its
sweep – the shape to expect from any lane that forked early), but the queued pass is withdrawn:
there is nothing to level.
CLOSED (2026-08-20, lane claude/tests-csproj-stale-badge) – the stale-validation-badge PIPELINE gap, and the residual the gate does not close
The class named above (“The stale-validation-badge class is a PIPELINE gap, not four accidents”) is
closed at the gate half. writeReadmeFile’s options.convertStdLib gate is now the named predicate
emitsPackageReadme = options.convertStdLib || rewriteOfCorePackage(projectFileName, options)
– the SAME widening validationPackBlock’s gate already took, against the same predicate, for the
same reason. Because rewriteOfCorePackage tests the OUTPUT LOCATION (under the runtime root’s
core\ tree) rather than the invocation mode, a corpus package levels its README on ANY
regeneration – -tests, single-package, -stdlib alike – while the litter rule the gate exists
for is preserved structurally: no fixture, example, -recurse output or end-user output path can
satisfy it. Guarded by TestPackageReadmeEmissionFollowsPackageProvenanceNotRunMode, both arms
proven failing-first by neutering each separately.
The measurement that matters most is a byte-identity, not a refresh. Three banked packages
(hash/adler32, crypto/rc4, unicode/utf8) had their badges deliberately staled, then were run
through -tests -test-action convert: with the pre-fix binary all three stayed stale (the defect
reproduces); with the fix all three returned to their committed bytes exactly. The committed
READMEs were emitted by -stdlib, so byte-identity is the proof that the -tests emission AGREES
with the -stdlib emission – this opens no new standing-restore family, which was the live risk
in widening the gate at all. A full -test-action all on hash/adler32 then validated 2 of 2 with
git status clean across src/core AND docs – zero dirt, zero .tests.csproj drift – and a
filtered -stdlib over the same three moved nothing but the six root attribution files at an EMPTY
numstat (the documented CRLF phantoms).
The residual: the board’s two formulations are NOT equivalent
The item offered them as alternatives – “let a -tests conversion that WROTE a proof page also
refresh that package’s README, or equivalently drop the convertStdLib gate to ‘stdlib package’
rather than ‘stdlib run’”. They are not equivalent, and the difference is an ordering the gate cannot
reach. The README is composed during CONVERSION; the proof page it reads is written at the END
of the COMPARE (emitValidationProofPage, testConversion.go). So within one -test-action all
the README is always built from the proof page as it stood BEFORE that run.
Consequence, measured by moving docs/validation/current/hash.adler32.md aside to simulate a fresh
bank and running one full all: the run validates 2 of 2 and writes a green page, and the README it
emitted in the same run reads Tests-not_yet_validated-orange beside its own green proof page.
The next conversion of that package levels it (verified: back to green, byte-identical to committed).
So: a package whose counts are UNCHANGED is now self-leveling, which is the common case and the one that was silently rotting. A package whose counts CHANGE – every fresh bank, and every rebank that moves a number – is still one run behind, and now visibly self-contradictory rather than merely stale. A bank still owes one extra conversion of its own package after the pipeline; that is cheaper than the whole-stdlib reconvert it replaces, but it is not nothing.
Closing it needs the OTHER formulation – re-emit the README after the compare has written the page.
That is a second emission point, not a gate change, and it carries a real hazard that is why this
lane did not take it uninstructed: -test-action compare does NOT convert, so the converter’s
package globals (packageDoc, packageSourceDir) are empty on that path, and naively re-calling
writeReadmeFile there would emit a README with no doc body – a destructive, corpus-wide rewrite
that would read as ordinary reconvert drift. Whoever takes it should either restrict the re-emission
to actions that actually converted (convert/all), or refresh only the badge LINE in the existing
file rather than re-rendering the document.
⛔ crypto/internal/edwards25519 CANNOT bank from any SIGNED-OFF stage of the ж-box design — its 98 objects are receiver-position and class-3b traffic, and Phase A’s mechanism has no constituency on the row’s path (2026-08-20, lane claude/zh-box-reduction-impl)
Briefed as the ж-box arc’s implementation lane with crypto/internal/edwards25519 54/55 → 55 as
the banking target. It banks nothing, and the reason is not a shortfall in the mechanism’s tuning:
the target row is not reachable by anything DESIGN-zh-box-reduction.md
has signed off, and that is a structural property of the row’s call path. The crypto/tls
canary the brief asked for ran on BOTH sides of the bogo merge and is reported below as a pair.
The base — the hard check failed, then passed mid-lane
b4d999a9c was absent when the lane opened: origin/claude/union-157 was dde5d53b5, carrying
606e41abb (the ruling half) but not 2e7fc3225 (the machinery half), which sat unmerged on
claude/bogo-host-conditional. It landed during the lane; the branch is re-based to bd79cb16e
and the hard check passes. The interval was not wasted — it bought the pre-machinery control
arm that makes the canary record below a genuine A/B rather than a single reading.
The finding — the row’s bill is B′ and class 3b, both outside this design’s sign-off
Measured, this host, bd79cb16e (-tests -test-action all -test-timeout 15m, explicit
-go2cspath): crypto/internal/edwards25519 is 54 matched / 1 divergent of 55, the sole
divergence TestAllocations: Go="pass" C#="fail", reading 98 objects/run (9,800 counted over
100 runs; 1,324,800 B → 13,248 B/run) against want 0. That reproduces the 2026-08-19
edwards25519-a figure to the object: the row has not moved, and it is stable, not drifting.
The reason is visible in the emitted sources, not inferred. The test body is
p := NewIdentityPoint(); p.Add(p, NewGeneratorPoint()); s := NewScalar()
testAllocationsSink ^= s.Bytes()[0]; testAllocationsSink ^= p.Bytes()[0]
and every allocation site on that path is one of three things, none of them Phase A’s:
@new<T>()— class 3b.NewIdentityPoint/NewGeneratorPointemit@new<Point>().Set(…);Point.Addemits@new<projCached>().FromP3(…)+@new<projP1xP1>().Add(…);NewScalaremits@new<Scalar>(). Five per run before any arithmetic. Real Go allocations Go’s compiler elides by inlining + escape analysis — Phase C, horizon, not scheduled (§1 class 3b, §3.6).- Field-ref boxes at METHOD-ARGUMENT positions — class 1 by shape, B′ by disposition.
field.Element.Multiplyemits asMultiply(this ж<Element> Ꮡv, ж<Element> Ꮡx, ж<Element> Ꮡy)— the parameters are still boxes, so every caller mints (Ꮡx.Multiply(Ꮡv.of(Point.Ꮡx), ᏑzInv)). Phase A’s lowering did land underneath it —feMul(ж<Element> Ꮡv, ref Element x, ref Element y), 14 lowered signatures in the package, the scalar-fiat family exactly as the A1 census priced — but a lowered leaf saves nothing at the caller of the boxed method that wraps it. heap()locals that exist to be METHOD RECEIVERS.Element.Invertalone declares ~15ref var zN = ref heap(new Element(), out var ᏑzN)locals whose addresses are taken solely to serve as receivers (Ꮡz2.Square(Ꮡz)). §3.2 X3 excludes “a method call onp” by rule — receivers stayжin Phase A.
Allocation-site counts on the executed path, from the committed .cs: projP1xP1.Add 16,
Element.Invert 9, Point.bytes 6, projCached.FromP3 4, Point.fromP1xP1 4, Scalar.bytes 2,
plus the five @new<T>() and checkInitialized’s variadic params-array — the right magnitude for
98, and not one of them a lowerable unexported package-level function’s pointer parameter.
This is the A3 measure’s own fact read from the other side, and nobody had read it onto this row:
nistec fell −96.5 % because its bill is fiat leaf functions; edwards25519’s bill is
point-arithmetic methods. §3.7 is explicit that B′’s constituency is receiver-chain traffic and
that B′ “needs its own design increment and its own measurement, and it is NOT part of this
sign-off”; §9’s plan ends at B1, which §4 states is “count-neutral except one row” (the &s[i]
element-ref) and byte-significant. No signed-off stage moves this row, which is why A2/A3 moved
it 109 → 98 and then nothing.
What the row would actually take — stated so it can be ruled, not chased
- B′ (method dual emission) — a
ref-receiver/ref-parameter overload beside theжmethod for direct calls on a statically-known receiver. The row’s dominant term, and a design increment rather than an implementation detail. - Class 3b — the five
@new<T>()per run. Phase C. - Residue:
checkInitialized’s variadic params-array;Bytes’array<byte>(32)backing.
A want-ZERO row needs all three. The near-budget ruling’s logic applies unchanged — 98-vs-0 is an optimization target, not an impossibility — so the row stays undisclosed; nothing here proposes otherwise.
The crypto/tls canary — run on BOTH sides of the merge, and the machinery is INERT on shape (a)
The brief predicted this host needs the machinery to read 400 + 2, and that this lane’s run would be the first LIVE shape-(b) proof. Neither held — reported as asked, either way.
| base | wall | sweep | TestBogoSuite Go / C# |
children | matched |
|
|---|---|---|---|---|---|---|
| pre-machinery | dde5d53b5 (no 2e7fc3225) |
795 s | PASS 400 | pass / fail | 3,242 | true |
| post-machinery | bd79cb16e |
902 s | PASS 400 | pass / fail | 3,242 | true |
Both runs are shape (a) — the ordinary pinned divergence, which the unannotated disclosure
already satisfied and whose Go-only children disclosureRoots already absorbed. Go’s BoGo baseline
reached completion AND expanded (3,242 TestBogoSuite/… children) on both. The roster row is
crypto/tls | 400 | 2, and the sweep’s matched count plus its disclosed-count check read
400 + 2 on both sides of the merge — host-stable, as the ruling requires.
What that pair does and does not prove:
- Proved: the machinery is inert on shape (a). Identical readings with and without
2e7fc3225— same 400, samematched:true, same 3,242 absorbed children. This is the non-regression half of the proof matrix, and it is precisely the half a fixture cannot supply: a fixture shows the new arm works, only a live run shows the old arm undisturbed. - NOT obtained: the live shape-(b) proof. It needs Go’s baseline to FAIL, which is network reachability plus the child’s 10-minute deadline — environmental, and board-recorded as nondeterministic on one machine across consecutive runs (3,243 subtests, then 402). Two consecutive runs here both passed.
- Therefore shape (b) is an opportunistic capture, not a schedulable lane item. Whoever next
meets a red
crypto/tlswhose C# side is unmoved should keep that run’sgo2cs_test_comparison.jsonrather than restoring past it. That is where the proof will come from; commissioning a lane to produce it would be commissioning a lane to wait.
Standing post-run dirt, classified and RESTORED (nothing banked from it)
crypto/internal/edwards25519/package_init.cs+7 / −0 — theinitᴛᴛtests()hook, the FOURTH-tests-closure shape CLAUDE.md names (REAL lines; it survives a numstat phantom filter). From this lane’s own-testsruns. Restored.- Six
crypto/tls/*_test.cs, after each sweep — pure CRLF phantoms (--numstatempty). Restored both times. - Nine untracked
edwards25519test artifacts — removed, not committed: the validated-package commit policy binds test sources to a package that validates, and this one does not.
What this lane did NOT do, stated plainly
- Banked nothing. No roster row moved, no proof page written, no test sources committed:
crypto/internal/edwards25519stays 54/55, the roster stays 157/215 (73.0 %). - Wrote no converter or golib code. Both available directions — B′ and Phase C — are outside the design’s sign-off, and the brief’s own discipline governs: anything deviating from settled semantics comes back to the coordinator, not self-ruled. The tree is clean.
- Did not run the full combined gate. It gates a bank; there is nothing to bank. The two instruments that were run are reported above with their arithmetic.
- Did not start B1. It is the one scheduled stage left, but §4 makes its own preconditions
design deliverables — the three-variant dispatch microbench decides the mechanism, and the
unsafe.Pointerrepresentation must be stated before implementation. It is also count-neutral except the&s[i]row, so it would not have reached this lane’s target either.
For the coordinator — the decision this lane hands back
Rule the edwards25519 row’s owner; it is not the signed-off ж-box arc. Either commission B′ as
its own design increment — the row’s dominant term, and §3.7 already names a constituency far
beyond it (runtime/proc 387 sites × 3 GOOS, h2_bundle 212, database/sql 154, os’s of() chains), so
the increment pays well past this one row — or move edwards25519 off the 75 %-terminal path and pick
a fourth candidate whose residual is reachable from a signed-off stage. The 98-vs-0 count stays
undisclosed either way.
RULING – B’ is COMMISSIONED as a 1.23.12-era design increment; edwards25519 moves OFF the 75% path; flag takes the fifth slot (coordinator, 2026-08-20)
The zh-box lane’s finding two sections up is ratified in full: no signed-off stage of
DESIGN-zh-box-reduction.md reaches crypto/internal/edwards25519’s want-zero row, the 98-vs-0
residual stays UNDISCLOSED (it is an optimization target, not an impossibility – disclosing it
would launder exactly what the host-limit bar forbids), and the row is stable at 54/55.
B’ (method dual emission) is commissioned as its own design increment with its own measurement
– scheduled for the 1.23.12 era, NOT before the terminal. Its constituency is where SS3.7
priced it: runtime/proc 387 sites x 3 GOOS, h2_bundle 212, database/sql 154 – the netpoll,
http and database arcs the campaign continues into after the terminal, on the corpus where every
row re-derives anyway (the H10 economics). Spending it now to reach one .1 row would buy the row
twice. edwards25519 banks when B’ and Phase C land, and it will be the natural first measure of
both.
The terminal path therefore re-ranks. After encoding/gob (#158, in flight):
net/netip (#159, model-selection arc, no ruling needed) -> sync/atomic (#160, at 104/108,
pending the alignment/pointer-identity ruling) -> runtime/debug (#161, pending the introspection
ruling) -> flag (#162) – ROOTED-NOT-TAKEN above at CS1929 x10, the Delta-rename and the
adapter disagreeing about one method name, owner ImplementGenerator. If any of the three
pending rows falls through its ruling, the replacement comes from the ranked near-miss queue, not
from re-litigating this one.
Also ratified from the same lane: shape (b) of the host-conditional pin is an OPPORTUNISTIC
capture. Standing instruction, no lane required: whoever meets a red crypto/tls whose C# side is
unmoved keeps that run’s go2cs_test_comparison.json before restoring past it.
⛔ MAP KEY/ELEM DIMS ARE DESCRIPTOR CARGO and both array<T>-length rows retire — but encoding/gob reaches 105 of 106 and does NOT bank, because reflect.ArrayOf/StructOf is not disclosable (2026-08-20, lane claude/map-key-elem-cargo, laptop G)
The queue top, taken on the recipe the cargo-recv entry wrote for it. The cargo lands, the
array<T> LENGTH class retires whole, and the retirement test applied mechanically says the row
still does not bank: 103 → 105 of 106, one residual the remedy does not clear and no class it
belongs to.
The recorded need was half the shape, and MEASUREMENT supplied the other half
The board recorded gob’s need as “map key/elem dims cargo” and located it exactly —
T1.Marr map[[2]string][2]*float64, decodeMap allocating from mtyp.Key()/mtyp.Elem(), a map
descriptor carrying no dims. That was right, and it was two of the four positions the arc needed.
The other two came from running the pipeline rather than from reading it:
| Position | Found by | What it is |
|---|---|---|
| a map field’s KEY dims | the recorded need | Key() had no cargo slot at all |
| a map field’s ELEM dims | the recorded need | Elem() had a slot; a map never filled it |
| a POINTER-chain field’s dims | the FIRST run | T1.N *[3]float64, Indirect.A ***[3]int — same root, unrecorded |
| the pointer hop on the VALUE side | the SECOND run | Value.Elem() read dims from the live value only |
The unifying statement is one sentence, and it is what the arc actually is: a struct FIELD’s dims
come from its zero instance, which reaches an array the field IS and nothing an array is BEHIND.
FieldArrayDims reads = new(N) back off Activator.CreateInstance(declaringType); on a pointer
field that instance holds a nil pointer with no pointee to measure, and on a map field a nil map
whose key and element types no entry could reveal. Both hops are ordinary at a decode target,
which is exactly a struct nothing has populated yet — so the datum has to be in the emitted C#, which
is the conclusion the func-PARAMETER position reached and takes the same carrier.
The design fits the GoArrayDims precedent because the slots are named for ACCESSORS, not for kinds
| Cargo slot | Attribute | Carries | Handed down by |
|---|---|---|---|
abi.Type.arrayDims |
[GoArrayDims] |
an ARRAY’s own dims, a POINTER’s pointee’s, a MAP’s element’s | Elem() — tail for an array, unshifted for a pointer and now a map |
abi.Type.keyDims |
[GoMapKeyDims] |
a MAP’s key’s dims | Key() |
Nothing about arrayDims changed meaning. Its cross-kind meaning was already “what Elem() hands
down” — that is why a POINTER carries its pointee’s unshifted — so a MAP simply joined the arm it
already had, and the one accessor with no slot got one. keyDims joins both interning keys
(abi.descriptorDimsKey, shared with reflect’s canonType) for the third time and the third
instance of one reason: map[[2]string]V and map[[3]string]V are distinct Go types over one
managed map<array<@string>, V>.
type T1 struct { // TestEndToEnd
Marr map[[2]string][2]*float64
N *[3]float64
}
type Indirect struct{ A ***[3]int } // TestIndirectSliceMapArray
[GoArrayDims(2), GoMapKeyDims(2)] public map<array<@string>, array<ж<float64>>> Marr;
[GoArrayDims(3)] public ж<array<float64>> N;
[GoArrayDims(3)] public ж<ж<ж<array<nint>>>> A; // ONE stamp, any depth
The fourth position was found by MEASUREMENT again, and it is the same lesson one layer over
With the stamps in, TestEndToEnd passed and TestIndirectSliceMapArray’s root moved one frame —
out of gob: wrong type (***[]int) for received field Direct.A and into
panic: reflect: reflect.Set using unaddressable value, inside growSlice. reflect.Value.Elem()
recovered a pointee’s dims from the LIVE value alone, while rtype.Elem hands the descriptor’s cargo
down unshifted. gob’s decIndirect walks a ***[3]int target by allocating each level from
value.Type().Elem(), so a hop reading the live value reads the nil pointer it is standing on,
allocates a zero-length array from the dimension-less descriptor, and the next hop measures that
zero as the truth. Value.Elem() now prefers the carried dims, falls back to the live measurement
(ValueOf(&[100]T{}).Elem().Type() still carries 100), and descends them through EVERY pointer hop —
not only the one whose pointee is the array, because ***[3]int’s intermediate pointees are pointers.
That is cargo-recv’s fourth-position lesson restated at the Value layer: a value the bridge hands
out must describe itself the way the descriptor does. Both arcs found their last position by running
the pipeline, not by reasoning about it, and in both cases the shape “looked covered”.
The retirement test, applied mechanically — and the row does NOT bank
| Row | Before | After | Root |
|---|---|---|---|
TestEndToEnd |
gob: length mismatch in decodeArray |
PASS | map key/elem dims |
TestIndirectSliceMapArray |
wrong type ([3]int) for received field Indirect.A |
PASS | pointer-chain field dims + the Value-side hop |
TestIgnoreDepthLimit |
typelinks: external … not implemented |
unchanged | reflect.ArrayOf → typesByString → the typelinks() stub |
The array<T> LENGTH class retires: both of its rows pass, and the census the reflect-minibridge
entry left has one line in it instead of two. The third row is not disclosable, and the bar rules
it out by name. TestIgnoreDepthLimit builds a 101-deep nested array with reflect.ArrayOf and
wraps it in reflect.StructOf — runtime TYPE CONSTRUCTION, which the managed runtime could in
principle perform. host-limit’s text is explicit that an entry “must name a structural property of
the deployment shape, never an unimplemented-but-fixable defect”, and the three
allocation/liveness classes do not reach it at all; the fifth, chan-direction, retired this morning
and was the only representation class there has ever been. So there is no class to disclose under,
the roster’s own rule stands (“a package that almost passes never appears”), and encoding/gob stays
off it at 105 of 106.
What banks is the converter arc, not the row. That is the honest split the brief asked for, and it is worth more than the row would have been: the cargo is corpus-wide machinery, the two rows it retired were the board’s oldest standing reflect-bridge chip, and what remains is a single named feature rather than a class.
Boundaries, stated rather than discovered later
- A field that IS an array is NOT stamped. Its
= new(N)initializer already carries the length through a route that survives a value copy; stamping it would duplicate the datum and churn every array-bearing struct in the corpus for nothing. This is why the whole-corpus footprint is SIX lines and not thousands. - A DEFINED array or map type is not stamped —
type Row [3]intbehind a pointer,type Set map[[2]string]bool. The same one-sentence boundary the chan direction draws, for the same reason: the managed form is a go2cs-gen wrapper rather thanarray<T>/map<K,V>. An ALIAS for one IS its target and is stamped. - A SECOND nesting level is not carried —
[][2]int,map[K]map[[2]string]V, a func field’s parameters. The cargo has exactly oneElem()slot and oneKey()slot, so a second level has nowhere to live and no measured consumer asks (the r39d rule).reflect.Type.String()still renders[][2]intas[][]int; the behavioral guard carries that row as a KIND comparison rather than asserting the divergence away, and says why in place. - A func PARAMETER of map type is not stamped.
[GoArrayDims]reaches parameters already, but nothing measured readsIn(i).Key().Len(). internal/reflectlite’s mirror is untouched. ItsElemalready passesnulldims by design and itsstructFieldDescriptorgates on Array, so the new stamps are invisible there — which is what keeps its banked 30 from moving. A lane that needs dims in reflectlite owes its own measurement.
The corpus footprint is SIX LINES, and the boundary rules are why
Seeded whole-stdlib reconvert — fresh root, single run, -stdlib -comments, 304 packages, 380 s,
converter exit 0 — with version.props and docs/validation seeded beside core so the README
badges compose. Marker gate 63 marked / 0 clobbered, path-precise and line-anchored (git grep
-lE '^\s*\[module:\s*(go\.)?GoManualConversion\]', whole files, re-measured not carried). Of 1,663
emitted artifacts: 0 new, 0 absent, 13 differ CR-insensitively, and the split is clean.
| Class | Count | What |
|---|---|---|
| this arc | 5 files / 6 lines, every one a stamp, ZERO off-shape | compress/flate/inflate.cs (bits *[maxNumLit+maxNumDist]int → 316; codebits *[numCodes]int → 19) · internal/fuzz/fuzz.cs (hashes map[[sha256.Size]byte]bool → key 32) · net/http/h2_bundle.cs (pings map[[8]byte]chan struct{} → key 8) · runtime/symtabinl.cs (inlTree *[1 << 20]inlinedCall → 1048576) · hpack/huffman.cs (children *[256]*node → 256) |
| pre-existing carries | 8 | gcimporter’s multi-result any-slot arm, plus the runtime box-compare family (alg, map, map_fast32, map_fast64, mbarrier, traceback, pprof/map) — exactly the set cargo-recv named and RESTORED, minus the two per-GOOS members a windows-only reconvert cannot see |
Zero unclassified. Only this arc’s five are banked; the eight carries are restored and belong to a
leveling regen. That the whole footprint is six lines is the boundary rules working rather than luck:
an ARRAY field keeps its = new(N) initializer, so the thousands of ordinary array fields in the
corpus do not move, and every Go constant behind a stamp (maxNumLit + maxNumDist, sha256.Size,
1 << 20) is resolved by go/types at conversion time.
No package_info.cs record moved, so no go generate is owed — and that is asserted by
TestStdLibMetadataInSync passing, not inferred from the absence of a diff.
One UNION artifact, rooted rather than assumed — and it is the merge-time rule again
CNR flagged ReflectChanDirection.csproj as a mover, which is not this arc’s shape at all (the diff
is csproj metadata: TargetFramework hoisted to a condition, Company/Copyright derived,
RepositoryType added). It is the union’s own artifact: 0efa5f79f (preflight-trio, which
levelled the csproj template) and 22940de2f (cargo-recv, which ADDED that project) are SIBLING
branches — neither is an ancestor of the other — so each lane’s CNR was green on its own tree and the
merged tree is the first place a project emitted by the OLD template meets the NEW one.
Proven rather than argued: the union-base converter was rebuilt from git archive
origin/claude/union-157 and re-emitted that one package, reproducing a byte-identical mover with
none of this change present. Levelled here, because it is one file and every future CNR sees it
otherwise. Worth carrying as the golden-side twin of the banked-row merge rule: a lane’s CNR proof
binds its own tree, and a merge can make a NEW project’s committed emission stale without either
side changing.
The residual, decomposed — it is two items and only one of them is a wall
TestIgnoreDepthLimit needs reflect.ArrayOf and reflect.StructOf, and this arc changed the
price of the first:
reflect.ArrayOf(n, elem)is now nearly free. An array type’s length is exactly what the dims cargo carries, so the hand-own issynthType(typeof(array<>).MakeGenericType(elem), [n, …elemDims])— notypelinks, no linker table. The auto form dies intypesByStringonly because it is reconstructing Go’s linker-allocatedarrayTyperecord, which the managed bridge never needs to.reflect.StructOf(fields)is the wall: runtime STRUCT synthesis, which wantsSystem.Reflection.Emit(and is therefore also an AOT question). A real feature arc.
Implementing ArrayOf alone does not flip the row — the test builds its 101-deep array and then
wraps it in a StructOf — so the honest statement is that gob’s last verdict is priced at one small
hand-own plus one feature, not at “the typelinks stub”.
Gates
⚠ Every wall-clock figure below was measured with a sibling lane active on the same machine
(a zh-box run out of the go-netpoll-managed-poller worktree, confirmed by parentage rather than
assumed) — so they are loaded numbers, at the top of the CLAUDE.md ranges, and should not be used to
re-baseline the budget table.
Converter go test ./... ok, 146.0 s, exit 0 — projitemsIntegrity_test included, so both new
.go files are registered; TestStdLibMetadataInSync passes, which is what ASSERTS that no
package_info.cs record moved rather than inferring it from a clean diff, so no go generate is owed.
GolibTests 176 / 176 passed, 0 failed — including the new
GoStructLayoutTests.FieldDims_ComeFromTheConverterStamp_WhereNoZeroInstanceCanMeasureThem.
Full check-no-regression.ps1: generated .cs and .csproj byte-identical across all 630
behavioral packages, 0 NOT MEASURED, 2 advisory converter warnings, exit 0, 769 s; preflight
solution integrity 632 / 632 (631 + this arc’s guard), path casing 4,541 / 4,541 (4,534 + the
guard’s 7 tracked files). Run at the COMMITTED tree, after the golden re-baseline — the first pass
had caught a stale main.cs.target, because UpdateTestTargets had run before the guard gained its
decIndirect walk.
Solution builds, both --no-incremental: go2cs-stdlib.slnx -p:GoTargetOS=windows 0 errors,
763 s (307 projects — this is what compiles all five corpus movers plus the abi/reflect
changes); go2cs.slnx Debug 0 errors, 1,130 s — the gate no other harness covers, and this arc
changes golib’s attribute surface and GoReflect.GoFieldInfo.
No linux build is owed here, and that is checked rather than assumed: every file this arc touches
is FLAT — compress/flate/inflate.cs, internal/fuzz/fuzz.cs, net/http/h2_bundle.cs,
runtime/symtabinl.cs, hpack/huffman.cs, internal/abi/type_impl.cs, reflect/value_impl.cs,
golib — none sits in a per-GOOS folder, so the default windows build compiles every one of them.
(The L3 lesson that a net/linux/-resident change needs its own target is real; it just does not
apply to this footprint.)
Full behavioral suite PASS at 603 projects, 2,056.8 s — Transpile 603/603, Compile 603/603,
Target 603/603 byte-identical, Output 577 compared / 0 failed (26 skipped, no package main),
0 timeouts / 0 NOT MEASURED. Not optional for this arc, and CNR does not substitute for it: CNR
is transpile-only, while what changed here is RUNTIME — GoReflect.GoFields, which every %v walk
and every json/xml member order reads, plus three reflect accessors and Value.Elem.
encoding/gob pipeline, -tests -test-action all -test-timeout 20m, three runs: 103 → 104 → 105
of 106, each root re-measured rather than predicted.
Canaries — the five largest banked reflect consumers BY VERDICT COUNT, recomputed from the roster
at gate time per the doctrine. The derivation is also CHECKED rather than pattern-matched: the
three larger rows are skipped because they are not reflect consumers, and that is measured —
go/doc/comment (10,059), crypto/rsa (559) and internal/zstd (536) use reflect in zero of
their converted test files, against 1–5 for each row below.
| Canary | Expected | Result |
|---|---|---|
go/internal/gcimporter |
583 | PASS (564 s) |
go/types |
557 | PASS (193 s) |
encoding/json |
491 | PASS (84 s) |
encoding/xml |
386 | PASS (104 s) |
crypto/tls |
400 | FAIL — the KNOWN false red; verified against the ruling, not assumed |
There is no own-row sweep, because there is no own row: the encoding/gob pipeline at 105 of 106 is
this lane’s measurement and it stands in that slot.
crypto/tls — the host-conditional ruling’s first live exercise, and it holds exactly
The coordinator’s ruling (same board, this morning) requires the C# verdict map to be verified
unmoved against the committed proof page before a red here is dismissed. Done mechanically, parsing
the proof page’s Verdicts table and the run’s own go2cs_test_comparison.json:
| Check | Result |
|---|---|
| C#-side differences vs the committed proof page | 0 — all 402 banked rows present, every verdict identical |
| banked rows absent from the live C# map | 0 |
| Go-side differences | 1 — TestBogoSuite, banked pass → live fail |
TestBogoSuite/… subtest rows |
go 3,242, C# 0 — the annotated row’s own baseline expansion, excluded from one-sided accounting by the ruling |
| one-sided rows OUTSIDE that expansion | 0 |
| language divergences outside that expansion | 1 — TestCertCache (go pass, C# fail), the banked codegen-liveness disclosure, unchanged |
So the row’s live shape is Go fail / C# fail — agreement on a host where the Go premise fails,
the second of the two shapes the adopted annotation accepts — and the C# side is provably untouched
by this arc. Nothing in a go2cs branch can move it: the Go side is go test -json over GOROOT’s own
sources with the Go toolchain. (cargo-recv measured 3,243 expansion rows this morning against this
run’s 3,242 — the count varying with how far the runner gets before its own deadline is exactly the
nondeterminism the ruling names.) The annotation implementation is R’s lane and is not taken here.
Post-sweep drift: 32 files, every one classified, all RESTORED
Twenty-eight are pure CRLF phantoms — modified with an EMPTY --numstat, the multi-line-literal
class. The four with real hunks are all named classes and none is this arc’s:
| File | Hunks | Class |
|---|---|---|
encoding/xml/package_init.cs |
+7 | the initᴛᴛtests hook — the -tests-closure shape named 2026-08-17 |
go/types/package_init.cs |
+7 | same |
go/internal/gcimporter/gcimporter.cs |
1/1 | the pre-existing multi-result any-slot carry, the same one the corpus census found |
go/types/package_test_info.cs |
−3 | three GoImplicitConv records the converter no longer emits — a stale banked test artifact, PROVEN pre-existing by re-converting go/types’ tests with the union-base converter, which drops the identical three |
That last one is the same shape cargo-recv recorded for crypto/tls’s committed test sources, and
it takes the same treatment: restored, recorded, and NOT levelled inside another lane’s arc.
The queue, after this
encoding/gob(106) is still the closest unbanked package, but its price is now NAMED rather than a class. 105 of 106; the last verdict costs one small hand-own (reflect.ArrayOf, roughly free over the dims cargo) plusreflect.StructOf, which is runtime struct synthesis overSystem.Reflection.Emitand a feature arc in its own right. Whoever wants a bank sooner should measure the next candidate rather than take this one — this lane did not, and no claim about “the next closest” is made here on evidence it does not have.reflect.ArrayOfas a standalone hand-own. Worth doing on its own merits even though it does not flip gob’s row:ArrayOfis reachable from user code, the auto form dies in thetypelinksstub for a reason the managed bridge never needs (it is reconstructing the linker’sarrayTyperecord), and the dims cargo now makes the honest implementation a one-liner.- The stale-validation-badge PIPELINE gap, unchanged and unclaimed since
cargo-recvrecorded it:writeReadmeFileis gated tooptions.convertStdLib, so a-testsrun writes a proof page and never refreshes the README beside it, and every bank owes the same manual reconvert. One line of gating, wants its own emission gate. sync/atomic’s zero-size-field layout, 5.%#xof auintptr, 6. the two variadic-argument walls (a SPREAD argument to a deferred variadic call; an empty variadic call passing an empty slice where Go passes nil) — all unchanged.
The array<T> LENGTH class leaves the standing-chip census; nothing joins it.
RULING x2 – sync/atomic’s alignment row closes as a FIX (the token becomes layout-truthful), and runtime-capability is minted as the fifth class with WriteHeapDump as its only member (coordinator, 2026-08-20)
Both held rulings, delivered together because they are the same charter question worked from opposite ends: when a managed runtime cannot hand a test what it asks for, is that a disclosure or a defect? The bar stays where host-limit put it – a disclosure names what is provably unsatisfiable, never what is unimplemented – and each ruling below is that bar applied once.
A. TestAutoAligned64’s pointer half: the identity token gains ALIGNMENT-TRUTHFUL construction. No fifth class; the row closes as a fix.
The row-harvest-3 rooting is accepted: Value.Pointer() is reflectPointerToken, an identity
token, not an address. But the assertion’s semantic content is Go’s align64 GUARANTEE – a layout
invariant the model genuinely honors (the atomics ARE atomic, and StructField.Offset already
answers from the memoized GoFieldOffsets walk). A token whose LOW BITS mirror the Go-computed
layout – mint bases 8-aligned, derive a field’s token as base plus its Go field offset – answers
p & 7 from the SAME metadata that answers Offset: truth read from layout metadata, not a
memory property fabricated. This is compatible with FINDING-managed-box-uintptr-lifetime as
written: the token stays non-dereferenceable and lifetime-free; it merely stops carrying
accidental low-bit noise that contradicts an invariant Go states and the model keeps. The lane
that takes it owns the construction details and must measure token-distinctness is preserved.
Ratified alongside, from the same rooting: the hammer family (TestHammer32/64,
TestHammerStoreLoad) is the zero-size-field LAYOUT EMISSION arc – explicit layout with
Go-computed offsets for structs containing zero-size fields; Reinterpret’s size guard is correct
and must not be relaxed. And RecordGoroutinePanic’s 693 MB serialization death is a bounded
harness-robustness fix (cap the record), queued – it costs 72 verdicts on hosts where containment
loses the timing race, which is a false host-dependence the harness owes nobody.
sync/atomic’s full price to bank: the token construction (1 row) + the layout arc (3 rows).
B. runtime/debug: the fifth class exists, and it admits ONE of the three capability roots.
runtime-capability – a test exercises a runtime facility whose output or behavior is
DEFINED over the replaced runtime’s own internals (type descriptors, heap layout, GC bookkeeping),
such that any managed rendering would be fabrication rather than implementation. The admission
test is one question: does a truthful managed implementation of the asserted behavior exist at
any cost? If yes, it is an arc with a price, never a disclosure.
Applied to the three roots the runtime-debug lane ranked:
WriteHeapDump(3 verdicts) – ADMITTED. Go’s dump format is defined over Go’s type descriptors describing a Go heap; the heap a managed build would describe is not one. No truthful form exists at any cost. Anti-laundering clause, binding: the disclosure pins the rows AS FAILING – the tests assert onlysize >= 1, and writing a fake byte to pass that vacuous assert is forbidden by this class’s own text.HeapReleased(1) and the per-GC pause history (1) – REFUSED. Both have truthful managed forms (the cumulativeTotalCommittedBytesdecrease; a gen2-callback recorder capturing real pause facts). They are priced together as the ReadMemStats measurement-surface design – one arc, one recorder, both facts from it – and queued, not disclosed.
The class joins the roster preamble WITH the first banking commit that uses it (exactly as
chan-direction did), not before. runtime/debug’s full price to bank: the position-map arc
(multi-consumer – log, log/slog, flag and this package all pin it) + the ReadMemStats
design + the 3-row disclosure. It HOLDS its slot on the terminal path at that price; if the
recorder design overruns, the substitute comes from the ranked queue via the measurement pass,
not from re-litigating the class.
CLOSED (2026-08-20, lane claude/readme-rc4-test-regen) – the badge residual takes the OTHER formulation, and the -tests record drop it surfaced is rooted
Two items, both from the stale-badge lane’s own findings.
1. The one-run-behind residual closes at a SECOND emission point
The gate half (emitsPackageReadme) could not reach this and said so: the README is composed during
CONVERSION while the proof page its Tests badge reads is written at the END of the COMPARE, so a package
whose counts CHANGE – every fresh bank – emitted one run behind. refreshPackageReadmeAfterProof now
re-emits the README immediately after emitValidationProofPage writes the page.
The hazard the write-up named is closed by CONSTRUCTION, not by a mode check. -test-action
build|run|compare do not convert, so packageDoc/packageSourceDir are empty on those paths and a naive
re-emission would write a doc-less README corpus-wide. The refresh therefore does not read those globals
at all: the conversion-time write records what it composed the README from (packageReadmeEmission), and
the refresh runs from that record or not at all – no conversion, no record, no write. The record is
taken INSIDE the emitsPackageReadme gate, so one decision governs both emission points, and the refresh
additionally requires the record to name the package being compared (a record cannot level someone else’s
README).
Measured, the gate half’s own demonstration INVERTED. docs/validation/current/hash.adler32.md moved
aside to simulate a fresh bank; ONE -test-action all:
- validates 2 of 2, writes the page, and emits
Tests-2%2F2_validated-brightgreenin the same run –git statusclean acrosssrc/core, i.e. byte-identical to the committed README (the previous measurement left it readingnot_yet_validatedbeside that same green page). -test-action compareover a README replaced by a sentinel leaves it byte-for-byte identical (md5 unchanged), and creates none after the file is deleted.
Guards extend the gate half’s family: TestPackageReadmeRefreshFollowsInProcessConversionNotRunMode
beside TestPackageReadmeEmissionFollowsPackageProvenanceNotRunMode. Both arms proven failing-first –
with the refresh removed the residual reproduces; with the NAIVE formulation (re-emit from the converter
globals, no record) every hazard assertion fires at once, including the doc-less rewrite and the
compare-only write.
Standing note for sweep readers. writeReadmeFile is idempotent, so an unchanged package rewrites
nothing and this opens no standing-restore family. The corollary: a README.md moving during a sweep now
means that package’s validation counts moved. Read it as a finding, not as dirt.
2. The -tests package_test_info.cs regen drop is e61758549, one notch too wide
Reproduced on both cases. A -tests regen of crypto/rc4 dropped TWO records –
[assembly: GoImplicitConv<...rc4_package.Cipher, ж<...rc4_package.Cipher>>(Indirect = true)] and
using testing = go.testing_package; – and of go/types dropped THREE (Basic, Interface, Tuple).
One root explains both, and it explains why the two rc4 lines travel together: they are emitted by the
SAME record site, which calls recordConversionPackageUsing on both operands as it records the pair.
The site is applyImplicitConversion’s struct arm, reached because the argument loop applies the call’s
FIRST parameter type to every argument: testEncrypt(t, desc, c, ...) pairs *testing.T (param 0) with
*Cipher (arg 2) and records the harmless pointer-boxing pair Cipher -> ж<Cipher>, registering
testing as the qualifier alias on the way past.
e61758549 (2026-08-18) widened typeDeclaredInConvertedPackage to subtract a WHITEBOX-PRODUCTION
declaration, to stop internal/reflectlite’s flag(typ.Kind()) minting a phantom partial struct flag
in the test class (CS1061). That reasoning is about HOSTING an operator, and it is right for a numeric
record. It is one notch too wide for the pointer-boxing route, which hosts nothing at all:
ImplicitConvGenerator looks the target up by struct declaration, finds none for golib’s ж<T>, and
continues before choosing a host. recordsRequireProductionMutation already stated exactly that for
the same shape; the predicate is now written once (pointerBoxConversionRecord) and both read it.
Why this is a defect of scope and not settled semantics, stated so it is cheap to overrule:
- the record is INERT in the only consumer –
ImplicitConvGeneratoris the sole reader ofGoImplicitConv, and it skips aж<T>target – so restoring it cannot re-open the phantom; - the phantom
e61758549fixed is a NUMERIC record, untouched here and still guarded byTestWhiteboxProductionNumericConvNotRecorded; - production conversions record the same shape everywhere in the corpus – it is the DOMINANT family,
193 of the 268
GoImplicitConvrecords across the emittedpackage_info.csfiles – so the whitebox-testspath had become inconsistent with production for a shape inert in both; - nothing gates it. CNR never runs
-tests, ande61758549’s own gates (CNR + a seeded whole-stdlib reconvert) cannot see apackage_test_info.cs. The only symptom is a-testsregen that stops reproducing committed bytes – which is how it was found.
The boundary the fix must not cross, and does not. BOTH-FOREIGN pairs stay declined, exactly as the
os syscall.Handle precedent had them before the whitebox exclusion existed. go/types alone reaches
nine of those (types.Basic, ast.FuncType, token.FileSet, …); admitting the pointer-box shape
unconditionally would have ADDED records rather than restored them. Only a whitebox-production operand is
readmitted, which is why the regen lands on the committed bytes exactly.
Gate: -tests -test-action convert over crypto/rc4 – package_test_info.cs byte-identical to
committed, whole-package git status clean. go/types likewise, its only remaining movers the documented
families (package_init.cs initᴛᴛtests() at +7 real lines, twelve CRLF-phantom *_test.cs), restored.
Guards TestWhiteboxProductionPointerBoxConvStillRecorded (three arms: the record, the qualifier alias it
carries, and the both-foreign boundary) and TestPointerBoxConversionRecordShape, each proven
failing-first by neutering the exemption away and then over-broad in turn.
Blast radius, MEASURED – three of seven, and they are banked here
Seven packages had a package_test_info.cs written between e61758549 and this fix, so each could hold
the shrunken form. Regenerated one by one: FOUR are unaffected (debug/elf, encoding/json,
internal/reflectlite, text/template – the site is not reached, or the package is not white-box
reference), and THREE were first banked inside that window, so they were born short and now regen
LARGER:
| package | restored by the fix |
|---|---|
crypto/tls |
six pointer-box records (Config, QUICConfig, clientHelloMsg, serverHelloDoneMsg, serverHelloMsg, ΔConnectionState) |
encoding/xml |
Decoder, plus the testing qualifier alias in BOTH anchor files |
html/template |
parse.TextNode – a DIRECT record, not Indirect – plus the parse alias in both |
Those five files are banked with the converter fix rather than left behind. Not banking them would make
every future sweep of those three packages report drift that is neither a CRLF phantom, nor a closure
re-emission, nor an init hook – precisely the stop and root-cause it class, on three packages,
indefinitely. Each re-validated at its banked count with the records in place: encoding/xml 386,
html/template 243, crypto/tls 400 + 2 disclosed. Every other mover was the documented family
(CRLF phantoms, the initᴛᴛtests() hook at +7 real lines, the proof page’s volatile date/commit line) and
was restored, not banked.
The positive control is the same census read the other way: go/types was banked BEFORE the window, and
its package_test_info.cs reproduces committed bytes exactly under the fix.
✅ CAPTURED (2026-08-20, lane claude/readme-rc4-test-regen) – the LIVE shape-(b) proof for TestBogoSuite’s host-conditional pin, off the opportunistic standing instruction
The bogo lane closed with this open: “NOT obtained: the live shape-(b) proof … Whoever next meets a red
crypto/tls whose C# side is unmoved should keep that run’s go2cs_test_comparison.json rather than
restoring past it.” The crypto/tls re-validation this lane owed for the rebank above is that run, on a
laptop (Ryzen 7 PRO 6850U) where Go’s own BoGo baseline went red.
Measured, -tests -test-action all -test-timeout 20m, exit 0:
| reading | |
|---|---|
TestBogoSuite |
Go fail / C# fail – agreement on FAILURE, which an UNANNOTATED disclosure never satisfies |
| Go side fan-out | reached it: 3,242 TestBogoSuite/… children |
| annotated root | ROOTED them – all 3,242 withdrawn, zero left as one-sided mismatches |
| rows | 402 Go / 402 C# |
| verdict | status: validated, matched: true, errors: 0, disclosed = TestBogoSuite + TestCertCache |
| roster row | 400 matched · 2 disclosed – unmoved |
The fan-out line is the load-bearing one the ruling flagged: shape (b) is precisely the shape in which the Go side reaches its case expansion, so an annotated root that failed to root would have left 3,242 one-sided rows and failed the package. It rooted. Together with the bogo lane’s own shape-(a) canary (the machinery proved INERT there) the proof matrix is now complete on both arms, live.
The run’s comparison artifact is banked verbatim as
evidence-crypto.tls-shape-b.json – it is normally git-ignored under
src/core, and it is not reproducible on demand (the board records the Go baseline as nondeterministic
across consecutive runs on one machine), so keeping it was the instruction. Delete it if the record is
wanted in prose only.
The proof PAGE was restored, not banked: the committed docs/validation/current/crypto.tls.md states
shape (a), and the annotation exists so the roster’s number never has to move with the host that last ran
it.
⛔ net/netip’s STRUCTURAL WALL FALLS — 0 of 267 → 210 of 267 on a two-move model-selection arc — but the package does NOT bank: all 57 residuals are ONE root, allocation behavior, and zero of them are disclosable (2026-08-20, lane claude/banking-net-netip)
The board handed this row on as “#159, model-selection arc, no ruling needed”, with the wall named
exactly right: “x509 needs the REFERENCE model to avoid a split identity, and netip’s fuzz_test.go
needs the RECOMPILE model to satisfy a nominal constraint. One package, one model, and no model
serves both files.” The first half of that is now closed — netip takes recompile, the suite builds
and runs, and 210 of its 267 verdicts agree with go test. The second half is where the row stops,
and it stops on a bar this campaign has already ruled twice.
The baseline is one root, not two — the recorded pair is already half closed
Re-measured on this base before any change: 5 errors, all CS0315, in fuzz_test.cs only. The
xml-netip-alias entry recorded “7 in 2 roots”; its CS0019 half (^uint64(0) &^ 1 keeping C#’s int)
closed at 144c60d2b — uint128_test.cs now emits ~(uint64)1 and the pair is gone. So the wall
this lane met was the structural root alone.
The rule: an adapter serves interface BOXING, never a nominal CONSTRAINT
checkStringParseRoundTrip[P netipTypeCmp] is called with Addr, AddrPort and Prefix — all
declared in the referenced production assembly — while netipTypeCmp is declared in fuzz_test.go.
The white-box reference model’s whole premise is that interface-implementation records are
RELOCATABLE: a production struct is foreign to the test compilation, so go2cs-gen emits a value or
pointer adapter class in the test anchor instead of a partial production struct. That premise holds
wherever the interface is reached by boxing. It has exactly one exception, and this is it: C# checks
where P : netipTypeCmp NOMINALLY, against the type argument itself. The only thing that can satisfy
it is the argument’s own base list; the only thing that can add to that is a partial declaration; and
a type closed inside a referenced assembly admits no partial. No adapter stands in that position.
Hence CS0315 five times, and hence the answer is to stop needing the reference model for this suite.
Two moves, both general:
| Move | What it does |
|---|---|
| The model-selection gate | constraintProxyFor is the (type parameter, type argument) core every instantiation form already routes through — a generic NAMED type via constraintProxyArg, a generic FUNCTION via constraintProxySigArg. It now also records the pairs where the argument is a production type and the parameter’s constraint is a TEST-declared method-set interface. A non-empty set joins recordsRequireProductionMutation at the fallback site, so the suite reconverts under recompile. The constraint side reuses isMethodSetBeyondComparable — the same predicate getGenericDefinition uses to choose the nominal arm — so the gate and the emission cannot disagree about which constraints are nominal |
| The bare-alias rule goes MODEL-INDEPENDENT | Behind the CS0315 stood one more error, and it was the xml-netip-alias lane’s own rule seen from the arm it had gated off: netip_test.cs spelling netip.AddrDetail for an alias export_test.go declares, CS0426. That fix required the white-box model, reasoning that only there does production live in another assembly. True, and beside the point — what makes the qualified spelling invalid is that a global using is a member of no class, which holds just as firmly when production is RECOMPILED in. Under recompile the alias is, if anything, more plainly in scope: production, internal and external are one compilation |
The second move needed a third, smaller one. testExternalVariant — “current variant is the external
<name>_test package” — was set only under the white-box model, so the recompile external half could
not tell it was the external half and the rule could not fire. Which variant is converting is a fact
about the SOURCES, not about the model; it is now set under every model. Nothing else reads it
outside the white-box path (whiteboxBridgeDeclaredType is reachable only through
testOwnedAdapterRef, which returns early unless the model is white-box reference), so widening it
moves exactly the one rule that needed it. The derivation was extracted to testVariantOptions so
the wiring itself is guardable — it is where this lane’s own first attempt failed, silently, with the
predicate correct and unreachable.
And a fourth, which is the same lesson again: the two models park the package-under-test’s path in
DIFFERENT fields. The reference models clear the self-import binding and retain it in
testProductionPath; a recompile conversion keeps the binding, so it stays in testPackagePath and
testProductionPath is EMPTY. The first version of the alias fix read testProductionPath alone,
answered empty under recompile, and did nothing — a fix that builds, ships, and is inert. The shared
accessor Options.packageUnderTestPath() is the remedy.
Blast radius, censused before the change — and it is net/netip alone
The model-selection gate can only fire where a _test.go file DECLARES an interface that is then
used as a type-parameter constraint. A parse of every package in the Go 1.23.1 source tree finds
that shape at four sites, total:
| Site | Fires? |
|---|---|
net/netip — netipTypeCmp (fuzz_test.go) |
yes — value arguments (Addr, AddrPort, Prefix), production-declared |
crypto/internal/nistec — nistPoint[P] |
no — POINTER arguments (*P224Point); the boxed form is the existing constraint-proxy machinery’s business, and the gate takes value named types only |
net/http — TBRun[T] |
no — pointer arguments, and *testing.T/*testing.B are FOREIGN types, not the package under test |
cmd/compile/internal/test — fooer |
no — cmd/ is not a converted package |
The bare-alias widening has its own radius, and it is empty on the roster: only two banked
packages are on the recompile model (crypto/ecdh, text/tabwriter) and NEITHER has an internal
test file at all — both are external-only suites that fell back from plain reference — so no
_test.go of the package under test declares anything for the rule to spell. The plain black-box
reference model cannot reach the arm for the same structural reason.
Both packages were re-converted as the control. The result is the interesting part: every
test-project artifact reproduces byte-identically — .tests.csproj, package_test_info.cs and
package_info_external_test.cs, which are exactly the files a moved model or a moved external-variant
flag would rewrite. What DID move is three files, all two already-documented phantom classes and
neither of them this lane’s: ecdh_test.cs and tabwriter_test.cs are CR-stripped IDENTICAL to
HEAD (the multi-line-literal CRLF phantom), and crypto/ecdh/package_init.cs gains the
initᴛᴛtests() hook as +7 REAL lines — the fourth -tests-closure shape CLAUDE.md names precisely
because it survives a numstat filter. All three restored.
0 of 267 → 210 of 267, and the residual is ONE root
267 Go verdicts, 21 excluded (20 benchmarks, 1 fuzz), 210 agree, 57 diverge. Every one of the 57 is an allocation assertion, and they collapse to three parents:
| Test | Rows | Assert | Reported |
|---|---|---|---|
TestNoAllocs/* |
47 | testing.AllocsPerRun(1000, f), want 0 |
1–10 |
TestAddrStringAllocs/* |
5 | want 1 | 2, 3, 106 |
TestParsePrefixAllocs/* |
2 | want 0 | 2 |
| (their three parent rows) | 3 | aggregate of the above | — |
Why NONE of it is disclosable — and the suite proves it against itself
The tempting move is alloc-count-semantics, the class io/strings/bytes established: Go’s
AllocsPerRun counts mallocs, the managed shim reported BYTES, so no allocation behavior could
satisfy a count assert. That class does not cover a single row here, for two independent reasons,
both measured rather than argued.
- Every one of the 54 leaf failures reports a COUNT, not bytes. The shim is no longer
byte-only: it charges golib’s own allocation sites — the structural mirror of Go’s
runtime.MemStats.Mallocs— and falls back to the byte figure only when bytes are nonzero and the count is zero. Zero rows here took that fallback. The unit mismatch the class names is simply not present. - The measurement reaches exact zero on this very suite, twice.
TestAddrStringAllocs/zeroandTestNoAllocs/IPv6Unspecifiedare want-ZERO alloc asserts that the C# side satisfies. The shim’s own contract is that zero bytes implies zero allocations exactly; two passing rows on the same code paths demonstrate the instrument can and does report the answer Go reports. A want-zero row that then reads 3 is reading three real allocations.
So the 49 want-zero rows are converted code allocating where Go does not, and the 5 want-one rows are
converted code allocating at least twice where Go allocates once (the counter is a documented LOWER
bound, so exceeding the want is one-directional evidence). That is an optimization target, not an
impossibility — precisely what the edwards25519 ruling this week refused to launder, and precisely
the distinction TestWriteStringAlloc was already held to (“the byte-derived shim CAN report 0, so
the unit-mismatch ruling does not cover it … a real divergence, not a disclosure”).
net/netip therefore does not bank. No roster row, no proof page, no badges, no disclosure
manifest, no committed test sources; the corpus is restored and this commit carries the converter
change alone.
What the row is worth now, and what it costs
It is one root away, and the root is named: netip’s allocation profile under conversion. The 106
readings for Addr.String() on IPv6 are the loud end and probably one shape (the expanded-form
builder), while the TestNoAllocs band at 1–10 is the broad end — 47 rows across constructors,
accessors and parsers. Nothing about it is structural: the suite builds, runs, and agrees on
everything that is not counting allocations. Ranked against the terminal path this is a .1-era
optimization arc, not a wall, and the row should be re-offered when one is commissioned.
Gates
Converter go test ./... ok, with four guards, each proven failing-first by neutering its own
fix — and the neuter of packageUnderTestPath fails the ALIAS guard too, which is the exact
inertness this lane paid for once:
| Guard | Pins |
|---|---|
TestNominalProductionConstraintForcesRecompile |
the gate, fed through constraintProxySigArg — the real instantiation path — with three negative controls: a test-declared argument, a production-declared constraint, and the recompile model itself |
TestTestDeclaredAliasSpelledBare |
now BOTH arms of the bare-alias rule; its recompile case models the real option shape (testPackagePath, not testProductionPath), because the reference models’ field would have passed for the wrong reason and pinned nothing |
TestVariantOptionsMarkExternalUnderEveryModel |
the external-variant flag under all three models, and the bridge overrides staying white-box-only |
TestPackageUnderTestPathFollowsTheModel |
the accessor, both fields and the production-conversion zero |
Full CNR byte-identical across all 629 behavioral packages, 0 NOT MEASURED, 2 advisory converter
warnings, solution-integrity and path-casing preflights OK · the two banked recompile-model packages
re-converted with every test-project artifact byte-identical (above) · six reflect-consumer canaries
re-swept green, recomputed from the roster at gate time (above). No golib change, no corpus regen, and
TestStdLibMetadataInSync passes untouched, so no go generate, go2cs.slnx or behavioral-suite
gate is owed.
⚠ One measurement was thrown away and re-run rather than reported, because it would have been a
false green of exactly the kind this repo catalogues: the first CNR built go2cs.exe at 21:53:42,
which fell INSIDE the window in which this lane was neutering its own fixes to prove the guards
failing-first. The binary under test was therefore not the binary being banked. The fixes are all
-tests-only paths and could not have changed a production transpile either way — which is precisely
why it would have passed, and precisely why it was not kept. Re-run from a rebuild at 21:58:02 with
the source final.
The canary sweeps, and three foreign movers proven foreign by CONTROL
All six pass at their exact banked counts: go/internal/gcimporter 583, go/types 557,
encoding/json 491, crypto/tls 400 + 2 disclosed, encoding/xml 386, html/template 243.
The tls row is worth naming: TestBogoSuite’s Go side flipped pass → fail on this host — shape
(b), the host asymmetry the host-conditional pin was built for — and the pin absorbed it, leaving the
row at 400 + 2 rather than red. That run’s go2cs_test_comparison.json was kept per the standing
opportunistic-capture instruction before the page was restored.
The sweeps left 37 modified corpus files: 32 CRLF phantoms and 5 with real content. Three are the
initᴛᴛtests() hook (encoding/xml, go/types, html/template package_init.cs). The other two
are not phantoms and were NOT assumed foreign — they were controlled:
| Mover | numstat | Verdict |
|---|---|---|
go/internal/gcimporter/gcimporter.cs |
1/1 | a PRODUCTION lambda gaining an explicit (@string, error) return type |
go/types/package_test_info.cs |
0/3 | three GoImplicitConv<T, ж<T>>(Indirect = true) records dropped — the go/types test-info regen-drop, R’s root |
Both reproduce byte-for-byte under the BASE converter with this lane’s changes stashed out, so
both are pre-existing corpus staleness rather than this arc’s. go/types also keeps its
package_info_internal_test.cs and an untouched .tests.csproj through the sweep — it is still on
the white-box model, which is the independent confirmation that the new gate never fired there, as
the four-site census says it cannot. All 37 restored; this commit carries no corpus change.
⚠ Process note on the canary derivation. CLAUDE.md says the five reflect-consumer canaries are
“recomputed from docs/ValidatedTestPackages.md at gate time, never carried forward”, and the
2026-08-19 derivation is recorded as go/internal/gcimporter 583, go/types 557, encoding/json
491, crypto/tls 402, encoding/xml 386. Recomputing it here, go/internal/gcimporter does not
import reflect anywhere — production or test. What it contains is the STRING "reflect.Value",
inside an expected-signature literal in gcimporter_test.go, which a text-grep derivation matches
and an import-based one does not. This lane did not resolve which reading was intended and swept the
UNION of both, since over-covering a canary set is free and under-covering it is what the rule exists
to prevent. Whoever owns the rule should pin the derivation itself; a rule that says “derive, never
remember” still has to say derive HOW.
RULING – canary derivation is by DIRECT IMPORT DECLARATION, never text-grep; and the record-drop scope judgment is RATIFIED (coordinator, 2026-08-20)
The canary gap the netip lane surfaced, closed. “The five largest banked reflect consumers BY
VERDICT, recomputed at gate time” now derives membership mechanically: a package is a reflect
consumer when reflect appears in the IMPORT DECLARATIONS of its production or test sources
(go list-shaped – Imports, TestImports, XTestImports), never by text search over file contents.
The two rejected readings each fail measurably: whole-file grep over-matches string DATA
(go/internal/gcimporter matches only "reflect.Value" inside expected-signature test fixtures
and imports reflect nowhere – data is not consumption); import-CLOSURE under-discriminates
(fmt sits in essentially every closure and imports reflect, so the qualifier degenerates to
“five largest rows, period” and loses the sensitivity it exists for). Direct-import is the middle
that tracks actual bridge exposure and is computable without judgment. Consequences: gcimporter
LEAVES the canary set unless a test of its own imports reflect; the set recomputes at gate time
as always; and when two defensible readings disagree mid-lane, sweeping the UNION – as the netip
lane did – is always acceptable. A superset canary sweep is never wrong; only a silently chosen
subset is.
The e61758549 scope judgment is ratified as delivered. The readme-rc4 lane judged the
whitebox-production exclusion one notch too wide (it reasons about hosting an operator, which the
pointer-boxing route never does) and restored rather than widened, with the both-foreign boundary
intact. The reasoning is adopted; nothing to overrule. Its three-package test-info bank
(crypto/tls, encoding/xml, html/template – born short inside the two-day window, each
re-validated at its banked count) is the correct application of the born-stale exception to the
restore rule, and go/types reproducing exactly is the control that makes it so.
The shape-(b) proof matrix is COMPLETE, twice over. Both laptops caught live Go-side BoGo
flaps within hours of each other: R’s during the tls test-info re-validation (fan-out reached,
3,242 children absorbed by the annotated root, zero one-sided, 400 + 2), G’s during the netip
canary sweep (same shape, same arithmetic). R banked the irreproducible comparison artifact at
docs/phase4/evidence-crypto.tls-shape-b.json. The opportunistic-capture standing instruction
has paid out in full; nothing about the pin remains unproven.
MEASUREMENT PASS (2026-08-20, lane claude/measure-queue-panic-cap) – six near-miss candidates repriced: net/mail VALIDATES at zero price, math/big is 224 verdicts behind one arc already commissioned
Ruling x2 left a fallback clause without a source: “if the recorder design overruns, the substitute comes from the ranked queue via the measurement pass, not from re-litigating the class.” This is that pass. No fixes were made and nothing was banked – a fix inside a measurement lane un-controls the measurement, and taking a row is the coordinator’s call, not a measurer’s.
Why these six, and why the queue needed re-measuring at all
The ranked near-miss census is dated 2026-08-16, and it is stale by construction: three of its own
“0 of N” rows have BANKED since (text/template 0 of 52, html/template 0 of 243, encoding/xml 0 of
386 are now rows 155-157). Selection was therefore “closest in that census, and not re-measured since”,
which is the board’s own standing lesson from the 23-package breadth bank – re-scout the tail after any
capability lands, not only the packages that capability names. Each was run -tests -test-action all,
artifact kept, tree restored.
The priced ranking
| package | rows | matched | divergent | residual root | class | price to bank |
|---|---|---|---|---|---|---|
net/mail |
11 | 11 | 0 | – | – | ZERO – the banking commit itself |
math/big |
226 | 224 | 2 | TestNewIntAllocs; TestMulUnbalanced |
alloc-count-semantics; open |
1 disclosure + the ReadMemStats arc (already commissioned) |
net/http/internal |
10 | 9 | 1 | TestChunkReaderAllocs 2/run vs 1 |
alloc-count-semantics |
1 disclosure, no converter work |
crypto/cipher |
14 | 13 | 1 | TestGCMAsm SKIPS where Go passes |
none of the five fits | a RULING first; then 0 or an arc |
debug/pe |
10 | 9 | 1 | reinterpret cannot re-length a managed array | not disclosable | rides the LAYOUT arc (commissioned) |
log |
9 | 7 | 2 | TestAll source position; TestDiscard 3/run vs 0 |
– ; alloc-count-semantics |
the position-map arc (commissioned) + 1 disclosure |
Excluded declarations are the ordinary deferred Example/Benchmark set in every row (math/big 67
benchmarks + 14 examples + 1 fuzz, crypto/cipher 9 + 10, log 5 + 2). One exclusion is NOT of that
kind and is named for the record: math/big’s TestCalibrate is excluded-disclosed as requiring
B.ResetTimer, a capability gap the harness already handles per-declaration.
The two that move the terminal path
net/mail validates 11 of 11, exit 0. It stood at 7 of 11 in the 2026-08-16 census, the four
TestAddress* rows infrastructure-erroring on System.ArgumentException: Indices low, high and max
represent a range outside bounds of the array reference – a three-index slice-bounds root. That root is
GONE. It was dissolved by an intervening arc and this lane does not attribute which one (attributing
it would need a bisect, and the measurement is the deliverable). Nothing is disclosed, nothing is owed:
the price is the banking commit.
math/big reproduces 224 of 226 exactly – the r58b pair, unmoved, on a third independent
measurement. It is by a wide margin the largest row available anywhere on the near-miss queue, and its
two residuals are NOT equal in price:
TestNewIntAllocs– 100 counted allocations over 100 runs = 1 object/run against Go’s want-zero. Textbookalloc-count-semantics, the shim’s own message verbatim. One manifest entry.TestMulUnbalanced–allocSizeratio 51 against Go’s bound of 10 (20,493,720 bytes over a 400,320-byte input). The test readsruntime.MemStats.TotalAllocaround one multiplication, and the managedReadMemStatsanswers that field TRUTHFULLY today –System.GC.GetTotalAllocatedBytes(precise: false)(runtime/managed_impl.cs). So this is NOT a stub row. Two candidate roots remain and this lane did not separate them:- the converted
nat.mulgenuinely allocates ~5x what Go’s does (temporaries in the Karatsuba path); GetTotalAllocatedBytes(precise: false)is PROCESS-WIDE and unsynchronized, so the window catches other threads’ allocations where Go’sGOMAXPROCS(1)window catches almost none.
The discriminator is cheap and it sits inside the ReadMemStats measurement-surface design Ruling B already commissioned for
runtime/debug’sHeapReleasedand pause history: make the read precise, then see whether the row still fails. If it does not,math/bigcosts ONE disclosure – 224 verdicts for a manifest entry, the best price-per-verdict on the board.- the converted
crypto/cipher is a CLASS question, not a defect – recorded for a ruling, not self-ruled
TestGCMAsm builds two AEADs, one via the assembly path and one generic, and skips itself when
reflect.TypeOf(asm) == reflect.TypeOf(generic) – Go’s own designed behavior on a platform with no
distinct assembly GCM. The converted corpus has exactly one GCM implementation, so the C# side takes the
skip branch the Go source defines. Go on windows/amd64 has the asm path, runs the body and passes.
The row is Go pass / C# skip: agreement with Go’s INTENT, disagreement with Go’s VERDICT.
Against the five classes: not host-limit (the host can do it), not alloc-count-semantics, not
chan-direction (retired), not codegen-liveness. runtime-capability REFUSES it by its own
admission test – a truthful managed second implementation exists at a cost, so it is an arc with a
price. But naming that arc is naming “build a second GCM whose only consumer is a differential test”,
which is a strange thing to buy. The honest options a coordinator has are: rule the source-defined
platform skip as skip-parity (price 0, row banks at 13 + 1 skip), or accept it as an arc. This lane
declines to choose.
debug/pe is the layout arc wearing different clothes
TestReadCOFFSymbolAuxInfo reads back COFFSymbolAuxFormat5._ – declared [3]uint8 in Go and emitted
correctly as new(3) – with 8 elements. Eight is exactly the length of the SOURCE struct’s
COFFSymbol.Name [8]uint8, and the site is
Ꮡsym.Reinterpret<COFFSymbol, COFFSymbolAuxFormat5>(). A managed array<T> is a REFERENCE carrying its
own length, so a reinterpret cannot re-length it and the destination field ends up holding the source’s
array. Go’s reinterpret is exact there (both structs are 18 bytes).
Same family as sync/atomic’s hammer rows – the C# struct is not the Go struct’s bytes – which
Ruling A commissioned as the zero-size-field LAYOUT EMISSION arc (explicit layout with Go-computed
offsets). Not disclosable: a truthful managed form exists, and it is that arc. debug/pe rides it, or
takes a one-file hand-own of its symbol reader.
The ranking, as a selection
If runtime/debug (#161) holds its slot at the price Ruling B set, nothing here displaces it. If it
overruns, the substitutes in order are:
net/mail– 11 verdicts, price zero, bankable the day someone takes it.net/http/internal– 9 + 1 disclosed, price one manifest entry in an existing class.math/big– 224 verdicts, the largest row on the queue, priced at one disclosure IF the ReadMemStats arc lands (which #161 is already buying). Taking #161 andmath/bigtogether is the highest-verdict pairing available.
log is the sleeper: at 7 of 9 its price is the position-map arc plus one disclosure, and that arc is
ALREADY a prerequisite of both #161 (runtime/debug) and #162 (flag). Whoever lands it should expect
log to fall out nearly free, and should re-measure log/slog (153 of 213 at last count) in the same
pass for the same reason.
What this lane did NOT do, stated plainly
- Banked nothing.
net/mailvalidates and its proof page was written by the run; the page and every test artifact were REMOVED, not committed. Banking is a row-taking act and the brief was the ranking. - Fixed nothing. Six packages measured against an unmodified converter, so every count above is comparable to every other count on this board.
- Did not attribute
net/mail’s recovery to a specific arc, and did not separateTestMulUnbalanced’s two candidate roots. Both are named above with their discriminators. - Did not re-measure the queue’s own rows (
encoding/gob,net/netip,sync/atomic,runtime/debug,flag) – three are in flight and two are freshly ruled.
Standing dirt, classified and RESTORED, nothing unclassified: math/big’s doc.cs/prime.cs (CRLF
phantoms, empty numstat) and its package_init.cs (+7 real lines, the initᴛᴛtests() hook – the fourth
-tests-closure shape CLAUDE.md names).
RULING – the terminal path is SELECTED from the measured ranking: net/mail banks now, log is #161, position-map is the linchpin arc (coordinator, 2026-08-21)
The measurement pass above is adopted as the selection instrument, and three of its judgments are
ratified on the way: the cap’s HEAD-KEEPING truncation (a disclosure signature pins the FIRST
failure by Contains – dropping the head would silently unpin disclosed rows roster-wide), the
panic record’s exemption from the aggregate cap, and the CNR skip (the branch carries zero
converter changes; CNR binds to converter changes, and GolibTests + the full go2cs.slnx build
are the gates a golib change owes).
The path to 162, selected:
| Row | Package | Price | Status |
|---|---|---|---|
| #158 | net/mail (11) |
ZERO – measured 11/11; a stale census said 7/11 and the root dissolved under an intervening arc | bank immediately |
| #159 | sync/atomic (108) |
token construction + zero-size layout arc (Ruling A) | lane in flight |
| #160 | runtime/debug (9) |
position-map + ReadMemStats design + 3-row runtime-capability disclosure (Ruling B) |
priced |
| #161 | log (9) |
position-map + one disclosure – the SLEEPER: its arc is already #160’s prerequisite | priced |
| #162 | flag |
ImplementGenerator Delta-rename fix (+ position-map exposure per the runtime-debug census) | rooted |
The position-map arc is the linchpin: its named consumer set (runtime/debug, log,
log/slog, flag) covers three of the five remaining rows. It is the highest-leverage single
arc left on the 1.23.1 campaign and should be staffed accordingly.
Reserves, ranked as measured: net/http/internal, then math/big – whose 2026-08-10
off-the-table status is SUPERSEDED by the fresh 224/226: once #160’s ReadMemStats arc lands, its
TestMulUnbalanced discriminator resolves and the row may cost one manifest entry for 224
verdicts – then gob-via-StructOf and netip-via-allocation.
Held for a future ruling, off the terminal path: crypto/cipher’s TestGCMAsm skip-parity
shape (agreement with Go’s intent, disagreement with its verdict, no class fits). Ruled when a
lane reaches it, not before.
🔁 flag RE-MEASURED at the MERGE RESULT — the Δ-rename root is LANDED, the row is 23 of 24, and its whole remaining price is the position-map arc at ONE miss, measured (2026-08-21, lane worktree-agent-a54643a27ddfc865c)
Briefed against the ROOTED, NOT TAKEN entry above as a live root to fix. It is not one any more,
and saying so precisely is most of this lane’s product: 7eeeda893 is an ancestor of master,
ImplementGenerator already carries ResolveForwardMemberName, and CollisionRenamedForwardTests
already guards both halves of it. What was genuinely missing is the reading nobody had taken — flag
at the union. claude/heavy-pair-7be2d2 measured 23 of 24 on its OWN tip; claude/edwards25519-a
measured master WITHOUT that tip and got the CS1929 ×10 wall back. Neither is a measurement of the
merge result, which is exactly the gap CLAUDE.md’s banked-row protection rule was written about.
The measurement
go2cs -tests -test-action all -test-timeout 10m "<GOROOT>/src/flag" src/core/flag, converter
rebuilt from HEAD first, in a worktree whose bin/obj came from no other tree.
| build | 0 errors — the CS1929 ×10 wall is gone at the UNION, not only on its lane tip |
| verdicts | 24 |
| matching | 23 |
| divergent | 1 — TestDefineAfterSet |
| skipped / disclosed | 0 / 0 |
| excluded | 6 Examples (Phase-4D) |
| status | failing — does NOT bank |
All five Δ-renamed flag.Value implementors the wall named (boolFlagVar, flagVar, interval,
URLValue, zeroPanicker) now compile and RUN: TestUserDefined, TestUserDefinedBool,
TestUserDefinedFunc, TestUserDefinedBoolFunc, TestUserDefinedBoolUsage and
TestUserDefinedForCommandLine all pass. That reproduces heavy-pair’s 23 of 24 exactly, on a tree
carrying every merge since — so the fix survived the union, which is the thing a lane-tip proof
cannot say.
The new fact — the residual is ONE miss now, and it is MEASURED rather than predicted
claude/edwards25519-a landed goSourcePath (Go spells source paths with forward slashes on every
platform; the CLR hands back the PDB’s backslashes) and predicted that with the separator half in,
“the arc’s remaining distance on flag is one miss, not two.” Nobody re-ran flag after it, because
from master flag did not build. The three strings side by side are the proof:
| string | |
|---|---|
| Go’s assertion | flag myFlag set at .*/flag_test.go:.* before being defined |
C# at 7eeeda893 (pre-goSourcePath, as recorded above) |
… set at C:\…\src\core\flag\flag_test.cs:1112 … |
| C# today | … set at D:/…/src/core/flag/flag_test.cs:1112 … |
The backslash form misses .*/ outright — it contains no / at all, so the regex fails before it
ever reaches the filename. Today’s form satisfies .*/ and then misses on exactly one token: .cs
where the assert wants .go. So the position-map arc’s entire remaining job on flag is the source
IDENTITY; the separator half of the two-half remedy is already paid, and the prediction is now a
measurement.
The doctrine applied mechanically — and it refuses the row, again
TestDefineAfterSet checked against all five ratified classes:
| class | admits? | why not |
|---|---|---|
alloc-profile |
no | no allocation is asserted |
alloc-count-semantics |
no | no count is asserted |
codegen-liveness |
no | nothing about collectibility |
host-limit |
no | its bar is a structural property of the DEPLOYMENT SHAPE that retires itself when the shape changes; a .cs file name does not retire that way |
runtime-capability |
no | its admission test is does a truthful managed implementation of the asserted behavior exist at any cost? — yes, and it is priced: #line or a per-package side-car. A priced arc is never a disclosure |
Same call heavy-pair made, re-derived rather than inherited. flag has no disclosable row and one
unfixed-here row, so it does not bank: no roster row, no proof page, no committed test sources.
Repricing — flag is the position-map arc’s CHEAPEST consumer and its cleanest acceptance test
The RULING above prices row #162 as “ImplementGenerator Delta-rename fix (+ position-map exposure)”.
The first half is spent. flag’s remaining price is the position-map arc and nothing else, which
makes it the only one of the arc’s four named consumers that banks on the arc ALONE:
| consumer | verdicts the arc buys | what else that consumer still needs to bank |
|---|---|---|
flag |
1 | nothing — 24/24, zero disclosures, zero other arcs |
log |
1 of its 2 | one disclosure |
runtime/debug |
5 of its 7 | the ReadMemStats measurement-surface design + the 3-row runtime-capability disclosure |
log/slog |
9 | 18 alloc-profile disclosures + the import-ordered-initialization arc |
That is worth more than one row to whoever staffs the arc: flag is its acceptance test. It is a
24-verdict suite whose single failing assertion is a bare regex over a file name, with every other
verdict already green and no disclosure manifest in the way — so the arc lands green on flag or it
does not land. The other three consumers cannot give that signal, because each of them would still be
red for reasons the arc does not own.
Independent verification of the guard, since the lane was here anyway
Charter §7, and cheap. Both failing-first claims in the 7eeeda893 entry above were re-derived on
this tree rather than taken on trust (GenTests now totals 26, grown by other lanes since):
| state | result |
|---|---|
| unmodified | 26/26 pass |
ResolveForwardMemberName neutered to return null |
exactly 3 fail — RenamedDeclarationIsResolvedThroughTheMarker, PointerAdapterImplementsTheInterfaceNameAndForwardsTheEmittedOne, RenamedValueReceiverStillResolvesItsReceiverExpression |
receiver lookup alone reverted to the Go name (ForwardReceivers[simpleMethodName]) |
exactly 1 fail — RenamedValueReceiverStillResolvesItsReceiverExpression |
Both neuters were reverted; the tree is byte-clean. The claim that the RECEIVER half is independently load-bearing holds — it is the half the original CS1929 diagnosis did not name.
For the next lane
- Do not re-take
flag’s root. It is landed, guarded, and verified at the union. TheROOTED, NOT TAKENentry above and the RULING’s#162price line are both SPENT as of this measurement. flagis a 23-of-24 row parked on ONE arc, and it is the arc’s cheapest and cleanest consumer. Whoever takes the position map should measureflagfirst and last.- ⚠ The generalizable trap this lane nearly re-paid: a brief naming a root is not evidence the root
is live.
git merge-base --is-ancestor <sha> HEADcosts nothing; edwards25519-a paid a pipeline run to learn it in one direction, and this lane would have paid a generator arc to learn it in the other. Read the OWNER of the named diagnosis (hereImplementGenerator) and grep it before designing.
Gates
None owed, and that is the honest accounting rather than a skip. No converter, golib, go2cs-gen,
corpus or test-source change was made — the diff is this board entry alone, and the two generator
neuters were reverted with a clean git status proven after each. Gates bind to change classes
(charter §5); a docs-only append changes none of them. The measurement’s own instrument — the
pipeline, run to completion, comparing against go test -json -count=1 — is reported above.
Standing dirt from the pipeline run, classified per CLAUDE.md and RESTORED, nothing unclassified:
src/core/flag/flag.cs (7/7 numstat — the -tests-closure alias shape: the wider test closure
collides os, so the alias emits Δos and the four references follow it; class 2) and
src/core/flag/package_init.cs (+7 real lines — the initᴛᴛtests() hook, the FOURTH -tests-closure
shape CLAUDE.md names). The nine untracked converted test artifacts were removed, not committed:
flag did not bank. No package_info.cs record moved, so no stdlib-metadata.txt regenerate is owed.
✅ BANKED – net/mail is roster row #158, 11 of 11, and it is the first bank to carry its own badge (2026-08-21, lane claude/net-mail-position-map)
The selection ruling put this row first at price ZERO. It banks at exactly that: no converter change, no golib change, no disclosure, no manifest. The pipeline run IS the whole change.
roster 157 / 215 (73.0%), 18,414 matching, 79 disclosed
-> 158 / 215 (73.5%), 18,425 matching, 79 disclosed (recomputed from the table itself)
The eleven verdicts are the RFC 5322 address-list grammar (quoted strings, comments, folding white
space), RFC 2047 encoded-words in both B and Q form including a custom WordDecoder, group syntax,
the obsolete and malformed matrix, Address.String() round-tripped back through the parser, and Date
header parsing with its CFWS forms. Three declarations are excluded-disclosed as the ordinary deferred
Example/Benchmark set.
What the stale census said, and what dissolved it
The 2026-08-16 near-miss census recorded net/mail at 7 of 11, the four TestAddress* rows
infrastructure-erroring on System.ArgumentException: Indices low, high and max represent a range
outside bounds of the array reference – a three-index slice-bounds root. The measurement pass found
all four passing, and this bank confirms it twice more (the run above, and the own-row sweep).
The root is not attributed, deliberately. It dissolved under an intervening arc, and naming which one would need a bisect across five weeks of merges to add nothing to the row. What the episode is evidence FOR is already board doctrine and is now paid out twice in eight days: re-scout the tail after any capability lands, not only the packages that capability names. A row sat one measurement away from free for an unknown number of weeks.
The badge levelled in its OWN run – the first bank that did
Measured, and worth recording because it retires a standing step every previous bank owed. Before the
pipeline: Tests-not_yet_validated-orange. After the SAME -test-action all invocation:
Tests-11%2F11_validated-brightgreen, linking validation/1.23.1.6/net.mail.html. That is
refreshPackageReadmeAfterProof (the badge lane’s second emission point) doing the thing it was built
for, on the first fresh bank to reach it. No hand-levelling, no follow-up reconvert, no board entry
owed to a later reader.
Gates
The bank carries zero code, so its gate is the pipeline plus the sweeps – and the canary set is derived at gate time by DIRECT IMPORT DECLARATION per the 2026-08-20 ruling, not remembered:
| Gate | Result |
|---|---|
-tests -test-action all (the bank’s own run) |
11 validated, 0 divergent, 3 excluded, 118 s |
own-row sweep -Filter net/mail |
PASS 11 |
canary go/types (557) |
PASS 557 |
canary encoding/json (491) |
PASS 491 |
canary crypto/tls (400 + 2) |
PASS 400 – and Go’s own BoGo baseline went red again on this run, the THIRD live shape-(b) capture; the annotated pin absorbed it and the row did not move |
canary encoding/xml (386) |
PASS 386 |
canary html/template (243) |
PASS 243 |
The derivation is worth showing once, because it reproduces the ruling’s own worked example: over the
158 banked rows, 62 declare a direct reflect import across Imports / TestImports / XTestImports,
and the five largest by verdict are go/types 557, encoding/json 491, crypto/tls 400,
encoding/xml 386, html/template 243. go/internal/gcimporter (583) is absent – the largest
banked row on the board, and correctly NOT a canary, exactly as the ruling predicted: it matches
"reflect.Value" only inside expected-signature test DATA and imports reflect nowhere.
Standing dirt classified and RESTORED, nothing unclassified: net/mail’s message.cs (CRLF phantom,
empty numstat) from the bank’s own run, and the canary packages’ *_test.cs phantoms from the sweeps.
⚠ THE POSITION-MAP ARC OPENS: the FILE half is written and measured – TestStack goes 5 misses to 1 – and it is NOT banked, because the same measurements produce THREE questions this lane will not self-rule (2026-08-21, lane claude/net-mail-position-map)
The linchpin arc, taken to the point where the decisions are decidable on evidence rather than argued. Everything below was measured on this box against Go controls; the implementation is ~45 lines in one hand-own and is described precisely enough to re-write in minutes.
The identity source: there is nothing to record. It is already derived.
The handoff priced this as needing a recorded Go-file identity. It does not. goFrameName ALREADY
derives the import path at run time and has been shipping it since the receiver half:
// "go.runtime.debug_package" -> "runtime/debug"
string importPath = typeName[3..packageSuffix].Replace('.', '/');
So the file half is that same derivation applied one field over: <importPath>/<stem>.go, where the
.cs stem IS the Go stem by construction (the converter emits <name>.go as <name>.cs, whole-file
hand-owns included – crypto/subtle/xor_generic.cs). No converter change, no corpus change, no new
artifact, no recorded attribute. The derivation was EXTRACTED (goImportPath) so a frame’s function
and its file read the same one: Go’s traceback pairs them, and two independent derivations could
disagree about which package a frame belongs to while each looked right alone.
Two shapes deliberately keep their .NET path, on the principle goFrameName already states – a frame
that is not converted Go code must not claim to be: a non-converted package class (golib, the BCL, the
host), and a *_impl.cs hand-own COMPANION, which supplements a package rather than replacing a Go file
(runtime/managed_impl.go does not exist, and answering it would fabricate a source rather than
translate one).
Two derivation rules, both found BY measurement, neither in the handoff
A TEST variant’s files live in the package-under-test’s DIRECTORY, so a test frame’s file and its
function name different things – and Go’s own TestStack asserts exactly that pairing:
frame("runtime/debug/stack_test.go", "runtime/debug_test.T.method"). The function keeps _test; the
file does not. Both variants carry a class suffix the directory does not, and the longer is stripped
first because it ends with the shorter:
| class | first measured | correct |
|---|---|---|
go.runtime.debug_test_package |
runtime/debug_test/stack_test.go |
runtime/debug/stack_test.go |
go.log_internal_test_package |
log_internal/log_test.go |
log/log_test.go |
The first cost three of TestStack’s four converted-frame assertions; the second, log’s long-file form.
Both are now right.
The acceptance test: 5 misses to 1, and the 1 is a question
runtime/debug’s TestStack, same pipeline, measured before and after:
before 5 file-prefix misses (all five code checks already passed)
after 1 -- and it is the HOST frame:
in line "\tC:/…/src/core/testing/TestExecution.cs:593", expected prefix "\ttesting/testing.go"
Every converted frame now names Go’s file exactly. The verdict does NOT move (2 of 9), because the test fails if any assertion fails – so the acceptance test cannot close without ruling question 2 below.
The yield signal: the file lands exactly, and log still does not move
log’s TestAll, measured after both derivation rules:
want ^.*/[A-Za-z0-9_\-]+\.go:(63|65): hello 23 world$
got log/log_test.go:69: hello 23 world (long form -- file EXACTLY Go's)
got log_test.go:69: hello 23 world (Lshortfile -- file EXACTLY Go's)
log stays 7 of 9. The file half is complete and it is not the yield; the LINE is.
Question 1 – the file half is NOT independently shippable, because it makes a MIXED position
The settled doctrine is written in the hand-own’s own header: “Frame.File/Line name the CONVERTED .cs
source, the source that honestly exists” and “Its file/line are therefore the converted .cs position,
NOT the .go one”. Today log_test.cs:69 is a real position in the tree the program was compiled from.
With the file half alone, log/log_test.go:69 is a position in neither tree: Go’s file, C#’s line.
The line was already not Go’s, so the change does not introduce line dishonesty – it trades internal
consistency for Go shape. That trade is the doctrine call, and the honest options are: accept the mixed
position as an interim; answer :0 (Go’s own unknown-line sentinel – honest, but it discards a
diagnostic that works today); or hold the file half until the line half lands, i.e. treat the position
map as INDIVISIBLE. This lane recommends the third and did not act on it.
Question 2 – may the hand-owned host claim testing/testing.go?
The last TestStack assertion expects the testing frame to name testing/testing.go. Our host is
hand-written C# in go.testing_runtime, and it deliberately declines Go identity – goFrameName’s own
text: “A frame that is not converted Go code (golib, the BCL, the test host) keeps its .NET name –
inventing a Go name for it would be a lie.”
The counter-argument is real: the host IS the testing package, hand-implemented, and a Go program
reading a traceback expects testing/testing.go there. That is the same shape as Ruling A’s alignment
token – truth read from what the model genuinely is, versus a property fabricated. It is one line in
the host to claim it, and it is the ONLY thing between the current state and the acceptance test.
Question 3 – the trimpath form is a property of the CORPUS, not of the runtime (measured regression)
The sharpest finding, and the reason nothing is banked. TestStack expects the bare -trimpath form
because the converted runtime.GOROOT() answers empty. But applying that form uniformly makes a
converted USER program diverge from Go where it currently agrees. Measured on the RuntimeCallerFrames
behavioral program, both binaries, same box:
| Go | C# with the file half | |
|---|---|---|
runtime.Caller file |
C:/…/RuntimeCallerFrames/main.go |
main/main.go |
| file is rooted | true | false |
Go answers an ABSOLUTE path for an ordinarily-built program; the trimpath form is a property of how
GOROOT packages are built, which go2cs has no notion of. So the transform cannot be applied uniformly,
and the runtime has no honest discriminator for “is this frame a GOROOT package” – the candidates are a
build-path convention (…/core/…, true for the corpus, silent for everything else) or a recorded marker
(a real emission change, and the thing this design otherwise avoids entirely).
The guard gap this exposed, and the probe that closes it
RuntimeCallerFrames passed all four phases with the change in – Output included. Its five
file-related assertions are separator BOOLEANS and an equality, every one of which is invariant under a
wholesale change of what the file names. A guard over a string property must assert the property, not a
predicate that survives it. The two-line probe that exposes the divergence is in the measurement above
(print the file, and whether it is rooted); it belongs in the guard whichever way question 3 is ruled.
Blast radius, recomputed – and the stdlib half of it is clean
A verdict can only move if something READS a frame file. Recomputed over the 158-row roster (the board’s
census said 11; it predates internal/reflectlite joining):
| reads a frame file | rows | verdicts |
|---|---|---|
own _test.go |
6 – encoding/json 491, io 60, context 57, sync 44, encoding/base64 17, log/slog/internal/benchmarks 3 |
672 |
| converted PRODUCTION code | 6 – go/types 557, database/sql 137, os/exec 74, internal/fuzz 52, internal/reflectlite 30, testing/slogtest 17 |
867 |
Swept with the file half IN: 20 distinct rows, 1,705 verdicts, zero failures – the 12 above plus 8
the substring filters pulled in free (bufio 80, io/ioutil 28, testing/iotest 18, io/fs 18,
internal/saferio 17, go/version 3, database/sql/driver 1, os/exec/internal/fdtest 1). The
stdlib blast radius is EMPTY. Every row that reads a frame file reads it for “where am I”, not for a
Go-tree property – which is what makes question 3 the whole decision: the change costs the corpus
nothing and costs converted USER programs their agreement with Go.
What this lane did NOT do
- Did not bank the code. Question 3 is a measured regression on the
-recurseproduct path, so shipping the file half uniformly would trade a stdlib row that does not move for user programs that do. The tree is clean; the implementation isgoImportPath+goSourceFileinruntime/managed_impl.cs, both call sites (appendGoFrames,internCallerFrame), and it is re-writable from this entry. - Did not touch the line half. Its two routes are still the board’s:
#line(PDB transport, +28-47% lines, CS diagnostics relocate onto.gofiles not in the project) and the side-car (a file and a csproj item per package). Both are corpus-wide emission changes – already flagged as coordinator territory – and question 1 says the file half should ride WITH whichever lands, not ahead of it. - Did not re-rule the host. Question 2 is one line and it is not this lane’s line.
Standing dirt from the arc’s measurement runs, classified and RESTORED
Six pipeline runs and thirteen sweeps moved nothing that was not already a named family: the
-tests-closure production re-emissions (bufio/{bufio,scan}.cs, internal/reflectlite/{swapper,type,
value}.cs – both in the sweep’s own $closureFiles), the initᴛᴛtests() hook (+7, go/types and
internal/fuzz), CRLF phantoms across every swept package, and os/exec’s proof page (date/commit plus
skip 8 -> 7, which encodes this host’s environment exactly as the archive/tar symlink precedent
does; the row PASSED 74 either way).
Two are one-line REAL movers and both are the born-stale class, restored rather than levelled per the
standing rule: encoding/base64/base64_test.cs gains a directional channel field’s .RecvOnly
initializer (the cargo-recv emission, which postdates that bank), and
database/sql/driver/package_test_info.cs gains global using Value = object; (the recorded
base64/base32/fmt alias-block staleness). Both level at their own rebank, not here.
RULING – the position map is INDIVISIBLE and build-shape-faithful; the host never claims testing/testing.go, and that premise becomes a host-limit entry (coordinator, 2026-08-21)
The net-mail-position-map lane’s three measured questions, ruled. The governing principle for all of them: no fabricated positions – a reported file:line pair must exist in the tree the file names, and every identity a frame reports must be a conversion-time FACT, never a plausible composite.
-
Indivisible – ratified as recommended. The file half alone mints
log/log_test.go:69, a position in neither tree. File and line ship together or not at all. The line half’s content is fixed by the principle – the reported line is the GO source line, derived from conversion-time facts (the converter knows every emitted statement’s Go position) – and its MECHANISM (per-file line table, frame-record side channel, or otherwise) is the arc’s design to make, reviewed per charter SS7 before implementation. -
The host’s doctrine stands: it never claims
testing/testing.go. A hand-own with no conversion relationship to Go’s source cannot honestly report a position in it – the line would be fabricated even where the file name is suggestive.TestStack’s fifth-frame assert rests on the premise that the testing framework’s frames come fromtesting/testing.go, which is a property of the test BINARY that the converted deployment shape structurally is not (the hand-owned host IS the design, per the ONE-testing-package ruling). That ishost-limit’s own text, and the assert becomes a signature-pinned host-limit entry whenruntime/debugbanks – with its retirement path named honestly: structural and permanent, unlike the os/exec relocatability entries, it does NOT retire at the .NET 10 single-file host, and the entry must say so. -
The identity form is BUILD-SHAPE-FAITHFUL – the lane’s measured
-recurseregression is ruled out of any landing shape. Go bakes the path at compile time: GOROOT-relative under the toolchain’s trimming for published stdlib, absolute for an ordinary untrimmed user build. go2cs reproduces exactly that, at CONVERSION time: the stdlib corpus reports the trimpath/import-path form (runtime/debug/stack.go– the truthful description of a package published without local paths), and a-recurseuser module reports what Go would have baked for the same build – the absolute source path as of conversion, recorded per file.main/main.gowhere Go answersC:/.../main.gois a divergence and does not land. -
The
RuntimeCallerFramesguard is blind to file identity (separator booleans pass under a wholesale identity change – the lane proved it by running the change through all four phases). Queued: strengthen it to assert real file identity alongside the arc, so the arc’s own guard is the one that would have caught this.
Consequences for the terminal path: flag (#162) banks under the indivisible pairing (its one
miss is the file token; a Go-mapped line satisfies its regex); log (#161) needs the full pairing
(its residual after the file half IS the line); runtime/debug (#160) takes the fifth-frame
host-limit entry alongside its runtime-capability disclosures. The arc is now fully specified:
one design increment, three consuming rows, acceptance measured on flag first and last.
MEASURED + RULED – the SS5.4 discriminator fires for ROOT (1): math/big’s TotalAlloc row is real over-allocation at 50.9x Go, and the row REROUTES to the zh-box arc (coordinator harvest of the S0/S1 lane, 2026-08-21)
The ReadMemStats S0/S1 measurement stage is merged (design doc SS7.1, ten subsections; GolibTests 191/191 on the lane’s tree; probes committed). The decisive number: T = P to within 40 bytes across six windows, with Go’s same-machine baseline at 1.01x its own bound – the converted path allocates at 50.9x Go, and the process-wide-counter hypothesis is dead. Consequences:
math/bigLEAVES the near-term reserves. The measurement pass’s “one manifest entry for 224 verdicts once #160 lands” is superseded: clearing the 10x bound needs ~80% of the converted path’s allocation removed, which is zh-box/B’ constituency – the 1.23.12-era arc, exactly whereedwards25519already routes.net/http/internalmoves up one in the reserve ranking.- All six ratified OQ recommendations SURVIVE with numbers (OQ-2’s literal cumulative form
measured drifting ~33.6 MB per release/reacquire cycle while the high-water form falls to 0 as
Go’s field does; recorder overhead below the noise floor; the OQ-6 contingency does not arise
–
TestFreeOSMemorycloses on both assertions under SS4.1). - The four SS7.1.9 refinements are RATIFIED, two of them binding on S2/S3: (1)
ReadMemStatsallocates 288 B/call TODAY (the GCMemoryInfoData box), masked lumpily into 25% ofnet/textproto’s bracket budget – the allocation-free precondition is work S2/S3 must DO, guard pinned at a 320 B ceiling with zero the target; (4) the recorder must verifyGeneration == MaxGenerationon the memory-info read – the defaultGCKind.Anywas measured moving off gen2 after one forced gen0. - The SS7.1.10 Debug-liveness measurement trap (a frame keeps its dead temporary live; the probe now carries a live-bytes control that prints INVALID instead of a number) is the same family as the charter’s verify-your-verification rule and is worth reading before writing any GC probe.
⚠ RULING A LANDS BOTH HALVES — TestAutoAligned64 + TestHammer32/64 close; sync/atomic reads 107 of 108 and does NOT bank, and the layout arc’s whole stdlib constituency turns out to be ONE struct (2026-08-21, lane claude/atomic-align-layout)
Ruling A commissioned two halves against sync/atomic: the identity token gains alignment-truthful
construction (1 row), and the zero-size-field layout emission arc closes the hammer family (3 rows).
Both are implemented and gated. Three of the four rows close. The fourth,
TestHammerStoreLoad, is the row the ruling itself queued to R.
Half 1 — the token becomes layout-truthful
ж<T>.PointerOrderToken now mints every allocation base 8-ALIGNED (the identity hash lifted clear of
the low 32 bits) and derives a field’s token as base + the field’s Go offset, read from the SAME
memoized GoFieldOffsets walk that answers StructField.Offset. TestAutoAligned64 asserts
Offset == 8 and ptr&7 == 0 about one struct; those are now one answer from one source rather than
two that happen to agree.
Both construction paths reach it with the same (struct type, field name) pair, so they agree by
derivation rather than coincidence: converted code arrives under go2cs-gen’s generated Ꮡ<field>
accessor, reflect’s FieldAliasBox under a DynamicMethod named goref_<field>. The reflect prefix
became a named constant so the pairing is a stated contract between siblings, not a coincidence of
two string literals.
Two decisions on the record rather than left for the next reader.
&sand&s.field@0now token ALIKE. The distinctness measurement the ruling required caught this on its first run — as a FAILURE, of an assertion this lane had written wrong. Go saysunsafe.Pointer(&s) == unsafe.Pointer(&s.a)whenais at offset 0, so tokening alike is the layout truth arriving, not a collision. The guard now pins that relation explicitly, with the wrong version described in its own comment so nobody “restores” it.- The ARRAY-ELEMENT arm keeps a raw element index below its (8-aligned) base rather than the
index scaled by element size, so
&s[1]of a[]int64does not answer 8-aligned. Knowing limit: the ruling asks for field tokens, no test asserts element alignment, and scaling would reorder every pointer-keyed mapinternal/fmtsortprints.
Half 2 — and the mechanism the ruling names has a sharp edge
Reinterpret’s size guard admits an alias only when SizeOf(TDst) <= SizeOf(T). Go’s
atomic.Int32 is struct{ _ noCopy; v int32 } — four bytes, v at offset 0 — while the naive C#
surrogate is eight, because a C# field always occupies at least one byte. So the hammer family’s
(*Int32)(unsafe.Pointer(uaddr)) over a *uint32 was refused at 8 > 4, the atomics acted on a
DETACHED COPY, and loads answered zero. Explicit layout at Go’s own offsets restores the size and
with it the alias.
C# has no zero-size struct, and that is not a detail. A zero-size field laid out at Go’s offset
SHARES bytes with the field Go puts there, and assigning it writes its one C# byte over the
neighbour — measured, 42 -> 0. Go’s write writes nothing. The remedy is readonly: the field stays
DECLARED, so reflect’s walk, NumField() and StructField.Offset still match Go, while the one
unfaithful operation becomes unexpressible rather than merely unlikely.
That remedy exposed a latent go2cs-gen defect. TypeGenerator skipped the Ꮡ_ accessor for a blank
field but not for the converter’s UNIQUIFIED repeats (__, ___), so it emitted a writable-ref
accessor for a field Go says has no address. Inert until the field went readonly, then CS8160. The
skip now covers the whole all-underscores blank family — which is what the rule always meant, and
what GoReflect.FieldAccess’s own header already documents as one Go concept.
The eligible population: 90 structs in, ONE struct out
Every narrowing below is a measurement, and two of them are corrections to this lane’s own first answer:
| Census step | Count | Why |
|---|---|---|
| stdlib structs with >=1 zero-size field | 90 | go/types over std, gc/amd64 sizes |
| minus MANAGED | 63 excluded | .NET forbids overlapping a managed reference with anything — the overlap is a TypeLoadException, not a layout |
minus array<T>-bearing |
— | golib’s array<T> is a struct over a SHARED T[] backing, so it is managed whatever its element is (this lane’s first census got this wrong and counted 31 eligible; the corrected number is 27) |
| minus EMBEDDED fields | — | an embed’s C# storage may be a ж<T> box, which a GO-type walk cannot see; the honest classification is “unknown”, and unknown leaves the arc. Costs runtime.mutex, sync.WaitGroup, internal/fuzz.pcgRand |
| minus NAMED zero-size fields | — | Go gives &s.pad an address, so go2cs-gen emits a writable accessor and readonly is CS8160 (found by CNR on ReflectStructTagCopy, whose layout carries pad empty). Blank fields have no address and no accessor, so only they take the readonly form |
| minus Go-size 0 | 9 excluded | C# has no zero-byte struct; Size = 0 means “natural size”. Emitting nothing is truthful, faking 1 is not |
What survives, measured by a seeded whole-corpus reconvert: one struct, runtime.gcBits
(runtime/windows/mheap.cs, Size = 1). sync/atomic’s six are hand-owned, so the converter never
emits them and the rows are closed by that hand edit. The converter emission is correct and general;
its constituency today is one struct, and saying “corpus-wide emission arc” without that number
would imply a sweep the corpus does not contain.
The arithmetic: 107 of 108, and the last row is R’s
Full-pipeline on this host, sync/atomic reads 35 recorded of 108 both before and after — the
host dies inside TestHammerStoreLoad and the cap on what gets RECORDED is not something this arc
can move. So the yield is stated by the row-harvest-3 containment method (host run directly, the
killing row excluded, diffed against the recorded go map), which is how the historical 99 and 104
were composed:
| pre-arc | post-arc | |
|---|---|---|
TestAutoAligned64 |
fail | pass |
TestHammer32 / TestHammer64 |
C#="fail" (the board’s “largest residual”: unsafe.Pointer Reinterpret write-back) |
pass both sides |
containment, TestHammerStoreLoad excluded |
— | 107 C# verdicts, 107 agree, 0 divergent |
TestHammerStoreLoad |
host death, 367 MB record | still fails; record 153 MB |
sync/atomic does NOT bank. The roster bar is that every Test matches, and one does not. The
residual is also NOT disclosable: runtime-capability’s admission test asks whether a truthful
managed implementation exists at any cost, and for a serialization cap the answer is plainly yes —
which is why the ruling queued it to R rather than admitting it.
⚠ This entry’s containment number predates the merge of R’s RecordGoroutinePanic cap
(05133488c), which this lane’s base did not carry. With the cap bounding the serialization death
AND the layout fix closing the lost writes the storm fed on, TestHammerStoreLoad may now record an
honest verdict; that re-measure is this lane’s immediate next step on the real union, and the row
either reaches 108 and banks, or fails honestly at 107 and waits on its own root.
Gates
Converter go test ./... ok, 145.7 s · GolibTests 185/185, including nine new guards
(four pinning the token construction and its distinctness, five the layout mechanism — the size, the
alias admitting, the measured clobber, and the readonly remedy) · full CNR clean but for this
lane’s own two artifacts, each green on all four phases: the new ZeroSizeFieldLayout behavioral
test and UnexportedEmbeddedMarker, whose golden was re-baselined for an intended change ·
go2cs.slnx Debug --no-incremental 0 errors (owed: golib changed) · seeded whole-corpus
reconvert exit 0, marker gate 63 marked / 0 violations, 1,662 emitted, 1,653 identical, 9 changed,
0 new — and 8 of the 9 are documented FOREIGN carries (gcimporter.cs, row-harvest-2’s; the seven
runtime unsafe.Pointer box-compare sites row-harvest-3 flagged for the next leveling regen),
leaving runtime/windows/mheap.cs as the only mover of this lane’s making · runtime built with
that one stamp applied, 0 errors, then restored. No corpus regen is banked here.
⚠ Three measurement failures this lane paid for, all its own, all recorded because each read
exactly like a real finding. (1) A corpus reconvert piped the converter through
Select-Object -First 10, which terminates the pipeline and KILLS the child: exit=-1 at ~71 of
304, reading precisely like a converter crash. CLAUDE.md records this trap, and this lane had cited
it hours earlier before using it anyway. (2) The replacement run seeded with PowerShell’s
Copy-Item -Recurse, which did NOT reproduce the corpus’s L3 per-GOOS folders — so every
platform-varying file emitted FLAT, producing 19 marker violations and 186 “NEW” files, which reads
like a catastrophic converter regression and is in fact a failed seed. The marker gate is what
caught it, which is the entire reason the ritual runs it before any conclusion. Seeding via tar
and verifying the count exactly (3,238 = 3,238) fixed it. (3) The first draft of the token
distinctness guard asserted something Go does not promise and failed correctly; the assertion was
wrong, not the code.
📉 RE-MEASURED on the union with R’s cap — sync/atomic is 107 of 108 FULL-PIPELINE (35 recorded → 108), and TestHammerStoreLoad’s residual is proven FOREIGN to the alignment arc by control (2026-08-21, lane claude/atomic-align-layout)
The entry above closed three of Ruling A’s four rows and stated its own gap: its containment number
predated R’s RecordGoroutinePanic cap (05133488c), so the fourth row could not be measured
honestly. The cap is now merged, this lane merged onto aaacb1e40, and the re-measure resolves the
fork the ruling left open.
The cap changes the measurement, not the verdict
| before the cap | with the cap | |
|---|---|---|
| C# verdicts RECORDED | 35 of 108 | 108 of 108 |
| how the run ended | host died inside TestHammerStoreLoad; 367 MB record failed to serialize |
clean; row contained as one per-test infrastructure-error |
| comparison artifact | 69–367 MB | 1.1 MB |
agreeing with go test |
not computable full-pipeline (107 by containment) | 107 of 108, FULL-PIPELINE |
So the 107 is no longer a containment composition — it is every verdict the differential harness
itself recorded, directly comparable to row-harvest-3’s 104 rather than composed differently. The
three rows this arc closed (TestAutoAligned64, TestHammer32, TestHammer64) hold on the union.
⚠ A correction to this lane’s own first reading of that file: an initial divergence count of ZERO was
a bad regex — it matched C#="[a-z]*" and the one divergence is C#="infrastructure-error", which
carries a hyphen. The count is 1, not 0.
The fork resolves the second way: the storm was NOT a consequence of the lost writes
The ruling’s re-measure question was whether TestHammerStoreLoad’s Fatalf storm was downstream of
the pre-fix detached-copy writes — in which case the layout fix would close it and the row would read
- It does not. With
TestHammer32/64fixed and passing, the pointer hammer still fails, now bounded and legible:
Pointer: %!x(uintptr=0) != %!x(uintptr=7864)
testing: FailNow called from a goroutine other than the test goroutine for TestHammerStoreLoad
hammerStoreLoadPointer treats the pointer VALUE as a packed pair and asserts
v & 0xFFFFFFFF == v >> 32. The observed v is N << 32 — low half zero, high half a small counter
— so the uintptr → unsafe.Pointer → uintptr round-trip is not preserving the number the test
stored.
That shape is NOT this arc’s, and the cap is what finally allowed the control
N << 32 resembles this arc’s new allocation-base format (identityHash << 32) closely enough that
attribution could not be assumed. The control had been impossible: at master the record was 367 MB
and never serialized, so the failure TEXT did not exist to compare. Under the cap it does.
Control run: golib reverted to the union’s version (no token change; the layout fix and the cap both retained), same single test, same host. Result — the identical shape:
Pointer: %!x(uintptr=0) != %!x(uintptr=12768)
Both sides also drop the same order of records (15.4M control vs 15.8M with the arc), so neither the
token construction nor the layout fix feeds the storm. The residual predates this arc entirely
and belongs to the unsafe.Pointer round-trip, not to the ж token or to zero-size layout.
Verdict: no roster row, and the residual is ROOTED not disclosed
sync/atomic stays off the roster at 107 of 108 — the bar is that every Test matches. The residual
is not disclosable: runtime-capability’s admission test asks whether a truthful managed
implementation exists at any cost, and a faithful uintptr ↔ unsafe.Pointer round-trip plainly has
one. It is an arc with a price, and the price is now much better specified than “host death”: one
named test, one named invariant (v & 0xFFFFFFFF == v >> 32 after a round-trip through
unsafe.Pointer), a bounded 1.1 MB artifact, and 107 of 108 sitting behind it.
Roster arithmetic is therefore UNCHANGED at 158 / 215 · 18,425 · 79. The row is one root away and the root is named; it waits on that root, not on ceremony.
Gates for the merge result
The (b) commit’s gates stand as recorded. On the merge result: golib and the capped
src/core/testing host both build clean, and the full sync/atomic pipeline runs to completion —
which is itself the merge-result evidence, since a banking merge owes its own row’s re-proof and this
lane’s row declines to bank. The corpus is restored: the -tests artifacts are removed and
type.cs.auto — the review sibling a -tests run refreshes and an overlay does not — is restored
rather than banked, per CleanupBacklog item 18.
✅ BANKED — sync/atomic validates 108/108 + 0 as row #159; the residual was the uintptr → unsafe.Pointer → uintptr round-trip, and closing it closes a GC-invisible dangling-reference hazard with it (2026-08-21, lane claude/unsafe-pointer-roundtrip)
The atomic-align-layout entries left this row at 107 of 108 full-pipeline with the residual rooted:
TestHammerStoreLoad failing Pointer: 0 != N (v = N << 32), proven FOREIGN to the alignment arc
by control, invariant named — v & 0xFFFFFFFF == v >> 32 must survive the round-trip. This lane
closed that root. The re-run validates: 108 go / 108 C# / 108 matched / 0 disclosed / 0 errors,
status: validated, with TestAutoAligned64, TestHammer32, TestHammer64 and
TestHammerStoreLoad all passing both sides in one run.
The mechanism, exactly
TestHammerStoreLoad reinterprets one shared uint64 as *unsafe.Pointer (and as
*atomic.Pointer[byte]) and hammers fabricated pointer VALUES through the atomic entry points —
Go’s own comment: “write barriers on values that aren’t real pointers.” Emitted, the reinterpret
is (ж<@unsafe.Pointer>)(uintptr)(paddr): a native-backed box whose pointee is a managed
CLASS. For such a box ref addr.Value is Unsafe.AsRef<Pointer>((void*)A) — the slot
reinterpreted as a CLR reference slot. Two consequences, each sufficient alone:
- The number never enters the slot.
StorePointerwrote the Pointer BOX’s reference; the stored value lived one indirection away, in an object nothing traced. - The reference is invisible to the GC. The slot is pinned
uint64[]storage — never scanned — so the stored box is collectible the moment the store returns, and the next load resurrects a dangling reference. Under the loop’s own allocation pressure gen0 recycles within milliseconds; the storm’s orderlyvhi − vlo = Darithmetic is one corrupt read seeding otherwise-healthy increments. (Go’sdebug.SetGCPercent(-1)cannot protect a managed host, and now nothing needs it to.)
The fix: the slot holds the pointer’s VALUE
Go’s semantics for that memory are unambiguous — the 8 bytes hold the pointer’s value. The managed
model cannot express that through ref Value (no ref Pointer can alias a number slot), so every
ACCESS converts number ↔ box at the entry point, where the pointee type is known:
| Layer | Change |
|---|---|
golib ж<T> |
IsNative/NativeAddress go PUBLIC as the discriminator (the seam documented the same way ManagedPointerTokens is — a runtime seam, not a Go surface), plus three atomic pointer-word accessors (ReadPointerWord, ExchangePointerWord, CompareExchangePointerWord). The one unsafe seam stays in golib: converter-emitted csproj compiles with AllowUnsafeBlocks=false, so the consumers could not host it |
sync/atomic non-generic (doc_impl.cs) |
LoadPointer/StorePointer/SwapPointer/CompareAndSwapPointer branch on IsNative: word ops on the slot, (uintptr)p reading a Pointer’s number nil-safely, new Pointer(n) marking the zero address nil — so nil round-trips exactly |
sync/atomic Pointer<T> methods (type.cs, hand-owned) |
Load/Store/Swap/CompareAndSwap branch identically; the mint (ж<T>)(uintptr)n is the same conversion the emitted reinterpret itself uses. Managed arms all unchanged |
CAS on the native arm compares NUMBERS — which is Go’s comparison for unsafe.Pointer and the
answer Pointer.Equals already gives, so no comparison surface moved.
The Fable-class check the assignment demanded: none of the three settled things bends
FINDING-managed-box-uintptr-lifetime— untouched. No token is minted differently, no lifetime rule changes; the fix is about SLOT CONTENT below the token layer. The uintptr → ж conversion (token resolve, then native box) is exactly as documented; this arc consumes it.- The alignment-truthful token construction — untouched, and re-proven in the same run
(
TestAutoAligned64green beside the hammer rows). Pointer.PointerOrderTokenequality/hash/ordering across its ~875 mint sites — untouched: no member ofPointerchanged at all.
No doctrine amendment was needed; the fix lands BELOW the doctrine, at the boundary where raw memory meets the managed pointer model. The write-up ratified in advance: the entry stands on measurements only.
Guards, proven failing-first
GolibTests.NativePointerSlotAtomicsTests, six tests: the slot-content assert (store, then read the
aliased storage back through its own managed box — DETERMINISTIC, no collector timing), the
across-a-collection round-trip, Swap/CAS by number, nil as the zero word, the Pointer<T> method
arm, and a single-threaded mini-hammer with forced collections. Neutered (the eight entry-point
arms stashed; golib accessors left in place), 4 of 6 fail — the four deterministic ones. The two
GC-shaped ones pass neutered because the test’s own locals root the stored box — recorded here so
nobody reads them as the failing-first proof; the slot-content assert is.
ROOTED, NOT TAKEN: the managed arm of CompareAndSwapPointer CASes by REFERENCE and reports by VALUE
Interlocked.CompareExchange(ref addr.Value, @new, old) == old over a MANAGED *unsafe.Pointer
slot: the CAS matches by reference, but the == verdict runs Pointer.Equals — by value. A
re-minted box holding the current number therefore FAILS the exchange and REPORTS success: a silent
lost CAS. Nothing banked reaches it (the managed arms are exercised and green through sync,
sync/atomic and every atomic.Value consumer), it is plainly fixable (a value-compare CAS loop
over the reference slot), so it is not disclosable — it is priced here for a follow-on and
deliberately not smuggled into a banking commit at zero coverage.
Gates and arithmetic
Full pipeline -test-action all validated, exit 0 — proof page
docs/validation/current/sync.atomic.md, the validation index, and the package README’s Tests
badge all emitted by the validating run itself · GolibTests full · go2cs.slnx Debug
--no-incremental · full behavioral suite · own-row sweep at the merge-result discipline’s lane
half · six reflect-consumer canaries derived at gate time by DIRECT IMPORT against this tree’s
159-row roster (go/types 557, encoding/json 491, crypto/tls 400 + 2, encoding/xml 386,
html/template 243, time 159; gcimporter absent by derivation) · committed test sources per the
validated-package policy · type.cs.auto RESTORED not banked (the review sibling, CleanupBacklog
18) · no converter change, no corpus regen, no package_info.cs record moved — no CNR and no
go generate owed.
Roster: 158 → 159 of 215 (74.0%) · 18,425 → 18,533 matching · 79 disclosed. Header recomputed from the table, not incremented.
🐧 MEASURED — the Linux operational-parity census: the fleet has a Linux lane again, the FIRST TWO rows ever VALIDATE on Linux, the native stdlib build is 0 errors, and every failure roots to a KNOWN class (2026-08-21, lane claude/linux-parity-census, laptop G + WSL2)
The assignment: stand up a Linux lane, run the instrument ladder, census what runs, what fails,
and what each failure roots to — platform gap vs harness gap vs corpus gap — priced per
PLAN-linux-operation.md’s F-series. All of that is below, and the headline outruns the
assignment: the Phase-4 differential pipeline, which F1 recorded as “structurally unavailable”
on Linux, validated two banked rows end to end — the L3 corpus quietly retired the premise.
The lane, and why it had to be rebuilt at all
The plan’s provisioned distro (F15 ✅, the r47a recipe; r48b’s measured ladder) ran on a 24-CPU
WSL2 host — the i9-13900K’s signature, the machine that died 2026-08-09. Every Linux number in
the plan was an orphaned baseline; nothing in the fleet could run the ladder. This lane rebuilt it
on laptop G, collaboratively (the human ran the elevated wsl --install -d Ubuntu-22.04 and one
process unstick; everything after was scripted):
- WSL 2.7.12 / Ubuntu 22.04, NO reboot — build 26200’s MSIX path avoided the restart entirely
(virtualization was already on in firmware). One trap for the next installer:
wsl --install’s post-registration OOBE launches an INTERACTIVE console that, headless, sits invisibly at the create-a-user prompt holding the WSL service transaction — every laterwslcommand,--terminateincluded, queues behind it with no diagnostic. Kill the installer processes; the distro is already registered, and-u rootbypasses user creation permanently. - F15 re-provisioning: ~4 minutes, user-space per the recipe — Go 1.23.1 (pinned), .NET
SDK 9.0.317, pwsh 7.5.4, fresh clone onto ext4 at master
d3e7a61e9. - The F2 pin re-verified on the platform it was written for:
core.autocrlfunset (Linux git default) andsrc/core/fmt/print.csstill materializes CRLF — the.gitattributes eol=crlfpin governs the checkout exactly as designed. Zero false drift.
The ladder — five rungs, ~24 minutes total, every verdict classified
| Rung | Result | Classification |
|---|---|---|
1 · converter go test ./... |
ok, 79.7 s, full suite — natively | no gap; ~1.8× FASTER than the same suite on the Windows side of this laptop |
2 · check-no-regression.ps1 |
byte-identical for 625 of 631; 6 NOT MEASURED | 1 by design (FindFirstFileData, the plan’s own note) + 5 new members of the same class — see the F8 growth below. Platform gap, priced |
3 · go2cs-stdlib.slnx -p:GoTargetOS=linux, NATIVE host |
0 errors, 149 warnings, ~7.9 min | no gap — the first native-Linux compile of the full 307-project converted stdlib in the project’s history. The Windows-host control run the same day: 0 errors, 149 warnings — the warning count matches exactly |
4 · behavioral shard (34 Array* projects, all four phases) |
first attempt: apphost libhostfxr not found → one-line harness gap (DOTNET_ROOT); retry: 24 of 34 fully green including Output, 10 fail exit code C# 2 vs Go 0 |
the 10 root to F1, self-diagnosed: TypeInitializationException → os_package → internal/poll → syscall → DllNotFoundException("kernel32.dll") — the windows-flavor corpus on a linux host, with the corpus’s own RID-fallback WARNING banner printing the situation AND the remedy. The 24 passers print the same banner and survive: their init path never touches os_package. F1’s blast radius is program-init-reaches-os, not all-programs — materially narrower than the plan’s “all 515 stdout comparisons fault” |
| 5 · validated-sweep shard | unicode/utf8 PASS 14/14 (92 s) · container/heap PASS 7/7 |
the first Phase-4 rows ever validated on Linux. The full differential pipeline — convert on a Linux host, build against the corpus, run under the Go-semantics test host, compare verdict-for-verdict with go test — end to end, twice. (container/heap’s first attempt failed CS0246 without DOTNET_ROOT; with it, clean — same one-line harness gap as rung 4, two instruments confirmed unblocked by one export) |
The one F-series repricing this census forces: F8’s set is SIX, not two
The five new conversion-time NOT MEASURED packages are all guards the Windows syscall arcs
added after r48b’s baseline, each dated to its creating commit: PointerOutParameter
(07d41538b, the **T out-parameter cell), SockaddrRoundTrip (1f641dade, the L10 blittable
seam), SystemCertVerify (cac5455a7, the CryptoAPI chain), WsaProtocolInfo (0e9c34bf1, the
WSAPROTOCOL_INFOW mirror), plus UnsafeStringEmpty (same symptom; provenance traces only to the
casing commit d3223d252 — confirm its root when gating). This is the expected shape of the
campaign’s own progress: every new Windows-semantic guard is a new F8 member. The gating work F8
priced should enumerate from CNR’s NOT MEASURED list at gate time, not from a frozen list of two.
What the census says the parity rung actually costs
Materially LESS than the plan priced, because two of its walls fell to other arcs in the interim:
- F1 is half-retired by measurement. The corpus compiles natively (rung 3), pure-compute
rows VALIDATE (rung 5), and the failing class self-diagnoses with its remedy. What remains of
F1 is plumbing, not architecture: the behavioral runner and the sweep need to bind the
GoTargetOS=linuxcorpus on a linux host (today a plain Debug build binds the windows default — the banner’s own instruction), and the syscall-backed rows need the netpoll-arc Linux halves measured row by row. Measure, don’t assume held: nobody predicted two rows would validate today. - One harness line:
DOTNET_ROOTmust be exported wherever the SDK lives outside the default location — apphosts (BehavioralRunner, the built test programs, sweep children) do not resolve the runtime fromPATH. Two instruments proven unblocked by the one export. Belongs beside$IsWindowsHostin_paths.ps1’s documentation or the lane-setup recipe. - The F15 recipe is current and fast (~4 min user-space on a fresh distro), with the OOBE transaction-lock trap above as its one new footnote.
No fixes landed beyond the sanctioned one-line unblock (an env var in the lane’s own scripts);
the corpus, converter, golib and harness are all untouched by this lane — the deliverable is this
census. The Windows-host GoTargetOS=linux control build (0 errors, 380 s, run before the distro
existed) stands as the cross-host pair to rung 3.
The next rung, sized by this census
A Linux lane that VALIDATES banked rows exists today at the cost of: one DOTNET_ROOT export +
-u root + the F15 recipe. The ordered work to widen it: (a) teach the two runners the
GoTargetOS host default (or a flag) so behavioral Output and syscall-adjacent sweeps bind the
linux corpus; (b) re-enumerate F8’s gating set from CNR at gate time (6 today); (c) sweep the
banked roster’s pure-compute rows on Linux wholesale — rung 5 suggests a large fraction validates
as-is, which would make the eventual “Linux-validated” column a measurement campaign, not a
porting one.
LANDED + ROUTED – ReadMemStats S2/S3 merges: the recorder is always-on, ReadMemStats reads 0.0 B/call, TestReadGCStats passes all nine – and TestFreeOSMemory routes to OBJECT LIFETIME as a codegen-liveness candidate (coordinator harvest, 2026-08-21)
The implementation lane delivered the ratified design end to end (GcPauseRecorder.cs, the
high-water HeapReleased with both honesty notes as code comments, NumForcedGC, the zero-rule
fields asserted by guard, runtime/metrics untouched per OQ-4). The binding refinements closed
hard: the 288 B/call GCMemoryInfoData box left the read path entirely – 0 B across all 200
bracketed windows, ceiling tightened 320 -> 0 – and every recorder read enforces
Generation == MaxGeneration. Union gates at the merge: GolibTests 211/211 (the GC surface
and the #159 native-slot change coexisting), spot sweeps of the protected consumers.
runtime/debug is now 3 of 9, and its remaining bill is fully composed:
TestStack– the position-map arc (in flight) + the ruled fifth-frame host-limit entry.WriteHeapDumpx3 – the ruledruntime-capabilitydisclosure.TestFreeOSMemory– ROUTED by measurement, and RULED a codegen-liveness CANDIDATE: the lane’s three-way probe (inline allocation in the measuring frame is not reclaimed; the same allocation behind a RETURNED call releases 33,689,600 B to the byte; invariant under Release and under untiered JIT) is the class’s own text – a frame’s dead slot held live for the frame’s lifetime. SS7.1.10’s “Debug build” attribution is NARROWED accordingly. The banking lane verifies the signature against the class at bank time; nothing is pinned today. The package banks when the position-map arc merges – likely at 4-5 matching + 4-5 disclosed, measured then, not predicted here.
Queue, small: (1) core/math/big and core/runtime/debug are in go2cs.slnx’s build closure
via GolibTests but unregistered in the .slnx – one line each, deferred twice now for lane
conflict-avoidance; take at the next quiet point. (2) time’s born-stale banked test sources
have now surfaced in THREE lanes – the leveling commit is due. (3) Two CLAUDE.md budget rows
re-measured this harvest (updated in the same commit): full go2cs.slnx ~3,546 s at 722
projects; full behavioral suite ~6,552 s at 603 packages (i7-5820K, 2026-08-21).
FUTURE ERA – step-through-GO debugging: the position-map tables are a source map, and .NET already has the player (user idea, recorded 2026-08-21)
An idea banked for a later era, so it is not lost to churn. The GoPositionMap line tables carry
exactly the data a debugger needs – and .NET’s native source-mapping machinery is #line
pragmas compiled into PDB sequence points (the mechanism behind stepping through .cshtml in
Razor). The committed corpus rightly refuses #line (+28-47% of lines against the reads-like-Go
objective), but a future OPT-IN build mode could inject the pragmas TRANSIENTLY at build time,
generated mechanically from the position-map tables – never committed, full fidelity:
breakpoints in .go files, stepping through Go source while the converted .NET assembly
executes, TypeScript-over-JavaScript style.
Composes further with SourceLink: PDB documents mapped to github.com/golang/go at the
pinned release tag would let a consumer F11 from their own C# into Go’s actual stdlib source,
version-exact, from a published go.<pkg> NuGet package – the same mechanism that steps into
the .NET BCL today, and a natural sibling of the nugetgo self-describing-package convention.
Era: post-terminal, .NET-10-or-later polish. Prerequisites already banked: the position-map arc (the data), the pinned-release publish convention (the SourceLink target). Nothing here is scheduled; this note exists so the right era finds it. —
2026-08-21 · Linux Phase-4 pipeline: the $(go2csPath) case-insensitive environment race — ROOT-CAUSED, harness-pinned, converter fix priced (lane G, claude/linux-measure-1)
Symptom (killed Linux measurement campaigns v1–v3): intermittent, package-shuffling hard
build failures inside -tests -test-action all — warning MSB9008: The referenced project
/root/go2csgen/go2cs-gen/go2cs-gen.csproj does not exist followed by a CS0246 storm on every
golib type (GoType, slice<>, ж<>, string…), reported by the sweep as Go="pass" C#=""
for the whole suite. 2–27 s fast-fails, ~coin-flip rate per pipeline child, retries sometimes
green (v3: 8 retry-passes vs 17 retry-fails), isolated purged re-runs always green (14/14),
two-package alternation reproducing 3-for-3 within ≤2 cycles. Every plausible suspect A/B-eliminated
along the way: shared compilation (UseSharedCompilation=false changed nothing), straggler
children (settle-to-zero + pkill), emission drift (csproj md5 stable), dotnet run alone
(14/14 clean), pwsh wrapper (bare-converter repro), GoTargetOS plumbing.
Root (binlog-proven): four links.
main.go:91-93— when envGO2CSPATHis unset, the converter defaults it tofilepath.Join(homeDir, "go2cs")andos.Setenvs it into its own environment. On the Linux lane the value is/root/go2cs— no trailing separator, and the clone happens to LIVE at~/go2cs, so the default names a real tree (on Windows%USERPROFILE%\go2csis the deploy-core root: valid or inert, never this repo’s src).testConversion.go:5663— every pipeline child getscmd.Env = append(os.Environ(), "go2csPath="+ensureTrailingSeparator(options.go2csPath)).os.Environ()now carriesGO2CSPATH=/root/go2cs; the append addsgo2csPath=/root/go2cs/src/. Two POSIX-distinct variables, both in the child env block.- MSBuild resolves environment-derived properties case-insensitively, so both entries map
to the ONE property
$(go2csPath)— and which value wins is enumeration-order-dependent inside the .NET/MSBuild env-table plumbing: a per-process coin flip. Diagnostic replay of the failing build’s binlog (MSBUILDDEBUGENGINE=1) states the losing draw verbatim:Property 'go2csPath' with value '/root/go2cs' expanded from the environment. - With
$(go2csPath)=/root/go2cs(sans slash),$(go2csPath)gen/...concatenates to/root/go2csgen/...and$(go2csPath)core/...to/root/go2cscore/...: the analyzer and every stdlib ProjectReference dangle (restore graph confirms:_RestoreGraphAbsoluteProjectPaths=/root/go2cscore/...), golib types vanish, storm follows.
Why Windows never saw it in five weeks of sweeps: Windows environment blocks are
case-insensitive at the OS level — GO2CSPATH and go2csPath are ONE slot, the converter’s
append lands last and wins deterministically. The race is structurally Linux/macOS-only
(POSIX case-sensitive environ + MSBuild case-insensitive property lookup). F-series
classification: harness/converter platform gap, not corpus, not CLR.
Neutralization (landed, sanctioned harness class): pin GO2CSPATH to the correct
slash-terminated src root so both names carry one value and either race winner is right —
_paths.ps1 now exports it on Linux hosts when unset (beside the existing GoTargetOS
default), and the campaign driver exports it in its env. Validated under the exact reproducing
conditions: the 3-for-3 alternation runs 8/8 clean; campaign v4 relaunched under the pin.
Converter fix (PRICED, not patched — converter code is out of the measurement lane’s scope):
the complete fix is at the child-env construction (testConversion.go:5663): scrub every
case-insensitive variant of go2csPath from os.Environ() before appending the canonical
entry — that closes the WHOLE class, including the nastier variant where a user’s ambient
GO2CSPATH names a different real tree and the child build nondeterministically binds the
wrong stdlib (a verdict that moves with the shell — exactly what the explicit -go2cspath
doctrine exists to prevent). Optional hygiene on top: main.go:93 need not os.Setenv at all —
the defaulted value is consumed immediately as the flag default and nothing downstream reads the
env var again. Guard shape: a converter unit test over the child-env builder asserting exactly
one case-insensitive go2csPath entry survives. Small, mechanical, Windows-behavior-neutral.
Full forensic trail (binlogs, replay, alternation logs): /root/f4* on the Linux lane distro;
mechanism summary in the fleet mailbox entry of this date.
THE POSITION-MAP ARC LANDS: one record per converted file carries a WHOLE position, log and flag bank on it, and runtime/debug’s TestStack comes down to its one structural frame (2026-08-21, lane claude/position-map-arc)
The ruling’s three answers, implemented and measured. The design note is committed FIRST and on its
own (docs/phase4/DESIGN-position-map.md) so a veto costs rework rather than archaeology; this entry
records what the mechanism measured, what it forced that the ruling did not fix, and the two rows.
The mechanism, in one sentence
One [assembly: GoPositionMap("<go file>", "<cs file>", "<table>")] per converted file, emitted INTO
that file, carrying the Go file’s identity AND its C#-line → Go-line table together.
Indivisibility is structural, not a rule. Both halves come from the one record, so no code path
exists that could report a Go file with a C# line. A frame either finds a record and reports a Go
position that exists, or finds none and reports the converted .cs position it always did — which is
what golib, the BCL, the hand-owned test host and every whole-file hand-own do, each for the reason
goFrameName already declines to invent a Go name for them. crypto/subtle/xor_generic.cs will not
name crypto/subtle/xor_generic.go: its C# was WRITTEN, and no line of it corresponds to a line of Go.
Priced against the alternatives, and measured
#line (the board’s PDB-transport route) is disqualified twice over: +28–47% of corpus LINES, and CS
diagnostics relocate onto .go files that are not in the project — the repo’s whole method is
root-causing against the emitted .cs. A side-car resource and a per-package generated .cs price
the same as the winner; what separates the winner is that two rules the others must STATE fall out of
it for free — a hand-own carries no record because it is never re-emitted, and an L3 per-GOOS variant
carries its own because the record is IN the variant.
Measured on a seeded whole-corpus reconvert:
| files carrying a record | 1,339 (all GOROOT-relative — the corpus is std, and std is what cmd/go trimpaths) |
| attribute text, total | 370,644 bytes (0.35 MB) |
corpus .cs, total |
38.73 MB |
| share of the corpus | 0.91% |
| largest single table | 7,168 chars, crypto/internal/nistec/fiat/p521_fiat64.cs |
The identity: three forms, and the middle one is FORCED
Discriminated by the SOURCE’s location, never by the conversion mode — which matters, because the
-tests pipeline converts GOROOT packages through a single-package run, and a mode-keyed rule would
hand log, flag and runtime/debug absolute GOROOT paths and fail TestStack outright.
A source beside its emitted .cs records its BARE NAME, rooted at run time against the .cs file’s
own compile-time directory. Baking the absolute path instead is not an available option: it names a
directory that does not exist on the next clone — a fabricated position on every other machine, and
check-no-regression reporting the whole behavioral corpus as drifted in every sibling worktree on
the first run. This is what lets the arc’s own guard assert Go agreement at all.
The ruling’s main/main.go regression is now structurally impossible
Not avoided — impossible. No code path anywhere composes a file name from a package, namespace or
class name; the runtime reads a recorded string and, in one case, prefixes it with a directory the
PDB gave it. positionMap_test.go pins it directly.
The guard the ruling queued, strengthened — and what it now measures
RuntimeCallerFrames passed all four phases under the file-half change because its five file
assertions were separator booleans and an equality, every one invariant under a wholesale change of
what the file names. It now asserts the property, printing VALUES so the stdout comparison against
go run . IS the assertion and no constant in the guard has to be kept in step with the source it
names:
go run . converted C#
caller file tail: RuntimeCallerFrames/main.go RuntimeCallerFrames/main.go
caller file rooted: true true
caller line: 27 27
caller line two frames up: 107 107
traceback names a go file: true true
The same five lines under the file half alone would have read main/main.go and false.
The rows
flag — 24 matched, 0 disclosed. Exactly the ruling’s prediction: its one miss was the file
token, and a Go-mapped line satisfies TestDefineAfterSet’s .*/flag_test.go:.*.
log — 8 matched, 1 disclosed. The yield signal the file half could not deliver: TestAll
passes, because log_test.go:63/:65 is now what the frame reports. Its residual is TestDiscard,
an at-most-one AllocsPerRun assert — alloc-profile, on bufio’s TestReadStringAllocs
precedent: Go’s one allocation is the variadic []any, and its func(b []byte) []byte closure over
format/v does not escape Output, so Go keeps it off the heap; the converted call must
heap-allocate the params array, the display class the C# compiler emits for that same closure, and its
delegate. Measured 3 golib-site allocations per run against Go’s 1, and that is a LOWER BOUND. The
count is comparable — it is WHERE the allocations live that differs, which is what separates this
class from alloc-count-semantics.
runtime/debug — 2 of 9, unchanged, and its TestStack is down to ONE assertion. Measured
independently of the board’s file-half run and agreeing with it exactly:
in line "\tC:/…/src/core/testing/TestExecution.cs:593", expected prefix "\ttesting/testing.go"
All four converted frames now name Go’s file. The fifth is frame("testing/testing.go", "") — the
test framework’s own frame — and it is the ruling’s point 2 in mechanism form: it is not that the host
DECLINES to claim testing/testing.go, it is that no conversion ever recorded a position for it. The
row does not bank here (its ReadMemStats half is a separate arc mid-measurement); the entry it will
take is ready to paste:
{
"name": "TestStack",
"class": "host-limit",
"signature": "expected prefix \"\\ttesting/testing.go\"",
"reason": "the fifth frame() assert requires the testing framework's own frame to name GOROOT/src/testing/testing.go, which is a property of Go's test BINARY: its testing package is compiled from that source. The converted deployment's testing package is the hand-owned host (src/core/testing) — the ONE-testing-package ruling's design, not an unimplemented conversion — so no conversion recorded a position for it and it honestly reports its own .cs position, exactly as golib and the BCL do. STRUCTURAL AND PERMANENT: unlike the relocatable-single-file entries, this does NOT retire when the host publishes self-contained and single-file, because the host would still be hand-written C# with no line-for-line relationship to Go's testing.go. It retires only if the test host itself becomes a conversion of Go's testing package, which the ONE-testing-package ruling deliberately forecloses"
}
What the mechanism forced that the ruling did not fix — written up, not self-ruled
The ruling says “stdlib → trimpath form via your goFrameName derivation + the two suffix rules”.
This design RECORDS it instead. Three reasons, and the departure is flagged rather than assumed:
the record has to exist for the line half regardless, so the derivation buys nothing; recording is
strictly closer to the ruling’s own principle (“a conversion-time FACT, never a plausible
composite”); and it retires the lane’s two measured suffix rules from the FILE half — a test
variant’s file is runtime/debug/stack_test.go because that is what the converter SAW, not because a
class-name suffix was stripped. The rules stay, unchanged, for the FUNCTION half, where Go genuinely
keeps _test (runtime/debug_test.T.method), and where goFrameName still derives from the package
because a function name IS a property of the package. Reverting to the derivation for GOROOT sources
is localized and priced in the design note’s §11.1.
The measurement that no amount of reading the code would have found
An in-text sentinel is invisible to consumers of the finished file but NOT to the converter’s own
reads of emitted text. On the first whole-corpus reconvert, 110 files emitted a block body where
they had emitted an expression-bodied lambda: convFuncLit decides that collapse by testing that
nothing but the block’s opening brace precedes the return, and it reads the block back as a string.
Nothing was wrong with the map; the text simply was not neutral. The rule is now explicit and
recorded in positionMapOperations.go — a site that INSPECTS or REWRITES captured block text reads it
through stripPositionSentinels, a site that merely APPENDS it must not — and the standing guard is
the corpus-wide byte-identity check below, which is why that check is a gate and not a formality.
Byte identity, and the 8 files that are LEVELLED rather than restored
Seeded reconvert against the committed tree, with each file’s own record removed: 3,234
byte-identical, 8 differing. All 8 were proven PRE-EXISTING by a control converter built from
HEAD — its emission differs from the committed tree in exactly the same 8 files, and my emission is
byte-identical to the control’s modulo the record. They are the born-stale class (the value-alias
emission in runtime/{alg,map,map_fast32,map_fast64,mbarrier,traceback}.cs and runtime/pprof/map.cs,
the multi-result any-slot return-type prefix in go/internal/gcimporter/gcimporter.cs), 28 lines in
total.
They are levelled, not restored, and the mechanism forces it: a position map is computed from the
text it describes, so restoring the text while keeping the record would produce wrong line numbers for
those 8 files, and dropping the record would leave 8 converted files — runtime/traceback.cs among
them — unmapped. Stated here because it is a change this bank carries that is not this arc’s.
Gates
| Gate | Result |
|---|---|
| design note committed FIRST, on its own | a3e1d2229 |
converter go test ./... (incl. projitemsIntegrity, TestStdLibMetadataInSync) |
green, 288 s – so no stdlib-metadata drift owed |
| seeded whole-corpus reconvert vs committed, record removed | 3,234 byte-identical / 8 pre-existing (control-proven) |
go2cs-stdlib.slnx |
307 projects, 0 errors, 409 s |
go2cs.slnx (owed after a golib/runtime API change) |
0 errors, 357 s |
| behavioral suite, all four phases | PASS 603/603 transpile+compile+golden, 577 stdout comparisons vs go run ., 0 failures, 1,775 s |
check-no-regression.ps1 |
NO REGRESSION – byte-identical .cs and .csproj across all 630 behavioral packages, 0 NOT MEASURED |
GolibTests |
181/181 |
-tests -test-action all – flag |
24 validated, 0 divergent |
-tests -test-action all – log |
8 validated, 1 disclosed (alloc-profile) |
-tests -test-action all – runtime/debug |
2 of 9, TestStack at ONE assertion (the host frame) |
| full validated sweep, 160 packages / 18,457 verdicts | 160 pass / 0 fail, 18,457 verdicts, 17,491 s (4 h 51 m) |
The sweep is the whole roster rather than a derived canary set, deliberately: this is a
corpus-wide EMISSION change – every converted file in the tree gained a record – so the
direct-import derivation (runtime: 53 banked rows, largest go/internal/gcimporter 583,
crypto/rsa 559, go/types 557, encoding/json 491, crypto/tls 400) and the board’s own
frame-file blast radius (12 rows / 1,539 verdicts) are both proper subsets of what ran.
Post-merge gates, at the MERGE RESULT
The lane-tip proof above is necessary and not sufficient — the tls-regression rule. claude/union-157
(42282b2aa, row #159 sync/atomic) merged in; both conflicts resolved by UNION, and the roster
arithmetic recomposed from the union rather than from either side: 161 / 215 = 74.9%, 18,565
matching verdicts, 80 disclosed, checked by summing the 161 rows rather than by trusting the header.
| Gate (post-merge) | Result |
|---|---|
go2cs-stdlib.slnx, merged corpus, --no-incremental |
0 errors, 466 s |
own row flag |
PASS 24 |
own row log (filter also swept log/slog/internal/benchmarks 3, testing/slogtest 17) |
PASS 8 / 3 / 3 pass, 0 fail |
the merge’s own new row sync/atomic |
PASS 108 — and re-validated through the pipeline at 108 / 1 skipped-identically / 0 divergent |
reflect canary go/types |
PASS 557 |
reflect canary encoding/json |
PASS 491 |
reflect canary crypto/tls |
PASS 400 |
reflect canary encoding/xml |
PASS 386 |
reflect canary html/template |
PASS 243 |
The canary set is DERIVED at gate time, not remembered: the merge touches
src/core/golib/GoReflect.FieldAccess.cs, which is reflect-bridge-touching, so the set is the five
largest banked reflect consumers by verdict count over THIS tree’s 161-row roster (64 rows qualify).
It differs from the list CLAUDE.md records — go/internal/gcimporter (583, the largest row overall)
is absent because it imports reflect nowhere — which is exactly why the rule says derive rather than
carry.
⚠ sync/atomic needs a budget above the pipeline’s 2-minute default: at the default its suite
self-terminates and reports Go="pass" C#="" for eighteen tests, which reads precisely like total
conversion failure and is not one. Validated at -test-timeout 15m, swept at -TestTimeout 20m.
A doctrine point the merge produced: a MERGE of a mapped file invalidates its map
Worth its own line because it generalizes past this arc. The map is a DERIVED artifact of the emitted text, so anything that changes that text without re-deriving the map leaves a plausible-but-wrong one — the exact class the ruling forbids, and silent, because no gate reads a line number against its source.
This merge did NOT trigger it, and why not is the instructive part. The one file the
claude/union-157 merge changes substantially, src/core/sync/atomic/type.cs (+69/−20, Ruling
A’s explicit layout), carries no record to invalidate — it is a whole-file [module:
GoManualConversion] hand-own, so by construction it was never mapped and its frames report the
converted .cs position. That is the indivisibility rule doing its job at exactly the moment it
would have mattered. What the merge DOES leave is the complementary state: sync/atomic’s converted
test artifacts arrive from a side that predates this change, so they are UNMAPPED rather than
mis-mapped — re-emission makes them uniform with the other 160 rows, and is owed for that reason.
The rule: a merge that changes any converted .cs owes a re-emission of that file’s package
before the gates. It is cheap — a filtered -stdlib, or the package’s own -tests -test-action
all, which re-emits and re-validates in one step — and it is mechanical, because the affected set is
exactly git diff --name-only <base> <theirs> -- 'src/core/**/*.cs' minus the files that carry no
record anyway (golib, *_impl.cs hand-owns, package_info.cs/package_init.cs). Same shape as the
standing “never convert twice into one root” rule: a step in the ritual, not a diagnosis to re-derive.
Standing dirt, classified and RESTORED
The -tests closure re-emissions from flag’s own pipeline run: flag/flag.cs’s using os ->
using Δos collision rename and flag/package_init.cs’s initᴛᴛtests() hook (+7). Both are the
named standing class and both are line-count NEUTRAL, so neither disturbs the position map that
now rides in those files – restored to the -stdlib emission, where the tree rests. log/log.cs
did not move at all. src/core/README.md came back as a CRLF phantom from the overlay (empty
numstat) and was restored. runtime/debug’s converted test artifacts were removed rather than
committed: the row does not bank here.
THE RELOCATION ROUND: the records move to the info files by user directive, the migration is the diff, and the design’s own SS6.4 doctrine caught the one defect before it banked (2026-08-21, lane claude/position-map-arc)
The merge was HELD one round: two coordinator entries posted before the lane’s final gates – the placement directive and its format addendum – went unread until the hold named them. Both misses would have been caught by a pre-gate mailbox poll; the lane adopts that rhythm. The round itself, in the order it ran:
The relocation (4c7fa5398)
Every [assembly: GoPositionMap] record leaves its converted file and lands in a delimited
expository block – // <GoSourcePositionMaps> … // </GoSourcePositionMaps> – in the
package-info file of the COMPILATION that compiles the mapped source: package_info.cs for
production, the test-info anchors for -tests variants, routed exactly as the GoImplement
records are. Semantics-free by construction: the record shape is unchanged, indivisibility is a
property of the record, and the assembly-scoped lookup cannot tell where an attribute was
declared – the runtime did not change by a byte. Verified per the directive: the
stdlib-metadata extract() does not scoop the block (pinned by
TestStdLibMetadataExtractIgnoresPositionMaps against a synthetic info file carrying all three
record families).
Two things the relocation surfaced that the in-file placement had made structurally free:
- The hand-own
.cs.autohazard. In-file, a review sibling’s (meaningless) record was harmless – the sibling is never compiled. Centralized, it would have landed in the COMPILEDpackage_info.cs: a table mapping lines of a hand-written file that does not contain them, the exact fabrication the ruling forbids.finalizePositionMaprecords nothing for amanualConversionvisit. - Merge semantics are load-bearing. The section follows
writePackageInfoFile’smergeExistingcontract because the recompile-model test assembly excludespackage_info.csfrom its compile items in favor of the seededpackage_test_info.cs– the seed is the only route production records have into that assembly. Key ownership is claimed by the conversion that writes the file (the-testsflow resets per VARIANT while its info files accumulate across variants; the recompile fallback re-invokes the whole conversion and must not double).
The migration (ea659f141) – and the defect the A/B caught
The diff IS the move: 1,811 corpus sources and 1,308 behavioral sources/goldens lose exactly
their two record lines; 490 corpus info files and 631 behavioral package_info.cs gain
sections. One named mover: runtime/windows/mheap.cs levels the union’s zero-size
explicit-layout emission with a freshly derived table. The 148-record delta between old and new
corpus counts closed exactly: declaration-only files (const tables, doc.cs) had EMPTY tables
– GoLineFor answers 0 for every line, behaviorally identical to no record – and the
relocated converter emits nothing for them.
The 469 committed test artifacts -stdlib cannot reach were migrated by script – and the
script’s first output was WRONG in a way nothing would have flagged red: it moved each table
verbatim while deleting the record’s two lines from the file top, leaving every C# line in
every moved table skewed by exactly +2. That is SS6.4’s doctrine – the map is derived from the
text it describes – firing on the migration itself, and it was caught because the round’s A/B
ran one package through the real pipeline and compared bytes: sort’s script tables differed
from the pipeline’s in their first delta. The fix re-derives (decode, shift every line by -2,
re-encode with the canonical encoder), validated closed-loop TWICE: sort’s 7 records and
log’s 1, byte-exact against pipeline ground truth in both rounds.
Gates, proportionate to a placement move per the hold entry
| Gate | Result |
|---|---|
| migration diff shape | exactly the move – every non-info file -2/0, one named mover |
check-no-regression.ps1 |
NO REGRESSION, byte-identical across all 631 behavioral packages |
migrated go2cs-stdlib.slnx, --no-incremental |
0 errors, 646 s |
GolibTests on the union |
211/211 – the hold entry’s predicted number |
converter go test ./... (incl. the new extract guard, TestStdLibMetadataInSync) |
green, 227 s |
RuntimeCallerFrames filtered, all four phases |
PASS 4/4 |
| pipeline re-validations | flag 24, log 8 + 1, sort A/B + own-row sweep PASS 63 |
| own-row + canary sweeps at the pushed tip | dispatched to the i9 worker as JOB-R1 (flag, log, sync/atomic, go/internal/gcimporter 583); results fold into the merge signal |
The 160/0 full sweep from the first round stands as the mechanism’s evidence per the hold entry
and was not re-run. SS11.1 (recording over derivation) and the stricter no-composition reading
are RATIFIED per the same entry; log’s alloc-profile disclosure and the 67-file
unmapped-until-their-own-rebank discipline are accepted.
⛔ MEASURED, DOES NOT BANK — runtime/debug’s NINTH verdict is TestSetCrashOutput, it was never in any inventory, and it is refused by every class including the one minted for this row (2026-08-21, lane claude/runtime-debug-bank)
The terminal row of the 1.23.1 campaign was fully composed on this board — Ruling B’s
runtime-capability class for WriteHeapDump, the ruled fifth-frame host-limit entry with its
drafted text, TestFreeOSMemory routed by measurement to codegen-liveness, and TestReadGCStats
closed by S2/S3. Eight of nine verdicts had an answer. The ninth did not, because no inventory
ever named it. Measured at d4c3e579d + a freshly built converter, -test-action all,
-test-timeout 15m, explicit -go2cspath:
| Test | go test |
go2cs | State | Composed bill |
|---|---|---|---|---|
TestReadGCStats |
pass | pass | MATCH | closed by the S2/S3 recorder ✓ |
TestSetGCPercent |
skip | skip | MATCH (skip parity, issue 20076) | — |
TestSetMaxThreadsOvf |
pass | pass | MATCH | — |
TestStack |
pass | fail | ONE assertion, the fifth/host frame | host-limit, drafted ✓ |
TestWriteHeapDumpNonempty |
pass | fail | WriteHeapDump throws |
runtime-capability ✓ |
TestWriteHeapDumpFinalizers |
pass | fail | WriteHeapDump throws |
runtime-capability ✓ |
TestWriteHeapDumpTypeName |
pass | fail | WriteHeapDump throws |
runtime-capability ✓ |
TestFreeOSMemory |
pass | fail | second assert, the frame’s dead 32 MB | codegen-liveness ✓ |
TestSetCrashOutput |
pass | fail | six assertions | NOTHING — never rooted |
3 matched · 6 divergent. All four expectations the bill stated were met exactly, including
TestStack at precisely one assertion (in line "\tD:/…/src/core/testing/TestExecution.cs:593",
expected prefix "\ttesting/testing.go" — the line above it is a testenv t.Log, not a failure).
The row does not bank on the ninth.
The census gap, stated plainly
The 2026-08-19 compile-wall entry recorded “2 of 9” and “the seven failures decompose into
four roots” — and its table names six tests. 9 − 2 = 7, so exactly one failure was carried
in the arithmetic and never in the prose. S2/S3 then closed TestReadGCStats, moving the row to
3 of 9 with six failures, five of them ruled. TestSetCrashOutput is the one that was counted but
never named, and it appears nowhere else on this board. It is not a regression: the hand-own it
lands on is unchanged since the 2026-08-01 consolidation (2e8066da6).
What TestSetCrashOutput actually measures, and why it is a defect
The test re-executes the test binary through exec.Command(os.Executable()) into TestMain’s
setcrashoutput arm, which creates a file, calls SetCrashOutput(f, CrashOptions{}),
println("hello"), then panic("oops"). The parent then requires both the crash file and the
child’s stderr to contain panic: oops, goroutine 1 and debug_test.TestMain, and requires
hello in stderr but not in the crash file.
A great deal of that works. The child spawns, the env propagates, hello reaches stderr, the panic
happens, the child exits 2, and the crash file is created and read back cleanly. Two things diverge:
- The crash file is empty —
crash = <<>>.runtime_setCrashFD(src/core/runtime/debug/stubs_impl.cs) remembers the fd and is deliberately inert: “the managed runtime writes crashes through its own handler, so the slot is remembered but inert.” - stderr carries a .NET dump, not a Go crash report —
System.AggregateException: One or more errors occurred. (oops) ---> go.PanicException: oopsover a CLR frame list, where Go writespanic: oops/ blank /goroutine 1 [running]:/ the Go-spelled traceback.
Against the five classes and the newly minted sixth:
| class | admits it? | why not |
|---|---|---|
alloc-profile / alloc-count-semantics / codegen-liveness |
no | nothing is being measured or collected |
host-limit |
no | its bar is a structural property of the DEPLOYMENT SHAPE, and the shape is demonstrably not the obstacle — unlike os/exec’s relocated installExe, this child started, ran, printed and panicked. What is missing is output FORMAT. The bar’s own text excludes “an unimplemented-but-fixable defect” |
runtime-capability |
no | it refuses this by its own admission test. Does a truthful managed implementation of the asserted behavior exist at any cost? Yes — and cheaply, because every ingredient is already banked and proven in this same package: debug.Stack() produces the traceback, goFrameName spells runtime/debug_test.TestMain Go’s way (the receiver half, 2026-08-19), and the position map now names the Go file and line — TestStack’s four converted frames all agree with Go on this very run. A priced arc is never a disclosure |
So the honest outcome is the one the charter asks for: the row declines. Disclosing it would
launder exactly what Ruling B’s anti-laundering clause and host-limit’s bar were both written to
refuse, and implementing the crash-report arc is not a banking lane’s change — it rewrites what
every converted program prints on an unhandled panic.
The roster does not move
161 / 215 = 74.9%, 18,565 matching verdicts, 80 disclosed — recomputed by summing the table’s 161 rows, unchanged. 75% waits on the arc below.
The remedy, priced for whoever takes it
One arc, two halves, both ordinary implementation work:
- A Go-format crash report on an unhandled panic. The test host’s top-level handler prints
panic: <value>, a blank line,goroutine N [running]:, then the Go-spelled traceback it can already produce. Blast radius is corpus-wide and behavioral — every converted program’s unhandled-panic stderr changes — so it wants a design note and charter §7 review, not a patch. Note the trap: the .NET wrapper text (System.AggregateException … ---> go.PanicException) is the host’s framing, so the fix belongs where the host decides what an escaped panic prints, not inruntime/debug. SetCrashOutput’s fd must receive that report. The slot already exists (s_crashFD); the crash printer tees to it. Mind the asymmetry the test pins: stderr getsprintlnoutput and the report; the crash file gets only the report.
Consumers beyond this row: ExampleSetCrashOutput_monitor (currently excluded as an Example,
Phase 4D) is the same surface, and any package whose suite asserts on panic output inherits half 1.
Two findings the next lane should not re-pay
TestFreeOSMemory’s failure signature MOVED, and a manifest pinned on this board’s older text would not have matched. The compile-wall entry recordedno memory released: 0 -> 0— the test’s FIRST assert. S2/S3’s high-waterHeapReleasednow makes that assert pass, and the failure has moved to the SECOND:less than 16777216 released: 0 -> 3031040. The routed shape is confirmed by that number — ~3 MB of unrelated memory really is released, while the 32 MB the test allocated inline in its own frame is not. Pin the stable prefixless than 16777216 released:and nothing further: the trailing0 -> 3031040is run-varying by construction.- The dirt classification for this package is empty, which is itself worth recording. A full
-test-action allleft zero tracked-file drift — no CRLF phantoms, no-tests-closure production re-flip, nopackage_init.csinitᴛᴛtests()hook, and no production.csprojchange (which would have been real drift). Everything the run produced was untracked pipeline output, removed on restore.git statusclean.
The four entries that ARE composed, ready to paste
Banked here so the arc’s closing lane pays nothing to re-derive them. Signatures verified against
this run’s captured output; TestStack’s is R’s drafted text, confirmed verbatim against the live
failure.
{
"schemaVersion": 1,
"disclosures": [
{
"name": "TestStack",
"class": "host-limit",
"signature": "expected prefix \"\\ttesting/testing.go\"",
"reason": "the fifth frame() assert requires the testing framework's own frame to name GOROOT/src/testing/testing.go, which is a property of Go's test BINARY: its testing package is compiled from that source. The converted deployment's testing package is the hand-owned host (src/core/testing) — the ONE-testing-package ruling's design, not an unimplemented conversion — so no conversion recorded a position for it and it honestly reports its own .cs position, exactly as golib and the BCL do. STRUCTURAL AND PERMANENT: unlike the relocatable-single-file entries, this does NOT retire when the host publishes self-contained and single-file, because the host would still be hand-written C# with no line-for-line relationship to Go's testing.go. It retires only if the test host itself becomes a conversion of Go's testing package, which the ONE-testing-package ruling deliberately forecloses"
},
{
"name": "TestWriteHeapDumpNonempty",
"class": "runtime-capability",
"signature": "WriteHeapDump is not supported by the managed runtime",
"reason": "Go's heap-dump format is a serialization of the Go heap through Go's own type descriptors; the heap a managed build would describe is not a Go heap and has no such descriptors, so any managed rendering would be fabrication rather than implementation and no truthful form exists at any cost. Pinned AS FAILING under the class's binding anti-laundering clause: this test checks only that the dump is non-empty (size >= 1) and never parses it, so a one-byte write would pass it while proving nothing — writing that byte is forbidden by this class's own text"
},
{
"name": "TestWriteHeapDumpFinalizers",
"class": "runtime-capability",
"signature": "WriteHeapDump is not supported by the managed runtime",
"reason": "Go's heap-dump format is a serialization of the Go heap through Go's own type descriptors; the heap a managed build would describe is not a Go heap and has no such descriptors, so any managed rendering would be fabrication rather than implementation and no truthful form exists at any cost. Pinned AS FAILING under the class's binding anti-laundering clause: this test checks only that the dump is non-empty and never parses it, so a one-byte write would pass it while proving nothing — writing that byte is forbidden by this class's own text"
},
{
"name": "TestWriteHeapDumpTypeName",
"class": "runtime-capability",
"signature": "WriteHeapDump is not supported by the managed runtime",
"reason": "Go's heap-dump format is a serialization of the Go heap through Go's own type descriptors; the heap a managed build would describe is not a Go heap and has no such descriptors, so any managed rendering would be fabrication rather than implementation and no truthful form exists at any cost. Pinned AS FAILING under the class's binding anti-laundering clause: this test checks only that the dump is non-empty and never parses it, so a one-byte write would pass it while proving nothing — writing that byte is forbidden by this class's own text"
},
{
"name": "TestFreeOSMemory",
"class": "codegen-liveness",
"signature": "less than 16777216 released:",
"reason": "the test allocates 32 MB inline in its own frame, clears the only named reference, and then asserts — from inside that same still-running frame — that FreeOSMemory has returned the memory to the OS. Go's per-safepoint liveness maps drop the allocation at its last use; the CLR reports a frame's slots live for the frame's whole lifetime, so the 32 MB is still rooted while the test is looking. Measured by the routing probe as a three-way control: the identical allocation behind a RETURNED call releases 33,689,600 B to the byte, and the inline form does not, invariant under Release and under untiered JIT. The first assert (HeapReleased must increase at all) PASSES on the S2/S3 high-water surface — 3,031,040 B of unrelated memory really is released — which is why the pin is the second assert's prefix and stops before its run-varying byte counts"
}
]
}
The runtime-capability class does NOT join the roster preamble here. Ruling B says it joins with
the first banking commit that uses it, exactly as chan-direction did, and there is no banking
commit — minting a class into published roster prose for a row that did not bank would leave the
preamble describing a class no manifest uses.
Gates
Stated as an accounting rather than a table of runs, because this commit changes one docs file and nothing else.
| Gate | Owed? | Result |
|---|---|---|
| own-row pipeline measurement | yes — it IS the finding | run above, 3 matched / 6 divergent, exit 1 |
| corpus restored | yes | git status clean; 9 untracked artifact paths + ignored pipeline output removed |
converter go test ./... |
no — no converter file moved, and no manifest entry is being committed for the loader to accept | not run; accounting stated |
check-no-regression.ps1 |
no — no converter file moved, so no emission can have changed | not run; accounting stated |
go2cs-stdlib.slnx / behavioral suite |
no — zero tracked corpus drift | not run; accounting stated |
| five-largest-reflect-consumer sweep (JOB-C1) | no — that gate protects a BANK; there is no bank, no corpus change and no converter change | not dispatched, deliberately — it would have spent an i9 hour proving a docs commit |
NIT, banked (user, 2026-08-21) – [assembly: go.GoPositionMap(...)] drops its redundant go. prefix
The emitted position-map records carry a go. namespace prefix the info files do not need – the
file’s existing using covers it, and every sibling assembly-level record (GoTypeAlias,
GoImplement, GoImplicitConv) is emitted unprefixed. One emission-string fix in the converter
plus the corpus-wide info-file diff it implies. RIDES THE QUEUED LEVELING REBANK (the time-class
born-stale leveling + map-coverage completion, due after the crossing) so one deliberate regen
levels all three families at once. Not taken inline by any current lane; the train does not stop.
2026-08-21 · Linux measurement campaign Part 3 — the full-roster census: 92/159 validate on Linux as-is; ONE nine-member seam prices most of the rest (lane G, claude/linux-measure-1)
The first wholesale Linux run of the banked roster is complete. Campaign v4 (WSL2 Ubuntu-22.04, laptop-class host, corpus at master d3e7a61e9, GoTargetOS=linux, per-package logs + ledger resume, GO2CSPATH pinned per the 2026-08-21 race entry): 159 rows swept · 92 PASS · 67 FAIL · zero infra recurrences after the pin (one benign purge-retry at row 10). Control leg: the i9’s JOB-001 swept the same roster on Windows at 0f2a66912 to 159/159 — so every Linux FAIL below is Linux-specific by measurement, not inference (SHA caveat: JOB-001 ran at union-157 = master + S2/S3; the delta touches golib GC surface only and no failing row’s shape implicates it).
PASS side (92): every pure-compute flagship — fmt 63, sort 63, strings 68, math 76, bufio 80, crypto/elliptic 82, internal/fuzz 52, unicode/utf8 14, internal/reflectlite 30, go/ast, go/constant, go/scanner, go/token, encoding/{asn1,base32,base64,ascii85,csv,hex,pem}, crypto/{aes,des,md5,sha256,sha512,dsa,elliptic,subtle,rc4,internal/*}, hash/* incl. maphash 22 inside its calibrated floor, index/suffixarray 12, math/{bits,cmplx,rand/v2}, container/*, text/scanner, text/template/parse, net/{url,mail,textproto,rpc/jsonrpc,http/fcgi,http/internal/ascii}, testing/{quick,iotest,slogtest}, time-free context/errors/expvar/maps/path/cmp… Full ledger in the campaign artifacts.
FAIL side (67), classified by MEASURED shape — five walls and a handful of singletons, not 67 defects:
- W1 — the
internal/pollruntime seam (61 rows countingcrypto/tlsbelow; the wall). The Linux flavor’sos.Openpath routes EVERY file open throughinternal/poll’s runtime poller, and all nine of its runtime hooks are unimplemented generated stubs (fd_poll_runtime.cs:runtime_pollServerInit/Open/Close/Wait/WaitCanceled/Reset/SetDeadline/Unblock+IsPollDescriptor). First fixture read →System.NotImplementedException: runtime_pollServerInit … at sync.Once.Do → poll.init → os.newFile → os.Open → mustLoadFile(caught live, bzip2). The Windows flavor’s file path never touches netpoll — hence 159/159 there. Attribution is measured, not pattern-matched: 50 rows carry the poll signature in their own retainedgo2cs_test_results.json(all ofcompress/*×5,debug/*×6,image/*×4,archive/{tar,zip},go/{parser,printer,format,doc/comment,internal/gccgoimporter},encoding/json,html/template,text/template,time,strconv,regexp,sync,io,io/ioutil,math/rand,mime,mime/multipart,path/filepath,crypto/{ecdh,ecdsa,ed25519,rsa,internal/hpke},cryptoroot,internal/{abi,zstd,testenv,types/errors,godebugs,coverage/pods},runtime/metrics);os/execis stack-proven (IsPollDescriptorin test init); 4 importer-family rows (go/types,go/internal/gcimporter,go/importer,go/internal/srcimporter) die before results are written with the same run-shape (first-open-in-init, W1-consistent, representative re-check owed when the arc lands);internal/diff/io/fsshare the shape with output text not captured; and a W1b mmap sub-class (crypto/sha1panic: invalid argumentinTestOutOfBoundsRead,bytespage-boundary tests) faults on the page-boundary/mmap syscall family instead. The host survives each fault (exit 0, INFRASTRUCTURE-ERROR per test) — it is per-test poisoning, so file-free tests inside these suites still pass. Pricing: ONE hand-own closing the nine-member seam —ServerInitno-op +pollOpenreturning a not-pollable errno degrades every fd to the blocking path, which is exactly Go’s own fallback whenepoll_ctlrejects regular files and behavior-parity with the Windows flavor. Small, mechanical, measured blast radius: up to ~58 of the 61 rows (mmap sub-class and importer family may hold residuals to re-measure). This is the highest-leverage single item on the Linux board. - W2 — Linux test-variant emission defects, converter (2 rows).
os/signal:signal_test.cs(344): error CS1503 'method group' → 'System.Action';syscall: tests-variant build failure (Linux-only test files reach constructs the emitter mishandles). Both compile-time, both need converter fixes measured against the Linux_testclosure. - W3 — converter crash on conversion (1 row).
plugin:go2cspanics inconversionDriver.go:228converting the suite (buildmode=plugin surface). Conversion-time, priced to the converter lane. - W4 — per-OS verdict arithmetic, roster design (1 row).
crypto/rand: Linux discovers 302 eligible verdicts vs the banked Windows 298 — the eligible-test set is platform-variant, and the roster’s expected-count column is implicitly Windows-shaped. “Linux parity” needs per-OS expected counts (or an OS-keyed roster column) before rows like this can even report honestly. - W5 — harness defect, FIXED THIS LANE (1 row).
crypto/tlsnever ran:run-validated-sweep.ps1’s duration table spelled both micro signs (U+00B5, U+03BC) as literal hashtable keys — distinct under Windows NLS folding, EQUAL under pwsh/ICU on Linux → “Duplicate keys ‘μs’” kills the sweep of any package whose deadline reaches the parser. Fixed (keys join behind ContainsKey guards); tls re-run under the fixed script: honest FAIL in 50 s with a package-levelinfrastructure-erroronruntime_pollServerInitbefore any test — the underlying row FOLDS INTO W1 (raising it to 61 rows); what W5 contributes is the harness fix itself. - W6 — capability-detection divergence (1 row).
internal/cpu:TestDisableAllCapabilities/TestDisableSSE3Go=”pass” C#=”skip” — the host skips x86 capability-disable tests Linux Go runs. - W7 — Linux-specific hang in
atomic.ValueCAS (1 row; a REAL finding, not budget).sync/atomictimed out at 20m; the 45m re-run proves it is not clock: verdicts stream normally (TestUnaligned64skip,TestValuepass, …) untilTestValueCompareAndSwapConcurrent(value_test.go:249) starts and runs 40+ minutes without completing — a hang/livelock in the concurrent CAS path on Linux, while the i9 clears the identical suite at speed. Adjacent to the row-#159 ж-box native-slot atomics; flagged to the owning lane (mine, if assigned) for a reproduce-and-root follow-on rather than disclosed away.
Corrections against my own mid-campaign signals: the shard-2/3 “CRLF-fixture” hypothesis is REFUTED — the cluster shape was real but the root is W1’s poll seam, proven by the live catch and the 50-row signature census. Recorded per the measure-don’t-guess discipline.
Campaign infrastructure (what later Linux campaigns inherit): the GO2CSPATH case-insensitive env race (rooted + pinned, see the 2026-08-21 board entry; converter dedupe queued coordinator-side), _paths.ps1’s Linux pins (GoTargetOS + GO2CSPATH), the sweep’s -Exact filter, the ICU-safe duration table, per-package log retention with an idempotent resume ledger, and the two-monitor watch pattern. Roster snapshot: 159 rows at d3e7a61e9; flag and log banked mid-campaign in R’s arc and are NOT in this census — a two-row follow-up shard is owed once R’s merge lands, after pulling the distro clone forward.
What “Linux parity” requires, by the numbers: 92 rows already hold. One nine-member seam (W1) prices most of the remaining distance (~58 rows). Two converter emission defects (W2), one converter crash (W3), one roster-design item (W4), one capability arc (W6), and per-host budget calibration (W7) close the rest. The harness half (W5 + the race + the pins) is already landed on this branch.
2026-08-21 · FINDING for ratification — the W7 “hang” is Gosched ring starvation under the dedicated-thread goroutine model, Linux-amplified ≥15×; correctness NOT implicated, row #159 exonerated (lane G, claude/gosched-ring-finding)
Answers the census’s W7 row and the assigned reproduce-and-root. TestValueCompareAndSwapConcurrent (value_test.go:249) is not hung and not a latent #159 defect — it is a measured scheduling pathology, four-cell matrix on one machine plus the i9 control:
| host | short (100 goroutines · 10k handoffs) | full (1000 goroutines · 100k handoffs) |
|---|---|---|
| Windows, laptop (16T) | — | PASS, 183 s wall |
| Linux WSL2, same silicon | PASS, 13 s | ≥45 min, package timeout |
| Windows, i9 (24T) | — | PASS inside normal suite time |
Mechanism (each link measured or read from source, none theorized):
- The test is a strict token-passing ring: after warmup, value
kcan be advanced ONLY by goroutinek mod m— all 100,000 CAS advances are sequential cross-goroutine handoffs, and every miss spins throughruntime.Gosched(). - Converted goroutines are DEDICATED OS THREADS by design (
golib/builtin.csrecords the doctrine: “goroutines get dedicated threads (Goroutine.Start)”), so the full test is 1000 CPU-bound kernel threads. Gosched()converts toThread.Yield()(runtime/managed_impl.cs:105). On Windows the scheduler rotates equal-priority ready threads on yield, so the ring advances (183 s at 16T, faster at 24T — still ~200× Go’s sub-second, the scale factor). On Linux,Thread.Yield()issched_yield(2), which CFS makes near-inert for CPU-bound threads: the yielder is commonly re-selected and the ONE runnable thread that can advance the ring waits out a fair-share epoch — per-handoff latency ∝ runnable threads, total ≥45 min, presenting as a hang (the OS factor, ≥15× at fixed hardware).- Correctness is untouched: Linux short-mode passes in 13 s;
Value.CompareAndSwap’s converted loop (value-equality gate + identity CAS + retry) is faithful; the suite’s other 107 verdicts pass on Linux, and nothing in the mechanism reaches the ж-box native-slot/token machinery of row #159.
Classification: a semantic-fidelity cost gap of the dedicated-thread goroutine model under mass-Gosched spin workloads, Linux-amplified by sched_yield semantics — the same class as the maphash “performance gap, not correctness” precedent, but with an OS multiplier that turns slow into effectively-hung on exactly the platform the Linux campaign measures.
Remedy options, priced (ratification required — Gosched’s contract and the dedicated-thread doctrine are settled surface):
- (a) RECOMMENDED — adaptive yield backoff inside
Gosched(): afterkconsecutive yields by one thread without rescheduling progress, escalateThread.Yield()→Thread.Sleep(1)(leaves the runqueue, letting the starved owner run), resetting on work.Goschedpromises “yields the processor, allowing other goroutines to run” and nothing about mechanism, so an adaptive escalation is contract-clean; it collapses the CFS pathology for every mass-spin workload at once, and the healthy-case cost is bounded by choosingk(small-N rings never reach the sleep tier). Measured acceptance target: Linux full ring within ~2× same-hardware Windows (i.e. minutes, not 45+). - (b) M:N cooperative goroutine scheduling — the architectural fix; out of scope as a priced item here, recorded as the horizon this class keeps pointing at.
- (c) Interim harness honesty if (a) waits: the row cannot bank on Linux and must not be disclosed away as CLR-impossible — it is a real, fixable gap; leave it FAIL with this finding as the named cause.
Verification set when (a) lands: the four-cell matrix re-run (Linux full must complete), sync/atomic 108/108 on both OSes, and the standard reflect-consumer canaries derived at gate time — plus a Windows A/B on the i9 confirming no healthy-case regression (the suite’s other hammer tests are the sensitive canaries for added Sleep latency).
2026-08-22 · The Gosched ring: the ratified inert-only backoff is landed and safe but does NOT close W7 — the local design space is measured out (four variants, each refuted or unresolved by a NAMED mechanism), and the class points at M:N (lane G, claude/gosched-ring-finding)
Status of the ratified remedy (a): implemented per ratification and LANDED on the branch — golib/runtime/GoschedBackoff.cs (internal, IVT), runtime.Gosched delegates, failing-first guards green both OSes (GolibTests 213/213), Windows full ring unregressed (183 s → 84 s, the delta is scheduler variance — the backoff provably never fires under contention), Windows own-row sweep at tip: PASS 108/108 in 240 s (.cs.auto refresh drift restored per the documented class). But the Linux acceptance cell FAILS — and the failure is mechanistic, not tuning. The finding’s own “sched_yield is near-inert” premise holds only for the IDLE case; the contended ring’s yields context-switch (~6.5 µs measured) to other WRONG threads, so no local inertness predicate can ever fire. W7’s row stays an honest FAIL per affirmed doctrine (c).
The prototype cartography (all measured on the reproducing distro, full ring = 1000 dedicated threads · 100,000 strictly-serial handoffs):
| variant | escalation signal | Linux full ring | refutation mechanism |
|---|---|---|---|
| A (ratified) | consecutive inert yields (!switched \|\| <2 µs) |
≥749 s, killed | never fires: contended yields are “effective” (6.5 µs switches to wrong threads) |
| B | wall-gap burst count, sleep every 64th | ≥658 s, killed | sleep-every-64th keeps ~30% of 1000 threads runnable → woken owner waits a 25–100 ms CFS epoch per handoff |
| C | B + full-drain past 4,096 | ≥662 s, killed | self-defeating reset: involuntary preemption under target contention inserts >500 µs wall gaps between consecutive calls — stack census caught the steady state (659 sleeping / 340 yielding, tier-2 capture, full-drain never reached) |
| D | C with thread-CPU-TIME gaps (preemption-proof) | ≥660 s, killed | unresolved at prototype quality: clock_gettime(CLOCK_THREAD_CPUTIME_ID) path unverified, and the completion floor math no longer explains the miss |
Two measured constants that reshape any future design: Thread.Sleep(1) actually costs 1.07 ms on Linux and 15.9 ms on Windows (granularity INVERTED from the naive assumption — any sleep-tier design taxes Windows 15× harder per escalation), and Go’s own ring completes because its Gosched is a ~100 ns userspace runqueue rotation over GOMAXPROCS threads — a floor no 1:1-dedicated-thread design reaches, since every handoff there pays kernel-primitive costs (yield storms, sleep quanta, or wake syscalls) times CFS’s ordering.
Recommendation: keep landed-A (it demonstrably closes the idle-spin shape, harms nothing measured, and its guards pin the behavior); leave W7 an honest FAIL with this cartography as the named cause; price the ring’s closure under the M:N goroutine scheduling horizon rather than further Gosched-local tuning — three refuted candidates with distinct mechanisms is the evidence the local space is exhausted, and this campaign now motivates M:N with hard numbers rather than architecture taste. Variant D’s residual uncertainty (pinvoke verification, serial-floor accounting) is recorded for whoever takes that lane; the prototypes live in the session scratchpad and the raw logs in the distro’s /root/ (ringB/C/D.log, stack1/2.txt).
RULING – W7’s disposition: honest FAIL, ring closure priced to the M:N horizon; Gosched tuning is CLOSED as an avenue (coordinator, 2026-08-22)
The backoff cartography’s recommendation is ratified as delivered. The landed inert-only backoff STAYS (it closes the idle-spin shape class-wide, improved the Windows ring 183 -> 84 s, and its guards pin it); the Linux ring row stays an HONEST FAIL, never disclosed – and further Gosched-local tuning is closed by measurement, not by fatigue: four designs refuted with named mechanisms, and the floor argument is structural (Go’s Gosched is a ~100 ns userspace runqueue rotation; a 1:1 dedicated-thread design pays a context switch, ~6.5 us measured, to a thread that is usually the wrong one). Ring closure is priced to the M:N scheduling horizon, where it joins the recorded constituency. Variant D is DEFERRED by this pricing – driving it to resolution would spend against an avenue this ruling closes; it reopens only if the M:N horizon slips far enough to re-price nearer-term relief. The two portable constants (Thread.Sleep(1) = 1.07 ms Linux / 15.9 ms Windows, inverted) are noted as the kind of measured fact every future scheduler design should start from.
2026-08-22 · LANDED + MEASURED — the internal/poll seam hand-own: the Linux flavor’s un-armable fds degrade to the blocking path; 28 of the census’s 61 W1 rows flip PASS on Linux, and the 33 that do not are attributed, row by row, to FIVE further seams that sit BEHIND the poller (lane R, claude/poll-seam-hand-own-5aadf1)
The assignment was the census’s W1 pricing, implemented at the layer the census named and then MEASURED against the same roster. One file landed — src/core/internal/poll/linux/runtime_netpoll_impl.cs — and the full 161-row Linux roster was re-run at the lane tip with per-row logs; the Windows control of the 61 W1 rows ran on the i9 (JOB-R2). No roster change (Linux rows do not formally bank until the per-OS arithmetic ruling lands); the deliverable is the flip count below — and the census’s “up to ~58 of 61” pricing is corrected by measurement to 28, because five more walls stand behind the poll wall for the rest. Every one of them is now named, rooted and priced.
What landed
- The contract, in Go’s own words. Linux’s
os.newFilemarks everyOpenFile/Pipe/socket descriptor pollable and asks the poller to arm it; Go’snetpollopenis oneepoll_ctl(ADD), the kernel refuses a regular file or directory withEPERM,pollOpenreturns(0, EPERM),FD.InitsetsisBlocking = 1, andnewFilerestores blocking mode and carries on (file_unix.go: “a file descriptor that is not supported by epoll/kqueue; for example, disk files on Linux systems”). The hand-own answers un-armable for every descriptor:runtime_pollServerInita no-op (keeping the Windows flavor’s sequencing flag),runtime_pollOpen→(0, EPERM)bound from the convertedsyscallpackage, the six ctx-taking bodies loud-unreachable (InvalidOperationException: no ctx is ever minted and every caller infd_poll_runtime.csguards onruntimeCtx == 0, so a returned code from a body that cannot legitimately run would be a lie),runtime_isPollServerDescriptorfalse (no epoll instance;os/exec’sTestMainasks it of every open fd),runtimeNanothe Windows shape verbatim (the flat declaration owes a body on every GOOS). From there the converted fallback runs as written —pollable()false,preparenil,evict/close/waitCanceledno-ops,waitnever reached (a blockingread(2)returns data or EOF, neverEAGAIN),SetDeadline→ErrNoDeadline. Zero os/poll edits; the Windows flavor is byte-untouched. - Placement = NETPOLL-S1’s (
3f79fec70): a per-GOOS_impl.cscarrying[module: go.GoManualConversion], riding the existing<Compile Include="$(GoTargetOS)/*.cs" />glob — no csproj change. Marker census (line-anchoredgit grepoversrc/core) 63 → 64. The L3 corpus guard admits it (no principalruntime_netpoll.cs;netpollis not a GOOS suffix), and a NEW converter unit guard pins the structural claim the placement relies on:TestMergeLeavesPrincipalLessCompanionsWhereTheyAre— two divergent per-GOOS copies of a principal-less companion merge with no error, no write, no removal (the multi-platform merge resolves a placement only for a companion whose principal was emitted, soreadSingleHandOwnCopy’s “DIFFERENT contents” refusal is unreachable for this shape). - What degrades, stated in the file and MEASURED rather than left to be discovered. Go arms pipes, FIFOs, ttys and sockets; this flavor cannot yet (no readiness poller —
DESIGN-netpoll-managed-poller.md§8’s explicit non-goal;fd_unix.go’s wait-then-retry consumer wants a different managed mechanism from the Windows completion model), so those take the blocking path with Go’s own contract for files withoutSetDeadline: a read blocks its goroutine’s thread, deadlines answerErrNoDeadline,Closedoes not cancel an in-flight I/O.os.Pipe’s read end does not unblock a blockedReadonClose— measured on the Linux flavor in an isolated clone: Go printsread unblocked: read |0: file already closed, the convertedPipeCloseUnblocksReadprintsread did NOT unblockand exits cleanly once the writer closes (it faulted at the stub before).netDial/Listenwould surfaceFD.Init’sEPERMasoperation not permitted— measured correction: today the Linux socket path never reachesFD.Init; it dies earlier atsyscall.Bind/ConnectinSockaddrInet4.sockaddr()’s uintptr round-trip (R5 below), so the EPERM arm is an honest error in waiting until that seam and then the socket half of Linux land as their own arcs. Nofstatdiscrimination (every descriptor is un-armable for one reason; EPERM is invisible on everyospath and a syscall per open buys nothing). darwin inherits the seam and the remedy file-for-file but does not build today — no unmeasured copy shipped.
Gates (lane tip measured at 19b63567d — the hand-own commit; the branch tip adds only the merge guard, the docs, and comment-only header corrections, proven comment-only by git diff 19b63567d..tip – src/core/internal/poll/linux/runtime_netpoll_impl.cs``)
| Gate | Result |
|---|---|
| GolibTests (Windows) | 211/211 |
go2cs-stdlib.slnx -p:GoTargetOS=windows (Windows host, laptop R) |
0 errors (6:00) |
go2cs-stdlib.slnx -p:GoTargetOS=linux (NATIVE, WSL2 Ubuntu-22.04, --no-incremental) |
0 errors, 149 warnings — the census’s exact count (566 s) |
converter go test ./... (Windows) |
ok, 288 s (incl. the new merge guard + the L3 corpus walk) |
L3 corpus guard TestCorpusHandOwnsFollowTheirPrincipals |
green |
| CNR / behavioral | not owed and not run: no converter change; the Windows flavor is byte-untouched (the diff is one new file under linux/, docs, and one converter TEST file) |
The measurement — Linux full-roster re-run at the lane tip
Harness: the repo’s own run-validated-sweep.ps1 -Filter <pkg> -Exact -SkipBuild per row under pwsh 7.5.4, _paths.ps1’s Linux pins (GoTargetOS=linux, GO2CSPATH), -TestTimeout 20m base with the script’s floors, per-row logs + an idempotent resume ledger, run detached (setsid under a held wsl.exe, the tree restored between rows). Host: laptop R’s WSL2 Ubuntu-22.04 — the distro was bare and was re-provisioned per F15 in ~4 min incl. clone (Go 1.23.1, SDK 9.0.317, pwsh 7.5.4; 16 threads, 15 GB); the linux slnx build was run first so the 161 per-package builds went incremental. Roster at 161 rows (the census’s 159 + flag, log).
Headline: 122 PASS / 37 FAIL / 2 COUNT of 161 — against the census’s 92 PASS / 67 FAIL of 159.
Flip arithmetic over the census’s 61 W1 rows (G’s verbatim ledger, mailbox 2026-08-22 00:20):
- 28 flip FAIL → PASS at their banked counts: compress/{bzip2 4, flate 64, gzip 15, lzw 17, zlib 6} · crypto/{ecdsa 82, rsa 559, internal/hpke 19} · debug/{macho 7, plan9obj 2} · go/{format 4, parser 173, printer 45, internal/gccgoimporter 4} · image/{draw 9, gif 28, jpeg 14, png 28} · internal/{coverage/pods 1, zstd 536} · io 60 · mime/multipart 52 · regexp 45 · runtime/metrics 2 · strconv 55 · image 8 · internal/sysinfo 1 · internal/xcoff 3 (these three measured in the isolated clone at
19b63567d, then re-measured in roster order by the main run). Every one of these died atruntime_pollServerIniton the first fixture open in the census; every one now runs its whole suite on the blocking path. - 33 remain not-PASS — and NONE is the poller. Each is attributed from the C# host’s own failure output (
go2cs_test_results.json/go2cs_test_comparison.json, retained per row) to the seam that sits behind the poll seam for that row; the classes, in order of leverage:
| Class | Mechanism (rooted) | Rows | Remedy (priced) |
|---|---|---|---|
R1 — syscall.Stat_t by address |
Stat_t (linux/amd64) carries array<int64> X__unused, a managed reference, so the CLR lays the struct out itself; fstatat/Fstat hand the kernel (uintptr)Ꮡstat — the pinned managed image — and the 144-byte stat record lands on a field order that is not the kernel’s. Stat/Lstat funnel through fstatat. Probe (converted program, isolated clone): os.Stat(dir) → err nil, isDir = false, mode = p---------; Readdirnames/ReadDir/Read correct (dirent-typed); Glob → 0 and WalkDir → 1 follow from IsDir(). The Linux instance of the “wrappers passing a non-blittable struct by ADDRESS” class. |
8: archive/zip (OpenReader sizes from Stat().Size() → “not a valid zip file” ×45), debug/dwarf (Glob over testdata → 11 subtests absent), html/template (3), path/filepath (wall-to-wall: Walk marks 0, Glob empty, EvalSymlinks “not a directory”, MkdirAll’s stat), io/ioutil (Stat size 0 want 3302), io/fs (WalkDir over DirFS), internal/diff (“no testdata”), go/internal/srcimporter (go/build over GOROOT src, by code path); partial in go/doc/comment, text/template |
the Windows Timezoneinformation precedent: TWO bodies (fstatat, Fstat) hand-owned in syscall/linux/ against a blittable mirror, transcribed into the managed Stat_t. The highest-leverage Linux item now — it gates every directory walk, every Stat-sized read. |
| R2 — the exec wall | os/exec process launch on Linux (fork/exec, ensurePidfd, the BeforeFork family) — reached through testenv.MustHaveGoBuild/GoToolPath (go build -o /dev/null runtime), the gc importer’s go list -export export-data lookup, and exec.Command(os.Args[0]) self-re-execs. The converted sync.OnceValue re-panics the foreign exception as panic: nil (recover() sees no Go panic, valid false → panic(p) with p nil), which masks the real cause — a small honesty defect in its own right. |
16: debug/buildinfo, debug/gosym (+R3), go/doc/comment (TestStd; +R1), text/template (TestLinkerGC; +R1), sync (TestMutexMisuse), math/rand (TestDefaultRace ×7), crypto/ecdh (TestLinker), crypto/ed25519 (TestEd25519Vectors), crypto (TestPureGoTag), internal/abi, internal/testenv, internal/types/errors (TestErrorCodeExamples gated), internal/godebugs, go/types (555 of 557 verdicts now produced — the census had it dying at first open; TestCheck/* + TestAtomicAlign via go list), go/internal/gcimporter (281/581; TestImportStdLib enumerates via go list std), go/importer |
the Linux process-launch seam (FINDING-linux-run-layer.md §5: rawVforkSyscall, runtime_BeforeFork/AfterFork/AfterForkInChild/BeforeExec/AfterExec) — a design-size item, not a wrapper repair. |
R4 — syscall.rawSyscallNoError is still an announcing stub |
syscall_linux_impl.cs deliberately left it (“until something genuinely needs them”); the NoError family — Getuid/Getgid/Geteuid/Getegid/Getpid/Getppid/Gettid/Umask — calls it; os/user.currentUID → os.Getuid → NotImplementedException, and the first throw poisons user.Current()’s sync.Once so every later call NREs. |
3: archive/tar (TestFileInfoHeader infrastructure-error + 8 NRE aftermath via tar.statUnix), time (TestSleep: time.interrupt → Kill(Getpid())), os/exec (PACKAGE level: the host dies in init before any verdict, 0/72; R2 behind it); also a test each in io/ioutil, path/filepath (root-uid guards) |
ONE body beside runtime_entersyscall in syscall_linux_impl.cs: RawSyscall6 ignoring errno. Trivial, and it now “genuinely” is needed. |
| R5 — the Linux sockaddr seam | SockaddrInet4.sockaddr() (syscall_linux.cs:541-552): Go’s p := (*[2]byte)(unsafe.Pointer(&sa.raw.Port)) converts to a uintptr round-trip minting an EMPTY array<byte> → index out of range at syscall.Bind/Connect; and RawSockaddrInet4.Addr [4]byte makes raw non-blittable for the by-address bind. The L10 Windows precedent (syscall/windows/syscall_windows_impl.cs, SockaddrRoundTrip), not yet mirrored into syscall/linux/. Reached BEFORE fd.init(). |
2: encoding/json (490/491; TestHTTPDecoding via httptest), crypto/tls (PACKAGE level: TestMain’s listener; 0/3646 — the census’s tls row was the pollServerInit package error; this is the next seam behind it) |
mirror the L10 hand-own into syscall/linux/. |
| W1b — mmap (census-named) | syscall.Mmap on the Linux flavor answers EINVAL for the page-boundary tests |
2: crypto/sha1 (TestOutOfBoundsRead panic: invalid argument), bytes (the four *NearPageBoundary tests) |
the Linux mmap wrapper; separate raw seam |
| R3 — self-binary identity | os.Args[0] is the managed PE DLL (MZ\x90\0), so elf.Open(os.Args[0]) fails “bad magic number ‘[77 90 144 0]’”; Go SKIPS these on windows (TestNoSectionOverlaps: “cmd/link doesn’t produce ELF binaries”), so they were never reachable on the Windows roster |
1: debug/elf (+3 tests in debug/gosym, Go=skip there on this host) | a host-limit disclosure class, or an os.Args[0] ruling |
| W4 — per-OS count | mime validates at 18 vs Windows-banked 17: comparison status VALIDATED, matched = true, TestLookupMallocs disclosed exactly as on Windows; the extra verdict is TestTypeByExtensionUNIX (//go:build unix) |
1: mime — functionally a Linux PASS; the COUNT is the roster-schema question the census already routed (W4) | the OS-keyed expected count |
| R6 — unrooted | time’s TestEnvVarUsage: ZoneinfoForTesting() reads “” after t.Setenv("ZONEINFO",…) on the Linux flavor (passes on Windows) |
1 test | its own look (zoneinfoOnce / platformZoneSources ordering on linux) |
Control legs:
- the census’s 92 PASS rows on Linux: 92 of 92 still PASS — zero regressions (the hand-own changes nothing for a row that never arms a descriptor, and the blocking path is what those rows’ file opens were already asking for); net over the census: 92 → 122 PASS (+28 W1 flips, +
plugin, +log;flag, new since the census, FAILs on the exec wall), 3 h 47 m of row time; - the other five census classes (W2 os/signal, syscall; W3 plugin; W4 crypto/rand; W6 internal/cpu; W7 sync/atomic) + flag/log: every one re-measured exactly as the census classified it, none touched by this change — W2
os/signal(signal_test.cs(344)CS1503) andsyscall(tests-variant build failure) unchanged; W4crypto/randCOUNT 302 vs banked 298 again, comparison VALIDATED with every verdict matching (so no W1 hid behind its count); W6internal/cpuGo=pass/C#=skip ×2 unchanged; W7sync/atomicthe 20-min package timeout insideTestValueCompareAndSwapConcurrent(this tree forked before the backoff merge); W3plugin— the conversion-time converter panic of the census did NOT reproduce at the union tip: PASS 1 (a post-census master change or a flake; measured, not explained). The two post-census banks, first-measured on Linux:logPASS 8;flag23/24 withTestExitCodeon the exec wall (R2); - Windows control (JOB-R2, i9, the 61 W1 rows at
19b63567d, each-Filter <pkg> -Exact): 62/62 PASS, 0 FAIL, 15,490 verdicts (61 net +image/colorextra-but-green; mailbox 2026-08-22 03:40).crypto/tlsPASS 400 in 669 s on a re-run after the i9’s own 8-min outer wrapper — tighter than the sweep’s internal budget — cut the first attempt (an infra artifact the i9 classified as such, not a verdict). The change is linux-flavor-only and the Windows corpus is byte-untouched; this is the measured proof.
What this does and does not change on the board
- W1 is closed for FILES and is no longer the wall; the wall behind it is R1. The poller’s fallback runs Go’s own contract; what remains behind the 61 rows is five seams, each smaller than the one just closed, and the leverage order is now R1 (2 bodies, 8+ rows and every directory walk) → R4 (1 body, 3+ rows) → R5 (the L10 mirror, 2 rows incl. the tls flagship) → W1b → R2 (design-size) → R3 (disclosure). The honest flip count replaces the census’s pricing: 28, not ~58 — the remaining rows were never blocked by the poller ALONE.
- The Linux readiness poller (pipes, FIFOs, ttys, sockets; deadlines; close-unblocks-read) stays the separate design the Windows design’s §8 names; nothing here pre-empts it, and the measured
PipeCloseUnblocksReadshape is the behavioral contract it will have to restore. - darwin: a byte-identical copy of this file into
internal/poll/darwin/is the remedy when that corpus builds. - R4’s one body and R1’s two bodies are priced here for routing; neither was taken in this lane so that the flip count measures the poll seam alone.
✅ BANKED — the Go-format crash report lands and runtime/debug is roster row #162 at 75.3%: the ninth verdict closed as a FIX, which is what every class had already ruled it was (2026-08-22, lane worktree-agent-aef2216c6bd01a654)
The previous entry measured TestSetCrashOutput, found it refused by all five classes and by the
sixth minted for this very row, and priced the remedy in two halves. Both halves are built, the test
passes all six of its assertions, and the row banks — 4 matched · 5 disclosed, the terminal
crossing of the 1.23.1 campaign.
The arc, in the order the mission required
The design note went first, in its own commit (docs/phase4/DESIGN-crash-report.md,
48712b2a4). Its whole content is that the target format is Go’s own and nothing is invented:
panic: <value>, a blank line, goroutine N [running]:, then the traceback — the shape recorded
verbatim in Go’s own runtime/debug/stack_test.go above TestSetCrashOutput’s read-back. Element 1
is PanicException.PanicText, banked. Elements 3 and 4 are appendGoFrames, which TestStack banks
against Go frame for frame. The arc composes banked ingredients and renders nothing new.
WHERE, per the trap this board named. The System.AggregateException … ---> go.PanicException
text is the host’s framing, so the printer lands at the two places a host decides what an escaped
panic prints — golib’s AppDomain.UnhandledException backstop (every converted program) and
TestHost.Run’s outer catch (the test host) — and never in runtime/debug, which owns only the
destination. A recovered panic is untouched: nothing on the recover path, the GoFrame
capture/claim protocol, or TestExecution’s per-test panic report moves.
The printer is golib’s, because golib is the only assembly the test host and every converted
program share (core/testing references golib and time, and nothing else). golib cannot spell a
Go frame name, so the dependency inverts exactly as RuntimeErrorPanic.IntegerDivideByZeroValue
already inverts it for the divide-by-zero panic VALUE: golib declares the hook, core/runtime fills
it from a module initializer, and with nothing registered the report falls back byte-identically
to golib’s old single line. That is the repository’s own ruled idiom for this layering, and it makes
an uninstalled renderer a strict non-regression rather than a wrong report. The alternative —
relocating ~400 lines of traceback machinery into golib so no hook is needed — was considered and
refused in the design: it rewrites machinery banked hours earlier (rows #160–#161) for no behavior a
consumer can observe.
The tee. SetCrashOutput’s descriptor slot moved from runtime/debug’s hand-own to golib, which
is where Go keeps it (runtime.crashFD, the symbol runtime_setCrashFD’s own //go:linkname
names); runtime_setCrashFD forwards. The asymmetry the test pins needs no rule — program output
reaches stderr through println/os.Stderr and never through the printer, so the descriptor only
ever receives the report. Exit code stays 2.
What the child now prints
TestSetCrashOutput re-executes the test binary, panics inside TestMain, and reads back:
panic: oops
goroutine 1 [running]:
runtime/debug_test.TestMain()
runtime/debug/stack_test.go:37
go.testing_runtime.TestHost.RunTests()
.../src/core/testing/TestHost.cs:271
…
identically from both the crash file and stderr, with hello present in stderr and absent from
the crash file, and the child exiting 2. All six assertions pass; before the arc all six failed.
The mixed frame spelling is the position map’s doctrine working as ruled, not a defect: TestMain
is converted Go and reports a Go position — runtime/debug/stack_test.go:37, which exists in
Go’s tree — while the frames beneath it are the hand-owned host and the BCL, which have no
conversion relationship to any Go source and honestly report their own. Go’s own report has
main.main() and _testmain.go there.
The arithmetic
4 matched · 5 divergent · 9 verdicts, exactly the previous entry’s table with its ninth row
flipped: TestReadGCStats, TestSetGCPercent (skip parity, issue 20076), TestSetMaxThreadsOvf and
now TestSetCrashOutput match; TestStack, three WriteHeapDump tests and TestFreeOSMemory are
the composed disclosures. The converter’s own verdict line: “Validated 4 tests against go test
(1 skipped identically on both sides, 5 disclosed-divergent (codegen-liveness, host-limit,
runtime-capability), 2 disclosed-unsupported declarations excluded).”
Roster, recomputed by summing its own table: 162 rows, 18,569 matching verdicts, 85 disclosed —
162 / 215 = 75.3% (161 / 18,565 / 80 before). The terminal marker of the 1.23.1 campaign; the
milestone ritual itself (MILESTONE-75pct-prep.md) is coordinator-owned and untouched here.
The five manifest entries, pinned as composed
Pasted verbatim from the previous entry’s block, and each signature re-verified against this lane’s own failure output rather than trusted:
| Test | Class | Signature, as pinned | Verified at this tip |
|---|---|---|---|
TestStack |
host-limit |
expected prefix "\ttesting/testing.go" |
✓ — the fifth-frame assert, exactly one line, TestExecution.cs:593 |
TestWriteHeapDumpNonempty |
runtime-capability |
WriteHeapDump is not supported by the managed runtime |
✓ |
TestWriteHeapDumpFinalizers |
runtime-capability |
(same) | ✓ |
TestWriteHeapDumpTypeName |
runtime-capability |
(same) | ✓ |
TestFreeOSMemory |
codegen-liveness |
less than 16777216 released: |
✓ — less than 16777216 released: 0 -> 3031040; the OLD no memory released: 0 -> 0 text would NOT have matched |
runtime-capability joins the roster preamble with this commit, per Ruling B — as
chan-direction did, not before — and the preamble’s count prose moves to five with its own history
sentence. host-limit’s bullet gains the permanent-retirement paragraph the position-map ruling
required: TestStack does NOT retire at a single-file host, and both the preamble and the pinned
reason say so.
2026-08-22 · LANDED + MEASURED — “the three bodies”: rawSyscallNoError and the struct stat mirror behind Fstat/fstatat close R4 and R1 on the Linux flavor; 7 (+1 validated at its Linux count) of the poll-seam lane’s 33 residual rows flip PASS, Linux roster 129/161 (+7 PASS / −8 FAIL / +1 COUNT over the 122 baseline) (lane R, claude/three-bodies-r4r1)
The assignment was the poll-seam entry’s R4 + R1 pricing — one body and two — implemented as per-GOOS hand-owns in the Linux syscall flavor and MEASURED against that lane’s own 122/161 baseline, W1-style: the rows the two classes attributed first, then the whole roster as the control; the i9 ran the Windows control of the same rows. R2 (the exec wall) and R5 (the sockaddr mirror) stayed off this lane, as routed.
What landed (branch claude/three-bodies-r4r1, tip = this entry's commit; the measured SHA for src/core is 9374cd61b (= c44b64500 + the value-type guard fix), src/core-identical to every later commit on the branch; the measured SHA is c44b64500, the hand-own commit)
- R4 —
rawSyscallNoError(syscall/linux/syscall_linux_impl.cs): Go’sasm_linux_amd64.sbareSYSCALL; MOVQ AX,r1; MOVQ DX,r2, i.e. the keystone libcsyscall(2)binding with the errno word dropped — not a swallowed error: its callers are the generated NoError family (Getpid/Getppid/Gettid/Getuid/Geteuid/Getgid/Getegid/Umask) plusforkExec’s getpid/getppid, syscalls that cannot fail. The file’s SCOPE paragraph now records why the announcing stub was retired (“until something genuinely needs them” —os/user.Current,time.interrupt,os/exec’s init genuinely did) and thatrawVforkSyscall/runtime_doAllThreadsSyscall/cgocallerstay announcing stubs. - R1 —
Fstat+fstatatover a blittablestruct stat(syscall/linux/zsyscall_linux_amd64_impl.cs, NEW): the convertedStat_tends in anarray<int64>managed reference (X__unused [3]int64), so it is not blittable and(uintptr)Ꮡstathanded the kernel the pinned MANAGED image (~128 bytes, CLR field order);fstat(2)/fstatat(2)wrote the 144-byte native record over it and 16 bytes past it — a quiet wrong answer (os.Stat(dir)→IsDir()==false,Mode()==p---------, err nil;Stat().Size()0) and a latent heap hazard, the class’s worst shape. The hand-own is the struct-passing class’s established remedy on Linux: a[StructLayout(Sequential)]NativeStatLinuxAmd64mirror (fixed int64[3]inline, size-checked at the boundary against 144), the keystone handed its address, a field-for-field copy into the convertedStat_ton success, errors and the nil-pointerEFAULTper the Go original, the path argument’sBytePtrFromString+ byte-box pinning kept verbatim (never the defect).Stat/Lstatstay auto (pure Go overfstataton linux/amd64). The two wrappers are displaced bymanualConversionFuncsunder a NEWgoosLinuxscope — darwin declares both names with libc-backed bodies that must keep working, Windows declares neither — guarded byTestLinuxOnlyEntriesAreScopedToLinux. The regeneratedlinux/zsyscall_linux_amd64.cs(seeded single-package reconvert at-platforms linux/amd64) differs by exactly the two placeholders. Marker census 64 → 65. - Guard: the
StatLayoutTruthbehavioral OUTPUT test — a temp tree;Stat/Lstat/File.Stattype bits, sizes, a saneModTime(theTimespeccopy);Glob,ReadDirwithInfo(),WalkDircounts;Getpid/Getppid/Getuid/Getgidas callable predicates — every line a boolean or a count, so Windows and Linux print the same bytes againstgo run. Registered ingo2cs.slnx(integrity 634/634), goldens viaUpdateTestTargets. - Deliberately NOT overlaid: the regen’s other delta,
syscall/linux/package_info.csgainingGoPositionMaprecords — PRE-EXISTING L3 drift from the position-map migration (at189ff02ccthe windows info file carries 8 records, linux/darwin 0); it is a per-GOOS leveling owed to the next Linux-flavor regen wave, not this fix.
Gates
| Gate | Result |
|---|---|
runtime/debug pipeline, -test-action all |
VALIDATES 4 + 5, exit 0 |
GolibTests (full) |
226/226 pass; 15 new crash-report guards, 5 proven failing-first |
| behavioral suite (full) | PASS 604/604 Transpile+Compile+Target, 578 Output compared, 0 failed, 26 skipped (1,673.5 s), zero tracked drift after 604 re-transpiles |
go2cs.slnx --no-incremental |
Build succeeded, 0 errors (31 m 17 s) |
converter go test ./... |
ok, exit 0 (235.7 s) — owed because a manifest is being committed |
| banking sweep (see below) | 8 pass / 0 fail, 2,262 verdicts, no count drift |
check-no-regression.ps1 |
not owed — no converter .go file moved, so no emission can have changed. The behavioral suite’s own Target phase byte-compared all 604 goldens (ok) and left zero tracked drift, which is the same evidence CNR produces |
The canary set, derived rather than remembered
The five-largest-reflect-consumer sweep derives its set at gate time by DIRECT import declaration
(Imports/TestImports/XTestImports from one go list -json std, never a text grep), per the
2026-08-20 ruling. At this tip that yields go/types (557), encoding/json (491), crypto/tls
(400), encoding/xml (386), html/template (243) out of 65 banked rows importing reflect
directly. That reproduces the set the atomic-align lane and the position-map lane each derived
independently, and the qualifying count tracks the roster as it should: 62 of 158 rows, then 64 of
161, now 65 of 162. go/internal/gcimporter (583, the largest banked row) is absent again, for the
already-recorded reason — it matches "reflect.Value" only inside expected-signature test DATA and
imports reflect nowhere — so the stale list is still the one CLAUDE.md carries, and re-deriving
is still what keeps it from being used.
Swept, each -Exact -TestTimeout 40m, plus database/sql (the panic-frame-sensitive row) and
sync, and the new row itself:
| Package | Verdicts | Result | Time |
|---|---|---|---|
sync |
44 | PASS | 40 s |
database/sql |
137 | PASS | 123 s |
html/template |
243 | PASS | 116 s |
encoding/xml |
386 | PASS | 109 s |
crypto/tls |
400 | PASS | 724 s |
encoding/json |
491 | PASS | 87 s |
go/types |
557 | PASS | 364 s |
runtime/debug |
4 | PASS | 25 s |
The set was not dispatched to the i9: a TO i9 · JOB-C2 entry must name a tip the i9 can fetch,
and this lane’s brief says to push nothing, so no tip exists off this box to bind. It was swept here.
Dirt
This row’s own dirt is empty, exactly as the previous entry recorded: a full -test-action all
produced no CRLF phantoms, no -tests-closure production re-flip, no package_init.cs hook and no
production .csproj change. The committed test sources, the manifest, the proof page, the index row
and the README badge are this bank’s deliberate additions. runtime/debug is the first banked row to
carry a fuzz seed corpus (testdata/fuzz/FuzzParseBuildInfoRoundTrip, three files, 118 bytes) —
only two GOROOT packages have one and the other is not banked — and it is committed because the
generated .tests.csproj references all three as <None Include=…>, so a committed csproj without
them would be broken.
The SWEEP found dirt in four OTHER packages, and it is banked-artifact staleness from two earlier arcs, not this one. Restored here rather than levelled, because levelling it would bury a bank under an unrelated regen — but recorded so it is not re-diagnosed:
encoding/xml,go/types,html/template—package_init.csgainsinitᴛᴛtests()and itsstatic partialdeclaration. The-testshook class; those three were banked before it was emitted.crypto/tls—package_test_info.csgains an empty<GoSourcePositionMaps>section. Its test artifacts were banked before yesterday’s position-map arc emitted that block at all.
No converter file moved in this lane, so neither can be attributable to it; all four packages passed at full expected count. This is precisely the banked test-source staleness charter §5 says the sweep is the only gate that can see.
Two harness findings, both budget rather than corpus, and both worth not re-paying:
- The behavioral suite’s first attempt hit the stock 2,400 s batch-build budget at 604
projects on this machine class and fell to per-project attribution — the exact false-red the
runner’s own note warns about. Re-run at
--build-timeout 9000it built clean in one shot. CLAUDE.md’s i7-5820K budget row wants raising; 2,400 s is no longer enough at this corpus size. - That same attempt’s Transpile phase hit its 60 s budget on one project and left
SubpackageFuncTypeParam.cszero bytes on disk. The next run’sUpToDatecheck then SKIPPED it — an empty file is still newer thango2cs.exe— and the batch build failed it withCSC : error CS5001: Program does not contain a static 'Main' method, which reads exactly like a converter regression. Deleting the truncated artifact and re-running that one project filtered passes all four phases in 50 s. A transpile timeout can leave a TRUNCATED.csthat the up-to-date check then protects — a new shape of the stale-output family. Here it surfaced as a false RED; the same mechanism could hide a real one.
2026-08-22 · The .NET 10 performance scout — same-silicon three-way: broad 10–20% JIT wins, String HALVES, three named regressions, and the bflat Fib anomaly attributed (lane G, claude/dotnet10-perf-scout)
Method. SDK 10.0.400 (GA-line — “current .NET 10”, not an RC) installed side-by-side to a user-local dir; the machine’s 9.0 default untouched. Because the corpus targets net9.0, the 10 leg selects the runtime via env (DOTNET_ROOT + DOTNET_ROLL_FORWARD=LatestMajor), verified by a FrameworkDescription probe (ambient .NET 9.0.18 → leg-B .NET 10.0.11 → restored) — both legs execute IDENTICAL IL, so the delta is pure runtime/JIT codegen. All legs same-day, same silicon (Ryzen 5 PRO 6650U, 6C/12T, G-LAPTOP — CORRECTED 2026-08-22 from an original “Ryzen 7 PRO 6850U … NOT the perf-canon 6650U host”: the pre-anchor fleet records had both laptops as 6850U, and this box probes as a 6650U, the perf-canon CLASS. Nothing in this entry moves — every figure here is a same-machine A/B and the ratios were always internal to this box — but the silicon is now named correctly per LANES.md), quiet box, run-performance.ps1 --no-aot, median-of-5 discipline unchanged. Go columns reproduce across legs within noise (e.g. Fib 118.3 vs 119.0) — the same-day control the method demands.
| **Execution time, milliseconds (Go | 9-JIT | 10-JIT | 10Δ vs 9):** |
| Benchmark | Go | .NET 9.0.18 JIT | .NET 10.0.11 JIT | Δ |
|---|---|---|---|---|
| Startup | 25.2/26.5 | 243.1 (9.66×) | 285.0 (10.75×) | +17% REGRESSION |
| Fib | 118.3/119.0 | 180.6 (1.53×) | 161.5 (1.36×) | −11% |
| Sieve | 67.7/77.0 | 110.5 (1.63×) | 98.4 (1.28×) | −11% |
| MatMul | 109.2/109.3 | 181.9 (1.67×) | 144.7 (1.32×) | −20% |
| String | 109.7/108.6 | 1,278.2 (11.66×) | 615.4 (5.67×) | −52% — HALVED |
| StringView | 18.7/19.3 | 21.2 (1.13×) | 19.1 (0.99×) | −10%, now AT Go parity |
| StringMatch | 197.4/196.9 | 1,005.6 (5.09×) | 873.6 (4.44×) | −13% |
| Map | 635.7/630.4 | 559.6 (0.88×) | 429.5 (0.68×) | −23% (already sub-Go, now 0.68×) |
| Sort | 144.9/145.0 | 429.9 (2.97×) | 366.9 (2.53×) | −15% |
| Channel | 41.9/41.3 | 83.7 (2.00×) | 105.8 (2.56×) | +26% REGRESSION |
| IfaceCall | 186.1/185.9 | 375.1 (2.02×) | 374.2 (2.01×) | ~0 |
| Iface | 95.8/96.7 | 523.4 (5.46×) | 567.2 (5.87×) | +8% regression |
| IfaceShell | 24.4/21.7 | 875.4 (35.86×) | 772.8 (35.59×) | −12% |
| RefLower | 234.5/238.8 | 605.2 (2.58×) | 654.2 (2.74×) | +8% regression |
Reading for the hop: the wins land exactly where the transpiled corpus hurts most — the String family (the 11.66× embarrassment halves to 5.67×; StringView reaches Go parity), dense compute (MatMul −20%), and Map (already beating Go, now 0.68×). The regressions are narrow and named: Startup +17% (JIT-path process start — AOT is the startup story anyway), Channel +26% (synchronization-heavy; worth a targeted look at the hop), Iface/RefLower +8% (interface dispatch shapes). Net: the hop’s CPU expectation is a solid single-digit-to-20% improvement across most of the corpus with a >2× win on string-heavy code, financed by two narrow regressions to re-measure at hop time.
The bflat breadcrumb (PLAN-bflat-perf-exploration.md Finding 4) — attributed: bflat’s Fib 70.9 ms is NOT generic .NET-10 codegen: the 10-JIT Fib is 161.5 ms (−11%, nothing like halving). AOT controls (same-day, both SDKs’ ILC, this box): 9-AOT Fib 178.2 ms (ILC 9.0.19, 964 s publish), “10-AOT” Fib 177.1 ms — identical (1,138 s publish) — and the identity is STRUCTURAL: SDK 10.0.400 publishing the net9.0 TFM resolves Microsoft.DotNet.ILCompiler/9.0.19 (runtime-pack-versioned), so no AOT leg reachable from this corpus runs ILC 10 at all. Attribution: the bflat halving requires the net10 ILC+framework PAIR bflat ships — nothing reachable from the net9.0 corpus reproduces it (10-JIT gives −11%, SDK-10 AOT is ILC-9-identical). It becomes measurable exactly AT the hop, and stands priced as a to-verify upside there, not as evidence for pre-hop action.
RC/toolchain friction for the hop plan: none encountered on the JIT path — SDK 10.0.400 installed side-by-side cleanly (official dotnet-install, -NoPath), env-based runtime selection worked first try, net9.0 IL runs on 10.0.11 under LatestMajor with zero NETSDK/analyzer noise, and the go2cs-gen analyzer (netstandard2.0) loaded unmodified. AOT-path friction, all measured: (1) the ILC runtime-pack binding above — the hop plan must not expect pre-hop AOT measurements; (2) the runner’s up-to-date check REUSES a stale publish across SDK-env changes (a 51 s “10-AOT leg” re-measured the 9-ILC binary; purge the benchmark’s bin/obj before any cross-SDK A/B); (3) Roslyn 10 newly warns CS7022 on PerformanceRunner’s top-level-statements + Runner.Main shape (benign); (4) a net9.0 app under the 10 SDK still RUNS on the 9 runtime — JIT legs need DOTNET_ROOT + DOTNET_ROLL_FORWARD=LatestMajor, verified by a FrameworkDescription probe, or the “10 leg” silently measures 9.
Discipline notes: no corpus changes, no version.props changes, no global.json committed — the worktree is byte-clean apart from the runner’s own regenerated artifacts (restored). Numbers are THIS box’s; the perf-canon README stays authoritative for its own host and was not touched (the runner’s README mirror was left unstaged/restored).
| converter go test ./... (Windows) | ok, 294 s (incl. TestLinuxOnlyEntriesAreScopedToLinux and the L3 corpus walk admitting the new linux/ companion) |
| syscall.csproj linux flavor, NATIVE in the distro (+ os, its first consumer) | 0 errors (syscall), 0 errors (os) at 9374cd61b |
| syscall.csproj windows flavor (untouched) | 0 errors (the Windows compile set does not contain the two new files; the registry entries are linux-scoped) |
| go2cs-stdlib.slnx -p:GoTargetOS=linux (NATIVE, --no-incremental) | 0 errors, 149 warnings — unchanged from the poll-seam lane’s gate (716 s) |
| go2cs-stdlib.slnx -p:GoTargetOS=windows | 0 errors (6:53) |
| GolibTests | 213/213 |
| check-no-regression.ps1 (converter registry changed) | NO REGRESSION — byte-identical across all 632 behavioral packages (2 advisory converter warnings): the linux-scoped registry entries have zero behavioral emission footprint |
| behavioral --filter StatLayoutTruth (Windows) | Transpile/Compile/Target/Output all PASS |
| solution integrity | 634/634 registered, path casing OK |
The measurement — Linux roster re-run at c44b64500 against the 122 baseline
Harness as the poll-seam lane’s: the repo’s own run-validated-sweep.ps1 -Filter <pkg> -Exact -SkipBuild per row under pwsh 7.5.4 on laptop R’s WSL2 Ubuntu-22.04, _paths.ps1’s Linux pins, -TestTimeout 20m base with the script’s floors, per-row logs + resume ledger, detached; the native linux slnx build first so the 161 per-package builds go incremental. Order: the 13 R1/R4-attributed rows, then the other 26 residuals of the 122/161 baseline, then the 122 PASS rows as the control.
The 13 attributed rows — every attribution cleared exactly as predicted:
- 7 flip to sweep-PASS at their banked counts:
archive/zip100 (was “not a valid zip file” ×45 from a mis-sized archive),debug/dwarf40 (Glob over testdata finds its 11 ELF fixtures),html/template243,io/ioutil28 (Stat size 0 want 3302and itsGetuidroot-guard test both),io/fs18,internal/diff13,archive/tar97 (os/user.Current→Getuidworks; the NRE aftermath is gone). - 1 validates at its Linux count:
path/filepath54 of 54 matching, comparison VALIDATED — reported as COUNT because the banked 61 is the Windows-shaped count (with its six host-conditional symlink subtests); the same W4 per-OS arithmetic asmimeandcrypto/rand. The wall-to-wall R1 row (Walk marks 0, Glob empty, EvalSymlinks “not a directory”, MkdirAll) is fixed. - 5 are improved to a residual of a DIFFERENT class:
time156/157 (TestSleep’sKill(Getpid())flipped; R6TestEnvVarUsageremains),go/doc/comment10,058/10,059 (all 53TestTestdatasubtests flipped;TestStd= R2),text/template51/52 (bothParseGlobtests flipped;TestLinkerGC= R2),os/exec— the R4 PACKAGE-LEVEL death is gone and the host now runs, 16 of 72 verdicts, the remainder the exec wall, exactly the “R2 behind R4” the poll-seam entry predicted — andgo/internal/srcimporter4/7 (the R1 tests flipped;TestImportStdLibfails on the GO side on this host,TestImportedTypes/TestCgoareMustHaveGoBuild/go tool cgopaths, R2 by code path). Re-measured after the control withbuild-essentialinstalled (CGO_ENABLED=1— JOB-005’s root, the same on this distro): the GO side then passes all 7, the C# side stays 4/7 with the same three as infrastructure-errors — a real R2 residual, not a host artifact; the F15 recipe gains the C-toolchain line on this branch.
The other 26 baseline residuals: all 26 re-measured at exactly their baseline sub-counts (bulk check of each row’s comparison JSON: encoding/json 490/491, sync 43 + 7 disclosed / 51, math/rand 36/43, go/types 504 matched of 555 produced, go/internal/gcimporter 15/281, crypto/ecdh 46/47, crypto/ed25519 7 + 1 / 9, bytes 82 + 6 / 92, crypto/sha1 12 + 1 / 14, flag 23/24, internal/cpu 6/8, sync/atomic 89/108 before its W7 timeout, crypto/tls/os/signal/syscall conversion-blocked, mime and crypto/rand validated at their Linux counts, …) — none was attributed to R1/R4 and none moved, up or down. Their classes stand as the poll-seam entry recorded them: R2 ×16, R5 ×2, W1b ×2, R3 ×1, W2 ×2, W4 ×2, W6 ×1, W7 ×1
Control — the 122 baseline PASS rows: 122 of 122 still PASS — zero regressions (4 h 40 m of row time in all; crypto/dsa 2,444 s and hash/maphash 2,026 s under the concurrent Windows gates, both inside their floors). With the three validated COUNT rows (mime 18, crypto/rand 302, path/filepath 54 — each comparison VALIDATED at its Linux count), 132 of 161 rows now validate functionally on Linux.
Headline: 129 PASS / 29 FAIL / 3 COUNT of 161 (+7 PASS / −8 FAIL / +1 COUNT over the poll-seam lane’s 122 / 37 / 2); 7 (+1 validated at its Linux count) residual rows flip to sweep-PASS, +1 validates at its Linux count. Windows control (JOB-R3, i9, the 39 baseline non-PASS rows at 5e5bf29c6): 39/39 PASS, 0 FAIL, 14,148 verdicts — the change is linux-flavor-only, measured.
What it does and does not change on the board
- R1 and R4 are closed for the rows they were attributed to. The struct-passing class has its first Linux members retired the way the Windows members were (mirror + copy, registry-displaced), and the NoError raw bottom is no longer a stub. What remains behind those rows is R2 (the exec wall — now the dominant Linux seam: 18 rows carry it as their ONLY residual), R5 (the sockaddr mirror, 2 rows incl.
crypto/tls), W1b (mmap, 2), R3 (1), R6 (1 test), and the per-OS count arithmetic (W4:mime,crypto/rand, nowpath/filepath). - Two small things for whoever touches the Linux flavor next:
syscall/linux/package_info.csowes theGoPositionMaprecords its windows sibling carries (pre-existing L3 drift, deliberately not overlaid here); and a barego2cs.exe <dir>on a machine with a stale deployed~/go2cssilently mints an EMPTY alias block (the default root is a Feb-2025 deploy on this laptop; only a MISSING root warns) — CLAUDE.md’s standing-go2cspathrule, silent variant named. - No roster changes (Linux rows do not formally bank until the per-OS arithmetic ruling); the deliverable is the measured flip count above.
2026-08-22 · LANDED + MEASURED — the exec wall falls: posix_spawn at the forkExec seam, and the R2 ladder flips 10 of 19 with every residual named (lane G, claude/exec-wall-impl)
The ratified design (DESIGN-linux-exec.md, all seven OQs) is implemented and measured. Three commits carry the arc: the OQ-6 GoFrame foreign-unwind fix (sequenced FIRST per ratification — failing-first guards red→green, so every residual below names its true exception instead of panic: nil), the whole-file hand-own src/core/syscall/linux/exec_unix.cs (forkExec rewritten on posix_spawn — fd shuffle as parent-computed file_actions data, pgid/sid/sigmask via spawnattr, addchdir_np probed-by-call for Dir, unmapped SysProcAttr fields failing by NAME, PidFD filled post-spawn via pidfd_open [OQ-4’s door, opened early: race-free pre-first-wait], every native buffer unmanaged-for-the-call-window per the exec_windows soundness rule; everything else in the file converted-verbatim, exec_linux.cs dead code untouched), and the LinuxSpawnBasics behavioral test (one golden, both spawn seams: Windows four-phase PASS at creation).
The 19-row Linux ladder at the tip (45m budgets, per-row logs in the distro’s /root/r2-logs/):
- 10 PASS at banked counts:
internal/godebugs1 ·internal/testenv7 ·crypto6 ·crypto/ecdh47 ·crypto/ed255198 ·go/importer3 ·text/template52 ·internal/types/errors155 ·go/doc/comment10,059 (the roster’s largest suite, spawninggo buildthroughoutTestStd) — plusgo/internal/gcimporter582/583 (go list stdenumeration works end to end; one verdict short, COUNT row). os/execitself: from 0-can’t-init (census) → 16/72 (three-bodies) → ~71/74 with three named residuals:TestCredentialNoSetGroupsthrough the NAMED credential wall under the root harness (OQ-3’s posture change is the fix, not code),TestPipeson a NEW stub (hasWaitingReaders), a danglingTestWaitInterrupt/SIGQUIT, andTestExtraFilesRacepanicking insyscall.sockaddrundernet.Listen— R’s R5 territory precisely; their in-flight mirror fixes that exact line (measured coordination, not collision: the residual is theirs by class, no file contention).- The other named next-walls (each with retained logs, none chased in this lane):
go/internal/srcimporteron a newsetUsesCgostub;math/randTestDefaultRace= race-detector capability divergence (the re-exec’d child cannot produce a race report);internal/abiTestFuncPCCompileError= go-toolchain-interaction (“go tool compile did not fail”);debug/gosymreading PE magic (MZ) where ELF is expected — a toolchain-output mystery worth its own root; and a four-row package-verdict-only class (sync,flag,debug/buildinfo,go/types) where every named test MATCHES and only the package summary diverges — the comparison’s summary seam, likely one root.
Gates: CNR byte-identical across all 633 behavioral packages (LinuxSpawnBasics included) · GolibTests Windows 232/232 expected (230 + the two OQ-6 guards; spawn-seam gates linux-compile-gated) · distro spawn gates (§5.1 synchronous-ENOENT + OQ-2 reaper-under-GC): both PASS — the synchronous contract measured (ENOENT from the call itself), the CLR reaper measured pid-targeted (unobserved child survives 8 forced gen-2 collections to a delayed Wait4) · Windows behavioral full suite: PASS, 606 projects (all four phases, 1,959 s — the GoFrame change proven across every defer/panic path at runtime) · Windows full-roster control (JOB-G1, i9): **all 162 rows / 18,569 verdicts accounted (first pass 160/2; both reds — a crypto/ecdsa CLR access-violation and a phantom CS8983 blaming a time/sleep.cs that compiled clean 11 rows later in the SAME run — retried green in isolation; three data points incl. an environment-invariant srcimporter flip converge on box-transient full-roster contention, and the retry arithmetic closes on the exact banked total)** — breadth deliberate, the GoFrame change touches every defer path · marker census 66 (line-anchored, re-measured; exec_unix.cs new) · syscall linux-flavor build 0 errors.
What “Linux parity” looks like after this arc: the R2 wall is DOWN as a class — process spawning works, the residuals are five SMALLER named seams (R5-sockaddr [R’s, in flight], hasWaitingReaders, setUsesCgo, the summary-verdict seam, the gosym toolchain mystery) plus two capability disclosures (race detector, root-posture credentials pending OQ-3’s harness line). Rows do not formally bank on Linux until the per-OS arithmetic ruling; the flip arithmetic above is the measured input to it.
2026-08-22 · LANDED + MEASURED — the “summary-verdict seam” dissolves into five measured roots; four rows flip on Linux, one validates at its per-OS count, and a GC-corruption wall class gains its Linux instance (lane G, claude/summary-seam)
The assignment’s premise corrected first, per the measure-don’t-guess discipline: the census’s four-row “package-verdict-only” class was a LOG ARTIFACT — my ladder’s sweep-log extraction saw only divergent records, and the sweep prints package rows when named tests match; the comparison JSONs held ordinary per-test residuals throughout. What the rooting actually found was better than one seam: a causal CHAIN of five, each measured into place, several load-bearing far beyond these rows.
os.Args[0]under an apphost was the managed assembly, not the process image (runtime/goargs_impl.cs). Probed both launch modes:GetCommandLineArgs()[0]reports the.dllin BOTH;Environment.ProcessPathis the apphost (apphost mode) or thedotnetmuxer. Everyexec.Command(os.Args[0])self-re-exec —syncTestMutexMisuse,flagTestExitCode, the idiom across Go’s suites — died onfork/exec …dll: permission denied. argv[0] is now ProcessPath when the process is not the muxer; muxer mode keeps the documented mirror. (os/execwas immune all along: Go 1.23 moved its helpers totestenv.Executable→ procfs.)- The pidfd door, closed wrongly and reopened by strace. A
-1sentinel inSysProcAttr.PidFDis NOT “unsupported” at os’s layer —ensurePidfdplants the field on its own kernel probe andgetPidfduses it UNCHECKED (waitid(P_PIDFD, -1, …) = EINVAL, every exit code -1). The spawn seam now fills a realpidfd_open(pid)— race-free pre-first-wait — and OQ-4’s deferral is re-scoped to “the fill is required; the WAIT path needed the fix below”. SiginfoChildwas a memory-corruption instance of the non-blittable-out-param wall (internal/syscall/unix/linux/siginfo_linux.cs, now a blittableSize=128hand-own). Go’s padding fields (_ [is64bit]int32,__ [100]byte) emit as golibarray<T>— managed CLASS REFERENCES — so every kernel offset past 12 shifted by 8:Statusread from a dead offset (every child exit = 0, flag/os-exec’sgot 0/got -1era), and the kernel’sPidword wrote RAW BITS OVER A LIVE GC REFERENCE. That is the WindowsTimezoneinformationclass exactly, remedied its established way — and it is a standing hazard pattern wherever the kernel writes into converted structs with array padding. Named-not-claimed lead: the fleet’scrypto/ecdsaCLR access-violation watch-item is precisely this corruption’s shape.- Two linkname rows the exec seam made reachable (converter + linux regen):
go/types.srcimporter_setUsesCgo(pull, handle-authorized, textproto’s shape) andsyscall.hasWaitingReaders(push from sync, bare shape, os.runtime_args’ precedent). One lesson each: the sweep collapses BUILD errors into bareFAIL <pkg>rows with zero diagnostics (the by-hand doctrine is what exposedCS0117behind three batches of silence — harness honesty item, priced), and a regen cannot reach a hand-own — sync’s push source went public only inrwmutex.cs.auto; the live hand-owned file carries the access change BY HAND with a comment naming the registry row. - The verdicts at the tip (Linux, banked counts):
syncPASS 44 ·flagPASS 24 ·go/internal/srcimporterPASS 7 ·go/typesPASS 557 — the roster’s second-largest suite, full count on Linux ·debug/buildinfocomparison-VALIDATED at 204 vs banked 197 (gcc un-gates the c-shared/pie subtests; the crypto/rand W4 per-OS-count class, one more row for the arithmetic ruling).os/execremains FAIL with a characterized residue — a mid-suite host death leaving acs=Noneblock, theTestLookPathdot-semantics family, and the root-posture credential case (OQ-3’s harness line still pending) — priced follow-on, logs retained.
Gates: converter go test 192 s green · CNR: byte-identical across all 633 behavioral packages (the linkname rows change nothing the behavioral corpus reaches) · GolibTests Windows: 230/230 (the two linux-gated spawn gates excluded by flavor here; green on the distro) · Windows stdlib slnx 0 errors, sync/syscall Windows rebuilds 0 errors (the linux-flavor files never compile there; the public push source is benign) · Linux flavor proven by the sweeps themselves · marker census 67 (line-anchored, re-measured; siginfo_linux.cs and exec_unix.cs this lane’s additions).
2026-08-22 · LANDED + MEASURED — the sockaddr mirror on Linux (R5, a PREREQUISITE that moves the socket wall from sockaddr() to FD.Init), the GoPositionMap leveling of syscall/linux, and W1b ROOTED to golib’s native-snapshot slice (lane R, claude/sockaddr-mmap-posmap)
The assignment was the poll-seam entry’s two two-row seams plus my own position-map finding. Two of the three re-sized under measurement before a line was written, and the entry says so first: R5’s two rows cannot flip from the sockaddr mirror alone, because a Linux socket is un-armable until a readiness poller exists (the mirror is the poller’s prerequisite, landed here); and W1b is not a syscall hand-own at all but golib’s unsafe.Slice-over-native SNAPSHOT (rooted by probe, priced, routed out by the coordinator as its own golib DESIGN increment — the two rows wait honestly). The third — the GoPositionMap drift — is leveled.
What landed (claude/sockaddr-mmap-posmap: lane work at a38f6c7da, merged with master 1d6542c73 as b2b271209; this entry’s commit is the tip; measured SHA e7800600d)
- R5 — the sockaddr family on the Linux flavor (
syscall/linux/sockaddr_linux_impl.cs, NEW; the L10 Windows hand-own mirrored arm for arm): the two INET encoders write the port arithmetically instead of through the(*[2]byte)alias that converts to a length-zeroarray<byte>;Bind/Connectbuild the nativesockaddr_in/_in6/_un/_ll/_nlimage in a stack buffer (writeNativeSockaddr, which calls Go’s ownsockaddr()first so there is ONE definition of what a Sockaddr means) and hand its address to the package’s own generated address-takingbind/connect;Getsockname/Getpeername/Accept4go through the trampoline with a stack buffer (their generated wrappers take a typedж<RawSockaddrAny>the kernel would fill by address) and decode withreadNativeSockaddr— Go’sanyToSockaddrarm for arm over a native image; andanyToSockaddritself becomes a flatten-then-decode of the managedRawSockaddrAny(Family at 0, Data 2..15, Pad 16..111), so any remaining auto caller decodes correctly once its own fill is. Displaced bymanualConversionFuncs: the encoders +Bind/Connect/Getsockname/Getpeernamenow carry agoosWindowsLinuxscope (one entry, each flavor’s file its own authority — the lock_sema/lock_futex precedent; darwin keeps its libc bodies),Accept4/anyToSockaddrgoosLinux; guarded byTestSockaddrFamilyIsScopedToEachHandOwningFlavor. Not covered, named:Recvfrom/Sendto/Recvmsg/Sendmsg(UDP/ancillary; L10 drew the same line). Marker census 65 → 66 (line-anchoredgit grepoversrc/core). - The
GoPositionMapleveling:syscall/linux/package_info.csgains the 21 position-map records its windows sibling already carried (the position-map migration’s Windows-host regen never re-emitted the per-GOOS linux info file) — taken from the same seeded single-package regen that produced the placeholders, so the records match the current converted files line for line. darwin’s info file has the same drift and is left for its own lane (that corpus does not build). - W1b — ROOTED, not fixed here. Probe (converted program, isolated clone):
syscall.Mmap(0, 0, 3·pagesize, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE)returns a 12,288-byte slice with err nil;Mprotect(b[:pagesize])→invalid argument;Munmap(b)→invalid argument; writes “succeed” into a copy. Mechanism (from source,unsafe.cs):unsafe.Sliceover a NATIVE pointer SNAPSHOTS the bytes into a managed slice — golib’s documented limitation — soMmap’s[]byteis not the mapping, andMprotect/Munmaphand the kernel a managed element address. The honest fix is a native-backedslice<T>in golib (aMemoryManager<T>over the mapping;Ꮡ(b, i)and(uintptr)yielding the native address; lifetime owned by the mapping) — a slice-model change with GolibTests + behavioral gates, outside “per-GOOS hand-owns”. Routed by the coordinator (mailbox, 2026-08-22): NOT taken in-lane — the slice model is settled-doctrine surface (identity, reslicing, append, GC pinning; adjacent to the ж-box family’s findings), so it queues as a commissioned design increment with this probe and the two rows as its measured bill;crypto/sha1andbyteskeep their W1b residual honestly until it lands.
Gates (measured SHA e7800600d, tip = this entry’s commit)
| Gate | Result |
|---|---|
converter go test ./... (Windows) |
ok, 309 s (incl. TestSockaddrFamilyIsScopedToEachHandOwningFlavor and the L3 corpus walk admitting the new linux/ companion) |
syscall.csproj + net.csproj linux flavor, NATIVE in the distro |
0 errors / 0 errors |
syscall.csproj windows flavor (untouched) |
0 errors |
syscall.csproj + net.csproj linux flavor, NATIVE, at the MERGED tip b2b271209 (this lane + G’s exec-wall in the same assembly; probe clone, --no-incremental) |
0 errors / 0 errors — the union gate |
go2cs-stdlib.slnx -p:GoTargetOS=linux (NATIVE, --no-incremental) |
0 errors, 149 warnings — unchanged count (692 s) |
go2cs-stdlib.slnx -p:GoTargetOS=windows |
0 errors |
| GolibTests (golib untouched) | 228/228 |
check-no-regression.ps1 (converter registry changed) |
NO REGRESSION — byte-identical across all 632 behavioral packages (2 advisory converter warnings): the scope change has zero behavioral emission footprint |
| solution integrity | CNR preflight: all 634 behavioral projects are registered; all 4555 tracked behavioral paths, path casing OK |
Windows control (JOB-R4, i9, 8 rows: encoding/json, crypto/tls, the six banked net/*) |
8/8 PASS, 0 FAIL, 1,010 verdicts (crypto/tls clean; i9, JOB-R4, at a38f6c7da) |
The measurement — Linux roster re-run at the measured SHA against the 129 baseline
Harness as the previous two lanes (the repo’s own sweep per row under pwsh, _paths.ps1’s Linux pins, -TestTimeout 20m + floors, per-row logs + resume ledger, detached, the native linux slnx build first). Order: encoding/json, crypto/tls, crypto/sha1, bytes, then the other 28 residuals of the 129 baseline, then the 129 PASS rows as the control.
R5’s two rows, the measured yield: both still FAIL, and both MOVED exactly as priced — encoding/json stays 490/491 with TestHTTPDecoding now failing as httptest: failed to listen on a port: listen tcp6 [::1]:0: operation not permitted (the sockaddr() index-out-of-range panic is gone; Bind succeeded; FD.Init → pollDesc.init → runtime_pollOpen’s EPERM is what net.Listen now returns), and crypto/tls’s package-level infrastructure-error is operation not permitted from its TestMain listener instead of the sockaddr panic. The socket wall is now FD.Init, i.e. the poller — the next lane’s design.
W1b’s two rows: crypto/sha1 and bytes unchanged (FAIL; the W1b shape — routed out, waiting honestly).
The other 28 baseline residuals: all 28 at exactly their baseline C# sub-counts (bulk check of each row’s comparison JSON — path/filepath validated 54/54, time 156/157, go/doc/comment 10,058/10,059, text/template 51/52, go/types 504, sync 43 + 7 disclosed, mime/crypto/rand validated at their Linux counts, os/signal/syscall conversion-blocked, sync/atomic 89/108 before its W7 timeout, …). The only movement is on the GO side of cgo-dependent suites now that build-essential is installed on this distro (JOB-005’s fix, applied after the three-bodies control): debug/buildinfo 197 → 204 Go verdicts, go/internal/gcimporter 581 → 582, go/internal/srcimporter 7/7 Go-pass — the C# side is unchanged in every one, so these are a Go-baseline effect, not a corpus one; the roster’s expected counts remain the Windows-shaped ones until the per-OS ruling
plugin — W3 re-exposed by gcc, the one control row that moved, attributed and REPRODUCED: it FAILs in 5 s at conversion time with the census’s exact W3 signature (go2cs panic at conversionDriver.go:228). Root, from the isolated probe clone at the measured SHA: with CGO_ENABLED=1 — build-essential has been on this distro since the three-bodies control (JOB-005’s fix) — plugin’s cgo file enters the package load, pkg.Syntax carries the cgo-processed file that pkg.GoFiles does not, and line 228 indexes pkg.GoFiles[i] by pkg.Syntax’s i → panic: runtime error: index out of range [2] with length 2; the same conversion under CGO_ENABLED=0 succeeds (exit 0). So the poll-seam and three-bodies lanes’ PASS plugin 1 was the CGO-off artifact and the census’s W3 verdict was right; the class is a converter defect with a one-line remedy (name the file from pkg.Fset.Position(file.Pos()).Filename, or walk CompiledGoFiles, rather than GoFiles[i]) — NOT taken in this lane (the converter is out of its scope beyond the registry), priced for whoever takes W3. It is the same Go-environment effect as the cgo-dependent count moves above, in its conversion-time form.
Control — the 129 baseline PASS rows: 128 of 129 stay PASS. The one mover is plugin — W3 re-exposed by the distro’s gcc, reproduced and attributed above; not a lane effect.
Headline: 128 PASS / 30 FAIL / 3 COUNT of 161 (−1 PASS / +1 FAIL, and that one is plugin’s environment flip, not the change, over the three-bodies lane’s 129 / 29 / 3) — by design, **0 flips: the lane’s yield is the moved wall, the leveled info file, and the rooted W1b, not rows.** Windows control (JOB-R4, i9, 8 rows at a38f6c7da): 8/8 PASS, 1,010 verdicts.
The Linux socket wall, priced (for routing)
With the sockaddr mirror landed, the Linux socket path is socket() → Bind/Connect OK → FD.Init → pollDesc.init → runtime_pollOpen → EPERM → net.Listen/Dial return operation not permitted. Closing it is the Linux readiness poller the netpoll design’s §8 names as a separate design; the simplest honest shape, priced for routing (NOT started in this lane):
- The poller itself, in
internal/poll/linux/runtime_netpoll_impl.cs(the file that today answers EPERM) — two shapes reach the kernel from the corpus as it stands, and the design note prices them against each other rather than this entry choosing: (i) the polling-loop shape — eachpollWait(ctx, mode)blocks the calling thread inpoll(2)on its one fd (SYS_POLLthrough the keystonesyscall(2)binding) with the deadline as the poll timeout, woken for unblock/close/deadline-change either by bounded slices or by a per-desc eventfd; no shared thread, no epoll surface, but a second fd per descriptor (or a slice’s latency on close) and — becausepoll(2)accepts regular files — its ownfstatto keep Go’s refusal of files (os.File.SetDeadlinemust still answerErrNoDeadline); (ii) Go’s own shape — oneepoll_create1instance, one eventfd, one background drain thread, and the Windows flavor’s managed descriptor state machine (Ready/Expired/generations/Timer,pollBlock/pollReset/pollSetDeadline/pollUnblock) lifted verbatim,EPOLL_CTL_ADDwithEPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLETexactly asruntime/linux/netpoll_epoll.csarms it, the kernel refusing regular files with EPERM for free. Both are per-GOOS hand-owns, not golib changes: thesyscallflavor already reachesSYS_EPOLL_CREATE1/CTL/WAIT,SYS_EVENTFD2,SYS_POLL, and itsEpollEvent(uint32,int32,int32, 12 bytes) is blittable — the kernel-facing buffers go through the keystone as stack/native images, never as ж addresses (the sockaddr andstruct statmirrors’ rule). Wake-vs-readiness separation, one waiter per mode (fdMutex above), the four-code contract and the §5 deadline semantics are the Windows file’s, verbatim, under either shape. - Yield, measured from this lane’s residuals:
encoding/json’sTestHTTPDecoding(1 test) andcrypto/tls’s whole suite (3,646 verdicts behind itsTestMainlistener) on the roster; off-roster, everynet/net/http/net/smtp/net/rpcLinux leg. Gate: theTcpLoopbackRoundTrip/NetDeadlineMatrix/NetListenSmokebehavioral tests already exist for Windows and would be the Linux guard once the runner binds the linux flavor. - W1b’s golib item (the native-backed
slice<T>) is orthogonal and smaller in rows (2) but larger in reach (unsafe.Sliceover any native pointer,syscall.Environ, future mmap’d I/O).
2026-08-22 · DESIGN PROPOSED — the Linux readiness poller: epoll + one drain thread + the Windows descriptor state machine, priced against the poll(2) loop; the socket wall’s remedy, not yet its landing (lane R, claude/linux-poller-design)
Commissioned on the mailbox at the close of the sockaddr-mirror lane (“the design note, not the implementation”), written per the ReadMemStats template: docs/phase4/DESIGN-linux-readiness-poller.md, STATUS PROPOSED, nine open questions each with a recommendation, none self-ruled. It is the half the Windows netpoll design’s §8 deferred, and it inherits that design’s §2 contract inventory and §5 deadline story verbatim rather than re-deriving them.
- The bill, measured (§1): with R5 landed the Linux socket path is
Bind OK → FD.Init → pollDesc.init → runtime_pollOpen → EPERM → "operation not permitted"(the exact strings fromencoding/json’sTestHTTPDecodingandcrypto/tls’sTestMain). On the roster the poller flips two rows and 3,647 verdicts —encoding/json490→491,crypto/tls0 of 3,646 Go-enumerated → runs (a W4 per-OS count) — and off it:PipeCloseUnblocksReadprints Go’s line, the three Windows netpoll guards (NetListenSmoke/NetDeadlineMatrix/TcpLoopbackRoundTrip) become the Linux guards, the S3 socket ledger’s Linux legs (net/smtp,net/http/{httptest,cgi,httputil,cookiejar},net/rpc) reach their Windows state, andos’s pipes regain Go’s semantics (deadlines,Closeunblocks). What it does not move is named: R2, W1b, R6, W2–W7, and the un-mirrored UDP/ancillary wrappers. - The mechanism space (§3), five candidates in one table: A
poll(2)per waiter + a per-desc eventfd; A′poll(2)in bounded slices (the sockaddr entry’s sketch); B epoll + ONE background drain thread + the Windows flavor’s managed desc/gate/deadline machinery lifted verbatim (Go’s ownnetpoll_epoll.gominus the scheduler); C .NETSocketover the fd; D wiring the converted runtime poller. Recommended: B — it owns one descriptor per process (A owns one per socket, visible toos/exec’s fd enumeration), it gets Go’s regular-file refusal from the kernel’s EPERM for free (A needs anfstatper open to keepos.File.SetDeadlineansweringErrNoDeadline), it adds zero syscalls to a blocked waiter (A′ taxes every idleAccept~20 wakeups/s), and ~200 gated lines (pollBlock/pollReset/pollSetDeadline/pollUnblock, generations,Timers) copy across. C is socket-only and takes over the fd; D is the Windows design’s §3.2 scheduler wall, unchanged. - The design (§4): edge-triggered
EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLETexactly as Go arms it, with the no-lost-edge argument written once (the consumer only waits after the kernel said EAGAIN;prepareclears; one waiter per mode);epoll_event.datacarries an opaque token (fd reuse is harmless by construction; table insert BEFOREEPOLL_CTL_ADD,EPOLL_CTL_DELBEFOREclose(2)—FD.destroy’s own ordering); Go’s mode mapping andEventErr(pollErrNotPollableon the read side); every kernel byte a native image through the keystonesyscall(2)binding — no ж address, no generated address-taking wrapper; no break eventfd (nothing needs to interrupt the drain thread — deadlines and unblocks reach waiters directly,EPOLL_CTL_*take effect inside an in-progress wait);EINTRretried as the normal case (never-restarted syscall; the CLR’s and G’sSIGCHLDhandlers will deliver it); deadlines are §5 of the Windows design with the cancel-and-harvest dimension GONE (a Linux timeout wakes a thread that owns nothing). One file, replaced in place (internal/poll/linux/runtime_netpoll_impl.cs); Windows flavor, golib, converter, keystone,os/net/syscalluntouched; under ⟨OQ-9⟩’s safeMarshalforminternal.poll.csprojis untouched too. os/os/execadjacency (§5), stated for G’s arc: pipes arm (the parent’s ends of a child’s stdio go from blockingread(2)to EAGAIN + park; the child’s ends stay blocking becauseos.File.Fd()→SetBlockingclearsO_NONBLOCKon THAT descriptor — Go’s existing discipline, which posix_spawn inherits through the sameProcAttr);epfdisEPOLL_CLOEXEC;TestExtraFilessees it andIsPollDescriptoranswers truthfully. A mailbox FYI to G goes with ratification.- Gates (§7): S0 four probes in the distro (EPERM on a regular file/dir, the 12-byte stride guard, the
EINTRrate,ADDduring an in-progress wait); S1 the file + the linux-native slnx + the Windows slnx control + GolibTests + the four behavioral programs run by hand on the distro (⟨OQ-6⟩) + the 161-row Linux re-run W1-style (control: the 129 stay PASS — the regular-file fallback via the kernel’s errno; measurement:encoding/json→ 491,crypto/tls→ runs) + the i9 8-row Windows control by JOB; S2 the deadline matrix andcrypto/tls’s Linux residuals; S3 the socket ledger’s Linux legs. CNR is not owed (no converter change). - OQs (§10): ⟨1⟩ mechanism → B; ⟨2⟩ eventfd → omit; ⟨3⟩ drain-thread failure → fail loud (Go’s
throw), never catch-and-continue; ⟨4⟩ regular-file refusal → the kernel’s EPERM alone; ⟨5⟩pollWaitCanceled(no Linux caller) → the shared loop, notUnreachable; ⟨6⟩ the Linux behavioral guards → by hand on the distro now, the runner’s linux binding as a harness item; ⟨7⟩ desc machinery → copy per-GOOS now, hoist later; ⟨8⟩crypto/tls’s Linux count → measure, report, bank nothing (per-OS ruling); ⟨9⟩ safeMarshalimages overunsafe+ marker → safe.
Nothing landed in src/. The implementation lane starts on ratification, from the S0 probes.
2026-08-22 · The ecdsa-AV lead — PROBED AND RETIRED: three limbs, each closed by census or construction; the watch-item stays box-transient (lane G, claude/av-lead)
The assignment: does crypto/ecdsa’s Windows execution path reach the SiginfoChild corruption family — any of the struct-passing census’s latent wrappers, or any array<T>-padding-in-kernel-struct site — such that the 0xc0000005 watch-item gains a mechanism? Bounded and measured; three limbs.
Limb 1 — the latent-wrapper census, mechanically intersected: ∅. go list -deps crypto/ecdsa + Test/XTest imports against the census’s open rows (GetIfEntry/net.Interfaces, getStartupInfo/unreached, the Cert* trio/crypto/x509 system verifier, the 13-member **T OUT-param class): the production closure reaches none; the TEST closure imports crypto/x509 — the sharpest moment of the probe — but every x509 call in the suite is MarshalPKCS8PrivateKey/ParsePKCS8PrivateKey, pure-Go ASN.1 with no path to the system store. No net, no process-startup surface.
Limb 2 — array<T>-padding-in-kernel-struct sites: the same census, the same ∅. On Windows that class IS the struct-passing census (an array<T> field is exactly what makes a struct non-blittable); no new members exist outside it (the census’s own discipline), and ecdsa reaches none.
Limb 3 — the ж→uintptr transient-pin window: closed BY CONSTRUCTION, not by absence of evidence. The dll_windows-era warning (“a compacting GC could invalidate a transient pinned address mid-call”) predates the current operator: implicit operator uintptr(ж<T>) calls EnsureStableAddress(), which installs a PERSISTENT PinnedBuffer.PinOnly held by the box and released only at box death — and a syscall’s argument-rooted box is reachable for the call’s whole duration. crypto/rand’s entropy reads under ecdsa’s big-int GC churn therefore have no movement window. (The exec_windows hand-own’s unmanaged-copy rule remains belt-and-braces for structs, not a correction of the pin.)
Verdict: no mechanism candidate — the stop-and-root clause does not fire. The watch-item’s own evidence (the i9’s three-point analysis: load-correlated, non-reproducible, a same-run phantom CS8983) stands as the best account: box-transient contention. What stays LIVE from the family: the SiginfoChild class is real and now has one Linux member fixed; the standing rule it adds — any struct the KERNEL writes whose converted form carries array<T> padding is corrupt-by-layout — applies to every FUTURE wrapper and to the Linux flavor’s remaining kernel-writing structs, and belongs in the wrapper-census’s preflight rather than in this watch-item.
RULING – per-OS verdict arithmetic: a count is a fact about (package, OS), and the roster gains an OS-keyed annotation, never a blend (coordinator, 2026-08-22)
The constituency that forced this: crypto/rand discovers 302 verdicts on Linux against 298
banked, path/filepath 54 against 61, debug/buildinfo 204 gcc-ungated against 197, mime 18
– every one a case where Go ITSELF runs a different test set per GOOS (build-tagged tests,
GOOS-keyed skips, capability gates). The principle follows directly:
- A verdict count is a fact about (package, OS). Differing counts across OSes are Go’s own design, not a divergence – PROVIDED the difference attributes to Go’s per-OS test sets, which the differential already enforces row by row (the C# count must match GO’S OWN count on the same OS, same as always). Counts are never averaged, blended, or footnoted away.
- The roster’s banked columns stay WINDOWS-authoritative for the 1.23.1 era – the header arithmetic, the badges, and the proof pages as they stand are the Windows campaign’s record.
- Linux validation gains a formal home: an OS-keyed row annotation. A row that validates
on a Linux leg records
linux: N + Dbeside its Windows columns (the host-conditional COUNT machinery generalized by one dimension – path/filepath’s floor-plus-named-set precedent is the shape). The header gains ONE Linux progress line (“Linux: X ofvalidated at their Linux counts"), summed from the annotations exactly as the Windows header sums from the table. - The sweep honors the dimension: under GoTargetOS=linux it validates a row against its linux annotation where one exists, and reports comparison-validated-at-count (today’s honest interim) where none does. Proof pages gain the OS column AT THE ANCHOR RELEASE, per the release-trigger design – not before, so the pages move once.
- Implementation routes to a bounded harness lane (roster parser + sweep check + the annotation format), landing WITH or BEFORE trigger condition (d)’s dual-OS consolidation sweep. The four constituency rows are its acceptance cases. Until it lands, Linux results keep their census/board homes exactly as the last five lanes have recorded them.
This is release-trigger condition (c) delivered as doctrine; its implementation is condition (d)’s prerequisite step, not a new gate.
FUTURE HARVEST (user, 2026-08-22) – the Span-unification sweep beyond the native-slice v1 minimal set
Banked so the little wins are not lost. The ratified native-backed slice design (OQ-1) lands
with a MINIMAL span-unified set (copy, bytealg, the string conversions) – deliberately,
so the correctness model ships unblended with harvest. The remainder is this item: once v1’s
branch-cost gate measures clean, sweep golib’s remaining bulk helpers onto the Span-over-either-
backing shape, one adoption at a time, each its own small measured win (zero-copy +
JIT-vectorization on the common managed path). The target surface is strings/bytes – the same
one the .NET 10 scout measured halving under the new JIT – so the adoptions compound with the
hop. Era: post-v1-measure, opportunistic; each adoption carries its own micro-gate (the Perf*
row it touches, no regression on the branch gate). These wins are individually small and
collectively the point.
LANDED – the per-OS annotation harness: the ruling’s implementation half, four rows encoded, Windows byte-identical (coordinator harvest, 2026-08-22)
Release-trigger condition (d)’s prerequisite is complete at 249b47b74. The grammar
(goos: N [+ D] as the row’s final middle-dot segment, both anchors load-bearing so prose
cannot false-parse; windows refused BY NAME since the columns are the Windows answer), the
four constituency rows encoded from their board sources and round-tripped (crypto/rand
linux: 302 · mime 18 + 1 · path/filepath 54 · debug/buildinfo 204), the header’s one
Linux progress line summed from annotations exactly as the Windows line sums from columns, a
51-check standalone format guard with TWO failing-first proofs, and the honest third bucket:
off-Windows rows without an annotation report comparison-validated-at-count (the NOT-MEASURED
shape, still exit 1). Windows behavior proven byte-identical three ways at three tips.
Two coordinator notes made durable: (1) the format guard stays STANDALONE until a quiet point decides its wiring – hooking it into the sweep preflight would add a new Windows failure mode, which the lane’s own gate correctly refused; (2) the one documented limit – host-conditional ABSORPTION on an OS-annotated row still rejects, because its evidence (the committed proof page) is Windows-shaped. Proof pages gain the OS column AT THE ANCHOR RELEASE per the ruling, and the rejection is honest until then.
The $(go2csPath) case-insensitive environment race: FIXED AT THE CONVERTER — the export is gone, every child env carries one spelling (2026-08-22, lane worktree-agent-a39a6070f9f4e34d9)
Closes the remedy PRICED in the 2026-08-21 rooting entry above. Both halves landed, not just the hygiene one — and the census is what forced that.
Census first (measure, don’t assume). GO2CSPATH is READ in exactly ONE place in the whole
converter — the defaulting block in main.go — and the value is consumed immediately as the
-go2cspath flag default; nothing reads it back afterward, and no MSBuild file in the tree
references an uppercase $(GO2CSPATH) (every reference is $(go2csPath)). So the os.Setenv had no
consumer and could simply go, which is what the coordinator’s routing note preferred. But the same
census settled the question the note left open: stop-exporting ALONE does not satisfy the
invariant. A user may set GO2CSPATH — it is the documented way to choose a runtime root, and it
is exactly what the Linux harness pin does — so honoring it as the flag default leaves it in
os.Environ(), and a plain append still hands the child TWO case-distinct spellings. The pin makes
both spellings carry the same value, which is why it neutralizes the failure; it does not close the
class.
Spawn census, for the record: the converter has THREE child-process sites — go env
(projectFileWriter.go), git rev-parse (testConversion.go), and runCommandWithTimeout — and
only the third reaches MSBuild, as the parent of every dotnet build/dotnet run/go test the
pipeline spawns. The packages.Config.Env sites spawn the Go toolchain, which does not read
go2csPath. One site to fix, not a family.
The fix (two functions, both in src/go2cs).
resolveGo2CSPathDefault(main.go) replaces the inline defaulting block and does NOTos.Setenv. The contrast is preserved in its comment so a later tidy-up does not restore symmetry by accident:GOROOT/GOPATHabove are exported deliberately, because thegochildren read them;GO2CSPATHhas no such consumer.childEnvWithGo2CSPath(testConversion.go) replacesappend(os.Environ(), "go2csPath="+…)atrunCommandWithTimeout— it drops every case-insensitive variant inherited from the parent, then appends the canonical entry with the resolved, separator-terminated root.
The invariant is stated in the source, once: a user-set GO2CSPATH is honored (as the flag
default); the converter never exports its own derived value; a child environment carries exactly one
spelling. The scrub is the clause that holds regardless of the invoking shell — including the
nastier variant the rooting entry named, where an ambient GO2CSPATH points at a DIFFERENT real tree
and the child build binds the wrong stdlib nondeterministically.
Guard, failing-first — src/go2cs/childEnvGo2CSPath_test.go, five tests under the plain
go test ./..., no new harness. The parent environment is constructed LITERALLY rather than read
from the OS, because only a POSIX environ can hold two case-variants at once — so the guard measures
the same shape on Windows, where the defect is unreproducible in vivo. Neuter proof, both halves:
| Neuter | Result |
|---|---|
childEnvWithGo2CSPath → plain append (the pre-fix shape) |
3 FAIL — child environment carries 4 go2csPath spellings, want exactly 1: [GO2CSPATH=/root/go2cs go2csPath=/stale/tree/ Go2CsPath=/another/tree go2csPath=\repo\src\] |
resolveGo2CSPathDefault → re-add the os.Setenv |
1 FAIL — converter exported its derived root as GO2CSPATH="C:\Users\ritchie\go2cs" |
Gates (converter change class, this machine, solo):
| Gate | Result |
|---|---|
converter go test ./... |
ok — 222s (carries the five new guards and projitemsIntegrity, which the new file is registered in) |
| full CNR, 633 behavioral packages | NO REGRESSION — byte-identical .cs + .csproj, 1,141s; 2 advisory warnings, 0 NOT MEASURED. Emission-neutral as predicted: the change moves only child-process environments, never emitted text |
-tests -test-action all on unicode/utf8 |
14/14 validated vs go test, 107s, git status -- src/core clean — the pipeline runs with the export gone |
The Linux proof is NOT this lane’s, and the harness pin STAYS. There is no distro on this box, so
what is proven here is that the fix is emission-neutral, that the pipeline still runs end to end, and
that the child-env shape is right by construction. The race itself is structurally POSIX-only and can
only be re-measured on Linux. Pin-retirement condition: a Linux lane runs the reproducing
configuration — the two-package alternation that failed 3-for-3 within ≤2 cycles — with GO2CSPATH
deliberately UNSET and _paths.ps1’s pin block removed, and gets a clean run; at that point the pin
in src/_paths.ps1 (and the campaign driver’s export) retires as dead weight rather than as
protection. Until then it stays and remains correct: it sets the variable the converter still honors
as a default, and the scrub makes the value it sets unreachable by the child regardless. One note for
whoever retires it — the pin’s comment cites main.go:93 and testConversion.go:5663, both stale
after this change; retire the comment with the block rather than repairing the line numbers.
The class trap (case-insensitive environment-variable races: Windows-immune, POSIX-live, MSBuild
property resolution as the collision site) is recorded in CLAUDE.md beside the harness/false-green
notes, stated generally enough to outlive this one variable.
—
2026-08-22 · LANDED + MEASURED — the native-backed slice<T>: W1b closes, both rows validate on Linux, and golib’s hottest type grows one word (lane G, claude/native-slice-impl)
The ratified design implemented as written (DESIGN-native-backed-slice.md, all five OQs). Three commits: the failing-first guard family, the model, and the census sweep the design demanded as its first commit — which is the entry’s most transferable lesson, below.
The change. slice<T> carries m_nativeBase beside its managed backing — the ж<T> dual-mode precedent (#159’s native-slot doctrine) applied to the slice header, 0 meaning “managed” so every pre-existing path is untouched but for a predicted branch. ONE creation door (slice<T>.OverNativeMemory, reached only from unsafe.Slice’s IsNative arm) enforces unmanaged-T with a named panic: the SiginfoChild corruption class made unrepresentable rather than discoverable. Both indexers return refs into the mapping; Reslice carries the base with identical window arithmetic; ToSpan is the discriminant-once unification point that copy, CopyTo, IndexOf and the string conversion now ride; append within capacity writes the mapping and past capacity DETACHES to managed — Go’s own spec, not an invention; builtin.Ꮡ’s two overloads mint address boxes, so (uintptr)Ꮡ(s,i) is the real address and Mprotect(b[:pagesize]) finally hands the kernel the mapping.
The acceptance case, measured on Linux — W1b is CLOSED:
| row | before (three lanes’ standing residual) | at the tip |
|---|---|---|
crypto/sha1 |
FAIL — TestOutOfBoundsRead panic: invalid argument |
comparison-VALIDATED, COUNT 13 vs banked 12 (per-OS); sole divergence TestAllocations — the CLR-impossible disclosure class |
bytes |
FAIL — four page-boundary tests as infrastructure-errors | comparison-VALIDATED, COUNT 86 vs banked 82 (per-OS); every page-boundary and mmap test PASSES; six residuals, all allocation-count |
Both rows join the per-OS-count constituency (crypto/rand 302, debug/buildinfo 204, mime 18, path/filepath 54) now before the coordinator’s arithmetic ruling.
The lesson worth carrying: the census is the work, not the epilogue. The design spec’d an m_array touch census as the implementation’s FIRST commit; the first pass shipped the obvious members and let bytes’ page-boundary tests find the struct Enumerator. The sweep that followed — done properly, every touch dispositioned — then found FOUR more the failure had not: a SECOND enumerator class (SliceEnumerator, unreached by the first fix), the IList<T> setter, buffer (now a named panic: pinning is a managed concept, and a native window’s answer is an element address), the byte-reinterpret alias, and header identity in GetHashCode/operator==. A defect that reveals one member is not a census; it is one member.
Gates: GolibTests 235/235 Windows · 237/237 Linux (the five-guard family: aliasing, real addresses, append-detach, cross-backing copy, and the crypto/subtle managed-aliasing regression arm) · behavioral full suite PASS 606 projects — the slice is under every converted line in the corpus · golib + unsafe builds 0 errors both flavors · perf trio A/B (§5.3’s measured branch-cost gate): String 1,223.0 and StringView 20.8 within noise; Sieve produced the arc’s methodological lesson — it read 145.9/144.2 ms against a 110.5 ms morning baseline (+30%), the inline unsafe block was found to cost inlinability and fixed (144.2 → 126.1 ms behind a NoInlining helper), and then a PAIRED same-session A/B exonerated the arc entirely: pre-change golib 142.3 ms vs with-fix 145.0 ms, both far above the morning figure. The regression was MACHINE DRIFT; the inlining fix is kept because it is right on its own merits. Doctrine: on a laptop, a perf comparison against an earlier-session baseline is not evidence — only a paired same-session A/B is · CNR: byte-identical across all 633 behavioral packages · Windows full-roster control (JOB-G2, i9): **162/162 PASS, 0 FAIL, exactly 18,569 verdicts — the full banked roster, clean on the first pass (no transient reds this time)** — breadth maximal by construction, and the native path is unreachable on Windows (no row mints a native pointer), so managed behavior must be identical.
What this retires: unsafe.cs’s documented snapshot limitation, and with it the class the board named “sufficient for reading a block a syscall returned” — every unsafe.Slice over native memory now ALIASES, so syscall.Environ rides the same arm (OQ-3’s ride-along, no behavior change: it reads once and never writes) and the snapshot arm is deleted rather than maintained beside its replacement.
2026-08-22 · MEASURED — R3’s Linux leg closes: debug/elf validates at 31, and debug/gosym’s last row is Go’s OWN skip reaching us honestly (lane G, claude/linux-smallitems)
The board’s “gosym MZ-magic mystery” was R3, and the argv[0] fix already dissolved it. R3 was recorded as: os.Args[0] is the managed PE image, so elf.Open(os.Args[0]) fails bad magic number '[77 90 144 0]' — the ASCII of MZ\x90\0. The summary-seam lane’s argv[0] correction (apphost mode reports the PROCESS image, not the managed .dll) changed what that call sees on Linux; nobody had re-measured the rows since. Measured now at master 71a95c8ff:
| row | recorded (R3) | measured at master |
|---|---|---|
debug/elf |
FAIL — elf.Open(os.Args[0]) on PE magic, 1 row + 3 gosym tests |
PASS 31 · comparison-VALIDATED, ZERO divergences |
debug/gosym |
FAIL, R3-attributed | one divergence: TestSymVersion, Go=pass C#=skip |
And that one divergence is not a defect — it is Go’s own skip, reached correctly. TestSymVersion → getTable → crack(os.Args[0]) → parse, whose FIRST act is if f.Section(".gosymtab") == nil { t.Skip("no .gosymtab section") }. The C# host reports exactly no .gosymtab section: elf.Open now SUCCEEDS on the apphost (the ELF parse works — that is the argv[0] fix delivering), the binary genuinely has no Go symbol table because it is a .NET apphost, and the converted run takes Go’s own documented skip path. Go’s Linux run doesn’t skip only because ITS test binary is a Go binary with a .gosymtab.
Classification: a host-limit disclosure of the relocatable/self-binary family — the same class as os/exec’s 27 banked Windows disclosures (a .NET apphost is not the thing Go’s self-inspecting tests assume), and NOT the R3 wall, which is closed. The row needs no code: it needs a signature-pinned disclosure at its formal Linux bank, alongside the per-OS-count annotation work the coordinator deferred for crypto/sha1/bytes.
What this leaves of R3: nothing on Linux. The board’s R3 row (debug/elf + 3 gosym tests) is retired by measurement; the residual is one honest disclosure line awaiting its bank.
2026-08-22 · LANDED + MEASURED — the Linux readiness poller: epoll + one drain thread + the Windows descriptor state machine; the socket family opens on Linux (encoding/json flips, crypto/tls goes 0 -> 400 matching), and two further walls are rooted behind it — one fixed, one routed (lane R, claude/linux-poller-impl)
The design was ratified this morning with all nine OQs as recommended; this is its S0 + S1, landed and measured the same day. One file — src/core/internal/poll/linux/runtime_netpoll_impl.cs, 674 lines — replaces the fallback poller in place: epoll_create1(EPOLL_CLOEXEC), ONE background drain thread in epoll_wait(-1), and the Windows flavor’s managed descriptor state machine (Ready/Expired/generations/Timer, pollBlock/pollReset/pollSetDeadline/pollUnblock) copied verbatim per ⟨OQ-7⟩ with Go’s eventErr arm added. Edge-triggered EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET exactly as runtime/linux/netpoll_epoll.cs arms it; epoll_event.data carries an opaque token (table insert BEFORE EPOLL_CTL_ADD, EPOLL_CTL_DEL BEFORE close(2) — FD.destroy’s own ordering); every kernel byte a native Marshal image through the keystone syscall(2) binding, so no ж address ever reaches the kernel and internal.poll.csproj’s shared <AllowUnsafeBlocks> stays false (⟨OQ-9⟩ — no regen owed, and the Windows build of internal/poll is byte-untouched). No break eventfd (⟨OQ-2⟩); EINTR retried (⟨OQ-3⟩ fail-loud otherwise); regular files refused by the kernel’s own EPERM (⟨OQ-4⟩). Nothing else in src/ changed — not os, net, syscall, golib, the converter, the keystone, or the Windows flavor.
S0 — the four kernel probes, and the one place the design was wrong about itself
Run in the distro as plain C# over libc (independent of go2cs), recorded in the design’s new §7.1 with its source:
| Probe | Measured | Consequence |
|---|---|---|
(a) epoll_ctl(ADD) by descriptor kind |
regular file EPERM, directory EPERM, pipe 0, TCP socket 0 | ⟨OQ-4⟩ holds: the kernel IS the regular-file refusal; no fstat, and the 28 fallback-flipped rows keep their exact errno |
(b) the packed 12-byte struct epoll_event |
round-trips through Marshal.WriteInt32/WriteInt64 at {0,4}, unaligned 8-byte read at offset 16 included |
⟨OQ-9⟩’s safe form suffices |
(c) EINTR rate under load |
0 in 20 s of 1 s slices while spawning 22,819 children (a SIGCHLD each) and running 7,758 gen0 GCs | The design’s own prose was wrong — it called EINTR “the normal case, not a corner”. Under the CLR it is rare: the runtime routes signals away from arbitrary threads. The retry stays (a never-restarted syscall is owed it), but as a correctness guard, not a hot path. Corrected in the doc and in the file. |
(d) EPOLL_CTL_ADD during an in-progress epoll_wait(-1) |
the blocked waiter got the new descriptor’s edge 1 ms later, no break write | ⟨OQ-2⟩ holds: the drain thread never needs interrupting |
S1 — the guards, by hand on the distro (⟨OQ-6⟩): all four byte-IDENTICAL to go run
| Guard | Result |
|---|---|
PipeCloseUnblocksRead |
IDENTICAL — prints Go’s read unblocked: read \|0: file already closed. Under the fallback this printed read did NOT unblock: the visible flip. os.Pipe arms; Close → evict wakes the parked reader. |
NetListenSmoke (8 lines) |
IDENTICAL — bind, distinct ports, accept deadline set/cleared, close, rebind, close-is-sticky |
TcpLoopbackRoundTrip (14) |
IDENTICAL — IPv4 and IPv6 round trips end to end, plus CloseRead/CloseWrite breaking blocked operations |
NetDeadlineMatrix (12) |
IDENTICAL — every assertion: blocked read times out and PARKS, expiry is STICKY, cleared/past/replaced deadlines, per-mode independence and 'r'+'w', closing beats timeout, and both race arms (expiry beats buffered data; data inside the deadline is delivered; no stale expiry afterwards — the generation check) |
That last row is what the Windows design called “the hard part” and priced its iteration budget for. It cost none: the copied state machine satisfies Go’s deadline semantics on the READINESS model unchanged — the strongest evidence available both for ⟨OQ-7⟩’s copy and for §4.7’s claim that dropping the cancel-and-harvest dimension leaves the race surface tractable.
Gates
| Gate | Result |
|---|---|
internal.poll.csproj linux flavor, NATIVE |
0 errors, 0 warnings |
go2cs-stdlib.slnx -p:GoTargetOS=linux (NATIVE, --no-incremental) |
0 errors, 149 warnings, 418 s — warning count unchanged from the sockaddr lane’s gate, so the poller adds none |
go2cs-stdlib.slnx -p:GoTargetOS=windows |
0 errors (--no-incremental, 6 m 52 s) — the control: the poller is under linux/ and the Windows build never compiles it |
| GolibTests (golib untouched) | 230/230 (golib untouched; run as the standing gate) |
check-no-regression.ps1 |
not owed — no converter change, no registry entry, nothing emitted moves |
| Windows control (JOB-R5, i9, the 8 JOB-R4 rows) | 8/8 PASS, 0 FAIL, 1,010 verdicts, 815 s at 00cc122c9 — identical to JOB-R4 row for row (encoding/json 491, crypto/tls 400, the six banked net/*), and its corpus drift is JOB-R4’s too. The poller change has zero Windows-visible footprint, as the per-GOOS file placement promised. |
The measurement — Linux roster re-run at 00cc122c9 against the sockaddr lane’s 128/30/3
crypto/tls — the flagship row, from 0 verdicts to 400 matching, which is the Windows banked count exactly. It was a package-level operation not permitted (0 of 3,646 Go-enumerated). With the poller alone the suite RAN but ate its 30 m deadline in TestVerifyHostname; with net.runtime_rand implemented (below) it completes, and the comparison at the lane tip a5429a611 reads:
| verdicts | pass | fail | |
|---|---|---|---|
| Go (linux, same machine) | 402 | 389 | 13 |
| C# host | 402 | 387 | 15 |
| comparable / agreeing / differing | 402 | 400 | 2 |
Thirteen tests fail on BOTH sides (environment-driven — the suite’s network- and BoGo-dependent arms), so the honest statement is: of the 402 verdicts both sides produce, 400 agree, and exactly two diverge. Both are attributed, and neither is the poller:
TestVerifyHostname— dialswww.google.comfor real (testenv.MustHaveExternalNetwork, thenDial). Blocked behind the UDP wall below. Note the SHAPE of its improvement: with the DNS stub in place the lookup goroutine threw and its caller waited forever, so the package ate its deadline and produced no verdicts at all; with the stub implemented the test simply FAILS, which is what lets the other 400 be measured.TestCertCache— setsruntime.SetFinalizeron anactiveCert, nils the reference, callsruntime.GC()and waits up to 4 s for the finalizer to decrement a refcount (cache.go:63,cache_test.go:69/78). The managed object-lifetime class — the same family asTestFreeOSMemory’s assertion 2 inDESIGN-readmemstats-surface.md§7.2.3 — and nothing to do with networking.
Per this morning’s per-OS ruling that is a fact about (crypto/tls, linux); it is reported, not blended with the Windows-authoritative columns, and nothing is banked.
Two walls behind the poller, both rooted with stacks rather than guessed:
net.runtime_rand— an unimplemented//go:linknamestub, FIXED here (src/core/net/dnsclient_impl.cs, one body, the shape of its three precedentsos/tempfile_impl.csandmath/rand’s two).netreaches it only through the pure-Go resolver —randIntpicks the DNS query ID (linux/dnsclient_unix.cs:54) and weights SRV selection and address shuffling — so Windows, which resolves viaGetAddrInfoW, never touched it and the stub survived the whole Windows campaign. On Linux the pure-Go resolver IS the resolver, so the platform’s FIRST name lookup died there, on a lookup goroutine, leaving its caller waiting forever: that is whycrypto/tlsate a 30-minute deadline instead of failing. Platform-neutral file;netbuilds 0 errors / 0 warnings; Windows behavior unchanged.- The UDP wall — measured, priced, NOT taken here. With (1) in, the TCP path matches Go exactly (
8.8.8.8:53->connection refusedin 99 ms vs Go’s 72 ms) but DNS still times out, and a loopback UDP probe names it: bind works, thenSystem.NotImplementedException: RecvfromInet4—internal/syscall/unix.RecvfromInet4(internal/syscall/unix/linux/net.cs:14) reached viainternal/poll.ReadFromInet4->net.readFrom->UDPConn.ReadFrom. It is one of eight//go:linknamestubs in that one file (Recvfrom/Sendto/SendmsgN/Recvmsg× Inet4/Inet6). This is precisely the seam the sockaddr lane recorded as uncovered, so it is that family’s next increment and its tools already exist (syscall/linux/sockaddr_linux_impl.cs’sreadNativeSockaddr/writeNativeSockaddrplus the keystone). Routed, not taken: this lane is the poller.
The 161-row roster re-run (at 00cc122c9, without the DNS fix — no other roster row resolves a name): 145 PASS / 11 FAIL / 5 COUNT of 161 (baseline, the sockaddr lane: 128 / 30 / 3) — 17 flips, ZERO regressions, plus two rows improving FAIL to a validated per-OS COUNT (debug/buildinfo 204, go/internal/gcimporter 582).
The attribution matters more than the total, and this entry will not claim what it did not do. My branch point is master 662b1595f, which already carried G’s exec-wall arc AND G’s summary-seam arc; the 128/30/3 baseline predates both. So of the 17 flips, ONE is the poller’s — encoding/json PASS 491, TestHTTPDecoding’s httptest loopback round trip, exactly the row the design’s bill named first — and the other sixteen (sync, math/rand, flag, crypto, crypto/ecdh, crypto/ed25519, go/types 557, go/importer, go/doc/comment, go/internal/srcimporter, text/template, debug/elf, internal/abi, internal/testenv, internal/types/errors, internal/godebugs) are G’s, already reported on their own board entries. crypto/tls reads FAIL in this ledger because the run predates the runtime_rand commit; its real Linux number is the 400-of-402 measured at the lane tip above.
Zero regressions is the number this lane is actually accountable for. The poller changes what os.Pipe, FIFOs, ttys and sockets DO — every one of them goes from blocking to armed — and nothing that passed before stopped passing, including the pipe-adjacent rows (bufio 80, io 60, io/fs 18, io/ioutil 28, os/exec/internal/fdtest 1, mime/multipart 52).
The 16 residuals are all known, attributed classes — nothing unexplained: crypto/tls (the UDP wall; 400/402 at tip), crypto/sha1 + bytes (W1b — closed upstream by G’s native-backed slice while this run was in flight, so those two are stale here rather than residuals of this lane), the per-OS COUNT constituency (path/filepath 54, mime 18, debug/buildinfo 204, go/internal/gcimporter 582, crypto/rand 302 — the ruling’s own acceptance cases), time (R6 ZONEINFO), os/exec (G’s exec residue), debug/gosym (G’s, now classified as a host-limit disclosure), internal/cpu (W6), os/signal + syscall (W2 test-variant emission), sync/atomic (W7, ruled), plugin (W3 — the converter panic gcc re-exposed, rooted in the sockaddr lane; FAIL in the baseline too, so unchanged).
What this closes, and what it does not
Closed. The Linux socket family is OPEN: listen, accept, connect, TCP read/write, deadlines and close-unblocks all behave as Go’s, down to a connection refused that matches to the error string. internal/poll’s ten contracts have real bodies on both shipping platforms now, and the fallback poller’s documented degradations are retired — os.Pipe’s Close unblocks a parked reader, SetDeadline on a pipe is honored, and a blocked read no longer holds its thread in read(2) where nothing can reach it. W1’s last remnant (the socket half the fallback could not serve) is gone; crypto/tls’s Linux leg exists at all for the first time.
Not closed, and named rather than implied:
- UDP — the eight
internal/syscall/unixlinkname stubs (§ the wall above). Everything datagram is blocked behind them, which today means DNS, which means anything that resolves a name. The sockaddr family’s next increment. netitself stays a FUTURE arc on both platforms, exactly as the Windows design has it: its Linux census is worth taking now that sockets work, but it is not this lane’s claim and the DNS half of it is behind the UDP wall anyway.- darwin — the pre-poller fallback remains its remedy when that corpus builds; kqueue is its own design.
- The S3 socket ledger (
net/smtp,net/http/{httptest,cgi,httputil,cookiejar},net/rpc) — reachable now, unmeasured here; the order file is staged and the measurement is a follow-on, not a claim. - No roster changes. Per the per-OS ruling that landed this morning, the Linux numbers above are facts about (package, linux) and live on this board until the annotation harness lands.
One process note worth carrying. Three detached sweep launches died before this lane’s run stuck, all with the same signature (log stops, distro alive, no diagnostic): setsid bash … still sits in the launching turn’s reapable process tree, and a git fetch from the /mnt/c local remote inside a no-tty session compounds it. setsid --fork nohup with stdio detached, plus pre-positioning the tree so the chain does no git work of its own, is what made it survive — the same class as CLAUDE.md’s existing background-reaping caveat, one layer deeper.
S3 — the off-roster socket ledger (in flight at the time of this entry)
net/smtp, net/http/{httptest,cgi,httputil,cookiejar} and net/rpc are reachable now in principle, and are being measured with the RAW -tests pipeline rather than the sweep — run-validated-sweep.ps1 is roster-driven and answers “No banked packages matched filter” for an off-roster package, which is worth recording because it is a five-second trap for the next lane that tries. First result: net/smtp is conversion-blocked (Go enumerates 19 verdicts, the C# host 0) — a test-CONVERSION wall, not a poller one, and therefore a converter item rather than something this lane’s file can reach. The remainder follow in the merge signal.
2026-08-22 · MEASURED — S3 of the Linux poller: FOUR of the six socket-ledger packages validate on Linux with ZERO divergences (142 verdicts), and the two that do not are off the poller’s axis (lane R, claude/linux-poller-impl)
The poller entry above closed with S3 in flight; this is its result, and it is the strongest consumer evidence the arc has. These are the packages the Windows netpoll design’s §7 froze as its unlock ledger — the ones that have been walled at FD.Init on Linux since the platform existed.
Measured at the lane tip a5429a611 in the isolated probe clone, with the RAW -tests pipeline:
| package | status | comparable | agree | differ | note |
|---|---|---|---|---|---|
net/smtp |
validated | 19 | 19 | 0 | Windows has it at 9/14, walled |
net/http/httptest |
validated | 55 | 55 | 0 | the loopback HTTP server the whole family rests on |
net/http/httputil |
validated | 53 | 53 | 0 | reverse proxy over real sockets |
net/rpc |
validated | 15 | 15 | 0 | |
net/http/cgi |
failing | 39 | 15 | 24 | not the poller — all 24 are TestCGI*/TestChild*, every one of which spawns a CHILD CGI process; the exec axis, and Windows has this row at 36/39 |
net/http/cookiejar |
conversion-blocked | — | — | — | not the poller — its emitted TEST HOST does not resolve golib (CS0234 on go.GoPositionMap, go.time_package in package_info.cs); a test-host emission gap on a package that has never been through -tests, i.e. a converter item |
142 verdicts across four packages, every one matching Go on the same machine. Nothing is banked — these are off-roster, and per the per-OS ruling they are facts about (package, linux) reported here.
A five-second harness trap, recorded so the next lane does not pay it twice. run-validated-sweep.ps1 is ROSTER-driven: -Filter <off-roster-pkg> -Exact throws “No banked packages matched filter” and returns in ~4 s, which is why off-roster candidates go through the raw pipeline. But the raw pipeline needs what the sweep supplies for free: src/_paths.ps1 pins $env:GoTargetOS = 'linux' on a Linux host so every child dotnet inherits it. A bare go2cs -tests from a shell does NOT get it — the test host then builds the Windows flavor and dies at run time with kernel32.dll.so: cannot open shared object file, which the pipeline reports as conversion-blocked and reads exactly like a converter wall. My first S3 pass produced three such phantom walls (net/smtp, net/http/httptest, net/http/cgi); all three evaporated on the re-run with GoTargetOS=linux exported, and two of them are in the validated column above. Export it in any raw -tests harness, and treat a conversion-blocked verdict whose log mentions kernel32 as a harness fault, never a finding.
2026-08-23 · Condition (d) verdict arithmetic — how the dual-OS consolidation legs are judged (coordinator ruling, relayed on the mailbox in flight)
The Linux leg reports the QUADRUPLE — PASS / FAIL / COUNT / CVAC — and three readings are fixed
before its verdict composes. (1) An attributed CVAC row is GREEN for (d): the comparison
validated at a count on this OS; the row is merely unbanked, which is a roster gap, not a
regression. (2) A FAIL row is within green iff attributable to an already-classified seam
(UDP wall, exec-adjacent residue, object-lifetime, and kin) — crypto/tls on Linux is the
worked example: FAIL with exactly the classified pair (TestVerifyHostname = UDP wall,
TestCertCache = object-lifetime), completing in 711 s where it previously ate a deadline
(the runtime_rand fix paying for itself). (3) The sweep’s exit code is NOT the leg’s
verdict: the script exits 1 whenever CVAC > 0 — its honest not-banked-for-banking posture —
so an exit-1 leg whose non-PASS rows are all attributed is a green leg. Judge by the quadruple
plus the attribution table, never the exit code alone.
Post-leg obligations minted by the same run: crypto/sha1 (linux: 13) and bytes (linux: 86)
annotations bank on master against the leg’s record, retiring their CVAC rows; crypto/tls
owes its own small per-OS item (a linux: annotation plus the per-OS disclosure shape for its
two classified divergences). Fleet trap recorded in flight: any harness that greps the sweep’s
verdict lines must include CVAC in its verdict class set — a pre-ruling regex records CVAC
rows as no-verdict, which reads exactly like a dead leg (R hit this at row 4 and lost ~14 min).
Per-row wall times are native as of 4e91a03e2 ([NNNs] on every verdict line, the H5 number);
the in-flight legs predate it and carry their own ledgers/mtime derivations instead.
2026-08-23 · FIRST darwin census (CI run 32611912106 at cfb8b295b) — the wall is ONE package, ONE root, and the CI matrix itself is proven
The user dispatched os-matrix.yml -f goos=darwin -f stage=census — the first time the darwin
corpus flavor has been BUILT anywhere. Both mac legs (osx-x64, osx-arm64) failed IDENTICALLY:
19 errors, all in os, all from one root — dir.cs’s three call sites of readdir
(lines 45/77/101, CS1061 + the CS8130/CS8183 deconstruction cascade). Root:
darwin/dir_darwin.cs carries the converter’s bodyless-placeholder comment for readdir
(“hand-converted with managed semantics in the package’s *_impl.cs”) — the suppression fired
for darwin exactly as for windows — but the companion exists ONLY as
windows/dir_windows_impl.cs (266 lines; Go’s windows readdir reinterprets a raw
GetFileInformationByHandleEx buffer). Go’s dir_darwin.go is the same raw-metal class (libc
DIR* walking via opendir/readdir_r), so suppression is CORRECT there too — what is missing
is the darwin companion that was never authored, because darwin never built. Linux needs
neither: dir_unix.go’s readdir auto-converts (it reads through f.pfd.ReadDirent), which is
why the linux flavor never surfaced this.
Two findings beyond the fix itself. (1) The dependency closure BELOW os compiles clean on
darwin — errors are own-errors of leaf-most failures, so syscall/internal/poll/runtime
darwin flavors all built; packages above os were skipped, so unknown walls MAY remain behind
it, but the measured state echoes the Linux wall’s shape (one package, one class). (2) A
converter/L3 invariant worth guarding: hand-own SUPPRESSION is platform-blind while the
IMPLEMENTATION is platform-scoped — the L3 routing should assert that every GOOS whose
emission suppresses a member also receives an impl companion providing it (a
marker-gate-shaped check, natural home platformHandOwn_test.go); today the gap is only
discoverable by building the flavor.
Work item minted: author darwin/dir_darwin_impl.cs (same signature as the windows companion’s
readdir(this ж<File>, nint n, readdirMode mode); the faithful path is libc
opendir/readdir_r through the darwin syscall flavor that now provably compiles, with the
windows companion as the structural model). Opus-class lane work, small and precedented; then
re-dispatch the census to find what stands behind os. The CI matrix itself is PROVEN by this
run: checkout, pinned toolchains, env report, ~10 min census build, artifact upload and the
skip-of-later-stages-on-failure all behaved exactly as designed on first dispatch.
2026-08-23 · CVAC amendment (G) — the summary-line classifier is the sharper trap
Amendment to the condition-(d) arithmetic entry above, from G’s harness audit: the verdict-line
grep is the EASY half of the CVAC hazard. The dangerous half is any harness that decides
PASS/FAIL from the sweep’s SUMMARY line (if '1 pass' → PASS; else FAIL): a CVAC row’s summary
is not “1 pass”, so it falls through to FAIL — a FALSE RED with the row’s own green CVAC line
sitting in the log contradicting it, which is harder to notice than R’s NOVERDICT blank. Rule:
classify from the VERDICT line, test CVAC first, and carry a cvac= column in any totals so a
green class is never silently absent. All five of G’s drivers and R’s leg driver are patched;
this entry exists for the next harness author.
2026-08-23 · FIRST native-Linux control (CI run 32613375229 at 43280f0bb) — GREEN, 208 s
The user dispatched os-matrix.yml -f goos=linux -f stage=behavioral-smoke on a hosted
ubuntu runner: converter build + filtered behavioral run, all green in 208 s. Why it matters:
every prior Linux datum in the campaign came through WSL on Windows hosts — this is the first
validation on NATIVE Linux, no WSL layer, no Windows host, and it corroborates the WSL evidence
independently at the smoke level. It also ran at the current master tip, four commits past the
consolidation SHA, so post-merge master is freshly proven on a second OS. Calibration figure
banked: 208 s smoke on a standard 4-core hosted runner (the CI-overflow shard sizing in
PLAN-hop-campaign.md can hang off it). The matrix has now proven BOTH of its stated purposes on
its first two dispatches: darwin census (found the readdir wall) and native-Linux control
(green). Stage-gating verified too - census and sweep-shard steps correctly skipped.
2026-08-23 · SECOND darwin census (run 32615001128 at fdaeae154) — wall #1 CLOSED on real hardware; wall #2 is two cgo-flavor leaves, and it is CONVERTER work
The re-dispatch against G’s branch, both mac legs byte-identical fingerprints: the 19 os
errors are GONE — the readdir companion compiles on real darwin, arm64 and x64 alike — and the
build marched deeper to the next leaves. The branch merged on that round-trip
(master 73f84546b, branch retired): companion + the red-then-green L3
suppression-companion guard + the projitems registration.
Wall #2: 10 errors, two leaf packages, all in darwin’s CGO-FLAVOR files — paths no other
GOOS compiles: os/user/darwin/cgo_lookup_unix.cs and net/darwin/cgo_unix.cs. The telling
fact: 7 of the 10 errors are in go2cs-gen GENERATED output, so this wall is
converter/generator emission, not a missing hand-own — a different species from wall #1.
Classes: (1) CS1729 x4 — ImplicitConvGenerator mints inverse conversions for the C-type mirror
wrappers (Byte/Int32/Int64/UInt32) assuming a 1-arg constructor those declarations do
not carry; (2) CS0246 x2 — TypeGenerator emits doBlockingWithCtx_result.g.cs for a GENERIC
function’s result struct without the type parameter in scope (a generics gap in the
struct-lift); (3) CS0266 x2 — long→uint numeric emission in cgo_lookup_unix.cs; (4) CS0246 x2
— addrinfoErrno→int inverse + a type-name slip in cgo_unix.cs. All four are reproducible on
Windows via -p:GoTargetOS=darwin local builds (G proved the flavor builds locally), so the mac
runners are the proof, not the dev loop. Work item minted; everything behind os/user and net
remains unmeasured until these leaves fall.
Board-mechanics note, recorded because it bit twice: the append that landed the native-Linux
entry used offset arithmetic against the guard line and ATE the < of the closing comment,
leaving ---!-- + the endraw tag + ... --> (the tag deliberately not spelled with its brace syntax here: quoted inside a raw guard it TERMINATES the guard – the exact defect that took Pages down at f37ba28ef, and then AGAIN via this very line before it was reworded) — Liquid still parsed (endraw terminates raw even there)
so Pages stayed up, but the junk rendered. This append repairs the guard and retires the
arithmetic: appends reconstruct the tail explicitly.
2026-08-23 · Span-unification census DONE — the harvest is nine discrete adoptions, not a sweep; one LATENT golib bug banked in passing
The future-harvest item is now scoped from evidence: full census at
CENSUS-span-unification.md (DRAFT, read-only, master
d03f086c1). Headline: golib is more span-clean than the hypothesis assumed — ToSpan() is
already the discriminant-once unification point, @string.Bytes already feeds the
comparison/hash/concat paths — so the win is nine discrete adoptions with ~10 further sites
dispositioned honest-no. First tranche by win×low-risk: C1 builtin.copy(slice<byte>, @string)
routes through the COPYING implicit conversion (a full string-length allocation + second copy
per call; every strings.Reader.Read pays it — fix is a two-line span copy); C2 same-type copy
unification over ToSpan (merges the managed/native fork); C3 the IByteSeq copy-constructor
element loops; C4 a ReadOnlySpan Append overload; C5 []rune(s) bypassing LINQ. All
golib-only, converter untouched, CNR as the verifier.
BANKED FINDING (latent correctness, found in passing): builtin.cs:863–864 — the
heterogeneous copy fallback indexes dst[dst.Low + i] / src[src.Low + i], but the slice
indexers are already window-relative, so any nonzero-Low operand DOUBLE-OFFSETS. Unreachable
from converted Go (Go’s copy is same-type — which is why no gate ever caught it); live for
hand-written/interop code only. The sibling overload’s own comment (builtin.cs:807–810) states
the rule the line breaks. Fix rides tranche C2.
Scheduling: ALL of it is golib, therefore inside the release-eve freeze — first post-release golib work, sized lane-scale. Process note from the census: several candidates delete CHARGED allocations, so the bytes/strings signature-pinned disclosure manifests may re-baseline in the favorable direction after the tranche — expect it, do not read it as drift. —
2026-08-23 · DESIGN PROPOSED — the Linux UDP seam: the eight //go:linkname datagram helpers, the decode half of the sockaddr mirror, and the last wall between the poller and name resolution (lane R, claude/linux-udp-design)
Commissioned in the overnight standing orders; written per house style: docs/phase4/DESIGN-linux-udp.md, STATUS PROPOSED, six OQs each with a recommendation, none self-ruled. Ratification requested on the mailbox.
- The wall (§1).
internal/syscall/unix/linux/net.csdeclares eight bodyless//go:linknamedatagram helpers —Recvfrom/Sendto/SendmsgN/Recvmsg× Inet4/Inet6 — and thePartialStubGeneratorfills them with throwing stubs. They are the corpus’s ENTIRE datagram surface:internal/poll’sReadFromInet4,WriteToInet4,ReadMsgInet4,WriteMsgInet4call them and nothing else does. Measured stack from the poller arc’s S2, plus a loopback UDP probe that isolates it exactly —ListenPacketbinds fine, the firstReadFromthrows, native Go completes the same round trip. - The bill (§1.2), and a CORRECTION to my own commissioning scope (§1.3). What this gates is DNS, hence every name lookup, hence
crypto/tls’sTestVerifyHostname(one of that row’s two divergences) and every converted program that resolves a name. What it does NOT gate, measured rather than assumed:net/http/cgi(24 divergences, allTestCGI*/TestChild*— child processes, the exec axis; Windows carries the same row at 36/39) andnet/http/cookiejar(conversion-blockedby a test-host golib reference gap,CS0234— a converter item, no datagram involved). The standing order named both as opening with this arc; they do not, and the entry says so first so the arc’s bill stays honest. The other four socket-ledger packages already validate without it. - The finding that shapes the design (§2.2).
syscall/linux/syscall_unix.cscarries CONVERTED bodies for the same eight — and nothing calls them; a census of the linux flavor finds zero call sites. They are dead code, valuable as a specification, and they confess both defects this arc must avoid in eleven lines: a MANAGEDRawSockaddrAnyhanded to the kernel by address (the open struct-passing class), and the very(*[2]byte)port alias that L10 and the sockaddr mirror retired for the ENCODE direction. So this arc is, in one sentence, the DECODE half of the sockaddr mirror — andSendtoInet4is nearly free, because Go’s own body callsto.sockaddr(), which the mirror already hand-owns. - The design (§3–§4). A new hand-own beside the stubs (⟨OQ-1⟩, per every linkname precedent in the corpus — the poller’s ten,
os/tempfile,math/rand×2,sync,net/dnsclient_impl), reusing the mirror’swriteNativeSockaddr/readNativeSockaddrandnativeSockaddrLen, over the keystone by number (SYS_SENDTO 44,SYS_RECVFROM 45,SYS_SENDMSG 46,SYS_RECVMSG 47). Payloads reach the kernel by PINNED slice-element address (no copy — the one managed storage golib can pin); addresses are stack images;fromis filled by assignment into the caller’s box. The msghdr pair needs a nativemsghdr+iovec(56/16 bytes, amd64, verified against the convertedztypes) and two-wayoobhandling, which is why it is staged second.EAGAIN/EINTRare RETURNED, never handled — the consumer’s loop parks on the poller, and a body that retried internally would read as “deadlines do not work on UDP” three arcs later. - Blast radius (§5) and the freeze. One new file, plus two mirror helpers made public (⟨OQ-2⟩), plus one emission-visible consequence: the file needs
unsafe, sointernal.syscall.unix.csprojflips<AllowUnsafeBlocks>for every GOOS (⟨OQ-3⟩ — accept, regenerate through a seeded reconvert, prove the flip is the only diff). Because that changes linux corpus emission, the implementation PARKS MERGED-READY under the release-eve freeze and goes first after the anchor release — stated in §7 so the staging is not mistaken for hesitancy. This design doc is docs-only and merges freely. - Staging (§7): S1 =
Recvfrom/Sendto×2 — closes the entire measured bill on its own; S2 = the msghdr pair (no consuming row today, which is why it is second rather than skipped); S3 = the consumer re-measure. Gates include a newUdpLoopbackRoundTripbehavioral guard (⟨OQ-5⟩), whose first draft is the probe written during the poller arc.
2026-08-23 · MEASURED — JOB-007’s Linux leg: the formal dual-OS consolidation sweep at 18770d083 is 152 green of 162 with ZERO regressions, and every one of the ten FAILs is an already-classified seam (lane R)
Release-trigger condition (d), Linux half. Run at merged master 18770d083 on R-LAPTOP (Ryzen 7 PRO 6850U, WSL2 Ubuntu 22.04), gate first: go2cs-stdlib.slnx -p:GoTargetOS=linux native --no-incremental 0 errors / 149 warnings in 465 s — the same warning count as before the poller merged, so the union added none. Sweep aggregate 19,113 s (5.3 h); per-row wall times banked in DATA-sweep-row-walltimes.md beside the i9’s Windows table (H5).
The arithmetic — a QUADRUPLE, because the per-OS ruling added a verdict class
149 PASS · 10 FAIL · 3 CVAC of 162 → 152 green. CVAC (“comparison-validated-at-count”) is the class the ruling’s item 4 introduced and it went live in this sweep: a row whose C# side matched Go’s own count on this OS but which has no linux: annotation to bank against. A CVAC row is validated, so it counts green. The three: crypto/sha1 13, bytes 86, go/internal/gcimporter 582.
Against my pre-merge 161-row measurement: 7 flips, ZERO regressions, 1 newly-measured row. The seven flips are the per-OS machinery landing rather than new code — path/filepath, debug/buildinfo, mime, crypto/rand gained linux: annotations and so validate instead of reporting COUNT; crypto/sha1 and bytes are G’s W1b closing; go/internal/gcimporter reports CVAC. The COUNT class is now empty.
The ten FAILs, each attributed — this is the green bar, so it is itemised rather than summarised
| row | wall | attribution |
|---|---|---|
crypto/tls |
711 s | 2 divergences of 402 comparable — TestVerifyHostname (behind the UDP wall, DESIGN-linux-udp.md) and TestCertCache (object-lifetime: SetFinalizer + forced GC + a 4 s refcount wait). 400 agree — the Windows banked count exactly |
time |
857 s | R6 — the ZONEINFO / TestEnvVarUsage row, unchanged since the census |
os/exec |
740 s | G’s named exec residue (mid-suite host death, TestLookPath dot-semantics) |
debug/gosym |
27 s | G’s host-limit disclosure — TestSymVersion is Go’s own skip (no .gosymtab section: the apphost is a .NET binary) |
internal/cpu |
26 s | W6 — capability-detection divergence |
os/signal |
36 s | W2 — test-variant emission defect (converter) |
syscall |
35 s | W2 — same class |
sync/atomic |
1,258 s | W7 — the Gosched ring, RULED as an honest FAIL pending the M:N horizon |
plugin |
6 s | W3 — the converter panic gcc re-exposed (conversionDriver.go:228 indexes GoFiles[i] by Syntax’s index under CGO_ENABLED=1) |
runtime/debug |
32 s | newly measured — row #162 did not exist in my earlier 161-row runs. TWO already-classified classes stacked: TestFreeOSMemory fails (the object-lifetime class DESIGN-readmemstats-surface.md §7.2.3 documents) and the host then dies mid-suite, leaving the other nine as cs=None — the shape G named in the exec residue |
Nothing unexplained, and nothing new. The one row that had never been measured on Linux resolves to two existing classes rather than a finding.
A harness note worth carrying
My per-row driver’s verdict regex was ^\s+(PASS|FAIL|COUNT)\s, written before the per-OS ruling, so the first two CVAC rows recorded as NOVERDICT — which reads exactly like “the sweep produced nothing”, i.e. like a broken leg. Caught at row 4 by reading the row logs, patched to include CVAC, the two bad ledger rows dropped, and the run resumed (the gate had already passed, so the resume skipped it; ~14 min lost). Any harness that greps the sweep’s verdict lines needs the same one-word patch.
2026-08-23 · UDP S1 DONE AND MEASURED (lane R, parked under the freeze) — the datagram seam closes; three findings outlive the arc
S1 works, measured on the distro: internal.syscall.unix + net build linux-native 0/0
with the REAL csproj; loopback UDP round trip AND a connected-UDP DNS exchange against the
resolv.conf nameserver are byte-identical to go run (32 out, 160 back, id and answer
count matching). Branch parked merged-ready (GPG park + emission-visible csproj change), first
in line after the release. The UdpLoopbackRoundTrip guard follows, parked the same way.
Finding 1 — the DNS residual is NOT a syscall wall. net.LookupHost still times out in
converted code while the transport underneath provably answers — the residual is in net’s own
resolver machinery, ABOVE the socket layer. Routed as a diagnosis-first item (root it, no blind
fix); S2’s msghdr pair is NOT implicated (connected UDP reads via plain read(2)).
Finding 2 — the L3 csproj-regen trap (⟨OQ-3⟩ amended in the design doc): a seeded single-package reconvert emits NO csproj; a single-TARGET stdlib filtered run regenerates one that silently DELETES the other GOOS ItemGroups. The instrument for an L3 package’s csproj regen is the THREE-target emission, full stop. Would have shipped a broken corpus if landed unmeasured.
Finding 3 — platform-specific hand-own companions need PRINCIPAL-LESS names. The L3 merge
routes X_impl.cs into every folder its principal X.cs occupies — net.cs is //go:build
unix, so a companion named net_impl.cs was copied into darwin/ carrying LINUX syscall
numbers (measured). Named net_linux_impl.cs it is principal-less and stays put; re-measured:
linux/ present, darwin/ absent. Rule: a companion whose BODY is platform-specific takes a
name with no principal, and says why in its header.
Fleet ledger — the WSL crash-dump disk shape: a three-target regen is a ~20 GB operation;
driving the WSL VHD to ENOSPC remounts the distro read-only AND writes enormous crash dumps to
%TEMP%\wsl-crashes on the HOST (136 GB observed, C: to 0 bytes). Check that directory first
when a box mysteriously reads 0 free; the ext4 inside was undamaged. R recovered to 136 GB
free; lane healthy.
2026-08-23 · The .slnx registrations item CLOSES — and the investigation re-scopes it correctly first
The queue item read “.slnx registrations (math/big, runtime/debug)”. Investigated before
editing, three facts established: (1) *.tests.csproj files are registered in NO solution BY
DESIGN — solutionGenerator.go:175-195 documents why (pipeline-staged, git-ignored inputs
mean a converted test project cannot build from a clean tree); the item was never about them.
(2) The REAL gap: core/math/big and core/runtime/debug sit in go2cs.slnx’s build closure
via GolibTests ProjectReferences but were unregistered — and the minimal CLOSED addition is
THREE entries, because core/bytes is math/big’s own direct dependency and was unregistered
too (BFS-verified; adding two would have broken the registered set’s transitive closure).
Landed as a hand edit to the hand-maintained solution, check-solution-integrity.ps1 green.
(3) The generated go2cs-stdlib.slnx needed nothing — both production csproj were already in
it.
Follow-up item minted (not taken now): the closure convention has drifted wide — the full
member closure of go2cs.slnx reaches 124 core projects and 55 are unregistered, dominated
by SystemCertVerify’s crypto/x509 closure plus net/context/json/flag/os.exec/os.signal/
path.filepath/time.tzdata. Same VS-only failure mode as ever (harnesses build by path), so it
rots invisibly; a leveling pass plus a gate that polices core registrations the way
check-solution-integrity.ps1 polices behavioral ones would retire the class. Itemized in the
investigation report. Also fixed in passing: CLAUDE.md’s stale mention of a
src/go2cs-examples.sln that no longer exists.
2026-08-23 · R’s resolver rooting: three findings, one severe — and the struct-passing census RE-PRICES to a leveling arc
Finding 1 — the LookupHost residual is RESPONSE HANDLING, not transport. An in-process
fake nameserver (echoes every query as a well-formed zero-answer response, same ID, same
question) splits it cleanly: Go accepts in 1 ms and reports “no such host”; the conversion
retries four identical, correctly-formed 42-byte queries and rejects every response, timing
out at ~10 s. Exonerated by individual probes: the S1 datagram transport, query
construction/randInt/runtime_rand, golib reslicing (the earlier 45-byte reading was the
probe’s own fault — finding 2), and the address decode. Remaining fork: the connected read
never delivers, or dnsmessage.Parser.Start/the ID compare rejects — fork-split by
instrumented scratch-clone build sanctioned, diagnosis-only past it.
Finding 2 — GENERATOR-CLASS: the converted type assertion misses interface satisfaction via
an EMBEDDED interface plus directly-added methods (c.(PacketConn) on a user type embedding
net.Conn with ReadFrom/WriteTo added — Go takes the UDP arm, the conversion takes TCP
framing). ImplementGenerator witness territory; ~40-line repro in R’s scratch; the live
resolver path is NOT affected (*UDPConn’s assertion works — proven by the fake-test’s own
42-byte queries). Queued for the generator lane (G) when its queue reaches it. Method note:
Go’s-own-baseline caught the first wrapper hiding the interface from BOTH runtimes — the only
reason a phantom defect was not reported.
Finding 3 — SEVERE, at master: net.Interfaces() kills the process on Linux
(AccessViolationException in anyToSockaddr ← Recvfrom ← NetlinkRIB): the generated
wrapper hands the kernel the address of a MANAGED RawSockaddrAny, and the kernel’s write
corrupts the embedded array<int8> reference — the AV-not-panic is the tell (a bounds-checked
indexer would panic on empty; only corrupted state AVs). Fix commissioned to R ahead of
everything: the mirror pattern (native image + typed decode), Recvfrom minimally,
Recvmsg/Sendmsg staying with S2’s evidence gate unless literally the same lines.
The re-pricing (coordinator ruling). G corroborated lane-to-lane within minutes: this is
the FIFTH confirmed instance of the kernel-writes-over-managed-array class
(Timezoneinformation, win32finddata1, ProcessEntry32, SiginfoChild, RawSockaddrAny)
— three platforms, one root, one remedy proven four times, and the class has escalated from
wrong values to memory corruption on a public API. The standing census’s
“deliberately not fixed speculatively” doctrine has EXPIRED on its own evidence bar: each new
instance now costs a diagnosis night that the proven mechanical remedy would have pre-empted.
Minted: the struct-passing leveling arc — sweep the censused wrappers (the board’s 9-row
census plus the instances named since) with the mirror pattern, G’s two darwin notes carried
in (**T out-params are a stacked second defect; [StructLayout(Sequential, Size=N)] with
explicit tail padding). Post-release, R’s queue after F3/the Windows wrappers/F1; per-wrapper
evidence (a probe per fix, no blind batch) still applies — it is the SCHEDULING that changed,
not the proof standard.
2026-08-23 · Census #3 (wall #3 pre-rooted from the coordinator seat) + two fleet-ledger traps + the Windows-UDP seam ruling
Wall #3 (run 32639211594 at d550cdc08): 9 errors, two leaves, both rooted before
dispatch. (1) vendor/golang.org/x/sys/cpu compiles BOTH a flat and a darwin/
package_info.cs — the L3 partial-routing question for a package_info that varies by GOOS.
(2) crypto/x509’s CS0234 on macos_package with the ProjectReference PRESENT and correct:
the darwin-exclusive internal/macos package holds sources only in darwin/ but its csproj
has NO GoTargetOS compile-selection block, so it builds a “successful” EMPTY assembly and
the dependent fails — the bookkeeping-artifact class through a new door; the fix belongs in
csproj emission for platform-exclusive packages. Dispatched to G; the next mac verdict proves
walls #2 and #3 together.
Windows UDP wrappers — ruled a SUBMIT-SEAM extension, not a wrapper fix (R, halted at the
design line): a Windows UDP send needs the operation record, a native OVERLAPPED and native
WSABUFs — all private to syscall’s WSA hand-own. A public Go-shaped seam on syscall is
rejected on that file’s own header doctrine; the ratified path is an amendment to
DESIGN-netpoll-managed-poller.md §4.3–4.5 extending the GoAsyncIO rendezvous, then
implementation under it. Not urgent (no roster row sends UDP on Windows); queued behind F1’s
resolver fork-split.
Fleet-ledger trap 1 — bash glob eats a PowerShell gate invocation:
powershell.exe -File <script> *>&1 from a BASH shell glob-expands the * into a positional
argument; the script rejects it, the wrapper exits 0, and an eight-second “pass” never ran the
gate. The *>&1 idiom is PowerShell-side only — from bash, quote it or omit it. (R, caught on
its own CNR run and re-run properly.)
Fleet-ledger trap 2 — partial-class FIELD-INITIALIZER ordering: a hand-own file of a
partial class must never initialize a field (e.g. = modws2_32.NewProc("WSASendTo")) from a
GENERATED sibling file’s static field — C# orders static field initializers within a type but
NOT across the files of a partial class, so the sibling can still be null and the first use
dies in a nil dereference far from the cause. Defer the lookup to first use (??=); any
hand-own reaching a generated mod*/proc* needs this. (R, found by crash while filling
WSASendtoInet4.)
2026-08-23 · The nested-field-pointer finding, CENSUSED and COMMISSIONED — 4 hazard sites, 1 function, 1 package: the family’s sixth sighting, smallest yet
The finding (lane R, F1’s fork-split): taking a pointer to a field of a NESTED struct
field and writing through it is silently lost — p := &b.header.questions; *p++ emits
count = Ꮡ(b.header).of(Δheader.Ꮡquestions), boxing a COPY of the intermediate field, so
the write lands in the copy. Rooted with a 40-line no-DNS repro (via-field-pointer and
slice-elem-field both lost; direct mutation fine). Surfaced as dnsmessage.Builder emitting
QDCOUNT=0 — which is BOTH real-world symptoms at once (nameservers ignore the malformed query;
the resolver waits out its deadline).
The census (CENSUS-nested-field-pointer.md, exhaustive
balanced-paren parse over all 5,565 Ꮡ(…) sites): the hazard shape appears at exactly
4 sites, all write-context, all in incrementSectionCount
(vendor/golang.org/x/net/dns/dnsmessage/message.cs:1349–1361) — not on the roster, but 13
files under src/core/net reference dnsmessage, so net’s future validation walks into it.
All lookalikes classified per-site: the two-arg element form is the PROVEN-correct aliasing
(guarded by SliceElementFieldAddress), the 6 one-arg .of lookalikes are read-context, and
the no-.of family is benign — with one watch item: 9 unix-only
Ꮡ(syscall.ForkLock).RLock/RUnlock write-context package-var sites activate with the Linux
campaign. Behavioral coverage of the hazard shape: ZERO — nothing contradicts the finding,
nothing guards the fix yet. Reproducibility note: the corpus address-of glyph is U+13D1
(a U+13E1 grep returns a false all-clear).
Family placement: the SIXTH sighting of the address-of-copy-boxing family (element aliasing, xml’s binding box, sync/atomic’s local box, gob = fifth), and the smallest. The ж-box arc is the PERFORMANCE axis over the same neighborhood and does not touch this correctness gap.
COMMISSIONED (to R, after the §4.7 implementation lands): direct fix with guard per the
family’s one-base-shape-per-fix precedent — the emission chains the address from the receiver
root (.of(...).of(...), a form already routine at 309 sites) instead of boxing the
intermediate field; one new behavioral guard for the write-through shape; dnsmessage regen;
CNR pass. Corpus churn today: one file, four lines. Parks under the freeze like the rest.
2026-08-23 · THE DARWIN CORPUS COMPILES — census run 32649840220 at c003d32af: ZERO errors on osx-x64 AND osx-arm64; there is no wall #4
The third GOOS reaches its Phase-3 moment. Census history, all four runs on real Apple hardware within ~24 hours of the FIRST darwin build ever attempted: 19 errors (os.readdir hand-own gap) → 10 (cgo-flavor emission classes) → 9 (the case defect + the selector-less csproj) → 0. Both architectures byte-agree at every step. Wall #1 was a missing platform companion; walls #2–3 were converter/emission classes rooted one layer deeper than each census read — and every root now carries a guard.
Credits where the record should hold them: lane G fixed all three walls (with two corrections of the coordinator’’s pre-roots and one of its own correction — the measured form won every time); lane R’’s readdir companion opened the door; the CI matrix was the only darwin compile surface the project has, and the coordinator-driven dispatch loop turned each fix-to-verdict round trip into minutes.
The branch (claude/darwin-cgo-flavor-emission @ c003d32af) PARKS merged-ready despite
the green — its corpus stage includes linux-flavor content (cpu’’s new
linux/package_info.cs), which is inside the release-eve freeze class; it merges in the
fixed post-release order (after the netlink fix). What darwin-COMPILES unlocks, priced but
not scheduled: the darwin census stage on CI flips from wall-finding to REGRESSION-GUARDING
(cheap, dispatchable at any branch tip); darwin behavioral-smoke becomes possible on the mac
runners; and operational validation (a darwin -tests lane) remains UNCOMMITTED —
evidence-ruled, per the ladder, not a rung by default.
2026-08-23 · F1 BANKED (e44bed59f, parked) — the sixth family sighting closes; two instrument-discipline traps join the ledger
The nested-field-pointer fix landed as commissioned: the emission chains one .of(…) per hop
from the receiver root, marking walks the same chain, single-hop stays byte-identical, pointer
hops excluded by type (already their own box). Guard ReceiverNestedFieldAddress is A/B-proven
in the direction that matters: against the UN-fixed converter it compiles clean and prints 0
for every value-chain write — the defect’’s exact scope, and why the guard had to be behavioral
rather than a golden. Seeded full-stdlib reconvert: zero corpus differences beyond the six-file
footprint; CNR clean across all 634. Two first-cut defects were caught by GATES, not by
reading: an IMPLICIT address (poly1305’’s promoted Sum(&mac) — no ast.UnaryExpr exists for
the marker scan to see) produced CS0103 that only the reconvert-and-BUILD caught — CNR could
not, no behavioral test has that shape; and name-matching the chain root over-marked a
shadowing local, caught by a golden churning with no behavior change. Object-identity matching
and the direct-ж requirement close both.
Ledger trap 3 — a per-package go2cs <pkg> reconvert into src/core is NOT a regen
instrument. Its closure differs from the -stdlib driver’’s; applying one as a regen emitted
csproj/.cs.auto/extra-file drift across three packages (recovered by checkout). The family
rule now has three members: single-package emits no csproj (OQ-3 amendment), single-TARGET
destroys L3 groups (same), and per-package-into-corpus drifts the closure. The only regen
instruments are the seeded -stdlib run (single flavor) and the three-target emission (L3).
Ledger trap 4 — the 10-minute foreground cap vs corpus-scale operations: a 2.5 GB seed copy
and a full -stdlib run each exceed a harness foreground budget on laptop-class machines —
seed in one call, launch the converter DETACHED in the next, per the established
Start-Process pattern.
2026-08-23 · Span tranche 1 (C1–C5) PARKED at c6e098926 — and a RULING: allocation-class golib changes gate by COUNTING, not timing
Tranche complete: GolibTests 276/276 (42 new guards), CNR byte-identical ×633 (the proof it
was golib-only), behavioral 606 green. Two scope corrections worth the record: C2b was TWO
inseparable defects — the banked double-offset plus an InvalidCastException in the same
heterogeneous-copy arm (int→long crashed rather than converting); no plain-primitive pair
reaches the arm AND survives the cast, so a guard for the offset alone would have been a test
written to pass — fixed together through one ConvertElement<T>. C4 was solved by
NARROWING: widening the existing span overload instead of adding a second, which would have
put a CS0121 ambiguity in front of every collection-expression call site — the exact hazard
the census flagged, avoided by subtraction.
The instrument ruling (coordinator, ratifying G’’s recommendation): the paired same-session
timing A/B returned +0.7–1.4% on treatment rows while the UNTOUCHED control (Sieve) moved
+5.0% and the identical-source Go binaries moved up to +17% between legs — host drift several
times the effect size. Read the control row first; when it moves more than the treatment,
the timing gate has said nothing. Standing rule for allocation-class golib work from
tranche 2 on: gate with AllocationCounter COUNTS (deterministic, host-independent, the
ByteSeqAllocationTests pattern) — C3’’s guard already asserts exactly-one-charged-allocation,
C1/C5 delete charged allocations outright — and reserve timing claims for a benchmark that
actually walks the changed path (a strings.NewReader read-loop perf row is MINTED as a
future perf-suite candidate, not commissioned). Sweep note standing: C1/C5 may re-baseline a
signature-pinned alloc-count disclosure FAVORABLY at the next sweep — expected, not drift.
2026-08-23 · FINDING (R): (Ꮡ<array<T>>)(uintptr) is MEMORY-UNSAFE by construction — 61 sites, 35 in runtime; DESIGN commissioned, fix is a representation decision
The mechanism, measured in GolibTests with no kernel/socket/async anywhere: array<T> is a
managed struct whose first field is a T[] reference; a native-backed Ꮡ materializes with
Unsafe.AsRef at the address — so the cast REINTERPRETS whatever bytes live there as a managed
array reference and dereferences it. Two regimes: zeroed memory → the reference reads null and
?? [] answers Length=0 — a SILENT wrong answer (this is the netpoll recv panic’’s true
mechanism, three layers from where it presented); memory filled with 0xAB →
Length=0xABABABAB — a fabricated managed reference handed to the GC, returning a number
by luck rather than safety. The source shape ((*[2]byte)(unsafe.Pointer(&x))) carries its
length IN THE TYPE; the emission erases it and array<T> has nowhere to hold an address — so
no spelling of the current types can be correct: the representation must change.
Census: 61 sites — runtime 35 (all flavors), syscall 14, internal/poll/windows 4, reflect
2, six elsewhere. Liveness UNAUDITED (stated, not glossed): ManagedPointerTokens.Resolve
rescues reflect-originated pointers, and the roster’’s 18,569 green verdicts empirically bound
what is reached today — the audit is part of the design, not a prior claim.
Release impact: NONE — the class exists at every validated SHA; same accounting as the
nested-field finding.
DESIGN COMMISSIONED (R — designs-first; a 61-site runtime-touching change gets no direct
fix): weigh remedy (1), a native-backed mode on array<T> mirroring the RATIFIED
native-backed slice<T> dual-mode (symmetry: one mental model, and that arc already measured
the hot-path branch cost this remedy re-raises), against remedy (2), a distinct
NativeArray<T> view implementing IArray<T> (visible/greppable, keeps array<T> untouched,
costs an emission change plus two types agreeing wherever IArray<T> is consumed — R’’s
recommendation, by the WSABUF-mirror reasoning). Remedy (3), pointer arithmetic, is recorded
REJECTED as primary (partial coverage — several sites pass the view onward) but available as a
peephole for constant-index sites under either. The design owes: the 61-site liveness audit,
the slice-arc’’s measured costs pulled in as evidence (G cc’‘d as that design’’s owner), and the
§4.8-fate measurement plan (with the byte-view fixed, whether recv needs the staging seam is a
one-run answer). Implementation parks post-release regardless of route.
2026-08-23 · Release-machinery hardening item (user-directed, from the 1.23.1.7 release run) — the flavor loop trusted a build output nothing verified
The finding, from the first genuinely-clean-tree release run ever taken: on R with SDK
9.0.316, dotnet build go2cs-stdlib.slnx completes successfully WITHOUT producing
gen/go2cs-gen/bin/Release/netstandard2.0 — a silent solution-member skip — and the
subsequent dotnet pack --no-build dies with “could not find a part of the path”. Every prior
box passed this loop only because LEFTOVER gen output from earlier builds masked the gap
(measured: the coordinator’’s same-day dry run wrote gen output at 6:37 AM on 9.0.317 — correct
behavior; R’’s two clean runs on 9.0.316 produced none — convicted twice). Flavor-independent
(linux merely bats first in the reversed order); SDK-dependent. The F15 recipe’’s exact
9.0.317 pin was already right.
Work item (post-release, small): push-nuget.ps1 gains POST-BUILD validation in the flavor
loop — after each build pass and before its pack, assert the load-bearing outputs exist
(minimally go2cs-gen.dll at its expected path, golib’’s assembly) and fail LOUDLY naming the
likely cause (“build completed without producing X — solution-member skip; check the SDK
against the F15 pin, 9.0.317”) instead of letting pack report a path error three steps later.
Stronger option to weigh in the same item: a repository global.json pinning the SDK band
(with an explicit rollForward policy) would turn the whole class into a fast, named
SDK-not-found failure — but it binds every developer build on every box, so it is a decision
with a blast radius, not a default; the item prices both and picks one.
—
Windows UDP: the send seam LANDED, and the read is the sixth struct-passing sighting (R, 2026-08-23)
internal/syscall/windows’ two WSASendtoInet4/6 linkname declarations were
PartialStubGenerator stubs, so every datagram send on Windows threw. They are hand-owned now
(windows/net_windows_impl.cs), submitting through the golib seam ratified as netpoll design
§4.7 — so syscall and internal/syscall/windows share ONE operation record without syscall
growing a Go-shaped public seam. Windows sends datagrams.
The read does not, and the measurement is the point. With the send fixed,
UdpLoopbackRoundTrip reaches ReadFromInet4 and panics index out of range [0] with length 0
in rawToSockaddrInet4. Attribution, because it is easy to get wrong: the SAME
(ж<array<byte>>)(uintptr)(new @unsafe.Pointer(...)) round-trip runs in the WRITE direction at
fd_windows.cs:1277 and works (TCP exercises it), so the round-trip is innocent. The difference
is the box — @new<syscall.RawSockaddrAny>(), managed arrays inside, handed to the kernel by
address. Per the AV-vs-panic rule, a clean bounds panic means the array is EMPTY, i.e. nothing was
materialised there. Sixth confirmed instance of the struct-passing class, on the DECODE side.
Pre-existing and previously unreachable (the send stub threw first) — the LocalTimeZone pattern
again: a real implementation exposes what the stub hid.
Its remedy is NOT this change’s shape. The received address arrives asynchronously, so a native
staging buffer must be decoded at HARVEST — §4.3’s decode-side problem, shared with AcceptEx’s
output buffer. UdpLoopbackRoundTrip therefore stays OUT of tree (registered it fails Windows,
unregistered it fails solution-integrity) and registers with the recv increment; it is
Linux-proven and parked.
Process-global registration and fake-based tests are in tension — resolve it INTERNALLY (R, 2026-08-23)
syscall registered its async operation factory lazily, from a TCP-shaped code path. A
datagram-only program touches none of it, so the first UDP send died with no operation factory
registered. Registering in a [ModuleInitializer] fixes it (the corpus already uses module
initializers for Go’s init()), but it means syscall claims the process-global factory in any
process that LOADS it — including GolibTests, which references core/syscall.
Signature to recognise: tests that pass filtered and fail in the full suite. Five of six new
primitive tests did exactly that; they could not win a registration race against a module
initializer no matter how they were ordered. The wrong fix is relaxing the public one-factory rule
so the tests pass — that rule is the property the seam exists to enforce (two owners for in-flight
operations is the failure it refuses). The right fix is an internal swap granted to GolibTests
through the InternalsVisibleTo golib already carries. Generalises beyond this seam.
2026-08-23 · LANDED — the second darwin wall falls: four emission classes closed, and a fold narrowed six times by the corpus itself (lane G, claude/darwin-cgo-flavor-emission)
Wall #2’s 10 errors are gone; both cgo-flavor leaves build 0 errors at -p:GoTargetOS=darwin. Four converter/generator classes, each rooted rather than pattern-matched:
- CS0266
long→uint(2). The constant fold’s unsigned arm declined every target narrower than uint64, so Go’s own negative-test constant1<<32 - 2fell to the literal path, widened tolong, and met auint32slot (_C_uid_t). Fold-then-cast — with five conditions and a story of its own, below. - CS1729 phantom hosts (4). NOT “ImplicitConvGenerator assumes a 1-arg ctor”, as the census read it: these records should never have been EMITTED.
os/userandnetdeclare their C-type mirrors as Go ALIASES TO PRIMITIVES (type _C_gid_t = uint32→global using _C_gid_t = uint), and the locality predicate counted an alias as a local declaration — so the generator hostedpartial struct UInt32on a BCL primitive. That is the phantom the predicate’s own comment exists to prevent, arriving through the alias door; the name RESOLVES (to the CLR spelling), so it fails on a missing constructor rather than a missing type. - CS0246 primitive sources (2). The same door from the other side: one genuinely-local operand admitted a record whose other side was
_C_int = int, and the generated operator dereferenced.Valueon a primitive. One shared predicate (typeIsPrimitiveAlias) closes both. - CS0246 unbound
T(2). A local named type inside a generic function closes over the function’s type parameters (doBlockingWithCtx[T any]’stype result struct{ res T; err error }), but the lift to package scope dropped them. The fix threads only the parameters the type ACTUALLY references — per the coordinator’s scoping directive — so a lift using none stays byte-identical; the declaration carries the binding, andliftedTypeMapcarries the constructed spelling so every use site agrees.
The fold’s six narrowings, because the method is the finding. The fix was correct at its motivating site from the first attempt and wrong everywhere else, and the local darwin build was GREEN at every step — only three-target corpus regenerations exposed it. Sites touched: 754 across 157 files → 46 → 12 → 10 → 2. What each pass taught is recorded in ConversionStrategies-Reference.md (“A NARROW-UNSIGNED target folds a constant only when nothing else can make it compile”): named-type identity loss, underlying-vs-named targets, named-constant readability, untyped-vs-typed operands, and target-width vs uint32 thresholds. The generalizable rule: a converter change is measured against the corpus, not against the file that motivated it.
Gates. os/user + net at darwin 0 errors · go2cs-stdlib.slnx windows 0 / linux 0 · converter go test ./... green (after every narrowing) · CNR byte-identical ×633 after one intended golden re-baseline (ConstSubexprOverflow, the test written for this exact construct; Output phase passed unchanged) · behavioral suite PASS 606 · marker census unchanged. Corpus staged narrowly: the darwin per-GOOS folders plus the two image/* files where the fold legitimately applies; an unrelated linux-flavor regen delta (a rename, a StructLayout import, an array-dims attribute) was left for whoever regenerates next rather than smuggled in.
Proof gate outstanding: the mac legs. My local darwin build succeeding is corroboration, not substitution — if CI disagrees, believe CI.
2026-08-23 · LANDED — the third darwin wall falls: a case-sensitive package class, and an L3 invariant the merge never asserted (lane G, claude/darwin-cgo-flavor-emission)
Wall #3’s 9 errors are gone, and both dispatch roots were one layer off. The fixes matter less than that, so they are recorded together.
Leaf 2 — crypto/x509 CS0234 on macos_package — is a NAMING defect, not an empty assembly. The dispatch read it as a missing $(GoTargetOS) selector block leaving zero sources compiled. Measured instead: the block is present, the Compile item set at darwin is all three darwin/*.cs, and the package builds 0 errors standalone. The real root is that crypto/x509/internal/macos is package macOS. The declaration side already names its class for the Go package (macOS_package); convertImportPathToNamespace composed every importer’s spelling from the import-path tail (macos_package). C# is case-sensitive, so the two never met — and the assembly genuinely contains no macos_package, which is exactly how a true observation (“the symbol isn’t there”) acquires the wrong mechanism.
The path-tail substitution existed but excluded the standard library, premised on “a stdlib package is named for its directory”. Censused across all three targets, that premise fails for exactly four paths: crypto/x509/internal/macos (darwin only), math/rand/v2 (already correct through the /vN branch), internal/trace/internal/testgen/go122 and runtime/internal/wasitest (nothing in the corpus imports either). Trusting the import graph everywhere therefore keeps the byte-identity the exclusion was asserting — with CNR as the proof rather than the comment as the premise. The fix restructures instead of special-casing: when the graph knows a package’s name, that name is the class segment; the /vN convention stays as the fallback. A narrower “substitute only when they differ” test would have looked equivalent and quietly broken the exotic case the convention exists for — a package literally named vN.
Leaf 1 — the duplicate package_info.cs — and two corrections against my own first measurement. The dispatch guessed the flat copy retires and the per-GOOS copies are routed. I diffed flat against darwin/, found them byte-identical, and concluded the opposite: shared, so flat is correct. Wrong. windows and darwin are identical for this package and linux is the one that differs (two extra position-map records for its own sources). The flat copy was the windows flavor all along. Comparing two copies of three proves nothing, and the dispatch’s guess was right.
The second correction is about credit. The live instance was healed by the merge’s existing plan loop — Stale copies removed: 1 is its variant branch retiring the flat copy once all three targets emitted the artifact. My reconciliation pass removed 0. The gap it closes is real and now guarded: the merge only reconciles logical paths some target re-emitted, and “emitted” means the bytes changed (needToWriteFile skips an identical write, which is what keeps a reconvert’s timestamps meaningful and what the seed sentinel rests on). A file every target reproduces exactly appears in no plan, so a corpus that already holds it flat and per-GOOS keeps both forever — invisibly, because the emitted csproj carries <Compile Include="*.cs" /> and <Compile Include="$(GoTargetOS)/*.cs" />, so the duplicate joins its own compilation (CS0579, CS1537 ×6). That state needs no mistake: an earlier regen routes a file per-GOOS, a later restore ritual recreates the flat copy, and both persist. HEAD genuinely held one. But it is a safety net that did not fire here, and it is not what fixed the wall.
Identical duplicates now retire; differing ones are deliberately not guessed at — with no emission data for a path no target rewrote, the merge cannot tell which platform owns the flat copy, and either choice silently breaks one, so it names both paths and fails the run. The scan tests isPlatformSourceFolder, not the directory name, because internal/syscall/windows is a real package whose directory is named for a GOOS.
Ride-along (coordinator-sanctioned): the staging seeder stops copying build outputs. Measured during this regen — the seed reported 60,931 files against a tracked corpus of roughly 4,000: ~57k build artifacts, copied once per target and three times per multi-platform run, the single largest cost in it. Nothing downstream reads them (the emitted-vs-seeded classification only considers files a target wrote, and a staging root is never built in), so the copy bought nothing.
Gates. crypto/x509 at darwin 0 errors · vendor/…/x/sys/cpu at darwin 0 errors · go2cs-stdlib.slnx windows 0 (cold, 395 s) / linux 0 (cold, 404 s), each after purging 916 output folders because a GoTargetOS switch changes the item set without changing a timestamp · converter go test ./... green with seven new guards, the naming one neuter-tested against the unfixed condition (it reports exactly the shipped macos_package spelling) · CNR byte-identical across all 633 behavioral packages · behavioral suite PASS 606 (Output 580 pass / 26 skip, 1,982 s) · go generate . no metadata drift · marker census re-measured at 70 marked files / 56 *_impl.cs companions, 0 clobbered.
Corpus staged narrowly, per wall #2’s precedent on this branch: the two crypto/x509/darwin files, and cpu’s package_info.cs → windows/ (git records a pure rename) plus a new linux/package_info.cs. The regen also carried ~24 linux/package_info.cs files gaining a <GoSourcePositionMaps> block, a runtime/windows implicit-conv record delta, a Δp.Value != v.Value → Δp != v pointer-comparison change, five .cs.auto refreshes and 52 CRLF phantoms — all other lanes’ arcs that landed without regens, restored rather than smuggled in.
Method note, the mirror of wall #2’s. Wall #2 taught that a converter change is measured against the corpus, not the file that motivated it. Wall #3 teaches: a pre-rooted dispatch is a hypothesis with evidence attached, not a finding — and so is your own first measurement. Both briefs described real observations and named the wrong mechanism; my own correction of one of them was itself wrong, from measuring two of three copies. The cheap checks that separated all three — read the csproj, diff every flavor, ask which code path actually removed the file — cost minutes each.
Proof gate outstanding: the mac legs, now against the wall-#3 tip rather than d550cdc08. One run, both walls.
F1 fixed: &recv.f1.f2 copy-boxed, and the two halves of an address-of fix must move together (R, 2026-08-23)
The converter recognised &recv.field but not &recv.f1.f2. The deeper chain fell through to
the Ꮡ(value) copy-box, so every write through the pointer went into a temporary – compiled
clean, ran clean, printed wrong numbers. Corpus census: 4 write-context sites, all in
dnsmessage’s incrementSectionCount, where it left the DNS header’s QDCOUNT at 0 and surfaced
three levels away as an unexplained resolver timeout. Fixed by walking the chain in BOTH the
emission (convUnaryExpr) and the scan that marks a method direct-ж
(bodyTakesReceiverFieldAddress); one-hop emission is byte-identical, so nothing else moved.
The lesson that generalises, and it cost a full-corpus build to learn: an address-of fix has
TWO halves – where the address is EMITTED and where the box is MARKED – and shipping one
without the other is a compile error, not a no-op. Marking is driven by scanning for an
explicit &recv.f1.f2. An implicit address is invisible to that scan because there is no
ast.UnaryExpr in the tree at all: h.mac.Sum(&mac) in poly1305, where Sum is promoted from
an embedded field, takes &h.mac.macGeneric implicitly. The first cut of the fix emitted the box
form there and produced CS0103: The name 'Ꮡh' does not exist – in a method whose receiver is
this ref MAC h. A deep chain now additionally requires the enclosing method to actually BE
direct-ж; implicit sites decline and keep their value-chain form, which is already correct for
them (a ref receiver reaches real storage).
CNR would NOT have caught it – no behavioral test has poly1305’s shape. It was caught by the seeded reconvert-and-BUILD that CLAUDE.md already requires for Ꮡ-machinery changes, which is the second time that rule has paid for itself. Corpus footprint after the fix: 6 files (dnsmessage
- its
netcaller, which now boxes the Builder, + a transitiveruntimepromotion), fullgo2cs-stdlib.slnxbuild succeeded.
Two process notes worth banking. (1) A per-package go2cs <pkg> reconvert into src/core is
NOT equivalent to a -stdlib run and must never be used to apply a regen: the closures differ, and
mine emitted csproj/.cs.auto/extra-file drift across runtime, net and poly1305 that took a
git checkout -- src/core to undo. Apply regens from ONE seeded full reconvert. (2) On this
machine a 2.5 GB seed copy plus a full -stdlib run both exceed the 10-minute foreground cap –
seed in one call, launch the converter DETACHED in the next, and poll; a foreground converter
killed at 300/304 leaves a root the ritual says to throw away.
2026-08-23 · LANDED — F2: a promoted interface method was not in the type’s GO METHOD SET, and the root was two layers below where it was reported (lane G, claude/implgen-embedded-witness)
The defect. A type satisfying an interface via an embedded interface plus directly-added methods was not recognized by a converted type assertion — a user type embedding net.Conn with ReadFrom/WriteTo added failed c.(net.PacketConn), so Go took the UDP arm and the conversion took TCP framing. Reported as ImplementGenerator witness territory.
Four controls, each of which killed a candidate root. Reproduced from scratch without net, in a two-package module:
| Variant | Result | What it kills |
|---|---|---|
| Same-package interfaces | works | “embedding is the defect” — the axis is that the interface is FOREIGN |
plain, both methods declared directly, no record |
works | “the GoImplement record is the mechanism” |
Direct call w.Read() |
works, emits w.Reader.Read() |
“the promotion is missing” — the converter resolves the hop at call sites |
holder, ordinary field whose name equals its type’s simple name |
Go says no, converter emits no record | “a name heuristic can drive the fix” — it cannot; the record can |
The root, two layers below the report. ImplementGenerator is exonerated: it already emits the promoted method as a real MEMBER, from the Promoted = true record it already writes. golib builds a type’s Go method set exclusively from EXTENSION methods (GetGoMethodSetCandidates), and a promoted method is the one kind of Go method that never becomes one. builtin.Implements<T> answers a DIRECT assert with C# is T — which is why asserting to the embedded interface itself always worked — while any other interface falls to the structural probe, which saw only the directly-declared half.
Fixed where the method is created, not where it is missed: the promoted method is now emitted as an extension method too, so it is an ordinary Go method — probe finds it, AdapterBinder binds it through the same candidate source, reflect.NumMethod counts it, as Go does.
Why not golib, having been sanctioned to fix it there — the invariant was working. Two refusals, both in golib’s own words: every method-set candidate is assumed to be an extension method whose FIRST PARAMETER is the receiver (PrefersBindableShape indexes GetParameters()[0]), so a no-argument member throws; and GetGoMethodSetEntries’ header states that probe, count and binder resolve through ONE source precisely so they cannot disagree — teaching only the probe would make Implements answer yes for a shell that then fails to bind. A design that refuses you for a stated reason is evidence, not an obstacle.
Method note — the root moved three times, and each move was a measurement, not a guess. Reported at ImplementGenerator; I placed it at “the members are never realized” (wrong — they are); then at “the record is the mechanism” (wrong — plain works without one); then at golib’s method set (right); then the remedy moved to the generator when golib refused it. Every step was one cheap control away, and the pattern that made it converge is the same one wall #3 produced: a pre-rooted report is a hypothesis with evidence attached, and so is your own last answer. Corrected before spending, twice.
Gates. Behavioral suite PASS 607 (Transpile/Compile/Target 607 each; Output 581 pass / 26 skip; 0 fail) · Target byte-identical and a full 607-project re-transpile left the tree clean, so no converter output moved · CNR byte-identical · guard neuter-tested (with the emission disabled its Output phase fails). Accessibility was measured before emitting: ExtensionMethodRegistry discovers with BindingFlags.NonPublic, so an unexported type’s twin is still found by a FOREIGN assembly’s assert — exactly where F2 bites.
The guard’s third row is the one to keep. EmbeddedInterfaceWitness prints NumMethod per row (2/2/1) so the method set cannot gain an entry it should not, and holder — whose C# field is identical to wrapper’s — is what stops any future fix from resting on the name heuristic that once made dwarf forward Common() through a named field and return a silently wrong answer.
(ж<array<T>>)(uintptr) is MEMORY-UNSAFE by construction — 61 sites, 35 in runtime (R, 2026-08-23)
array<T> is a MANAGED struct whose first field is a T[] reference (array.cs:47,
Backing => m_array ?? []), and a native-backed ж<T> materialises its value with
Unsafe.AsRef<T>((void*)m_nativeAddr) (ж.cs:250). Together those mean
(ж<array<byte>>)(uintptr)(…) reinterprets whatever bytes live at that address as a managed
array reference and then dereferences it.
Measured in GolibTests against golib directly — no kernel, no socket, no async:
| memory at the address | result |
|---|---|
| zeroed | Length=0 (the reference reads null, ?? [] gives the empty array) — a SILENT WRONG ANSWER |
filled 0xAB |
Length=-1414812757, i.e. 0xABABABAB — it fabricated a managed reference out of the filler bytes and dereferenced it, returning a number instead of faulting BY LUCK |
The second row is the finding: this is a type-safety hole, not a wrong result. Any real data — a
filled sockaddr, a siginfo, a register block — takes that path.
Census: 61 sites. runtime (+ per-GOOS) 35, syscall darwin/linux 14, internal/poll/windows
4, reflect 2, and 6 across net/darwin, the registry wrapper, vendor/…/route, vendor/…/sha3.
Not yet audited for liveness — the ManagedPointerTokens.Resolve arm rescues reflect-originated
pointers, and many sites are simply unreached on the current roster.
The source shape is unrepresentable, not mis-emitted. Go’s
p := (*[2]byte)(unsafe.Pointer(&pp.Port)) carries the length IN THE TYPE; the emission erases it,
and array<T> has nowhere to put an address anyway (it holds a T[], not pointer+length). No
converter-side spelling of the current types can be correct — the representation has to change.
Three remedies priced on the mailbox (native-backed array<T>; a distinct NativeArray<T> view;
pointer arithmetic per site); placement is the coordinator’s.
⚠ CORRECTION to the three entries above it: the Windows UDP read is NOT the struct-passing class (R, 2026-08-23)
I banked the recv panic as “the sixth struct-passing sighting, decode side” in the §4.7 board entry, in netpoll §4.7.6 and §4.8.1, and in ConversionStrategies-Reference. That attribution is wrong and it was mine. The panic is the byte-view defect above: a golib representation hole with no kernel anywhere near it.
The argument I used was itself false, and the way it was false is the lesson. I wrote that the
identical round-trip “runs in the WRITE direction at fd_windows.cs:1277 and works today — TCP
exercises it”, and concluded the round-trip was innocent and the managed box guilty. TCP does not
exercise it: sockaddrInet4ToRaw has exactly three callers — WriteMsg, WriteMsgInet4,
WriteMsgInet6 (:1456/1490/1520), the WSASendMsg path — and nothing on the roster reaches them.
The write direction was unexercised, not working, and it carries the same expression writing
p.Value[0] = …, so it fails identically the moment anything calls it.
“A passes, therefore the code A shares with B is sound” requires checking that A actually runs B’s code. I inferred it from a suite being green and did not check the call graph — the same species of unmeasured inference the AV-vs-panic rule exists to replace, applied to my own claim instead of someone else’s. The probe that settled it took two minutes and should have come first.
2026-08-23 · Release-machinery item #2 (user-directed): the NEWS lands BEFORE the tag mints, so the tag’’s tree carries the announcement
From 1.23.1.7’’s ritual: the nuget-<version> tag deliberately mints at Phase 1’’s pre-build
point (provenance — the READMEs frozen inside the published packages link tree/nuget-<ver>),
but the NEWS/milestone text was applied AFTER the release — so a visitor browsing the repo AT
the tag sees a NEWS block that predates the announcement. The tag cannot move (it anchors the
shipped binaries’’ exact tree; the post-release master contains merged work the binaries do not).
Rule for the next release: the §4-style announcement text — NEWS block, NEWS.md entry,
milestone table rows — is applied to master BEFORE release-nuget.bat runs, so Phase 1’’s tag
contains it. The version placeholder is deterministic pre-bump (build+1, the would-be version
the dry run already computes), and links into docs/validation/<version>/ resolve on the live
site regardless of which tree the visitor browses. Add the step to the release checklist
(MILESTONE-prep §3-style docs) alongside the existing hardening item #1 (the post-build
gen-output assertion, landed) and the pack-race root-cause (in forensics).
2026-08-24 · THE COMPLETION LEDGER CLOSES — JOB-013 green at 8922d453a; the .NET 10 HOP ERA OPENS
The user’’s sequencing rule — complete the arc, then the hop — is satisfied by measurement.
JOB-013: sweep 162/162 (18,569 = 18,569, verdict arithmetic closed by hand), behavioral
609/609, drift matching the documented shapes. Every ledger item closed on its own evidence:
the three JOB-010 regression shapes (each a SUBTRACTION — the twin is always internal, the
twins leave bare-name lookup, and the promoted pair registers only when types.Implements
holds), the leveling bank (which proved itself complete by regenerating to nothing), the
provenance increment end to end (mechanism → closed-form audit → consumer, with the suite that
killed the safety floor passing the mechanism 609/609), and the pack-race forensics
(measured-and-hardened: node reuse defused, the repair kept as an alternation instrument).
H1.4 rode alongside and closed the false-green catalogue’’s last route — all five are shut,
so the corpus hop cannot be lied to by its own gates.
One rider, non-gating: JOB-013 reported 8 files of unclassified drift, all pure deletions of
erroneous Promoted = true records — verified against master: Shape C’’s fix is correct and the
COMMITTED corpus test-info for five packages simply predates it. That is intended emission
drift, banked by the proper instrument (a per-package -tests regen, never a hand edit), and it
gates nothing: the sweep that measured it was itself 162/162 green.
Era transition. The initial era closed with a signed, published, validated anchor
(1.23.1.7, over 75% of the testable standard library, on Windows and Linux). The new era opens
on the ladder’’s next rung: .NET 10 first (mandatory before .NET 9’’s 2026-11-10 EOL;
DotNetMigration.md executed AS WRITTEN, its first run doubling as its shakedown, deviations
fixing the runbook in-stage), then Go 1.23.12 (recon banked, the W=4 shard map computed
from real per-row wall times). Seats: G owns the .NET 10 before/after performance report —
fresh baselines with the bflat exploration’’s lessons folded in and prediction N5 closed on the
AOT leg; R owns Stage-0 fleet provisioning; the i9 owns the stage gates it has owned all along.
—
2026-08-25 · THE DOC HIERARCHY INVERTS — the runbooks lead, and nothing was deleted to do it
The era directive above says the runbooks are executed AS WRITTEN, deviations fixing the runbook
in-stage. The tree did not say that. Exactly one sentence contradicted it —
GoCorpusMigration.md’s header, “where they disagree about what to do, the plan governs” — and
everything else pointed runbook-ward already. A five-commit docs train on doc-consolidation
makes the tree state what it practices. Its internal amendments are stamped 2026-08-24, the day
the design was ruled.
The ordering was load-bearing, not stylistic. The plans carried four statements the runbooks
and the tree had already falsified, so those were corrected FIRST (commit 1) and authority moved
SECOND (commit 3). Inverting first would have crowned a falsehood in the new direction. The
falsified four: H1 step 1 asking for GOROOT/VERSION (a pin redirects it silently); 1.4.2’s
“proposed remedy” for a hole closed at H1.4; “the Linux corpus does not yet build”, overtaken when
the wall fell 2026-08-14 at 307/307; and OQ-14’s “every hop publishes”, which was framed over
Go-version hops and needed OQ-H3’s scoping.
The runbooks caught up to their records (commits 2a/2b) — nine lessons absorbed into
GoCorpusMigration.md, four into DotNetMigration.md, each a rule that generalizes, each with its
destination in the SAME commit as its removal so the diff proves nothing was lost. The sharpest:
DERIVE the reserved set at generation time (the copied list drifted twice); a CRASH is not a
divergence and no disclosure absorbs one; fragility has TWO axes and a signature-oriented triage
looks at only one; a row with NO manifest compares strictly, so strict-compare rows with
upstream-changed production code outrank big manifests; and bank a migration’s INPUTS in the commit
that claims them, because the report is not the artifact. DotNetMigration.md also stopped
prescribing a defect: it told a hop to hoist the TFM, and the hoist was tried in that stage and
falsified. A hoist still needs an editor; a derivation needs nobody.
Nothing was deleted. PLAN-corpus-upgrade.md sections 2, 3 and 4 became anchor-preserving
pointer shells: every H-heading survives because they are a citation namespace — migrate-gorelease.ps1
cites H2 and ConverterBuildInputs.cs cites H1.4 by name — and all nineteen OQ-n marks survive
because section 8’s rulings are what a runbook “(ruled)” resolves against. Two readings with no
other home were retained rather than shelled. The counterweight that makes the inversion safe is
stated in both runbook headers: a runbook edit never reopens a ruling.
Records got their state lines (commit 4). Five documents still said DRAFT while the work they
scouted had shipped. The 1.23.12 rehearsal’s eight findings each carry a disposition now: six
closed against named commits, one (the Linux-vs-Windows t_r question) resolved by standing
rule — LANES.md already rules that cost inputs come from fresh calibration at campaign recon, so
the leg is measured with k and s_w rather than ruled separately — and one carried as evidence.
Finding H’s last third closed too: the rehearsal’s own three raws are banked verbatim in
hopA-inputs/, so no hop input is living in a session scratchpad any more.
Hop A’s one open blocker is now visible from the campaign plan, not only from an orphan record:
the asynctimerchan=2 AV, which is bounded runtime work owed BEFORE H10.
Doc types are defined in Glossary.md — runbook, plan, hop, record, board, mailbox — and
CLAUDE.md carries the one-paragraph ladder. Routing rule, in one line: procedure to the runbook
in-stage, harness and gate doctrine to CLAUDE.md, findings and measurements here. The mailbox
header said “doctrine lands on the board” and predated the runbooks; it now routes by kind. That
header also lives on claude/mailbox and the coordinator mirrors it there.
Docs-only: 21 files edited plus 4 created, all under docs/ and CLAUDE.md. Zero source, zero
corpus, zero goldens — so CNR is not owed, and the gate each commit DID carry is link integrity
over its touched files. One tier of hygiene is deliberately deferred past the hop’s start:
DotNetMigration.md’s Stage-0 incident narratives, whose home is STAGE0-provisioning.md.
2026-08-25 · MEASURED at the new pins — math/big does NOT bank, and the TestMulUnbalanced ratio moved the WRONG way: 51x -> 59x across the hop (lane R, claude/laneR-nethttp-mathbig)
The coordinator’s addendum commissioned a re-measurement of math/big at net10.0 + Go 1.23.12 with an
explicit either-way question: if .NET 10 brought the converted nat.mul inside Go’s 10x bound the row
banks at one disclosure; if not, the row stays with the zh-box/B’ arc and the fresh reading is that
arc’s exhibit. It is not inside the bound, and it is further out than it was.
| measurement | allocSize | ratio (Go’s assert) | pins |
|---|---|---|---|
| board, r58b / r59 | 20,499,128 B | 51 | Go 1.23.1 · .NET 9 |
| this lane, run 1 | 23,767,976 B | 59 | Go 1.23.12 · .NET 10 |
| this lane, run 2 | 23,750,664 B | 59 | Go 1.23.12 · .NET 10 |
| Go itself, same assert | — | passes (bound is 10) | Go 1.23.12 |
inputSize is fixed by the test at (50000+40) x 8 = 400,320 B, and the assert is
ratio := allocSize / inputSize; ratio > 10 — so the second %d in the failure text IS the computed
ratio, not the bound. Read it that way; it is easy to misread as “59 times” being a budget. The two
runs sit 0.07% apart — the same noise scale as the board’s own 20,499,128 against r58b’s
20,487,208 — so 59 is a stable reading, not a sample. Across the two hops the converted path
allocates +3,268,848 B more than it did: +15.9%.
Verdicts otherwise unmoved: 224 of 226, the same two rows as at 1.23.1 (TestMulUnbalanced,
TestNewIntAllocs). Unlike net/http/internal, whose count moved 9+1 -> 14+1 on the release’s
bare-LF hardening, math/big’s suite did not grow at 1.23.12 — 226 on both sides. TestNewIntAllocs
re-measures at exactly 1 obj/run on all seven NewInt shapes (want 0), verbatim the prior
reading — class 3b, the escape-analysis elision.
Consequences.
math/bigstays OFF the near-term reserves, where the 2026-08-21 ruling placed it. Nothing about the hop rehabilitates it, and the row’s price went UP rather than down.- The +15.9% is not attributed here, deliberately. This was a measurement lane; attributing it would need the decomposition probe the arc owns, and a fix inside a measurement lane un-controls the measurement. It is filed as a question the zh-box/B’ arc should carry: whichever of net10.0 or go1.23.12 moved it, the arc’s target just got ~16% bigger, and an arc sized against 51x is sized against a stale number.
- The row is not disclosable — the 2026-08-21 ruling stands, and this measurement reinforces it
rather than reopening it. A disclosure names what is provably unsatisfiable; a quantitative excess
an arc can reduce is not that. (Contrast the same day’s
net/http/internalcall, where the excess is one object and structurally inherent, and the disclosure IS correct — the two rows together are a usable worked example of where the line sits.)
2026-08-25 · POST-HOP FRONTIER FOLD — the frontier re-derives twice and agrees to the digit, row 163 banks, tier-1 re-prices at the new pins, and three rulings land (coordinator)
The march to 100% resumes on net10.0 + go1.23.12. The frontier was derived twice independently —
a coordinator-side Opus lane and i9’s JOB-019 step 1, different scripts, both under parse-count
and subset controls — and agrees exactly: 215 testable − 162 banked = 53 at derivation time.
Six of the 53 are ruled out on windows/amd64 (internal/runtime/syscall, internal/syscall/unix,
net/internal/socktest, log/syslog, runtime/race — no eligible Test declarations on this
target; os/user — Go’s own oracle fails TestGroupIds, no clean baseline exists), so the honest
Windows frontier was 47, and with this fold’s bank it is 46 of 52. Four frontier rows are
already Linux-proven at zero divergences (net/smtp 19, net/http/httptest 55,
net/http/httputil 53, net/rpc 15 — the 2026-08-22 S3 ledger) and Windows-socket-walled; the
per-OS roster-shape question they pose is HELD at the coordinator, unruled here.
Row 163 banks: net/http/internal, 14 matching + 1 disclosed at Go 1.23.12 (lane R,
claude/laneR-nethttp-mathbig). The count MOVED from the board’s 1.23.1 pricing of 9+1 — the
release’s bare-LF request-smuggling hardening added TestChunkInvalidInputs and its four
subtests — which is the hop doctrine (measure, never assume) earning its keep on the first
post-hop bank. Post-merge filtered sweep at the MERGE RESULT: PASS 14, exit 0; roster header
recomputed from the table under a parse-count control: 163 rows / 18,612 matching / 86
disclosed, delta exactly this row.
RULING — the alloc-assert triage recipe is doctrine, and the row’s disclosure class was corrected by it
R’s reclassification of TestChunkReaderAllocs from the dispatch’s alloc-count-semantics to
alloc-profile is RATIFIED, with its rooting: the converted NewChunkedReader allocates the
box (Go’s &chunkedReader{} analogue) PLUS a chunkedReader->io.Reader interface shell, and
DESIGN-iface-shell-caching.md §2 rules the shell structural — C# has no two-word interface
value; nothing removes it short of IDynamicInterfaceCastable; the measured loop mints a fresh
reader per iteration so no cache amortizes it. The triage recipe generalizes and is now the
standard (open the emission for the function under the assert; count the allocations on the
measured path):
- the converted path allocates an object Go’s does not (interface shell, ж box, captured-closure
frame) →
alloc-profile— disclosable only when the extra object is structurally inherent; - comparable counts, merely MORE of the same → a quantitative excess an arc can reduce — never disclosed; route it (math/big is the worked example);
- the COUNTER itself is incomparable (context’s byte-derived shim) →
alloc-count-semantics, the RARE class (6 rows corpus-wide), never the default.
RULING — TestGCMAsm closes as a source-defined platform skip; the manifest class platform-skip is minted
Held since the 2026-08-20 measurement pass; i9’s tier-1 re-measured the identical shape at the
new pins (12/13, C# skips "no assembly implementation of GCM", gcm_test.go’s own skip). The
ruling: the C# side executes the skip branch Go’s own test source defines for a platform
without a distinct assembly GCM implementation, and the managed corpus IS such a platform — by
design and permanently (no .s codepaths). The divergence is between two PLATFORMS’ verdicts on
one test, not between Go and the conversion on one platform. runtime-capability’s refusal
stands (a truthful second GCM exists at a cost); this class exists because that cost buys nothing
Go’s own source does not already define away — building a second GCM whose only consumer is a
differential test manufactures a platform property instead of measuring one.
Admission test, binding: the skip taken must be the UPSTREAM test’s own skip statement,
conditioned on a platform property the deployment genuinely and by-design holds — never a
host-limit workaround, never a skip the harness injects, never a skip added by conversion.
Anti-laundering: the signature pins Go’s own skip message from the test source, and the row
records the verdict pair (Go pass / C# skip) openly. crypto/cipher banks at 12 matching + 1
disclosed (platform-skip); the banking lane implements; the class joins the roster preamble
WITH the banking commit, per the chan-direction/runtime-capability precedent.
RULING — the ж-box arc re-sizes against 59x, not 51x
R re-measured math/big’s TestMulUnbalanced at the new pins twice (0.07% apart): allocSize
23.77 MB / ratio 59 vs the banked 51 — +15.9% allocated bytes across the two hops,
unattributed by design (a measurement lane does not decompose; the arc owns the probe). The row
stays with the ж-box/B’ constituency per the 2026-08-21 ruling; an arc sized against 51x is sized
against a stale target. TestNewIntAllocs re-measures verbatim (1 obj/run, all seven shapes);
224 of 226, suite unchanged at 1.23.12.
Tier-1 shard, folded (i9 JOB-019 — measurement only, artifacts retained per row)
encoding/gob100/107 (5 matching skips): ONE divergence,TestIgnoreDepthLimit→typelinksNotImplementedException viareflect.typesByString— consistent with the StructOf/ArrayOf pricing. Thereflect.ArrayOfhand-own is in flight, andDESIGN-reflect-structof.mdis RATIFIED WITH AMENDMENTS (nine, all applied by its lane): an adversarial review measured three mechanism defects on paper before any implementation —collectGoFieldsreads array dims from a ZERO INSTANCE (FieldArrayDimsoverActivator.CreateInstance), not from[GoArrayDims], so the synthesizer must emit a parameterless ctor initializing array-kinded fields; the intern key must carry dims/chanDir/ keyDims cargo (aSystem.Typecannot separate[1]intfrom[2]int); embeddedness rides theʗfield-name prefix and aString()-based guard cannot see it. Plus: the per-type mint cost is 875–925 µs WITH the forced[GoType("dyn")]stamp (the stamp costs +73–78%; TFM is not the variable), and OQ-2’s Run-vs-RunAndCollect delta was an in-process measurement-order artifact. StructOf implementation staffs as its own lane AFTER ArrayOf merges.net/netip210 + 58 alloc-assert fails collapsing to 3 top-level tests — class TBD under the recipe above; the emission read is dispatched. NOT pre-classified, deliberately.crypto/cipher12/13 → banks under theplatform-skipruling; dispatched.debug/pe9/10 — the[3]uint8aux-record read back as 8 elements, unchanged; rides the zero-size-field LAYOUT arc or a one-file hand-own of the symbol reader.log/slog— three shapes: (1) alloc-assert rows, class TBD under the recipe; (2) five logic divergences (TestJSONAndTextHandlers/Source/json,TestCallDepth,TestLogLoggerLevelForDefaultHandler,TestRecordSource,TestSetDefault); (3) a NEW harness-termination finding — afterTestSetDefaultruns and fails, the host emits no further run events for ~10 remaining top-level tests (Go="pass" C#=""mass-empty tail that is neither the file-lock nor the deadline shape: no orphaned child, no partial run event — the process stops advancing between tests).TestSetDefaultmutates the package default logger; the plausible surface is host progress reporting entangled with that global. A diagnosis lane is dispatched; the finding gates log/slog’s row regardless of the alloc classes.
FINDING ROOTED — log/slog’s “harness termination” is not a harness defect: the host DIED, and it said so
Lane claude/slog-harness-halt reproduced i9’s tier-1 shape exactly at cc90c2bf1 (Windows,
go1.23.12, net10.0, -test-timeout 15m, 209 s): 181 terminal verdicts, the last of them
TestSetDefault → fail, and then nothing for the ten ordinally-later top-level tests. The host
did not stop advancing. It exited on an unrecovered goroutine panic, which is deliberate
Go fidelity (TestRunner.cs:158-170), and it recorded exactly that before dying — the final event
in go2cs_test_results.json is a package-level fail whose output reads “test binary died on an
unrecovered panic in a goroutine”, and the host’s stderr additionally prints Go’s own crash-report
form naming log/slog.(*defaultHandler).Handle() at handler.go:117. The diagnosis was already
in the artifacts; it was read as emptiness. No host change is owed for the termination itself.
Root chain, mechanical. Go guarantees an imported package is fully initialized before the
importing package’s own initialization. go2cs maps a Go init to [GoInit] = .NET
[ModuleInitializer], whose guarantee is weaker — a module constructor runs at first access to
that module — and golib’s builtin.initPackage (src/core/golib/builtin.cs:213-236, whose
doc-comment states Go’s rule verbatim) exists precisely to close that gap. But the converter emits
that forcing only for BLANK imports (src/go2cs/visitImportSpec.go:370, into
v.blankImportInits). For a NAMED import the ordering is therefore absent, and log/slog is a
package where the difference is observable:
src/core/log/log.cs:268-269—log’sinitassignsΔinternal.DefaultOutput.src/core/log/slog/logger.cs:57-59—log/slog’sinitREADSloginternal.DefaultOutput. The test host touchesslogfirst, so slog’s module ctor runs whilelog’s has not: the read yields nil.src/core/log/slog/handler.cs:92-97—newDefaultHandler(output)stores that nil indefaultHandler.output. The value is CAPTURED, never re-read, solog’s later initialization (any test callinglog.SetOutput) cannot repair it.src/core/log/slog/handler.cs:120—return h.output(r.PC, buf.ValueSlot);→ NullReferenceException → rendered as Go’s “invalid memory address or nil pointer dereference”.
The consequence then splits by THREAD, which is the whole reason this looked like two unrelated findings:
- on the test’s own thread —
TestLogLoggerLevelForDefaultHandler(logger_test.cs:513viaDebug,logger.cs:332) — the panic is caught atTestExecution.cs:609-613and contained as one ordinaryfail. It is one of the five “logic divergences” listed in thelog/slogentry above, not an independent defect. - on a GOROUTINE —
TestSetDefault’sgo func(){ Info("A"); … }(logger_test.cs:469viaInfo,logger.cs:346, throughGoroutine.Run,golib/runtime/Goroutine.cs:375) — it escapes the goroutine root, reachesGoroutine.ObserveUnhandledPanic→TestHost.cs:413-425→TestRunner.cs:205-217, which flushes the evidence and lets the process die. Every ordinally later test loses its verdict. It IS a contiguous alphabetical tail, so CLAUDE.md’s mass-empty classification held all along — it was produced by process death rather than by a deadline, which is why no timeout signature appeared. Worth adding to that classification: a contiguous alphabetical tail has THREE causes, not two, and the third names itself in the results file’s last event.
A/B proof. One line at the top of slog’s init — builtin.initPackage(typeof(log_package)),
i.e. the forcing the converter already knows how to emit — moves TestSetDefault fail → pass
and TestLogLoggerLevelForDefaultHandler fail → pass, and the whole tail executes:
214 terminal verdicts, 190 pass / 24 fail, against 181 terminal with the run cut short —
+33, i.e. the ten unreported top-level tests and their subtests. (Both figures derive from the
strict 1:1 run/terminal event pairing the host emits: 362 events baseline, 428 with the probe.) The
24 remaining failures are the alloc-assert rows plus the Source/CallDepth/RecordSource
naming divergences already priced above — untouched by this, and unrelated to it. The probe was
reverted; nothing is banked from it.
Classification: converter defect (Go cross-package init ORDER not reproduced for named imports), surfacing as a converted-code nil-func panic. Not a test-host defect, not a golib defect, not a measurement artifact.
Priced remedy — NOT small, deliberately not started. The machinery is all built and proven
(hook emission, per-(assembly, package) dedup via packageBlankImportForces, the
noInitPseudoPackages fence, the hand-own .cs.auto fence, the marker splice); what changes is
only the TRIGGER. Two honest trigger sets:
- (a) every import. Matches Go exactly. Largest footprint: a hook per import per package, corpus-wide, and every referenced assembly loads eagerly at startup.
- (b) every import whose module ctor is non-empty transitively — i.e. the package or any
package it imports has an
init. Observationally EQUIVALENT to (a) (forcing an empty module ctor is a guaranteed no-op — the same reasoningnoInitPseudoPackagesalready applies tounsafe/builtin/C), at a fraction of the emission. This is the recommended shape, and it needs one new thing: the converter must KNOW whether an imported package initializes, which means recording it where the other cross-package facts already live (apackage_info.csassembly attribute, orstdlib-metadata.txt).
⚠ A read-set heuristic cannot substitute. The tempting narrow rule — “force only imports whose
symbols the importer’s init references” — MISSES this exact case: slog’s init reads
log/internal.DefaultOutput, but the package whose init WRITES it is log. The dependency that
must be forced is not the one the init statement names. Only the unconditional (or
transitive-has-init) rule catches it.
Cost to land: converter change + the recorded-init marker + go generate . for
stdlib-metadata.txt; a full-corpus regen and rebank (the emission changes nearly everywhere);
a roster re-sweep, because eager module-ctor execution can surface latent order dependence in any
banked row (that is the point of the fix, and it is also its risk); and a behavioral guard in the
class of the existing blank-import guard, exercising a NAMED import whose init side effect the
importer’s init reads. log/slog’s row is gated on it: with the fix the package reaches
190/214 in one run, without it the suite cannot finish at all.
Cheap optional follow-up, independent of the above: the comparison artifact
(go2cs_test_comparison.json) does not lift the host’s “test binary died” package event, so a
consumer reading only that file sees an unexplained empty tail. The signal exists in
go2cs_test_results.json and on stderr; surfacing it in the comparison would have made this a
five-minute read instead of a lane.
2026-08-25 · reflect.ArrayOf LANDS as a standalone hand-own — the commission’s one-liner held, and the guard found an EMISSION gap next door (lane claude/reflect-arrayof, coordinator i7)
Queue item 2 from the map-key-elem-cargo lane’s closing list, taken on its own merits. The
prediction that arc made — “reflect.ArrayOf(n, elem) is now nearly free … the hand-own is
synthType(typeof(array<>).MakeGenericType(elem), [n, …elemDims])” — is confirmed: the
implementation is 14 lines in reflect/value_impl.cs beside its sibling constructor PointerTo,
plus one manualConversionFuncs["reflect"]["ArrayOf"] registry entry and the reflect regen that
turns the auto body into its placeholder. No typelinks, no linker table, no new machinery in golib
or the descriptor layer — the dims cargo was already carrying everything the length needed.
What the auto form was doing, stated once more because it is the reusable shape: it died in
typesByString → typelinks() not because the caller asked for anything exotic but because it was
reconstructing Go’s linker-allocated arrayType record. Every accessor that record exists to feed —
Len/Elem/Size/Align/String — the managed bridge already answers from (System.Type, dims).
The class generalizes: a Go body that fails inside a LINKER-table lookup is usually not a hard
wall, it is a body reconstructing something the managed side never lost.
Identity, not resemblance. canonType keys on the managed type PLUS the dims rendering, so
ArrayOf(3, TypeOf(byte)) and TypeOf([3]byte{}) intern to the SAME reflect.Type. Every row of
the new ReflectArrayOf behavioral guard is that identity claim — scalar, [2]Celsius (defined
element), [2]*uint8, [2]pair (struct element, size from Go’s own field layout), [0]uint8,
nested [2][3]uint8, and a five-deep composition — plus the value side (New/Index/SetUint/
Zero/DeepEqual) and the negative-length panic. Failing-first proven: at master the guard
reports exit code mismatch: C# 2 vs Go 0 — C# stderr: "System.NotImplementedException: typelinks:
external (assembly or cgo) function is not implemented"; with the change, PASS on all four phases.
This does NOT flip encoding/gob’s row and no claim is made that it does —
TestIgnoreDepthLimit wraps its 101-deep array in a reflect.StructOf, still the feature arc
(System.Reflection.Emit, and an AOT question) the previous lane named. gob stays 105 of 106.
Recorded limitation, deliberately not worked around: an array descriptor has no slot to hand a
channel’s DIRECTION or a map KEY’s dims down — abi.Type.Elem descends those through a POINTER only
— so ArrayOf(n, chan<- T) describes [n]chan T. A DECLARED [n]chan<- T reads back exactly the
same way today, so this is the cargo model’s shape rather than the constructor’s (the r39d rule).
The find next door: an empty NESTED composite literal drops its inner dimension (NOT this lane’s, not fixed)
The guard’s nested rows failed on their first run, and the cause is not ArrayOf. The converter
emits the two forms differently:
| Go | emitted C# | TypeOf(x).Elem().Len() |
|---|---|---|
var x [2][3]uint8 |
new(2, () => new(3)) |
3 — correct |
x := [2][3]uint8{} |
new array<uint8>[]{}.array(2) |
0 — Go says 3 |
The empty literal’s two elements are default(array<uint8>), i.e. length ZERO, so the inner
dimension is gone before reflection is involved at all — ArrayDimsOfValue then measures the first
element honestly and reports [2, 0]. Confirmed by reading both emissions from a two-line probe
(go run says 3 3); the consequence was measured through the guard, where the constructed
[2][3]uint8 compared unequal to the literal-built one while every accessor on the constructed side
answered correctly. Reachable from any [N][M]T{} literal with no reflection in sight, and it
is an emission fix (converter + a corpus regen + CNR), so it is recorded here and left for whoever
takes it. The guard compares against the declared var form, which is what “the type a declaration
produces” honestly means on this bridge, and its source says so at the site so nobody switches it
back.
Gates
Converter go test -count=1 ./... ok, 207.1 s, exit 0 — TestStdLibMetadataInSync included, and
go generate . reproduced stdlib-metadata.txt byte-identical, so no metadata was owed and that
is asserted rather than inferred. GolibTests 299 / 299, 0 failed (112.9 s). Full
go2cs-stdlib.slnx Debug (windows target) 0 errors, 315 s. Filtered ReflectArrayOf all four
phases PASS. The reflect regen ran seeded per the ritual, with a control first: the same
seeded reconvert at master reproduced all 15 committed reflect files byte-identical, so the two
files that moved afterwards are the change and nothing else.
⚠ Provisioning note for anyone gating on the coordinator i7: the box’s default dotnet is SDK
9.0.317, which cannot target net10.0 at all (NETSDK1045); the 10.0.400 SDK is a side-by-side
install at C:\Users\ritchie\dotnet10. The default go is 1.23.1 out of C:\Program Files\Go
and a machine-level GOROOT names that same 1.23.1 tree, while the corpus and version.props are
1.23.12 (at %USERPROFILE%\sdk\go1.23.12) — the exact both-pins-needed shape
GoCorpusMigration.md §1 records. Both pins must be set in the SAME invocation as every gate.
RULING (owner, 2026-08-25) — the campaign’s terminal denominator is the IMPLEMENTABLE test set, with the excluded packages fully disclosed, each with its why
The 100% march’s target is ruled: 100% of the packages whose test suites a faithful managed conversion can honestly validate, with every exclusion carried as a first-class, visitor-visible ledger — package, verdict count, exclusion class, one-line mechanism, link to the board rooting. Nothing disappears silently: BOTH numbers are always reported (the naive testable count and the honest denominator), exactly as the H10 gate already requires absolute-and-percentage together.
The admission bar for exclusion is the disclosure bar’s sibling, and it is strict: a package is excluded only when validation is PROVABLY meaningless or impossible — never because it is hard, unimplemented, or expensive. Three classes are in evidence so far; each future exclusion is ruled individually, on measurement, like every disclosure:
- E1 — no eligible tests on the target platform. Go’s own build constraints yield an empty
eligible set on windows/amd64:
internal/runtime/syscall,internal/syscall/unix,net/internal/socktest,log/syslog,runtime/race(five, measured in the frontier derivation). There is nothing to validate; the comparison is vacuous by Go’s own definition. - E2 — broken oracle. Go’s own suite fails on the reference side (
os/user:TestGroupIdsfails ingo testitself), so no clean differential baseline exists. - E3 — the test’s subject IS the replaced representation. The suite measures the raw memory
model a safe managed runtime deliberately does not have, so any pass would be fabrication:
internal/unsafeheader(fabricating live slices/strings by raw header aliasing) is the clean case, rooted in JOB-019 tier-2. Candidates to be RULED when reached, not assumed:internal/concurrent(whitebox consumer of the exact implementation the hand-own replaced, ruled structurally unbankable 2026-08-19) and possiblyinternal/weak(same tension, 1 of 3).
Mechanics (lands as one docs change when JOB-019 tier-2 completes, so the first ledger is
complete rather than dribbled): docs/ValidatedTestPackages.md gains an “Excluded packages”
section in the ledger shape above; the progress header reports banked / honest-denominator
beside the naive count. No harness change — excluded packages simply are not rows, and the sweep
already reads only rows. The anti-laundering clause carries over verbatim: an exclusion whose
mechanism is later implemented (or whose oracle is fixed upstream) REJOINS the denominator the
day the evidence changes, exactly as chan-direction retired the other way.
RULING (owner, 2026-08-26) — the campaign’s horizon extends to THREE-PLATFORM PARITY: 100% test validation on Windows, Linux and Darwin before leaving Go 1.24
Ruled the night 1.23.12.1 published (175/208 = 84.1% honest on Windows). The goal: every platform reaches 100% of its own implementable denominator, with its own disclosure ledger — disclosures and ruled impossibilities withstanding, exactly as the Windows march defines them.
Sequencing is conditional on measured arc speed, not preference:
- Linux/Darwin parity arcs that move QUICKLY bank on 1.23.12, BEFORE the 1.24 corpus migration.
- Arcs that will take a while do not gate the hop: 1.24 proceeds after Windows-100%, and parity work continues on 1.24.
- Either way, parity is a 1.24-era exit criterion.
What this queues immediately:
- A Linux seam-ledger re-measure at current master before pricing the parity arc — the ledger (R1 Stat_t by-address, R2 exec wall, R4 rawSyscallNoError, R5 sockaddr, W1b mmap, W6 cpu caps) predates weeks of arcs, and the Windows socket wall’s silent collapse is the standing precedent for why prices are re-measured, never carried.
- The Darwin run layer (FINDING-darwin-run-layer; linux’s syscall keystone is the template) is the LONG POLE — it gates every Darwin row and should start early, in parallel with the Windows tail, regardless of which side of the hop Darwin parity lands on.
- The per-OS roster mechanics ruling (held since the four Linux-proven rows) becomes part of this goal’s design: per-OS honest denominators, per-OS disclosure ledgers, and the roster shape that reports all three without averaging — building on the existing linux-annotation convention.
2026-08-26 · JOB-024 FOLD + TWO RULINGS — the Linux bill lands (honest 146/175 = 83.4%, 0.7pt from Windows); R2 is the parity arc; the ж-box sequence goes B1-design-first
The complete Linux re-measure at 59af260e0 (R, all 175 rows, per-row logs, 4.3 h): 144
PASS + 2 DISC-validating = 146/175 honest (83.4%) vs Windows 84.1% on the same roster.
Baseline chain 92/159 → 122/161 → 129/161 → 146/175. Three seams COLLAPSED silently this
season — R5 sockaddr (json full 491; tls 0/3646 → 396 agree), R3 self-binary (the Linux
apphost is genuine ELF), W1b mmap — while R1/R4/W1 hold closed at floor scale. R2 (exec)
absorbs 21 of 29 FAILs with two shape regressions inside it (os/exec 16/72→1/88, gcimporter
281→15 — same attribution, bigger crater, diff these first) and ONE new real test
(tls.TestCertCache, unrooted). Standing: W2 ×2 converter fixes, W3 plugin crash, W6 caps, W7
(ruled, honest FAIL, backoff changes nothing — re-confirmed), R6 zoneinfo (1 test), and a NEW
host-limit family: runtime/debug TestPanicOnFault — the Linux CLR cannot recover hardware
faults (no SEH), a structurally per-OS disclosure candidate. R’’s shard-2 ed25519
disclosure-capture finding is RETRACTED by its own re-read; disclosure machinery is
per-OS-identical on the evidence (Windows A/B 8/8 ran anyway). Full ledger: R’’s JOB-024 entries,
2026-08-26.
RULING — the Linux parity arc is R2, and it is ONE design item. 21 rows hang on the
exec/process-launch surface; the arc’’s docket: the two shape regressions first, then the design
note over FINDING-linux-run-layer §5’’s remaining surface (the landed exec-wall implementation is
the foundation; checkClonePidfd and the go-build/testenv re-exec family are the new members),
with tls.TestCertCache riding as its one unrooted test. The seven small items land as: two
bare-annotation fixes (immediate), W2/W3 converter fixes (lane-sized), W6/R6 singletons, and the
per-OS roster mechanics as their own DESIGN (annotation grammar, platform-exclusive rows —
internal/syscall/windows/registry is the clean case — per-OS counts and floors; a design note
is commissioned).
RULING — the ж-box sequence: B1’’s design increment goes FIRST; B′-S1 is DEFERRED. Grounds
from S0b’’s measurement: the receiver half was already near-optimal (emission delta 5 boxes vs
the census’’s 560 — census local-counts are NOT emission counts), and the arc’’s measured
allocation mass (WriteString’’s 17 direct-ж, math/big’’s 59x, netip’’s gradient, nistec’’s four
curves) lives in mechanism-B/C territory, not the parameter half. S1 re-opens only if B1’’s
emission-priced numbers say the parameter half still matters. B1’’s design must resolve P-F5
(unsafe.Pointer-subclassing vs kind-as-type) and carry the P-F2 three-variant microbench
precondition; the Reinterpret source-retention shape (NetShareAdd) is a named input. G’’s two
S0b riders bind: the slog-class A/Bs carry byte-identical controls, and census-grade figures are
never quoted as emission-grade.
2026-08-26 — The fold: stop-the-line resolved as three mechanisms; two corruption classes named; the giants re-censused; host-identity minted; row 176
Context. JOB-023 (the 175-row confirmation sweep at the rebank tip) went red on three rows and stop-the-line held all banking. Resolution: THREE distinct mechanisms, one real. Master was never regressed — the union train’s one latent defect was invisible to every merge gate because gates run BANKED (pre-union) test sources; only re-derivation exposes that class. All figures below banked at master 57339895d.
1. The one real defect — self-shadowing forcing hooks (FIXED, merged). The init-order arc’s
typeof(<pkg>.<pkg>_package) forcing references are captured by a package-level type named for
the target’s leading segment emitted into the same class (Go’s own image_test.go declares
type image interface). Fix: collision-gated global:: root-qualification — footprint
provably zero AND complete (the corpus compiles ⟹ no production instance can exist); CNR 646/646
byte-identical; image re-derives 8/8 = banked. Guard: ImportSegmentTypeShadow (both
production- and test-scope shadowing).
2. gcimporter — COUNT-BINDING RULING (no defect). 583 = 303 (TestImportStdLib) + 266
(TestImportTypeparamTests) + 14 standalone, confirmed to the digit. A GOTOOLCHAIN-resolved
module-cache GOROOT ships no test/ tree → 318 = 303+1+14, the typeparam parent running with
zero subtests, silently. Ruling: banked counts bind to a full-SDK GOROOT; a sweep host
resolving a toolchain-download GOROOT is a SWEEP-ENV defect, never row variance.
3. srcimporter — sweep-host capability gap (no defect). Go’s own oracle needs a C toolchain
(go tool cgo walk); on a cgo-less host the GO side fails while the converted side passes.
Routes to a cgo-capable host. Sweep-host doctrine (new): a sweeper needs (a) full-SDK GOROOT
with test/, (b) GOTOOLCHAIN=local beside the explicit GOROOT (else silent redirect),
(c) per-box profile paths — never another box’s home path (silent fallback), (d) a C toolchain
for cgo-oracle rows.
Row 176 — log/slog banked (213 terminal / 194 matching / 19 disclosed) behind the caller-info
fix: -tests emits internal-test files as <pkg>_internal_test_package and the frame-name
derivation kept the suffix where Go strips it — the external half had worked by accident.
host-identity minted (seventh disclosure class): an assert satisfiable only by the hand-owned
test host claiming testing.tRunner/testing.go identity — F15b makes the host a structural
replacement and the position-map ruling forbids the fabrication; truth IS the divergence. Bar:
host frames only — a missing converted-code frame is a DEFECT (the same test carried both sides:
four look-alike rows fixed, one disclosed). Permanent by design, like codegen-liveness.
Two corruption classes, now distinct (both with the moving-ExecutionEngineException signature):
- Sockaddr FIELD OVERLAY —
sockaddrInet4/6ToRaw, the un-hand-owned ENCODE twins of the hand-owned decode halves:Familywrites at offset 8 over the low half of a live object reference (v4 safe by layout accident). THEnethost-killer (308-name tail). Fix in flight:claude/poll-sockaddr-encode. NOTE: syscall.Environ was EXONERATED by measurement (faithful, now guarded byEnvironBlockWalk) — the census’s Environ attribution was the moving-site signature sampling the next allocator. CENSUS AMENDMENT: CENSUS-giants-2026-08-26 §3/§7 “Environ first” sequencing dissolves; net/http has NO prerequisite. - ж→uintptr LIFETIME GAP — the pin lives on the box, the registry is deliberately weak, and
the JIT retires the box at address-extraction BEFORE the consuming native call. Convicted on
Linux by symbolized GC frames (mark phase reading a NULL MethodTable); platform-independent in
principle. Fix order ruled: R prototypes the Linux KEYSTONE TETHER (strong ref for syscall
duration, one file, no emission change); emission-level KeepAlive / strong-RegisterPinned route
to the ж-box design on the tether’s evidence. OPEN census item: enumerate Windows
(uintptr)Ꮡxzsyscall crossings reachable from banked rows.
The giants (CENSUS-giants-2026-08-26, merged): 3,105 verdicts, zero capability walls.
net RUNS (120 matching; ~85% reach behind the overlay fix). net/http = 1,352 verdicts
behind ONE capture-prologue defect (staffed). reflect = one constant.ToInt construct at
the surface (unstaffed). runtime = closure-dependent linkname lowering closing a 2-node csproj
cycle — design question, do not staff.
slices arc (branch green, next window): conversion-blocked → 114/122; the “one gap” wore four
faces (all fact-gated fixes) + a fifth variadic-method-value defect. NEW GOLIB ITEMS (5 verdicts,
one mini-arc): zero-size-element slice allocation ([]struct{} of MaxInt must not allocate);
subslice −1 sentinel colliding with a real negative index (Go’s s[-1:] panic never fires);
operator-bound min/max not propagating NaN. Plus 3 AllocsPerRun rows for the manifest at
bank time. mvreturn arc (in flight, banks x509): defect A = return-operand evaluation order
(return o, o.mutate() — gc evaluates the call first, C# tuple literals left-to-right; NOVEL);
defect B = multi-value return ~-unwrapping a box into an interface result.
Findings collection: (a) FOURTH all-empty cause — a hard test-host build failure (the tell is
error CS… in converter stderr; by shape indistinguishable from the file lock). (b) CNR is
silent during transpile BY DESIGN — liveness = process churn, never log growth. (c) CNR’s git
status is scoped to the behavioral tree; concurrent -tests corpus writes cannot pollute it.
(d) PS 5.1 hashtables are case-INsensitive by default — a name-collision census reported
Image/image phantoms; censuses need [StringComparer]::Ordinal (the false-POSITIVE
sibling of the grep -P false-empty). (e) Mailbox appends via Get-Content/Out-File flip every
line’s EOL — the ReadAllText/WriteAllText UTF8-no-BOM pattern is mandatory.
Rebank state: JOB-023 closed 172/175 with exactly the three explained reds; the leveling rebank re-derives at 57339895d. Stale-source members enumerated: corpus-wide init hooks + flag, log, runtime/debug, sync, syscall, log/slog/internal/benchmarks.
2026-08-26 (evening) — Fold #2: four windows in one day; the giants’ walls fall; the corruption hunt reaches a single hypothesis; the arc queue for the reset
Day’s arithmetic at window four (0a03b7ac7): 175 → 177 rows (log/slog 194+19 with the host-identity class minted; crypto/x509 341/341, zero disclosed), 18,979 → 19,514+ matching, 85.1% honest of 208. Four union-gated windows, zero rollbacks, every merge preflighted from its base and duplication-audited.
The giants, end-of-day state:
net: 120 → 291 matching, crash class EXTINCT (the sockaddr chain: three struct-passing defects each behind the last; the WSAEINVAL-behind-a-lazy-extension-pointer diagnosis lesson). ReadMsg family flips with the WSARecvMsg completion-carried decode (merge pending its final). Remaining, all named: the DETERMINISTIC sendfile-family stop (time-independent — 60m released nothing), netpoll-deadline family (43), writev (9), DNS/environmental (17), two arrayitems. net/http: 46 declaration diagnostics → 0 → 81 body diagnostics → 24, across three commissions on one lane. Remaining: C1 (ImplementGenerator single-hop embed selection cannot serve MIXED embed kinds — per-member resolution is a shape DESIGN in the most collision-sensitive area), C2 (forwarder-side distinct-instantiations wall — silent-behavior risk, wants a guard-driven pass), D (http.Header map-reference modeling — DESIGN), E/F smalls.reflect: conversion wall FALLS (constant.ToInt, class bounded by 24-shape measurement). Residual is NOT B2-gated: ONE mechanism (function-local type-lift does not handle foreign underlying types — the 73-diagnostic wall + 14 of 15 semantic sites) + one capability ceiling (19-param func exceeds Func<> arity — needs a generated delegate). all_test.cs (10,639 lines): zero diagnostics. Go denominator measured 388, not the census’s 396 (−8 flagged, unexplained).runtime: unchanged — design question, unstaffed by ruling.
os/exec corruption hunt (R, 12+ reproductions): every native corridor cleared by three landed soundness fixes (wait buffers; keystone tether; array-provenance registration — case 1 of PinnableStorage’s taxonomy had never joined the provenance record). Fingerprint: a byte-block smash into managed heap (verifyheap: 4 contiguous errors, unaligned member as SIDE EFFECT). Bisect session 1 cleared hashtriemap (VICTIM, not writer), ReinterpretRef (size guard forbids widening), and golib has NO unaligned-write primitive — the single surviving hypothesis is a BLOCK COPY into managed heap with a native-slice-derived WRONG COUNT (copy/ToSpan paths), one count-vs-extent assert from conviction. B2 clause: conviction in kind-split files routes the fix to that arc with an interim guard.
Rulings this fold: (1) count-binding — banked counts bind to a full-SDK GOROOT; a toolchain-download GOROOT on a sweep host is a sweep-env defect, never row variance (proven: 583 = 303+266+14; the 265 is GOROOT/test/typeparam). (2) The initᴛᴛtests hook-banking amendment (now in CLAUDE.md, dated). (3) The slice-shaped-spread arc (Span int32 length ceiling under append spreads; 679 sites/225 files priced) is APPROVED, sequenced POST-B2, before the full leveling. (4) Full-roster test-source leveling: POST-B2, one deliberate pass (r40 precedent); the floor (8 noisy rows) is merged. (5) slices banks at 119+3 when the spread arc lands (alloc rows pre-classified: Concat/Grow = alloc-count-semantics, Insert = alloc-profile).
Findings collection (each paid for today): git add -A after a sweep stages the hook shape
silently (staging corollary, now in CLAUDE.md); tr -d '\r\ufeff' has no \u escape — it
strips u/f/e from PATHS and fakes a death (false-red sibling of route #6; use sed or no-BOM
writes); go test’s 10m default panic-timeout fires under CNR-concurrent load with zero FAIL
lines — pass explicit -timeout on loaded runs; ALL-EMPTY has a documented SECOND cause (test
assembly never built — check converter/build stderr for CS lines before the file-lock
diagnosis); the production recover path UNWINDS PAST THE EMIT — a panicking file is silently
missing at exit 0 and surfaces as consumer CS0103/CS0246 pointing away from the cause;
GCStress is impractical at corpus scale (starves startup JIT, stresses the build) — HeapVerify
is the corpus-scale instrument; CoreCLR’s crash-handler fork/wait choreography defeats naive
live-attach and masquerades as a corpus wait.
Sweep-host doctrine (from fold #1, now proven end to end on the sweeper): full-SDK GOROOT
with test/, explicit GOTOOLCHAIN=local, per-box profile paths, C toolchain for cgo-oracle
rows (the fleet currently has NO cgo-capable host — G’s box untested, owner-batched otherwise).
Arc queue at the reset (post-budget, ranked): local-type-lift (reflect’s first-order residual); C1 mixed-embed per-member design + C2 forwarder marshal; slice-shaped-spread (post-B2); http.Header modeling design; the sendfile-family deterministic stop; netpoll-deadline family; Func-arity generated delegate; writev capability decision; unique weak-ref; runtime linkname design; production-recover loud-fail honesty fix; full leveling (post-B2).
2026-08-29 · Fold #3: net’s empty class goes extinct; the fixture saga ends on the BUNDLER; a silent SUBTRACTION reds the linux corpus at master; pprof reclassifies to a frontier; the defer/go capture defect is narrowed to synthesized lambdas
Source: the fleet mailbox, 2026-08-28 and 2026-08-29 entries (R/R-LAPTOP, G/laptop, i9/sweeper, the local lanes, and COORD rulings). Folded here per the doc-authority ladder — the mailbox is transport, this is the record. Where an item exists only as a coordinator relay of a LOCAL lane’s report, it is attributed as such.
Window ledger for the two days: master 1d01200a9 (ten) → 9f2973b96 (eleven — the Uname
root and the five proactive struct-passing mirrors) → 45e0a97a8 (thirteen — smalls-sweep,
local-type-lift, net-residuals, net/http’s first run) → 5ff05d8b6 (fourteen — the Uname restore
- the newUnixFile linkname registry row; the linux corpus green AT MASTER for the first time
since the subtraction, 0 errors, 8m26s). Window twelve (the composed host pair + the fixture
pair + the bogo record) gated at
f477da149ong-seam-verify, sequenced BEHIND fourteen so G’s two-sided guard would land on a base where Uname’s body exists.
1. The TransmitFile netpoll seam — net 291 → 440, the EMPTY class extinct; and the two-phase ordering rule
The net-residuals lane (local; reported through COORD’s 2026-08-29 entry) landed the TransmitFile seam hand-own, and the arithmetic closed exactly: net 291 → 440 matching, with the empty class now EXTINCT (the crash class had gone extinct at 291 in fold #2). The 155 unreported verdicts were ONE deadlock seen from two phases — the serial tail plus the parked parallel batch — not 155 divergences.
The bridging arithmetic (COORD reconciliation — derived from the lane’s recorded figures; each equation closes exactly). Three of this story’s figures sit at different scales — 155 unreported, 43 deadline-named, 149 gained — so the bridge is stated as a ledger rather than left to inference. net’s suite is 475. Pre-fix the run held 291 matching + 155 unreported + 29 mismatched, the 155 being the serial tail past the deadlock point plus the parked parallel batch — and the 43-name deadline family is the deadline-named SUBSET of that parked batch, not a third population. The TransmitFile fix un-parked all 155, of which 149 became matching (291 → 440) and 6 newly mismatched (29 + 6 = 35, the residual inventory the lane named). Provenance, so the ledger is auditable rather than believed: 291, 440, 155, 35 and 43 are recorded figures; 475 is forced by two of them (440 + 35), and 29 (475 − 291 − 155), 149 (440 − 291) and 6 (35 − 29) are each forced by two more. Post-fix the ledger closes at 440 + 35 = 475 with zero unreported — the empty class’s extinction, stated as arithmetic.
The ordering-rule clause this mints, fold-bound by COORD and now in CLAUDE.md’s
-test-timeout taxonomy: the converted host reports in two phases — serial tests first,
then the parallel batch. A single serial-phase death therefore leaves a contiguous tail in the
serial phase AND parks the whole parallel batch unreported; the union reads as SCATTERED
because the parallel names interleave alphabetically with the serial ones. So the existing tell
(“contiguous alphabetical tail = died partway; scattered = genuine divergence”) has exactly one
exception: a scattered empty set that EXACTLY EQUALS the package’s t.Parallel() set is one
serial-phase death, not divergence. Compare the empty set against the parallel set — one grep —
before believing “scattered”. net’s “deadline family” (43 names in fold #2’s own residual
inventory) was precisely this shape.
Fold-bound alongside it, flagged and deliberately NOT chased: a load-induced
NativeOverlapped lifetime death, nondeterministic, named by the net lane. Recorded so the
next sighting is a second sighting rather than a discovery.
Three rulings from the same report:
writevis an ARC, not a capability (accepted as briefed): an unexported same-package interface reached only through a dynamic assertion gets noGoImplementrecord — the exported-only gate’s premise fails there — plus the layer-2 adapter-resolution question. A converter EMISSION arc, and the class reaches beyondnet.- The “17 DNS/environmental” classification is CORRECTED on the record: they are a real resolver divergence — the converted resolver answers no-record for MX/NS/TXT/SRV/PTR — an arc, not weather. The residual-35 inventory stands as the lane named it.
- The defer-loop-capture class is a CORRECTNESS ARC, top of the converter queue (see §5).
2. The publish-fixture saga: the deleter is the single-file BUNDLER
The longest thread of the two days, and the one where both lanes’ proposed fixes were the same non-fix. Reconstructed in order because the ORDER is the lesson.
- R’s report (2026-08-29): the
timerow’sTestLoadLocationFromTZDataSlim— banked pass/pass indocs/validation/current/time.md, andtimecarries no disclosures manifest so it compares strictly — was FAILING, taking the bankedlinux: 167to 166. Root as read: the fixtures exist in GOROOT, in the converted package, and in the RID build output, butbin/tests/publish/held an EMPTYtestdata/, and the pipeline runs the host frompublish/.CopyFixturesthen deliberately skips what is absent (if (!File.Exists(source)) continue;) — correct for a relocated lone-exe copy, and here it converted “the publish step dropped every fixture” into a quiet per-test ENOENT. Proposed fix: emitCopyToPublishDirectory="PreserveNewest"in the-testscsproj template. - G measured Windows and found publish CORRECT —
crypto/tls122/122 fixtures in publish,time4/4 from a fresh convert-then-publish — and reported “blast radius on this platform is ZERO rows”, hypothesising a LinuxAppContext.BaseDirectoryself-extraction difference. - R withdrew his own report: a FRESH Linux convert+publish carries all 4. He had read an
existing publish tree and reported it as what publish produces. (His stated rule: an
artifact’s PRESENCE says nothing about what the current inputs produce — wipe and regenerate
before reading a build tree as evidence. He had caught the identical shape an hour earlier on
stale
PartialStubGeneratorstubs and still did not apply it one directory over.) G then dropped his re-addedCopyToPublishDirectoryhalf as unmeasured emission churn. - R isolated the real mechanism with three plain
dotnet publishcommands, no pipeline, no test host: publish #1 → 4 files, publish #2 → 0, publish #3 → 0, exit 0 every time, monotonic not alternating. The act of measuring removed the fixtures —compareGoAndConverted TestscallspublishTestHostitself, so his “count taken just before the compare” was taken before the publish that destroyed it. R then ran the Windows arm: 4 → 0 → 0, identical. The mechanism is the SDK’s, not a platform’s; no contradiction between the two lanes’ results had ever existed. - The exposure rule, from source:
publishTestHostruns `dotnet publish -o/bin/tests/publish` — a **deterministic path that is never cleaned** (`testConversion.go`). So `-test-action build` publishes and `-test-action compare` publishes **again** (a build-then-compare sequence IS the 4→0 repro by construction); `-test-action all` (the sweep's invocation) publishes exactly once per row per invocation; `bin/` is gitignored, so a row never published **on that box** gets publish #1 and its fixtures. **A fixture-declaring row is therefore correct on its FIRST run on a given box and silently fixture-less on every subsequent one.** No banked verdict is retroactively wrong — the rows banked on the runs that HAD their fixtures. What is wrong is every RE-measurement of such a row since the single-file arc, **including any future banked-row protection sweep**, which is exactly the gate that would have caught a real regression. -
G’s three-arm isolation named the deleter and killed both proposals:
arm publish #1 #2 #3 as emitted today (single-file) 4 0 0 CopyToPublishDirectory="PreserveNewest"added4 0 0 -p:PublishSingleFile=false4 4 4 ExcludeFromSingleFile="true"(single-file kept)4 4 4 The deleter is the single-file BUNDLER, which on a republish reclaims the output directory for the files it owns and takes the loose content with it. The item was reaching publish the whole time — the bundler removed it afterwards, which is why
CopyToPublishDirectorychanges nothing.ExcludeFromSingleFileis the right SHAPE rather than a workaround: a fixture MUST be a loose file because the test opens it by relative path (os.Open("testdata/x")), and the attribute states exactly that property, so the bundler stops claiming a file it should never have claimed. Landed in the converter (testConversion.go, BOTH emission sites — the plain fixture and the sharedup<N>link form) with the three-arm table in the comment, verified end to end (rebuild → reconverttime→ attribute present → three publishes → 4/4/4). It also satisfies R’s design constraint on its own terms: nothing is staged BEFORE publish (a pre-publish copy step would simply be undone) — the file is removed from the bundler’s claim. - The loud-skip companion, and why it is not merely a guard rail. The silent skip is G’s own
from the single-file arc and had a real reason (a relocated lone-exe copy legitimately has no
fixtures beside it and must still start). The discriminator landed with the fix: fail the run
when a suite declares fixtures and none resolve WHILE the host is not a lone relocated copy (a
published host’s directory holds many files; a relocated copy holds one) — which keeps
os/exec’s re-exec path working. Under the bundler mechanism it fires on exactly the runs that would otherwise report a silent false failure. Both lanes converged on it: the diagnosis was partly wrong for an hour precisely because the harness said nothing either way.
The publish-#1 doctrine, ratified by COORD and fold-bound: a blast-radius-zero claim must
state what observation would have made it non-zero. Every count in G’s Windows negative was a
publish #1, which is structurally blind to a republish-only bug — so the negative was
unsupported even though every measurement inside it was sound. (G’s own correction on the record:
“sound as measurements, wrong as a conclusion”.) The companion methodological line from the same
thread: naming a mechanism justifies re-landing A fix, not THAT fix — G nearly re-landed
CopyToPublishDirectory as “measurement-justified” on the strength of the mechanism being named
rather than on its own measurement; the three-arm table is what stopped it.
3. The Uname silent SUBTRACTION — the linux corpus RED at master with a clean git status
The defect. The converter registration that DISPLACES the generated syscall.Uname wrapper
merged; the hand-own *_impl.cs BODY it displaces to did not. Master’s
zsyscall_linux_amd64.cs:979 held a placeholder comment pointing at a body that does not exist,
and both _impl.cs files DISCUSS Uname — G’s header even promises “Uname is below” — while
nothing in syscall/linux defined it. Clean merge, no conflict, no warning, clean working tree;
the whole linux stdlib failed -p:GoTargetOS=linux on
kernel_version_linux.cs(21,27): error CS0117: 'syscall_package' does not contain a definition for
'Uname'. Found by the local linkname-seam lane against master’s own blobs, days after the seam.
The fix (R, dcac8df44 on claude/laneR-uname): 48abfbc54’s body transplanted verbatim
onto current master — 76 insertions, zero deletions, uniformly CRLF. R deliberately did NOT
re-apply his old header prose, since G’s 8ac224105 rewrite supersedes it and the narrative needs
no second telling.
Measured A/B on internal/syscall/unix (the consumer holding the call site), at
-p:GoTargetOS=linux --no-incremental:
Uname definitions |
verdict | |
|---|---|---|
| RED (master) | 0 | Build FAILED, exit 1 — the CS0117 by name |
GREEN (dcac8df44) |
1 | Build succeeded, exit 0 |
Full linux corpus gate (go2cs-stdlib.slnx -c Debug -p:GoTargetOS=linux --no-incremental
-p:UseSharedCompilation=false): 0 errors, 153 warnings, 7m40s — 307 projects. Uname was the
whole of it; closing the leaf-most CS0117 un-skipped no tail of latent dependents. R stated the
discriminators rather than trusting “Build succeeded” on an 84-byte -clp:ErrorsOnly log: the
7m40s elapsed (squarely on CLAUDE.md’s linux-corpus figure, impossible for a no-op) and
freshly written assemblies across the tree inside the build window (syscall 31,
internal/syscall/unix 32, os 48, fmt 54, runtime 20, net 61; timestamps 09:32–09:34).
One process disclosure R volunteered because it would have FLATTERED the fix: his first reproduction attempt is discarded, not reported — he launched it and then edited the tree while that build was still reading it. It returned “Build succeeded, 0 errors”, the answer that would have looked like evidence. A build whose inputs changed under it measures nothing.
The preflight adopted (now in CLAUDE.md’s concurrent-lanes section). This is the mirror of
the silent DUPLICATION the integration rules already catch: a silent SUBTRACTION, where one
lane removes a definition because another lane’s branch supplies it and the merge drops the
supplier. Both diffs are pure additions/removals, git merges them without complaint, and the
result compiles nowhere. Mechanical remedy: if git diff --name-only <base>..<branch> shows a
manualConversionFuncs registration or a generated-body deletion, assert the matching *_impl.cs
body is present in the MERGE RESULT — the same shape as the existing package_info.cs ⟹
stdlib-metadata.txt preflight.
G’s two-sided guard, and the one-sided-property lesson (also now in CLAUDE.md). G named his
own check’s blind spot after the field beat it. The ten-names/zero-bodies property he offered
the fleet as the merge-seam instrument — every linux-scoped struct-passing name has ZERO generated
bodies and EXACTLY ONE placeholder, run twice over all ten names — verifies the generated wrapper
was DISPLACED and never that the displacement has a DESTINATION. A placeholder pointing at a
non-existent hand-own passes it cleanly, which is exactly what master held; verified rather than
assumed, g-seam-verify carried the same gap (registration present, placeholder present, body
absent) and the window-twelve unit would have shipped it had R’s fix not landed first. The guard
G built in response is a converter-suite test (the go test ./... tier, beside
projitemsIntegrity_test): for every manualConversionFuncs entry the corpus must contain a
DEFINITION for that name under the scoped platform — registration ⇒ displaced wrapper ⇒
body, all three, or the test names the missing leg; plus the reverse direction (a dead hand-own
nothing displaces) where the corpus shape allows it cheaply. That turns the class into a red
converter suite at the merge instead of a red linux corpus days later. COORD’s fold-bound
statement: a displacement property must assert the destination exists — every seam check carries
both sides of the ledger, or it passes the exact failure it was written for in mirror form.
Merge order was adjusted for it: thirteen → claude/laneR-uname → G’s twelve re-signal, so the
guard would land on a base where the body exists.
4. runtime/pprof reclassified: a CAPABILITY FRONTIER, not a row waiting on one stub (R)
R measured the wall ladder to the bottom with three runs on the same published host, each excluding what the previous proved fatal. Four rungs, not one, and the hopeful “183 rows behind the stub” figure does not survive measurement — COORD retracted it on the record (the “183 rows enumerate” phrasing in window thirteen’s merge message stands as a BUILD statement only).
nanotime1— CLEARED.claude/laneR-nanotime@b15d01641:MonotonicClock.Nanoseconds()in golib (Stopwatch-derived, seconds/remainder split so the ns multiply cannot overflow) behind per-GOOSruntime/{windows,linux}/nanotime_impl.cs— per-GOOS because darwin already carries a real body. Failing-first proven as a PROPERTY: two throwingnanotime1stubs before, 0 after a clean rebuild, with the build succeeding (a stub plus a body would be CS0111). GolibTests 389/389 (386→389, three clock guards each pinned to a distinct failure mode); windows runtime--no-incrementalexit 0; and the linux leg verified DISCRIMINATINGLY — nonanotime1stub generated against the bodyless partial atlinux/stubs3.cs:10, because a green build alone would not prove the file was in the compile set. (The branch is gated on both targets but was never queued into a window — window FIFTEEN material.)getg/ the CPU-profile family — NON-fatal (per-testinfrastructure-error, the run continues) and NOT a bounded single. R scouted it because the queue would otherwise have paid for the framing, and found three frames, not one:setcpuprofilerate(windows/proc.cs:5556) usesgetg()forgp.m.locks++/--ONLY — a preemption-disable pair that is a no-op in the managed model, and this frame alone WOULD be a narrow hand-own;setThreadCPUProfiler(windows/os_windows.cs:1238) callsgetg()again and stores intom.profilehz, a realmfield;setProcessCPUProfiler(:1225) callsnewm(profileLoop, nil, -1)— it spawns an M running Go’s profile loop, and sample delivery then walks goroutine stacks from another thread, i.e. the same cross-thread stack-walk boundary the SIGQUIT arc measured. There are 560getg()call sites corpus-wide readingstack.lo/hi,m,p,goid,preempt,gsignal;stubs_impl.csleftgetgthrowing on purpose, with the rationale written down (any path reaching it “surfaces as a loud, locatable failure rather than quietly operating on a fabricated goroutine descriptor”). A managedgetgcan return an identity; it cannot make those fields true — implementing it converts one loud failure into 559 quiet ones, the exact trade the repo’s doctrine refuses. Recommendation: classify the CPU-profile familyruntime-capability. The cheap honest lever, if the row is ever worth carrying, is the narrow one — hand-ownsetcpuprofilerateto drop the managed no-opm.lockspair (thesystemstack“that branch is the only branch” argument) — clearing frame 1 without minting a fabricatedg. NOT cut.awaitBlockedGoroutine— the host-killer, and it is a SHARED TEST HELPER, not a test. ExcludingTestBlockMutexProfileInlineExpansionmoved the identical death toTestBlockProfile— same helper, same panic, same masking. It spins until a stack dump shows a goroutine in a NAMED wait state ([chan receive],[sync.Mutex.Lock],[select],[sync.Cond.Wait]); on timeout it panics ON A GOROUTINE, which kills the binary — and that is faithful: Go does the same. 13 call sites. This is what produced the first run’s contiguous alphabetical tail (2 reported / 181 empty — read correctly by the existing CLAUDE.md tell as a run that died partway, not 181 divergences). R corrected himself twice here: he first blamed a single test (wrong — it is the helper) and called it a hang (wrong — the deadline fired correctly both times; it panics). So the row needs goroutine wait-state names in stack dumps before the block/mutex family can even report.- SIGSEGV. With the whole block/mutex family excluded the host reached 15 verdicts and died
with exit 139 inside
TestGenericsHashKeyInPprofBuilder(protomem_test.go:121) — not a throw, not a deadline, a hard access violation.
The measured tally behind the stub: 37 tests requested, 15 reached, then the crash — 1 pass
(TestContextLabels), 10 fail, 3 infrastructure-error, 1 skip. TestFakeMapping’s
infrastructure-error comes up through WriteTo → writeHeap → writeHeapInternal, so the
heap-profile writer is stubbed too.
Disposition. The board takes runtime/pprof off the near-bankable shelf: it is a
capability frontier (goroutine wait-state reporting, an M-spawning profile loop, a cross-thread
stack walk, a stubbed heap-profile writer) — sequenced LAST in the Windows chase queue, possibly
the release’s honest disclosure story. The Windows chase to 100% now reads: net residual-35 (five
named arcs), net/http (AV root, then buckets), internal/trace (staging fix rides window twelve),
reflect (4 diagnostics — the Δ-marker arc plus the arity single), pprof (frontier, last).
Rung-4 disposition, and it is a clean acquittal. R flagged the caveat against his own finding:
all four rungs ran on a throwaway composition — his clock plus origin/claude/smalls-sweep,
whose fixes live in constraintOperations.go, i.e. GENERICS CONSTRAINT EMISSION — and rung 4’s
segfault is in a generics test. Plausible causal link, so not attributable to master.
Routed to i9 (serial, bounded, Windows) once smalls-sweep was genuinely on master. i9’s
methodology note is worth keeping: claude/laneR-nanotime is NOT an ancestor of 45e0a97a8,
so testing bare master would have answered nothing (every pprof test throws at rung 1 first) — so
i9 recreated R’s effective composition on top of real master (window-thirteen worktree +
claude/laneR-nanotime merged locally, never pushed). Result: NO SEGFAULT on the real merged
tree. TestGenericsHashKeyInPprofBuilder in isolation gives a clean managed
INFRASTRUCTURE-ERROR — System.NotImplementedException: pprof_memProfileInternal: external
(assembly or cgo) function is not implemented, at
pprof_package.pprof_memProfileInternal.3.stub.g.cs:27 via writeHeapInternal → writeHeap →
WriteHeapProfile; process exit 1, a normal exception caught and reported by the framework — no
access violation, no unhandled native fault, nothing resembling exit 139. i9 stated the limit
rather than the flattering read: this cannot distinguish “purely a composition artifact” from
“platform-specific to begin with” (R’s report did not name the box). The Linux-at-real-master leg
stays deliberately UNMEASURED and parked — it gates nothing until the pprof frontier work begins,
and nobody spends a hard window on it. The stub it now surfaces
(pprof_memProfileInternal feeding WriteHeapProfile) is not a new finding — it is rung 3’s
heap-profile-writer gap, confirmed still present.
5. The defer/go capture defect, as R narrowed it: the transform misses SYNTHESIZED lambdas
Commissioned as the correctness arc off the net root-cause (witness: TestConcurrentSetDeadline).
R rooted it against real emission before touching anything — branch claude/laneR-defer-loop,
base 45e0a97a8, probe src/tests/Behavioral/DeferLoopCapture, every shape measured Go-vs-C#
rather than reasoned about — and the arc is both NARROWER and WIDER than briefed:
| shape | Go | C# | verdict |
|---|---|---|---|
| receiver reassigned after defer, NO loop | close first |
close first |
ALREADY CORRECT |
defer c[i].Close() in a 3-clause loop (the witness) |
conn2/1/0 | PANIC: index out of range [3] with length 3 | BROKEN |
| closure capture, 3-clause loop | 2,1,0 | 2,1,0 | ALREADY CORRECT |
| closure capture, range loop | z,y,x | z,y,x | ALREADY CORRECT |
| deferred plain ARGUMENTS | 2,1,0 | 2,1,0 | ALREADY CORRECT |
go c[i].Close() in a 3-clause loop |
g2/g1/g0 | same defect (emission confirmed) | BROKEN |
Both halves of the brief were wrong as stated, and COORD owned the overstatement. (a) For a
source-level func literal the converter ALREADY implements Go ≥1.22 per-iteration semantics —
it renames the loop variable and emits the per-iteration copy,
for (nint iᴛ1 = 0; …) { var i = iᴛ1; … }; that path is correct today and needs no work. (b) The
deferred receiver is ALREADY hoisted for simple receivers by the ʗ capture-snapshot
machinery — var xʗ1 = x; defer(() => xʗ1.Close(), …) — proven by the no-loop probe behaving
exactly like Go.
The actual defect, stated precisely. The per-iteration transform is triggered by the
variable-analysis pass finding a func literal in the Go SOURCE. defer c[i].Close() and
go c[i].Close() contain no func literal — visitDeferStmt.go:188 / visitGoStmt.go:202
SYNTHESIZE one (callExpr = "() => " + callExpr) long AFTER that analysis has run. So the
synthesized lambda captures variables the capture analysis never knew about, and consequently:
the loop variable gets no per-iteration copy (for (nint i …), not iᴛ1), and the ʗ hoist
is SHALLOW — it snapshots the receiver’s ROOT (var cʗ1 = c;) but leaves the SUBSCRIPT to
be read at run time. Emission proof, verbatim, at both sites:
for (nint i = 0; i < 3; i++) {
c[i] = new connжcloser(…);
var cʗ1 = c;
defer(() => cʗ1[i].Close(), ref ᒐ); // and: goǃ(() => cʗ1[i].Close());
}
with i == 3 when it fires. Why the witness reaches the lambda arm at all: net.Conn.Close()
RETURNS error, so hasResults == true forces the "() => " + callExpr fallback instead of the
method-group form (which binds its receiver eagerly and is correct). Same for namedFuncType,
variadicCallee, variadicLit. The file’s own variadic comment reasons about exactly this hazard
— “there are no arguments whose defer-time evaluation the wrap could disturb” — but the
reasoning was applied to ARGUMENTS only; the RECEIVER was never in scope.
Consequences for the arc: (1) go statements are in scope and were not named in the brief
— same mechanism, same line, different file; (2) the guard’s briefed “three shapes each proven red
with its own fix removed” re-bases — three of the briefed shapes are green at master, so they are
green CONTROLS that must stay green, and the genuinely red shapes are the two synthesized-lambda
ones; (3) corpus reach is narrower than “every 3-clause loop” — it is deferred/go calls whose
callee forces the lambda arm AND whose receiver expression is not a plain identifier.
Fix in flight, direction ratified: hoist the RESOLVED receiver expression (not its root)
into a defer-statement-time temp in both synthesized-lambda arms. It also yields the Go-correct
per-iteration value without touching the source-literal transform, and preserves Go’s
statement-time evaluation of side-effecting subscripts (c[f()] runs f once, at the right time)
for free. Deliverables still owed: the behavioral guard with its controls, CNR, and — because the
reach is corpus-wide — a seeded reconvert-and-BUILD census of how many emission sites move.
6. Named, priced, and deliberately NOT chased
- The CS1113 value-receiver method-group defect (R, incidental to §5, same file, adjacent
decision):
defer xs[i].show()whereshowhas a VALUE receiver emits a method group that does not compile —CS1113: Extension method … defined on value type … cannot be used to create delegates. Ordinary Go; no stdlib package writes it, which is why the corpus is green. Logged as a named arc candidate, priced accordingly, separate from the capture arc. - The load-induced
NativeOverlappedlifetime death (net lane): nondeterministic, flagged, not chased. See §1. -test-action comparealone does not stage testdata (the net lane’s four hosts-file tests prove it) — a measured member of G’s staging fix’s scope, folded into the same fix.- The DNS record-type divergence — reclassified out of “environmental”: the converted resolver answers no-record for MX/NS/TXT/SRV/PTR, 17 verdicts, a real arc. See §1.
writev— ruled a converter EMISSION arc, not a capability. See §1.- The Δ-collision-marker accessibility arc (commissioned to G, post-window-twelve) — the
reflect final’s declined rider, now its own campaign.
generatedTypeScope(converter) andCommon.GetScope(go2cs-gen) both read the leadingΔas an export capital → public, while nested siblings are internal; the fix strips the marker in BOTH, together (go2cs-gen already hasGetSimpleName(…, dropCollisionPrefix: true)). Leveling-scale — it moves effective accessibility of every Δ-renamed type and method across 305 packages — so it owes its own corpus proof: seeded reconvert, corpus build on windows AND linux (L3), goldens re-baselined if behavioral emission moves, and the reflect-canary five at the lane gate. Payoff: 3 of reflect’s remaining 4 diagnostics (the wall having already fallen 73 → 4 on the local-type-lift arc in window thirteen). - The
Func 17–24arity ladder (local lane) — reflect’s 4th and last diagnostic, and worth recording that R REFUSED it as first written and was right to:golib/variadic.cstops out at 8 fixed params, so “17+” did not match the file the item named. Re-specified: it is NOT theꓸꓸꓸvariadic family — the BCL’sSystem.Func<>tops out at 17 generic parameters (16 args- result) and reflect’s
abi_test.goneeds 21. The work: declare plaingo.Func/go.Actiondelegates in golib (a new file besidevariadic.csis fine) for parameter counts 17 through 24, BCL-style variance (inparams,outresult), namespacego, and only 17+ so arities ≤16 keep bindingSystem.Funcunambiguously. Zero converter change — the emitted name is unqualified and C# lookup is arity-aware. Guard: one GolibTests case constructing and invoking a 20-paramgo.Func. Golib API change ⟹ one fullgo2cs.slnxbuild before banking.
- result) and reflect’s
7. Instrument-discipline collection (each paid for in these two days)
The two days produced one family repeatedly, and both lanes named it the same way: the absence of a failure signal is not the presence of correctness. Members, in the order they were paid for:
- A verdict rule that tests for the ABSENCE of crash strings reports a FALSE GREEN when nothing
ran. R’s Uname A/B printed “0 crash / 4 clean of 4” while
dotnet publishhad failed — FALSE- GREEN route #6’s exact shape. Two tells were in hand and rationalized away: the published host’s mtime was unchanged across the “rebuild”, and his own displacement check printedgenerated wrapper displaced? 1— the literal answer that it had NOT been. Remedy: the round verdict now requires positive evidence (≥20"action":"pass"records) and reports NOT RUN otherwise, naming the log line to read. - The same instrument then produced the mirror FALSE RED, matching
TestWaitInterrupt/SIGQUIT’s own expectedsignal: quit (core dumped)payload as a host crash. Both errors share one root: pattern-matching TEXT instead of asserting a PROPERTY of the run. The ground truth that settled it was three indicators sharing no machinery — pass records flat at 72 across 7 rounds vs ragged 45–59, every death marker (^panic:,Process terminated,test binary died) present before and absent after, and the pipeline’s own verdict moving from killed by SIGSEGV to exit status 1. - A build claim read off a
Select-Stringfor0 Error(s)would also match a build that never compiled the file. G re-ran asserting the process exit code plus positive existence of the five mirror types in the BUILT linuxsyscall.dll. Absence of failure is not presence of the fix. - Git reading “no textual conflict” as a correct merge.
zsyscall_linux_amd64.csauto-merged cleanly (R’s body deletion and G’s placeholder replacements touched disjoint regions) into a file that compiles and that no converter run would ever emit. Rule, now in the class header: a generated file a hand-own partly displaces is REGENERATED at a seam, never merged; a clean auto-merge there is evidence of nothing, and the checkable property is the ten-names/zero-bodies assertion (with §3’s destination half added). git checkout --ours/--theirsis a DELETE of everything the other side added, and it looks like no decision at all. ResolvingTestHost.cswith--theirssilently reverted G’s own pass-through fix; caught by asserting the PROPERTY (verdict call sites must be 0). A merge resolution is a change, and it gets checked like one.- A revert of anything another lane may have merged is a CROSS-LANE change. R reverted a
superseded commit and reasoned “nets to nil” from
master..HEAD; G simulated the merge and measured the parse relocation STRIPPED with no conflict. The merge base was R’s own fix commit, not master. A branch’s diff against master says nothing about what it contributes against a merge base that master does not contain — assert at the merge RESULT, always. Adopted as window ritual for host-touching merges (the four pair-properties table). git add <path>commits whatever that path currently holds, not the change you have in mind — R’s unmeasured TZ hunks rode into an ordering commit whose message described only the relocation, and reached the window-twelve unit. A commit message describing one change is not a claim the commit contains only that change; assertgit show --statagainst the intent before pushing.$?after a pipeline whose last element is anechoreports the echo, so G’s first witness-sweep attempt died on[CmdletBinding()](a.ps1invoked from the bash tool) and printed exit 0. Two rules: never invoke a repo.ps1from the bash tool, and assert$LASTEXITCODEexplicitly.- A naive string grep cannot tell code from prose ABOUT code — G’s post-merge property check matched the old error message inside his own explanatory comment and read as the merge reintroducing the fix he had removed. A false RED costs the same trust as a false green.
- A stage log that reads a verdict out of an EMPTY log is the “never ran” shape, not a red — R’s rebuilt TZ instrument asserts each stage’s exit code, keeps full output, requires the host binary’s mtime to move, and refuses to read a verdict out of a zero-byte compare log.
- An artifact’s PRESENCE says nothing about what current inputs produce — wipe and regenerate before reading a build tree as evidence (§2, twice in one session).
- A build whose inputs changed under it measures nothing (§3, R’s discarded first repro).
- A system-wide Go on PATH beat an explicitly set GOROOT, so the Go side silently never built
(
compile: version go1.23.12 does not match go tool version go1.23.2) — the standing pin already puts the SDK’s bin FIRST on PATH, and this is the measurement of WHY that ordering is load-bearing. Second confident-looking-output env trap on that box, beside the GOROOT slash-spelling trap. A comparison where one side didn’t compile isn’t a measurement. - The
CurrentDirectorycross-worktree hazard (local linkname-seam lane): .NET pins the processCurrentDirectoryto the session’s ORIGINAL worktree regardless ofSet-Location, so any RELATIVE path handed to the static file APIs touches ANOTHER LANE’S TREE. Absolute paths only.
8. Ledger of what else landed in these two days (context for the rows, not the fold’s subject)
syscall.Unamewas the os/exec heap corruptor —uname(2)writing 390 bytes of inline character data over sixarray<int8>managed reference slots (and ~246 bytes past the object), convicted byverifyheap(118,832 objects, 6 errors in one contiguous ~0x180-byte run holding ago.array<System.SByte>+<GetEnumerator>d__50) and by 7 rounds dead / 7 rounds complete at a flat 72 passes. The struct-passing class’s deferral rule is corrected: “per-member, WHEN REACHED” is right for a wrongness class and wrong for a corruption sub-class — forarray<>-bearing members the kernel writes over GC-tracked references, so the five remaining members (Select/FdSet,FcntlFlock,Statfs/Fstatfs,Sysinfo,Adjtimex) were closed proactively on the mechanism. Landing rule minted: a struct-passing hand-own is not landable without its generated body in the same commit (manualConversionFuncsdisplaces at CONVERSION time, and the-testspipeline never regeneratessyscall, so the CS0111 appears only at a build of that flavor).- os/exec Linux climbed 45–59 ragged (host killed by SIGSEGV, 7/7) → 72 (Uname mirror) → 81
(helper-gate made effective) → 83 (PWD follows the chdir) → 85 (SIGQUIT). Residual is two,
both ruled:
TestCredentialNoSetGroupsDECLINED as a named refusal (a truthful implementation needs credential/setgroups manipulation between fork and exec — the fork-exec dance this corpus deliberately retired for FD-hygiene soundness, andposix_spawnoffers no setgroups file-action; one test does not buy back a retired hazard), andTestExtraFilesRace/newUnixFilecommissioned to a local lane as a converter-level linkname seam (landed window fourteen). - SIGQUIT’s root was better than its brief: SIGQUIT is
_SigThrow, not_SigKill— Go prints the traceback and callsexit(2); dying by the signal happens only underGOTRACEBACK=crash. One root, two defects (signal 3 was insigDiesByDefaultAND there was no report). The truthful half passes honestly, and the deliberate convention break is the honesty call: the capturing thread is .NET’s signal-dispatch thread and not a goroutine, so the block writesgoroutine (signal handler) [running]:, reports the live goroutine COUNT (which the registry genuinely knows), and declares the unwalkable stacks missing rather than reconstructed. Regression witness: banked os/signal 29 + 2, unchanged. - The TZ pin finding narrowed twice. R’s “corpus-wide inert” was wrong in scope; G measured the
Windows flavor as genuinely effective (
env_windows.csimplementsGetenvover Win32GetEnvironmentVariable, which reads the live block .NET updates) and left a permanent GolibTests guard. The inert half is linux/darwin only, with two independent breaks there (env_unix.csanswers from aruntime_envs()startup snapshot, AND .NET on unix never callssetenv(3)). Then R’s own A/B — the two crossed window-twelve tips differing by exactly the two lines — measured the publishing hunks INERT on thetimerow (byte-comparable logs, the same single mismatch on both arms), and R said so rather than letting G’s drop look vindicated by a result it did not produce. It was that control run that surfaced §2. - The bogo/host-limit arc closed with the class NOT emptied, on honest evidence. Two real host
defects were exposed and fixed en route — the host pre-empting unrecognized flags with its own
verdict (any package that overrides
flag.Usagediverged), andTestFlagBridge.Parse()running inRun()beforeRunTestsreachesTestMain(relocated toM.Run, Go’s own position attesting.go:1944; the no-TestMain branch routes throughm.Run()as Go’s generated main does, leaving ONE parse site). i9’s exit-code discriminator is what rooted the second — 2 is flag’s default Usage, 89 is the package’s override, so 2 proves the override was not in effect. The isolated run: 861 PASS / 2,381 SKIP / 0 FAIL, exit 0, wall 1,046.3 s against a 600 s wall on a 24-thread box (1,316 s on the 6650U). Four measurements across two machines fixed the partition as a property: runnable 861 / skipped 2,381, invariant under ~4× timeout pressure — contention only redistributes PASS/FAIL inside the runnable set. Same verdict as before, entirely different evidence: the entry now stands on Go’s own contract rather than on a host defect, with the retirement path updated from “fast startup” to “steady-state TLS throughput”. G’s own correction: the lone0xc0000409he said “will not be explained by a busy box” was, and is gone on the clean run. - Two lanes independently cut the same
M.Runrelocation within minutes — the silent-duplication hazard arriving exactly as the integration rules advertise. What caught it was not tooling: it was the coordinator reading two entries describing one job. Worth recording as a limit — that hazard is invisible to every mechanical check the fleet has.
Arc queue after this fold (Windows chase to 100%, ranked): net residual-35 (five named arcs);
net/http (AV root, then buckets); internal/trace (staging fix rides window twelve); reflect (the
Δ-collision-marker accessibility leveling + the Func 17–24 arity single); the defer/go
synthesized-lambda capture arc (in flight); runtime/pprof (capability frontier — last, possibly
the release’s honest disclosure story). Also queued: the TZ unix half (R’s family, on its own
evidence); the netip deref-only box-chain arc (retitled on the record — NOT B′-S1 re-opened;
the 50/33/17 decomposition has the receiver third smallest, and halves() censused 1-of-1
corpus-wide with the Ꮡ(value).of(…) chain at 5 sites, so it is not converter machinery);
writev; the CS1113 value-receiver method-group arc.
2026-08-29 (overnight) · Fold #4: six windows in one night; Linux runs 27 → 177 of 183 applicable; reflect executes for the first time; one blocked test yields a census pattern, a golib class, and a CoreCLR liveness proof; the 345x handshake resolves host-conditional; the coordinator’s own blind window joins the ledger
Source: the fleet mailbox, the 2026-08-29 overnight range — mailbox commits 95345838b
(window twenty-seven announced, 00:54) through a1e42cae3 (G’s exit-code convergence note,
05:41), extended through 59e9c6f88 (the /h2 settlement, ~05:45) when four posts landed that
completed §5’s measurement mid-draft. Voices: R/R-LAPTOP, G/G-LAPTOP, i9/sweeper,
three local converter/golib lanes, and COORD rulings. Folded per the doc-authority ladder — the
mailbox is transport, this is the record. Every point-in-time figure below is as of
2026-08-29 and anchored to the window that produced it; none is durable.
Window ledger for the night: master 6f529e29f (twenty-seven — the roster’s last red
clears: reflect.Set assignability takes testing/quick 7/8 → 8/8, AF_UNIX’s
subset-constructor fix lands, reflect’s host stands at 8 build errors) → 1a416ba08
(twenty-eight — the triple fold: G’s double bank ce82439b1 takes the roster to 184 rows, i9’s
gosym rebank 2ce5fa643, R’s annotation batches 1–3) → dde1233aa (twenty-nine — R batches
4–6 + BOTH of the night’s first doctrine commits, f68e1a0e5 and dde1233aa itself) →
78fc9544c (thirty — reflect 8 → 0 build errors and FIRST EXECUTION, the seam error-kind
fix, R batches 7–9) → f17693d38 (the linux: n/a convention) → 2e3f6b265 (thirty-one —
the continue-wrapper converter fix via 450ea4a60/aa206f02b, plus the tail rule’s freshness
caveat) → 9e6eb5dc0 (thirty-two — -test-filter + the SetFinalizerBridge guard) →
f3e8a1979 (thirty-three — R batches 10–12, Linux 176 of 183 applicable). Between windows,
two standalone masters: 1dc689fcc (the sweep’s comparison reader made case-safe) and
ab3cbd921 (its parameter-comment residual).
1. The night’s arc, summarized
Six windows (twenty-eight through thirty-three) merged in one overnight shift, zero rollbacks. What moved (all figures as of 2026-08-29):
- The roster: 182 → 184 rows · 25,331 matching · 89 disclosed — 85.6% of 215, honest 88.5%
of the 208 implementable, FOUR rows from the 90% bar (w28, master
1a416ba08). G’s double bank (go/build57|1 +internal/coverage/cfile15|1, both host-identity) carried two habits worth the fold: signatures captured, not inferred —go/build’s emitted text was the no-rootImportPath="."form, NOT thestd/-prefixed form its sibling’s mechanism predicted, so a signature written from reading the Go source would have silently failed to bind — and the roster guard catching two stale header lines her hand edit missed (mailboxec9f2d0d4). - The Linux annotation campaign COMPLETED: 27 → 177 of 183 applicable rows · 12,877 → 21,798
matching · 89 disclosed · guard 299 → 472 checks (R, eleven batches + the four long-floor
rows; 176 at the w33 merge
f3e8a1979, 177 on R’s final push976103765; campaign-complete post mailbox1214021e2). The floor run closed 4/4 PASS —crypto/internal/mlkem76812 [448s],crypto/dsa4 [3,895s],index/suffixarray12 [1,066s],hash/maphash22 [1,561s].crypto/dsaat 65 minutes is the row that justifies the floor table: any budget under an hour false-reds it, which is exactly what R’s ownsync/atomicrun did at 30m. Every measurable Linux row is measured; the six that remain are named and none is unmeasured for want of sweeping (§7). Linux stands at 96.2% of applicable (COORD, mailboxc8fc7dc5c). En route, batch 6 (codecs, hashes, image decoders, math) went 14-for-14 clean — itself a finding: every per-OS divergence this campaign surfaced sits in an OS-facing row, the strongest evidence yet that the Linux frontier is a bounded set of named seams, not a diffuse tax (mailboxa1fcd3d06, COORDd8c7f194b). - reflect COMPILES AND RUNS for the first time in project history (w30, master
78fc9544c): the final-six lane took the test host 8 → 0 build errors — six converter roots, four NOT where the handoff placed them (the text-first rule’s yield), six red-proven behavioral guards, one deliberately withdrawn fix arm (a reinterpret that access-violated under measurement — the loud CS0030 kept over the silent lost write). First execution: 206 started, 99 pass / 93 fail / 1 skip before the 30m deadline;TestOffsetLockate ~28 min of wall on ~10s of CPU — a BLOCK, not slowness — and 11 scattered non-reporters are a separate defect. Honest non-bank by the lane (deadline-truncated, partially contended). Evidence stack at the merge: converter test green, CNR byte-identical across 672 with ONE intended golden, canaries solo and exact — 2,358/0 across gcimporter/go-types/json/xml/x509 (COORD, mailbox34c836a54,74dbb7904). The reflect DIVERGENCE arc opens as its own campaign. - Three converter changes landed on master in one night, each through its own lane and a
full merge-window gate: the reflect final-six (above); the continue-wrapper fix — a Go
continueinside the syntheticdo{…}while(false)switch-breakwrapper bound to the WRAPPER (itself a C# iteration statement), exited on its false condition, and fell through past the switch, the fallthrough hazard’s symmetric twin (breakwas retargeted by the wrapper;continuewas never considered) — G’s find mid-wait, PROVEN by a compiled net10 program not reasoned, live in exactly net/httpcookie.cs’smax-age/expirescases (the only two wrapped, hence the only two leaking — and the symptom was diagnostic:ExpiresANDRawExpiresboth CORRECT while the attribute ALSO landed inUnparsed, possible only if the body ran to completion then fell through), fixed by a local lane to the ruled goto-label lowering with 110 working wrapper sites byte-identical (mailbox485e81337, GO1f8593dc9; master450ea4a60/aa206f02b, w31); and G’s-test-filter(the block-gated census instrument, §3; master9e6eb5dc0, w32, CNR byte-identical across 674 — the flag leaked nothing into emission, exactly as predicted at approval). - The seam error-kind fix (R; master via w30
78fc9544c, witness7b3fa26ba+ fix36aa2bbde): syscall’s posix_spawn refusal now answerserrors.ErrUnsupportedthrough the Is-method form, all EIGHT skip-guarded rows land as skip-MATCHES, Linux divergences 22 → 13 — one BETTER than R’s predicted 14, and he accounted for the beat (TestCloneNEWUSERAndRemap’s parent passes once its subtests skip); the verdict-spread arithmetic closed to zero (mailbox2484b454d). - The
linux: n/aconvention landed (masterf17693d38): a platform-exclusive row is permanently inapplicable, never pending, and the Linux header gains its honest denominator — “N of M applicable” (§7). - i9’s full-roster sweep at master
1a416ba08: 183 pass / 1 fail in 8,666s (~2h24m) — the one red (go/internal/srcimporter) is the standing i9 no-cgo host-provisioning gap, where Go’s own side fails first (Go="fail" C#="pass", confirmed against the comparison JSON, not memory) — the strongest master-green full-tree proof this campaign holds, and the clean baseline for the 90% push (mailbox771821894).
2. Instrument doctrine — the night’s dominant theme, stated as classes
The night’s densest yield is not a row count; it is a convergent family of instrument-discipline findings, several arriving independently in two or three lanes within hours. Each entry below is a CLASS with its instances cited. Where the lesson is already landed doctrine, the master commit is cited rather than the text restated.
(a) The results-file TAIL rule, and its same-night freshness caveat. A deadline kill states
itself as an explicit {"action":"timeout"} final event; the tail is read FIRST, before any
empty-set shape analysis, and quoted in any census that reports empty verdicts (doctrine: master
f68e1a0e5). Three deadline-kill instances in one night: G’s net/http 25m run, whose kill she
published as “213 divergences across 87 parents” before the tail showed her own
-test-timeout had killed the host mid-run — withdrawn within the hour with the arithmetic that
proves the kill, 1,147 run vs 1,134 terminal events, 13 in flight (mailbox 856ed906d); R’s
sync/atomic 30m timeout, initially sitting in a red list beside genuine divergences until he
drew the split himself (mailbox a1fcd3d06); and G’s 90m re-run, where the rule was OBEYED —
the tail gate she had positive-controlled against the known-bad 25m file fired, she quoted no
census, and the identical-runs comparison became the block proof of §3 instead of a third
misread (mailbox 727e6e206). crypto/dsa’s 65-minute floor run (§1) is the same kill wearing
a “genuine red” costume — the row any sub-hour budget false-reds. The caveat arrived the same
night the rule landed (doctrine: master 2e3f6b265): a --run-filtered host does not rewrite
go2cs_test_results.json/.xml — G’s four-way A/B, order-independent and
exit-code-independent, obvious candidates eliminated, mechanism deliberately unasserted — so
a stale results file beside a fresh comparison is NOT a deadline kill, a gated census gates on
the CAPTURED STREAM (the artifact the comparison is actually built from), and the check is one
timestamp compare (mailbox 0b1c64fb5, banked defa27f62). The --run/results-rewrite defect itself is a routed chip (hand-owned testing
host, WriteResults path), mechanism still owed.
(b) One class, two statements: “no negative believed without exit code and raw tail” (R) and
“read the OUTPUT, not the status” (G). Two lanes hit the identical instrument-level trap
independently in one session, which makes it systemic — a property of the tooling, not a
personal tally (G’s own framing, mailbox a1e42cae3). R’s instances: every GUARD EXIT: 0 he
quoted this session measured tail’s exit, not the guard’s (check-roster-format.ps1 | tail -3;
echo $? — hollow for exactly as long as the gate kept passing, exposed only when a genuine
failure made the real exit 1; mailbox e7f62a4e8); a wrong-path probe (directory probed, file
existed) that declared an artifact destroyed and got a fix approved on the false premise — one
bad probe, two wrong conclusions (mailbox b83784ddc, corrected 32ebce5cb); go: command
not found → exit 127 → empty output reading as “no skips.” His full five-defect tally — the
|-blind header parser (caught by the guard, not by him — the re-derive-never-trust design
doing its one job, with batches 1–8 provably unaffected because the guard AGREED with his
header on each); the piped exit codes; the wrong-path probe; the five-commit-stale lane (§2j);
and a lapse leaving the session on a single watcher leg — closes on the shared shape: a
broken probe returning a confident negative is more dangerous than one returning nothing,
because emptiness invites suspicion and a plausible answer does not (mailbox 1214021e2). G’s five, in her order (mailbox a1e42cae3): the Store’s python stub
prints “Python was not found” and exits 0; powershell … | head -3; echo rc=$? reports
head’s status (“confirmed” a script had run when it had not); a || echo "(none)" fallback
hung off a pipeline ending in sed that can never fire; the exit $true-is-exit-1 liveness
loop (already in CLAUDE.md — she walked into its shape anyway); and the non-exit-code sibling, a
host A/B run without GOROOT returning exit=2 with no files, indistinguishable from the defect
under investigation until the TEXT named fixture staging. The one habit that caught all five:
read the output, not the status — every one announced itself in words while the exit code lied
or belonged to something else. R’s guard catching his own header parser is the same lesson
with a gate instead of eyes, which is strictly better. Concrete and cheap: ${PIPESTATUS[0]}
in bash, $LASTEXITCODE captured IMMEDIATELY in PowerShell, never a || fallback off a
pipeline whose last stage is a formatter. R’s rule was fold-bound at COORD d53816a8a; G’s
convergence note closed the range.
(c) “No all-clear wider than the census that produced it.” G found net/http’s
case-only-differing test names (…/GZIP vs …/gzip) break PS 5.1’s ConvertFrom-Json,
checked the Go WRITER (case-sensitive by construction, all eleven case-folds on file paths),
and wrote “the product is sound… nothing to route” — a claim covering the repository from a
check covering one component. COORD then found the sweep’s own comparison READER folding the
very pair she had published, fixed at master 1dc689fcc (ordinal dictionaries, a fold-detector
fixture whose two case-varying keys carry two DIFFERENT values — a positive control must be
able to fail — guard 369, smoke-proven). The instructive half, in her words: she warned
everyone about “our own downstream tooling” and never asked whether the repo WAS downstream
tooling — validationProofPages.go was already in her own grep output and she read past it
because she went looking for a writer and stopped at one. She then ran the census she owed —
all SEVEN consumers, with symbols.json cleared for a STRUCTURAL reason (an array of records
cannot fold, which stays true as symbols are added) rather than an empirical one (mailbox
dcb799b81, 34c836a54, 10cbd86a4; residual comment fixed master ab3cbd921; fold-bound
COORD 432f8da2b: a claim’s scope is the list of consumers actually checked, and the warning
you issue outward applies to your own repo first). The same family arrived in miniature an
hour later: her first continue-wrapper census used a [^{}]-bounded regex that cannot span
nested braces and reported ONE file; the nesting-aware scanner found 4 sites across 3 files
among the corpus’s 116 wrapper sites — the instrument’s blind spot reported a smaller world
(mailbox 485e81337, fold-bound beside the scope lesson at 1f8593dc9).
(d) The symmetric form, minted at the range’s very end: “a red generalizes no better than a
green.” G’s “no knob — the /h2 rows cannot be bought with patience” (§5) was a negative
all-clear — a claim of universal impossibility from a single host — and i9’s desktop then
cleared Go’s real 500ms ceiling 3/3. Her measurement was right for her box; the generalization
was not, and she named it as the exact MIRROR of (c): she had applied the scope discipline to
positive findings and not to this one, so stating the rule for greens evidently does not
immunize against reds (mailbox 883a1c856, ratified 59e9c6f88).
(e) “The control must be checked as hard as the experiment” — and its sharpest phrasing yet:
“it survived, rather than being known to hold, and those are different states.” G stress-tested
her own published parked-receiver class claim unprompted. It HOLDS — 8/8 vs 8/8, both sides
deterministic — but the attempt to narrow the holder’s shape produced a probe that differed from
the reproducer only by an os.Args read and an UNEXECUTED if-block, and that was enough to
flip Go’s side (the second closure’s presence changes what Go’s stack maps report live; Go
makes no promptness guarantee, which is precisely what one leans on at this granularity). So the
slot-shape question is UNANSWERED and that probe cannot answer it, since its own control moves;
any finalizer-timing probe owes a stability run on BOTH sides before its result is quoted
(eight runs cost seconds); and the reproducer is safe to build on because it was measured this
way, not because it was written carefully (mailbox c12a4af9a — her third arrival of the same
lesson, after the GZIP/gzip trap and the GOROOT-less probe). Absorbed into the golib fix lane’s
protocol as three BINDING rules: 8-per-side stability tally first; the probe stays
BYTE-IDENTICAL across the pre/post-fix A/B (no prints, no args, not even an unexecuted branch);
the guard’s golden banks only after its own stability pair (COORD fd7727697). She then applied
it to her own new artifact: SetFinalizerBridge’s stability was measured 8/8 per side before
the test was written, not after it passed (mailbox 67020dc82).
(f) The byte-identical-probe corollary, self-caught, and the operating-instruction defect it
did not cover. G argued her probes were scaffolding and the numbers the durable artifact — a
rebuild from description would do — then overturned herself one post later: that holds for a
REPRODUCTION and fails for an A/B, because a cross-host comparison whose probe was
independently rebuilt cannot tell a host difference from a probe difference. Same rule the golib
lane already carried, out of her own protocol: stated for others, reasoned past for herself,
one post after writing it down (mailbox 4c7c4b9b3; probes pushed as bytes, g-probes @
f54087b3b, sha256-pinned, dirs -text so “the same bytes” stays true rather than nearly
true). Then the range’s last correction found the half the hashes did not cover: her
prediction post named the WRONG COMMAND against her own source — the probe’s argument is the
BUDGET and it sets WriteTimeout = timeout/2, so “the deciding run is 500” gave a 250ms
WriteTimeout, half the ceiling; the deciding run was 1000. i9 ran 500 first, got FAIL ×3, and
caught the mislabel by re-reading her main.go rather than trusting her prose — had they
trusted it, a correctly-executed byte-identical run would have delivered a confidently
mislabeled result. Her ruling on herself: that is a defect in the PROBE, not just the post —
an argument that is not the quantity of interest invites exactly this — so fix the interface,
not the sentence (the probe takes the WriteTimeout directly or names units in its output; the
recorded hashes change and are re-posted, since a hash that no longer matches what is described
is worse than none) (mailbox 883a1c856, 5e5138dd6). And the pair of experiments is now
doctrine, ratified from i9’s accidental both-runs design: byte-identical isolates the HOST;
an independent rebuild tests the FINDING — different questions, both worth having, named as
such (mailbox 3d2488a1b, 3895fdd0a, ratified 59e9c6f88).
(g) The coordinator’s own-push blind window — the coordinator’s failure, with the same
prominence as the lanes’. The verified-push pattern fetches and resets to the remote tip
before appending, silently ABSORBING posts that landed since the last read; the monitor’s
next fire was dismissed as “self-detection” without diffing the range, and the read anchor then
advanced past the absorbed posts. Seven posts were skipped in one night — including i9’s
full-roster RESULT (mailbox 771821894, posted on time; the coordinator’s “result still owed”
nudge was WRONG, apology on the record) and R’s cgo-crash routing request (mailbox e8deb73e7,
which then waited three hours) — and seven was itself an undercount: G’s probe-push
announcement was among the absorbed, which is why window thirty-three issued a stale
rebuild-from-specs instruction against bytes already two hours on the remote (caught by G
re-raising with the remote verified via ls-remote, not her local branch; mailbox 3d2488a1b,
owned at 59e9c6f88). The amended rule, now in the coordinator’s own doctrine: a monitor fire
is NEVER pure self-detection; the diff runs from the last hash actually READ, every time,
own-post tip or not (mailbox c8fc7dc5c). R’s reconciliation-counter lesson and G’s scope
lesson were the same shape in other hands; the coordinator’s is now on the same page.
(h) The adjacent-INSERT merge hazard. Master inserted go/build immediately above
go/build/constraint, a row R had annotated; git folded both into ONE conflict hunk where
either single-side resolution silently loses a row (--ours drops G’s new row entirely,
--theirs drops R’s annotation) and nothing in the markers says so. R resolved keeping both,
asserted the row count across the merge, and validated his re-derived header by positive
control first — the same parser against master’s own blob had to reproduce master’s banked
header exactly, and did (mailbox a1fcd3d06). Doctrine landed the same window: master
dde1233aa (cited, not restated).
(i) “The capability exists and one path fails to reach it” — the earned diagnostic prior.
G’s count on the night: the fifteenth time this session the answer took that form — the
load-bearing instance being the SetFinalizer bridge itself, expected to be a stub, found to be
a real hand-owned ConditionalWeakTable bridge keyed on the referent, and then PROVEN firing by a
minimal probe, which forced the retention conclusion in §4 (her post, mailbox 1b0ad1978;
fold-bound as the prior at COORD 5bcadf49d). The prior’s proper use is search-ordering, not
conclusion-jumping: it says check the reach path before re-litigating the capability, and it
earned that status one elimination at a time.
(j) The staleness family, both directions. R’s reconciliation counter refused to read “15
of 16 reported” as complete and thereby caught his lane FIVE COMMITS STALE at its provisioning
commit — among the missing was 635b27c7c (reflect.Set), the reflect-bridge class whose
canary doctrine his banked counts implicated; the control run then validated
reflectlite/sort/fmt against his own banked annotations (the (linux) suffixes), retiring
the canary question for all 102 prior rows on a corpus that includes the fix (mailbox
32ebce5cb, COORD d53816a8a). G ran the implied check on her own lane unprompted and
pre-registered the result BEFORE her census landed: 7 commits behind, but git diff
--name-only HEAD..origin/master -- src/ = zero src/ files, so her in-flight run measured
master’s code exactly — “identical where it counts” being actionable where raw commit-distance
(“7 behind”) invites an unneeded rebase (mailbox 5c4d14b7e). Same shape as R’s syscall
finding: two true statements, only one answering the question.
3. The block-gated census pattern — named, constrained, instrumented, and paid for in one run
The pattern (COORD ruling, mailbox b42c13e16): when a suite is held hostage by a PROVEN
deterministic block, gate the block and census what is behind it — gate-then-census, never
root-first-while-the-suite-waits. Four constraints keep it honest:
- Symmetric — the named test is excluded on BOTH sides (Go reference run and converted host, same filter), so the comparison stays apples-to-apples.
- Visible — the census output names its exclusion list in the header; a gated census that reads like a full one is the false-green shape.
- Runner-level only — no skip added to converted sources, no harness-injected skip dressed as the suite’s own; the anti-laundering clause applies to diagnostics too.
- Diagnostic, never banked — the ROW banks only from an ungated run, after the block is rooted or disclosed under a legitimate class; stated in the census, every time.
The block-proof standard is two-budgets-zero-delta. G’s 25m and 90m net/http runs were
IDENTICAL in every particular — 2,283 events both, 1,146 started / 1,134 terminal / 12 in
flight both, same last event (run TestTransportGCRequest/h1/Body), same verdict pairs — so
3.6x the budget bought literally zero progress: not slowness, a BLOCK, structurally provable
besides (the test ends in an unbounded for/select with no deadline of its own, exiting only
when a finalizer fires; §4) (mailbox 727e6e206).
The mechanism did not exist, and G reported the gap before assuming past it (mailbox
2719e9df4): compareGoAndConvertedTests hardcoded both arg lists; the converted host has
run but not skip; and capabilityGatedDeclaration gates at CONVERSION, failing constraints
1 and 3 at once. The smallest satisfying mechanism — -test-filter, ONE flag threaded
VERBATIM to both invocations as -run — was built, gated, and merged the same night (mailbox 9e195c5ec; branch
g-test-filter @ 687b0f922, guard 33a2ca242, master 9e6eb5dc0; +34/−4 across three
files). Its two load-bearing design points, on the record because neither is stylistic: the
flag never composes the string, it forwards it — the two logged command lines carry the
identical filter, auditable by eye, which is what constraint 1 means operationally (a cleverer
flag building each side’s filter would be the very thing constraint 1 prevents); and validation
compiles the pattern with Go’s RE2, the stricter of the two engines it feeds, so a pattern
only .NET would accept dies at the flag naming itself. -run beat -skip because run exists
on BOTH sides today; exclusion is an anchored include-list — RE2 has no lookahead, and an
explicit include-list IS the exclusion list inverted and visible (~12.6KB for net/http’s ~450
parents, inside Windows’ 32KB). Constraint 4 rides in the help text, because the flag is the
last thing read before running one. Collision handling was the concurrent-lanes discipline
working UPSTREAM of a merge for once: G flagged the possible reflect-lane collision before
cutting; answer, no collision — the reflect TestOffsetLock follow-up was sequencing, not
staffing, and will CONSUME her mechanism (mailbox ed55cfaf5).
The result paid for the whole pattern in one run (mailbox 0b1c64fb5, accepted
defa27f62): excluding TestTransportGCRequest ONLY (569 of 570 parents kept, the exclusion
named in the header), net/http’s apparent 228 divergences across 92 parents collapsed to 18
across 10 — and the whole suite ran in 342 seconds against two runs that burned 25 and 90
minutes finishing nothing. Constraint-1 symmetry was proven arithmetically, not asserted: Go
1,352 → 1,345 names (exactly minus the excluded parent and its six subtests), C# 1,135 →
1,345 — both sides enumerating the IDENTICAL 1,345; pass/pass 1,111 → 1,309, absent 213 → 1,
and the arithmetic closes at 1,346 = the union. The 213 “divergences” of the blocked runs were
tests that never got to RUN. G’s by-parent rooting then collapsed further: 10 parents → 6
roots (mailbox c83808de7) — the continue-wrapper cookie pair (FIXED on master at w31; a
re-census at ≥ 2e3f6b265 expects 16/8); anonymous-function naming (3 parents / 7 rows, one
root absorbing TestTimeoutHandlerSuperfluousLogs — we number func literals from 0 and
unrepresent nesting; remedy is DESIGN-position-map §8/§11.1’s converter-records-the-ordinal,
with ordinal+1 REJECTED as binding since Roslyn’s b__X_Y closure-group index does not
correspond to Go’s per-function counter — silently-wrong beats visibly-wrong nowhere;
dispatched to a local lane, COORD 8b67f0fb8); the h2 write-deadline pair (§5);
TestRegisterErr (3 rows, ONE defect — adapter shell type leaking into a rendered name plus a
typed nil printing as a moving address; the standing golib chip, and the survival ruling is its
own small lesson: the rendered shell-type string PROVES the subtest ran, since a timeout victim
produces no name at all — COORD d8c7f194b); TestReadRequest’s formatter hiding a real
channel inequality; and TestServeMuxHandleFuncWithNilHandler throwing at delegate
construction BEFORE Go’s panic site. The frontier is countable on one hand.
4. The parked-receiver residual: from a 25-minute timeout misread to a CoreCLR liveness proof
The night’s deepest chain, and the fold records the ELIMINATIONS with the finding, so nobody re-walks them.
The elimination chain (G, black-box, each step measured):
- Closure display-class merging — refuted by the emitted code:
reqis a plain local of the IIFE lambda, the finalizer lambda captures only the channel; Roslyn hoists nothing here (mailbox1b0ad1978). reqCancelerunfaithfully converted — refuted: byte-faithful totransport.go:869-878, and Go passes with the identical map in place (same post).- The SetFinalizer bridge itself — refuted by a working minimal probe: converted
finalizers FIRE (same post; the probe became the
SetFinalizerBridgeguard, master33a2ca242— zero prior coverage of a hand-owned, subtle, hard-won bridge). channel<T>retaining its last-received value generally — refuted; a main-frame receive releases. (Footnote preserved per her discipline: on the buffered variant Go itself reported RETAINED, so that variant tested nothing she designed and she drew nothing from it — a control that fails its own premise gets discarded, not explained.)- Debug-JIT local liveness — refuted; Release behaves identically (mailbox
6a344b170).
Along the way the instrument collapsed from 342s to two seconds (a standalone 45-line
bisector), and the bisection localized the holder: construction clean, the ROUND TRIP retains,
not the Transport object, and CloseIdleConnections() releases — an idle persistConn
retains the request it last served, rooted independently by its live readLoop/writeLoop
goroutines (mailbox 6a344b170). The class then confirmed HTTP-free in 40 lines: a value
delivered to a PARKED goroutine receiver is retained after receipt — Go releases while the
goroutine parks, C# retains, Debug and Release alike, with the parks-again ingredient isolated
against a main-frame-receive control; WaiterQueue exonerated (Remove fully unlinks), the
registry exonerated, and Waiter.Elem named a SUSPECT, explicitly unproven (mailbox
9dce27557). COORD’s declaration-hoisting candidate died cleanly — the reproducer has no inner
lambda to carry it (COORD 78dc3cd19, killed 980d617e4).
The golib lane’s resolution (gcroot, not plausibility; COORD relay 636b7da5c): the
payload is held by the receiver goroutine’s OWN loop-frame stack slots — heap dumps mid-park
after forced compacting gen2 GCs show exactly 3 stack-slot roots (the v local plus
Roslyn’s tuple-deconstruction temps) and ZERO heap paths; with tiering off, still 8/8
RETAINED via ONE untracked struct-temp slot. G’s suspect died twice: source shows a fresh
Waiter per park (no pooling), and the dump shows the dequeued first-park waiter COLLECTED
outright. Three sealing A/Bs: clearing Elem changes nothing (8/8 retained); moving the
receive into a NAMED FUNCTION releases 8/8 with zero golib change — frame residency is the
whole mechanism; and two sequential payloads show a SINGLE OVERWRITTEN SLOT — first released,
second retained. Bounded at one last-delivered value per parked receiver goroutine. Never
accumulates.
Mechanism, named: Go’s per-PC stack maps kill the local at the loop back-edge, so Go’s GC frees it while parked; CoreCLR reports untracked frame slots live for the standing frame — a goroutine body called once never tiers up, and even full-opt keeps the unpromoted tuple temp. golib cannot reach a caller’s frame; a converter dead-store cannot reach the Roslyn temp, and full-opt would elide it anyway.
Rulings (COORD 636b7da5c): (1) recorded here as a platform-liveness fidelity
residual — bounded (frame-variant and single-slot bounds above), NOT a golib defect, NOT a
converter defect; the same artifact family as Go’s own finalizer non-promptness, which G
herself showed flips on an unexecuted branch (§2e). (2) TestTransportGCRequest routes to the
EXISTING codegen-liveness disclosure class when net/http banks — the class TestCertCache
already grounds; the test asserts on collection that CoreCLR’s model cannot promise for a
parked frame, and the 40-line reproducer + gcroot transcripts are the proof page’s evidence
when that row lands. It stays gated in any diagnostic census until then. (3) A
converter-territory narrowing (helper-frame receives) is board-parked, explicitly NOT
scheduled — it would reshape every channel receive for a memory-promptness nicety, and
nothing correctness-bearing needs it before 1.24. (4) No commits banked, and no guard is
constructible — no red-to-green exists for a platform property; the lane’s zero-change
stop-and-report was correct. COORD’s summary line stands as the arc’s epitaph: one night, one
test — a 25-minute timeout misread, a proven block, a census pattern, a mechanism gap, a
runtime-class hypothesis, and a CoreCLR liveness proof, each step measured.
5. The crypto/tls handshake: two-instrument closure, then the no-knob sting dissolves on the fast host
The measurement (G, mailbox d8d1d1e64): converted-C# TLS handshake
with NO WriteTimeout set and keep-alives disabled, three runs per side — Go mean 2ms / worst
3ms; C# mean ~691–705ms / worst ~1,078–1,130ms on G-LAPTOP. ~345x. It settles net/http’s
h2 write-deadline pair (§3’s Root B): the test’s effective deadlines are 125/250/500ms and the
handshake exceeds every one — the deadline is applied CORRECTLY and cannot be met, so the
“apply-the-deadline-where-Go-does-not” semantic alternative died unfixed, to its author’s
stated relief.
Two instruments, one crossover — the closing form. The direct measurement predicts a
pass/fail crossover between 500ms and 2s of WriteTimeout; COORD’s discriminator (a budget
sweep at deadlines the real test cannot reach), run by G exactly as specified, landed it
exactly there — 250ms FAIL, 1s FAIL, 4s PASS, 16s PASS, three byte-identical runs (mailbox
33d765f76). Neither alone
sufficed: the measurement could not rule out a semantic fault surfacing only once timing is
met. Two independent lines agreeing is the standard closing form now (COORD 0e6753732).
The sting, precisely drawn and then precisely dissolved. G’s distinction vs the maphash
precedent: maphash is slow but PAYABLE — a bigger -test-timeout buys it; here the budget is
tryTimeouts (serve_test.go:980), hardcoded {250ms, 500ms, 1s} in Go’s own test source, so
the largest WriteTimeout the real test ever sets is 500ms — no knob (mailbox 33d765f76).
Then the fast-host leg ran: i9, on G’s sha256-verified probe bytes (g-probes @ f54087b3b),
measured its handshake at 368–389ms mean / 601–624ms worst (~1.8x faster than G-LAPTOP, still
~190x Go) and PASSED Go’s real 500ms ceiling 3/3 both sides — i9’s own crossover sits
between 250 and 500ms of WriteTimeout, tighter than G-LAPTOP’s, which failed even at 500ms
(mailbox 5e5138dd6).
RULED (mailbox 59e9c6f88): the /h2 rows are a host-speed-conditional performance gap
— they bank the bogo way (the capable-host leg), no disclosure and no new class needed;
G’s “cannot be bought with patience” stands correctly scoped to her host class (§2d carries the
generalization lesson). The disclosure wording had been deferred DELIBERATELY through the whole
thread — a disclosure written now would describe a number we intend to change (COORD
9904336ef) — and the deferral was vindicated by dissolving.
Banked forward: the handshake gap itself — G-LAPTOP ~700ms / i9 ~380ms / Go ~2ms, a
two-host baseline — rides to the post-1.24 performance queue with profiling as its first
step; 345x in a crypto path smells pathological, which is the good kind of gap (a cliff in the
profile). crypto/tls is a BANKED row (402 verdicts as of 2026-08-29, a reflect-bridge
canary), so this is a real-world cost in a package nobody thinks of as unfinished — G flagged
it as a board candidate with the measurement attached and declined to chase it, which was the
right shape (COORD 9904336ef, a0c257759).
6. Two named product items out of the Linux endgame
The cgo Syntax-pairing converter crash — go2cs -tests cannot convert ANY cgo package on
Linux (R, mailbox e8deb73e7). conversionDriver.go:228 does pkg.GoFiles[i] inside
for i := range pkg.Syntax — but go/packages fills Syntax in parallel with
CompiledGoFiles, not GoFiles, and for a cgo package the two differ (plugin on
linux/amd64 is GoFiles=1 PLUS CgoFiles=1), so the loop walks off the end. Measured as a
CLASS with controls on both sides: plugin panics index-out-of-range [2]len2, os/user
[6]len6, internal/testpty [2]len2 — all at that identical line — and hash/adler32 (non-cgo)
exits 0 with zero panics as the negative control. Census on linux/amd64 is FIVE:
internal/testpty, net, os/user, plugin, runtime/cgo — a standing blocker on a
substantial future target, not a one-row curiosity. Windows never meets it (no cgo files there,
equal lengths), which is why the row banks on Windows and is unmeasurable on Linux.
DISPATCHED as the night’s third converter lane (branch claude/cgo-syntax-pairing): the
pairing fixed as a CLASS with a full pairing-site census ordered (not just line 228), a
platform-neutral red-first unit guard, CNR to prove non-cgo byte-identity; Linux-side
confirmation (plugin/os/user converting clean) is the Linux lane’s post-merge follow-up —
unblocking net’s future Linux leg (COORD c8fc7dc5c).
The runtime/debug fault-to-panic bridge (R, same post; carried in the w33 record, master
f3e8a1979). The row’s apparent nineteen divergences are ONE host crash: the empties are a
contiguous alphabetical tail from the crash point (the documented died-partway signature —
NOT MEASURED), TestFreeOSMemory already binds its pinned Windows codegen-liveness disclosure
signature exactly, and the whole red is TestPanicOnFault taking a fatal
AccessViolationException where Go recovers a panic — the test deliberately faults to prove Go
turns a bad dereference into a recoverable panic, and the managed host dies instead. The
fault-to-panic bridge in the signal path is a named golib/runtime queue item; cost today is
eight unrelated verdicts, and absent the crash the row plausibly banks at its Windows shape
(4 + 5).
7. Rulings ledger (one line each, with citations)
- syscall’s Linux red is NOT a regression — a disjoint population. Windows and Linux
syscall are almost entirely disjoint suites (11 Linux test files, ZERO
*_windows_test.go); the roster’s Windows 65 names tests Linux does not have, and “65/65 on Linux” was the sweep’s comparison-validated-at-count wording, an answer to a different question. No new convention needed — thecrypto/randprecedent (linux: 302 beside Windows 298, disjoint by design) already covers it; the row annotateslinux: N + Dwhen and only when the Linux suite validates, and the total-disjointness fact goes on the proof page at banking (Rb83784ddc, RULEDfc30c442a). - The ErrUnsupported seam framework for syscall’s remaining 13: the 6 skip-divergent rows
fit the EXISTING platform-skip class as written (the skip taken is Go’s OWN
SyscallIsNotSupportedguard, conditioned on a property the deployment genuinely and by-design holds; anti-laundering unchanged); the 3 no-guard rows are standard disclosures with the seam’s refusal text pinned (class named at banking, working name runtime-capability); the 2 intrinsics + 2 sendmsg rows stay FIXES, never disclosures — so syscall’s Linux annotation is reachable by fixing roots 2+3 and banking with 6+3 disclosed (RULED683f5c8b8; fix landed w30, §1; the Is-method refinement4a91b2515needed no ruling — the landed fix already embodied it). - debug/gosym banks
linux: 9 + 1, host-identity:TestSymVersioncracksos.Args[0]— the RUNNING TEST BINARY — and a .NET apphost carries no Go pclntab, so Go’s OWN second guard fires; Windows cannot expose it (skipIfNotELFskips both sides, hence the banked 10/10). Both of i9’s candidate causes were EXCLUDED by falsification —internal/coverage/cfile, same regen era, identical_IsPublishingblock, PASSED on Linux 15+1 — and the rebank is innocent (Rc016a2cd6, RULEDc8fc7dc5c; signature pinned from the run:not relevant to Go 1.2+ symbol table). - The
linux: n/aconvention (masterf17693d38): a platform-exclusive row is permanently inapplicable — parseable annotation, three populations counted separately, honest “N of M applicable” denominator,windows: n/arefused as the back door it would be; guard 434 with the negative control fired BY NAME, and the Windows-unchanged proof run live on the registry row itself (finde7f62a4e8, RULEDdabbfb638, landed8d56d05d9). Load-bearing for the parity goal: 100% must be reachable to mean anything. - The GolibTests-never-runs-on-Linux gate-coverage hole: the ratified OQ-2 seam gate
(
UnobservedChildSurvivesUntilWait) fails on Linux with a nil-deref — pre-existing and deterministic by a three-repeats-per-side A/B at7b3fa26ba/36aa2bbde, exonerating the seam fix where reasoning could not — andGolibTestsappears in NO workflow and NO sweep (onlymigrate-tfm.ps1), so it may never have executed on Linux at all. A proof that cannot run protects nothing — same species as the “nothing routinely builds go2cs.slnx” wall. The nil-deref is a named Linux-frontier item at R’s two SHAs; the coverage hole is the durable half, proposed home the os-matrix workflow or the Linux lane ritual (R2484b454d, board-bound683f5c8b8). - The 13-file
len(fixed-array)constant-folding drift joins the leveling queue (crypto/cipher/gcm, the four nistec curves, edwards25519 field, internal/bisect, five runtime files, time): an already-landed converter arc whose regen never covered them, found by the continue-wrapper lane’s census and deliberately left for the deliberate leveling pass — flagged in the fix’s own bank so it cannot be mistaken for that change’s drift (COORD297d7923e, master450ea4a60). - The five-package
package_test_info.csstaleness is queued rebank material: proven PRE-EXISTING at6f529e29fby a base-commit converter rebuild, so it belongs to a deliberate rebank window, not to any of tonight’s arcs (COORD74dbb7904). - Routed chips, standing: the
--run/results-rewrite host defect (§2a; mechanism owed); theTestRegisterErrgolib pair — shell type in rendered names + typed-nil identity — for the reflect/golib lane (d8c7f194b);TestReadRequest’s formatter and the nil-handler delegate construction (1f8593dc9); the func-literal-ordinal converter lane in flight with its STOP order on schema/corpus footprint (8b67f0fb8).
Arc queue after this fold (as sequenced at the range’s close): the reflect divergence arc (OffsetLock block + 11 non-reporters + named panic roots — the block-gated census pattern’s second customer); the func-literal ordinals lane (in flight, local); the cgo Syntax-pairing lane (in flight, local); the net/http residue (TestReadRequest + nil-handler chips, the TestRegisterErr golib pair, then banking against the codegen-liveness disclosure for TestTransportGCRequest); the Linux tail — os/exec (parked genuine divergence), sync/atomic (re-run with a floor), syscall roots 2+3 then bank, gosym 9+1 bank (i9’s confirming re-run at leisure), the fault-to-panic bridge; the post-1.24 perf queue gains the two-host TLS-handshake baseline with profiling first; and the board-parked, explicitly unscheduled converter narrowing for helper-frame receives stays parked.
2026-08-29 · internal/godebug BANKS at 5 | 0 by completing its own hand-own — and the bisect arm is the first measured proof the managed traceback surface is hash-stable across processes; internal/concurrent re-measured at 1.23.12: the structural non-bank stands, with a second, LIVE whitebox surface the 2026-08-19 entry did not name (lane claude/local-internal-banks, i7-5820K)
internal/godebug 5|0 (was 3 of 5). The first pipeline pass reproduced the prior census
exactly — TestGet pass, TestBisectTestCase pass, TestPanicNilRace skip=skip, TestCmdBisect
bisect: fatal error: target failed without printing any matches, TestMetrics
NonDefault value = 0, want 3. Both failures were the hand-own’s own DOCUMENTED shortcuts
(“IncNonDefault is inert”, “the value#pattern suffix is stripped, enabling the setting
unconditionally”), and both were rooted rather than disclosed — runtime-capability’s admission
test answers YES for each (a truthful managed implementation exists), so no class could take them.
- Metrics arm — the consumer OQ-4’s neighborhood was waiting for arrived. The converted
runtime already carried the entire receiving side: initMetrics seeds
/godebug/non-default-behavior/<name>:eventswith a compute0 placeholder for every non-Opaque entry, and the auto-convertedgodebug_registerMetric(metrics.cs) swaps in a real reader. The whole gap was godebug’s inert IncNonDefault plus cross-assembly reach: the linkname target isinternal, and internal.godebug (mirroring Go’s import constraint) had no runtime reference. Remedy is the registerPoolCleanup pattern’s third instance — a publicgodebugRegisterMetricshim in runtime/managed_impl.cs, a godebug→runtime ProjectReference (acyclic: internal/bisect already references runtime), and real per-name counters in the hand-own (ConcurrentDictionary keyed by name = Go’s shared-per-name cache semantics; once-per-name registration = nonDefaultOnce; Go’s unregistrable-setting panic preserved verbatim). The readMetricsManaged divergence note’s “the wiring waits for a consumer that demands it” is now half-answered: the godebug counters are wired; the heap-stat closures (⟨OQ-4⟩) remain exactly as stated. - Bisect arm — cmd/bisect drives the converted host end to end.
value#patternnow parses into a real convertedbisect.Newmatcher consulted per call in Value (Go’sv.bisect != nil && !v.bisect.Stack(&stderr)), reporting through a hand-writtenbisect.Writerover fd 2. TestCmdBisect passes FOR REAL: x/tools’ cmd/bisect (spawned viago run) runs the converted test-host binary through a whole bisect session — all-disabled probe, marker collection, subset narrowing — and converges on exactly the threeBISECT BUGlines the test’s own source marks. That is the first measured proof that captureCallers’ interned PC tokens are bisect-stable: bisect.stack hashes ASLR-normalized PC DELTAS (stk[i] -= stk[0]) and compares them across DOZENS of separate host processes, so same-site-same-hash and different-site-different-hash both held cross-process — the token model’s first-seen-order interning is deterministic for a deterministic execution prefix. Value is pinned[MethodImpl(NoInlining)]because its frame is part of the hashed surface (captureCallers’ own documented inline-hop reasoning); the identified residual — a JIT-tier flip inlining the small convertedbisect.Stackwrapper mid-session and re-hashing every site — did not materialize in the measured runs.
Gates: pipeline validated 5/5 (1 skipped identically on both sides), proof page emitted at
converter 773afa2c2; post-bank filtered sweep PASS 1 at 5 [85s], exit 0; check-roster-format
479 checks at 190 rows; zero compiler warnings in the changed files; package_info.cs untouched
(no stdlib-metadata regen owed); full go2cs.slnx built once after the runtime-API addition per
doctrine. Roster: 190/215 = 88.4% · 26,048 matching · 148 disclosed · honest 190/208 = 91.3%;
the new Windows-only row moves the Linux applicable denominator 188 → 189 (178 validated there,
unchanged).
internal/concurrent — re-measured at Go 1.23.12: closed-by-design STANDS, and gets stronger.
Conversion succeeds (the 2026-08-19 emitAutoConversionSiblings crash fix holds), the tests build
fails on exactly the banked single diagnostic — hashtriemap_test.cs(418,145): error CS0426:
The type name 'node<,>' does not exist in the type 'concurrent_package' (the line moved 406→418
with the 1.23.12 emission; a direct rebuild of the tests project confirms it is the ONLY
diagnostic) — so the comparison never runs and the 20 verdicts stay unmeasured. One NEW fact the
2026-08-19 entry did not name: the whitebox surface is not just the dead dumpMap/dumpNode
scaffolding. TestHashTrieMapBadHash LIVE-assigns the private keyHash field
(m.keyHash = func(_ unsafe.Pointer, _ uintptr) uintptr { return 0 }, hashtriemap_test.go:29)
to force total collisions — a live test writing the replaced implementation’s private hash hook.
A comparer-backed pluggable hash could honor that one truthfully, but dumpNode’s
node/indirect/entry types still have no truthful managed referent, so the compile wall
stands either way and the forbidden move (dead scaffolding for dead code) buys nothing. The row
stays inside the naive denominator awaiting its individual ruling, exactly as the exclusion
ledger’s candidates paragraph records. No artifacts banked.
unique classified at master: an HONEST NON-BANK — 7 of 20 measured, and a naming blocker that no disclosure can express (2026-09-01, coordinator lane)
Commissioned to classify unique’s residual toward a bank on the tracker’s reading
(“16/20, residual isolated pure, disclosure-shaped — GC-liveness territory”). The
classification came back non-bankable, on a blocker that is not a liveness question at
all. Same outcome shape as the same day’s os attempt, and for the same doctrinal reason: a
disclosure names a structural property of the deployment shape, never an
unimplemented-but-fixable defect.
Full pipeline at master 6aa91b0f6, Go 1.23.12, explicit -test-timeout 10m:
go2cs -tests -test-action all <GOROOT>\src\unique <worktree>\src\core\unique, exit 1.
The results-file tail carries NO action:"timeout" event — the run completed; the
mass-empty family is not in play and no shape inference was needed.
The arithmetic — and a correction to the tracker’s row
The C# host produced 20 terminal rows: 8 pass, 12 fail. Against Go’s 20, matching by name and verdict, 7 match:
| Set | Rows | State |
|---|---|---|
TestMakeCloneSeq + 6 of its 7 subtests |
7 | match (pass/pass) — the r41c makeCloneSeq root stays closed |
TestHandle 8 value subtests |
8 | Go pass / C# fail — checkMapsFor |
TestHandle parent |
1 | Go pass / C# fail (propagated) |
TestMakeClonesStrings |
1 | Go pass / C# fail |
testEface naming pairs |
3 logical (6 comparison rows) | one side has NO matching row |
The tracker’s “16/20” is contradicted, and the coincidence is diagnostic: 16 is exactly the
MISMATCH-entry count in the comparison’s own error string. The reading appears to be the
mismatch count transcribed as the match count. The last board-recorded figure before this was
4 of 19 (r43e), and that entry already named “the TypeFor/Name subtest-naming rows” as an
unresolved root — so the optimistic row was never backed by a measurement here. Corrected in
docs/phase4/TRACKER-100-percent.md with this run as the evidence.
Blocker A — the testEface naming divergence. NOT disclosable, on two independent grounds
type testEface any is emitted as global using testEface = object; — a C# using
alias, not a type. The Go name is ERASED at conversion, so reflect.TypeFor[T]().Name()
answers the empty string where Go answers testEface, and t.Run then names the subtest from
that empty string. Measured, both directions:
- C# emits
TestHandle//<nil>,TestHandle//"hello",TestMakeCloneSeq/#00(#00being Go’s own fallback for an emptyt.Runname). - Go emits
TestHandle/testEface/<nil>,TestHandle/testEface/"hello",TestMakeCloneSeq/testEface.
This is systemic, not a unique quirk: a census of the committed corpus finds 167
global using <Name> = object; sites — crypto.PublicKey, crypto.PrivateKey,
crypto.DecrypterOpts and their re-exports among them. Every named type whose underlying type
is the empty interface loses its name to reflect.
It cannot be disclosed:
- Doctrine. The CLR is perfectly capable of carrying a named type here; the erasure is an
EMISSION-MODEL choice (it is what makes Go’s universal assignability to
anyfall out of C# assignment for free). That is an unimplemented-but-fixable property of the converter, not a structural property of the deployment shape — the exact bar today’sosruling set. - Mechanism, and this one is decisive on its own. A disclosure pins a signature on a
FAILING NAMED ROW. Here the two sides do not share a row to pin: Go’s
.../testEface/<nil>has no C# counterpart and C#’s...//<nil>has no Go counterpart. The oracle matches by test name, so a name mismatch is unpinnable — the same mechanism gap thatDESIGN-object-lifetime-disclosure.md§3c records for the hang shape, met from a new direction.
Named remedy (owner of the row, not this lane): carry the Go name for a
named-empty-interface type into the emission so reflect can recover it, without losing implicit
assignability. Blast radius is the 167 alias sites; crypto’s key types make it a corpus-wide
reflect-fidelity question, not a test-naming nicety.
Blocker B — the 10 GC rows ARE codegen-liveness, measured, and disclosure-shaped once A is gone
The eight checkMapsFor failures all report Go’s own message
value <v> still referenced a handle (or tiny block?), and TestMakeClonesStrings reports
string was improperly retained. Both ask the collector to take an object while the asserting
frame is still running.
The converted test frame is faithful and the shape is the banked one: v0 and v1 are plain
locals of the subtest lambda; Handle<T> is a struct holding ж<T>; Value() is an
extension method taking that struct by value; and drainMaps + checkMapsFor run in the
SAME frame. By-value struct consumption materializes a caller temp that CoreCLR’s GC info
reports live for the whole method — verbatim the TestOnceXGC mechanism already banked in
src/core/sync/go2cs_test_disclosures.json.
Measured, not asserted — frame-residency A/B over the real internal/weak machinery,
ONE ARM PER PROCESS, four fresh processes per arm:
| Arm | What it does | Result |
|---|---|---|
static (positive control) |
box rooted by a static field | RETAINED 4/4 |
callee (positive control) |
box built in a NoInlining callee; only the weak Pointer returned |
RELEASED 4/4 |
frame (the measurement) |
box is a frame LOCAL, last use is a by-value consumption, runtime.GC() and the check in that same frame |
RETAINED 4/4 |
Both controls fire in their own direction, so the probe can observe retention AND release — it is not a gate that cannot go red. The conclusion is the platform-liveness lane’s, reached independently: frame residency is the whole mechanism.
This also answers the commissioning question about the weak/CWT machinery directly: there is
NO leak and NO named fixable remedy there. The callee arm releases 4/4 through
internal/weak’s WeakReference + ConditionalWeakTable canonical index and runtime.GC()’s
unique-cleanup arm — the machinery releases exactly when nothing roots the box. unique’s
intern map, weak.Pointer’s handle indirection and the cleanup wiring in
runtime/managed_impl.cs are all exonerated.
So the 10 rows are codegen-liveness (structural — the object CANNOT be collected while the
frame runs), not object-lifetime (temporal): they fail
DESIGN-object-lifetime-disclosure.md §2’s “genuinely unreachable at that point” clause,
which is precisely the sibling boundary that document’s ⟨OQ-L1⟩ ratified.
Disposition
unique does not bank. Its test artifacts were restored, not committed — the standing
outcome for this package, now for a rooted reason rather than a wall. All 11 dirt items
classified as known non-drift before restoring: one CRLF phantom (doc.cs, empty numstat),
one .cs.auto sibling, the init-forcing hook RELOCATING from handle.cs into
package_info.cs’s <ImportInitializers> block, the position-map funcLit argument (the 5th
closure shape), and the go2cs.SynthesizedStructs InternalsVisibleTo grant. No real drift.
The row is bankable the day Blocker A is fixed — at that point all 10 remaining rows are
codegen-liveness disclosures whose A/B evidence is the table above, and the arithmetic closes
at 20/20 with 10 disclosed. Nothing else stands in the way.
OPEN (2026-09-02, found by lane G-LAPTOP while building the receiver-snapshot family’s untested variants) — a VALUE-receiver method value over a POINTER-typed receiver expression emits the box where the value is wanted: CS1929, pre-existing, unreached in production
The shape. Go’s implicit deref through a field path: rh.p.label where p *frame and label
has a VALUE receiver is (*rh.p).label — what Go saves at method-value evaluation is the POINTEE’s
copy. The converter renders the wrapper over the pointer expression, so the generated extension method
(which takes the value) is offered a ж<frame>: error CS1929. Two errors on a three-arm probe.
Attribution, by the five-minute control (CLAUDE.md’s “revert the fix, build past the blocker”).
The error first appeared in a file commit 3 of the receiver-snapshot family had just rewritten, and
was about to be reported as that commit’s regression; the family-tip converter (commits 1+2 only)
reproduces the IDENTICAL two errors on the same program. The family neither caused nor cures it.
Commit 3 (claude/g-receiver-eval-once) carries a guard that SKIPS the hoist for this shape so it
does not rewrite emission it cannot render correctly (“do not hoist what you cannot render”).
Reach. Unreached in the production corpus (307/0 compiles at master); a converter GAP, not a live break. Any corpus site the commit-3 census instrument finds in this shape is counted here, never under the family’s diff.
Remedy (unassigned; sized cut wanted). The correct emission hoists the pointee COPY at evaluation
(var recvʗ1 = rh.p.Value;-shaped, i.e. the selector’s own auto-deref reproduced faithfully) and
binds the wrapper to it. Getting the auto-deref subtly wrong is how a receiver ends up aliasing the
wrong storage, which is why it was named rather than guessed at. Guard: a behavioral position with
a pointer-typed field receiver and a value-receiver method, mutation between creation and call,
output-compared against go run. Candidate owner: a coordinator sub-agent after the family closes.
2026-09-02 · Two Linux-axis findings from the never-measured sixteen: netip’s disclosure set is the managed allocation regime rather than the Windows host, and cgo state is a per-package build variable with a measured seven-member class (lane C1, cloud Linux)
1. net/netip’s 57 disclosures reproduce leaf-for-leaf on Linux — the zh-box arc owes ONE pass, not
two. The row validated at linux: 210 + 57 against a Windows 210 + 57: identical on both
numbers. Those 57 are the three AllocsPerRun families — TestNoAllocs, TestAddrStringAllocs,
TestParsePrefixAllocs — want-0/want-1 asserts Go satisfies by stack allocation and inlining where
the converted path measurably heap-allocates, each leaf pinned on its own counter line. Their
reproducing identically on a second OS says the set is a property of the managed allocation
regime, not of the Windows host or its toolchain.
What that changes for the arc’s owner: the netip harvest retires the same 57 leaves on both platforms when the reduction lands, so the arc owes one measurement pass and one bank, not a Windows pass plus a Linux re-derivation. If a future run finds the two platforms’ disclosed sets diverging, that is a real change in the allocation regime and not noise — the invariance is now a recorded baseline it can be measured against.
2. cgo state is a per-package variable that decides which PRODUCTION files exist, and the class has
exactly seven members at Go 1.23.12. The corpus’s emission state is CGO_ENABLED=0. A sweep
converting under CGO_ENABLED=1 therefore compiles a different source set than the committed tree
holds for any package whose file selection is cgo-conditional: declarations migrate between files, the
stale other-selection file remains, and the build dies on the duplicates — zero verdicts, and it
reads exactly like a converter regression.
Established by a one-variable A/B on os/user, same host, same tree, same row:
| arm | result |
|---|---|
CGO_ENABLED=1 |
FAIL in 12 s, zero verdicts, closure build dying; leaves cgo_unix_test.cs / cgo_user_test.cs behind, artifacts with no Windows counterpart |
CGO_ENABLED=0 |
validated at 12 — all agreeing, 0 disclosed, 0 withdrawn, a strict superset of the 5 banked Windows names, nothing absent |
The census, so the class is met once rather than one package at a time. Grepping the roster’s 199
applicable rows for //go:build lines mentioning cgo, split by production vs test:
| package | prod | test | consequence |
|---|---|---|---|
net |
16 | 1 | build failure — cgo_stub.go (unix && !cgo && !darwin) vs cgo_unix.go ((cgo && unix) \|\| darwin) are mutually exclusive; the corpus holds net/linux/cgo_stub.cs, so it is the cgo-OFF side. Measured FAIL at 183 s with cgo_stub.cs absent from the run’s own drift list — i.e. not re-emitted, because it was not selected. Needs the pin. |
os/user |
7 | 4 | build failure — pinned; validates at 12 |
plugin |
2 | 0 | converter crash — plugin_dlopen.go ((linux && cgo) \|\| …) is literal C (import "C", #include <dlfcn.h>); plugin_stubs.go (… \|\| !cgo) is pure Go, and the corpus holds plugin_stubs.cs. So the crash at conversionDriver.go:228 is a cgo-state artifact, not a converter defect — a candidate for the pin, and the reading of that row’s root should be corrected. UNMEASURED as of this entry; C1 runs the arm. |
crypto/internal/boring |
1 | 0 | inert — notboring.go’s constraint is a negated conjunction containing boringcrypto, already true when that tag is off, so cgo does not move its selection. Banked linux: 3, no pin. |
debug/pe |
0 | 1 | count only — file_cgo_test.go //go:build cgo holds TestDefaultLinkerDWARF / TestInternalLinkerDWARF / TestExternalLinkerDWARF, which ARE the row’s Linux surplus. linux: 13 vs Windows 10 is cgo-ON’s three extra tests, and they pass because on Linux both runtimes skip them in agreement (testDWARF skips when GOOS != "windows"). |
os/exec |
0 | 1 | count only — exec_linux_test.go linux && cgo; unmeasured (R2 docket) |
os/signal |
0 | 1 | count only — signal_cgo_test.go; banked linux: 29 + 2 |
The rule the class yields, and the trap in it: the count moves in BOTH directions, so neither cgo
state is the safe default. Pinning off fixes net and os/user and would reduce debug/pe from
13 to 10; a session-wide zero brings the three cgo-ON-derived rows (debug/buildinfo 204,
go/internal/gcimporter 582, go/internal/srcimporter 7) back short. The remedy is therefore a
per-package table ($cgoOffPackages beside $longTimeouts in run-validated-sweep.ps1, applied
around the converter invocation and restored in a finally), pinning what the corpus’s emission state
requires and leaving every other row alone. A test-only-conditional row’s annotation is only
meaningful beside the cgo state it was taken in, which is worth stating on the row rather than
assuming.
Carried into the runbook (docs/GoCorpusMigration.md §3.3) rather than left here, because a
version hop re-derives every row and would meet this on its first cgo-conditional package: the
existing precondition covers the Go side (a count discrepancy); this is the converted side (a build
failure), and the census is the cheap way to meet the class once.
TryMarshalAssignable’s named/unnamed unwrap arms are CORRECT — the “narrow the wrong-admit class” arc is refuted by a 70,071-admit census, and the whole wrong class is ONE known row (2026-09-02, lane R-LAPTOP, reflect tail)
The reflection bridge’s GoReflect.TryMarshalAssignable (golib GoReflect.ValueMarshalling.cs)
has two arms that admit a value across a Go named↔unnamed boundary: A-wrap constructs the value
into a named wrapper through its generated single-argument constructor, B-unwrap unwraps a named
wrapper’s m_value into its underlying-typed slot. Go’s ASSIGNMENT rule requires identical
underlying types with at least one side UNNAMED, and a standing question was whether these arms
admit the both-NAMED case they should refuse (a wrong admit would let type S string fill a
string slot, which Go’s assignment forbids — reflect.Value.MapIndex’s TestMap row asserts
exactly that panic and the bridge answered a miss instead).
The arc as framed — instrument the arms, classify every admit, narrow the wrong class in the
shared helper — is REFUTED, because the shared helper is also the CONVERSION path, and Go’s
Convert legally admits both-named (type A int → type B int is a valid conversion; the same
pair is not a valid assignment). An env-gated stderr trace on both arms, tagged with the arm and
the calling method two frames up, over a full reflect -tests run:
total admits captured 70,071
conversion path (TryConvertTo / TryByteSliceAs) 70,065 <- both-named is GO-LEGAL here
assignment path, correct-Go (one side unnamed) 5
assignment path, BOTH-NAMED and WRONG 1 <- the entire wrong class
The 70,065 conversion admits are dominated by six shapes at 10–20k each — the
TestArrayOfGC/ChanOfGC/MapOfGC… GC tests round-tripping through reflect.Value.Convert.
Narrowing the arm to Go’s assignment rule, as the arc directed, would refuse all 70,065 legal
conversions to fix one row. So the shared helper must NOT be touched.
The one wrong admit is B-unwrap from reflect.Value.MapIndex: TestMap_S → string — the same
row already on the reflect tail, left honestly red by the MapIndex key-order commit (e9cea1e3b,
2026-09-01), whose in-code note already records that the naive tightening is measured-wrong.
Disposition — the remedy is a per-ASSIGNMENT-caller gate, worth exactly one row
The fix belongs at the handful of ASSIGNMENT callers (MapIndex, and its Set/SetMapIndex
siblings — the sites that apply Go’s assignment relation, distinct from the Set{Int,…}/Convert
callers that apply conversion), NOT in the shared helper. Two constraints, both measured:
- NOT the bridge’s own
reflect.Type.AssignableTo— replacing theMapIndexgate withkey.Type().AssignableTo(keyDeclared)does NOT fix TestMap and DOES break TestArrayOfGenericAlg (reflect 48 → 49, 0 fixed / 1 broken; measured 2026-09-01). So the bridge’s AssignableTo is not a correct drop-in for this predicate. - NOT the conversion helper — that is the 70,065-admit path this census exists to protect.
The residual arc is therefore a CORRECT named/unnamed assignability predicate — one side genuinely
unnamed, where “unnamed” is the raw golib container / ж<T> box / delegate / array-of-CLR and a
predeclared type like string is NAMED (Go spec: “Predeclared types, defined types, and type
parameters are called named types”) — gated at the ~5 assignment callers. Worth exactly the one
TestMap row. Approved and owned by lane R (sizing-first); alternatively that row may be absorbed by
the typed-nil-func neighborhood if it reaches the same site first, in which case it is one row
reported superseded rather than cut twice.
Instrument note for the next reader: the trace lines concatenate under the test host’s stderr
capture, so a per-LINE parse reports ~60 where the ASCII-grep count is 70,071 — parse with a
whole-file findall, not per line, and the two reconcile. The instrument was reverted after the
run (golib byte-identical to master); this record is the deliverable.
FINDINGS (2026-09-02) — two shapes worth meeting before the next bridge or method-value arc
One rule, TWO minters — a bridge change can be green on the probe and wrong on the row.
reflect.rtype.Field(i) mints its struct-field descriptor through its own structFieldDescriptor,
beside abi.synthesizeStructType, whose header states the one-rule invariant both are supposed to
honor. A change that substitutes at one minter and not the other passes a probe aimed at the other
and fails the verdict — so a descriptor/bridge change enumerates the minters before it measures,
and either substitutes at both or states why one is out of scope.
The method-value family’s fourth face has TWO mechanisms, not one (4 of 17 sites red-first). M1 — the receiver EXPRESSION is deferred into the wrapper lambda, so any non-trivial expression at a lambda site re-executes per call; this is kind-independent. M2 — the root-ident snapshot aliases through a REFERENCE-semantics base (the value-receiver lambda path), which is a third axis the M1 predicate never reads. The pairing is the lesson: one commit needed two axes in its control where it varied one, and the next needed two mechanisms where it saw one — the same error in both directions. Both are covered by the evaluate-once ruling; recorded here so a future census names both before it counts.
2026-09-02 · net/http’s h2 write-deadline rows are a HOST-CAPACITY margin, not a Linux property — the managed TLS handshake bracket, and a falsifiable prediction for the Linux row (lane C1, cloud Linux, 4 cores)
The row’s own committed disclosure predicted this by name. net/http carries one manifest entry,
TestWriteDeadlineExtendedOnNewRequest/h2, class performance-margin, signature
TLS handshake error from. Its text brackets the managed handshake using the sibling test as the
instrument: TestWriteDeadlineEnforcedPerStream/h2 runs Go’s own tryTimeouts ladder, setting
Server.WriteTimeout to 125 ms, then 250 ms, then 500 ms on successive attempts, and on the reference
box 125 ms and 250 ms fail with that exact signature while 500 ms passes — bracketing the handshake
to (250 ms, 500 ms].
On a 4-core cloud container under the full 1,345-test suite, all three rungs fail — including the
500 ms one — so the handshake there exceeds 500 ms. The mechanism is Go’s own: conn.serve() arms the
server write deadline before calling Handshake, so WriteTimeout bounds the TLS handshake itself
rather than only post-handshake response writes. The client therefore loses Get #1, and the
write-deadline semantics these tests were written to check are never reached at all.
Three observations isolate it, and each had an available opposite reading:
/h1PASSES, only/h2fails, in both tests. h1 carries no TLS, so TLS is the variable rather than the deadline logic.- The failure is on Get #1, inside the handshake — so no semantic claim about write-deadline enforcement or extension can be drawn from these failures in either direction.
- Filtered on an idle box,
EnforcedPerStreamPASSES (-test-action compare, exit 0, the disclosed sibling absorbed asperformance-margin, nothing else divergent). That control is what turns “fails under load” from a hunch into a measurement: same host, same binary, load the only variable.
Arithmetic, corrected. The undisclosed shortfall is 2 verdicts, not 4: go=1345,
disclosed=2 (the Extended pair), leaving TestWriteDeadlineEnforcedPerStream and its /h2 as the
only undisclosed disagreements. An earlier posting of 4 counted the already-disclosed pair and is
withdrawn.
No disclosure was minted, deliberately, and this is the case where minting one would be wrong rather
than merely out of a lane’s authority. The reference bracket says 500 ms passes, so
EnforcedPerStream/h2 is not a cannot — it is a row this host is too small to run. Disclosing it
would convert a host-capacity limit into a permanent corpus claim, and it would destroy the instrument
the existing disclosure depends on, since that entry’s bracket is this test’s ladder.
PREDICTION, stated so a later run can falsify it: on any Linux host that keeps the managed h2
handshake inside 500 ms under full-suite load, net/http validates at linux: 1343 + 2 —
identical to its Windows columns, with no new disclosure and no change to the committed manifest. If a
larger-host run reports anything else, the difference is real and this entry is wrong. Until such a
run exists the row stays unannotated for Linux; a 4-core container is not evidence about the
corpus. Note for whoever takes it: the GitHub OS-matrix ubuntu runners are the same shape as the
container that produced this, so they cannot answer it.
The general form, which outlives this row: a timing-shaped divergence measured on a host smaller than the reference is a statement about the host until a control separates the two. The cheap control is the one used here — re-run the same tests filtered, on an idle box, and see whether they pass. Where the row’s own disclosure already carries a measured bracket, the bracket is the yardstick and the only question is which side of it the host sits on.
Amendment, 2026-09-02 (same lane, same day) — the prediction above is FALSIFIED, and the rung values it quotes do not exist
The entry states: “on any Linux host that keeps the managed h2 handshake inside 500 ms under full-suite
load, net/http validates at linux: 1343 + 2… If a larger-host run reports anything else, the
difference is real and this entry is wrong.” A larger-host run reported otherwise, so the entry is
wrong by its own terms. Measured on a 10-thread Linux host, solo, 572 s wall against a 40 m budget with
zero "action":"timeout" events in the record: 1341 matching + 2 disclosed + 2 UNDISCLOSED —
TestWriteDeadlineEnforcedPerStream and its /h2, the same two — with Go passing on that same host in
the same run as the native control. The row does not bank there either.
Second error, and it originates upstream of me but I propagated it. This entry quotes the retry ladder
as 125 ms, then 250 ms, then 500 ms, taken from the row’s committed performance-margin disclosure.
Go 1.23.12’s actual ladder is {250 * time.Millisecond, 500 * time.Millisecond, 1 * time.Second}
(net/http/serve_test.go:980). No rung of the quoted description exists in this release. I took the
prose as the record instead of re-deriving from source — precisely the failure CLAUDE.md names when it
says a finding’s description is not its record — and I did so while building a prediction on top of it.
The reference bracket (250 ms, 500 ms] appears to survive the correction (two failures then a pass,
with the rung values mis-transcribed one position down), but that is a reading of someone else’s
measurement, not a re-measurement, and it is flagged as such.
What the corrected ladder does to this entry’s own numbers: “all three rungs fail on the container, so the handshake exceeds 500 ms” should read exceeds 1 s. My conclusion was understated, not overstated.
Where the load hypothesis actually lands, stated more carefully than I stated it. It is not dead and it is not what I claimed. Two controls now exist and they do not point at host size:
| condition | host | result |
|---|---|---|
| full 1,345-test suite | 4 cores | all rungs fail |
| full 1,345-test suite | 10 cores | all rungs fail (250 / 500 / 1000) |
| the two tests FILTERED, idle box | 4 cores | passes |
So the divergence is concurrency-sensitive but not core-count-sensitive in the 4→10 range: under full-suite load the managed h2 handshake exceeds 1 s on both hosts measured, and adding cores did not buy a rung. My error was inferring “this host is too small” from “load makes the difference” — a real observation and the wrong conclusion drawn from it, since the filtered control only ever showed that load matters, never that capacity was the binding constraint.
The general form in this entry needs the same correction. It says a timing-shaped divergence measured on a host smaller than the reference “is a statement about the host until a control separates the two”, and offers the filtered re-run as the cheap control. The filtered control separates load from no load; it does not separate this host from a bigger host, and I used it as though it did. The control that answers host size is a bigger host, which is what falsified this. Keep the cheap control — it is still worth running first — but do not let a filtered pass license a prediction about hardware.
Still not minting a disclosure, and now for a firmer reason than the entry gave: two hosts and a native Go control on the larger of them say the converted h2 handshake does not clear 1 s under suite concurrency, which makes this a real converted-side property rather than a host limit — but establishing that is a root-cause question about the managed TLS handshake under load, not a disclosure to mint from this lane. The row stays unannotated for Linux.
2026-09-02 · net’s 73 unreported Linux verdicts have ONE root, and it is not the deadline: TestUnixgramServer/0 blocks the converted host indefinitely — proven by a 40 m/60 m A/B whose event streams are identical (lane C1, cloud Linux, 4 cores)
The measurement that settles it. The same net suite was run twice on one host, cgo-OFF, differing only
in -TestTimeout (40 m, then 60 m). The two event streams are identical:
| 40 m run | 60 m run | |
|---|---|---|
| tests started | 528 | 528 |
| terminal verdicts | 501 (383 pass / 63 skip / 55 fail) | 501 (383 pass / 63 skip / 55 fail) |
orphaned (run, never terminal) |
27 | 27 |
| last event in the stream | TestUnixgramServer/0 run |
TestUnixgramServer/0 run |
| tail event | elapsed 2400, package timeout after 00:40:00 |
elapsed 3600, package timeout after 01:00:00 |
Terminal-set symmetric difference: empty. Orphan-set symmetric difference: empty. Twenty extra minutes bought zero additional verdicts. A suite that is merely slow makes progress; this one does not, so the deadline is not the constraint and raising it further cannot help.
The block is a single test, and the stream says so positionally. TestUnixgramServer run is event
1029 of 1031 and TestUnixgramServer/0 run is event 1030 — nothing follows. The other 26 orphans
are not independent hangs: 24 carry t.Parallel() and are the parked parallel batch (the documented
two-phase shape — a serial-phase death leaves the whole parallel batch unreported), and the remaining
two, TestAllocs and TestUDPIPVersionReadMsg, each emit an explicit infrastructure-error and the
phase moves on past them. Go’s own side passes TestUnixgramServer and all three subtests.
It sits in a coherent unixgram cluster, which is what makes it a root rather than an oddity:
TestReadUnixgramWithUnnamedSocket go=pass C#=fail connection refused | read unixgram /tmp/.../sock: i/o timeout
TestUnixgramLinuxAbstractLongName go=pass C#=fail connection refused | read unixgram @abstract_test: i/o timeout
TestUnixgramConnLocalAndRemoteNames go=pass C#=fail got 0x7ef6ac05ed18; want 0x7ef6ac05f368
TestUnixgramServer/0 go=pass C#=(hang)
TestUnixgramAutobind go=pass C#=pass
Two of the three failures share one shape — a datagram send refused, then the peer’s read reaching its
deadline — and TestUnixgramServer/0 performs that same round trip. What it does NOT share is the
outcome, and that is the part worth stating precisely: packetTransponder (mockserver_test.go:440)
sets SetDeadline, SetReadDeadline and SetWriteDeadline to someTimeout before its ReadFrom,
so a conn honouring its deadline cannot block there indefinitely — it would error, send on the channel,
and close(ch) would end the test’s select loop. The hang therefore localises to either a deadline not
being honoured on that conn or a block earlier in the subtest (ListenPacket, newLocalServer,
buildup). I have not distinguished those, and this entry does not claim to.
Correction to my own earlier posting. I recorded net’s 40 m result as “needs more than 40 m on this
host class — a deadline datapoint for the ledger, not a divergence.” Both halves are wrong: it does not
need more time, and it is a divergence. The 60 m run is what falsified it.
Refinement to R’s Linux-frontier map, not a contradiction of it. R recorded 73 unreported; this host
reproduced 73 exactly (go 576 / C# 503), along with Buffers_WriteTo’s writev-9 to the digit. What is new
is that the 73 are not 73 problems. They are one hang plus its consequences: the serial tail
alphabetically at-or-after TestUnixgramServer (TestUnixgramWrite, TestUnixgramZeroByteBuffer,
TestUnixgramZeroBytePayload, TestZeroByteRead, …) and the 24 parked parallel parents with their
subtests. Closing one test therefore unblocks the whole tail, which changes what fixing it is worth by
an order of magnitude — the same re-pricing runtime/debug’s TestPanicOnFault needed.
FALSIFIABLE PREDICTION, and the run that tests it is already tasked. G is standing up WSL2 as the
fleet’s big Linux host and is to run net at 60 m. If the hang is a deadlock, that run stops at
TestUnixgramServer/0 with the same 501 terminal verdicts on a much larger box. If instead it runs
further, the block is contention on a 4-core container and this entry is wrong. Either way the answer is
one run away and needs no new instrument.
The general form. Two runs of one suite at different deadlines are a cheap and decisive instrument: if
the verdict sets are equal, the deadline is not the constraint and the tail is a block to be located, not
time to be bought. A deadline-killed run should be A/B’d against a longer one before its shortfall is
priced, because “needs a bigger timeout” and “hangs on one test” produce the identical tail event and the
identical NOT MEASURED verdict, and only the A/B separates them.
2026-09-02 · net/http’s h2 write-deadline rows are NOT a host-capacity margin — BOTH predictions falsified on a 10-core host, and the residual is managed-vs-native TLS handshake latency that scales with SINGLE-THREAD speed (lane G, G-LAPTOP WSL2, 10 cores, solo)
The entry above staffed this row on core count: the theory was that a 4-vCPU container under
1,345-test load could not hold the h2 write-deadline pair, and that a wider Linux host would clear it
at linux: 1343 + 2 with no new disclosure. That prediction is falsified, and so is the theory
behind it. Both are recorded here by name so the row is not re-staffed on either.
Measured
| host | G-LAPTOP WSL2, nproc 10, 16 GB, solo, Ubuntu 22.04.5, kernel 6.18.33.2 |
| toolchain | go 1.23.12 (GOROOT=/usr/local/go1.23.12, bare go version verified), .NET SDK 10.0.400, GOTOOLCHAIN=local, clone at master 3bbb04ca4 |
| wall | 572 s against a 40 m budget |
"action":"timeout" events |
0 — read FIRST; this is a real failure, not a deadline kill |
| go | 1345 entries — 1329 pass, 16 skip |
| C# | 1345 entries — 1325 pass, 16 skip, 4 fail |
| result | 1341 matching + 2 disclosed + 2 UNDISCLOSED (predicted: 1343 + 2) |
The 2 undisclosed are TestWriteDeadlineEnforcedPerStream and /h2 — the pair the entry above
predicted would PASS here. TestWriteDeadlineExtendedOnNewRequest + /h2 absorbed as
performance-margin exactly as expected.
The failure text, and the h1/h2 split
TestWriteDeadlineEnforcedPerStream/h2 fail elapsed=1.5619664
server log: http: TLS handshake error from 127.0.0.1:42840:
write tcp 127.0.0.1:43587->127.0.0.1:42840: i/o timeout
failed at 250000000 → retrying at 500000000
failed at 500000000 → retrying at 1000000000
failed at 1000000000 → all attempts failed
/h1 passes on BOTH tests (0.51 s, 0.38 s); only the TLS-bearing /h2 fails. That split is the
row’s own confirmation that the handshake — not the WriteTimeout mechanism generally — is what the
deadline cannot cover.
The control that makes this clean: GO PASSED ON THE SAME HOST, SAME RUN
The obvious confound on a WSL2 box is virtualized loopback inflating a 127.0.0.1 TLS handshake. It is exonerated without a new experiment, because the native control was already in the data: Go’s side passed every one of these tests on this host, this loopback, this run, completing inside the 250 ms rung. A network-stack explanation would have failed both sides. Parallelism is exonerated the same way — 10 cores, solo, a wider miss than the 4-vCPU container.
What remains is managed-vs-native TLS handshake latency, serial: (250 ms, 500 ms] on the i9 reference, > 1 s here. The consistent reading is single-thread speed — a mobile part versus a desktop — and TLS handshake work does not parallelise.
Correction to the committed disclosure’s ladder
The manifest entry describes tryTimeouts as 125 ms / 250 ms / 500 ms. Go 1.23.12’s actual ladder
(net/http/serve_test.go:980) is {250 ms, 500 ms, 1 s} — no rung of the stated description
exists in this release. The bracket conclusion survives (the author almost certainly mis-transcribed
the rung values one position down: two failures then a pass IS (250 ms, 500 ms]), and the
correction makes the host comparison rung-for-rung rather than an inference across two
differently-described ladders:
| host | 250 ms | 500 ms | 1 s | managed handshake |
|---|---|---|---|---|
| i9 (reference) | fail | pass | — | (250 ms, 500 ms] |
| G-LAPTOP WSL2, 10 cores, solo | fail | fail | fail | > 1 s |
Standing
No disclosure minted — the reference bracket still says 500 ms passes on a fast desktop, so this is a slow host, not a cannot. The Linux row stays UNANNOTATED. The host’s role is recorded honestly: right for wide rows and for rows a container deadline-kills, wrong for near-threshold serial-latency rows — do not route a timing row here on core count.
The instrument this owes: the gap is a number nobody has measured directly. A PerfTlsHandshake
row in src/tests/Performance (N loopback TLS 1.3 handshakes, in-memory cert, self-timed, compared
across Go / C# JIT / C# AOT) turns “the ladder fails” into a ratio measurable on every host in the
fleet, with the i9 — the fastest single thread available — as the natural reference. Sized before
cut, per the standing rule.
Amendment, 2026-09-02 (same lane, same day) — the dichotomy above is FALSE, and the root is simpler: someTimeout is ONE HOUR
The entry above says packetTransponder sets three deadlines before its ReadFrom, “so a conn honouring
its deadline cannot block there — it would error, send, and close(ch) would release the test’s select
loop”, and concludes the hang must be either a deadline not honoured or a block earlier in the subtest.
net’s own test constant is someTimeout = 1 * time.Hour (conn_test.go:18). A conn honouring that
deadline blocks for an hour, which is longer than any package deadline the pipeline has ever given this
package — so the hang needs no deadline defect to explain it, and the dichotomy the entry poses is not
a real fork. I read the helper and not the constant, and the correction is mine.
What a filtered diagnostic then established, with the test running ALONE on an idle container — which also removes contention as an explanation, the reading a 4-core box most needed to rule out:
- Both unixgram sockets are BOUND and open.
/proc/net/unixcarries both endpoints as Type 2 (SOCK_DGRAM) —/tmp/<n>/sockfor the server and the client — with live fds on the host process. SoListenPacket,newLocalServerandbuildupall completed: the “block earlier in the subtest” half of the dichotomy is eliminated by measurement, not by argument. - Both peers are parked in
ReadFrom. The twogoroutine-Nthreads and bothgo2cs test:threads sit infutex_do_wait;packetTransceiverreached itsReadFrom, which means itsWriteToreturned without error (an error would have gone downchand freed the select loop). - The netpoller is idle.
go2cs-netpollsits inep_pollwith nothing to deliver — no readiness pending on the registered descriptors.
So the root is the cluster’s root, not a separate one: a unixgram WriteTo reports success while the
peer’s ReadFrom never receives. TestReadUnixgramWithUnnamedSocket and TestUnixgramLinuxAbstractLongName
show the same failure with a short deadline and therefore FAIL (connection refused, then read
i/o timeout); TestUnixgramServer/0 shows it with a one-hour deadline and therefore HANGS. One defect,
two presentations, and the deadline value is the only thing that decides which.
That also re-prices the fix the right way round: this is not “a hang to unblock” plus “two failures to root” — it is one unixgram delivery defect worth three failing tests and 73 unreported verdicts.
Unproven and left unproven deliberately: whether the datagram reaches the kernel at all. ss is not
installed on this container and /proc/net/unix carries no queue depths, so I could not read the peer
socket’s receive queue while it hung. The idle netpoller is suggestive — a queued datagram should have
raised readiness — but that inference assumes those fds are registered with that epoll, which I did not
verify. A host with ss can settle it in one command during the hang, and that is the next measurement
rather than a conclusion drawn here.
Filtered control, run after the above and decisive on the contention question. TestUnixgramServer
alone, -test-filter, 6 m package deadline, nothing else on the box. The host’s entire record is four
events:
(pkg) run
TestUnixgramServer run
TestUnixgramServer/0 run
(pkg) timeout 360 "package timeout after 00:06:00"
Six minutes, zero progress past the run event, with the machine otherwise idle — while Go passes the
parent and all three subtests. Contention is eliminated as an explanation, which was the reading a
4-core container most needed ruled out, and the block reproduces identically at a third deadline (6 m
alongside 40 m and 60 m). Three deadlines, one result.
(Diagnostic only, per the standing rule — a -test-filter record never banks a row, and this one was
deleted rather than left for a later run to read as its own.)
Amendment, 2026-09-02 — the prediction is CONFIRMED on a 10-thread host, independently, and the 73 rows are verified as one root rather than argued into one
The entry above predicted: on a >4-core Linux host at 60 m, net stops at TestUnixgramServer/0 with
501 terminal verdicts and the same 27 orphans; if it instead completes, the block is contention on a small
container and this entry is wrong. A different lane ran it on a 10-thread WSL2 host, solo, cgo-OFF, and
the run deadline-killed at 60 m with the final run pair TestUnixgramServer → TestUnixgramServer/0
— the predicted test, named in advance.
| this container (4 cores) | the big host (10 threads) | |
|---|---|---|
| terminal verdicts | 501 | 506 |
| orphaned | 27 | 28 |
last run event |
TestUnixgramServer/0 |
TestUnixgramServer/0 |
| tail | timeout after 01:00:00 |
timeout after 01:00:00 |
Within one on each count, on two hosts differing by 2.5x in thread count. Contention is dead as an explanation — it was already ruled out here by the filtered single-test control, and a bigger box reproducing the same block at the same test settles it from the other direction.
And the one-root claim was VERIFIED there rather than inherited from this entry. The absent set on that
host is alphabetically EARLY (TestAcceptTimeout, TestCloseRead, TestConnClose…), which is not a
deadline tail’s shape — the shape heuristics alone would have read it as scattered and published dozens of
findings. Parsing GOROOT’s net/*_test.go gave 304 test functions of which 28 carry t.Parallel(); of the
41 absent top-level names, 28 are exactly that parallel set with zero of them reporting, and the
remaining 13 are a contiguous U→W alphabetical tail beginning exactly at the block. Serial phase dies at
TestUnixgramServer/0, takes the 13 serial tests after it, and the parallel batch never starts.
41 names / 73 verdict rows / ONE root, derived independently.
That is the difference between one finding and seventy-three, and it is the two-phase rule doing precisely what it was written for.
What the confirmation does NOT settle, and is worth keeping separate: the unixgram delivery defect
itself is still un-rooted below the observation that a WriteTo reports success while the peer’s
ReadFrom never receives. The queue-depth question this entry left open (ss -xa during the hang) remains
the next cheap measurement, and that host has ss.
Amendment, 2026-09-02 — WriteTo DOES error. A 60-line reproducer falsifies this entry’s “reports success” claim, and the corrected reading points AT the sockaddr seam where the old one pointed away from it
The two amendments above rest on a sentence this one retracts: “a unixgram WriteTo reports success
while the peer’s ReadFrom never receives”. It does not report success. Reduced to two sockets in a
temp directory, one WriteTo, one ReadFrom under a 1-second deadline — short on purpose, since
someTimeout = 1 * time.Hour is the only reason the upstream failure presents as a hang rather than a
verdict:
GO write: n=19 err=<nil>
read: n=19 payload="UNIXGRAM ROUND TRIP" peer_nil=false
C# write: n=0 err=write unixgram …/cli.sock->…/srv.sock: sendto: connection refused
read: FAILED err=read unixgram …/srv.sock: i/o timeout
Three runs each, byte-identical every time; the converted build is clean with zero strict errors. No test framework, no sweep, no deadline arithmetic.
How the wrong claim got published, because the mechanism is the reusable part. It was an INFERENCE
dressed as an observation: packetTransceiver reaches its ReadFrom, therefore its WriteTo must have
returned nil. The host’s event stream carries no event for a write’s return value, so I never saw it —
and the entry should have said “not observed” instead of asserting the opposite. A /proc inspection and
a filtered control both agreed with everything around that sentence, which is exactly why it survived:
surrounding measurements corroborate the frame, not the unmeasured claim inside it.
The hang survives the correction and is now mechanical rather than inferred, which is why this
sharpens the entry instead of overturning it. The write fails, so packetTransceiver sends on trch and
returns, closing it. packetTransponder is parked in ReadFrom for an hour because nothing arrives, so
tpch never closes — and the test’s loop is for trch != nil || tpch != nil (server_test.go:308),
waiting for BOTH. One channel closes, the other cannot, and the host sits until the package deadline. The
73 rows follow from that with no lost datagram required.
Two independent measurements now converge on the same boundary. A sibling lane’s ss -xa during the
hang reports both sockets at Recv-Q 0 / Send-Q 0 — the datagram reaches no queue; this reproducer
reports ECONNREFUSED from sendto, n=0 — it is never accepted. The send does not leave, and the
kernel rejects it rather than dropping it.
That relocates the suspect. ECONNREFUSED on sendto to a bound, existing unixgram socket is the
kernel objecting to the DESTINATION — i.e. the sockaddr encoder. net/linux/fd_unix.cs:21 pins
writeToSyscallName = "sendto", and Sendto sits in the Linux sockaddr seam’s own
deliberately-not-covered list beside Sendmsg, behind an evidence gate whose stated rule is “fix a
censused wrapper when a suite REACHES it”. The “silently lost” reading pointed away from that seam; the
measured one points into it.
Still labelled a hypothesis: that this and syscall’s TestPassFD / TestSCMCredentials EISCONN
pair share one root. Both are unix-domain sends whose destination is handed to an uncovered wrapper and
both fail at the kernel boundary — but ECONNREFUSED and EISCONN are different rejections, and no one has
yet read what sendto writes for a SockaddrUnix. That read is one function and it is the next step.
The reproducer is not committed. It would be a known-failing behavioral test today, and the suite’s value is that it is green; it lands as a guard when the seam closes.
2026-09-02 · reflect crash-residue item 2 BANKED, and the MakeFunc/Variadic family SPLIT so nobody re-derives it (lane R-LAPTOP, reflect tail)
Two rows off the crash residue, banked on claude/reflect-tail-r-lite (rebased onto master 21f7c9677):
- unsafeslice /
TestSliceAt— DISCLOSED, not green (coordinator ruling 2026-09-02, token-unification REFUSED). The SliceAt hand-own aliases the pointer’s memory faithfully —(ж<T>)(uintptr)precovers the managed box the reflect projection handed out,@unsafe.Slice<T>windows it — and ports Go runtime.unsafeslice’s three panics; len/cap/nil and every panic agree with Go. The ONE unrepresentable assertion iss.Pointer() == uintptr(&p[0]): reflect’sValue.Pointer()is a STORAGE-IDENTITY token (HashCode.Combine(backing, low), kept stable for encoding/json’s cycle detector and internal/fmtsort), whileuintptr(unsafe.Pointer(&p[0]))is the pinned data address — two managed projections of one storage that coincide only for native memory. Classruntime-capability, in reflect’s manifest. - MakeFunc-variadic /
TestMakeFuncVariadic— GREEN. The refusal was mechanical, not fundamental: a Go variadic lowers to a delegate with aparams Span<T>tail an expression tree cannot carry, but a LAMBDA can. The make-trampoline family (GoReflect.MakeVariadicDelegate.cs, 0..8 fixed × Action/Func) is the reverse ofInvokeVariadic’s call family;CompileGoFuncFactoryroutes a byref-like tail to it.
THE SPLIT — do not re-derive it. The other five MakeFunc/Variadic reds sit on DISTINCT roots and are NOT moved by the two above (crash frames measured 2026-09-02, e0dcdb4f5, train-4-stable):
TestMakeFuncStackCopy—reflect: call of reflect.Value.Type on zero Value(abiTypeSlow), a zero-Value handling issue in the call path.TestMakeFuncValidReturnAssignments—reflect: call of MakeFunc with non-Func type(a different MakeFunc guard, not the variadic path).TestReflectMakeFuncCallABI— an ABI value mismatch (27-verdict family, the largest single mismatch; the tuple-nesting >7 arm is a separate makefunc_impl concern).TestVariadicMethodValue— variadic METHOD type (not MakeFunc).TestVariadicType— a nil-deref in the test body (a type-construction issue, not MakeFunc).
Reports received to this ledger (recorded, NOT re-derived):
- C2 (item 3,
30347fbd45):TestStructOfTooLarge→ GREEN — the StructOfnint-vs-uintptrsize accumulator; the root sat one layer below the missing panics and fixing it corrected three other call sites. First green out of the StructOf synthesis arc; C2 does not claim the reflect number. - C2 (gcbits):
TestGCBitson theNewAt/ptrTowall. - G (StructOf-embedded trio): row 3 GO after the freeze; rows 1–2 are the synthesis root’s (C2’s arc).
The 53× RSA gap is NOT arithmetic — math/bits intrinsics measured, TWO NULLS (G, 2026-09-02)
The finding outranks the cut that produced it. Reached from net/http’s h2 write-deadline pair
(itself a Debug-build margin, see the pipeline-configuration ruling); this is the residual underneath.
What was measured (Release + DOTNET_TieredCompilation=0, one host, sequential, distinct records)
| measurement | Go | converted | ratio |
|---|---|---|---|
| TLS 1.3 handshake, steady-state median | 2.59 ms | 57.87 ms | 22× |
| TLS 1.3 handshake, first in process | 3.73 ms | 1.1530 s | — |
| RSA-2048 PSS signature | 0.834 ms | 44.5 ms (WSL) / 64.6 ms (Windows) | 53× / 77× |
The signature is 79% of the handshake residual — direct attribution, since a TLS 1.3 handshake performs exactly ONE server CertificateVerify signature.
The hypothesis, and why it was WRONG
Go intrinsifies the whole math/bits family (Mul64→MULQ, OnesCount64→POPCNT,
LeadingZeros64→LZCNT, RotateLeft64→ROL, ReverseBytes64→BSWAP) and additionally aliases
math/big’s own mulWW to the Mul64 intrinsic (ssa.go:5113→:5022), while declaring
addMulVVW bodyless in arith_decl.go (hand-written assembly). go2cs necessarily emits the portable
fallbacks. The natural conclusion — that this emulation is the 53× — is false.
Cut and measured (claude/g-mathbits-intrinsics 1de4445dc, kept as a negative-result branch):
sixteen functions hand-owned onto Math.BigMul / BitOperations / BinaryPrimitives / UInt128.
- Workload A/B — RSA-2048 signature: NULL. 64.59 ms before, 64.65 ms after. Instrument proven wired: the after assembly contains
BigMul/BitOperations/BinaryPrimitivesand the before assembly contains none (checker positive-controlled againstMul64/bits_package, after a firststrings-based scan returned a false zero for names that MUST be present). - Primitive micro-probe — the intrinsics ARE faster, and it does not matter:
| primitive | Go | before | after | speedup | after ÷ Go |
|---|---|---|---|---|---|
Mul64 |
0.474 ns | 5.759 ns | 3.025 ns | 1.90× | 6.4× |
OnesCount64 |
0.230 ns | 5.177 ns | 2.908 ns | 1.78× | 12.6× |
RotateLeft64 |
0.740 ns | 4.179 ns | 2.618 ns | 1.60× | 3.5× |
Add64 |
0.506 ns | 4.883 ns | 4.814 ns | 1.01× TIED | 9.5× |
- Hash-path A/B —
hash/maphashat Release+TC0: NULL. 353 s before, 340 s after (arm walls 355.3 / 344.9 s) — -3.7%, against a 20% bank threshold; 22/22 PASS on both arms. This was the one workload the RSA A/B could not see (RotateLeft,OnesCount,ReverseBytes), and the 1.6-1.8x primitive gains are swamped there exactly as the 1.90xMul64gain was in RSA.
An RSA-2048 CRT modexp performs ~5×10⁵ word multiplies; at 5.76→3.03 ns that is ~1.4 ms saved against a 64.6 ms signature — ~2%, inside the arms’ own spread. That is the whole explanation of the null.
What it means for the seam hunt — do not re-walk these
math/bitsis NOT the seam. Correct cut, measured zero at workload level.math/big’saddMulVVWis probably not either — it is the same arithmetic-level fix to a cost that is not arithmetic.- The residual is the emission’s value/slice plumbing. Even after the cut,
Mul64costs 6.4× Go for what is ONEmulinstruction on both sides. That factor is call/return across thebits_packageboundary, the tuple return, andslice<Word>indexing with bounds checks — not the operation. A single-instruction operation costing 6.4× in the emission’s calling convention is the number that hypothesis now has. Add64/Sub64gain nothing fromUInt128— the JIT does not lower it better than Go’s bit-algebra reconstruction. Anyone revisiting this should drop those two first.
Correctness note (the cut is right, just not useful)
math/bits 26/26 and math/big 224/224 both unmoved with the sixteen replacements live —
real evidence the mappings are semantically correct at Go’s edges (LeadingZeros64(0)==64,
Len64(0)==0, RotateLeft64 with negative k).
Eliminated by measurement, so nobody re-walks them
Platform (Windows ≡ WSL to within ~1 ms on the same silicon), certificate/key type (httptest and
PerfTlsHandshake embed the SAME RSA-2048 testcert), session resumption (PerfTlsHandshake sets no
ClientSessionCache; Go’s default is nil), .NET ThreadPool starvation (golib/builtin.cs:79 —
goroutines get dedicated threads). ⚠ And the ~21% non-signature remainder is not nistec: TLS
1.3’s default key share is X25519 (handshake_client.go:153), so that segment is
crypto/ecdh → crypto/internal/edwards25519/field plus the record layer and parsing.
addMulVVW as emitted is 13.1× a raw loop — the seam is CALL GRANULARITY, apportioned (G, 2026-09-02)
Follows the math/bits two-null block above. That cut proved the arithmetic is not the 53× RSA gap;
this measures what is. math/big’s addMulVVW inner loop — the innermost loop of Montgomery
multiplication, i.e. of every RSA private-key operation — 32 words × 200 k reps, Release +
DOTNET_TieredCompilation=0, same data and an observed sink for every variant so none is dead code.
Two runs, spread under 2%.
Variants — the table names what each one REMOVES, and the attributions are DERIVED from it
| variant | container | multiply | assembly | attr | ns/word | vs B | |
|---|---|---|---|---|---|---|---|
| F FULL emitted | slice<Word> |
bits.Mul |
cross | — | 24.15 – 24.35 | 13.1× | |
| A emitted | slice<nuint> |
bits.Mul |
cross | — | 22.31 – 22.38 | 12.1× | |
| C | Span<ulong> |
bits.Mul |
cross | — | 14.23 – 14.34 | 7.7× | |
| E-CROSS | slice<nuint> |
bits.Mul |
cross | yes | 10.96 – 11.24 | 6.0× | |
| G | slice<nuint> |
local copy | same | — | 5.36 – 5.38 | 2.9× | |
| H | slice<nuint> |
local copy | same | yes | 10.95 – 11.06 | 5.9× | (UntypedInt branch) |
| E | slice<nuint> |
local copy | same | yes | 4.01 – 4.05 | 2.2× | |
| D | slice<nuint> |
Math.BigMul |
— | — | 2.72 – 2.77 | 1.5× | |
| B raw | Span<ulong> |
Math.BigMul |
— | — | 1.85 – 1.86 | 1× |
Word is math/big’s own generated [GoType("num:nuint")] struct, copied verbatim from its
Generated/ output — not a reproduction.
Apportionment — every pair below differs in EXACTLY ONE thing
UntypedInt `UintSize` branch H / E 2.72 - 2.73x <- DOMINANT
AggressiveInlining, same-asm G / E 1.32 - 1.42x
golib slice vs Span D / B 1.47 - 1.50x
Word generated-struct wrapper F / A 1.08 - 1.09x <- nearly free
assembly boundary H vs E-CROSS WITHIN NOISE -> ~1.0x
The seam is the EMITTED BODY, and the assembly boundary is not part of it. H (same assembly)
and E-CROSS (cross assembly) carry the identical body and the identical attribute and read
10.95–11.06 against 10.96–12.31 — the same number. A JIT that inlines identical IL emits
identical machine code whichever assembly it came from, and that is what the measurement shows.
Two mechanisms, both in the body:
UintSize == 32is a struct comparison, evaluated per call — 2.72×.bits.cs:21emitspublic static UntypedInt UintSize => 64;— a property returning the generatedUntypedIntstruct, whoseoperator ==isleft.Equals(right)over a privateComparethe JIT compiles standalone at IL 141 and never inlines. Go folds this branch at compile time; the emission evaluates it on everybits.Muland everybits.Add. This is a converter-level property of untyped constants, not ofmath/bits— any emittedUntypedIntcompared against a literal in a hot path pays it.- IL size over the inlining budget — 1.32–1.42×.
bits_package:MulandAddare 83 and 87 IL bytes from the two-level chain, tuples and conversions, so the JIT declines by default;AggressiveInliningoverrides it.
⚠ ~1.5× of A remains unapportioned and is named rather than absorbed: 4.02 × 2.72 × 1.38 = 15.1
against a measured A of 22.4. The likely reading is compounding — an un-inlined call whose body
itself contains a non-inlined UntypedInt.Compare pays both, and the two do not multiply cleanly. It
is not claimed as boundary cost.
⚠ CORRECTED BEFORE MERGE — what this block said first, and why it was wrong
This block originally read “cross-assembly boundary A/G 4.16–4.17× ← DOMINANT” and concluded “the assembly boundary is the seam.” That was wrong, and the number was an artifact of the instrument.
The tell was raised by the coordinator: E (4.0 ns/word) and E-CROSS (11.1) are the same code with
the same attribute on the same four methods, and a JIT cannot produce 2.75× from provenance alone. Two
falsifiers settled it — the assemblies were all optimized (IsJITOptimizerDisabled=False read
in-process, so not a Debug-callee artifact), and the JIT summary showed the cross-assembly chain was
inlined under the attribute (bits_package absent from the arm’s compile list entirely). The gap was
my hand-written G/E copies using const int UintSizeLocal = 64 where the emission uses the
UntypedInt property. Variant H restores that one difference and reproduces E-CROSS exactly.
The failure is the same class the block already warned about — A/G differed on TWO axes, assembly
AND body — committed by the person who wrote the removes-column rule one section earlier. Kept
visible rather than quietly replaced, because a retracted number with its cause is worth more than a
corrected one without.
A same-assembly copy with no attribute at all is already 4.2× faster than the emitted form.
E’s prediction, written BEFORE the run and scored
“E improves substantially but does NOT reach B: I predict 2–6× of B (3.7–11.4 ns/word).”
Both E arms landed inside it — same-assembly 2.2×, cross-assembly 6.0×. The stated mechanism also
held: inlining removed call overhead but the tuple materialisation and nuint↔uint64 conversions at
two levels survive, which is the 2.2× floor.
What each remedy candidate is now worth| candidate | measured | converter change? ||—|–:|—|| one-level word-size hand-own (Mul/Add/Sub → a single BCL call each) | ~3.7× (removes the 2.72× branch AND the 1.38× decline together) | no — a hand-own || AggressiveInlining alone, body unchanged | 1.32 – 1.42× | no || converter intrinsic table, emit at the site | not needed for most of the above | yes || additionally fix slice + tuples (raw ceiling) | 12.1× | golib + emission |⚠ The one-level hand-own is the level the WITHDRAWN math/bits cut did NOT register — it registeredMul64/Add64/Sub64 and not the word-size Mul/Add/Sub that math/big actually calls. Sothat cut left the two-level chain and the UintSize branch standing, which explains its 0.0% farbetter than “intrinsics do not help”.
⚠ Two labelling corrections, recorded because they are the failure mode here
- The first run printed the slice and call attributions swapped (variant C removes the slice and keeps the call; D does the opposite). Numbers right, labels backwards — it would have pointed the design item at golib’s slice instead of the call boundary.
C/Gwas then quoted as “cross-assembly boundary” while differing in container AND assembly. The clean pair is A/G, and the true factor is 4.17×, not 2.7×.
Both survive review by looking self-consistent, which is why the table above names what each variant REMOVES and the attribution lines are derived from that column rather than typed beside it.
Method
Instrument is a scratch console (CConv, plus CConvE against a scratch core/math/bits copy whose
Mul/Mul64/Add/Add64 carry the attribute — never the corpus). Call depth was checked first
and is two (bits.Mul → Mul64, bits.Add → Add64, each materialising and destructuring a
ValueTuple), so the attribute went on all four levels; on the outer pair alone it would have measured
the wrong thing. Both consoles reference golib and math/bits by ABSOLUTE path, so the
per-configuration $(go2csPath) Release trap cannot reach them. Neither is proposed for banking.
2026-09-02 · EnvironBlockWalk’s golden is per-GOOS and only Windows can satisfy it — a behavioural-corpus instance of the L3 alias question, stated as a finding with no fix (lane C1, cloud Linux)
The observation, from a Linux CNR at master. EnvironBlockWalk/package_info.cs is one of twelve
files a Linux CNR reports as CHANGED, and it is the only one of the twelve that is informative: the
other eleven belong to the six platform-exclusive packages whose byte-identical check CNR itself
declares vacuous. This one type-checks, converts fully, and still differs — so it is a real,
standing, per-GOOS golden difference rather than an artifact of an unmeasurable package.
What differs, and why. The committed golden carries three imported aliases:
18: global using syscallꓸHandle = go.syscall_package.ΔHandle;
19: global using syscallꓸSignal = go.syscall_package.ΔSignal;
20: global using syscallꓸSockaddr = go.syscall_package.ΔSockaddr;
and syscall’s own per-GOOS declarations are split:
GoTypeAlias windows linux darwin
Handle 1 0 0
Signal 1 1 1
Sockaddr 1 0 0
A Linux conversion re-derives the imported-alias section from the linux flavour and therefore emits
only syscallꓸSignal, dropping the two that flavour never declares. The golden was minted on
Windows, where all three exist. Neither emission is wrong for its own flavour; the golden simply
encodes one of them.
Why it is NOT platform-exclusive, which is the load-bearing distinction. The program uses
syscall.Environ and syscall.Getenv — both platform-neutral. It compiles and runs on every
flavour, so it is correctly absent from F8’s gating set (six windows-native packages), and marking it
platform-exclusive to silence the diff would be false: it would suppress a package that genuinely
works everywhere.
Same shape as the withdrawn L3 alias arc, arriving through the behavioural corpus instead of the
stdlib. That arc was withdrawn when a differential showed the Linux emission re-derives the section
wholesale rather than merging a windows-seeded alias forward — measured on syscall’s flat
package_test_info.cs. This is that same mechanism seen in a behavioural golden, and it is why the
census the arc produced was kept even though its code was not: the shape is real, it simply is not
the defect that arc’s predicate targeted.
NO FIX PROPOSED, deliberately. Three shapes exist and each has a real cost: mint the golden per-GOOS (a golden set per flavour, and every future behavioural golden inherits the question); make the goldens alias-insensitive (weakens a byte-exact comparison to silence one line); or accept that a Linux CNR reports one CHANGED file and diff it against this named baseline (free, and what the lane does today). Choosing between them is a goldens-strategy decision, not a lane call, and nothing is blocked meanwhile — the diff is one file, named here, and reproducible from any Linux CNR at master.
The general form. A per-GOOS golden difference in a package that type-checks everywhere is NOT the same class as a platform-exclusive package, and the two must not be conflated: the first is a golden that encodes one flavour’s emission, the second is code one flavour cannot compile. F8’s marker answers the second and must not be reached for to silence the first.
2026-09-02 · The Linux-parity residual is FIVE rows and FOUR unrelated roots — the state of each, so the next reader starts from it rather than re-deriving (lane C1, cloud Linux)
Master 64a064098 carries Linux: 194 of 199 applicable. The five unannotated rows are not a
backlog of one kind; they are four unrelated roots plus one row waiting on another lane’s arc. Stated
per-row because the whole cost of this block is that the next lane does not re-derive it.
| row | state | root | owner |
|---|---|---|---|
runtime/debug |
CUT, banks 4 + 6 |
TestPanicOnFault takes the process down (mmaps PROT_READ, writes, expects SIGSEGV→panic; no SEH equivalent on the CLR) |
C1 — rebases after train 11 |
syscall |
13 mints banked, 14th approved | the posix_spawn seam, five refused SysProcAttr fields |
C1 |
net |
ROOTED, not open | RawSockaddrUnix 24 managed bytes vs the kernel’s 110 |
C2 (sockaddr seam) |
net/http |
measured; needs a bigger host | managed h2 TLS handshake vs WriteTimeout under load |
bank on G’s WSL |
internal/poll |
open, blocked | waits on the typed-nil widening arm | whichever lane takes that root |
runtime/debug — cut, not pending. TestPanicOnFault is worth NINE verdicts rather than one: the
host reports a single verdict (TestFreeOSMemory, first alphabetically) and tests 2..10 are a
contiguous alphabetical tail of absences, because the process dies. Excluded through the host-fatal
class — withdrawn from BOTH sides by name and COUNTED in disclosed — the row derives 4 + 6 from its
own comparison record, matching its Windows columns plus the new entry.
syscall — the residue is three roots, not one row. The thirteen platform-skip mints and the
approved fourteenth (TestExecPtrace, host-limit) all discharge ONE property: the posix_spawn seam
refusing Credential, Cloneflags, Unshareflags, Chroot, Ptrace — the same property os/exec’s
TestCredentialNoSetGroups discloses, reached from a second package. What remains after them is
four rows across three unrelated roots: runtime_BeforeExec unimplemented (TestExec), the
EISCONN pair (TestPassFD, TestSCMCredentials — the sockaddr seam, C2’s), and Setegid/Seteuid
answering “operation not supported” (TestSetuidEtc).
net — rooted, and it is one defect, not 73. The 73 unreported verdicts collapse to a single
unixgram delivery failure: sendto returns ECONNREFUSED because the encoder hands the kernel a
managed RawSockaddrUnix whose Path is a 16-byte reference where sun_path[108] is expected. The
apparent “hang” is the same defect with a one-hour deadline (someTimeout) instead of a short one.
Two independent measurements agree: zero queue depth on both sockets during the block, and
ECONNREFUSED with n=0 from a 60-line reproducer.
net/http — a host limit, not a Linux property. The row’s own committed performance-margin
disclosure brackets the managed TLS handshake to (250 ms, 500 ms] on the reference box using the
sibling test’s tryTimeouts ladder as the instrument. A 4-core container misses the 500 ms rung under
the full 1,345-test suite and PASSES the same tests filtered on an idle box — so the shortfall is
load. No disclosure was minted, deliberately: doing so would convert a host-capacity limit into a
permanent corpus claim AND destroy the instrument the existing entry depends on.
The general form, which is why this is a block rather than a list. “Unannotated” is a property of the ROSTER, not a diagnosis: of five such rows here, one is cut and waiting on a train, one is rooted and reassigned by root, one is a host limit needing no code at all, one is blocked on another arc, and only the residue of the fifth is genuinely open work. A residual counted by rows over-states the work remaining by a factor of several, and counting it by ROOTS is what made the C1/C2 split resolvable by evidence rather than by territory.
2026-09-02 · The struct-passing seam restated at its TRUE size: the CLR AUTO-LAYOUTS any struct holding a reference, so the kernel reads the WRONG FIELD — not a wrong-width one (lane C2, cloud Linux; offsets measured, not reasoned)
The class was under-described for as long as it has been known, and the under-description sends people
to the wrong fix. Every prior statement of it — including the one in this lane’s own train-11 commit —
said some version of “one word where four bytes belong”: a ж<T> or array<T> field is 8 or 16 bytes of
object reference where the kernel wants inline storage, so the fields after it are pushed along. That
is a WIDTH story, and a width story invites a width remedy: widen the field, mirror the declared order,
count the bytes again. It is wrong. A C# struct containing an object reference does not get sequential
layout at all — the CLR is free to reorder its fields, and it does. The kernel does not read a
too-narrow version of the field the code assigned; it reads a different field.
Measured with Unsafe.ByteOffset against the converted types themselves, GoTargetOS=linux, x64:
Msghdr SizeOf=80 native=56
Name managed= 0 native= 0
Namelen managed= 40 native= 8 <-- MOVED
Pad_cgo_0 managed= 48 native= 12 <-- MOVED
Iov managed= 8 native= 16 <-- MOVED
Iovlen managed= 24 native= 24
Control managed= 16 native= 32 <-- MOVED
Controllen managed= 32 native= 40 <-- MOVED
Flags managed= 44 native= 48 <-- MOVED
Pad_cgo_1 managed= 64 native= 52 <-- MOVED
RawSockaddrUnix SizeOf=24 native=110
Family managed= 0 native= 0
Path managed= 8 native= 2 <-- MOVED
RawSockaddrInet4 SizeOf=40 native=16
Family managed= 0 native= 0
Port managed= 2 native= 2
Addr managed= 8 native= 4 <-- MOVED
Zero managed= 24 native= 8 <-- MOVED
Iovec SizeOf=16 native=16
Base managed= 0 native= 0
Len managed= 8 native= 8
Msghdr is the worked example, and it explains two errnos that had no explanation. Of nine fields,
only Name (0) and Iovlen (24) land where the kernel expects them. At native offset 8 — where the
kernel reads msg_namelen — sits Iov, an object reference: always non-zero on a live message, and
huge. A non-zero msg_namelen on an established unix STREAM socket is EISCONN by definition
(unix_stream_sendmsg); on the DATAGRAM path the same non-zero namelen sends the kernel to validate an
“address” at msg_name — the NativeBox<byte>(0) heap object a nil to produces — and that is
EINVAL. Both were observed at master, three runs each, on a reduction of Go’s own
TestSCMCredentials; both were previously attributed to the nil-name conversion alone, which cannot be
the whole story because the C# code does assign Namelen = 0 — at managed offset 40, which the kernel
reads as part of msg_controllen.
Iovec is the converse and is the sharper half of the finding. Every offset correct, SizeOf
correct — and still unusable, because Base is a managed reference: the kernel reads a heap address
where a data address belongs. Right place, wrong value. So “the offsets check out” is not a
clearance, and neither is “the size matches”.
RawSockaddrInet4 is the confirmation, derived twice, months apart, by different instruments. The
train-8 Sendto arc instrumented the generated body and dumped the sixteen bytes it hands the kernel:
02 00 AE 54 00 00 00 00 30 04 4A 68 ED 7F 00 00
family port ADDRESS the reference sitting where Zero belongs
Family and port right, four ZERO bytes at native offset 4, a heap pointer at native offset 8. That is
exactly what Addr managed=8, Zero managed=24 predicts, and the byte dump was taken before this probe
existed. Two independent derivations agreeing is what makes the offsets safe to build on.
Why RawSockaddrUnix announced itself where RawSockaddrInet4 hid. Both are equally broken. Inet4’s
damage was invisible on loopback because a destination of 0.0.0.0 means “this host” to Linux, so a
loopback datagram arrived anyway (every non-loopback destination went silently elsewhere — the silent
variant). Unix’s sun_path starts at native offset 2, where the managed struct has the low half of
Family plus padding, and the path bytes are 6 bytes further on behind a heap pointer; the path names
nothing that exists, so sendto answers ECONNREFUSED immediately. The loud one was found first, which
is an accident of address family, not a difference in severity.
DOCTRINE (accumulator 179). The remedy for this class is encode into a native buffer (what
writeNativeSockaddr and NativeMsghdr do) or an explicit-layout blittable mirror — never a managed
struct handed to the kernel by address, and never a width adjustment to the declared struct. Two
corollaries the measurements force:
- A struct with no reference fields is not automatically safe and a struct with correct offsets is not
safe either — check the field CONTENTS as well (
Iovec). - Do not reason about a managed layout; measure it.
Unsafe.ByteOffsetover the converted type is four lines and settles it; two of the three structs above had a documented mechanism that was wrong in the detail that decides the remedy.
Reproducing it. A console project referencing core/golib and the package under test, built with
-p:GoTargetOS=linux -p:go2csPath=<repo>/src/; for each field,
Unsafe.ByteOffset(ref Unsafe.As<TStruct,byte>(ref s), ref Unsafe.As<TField,byte>(ref s.Field)), printed
against the native offsets from the platform headers. Nothing is mutated and nothing is written to the
corpus, so it is safe to run against any tree.
Where it was found: attributing C1’s two Linux roots of the sockaddr seam (RawSockaddrUnix
ECONNREFUSED — closed at master by train 8’s Sendto, attributed by a three-arm A/B; the
EISCONN/EINVAL pair — closed by train 11’s SendmsgN). C1’s handover reported RawSockaddrUnix as
“24 managed bytes where the kernel wants 110, Path a 16-byte reference instead of 108 inline bytes”:
the byte counts were right, the mechanism was the width story, and C1 has recorded the correction from
their side. The lane’s own train-11 commit message carried the same error and is corrected in
cc38f0082e.
2026-09-02 — C1 — syscall Linux residue: the two roots assigned to C1, rooted end to end (one is a two-line no-op; the other is not a defect at all but a branch choice, and the choice is measurably wrong)
Root 1 was cut, measured, and WITHDRAWN — the empty bodies FORK BOMB the row (96 processes in
~7 minutes). Root 2 lands on cgocaller, which the sibling hand-own’s own header parks as “the cgo
boundary, a separate question”, so it is posted for a ruling rather than taken. Branch
claude/c1-syscall-exec-hooks is at c2d140db5 and is comment-only against master —
47 added lines, zero behavior, diff-verified. be61a6fa1 (the code half) must not be seated.
Root 1 — runtime_BeforeExec / runtime_AfterExec (TestExec): scheduler bookkeeping with no managed counterpart, exactly the banked entersyscall/exitsyscall judgment
Symptom. System.NotImplementedException: runtime_BeforeExec: external (assembly or cgo)
function is not implemented — PartialStubGenerator.cs:111’s verbatim text, thrown on the first
syscall.Exec.
Chain, each link read rather than assumed.
| link | evidence |
|---|---|
| declaration | src/core/syscall/linux/exec_unix.cs:247,249 — bodyless internal static partial void, from exec_unix.go’s two //go:linkname pulls |
| callers | exec_unix.cs:275 (before execve) and :302 (after a failed execve) — the ONLY two, both inside Exec |
| no body ⟹ stub | PartialStubGenerator emits the throwing stub for a bodyless partial |
| Go’s body | $GOROOT/src/runtime/proc.go:4992 = execLock.lock() + a darwin/ios-only pendingPreemptSignals drain; :5008 = execLock.unlock() |
Why nothing is the right body — and it is stronger than “unnecessary”. execLock has exactly
two readers in the whole runtime, both read out of the 1.23.12 source:
proc.go:2839/2844—newm1/newosproc, i.e. Go’s own OS-thread creation (// Prevent process clone, issue #19546).signal_unix.go:372/389—preemptM, and that reader isif GOOS == "darwin" || GOOS == "ios", i.e. unreachable on linux and part of async-preemption machinery the managed model never runs.
So on linux the write-lock’s sole purpose is to serialize against newosproc. A converted program
has no newosproc: threads are created by the CLR, which does not consult runtime_package. The
lock is therefore not merely unneeded here — it could not serialize anything even if it were
taken, which is the honest reason rather than a convenience. The darwin drain reads
pendingPreemptSignals, a counter only preemptM increments, so it is unreachable twice over.
The post-condition is the same: Exec replaces the process image via SYS_EXECVE, so no runtime
bookkeeping survives to be left inconsistent; and on the failure path AfterExec releases a lock
that was never taken.
Both are func(). Nothing is computed, so there is no plausible answer to fabricate — the same
argument syscall_linux_impl.cs’s header already records for runtime_entersyscall /
runtime_exitsyscall, and for the same class of obligation.
Placement is forced by the facts, not chosen. exec_unix.cs exists per-GOOS in linux/ and
darwin/. The linux copy is ALREADY a whole-file hand-own ([module: go.GoManualConversion],
line 34 — the posix_spawn seam), so the bodies belong in it, 28 lines above their caller. The
darwin copy carries zero markers — it is converted output a -stdlib reconvert would clobber
— so darwin keeps its throwing stub and is unchanged by this cut. (Darwin has no run layer;
FINDING-darwin-run-layer.md owns that.) A flat file is impossible: the declaration does not
exist on Windows, so a flat implementing part is CS0759.
“Why not forward?” — asked, because runtime DOES carry a real converted body, and answered.
runtime/linux/proc.cs:4983 holds syscall_runtime_BeforeExec/AfterExec fully converted
(execLock.@lock()/.unlock(), darwin drain and all), pushed here by runtime’s
//go:linkname syscall_runtime_BeforeExec syscall.runtime_BeforeExec. Two things stand between
that and a forwarder, and only the second is a judgment:
- Mechanical. The converter emits a linkname target
publiconly for rows in the curatedlinknameForwardTargetswhitelist (visitFuncDecl.go:1877). This pair is not in it, so the runtime body isinternaland unreachable across the assembly boundary. Adding the row is a converter change, hence a ruling, not a lane cut. (The forwarder direction is fine —syscall → runtime, an edge Go’s own imports already carry, so no W1-style graph cycle. I checked that specifically, because the push shape is what produced W1’s six.) - Substantive. It would buy nothing. The lock’s only linux reader is
newosproc, which this model never calls, so a forwarded lock/unlock pair guards nothing while running the runtime’s mutex protocol at exec time — risk for no protection. Empty body and forward are semantically identical here; they stop being identical only if the managed runtime ever routes thread creation throughnewm1, and that is the fact to re-check before preferring one.
Recorded in the source comment too, so it is not rediscovered as an oversight. If the coordinator prefers the forward on faithfulness grounds, the registry row is the whole change and this cut retires cleanly.
Size: two bodies plus the rationale, one already-hand-owned file, no converter change, no registration, no other platform touched.
…and then the measurement said no, so it is withdrawn
The argument above is sound and I still hold it. The row still did not improve — it fork bombed:
96 syscall.tests processes in ~7 minutes, ~1 per 3 s, each a CHILD of the last. That chain is
itself the proof execve did not replace the image, since execve keeps the pid. Three sampled
generations carried garbage /proc/<pid>/cmdline and empty /proc/<pid>/environ; a fourth was an
ordinary spawn from TestDeathSignal, so UNFILTERED suites were running. Positive control
exonerating the run filter: the host honors -test.run=^TestZeroSysProcAttr$ and runs it alone.
The reading, labelled as the INFERENCE it is: Exec hands execve MANAGED memory —
argv0p/argvp/envvp from BytePtrFromString/SlicePtrFromStrings, passed as
(uintptr)@unsafe.Pointer.FromRef(ref (Ꮡ(argvp, 0)).Value), a **byte into the managed heap. The
exec’d image comes up with corrupted argv and environ, loses -test.run and
GO_WANT_HELPER_PROCESS, runs the whole suite including TestExec, and spawns the next
generation. That is the open “wrapper passes managed memory by address” class this board already
tracks, reached through a new door — and this one is on Linux.
So the throwing stub is, accidentally, the recursion brake, and one honest infrastructure-error
beats a fork bomb on every host that sweeps the row. Next step recorded at the site rather than in
a plan: marshal Exec’s argv/envp into UNMANAGED memory BEFORE re-attempting the bodies — the
same file already does exactly that for the posix_spawn seam
(MarshalStringZ/MarshalStringVector/FreeStringVector), under its header’s rule that every
buffer handed to a native call lives in unmanaged memory for the duration and is freed in a
finally.
Root 2 — Setegid/Seteuid “operation not supported”: the banked ENOTSUP is correct for AllThreadsSyscall and wrong for these callers, because Go offers them a second implementation the managed host is a better fit for
Symptom. TestSetuidEtc: [0] "Setegid(1)" failed: operation not supported.
Chain. Setegid(1) → cgo_libc_setegid == nil (no cgo layer in a converted binary) →
AllThreadsSyscall(SYS_SETRESGID, minus1, 1, minus1) (syscall_linux.cs:1103) →
runtime_doAllThreadsSyscall → the banked hand-own’s deliberate ENOTSUP
(syscall_linux_impl.cs:96) → errnoErr(95) → zerrors_linux_amd64.cs:1611 [95] = "operation
not supported" → t.Errorf("[%d] %q failed: %v"). Every link read; the string is the errno’s,
not a paraphrase.
This is not a stub and not a bug. runtime_doAllThreadsSyscall’s ENOTSUP is banked with its
rationale, and the rationale is right about its own function: a managed host owns threads Go’s
runtime never sees, which is precisely the condition under which Go itself answers ENOTSUP, and
that answer is what turned TestAllThreadsSyscallSignals from an infrastructure-error into the
same skip Go takes.
What the rooting adds is that these nine callers never had to reach it. Read from the 1.23.12
source: all nine linux credential setters — Setegid, Seteuid, Setgid, Setuid,
Setgroups, Setregid, Setreuid, Setresgid, Setresuid — carry TWO implementations and pick
by whether cgo is linked:
if cgo_libc_setegid == nil {
if _, _, e1 := AllThreadsSyscall(SYS_SETRESGID, minus1, uintptr(egid), minus1); e1 != 0 { … }
} else if ret := cgocaller(cgo_libc_setegid, uintptr(egid)); ret != 0 { … }
A cgo Go binary passes TestSetuidEtc — it never sees ENOTSUP, because Go deliberately routes
to libc’s setegid, which glibc implements with the nptl setxid broadcast that reaches every
thread, foreign ones included. The ENOTSUP branch is Go’s answer for the non-cgo runtime whose
stop-the-world replay is the only broadcast it has.
So the converted program takes the ENOTSUP branch for one reason only: cgo_libc_setegid is nil.
And the argument the hand-own makes — the managed host has foreign threads the runtime cannot
broadcast over — is the argument for it being the cgo case, which is the branch that WORKS.
The corpus is one branch away from the right answer and is taking the other one.
The recommendation’s own measurement — libc’s broadcast is real, and it lands on Go’s expected string
The case for (a) rests on a claim it would be cheap to assert and wrong to: libc’s setegid
reaches threads the caller did not create. Measured on this host rather than believed
(a lane-local SetxidProbe — a thread parks, main calls setegid(1), both read their
own /proc/self/task/<tid>/status):
BEFORE (uid=0):
main tid=…766 Gid: 0 0 0 0
thread tid=…767 Gid: 0 0 0 0
AFTER setegid(1) returned 0:
main tid=…766 Gid: 0 1 0 1
thread tid=…767 Gid: 0 1 0 1
The parked thread moved. And 0 1 0 1 is byte-for-byte what TestSetuidEtc compares against
for that row ({call: "Setegid(1)", filter: "Gid:", expect: "\t0\t1\t0\t1"}), so this is not
“a different error” — it is the passing value.
Scope, stated because it is the one place this could fail: glibc’s setxid broadcast walks its
OWN thread list, i.e. every pthread_create thread. .NET’s Linux threads are pthreads, so CLR
threads are on that list; a thread made by a bare clone(2) behind glibc’s back would not be, and
nothing in the converted corpus makes one. The probe’s parked thread is exactly the shape in
question: foreign to Go’s runtime, ordinary to glibc.
Why this is a ruling and not a cut. Reaching that branch means cgocaller and the nine
cgo_libc_* pointers, and syscall_linux_impl.cs’s header explicitly parks cgocaller as a
separate question. Two shapes, and the existing ruling already prefers one:
- (a) implement
cgocalleragainst the libc keystone and populate the nine pointers. This is the shapeFINDING-linux-run-layer.mdOQ-1 already ruled — “bind libc, one keystone P/Invoke, not N per-call hand-owns” — and the keystone exists (internal/runtime/syscall/linux/ syscall_linux_impl.cs,[LibraryImport("libc")]). - (b) nine hand-own bodies P/Invoking libc directly — the per-call shape that ruling rejected.
I have not built either. What I can say from measurement is that the branch is the defect, not the ENOTSUP, and that the fix’s blast radius is nine functions and one test.
Go’s own test guards make the split explicit, and this is the strongest evidence in the block.
All three tests that call AllThreadsSyscall directly open by skipping on ENOTSUP:
if _, _, err := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_KEEPCAPS, 0, 0); err == syscall.ENOTSUP {
t.Skip("AllThreadsSyscall disabled with cgo")
}
TestAllThreadsSyscall :304, TestAllThreadsSyscallError :593, TestAllThreadsSyscallBlockedSyscall
:606 — and those three skip correctly on both runtimes today, which is exactly what the banked
hand-own bought.
TestSetuidEtc has no ENOTSUP guard, and its only skips are non-root, the swarming builder, and
alpine (:524-535). Go did not forget it: in Go, ENOTSUP is a legitimate answer for the raw API and
an impossible one for the nine wrappers, because whichever branch a build takes the wrappers WORK
— cgo routes them to libc, non-cgo to a runtime broadcast that really happens. The converted
corpus returns it for both because they share a bottom the nine were never meant to reach on a host
with foreign threads.
None of the three guards is disturbed by fixing the branch: they call the raw API, which keeps answering ENOTSUP.
Host state, checked so the row is not read as a skip: id -u = 0, /etc/alpine-release
absent, USER unset — none of TestSetuidEtc’s three skip guards fires, so the full 20-entry
table runs on both sides.
The measurement — the rooting’s own control, stated before the run and then run
Prediction, written before the sweep: if AllThreadsSyscall→ENOTSUP is the root, then every
TestSetuidEtc entry fails with the identical string, not the two named, because all of them call
one of those nine. If only some fail, this rooting is wrong.
Run: run-validated-sweep.ps1 -Filter syscall -Exact -TestTimeout 30m under the pinned wrapper
(gate printed go version go1.23.12 linux/amd64, PIN VERIFIED), at master 64a064098, 287 s.
Record preserved before the restore.
Confirmed, and I was off by one in the count — the table is 21 entries, not 20 (indices [0]
through [20]), and all 21 fail:
[0] "Setegid(1)" failed: operation not supported
[1] "Setegid(0)" failed: operation not supported
[2] "Seteuid(1)" failed: operation not supported
[3] "Setuid(0)" failed: operation not supported
[4] "Setgid(1)" failed: operation not supported
…
[18] "Setresuid(1,0,2)" failed: operation not supported
[19] "Setresuid(0,2,1)" failed: operation not supported
[20] "Setresuid(0,0,0)" failed: operation not supported
Nine distinct functions, twenty-one calls, one string. Go: pass.
And the contrast is in the SAME record, which is the part worth keeping. The three tests that
call the raw API report go=skip cs=skip — agreeing — on the C# side via
"AllThreadsSyscall disabled with cgo", reached through the hand-own’s ENOTSUP:
| test | go | c# |
|---|---|---|
TestAllThreadsSyscall |
skip | skip |
TestAllThreadsSyscallError |
skip | skip |
TestAllThreadsSyscallBlockedSyscall |
skip | skip |
TestSetuidEtc |
pass | fail |
One ENOTSUP, one record: it makes three tests agree and the fourth diverge. That is the whole finding in four rows — the answer is right for the API and wrong for the wrappers, because Go routes the wrappers around it precisely when the host is the shape this one is.
Root 1’s verdict came back with its own stack, and it exonerates everything around it:
INFRASTRUCTURE-ERROR TestExecHelper — System.NotImplementedException:
runtime_BeforeExec: external (assembly or cgo) function is not implemented
at go.syscall_package.runtime_BeforeExec() … PartialStubGenerator/…runtime_BeforeExec.3.stub.g.cs:18
at go.syscall_package.Exec(…) … src/core/syscall/linux/exec_unix.cs:275
at go.syscall_test_package.TestExecHelper(…) … src/core/syscall/exec_unix_test.cs:374
Read the frames: the PARENT spawned the child successfully (the posix_spawn seam works), the child
entered TestExecHelper, and the throw is on the first line of Exec that isn’t argument
marshalling. The stub is the only thing between this test and a pass — nothing else in the exec
path is implicated.
Residue arithmetic, and what it means for the row
55 rows both sides. 37 agreeing + 13 disclosed (the banked posix_spawn platform-skip set) + 5
errors = 55 — it closes, so nothing is unaccounted for.
| remaining error | owner |
|---|---|
TestExec |
root 1, C1 — cut, measured, WITHDRAWN (see below); the stub is the recursion brake |
TestSetuidEtc |
root 2, C1 — posted for a ruling, not cut |
TestExecPtrace |
the approved 14th mint (host-limit), staged |
TestPassFD |
C2 (the SCM_RIGHTS seam) |
TestSCMCredentials |
C2 (the SCM_RIGHTS seam) |
The 5 → 4 fall the 14th mint was expected to produce is confirmed as available: the residue is
exactly the five named, and TestExecPtrace is one of them.
– C1
2026-09-02 · HOST QUALIFICATION — i9’s Go-side bogo runner FLAKES: seven crypto/tls cases failed on the ORACLE with the converted side clean, and the ruled reading is “not a row finding” (lane i9, coordinator ruling same day)
Recorded here because the run it came from is otherwise indistinguishable from a converted-code
regression: the sweep prints FAIL crypto/tls, the row’s status is "failing" and matched is
false, and none of that is about the conversion.
The run. Run 3 of the three-run standard applied to the row’s earlier host death — quiet box
(nothing else running), Release + DOTNET_TieredCompilation=0, 2026-09-02 20:48:04–20:54:59Z, 414 s.
go entries 3644 / csharp entries 3644 identical to the passing run 2
environment { configuration: Release, tiered: false,
oracleGoVersion: go version go1.23.12 windows/amd64 }
status "failing" matched false
errors: ORACLE-side (Go=fail C#=pass) 7
CONVERTED-side (Go=pass C#=fail) 0
process-level 2
disclosed: TestCertCache (codegen-liveness) -- absorbed correctly
The seven, named, so a later sighting can be compared rather than re-derived:
TestBogoSuite Go=fail C#=pass
TestBogoSuite/Downgrade-TLS10-Client Go=fail C#=pass
TestBogoSuite/Downgrade-TLS12-Client Go=fail C#=pass
TestBogoSuite/MinimumVersion-Client-TLS11-TLS1-TLS Go=fail C#=pass
TestBogoSuite/MinimumVersion-Client-TLS13-TLS12-TLS Go=fail C#=pass
TestBogoSuite/MinimumVersion-Client2-TLS13-TLS11-TLS Go=fail C#=pass
TestBogoSuite/WrongMessageType-TLS13-ServerHello-TLS Go=fail C#=pass
Six version-negotiation / downgrade cases plus the parent. Every one failed on Go’s own bogo runner while the converted side passed, which by construction cannot be converted-code drift: the converted code produced the right answer and the oracle did not.
The ruling (COORD, 2026-09-02), because the shape invites the wrong three reactions. A run whose
failure set is entirely Go=fail / C#=pass with zero converted-side failures is a run in which
the ORACLE failed to produce a reference — the os/user oracle-side shape arriving as a FLAKE rather
than a deterministic host limit. Therefore:
- The row’s bank verdict is taken from a run with a CLEAN oracle. Run 2 (3,644 / 3,644,
matched: true, sweep exit 0) is exactly that. - This is evidence about the HOST’s oracle, recorded here with the cases and the date. It is never a disclosure on the row and never a converted-side finding.
_roster.ps1grows no fourth arm. The three proven host states (full count / capability-absent / host-limit) are unchanged; this is not a fourth one.- What the SWEEP owes is routed as its own cut (coordinator’s, not this lane’s): when a row’s failure set is oracle-only, re-run the row ONCE before failing it, and on a second oracle-only result fail it as oracle unstable on this host — the three-run standard applied to the oracle side, so a Go flake can never silently fail a clean converted row. Until that lands, a lane meeting this shape re-runs by hand and cites this entry.
The near-miss that makes it worth a board entry. The sweep line reads FAIL crypto/tls [414s]
and the child reports exit status 1; that was read as the earlier access violation recurring and
almost written up as a second crash. It is not — exit status 1 is go test’s ordinary “tests
failed”, a crash is 0xc0000005 / 3221225477, and this record carries zero crash signatures and
no deadline event in either spelling. Read the failure MODE before writing the word “crash”: the
crash-signature grep and the results tail are one command each and they decide it.
2026-09-02 · OPEN, and NOT a row property — a net/http A/B carried 390 unreported verdicts that the census’s own clean run of the same row, same host, same configuration did NOT (lane i9)
Banked so the next lane meeting this does not spend the run I spent proving it is not what it looks
like. It looks like a Release regression in net/http. It is not.
The pair. Both at Release, same host, same converter, corpus restored from HEAD before each:
| run | context | unreported (Go="pass" C#="") |
errors |
|---|---|---|---|
census net/http |
in-shard, behind other packages (WARM), 174 s | 0 | 2 — both TestRegisterErr |
| A/B arm A | row alone, go2cs_test_manifest.json deleted first (COLD), 352 s |
390 | 398 |
| A/B arm B | as arm A but -TestTiered, 358 s |
390 | 396 |
The two A/B arms’ unreported NAME SETS are byte-identical (0 differing lines), and neither record
carries a timeout event in either spelling; both hosts exited at ~355 s against a 600 s wall. So it
is not a deadline kill, not slowness, and not tiering — it is deterministic and identical across a
varied axis.
The census’s record settles what it is not. i9-shard1-moved-rows/net.http.comparison.json is the
same row at the same configuration: Release, tiered: false on the same host, and it reports all
1,343 verdicts with exactly two errors. A row that reports everything in one run and parks 390 in the
next is not exhibiting a property of the row, the configuration, or the conversion.
What differs, and what is NOT asserted. The census run was warm and in-shard; the A/B arms each
deleted the manifest and paid a full rebuild (the 174 s → 352 s doubling). That is the only difference
identified, and the mechanism is unrooted — a cold rebuild has no obvious path to 390 unreported
verdicts, and the standing shape heuristics do not settle it either (no deadline event to read; the
set is neither a clean alphabetical tail nor plainly the t.Parallel() set). It is recorded as an
open observation, not a diagnosis.
Two rules it re-earns. State cold-vs-warm when comparing two runs — it is the only variable that
separated these three. And preserve a failed row’s comparison record before any restore: the census
record is the entire reason this could be classified at all, and it was nearly missed because a
find … | head truncated it out of view and the absence was read as a fact. A filtered view answers
a different question than the one asked.
2026-09-02 — coordinator sub-agent — the 215 re-derived from go list std: net/http/pprof was in NO accounting, one ledger row is OUTSIDE the denominator it is subtracted from, and the recorded 216 − testing derivation disagrees with today’s by exactly that pair
A read-only census over the worktree found four record contradictions in the roster’s own
arithmetic. Every number below was re-derived from primary sources for this entry rather than
taken from the census: go list std with GOROOT pinned explicitly to the go1.23.12 root
(go version reports the binary’s build stamp, not the root it resolves — an ambient GOROOT
pointing at a go1.23.1 installation resolved package directories under that root while
go version still printed go1.23.12, so the pin is stated, not assumed), a func Test regex over
every *_test.go in each package’s GOROOT directory, and a .csproj probe under src/core.
The six numbers
| # | Measure | Value | How |
|---|---|---|---|
| 1 | go list std, windows/amd64, go1.23.12 |
306 | GOROOT pinned; go env GOROOT echoed back before the run |
| 2 | …carrying ≥1 func Test in GOROOT sources |
219 | tag-INDEPENDENT glob over *_test.go; the regex admits a bare func Test(t *testing.T), which internal/diff — a banked row — declares, so the stricter ^func Test[A-Z] form would manufacture a banked-not-testable contradiction |
| 3 | …and converted (a production .csproj under src/core) |
215 | reproduces the roster header’s denominator exactly |
| 4 | banked roster rows | 201 | parsed with the roster’s own committed row regex; Tests column sums to 27,734 and Disclosed to 154, both matching the header to the digit |
| 5 | remaining | 14 | 215 − 201 |
| 6 | 14 = 3 lane-owned + 5 ledger-inside-215 + 6 unowned | closes | see below |
The four packages separating 219 from 215 — embed/internal/embedtest,
internal/coverage/test, net/internal/cgotest, runtime/internal/wasitest — are GOROOT
directories with zero non-test .go files and no .csproj: there is no production package for
a host to reference. Only embedtest carries a ruling (2026-08-11). The other three have no board
row of any kind.
Positive controls on the func Test count: strings 68, unicode/utf8 14, sort 29, cmp 4,
internal/diff 1 — each a banked row whose count is consistent with its record. Negatives:
unsafe 0, internal/goarch 0.
The 14, by disposition
- 5 are ledger rows inside the 215 —
internal/syscall/unix,net/internal/socktest,log/syslog,runtime/race,internal/unsafeheader. - 3 are lane-owned —
reflect(R-LAPTOP),runtime(i9),unique(coordinator sub-agent, via the type-name carrier arc; the row is not independently dispatchable). - 6 have no lane named in any dated record, with their last recorded state:
| Package | func Test |
Last recorded state (source) |
|---|---|---|
os |
225 | 682 / 686. Two implementable blockers, neither disclosable: the NetShareAdd byte-buffer fork (“queued, next free full lane”) and the WriteStringAlloc machinery-alloc arc (ElemRefBox (T[],nint) increment queued). — tracker, re-derived 2026-09-02 |
testing |
59 | Option 1 ruled, sequenced post-wave (see below). No verdict count has ever been recorded on this board. |
runtime/pprof |
45 | 37 requested, 15 reached, then the crash — 1 pass, 10 fail, 3 infrastructure-error, 1 skip. Reclassified a capability frontier, “sequenced LAST in the Windows chase queue, possibly the release’s honest disclosure story”. — Fold #3, 2026-08-29 (R) |
net/http/pprof |
4 | 5 of 15. “TestHandlers fails and seven of its subtests infrastructure-error … TestDeltaProfile skips where Go passes. Profile collection has no managed body — sibling of runtime/pprof’s and runtime/trace’s stubs” — Scout batch 2, 2026-08-14 (lane B), under “Four packages measured for the FIRST time” |
runtime/trace |
2 | 0 of 2. “NotImplementedException: getg … Both tests enter the tracer through getg; no managed body exists.” — 2026-08-14; restated the same day in the “no re-measure owed until a relevant capability lands” list. ⚠ That condition has arguably fired: the getg module-init no-op equivalence landed at 65b6dd5ba. Whether it reaches this path is unmeasured — 2 verdicts, the cheapest probe on the list. |
crypto/internal/boring/bcache |
1 | 0 of 1. “NotImplementedException: registerCache … a PartialStubGenerator stub reached from Register[K,V]” — Scout batch 2, 2026-08-14. Still unruled, so the roster keeps it inside the naive denominator. |
Finding 1 — net/http/pprof appeared in NO accounting
It is a converted package (src/core/net/http/pprof/net.http.pprof.csproj, pprof.cs,
package_info.cs), it is inside the 215, it declares four func Test, and it was measured 5 of
15 on this board. It appears in no roster row, no exclusion-ledger row, and not in the
tracker’s list of remaining rows, which names eight. Before the correction below,
grep 'http/pprof' docs/ValidatedTestPackages.md returned nothing — positive control on the
grep: net/http returned 10 hits in the same file, so the empty was absence, not a broken probe.
The implementable remainder is therefore nine, not eight: the tracker’s eight plus this row.
201 + 9 + 5 = 215. The roster’s Excluded packages section now derives the 215 from go list std
and names all fourteen, this row among them.
Finding 2 — one ledger row is OUTSIDE the naive denominator, so 215 − 6 subtracts a non-member
internal/runtime/syscall is not in go list std on windows/amd64 at all — build constraints
exclude every file, which is exactly what its own E1 mechanism says (“there is not even a package to
convert”). It therefore cannot be a member of a set derived from that listing. Five of the six
exclusions are inside the 215; strictly, the Windows-axis implementable set is 210 and the ratio
is 201 / 210 — 95.7%, where the header states 209 and 96.2%.
This board asserted the opposite on 2026-08-09 (the r56a breadth harvest, “the twelve rooted
non-validators”): “internal/runtime/syscall … Joins net/internal/socktest,
internal/syscall/unix, log/syslog and runtime/race: in the naive 215 denominator, cannot
bank on this target.” The first four of that group are in the 215 (their sources define tests
that Go’s constraints then deselect — the count above is tag-independent, which is precisely why
they are inside it and then subtracted). The fifth is not, because there is no package on this
target for the glob to reach. The grouping was right about four rows and wrong about the one it was
written for.
Nothing published was changed for this. src/check-roster-format.ps1 computes
implementable = testable − ledger.Count, i.e. it assumes every ledger row is inside the naive
denominator, and it has no check that can see otherwise — so the two corrections available are (a)
strike the row from the Windows ledger and re-derive the header, or (b) keep it and teach the
guard to subtract only in-denominator rows. Both move a published headline; both are rulings, not
docs fixes. Owed to the owner. Note the row is a genuine Linux-axis testable package —
src/core/internal/runtime/syscall is converted with an L3 linux/ folder and GOROOT carries
syscall_linux_test.go with one func Test — so option (a) must not lose the measurement.
Finding 3 — the recorded derivation of 215 and today’s disagree by exactly one swap
Both derivations land on 215, by different routes and with different memberships. Quoted in full, both sides:
2026-08-17, HARVEST — the pure-compute tail re-measured, “The enumeration, and what it excludes”: “305 converted package directories under
src/corecarry a production.csproj(306 counting the hand-writtengolib); 216 have afunc Testin their Go 1.23.1 GOROOT sources; minus hand-ownedtestingthat is the roster header’s 215, and 215 − 150 banked = 65 unbanked.”
2026-09-02, this entry:
go list stdon windows/amd64 = 306 → with afunc Testin GOROOT sources = 219 → converted = 215.testingis an ordinary member (59func Test,src/core/testing/testing.csproj);internal/runtime/syscallis absent from the listing.
The reconciliation is exact and is the same pair as Finding 2. The 2026-08-17 route enumerates
src/core directories, which counts internal/runtime/syscall IN (it is converted) and
then hand-subtracts testing OUT (it is hand-owned). Today’s route enumerates go list std
on this target, which counts testing IN and internal/runtime/syscall OUT. One swap,
both totals 215, and the difference is invisible in the total — which is how the phantom survived
three weeks of arithmetic that “came out right”.
The roster now follows the second derivation, and says so. Two reasons, both on the record:
today’s live arithmetic requires it (201 banked + 14 remaining = 215 only if testing is inside,
and the tracker’s remaining list has named testing throughout), and the owner ruling below puts
testing on the road to a validating row — a row that can bank must be inside the denominator it
banks against. The 2026-08-17 derivation stands as the record of its own date; it is not rewritten.
Finding 4 — testing’s “Option 1 ruled” — the ruling IS located, and it is owed a durable home
The tracker’s ”| testing | meta | Option 1 ruled; post-wave. |” cites no ruling, and nothing on this board states what Option 1 is. Both halves were found:
- What Option 1 is —
docs/phase4/CENSUS-testing-osuser-rows.md, “Option 1 — validate the meaningful subset; E-class the rest, by bucket”: build and run bucket D (10 verdicts, the public-API in-process self-tests) through the pipeline and rule buckets A/B/C excluded with a per-bucket mechanism — A (20 whitebox host internals) E3, B (21 subprocess re-exec) under the E1/-raceprecedent and the ruled host-identity disclosure, C (8 benchmark machinery) Phase-4D. The census names its own real cost: “the-testsguard + a landing place for convertedtestingtest sources that does not collide with the host (the F15b problem) — the real cost, and it is a converter/layout change, not a test change”, and the precedent it sets: “this would be the first row admitted on a ruled subset of its own suite.” The four buckets are stated disjoint and to sum exactly to 59. - The ruling itself —
docs/phase4/MAILBOX.mdon branchclaude/mailbox, commitb88ab4b20, 2026-08-30, “COORD: OWNER RULING — testing takes Option 1 (revisitable)”, quoting the owner: “Option 1 is fine – can always revisit at a later day if a use case or argument makes a stronger case for other (or new) options.” It also fixes the implementation’s first increment (close the unguarded-tests-on-testingF15b collision) and sequences it behind the W3 endgame.
⚠ The ruling has never landed outside the mailbox, whose own protocol says “campaign rulings →
the BOARD … The mailbox is transport, not record; an answer given here that changes doctrine is a
defect until it lands in its durable home.” Master’s docs/phase4/MAILBOX.md is the 36-line
protocol stub — the entry lives only on the mailbox branch, so a reader at master could not reach it
from either the tracker or this board. This entry is that landing. Its consequence for the
arithmetic is Finding 3’s: under Option 1 testing becomes a partially-validating row, so it is
inside the denominator, not subtracted from it.
Finding 5 — a guard-shape defect on an unmerged branch (not present at master; recorded so the merge can carry the fix)
Commit 1fb37f437 on origin/claude/c1-board-syscall-roots appends C1’s syscall Linux-residue
section by splitting this file’s final guard line in two: it replaces the one-line
<!-- (endraw) … --> guard with the bare opener <!-- , writes the section, and re-adds the tail
half (endraw) … --> as the new last line. The guard is a single HTML comment whose purpose is to
hide a Liquid tag; split that way, the whole appended section (284 lines) sits inside an HTML
comment and renders nowhere on the published page, and the file carries two guard fragments where
doctrine requires exactly one, final. It is greppable in source, which is why it survives review.
At master 62c63b572 the file is well-formed — a comment-balance pass finds exactly two
self-closing HTML comments, the guard at line 1 and the guard at the final line, and exactly one
literal Liquid raw open and one endraw close, in that order, with no conflict markers. The four
interior --> hits are not comment closers: three are .NET exception text
(---> go.PanicException) quoted in prose, and the fourth is this board’s own note about this exact
hazard (2026-08-23), which spells the tag without its brace syntax on purpose because quoting it
inside the raw guard would terminate the guard. So there was nothing to repair here and
this entry does not touch that region. The one-line remedy at the merge: delete the orphan
<!-- line and place the appended section above the intact guard line, which is what every
other append on this board does.
Instruments: read-only. No build, test, converter or sweep was run. check-roster-format.ps1 was
run read-only before and after the roster edit — identical both times (544 of 546 checks pass; the
two failures are pre-existing at master 62c63b572, both execution args: release-tc0 fixture
assertions about the retired -test-release-tc0 converter flag, fixed on
origin/claude/i9-roster-guard-testconfig and not touched here). Positive control on the guard’s
ledger arithmetic: injecting one fake ledger row moved it to 6 of 548, naming all four arithmetic
assertions (excluded count 7 vs 6, difference 208 vs 209, denominator 208 vs 209, percentage 96.6 vs
96.2); the file was restored byte-identical (SHA-256 verified) and the guard returned to 2 of 546 —
so the prose added to the roster is invisible to both the ledger and roster parsers, and their green
is a measurement rather than a vacuum.
– coordinator sub-agent
2026-09-02 — coordinator sub-agent — OWNER RULING: the implementable denominator is the STRICT set, 210 — the phantom ledger row struck, net/http/pprof named, and the published 209 reconciled as one swap
The contradiction the previous entry recorded as owed to the owner is ruled. The roster’s implementable denominator is 210, not 209, and the honest ratio is 201 / 210 — 95.7%. Nothing about a banked row moves; what moves is one subtraction that was never legitimate.
THE ARITHMETIC, END TO END. The naive denominator is unchanged at 215 — every converted
package whose Go 1.23.12 sources define a Test function, derived in the roster from
go list std (306) → carries a func Test (219) → exists in the corpus as a converted package
(215). Of those 215, five are legitimately excluded, not six:
internal/syscall/unix(E1),net/internal/socktest(E1),log/syslog(E1),runtime/race(E1),internal/unsafeheader(E3) — all five members ofgo list stdon windows/amd64, all five inside the 215, all five subtracted from it.internal/runtime/syscallwas the sixth and is struck: it is not ingo list stdon windows/amd64 at all, so it was never a member of the set it was being subtracted from. Re-verified for this entry withGOROOTpinned explicitly to thego1.23.12root (go versionreports the binary’s build stamp, not the root it resolves, so the pin is stated):go version→go1.23.12 windows/amd64,go env GOROOT→ the pinned root,go env GOOS/GOARCH→windows/amd64,go list std→ 306 entries,internal/runtime/syscallabsent, and the other five ledger rows plustestingandnet/http/pprofall present.
So 215 − 5 = 210, and 201 / 210 = 95.7% (the format guard rounds it, away from zero, from the
two counts it derives itself).
THE OLD 209 RECONCILED — one swap, not one error. The 215 was reachable by two live memberships
that differ by exactly one package each way, and both land on 215, which is how the phantom survived
weeks of arithmetic that “came out right”. The board’s 2026-08-17 derivation counted src/core
directories whose GOROOT sources define a Test: internal/runtime/syscall in (it is converted,
with an L3 linux/ folder, and GOROOT carries syscall_linux_test.go), hand-owned testing
subtracted. The 2026-09-02 derivation counts go list std on this target: testing in (an
ordinary member, 59 func Test, a .csproj), internal/runtime/syscall out. Under the first
membership 215 − 6 = 209 was exact; under the second it was one too many. The campaign follows the
second — the owner ruling of 2026-08-30 puts testing on the road to a validating row (Option 1,
bucket D banks), and a row that can bank must be inside the denominator it banks against — so the
ledger, not the derivation, is what had to move.
THE SECOND CORRECTION. net/http/pprof is converted (src/core/net/http/pprof, with its own
.csproj and pprof.cs), declares four func Test in GOROOT, and was measured 5 of 15 on
2026-08-14 (this board, Scout batch 2) — and appeared in no roster row, no ledger row and no
tracker list. With the ledger corrected the remainder closes from both sides: 210 − 201 = 9, and
the nine are reflect, runtime, unique (lane-owned) plus os, testing, runtime/pprof,
net/http/pprof, runtime/trace, crypto/internal/boring/bcache. The implementable remainder is
nine, and every surface that names it now names the same nine.
WHAT THE STRIKE DOES NOT DO: lose the measurement. internal/runtime/syscall is a genuine
Linux-axis testable package — converted with an L3 linux/ folder, and GOROOT carries
syscall_linux_test.go. What the strike removes is a Windows exclusion that was excluding
nothing. Its E1 reading is kept verbatim in the note that now sits under the ledger table, explicitly
as the Windows half of its story, and the row belongs to Linux’s own denominator when the per-OS
denominators land (DESIGN-peros-roster.md already computes windows as 215 − 5 = 210 on its own
scoping, which is the same number from the other direction).
SURFACES. docs/ValidatedTestPackages.md — header line (215 − 5 excluded = 210, 201 / 210,
95.7%, “The five”, plus one sentence of prose in the header itself naming why the denominator moved);
the exclusion section’s opening count (six → five); the ledger table (row struck) with a new two-paragraph
note under it carrying the strike’s reason and the Linux-axis measurement; os/user’s rejoin
arithmetic (208 → 209 becomes 209 → 210, with the as-published figures kept in a parenthesis);
and the derivation subsection updated from owed to the owner to ruled, its E1 bullet, its
disposition bullet and its remainder line now closing against the header (210 − 201 = 9).
docs/phase4/TRACKER-100-percent.md — the on-master ratio (201 / 210 = 95.7% honest), the
implementable-remainder count (8 → 9) and its named list (net/http/pprof added, bcache re-labelled
the ninth, “implementable 209” → 210); that file declares the roster its authority and states that it
is stale when the two disagree, so leaving it at 96.2% was not an option. Deliberately NOT touched:
docs/NEWS.md and docs/news/* (dated history, true when written); docs/phase4/SESSION-ROLL-*
(dated session records); docs/phase4/DESIGN-roster-per-os-bubbles.md and
docs/phase4/DESIGN-peros-roster.md (design proposals quoting a point-in-time header — the former’s
quoted Linux line is already stale by 17 rows, which is what makes it a snapshot rather than a live
surface); every earlier board entry (append-only). docs/README.md quotes the naive 215, which this
ruling does not move, and src/go2cs/validationProofPages.go with its
testdata/validationproof/expected.md carry no campaign figure at all — both verified by grep, both
correctly untouched.
THE GUARD IS THE CALCULATOR, AND IT NEEDED NO CHANGE. check-roster-format.ps1 computes
implementable = testable − ledger.Count and recomputes the difference, the denominator and the
percentage from the table — so striking the row is the whole edit and the header follows.
Before: roster format guard: 550 checks pass (201 rows, 195 with a linux annotation, 4 with an
execution config, 6 excluded), exit 0. After: roster format guard: 548 checks pass (201 rows, 195
with a linux annotation, 4 with an execution config, 5 excluded), exit 0 — two fewer checks because
the two per-ledger-row assertions (ruled class, five-column render) go with the struck row.
Positive control, because a green that cannot go red is not a measurement: the header was reverted
to (215 − 6 excluded = 209): 201 / 209 — 96.2% against the corrected ledger, and the guard went RED
at exactly the four arithmetic assertions — excluded count expected 5 got 6, stated difference
expected 210 got 209, denominator expected 210 got 209, percentage expected 95.7 got 96.2 —
exit 1. The file was restored and verified byte-identical by SHA-256
(99cfc67667dfa3ae452c4bfe3d6046b1e2fc9879c88eb172c6f44514a5acb2fc), and the guard returned to 548
of 548, exit 0.
Instruments: read-only apart from the three documents edited. No build, converter run, test or sweep
— a train battery was running on the host. The go list std verification is a toolchain query
against a pinned GOROOT and touches no repository file.
– coordinator sub-agent
2026-09-02 – net/http’s h2 deadline pair survives a clean golib A/B, and the row needs MORE than 30m on the i7 at Debug (lane claude/sub-os-row)
Two measurements, taken as canary work for the NetShareAdd retention and worth keeping on their
own account.
1. The h2 pair is not moved by a golib pointer change – measured, not assumed. net/http is the
second-largest reflect importer on the derived canary set, and it FAILED on the change’s arm with
exactly four divergent rows: TestWriteDeadlineEnforcedPerStream, its /h2,
TestWriteDeadlineExtendedOnNewRequest, its /h2. The A/B was run rather than the failure excused
by the known-artifact note: with the change REVERTED to the parent commit, the SAME four rows fail,
identically. Two legs of the three-run standard (fail-with, fail-without); the re-restored third leg
was not run, and this entry does not claim it. This is an independent confirmation of the
build-CONFIGURATION reading CLAUDE.md records for that pair (Debug publish + default tiering) from a
direction that lane did not take – a corpus-wide golib change moves it zero rows.
2. net/http is UNDER-BUDGETED at 30m on this machine class, and the two arms bracket it. The
change’s arm completed in 1,836 s; the clean arm took 2,171 s and its results tail states
package timeout after 00:30:00 outright. The deadline kill’s signature is the documented one and is
worth restating because it nearly reads as a regression: the killed arm showed 18 extra empty
verdicts on top of the four real failures – a contiguous alphabetical tail
(TestWriteHeader*, TestWriteResponse, TestWriteSetCookies, TestZeroLengthPostAndResponse*)
plus the parked parallel batch interleaved among it, which reads scattered and is not. Two arms of
the same package, 335 s apart, one side of the budget each: that is the whole margin. net/http is
not in run-validated-sweep.ps1’s $longTimeouts, and on this host at Debug it wants a floor the
way net (40m) and crypto/tls (30m) already have one. Not added here – a floor is a change to the
shared sweep and belongs to whoever owns the Release-config flip that is about to re-time every row
anyway – but the number is recorded so the next lane meets the measurement instead of the surprise.
2026-09-03 — internal/poll validates on Linux — TestSplicePipePool disclosed codegen-liveness, with the heap root path verbatim
internal/poll is a Windows-banked row (19 · 0). On Linux Go compiles a different test set: the
COM-port and WSASocket rows disappear and splice_linux_test.go appears, so the Linux arithmetic is
its own. Every Linux verdict matches go test except one, and that one is disclosed rather than
tolerated.
The divergence
TestSplicePipePool allocates 64 splice pipes, returns them all to sync.Pool, drops the slice
that held them, then GCs in a loop and asserts every write descriptor has been closed by the pipes’
finalizers. Under the CLR none ever is: the loop spins until the test’s own deadline and reports
all 64 descriptors leaked.
The root, measured rather than argued
A heap dump taken while the loop was spinning, gcroot on a leaked splicePipe box (Release
publish, dotnet-dump) — verbatim:
Thread 6e5c:
7eef53ffe440 55bf47f64a84 go.internal.poll_test_package.TestSplicePipePool(go.`1<T>)
rbp-270: 00007eef53ffe5a0
-> 7eef8e89ba48 go.<go.internal.poll_package+splicePipe>[]
-> 7eef8e8974d0 go.StandardBox<go.internal.poll_package+splicePipe>
rbp-138: 00007eef53ffe6d8
-> 7eef8e89ba48 go.<go.internal.poll_package+splicePipe>[]
-> 7eef8e8974d0 go.StandardBox<go.internal.poll_package+splicePipe>
rbp-b0: 00007eef53ffe760
-> 7eef8e89ba48 go.<go.internal.poll_package+splicePipe>[]
-> 7eef8e8974d0 go.StandardBox<go.internal.poll_package+splicePipe>
Found 3 unique roots.
Every root is a frame slot of the test’s own method — three of them, all pointing at the one
splicePipe[] backing array, which holds all 64 boxes. The named local ps is assigned default!
at its source line, so none of the three is ps; they are slice-header copies — the append
result and JIT spill temps — with no source-level name to null. Go’s precise stack maps report
those dead at that point; the CLR reports them live. The pipes therefore stay reachable, their
finalizers never become due, and destroyPipe never runs.
Three controls, so the attribution is a measurement
sync.Poolis not the retainer. Its chains dumped empty —poolCleanuphad already released everything it cached. Pool retention stays separately observable, which matters for the scope note below.- The JIT’s optimization level does not reach it. Debug vs Release+TC0: zero verdicts moved, identical descriptor set.
- The one emission-level copy that could be nulled is not a root. The converter’s range
enumeration over
ps(foreach (var (_, vᴛ1) in ps)) was replaced with an index loop — one axis, everything else untouched — and the live count was unchanged to the object: 64 boxes, 67 sentinels, identical to baseline. The null was controlled by confirming the built assembly postdated the patched source, so it is a measurement and not an unapplied patch.
Control 3 falsifies “the converter’s enumeration keeps it alive” to the object and leaves the
managed frame itself — the same conservatism sync’s TestOnceXGC and TestPoolGC already
disclose, and the same class string: codegen-liveness (not gc-liveness; the corpus class is
codegen-liveness and that is what the sweep matches).
The disclosure’s scope note, stated rather than buried
The pinned signature is leaked descriptors: , the failure’s message prefix. The descriptor
numbers vary per run, so — unlike TestPoolGC, which pins an exact count — a partial leak would
match this entry too. A real retention regression in the pipe pool could hide behind it. The
compensating observation is control 1: the pool’s chains were measured empty, so pool retention is
separately observable and a regression there would show as something other than this signature.
The note is carried inside the manifest entry’s own reason, where the next reader stands.
A measured property worth carrying: this row’s wall is a function of the BUDGET, not of the work
TestSplicePipePool derives its own timeout from t.Deadline() — timeout = deadline.Sub(now)
less 10% — so when it cannot succeed it consumes 90% of whatever package deadline it is given
and nothing runs after it (it is alphabetically last in this suite). Two points, measured:
-test-timeout |
TestSplicePipePool elapsed |
package total | the other 12 verdicts |
|---|---|---|---|
| 60m | 3234.5 s | 3240.6 s | 6.1 s |
| 15m | 804.6 s | 810.6 s | 6.1 s |
(The other twelve verdicts run before it and cost 6.1 s on both points; nothing measurable runs after it. The column is labelled for what was measured — the suite minus this one test — rather than for where it sits in the ordering.)
Total ≈ 0.9 × T + 6 s, on both points. The consequence is the opposite of the usual one: this row
needs no $longTimeouts floor at all. A floor would not protect it — it cannot time out, because
it always finishes inside 90% of its budget — it would only make the row cost that much wall on
every sweep. At the sweep’s default 10m the row costs ~9 min; at a 60m floor it would cost ~54 min
for exactly the same verdicts. The general shape is worth naming: a test that sizes itself from
t.Deadline() converts a deadline into a duration, so the budget-vs-wall reasoning the timeout
table encodes runs backwards for it — bigger budget, longer row, same result.
The arithmetic, and a correction to the figure it was commissioned at
Two bare sweeps — no -TestConfig, no -TestTimeout, so the row publishes under the configuration
of record with per-row annotations respected, which is what makes a sweep bank-eligible:
| run | tree | result | wall |
|---|---|---|---|
| control | f3a6e6ac8 (pre-train-14 base + this disclosure) |
CVAC internal/poll 12 (validated) |
726 s |
| bank | 7abd9e442 (rebased on landed master 8c15217c8) |
CVAC internal/poll 12 (validated) |
717 s |
Identical. Train 14 carried converter, golib, go2cs-gen and run-validated-sweep.ps1 changes, so
the bank was re-taken at the merge result rather than inherited from the base — and it moved
nothing for this row. Both comparison records read matched: true, status: validated,
configuration: Release, tiered: false, and
oracleGoVersion: "go version go1.23.12 linux/amd64".
Thirteen Linux verdicts: twelve pass, one disclosed — so the annotation is linux: 12 + 1. The
bank was commissioned at “13 with 1 disclosed”; that counts the disclosed verdict into the matched
half. The converter’s own README badge arithmetic agrees independently — it rewrote itself to
12/13 during the sweep. The roster header moves 195 → 196 of 199 applicable rows, 22,583 →
22,595 matching verdicts, 159 → 160 disclosed; the format guard computed all three
independently (551 checks pass), and the prediction recorded before running it held on all three.
Three artifacts this bank does NOT produce, each by derivation rather than by omission
A Linux-axis bank is not a Windows bank with a different number, and three of the usual artifacts are answered by the corpus rather than by doing them:
- Committed test sources. Corpus-wide: 22 committed
*_windows_test.cs, 0*_linux_test.csor*_unix_test.cs. The committed emission is the Windows record exactly as the Tests and Disclosed columns are.internal/poll’s test set genuinely differs by GOOS — itserror_test.csalone is 30 lines shorter under Linux — so committing the Linux flavour would overwrite the Windows row’s own reproducible sources. - The proof page. 0 of the pages under
docs/validation/current/carry a Linux section; the only two files mentioning “linux” do so in Windows-run test names. The Linux arithmetic’s home is the roster annotation, which is where this bank put it. - A
$longTimeoutsfloor. Measured above: the row cannot time out, so a floor cannot protect it and would only make it cost ~54 min a sweep instead of ~9.
A fourth artifact belongs with (2) and is easy to bank by accident: the package README badge. A
Linux sweep rewrote src/core/internal/poll/README.md from the Windows record 19/19 to the Linux
12/13. It composes from the committed proof page’s counts, so it is a Windows artifact too and is
restored, not banked, from a Linux host.
One correction to the doctrine’s own wording while it is in view: the pipeline’s comparison record
for this row is a FILE, go2cs_test_comparison.json, not the go2cs_test_comparison/results.json
directory the tail-reading rules name. The rules still apply; the path does not.
– C1
2026-09-02 · crypto/internal/boring/bcache BANKS at 1 of 1 — the board’s registerCache root was two days stale, and the flake that withheld the bank did not reproduce in 30 runs at the Release + tiering-off default (coordinator sub-agent; coordinator ruling same day)
The row banked. The defect behind its flake did NOT, and is recorded here as open rather than closed.
What this board said, and why it was wrong
The row’s standing entry — Scout batch 2, 2026-08-14, restated in the 2026-09-02 remainder table —
read: “0 of 1. NotImplementedException: registerCache … a PartialStubGenerator stub reached
from Register[K,V]“, and “Still unruled, so the roster keeps it inside the naive denominator.”
That root was closed on 2026-08-30 by 378951155 — “bcache: wire registerCache truthfully — a
clear DELEGATE, not an address” — and no board row records it. Go’s Register hands the runtime
the ADDRESS of the cache’s ptable word for clearpools to nil with atomicstorep; that word is
an atomic.Pointer[cacheTable[K,V]] whose managed slot holds a ж<T> REFERENCE, so the storage is
not pinnable, the provenance record can never satisfy IsPinnedAt, and ManagedPointerTokens
answers MISS for the number by design. The commit replaced the address with a clear DELEGATE — the
currency clearpools’ other two arms already use — bound to the package’s own Clear, which Go’s
doc comment names as exactly what the collector performs here.
That commit measured 1/1 and deliberately did not bank, in its own words: “NOT BANKED, and the roster row + proof page are deliberately not carried: the suite is FLAKY at ~70% under the default config, from a defect this change neither introduces nor touches.”
The re-measure, 2026-09-02, at master 01a7fdefe
Pipeline, windows/amd64, -tests -test-action all -test-timeout 10m with -go2cspath pinned to the
worktree; converter built from the same tree, go version <exe> = go1.23.12. The comparison record
reads back configuration: "Release", tiered: false, oracleGoVersion: "go version go1.23.12
windows/amd64" — the configuration of RECORD since the 2026-09-02 default flip, and the axis the
prior measurement did not have.
| Test | go test |
go2cs |
|---|---|---|
TestCache |
pass | pass |
status: validated, matched: true, disclosed: [], errors: []. The results-file TAIL was read
FIRST and carries no timeout event in either the plain or the escaped form; it ends on the
package’s own "action":"pass".
The flake, sampled rather than assumed — 30 consecutive runs, zero failures
378951155 measured the failure rate under the then-default Debug configuration: 7/10 at
the default, 4/10 with DOTNET_TieredCompilation=0, 3/10 with the sentinel disarmed, and
10/10 under DOTNET_GCgen0size=1GB — GC-correlated, not a tiering artifact and not the
sentinel’s.
At the new default this lane ran 30 consecutive comparisons: 30 validated, 0 failures (runs 1–9
records preserved individually; runs 10–30 sampled with failure-record preservation armed, which
never fired). Repeat runs used -test-action compare against the existing digest-validated
artifacts, so each is a fresh publish-and-run of the same emission rather than a re-conversion.
The reading, stated as a bound and not as a cure. Against the prior 30% failure rate, P(30 consecutive passes) ≈ 2.3 × 10⁻⁵; the 95% one-sided upper bound on the failure rate implied by 30/30 is ≈9.5%. So the Release configuration has moved this row’s behaviour by a wide margin — most plausibly because Release’s allocation and optimization profile closes the window the race needs — but 9.5% is not zero, and nothing in this lane fixed anything. No golib, converter or corpus line changed; the only variable is the published configuration.
OPEN — the element-aliasing lost write under concurrent CAS plus collection
378951155 rooted the flake and the root is untouched: every failure it saw was confined to the
test’s final section, which uses an UNREGISTERED cache and loses entries under concurrent Put,
while the registered-cache/runtime.GC() assertion that the commit owns passed in every one of 40+
runs. Its own placement: “the single-threaded 10,000-entry section never failed, which places it in
golib’s element-aliasing machinery under concurrent CAS plus collection, not in bcache.”
That is where it stays. Banking this row does not close it, and the row is not evidence that it is closed — it is evidence that the configuration of record does not currently reach it at a rate 30 runs can see. If a future sweep reds this row, this entry is the first thing to read: the suspect is the aliasing defect, not whatever change is in flight.
What banked
Four test artifacts (cache_test.cs, package_test_info.cs, go2cs_test_host.cs,
crypto.internal.boring.bcache.tests.csproj), the generated proof page, its docs/validation/index.md
row, and the roster row. Two properties worth recording because the package is one of the four
hand-owned-by-consequence members (every non-test Go file hand-owned, so the driver continues
before writeProjectFile and its .csproj/package_info.cs/README.md are never re-emitted):
- The production
.csprojdid not change — it already carries<Compile Remove="*_test.cs;package_test_info.cs;go2cs_test_host.cs" />, so the bank needed no production edit at all, andpackage_info.csandcache.csare untouched. cache.cs.autowas deliberately NOT carried. The-testsrun re-emits that review sibling with theinitᴛᴛimportꓸsyncꓸatomicforced-init hook REMOVED — the standing init-hook relocation, which moves the hook intopackage_info.cs. For this package that relocation can never land, becausepackage_info.csis never re-emitted; the committed hand-ownedcache.cscarries the hook and is self-consistent. Carrying the.cs.autoline would have put an unbanked relocation into the tree for no gain. Restored, per the standing.cs.autoclass.
The roster row carries no execution: segment (it validated at the default) and no linux:
segment (measured on windows/amd64 only). The absent Linux annotation is load-bearing rather than an
omission: an unannotated row makes no Linux claim and reports comparison-validated-at-count there,
so it grows the Linux applicable denominator 199 → 200 without touching the numerator. bcache’s
Go sources carry no build constraint, so it is genuinely applicable on Linux and genuinely
unmeasured — the honest direction, and the header records it.
Instruments: the -tests pipeline (1 conversion + 30 comparisons) and check-roster-format.ps1.
The roster guard is the calculator and was run before and after: before, 550 checks pass (201 rows,
195 with a linux annotation, 4 with an execution config, 6 excluded); with the row inserted and the
header untouched it FAILED 6 of 551, naming every derived number it disagreed with (package count
202 vs 201, verdicts 27735 vs 27734, 94.0 vs 93.5, honest numerator 202 vs 201, 96.7 vs 96.2, linux
applicable denominator 200 vs 199) — so the header was recomputed FROM the guard rather than
incremented by hand, and after, 551 checks pass (202 rows, 195 with a linux annotation, 4 with an
execution config, 6 excluded), exit 0. That mid-state failure is this entry’s positive control on
the guard: it can go red, and it named the right six assertions. Both comparison records and both
results files were preserved to distinct paths before the tree was restored.
– coordinator sub-agent
2026-09-03 — net validates on Linux — 577 + 2 at 6fa031d08, the DNS/socket stack’s Linux axis closed by four seats from three lanes
The campaign’s largest single row banks on the Linux axis: 577 matched · 2 disclosed, with 18 of the matched being identical skips on both sides, 58 declarations excluded (benchmarks deferred to 4D + unsupported), and 0 errors. status: validated, measured on the WSL bank host (G-LAPTOP) at train-16 master 6fa031d08 under the sweep’s own invocation for the row — go2cs -tests -test-action all -go2cspath <clone>/src <GOROOT>/src/net <clone>/src/core/net, output into the corpus dir so the dnsclient_impl.cs hand-own survives. configuration: Release, tiered: false, oracleGoVersion: "go version go1.23.12 linux/amd64", CGO_ENABLED=0, GoTargetOS=linux; BANK_EXIT=0, zero empty verdicts, no timeout event. The comparison record holds 579 tests each side; the two distributions — go {560 pass · 18 skip · 1 fail} and C# {558 pass · 18 skip · 3 fail} — agree on 577.
Why the Linux count (577) exceeds the Windows count (472)
A verdict count is a fact about a package and an OS: Go runs a different test set per GOOS — build-tagged tests, capability gates, GOOS-keyed skips — and the Linux socket surface exercises paths the hand-owned Windows WSA mirrors do not. The 577 is read from the run’s OWN comparison record, never blended with the Windows 472; the row keeps the Windows record in its Tests/Disclosed columns and carries the Linux arithmetic in its linux: 577 + 2 annotation, as the axis convention requires.
First contact closed by four seats from three lanes
This row is where the fleet’s Linux axis has been converging since first contact. Four roots, each measured and named by its lane, are all landed at this master: the dual-embed interface-witness fix (35 DNS verdicts — a fakeDNSPacketConn that dual-embeds an interface and a struct got no witness, so c.(PacketConn) read false and a UDP conn took the stream arm, TCP-prefixing the query; train 13), C1’s Iovec layout seat (the TestBuffers writev path; train 15), C2’s abstract-unix-name seat (train 15), and C2’s multicast mreq seat (train 16). With all four in, net reaches 577.
The two alloc-profile disclosures
Both are zero-alloc asserts guarded by SkipIfOptimizationOff — Go reaches zero only through escape analysis, and the converted path cannot, at three structural sites: golib’s slice<T> is a struct over a heap T[], every []byte crossing the Conn/PacketConn interface surface is a managed object, and per-call syscall marshalling allocates its own buffers. The CLR publishes no in-process malloc counter, so go2cs’s own runtime counter (golib’s allocation sites, the structural mirror of runtime.MemStats.Mallocs) is the reported pin — the golib-site lower-bound mechanism the class already carries, the same shape as bytes/strings.
TestAllocs— the UDP hot path:WriteMsgUDPAddrPort/ReadMsgUDPAddrPortrun 1,000 times, steady-state allocation count required to be zero; the converted counter reports ~71 allocations per run (71,000 over 1,000).TestTCPReadWriteAllocs— the TCP hot path:Read/Writeon a loopback conn, 1,000 runs, required to allocate nothing; reports ~35 allocations per run (35,000 over 1,000). Lower thanTestAllocsbecause the TCP path marshals no msghdr and carries no address value — itself evidence the count tracks the converted call shape, not a defect in net.
TestLookupCNAME on the host-qualification ledger — universal drift, not a row disclosure
Go’s own net suite (cgo off) fails exactly one test on this host — TestLookupCNAME — because a live public CDN CNAME record drifted from Go’s expected value. It is universal drift: three independent resolvers agree the record moved, so it is not any one host’s failure. In the bank it is go=fail / cs=fail — both sides observe the same world and agree — a MATCHED verdict, folded into the 577, and NOT a per-row disclosure. It is recorded here, on the host-qualification ledger, per the standing rule; if the record ever returns to Go’s expectation the row reads pass/pass and nothing here changes. The 18 TestLookupNoSuchHost leaves pass, confirming the host’s re-qualified state (the fec0:0:0:ffff::* placeholder-resolver class that gated net’s Windows bank stays closed here).
Four artifacts this bank does NOT produce, each by derivation rather than by omission
- No committed Linux test sources. The corpus’s committed emission is the Windows record and this row’s test set differs by
GOOS; committing the Linux flavour would overwrite the Windows row’s own sources. (Emitted into the bank clone for the measurement, then cleaned.) - No proof page. None of the validation pages carries a Linux section; the
linux: 577 + 2annotation is the home. - No
$longTimeoutsentry owed.netalready carries its 40m floor; the run finished in ~4 min (warm dependency closure), never approaching it. - No README badge rewrite. The badge composes from the Windows proof page; a Linux sweep did not touch it here, and it is left as it stands.
– G