schema lock: fixed tables evolve append-only, and the compiler holds the line - #825
Merged
Merged
Conversation
…the line A fixed-size table is a plain C record: no ids, no terminators, walked by offset, so the field ORDER and the field WIDTHS are the whole contract. Everything the id-table wire absorbs — a reordered field, a removed one, a widened one — a fixed table reads as garbage, silently, with no counter to fire. The owner ruled the discipline: "append only, and deprecation possibly. might keep it light weight?" Network Next ran the same rule by hand for years — "manual stuff, and it is a footgun, but it worked." This is that discipline with the footgun taken away. - internal/lockfile: schema.lock beside the unit's schema files, written by the compiler only. Per fixed table, the field sequence in declared order — each entry (wire id, kind, width in the record, deprecated flag) — plus the hash of that sequence. The check: the locked sequence must be a PREFIX of the live one, entry for entry, with `deprecated` allowed only to turn on. - `| deprecated`, a tag: the slot stays where it is, the writer emits the field's default, the reader ignores it, and a guard may not name it. One-way. - `schema lock` [--print]: the only writer, and it refuses to write a lock the check would refuse. - Compiler.SchemaLock, set by `schema check` and `schema generate`. - docs/SPEC-TABLES.md §2.10, in the LANGUAGE section: the rule, the marker, compaction as a new table under a new name, the lock, the refusals. - Fifteen committed locks over every unit in the tree with a fixed table. Which tables are fixed is derived (!ir.VariableTables) in ONE place, lockfile.FixedTables, with a TODO naming the `fixed table` keyword's declared flag: when that branch lands, the switch is one line. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
Author
|
Coordinator note (Rowan): take the STRICT reading. A schema whose fixed tables have drifted from the lock in any way, including a plain append, fails |
The coordinator's reading, and the strict one: a schema whose fixed tables have drifted from the lock in ANY way fails `schema check` and `schema generate` — a plain append included. The APPEND-ONLY rule says what a fixed table may BECOME; it never said the committed file may lag behind what the table already is. A lock that trails the declaration is a record of a layout nobody compiles, and the file in the tree is the record. So the comparison now has two readings, and they differ in ONE thing: what to make of a declaration that has moved past the lock. lockfile.Current is the check's — the lock is the live sequence, entry for entry, table for table. lockfile.Appendable is `schema lock`'s — the locked sequence is a PREFIX of the live one, which is what lets the ONE WRITER move the file at all. Every break refused before is refused by both verbs still: a reorder, a removal, a widening, a kind change, an un-deprecation, a table withdrawn, a hand-edited file. The three changes the rule ALLOWS — an append, a new fixed table, a `deprecated` marker — are the ones that were passing a stale lock, and they are one finding under one sentence, because the remedy is one command. It names the table and the first entry the lock lacks, and a table with no fields yet has no entry to name, so it names the table alone. The effect is the one the coordinator asked for: a change to a fixed table is never in the tree without the record of it, because the compile that would have shipped it refuses first and says what to run. The committed locks are current, so `make check` and the corpus test are unmoved, and generation over every unit in the tree — which now runs the strict check on each — regenerates with zero drift. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…er alone The owner asked the file the question it exists to answer: "Is there anything schema check cannot catch in a fixed table? Can we fix that so it does?" The lock held a field's id, its kind and its width — which is what a reader walking to an offset stands on ONLY if nothing else about the field can move underneath it. Six things could, and the file now holds them all. Rendering version 1 → 2, which makes every committed lock stale at once, deliberately and visibly. - THE DEFAULT, declared or implicit, on every entry. "No default declared" is not the absence of a fact — it is the fact that a fresh record holds zero in that slot. A deprecated slot holds it, an absent field on the id-table wire reads back as it, and a reader handed an older writer's record fills the missing field from it, so a moved default changes what every such record means. `default=` renders the EVALUATED value: an integer, a float, `false`, `bytes:<hex>`, `variant:Gunship`, `mask:0`, `born:N` for an array's fresh count, and `zero` for a nested record or union whose own block holds the rest. - THE RANGE: `min`, `max`, a compressed float's `res`, and a fixed-point field's `frac`. `fixed(16,16)` and `fixed(24,8)` are one width under one kind and read one stored raw integer as two different numbers, with no counter to fire; the bounds are the scale the value comes back at. - THE `?`, compared BEFORE the width because it is the exact account of a move the width would report vaguely: an optional carries a presence bool beside its value inside the record. - EVERY ENUM AND FLAGS TYPE the fixed tables reach, as its value list in declared order, in a block of its own, append-only. In a fixed record an enum rides as its dense ordinal and a flags variant is its bit position: the list is the mapping from a stored number to a meaning, so it grows at the END or it lies. A reorder, a rename, an insert and a removal are refused; an append is allowed and still has to be written down. - EVERY UNION they reach, arm order, the same rule for the same reason — the tag is the arm's position. - EVERY TYPE IN THE CLOSURE and not the root alone. A nested `type`'s fields ARE the holder's bytes, so a field retyped one level in moves what a reader of the root gets back without moving one line of the root's entries. Each gets a `type <Name> layout=0x…` block rendered exactly as a table's is. A field appended at the bottom of a nested record is NOT an append — it grows the holder's slot and slides every field after it — so the compile reports both halves, the holder's width first. The refusals keep §2.10's shape: the declaration and the first differing line by name, the reason, and the remedy, which is always one of three — append, deprecate and add, or a new table under a new name. The block keyword is the declaration's own word, so a refusal reads as the schema reads: `fixed table Config`, `type GunnerSettings`, `enum Hull`, `flags Perks`, `union Effect`. AND §2.10 NOW NAMES THE ONE CLASS THAT IS BEYOND THE FILE: a field that keeps its name, its id, its kind, its width, its default, its range and its `?` and changes only what it MEANS — seconds becoming milliseconds, an index counted from zero becoming one counted from one — moves no fact the lock records and no byte a reader could compare, so nothing here can refuse it, and the rule for it is the rule for every other break: deprecate and add. Held both ways, on a second fixture built in two halves that mirror each other field for field: a FIXED table and its closure, beside a VARIABLE-LENGTH one (it holds a pointer) and a closure only it reaches. Thirteen edits are each measured three times — refused on the fixed half with the line named; passing in silence on the variable-length half, where the id-table wire is what those edits are FOR and the lock has no opinion; and refused by `schema lock` too, with the file left exactly as it sat. Beside them: an append to an enum, to a flags mask and to a union, each refused as stale and then written with every line before it untouched; the nested append that slides its holder, refused twice; a type dropped from the closure in all four spellings; a hand-edited value list caught by its own hash; and the widened file through its own parser. A lock written under an older rendering version is the one file `schema lock` will not repair — the version is the compiler's own and there is no history to salvage, unlike a baseline — so that refusal now names the remedy that works rather than sending a user around a loop between the two verbs. The fifteen committed locks are regenerated. Between them the corpus's fixed tables reach 99 records, 16 enums, 3 flags masks, 6 unions and 14 nested types, every one of which the file now holds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
marked this pull request as ready for review
September 9, 2026 19:53
Johnny Grok. The lock recorded a field's name, id, kind, width, default, range and `?`. A kind-13 slot that kept all of those and changed Buff to Debuff was silent. So were ?Buff to ?Debuff, a union arm whose payload types swapped with the names kept, and [4]int32 to [4]float32. Four probes, zero errors. Rendering version 2 → 3. The new facts sit on the hashed lines, so a v2 lock is deleted and rewritten, the same sentence v1 took. - kind 13 and 15: `held=Buff@0x...` — the type's wire name and its layout hash (a union's values hash). `?Buff` is still optional plus held=. - kind 14: `elem=4/4` — element kind and width, and held= beside it when the element is a named type. - a union arm: `payload=Buff@0x...` beside the arm name; a scalar arm keeps the type spelling, `payload=int32`. Diff refuses when the held name, the array element, or the arm payload type moves. The sentence names the slot and both types. The remedy is deprecate and append. A same-name hash-only move on the holder is left to the nested type's own block, so a change inside Buff still names Buff. The four probes are four rows in lockclosure_test.go, each measured three ways on the existing Config/Bag fixture. The committed locks are regenerated.
Johnny Grok.
Johnny Grok. The lock recorded which type a kind-13/15 slot holds, and an array's element kind. A kind-7 slot that kept all of that and changed Hull to Cargo was silent. So were Perks to Rigging (kind 9), and [4]Hull to [4]Cargo (elem=7/1, no held=). Both types stayed in the closure through Manifest, so nothing fired. Rendering version 3 → 4. The new facts sit on the hashed lines, so a v3 lock is deleted and rewritten, the same sentence v1 took. - kind 7 and 9: `held=Hull@0x...` — the type's wire name and its values hash. - kind 14 of enum/flags: held= beside elem=. - a `default:` arm calls held() for every other kind, so a future named slot is not silent the same way. Diff reuses the existing held= sentence. The three probes are three rows in lockclosure_test.go, each measured three ways on the Config/Bag fixture. The committed locks are regenerated.
The sweep that gave kind 7 and kind 9 a held= stopped one kind short. A kind-16 slot — `[Hull]int32` — recorded neither the key nor the element: `field by_hull kind=16 width=12 default=born:0` and nothing else. So on the Manifest fixture that found the enum-slot gap, three more edits were silent with the lock byte-identical and `schema check` at exit 0: - `[Hull]int32` → `[Cargo]int32`: the key's variants ARE the slots, so a value written into Gunship's slot reads back as Ice. - `[Hull]int32` → `[Hull]uint32`: same width, same slot count. - `[Hull]int32` → `[Hull]float32`: stored integers read back as floats. held= reached kind 16 already, but held= is the ELEMENT's named type — the corpus shows `held=TurretConfig@0x...` on a keyed array — and the key is a different fact. So it gets its own: `key=Hull@0xc3cc3881c580dd9d`, the enum's wire name and its values hash, beside the `elem=` every other array carries. `ir.buildversion` already spells a keyed array's key this way. Rendering version 4 → 5. v4 shipped in no release — it is this PR's own — but several trees are tracking this branch, and a v4 lock met by a v5 compiler should hear "delete it and write it again" and not a layout-hash refusal whose remedy is the wrong one. - renderTable takes held= for EVERY kind unconditionally, rather than through a switch whose default arm and whose 13/15 arm did the same thing. The two array kinds add elem=; a field with a KeyEnumRef adds key=. - parseHeld reads both held= and key= and names the one it was given. - the diff compares the key after the element, before the range. - two more closureBreaks rows, each measured the file's three ways. Their key enums (Bay/Deck, Carton/Rack) are keys and nothing else, so no other row's edit moves the width of the slot array they size, and both sit in Manifest/Invoice so a repointed key is the ONE thing the swap changes. - TestALockFromTheRenderingJustBeforeThisOneSaysTheSameThing: not a two-line stub but a WHOLE well-formed lock one version back, which is the file a person actually meets. Both verbs name the one remedy, `schema lock` writes nothing over it, and the remedy reproduces this rendering byte for byte. Eight keyed-array entries across tables/examples, tables/block and tables/scalars gained key= and elem=. All fifteen committed locks regenerate byte for byte, twice — in place, and deleted and rewritten from scratch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The keyed-array clause pushed the line to 116 columns. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gafferongames
added a commit
that referenced
this pull request
Sep 10, 2026
Keep both #825's schema lock (SchemaLock) and this branch's --fixed-record-limit plus the form-byte check. Conflicts were cmd/schema/main.go and compiler/compiler.go. Johnny Grok.
gafferongames
added a commit
that referenced
this pull request
Sep 10, 2026
#825's lock derived the set from ir.VariableTables. The keyword is the class now, so FixedTables reads FixedDeclared. Fixtures that were inferred-fixed take `fixed table`. tables/examples/schema.lock drops Patrol, which is a plain table because a guarded branch is variable by design. generated/bench/tables/c re-pins TableMixed as the fixed class. Johnny Grok.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Glenn's decisions
Fixed tables evolve append-only, enforced by a lock file the compiler owns.
His words:
On the mechanism:
And then, of the mechanism itself:
That question is what the lock's second half answers: the order and the widths
are what a reader stands on ONLY if nothing else about a field can move
underneath it, and six things could.
Network Next did this by hand for years — a
uint8version at the front, bumpon change, append at the bottom, manual upgrades for a limited time:
It worked because the discipline is right. It was a footgun because nothing
enforced it. This PR is that discipline with the footgun taken away.
Why fixed tables need it
A variable-length table rides the id-table wire, where evolution is wire
tolerance: fields may be added, removed and reordered, and the reader finds
each one by its id. A fixed-size table has none of that. It is a plain C
record — no ids, no terminators, no names, walked by offset — so the field
ORDER and the field WIDTHS are the contract. A reordered field, a removed
one, a widened one: every one of those is read as garbage by a reader holding
an older record, silently, with no counter to fire.
The mechanism
1.
schema.lock, committed beside the schemas at the unit root, written bythe compiler only. For every fixed table, its field sequence in declared order
— each entry carrying the field's id, kind, width, default
declared or implicit, declared range and resolution,
?anddeprecatedmarker — and then a block for every type those tablesreach: a nested
typeas a record of its own, and anenum, aflagsmaskor a
unionas its value list in declared order. Each block carries thehash of its lines.
What the lock holds, in full:
min,max, a compressed float'sresfracfixed(16,16)andfixed(24,8)are one width under one kind and read one stored number as two different values?deprecatedmarkertype's fields ARE the holder's bytes, so a field retyped one level in moves what a reader of the root gets back without moving one line of the root's entriesheld=Hull@0x…, on kinds 13, 15, 7, 9 and an array of any of themHull→Cargoreads a stored ordinal 1 back asIcewhereGunshipwas writtenelem=7/1, on kinds 14 and 16key=Hull@0x…, on kind 16The first row is the whole of the original revision. Everything under it
arrived since, and the rendering version has moved with each widening —
1→2→3→4→5— because every one of these facts sits on ahashed line, and a lock that does not carry them cannot be repaired by hand.
Each bump makes every committed lock stale at once, deliberately and visibly.
The name is compared, not the hash. Two enums with identical value lists
hash identically —
enum Hullandenum Rankover the same three variantsare both
values=0xc3cc3881c580dd9d— soheld=andkey=carry the wireNAME beside the hash, and it is the name the diff compares. A slot repointed
between two structurally identical types is still a slot that means something
else.
The one class it cannot catch, and the spec says so. A field that keeps its
name, its id, its kind, its width, its default, its range and its
?andchanges only what it MEANS —
timeoutcounted in seconds becomingtimeoutcounted in milliseconds, an index counted from zero becoming one counted from
one — moves no fact the lock records and no byte a reader could compare. The
rule for it is the rule for every other break: deprecate and add.
An append inside a nested record is not an append. A field at the bottom of
a
typea fixed table holds by value grows the holder's slot, which slidesevery field after it, so the compile reports both halves: the holder's width
first, and the nested record's own new entry beside it. A nested record has no
bottom of its own; the bottom belongs to the table.
The width is the field's WHOLE storage in the record — an array's elements
together, a
string(N)'s buffer and its length companion, an optional's valueand its presence bool — because that is the fact a reader standing at an offset
is standing on. The layout hash is derived and written down anyway: it and the
entries above it are one statement made twice, so a lock whose halves disagree
has been edited by a hand rather than written by the compiler.
2. The check, on every compile (
schema check,schema generate): THELOCK IN THE TREE IS THE LIVE SEQUENCE, entry for entry, value for value, block
for block. The
APPEND-ONLY rule says what a fixed table may BECOME; the check says the
committed file must BE what the unit declares today, so a lock the declaration
has moved past — an appended field, a new fixed table, a
deprecatedmarkernot written down — is STALE and refused like any other drift, naming the table,
the first entry the lock lacks, and the one command that fixes it. Every change
to a fixed table therefore lands in the same commit as the record of it. No
file, no check.
3.
schema lockrewrites the file and only ever appends entries andvalues, adds blocks or flips
deprecatedon. It runs the check's owncomparison first and refuses every break the check refuses — a reorder, a
removal, a widening, a kind change, a moved default, a moved range, a flipped
?, a reordered value list, an un-deprecation, a table withdrawn, a type gonefrom the closure, a hand-edited file — so the
one command that moves the file cannot be the one that breaks the rule. The ONE
difference between the two readings is the one this command exists for: where
the check refuses a declaration the lock has not caught up to, this command
writes it down.
--printwrites nothing.A lock written under an OLDER rendering version is the one file this command
will not repair — the version is the compiler's own, and unlike a baseline
there is no history to salvage — so its refusal says the remedy that works:
… delete it and write it again withschema lock``. Every bump in this branchkeeps that path live, and it is walked twice by test: once from a two-line
stub, and once from a WHOLE well-formed lock one version back, which is the
file a person actually meets. Both verbs name the one remedy,
schema lockwrites nothing over the file it cannot read, and the remedy reproduces the
current rendering byte for byte.
There is no
--reasonand no history, unliketables-baseline --update:moving a baseline declares a break with data already written, while every write
a lock accepts is an append, and an append breaks nothing.
4.
| deprecated, a TAG (the valueless half of the qualification section —there is nothing for it to take a value of). The slot stays exactly where it
is, the writer emits the field's default, the reader ignores it, and the one
NEW USE the compiler refuses is a guard: an
ifcondition may not name adeprecated field. The other half of "guard or key" the language already gives
for free — a map key takes no qualification at all (§2.8), so a key can never
carry the marker. It is one-way.
5.
was = "old_name"keeps the field's id, and the lock records WIREnames, so a rename moves not one line of the file.
6. Compaction is a new fixed table under a new name. No mechanism, one
sentence in the spec.
The refusals, verbatim
Reorder / insert before the end / removal from the middle:
Removal from the end:
Width change (checked before the kind, because it is the fact that slides every field after it):
Kind change at the same width (
float32read asint32moves no byte and still lies):Un-deprecation:
A moved default, declared or implicit:
A moved range, resolution or fixed-point scale — none of which moves a byte:
A flipped
?, checked before the width because it is the more exact account ofthe same move:
A reordered, renamed or inserted enum variant,
flagsvariant or union arm:A change one level in, inside a nested
type— the root's own entries do notmove one line:
A slot repointed at a different type, at the same width and with every other
fact of the entry unmoved. One sentence covers a nested record, a union, an
enum, a flags mask and an array of any of them, because it is one break:
An array's element respelled at the same total width — a
[4]Hullthat became[4]Cargo, or a[Hull]int32that became[Hull]float32:A keyed array pointed at a different key enum.
HullandCargohave threevariants each, so the slot is the same width and the slot COUNT is the same
too; what moved is which variant each slot belongs to:
A type gone from the closure:
An APPEND to an enum, allowed by the rule and still refused until it is
written down —
schema lockthen records it, with every line before ituntouched:
A fixed table that is gone:
A lock hand-edited to lie:
A stale lock — an appended field, an appended variant or arm, a new block, a
deprecatedmarker the file has not caught up to. One sentence, one remedy; only the clause naming the differencevaries:
A deprecation reads
… is deprecated in the declaration and live in the lock,and a new block with no line yet has none to name:
fixed table Marker is in the declaration and not in the lock — ….A guard on a deprecated field:
Which tables are FIXED
origin/fixed-table-keyworddoes not exist yet, so this branches frommainand derives the set with
!ir.VariableTables(u)[name], in one place —lockfile.FixedTables— carrying aTODO(fixed-table-keyword)naming thedeclared flag. When the keyword lands, the switch is one line in that
function's body.
Locks added (15)
examples-wide,tables/arms,tables/backend,tables/block,tables/blockhome,tables/examples,tables/lists,tables/maps,tables/messages,tables/pointers,tables/scalars,tables/stream,tables/vocab,tables/vocab9,test/table-base64.Every other unit in the tree declares no fixed table and carries no lock; a
TestEveryUnitWithFixedTablesIsLockedkeeps that register honest and fails theday a unit grows its first fixed table without one.
test/tables/holds manysingle-file units in one directory (SPEC §3.2), so it has no place for a unit
lock and is not covered.
Held by test
internal/lockfileruns the whole mechanism over a fixture package, both ways:append / new table / new fieldless table /
deprecated→ the check REFUSESuntil
schema lockwrites it, then passes, and the write is an append (the oldentries are a prefix of the new ones, and
ir.RecordLayoutproves adeprecation moved no byte); reorder / removal (middle and end) / widen / kind
change / insert in the middle / un-deprecation / a table withdrawn / a
hand-edited lock → each refused with the entry named; a reorder, a removal and
a kind change are refused BY BOTH VERBS with the file left alone;
was =rename → not a change, so nothing to catch up to and the file does not move;
the driver's own load path refuses.
compiler/lockdeprecated_test.gogenerates the same schema with and withoutthe marker across cpp / c / go / cs and requires the outputs to be identical
but for the reflection descriptors' tags column — so the slot is still
declared, still initialized to its default, its offset assertion unchanged, and
still written, and the claim assumes no particular wire.
internal/lockfile/lockclosure_test.gois the widening, measured on a secondfixture built in TWO HALVES that mirror each other field for field: a FIXED
Configand everything it reaches, beside a VARIABLE-LENGTHBag(it holds a*Bag) and a closure only it reaches. Every fact the file gained is measuredthree ways —
TestClosureBreakRefused,TestClosureBreakOnAVariableTableIsNotTheLocksBusiness,TestClosureBreakRefusedByLockToo— over twenty-one edits: a moved default, adefault given to a slot that had none, a widened integer range, a moved
resolution, a moved fixed-point scale at the same width, a
?turned on, a?turned off, a kind change inside a nested
type, a nested slot repointed atanother type, an optional nested slot the same way, a union arm's payloads
swapped with the names kept, an array's element type, an enum slot repointed,
a flags slot repointed, an array of enums repointed, a keyed array's key enum
repointed, a keyed array's element respelled at the same width, and a
reordered, renamed or removed enum variant,
flagsvariant or union arm. Each is refused on thefixed half with the line named; each of them on the variable-length half
passes in silence and moves no byte of the file, because a
tablewith apointer in it rides the id-table wire where those edits are what the wire is
FOR; and
schema lockrefuses every one with the same sentence and leaves thefile exactly as it sat.
TestClosureAppendRefusedUntilLockedruns the appends the rule allows — avariant at the end of an enum, of a
flagsmask, an arm at the end of a union— refused as STALE and then written by
schema lock, with every line beforethem untouched and the file strictly longer.
TestNestedRecordAppendSlidesTheHolderis the case that is NOT an append: tworefusals, the holder's width first and the nested record's entry beside it.
TestTypeLeavingTheClosureRefusedcovers all four spellings (type,enum,flags,union);TestHandEditedValueListRefusedcatches a value list editedaway from its hash;
TestLockHoldsTheWholeClosureandTestValueListRoundTripspin the file's shape and its parser; andTestALockFromAnOlderRenderingSaysWhatToDowalks the version bump's own path —refused by both verbs with one remedy, and locking cleanly once it is taken.
internal/lockfile/corpus_test.goregenerates every committed lock byte forbyte and compiles each of those units with the check on. All fifteen committed
locks were regenerated under the widened rendering: the corpus's fixed tables
between them reach 99 fixed tables, 16 enums, 3
flagsmasks, 6 unions and 14nested
typerecords, every one of which is now held — and eight enum-keyedarray entries across
tables/examples,tables/blockandtables/scalarsnow name their key and their element.
Gates
go build ./...,go vet ./...,go test ./...— cleanmake check— clean over the whole corpusgenerated/regenerated with this compiler — byte-identical, zero driftgolangci-lint— 0 issues;modernize— cleandeleted and rewritten from scratch
make shape-gate— cleancompiler/porting_test.go) — greenmake checkover the tree's committed locks — clean; they are current, andunder the strict reading that is now a claim the gate makes
Each new comparison was sabotaged to prove its row is load-bearing. Drop
the
*ir.Enumarm fromheld()and the enum-slot and enum-array rows failand nothing else does; drop
*ir.Flagsand the flags row fails; stop therenderer recording
key=, or stop the diff comparing it, and the keyed-arrayrow fails; take kind 16 out of the
elem=arm and the keyed-element row fails.Every one of them turned exactly the intended rows red and left the rest
green, and the sources restored byte-identical after each.
🤖 Generated with Claude Code