Skip to content

tables: JSON map key identity for padded and decimal spellings, and the prefix control's blade (#614) - #625

Merged
rowan-claude merged 7 commits into
mainfrom
fix-614-json-map-keys
Sep 7, 2026
Merged

tables: JSON map key identity for padded and decimal spellings, and the prefix control's blade (#614)#625
rowan-claude merged 7 commits into
mainfrom
fix-614-json-map-keys

Conversation

@rowan-claude

@rowan-claude rowan-claude commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Closes #614.

Stella's three probes from the #607 re-run, red first in both engines, then one
rule each at the point the defect entered; and the prefix control repaired so
the branches it walks out through establish the defect it names.

What lands

The key is the spelling and nothing around it. Both key readers walked the
token with the walk that skips whitespace and comments. That is right BETWEEN
tokens and wrong INSIDE one: the walk consumed the padding, the "did the number
end at the token's end" check then passed, and the digit path was handed the
padded bytes. The walk now begins at the token's first byte, so a padded
spelling is not a JSON number at all and is malformed on the terms §16.2 gives
1-2, with the read stopping where §16.1 stops it.

Nothing on a KEY's path is a double. The one checked interpretation (#607's
C7) read a token carrying a fraction or an exponent through strtod /
ParseFloat, so a 53-bit mantissa decided the identity of a 64-bit key. A key's
digits are now read where they stand with the decimal exponent moving the point
-- the same normalization the wide kinds already use, ParseWide and
TableJsonReadWide, over one 64-bit lane. A zero fraction is the integer it
spells at every magnitude the kind holds.

The exact reader is the KEY path's and no other's. It lives in
TableJsonInterpretExact / interpretIntegerExact, beside the interpretation
the FIELD path keeps: TableJsonInterpret / interpretInteger, unchanged, with
the double and with finite. That split is the whole of the decision in
silence 2 below. A key is an IDENTITY, so two spellings a mantissa cannot tell
apart are two keys and the reference may rule alone; a field's value is a
quantity under a clamp, read the same way by the C, Go and Rust ports over the
same texts, and moving it moves four readers at once.

Engines: internal/codegen/cpptable/json.go (TableJsonMapKeyValue,
TableJsonInterpretExact) and internal/tabletext/read.go (placeMapKey,
interpretIntegerExact). No other backend carries a map key in JSON.

Red first

The gates are the two that already hold §16.2's map key rule: the C++
reference's map gate and the tool's own. Commit a9137b9 is the probes alone.

FAIL test/tables/maps_main.cpp:1088: !EdgeRowFromJson( eb, t, (int64_t) strlen( t ), &r )
FAIL test/tables/maps_main.cpp:1089: r.malformed
FAIL test/tables/maps_main.cpp:1108: eb.GetRoot()->ids.count = 1, want 2
FAIL test/tables/maps_main.cpp:1109: r.duplicate = 1, want 0
FAIL test/tables/maps_main.cpp:1113: EdgeRowIdsFind( eb.arena, eb.GetRoot()->ids, 9007199254740993ull ) != NULL
FAIL test/tables/maps_main.cpp:1128: r.kind_mismatch = 1, want 0
FAIL test/tables/maps_main.cpp:1130: r.duplicate = 0, want 1
FAIL test/tables/maps_main.cpp:1133: item->count = 1, want 2

8 map check(s) failed
maps_test.go:193: a padded key is not a JSON number: the read must stop,
    got {Unknown:0 KindMismatch:0 Clamped:0 Duplicate:1 Malformed:false}
maps_test.go:226: two keys 2^53 apart are two keys: {Duplicate:1}
maps_test.go:234: expected 2 entries, got 1
maps_test.go:257: the decimal spelling of UINT64_MAX is a key the kind holds:
    {KindMismatch:1 Duplicate:0}

The padded key parted the two engines, which is the sharpest reading of the
defect: the tool's byte arithmetic made " 2" the key 2402, colliding with
the literal 2402 beside it, while the reference's saturated and dropped the
entry as kind_mismatch. Neither is the page's answer, and one rule cannot
produce two.

Green after, both engines, on the same rows.

The FIELD path does not move, and here is the measurement

The exact reader reached every integer field in an earlier revision of this
branch, which parted the reference from the ports on a general path. It is now
the key path's alone, and the field path is byte-for-byte what origin/main
produces. Measured on tabledemo RootConfig through the C++ reference and
through the tool, main compiled into a scratch tree for the baseline:

token, in this field main (reference and tool) this branch (reference and tool)
1e309 in badge uint8 0, kind_mismatch=1 0, kind_mismatch=1
-1e309 in timestamp int64 0, kind_mismatch=1 0, kind_mismatch=1
9007199254740993.0 in epoch uint64 9007199254740992 9007199254740992
18446744073709551615 in epoch uint64 18446744073709551615, clamped=0 18446744073709551615, clamped=0
18446744073709551615.0 in epoch uint64 18446744073709551615, clamped=1 18446744073709551615, clamped=1
2.5 in badge uint8 0, kind_mismatch=1 0, kind_mismatch=1
1e400 in precision float64 0, kind_mismatch=1 0, kind_mismatch=1

Every cell equals main's, in both engines, and the four rows the earlier
revision moved (1e309 to 255 with clamped=2, -1e309 to INT64_MIN with
clamped=2, the 2^53 spelling to ...993, the decimal UINT64_MAX to clamped=0)
are back where the ports have them. TestRejectedRepeatKeepsTheFirstValue takes
its original 1e309 token back with them, and
TestAnIntegerFieldReadsItsTokenThroughTheFloat beside it pins four of the
table's rows against the ports, so a future re-unification has to go red first.

schema pack says the same thing at the CLI. A one-profile tree whose
0-ace.json is { "badge": 1e309, "epoch": 9007199254740993.0 }, packed by this
branch and by main's compiler:

report: unknown 0, kind_mismatch 1, widened 0, clamped 0, duplicate 0, malformed false
schema: the report is not silent -- pass --tolerate to accept it

Identical from both, and with --tolerate the 63 wire bytes compare equal.

The KEY rows are unmoved by the restoration: the map gate is green in both
engines, a padded key is still malformed, two keys 2^53 apart are still two
keys, and the decimal spelling of UINT64_MAX is still a key.

The controls

tables-json-clamp-prefix-negative-control had two of four branches spelling
return 0 -- success, "red" -- over a run that observed no prefix at all. Both
now fail. Quoted against a walker sabotaged to refuse the read, and against one
whose clamp counter is wrong as well as its bytes:

---- readfail / old control ----
clamp prefix negative control: the sabotaged walker would not read at all: red
exit=0
---- readfail / new control ----
FAIL clamp prefix negative control: the walker would not read this text at
      all, so the clamp was never reached and the stored bytes say nothing.
      A failed read does not establish the defect this control names.
exit=1
---- countfail / old control ----
clamp prefix negative control: the clamp went uncounted as well: red, but for the wrong reason
exit=0
---- countfail / new control ----
FAIL clamp prefix negative control: the read counted 3 clamps where the
      text spells exactly one, so this run is not the one-clamp read the
      control reads bytes out of.
exit=1

The target now runs the control BOTH ways, because a binary that only ever meets
the sabotage cannot show it has a blade. Under the sabotage it must see the
wrong bytes; against the honest walker it must find nothing to see:

./build/schema_test_json_clamp_prefix_negative
clamp prefix negative control: the scan resumed past the code point that did not fit,
      storing 16 bytes the text never spelled in that order: red, as required

build/schema_test_json_clamp_prefix_honest
FAIL clamp prefix negative control: a walker whose scan resumes after an
      over-long code point still stored the prefix.
exit=1

negative control: the clamp prefix control sees the defect under the sabotage and nothing without it

The fifteen tables-maps-*-negative-control sabotages are unchanged and stay
green, keydomain and keyidentity included -- the two nearest this diff.

The page, and what it left to be decided

§16.2 settles the map key rule in one clause: an integer key "is its decimal
spelling, quoted, read by the integer rule above and by nothing else, so a token
that rule calls malformed makes the key malformed and one with a fractional
value or out of the key kind's range drops its entry as kind_mismatch". Three
things the page does not spell in those words, decided here on reduce-cases and
listed for the owner:

  1. Whitespace inside a key. The page never says the word. The integer rule
    says "the token grammar is RFC 8259's", and RFC 8259's number production has
    no whitespace in it, so a padded spelling is "a token that is not a JSON
    number at all" and the page's own answer for that is malformed. Decided:
    malformed, the read stops. The alternative -- trim and accept -- gives one
    entry two names, which is the identity corruption the clause exists to
    prevent.

  2. Whether a FIELD reads its token the way a KEY now does. Not decided here,
    and unchanged by this PR.
    The exact reader is the key path's alone. On the
    field path the reference and the tool still read a fractional or exponent
    spelling through the double, exactly as ctable/json.go:1349-1382,
    gotable/json.go:1392-1430 and rusttable/runtime.go:2150-2190 do, so
    9007199254740993.0 in a uint64 lands ...992 in all four and 1e309 in a
    uint8 is kind_mismatch in all four. That is the reference agreeing with
    its ports, not a defect this PR leaves standing: the reading is one the
    OWNER settles for all nine at once, and it is put to him in Owner ruling wanted: exact integer interpretation for integer FIELDS in the text form, across the nine (1e309, decimal spellings at 2^53, decimal UINT64_MAX) #629 with the
    measured table. Under either answer there the nine move together, and this
    PR moves none of them.

  3. Where these rows live. The task suggested json-hostile rows. The
    mapdemo unit is not a conformance unit -- no unit in MANIFEST.txt declares
    a map -- so the probes went to the two JSON walk gates that already hold
    §16.2's key rule (test/tables/maps_main.cpp,
    test/conformance/harness/maps_test.go), which is where tables: the C++ JSON walker's map keys, placement failures and one numeric interpretation (#566) #607 put the rows
    these extend. Admitting mapdemo to the conformance corpus is a larger,
    separate move and is the owner's to want.

#609's retained decision is untouched and now PINNED in both engines rather than
held by code alone: an integer key past the scan's 255-byte buffer drops as
kind_mismatch, and the row is built so length alone cannot carry it. The key is
256 bytes and spells the integer 1, a value the kind holds, and the prefix that
fits the buffer spells 1 as well -- so a read that truncated would merge it
into the entry beside it and call two keys one. At 255 bytes the same text reads
the key and counts a duplicate, which is what makes the row a reading of the
bound and not of the length. EdgeRow.names' identity row is green beside it.

The goldens

33 files, one class of change: 32 *Table.cpp under testdata/golden and
generated/bench/tables/cpp/BenchTableTable.cpp, which is the same walk in the
tables bench corpus. Every one is a translation unit carrying the JSON walk
(§13.5 puts the walk in the .cpp, which is why no header moved), and the diff in
each is these lines and nothing else. It has exactly two shapes:

25 files   +80 -3    the exact reader added beside the field's, and the header
                     comment that says which path each is for
 8 files   +91 -5    the same, plus the map key call site: the walk that starts
                     at the token's first byte, and TableJsonInterpretExact

The eight are the map-bearing units (tables/maps, tables/lists). No wire
golden, no id, no dump, no header moved. bench/LOCK records the bench corpus as
under no locked prefix; the prefix list is empty (lifted 2026-09-05) and the
standing gate names the C/C++ PACKET emitters, which this diff does not touch.

origin/main, merged

origin/main moved twice under this branch and both are merged in, never
rebased: a0f32ed (#624, #615) and 5e04d3f (#627, docs). make update-goldens
after each merge moved nothing, so the merge result and the regenerated tree
agree byte for byte, and no golden was touched by hand.

make test, on the bench

make test whole, on this Mac Studio, on the merged head. It stops at
build-conformance-cs with /bin/sh: dotnet: command not found -- the absent
toolchain, not a result. go test ./... is clean.

Toolchain absences on this bench (#599): no node, dart, java, elixir or dotnet,
so the JS, Dart, Java and Elixir legs skip silently and the C# legs stop. CI has
all of them, and its twenty checks are green on 6f6be17.

Twenty FAILED lines stand in the log before the stop and every one is a
negative control reporting its own red, each sitting beside its
negative control: ... turns ... RED line. No non-toolchain failure anywhere
in the run.
The gates this PR touches, green in it:

maps: all checks passed (docs/SPEC-TABLES.md §2.8)          (plain and asan)
tables map-walk gate: one map half, byte-identical in 3 map-bearing .cpp files
negative control: keyidentity turns the MAP GATE red — 2 failures
negative control: keydomain turns the MAP GATE red — 4 failures

The four targets named on this work were then run on their own, past the stop,
and are green: tables-maps, tables-json-map-walk,
tables-json-clamp-prefix-negative-control, and
tables-maps-negative-controls, whose fifteen sabotages each turn the map gate
red as required, keyidentity and keydomain among them.

negative control: the clamp prefix control sees the defect under the sabotage
                  and nothing without it

rowan-claude and others added 3 commits September 6, 2026 19:37
The three identity corruptions #614 files, as rows on the two gates that
already hold §16.2's map key rule: the C++ reference's map gate and the
tool's own. All three are red before the repair.

  FAIL test/tables/maps_main.cpp:1088: !EdgeRowFromJson( eb, t, ... )
  FAIL test/tables/maps_main.cpp:1108: eb.GetRoot()->ids.count = 1, want 2
  FAIL test/tables/maps_main.cpp:1128: r.kind_mismatch = 1, want 0

  maps_test.go:193: a padded key is not a JSON number: the read must stop,
      got {KindMismatch:0 Clamped:0 Duplicate:1 Malformed:false}
  maps_test.go:234: expected 2 entries, got 1
  maps_test.go:257: the decimal spelling of UINT64_MAX is a key the kind
      holds: {KindMismatch:1 Duplicate:0}

The padded key parts the two engines, which is the sharpest reading of the
defect: the tool's digit path takes the space as a byte and lands the key
2402, colliding with the literal 2402 beside it, while the reference's
saturates and drops the entry as kind_mismatch. Neither is the page's
answer, and one rule cannot produce two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ing alone (#614)

Both engines, one rule each, at the point the defect entered.

THE KEY IS THE SPELLING AND NOTHING AROUND IT. Both key readers walked the
token with the walk that skips whitespace and comments, which is right
BETWEEN tokens and wrong INSIDE one: the walk consumed the padding, the
length check then passed, and the digit path was handed the padded bytes.
The two engines did not even agree on the wreckage -- the tool's byte
arithmetic made " 2" the key 2402, colliding with the literal 2402, and the
reference's saturated and dropped the entry. The walk now begins at the
token's first byte, so a padded spelling is not a JSON number at all and is
malformed on the terms "1-2" is.

NOTHING ON THE PATH IS A DOUBLE. The one interpretation read a token with a
fraction or an exponent through strtod/ParseFloat, so a 53-bit mantissa
decided the identity of a 64-bit key: 9007199254740993.0 rounded onto its
neighbour and merged with it, and 18446744073709551615.0 rounded UP to 2^64
and was dropped as outside a domain it sits exactly inside. The token's
digits are now read where they stand with the decimal exponent moving the
point -- the normalization the wide kinds already use (ParseWide,
TableJsonReadWide), over one 64-bit lane -- so a zero fraction is the
integer it spells at every magnitude the kind holds.

`finite` goes with the double that produced it. No exact reader has an
infinity, so the flag could only ever be true; a field's magnitude past
sixty-four bits saturates and CLAMPS, which is the page's rule for it
(§16.2 Bounds, and the wide kinds' "saturates ... as an int64 field
saturates at INT64_MAX"). That moves one hand-authored row: 1e309 in a
uint8 was kind_mismatch through the double's infinity and is now a clamp to
255. The row it was standing in wanted a value the walk REFUSES, so it takes
a fraction instead, and the corrected rule gets a row of its own beside the
float field's, where an infinity IS a kind_mismatch and stays one.

The goldens: 37 emitted .cpp files, every one a translation unit carrying
the JSON walk, and the diff in each is these lines. No header, no wire, no
id, no dump.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…clamped count (#614)

Two of the control's four branches spelled `return 0` -- success, "red" --
over a run that observed no prefix at all. A sabotage that broke the read
outright, or moved the ledger as well as the bytes, walked out through one
of them and the control reported the defect it had not reached. Both now
fail, and each says which of the three outcomes it saw.

Quoted against a walker sabotaged to refuse the read, and against one whose
clamp counter is wrong as well as its bytes:

  ---- readfail / old control ----
  clamp prefix negative control: the sabotaged walker would not read at all: red
  exit=0
  ---- readfail / new control ----
  FAIL clamp prefix negative control: the walker would not read this text at
        all, so the clamp was never reached and the stored bytes say nothing.
  exit=1
  ---- countfail / old control ----
  clamp prefix negative control: the clamp went uncounted as well: red, but for the wrong reason
  exit=0
  ---- countfail / new control ----
  FAIL clamp prefix negative control: the read counted 3 clamps where the
        text spells exactly one ...
  exit=1

And the target now runs the control BOTH ways. A binary that only ever meets
the sabotage cannot show it has a blade, so the honest walker is the second
arm and the control must find nothing to see there:

  ./build/schema_test_json_clamp_prefix_negative
  clamp prefix negative control: the scan resumed past the code point that did not fit,
        storing 16 bytes the text never spelled in that order: red, as required
  build/schema_test_json_clamp_prefix_honest
  FAIL clamp prefix negative control: a walker whose scan resumes after an
        over-long code point still stored the prefix.
  exit=1
  negative control: the clamp prefix control sees the defect under the sabotage and nothing without it

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rowan-claude and others added 4 commits September 6, 2026 20:28
The exact reader landed on EVERY integer field, and a reference must not part
from its ports on a general path without a ruling. Measured on tabledemo
RootConfig: 1e309 in a uint8 was 255 with clamped=2 in the reference and the
tool where the C, Go and Rust readers answer 0 with kind_mismatch=1, and
9007199254740993.0 in a uint64 was ...993 against the ports' ...992. That is a
change to four readers, not to one walk, and it is not what #614 asks for.

So the two interpretations are two functions. TableJsonInterpretExact and
interpretIntegerExact read the token's digits where they stand and carry the
map key path, which is where a spelling is an IDENTITY and two tokens a
53-bit mantissa cannot tell apart are two keys. TableJsonInterpret and
interpretInteger go back to the double, which is the interpretation the ports
share, and the field path reads through them again.

The five-token table on tabledemo RootConfig is byte-for-byte main's again, in
the reference and in the tool: 1e309 in a uint8 and -1e309 in an int64 are
kind_mismatch with nothing stored, 9007199254740993.0 in a uint64 is
9007199254740992, the decimal spelling of UINT64_MAX is itself, 2.5 in a uint8
is kind_mismatch, and 1e400 in a float64 is kind_mismatch. The key rows are
unmoved: the map gate is green in both engines, and a padded key is still
malformed, two keys 2^53 apart are still two keys, and the decimal spelling of
UINT64_MAX is still a key.

TestRejectedRepeatKeepsTheFirstValue takes its original token back, since
1e309 in a uint8 refuses the repeat again, and the row beside it now pins the
field path against the ports rather than against the exact reader.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…assert (#614)

#609's retained decision gets a row in both engines: an integer key past the
scan's 255-byte buffer drops as kind_mismatch, and the row is built so length
alone cannot carry it. The key is 256 bytes and spells the integer 1, which the
kind holds, and the prefix that fits the buffer spells 1 as well, so a read that
truncated would merge it into the entry beside it and call two keys one. At 255
bytes the same text reads the key and counts a duplicate, which is what makes
the row a reading of the bound rather than of the length.

The clamp prefix control's header and its Makefile note now state what the
control asserts rather than what it used to spell, in the present tense the
house wants.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rowan-claude
rowan-claude merged commit 19a2372 into main Sep 7, 2026
20 checks passed
@rowan-claude
rowan-claude deleted the fix-614-json-map-keys branch September 7, 2026 03:58
rowan-claude added a commit that referenced this pull request Sep 7, 2026
NOT THIS ISSUE'S CHANGE, and it is here because the merge carries it.

main is red at 19a2372 on its own CI run 34081461383, and it was green
at bb5f3bd. #625 moved `internal/codegen/cpptable/json.go` and re-pinned
the map goldens it knew about, DepthTable.cpp, FleetTable.cpp and
RowsTable.cpp, each by the same 91 lines. Text.schema and its golden
landed in #626, which merged after #625's branch was cut, so
maps/TextTable.cpp never got the re-pin and `make tables-block-zero-cost`
reports it moved.

The gap is exactly the 91 lines the three siblings took, the
TableJsonInterpretExact reader and the decimal band beside it, which
every table holding a map now emits. The file here is the generator's
own output, copied by the path `make update-goldens` copies it, with no
hand edit: copying the whole maps directory moves this one file and
nothing else, which is what says the other three were already current.

    block zero-cost gate: 105 Table sources byte-identical to their pins

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rowan-claude added a commit that referenced this pull request Sep 7, 2026
…ero-cost gate on main) (#664)

#625 moved the emitted JSON walker in every map-bearing unit and regenerated the goldens on its branch before #626's TextTable golden existed on it; the merge of main carried the golden in unchanged, so main's committed TextTable.cpp lags the compiler by the key-reader change and the block zero-cost gate refuses main. Regenerated with main's compiler; no header, wire pin or id moves.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
rowan-claude added a commit that referenced this pull request Sep 7, 2026
…ure (#605) (#657)

* red first: the enum-bound corpus, ten shapes and three controls (#605)

SPEC-TABLES.md §2.4 and §11 state the refusal on the bound's PROVENANCE:
a positional array whose bound folds from an enum is refused in a TABLE
BODY and in a UNION ARM, in every spelling the bound has. The checker
follows the SPELLING, so eight of the ten shapes compile clean today.

test/tables/enumbound holds one unit a shape. Against this commit:

    ArmConstCount.schema    COMPILES (exit 0)
    ArmConstMax.schema      COMPILES (exit 0)
    ArmCount.schema         COMPILES (exit 0)
    ArmFolded.schema        COMPILES (exit 0)
    ArmMax.schema           REFUSED, naming neither the arm nor the table
    BodyConstCount.schema   COMPILES (exit 0)
    BodyConstMax.schema     COMPILES (exit 0)
    BodyCount.schema        COMPILES (exit 0)
    BodyFolded.schema       COMPILES (exit 0)
    BodyMax.schema          REFUSED, naming neither the table
    ControlPacket.schema    COMPILES (exit 0)
    ControlPlain.schema     COMPILES (exit 0)
    ControlTypeHeld.schema  COMPILES (exit 0)

TestEnumBoundProvenanceCorpus reads the directory and holds each file to
its answer, so a shape is red the moment it compiles or the diagnostic
stops naming the field, the enum, the constant the field spells and
`[E]T` as the fix. All ten refused rows are red at this commit and the
three controls are green.

The controls hold the other edge: the packet wire, a bound that folds
from no enum, and the `type`-held case schema#606 rules on, which this
issue does not decide and this corpus does not move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* check: the enum-bound refusal follows the bound's provenance (#605)

The rule docs/SPEC-TABLES.md §2.4 and §11 state reads the bound's
PROVENANCE and the checker read its SPELLING, so `[E.Count]T` and `[N]T`
under a `const N` that folds from either compiled in a table body and in
a union arm and carried the positional class §4.1 counts as closed.

`enumBoundProvenance` follows a bound to the enum it folds from, through
named constants and constant arithmetic at any depth (SPEC.md §4.2),
carrying its own visiting guard over the constant graph. It looks at an
ENUM alone: a `flags` bound is refused by its own rule, and a bound that
reaches neither enum nor flags is a plain positional array.

`checkPositionalEnumBoundInClosure` runs once the closure is known, which
is what lets a union arm's diagnostic name the table that reaches the
union, as #572's closure refusal names the edge that pulled a `type` in.
THE WALK IS THE FENCE: it starts at TABLE bodies and descends UNIONS
alone, so the `type` a table closure reaches is never visited and
schema#606 keeps its ruling, and a `type` no table reaches is not in the
closure at all, so the packet wire is untouched.

The resolve-time spelling check is gone; the bound still evaluates
there, because a bound that cannot be evaluated is a different
diagnostic and belongs at the field.

`exprSpelling` now renders constant arithmetic, so a bound like
`[Grade.Max + 1]` is quoted back as the source spells it instead of as
the placeholder `N`.

test/tables/V1.schema and V2.schema: the refusal caught two live fields.
`tally` in both generations and `ledger` in V1 were sized `[Grade.Max +
1]`, a positional array in a table body whose bound folds from an enum,
which is the shape the page says is refused and the fixture rested on
the gap. Both keep their extents, 3 in V1 and 4 in V2, spelled as the
plain constants TallySlots and LedgerSlots, so every generation the
evolution test exercises is unmoved: the growing bound, the clamped
count, and V1-positional against V2-keyed for `ledger`. `schema fmt`
re-canonicalized the attribute column in both files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* check: the enum-bound gate and its negative control (#605)

`check-enum-bound-negative-control` runs the compiler over
test/tables/enumbound and reads three answers back.

THE GATE: each of the ten refused shapes must be refused, and each
diagnostic must name the field or the arm, the enum, the constant where
the bound reaches the enum through one, and `[E]T` as the fix. The arm
rows must name the table that reaches the union. The THREE POSITIVE
CONTROLS must compile: the packet wire, whose `[E.Max]T` is a plain
array the connect gate covers; a bound that folds from no enum; and the
`type`-held case, which is schema#606's ruling and not this refusal's.

THE NEGATIVE CONTROL is the one §2.4 names: it removes the CONSTANT FOLD
from the bound check through `go build -overlay`, writing no tracked
file, and every row whose bound reaches its enum through a constant must
then compile clean. It is targeted rather than blanket, so the four
direct spellings must stay refused under the same sabotage. A control
that turned the whole rule off would go red for a reason that says
nothing about the fold.

Both halves are green:

    gate: ten shapes refused on the bound's provenance, three controls compile
    negative control: without the constant fold, six folded rows compile clean
    and the four direct spellings stay refused

Wired into `make test` beside check-zero-range-negative-control. It
compiles no C++ and needs no toolchain beyond Go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: the enum-bound refusal's status lines, on the tree (#605)

SPEC-TABLES.md §2.4's CHECKER STATUS paragraph stated a split between
the rule and the checker, and the paragraph carried its own instruction
to be deleted by the PR that closes it. It now states one thing: the
refusal reads the bound's provenance and not its text, in a table body
and in a union arm, and it names what the diagnostic names. §11's
checker-status line says the same in its own register.

Both RULING STATUS lines for schema#606 stay exactly as they were: the
type-held case is the owner's and this refusal does not reach it.

VERSIONING.md: the #540 row leaves "Owed before 3.0.0", whose contract
is a claim the page makes with the repository not yet behind it. The
repository is behind it. The evolution table's "a keyed array made
positional" row named the table body alone, which #605 flagged; it now
names the union arm as well and states the provenance rule, which is the
whole of what the checker refuses there.

USAGE.md carried the same split as an italic caveat, and a caveat that
is no longer true is worse than one out of scope, so it is replaced by
what the diagnostic actually says. The #606 caveat beside it stays.

Present tense, no history, no em dashes added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* goldens: re-pin maps/TextTable.cpp, which main left behind (#625, #626)

NOT THIS ISSUE'S CHANGE, and it is here because the merge carries it.

main is red at 19a2372 on its own CI run 34081461383, and it was green
at bb5f3bd. #625 moved `internal/codegen/cpptable/json.go` and re-pinned
the map goldens it knew about, DepthTable.cpp, FleetTable.cpp and
RowsTable.cpp, each by the same 91 lines. Text.schema and its golden
landed in #626, which merged after #625's branch was cut, so
maps/TextTable.cpp never got the re-pin and `make tables-block-zero-cost`
reports it moved.

The gap is exactly the 91 lines the three siblings took, the
TableJsonInterpretExact reader and the decimal band beside it, which
every table holding a map now emits. The file here is the generator's
own output, copied by the path `make update-goldens` copies it, with no
hand edit: copying the whole maps directory moves this one file and
nothing else, which is what says the other three were already current.

    block zero-cost gate: 105 Table sources byte-identical to their pins

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* corpus: drop an em dash from the enum-bound README (#605)

House style takes none in anything added here, and the heading carried
one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JSON map keys after #607: whitespace and decimal spellings still corrupt key identity, and the prefix control does not establish its defect

1 participant