Pass 11 follow-up: golden-lock the ratified discriminant tables; fix three spec-text inaccuracies

Audit follow-up to b2f2e20. The ratification was byte-faithful, but the
audit found a gap between the protection the worklist claims ("a
golden-bytes test already locks every byte-layout item") and the
protection actually in place for several newly-normative tables, plus
three small inaccuracies in the ratified spec text.

Golden locks (close the round-trip-vs-golden gap):
- ChunkKind: chunk_kind_discriminants_are_golden pins the literal 0..=8.
  ChunkKind::canonical_bytes() is in the chunk hash preimage, so the
  prior round-trip-only test would let a coordinated renumbering silently
  change every chunk content address while passing.
- CompressionAlgorithm: compression_algorithm_encoding_is_golden pins the
  exact bytes (None -> [0,0], not a bare tag).
- ProfileId (load-bearing superblock field): profile_id_discriminants_are_golden
  pins the u32 discriminants and the fixed 20-byte encoding.
- ResolutionAction / TransactionCategory / ObjectKind: *_discriminants_are_golden
  pin the canonical discriminants (ObjectKind feeds the anomaly id;
  ResolutionAction/TransactionCategory feed operation content hashes).
- IntegrityAnomalyId: integrity_anomaly_id_byte_form_is_locked golden-locks
  the MUSCSANM-derived id (cross-replica agreement is a conformance
  property; it previously had no byte-form golden).

Spec-text fixes (core_spec.tex):
- CompressionAlgorithm: "None = 0 (no payload)" was wrong; the code writes
  a fixed two bytes (discriminant + always-present parameter byte). Text
  now states the fixed-width framing.
- ProfileId: "a single discriminant followed by any variant payload" was
  wrong; it is a u32-LE discriminant + a fixed 16-byte registry id (zero
  unless Custom), 20 bytes total. Text now matches the only encoding.
- TupletRatio listing showed `pub` fields (freely constructible by struct
  literal), contradicting req:time:tuplet-ratio-construction. Listing now
  shows private fields + the checked `new`/`actual()`/`notated()`, matching
  the code.

Test honesty:
- testkit resolution_action generator now emits Dismiss (rng.below(6)); it
  previously skipped the variant, leaving the Dismiss path unfuzzed.

Verification: cargo test --workspace (433 pass, +7), clippy -D warnings
clean, fmt clean; spec rebuilds (lualatex/latexmk, 0 undefined refs, 254pp).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-06-22 09:22:55 -04:00
parent b2f2e204a7
commit a7adbdc5a1
9 changed files with 167 additions and 14 deletions

View File

@ -314,6 +314,41 @@ mod tests {
assert_eq!(ChunkKind::from_discriminant(9), None);
}
#[test]
fn chunk_kind_discriminants_are_golden() {
// RATIFIED by Pass 11 (item 1.5, req:format:chunkkind-discriminants):
// the declaration-order discriminant byte is in the chunk hash preimage,
// so the *literal* values are normative. The round-trip test above is
// invariant under a coordinated renumbering; this locks the values
// themselves so a reorder breaks deliberately (and cannot silently
// change every chunk's content address).
assert_eq!(ChunkKind::OperationEnvelopeBlock.discriminant(), 0);
assert_eq!(ChunkKind::OperationIndex.discriminant(), 1);
assert_eq!(ChunkKind::Snapshot.discriminant(), 2);
assert_eq!(ChunkKind::Blob.discriminant(), 3);
assert_eq!(ChunkKind::ExtensionData.discriminant(), 4);
assert_eq!(ChunkKind::TextProjection.discriminant(), 5);
assert_eq!(ChunkKind::LayoutCache.discriminant(), 6);
assert_eq!(ChunkKind::IntegrityIndex.discriminant(), 7);
assert_eq!(ChunkKind::Manifest.discriminant(), 8);
}
#[test]
fn compression_algorithm_encoding_is_golden() {
// RATIFIED by Pass 11 (item 1.5, req:format:chunkkind-discriminants):
// CompressionAlgorithm encodes as a fixed two bytes — a discriminant
// byte plus an always-present parameter byte. `None` carries a zero
// parameter byte (it is NOT a bare tag), so lock the exact bytes.
let enc = |c: CompressionAlgorithm| {
let mut w = Writer::new();
c.encode(&mut w);
w.into_bytes()
};
assert_eq!(enc(CompressionAlgorithm::None), vec![0, 0]);
assert_eq!(enc(CompressionAlgorithm::Zstd { level: 9 }), vec![1, 9]);
assert_eq!(enc(CompressionAlgorithm::Reserved(7)), vec![2, 7]);
}
#[test]
fn manifest_and_ordinary_chunks_use_distinct_domains() {
// A manifest payload and an operation block with identical bytes must

View File

@ -406,6 +406,25 @@ mod tests {
}
}
#[test]
fn profile_id_discriminants_are_golden() {
// RATIFIED by Pass 11 (item 1.5, req:format:profileid-discriminants):
// ProfileId is a load-bearing superblock-selection field, so the literal
// u32 discriminants are normative. Lock the values, not just round-trip.
assert_eq!(ProfileId::Full.discriminant(), 0);
assert_eq!(ProfileId::ReadOnly.discriminant(), 1);
assert_eq!(ProfileId::Lite.discriminant(), 2);
assert_eq!(
ProfileId::Custom(ProfileRegistryId([0; 16])).discriminant(),
3
);
// And the fixed-width encoding: a u32-LE discriminant + 16-byte registry
// id (zero unless Custom) = 20 bytes; Full is twenty zero bytes.
let mut w = Writer::new();
ProfileId::Full.encode(&mut w);
assert_eq!(w.into_bytes(), vec![0u8; 20]);
}
#[test]
fn superblock_round_trips_through_256_bytes() {
let original = Superblock {

View File

@ -303,6 +303,29 @@ mod tests {
}
}
#[test]
fn integrity_anomaly_id_byte_form_is_locked() {
// Golden: locks the MUSCSANM-derived anomaly id for a fixed kind.
// RATIFIED by Pass 11 (item 1.4, req:graph:integrity-anomaly-id): the
// identity is content-derived so two replicas observing the same failure
// agree on it, which is a conformance property. A change to the domain
// tag, the kind's canonical encoding, or the derivation breaks this
// deliberately.
let kind = IntegrityAnomalyKind::OperationSlotEquivocated {
operation_id: OperationId::new(ReplicaId(7), 42),
};
let id = IntegrityAnomaly::new(kind).id;
// First 8 bytes are the SYSTEM_DERIVED replica; the id is in that namespace.
assert_eq!(
&id.canonical_bytes()[0..8],
&ReplicaId::SYSTEM_DERIVED.to_be_bytes()
);
const GOLDEN: [u8; 16] = [
255, 255, 255, 255, 255, 255, 255, 255, 81, 178, 18, 20, 252, 222, 201, 215,
];
assert_eq!(id.canonical_bytes(), GOLDEN);
}
#[test]
fn monotone_stream_has_no_anomaly() {
let a = env(1, 0, 10, 0);

View File

@ -400,6 +400,33 @@ mod tests {
TypedObjectId::Event(EventId::from_raw(n))
}
#[test]
fn resolution_action_discriminants_are_golden() {
// RATIFIED by Pass 11 (items 2.5 / 1.5): ResolutionAction is canonically
// encoded into operation content, so its discriminants are normative.
// Dismiss = 4 was inserted ahead of Registered (now 5); lock the literal
// values so any future reorder breaks deliberately rather than silently
// shifting the wire form.
assert_eq!(ResolutionAction::AcceptLoser.discriminant(), 0);
assert_eq!(ResolutionAction::KeepWinner.discriminant(), 1);
assert_eq!(
ResolutionAction::Override {
override_operation: op(1, 1)
}
.discriminant(),
2
);
assert_eq!(
ResolutionAction::Reanchor { new_target: obj(7) }.discriminant(),
3
);
assert_eq!(ResolutionAction::Dismiss.discriminant(), 4);
assert_eq!(
ResolutionAction::Registered(ResolutionRegistryId(0)).discriminant(),
5
);
}
#[test]
fn conflict_id_is_order_independent_in_its_inputs() {
let kind = ConflictKind::TombstonedTarget {

View File

@ -537,6 +537,20 @@ mod tests {
use super::*;
use epiphany_core::{ReplicaId, SlurId};
#[test]
fn transaction_category_discriminants_are_golden() {
// RATIFIED by Pass 11 (item 2.4, req:semops:transaction-category): the
// declaration-order discriminants are normative and canonically encoded.
assert_eq!(TransactionCategory::NoteEntry.discriminant(), 0);
assert_eq!(TransactionCategory::Structural.discriminant(), 1);
assert_eq!(TransactionCategory::Layout.discriminant(), 2);
assert_eq!(TransactionCategory::Import.discriminant(), 3);
assert_eq!(
TransactionCategory::Registered(OperationKindRegistryId(0)).discriminant(),
4
);
}
#[test]
fn kind_tag_projects_and_round_trips_registered_id() {
let reg = OperationKindRegistryId(7);

View File

@ -192,6 +192,20 @@ mod tests {
use super::*;
use epiphany_determinism::sort_canonical;
#[test]
fn object_kind_discriminants_are_golden() {
// RATIFIED by Pass 11 (item 2.4, req:graph:object-kind-vocab): the
// discriminant byte feeds an IntegrityAnomaly's content-derived
// identity, so the literal values are normative and must agree across
// replicas. Lock them.
assert_eq!(ObjectKind::Voice.discriminant(), 0);
assert_eq!(ObjectKind::Pitch.discriminant(), 1);
assert_eq!(
ObjectKind::Registered(OperationKindRegistryId(0)).discriminant(),
2
);
}
#[test]
fn registry_ids_order_by_canonical_bytes() {
let mut v = vec![

View File

@ -308,9 +308,10 @@ fn obj_pitch(n: u64) -> PitchId {
PitchId::new(OBJ_REPLICA, n)
}
/// A conflict-resolution action (every core and registered variant).
/// A conflict-resolution action (every core and registered variant,
/// including `Dismiss`).
pub fn resolution_action(rng: &mut Rng) -> ResolutionAction {
match rng.below(5) {
match rng.below(6) {
0 => ResolutionAction::AcceptLoser,
1 => ResolutionAction::KeepWinner,
2 => ResolutionAction::Override {
@ -319,6 +320,7 @@ pub fn resolution_action(rng: &mut Rng) -> ResolutionAction {
3 => ResolutionAction::Reanchor {
new_target: typed_object_id(rng),
},
4 => ResolutionAction::Dismiss,
_ => ResolutionAction::Registered(ResolutionRegistryId(
((rng.next_u64() as u128) << 64) | rng.next_u64() as u128,
)),

Binary file not shown.

View File

@ -2210,8 +2210,19 @@ pub struct Tuplet {
}
pub struct TupletRatio {
pub actual: u32,
pub notated: u32,
// Fields are private: a TupletRatio is constructed only through the
// checked `new`, so a degenerate ratio is never representable (see
// the requirement below). The fields are read via `actual()`/`notated()`.
actual: u32,
notated: u32,
}
impl TupletRatio {
/// Returns `None` for a degenerate ratio (either term zero, or
/// `actual == notated`); otherwise the well-formed ratio.
pub fn new(actual: u32, notated: u32) -> Option<TupletRatio> { /* checked */ }
pub fn actual(&self) -> u32 { /* ... */ }
pub fn notated(&self) -> u32 { /* ... */ }
}
\end{lstlisting}
@ -9642,11 +9653,15 @@ pub struct ChunkId(pub ContentHash);
\end{tabular}
\end{center}
\texttt{CompressionAlgorithm} likewise encodes as a single
declaration-order discriminant byte followed by any variant payload:
\texttt{None} $= 0$ (no payload); \texttt{Zstd\{level\}} $= 1$
followed by the \texttt{level} byte; \texttt{Reserved(u8)} $= 2$
followed by the reserved byte. Compression is \emph{not} part of a
\texttt{CompressionAlgorithm} encodes as a fixed \emph{two} bytes: a
declaration-order discriminant byte followed by a single parameter
byte that is always present. \texttt{None} $= 0$ (the parameter byte
is present but zero, and ignored on read); \texttt{Zstd\{level\}}
$= 1$ with the \texttt{level} in the parameter byte;
\texttt{Reserved(u8)} $= 2$ with the reserved value in the parameter
byte. The parameter byte is written even for \texttt{None}, so the
encoding is fixed-width and a reader always consumes two bytes.
Compression is \emph{not} part of a
chunk's content identity (it is metadata on the \texttt{ChunkRef}),
so these discriminants are stable but do not affect chunk hashes.
@ -10408,11 +10423,15 @@ pub struct ProfileConstraints {
\begin{requirement}
\label{req:format:profileid-discriminants}
\texttt{ProfileId} encodes as a single declaration-order
discriminant followed by any variant payload: \texttt{Full} $= 0$,
\texttt{ReadOnly} $= 1$, \texttt{Lite} $= 2$,
\texttt{Custom(ProfileRegistryId)} $= 3$ followed by the registry
id's canonical bytes. This assignment is normative and stable.
\texttt{ProfileId} encodes as a fixed \emph{20} bytes: a
little-endian \texttt{u32} declaration-order discriminant
(\texttt{Full} $= 0$, \texttt{ReadOnly} $= 1$, \texttt{Lite} $= 2$,
\texttt{Custom} $= 3$) followed by a 16-byte
\texttt{ProfileRegistryId}. The trailing 16 bytes carry the registry
id for \texttt{Custom} and are zero for the other three variants; the
field is always present, so the encoding is fixed-width (the
superblock reserves a fixed slot for it). This assignment is
normative and stable.
\texttt{ProfileId} participates in superblock selection
(Section~\ref{sec:format:bundle}, the load-bearing field set), so
its discriminants are part of that comparison.