Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ jobs:
- name: go test
run: go test ./...

# The projection's three controls (SPEC §3.1): a projection that stopped
# carrying enum and flags variant names, or a union's arm names, would
# let a reorder ship as a false match, and one without the codec law line
# would let a rounding change ship as one. Only a control that has gone
# red proves a gate is watching. All three are Go-only and take seconds,
# so they belong on the diff.
- name: the projection's gates go red without what they watch
run: |
make projection-variant-order-negative-control
make projection-wire-law-negative-control
make projection-union-arm-order-negative-control

# THE GENERATED TREE IS CURRENT (issue #30). The committed generated/ tree is
# what this repo treats as ground truth — findings are receipted against its
# line numbers — so a compiler change that moves the emitted text and leaves
Expand Down
110 changes: 110 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -1720,6 +1720,113 @@ check-zero-range-negative-control: bin/schema test/zero_range_negative_main.cpp
test/zero_range_negative_main.cpp -o build/schema_test_zero_range_negative
./build/schema_test_zero_range_negative

# THE VARIANT-ORDER NEGATIVE CONTROL (SPEC §3.1, issue #462). An enum value
# rides as its declaration ordinal and a flags variant as its bit position, so
# the projection carries both declarations' variant names in declaration order:
# without them a reorder is invisible and two builds either side of an
# alphabetized enum hold ONE id while every ordinal means something else.
#
# The control takes the variant lists back out of a COPY of the rendering
# through `go test -overlay` — no tracked file is written — and the reorder
# gate must go RED, on that surface and NOT on the union's, whose order rides
# in its payload types and must stay green under the same sabotage.
.PHONY: projection-variant-order-negative-control
projection-variant-order-negative-control:
@mkdir -p build
@sed -e 's|fmt.Fprintf(&b, " variant %d name=.*, i+1, v)$$|_, _ = i, v // SABOTAGED: the enum variant list removed|' \
-e 's|fmt.Fprintf(&b, " bit %d name=.*|_, _ = i, v // SABOTAGED: the flags variant list removed|' \
ir/projection.go > build/projection-no-variants.gotext
@[ "$$(grep -c SABOTAGED build/projection-no-variants.gotext)" = "2" ] || \
{ echo "NEGATIVE CONTROL FAILED: the sabotage did not remove both variant lists"; exit 1; }
@printf '{"Replace":{"%s/ir/projection.go":"%s/build/projection-no-variants.gotext"}}\n' \
"$(CURDIR)" "$(CURDIR)" > build/projection-no-variants-overlay.json
@if go test -count=1 -overlay=build/projection-no-variants-overlay.json \
./internal/check -run TestIdMovesUnderVariantOrder > build/projection-no-variants.log 2>&1; then \
echo "NEGATIVE CONTROL FAILED: the reorder gate passed with the variant lists gone"; \
cat build/projection-no-variants.log; exit 1; \
fi
@grep -q "an enum reordered did NOT move the protocol id" build/projection-no-variants.log || \
{ echo "NEGATIVE CONTROL FAILED: it went red, but not on the enum reorder"; \
cat build/projection-no-variants.log; exit 1; }
@grep -q "a flags declaration reordered did NOT move the protocol id" build/projection-no-variants.log || \
{ echo "NEGATIVE CONTROL FAILED: it went red, but not on the flags reorder"; \
cat build/projection-no-variants.log; exit 1; }
@go test -count=1 -overlay=build/projection-no-variants-overlay.json \
./internal/check -run 'TestUnionId' > build/projection-no-variants-union.log 2>&1 || \
{ echo "NEGATIVE CONTROL FAILED: the sabotage reddened the union gate too — it is not surgical"; \
cat build/projection-no-variants-union.log; exit 1; }
@echo "negative control: without the variant lists the enum and flags reorders go silent, and the union gate stays green"

# THE CODEC LAW NEGATIVE CONTROL (SPEC §3.1, issue #463). The projection's
# second version line is what a compiler change that moves the BYTES under an
# unchanged shape rides on — the 2026-08-15 rounding amendment moved bytes with
# every id unmoved, which is a false match. Take the line out of a COPY of the
# rendering and both law gates must go red, while the variant-order gate,
# which the line has nothing to do with, stays green.
.PHONY: projection-wire-law-negative-control
projection-wire-law-negative-control:
@mkdir -p build
@sed -e 's|fmt.Fprintf(&b, "schema-wire-law .*|// SABOTAGED: the codec law line removed|' \
ir/projection.go > build/projection-no-wire-law.gotext
@grep -q SABOTAGED build/projection-no-wire-law.gotext || \
{ echo "NEGATIVE CONTROL FAILED: the sabotage did not remove the codec law line"; exit 1; }
@printf '{"Replace":{"%s/ir/projection.go":"%s/build/projection-no-wire-law.gotext"}}\n' \
"$(CURDIR)" "$(CURDIR)" > build/projection-no-wire-law-overlay.json
@if go test -count=1 -overlay=build/projection-no-wire-law-overlay.json \
./internal/check -run TestWireLawLineMovesTheId > build/projection-no-wire-law.log 2>&1; then \
echo "NEGATIVE CONTROL FAILED: the law gate passed with the line gone"; \
cat build/projection-no-wire-law.log; exit 1; \
fi
@grep -q "the projection must open with its rendering version and then its codec law" build/projection-no-wire-law.log || \
{ echo "NEGATIVE CONTROL FAILED: it went red, but not on the missing law line"; \
cat build/projection-no-wire-law.log; exit 1; }
@if go test -count=1 -overlay=build/projection-no-wire-law-overlay.json \
./internal/goldens -run TestWireLawBumpMovesEveryId > build/projection-no-wire-law-corpus.log 2>&1; then \
echo "NEGATIVE CONTROL FAILED: every corpus id survived the line's removal in silence"; \
cat build/projection-no-wire-law-corpus.log; exit 1; \
fi
@grep -q "does not carry the codec law line" build/projection-no-wire-law-corpus.log || \
{ echo "NEGATIVE CONTROL FAILED: the corpus gate went red for another reason"; \
cat build/projection-no-wire-law-corpus.log; exit 1; }
@go test -count=1 -overlay=build/projection-no-wire-law-overlay.json \
./internal/check -run TestIdMovesUnderVariantOrder > build/projection-no-wire-law-variants.log 2>&1 || \
{ echo "NEGATIVE CONTROL FAILED: the sabotage reddened the variant-order gate too — it is not surgical"; \
cat build/projection-no-wire-law-variants.log; exit 1; }
@echo "negative control: without the codec law line both law gates go red, and the variant-order gate stays green"

# THE UNION ARM-ORDER NEGATIVE CONTROL (SPEC §3.1, §4.8, issue #491). A union's
# arm order rides in its payload types only while the arms DIFFER in type: two
# arms of one type reorder with every projected type unmoved, so the arm names
# project beside them. Take the names out of a COPY of the rendering and the
# same-typed reorder goes silent, while the enum and flags gate — which the arm
# names have nothing to do with — stays green.
.PHONY: projection-union-arm-order-negative-control
projection-union-arm-order-negative-control:
@mkdir -p build
@sed -E -e 's|" variant %d name=%s payload=|" variant %d payload=|' \
-e 's|i\+1, v\.Name, v\.Type\)|i+1, v.Type) // SABOTAGED: the arm names removed|' \
ir/projection.go > build/projection-no-arm-names.gotext
@grep -q SABOTAGED build/projection-no-arm-names.gotext || \
{ echo "NEGATIVE CONTROL FAILED: the sabotage did not remove the arm names"; exit 1; }
@printf '{"Replace":{"%s/ir/projection.go":"%s/build/projection-no-arm-names.gotext"}}\n' \
"$(CURDIR)" "$(CURDIR)" > build/projection-no-arm-names-overlay.json
@if go test -count=1 -overlay=build/projection-no-arm-names-overlay.json \
./internal/check -run 'TestUnionId' > build/projection-no-arm-names.log 2>&1; then \
echo "NEGATIVE CONTROL FAILED: the union gates passed with the arm names gone"; \
cat build/projection-no-arm-names.log; exit 1; \
fi
@grep -q "two arms of one payload type reordered and the id did not move" build/projection-no-arm-names.log || \
{ echo "NEGATIVE CONTROL FAILED: it went red, but not on the same-typed reorder"; \
cat build/projection-no-arm-names.log; exit 1; }
@grep -q "an arm renamed (arm order is spelled in names) did NOT move the protocol id" build/projection-no-arm-names.log || \
{ echo "NEGATIVE CONTROL FAILED: it went red, but not on the arm rename"; \
cat build/projection-no-arm-names.log; exit 1; }
@go test -count=1 -overlay=build/projection-no-arm-names-overlay.json \
./internal/check -run TestIdMovesUnderVariantOrder > build/projection-no-arm-names-variants.log 2>&1 || \
{ echo "NEGATIVE CONTROL FAILED: the sabotage reddened the variant-order gate too — it is not surgical"; \
cat build/projection-no-arm-names-variants.log; exit 1; }
@echo "negative control: without the arm names a same-typed union reorder goes silent, and the variant-order gate stays green"

# Deliberately compiled WITHOUT -I$(SERIALIZE): the generated Table headers
# carry no serialize dependency, and this build proves it stays that way.
#
Expand Down Expand Up @@ -2106,6 +2213,9 @@ test: build/schema_test build/schema_test_guard build/schema_test_tables build/s
./build/schema_test
./build/schema_test_guard
$(MAKE) check-zero-range-negative-control
$(MAKE) projection-variant-order-negative-control
$(MAKE) projection-wire-law-negative-control
$(MAKE) projection-union-arm-order-negative-control
./build/schema_test_tables
./build/schema_test_tables_asan
# THE WIRE FUZZER (docs/SPEC-TABLES.md §4.2): the tolerant read on hostile
Expand Down
11 changes: 8 additions & 3 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,14 @@ serve, and Protobuf does.

The id hashes a **wire shape projection**, not the source text, so an edit
that moves no bytes does not move the id: a comment, a blank line, a renamed
file, a renamed enum variant. Run `schema projection` to see exactly what it
depends on — it is deliberately printable, because a wire-affecting fact
missing from that text would be a fact the id ignores.
file, a renamed `const`. Renaming an enum variant, a flags variant
or a union arm is not in that list: the ordinal is the wire and the ordered
names are the only record of which ordinal means what, so a reorder — and
therefore a rename — moves the id.

Run `schema projection` to see exactly what it depends on — it is
deliberately printable, because a wire-affecting fact missing from that text
would be a fact the id ignores.

## Why AGPL? My lawyer will hate this.

Expand Down
27 changes: 15 additions & 12 deletions docs/SPEC-TABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3108,13 +3108,13 @@ only.
| the edit | the read report | the baseline | the build version |
|---|---|---|---|
| a specified DEFAULT changed, added or removed | silent | **refuses** | **moves** |
| a FLAGS variant inserted, removed, reordered or renamed in place | silent | **refuses** | no — a mask rides raw and a load copies it verbatim |
| a FLAGS variant inserted, removed, reordered or renamed in place | silent | **refuses** | **moves**, through the protocol id: the bit positions are the declaration's variant names and they ride in the wire-shape projection (SPEC.md §3.1). The cook projection carries them itself once #435 lands |
| a field's REFERENT dropped, or swapped for one that cannot stand in | silent | **refuses** | **moves** |
| a field's wire KIND, or an array's ELEMENT kind, changed | `kind_mismatch` | **refuses** | **moves** |
| an array changed between keyed and positional, or its KEY enum swapped | `kind_mismatch` | **refuses** | **moves** |
| a declared RANGE tightened — a maximum lowered, a minimum raised, or a range declared where the field had none | `clamped` | **warns** — the `min=`/`max=` tokens are extents like a capacity (§18.1) | **moves** |
| a fixed field's `F` moved under the same storage width | silent — the kind carries the width and the signedness, and `F` is a declaration-side fact like a resolution (§3) | **refuses** — the `frac=` token is a fixed fact (§18.1) | **moves** |
| an `enum`'s or a `union`'s variant order or names moved | `unknown` for a name this reader lacks; a reorder is silent and safe | warns on a removal or a vanished name | **moves** |
| an `enum`'s or a `union`'s variant order or names moved | `unknown` for a name this reader lacks; a reorder is silent and safe | warns on a removal or a vanished name | **moves**, and so does the protocol id: both vocabularies project their ordered names (SPEC.md §3.1) |
| a union ARM's declared TYPE changed | `kind_mismatch` or `malformed` wherever the arm's `L` or its framing can see it; **silent** for the four retypes §3 names | **refuses** | **moves** |
| a union arm moved between a BODY and a POINTER — `T` to `*T` | `kind_mismatch` or `malformed`, in both directions: no conforming writer emits a four-byte body, and a node index read as one is stopped by the terminator rule (§3) | **refuses** — no kind byte separates the two one level down, which is why this row differs from the FIELD's above | **moves** |
| a field added, removed or reordered | `unknown` for an id this reader lacks; an absent field defaults | passes; a removal AND an addition in one table in one edit **warn** as the pair a bare rename leaves (§18.2) | **moves** |
Expand Down Expand Up @@ -4446,7 +4446,7 @@ with `scene.name = "hi"`, `scene.head = A`, `A.next = B`, `A.value = 1`,
length at 8 — the buffer aligns at one and the length at four — then `head`
at 16 and `palette` at 24, `sizeof=32 alignof=8`. `schema build-version
--facts` prints those same numbers, and the id over them is
`0x4efe97313c704bb5`.
`0x355ef4922f004a7f`.
- **The numbering** is §3.1's walk: the root is 1; `head` reaches A, so A is
2; descending A, `next` reaches B, so B is 3; then `palette` reaches P, so
P is 4.
Expand All @@ -4462,7 +4462,7 @@ with `scene.name = "hi"`, `scene.head = A`, `A.next = B`, `A.value = 1`,

```
0000 53 43 48 4d 43 4f 4f 4b magic "SCHMCOOK"
0008 b5 4b 70 3c 31 97 fe 4e build_version 0x4efe97313c704bb5
0008 7f 4a 00 2f 92 f4 5e 35 build_version 0x355ef4922f004a7f
0010 01 00 00 00 00 00 00 00 byte_order = 1 (little)
0018 48 00 00 00 00 00 00 00 data_length = 72
0020 40 00 00 00 00 00 00 00 attribution_length = 64
Expand Down Expand Up @@ -9887,10 +9887,14 @@ a second digest.
line sees: a specified default changed; **a declared range tightened**; **a
`bits(N)` narrowed within one storage width** — the case where the implied
range moves and the storage kind, the size and the wire id do not; an `enum`
variant renamed; two `enum` variants swapped; **a `union` arm RENAMED** —
the rename and not a reorder, because SPEC.md §3.1 already puts union
variant ORDER in the protocol id, so a reorder would pass through group 1
with group 3's union fact deleted.
variant renamed; two `enum` variants swapped; **a `union` arm RENAMED**;
**a `flags` variant REORDERED, and one RENAMED**. Those four rows now ride
group 1 as well, because SPEC.md §3.1 projects every vocabulary's ordered
names and the protocol id rides here in whole (§20.1) — so each proves the
build version moves, and none of them isolates group 3's own vocabulary
tokens any more. **What isolates them is a TABLE-ARMED union's arm renamed**
(§2.6): such a union has no packet wire, so the protocol id cannot cover it
and only the cook projection's `union=`/`payload=` tokens can see the edit.
- **The layout group's own controls**: a field's KIND changed with its width
unmoved; a field's offset moved with the record's `sizeof` unmoved; **a
declared maximum raised** — which moves it, and whose §19.4 consequence is
Expand All @@ -9907,10 +9911,9 @@ a second digest.
unchanged.

- **The exclusions, each with the edit that proves it**: a `was` rename moves
nothing; **a `flags` variant REORDERED moves nothing** — the row the
discipline of §4.1 and the baseline of §18 own instead; a `flags` variant
renamed moves nothing; **a flags field's REFERENT swapped for a same-width
other moves nothing** — the negative control for the missing `flags=` token;
nothing; **a flags field's REFERENT swapped for a same-width
other moves nothing** — the negative control for the missing `flags=` token,
and a table body's field, so the protocol id does not cover it either;
a guard added or removed moves nothing; a `json` key changed moves nothing;
a comment, a file split and a reorder of two records' declarations move
nothing.
Expand Down
Loading
Loading