spec: the wstring table kind, widening, float16, Open reasons, text comments (#523) - #532
Merged
Conversation
Contributor
Author
|
Rulings from the cold read, under the owner's word of 2026-09-04:
The named page changes from the read land on this branch before it leaves draft. |
rowan-claude
pushed a commit
that referenced
this pull request
Sep 4, 2026
… constant (#530) The cold reader's two remaining edits on the second pass, plus the two statements it asked for. - The tail, in fixed order after the closure's field, variant and arm names: the reserved node-table id, the three blob type ids as bytes, string and wstring (§3.1, and #532 for the third), then the name id of EVERY table in the closure, not only pointer targets, in the projection's sorted record order. The tail is UNCONDITIONAL and the choice is stated: if only pointer targets carried a type id, adding a *T to an existing table would move every slot after it, and if the node-table and blob ids rode only for a unit with a variable class, a unit's first pointer would move the whole tail. A slot number is a compile-time constant a generated field header carries, so it must not drift under an edit that has nothing to do with it. - The resolved-form paragraph said "declaration order" where the connection's slots are the PROJECTION's order, which sorts records and vocabularies by name. Fixed. - The announcement is a compile-time constant of the unit, so a backend may emit Announce and AnnounceMeasure as a constant byte array and its length rather than as a walk, and the C++ reference states which it does. - The past-127-ids vector has a home: test/vocabgen writes tables/vocab, unit key vocabdemo, ten tables of thirteen fields, generated on test/cookgen's precedent and committed like any other corpus data, because a golden a generator has to re-derive is not a golden. The backenddemo announcement moves with the tail, from twenty-one entries and 188 bytes to twenty-nine and 252: twenty ids in its records, eight in its tail, and the reserved build-version id at slot 1. Message sizes do not move, every slot is still one byte, and the payback stays in the second round.
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
…530) * spec: the message form, an id table scoped to the connection (#523) Ruling 10 on #523. A second wire form under the form byte moves the id table off every message and onto the connection, and the file form does not change. SPEC-TABLES.md 3.3, new - Form byte 2 is the message form: the form byte and the root body, no trailer. The body is byte-identical to the file form's, so no kind is spent, no payload row moves and no skip rule changes. - References resolve against the connection's table, per direction. The resolution rule itself is unchanged. - The id table message announces it, and it is an ordinary form-1 file: one field, the build version, under the reserved id 0xFFFFFFFFFFFFFFFE, and a trailer that is the connection's table. The most recent announcement from a peer governs that peer's messages. - The build version is a label and never a gate. 20.5 stands. - A peer with no table refuses the message by name, no counter moves. The two recoveries are the sender's: re-announce, or write the file form. A receiver never guesses a table and never falls back on its own. - A reference past the table is malformed, and in the root body, which has no enclosing length, the whole message is. - Elision, defaults and the read report are unchanged. Retention loads unchanged and drops on a form-2 save, with the reason and the two answers, a file-form save and a verbatim relay. - Security: a lying build version buys nothing, a connection table is bounded by the receiver at 4096 entries, announcements are capped, a reference storm is linear and allocates nothing. - The packet wire is out of scope, and the page says why: it is same-or-refuse on the protocol id, and these peers do not ship together. - The measured effect as the page's own claim, 58 / 225 / 48 against 106 / 273 / 104, and goldens: the three messages as six named vectors in each form, with the unit, the connection and the manifest lines. SPEC-TABLES.md elsewhere - 3: the form byte bullet names both forms; the tiny-message paragraph names the message form beside the type stream. - 3, 5, 11: two ids are held back now, not one. VERSIONING.md - A section of its own between the wire form and the text form: what the form is, what the build version keys and does not gate, the refusal, what does not move, retention across the forms, promise 7's owed wording, and the two sharp edges. - Owed before 3.0.0 gains #523. Nothing is implemented: the subsection states its backend status, and the form is built in the C++ reference next. * spec: the announcement is the unit's vocabulary, and SaveRetain refuses (#530) The two owner rulings from the cold read of #530, and the named changes. Ruling 1: a peer announces its unit's WHOLE closure vocabulary, once per connection per direction, in the cook projection's order. - The table is a pure function of the build version, so "keyed by the build version" is literal. The writer's slot numbers are compile-time constants and there is no runtime slot lookup on the send path. The receiver resolves once. - The re-announcement state machine is gone: no "most recent governs", no amendment on a vocabulary change. A second announcement on a connection is refused by name, replaces nothing, and the receiver closes. A build change is a new process and a new connection. - The bound stays 4096 entries. A 500-id unit announces 4 KB once, which is the cost of ruling out re-announcement and is stated, not buried. - The heading says what the table now is. Ruling 2: SaveRetain under form 2 REFUSES BY NAME and returns -1, on 6.6's misuse precedent, never a silent drop. Named changes: - (a) The body-identity claim was false: a file's first-use slots are not the connection's slots. Replaced with identity under RESOLUTION, 6.6's own normal form, every reference replaced by the id it names and every length recomputed, plus a pinned byte length per vector. - (b) A refused announcement sets no table, and there is no earlier one to keep. The contradiction is gone. - (c) A reference past the table in the root body follows 3: the body stops, malformed counts once, and the fields decoded before it stand. No second pass, no message thrown away whole. - (d) 3.1 now states the rule for BOTH reserved ids, so 3.3 cites a rule that exists. - (e) The surface is named and owed to 11's claimed set on 6.6's precedent: TableVocabulary, Announce / AnnounceMeasure / AnnounceRead, LoadMessage / MeasureMessage / SaveMessage, four refusal reason values, and Dart's spellings. Root dispatch is the application's and message framing is the transport's, both stated. The node-table id and every pointer target's type id are entries of the announcement, stated. - (f) A connection is a transport connection, per reliable ordered channel. A restart is a new connection with empty tables, a receiver caches nothing across connections, and stateless request-response is out of scope by name. - (g) 3's form-byte pin now uses forms 0, 3 and 0xFF, since 2 is a known form with rows of its own. Promise 7 amended in this PR. One word, "key", on both pages. - (h) Four more vectors: a slot at or past 128, per-direction independence, a pointered message, and a refused second announcement. The writer's new-id question is gone by ruling 1, since an id outside the vocabulary cannot occur. The announcement for the backenddemo unit is 188 bytes, twenty-one entries (twelve distinct field names, eight variant names, the reserved id), counted against the compiler's own projection. Message sizes are unchanged at 58 / 225 / 48 / 2 / 27 / 2, and the payback moves into the second round. * spec: the vocabulary tail is unconditional, and the announcement is a constant (#530) The cold reader's two remaining edits on the second pass, plus the two statements it asked for. - The tail, in fixed order after the closure's field, variant and arm names: the reserved node-table id, the three blob type ids as bytes, string and wstring (§3.1, and #532 for the third), then the name id of EVERY table in the closure, not only pointer targets, in the projection's sorted record order. The tail is UNCONDITIONAL and the choice is stated: if only pointer targets carried a type id, adding a *T to an existing table would move every slot after it, and if the node-table and blob ids rode only for a unit with a variable class, a unit's first pointer would move the whole tail. A slot number is a compile-time constant a generated field header carries, so it must not drift under an edit that has nothing to do with it. - The resolved-form paragraph said "declaration order" where the connection's slots are the PROJECTION's order, which sorts records and vocabularies by name. Fixed. - The announcement is a compile-time constant of the unit, so a backend may emit Announce and AnnounceMeasure as a constant byte array and its length rather than as a walk, and the C++ reference states which it does. - The past-127-ids vector has a home: test/vocabgen writes tables/vocab, unit key vocabdemo, ten tables of thirteen fields, generated on test/cookgen's precedent and committed like any other corpus data, because a golden a generator has to re-derive is not a golden. The backenddemo announcement moves with the tail, from twenty-one entries and 188 bytes to twenty-nine and 252: twenty ids in its records, eight in its tail, and the reserved build-version id at slot 1. Message sizes do not move, every slot is still one byte, and the payback stays in the second round.
rowan-claude
pushed a commit
that referenced
this pull request
Sep 4, 2026
Ruling 3 on schema#523: the map's storage, walks and measure without the key and the sort, wire-neutral under the existing array kind and the wire's 64-bit counts. Plus the four owner rulings and the thirteen named changes from the cold read of #531. New §2.9. The declaration is `[]T` and `[]*T`, legal in a table body only, refused in a `type` body and therefore on the packet wire, and refused as a union arm. Storage is the map's slot exactly: an `int64` self-relative reference and an `int32` count, sixteen bytes, with the elements laid by value in the holder's node extent, lists and maps ordered as one population pre-order. The wire is kind `14` at the element's own kind with the live count, so `[]T` and `[..N]T` are the same bytes and a bound is added or removed without touching a stored file. The text form is a JSON array with no bound to clamp against. The owner rulings: - ERASE EXISTS, by the map's own mechanism, addressed by the element's POINTER: dead bit, live count, the four walks skip it. The save-edit cycle is the tool's path and a game's inventory removes in slot order, which a map by id cannot keep. Retention then restates §6.6's hazard honestly: a region's count and elements are ordinary writable memory, the hazard applies unchanged, and the builder is not on that path. - `LoadMeasure`'s `-1` CARRIES A REASON, from the enum ruling 8 gives `Open` and `BlockOpen`, one value per clause on §7's own rule, stated once in §6.5: `unknown_form`, `count_over_length`, `count_over_extent_cap`, `blob_over_size_cap`, `data_cycle`. The name is #532's to define, so this page cites it by description and section. - The index accessor is BOUNDS-CHECKED IN EVERY BUILD, on §2.4's rule: the extent came from a file, so there is no undefined-behavior path in release. - A MAP IS NOT A UNION ARM EITHER, refused by name in §2.6 and §11 on the same ground `[]T` is: both put their elements in the holder's node extent through a placement walk over what the record reaches BY VALUE, and an arm is reached by value only when its tag says so, so an arm's array would make the extent depend on a discriminant. §2.6's admission of a map at an arm predates this PR and is corrected here, because §2.9's refusal made it load-bearing. No corpus schema declares one. The named changes: the two overflow outcomes stated per path, region against builder, with the agreement claim scoped to what either path decodes; the `[][]T` fix spelled as a TABLE wrapper, since a `type` body refuses a list; the four `LoadMeasure` refusals moved to a named unit test with synthetic counts, because a refusal produces no counters and no golden can carry two gigabytes; the baseline rule stated as the capacity fact `bound=` carries when it appears or vanishes, with `elem=`, `type=` and `kind=` still judging; `array=unbounded` added to §20.2's grammar and §20.1's fact table, and the arm line's own token set widened to name what it refuses; the map's descriptor columns documented in §8.1 for the first time, with `array_bound = 0` as the tell that a field's offset names a reference rather than the first element, and the list taking them unchanged; the clamp control pinned above 2^16; `list_migrates` and `list_erased` added to the corpus; one clause in SPEC.md §1 scoping "no unbounded collections" to the type wire; the §4.2 and §4.3 cites fixed, `?[..N]T` distinguished from `[]?T` with `[]*T` and a null slot named as what serves; `string(N)` and `bytes(N)` dropped from the element-set sentence, which is now the rule that `[]T` admits exactly what `[..N]T` admits; and the additions count corrected against the bullets that follow it. Specification only, no code. Ports are a named follow-on (§15). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rowan-claude
force-pushed
the
spec-kinds-and-rules
branch
from
September 4, 2026 15:58
b0a44e6 to
1f360ea
Compare
gafferongames
added a commit
that referenced
this pull request
Sep 4, 2026
Ruling 3 on schema#523: the map's storage, walks and measure without the key and the sort, wire-neutral under the existing array kind and the wire's 64-bit counts. Plus the four owner rulings and the thirteen named changes from the cold read of #531. New §2.9. The declaration is `[]T` and `[]*T`, legal in a table body only, refused in a `type` body and therefore on the packet wire, and refused as a union arm. Storage is the map's slot exactly: an `int64` self-relative reference and an `int32` count, sixteen bytes, with the elements laid by value in the holder's node extent, lists and maps ordered as one population pre-order. The wire is kind `14` at the element's own kind with the live count, so `[]T` and `[..N]T` are the same bytes and a bound is added or removed without touching a stored file. The text form is a JSON array with no bound to clamp against. The owner rulings: - ERASE EXISTS, by the map's own mechanism, addressed by the element's POINTER: dead bit, live count, the four walks skip it. The save-edit cycle is the tool's path and a game's inventory removes in slot order, which a map by id cannot keep. Retention then restates §6.6's hazard honestly: a region's count and elements are ordinary writable memory, the hazard applies unchanged, and the builder is not on that path. - `LoadMeasure`'s `-1` CARRIES A REASON, from the enum ruling 8 gives `Open` and `BlockOpen`, one value per clause on §7's own rule, stated once in §6.5: `unknown_form`, `count_over_length`, `count_over_extent_cap`, `blob_over_size_cap`, `data_cycle`. The name is #532's to define, so this page cites it by description and section. - The index accessor is BOUNDS-CHECKED IN EVERY BUILD, on §2.4's rule: the extent came from a file, so there is no undefined-behavior path in release. - A MAP IS NOT A UNION ARM EITHER, refused by name in §2.6 and §11 on the same ground `[]T` is: both put their elements in the holder's node extent through a placement walk over what the record reaches BY VALUE, and an arm is reached by value only when its tag says so, so an arm's array would make the extent depend on a discriminant. §2.6's admission of a map at an arm predates this PR and is corrected here, because §2.9's refusal made it load-bearing. No corpus schema declares one. The named changes: the two overflow outcomes stated per path, region against builder, with the agreement claim scoped to what either path decodes; the `[][]T` fix spelled as a TABLE wrapper, since a `type` body refuses a list; the four `LoadMeasure` refusals moved to a named unit test with synthetic counts, because a refusal produces no counters and no golden can carry two gigabytes; the baseline rule stated as the capacity fact `bound=` carries when it appears or vanishes, with `elem=`, `type=` and `kind=` still judging; `array=unbounded` added to §20.2's grammar and §20.1's fact table, and the arm line's own token set widened to name what it refuses; the map's descriptor columns documented in §8.1 for the first time, with `array_bound = 0` as the tell that a field's offset names a reference rather than the first element, and the list taking them unchanged; the clamp control pinned above 2^16; `list_migrates` and `list_erased` added to the corpus; one clause in SPEC.md §1 scoping "no unbounded collections" to the type wire; the §4.2 and §4.3 cites fixed, `?[..N]T` distinguished from `[]?T` with `[]*T` and a null slot named as what serves; `string(N)` and `bytes(N)` dropped from the element-set sentence, which is now the rule that `[]T` admits exactly what `[..N]T` admits; and the additions count corrected against the bullets that follow it. Specification only, no code. Ports are a named follow-on (§15). Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…omments (#523) Six rulings from #523, specification only. 1. wstring on the table wire: kind 33, `*wstring` under the reserved blob id fnv1a64("wstring"), the cooked storage row, the text row, the LoadMeasure term, the map-key refusal by name, and the #510 closure refusal deleted. 2. Widening on read: an integer kind into a wider one of the same signedness, and f32 into f64, decodes exactly under a new `widened` counter. Every other pair stays kind_mismatch. The silent class does not move. 3. Flags storage stays uint64 in every target, with the reason. 4. float16 declined, kind 34 reserved by name. 5. Open and BlockOpen fill a TableOpenReason beside the null. 6. The text form accepts // and /* */ comments on read and never writes them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the cold read's named changes (#523)
… its stated scope (#531)
rowan-claude
force-pushed
the
spec-kinds-and-rules
branch
from
September 4, 2026 16:02
1f360ea to
79176ef
Compare
…als, one header on the refusal table
gafferongames
marked this pull request as ready for review
September 4, 2026 16:06
This was referenced Sep 4, 2026
gafferongames
added a commit
that referenced
this pull request
Sep 6, 2026
…mments (#532) (#592) * spec: widening on read, the refusal reasons, comments and ill-formed text The `widened` counter is the sixth report member and its status line goes. `TableRefuseReason` is a NATIVE enum per target, carried on each language's standard error path, and the C++ reference now carries it on Open, BlockOpen and LoadMeasure's -1, so the three "OWED, not emitted" lines go with it. The reason is written on the refusal path only, which is what makes a successful open cost nothing. Comments are accepted in the text form on read and never written. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * wire: widen on read at every kind-comparison site, and refuse ill-formed text The oracle counts `widened` where a kind grew since the writer, at a field, an arm, an array element and a map key, for the two integer ladders and f32 into f64, once per field and once per map. A kind 12 payload that is not well-formed UTF-8, or carries a zero byte, is damage: the field reads its declared default, one malformed counts, and a clamp cuts at a code point boundary. Comments are read in the text form and never written, an unclosed block comment malformed. Reserved kind 34 is refused as damage by name. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * cpp: widening, the refusal reasons and the text rules in the reference Every unit carries TableRefuseReason, and Open, BlockOpen and LoadMeasure name the first failing clause in section 7's order, bad_alignment ahead of both truncated clauses, the base's alignment last because it is the one clause that reads nothing out of the file. Every block prologue word is read bytewise so that ordering holds. The widening branch lives inside the kind-mismatch branch a reader already takes, so a payload under the declared kind never reaches it. Ill-formed kind 12 text is refused at a field, an arm, a map key and a *string record, with the clamp at a code point boundary. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * conformance: the widened, refusal-reason and comment rows The report spelling grows a sixth counter, the reports pins move with it, and the wire fuzzer gains a widening strategy over arms, elements and keys and a text strategy over every kind 12 position. Two new surfaces, cook-reason and block-reason, pin one row per reason over the forgery fixtures. The json hostile tree gains the comment cases, accepted and refused. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the fuzz leg's reason parameter, the bench tree and the reports pins Every LoadMeasure now takes the reason out-parameter, so the wire fuzzer's leg carries the unit's own TableRefuseReason as a template parameter. The length negative control changes what it quotes: the content rule reads a kind 12 payload as it arrives, over the whole of L, so a leg that lost room(len) walks off the buffer and dies on the mutant where it used to clamp a neighbour's bytes. The arm evolution row moves from one kind_mismatch to one widened. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: the block's caller clauses in C++, and the zero-cost gate's own prefix BlockOpen's truncated and unaligned_base are pinned in test/tables, beside the match that writes nothing, because each is a clause over the buffer the CALLER passed and no patched word of an image reaches it: the cross-language battery keeps its eleven rows and every port stays green on them. The zero-cost scan sanctions TableRefuseReason by its whole spelling, since TableRef is a prefix of it and the extraction takes the longest alternative. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: pin the reason on a variable root's measure and the block's own clauses A form byte this build does not carry makes SceneLoadMeasure -1 with unknown_form, and a form it does carry writes nothing at all beside the size. BlockOpen's own readings are pinned where each is actually reachable: a null base and a length under the prologue by their values, a length whose arrays no longer fit as bad_layout, and the base's alignment over a WHOLE image copied to an unaligned address, which is what it takes to reach the last clause. The remaining widened lines in USAGE, VERSIONING and COMPETITION go. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * tables: re-pin the C++ source goldens, and the Go gates that read the enum The generated C++ sources gain the refusal enum, the widening helpers and the text helpers, so testdata/golden/tables is re-pinned deliberately. NOT ONE WIRE GOLDEN MOVES: this changes what a reader does with bytes it would have skipped, never what a writer puts on the wire. The Go zero-cost test strips TableRefuseReason before its scan for the same reason the Makefile's does, and the pack corpus reads the sixth counter. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * harness: one markText for the two kind 12 frames The arm's own copy of the backward search goes, and the search itself reads as slices.Backward. Nothing about which lengths the text pass aims at moves. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * build: the block maximum control brackets the check where it now sits The declared-maximum comment moved ahead of its check when BlockOpen started naming its refusals, so the sabotage's address range ran to the end of the file. It brackets the comment and the check in the order they are written. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * build: the cook open-cost walk sabotage anchors on the refusing return TableCookOpen's last clause names its reason now, so the sabotage's anchor follows it. What the control plants and what it proves are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * text: a byte that is not a code point reads as U+FFFD The text form could put a raw ill-formed byte into storage, and the wire it then wrote was one this PR's reader calls malformed. Section 16.3's rule now runs at the point the defect enters, in the engine and in the C++ walk both: a byte in a string body that is not part of a well-formed sequence reads as one U+FFFD, exactly as a lone surrogate escape already did, so storage the text form built is storage the wire can carry. The str-illformed-at-bound row moves to one clamped, because three bytes where one stood pass the bound. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * text: the tests and goldens follow the replacement to the read The two tests that pinned a raw ill-formed byte riding into storage now pin what replaces it, and the text form is byte-stable from the first lap rather than after one. NOT ONE WIRE GOLDEN MOVES. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * wire: §4's widening row on the message form, at the field, the arm, the element and the map key §3.3 holds §4's evolution rows to the file form's word, and the widening row was the one the message reader did not carry: an announced kind below this reader's on the same ladder was met as a kind mismatch and skipped. Both engines now take it. The payload already rode at the width the announcement states, so the branch sits inside the mismatch branch the reader already took and the matching path pays nothing. The two shapes the announced width does not settle on its own are handled beside it: f32 into f64 carries a NaN's payload on the bits rather than through the hardware conversion, and a narrow integer into a 128-bit declaration is sign or zero extended before this reader's own bound meets it. A map counts ONE widened at its key however many entries carry it, and an array ONE for the field however many elements it holds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs and tests: the block padding reading, the blob size cap, the clause order and the prose pass The used extent is DERIVED from the arrays, so BlockOpen reads every triple before it compares the extent to the caller's bytes. §19.2 listed the two the other way round in both places it lists them, and the page now says the order the arithmetic forces and why. That clause's own reading joins the cross-language surface through the extent column rather than a patched word: block_extent_padding claims one byte less than block_render's used extent takes in rounding, on the precedent cook_claim_one_short set. unaligned_base stays C++-only, because it is a clause over the ADDRESS a caller passes and a manifest row hands a driver a path. The C++ BlockOpen gate gains the reading beside it. blob_over_size_cap is reachable without a four gigabyte input: <Root>NodeStorage is a pure function of a type id and a length, so the test hands it one past the cap and one at it. data_cycle is the one value no call writes today. The ruling is that it gets a carrier rather than losing its name: Measure and CookMeasure take the same out-parameter LoadMeasure takes, and threading it through the pack walk is a named follow-on. The file measure answers unknown_form for any byte that is not the file form, form 2 included, and the page now says so beside the value. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs and harness: the message form's widening row and where its instrument stops The wire fuzzer's widen pass has no purchase on form 2: a body carries no kind byte, so the kind a reader compares is the announcement's, which both engine and leg read from their own unit. A widening mutant would have to hand both sides a forged announcement and the driver's protocol carries a body. The row is held by a peer whose declaration has moved instead, and the page and the harness both say so. The block padding divergence is filed as schema#598 and the manifest comment names the five ports and cross-references #387. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * conformance: five drivers honor a claim shorter than the image Rust, C#, Java, JavaScript and Dart raised a short extent claim to the image's own length, so BlockOpen's truncated clause over the caller's bytes was unreachable in five of nine languages and block_extent_padding answered open there against the reference's refuse. Each now allocates the claim and copies only what fits, which is what the comment in each already said. C# was copying bytes.Length into a buffer sized by the claim, and Dart was doing the same through setRange, so both carried a real overrun behind the clamp. Every port refuses the row now, and the manifest comment records what happened rather than a divergence that no longer stands (schema#598). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * prose: plain sentences in the round's own comments and page text Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- 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.
Rulings 1, 5, 6, 7, 8 and 9 of #523, on the owner's word of 2026-09-04. Specification only, no code. Rebased onto main after #507, #527, #529, #530 and #531.
The blocking ruling from the cold read: ill-formed text is framing-class damage
On the table wire, ill-formed text content is damage and not data. A kind
33payload carrying an unpaired surrogate or an interior zero unit, and a kind12payload carrying malformed UTF-8 or an interior zero byte, each leave the field at its declared default, count onemalformed, and let the parent read on pastL. Kind12's silent acceptance was an inherited gap, not a precedent, and it is fixed here rather than deferred: the two text kinds now hold one rule.That inverts what the first draft of this PR said, and it makes the page simpler rather than more complicated:
Lsays where the next field begins. Neither accepts it, which is what lets nine targets owe each other one verdict on one payload.U+FFFDrule becomes the text form's rule for its own storage only — an instance a PROGRAM built, or one a lone surrogate escape introduced — and it now states the two spellings separately because the two failures differ: a code point is escaped and preserved (\^@for a zero byte or zero unit), and what is not a code point at all is replaced (U+FFFDfor an ill-formed UTF-8 sequence or an unpaired surrogate).Two further rulings landed with it:
34is a NAME reservation for a later major and nothing else. It is not in this major's set, no writer emits it, no reader has a rule for it, and a conforming later-major writer carries a new kind under the escape kind31(Reserve one escape kind on the table wire: length-prefixed opaque, skip me #434) so this major's readers skip it and countunknown. A reader of this major therefore meets34only as damage, exactly as it meets35— which is the correct answer, because a bare34is a writer that ignored the escape.Opencheck order is §7's own enumeration order, one order for both accelerators: magic, byte order, build version, reserved words zero, thealignmentword validated, the two part lengths against the caller's length, the root's storage inside the data part, the base alignment. Sobad_alignmentprecedes bothtruncatedclauses, and the page says why — thealignmentword is the one field the check computes WITH, so the part-length arithmetic below it would otherwise run over a forgery. §19.2's list is reordered to match, and a block's prologue has NO reserved words and noalignmentword (§19.1: exactlymagic,build_version,byte_order), soreserved_not_zeroandbad_alignmentnever fire for a block and the page says that rather than leaving it ambiguous.1. wstring on the table wire (folds #522)
wstring(N)gets kind 33 in §3's closed set:L, thenLbytes holdingL / 2UTF-16 code units, two bytes each little-endian. An oddLis framing damage. Its unbounded twin is*wstring, a blob node under the third reserved idfnv1a64( "wstring" ), besidebytesandstring. The layout row ischar16_t[N + 1]then anint32used length in code units (§7.2), the blob node is au64byte length, the units and a zero unit, the text row is a JSON string transcoded at the boundary (§16.2), and the LoadMeasure term is the framed length plus the eight-byte header plus the terminator the reserved type id names.wstring(N)is refused as a map key by name in §2.8, the diagnostic namingstring(N). The §11 closure refusal from #510 is deleted, and the arm row, the skip rule, the retain-unknown copy list, the fuzzer strategies andcook-check's companion list all gain the kind.The table-form goldens take the packet names with
-tableon them, and the paragraph states the whole verdict table, including which of the corpus's seventeen refusals do not carry over and why.Cold-reader questions.
lengthis BYTES and always even, not a code unit count. That keeps one arithmetic for all three blob kinds in the wire, the region andcook-check. Is the parallel worth the half step a reader has to do?*wstringearning its place, or should the ruling's reserved id have been reserved and left unbuilt until a field asks for it?memcmpover little-endian units. Does that argument hold, or is the honest reason simply that no wide order is worth standardizing?TableWStringAt's view carries a length in UNITS where the two narrow views carry bytes, andAllocWString( n )takes a unit count whereAllocBytes/AllocStringtake bytes. Is the family better with one inconsistency and no call-site doubling, or with one unit and a* 2at every wide call?2. Widening on read
A new
widenedcounter, sixth in the read report. The signed ladder is kinds2, 3, 4, 5, 18and the unsigned one6, 7, 8, 9, 19, each kind accepting every kind below it on its own ladder, plus10into11. Everything else stayskind_mismatch, the narrowing direction included. The rule is decided by the kind pair alone, so it holds at a field, an arm, an array element and a map key. §4's report line, §4.1's evolution table, §4.2's fuzzer and VERSIONING.md's read report, "Widen a field" pattern and "no widening path" sharp edge all move. The silent class stays at four.Cold-reader questions.
i128/u128as the top rung of each ladder is a reading of "an integer kind", not a ruling. Should it be there?flagsfield rides as kind9, so it accepts6through9like theuint64it is indistinguishable from. Shouldflagsbe excepted, or is the special case worse than the pair?3. Flags storage stays uint64
SPEC.md §4.2 states the reason: a mask rides as a raw
uint64on the table wire and a load copies it verbatim, so full-width storage is what lets an older build load bits it cannot name, hold them, and write them back. Derived storage would drop them with nothing able to report it.Cold-reader question. The property is the TABLE wire's. The packet wire reads W bits and never sees the appended ones. Is the reason stated at the right altitude sitting in SPEC.md?
4. float16
Declined for this major, kind
34reserved by name in §3's kind table withbits(16)plus application conversion named as the spelling. One bullet in SPEC.md §4.10, one paragraph in §3, one clause in VERSIONING.md's kind space. The fuzzer plants34and requires framing damage rather than an escape.Cold-reader question. Is reserving a number in a closed set the right shape, or does the escape kind
31already cover a later addition well enough that34is a promise the page did not need to make?5. Open and BlockOpen name their refusal
A
TableRefuseReasonout-parameter beside the null, filled on every call, with one value per clause of §7's existing check list:ok,not_a_cook,foreign_order,wrong_build_version,truncated,reserved_not_zero,bad_alignment,unaligned_base, plusbad_layoutfor a block. The check runs in that order and the first failing clause names the reason, so nine languages answer alike on one file. The parameter is optional in every target, so no call site moves.TableRefuseReasonjoins §11's claimed names. VERSIONING.md's "Open refuses in silence" edge is rewritten.Cold-reader questions.
bad_alignmentfor the header word andunaligned_basefor the caller's pointer. The ruling said "unaligned", one name. Is the split worth it? The argument is that only one of the two is the caller's own fix.not_a_cook, because a header whosebyte_orderword contradicts its own magic describes no cook in either order. Is folding it right, or does it want a ninth value?6. Comments in the text form
//and/* */, non-nesting, legal wherever whitespace is, ordinary text inside a string, an unclosed/*ismalformed. Neither writer ever emits one. §16.2's "Comments are not JSON and are refused" is replaced, the round-trip guarantee is restated to say what it covers — the form's own output, not a hand-edited text, which loses its comments and trailing commas the same way it loses its line breaks and key order — and §17.3 gains thepackthenunpacklap. Held by test in §16.5, including the writer gate thatToJsonof every corpus instance contains no/outside a string.Cold-reader questions.
unpackneed a flag that refuses to overwrite a file it did not write?/* */worth carrying at all, or is//alone the whole want?Decisions the page made that #523 or the cold read did not
*wstringblob's length is a byte length, always even, rather than a code unit count, so the three blob kinds share one arithmetic in the wire, the region andcook-check.malformedtoo, on §3.1's own terms — every slot naming that blob reads null. The ruling named the two field kinds; a blob is where the same bytes ride at a node, and leaving it out would have been the gap again one level down.12clamp cuts at a code point boundary and a kind33clamp atNunits, dropping a high surrogate whose low half did not fit — the same cuts §16.2 already states for the text form, so both forms land the same bytes.\^@in the text form, notU+FFFD. They areU+0000, a code point JSON has an escape for. Only what is not a code point is replaced.kind_mismatchit replaces. The fuzzer strategy now plants it at all four.flagsis not excepted from widening, because the rule is the kind pair and nothing else.widenedis not a loss counter — out of the never-clobber condition (stated explicitly in VERSIONING.md now), out of the silent class, and zero in the text form.uint64. And it names the cost: an older build that loaded appended bits out of a table and then puts that mask on a PACKET drops them by width, silently, with no counter on either wire able to say so.bad_layoutis the block's own value, and the two cook values a block cannot produce are named as never firing rather than left ambiguous.TableRefuseReasonis disambiguated from spec: the message form, an id table scoped to the connection (#523) #530's message-form refusal values in one paragraph: different enums, different paths, never returned by one call.TableWStringAtanswers a view whose length is a COUNT OF UNITS andAllocWString( n )takes a unit count, because every other wstring number on the page is a unit count and a byte argument here would be the one call a caller doubles at the call site. Dart spells themtableWStringAtandallocWString. All three trios are claimed in §11./*ismalformed; a//ends at end of line or end of input, so a hand-edited last line without a trailing newline is accepted.packthenunpackover a hand-authored tree is explicitly not byte-stable: comments and trailing commas go the way line breaks and key order already do.widenedexplicitly outside the condition.One defect found on main and fixed in passing
#530's change (g) did not land. Its commit message says "3's form-byte pin now uses forms 0, 3 and 0xFF, since 2 is a known form with rows of its own", but §4.2's fuzzer strategy on main still plants form byte
2and requires "a named refusal and never damage" — which is now wrong, because2IS a known form with rules of its own (§3.3). Since this PR edits that same list, the pin is corrected to3here, with the reason stated inline. Flagging it rather than burying it, in case #530's author intended a different site.Reconciliation with #530 and #531
#530, the message form.
TableRefuseReasonis disambiguated from §3.3's message-form refusal values in one paragraph: aTableRefuseReasonmeans a FILE was refused, one of §3.3's means a MESSAGE on a connection was, and the recoveries have different owners.#531, unbounded arrays.
LoadMeasurereason table says its-1"carries the SAME ENUM the accelerators' refusals carry" and referred to it only by description, because this PR merges second. It now NAMESTableRefuseReasonand cites §7 and §11.LoadMeasure's-1name".[]Tentry and this PR's wide spellings.The name is
TableRefuseReason, ruled. The enum wasTableOpenReasonin the first draft, and #531's ruling thatLoadMeasure's-1carries the same enum made a name built onOpenwrong at half its call sites — nothing is opened by a measure. It is renamed across §6.5, §7, §19.2 and §11's claim, with the Dart spelling stated (TableRefuseReasonkeeps its PascalCase there, its values take that backend's lowerCamelCase, the shapeTableCookRef.outsidealready takes), and §7 states the reason for the name rather than leaving it to be inferred. #531's §6.5 reference, which could only describe the enum because this PR merges second, now resolves to it by name.Merge check
git merge-treeagainstmain@ dd39038 (after #530 and #531) is clean, 0 conflicts. Nothing else is open that touches these files.Two edits are shaped to stay out of #531's way and were kept that way through the rebase: §11's byte-buffers bullet gains
*wstringat the END of the bullet, and §2.8's VALUE list carries the wide spellings one line above the*bytes/*stringline. No prose was weakened for it.Known stale, left for the docs pass
docs/COMPARISON-TABLES.mdrows 162 and 272 still list five report fields. Those are the D-cells #523's rulings send to the docs pass.testdata/conformance/tables/FORMAT.mdandtest/conformance/README.mdcarry the harness's counter tuple and move with the implementation PR, as does the §4 arm-evolution row's manifest pin.Cold read
Verified at
79176ef. Three named edits landed after it, plus theTableRefuseReasonrename the reader's fetch missed:wstring(N)besidestring(N)andbytes(N).reason / what the check foundheader above theclause / reasonone, left from the reorder. One table, one header, and theokrow's clause cell now reads "no clause fails".Ready for review. Do not merge.