P13-S22: the tag space gets a second witness

`operation_kind_tag_vocabulary!` generates `discriminant()`,
`from_discriminant()`, `catalog_name()`, `introduced_minor()` and
`PAYLOAD_FREE` from one list, and `PAYLOAD_FREE` carries variants only.
Every derived test therefore obtains a tag and its byte from the same
invocation and asserts `$disc == $disc`. It cannot disagree with the
macro, because it is the macro.

The kind side never had this problem: `OperationKind::discriminant()` is
a hand-written match, so two independent statements exist and
`operation_kind_wire_discriminants_are_golden` asserts they agree. This
adds the tag side's second statement — `tag_wire_discriminants_are_golden`,
a hand-typed 40-row literal table transcribed by reading the macro
invocation rather than derived from its output.

Coverage is computed, association is not: a 40-long array does not prove
forty distinct tags, so the table's totality over the vocabulary is
asserted separately, and the comment says why that is not circular.
Payload-free tags assert the whole canonical byte vector — which also
proves the length-1 property the retired test had and the kind-side
idiom lacks — while `Registered`, the one tag carrying a payload,
asserts `[0]` alone.

Retires `phase3_tag_discriminants_are_golden`, whose whole subject was
tag→byte for 24–29. Keeps the three assertions at payload.rs:3086,
reduce.rs:12744 and reduce.rs:15941: the table duplicates their tag→byte
subclaim but not the kind-and-tag pairings that contain them, nor G3b's
local mutation evidence.

Signed by the coordinated 32↔33 permutation — swapping the discriminant
literals *and* the declaration lines, so `PAYLOAD_FREE` still emits
ascending discriminants and every derived artifact stays byte-identical.
Before: 1558/0, silent. After: fails naming SetCanvasLayoutDefaults.

No wire, schema, specification or corpus change. Suite 1558/0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
This commit is contained in:
Levi Neuwirth 2026-07-31 19:28:28 -04:00
parent 3397c0573e
commit 6e4e45587f
2 changed files with 166 additions and 16 deletions

View File

@ -2198,12 +2198,14 @@ mod tests {
// that class of stale hand-maintained table. Extended to 40 entries and // that class of stale hand-maintained table. Extended to 40 entries and
// closed 2026-07-30. `OperationKind::discriminant()` is a hand-written // closed 2026-07-30. `OperationKind::discriminant()` is a hand-written
// match, so a discriminant can be edited there without touching the // match, so a discriminant can be edited there without touching the
// enum; the table below is what makes that edit fail. The sibling tag // enum; the table below is what makes that edit fail. The tag half is
// half needs no such extension: `the_tag_vocabulary_is_complete` is // not exempt from an equivalent guard: its derived tests (among them
// derived from `OperationKindTag::PAYLOAD_FREE` with a computed bound // `the_tag_vocabulary_is_complete`) prove the vocabulary's
// rather than a spelled one, so it already covers every tag including // completeness, density and round-trip — never which byte a tag
// 30..=39. Adding a kind here is not optional bookkeeping — a kind // holds — and `tag_wire_discriminants_are_golden` (this file) is what
// appended without a row rediscovers P13-S15. // pins that mapping (`spec/CONTRACT_P13S22_TAGLOCK.md`). Adding a kind
// here is not optional bookkeeping — a kind appended without a row
// rediscovers P13-S15.
use crate::valuegen; use crate::valuegen;
use epiphany_core::{MusicalDuration, MusicalPosition, TimeSignatureId}; use epiphany_core::{MusicalDuration, MusicalPosition, TimeSignatureId};
@ -2725,20 +2727,168 @@ mod tests {
} }
#[test] #[test]
fn phase3_tag_discriminants_are_golden() { fn tag_wire_discriminants_are_golden() {
// GOLDEN LOCK (Phase-3 first tranche): appended past the ratified // GOLDEN LOCK, tag side (`spec/CONTRACT_P13S22_TAGLOCK.md`,
// 0..=23; the values below never change. // disposition A). `operation_kind_tag_vocabulary!` (`:695`ff)
for (tag, expected) in [ // generates `discriminant()`, `from_discriminant()`, `PAYLOAD_FREE`
(OperationKindTag::InsertStaff, 24u8), // and friends from ONE list, so every existing derived test that
// pairs a tag with its byte via
// `PAYLOAD_FREE.iter().map(|t| (t, t.discriminant()))` asserts
// `$disc == $disc` — it IS the macro, and cannot disagree with it.
// The table below is hand-typed, transcribed independently by
// reading the macro invocation (`:754``:793`) and
// `REGISTERED_TAG_DISCRIMINANT` (`:679`), so it is the tag side's
// first genuinely second statement of the mapping.
//
// FORBIDDEN ABSOLUTELY: deriving any row, the length, or the
// ordering from `PAYLOAD_FREE`, `discriminant()`,
// `from_discriminant()`, or any other macro output — the same
// warning appears at `:2638``:2639`,
// `testkit/tests/text_projection_grammar.rs:312``:314`, and
// `layout-ir/src/barrier.rs:1171``:1172`. A table so derived is
// born green.
//
// Coverage vs. association, and why the split below is not
// circular: the block asserting the table's tag set and byte set
// match the vocabulary is legitimately COMPUTED — a 40-long array
// does not by itself prove forty distinct tags, since duplicate
// rows would satisfy the length. That block proves the table is
// TOTAL over the vocabulary; it says NOTHING about which tag holds
// which byte, which only the literal rows below state.
//
// Redundancy is intended: this table duplicates the tag→byte
// subclaim already made by `payload.rs:3086`, `reduce.rs:12744` and
// `reduce.rs:15941`. Those three stay — each is the byte-half of a
// paired kind-and-tag statement in one place
// (`…kind_and_tag_discriminant_are_39`, `…kind_and_tag_are_both_34`,
// `…in_both_spaces`) — do not "consolidate" them into this table and
// break that locality. This table replaces only
// `phase3_tag_discriminants_are_golden` (retired), whose entire
// subject was tag→byte for 2429 and nothing else.
let table: [(OperationKindTag, u8); 40] = [
(OperationKindTag::InsertEvent, 0),
(OperationKindTag::DeleteEvent, 1),
(OperationKindTag::ModifyEvent, 2),
(OperationKindTag::RespellPitch, 3),
(OperationKindTag::Transpose, 4),
(OperationKindTag::CreateCrossCutting, 5),
(OperationKindTag::DeleteCrossCutting, 6),
(OperationKindTag::ModifyCrossCutting, 7),
(OperationKindTag::ChangeRegionTimeModel, 8),
(OperationKindTag::InsertRegion, 9),
(OperationKindTag::DeleteRegion, 10),
(OperationKindTag::InsertStaffInstance, 11),
(OperationKindTag::DeleteStaffInstance, 12),
(OperationKindTag::SetUserSystemBreak, 13),
(OperationKindTag::SetUserPageBreak, 14),
(OperationKindTag::DeclareTransaction, 15),
(
OperationKindTag::Registered(OperationKindRegistryId(
0x0102_0304_0506_0708_090A_0B0C_0D0E_0F10,
)),
16,
),
(OperationKindTag::InsertIdentifiedPitch, 17),
(OperationKindTag::DeleteIdentifiedPitch, 18),
(OperationKindTag::ModifyIdentifiedPitch, 19),
(OperationKindTag::CreateVoice, 20),
(OperationKindTag::DeleteVoice, 21),
(OperationKindTag::SetMetadata, 22),
(OperationKindTag::SetMetricGrid, 23),
(OperationKindTag::InsertStaff, 24),
(OperationKindTag::SetTimeSignature, 25), (OperationKindTag::SetTimeSignature, 25),
(OperationKindTag::SetTempoSegment, 26), (OperationKindTag::SetTempoSegment, 26),
(OperationKindTag::SetStaffLayout, 27), (OperationKindTag::SetStaffLayout, 27),
// Schema-major-2 revision (repeat authoring).
(OperationKindTag::CreateRepeatStructure, 28), (OperationKindTag::CreateRepeatStructure, 28),
(OperationKindTag::DeleteRepeatStructure, 29), (OperationKindTag::DeleteRepeatStructure, 29),
] { (OperationKindTag::TransposeInterval, 30),
assert_eq!(tag.discriminant(), expected); (OperationKindTag::CreateInstrument, 31),
assert_eq!(tag.to_canonical_bytes(), vec![expected]); (OperationKindTag::SetCanvasLayoutDefaults, 32),
(OperationKindTag::SetSpellingPrecedence, 33),
(OperationKindTag::SetTuningContext, 34),
(OperationKindTag::CreateStaffGroup, 35),
(OperationKindTag::CreatePartDefinition, 36),
(OperationKindTag::CreateAnalysisLayer, 37),
(OperationKindTag::CreateView, 38),
(OperationKindTag::CreateMeasure, 39),
];
// --- Coverage (derived; see the header comment above) ---
{
let mut expected: Vec<OperationKindTag> = OperationKindTag::PAYLOAD_FREE.to_vec();
expected.push(OperationKindTag::Registered(OperationKindRegistryId(0)));
assert_eq!(
expected.len(),
40,
"sanity: the vocabulary itself is 40 wide"
);
// Tag-set equality with `Registered`'s payload id normalized
// away: only its *presence* is a vocabulary fact, the id is
// per-message data the table's own row (above) does not pin.
let normalize = |t: &OperationKindTag| match t {
OperationKindTag::Registered(_) => {
OperationKindTag::Registered(OperationKindRegistryId(0))
}
other => *other,
};
let table_tags: std::collections::BTreeSet<OperationKindTag> =
table.iter().map(|(t, _)| normalize(t)).collect();
let expected_tags: std::collections::BTreeSet<OperationKindTag> =
expected.iter().map(normalize).collect();
assert_eq!(
table_tags, expected_tags,
"the table's tag set must equal PAYLOAD_FREE union {{Registered}}, each exactly once"
);
assert_eq!(
table_tags.len(),
40,
"duplicate rows would satisfy the length without covering the vocabulary"
);
let byte_set: std::collections::BTreeSet<u8> = table.iter().map(|(_, b)| *b).collect();
assert_eq!(
byte_set,
(0u8..40).collect::<std::collections::BTreeSet<u8>>(),
"the table's byte set must be exactly 0..=39, no gaps, no repeats"
);
}
// --- Association: the payload-free / `Registered` asymmetry ---
// `Registered` (byte 16) carries a 16-byte registry id, so its
// canonical encoding is 17 bytes; asserting the WHOLE vector against
// `vec![16]` would fail on encoding length, not on the mapping this
// test exists to pin, so it is checked by `[0]` only. Every other
// (payload-free) tag's encoding is exactly one byte, and asserting
// the whole vector — not just `[0]` — is what also proves that
// length-1 property: the property `phase3_tag_discriminants_are_golden`
// had and the kind-side idiom (`[0]` only) does not. `Registered` is
// the sole non-payload-free tag in this table.
for (tag, expected) in &table {
assert_eq!(
tag.discriminant(),
*expected,
"wire discriminant for {:?} moved — canonical encodings are append-only",
tag,
);
match tag {
OperationKindTag::Registered(_) => {
assert_eq!(
tag.to_canonical_bytes()[0],
*expected,
"wire discriminant for {:?} moved — canonical encodings are append-only",
tag,
);
}
_ => {
assert_eq!(
tag.to_canonical_bytes(),
vec![*expected],
"wire discriminant for {:?} moved — canonical encodings are append-only",
tag,
);
}
}
} }
} }

File diff suppressed because one or more lines are too long