projection: scope the wire shape by reachability, ProjectionVersion 3 (#524, #540) - #603
Merged
Conversation
…#524, #540) The projection is now the CLOSURE over the unit's `type` declarations rather than a listing of the unit (SPEC.md §3.1). Every `type` is a root, `flags` projects unconditionally, and an enum or a union no `type` reaches is out: a content enum only `table` bodies read stops buying a coordinated redeploy of both ends for a byte no packet carries. The walk carries SPEC §3.1's eight edges, and internal/check holds one negative control per edge kind, each an enum reachable only through that edge and edited by a variant REORDER, which moves no folded number in the text and so is caught only by the walk having reached the declaration. `[E.Max]T` is refused in a table body (#540, SPEC-TABLES.md §2.4, §11), which is what leaves `flags` as the only positional vocabulary a table has and therefore the only exception the scoping needs. The spelling stays legal in a `type` body, where it is a plain array on the packet wire. Every unit's id moves once and no wire byte moves with it: the 24 packet wire goldens are byte-identical, 172 of the 192 table wire goldens are byte- identical, and the 20 that moved each moved exactly 8 bytes, the build version the cook, block and announcement forms carry (§20). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… new ids `make update-goldens` does not reach two places the moved ids ride: - the CONFORMANCE cook-write corpus, 92 files, regenerated with `make conformance-generate`. Each moved exactly 8 bytes, the build version in the cook header (docs/SPEC-TABLES.md §7.1, §20), and nothing else. - the BENCH corpora for the eight non-C++ targets, regenerated from bench/corpus. 25 files, 41 lines, every one of them an id constant or a banner. The tutorial's Part 1 id is re-derived the same way Part 5's was — the committed unit reproduces the old number under the compiler on main — and the `build-version --facts` block's `protocol` line is the Part 5 id verbatim, so it moves with it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…gets The remaining generated/*/.stamp trees CI regenerates: generated/bench/tables for c, cs, dart, elixir, go, java, js and rust. Every changed line is an id or a build-version constant. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`[<Union>Type.Max]T` is edge 3 through a name no declaration spells (SPEC §4.2, §4.8). The array is positional over the arms, so slot i belongs to arm i + 1 and a reorder changes what every element is. Red with the generated-set resolution removed from the walk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An export is a semver commitment and justifies itself on schema's own needs (CLAUDE.md, docs/VERSIONING.md). Nothing outside this rendering has a use for the reached set. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Sep 6, 2026
…ue folds §3.1's control list claimed one case per edge kind and eight controls, and named "a union named only as an arm payload" as edge 6's. That unit cannot compile: a union in a `type` body takes `type` payloads only, every `type` is a root, so an arm payload is in the closure before the arm is walked. State seven isolable controls, and state edge 6's payload as a root by construction whose obligation is TestUnionOutsideTheClosureMovesNoId's arm-edit pair. Edge 5 said the walk followed "the bound or the default". It follows an extent and not a value: state the rule as an extent reaches and a value folds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The build version's digest opens with the protocol id, so scoping the projection by reachability moved every one on the page. Parts 11, 12 and 13 still printed the pre-change numbers. Re-derived by assembling the tutorial's own unit as the page builds it and running `schema build-version .` on each of the three states: Part 11 base 0x4186cdc67f83f559 -> 0x8a4897ed86a715f6 Part 11 with par_time added 0x7d56db7dd7e25376 -> 0x8272fb7f3068abdf Part 12 with Render.schema 0x1b087f2fcefc4628 -> 0x5042e348da4b84ea A compiler built at origin/main reproduces all three old numbers over the same three units, which is the control that the assembly is the page's unit and the move is this branch's. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ng refusal The ProjectionVersion ledger narrated what versions 1 and 2 could not see. State what version 3 renders. The map-descent comment said the descent reaches nothing "today"; a `type` body cannot spell a map, so no unit reaches it. The edge-control comment likewise dated itself, and its header quoted a page line that now reads seven isolable cases. Point edge 6 at the arm-edit pair that holds it. The [E.Max]T-in-a-table refusal carried an em dash and a semicolon. Plain sentences. The substring both tests match on is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rowan-claude
added a commit
that referenced
this pull request
Sep 6, 2026
The projection landed (#603), which carries #524 whole and #540 in part, so the four conflicts are all one question: what the pages say the checker does today. `schema check` now refuses `[E.Max]T` in a table body and in a union arm, which is what main deleted the "NOT REFUSED YET" lines for. It still accepts `[E.Count]T` and `[N]T` under a `const N` that folds from either, because `checkPositionalKeyedSpelling` matches the SPELLING where this branch's rule follows the bound's PROVENANCE. The merged status lines say exactly that, in SPEC-TABLES.md §2.4 and §11, in USAGE.md's keyed-array section and in VERSIONING.md's owed list, and #540 stays owed for the gap alone. The #524 entry goes, landed whole. The #606 fence stands untouched: the type-held case is stated as ruled and not refused, and no sentence in §2.4 or §6.6 commits to the closure-wide refusal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Closes #524. Closes #540.
What lands
The wire-shape projection is the CLOSURE over the unit's
typedeclarations (SPEC.md §3.1, §3.2), not a listing of the unit. Everytypeis a root, because the language has no way to say which types go on a wire.flagsprojects unconditionally and is the one exception. An enum or a union notypereaches is out of the text and out of the id, so a content enum onlytablebodies read stops buying a coordinated redeploy of both ends for a byte no packet carries.ProjectionVersionis3. Every id in existence moves once.[E.Max]Tis refused in a table body (#540, SPEC-TABLES.md §2.4, §11), which is what §3.2's no-fifth-member claim rests on: with the positional spelling gone, an enum a table reaches rides by variant name at every site, andflagsis the only positional vocabulary a table has left. The spelling stays legal in atypebody, where it is a plain array on the packet wire and every fact of it projects.The eight edges, and one negative control per edge kind
ir.projectionClosurewalks SPEC §3.1's eight edges and nothing else: a field's named type, an array's element type, an array's bound, a keyed array's key enum, a constant's value expression, a union arm's payload and field facts, both sides of a branch, and every item of a reached type transitively.internal/check/projection_test.go'sTestReachabilityEdgeControlsholds one case per edge kind, each an enum reachable only through that edge, edited by a variant REORDER. A reorder moves no folded number anywhere in the projection text, so the id moves only if the walk reached the declaration and put its ordered variant names in. Each case is red when its edge is deleted from the walk:EDGE 6 CANNOT BE ISOLATED, and the page now states that rather than claiming eight reds. A union in a
typebody takestypepayloads only (SPEC-TABLES.md §2.6, and the checker refuses every other arm there by name), and everytypeis a root, so a projected union's arm payload is a root by construction and is in the closure before the arm is walked. Deleting the arm descent from the walk leaves case 6 green. The descent is implemented because the rule is the rule, and the case is held as the path the page names, red through edge 1 reaching the union and edge 8 descending into the payload type.What holds edge 6 is the arm-edit pair in
TestUnionOutsideTheClosureMovesNoId: every arm edit a union inside the closure can express moves the id, and every arm edit a union notypereaches can express moves none. SPEC.md §3.1 was rewritten to say seven isolable controls and to name that pair as edge 6's obligation. Its old list named "a union named only as an arm payload" as the eighth control, which is a unit that cannot be written.#540's control, with the refusal disabled:
Every id moved once, and no wire byte moved with it
WHAT ACTUALLY LEFT THE TEXT, unit by unit, so the change is auditable rather than asserted. 34 declarations across the corpus, every one an enum or a union no
typereaches:examples/is the clearest reading of the rule: exactlyMissileTypeandPropTypeleft it, both declared inEnums.schemaand named by notypein the unit. The units at 0 moved their ids too, on the version line alone, which is what "every id in existence moves once" means.17 units, each id moving exactly once: armdemo, bench, benchtable, blobdemo, blockdemo, blockhome, example, graphdemo, listdemo, ludicrous, mapdemo, messagedemo, realworld, scalardemo, streamdemo, tabledemo, wide. The goldens were regenerated with
make update-goldens, never hand-edited.The wire-bytes control:
testdata/wire/*.bintestdata/wire/tables/*.bintestdata/conformance/tables/cook-write/*.cookEvery one of the 112 changed binaries moved within the one 8-byte BUILD-VERSION field and nowhere else: the field the cook header, the block form and the message announcement each carry (§20), which derives from this projection and therefore moves with it. 103 of the 112 differ in all 8 of those bytes. Nine differ in 7, all of them the
graphunit, whose new build version happens to share one byte with its old one. In every case the differing offsets are bytes 9 through 16, which is that field. Measured over all 112, not asserted:So: a unit's wire golden is byte-identical before and after while its id line moves. That is the whole claim, and the 24 packet goldens are the clean half of it.
Tests inverted by the rule, deliberately
TestTableArmsAndTablesLeaveTheIdAlone(wasTestTableArmsProjectAndTablesDoNot): a union only a table closure reaches now moves no protocol id at all, which is §3.2's stated consequence. A union with a TABLE ARM is excluded whole and needs no rule of its own, because it is a table-closure construct, atypebody refuses one by name, and the closure never reaches it.TestTableArmedUnionArmRenameMovesTheBuildVersionAlone(was...MovesBothIds): the arm rename moves the BUILD VERSION and not the protocol id. That is the division scoping makes explicit: the connect gate holds the packet wire, and §20.2's cook projection holds the table-only vocabulary.TestUnionOutsideTheClosureMovesNoIdholds both directions in one place: the table-held union's arm edits move nothing, the type-held union's arm edits move the id.TestProtocolFreeUnitIdPinned's probe gained aHoldertype so that every declaration in it is reached. The pin is a RENDERING pin, and a probe whose union and enum dropped out of the text would have stopped being one.Docs
Four #541 status lines deleted, each made true here: SPEC.md §3.1's "Compiler status: NOT SCOPED YET", SPEC-TABLES.md §2.4's and §11's "CHECKER STATUS: NOT REFUSED YET", USAGE.md's not-scoped-yet aside and its #540 aside. VERSIONING.md's owed-before-3.0.0 rows for #524 and #540 are gone and its evolution table no longer says the checker does not refuse.
SPEC.md §3.1's control list is corrected: seven isolable controls rather than eight, edge 6 named as a root by construction with the arm-edit pair as its obligation, and edge 5's rule stated as an extent reaches and a value folds. The matching comment in
internal/check/projection_test.gofollows it.TUTORIAL.md's three printed build versions are re-derived (silence 6 below). Its
schema projectionsample is regenerated: version3, andPendingandWeaponare gone from it because nothing in Starlight's types names them. The tutorial now teaches why, which is the clearest place in the tree to teach it. Both printed ids move with it. The reconstruction was verified against the compiler onmainbefore it was used. The tutorial's committed unit reproduces0xb786cca203ebb6eaand0x799890f44a60c51fexactly under ProjectionVersion 2, so the new numbers are the same unit under the new rendering and not a guess.docs/USAGE.md's printed build version fortables/blockmoved with the id.Owner law
This PR adds no diagnostic and touches no read or write path: the change is a compile-time projection, and the generated codecs are byte-identical apart from the id constant in each banner. It moves neither the C nor the C++ packet emitter, so the standing reproduction-row gate is not reached (
git diff main...HEAD -- internal/codegen/c internal/codegen/cppis empty).Silences the page leaves, decided nowhere
[E.Count]Tin a table body is not refused. The page names[E.Max]Tand the refusal is written to that spelling exactly.[N]Twhereconst N = E.Max, in a table body is not refused: the const hides the enum and the page's refusal is spelled atE.Max.[E.Max]Tinside atypea TABLE reaches stays legal, and this one is worth an owner ruling rather than a shrug. §2.4 says the refusal "is the TABLE body's alone", so the checker refuses it in a table body and nowhere else. Atypea table holds rides the table wire as a kind-13 body, and its[E.Max]Trides inside that body under kind14, positional. Verified on a probe:table Root { inner Inner }withtype Inner { slots [E.Max]uint8 }emitsw.Put8(14)forslots. So a variant inserted in the MIDDLE ofElands every later element one slot off, in every stored file, with nothing on the wire that can say so, which is exactly the shape §2.4 refuses in a table body and exactly the class §4.1 counts as closed. The connect gate does move (the enum is type-reached, so the protocol id moves) and the tables baseline does flag it, but a save file carries neither. Either the refusal extends to everytypea table closure reaches, or §2.4, §3.2 and §4.1 gain a fifth member. This PR implements what the page states and names the gap.| max = E.Maxcontributes only a folded number, which is already in the text and already moves when a variant is added, and no ordinal of it means anything on the wire, so reaching it would only cost churn. Edge 5 used to read "the bound or the default that does", which reads either way. It now states the rule as an extent reaches and a value folds.The page prints three of the four as cook output rather than as a bare number, so those lines were produced by the page's own cook commands and reproduce whole, byte counts included:
The control that the reassembly is the page's unit: a compiler built at
origin/mainprints0x4186cdc67f83f559,0x7d56db7dd7e25376and0x1b087f2fcefc4628over those same three directories, and0xb786cca203ebb6eafor the pre-change protocol id. So the numbers moved because this branch moved them, not because the assembly drifted.schema id .over the base state prints0x60d7cbad6bb296f2, the page's committed value, andschema build-version --facts .reproduces the page's facts block line for line.///line saying no comment kind is read yet that spec: doc and tags columns, field-level tags (#523) #529/Design lock-in before the sweep: the cold read's shortest list #523 landed in 02b3fc1. Both are in flight in that PR, so this one leaves them alone rather than racing it.