The standing runbook for moving the converted standard library — and everything derived from it:
the goldens, the validation roster, the proof pages, the disclosure manifests, the published
packages — from one Go release to the next. It is version-agnostic by design: it names
instruments, gates and traps, never a particular release.
This runbook leads. It is the living procedure for a corpus migration: the canonical
H0–H12 (+H4a, H7a) step inventory is the one maintained here, amended in-stage as lessons are
learned — the discipline its first execution already practiced, ratified as the era rule
(board, 2026-08-24: runbooks are executed as written, deviations fixing the runbook in the stage
that finds them). The strategy lives in
PLAN-corpus-upgrade.md — which releases, in which order, under which
ruled frame — and every “(ruled)” below points at a ruling recorded there (§8) or in an instance
plan. A runbook edit never reopens a ruling: a change that would contradict one requires a new
ruling first, recorded where the old one lives. Where this document and any plan disagree about
procedure, the plan is stale — fix this document if it is wrong, and mark the plan superseded in
the same change.
Companion: DotNetMigration.md, the same for a new .NET release. The two
are separate documents because they are separate hops — one variable at a time is the rule that
makes either measurable.
No frozen figures. Roster rows, verdict totals, package counts, marker-census counts and wall
times are named by instrument and re-measured at the migration. Two classes are re-measured by
standing rule and never carried at all: the hand-own marker census, and the per-release standard
library delta. Where a budget matters, this document names the row in CLAUDE.md’s measured budget
table rather than copying a number that goes stale.
1. Shape of a corpus migration
A corpus migration moves the Go release the corpus is converted from. It moves nothing about the
.NET runtime, and it changes the converter only where the new release’s language requires it.
Two properties determine almost everything about how expensive a given migration is:
Property
Cheap end
Expensive end
Language delta
none — a patch-level move within one minor
new syntax or new type-system surface, which is converter work with its own design
Package delta
none
packages added, removed, promoted, or reorganized wholesale
A patch-level migration within one minor is the cheap end on both axes and is the right rehearsal
for the machinery: the pin guard fires for real, the hand-own differential runs for the first time,
the badges churn, the release ritual is exercised — all without a language change to confound them.
1.1 What moves emitted C# even when the Go source did not
Three channels, and knowing which are live for a given migration is the difference between reading a
diff and drowning in one:
Release-tag expansion. The converter derives the go1.1 … go1.N build-tag set from the Go
version and evaluates build constraints with it, so a minor bump flips every //go:build go1.N
guard in the Go tree and changes which files each package includes. The derivation is
minor-keyed (releaseTagsForVersion, src/go2cs/directiveOperations.go, trims any patch
suffix), so a patch-level migration has zero release-tag delta — verify this against the
source for the migration at hand rather than assuming either way.
Imported type aliases. An emitted project reads each imported package’s package_info.cs to
mint its <ImportedTypeAliases> block, so a moved dependency moves its dependents’ emission —
including the behavioral goldens’, whose own Go sources never change.
Upstream source. The ordinary channel, and the one the migration is for.
1.2 The toolchain rule, and why it is step one
The converter type-checks from source using the go/ast + go/parser + go/typescompiled into
go2cs.exe. Therefore:
To convert Go 1.N sources, go2cs.exe must be BUILT with a Go toolchain ≥ 1.N.
This is why the toolchain move is the first step of every migration and not a housekeeping item.
And it opens a false-green route the harnesses do not close. Every rebuild predicate rebuilds the
converter when a converter *.go file is newer than the binary. Installing a new Go toolchain
touches none of them, so every predicate still says “up to date” and every gate keeps running a
binary whose front end is the old release’s, against the new release’s sources. It does not
fail cleanly — the old parser mis-parses or rejects new constructs and the run degrades into the
converter’s best-effort “did not fully type-check” path, which check-no-regression.ps1 reports as
NOT MEASURED (good) and the runners do not.
The hole is closed, and the closure is structural rather than remembered. Every Go binary
already embeds the release that built it, so nothing needed stamping: the three rebuild predicates
delegate to ONE shared helper (src/tests/ConverterBuildInputs.cs) that reads the binary’s embedded
release back, compares it against the live go env GOVERSION, and fails stale-wards — an
unreadable stamp or an unanswerable GOVERSION forces the rebuild rather than excusing it. A
toolchain hop invalidates go2cs.exe by itself; no explicit go build is owed, and no gate runs
against a stale converter. The same helper derives its input set from the converter’s own
//go:embed directives, so an embedded-asset edit — a csproj template, the package_info.cs
skeleton, a publish profile — invalidates the binary too; that sibling route is why the compare
landed in one place rather than three. Full statement:
DotNetMigration.md §5.2. Closure record (⟨OQ-6⟩, landed 2026-08-24):
PLAN-corpus-upgrade.md §1.4.2.
⚠ A stamp cannot close the CONFIGURATION form of the same shape — a toolchain pin that silently
substitutes another release at conversion time, while the stamp truthfully names the toolchain that
built the exe. That one is H1 step 1’s, and it is verified by running, never by reading.
2. The step ladder
This section is the canonical H0–H12 (+H4a, H7a) inventory and its procedure. It was generalized from
PLAN-corpus-upgrade.md §2, which now points here; the ⟨OQ-n⟩ rulings
behind each “(ruled)” remain recorded in that plan’s §8. Steps marked GATE are pass/fail and block
the next; steps marked ⟲ are re-measured at every migration and never carried forward.
Three orderings are not negotiable, and each has a mechanical reason rather than a preference:
The toolchain step and the pin bump land as ONE reviewable pair. Between them the binary claims
the new release (its embedded runtime version is what the NuGet compatibility guard reads) while
version.props still names the old one — so a NuGet-referencing conversion in that window refuses
legitimate old-pin modules and accepts new-pin ones for a corpus that does not exist yet. Silent,
and it only bites a user.
The pin bump precedes the reconvert.checkCorpusToolchainPin refuses -stdlib and -tests
otherwise — and the guard’s own error text prescribes the remedy verbatim: “if the corpus is
deliberately moving to X, bump <GoStdLibVersion> to X first.” The ordering is sanctioned by the
code, not invented.
The baseline capture precedes replacing the old Go tree. With side-by-side installs — which
every migration should use — this relaxes to “precedes the reconvert”.
Everything else may be reordered by the executing lane.
AMENDED 2026-09-07 — a FOURTH ordering, and it runs ahead of the H1↔H2 pair: the OUTGOING
corpus’s final NuGet release ships FIRST. The mechanism is one line of the pin instrument.
src/migrate-gorelease.ps1resets <GoBuildNumber> to 0 at the
pin stage — line 895, inside the src/version.props arm that -KeepBuildNumber guards — enacting
H2’s own ruling that the build number resets per release. So the moment H2 lands, the outgoing
release has no build counter left to advance: the next publish is <new-release>.1, a version of a
corpus the outgoing record never measured, and the outgoing corpus can never again be published at
its own release. Whatever it had shipped when H2 landed is what it shipped, permanently.
The step: ahead of H1/H2, publish the outgoing corpus’s final release with
src/release-nuget.ps1, running H12’s release ritual in full — the
announcement text on the branch, the pre-pack signed tag, the write-once proof snapshot, both badge
retargets, the recomputed re-verification pass. That release is what freezes the outgoing roster,
its proof pages and every package README at the record they reached; it is the outgoing corpus’s
anchor, and it is the last moment one can be minted. It sits ahead of H1 rather than merely ahead of
H2 because the first ordering above already binds H1 and H2 into one reviewable pair.
H11 and H12 below are written for the incoming corpus and are unchanged; this step is their
outgoing-side twin, and the two are the same ritual pointed at the two ends of the hop.
-KeepBuildNumber is not a substitute: it holds a counter across the boundary that counter is
defined to reset at, which is a different and unruled thing.
Worked instance — the 1.23 → 1.24 hop. It proceeds with the outgoing record closed at its
anchor rather than driven to 100% (owner ruling, 2026-09-07; the reasoning is recorded in
ValidatedTestPackages.md’s Excluded packages block and in
PLAN-corpus-upgrade.md §1.3’s dated amendment). The rows still unbanked
at the anchor are neither carried forward nor owed anything special: H10 re-derives every row from
scratch at the new release regardless, so they re-bank there on exactly the footing of the rows
that did bank.
H0 — Baseline capture ⟲
Capture, on the outgoing toolchain and the new converter build, everything the migration will
diff against: the hand-own .cs.auto baseline, the package census, the roster snapshot, the
disclosure manifests.
⚠ Generate the .cs.auto baseline fresh, from a seeded old-release regen — never from the committed
siblings. The overlay rule excludes *.cs.auto in order to protect the hand-owned .cs beside it,
so the tracked siblings are frozen on their own schedule and a materially stale baseline poisons
the differential. This is a ruled decision, not a preference.
H1 — Toolchain provisioning GATE
Install the target release side-by-side; confirm the target actually executes — run
<target-root>/bin/go version and require its OUTPUT to name the exact target. Reading
GOROOT/VERSION is not a verification (measured 2026-08-24, hop-A provisioning). Go 1.21+
toolchain switching obeys a GOTOOLCHAIN pin (go env GOTOOLCHAIN, persisted in the user’s
go/env) ahead of whichever binary is invoked, and the redirect is silent: the VERSION
file, the target’s own bin/go, and even the official download shim can disagree, and only the
ones that run tell the truth. A leg that trusted the file would emit the whole corpus with the
OLD toolchain while believing otherwise — §1.2’s false-green shape arriving through
configuration, which no binary stamp can catch. Check go env GOTOOLCHAIN explicitly; a pin
naming another release must be resolved, or overridden per-invocation (GOTOOLCHAIN=<target>,
or GOTOOLCHAIN=local with the target’s GOROOT), before any step below runs. Prefer the
per-invocation override to editing the pin: the pin is a machine default, outside the standing
install grant, and while the hop is in flight it is protective — it keeps every other process
on the box on the outgoing release until the migration deliberately moves.
⚠ GOTOOLCHAIN is only HALF the override on a box that also pins GOROOT (measured
2026-08-25, H2’s smoke gate, first execution). A user-level GOROOT environment variable names
the TREE, and the two answers diverge silently: under GOTOOLCHAIN=<target>, go env GOROOT
reports the selected toolchain’s root while the process environment still carries the pinned
one — and -stdlib converts the tree the ENVIRONMENT names. The leg would have emitted the OLD
release’s sources into a corpus whose every gate then measures against NEW-release goldens, each
side internally consistent — except the converter’s own pin-vs-tree guard refused, and its
message named the mechanism. A hop leg’s environment therefore sets both —
GOTOOLCHAIN=<target> and GOROOT=<target-root> — per-invocation, both pins left in place.
Provisioning records which pins a box carries; the fleet has held every combination.
⚠ Fleet boxes are configured oppositely and neither lane’s experience predicts the other’s:
a pinned box switches DOWN, silently ignoring a newly installed release; an auto box
switches UP, silently downloading one a go.mod asks for. Both make “the SDK is installed”
insufficient as provisioning evidence, in opposite directions. Worked instance with the resolved
per-box values: phase4/STAGE0-provisioning.md, its hop-A
section.
⚠ An auto-fetched toolchain is READ-ONLY, and the attribute travels (measured 2026-08-25,
the i9’s reserved shard). GOTOOLCHAIN=auto downloads into the per-user module cache, where Go
marks every file read-only by design — a manually provisioned side-by-side SDK
(~/sdk/<release>) is not. Any harness that COPIES fixtures out of that tree carries the
attribute along (.NET’s File.Copy propagates ReadOnly with the content), and the first
write onto a copy throws UnauthorizedAccessException — which presents as a mass
Go="pass" C#="" file-lock signature, and the stale partial copy it leaves behind then presents
as an unrelated CS0234 on retry, convincingly mimicking other catalogued traps. The fix is
one attribute strip, in place, once: clear IsReadOnly recursively on the cached toolchain
directory — no copy, no relocation. A box-configuration trap, not a harness bug: an
auto-configured box meets it identically every hop, a manually-provisioned one never does.
Move the converter module’s go directive to the target (ruled: it moves each migration).
Bump the golang.org/x/tools and golang.org/x/mod requirements to releases contemporary with
the target. The export-data policy bounds how far they may lag. This is a separate commit with
its own CNR (ruled) — a dependency bump that can move emitted bytes must be visible on its own.
go build the converter on the new toolchain; converter go test ./... green.
Verify the stale-binary guard held (§1.2 — closed 2026-08-24; the harnesses compare the
binary’s embedded release against the live toolchain) before any harness runs. This is a check,
not a task: the guard fires on its own, and the step exists only to confirm the rebuild it forces
actually happened.
Gate: converter unit tests green andgo2cs.exe demonstrably built by the new toolchain.
H2 — The pin bump GATE
Bump <GoStdLibVersion> in src/version.props to the exact target release, and settle the build
number’s policy at the same moment (ruled: it resets per release). Nothing else changes in this
commit beyond what the instrument itself edits — the pin, the build-number reset, H1.2’s go
directive, and the prose that states the release as present-tense fact. Deliberately a small,
reviewable, revertible move, landing as one pair with H1.
The instrument is src/migrate-gorelease.ps1 (.bat launcher
beside it). A bare run is a census: it classifies every place in the tree that spells or derives
the Go release into five classes — source-of-truth, doc-statement, derived-by-regen,
derived-at-runtime, must-not-change — and changes nothing. With -To <release> -Apply it performs
exactly two of those classes: the pin itself (<GoStdLibVersion>, the build-number reset, and H1.2’s
go directive in src/go2cs/go.mod, which -SkipGoMod leaves alone) and the prose that states the
release as present-tense fact, each by a named anchor whose match count is asserted rather than
substituted blindly. It supports -WhatIf, refuses to run when the working tree is dirty in the
files it would touch, and re-reads its own output afterwards to prove zero sites remain — so it is
idempotent, and re-running it is the verification. Its discovery sweep reports anything it cannot
classify as UNCLASSIFIED rather than guessing, which is how a newly-introduced site announces
itself at the next migration instead of being missed.
What it does not do, and will not pretend to: it does not reconvert (it prints the seeded
reconvert and the layout-L3 multi-target emission for H5/H8), it runs no gate, it does not touch the
roster’s rows or arithmetic, and it makes none of the migration’s judgements — H3’s package census,
H6’s hand-own differential, §4’s golden-drift triage and H10’s per-row re-derivation are all
readings a person makes. It also leaves the converter tool version alone: that is
set-version.ps1’s Windows PE resource and is independent of version.props.
Gate: a single-package -stdlib smoke conversion no longer refuses.
Ruling 2026-09-08 — the H2→H5 window: the converter at go1.24.13, the corpus still at 1.23.12
H2 bumps the corpus pin; H1 step 2 has already moved the converter module’sgo directive, so the
tree carries two releases until H5’s regen closes the window. At master f4d2b981b (train 43)
src/go2cs/go.mod requires go1.24.13 while src/version.props’s <GoStdLibVersion> still reads
1.23.12 — the converter’s build toolchain hopped, the corpus release did not. ⚠ H1 does not rule this,
and the runbook did not state it before now — H1 is a five-step list whose steps 2 and 3 rule the pin bump
alone — but its step 1 warning block supplies the mechanism: a hop leg sets bothGOTOOLCHAIN and
GOROOT per invocation, and -stdlib converts the tree the ENVIRONMENT names, which is why the pipeline
under a 1.23.12 GOROOT converts 1.23.12 sources. Ruled 014bfe84f, corrected 84b5913098, measured
e96349c54.
Both arms, measured at f4d2b981b: building src/go2cs under the 1.23.12 pin exits 1 with no binary
(go: go.mod requires go >= 1.24.13 (running go 1.23.12; GOTOOLCHAIN=local)); the same build under the
1.24.13 pin exits 0. Arm 2 is what makes the refusal a toolchain fact rather than a broken build.
The converter suite runs under the 1.24.13 pin — GOROOT=<sdk>/go1.24.13, its bin first on PATH,
GOTOOLCHAIN=local, bare go versionasserted rather than merely printed. The behavioral suite and CNR run
under the two-pin PAIRING — the fifth arm below — since 2026-09-08; the sentence that stood here until then ran
them under the 1.24.13 pin, which reads the mixed-state artifact the fourth arm names.
⚠ Two staleness guards, and the INVOCATION decides which applies.go2cs -tests invoked DIRECTLY meets
only the converter’s own mtime guard (converterStaleness.go), so a freshly built 1.24.13 binary passes under
either shell. Anything HARNESS-driven — BehavioralRunner, MSTest, PerformanceRunner, the sweep’s own build
step — meets ConverterBuildInputs.IsConverterStale, comparing the binary’s embedded release against the live
GOVERSION: a 1.24.13 binary reads STALE in a 1.23.12 shell, the harness rebuilds, and that rebuild REFUSES.
-tests rows and run-validated-sweep.ps1 against the still-1.23.12 corpus, in order:
1 build src/go2cs under the 1.24.13 pin exit 0, binary produced
2 run rows under the 1.23.12 pin, -SkipBuild MANDATORY guard passes: 1.23.12 == version.props
3 assert the comparison record's oracleGoVersion reads go1.23.12
⚠ Step 2’s “under the 1.23.12 pin” is the ENVIRONMENT, not the flag.-goroot selects the corpus SOURCE
tree and does not isolate the converter’s package loader: the ambient GOROOT leaks into go/packages’
resolution of internal/abi, so a -tests run issued from the shell that BUILT the converter (1.24.13 still
exported) fails the runtime row with ~150 errors shaped like undefined: abi.MapBucketCount and use of
internal package internal/abi not allowed — a wall that impersonates a corpus break at exactly the moment a
pin moved. Measured one-variable by i9 (1cf3af363): identical command line, -goroot <sdk>/go1.23.12 in
both runs; ambient GOROOT 1.24.13 → rc 1, nothing emitted; ambient 1.23.12 → rc 0, clean. G (072c283023)
places it as H1 step 1’s ruled mechanism — -stdlib converts the tree the ENVIRONMENT names — reaching the
-tests driver, plus the half H1 does not say: the flag does not override the environment. So the converter
build and the corpus run happen in SEPARATE shells, or the run re-exports GOROOT and PATH to the corpus pin
before invoking go2cs; -goroot alone is not the pin.
⚠ -SkipBuild is mandatory, not stylistic. The sweep’s toolchain guard does not refuse the mixed state, it
requires it — throwing when the running release differs from version.props, so it passes under 1.23.12
and throws under 1.24.13 — and none of its four switches touches the pin. But its line ~334 is
if (-not $SkipBuild) { … }, so a bare sweep builds the converter under the 1.23.12 pin and dies at the
refusal above before a row starts.
⚠ A pre-hop-pinned instrument is UNBUILDABLE from master in this window, so a reading taken with one is
tree-locked to the pre-train-43 checkout it was built from — R’s 6 VALID / 12 HOOK-ONLY / 10 GENUINELY STALE
base classification (daa57a1f9) is one. Re-measuring from master necessarily uses a 1.24.13-built front end:
a different instrument, named with its pin on both sides, never a refutation.
Train batteries take the same split per LEG. Train 43’s assembly pinned each leg
(coord-train43-assemble.sh), with a negative control that a module declaring go 1.24.13 must REFUSE under
the 1.23.12 pin; a cost-canary or sweep leg here takes the two-pin shape above, or is stated UNMEASURED.
⚠ Fourth arm (2026-09-08, i9 0858372b5, three arms with the converter binary held byte-identical): under the 1.24.13 run pin the behavioral CNR and the behavioral suite read a MIXED-STATE emission on runtime-importing projects. The Δ on the runtime PACKAGE alias is decided from the loaded closure — i.e. from the run GOROOT — so the same binary emits using Δruntime = runtime_package; under a 1.23.12 environment and the bare using runtime = runtime_package; under 1.24.13; the bare form does not compile against the 1.23.12 corpus (CS0576, namespace go holds a runtime definition). Exactly EIGHT behavioral goldens read CHANGED under the 1.24.13 pin (35 lines, every one the Δ-drop, nothing else), and those eight fail Target AND Compile there — a named EXPECTED SET for any battery leg run under that pin, any other member a finding. Under the two-pin pairing (converter built at 1.24.13, environment re-exported to 1.23.12) all eight are byte-identical to the goldens committed at f4d2b981b. Consequences ruled: goldens are re-baselined at H5 with the hopped corpus in the reference graph, never in the window — H9 as planned for the window is DEFERRED to H5 (a golden re-baselined under a run GOROOT that differs from the corpus’s release records the emission for a corpus that does not exist, and Target green + Compile red is its tell); the two-pin pairing is the zero-drift instrument of record for a converter change in the window; and the closure predicate behind the stamp is named (measured on the i7 the same night, five arms, one binary): computeImportAliasRenames in src/go2cs/importAliasOperations.go records every non-final import-path prefix of the go/packages closure as a child namespace and renames a direct import whose name collides — so it reads the Go closure at the LOADER’s release while the collision is decided by the C# namespace set of the corpus’s transitive reference closure at the CORPUS’s release; the two agree iff the releases agree. The eight collide on the ANCESTOR of namespace go.runtime.@internal; (declared by runtime/internal/{math,sys}, which the 1.23 core/runtime.csproj references and which Go 1.24 moved to internal/runtime/*). Ruled fix (G’s cut, rides the train after 44): the child-namespace set is the UNION of the Go closure and the referenced corpus’s transitive csproj closure under -go2cspath (exact, never directory existence), falling back to the Go closure where no corpus csproj exists; a decision-level guard over synthetic closures plus a fixture corpus tree; acceptance is CNR under the 1.24.13 pin reading 0 CHANGED on the 1.23.12 corpus. At H5 the eight drop the Δ legitimately (none imports a runtime/<sub> package) and re-baseline there.
⚠ Fifth arm (2026-09-08, measured on two hosts): CNR and the behavioral suite run under the two-pin PAIRING — environment re-exported to 1.23.12 (GOROOT, its bin first on PATH), GOTOOLCHAIN left at auto — with ZERO drift as the expectation, and the eight-member artifact set is retired from batteries. The module graph enforces the pairing: src/go2cs/go.mod says go 1.24.13 while every corpus module says go 1.23, so Go’s toolchain rule switches ONLY the converter’s build up and every behavioral or corpus package loads at 1.23.12. CNR under that shell read 722 byte-identical / 0 NOT MEASURED with the converter still go1.24.13 afterwards (i9 ef05467a3); the behavioral runner read all eight artifact projects green in all four phases, Δruntime present in every emission, goldens byte-matched, tree clean (the i7). Five mechanics ride with it. (1) The runner’s staleness predicate reads go env GOVERSION at the RUNNER’s cwd (src/tests/Behavioral, no module: 1.23.12) against the binary’s embedded 1.24.13, so it REBUILDS the converter on every invocation — ~1.8 s, the content-addressed cache re-links only — which fails safe (never a stale binary, never a Transpile skip); CNR is immune by its unconditional go build. (2) The pin is ASSERTED from a directory with NO go.mod: inside src/go2cs both go version and go env GOROOT report the SWITCHED toolchain under auto while command -v go still resolves under the pinned root (i9 0eef5b66c), and the produced binary is verified with go version <exe>, which no cwd can switch. (3) GOTOOLCHAIN=local BREAKS the single-root pairing outright (the converter cannot be built under the run pin); a SPLIT pin naming GOROOT_BUILD=<sdk>/go1.24.13 on each build and GOROOT_CONVERT=<sdk>/go1.23.12 on each conversion needs no switch and works under either setting (G 8f73ed9a6) — the right spelling for a hand-invoked two-arm instrument. (4) The conversion half still reaches go one process down — the converter’s package loader shells out to it — so a module declaring ABOVE the convert pin switches SILENTLY under auto (a probe declaring go 1.24.13 loaded the 1.24.13 stdlib with GOROOT naming 1.23.12; under local it refused verbatim; i9 28b5ba6b4): the corpus is held at 1.23.12 by its own go 1.23 directives, not by the naming of roots, and an end-user -recurse module or a hopped corpus against a stale pin is exactly the case that moves. (5) The switch resolves through the MODULE CACHE (golang.org/toolchain@v0.0.1-go1.24.13), so a cold or offline box fetches at that point. The 1.24.13-pinned CNR stays as the alias defect’s own instrument — the fix’s acceptance is that reading dropping from eight to zero — never as a battery leg.
H3 — Package census ⟲
Diff the conversion queue’s package set against the outgoing corpus: added, removed,
renamed or promoted, and experiment-gated and therefore deliberately absent. The last category
matters as much as the others: experiment-gated packages stay out until they graduate to the default
package set (ruled), and naming them explicitly is what stops a later reader re-diagnosing a
“missing package”.
Deliverable: a census document under docs/phase4/, in the shape of the existing census docs. A
patch-level migration should produce an empty census; a non-empty one is a finding.
H4 — Converter feature work GATE
Whatever the release’s language delta requires, plus whatever the census surfaced. Each item follows
the standing repository discipline: root-cause against emitted .cs, land a behavioral regression
test, update the conversion-strategy reference (and the summary only if the headline mapping moved),
and prove check-no-regression clean on the outgoing corpus where the change is meant to be
neutral.
Two recurring work items belong here by ruling rather than being discovered as audit findings:
The hand-owned test host.src/core/testing is skip-listed and never converted, so it follows
nothing automatically while upstream keeps adding to testing’s API. It is a named work item of
every migration that adds one.
The go.mod readers. New go.mod verbs are silently dropped by lax parsing, so any new
directive in the target release owes a re-check of the converter’s go.mod handling rather than an
assumption of safety.
Gate: CNR byte-identical over the full behavioral corpus, zero NOT MEASURED. Budget from
CLAUDE.md’s check-no-regression.ps1 row, from the top of its range.
H4a — The opening deliberate-regen slot
A standing slot, not a step with fixed contents. The repository accumulates queued leveling
items: converter emission changes that landed without their corpus regen, born-stale banked
artifacts, and cosmetic emission nits explicitly deferred to “the next deliberate regen”. Each is
individually too small to justify a full reconvert, and the standing rule is restore rather than
level until one regen can carry them all.
The queue is a LEDGER, not a memory, and it lives in three places a migration reads together:
CleanupBacklog.md (numbered housekeeping items), the unbanked intended
drift inventory under docs/phase4/ (converter arcs that landed without their regen, each row
carrying the evidence checkable against the committed tree today), and the BOARD’s standing
born-stale, restore rather than level entries, which name each deferred artifact at its banked
counts. A queued item recorded in none of them is one nobody will find at the regen — so
deferring to “the next deliberate regen” is not complete until the deferral has a ledger row.
The hand-own census this slot always runs: for every marked hand-own, did the hop move any of its
principal’s declarations into a file the new release newly selects and the reconvert therefore emits?
A hand-own whose Go principal shrank between releases has a twin in the emission: the declaration
the hand-own still carries is now also auto-converted from the file it moved to. The answer is a
per-declaration body displacement (manualConversionFuncs), never a field or a file exclusion, and
it is hop-conditional by construction — the registration’s guard is red at the old release, so it
lands with the hop on the version branch, never on master ahead of it.
A corpus migration is that regen. Schedule the bundle before H5, for one reason: H5’s overlay
diff is the migration’s primary signal, and every un-levelled artifact is noise inside it. Levelling
first is what makes the upstream delta readable.
The bundle owes, in one commit series:
every queued converter emission fix, each with its own CNR;
a seeded full reconvert;
go generate . in src/go2cs — stdlib-metadata.txt is generated FROM the corpus and gated
by TestStdLibMetadataInSync under the plain converter go test, so a regen banked without it
leaves the converter gate red at master for whoever runs it next, not for the lane that caused
it;
the born-stale rows re-swept at their banked counts — the whole point of the class is that the
staleness is emission drift, not a verdict change, so the sweep is unaffected.
Any small deferred housekeeping that needs a quiet point (unregistered solution members, and the
like) rides here too.
Amendment 2026-09-13 — the pin assertion, both halves and the pairing, as copyable blocks
H4a and every step of the H5 series are run by hand, so no harness pins them. Placeholders used from here to H9:
placeholder
meaning
defined from
<landing>
master after the train-47 landing, its tree hash asserted (git rev-parse <landing>^{tree}) before use
H4a
<tree>
a clean detached worktree: of <landing> at H4a; of the version branch at <H2> from H2 onward
H4a (<landing>), H2 (<H2>)
<H2>
the version branch’s H2 commit, named by COORD. It does not exist at H4a and no H4a step reads it
H2
<GOROOT-1.23.12>, <GOROOT-1.24.13>
spelled exactly as go env GOROOT prints them, backslashes and all (floor 6)
—
<GOROOT-…-posix>
the same roots in the shell’s spelling (/c/...)
—
<stage>
a drive-letter, forward-slash directory outside every clone, with no module above it; <stage-posix> its /c/... spelling
H4a
<build root>
<stage>/h5 for the ladder; <tree> for the gate (the per-flavour build amendment under H7)
H7
No path contains a space. Each step is a Git Bash script with set -uo pipefail and export
MSYS_NO_PATHCONV=1 (no -e), so every call below is written || exit 3: a function’s return 3 stops
nothing by itself (floor 7).
pin (){# pin <release> <GOROOT, backslash spelling as `go env GOROOT` prints it> <the same root, POSIX>export GOROOT="$2"GOTOOLCHAIN=local PATH="$3/bin:$PATH"local d m v r f;d=$(mktemp-d'<stage-posix>/pin.XXXXXX')||return 3
m=$(cd"$d"&& go env GOMOD);v=$(cd"$d"&& go version | awk'{print $3}')r=$(cd"$d"&& go env GOROOT);f=$(head-n1"$3/VERSION" | tr-d'\r')echo" pin: taken from ${d##*/} under <stage> (GOMOD=$m), in this script's environment: $(cd"$d"&& go version); VERSION $f"rmdir"$d"case"$m"in''|NUL|/dev/null);;*)echo"ABORT: the pin directory is inside a module";return 3 ;;esac["$v"="$1"]&&["$f"="$1"]&&["$r"="$2"]||{echo"ABORT: pin is not $1";return 3;}}
pin_pair (){# the H2->H5 window pairing at go1.23.12: pin_pair <GOROOT-1.23.12> <GOROOT-1.23.12-posix>unset GOTOOLCHAIN;export GOROOT="$1"PATH="$2/bin:$PATH"local d m v r f t;d=$(mktemp-d'<stage-posix>/pin.XXXXXX')||return 3
m=$(cd"$d"&& go env GOMOD);v=$(cd"$d"&& go version | awk'{print $3}')r=$(cd"$d"&& go env GOROOT);t=$(cd"$d"&& go env GOTOOLCHAIN);f=$(head-n1"$2/VERSION" | tr-d'\r')echo" pin_pair: taken from ${d##*/} under <stage> (GOMOD=$m, GOTOOLCHAIN=$t): $(cd"$d"&& go version); VERSION $f"rmdir"$d"case"$m"in''|NUL|/dev/null);;*)echo"ABORT: the pin directory is inside a module";return 3 ;;esac["$t"= auto ]||{echo"STOP: GOTOOLCHAIN reads $t (a user-level go env -w value): post it; never go env -w on a fleet box";return 3;}["$v"= go1.23.12 ]&&["$f"= go1.23.12 ]&&["$r"="$1"]||{echo"ABORT: pairing is not go1.23.12";return 3;}}
Build half — pin go1.24.13 '<GOROOT-1.24.13>' '<GOROOT-1.24.13-posix>' || exit 3, then build (the
seeded-roots amendment under H5; src/go2cs/go.mod already reads go 1.24.13 at a02ac3df3); after it,
go version <exe> reads go1.24.13.
Convert half, H5 — the same pin, || exit 3. Convert half, H4a — pin go1.23.12 '<GOROOT-1.23.12>'
'<GOROOT-1.23.12-posix>' || exit 3 in a SEPARATE script: the split pin. The converter is never built in a
convert-half shell.
Window batteries (CNR and the behavioral suite while the corpus is still 1.23.12) take pin_pair … || exit 3.
-tests rows and the sweep take the split instead: the converter built under pin go1.24.13, the pipeline
run under go1.23.12, and -SkipBuild MANDATORY for the sweep (this document’s H2 ruling, the two staleness
guards; KICKOFF-fleet.md:176).
After every conversion, read what the converter read. On a converter carrying 7c1d8832f
(git -C '<tree>' merge-base --is-ancestor 7c1d8832f HEAD), its log’s toolchain: GOROOT … (VERSION <rel>,
read in-process) line names <rel>. 7c1d8832f is in master since train 48 (271300cea0 and after; the
provenance line lives at src/go2cs/toolchainResolution.go), so a <landing> at or after it always has the row; on an
older tree the line does not exist, the row is NOT AVAILABLE, and the first arm is the pin assertion plus the mcleanup
presence arm — posted as the weaker instrument. Post the bare go version line
and the VERSION token — never a GOROOT value.
Amendment 2026-09-13 — for the 1.23 → 1.24 hop, H4a is a staging BASELINE, not a landing
The outgoing record is frozen at its anchor, so a levelled 1.23.12 corpus has nothing to publish or bank.
H4a runs as ONE seeded three-target 1.23.12 regen into a staging root, on <landing> as its <tree>, by the
same converter binary that runs H5, and nothing from it is committed. That root is at once (a) H0’s fresh
.cs.auto baseline, (b) H6’s old-side .auto, and (c) H5’s overlay comparand: master → H4a is the queued
levelling noise, H4a → H5 the upstream delta. Of the bundle above: item 2 (the seeded full reconvert) runs
as this staging baseline and is not committed; item 3 (go generate .) moves to the H5 series (the overlay
amendment under H5); item 4’s three ledgers are consumed in H5’s triage of master → H4a, not swept at 1.23.12;
item 1’s disposition (queued fixes, each with CNR) is not ruled for this hop — owed to COORD, and not run
until it is.
Entry gate. H4 closed: every converter cut this hop needs has landed on <landing>. Any later change
under src/go2cs or src/gen (read at H5 by git diff --quiet <landing> <H2> -- src/go2cs src/gen) means the
binary is not H5’s: re-run H4a from <H2>’s converter into a fresh <stage>, never reuse this one.
Build the binary once and seed BOTH roots from <landing> — the seeded-roots amendment under H5. The H4a
root’s version.props is <landing>’s (1.23.12); the H5 root’s is written at H5 from <H2>.
pin go1.23.12 '<GOROOT-1.23.12>' '<GOROOT-1.23.12-posix>' || exit 3, then the reconvert amendment’s command
under H5 with R=h4a.
That amendment’s checks, with two expectations inverted: runtime/mcleanup.cs ABSENT, the provenance line
(where available) naming go1.23.12. H4a is run A of H6’s named blind spot: all three Failed: lines
read 0, its marker gate reads zero violations, and its package count is asserted against the outgoing
corpus’s before any H6 or H5 use:
want=$(git -C'<tree>'-c core.quotePath=false ls-files --'src/core/*package_info.cs' | wc-l)for g in windows linux darwin;do echo" $g: package_info.cs in stage $(find '<stage>/h4a-stage/'"$g"'-amd64'-name package_info.cs | wc-l) of $want tracked";done
Want: every difference NAMED (a skip-listed package, a platform-exclusive package of another flavour, a
per-GOOS folder); an unnamed difference is a STOP. The predicate is NOT MEASURED against a real stage root.
Keep <stage>/h4a whole, .cs.auto included, until H6’s audit closes. Never convert into it again (floor 1).
H4a’s counts are recorded in its post and the share manifest (the artifacts amendment under H5); nothing is committed.
H5 — Seeded full reconvert GATE
CLAUDE.md’s reconvert ritual, unchanged and unabridged. A migration is the most likely moment to
skip a step of it, so the non-negotiables are restated rather than referenced:
Seed first. Copy src/core, src/version.props and docs/validation into the staging root,
mirroring the src/ layout, and convert with -go2cspath <staging>/src. An unseeded root gives the
hand-own marker nothing to detect, so every whole-file hand-own is emitted as a plain .cs and the
overlay rule protects nothing — the auto conversions compile and are operationally broken. Since
the per-GOOS corpus layout landed, an unseeded root also breaks layout adoption: there is no
per-GOOS folder to route into, so every platform-varying file lands flat and the next build compiles
two copies.
A BUILD of the staged corpus needs two more things seeded: src/gen and
src/Directory.Build.props. The seed list above is what a conversion needs; without the generator
project every generated half is missing, and the failure reads exactly like a corpus defect (62
CS8795/CS1739/CS0029 errors in internal/runtime/atomic and internal/goarch, upstream of every site
the reading set out to reproduce). The tell is location, not count: a reproduction that fails
somewhere other than the sites it set out to reproduce has not reproduced.
Never convert twice into one staging root, and never let two conversions overlap in one. Delete
and re-seed per run, and confirm no converter process is alive before starting. The recorded failure
is a single corrupted file with unresolved lift markers that reads exactly like a converter
regression and is not one.
Wrap the converter call so its stderr warnings do not abort the wrapper — a terminating
error-action policy turns a native stderr line into a fatal, which is how the overlapping-run
corruption happened in the first place.
The marker gate is PATH-PRECISE, line-anchored, whole-file, and re-measured ⟲. Per marked path,
the staging root must not hold a freshly-emitted plain .cs — either a .cs.auto sits beside
it, or nothing was emitted there. Counts intentionally differ from the census, in both directions,
so a same-count assertion is wrong. Three census traps, each paid for: a head-window scan
under-counts badly (markers sit below long license blocks); an unanchored match over-counts
(placeholder comments mention the marker); and a default ripgrep honors src/core/.gitignore and
under-counts — census with git grep or a raw filesystem walk.
Classify emitted-vs-seeded by a sentinel modification time, not by content: seeding puts every
repository file in the staging root, so an overlay can never reveal a file the converter has
stopped emitting unless the classification is time-based. A hop’s corpus-side DELETION bill is
a first-class number, and this classification is the only thing that can see it. The 1.24 trial
measured 31 files — 28 whose principal Go file is gone, 2 build-tag flips (sync/map.cs among
them), 1 other — and an unclassified stale sibling is not a diff but a COMPILE ERROR: the
aliastypeparams baseline flip emits the _on file while the seed still holds the _off one,
i.e. CS0102. State the bill with the emission census; do not discover it at the build.
Overlay .cs, .csproj and README.md, excluding *.cs.auto. Two knowns that are not drift:
the root attribution files the converter re-copies (modified with an empty numstat — pure
line-ending phantoms, restore them), and the hand-owned-by-consequence packages, whose single Go
file is entirely hand-owned so the driver never reaches project-file emission and their .csproj,
package_info.cs and README.md are never re-emitted. A migration that adds a package to that
class must notice.
Gate: overlay completes with the marker gate at zero violations and every diff classified
(§4).
Amendment 2026-09-07 — the DELETION PASS is a required step, and it has an instrument
The bullet above states the deletion bill as a number to report. The 1.24.13 rehearsal
(docs/phase4/REHEARSAL-h5-go124.md §3) showed that reporting it is
not enough: the stale files have to be removed from the staging root before the overlay, and nothing
in the ritual performed a deletion. That rehearsal’s first build died in 116 s having measured
nothing — internal/goexperiment/exp_aliastypeparams_off.cs (seeded, 1.23.12) and
exp_aliastypeparams_on.cs (emitted, 1.24.13) both declare AliasTypeParams, the package csproj globs
*.cs so both compile, and the result is CS0102 ×2 in a leaf essentially the whole corpus depends
on. A stale sibling is not a diff to classify at leisure; at a hop it is a compile error in front of
every other measurement.
So H5 gains a step, between the reconvert and the overlay:
H5c — deletion pass. Run src/reconvert-deletions.ps1 (launcher reconvert-deletions.bat)
against the staging root, dry-run first, then with -Apply. Only then overlay.
The sentinel is a file created immediately before the conversion starts; its modification time is
the seeded/emitted boundary (-SentinelTime <datetime> is the same input without a file).
-SourceGoRoot is the outgoing release’s GOROOT — the one the committed corpus was converted
from — and it decides what is a deletion candidate at all (see What makes a file a candidate
below). Its expected release is derived from <GoStdLibVersion> in src/version.props unless
-ExpectSourceGo is passed. The instrument refuses — exit 3, before printing any table — on a
missing or ambiguous sentinel, on a -Root that resolves to the repository’s own src/core, or
when go version under eitherGOROOT disagrees with its expected release. A deletion pass
aimed at the wrong release deletes the wrong files, so that is a refusal and not a warning.
Why the modification time alone cannot decide a deletion, and Go must be asked. The converter’s
write path skips a write whose bytes are identical (needToWriteFile), so a file whose emission did not
change between the two releases keeps its seed timestamp and reads SEEDED exactly like a file that
stopped being emitted. The rehearsal measured 1,292 seeded-not-rewritten production .cs against
25 real deletions — the seeded set is ~50× the deletion set, and a timestamp-only pass would destroy
the corpus. The timestamp answers only “is this a candidate”. Go answers “should it exist”, via
go list -f ' ' <importpath> under the target GOROOT with CGO_ENABLED=0
(the corpus’s own emission state), GOTOOLCHAIN=local, and the file’s own GOOS.
What makes a file a CANDIDATE — the question the pass got wrong on its first real run, corrected
2026-09-07. “Does Go still select this file’s principal at the target?” is only meaningful for a
file the converter emits. As first landed the pass asked it of every seeded .cs, and lane R’s first
dry run against a three-target scratch (mailbox 1f5e8f276, dry run, not applied) returned a
205-row delete set of which 117 rows were src/core/golib/*.cs (116) and src/core/go2cs/Symbols.cs
(1) — the hand-written runtime and the Symbols shared project — classified DELETE-ABSENT because
“package not in std at target” is true and irrelevant for a directory that was never a Go package.
The marker arm could not save them: golib correctly carries no [module: GoManualConversion] marker,
because nothing converts into it and there is no generated body for a marker to displace. The one
directory that needs no marker is the one the marker guard does not protect.
The candidate test is therefore positive and first: a seeded file is a candidate only if its
resolved import path is in go list std at the SOURCE release (-SourceGoRoot, per flavour) and
is not skip-listed by the converter’s own isNonConvertedStdLibPackage. Asking the source
rather than the target is what keeps a removed package a candidate — internal/weak is in std at
1.23.12 and gone at 1.24.13, which is exactly the DELETE-ABSENT the pass exists to find.
The classes, each printed with its count whether or not it is zero:
class
meaning
deleted?
NOT-A-CONVERSION-TARGET
the converter does not emit into this directory at all: a hand-written repository root (golib/, go2cs/), a std package the converter skip-lists (unsafe, builtin, testing, cmd…), an import path absent from std at the source release, or a .cs sitting directly in core/
never — tested first, ahead of the marker scan and any target lookup
PROTECTED
line-anchored [module: GoManualConversion], or an *_impl.cs companion, inside a package the converter does emit
never — a hand-own is an H6 reconciliation item, not a deletion
KEEP-SELECTED
Go still selects the principal at the target for this flavour
no — the dominant class, and the pass’s own negative control
DELETE-ABSENT
the principal, or its whole package, is gone at the target (H3 removals)
yes
DELETE-DESELECTED
the principal still exists on disk but Go does not select it for this flavour — a build-tag or GOEXPERIMENT flip
yes
UNRESOLVED
no Go principal is derivable inside a package the converter emits — generated metadata (package_info.cs, package_init.cs) and anything else whose stem maps to no .go name
never
UNRESOLVED stops the step for a human. Those rows are always listed, and the class is not
hypothetical and not automatable from a file name: the rehearsal’s 25 contains exactly one,
crypto/ecdh/package_init.cs, a genuinely stale generated file whose staleness only a reader can
confirm. Deleting it on a guess and dropping it silently are both wrong; the pass does neither and
refuses to report success until somebody has disposed of it.
Exit 2 means NOTHING WAS DELETED. As first landed the UNRESOLVED check ran after the
Remove-Item loop, so a run that exited 2 had already deleted — a report wearing a refusal’s exit
code, measured at HEAD as 7 files removed on an exit-2 run, golib/ and go2cs/ among them. Every
check that can produce exit 2 now runs first: the UNRESOLVED check, the delete-set decomposition
(printed per class before the loop), and a trespass assertion — no delete row may sit under a
hand-written root or a skip-listed package directory, re-derived from the path rather than from the
go list that classified it, and re-asked per row immediately before each irreversible act. With
-Apply, UNRESOLVED rows are a refusal rather than a footnote: dispose of them, then re-run.
The skip-list is a MIRROR and it is guarded.go list std names unsafe and testing like any
other package, so no Go question can exclude them; the converter’s isNonConvertedStdLibPackage
(src/go2cs/stdLibConverter.go) is the only authority and the script carries a copy of it.
reconvertDeletionsSkipList_test.go, under the plain go test ./... in src/go2cs, extracts the
script’s literal and compares the two sets in both directions — an extra name in the script keeps
stale files that should go, a missing one offers a hand-owned package’s files for deletion. Because the
.ps1 sits outside the converter’s module root, cmd/go drops it from the test’s input fingerprint:
any change to the script owes go test -count=1 ./....
Files the run emitted are not candidates at all, and neither are the <Compile Remove>d test-host
artifacts (package_test_info.cs, go2cs_test_host.cs, *_test.cs) — a stale one cannot produce the
CS0102 this pass exists to prevent, and admitting them buries the real rows (the rehearsal subtracted
384 of them from the same arithmetic). They are counted, not listed.
Two notes on reading the bill. The -platforms multi-target emission is what H5 actually runs
(§H8’s requirement, restated in the rehearsal’s §8), and a single-target run recomputes only its own
flavour — so a deletion pass over a single-target root answers only for that flavour, and an L3 per-GOOS
folder is asked under its ownGOOS regardless of the run’s target. And this amendment does not
revise the bullet above: that bullet’s 31 is an earlier trial’s reading and stands as its own dated
measurement; the rehearsal’s 25 is a different run of a different release with a different
instrument. Neither supersedes the other, and both are point-in-time — re-measure, never carry the
count.
Amendment 2026-09-13 — the seeded roots, exactly: two roots, one seed, one binary
Preconditions, at H4a.git -C '<tree>' status --porcelain empty and <tree> at <landing>; ≥ 25 GB free
on <stage>’s drive (floor 12); no converter alive on the box —
powershell -NoProfile -Command "@(Get-Process go2cs -ErrorAction SilentlyContinue).Count" reads 0, a count
and never a kill (floors 1, 5). Preconditions, at H5 (once <H2> exists): <tree> re-pointed at <H2>,
clean; git -C '<tree>' diff --quiet <landing> <H2> -- src/core src/gen src/Directory.Build.props docs/validation
(H2 moves none of the seed, so the seed taken at H4a serves both releases; non-quiet is a STOP, posted by file
list); and the two commits the H5 steps read are in <H2>:
git -C'<tree>' merge-base --is-ancestor 826045a74 '<H2>'||{echo"STOP: h5-removals.txt (826045a74) not in H2";exit 3;}
git -C'<tree>' merge-base --is-ancestor 7c1d8832f '<H2>'&&echo" provenance line AVAILABLE"||echo" provenance line NOT AVAILABLE (7c1d8832f not in H2)"
Build half, at H4a (pin go1.24.13 … || exit 3, the pin-assertion amendment under H4a):
mkdir-p'<stage>/bin''<stage>/logs';[-e'<stage>/bin/go2cs.exe']&&{echo"ABORT: a binary already stands";exit 4;}(cd'<tree>/src/go2cs'&& go build -o'<stage>/bin/go2cs.exe'.);rc=$?["$rc"= 0 ]&&[-f'<stage>/bin/go2cs.exe']||{echo"ABORT: build rc=$rc, or no binary at the invoked path";exit 4;}["$(go version '<stage>/bin/go2cs.exe' | awk'{print $2}')"= go1.24.13 ]||{echo"ABORT: embedded toolchain";exit 4;}(cd'<stage>/bin'&&sha256sum go2cs.exe > go2cs.exe.sha256 )||exit 4
Built ONCE. Before each conversion the reconvert amendment copies the checked hash to <stage>/<R>.exe.sha256;
before H6 and before the d-hop comparand, cmp '<stage>/h4a.exe.sha256' '<stage>/h5.exe.sha256' must pass.
Seed BOTH roots at H4a, before either converts:
for R in h4a h5;do[-e"<stage>/$R"]&&{echo"ABORT: <stage>/$R exists";exit 5;}mkdir-p"<stage>/$R/src""<stage>/$R/docs"(cd'<tree>/src'&&tar-cf - --exclude=bin --exclude=obj --exclude=Generated core gen ) | (cd"<stage>/$R/src"&&tar-xf - )||exit 5
cp'<tree>/src/Directory.Build.props'"<stage>/$R/src/"||exit 5
(cd'<tree>/docs'&&tar-cf - validation ) | (cd"<stage>/$R/docs"&&tar-xf - )||exit 5
done
git -C'<tree>' show '<landing>:src/version.props'>'<stage>/h4a/src/version.props'||exit 5
want=$(git -C'<tree>'-c core.quotePath=false ls-tree -r--name-only HEAD -- src/core | grep-c'[.]cs$')for R in h4a h5;do
have=$(find "<stage>/$R/src/core"-name'*.cs' | wc-l)echo" $R: seeded .cs $have of $want tracked"["$have"="$want"]||{echo"ABORT: $R seed is partial";exit 5;}done
pv (){grep-oE'<GoStdLibVersion>[^<]+'"$1" | tail-n1 | sed's/.*>//';}["$(pv '<stage>/h4a/src/version.props')"= 1.23.12 ]||{echo"ABORT: h4a pin";exit 5;}
The version.props pin is ASSERTED, never only printed: a root whose pin cannot be read runs the corpus pin guard
inert.
| seed member | why it is there |
|:–|:–|
| src/core minus bin/obj/Generated | floor 2: the marker detector and layout L3’s per-GOOS routing need it |
| src/version.props | the corpus pin guard reads it beside core; H5’s root takes <H2>’s file — the pin AND the reset build number the emitted badges read — never a sed of the pin line; H4a’s takes <landing>’s |
| docs/validation | the README Tests and Source·C# badges read it with version.props; without either, both badges vanish corpus-wide |
| src/Directory.Build.props, src/gen minus build dirs | to BUILD the root: core/Directory.Build.props imports the file above it (the TFM) and resolves the analyzer at $(go2csPath)gen/go2cs-gen, which the generated solution also lists |
Amendment 2026-09-13 — the three-target reconvert: command line, sentinel, checks
Convert half (pin go1.24.13 … || exit 3 for R=h5; pin go1.23.12 … || exit 3 in a separate script for R=h4a):
below: the box’s own GOROOT basenames, not a guessed name shape
0 before any overlay (0 read on lane R’s box, 2026-09-13: a reading, not a want)
marker gate
below
0 violations, 0 missing
n=0;for b in"$(basename'<GOROOT-1.23.12-posix>')""$(basename'<GOROOT-1.24.13-posix>')";do
n=$(( n +$(grep-rlF--include=package_info.cs "$b""<stage>/$R/src/core" | wc-l)));done;echo" metadata naming a Go root basename: $n"MARK='^[[:space:]]*[[][[:space:]]*module[[:space:]]*:[[:space:]]*(go[.])?[[:space:]]*GoManualConversion(Attribute)?[[:space:]]*[]]'
git -C'<tree>'grep-l-E"$MARK"--'src/core/*.cs'>"<stage>/logs/$R-marked.txt"v=0;while IFS=read-r p;do
f="<stage>/$R/$p"[-f"$f"]||{echo" MISSING $p";v=$((v+1));continue;}if["$f"-nt"$SENT"]&&!grep-qE"$MARK""$f";then echo" VIOLATION $p";v=$((v+1));fi
done < "<stage>/logs/$R-marked.txt"echo" marked paths $(wc-l < "<stage>/logs/$R-marked.txt"); violations $v"
A GOROOT basename that is a common word (go) makes the metadata row meaningless: post the basenames’ shape
(never their value) and read the row as NOT AVAILABLE.
Amendment 2026-09-13 — H5c as a procedure: both GOROOTs, the fourteen as the EXPECTED set, and the executable delete until the instrument amendment is seated
A seeded root keeps every file the new pin no longer emits. Nothing in the reconvert or the overlay removes one;
that is why this step exists. The H5c filter keys on the two-pin difference or the .cs.auto sibling, never on a
file name. The hazard is a NAME-keyed filter in a hand-built keep or survivor list: sort/sort_impl_go121.cs is
converted output (Go’s own sort_impl_go121.go, retired at 1.24), and a keep list built on *_impl* would keep
it. The instrument’s own name test (*_impl.cs) does not match that file; its dry run classifies it
DELETE-ABSENT (principal removed at target).
1. Inputs, before the first run. Save two programs under <stage>/logs/. extract.awk turns ONE flavour’s
report into rows and fails when its rows disagree with the report’s own header counts, or when any of the six
class headers is absent (a refused run prints no table) — a broken extraction, never a reading:
2. Dry run, every flavour, gated on the instrument’s exit, extracted as it lands.-ExpectSourceGo is ALWAYS
passed: omitted, it is derived from the version.props of the tree the SCRIPT lives in, which after H2 names the
target, and the run refuses.
S='<stage>'for os in windows linux darwin;do
L="$S/logs/h5c-dry-$os-$(date +%Y%m%d-%H%M%S).log"
powershell -NoProfile-ExecutionPolicy Bypass -File'<tree>/src/reconvert-deletions.ps1'\-Root"$S/h5/src"-GoRoot'<GOROOT-1.24.13>'-ExpectGo go1.24.13 \-SourceGoRoot'<GOROOT-1.23.12>'-ExpectSourceGo go1.23.12 \-Sentinel"$S/h5.run.stamp"-Goos$os-Goarch amd64 >"$L" 2>&1
rc=$?;echo" $os exit=$rc log $(basename"$L")"case$rcin 0|2);;*)echo" $os REFUSED rc=$rc: STOP, post the log";exit 3 ;;esac# 2 = UNRESOLVED rows stand, NOTHING deletedawk-f"$S/logs/extract.awk""$L">"$S/logs/h5c-$os.tsv"||{echo" $os STOP: extraction disagrees with the report";exit 3;}awk-F'\t'-f"$S/logs/under.awk""$S/logs/removals.lst""$S/logs/h5c-$os.tsv">"$S/logs/h5c-$os.mapped"||exit 3
done
3. docs/phase4/h5-removals.txt is the EXPECTED set, never a delete list.
S='<stage>';for os in windows linux darwin;do
echo" $os (a) removed-package rows outside the list: $(awk-F'\t''$1 == "-" && $4 ~ /package not in std at target/'"$S/logs/h5c-$os.mapped" | wc-l)"(cd"$S/h5/src/core"&&while IFS=read-r D;do case"$D"in'#'*|'')continue;;esac[-d"$D"]&& find "$D"-name'*.cs'!-newer"$S/h5.run.stamp"!-name'*_test.cs'!-name package_test_info.cs !-name go2cs_test_host.cs !-name'*.g.cs'done < "$S/logs/removals.lst") | sort-u | sed's/^/SEEDED\t/' | awk-F'\t'-f"$S/logs/under.awk""$S/logs/removals.lst" - | cut-f1 | sort | uniq-c>"$S/logs/removed-seeded.cnt"awk-F'\t''$1 != "-" { print $1 }'"$S/logs/h5c-$os.mapped" | sort | uniq-c>"$S/logs/removed-rows-$os.cnt"
diff "$S/logs/removed-seeded.cnt""$S/logs/removed-rows-$os.cnt"> /dev/null &&echo" $os (b) every seeded file of a listed package is a row"||echo" $os (b) STOP: a listed package keeps a file"done
(a) ≠ 0 — a package absent at the target that H3 did not list: STOP, post it by name.
(b) STOP — a file of a removed package classified KEEP or not-a-target: post the diff.
(c) The residue, posted as a set: every PROTECTED and UNRESOLVED row under a listed directory, plus
find <stage>/h5/src/core/<D> -type f ! -name '*.cs' for each (.csproj, README.md, .cs.auto, icons).
(d) The name trap, by the ruled key. Over the UNION of the three flavour files (a single flavour answers
only for per-GOOS folders and packages in std on all three: the conversion-target test runs before the name
test and uses the flavour’s own std set), take the two-pin difference: a path is produced in a root when it,
or its .cs.auto sibling, is newer than that root’s sentinel; survivors are produced-in-h4a minus
produced-in-h5. A survivor in any PROTECTED or KEEP row is a STOP item, named:
NOT MEASURED against real roots. A *_impl stem test, if kept at all, is a labelled second arm.
(e) Per-file rows outside the list (principal removed at target, present but not selected for …) are
posted by name; they become checkable when h5-removals.txt gains its per-file amendment (C1’s two-pin
survivor proposal and the fifth rehearsal’s re-derivation, agreeing).
(f) Flat rows (no windows/linux/darwin segment) that are not in the same class in all three
flavour files are posted by name: an -Apply deletes a flat file on one flavour’s selection alone.
4. The UNRESOLVED disposition, row by row, posted before any removal. The rule of record (COORD 5123a14a2):
an UNRESOLVED row is STALE only when BOTH hold — not emitted by any target this run (no copy newer than
<stage>/h5.run.stamp on any per-target STAGE root; the roots are seeded, so the merged root’s mtime cannot say)
AND its package is absent at go1.24.13. Never delete an UNRESOLVED row emitted this run, nor the
package_info.cs of a package kept alive by a PROTECTED hand-own.
pin go1.24.13 '<GOROOT-1.24.13>''<GOROOT-1.24.13-posix>'||exit 3
S='<stage>';ST="$S/h5-stage"for g in windows linux darwin;do(cd"$S"&&GOOS=$g go list std );done | LC_ALL=C sort-u>"$S/logs/std-1.24.13.txt"||exit 3
for os in windows linux darwin;do awk-F'\t''$1 == "UNRESOLVED" { print $2 }'"$S/logs/h5c-$os.tsv";done | LC_ALL=C sort-u>"$S/logs/h5c-unresolved.txt"for os in windows linux darwin;do awk-F'\t''$1 == "PROTECTED" { p = $2; sub(/\/[^\/]*$/, "", p); print p }'"$S/logs/h5c-$os.tsv";done | LC_ALL=C sort-u>"$S/logs/protected-dirs.txt"while IFS=read-r p;do
e=no;for g in windows linux darwin;do[-n"$(find "$ST/$g-amd64"-path"*/core/$p"-newer"$S/h5.run.stamp"-print-quit)"]&&e=yes;done
dir=${p%/*};pkg=$(printf'%s\n'"$dir" | sed-E's#/(windows|linux|darwin)(/|$)#\2#g')live=no;grep-qxF"$pkg""$S/logs/std-1.24.13.txt"&&live=yes
keep=no;grep-qxF"$dir""$S/logs/protected-dirs.txt"&&keep=yes
if[$e=yes];then d=KEEP-EMITTED;elif[$keep=yes];then d=KEEP-HANDOWN-CONSEQUENCE;elif[$live= no ];then d=STALE;else d=READ;fi
printf'%s\temitted=%s\tpackage-live=%s\tprotected-in-dir=%s\t%s\n'"$p"$e$live$keep$ddone < "$S/logs/h5c-unresolved.txt">"$S/logs/h5c-unresolved-disposition.tsv"awk-F'\t''$5 == "STALE" { print $1 }'"$S/logs/h5c-unresolved-disposition.tsv">"$S/logs/h5c-unresolved-stale.txt"cut-f5"$S/logs/h5c-unresolved-disposition.tsv" | sort | uniq-c
A READ row (not emitted, package live) is disposed by a reader and posted by name. This document’s own named
case is one: crypto/ecdh/package_init.cs, “a genuinely stale generated file” in a LIVE package (the paragraph
above). The rule of record does not admit it; lane R’s model delete did, by name. Post it as a named item; append
it to h5c-unresolved-stale.txt only on COORD’s word. The package predicate (stripping a GOOS segment for layout
L3) and the stage-root find are NOT MEASURED on real roots.
5. The delete — the executable interim, until the instrument amendment is seated.-Apply refuses while
ANY UNRESOLVED row stands, and current metadata is always UNRESOLVED, so -Apply cannot complete on a
three-target root. COORD 5123a14a2 accepted the instrument amendment “-Apply admits an UNRESOLVED row the run
itself EMITTED” as an item; it is implemented on no tree. Until it is seated, the delete is this script, in
<stage>/h5 only, from a per-run copy:
#!/usr/bin/bash# H5c interim delete (COORD 5123a14a2). Deletes DELETE-ABSENT + DELETE-DESELECTED + the STALE UNRESOLVED rows. <stage>/h5 only.set-uo pipefail;export MSYS_NO_PATHCONV=1
S='<stage-posix>';C="$S/h5/src/core";LOGS="$S/logs"MARK='^[[:space:]]*[[][[:space:]]*module[[:space:]]*:[[:space:]]*(go[.])?[[:space:]]*GoManualConversion(Attribute)?[[:space:]]*[]]'[-d"$C"]&&[-f"$S/h5.run.stamp"]||{echo"ABORT: not an H5 staging root";exit 2;}# 1. the three flavours' DELETE classes must agree, or a flat file would go on one flavour's selectionfor os in windows linux darwin;do awk-F'\t''$1 == "DELETE-ABSENT" || $1 == "DELETE-DESELECTED" { print $2 }'"$LOGS/h5c-$os.tsv" | LC_ALL=C sort-u>"$LOGS/h5c-del-$os.txt";done
cmp -s"$LOGS/h5c-del-windows.txt""$LOGS/h5c-del-linux.txt"&& cmp -s"$LOGS/h5c-del-windows.txt""$LOGS/h5c-del-darwin.txt"||{echo"STOP: the flavours' DELETE sets differ; post the diff";exit 2;}# 2. backup first; tar reads a drive-letter archive name as a remote host, so the name is POSIX and --force-local is passedBK="$S/h5-pre-apply.tar";[-e"$BK"]&&{echo"ABORT: $BK exists";exit 2;}(cd"$S/h5/src"&&tar--force-local-cf"$BK" core )||{echo"ABORT: backup failed";exit 3;}["$(tar--force-local-tf"$BK" | wc-l)"-gt 0 ]||{echo"ABORT: backup is empty";exit 3;}sha256sum"$BK">"$BK.sha256"# 3. the delete setLIST="$LOGS/h5c-delete-set.txt"cat"$LOGS/h5c-del-windows.txt""$LOGS/h5c-unresolved-stale.txt" | LC_ALL=C sort-u>"$LIST"echo"delete set: $(wc-l < "$LIST") paths"before=$(grep-rlE"$MARK""$C"--include='*.cs' | wc-l)# 4. assert EVERY row before ANY removalbad=0
while IFS=read-r p;do
case"$p"in golib/*|go2cs/*|unsafe/*|testing/*|builtin/*)echo" TRESPASS $p";bad=$((bad+1));continue;;esaccase"$p"in*_impl.cs)echo" IMPL-COMPANION $p";bad=$((bad+1));continue;;esac[-f"$C/$p"]||{echo" MISSING $p";bad=$((bad+1));continue;}grep-qE"$MARK""$C/$p"&&{echo" HAND-OWN $p";bad=$((bad+1));}grep-qxF"$p""$LOGS/h5c-unresolved.txt"&&!grep-qxF"$p""$LOGS/h5c-unresolved-stale.txt"&&{echo" UNRESOLVED-NOT-STALE $p";bad=$((bad+1));}done < "$LIST"["$bad"-eq 0 ]||{echo"ABORT: $bad row(s) failed the pre-delete assertions; nothing deleted";exit 4;}# 5. remove, then assert the post-conditionn=0;while IFS=read-r p;do rm-f--"$C/$p"&&n=$((n+1));done < "$LIST"left=0;while IFS=read-r p;do[-e"$C/$p"]&&{echo" STILL PRESENT $p";left=$((left+1));};done < "$LIST"after=$(grep-rlE"$MARK""$C"--include='*.cs' | wc-l)echo"removed $n; still present $left (want 0); marked hand-owns $before before, $after after (want equal)"["$left"= 0 ]&&["$before"="$after"]||{echo"ABORT: post-condition failed; restore from $BK";exit 5;}
Do not hand-delete outside this script, do not touch a modification time, do not line-filter a .slnx. It
deletes .cs rows only: the fourteen’s .csproj, README.md, icons and .cs.auto stay (step 3(c)’s residue),
and the generated solution lists every .csproj on disk. Once the instrument amendment is seated, it replaces
this script: step 2’s loop with -Apply appended, gated on rc 0 per flavour (rc 2 means nothing was deleted),
one log per flavour carrying its deleted … lines and re-walked after arithmetic, the backup above first.
` (must name both defects on the unpatched tree), then apply, then `--verify` again (must pass), the
mcleanup hand-own body carried from `claude/c1-mcleanup-handown` or its successor — and only then build `runtime`.
Without it the 1.24 build cannot get past `runtime`. **The carry hazard:** re-deriving `runtime2.cs`/`mfinal.cs`
at H5 takes the hand-own body from `claude/c1-mcleanup-handown` (or its successor), never from the landing tree;
`mfinal.cs`'s `createfing` must read `GoFinalizerQueue.EnsureRunner()` afterwards, and `finalizerDoorGuard_test.go`
asserts it under the plain `go test`
Spelled for this layout (`` is the scratch CORE directory, per the applier's usage line; until train 49
lands, the applier is read from its branch):
```bash
S=''; A="$S/bin/apply-h5-c1-1-rederives.sh"; C="$S/h5/src/core"; P=''; M=''
git -C '' show "$P:src/apply-h5-c1-1-rederives.sh" > "$A" || exit 3
( git -C '' archive "$M" -- src/core/runtime/mfinal.cs src/core/runtime/mcleanup.cs src/core/runtime/mcleanup.cs.auto | tar -xf - --strip-components=1 -C "$S/h5/src" ) || exit 3
bash "$A" --verify "$C" > "$S/logs/c1-verify-pre.log" 2>&1; echo " pre-verify rc=$? (want 1, naming BOTH defects)"
bash "$A" "$C" > "$S/logs/c1-apply.log" 2>&1; rc=$?; echo " apply rc=$rc (want 0)"; [ "$rc" = 0 ] || exit 6
bash "$A" --verify "$C" > "$S/logs/c1-verify-post.log" 2>&1; rc=$?; echo " post-verify rc=$rc (want 0)"; [ "$rc" = 0 ] || exit 6
```
The applier's own arms are the step's controls; this runbook cites them rather than restating them
(`--self-test`, `--verify`, apply; exit 0 / 1 / 2). ⚠ **Its apply mode REFUSES (rc 2, "H5c has not run") while
`runtime/internal/sys` exists as a directory, and step 5 LEAVES that directory.** On the fifth rehearsal's real
post-H5c root it still held two `.csproj`, `README.md`, two icons and three test `.cs` files. That refusal is
therefore the EXPECTED outcome of this step as written, until C1 or COORD rules the precondition or the residue.
STOP and post it to COORD and C1; never remove the residue by hand. `--self-test` uses `python3`. i9's reproduction of lane R's fifth rehearsal
§1–§2 ends here: the applier `--verify`, apply, `--verify`, then the `runtime` build — the first rung of the H5
executor.
#### Amendment 2026-09-13 — the overlay comparand, the overlay, and `go generate`
**1. The comparand** (read-only, both roots from one binary — `cmp /h4a.exe.sha256 /h5.exe.sha256`
first). **`d-level` is taken immediately after both conversions. `d-hop` is taken after H5c's step 5 removal and
the C1 patch, and before the overlay** — taken earlier it cannot show a deletion, because the stale seed sits
identically in both roots:
```bash
cd '' && cmp h4a.exe.sha256 h5.exe.sha256 || exit 4
diff -rq --strip-trailing-cr -x bin -x obj -x Generated -x '*.cs.auto' '/src/core' h4a/src/core > logs/d-level.txt
diff -rq -x bin -x obj -x Generated -x '*.cs.auto' h4a/src/core h5/src/core > logs/d-hop.txt
```
`` is a clean worktree of ``. `d-level` (master → H4a) is the queued levelling noise: each
row matched to a CleanupBacklog item, an unbanked-intended-drift row or a BOARD born-stale entry, else posted
UNCLASSIFIED. `d-hop` (H4a → H5) is the upstream delta: every row through §4, T0 to T5; H5c's removals read as
`Only in h4a` rows. A path in both is classified in `d-hop`, its `d-level` part named.
**2. The overlay** — after the H5c amendment's steps 5 and 6, and after the H8 census (the H8 amendment reads a
clean ``) — `.cs`, `.csproj` and `README.md`, never `*.cs.auto`, a straight copy:
```bash
( cd '/h5/src' && find core \( -name '*.cs' -o -name '*.csproj' -o -name README.md \) \
-not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/Generated/*' ) > '/logs/overlay-set.txt'
( cd '/h5/src' && tar -cf - -T '/logs/overlay-set.txt' ) | ( cd '/src' && tar -xf - ); echo "rc=$?"
git -C '' status --porcelain > '/logs/overlay-status.txt'
grep -c '^ D' '/logs/overlay-status.txt' # 0: a copy deletes nothing (floor 8); grep -c exits 1 on a zero count
```
Then the marker census re-measured in ``, T0 phantoms restored per §4, and staging by explicit path —
never `git add -A` (floor 8). `` is a worktree ON the version branch COORD names (checked out at `
`, not
detached, before the overlay); commits are signed; announce, then push (floor 9).
**3. STOP — the overlay cannot carry a deletion.** A file H5c removed from the staging root is still tracked
in `src/core`. Compute and POST the set; commit nothing from it until COORD rules how it lands:
```bash
( cd '' && git -c core.quotePath=false ls-files -- 'src/core/*.cs' 'src/core/*.csproj' 'src/core/*README.md' ) | sed 's#^src/##' | LC_ALL=C sort > '/logs/tracked.txt'
LC_ALL=C sort '/logs/overlay-set.txt' > '/logs/staged.txt'
LC_ALL=C comm -23 '/logs/tracked.txt' '/logs/staged.txt' > '/logs/absent-in-stage.txt'
```
Its want: exactly the step-5 delete set (prefixed `core/`), plus whatever the ruling removes. **`go2cs-stdlib.slnx`**
is adopted from `/h5/src` verbatim, and only after that ruling: the generator lists every `.csproj` on disk,
so while the residue stands it lists the residue.
**4. `go generate .`** — `stdlib-metadata.txt` is generated from every `package_info.cs` under `src/core` and gated
by `TestStdLibMetadataInSync`. A no-write PREDICTION is available as soon as the H5 root exists, under its pin:
```bash
pin go1.24.13 '' '' || exit 3
( cd '/src/go2cs' && go run ./internal/genstdlibmeta '/h5/src/core' '/logs/stdlib-metadata.predicted.txt' ) || exit 6
diff '/src/go2cs/stdlib-metadata.txt' '/logs/stdlib-metadata.predicted.txt' > '/logs/stdlib-metadata.predicted.diff'
```
The real run comes AFTER the last commit of the series that moves a `package_info.cs` — the removal's ruling
applied, the hand-own branch merged, any seat re-minting metadata — because every leftover `package_info.cs`
contributes records:
```bash
pin go1.24.13 '' '' || exit 3
( cd '/src/go2cs' && go generate . ); rc=$?; [ "$rc" = 0 ] || exit 6
git -C '' status --porcelain -- src/go2cs src/core/go2cs # want: only src/go2cs/stdlib-metadata.txt
( cd '/src/go2cs' && go test -count=1 ./... ) > "/logs/converter-test-$(date +%Y%m%d-%H%M%S).log" 2>&1; echo "rc=$?"
```
Commit the regenerated file with the change that moved it; re-run if a later commit moves a `package_info.cs`.
#### Amendment 2026-09-13 — where the series' artifacts go, and what each step posts
- **Layout.** `/bin` (the binary, its `sha256`, the C1 applier copy), `/h4a`, `/h5`,
`/-stage`, `/.run.stamp`, `/.exe.sha256`, `/logs`. Never a session scratch
directory, never another lane's root.
- **Logs.** One per step per run, a full date-time in the name, the whole stream (`> log 2>&1`, never `| tail`,
floor 7). Every leg longer than a few minutes runs from a per-run COPY of its script (floor 4).
- **Manifest.** After each step, over exactly what is shipped:
`( cd '' && { find bin logs -type f; find h4a h5 -type f -not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/Generated/*'; } | LC_ALL=C sort | xargs -d '\n' sha256sum ) > '/MANIFEST.sha256'`.
The roots and the logs go to i9's share as mapped from the i7 (COORD `220cffc7b`, "Fleet shares") under a dated
folder with that manifest; the post names the folder and the manifest's hash, and COORD pulls. Never a committed binary.
- **Each post:** the tree SHA and tree hash; `go version `'s token and the exe's sha256; each pin half's bare
`go version` line and the directory name it was taken from; the command line with placeholders; exit code and
wall; the count lines verbatim; every prediction quoted as worded beside its reading; the box's load; the
manifest hash. Announce before pushing.
- **The record.** On the version branch, the counts ride in the message of the commit that carries the step; the
logs on the share. H4a commits nothing: its counts live in its post and the share manifest. Where the real
series' readings are recorded as a `docs/phase4/` file is not ruled — ask COORD before the first commit.
### H6 — The hand-own re-audit ⟲ **GATE**
The step that distinguishes a corpus *upgrade* from a corpus *regeneration*, and the one a migration
is most likely to skip **because everything compiles without it**.
> **Instrument: [`src/handown-census.ps1`](../src/handown-census.ps1)** (runway dispatch,
> 2026-08-24) — the differential CENSUS half of this gate, so the review starts from a list instead
> of from everything. For every `[module: GoManualConversion]`-marked file (re-measured each run,
> line-anchored, whole-file) it maps the upstream Go source the hand-own replaces and classifies it
> across `-FromGoRoot`/`-ToGoRoot`: **untouched** / **touched-trivial** (comment-and-whitespace
> only — Go `//go:` directives count as CODE, not comments) / **touched-substantive** (the review
> list) / **no-upstream-counterpart** (hand-additions; reviewed via their principal). Read-only,
> self-verifying (classes must sum to the marker census), and conservative in one direction only:
> every stripper bailout classifies substantive, because over-reporting sends a human to look.
> **What it does NOT do: the judgment.** Every substantive row still gets the human review below —
> the instrument decides where H6 looks, never what H6 concludes.
>
> **The shape to expect: the review list is a small fraction of the census.** Its first execution
> reduced a census of dozens of marked files to a **single-digit** substantive set, with the rest
> split between untouched and no-upstream-counterpart. That ratio is the instrument's whole value and
> it is also the reason to distrust a run that does not show it — a substantive class near the census
> size means a stripper bailing out, not upstream churn. The figures of any one execution are that
> migration's record, not this document's: the first run's are in
> [`phase4/RECON-go12312-diff.md`](/phase4/RECON-go12312-diff.html), where each substantive row was
> independently cross-checked against the upstream package table.
**The failure mode.** A hand-owned file is frozen at the semantics of the release it was written
against. When upstream **adds** code inside that file — a new branch, a new field, a hardening fix —
the hand-own does not receive it. Nothing fails: the file is excluded from the convert set, the corpus
compiles, the suites are green, and the package's own tests may not cover the added path. **The defect
is silent and operational, and it surfaces later as an inexplicable divergence in a package nobody was
working on.** *Newly-added* upstream code is the dangerous class; a *changed* line often shows up as a
behavioral divergence, an added branch shows up as nothing.
> **AMENDED 2026-09-16 — H6's census ENUMERATES SURVIVORS, so a hand-own that was DELETED WITH ITS
> PACKAGE leaves no row, and the class it cured returns uncured in whatever replaced it.** The failure
> mode above is upstream *adding* code inside a hand-own that cannot receive it. This is its sibling and
> it is quieter: the hand-own does not go stale, it **ceases to exist**, and with it the only statement
> of why the automatic conversion of that file was unsafe.
>
> **Measured, not argued** (`src/handown-census.ps1:122`): the marker census is
> `git grep -l -E '^\s*\[module:\s*(go\.)?GoManualConversion\]' -- '*.cs'` **over the working tree** —
> the corpus as it is now. A file already removed is not in `$marked`, so it is not misclassified, it is
> **absent from the census entirely**. The instrument's own `from`/`to` existence test at `:144-150`
> handles the *upstream Go source* appearing or vanishing and correctly calls that `touched-substantive`
> — "always a human look" — but that test presumes the hand-own still exists to be mapped. Nothing in
> the gate looks at the set that is gone.
>
> **The instance that produced this amendment** (go1.23.12 → go1.24.13, `docs/phase4` q97):
> `src/core/vendor/golang.org/x/crypto/sha3/` held a hand-owned `xor.cs` whose comment names its own
> failure mode verbatim — the raw-address reinterpret "fabricated an `array` backing reference out
> of the keccak state's own DATA" — and cured it with `MemoryMarshal.AsBytes` over the array's span, a
> genuine aliasing view. At the new pin that package is **15 files → 0**: upstream moved the
> implementation to `crypto/internal/fips140/sha3`, the hand-own was dropped with the package it lived
> in, and the replacement re-emits the same length-changing raw-address reinterpret at
> `keccakf.cs:61` — with **no `GoManualConversion` marker anywhere in the new package**. It surfaced as
> four test panics carrying *negative* lengths out of `golib/array.cs`'s bounds check, which is the
> signature of a fabricated `array` struct rather than an out-of-range index. H6 passed. The corpus
> compiled. The cure had simply been deleted.
>
> **The step, and it is mechanical.** At H6, before any substantive-row review, compute the
> **retired-hand-own set** — markers present in the OUTGOING corpus and absent from the incoming one.
>
> ⚠ **`` is the MERGE-BASE of the release branch and master, never master's tip.** A hop
> runs on a long-lived release branch while master keeps moving; a hand-own that landed on master *after*
> the branches diverged was never on the release branch to be retired from it, and a baseline taken at
> master's tip reports it as retired. Corrected in review by a second lane, who re-ran the ancestry test
> on the first run's rows: six of seven survived, and the one that did not had landed on master after the
> divergence. **Assert it per row** — `git merge-base --is-ancestor
> ` — because that assertion is what catches the class, not the choice of baseline alone.
>
> ⚠ **And a row that fails that assertion is not discarded — it is RE-ROUTED.** It is not a hop
> retirement; it is a **carry-forward gap**: a cure that exists on master and has never reached the
> release branch. That is a different finding and can be a worse one. The instance here: the excluded
> row's cure keeps a runtime test off a fatal path, the release branch carries the seat that makes that
> path an uninterceptable process exit, and the host kill it prevents was measured at 57 verdicts. **The
> false positive was worth more than the row it displaced**, so the step files these rather than dropping
> them, and the hop's own carry-forward census owns them.
>
> ```
> git grep -l -E '^\s*\[module:\s*(go\.)?GoManualConversion\]' -- 'src/core/**/*.cs' | sed "s#^:##" | sort > logs/handowns-outgoing.txt
> git grep -l -E '^\s*\[module:\s*(go\.)?GoManualConversion\]' -- 'src/core/**/*.cs' | sed "s#^:##" | sort > logs/handowns-incoming.txt
> LC_ALL=C comm -23 logs/handowns-outgoing.txt logs/handowns-incoming.txt > logs/handowns-retired.txt
> ```
>
> where `` is `git merge-base master`.
>
> **One row per retired hand-own, and the row is not closed by the deletion being correct.** For each:
>
> | field | what it must carry |
> |---|---|
> | the hand-own | path at the outgoing pin, and the *class it cured* in its own words — a hand-own that cannot say what it was for is itself the finding |
> | why it went | package dropped · file renamed · upstream absorbed the fix · the hand-own's reason expired |
> | the replacement | the package that now carries those semantics at the new pin, **named**, or `none` with the reason |
> | the class, re-censused | the cured class's predicate run at the NEW pin over the replacement — **cured / moved / gone**, with the count |
> | the marker | whether the replacement carries a hand-own of its own, measured, not assumed |
>
> **The verdict a row needs is `cured` or `gone`.** `moved` — the class present at the new pin in a file
> with no hand-own — is an H6 **GATE FAILURE**, exactly as a substantive row left unreviewed is, and it
> is cured before H6 closes.
>
> ⚠ **Do not read the retired set as a delete list to approve.** Every one of these deletions was
> *correct*: the package really did go away. The question H6 asks is not whether the file should have
> been removed — it is **whether the reason it existed went with it**.
>
> *Named blind spot:* the set is keyed on the marker, so a hand-own retired by having its marker removed
> while the file stays reads as present. That is the whole-file-replacement class the marker exists to
> mark, and losing the marker is already an H6 substantive row by the census's own arithmetic — stated
> here so the two are not confused.
>
> *Durable form, after the hop:* a guard in `src/go2cs/internal/repoguard` carrying the previous pin's
> marker list and asserting that every absent entry has a closed row. Banked, not cut during a
> migration — the list's baseline moves at every hop and a guard whose baseline moves needs the hop to
> be over before it can be written honestly.
**The instrument** is `.cs.auto` — the converter's answer to *"what would the automatic conversion of
this file be, today, from this Go tree?"*
**The diff is `.auto`(old release) vs `.auto`(new release), per hand-own — never `.auto` against the
hand-owned `.cs`.** The latter is dominated by the hand-own's *intended* divergence and is unreadable;
the former isolates the upstream delta. **Both `.auto` files must be produced by the SAME converter
binary**, or converter drift contaminates the release axis and the classification is worthless. Each
staging root carries its **own** `version.props` pinned to its own release, so the pin guard passes on
both sides.
*Named blind spot:* if the new converter build cannot parse the OLD tree cleanly, run A degrades and
the baseline is suspect. **Assert run A's package count and marker gate against the outgoing corpus's
before trusting it.**
**Classification — every delta, explicitly, one of three:**
| Class | Meaning | Required record |
|:--|:--|:--|
| **(a) ABSORBED** | the upstream change is real and has been carried into the hand-own | the commit that carried it; a test or gate that observes it |
| **(b) N/A** | the upstream change does not apply to the managed implementation | **the reason, written out** — never the bare letters |
| **(c) REWRITE OWED** | the hand-own must change and has not yet | a named work item, gating the migration or explicitly deferred with owner and reason |
An **empty** diff still gets a record (`unchanged`, with both hashes). A hand-own the run emitted
**no** `.auto` for gets a record too, and that record is **a defect in the audit, not a pass**: either
the seed did not take at that path, or the marker predicate could not see the marker.
⚠ **Two populations, and conflating them makes the gate either a false alarm or a rubber stamp**
(ruled): the **audit** covers *all* hand-owns; the **`.auto` differential** reaches only the ones the
converter re-emits. A supplemental `*_impl.cs` companion has no Go counterpart and therefore no
`.auto` — it is audited **against its principal's `.auto` diff**. A hand-owned *package* is audited by
**manual upstream diff**. **Every record names its evidence class.**
**The completeness gate:**
> No migration's corpus is adopted until every hand-own in the **re-measured** census has a classified
> delta record in that migration's audit file, and every (c) is either closed or explicitly deferred
> with an owner.
Mechanically: re-measure the line-anchored census over `src/core`; assert every marked path appears
exactly once in the audit file; assert every row's class is one of `unchanged`/`a`/`b`/`c`; assert
every `b` carries a non-empty reason and every `c` a work-item reference; assert **zero** rows in the
"no `.auto` emitted" state. Exit non-zero on any violation — the same shape as the repository's other
preflights: cheap, by-path, and impossible to pass vacuously.
Deliverable: one audit file per migration under `docs/phase4/` (ruled), rather than per-package notes,
because the completeness gate must be checkable in one place.
#### Amendment 2026-09-13 — for the 1.23 → 1.24 hop, where the `.auto` pair comes from
- **Old side** `/h4a/src/core/**/*.cs.auto`; **new side** `/h5/src/core/**/*.cs.auto` — one binary,
`cmp /h4a.exe.sha256 /h5.exe.sha256` passing, each root with its own `version.props`.
- **Population:** the marked paths by the census instrument's own predicate, `go.`-qualified spelling included —
never a literal grep, which undercounts.
- **Run A's package count** is asserted against the outgoing corpus's in the H4a baseline amendment (step 4).
- **The differential**, one row per marked path, into `/logs/h6-auto-pair.txt`:
```bash
S=''; while IFS= read -r p; do q=${p#src/}
a="$S/h4a/src/$q.auto"; b="$S/h5/src/$q.auto"
if [ ! -f "$a" ] || [ ! -f "$b" ]; then s=MISSING-AUTO; elif cmp -s "$a" "$b"; then s=IDENTICAL; else s=CHANGED; fi
na=old; [ -f "$a" ] && [ "$a" -nt "$S/h4a.run.stamp" ] && na=new; nb=old; [ -f "$b" ] && [ "$b" -nt "$S/h5.run.stamp" ] && nb=new
printf '%s\t%s\th4a-auto=%s\th5-auto=%s\n' "$p" "$s" "$na" "$nb"
done < "$S/logs/h5-marked.txt" > "$S/logs/h6-auto-pair.txt"
```
and the census: `powershell -NoProfile -ExecutionPolicy Bypass -File '/src/handown-census.ps1'
-FromGoRoot '' > '/logs/h6-census-.log' 2>&1`
(on the train-47 union the script takes exactly those two parameters plus `-ListUntouched`).
- **Undetermined is not unchanged.** A seeded root cannot tell an `.auto` the run rewrote to identical bytes from
one it never wrote: identical writes are skipped at the merge. A row `IDENTICAL` with `old` on both sides is
recorded `no .auto emitted — undetermined`, which the completeness gate counts as a defect, until a
discriminator is ruled. A candidate discriminator: the sibling writer creates each `.cs.auto` unconditionally in
the per-target STAGE root, so a copy under `/-stage/-amd64` newer than the sentinel may say it
was emitted — NOT MEASURED, and not a ruling.
- **The completeness gate has no instrument** on `a02ac3df3` or the train-47 union: STOP before scoring it; post
the differential and the census; COORD rules the gate script and its exit condition.
- **The relocation blind spot.** A frozen hand-own present in both trees passes any set comparison while carrying
declarations the target release moved (`runtime/internal/sys` → `internal/runtime/sys`). The package-alias
census (`docs/phase4/CENSUS-h6-handown-package-aliases.md`) is read beside every substantive row.
- **The audit file** is `docs/phase4/AUDIT-h6-handown-go124.md`: one row per marked path. R cut the skeleton
(`d18059950`, train 48 seat 4) and G filled it (blocks 1–19, landed on master at `4e672aa4a`, 144 of 145 rows
classified, row 130 recorded REWRITE OWED (c)); it reaches the version branch with master at H12.
### H7 — Compile parity **GATE**
Full `go2cs-stdlib.slnx` build with shared compilation disabled, zero errors, **skipped-dependents
enumerated and zero** (a dependent of a failed project is skipped, not errored — count them). Run
**every** buildable `$(GoTargetOS)` flavor, purging `bin`/`obj`/`Generated` between switches.
**Gate: 100 % of the migration's package set compiles.** Not "as many as before" — 100 %, per the
frame.
#### Amendment 2026-09-13 — the per-flavour build, and the scoring method for the ladder's sites
The gate reads the version-branch tree after the H5 series (`` = ``). The same procedure over
`/h5/src` (`` = `/h5`) is the LADDER — the gate's rehearsal, labelled as one. **The
ladder runs only after H5c's step 5 removal and step 6's C1 patch**: built before the removal it compiles the
stale seed siblings (CS0102), and before the patch it cannot get past `runtime`. The ladder is done when it reads
**zero unique sites on all three flavours with own assemblies at the corpus's order: projects minus the
platform-exclusive set, per flavour.** One flavour per invocation, serially, from a per-run copy of the script; a
box whose default .NET SDK lags the corpus TFM exports the .NET pair first (H10 step 1).
```bash
export MSYS_NO_PATHCONV=1 MSBUILDDISABLENODEREUSE=1 DOTNET_CLI_TELEMETRY_OPTOUT=1
FL=${1:?flavour}; SRC='/src'; LOG="/logs/build-$FL-$(date +%Y%m%d-%H%M%S).log"
[ -f "$SRC/core/golib/golib.csproj" ] && [ -f "$SRC/go2cs-stdlib.slnx" ] || { echo "ABORT: $SRC is not a go2cs src root"; exit 3; }
P=$(find "$SRC" -type d \( -name bin -o -name obj -o -name Generated \) -prune -print | wc -l)
find "$SRC" -type d \( -name bin -o -name obj -o -name Generated \) -prune -exec rm -rf {} + 2>/dev/null
left=$(find "$SRC" -type d \( -name bin -o -name obj -o -name Generated \) -prune -print | wc -l)
echo " purged $P remaining $left"; [ "$left" = 0 ] || { echo "ABORT: purge incomplete ($left)"; exit 3; }
START=$(date +%s)
dotnet build "$SRC/go2cs-stdlib.slnx" -c Debug -p:GoTargetOS="$FL" --no-incremental -m -p:UseSharedCompilation=false > "$LOG" 2>&1
rc=$?
echo " exit $rc after $(( $(date +%s) - START ))s NULs $(head -c 200 "$LOG" | tr -d -c '\000' | wc -c)"
echo " CS occurrences $(grep -aoE 'error CS[0-9]+' "$LOG" | wc -l) MSB/NETSDK occurrences $(grep -aoE 'error (MSB|NETSDK)[0-9]+' "$LOG" | wc -l)"
grep -aoE '[A-Za-z0-9_./\\-]+[.]cs[(][0-9]+,[0-9]+[)]: error CS[0-9]+' "$LOG" | sed 's#\\#/#g; s#.*/core/#core/#' | LC_ALL=C sort -u > "$LOG.sites"
echo " unique sites $(wc -l < "$LOG.sites") ROOTS $(grep -vc Generated "$LOG.sites") CASCADE $(grep -c Generated "$LOG.sites")"
built=0; unbuilt=0; : > "$LOG.unbuilt"
while IFS= read -r p; do
d=$(dirname "$p"); a=$(grep -aoE '[^<]+' "$p" | head -n1 | sed 's///'); [ -n "$a" ] || a=$(basename "$p" .csproj)
if ls "$d"/bin/Debug/*/"$a".dll > /dev/null 2>&1; then built=$((built+1)); else unbuilt=$((unbuilt+1)); echo "${p#*src/core/}" >> "$LOG.unbuilt"; fi
done < <(find "$SRC/core" -name '*.csproj' ! -name '*.tests.csproj')
echo " ASM (own assemblies) $built projects $((built+unbuilt)) none $unbuilt"
```
(`grep -c` exits 1 on a zero count; these lines print, they do not gate.) All counts are read before the next
flavour's purge; on the version-branch tree, `git status --porcelain | grep '^ D'` prints nothing after every purge
(floor 8). **Per flavour the gate is:** exit 0; CS and MSB/NETSDK occurrences 0; every project in `$LOG.unbuilt`
is platform-exclusive to another flavour — its package absent from `GOOS= go list std` under `pin
go1.24.13 … || exit 3`, run from a no-module directory.
**Scoring rules.**
- **Units never mix.** Per flavour: CS occurrences (they move with console verbosity and with every referencing
project), unique sites (`file(line,col): error CSnnnn`, keyed after `core/`), ROOTS (unique sites outside
`Generated`), CASCADE (unique sites inside `Generated`), and ASM (own assemblies, the gate's arithmetic). A
comparison names both units and both trees; a step between readings that moved more than one axis is not
attributed to one of them. A dll-file count with copies included is not ASM.
- **Every unique site is assigned to one owned class** — leftover seed (H5c), frozen metadata, converter emission,
hand-own, generator cascade — with its owner; a CASCADE site takes the owner of a ROOT in the same project, and
a site in no class is the finding.
- **Predictions are posted before the build and scored as worded**, the wording quoted beside the reading; the
predictions of record are those owed in `REHEARSAL-h5-go124.md` §15 §8, each with its premise re-read at the
tree first (a prediction whose premise is absent is posted VOID with the reading that voids it, never scored).
A falsifier stated in advance is scored even when the mechanism it tested survives.
#### Amendment 2026-09-16 — the per-flavour gate as executed at the 1.23 → 1.24 hop: arm 4 is inert where every project builds; the build root's spelling; arms gate on their command's rc
- **Arm 4 is inert on this corpus.** Every csproj under `src/core` builds regardless of `-p:GoTargetOS`, so
`$LOG.unbuilt` is EMPTY on every flavour and "every unbuilt project is platform-exclusive to another flavour"
is MET over zero items — vacuous, never evidence. Measured at the 1.24.13 hop's compile-parity tree on all
three flavours: unbuilt 0, ASM 343 of 343 (windows read on two boxes, linux and darwin on one). The arms that
carry a per-flavour reading are: exit 0; CS occurrences 0; MSB/NETSDK occurrences 0; unique sites 0; and
ASM = projects. Platform exclusivity is a property of the EMISSION's per-GOOS file sets, read at H8's census,
not of projects.
- **Minus golib.** `golib` is absent from every `go list std` set and must nonetheless be BUILT; any arm keyed on
"absent from `go list std`" passes it as legitimately unbuilt. Where arm 4's derivation is still used (e.g. to
size an expected unbuilt set), subtract golib first — and expect the arithmetic NOT to predict unbuiltness on a
corpus where every project builds: the linux prediction of 338 missed against a measured 343 for exactly this
reason.
- **Spell `` as a drive-letter path (`C:/…`), never `/c/…`.** The script exports `MSYS_NO_PATHCONV=1`
for dotnet's `-p:` arguments, so a POSIX spelling reaches native `dotnet` and `git` UNCONVERTED: `dotnet build`
fails on a non-existent project path, and `git -C` dies with "cannot change to … No such file or directory".
One launch was lost this way, and three added git arms printed PASS from `wc`/`grep` over a failed command.
- **Every arm gates on its command's own rc**, never on the shape of its output; the HEAD arm asserts a sha-shaped
value, not merely non-empty. An arm that cannot distinguish "clean" from "the command died" is not an arm — and
a guard's DESCRIPTION is not the guard: state the mechanism actually implemented, and run it on real input
before quoting it.
- **ASM units.** The script's ASM counts own assemblies over core csproj excluding `*.tests.csproj` (343 at this
hop = the census's core population). A "distinct produced assemblies" count from the build log reads one higher
(344: the solution's one non-core member). Name both units; never reconcile a difference of one by feel.
### H7a — The master fold **GATE**
**The ladder has no rung that folds master into the release branch, and the run rungs need one.** A hop
runs on a long-lived release branch while master keeps taking cuts. Everything from H8 onward *runs* the
corpus — re-emits it, rebanks goldens, re-derives the roster — and a run rung measures the tree it is
given. If master holds cures the branch has never carried, H8 onward measures a corpus that is wrong in
ways the migration did not introduce and cannot see.
**Ruled after an instance, not in advance.** At the go1.23.12 → go1.24.13 hop, master held 105 commits
the release branch had never carried. Among them: a managed `gcTestIsReachable` and the stop that keeps
the fatal path from ending the test host. The release branch carried the seat that makes `runtime.throw`
an uninterceptable process exit, and *not* the cure that keeps the runtime test off it — so the host kill
that had cost the runtime row 57 verdicts was live at the branch tip, silently, with the hand-own and its
converter registration absent **together** rather than as a dangling displacement that would have failed
loudly. It was found by a retired-hand-own census reporting a false positive (see H6), not by anything
looking for it.
**The step: ONE merge, master INTO the release branch, as the first act after the last compile-side seat
lands and before any run rung.** Never a rebase — the branch's SHAs are posted, and a hop's seats are
announced refs.
**Sized before it is taken, and the sizing is a post of its own:**
| | what it must carry |
|---|---|
| the pins | master, the branch tip, and `git merge-base` of the two — **all three re-read at the act**, because both move while the sizing is written |
| the range by path class | the master-only commits classified (docs and instruments / converter guards / converter production / corpus / repo config), merges counted separately because a merge carries its children's paths, and **no residue** — a commit matching no class is named, not dropped |
| what the branch LACKS | every hand-own and every emission-or-CLI change in the range, **named**, not counted. These are the fold's reason; the docs and guards are why the commit count is large and are not why the fold exists |
| the carry-forward set | marked files present on master and absent at the tip, each classified **relocated / gone / gap** — a relocation is verified by its counterpart's PATH, never by basename |
| the conflict set | `merge-tree --write-tree` at those two SHAs **with the ARGUMENT ORDER named**, by path, with the fingerprint stamped — the stamp differs by order while the conflicting paths do not, so the act is scored in the order the prediction stamped in |
| the prediction | what the merged tree must equal **and in which sense** (see *Scored on*), and the falsifiers |
**Scored on:**
- **the dry-run stamp, scored as what it is.** `merge-tree --write-tree` fingerprints the **inputs and
the merge machinery**, not the commit:
- a **conflict-free** fold — the merged tree is **byte-identical** to the stamp;
- a **conflicted** fold — the stamp **cannot be equalled**: it carries markers for every conflicted
path, and the landed tree differs from it by exactly the resolved paths. Score instead that the
resolved paths are **exactly** the predicted set and that each resolution matches its ruled class;
if a stampable figure is wanted, predict the **post-resolution** tree before the commit and score
against that. ⚠ Scoring a conflicted fold against the dry-run SHA asks for a tree the act cannot
produce, and a reader who takes it literally reads a **correct** fold as a miss;
- the conflict set exactly the predicted paths — **no path resolved that was not predicted**;
- ⚠ **silent subtraction, per symbol and in BOTH directions.** A fold crosses every seat the hop has
landed. Each landed marker is asserted **by name at its count**; a clean merge rc says "no conflict",
never "nothing dropped";
- each carry-forward gap present after, **with the whole of its cure** — a hand-own landed without its
converter registration is a partial fold, and partial is how this class hides;
- relocated packages still at their NEW paths and the old paths **not resurrected** — a modify/delete
resolved the wrong way re-creates a package the hop retired;
- any source root the range does not touch **byte-identical**; movement there is a MISS, not a bonus.
**Resolution by class, decided before the merge rather than at the conflict:**
- **regenerable metadata** (`package_info.cs`, `.csproj`) — **re-minted**, never hand-merged. These are
artifacts; a hand merge of an artifact is a hand-written artifact;
- **projitems** — the **union**, and the row count asserted afterwards with no duplicates;
- **a modify/delete where the delete is the hop's own package retirement** — the **delete stands**;
- **a code conflict whose two sides are a displacement and the body it displaces** — **if the file is
HAND-OWNED**, master's side, and the displacement's registration lands **with** it. **If the file is
EMITTED, it is RE-MINTED from the merged converter**, exactly as the regenerable metadata above is:
the merged tree carries the displacement's registration, so the re-mint emits the file *without* the
displaced body and *with* the hop's own calling convention. ⚠ **An emitted file is never resolved by
SIDE** — taking master's side reinstates master's **pre-hop emission of the whole file**, including
every call written against a signature the hop re-signed.
**⚠ The closing check is the cheapest proof the fold did what it is for:** re-run H6's retired-hand-own
step. After the fold the merge-base *is* master's tip, so that step reads clean **by construction** — and
if it does not, the fold is incomplete and the rows it still reports are the gaps it failed to carry.
*Named blind spot:* the fold answers "what has master got that the branch has not". It says nothing about
the reverse, and nothing about a cure that exists in **neither** — a class that was broken before the hop
began stays broken and is H6's and H7's to find, not this step's.
*What this step is not:* a licence to take master's tip mid-hop whenever it is convenient. It is **one**
merge at **one** boundary, sized and predicted, because a release branch that keeps re-merging master has
stopped being a release branch.
### H8 — Multi-platform re-emission **GATE** ⟲
Re-run the multi-target emission and the platform census, and diff the manifest against the outgoing
one. A migration changes the platform axis in **two** directions at once: new packages may be
platform-varying, and existing ones may stop being so. The per-GOOS package count is a measurement,
not a constant.
**Gate:** the platform manifest's marker gate is zero per target, and the default-flavor build
reproduces the single-target build byte-for-byte.
#### Amendment 2026-09-13 — for the 1.23 → 1.24 hop, H8's emission is H5's, and the manifest's comparand is owed
- The three-target emission IS the H5 reconvert; H8 does not re-run it.
- The census, under its own `pin go1.24.13 '' '' || exit 3`, with the H5
binary, **before the overlay** (the only CLEAN tree whose `version.props` reads 1.24.13 is `` at `
`, not
detached, before the overlay); commits are signed; announce, then push (floor 9).
**3. STOP — the overlay cannot carry a deletion.** A file H5c removed from the staging root is still tracked
in `src/core`. Compute and POST the set; commit nothing from it until COORD rules how it lands:
```bash
( cd '' && git -c core.quotePath=false ls-files -- 'src/core/*.cs' 'src/core/*.csproj' 'src/core/*README.md' ) | sed 's#^src/##' | LC_ALL=C sort > '/logs/tracked.txt'
LC_ALL=C sort '/logs/overlay-set.txt' > '/logs/staged.txt'
LC_ALL=C comm -23 '/logs/tracked.txt' '/logs/staged.txt' > '/logs/absent-in-stage.txt'
```
Its want: exactly the step-5 delete set (prefixed `core/`), plus whatever the ruling removes. **`go2cs-stdlib.slnx`**
is adopted from `/h5/src` verbatim, and only after that ruling: the generator lists every `.csproj` on disk,
so while the residue stands it lists the residue.
**4. `go generate .`** — `stdlib-metadata.txt` is generated from every `package_info.cs` under `src/core` and gated
by `TestStdLibMetadataInSync`. A no-write PREDICTION is available as soon as the H5 root exists, under its pin:
```bash
pin go1.24.13 '' '' || exit 3
( cd '/src/go2cs' && go run ./internal/genstdlibmeta '/h5/src/core' '/logs/stdlib-metadata.predicted.txt' ) || exit 6
diff '/src/go2cs/stdlib-metadata.txt' '/logs/stdlib-metadata.predicted.txt' > '/logs/stdlib-metadata.predicted.diff'
```
The real run comes AFTER the last commit of the series that moves a `package_info.cs` — the removal's ruling
applied, the hand-own branch merged, any seat re-minting metadata — because every leftover `package_info.cs`
contributes records:
```bash
pin go1.24.13 '' '' || exit 3
( cd '/src/go2cs' && go generate . ); rc=$?; [ "$rc" = 0 ] || exit 6
git -C '' status --porcelain -- src/go2cs src/core/go2cs # want: only src/go2cs/stdlib-metadata.txt
( cd '/src/go2cs' && go test -count=1 ./... ) > "/logs/converter-test-$(date +%Y%m%d-%H%M%S).log" 2>&1; echo "rc=$?"
```
Commit the regenerated file with the change that moved it; re-run if a later commit moves a `package_info.cs`.
#### Amendment 2026-09-13 — where the series' artifacts go, and what each step posts
- **Layout.** `/bin` (the binary, its `sha256`, the C1 applier copy), `/h4a`, `/h5`,
`/-stage`, `/.run.stamp`, `/.exe.sha256`, `/logs`. Never a session scratch
directory, never another lane's root.
- **Logs.** One per step per run, a full date-time in the name, the whole stream (`> log 2>&1`, never `| tail`,
floor 7). Every leg longer than a few minutes runs from a per-run COPY of its script (floor 4).
- **Manifest.** After each step, over exactly what is shipped:
`( cd '' && { find bin logs -type f; find h4a h5 -type f -not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/Generated/*'; } | LC_ALL=C sort | xargs -d '\n' sha256sum ) > '/MANIFEST.sha256'`.
The roots and the logs go to i9's share as mapped from the i7 (COORD `220cffc7b`, "Fleet shares") under a dated
folder with that manifest; the post names the folder and the manifest's hash, and COORD pulls. Never a committed binary.
- **Each post:** the tree SHA and tree hash; `go version `'s token and the exe's sha256; each pin half's bare
`go version` line and the directory name it was taken from; the command line with placeholders; exit code and
wall; the count lines verbatim; every prediction quoted as worded beside its reading; the box's load; the
manifest hash. Announce before pushing.
- **The record.** On the version branch, the counts ride in the message of the commit that carries the step; the
logs on the share. H4a commits nothing: its counts live in its post and the share manifest. Where the real
series' readings are recorded as a `docs/phase4/` file is not ruled — ask COORD before the first commit.
### H6 — The hand-own re-audit ⟲ **GATE**
The step that distinguishes a corpus *upgrade* from a corpus *regeneration*, and the one a migration
is most likely to skip **because everything compiles without it**.
> **Instrument: [`src/handown-census.ps1`](../src/handown-census.ps1)** (runway dispatch,
> 2026-08-24) — the differential CENSUS half of this gate, so the review starts from a list instead
> of from everything. For every `[module: GoManualConversion]`-marked file (re-measured each run,
> line-anchored, whole-file) it maps the upstream Go source the hand-own replaces and classifies it
> across `-FromGoRoot`/`-ToGoRoot`: **untouched** / **touched-trivial** (comment-and-whitespace
> only — Go `//go:` directives count as CODE, not comments) / **touched-substantive** (the review
> list) / **no-upstream-counterpart** (hand-additions; reviewed via their principal). Read-only,
> self-verifying (classes must sum to the marker census), and conservative in one direction only:
> every stripper bailout classifies substantive, because over-reporting sends a human to look.
> **What it does NOT do: the judgment.** Every substantive row still gets the human review below —
> the instrument decides where H6 looks, never what H6 concludes.
>
> **The shape to expect: the review list is a small fraction of the census.** Its first execution
> reduced a census of dozens of marked files to a **single-digit** substantive set, with the rest
> split between untouched and no-upstream-counterpart. That ratio is the instrument's whole value and
> it is also the reason to distrust a run that does not show it — a substantive class near the census
> size means a stripper bailing out, not upstream churn. The figures of any one execution are that
> migration's record, not this document's: the first run's are in
> [`phase4/RECON-go12312-diff.md`](/phase4/RECON-go12312-diff.html), where each substantive row was
> independently cross-checked against the upstream package table.
**The failure mode.** A hand-owned file is frozen at the semantics of the release it was written
against. When upstream **adds** code inside that file — a new branch, a new field, a hardening fix —
the hand-own does not receive it. Nothing fails: the file is excluded from the convert set, the corpus
compiles, the suites are green, and the package's own tests may not cover the added path. **The defect
is silent and operational, and it surfaces later as an inexplicable divergence in a package nobody was
working on.** *Newly-added* upstream code is the dangerous class; a *changed* line often shows up as a
behavioral divergence, an added branch shows up as nothing.
> **AMENDED 2026-09-16 — H6's census ENUMERATES SURVIVORS, so a hand-own that was DELETED WITH ITS
> PACKAGE leaves no row, and the class it cured returns uncured in whatever replaced it.** The failure
> mode above is upstream *adding* code inside a hand-own that cannot receive it. This is its sibling and
> it is quieter: the hand-own does not go stale, it **ceases to exist**, and with it the only statement
> of why the automatic conversion of that file was unsafe.
>
> **Measured, not argued** (`src/handown-census.ps1:122`): the marker census is
> `git grep -l -E '^\s*\[module:\s*(go\.)?GoManualConversion\]' -- '*.cs'` **over the working tree** —
> the corpus as it is now. A file already removed is not in `$marked`, so it is not misclassified, it is
> **absent from the census entirely**. The instrument's own `from`/`to` existence test at `:144-150`
> handles the *upstream Go source* appearing or vanishing and correctly calls that `touched-substantive`
> — "always a human look" — but that test presumes the hand-own still exists to be mapped. Nothing in
> the gate looks at the set that is gone.
>
> **The instance that produced this amendment** (go1.23.12 → go1.24.13, `docs/phase4` q97):
> `src/core/vendor/golang.org/x/crypto/sha3/` held a hand-owned `xor.cs` whose comment names its own
> failure mode verbatim — the raw-address reinterpret "fabricated an `array` backing reference out
> of the keccak state's own DATA" — and cured it with `MemoryMarshal.AsBytes` over the array's span, a
> genuine aliasing view. At the new pin that package is **15 files → 0**: upstream moved the
> implementation to `crypto/internal/fips140/sha3`, the hand-own was dropped with the package it lived
> in, and the replacement re-emits the same length-changing raw-address reinterpret at
> `keccakf.cs:61` — with **no `GoManualConversion` marker anywhere in the new package**. It surfaced as
> four test panics carrying *negative* lengths out of `golib/array.cs`'s bounds check, which is the
> signature of a fabricated `array` struct rather than an out-of-range index. H6 passed. The corpus
> compiled. The cure had simply been deleted.
>
> **The step, and it is mechanical.** At H6, before any substantive-row review, compute the
> **retired-hand-own set** — markers present in the OUTGOING corpus and absent from the incoming one.
>
> ⚠ **`` is the MERGE-BASE of the release branch and master, never master's tip.** A hop
> runs on a long-lived release branch while master keeps moving; a hand-own that landed on master *after*
> the branches diverged was never on the release branch to be retired from it, and a baseline taken at
> master's tip reports it as retired. Corrected in review by a second lane, who re-ran the ancestry test
> on the first run's rows: six of seven survived, and the one that did not had landed on master after the
> divergence. **Assert it per row** — `git merge-base --is-ancestor
> ` — because that assertion is what catches the class, not the choice of baseline alone.
>
> ⚠ **And a row that fails that assertion is not discarded — it is RE-ROUTED.** It is not a hop
> retirement; it is a **carry-forward gap**: a cure that exists on master and has never reached the
> release branch. That is a different finding and can be a worse one. The instance here: the excluded
> row's cure keeps a runtime test off a fatal path, the release branch carries the seat that makes that
> path an uninterceptable process exit, and the host kill it prevents was measured at 57 verdicts. **The
> false positive was worth more than the row it displaced**, so the step files these rather than dropping
> them, and the hop's own carry-forward census owns them.
>
> ```
> git grep -l -E '^\s*\[module:\s*(go\.)?GoManualConversion\]' -- 'src/core/**/*.cs' | sed "s#^:##" | sort > logs/handowns-outgoing.txt
> git grep -l -E '^\s*\[module:\s*(go\.)?GoManualConversion\]' -- 'src/core/**/*.cs' | sed "s#^:##" | sort > logs/handowns-incoming.txt
> LC_ALL=C comm -23 logs/handowns-outgoing.txt logs/handowns-incoming.txt > logs/handowns-retired.txt
> ```
>
> where `` is `git merge-base master`.
>
> **One row per retired hand-own, and the row is not closed by the deletion being correct.** For each:
>
> | field | what it must carry |
> |---|---|
> | the hand-own | path at the outgoing pin, and the *class it cured* in its own words — a hand-own that cannot say what it was for is itself the finding |
> | why it went | package dropped · file renamed · upstream absorbed the fix · the hand-own's reason expired |
> | the replacement | the package that now carries those semantics at the new pin, **named**, or `none` with the reason |
> | the class, re-censused | the cured class's predicate run at the NEW pin over the replacement — **cured / moved / gone**, with the count |
> | the marker | whether the replacement carries a hand-own of its own, measured, not assumed |
>
> **The verdict a row needs is `cured` or `gone`.** `moved` — the class present at the new pin in a file
> with no hand-own — is an H6 **GATE FAILURE**, exactly as a substantive row left unreviewed is, and it
> is cured before H6 closes.
>
> ⚠ **Do not read the retired set as a delete list to approve.** Every one of these deletions was
> *correct*: the package really did go away. The question H6 asks is not whether the file should have
> been removed — it is **whether the reason it existed went with it**.
>
> *Named blind spot:* the set is keyed on the marker, so a hand-own retired by having its marker removed
> while the file stays reads as present. That is the whole-file-replacement class the marker exists to
> mark, and losing the marker is already an H6 substantive row by the census's own arithmetic — stated
> here so the two are not confused.
>
> *Durable form, after the hop:* a guard in `src/go2cs/internal/repoguard` carrying the previous pin's
> marker list and asserting that every absent entry has a closed row. Banked, not cut during a
> migration — the list's baseline moves at every hop and a guard whose baseline moves needs the hop to
> be over before it can be written honestly.
**The instrument** is `.cs.auto` — the converter's answer to *"what would the automatic conversion of
this file be, today, from this Go tree?"*
**The diff is `.auto`(old release) vs `.auto`(new release), per hand-own — never `.auto` against the
hand-owned `.cs`.** The latter is dominated by the hand-own's *intended* divergence and is unreadable;
the former isolates the upstream delta. **Both `.auto` files must be produced by the SAME converter
binary**, or converter drift contaminates the release axis and the classification is worthless. Each
staging root carries its **own** `version.props` pinned to its own release, so the pin guard passes on
both sides.
*Named blind spot:* if the new converter build cannot parse the OLD tree cleanly, run A degrades and
the baseline is suspect. **Assert run A's package count and marker gate against the outgoing corpus's
before trusting it.**
**Classification — every delta, explicitly, one of three:**
| Class | Meaning | Required record |
|:--|:--|:--|
| **(a) ABSORBED** | the upstream change is real and has been carried into the hand-own | the commit that carried it; a test or gate that observes it |
| **(b) N/A** | the upstream change does not apply to the managed implementation | **the reason, written out** — never the bare letters |
| **(c) REWRITE OWED** | the hand-own must change and has not yet | a named work item, gating the migration or explicitly deferred with owner and reason |
An **empty** diff still gets a record (`unchanged`, with both hashes). A hand-own the run emitted
**no** `.auto` for gets a record too, and that record is **a defect in the audit, not a pass**: either
the seed did not take at that path, or the marker predicate could not see the marker.
⚠ **Two populations, and conflating them makes the gate either a false alarm or a rubber stamp**
(ruled): the **audit** covers *all* hand-owns; the **`.auto` differential** reaches only the ones the
converter re-emits. A supplemental `*_impl.cs` companion has no Go counterpart and therefore no
`.auto` — it is audited **against its principal's `.auto` diff**. A hand-owned *package* is audited by
**manual upstream diff**. **Every record names its evidence class.**
**The completeness gate:**
> No migration's corpus is adopted until every hand-own in the **re-measured** census has a classified
> delta record in that migration's audit file, and every (c) is either closed or explicitly deferred
> with an owner.
Mechanically: re-measure the line-anchored census over `src/core`; assert every marked path appears
exactly once in the audit file; assert every row's class is one of `unchanged`/`a`/`b`/`c`; assert
every `b` carries a non-empty reason and every `c` a work-item reference; assert **zero** rows in the
"no `.auto` emitted" state. Exit non-zero on any violation — the same shape as the repository's other
preflights: cheap, by-path, and impossible to pass vacuously.
Deliverable: one audit file per migration under `docs/phase4/` (ruled), rather than per-package notes,
because the completeness gate must be checkable in one place.
#### Amendment 2026-09-13 — for the 1.23 → 1.24 hop, where the `.auto` pair comes from
- **Old side** `/h4a/src/core/**/*.cs.auto`; **new side** `/h5/src/core/**/*.cs.auto` — one binary,
`cmp /h4a.exe.sha256 /h5.exe.sha256` passing, each root with its own `version.props`.
- **Population:** the marked paths by the census instrument's own predicate, `go.`-qualified spelling included —
never a literal grep, which undercounts.
- **Run A's package count** is asserted against the outgoing corpus's in the H4a baseline amendment (step 4).
- **The differential**, one row per marked path, into `/logs/h6-auto-pair.txt`:
```bash
S=''; while IFS= read -r p; do q=${p#src/}
a="$S/h4a/src/$q.auto"; b="$S/h5/src/$q.auto"
if [ ! -f "$a" ] || [ ! -f "$b" ]; then s=MISSING-AUTO; elif cmp -s "$a" "$b"; then s=IDENTICAL; else s=CHANGED; fi
na=old; [ -f "$a" ] && [ "$a" -nt "$S/h4a.run.stamp" ] && na=new; nb=old; [ -f "$b" ] && [ "$b" -nt "$S/h5.run.stamp" ] && nb=new
printf '%s\t%s\th4a-auto=%s\th5-auto=%s\n' "$p" "$s" "$na" "$nb"
done < "$S/logs/h5-marked.txt" > "$S/logs/h6-auto-pair.txt"
```
and the census: `powershell -NoProfile -ExecutionPolicy Bypass -File '/src/handown-census.ps1'
-FromGoRoot '' -ToGoRoot '' > '/logs/h6-census-.log' 2>&1`
(on the train-47 union the script takes exactly those two parameters plus `-ListUntouched`).
- **Undetermined is not unchanged.** A seeded root cannot tell an `.auto` the run rewrote to identical bytes from
one it never wrote: identical writes are skipped at the merge. A row `IDENTICAL` with `old` on both sides is
recorded `no .auto emitted — undetermined`, which the completeness gate counts as a defect, until a
discriminator is ruled. A candidate discriminator: the sibling writer creates each `.cs.auto` unconditionally in
the per-target STAGE root, so a copy under `/-stage/-amd64` newer than the sentinel may say it
was emitted — NOT MEASURED, and not a ruling.
- **The completeness gate has no instrument** on `a02ac3df3` or the train-47 union: STOP before scoring it; post
the differential and the census; COORD rules the gate script and its exit condition.
- **The relocation blind spot.** A frozen hand-own present in both trees passes any set comparison while carrying
declarations the target release moved (`runtime/internal/sys` → `internal/runtime/sys`). The package-alias
census (`docs/phase4/CENSUS-h6-handown-package-aliases.md`) is read beside every substantive row.
- **The audit file** is `docs/phase4/AUDIT-h6-handown-go124.md`: one row per marked path. R cut the skeleton
(`d18059950`, train 48 seat 4) and G filled it (blocks 1–19, landed on master at `4e672aa4a`, 144 of 145 rows
classified, row 130 recorded REWRITE OWED (c)); it reaches the version branch with master at H12.
### H7 — Compile parity **GATE**
Full `go2cs-stdlib.slnx` build with shared compilation disabled, zero errors, **skipped-dependents
enumerated and zero** (a dependent of a failed project is skipped, not errored — count them). Run
**every** buildable `$(GoTargetOS)` flavor, purging `bin`/`obj`/`Generated` between switches.
**Gate: 100 % of the migration's package set compiles.** Not "as many as before" — 100 %, per the
frame.
#### Amendment 2026-09-13 — the per-flavour build, and the scoring method for the ladder's sites
The gate reads the version-branch tree after the H5 series (`` = ``). The same procedure over
`/h5/src` (`` = `/h5`) is the LADDER — the gate's rehearsal, labelled as one. **The
ladder runs only after H5c's step 5 removal and step 6's C1 patch**: built before the removal it compiles the
stale seed siblings (CS0102), and before the patch it cannot get past `runtime`. The ladder is done when it reads
**zero unique sites on all three flavours with own assemblies at the corpus's order: projects minus the
platform-exclusive set, per flavour.** One flavour per invocation, serially, from a per-run copy of the script; a
box whose default .NET SDK lags the corpus TFM exports the .NET pair first (H10 step 1).
```bash
export MSYS_NO_PATHCONV=1 MSBUILDDISABLENODEREUSE=1 DOTNET_CLI_TELEMETRY_OPTOUT=1
FL=${1:?flavour}; SRC='/src'; LOG="/logs/build-$FL-$(date +%Y%m%d-%H%M%S).log"
[ -f "$SRC/core/golib/golib.csproj" ] && [ -f "$SRC/go2cs-stdlib.slnx" ] || { echo "ABORT: $SRC is not a go2cs src root"; exit 3; }
P=$(find "$SRC" -type d \( -name bin -o -name obj -o -name Generated \) -prune -print | wc -l)
find "$SRC" -type d \( -name bin -o -name obj -o -name Generated \) -prune -exec rm -rf {} + 2>/dev/null
left=$(find "$SRC" -type d \( -name bin -o -name obj -o -name Generated \) -prune -print | wc -l)
echo " purged $P remaining $left"; [ "$left" = 0 ] || { echo "ABORT: purge incomplete ($left)"; exit 3; }
START=$(date +%s)
dotnet build "$SRC/go2cs-stdlib.slnx" -c Debug -p:GoTargetOS="$FL" --no-incremental -m -p:UseSharedCompilation=false > "$LOG" 2>&1
rc=$?
echo " exit $rc after $(( $(date +%s) - START ))s NULs $(head -c 200 "$LOG" | tr -d -c '\000' | wc -c)"
echo " CS occurrences $(grep -aoE 'error CS[0-9]+' "$LOG" | wc -l) MSB/NETSDK occurrences $(grep -aoE 'error (MSB|NETSDK)[0-9]+' "$LOG" | wc -l)"
grep -aoE '[A-Za-z0-9_./\\-]+[.]cs[(][0-9]+,[0-9]+[)]: error CS[0-9]+' "$LOG" | sed 's#\\#/#g; s#.*/core/#core/#' | LC_ALL=C sort -u > "$LOG.sites"
echo " unique sites $(wc -l < "$LOG.sites") ROOTS $(grep -vc Generated "$LOG.sites") CASCADE $(grep -c Generated "$LOG.sites")"
built=0; unbuilt=0; : > "$LOG.unbuilt"
while IFS= read -r p; do
d=$(dirname "$p"); a=$(grep -aoE '[^<]+' "$p" | head -n1 | sed 's///'); [ -n "$a" ] || a=$(basename "$p" .csproj)
if ls "$d"/bin/Debug/*/"$a".dll > /dev/null 2>&1; then built=$((built+1)); else unbuilt=$((unbuilt+1)); echo "${p#*src/core/}" >> "$LOG.unbuilt"; fi
done < <(find "$SRC/core" -name '*.csproj' ! -name '*.tests.csproj')
echo " ASM (own assemblies) $built projects $((built+unbuilt)) none $unbuilt"
```
(`grep -c` exits 1 on a zero count; these lines print, they do not gate.) All counts are read before the next
flavour's purge; on the version-branch tree, `git status --porcelain | grep '^ D'` prints nothing after every purge
(floor 8). **Per flavour the gate is:** exit 0; CS and MSB/NETSDK occurrences 0; every project in `$LOG.unbuilt`
is platform-exclusive to another flavour — its package absent from `GOOS= go list std` under `pin
go1.24.13 … || exit 3`, run from a no-module directory.
**Scoring rules.**
- **Units never mix.** Per flavour: CS occurrences (they move with console verbosity and with every referencing
project), unique sites (`file(line,col): error CSnnnn`, keyed after `core/`), ROOTS (unique sites outside
`Generated`), CASCADE (unique sites inside `Generated`), and ASM (own assemblies, the gate's arithmetic). A
comparison names both units and both trees; a step between readings that moved more than one axis is not
attributed to one of them. A dll-file count with copies included is not ASM.
- **Every unique site is assigned to one owned class** — leftover seed (H5c), frozen metadata, converter emission,
hand-own, generator cascade — with its owner; a CASCADE site takes the owner of a ROOT in the same project, and
a site in no class is the finding.
- **Predictions are posted before the build and scored as worded**, the wording quoted beside the reading; the
predictions of record are those owed in `REHEARSAL-h5-go124.md` §15 §8, each with its premise re-read at the
tree first (a prediction whose premise is absent is posted VOID with the reading that voids it, never scored).
A falsifier stated in advance is scored even when the mechanism it tested survives.
#### Amendment 2026-09-16 — the per-flavour gate as executed at the 1.23 → 1.24 hop: arm 4 is inert where every project builds; the build root's spelling; arms gate on their command's rc
- **Arm 4 is inert on this corpus.** Every csproj under `src/core` builds regardless of `-p:GoTargetOS`, so
`$LOG.unbuilt` is EMPTY on every flavour and "every unbuilt project is platform-exclusive to another flavour"
is MET over zero items — vacuous, never evidence. Measured at the 1.24.13 hop's compile-parity tree on all
three flavours: unbuilt 0, ASM 343 of 343 (windows read on two boxes, linux and darwin on one). The arms that
carry a per-flavour reading are: exit 0; CS occurrences 0; MSB/NETSDK occurrences 0; unique sites 0; and
ASM = projects. Platform exclusivity is a property of the EMISSION's per-GOOS file sets, read at H8's census,
not of projects.
- **Minus golib.** `golib` is absent from every `go list std` set and must nonetheless be BUILT; any arm keyed on
"absent from `go list std`" passes it as legitimately unbuilt. Where arm 4's derivation is still used (e.g. to
size an expected unbuilt set), subtract golib first — and expect the arithmetic NOT to predict unbuiltness on a
corpus where every project builds: the linux prediction of 338 missed against a measured 343 for exactly this
reason.
- **Spell `` as a drive-letter path (`C:/…`), never `/c/…`.** The script exports `MSYS_NO_PATHCONV=1`
for dotnet's `-p:` arguments, so a POSIX spelling reaches native `dotnet` and `git` UNCONVERTED: `dotnet build`
fails on a non-existent project path, and `git -C` dies with "cannot change to … No such file or directory".
One launch was lost this way, and three added git arms printed PASS from `wc`/`grep` over a failed command.
- **Every arm gates on its command's own rc**, never on the shape of its output; the HEAD arm asserts a sha-shaped
value, not merely non-empty. An arm that cannot distinguish "clean" from "the command died" is not an arm — and
a guard's DESCRIPTION is not the guard: state the mechanism actually implemented, and run it on real input
before quoting it.
- **ASM units.** The script's ASM counts own assemblies over core csproj excluding `*.tests.csproj` (343 at this
hop = the census's core population). A "distinct produced assemblies" count from the build log reads one higher
(344: the solution's one non-core member). Name both units; never reconcile a difference of one by feel.
### H7a — The master fold **GATE**
**The ladder has no rung that folds master into the release branch, and the run rungs need one.** A hop
runs on a long-lived release branch while master keeps taking cuts. Everything from H8 onward *runs* the
corpus — re-emits it, rebanks goldens, re-derives the roster — and a run rung measures the tree it is
given. If master holds cures the branch has never carried, H8 onward measures a corpus that is wrong in
ways the migration did not introduce and cannot see.
**Ruled after an instance, not in advance.** At the go1.23.12 → go1.24.13 hop, master held 105 commits
the release branch had never carried. Among them: a managed `gcTestIsReachable` and the stop that keeps
the fatal path from ending the test host. The release branch carried the seat that makes `runtime.throw`
an uninterceptable process exit, and *not* the cure that keeps the runtime test off it — so the host kill
that had cost the runtime row 57 verdicts was live at the branch tip, silently, with the hand-own and its
converter registration absent **together** rather than as a dangling displacement that would have failed
loudly. It was found by a retired-hand-own census reporting a false positive (see H6), not by anything
looking for it.
**The step: ONE merge, master INTO the release branch, as the first act after the last compile-side seat
lands and before any run rung.** Never a rebase — the branch's SHAs are posted, and a hop's seats are
announced refs.
**Sized before it is taken, and the sizing is a post of its own:**
| | what it must carry |
|---|---|
| the pins | master, the branch tip, and `git merge-base` of the two — **all three re-read at the act**, because both move while the sizing is written |
| the range by path class | the master-only commits classified (docs and instruments / converter guards / converter production / corpus / repo config), merges counted separately because a merge carries its children's paths, and **no residue** — a commit matching no class is named, not dropped |
| what the branch LACKS | every hand-own and every emission-or-CLI change in the range, **named**, not counted. These are the fold's reason; the docs and guards are why the commit count is large and are not why the fold exists |
| the carry-forward set | marked files present on master and absent at the tip, each classified **relocated / gone / gap** — a relocation is verified by its counterpart's PATH, never by basename |
| the conflict set | `merge-tree --write-tree` at those two SHAs **with the ARGUMENT ORDER named**, by path, with the fingerprint stamped — the stamp differs by order while the conflicting paths do not, so the act is scored in the order the prediction stamped in |
| the prediction | what the merged tree must equal **and in which sense** (see *Scored on*), and the falsifiers |
**Scored on:**
- **the dry-run stamp, scored as what it is.** `merge-tree --write-tree` fingerprints the **inputs and
the merge machinery**, not the commit:
- a **conflict-free** fold — the merged tree is **byte-identical** to the stamp;
- a **conflicted** fold — the stamp **cannot be equalled**: it carries markers for every conflicted
path, and the landed tree differs from it by exactly the resolved paths. Score instead that the
resolved paths are **exactly** the predicted set and that each resolution matches its ruled class;
if a stampable figure is wanted, predict the **post-resolution** tree before the commit and score
against that. ⚠ Scoring a conflicted fold against the dry-run SHA asks for a tree the act cannot
produce, and a reader who takes it literally reads a **correct** fold as a miss;
- the conflict set exactly the predicted paths — **no path resolved that was not predicted**;
- ⚠ **silent subtraction, per symbol and in BOTH directions.** A fold crosses every seat the hop has
landed. Each landed marker is asserted **by name at its count**; a clean merge rc says "no conflict",
never "nothing dropped";
- each carry-forward gap present after, **with the whole of its cure** — a hand-own landed without its
converter registration is a partial fold, and partial is how this class hides;
- relocated packages still at their NEW paths and the old paths **not resurrected** — a modify/delete
resolved the wrong way re-creates a package the hop retired;
- any source root the range does not touch **byte-identical**; movement there is a MISS, not a bonus.
**Resolution by class, decided before the merge rather than at the conflict:**
- **regenerable metadata** (`package_info.cs`, `.csproj`) — **re-minted**, never hand-merged. These are
artifacts; a hand merge of an artifact is a hand-written artifact;
- **projitems** — the **union**, and the row count asserted afterwards with no duplicates;
- **a modify/delete where the delete is the hop's own package retirement** — the **delete stands**;
- **a code conflict whose two sides are a displacement and the body it displaces** — **if the file is
HAND-OWNED**, master's side, and the displacement's registration lands **with** it. **If the file is
EMITTED, it is RE-MINTED from the merged converter**, exactly as the regenerable metadata above is:
the merged tree carries the displacement's registration, so the re-mint emits the file *without* the
displaced body and *with* the hop's own calling convention. ⚠ **An emitted file is never resolved by
SIDE** — taking master's side reinstates master's **pre-hop emission of the whole file**, including
every call written against a signature the hop re-signed.
**⚠ The closing check is the cheapest proof the fold did what it is for:** re-run H6's retired-hand-own
step. After the fold the merge-base *is* master's tip, so that step reads clean **by construction** — and
if it does not, the fold is incomplete and the rows it still reports are the gaps it failed to carry.
*Named blind spot:* the fold answers "what has master got that the branch has not". It says nothing about
the reverse, and nothing about a cure that exists in **neither** — a class that was broken before the hop
began stays broken and is H6's and H7's to find, not this step's.
*What this step is not:* a licence to take master's tip mid-hop whenever it is convenient. It is **one**
merge at **one** boundary, sized and predicted, because a release branch that keeps re-merging master has
stopped being a release branch.
### H8 — Multi-platform re-emission **GATE** ⟲
Re-run the multi-target emission and the platform census, and diff the manifest against the outgoing
one. A migration changes the platform axis in **two** directions at once: new packages may be
platform-varying, and existing ones may stop being so. The per-GOOS package count is a measurement,
not a constant.
**Gate:** the platform manifest's marker gate is zero per target, and the default-flavor build
reproduces the single-target build byte-for-byte.
#### Amendment 2026-09-13 — for the 1.23 → 1.24 hop, H8's emission is H5's, and the manifest's comparand is owed
- The three-target emission IS the H5 reconvert; H8 does not re-run it.
- The census, under its own `pin go1.24.13 '' '' || exit 3`, with the H5
binary, **before the overlay** (the only CLEAN tree whose `version.props` reads 1.24.13 is `` at `
`
before the overlay dirties it, or a second clean worktree of `
`
before the overlay dirties it, or a second clean worktree of `
`), into a directory never reused:
```bash
'/bin/go2cs.exe' -stdlib -comments -platforms windows/amd64,linux/amd64,darwin/amd64 \
-platform-census '/census-1.24.13' -go2cspath '/src' > "/logs/census-1.24.13-$(date +%Y%m%d-%H%M%S).log" 2>&1
```
`-comments` because the census's control target is supposed to reproduce the seed byte for byte, and the seed
was emitted with comments. Read `/census-1.24.13/platform-manifest.json`'s class counts
(shared / variant / partial / exclusive) and the per-target marker gate (must be zero).
- `-goroot` and the loader: the loader follows the environment. A converter carrying `7c1d8832f` refuses a flag
that disagrees with a set environment, and exports the flag only when the environment is unset. The pin
assertion, which always exports GOROOT, is the selector.
- **STOP before scoring the gate.** No outgoing manifest is committed, and neither its comparand nor the
default-flavour byte-identity arm has a procedure at this hop. Post the 1.24.13 manifest's per-target marker gate
and class counts; COORD rules the comparand.
#### Amendment 2026-09-19 (C2) — the comparand's provenance, the byte-identity arm, and the predicted deltas
The amendment above stops at *"neither its comparand nor the default-flavour byte-identity arm has a
procedure at this hop"*. This closes both. The instrument is [`src/h8-comparand.sh`](../src/h8-comparand.sh)
(`selftest`: 20 arms, every one **made to fail and restored**); it is a reader of manifests and package
sets, converts nothing, and writes into no corpus.
##### (a) The 1.23.12 outgoing manifest is **PRODUCED**, not recovered
Four candidates were measured before one was chosen. Three are refused, and two of them are refused
for reasons that would not have shown up as an error:
| candidate | verdict |
|:--|:--|
| a committed 1.23.12 platform manifest | **does not exist** — no `platform-manifest` file is tracked on any ref, and none ever has been |
| the preserved **half-A** staging roots (`c883a2dc7` §3) | ⚠ **WRONG RELEASE.** Half A's own recipe pins `GOROOT` to the go1.24.13 SDK and notes `version.props` already reads 1.24.13 — half A is the **incoming** side. Scored against G's 1.24.13 manifest it compares the release with itself: **an arm that cannot fail**, reporting a perfect zero delta |
| the preserved **half-B** staging roots (`a5534b5de` §2) | right release (go1.23.12, three targets) but **wrong artifact kind**: half B ran `-platform-stage`, the emission, not `-platform-census`, and its manifests cover **seeded** staging roots — see the seed tell below |
| the **H0** baseline | **does not contain one.** H0 captures the `.cs.auto` baseline, the package census, the roster snapshot and the disclosure manifests; the platform manifest is not among them |
⚠ **A seeded-root manifest is not a census.** A seeded staging root's path set is *(seed ∪ emitted)*
and all three targets share one seed, so such a manifest carries **no emitted-vs-seeded
discriminator** — which is precisely why the converter's own census answers that question with a
sentinel MTIME instead of content. Classify three seeded-root manifests and the `partial` and
`exclusive` counts come from the **seed's** path set rather than from any emission, while looking
exactly like class counts. The two preserved halves show the shape directly: half B's roots hold
3990 / 3995 / 3993 `.cs` against a 3896-file seed, and half A's hold 3898 on all three — the
difference is how far each seed already sits from the release being emitted, not a platform axis.
**The tell, and it is cheap:** in a true per-target emission census a `*_windows.*` artifact **cannot**
be emitted by the linux or darwin target. `h8-comparand.sh classify` refuses a triple in which a
platform-suffixed artifact appears in a foreign target's manifest, rather than returning a number
that reads like a census. `--seeded-content-only` accepts such a triple for the one question it *can*
answer — which shared paths differ in content across targets — and labels its own output as not
emission classes.
**Therefore the outgoing manifest is produced by running the same instrument under the outgoing pin**,
one axis from the 1.24.13 census (`GOROOT` + `version.props`), same binary, same flags, same seed,
into a directory never reused:
```bash
'/bin/go2cs.exe' -stdlib -comments -platforms windows/amd64,linux/amd64,darwin/amd64 \
-platform-census '/census-1.23.12' -go2cspath '/logs/census-1.23.12-$(date +%Y%m%d-%H%M%S).log" 2>&1
```
⚠ `version.props` must be the **outgoing** release's, verbatim: with the incoming 1.24.13 pin the
converter **refuses, exit 1, by design** (measured, `a5534b5de` §2). That refusal is the arm proving
the outgoing leg really ran against the outgoing tree, so it is a feature of this step, not an
obstacle to route around.
Half B is **not** discarded — it is the corroborator. Its three per-target manifests answer the
content axis under `classify --seeded-content-only`, and a variant count from the produced census
that disagrees with half B's content partition over the shared path set is a finding in one of the
two, named before either is believed.
##### (b) The default-flavour byte-identity arm
The gate's wording is *"the default-flavor build reproduces the single-target build byte-for-byte"*.
The two emissions, spelled:
- **E1, the single-target build** — `-stdlib -comments -platforms