Land M3 (Agent F): make criteria 1 & 4 honest
The audit flagged Agent F's criterion 1 (convergence) and criterion 4
(serialization) as testing the reducer-bookkeeping projection
(MaterializedState) while claiming to be full-Score gates. This makes them
honest, per item 4 of the v0 follow-up plan.
Criterion 1 — real-Score convergence:
- generators::graph_edit_session builds a real ~50-bar, two-voice edit
session targeting a base Score's actual voices (so it survives reduce_onto,
which rejects unknown voices).
- convergence::{assert_graph_convergence, run_graph_convergence} reduce that
session onto a real epiphany_core::Score via OperationSet::reduce_onto and
assert the entire GraphMaterialization (graph + bookkeeping) is identical
across delivery orders, passes check_invariants, and genuinely grows both
voices (non-vacuity).
- acceptance criterion_1_convergence now drives this; the former bookkeeping
convergence is retained and renamed reducer_bookkeeping_convergence.
Criterion 4 — honest serialization tiers:
- criterion_4_canonical_serialization_stability keeps the real typed/manifest
round-trips; the MaterializedState round-trip is split out as
reducer_bookkeeping_serialization.
- full_score_materialization_is_reproducible asserts the materialized Score is
reproducible across orders (the determinism precondition for a byte codec),
achievable without the codec.
- criterion_4_full_score_byte_roundtrip is #[ignore]'d pending item 5's
whole-score codec (visible as ignored, never falsely green).
Negative regression guards (src/negative.rs): one guard per audited M1 defect
(inverted causal/HLC order, missing predecessor via vector, HLC 100/200/50
quarantine-from-0, tx rollback of member conflicts, causally-ordered
same-position non-promotion, partial-duration overlap), driven through the real
epiphany_ops API with explicit negative controls. Wired into acceptance and the
conformance suite. Crate/README docs updated to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e9c4bad7a6
commit
9b0d3e8e2c
|
|
@ -11,12 +11,20 @@ It provides:
|
|||
(`epiphany-determinism`), B (`epiphany-core`), C (`epiphany-ops`), D
|
||||
(`epiphany-bundle`), and E (`epiphany-layout-ir`). Agent B's score-graph
|
||||
generators/shrinkers are re-exported as `generators::graph`.
|
||||
- **The canonical round-trip harness** (`roundtrip`) — v0 acceptance criterion 4.
|
||||
- **The CRDT convergence harness** (`convergence`) — criteria 1 and 5.
|
||||
- **The canonical round-trip harness** (`roundtrip`) — v0 acceptance criterion 4
|
||||
(typed values + bundle container; the bookkeeping `MaterializedState` round-trip
|
||||
is retained as `assert_reduction_serialization_stable`).
|
||||
- **The CRDT convergence harness** (`convergence`) — criteria 1 and 5. Criterion
|
||||
1 proper is **real-Score** convergence through `reduce_onto`
|
||||
(`run_graph_convergence`); the byte-canonical bookkeeping-projection
|
||||
convergence (`assert_convergence`) backs criterion 5.
|
||||
- **The equivocation harness** (`equivocation`) — criterion 3.
|
||||
- **The crash-recovery harness** (`bundle_harness`) — Agent D's gate, criterion 2.
|
||||
- **The manifest-selection harness** (`bundle_harness`).
|
||||
- **The layout round-trip harness** (`layout_stub`) — criterion 6.
|
||||
- **The audit regression guards** (`negative`) — one guard per defect the Agent C
|
||||
framework audit surfaced (the M1 fixes), so a regression trips this suite
|
||||
directly.
|
||||
|
||||
## All harnesses are real
|
||||
|
||||
|
|
@ -43,9 +51,32 @@ signature; the provenance-preservation contract is implemented and tested inside
|
|||
that crate. (The "stub" in the module name now refers to the spec-sanctioned
|
||||
*stub constraint solver*, not to a stubbed crate.)
|
||||
|
||||
## Criterion 1: real-Score vs. reducer-bookkeeping convergence
|
||||
|
||||
Criterion 1 proper (`convergence::run_graph_convergence`, the acceptance
|
||||
`criterion_1_convergence` test) is **real-Score** convergence: a real ~50-bar,
|
||||
two-voice base `epiphany_core::Score` is edited by two replicas through
|
||||
`OperationSet::reduce_onto`, and the entire materialized graph — arena, voices,
|
||||
tombstones, cross-cutting, *and* the bookkeeping state — must be **identical**
|
||||
under every delivery order, pass `check_invariants`, and genuinely grow both
|
||||
edited voices (non-vacuity). The session targets the base's actual voice ids
|
||||
(`generators::graph_edit_session`), so it exercises the integration point, not a
|
||||
synthetic id space.
|
||||
|
||||
The earlier, narrower gate is retained and honestly renamed
|
||||
(`reducer_bookkeeping_convergence`): it converges the byte-canonical
|
||||
**bookkeeping projection** (`OperationSet::reduce` →
|
||||
`MaterializedState::canonical_bytes`) — the Chapter 6 §6.3 ledger (effects,
|
||||
conflicts, anomalies, tombstones, spellings, pending), not the full musical
|
||||
graph. It still backs criterion 5 and proves causal-first ordering
|
||||
(`convergence::assert_causal_order_respected`,
|
||||
`run_authoritative_reduction_gate`). The bookkeeping two-staff scenario remains
|
||||
*instantiated* — a real ~50-bar (`TWO_STAFF_BARS`) session whose staves are
|
||||
asserted populated by `generators::assert_two_staff_populated`, not just modeled.
|
||||
|
||||
## Criterion 4: what is and isn't tested
|
||||
|
||||
Criterion 4 has two qualitatively different tiers:
|
||||
Criterion 4 has three tiers — two asserted now, one pending item 5:
|
||||
|
||||
- **Real decode round-trips** (these catch decoder / canonicalization defects):
|
||||
the generic `CanonicalEncode`/`CanonicalDecode` property swept across every
|
||||
|
|
@ -58,33 +89,31 @@ Criterion 4 has two qualitatively different tiers:
|
|||
(`assert_manifest_decode_rejects_corruption`,
|
||||
`assert_header_decode_rejects_corruption`).
|
||||
|
||||
- **A real score-state decode tier** (`assert_reduction_serialization_stable`): a real
|
||||
`OperationSet` is reduced to its `MaterializedState::canonical_bytes()` — the
|
||||
canonical serialized score state (Chapter 6: *"the materialized graph is a
|
||||
deterministic reduction of the operation set"*) — which is stored as a
|
||||
`Snapshot` chunk referenced by the manifest's `canonical_base` (its correct
|
||||
semantic home), survives the bundle's content-addressed store (hash-verified on
|
||||
reopen), decodes through `MaterializedState::decode_canonical`, compares
|
||||
structurally with the original reduction, and re-serializes byte-identically.
|
||||
The decoder validates nested tags, lengths, primitive values, canonical form,
|
||||
and trailing bytes. Musical sensitivity is proven two ways:
|
||||
- **A reducer-bookkeeping serialization tier** (`reducer_bookkeeping_serialization`,
|
||||
via `assert_reduction_serialization_stable`): a real `OperationSet` is reduced
|
||||
to its `MaterializedState::canonical_bytes()` — the canonical *bookkeeping*
|
||||
state, **not** the whole musical `Score` — which is stored as a `Snapshot`
|
||||
chunk referenced by the manifest's `canonical_base`, survives the bundle's
|
||||
content-addressed store (hash-verified on reopen), decodes through
|
||||
`MaterializedState::decode_canonical`, compares structurally with the original
|
||||
reduction, and re-serializes byte-identically. The decoder validates nested
|
||||
tags, lengths, primitive values, canonical form, and trailing bytes. Musical
|
||||
sensitivity is proven two ways:
|
||||
`assert_content_mutation_changes_serialization` (a cloned operation set with
|
||||
**identical** ids/stamps/causal contexts but one changed payload reduces to
|
||||
*different* bytes — the rebuttal to an id-only serializer) and
|
||||
`assert_distinct_scores_serialize_differently`.
|
||||
`assert_distinct_scores_serialize_differently`. The materialized real `Score`
|
||||
itself is shown reproducible today (`full_score_materialization_is_reproducible`,
|
||||
structural equality across delivery orders) — the determinism precondition a
|
||||
byte codec depends on.
|
||||
|
||||
The convergence harness additionally proves the histories it generates honor the
|
||||
**HLC authoring contract** and that the canonical order is genuinely
|
||||
**causal-first** (`convergence::assert_causal_order_respected`), not merely
|
||||
permutation-invariant. `convergence::run_authoritative_reduction_gate` is the
|
||||
suite's authoritative criterion-1/5 gate: it asserts causal-order correctness
|
||||
*and* reduction determinism over many conformant sets. Agent C's re-exported
|
||||
`ops_reduction_determinism_fuzz` is run in addition; its baseline generator now
|
||||
uses causal HLC authoring and retains explicit anomaly injection. The
|
||||
criterion-1 two-staff scenario is
|
||||
*instantiated* — a real ~50-bar (`TWO_STAFF_BARS`), two-staff editing session
|
||||
whose staves are asserted populated by `generators::assert_two_staff_populated`,
|
||||
not just modeled.
|
||||
- **The full-`Score` byte round-trip is pending item 5 (Agent B).** No
|
||||
whole-score canonical codec (`CanonicalEncode`/`CanonicalDecode for Score`)
|
||||
exists yet, so `criterion_4_full_score_byte_roundtrip` is marked `#[ignore]`
|
||||
(visible as *ignored*, never falsely green) rather than asserted on the
|
||||
bookkeeping projection and passed off as a whole-Score gate. When item 5 lands
|
||||
the codec, drop the attribute and assert the real byte cycle through a bundle
|
||||
snapshot.
|
||||
|
||||
## Decisions (per QUICKSTART "Make each one once and document it")
|
||||
|
||||
|
|
@ -103,10 +132,14 @@ not just modeled.
|
|||
|
||||
Per the QUICKSTART, implementation-discovered gaps are batched, not improvised:
|
||||
|
||||
- **Whole-graph (`epiphany_core::Score`) wire format.** Criterion 4 is now a real
|
||||
decode round-trip at the canonical Chapter-6 `MaterializedState` layer. A
|
||||
separate direct wire format for the richer core `Score` remains owned by the
|
||||
Binary Format companion.
|
||||
- **Whole-graph (`epiphany_core::Score`) wire format — pending item 5 (Agent B).**
|
||||
Criterion 4 is a real decode round-trip at the canonical Chapter-6
|
||||
`MaterializedState` layer; the materialized `Score` is shown *reproducible*
|
||||
today. A direct canonical byte codec for the richer core `Score` does not exist
|
||||
yet (it is item 5's "whole-score codec", to be reconciled with the Binary
|
||||
Format companion), so the whole-`Score` byte round-trip
|
||||
(`criterion_4_full_score_byte_roundtrip`) is an explicit `#[ignore]`'d gate
|
||||
rather than a falsely-green assertion.
|
||||
- **Layout harness re-pointed.** `epiphany-layout-ir` has landed, so `layout_stub`
|
||||
now drives the real IR types behind the same `round_trip` signature (done). IR
|
||||
coordinates are f32 staff spaces, quantized only when serializing canonical
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
//! first violation.
|
||||
|
||||
use epiphany_testkit::{
|
||||
bundle_harness, convergence, equivocation, fixtures, generators, layout_stub, roundtrip, Rng,
|
||||
bundle_harness, convergence, equivocation, fixtures, generators, layout_stub, negative,
|
||||
roundtrip, Rng,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
|
|
@ -37,8 +38,9 @@ fn main() {
|
|||
eprintln!("[1/8] canonical round-trip corpus: {iters} iters");
|
||||
roundtrip::run_roundtrip_corpus(iters, 0x00C0_FFEE_1234_5678);
|
||||
|
||||
// 1b. Bundle manifest + score serialization stability.
|
||||
eprintln!("[1b ] manifest + score serialization stability");
|
||||
// 1b. Bundle manifest + reducer-bookkeeping serialization stability.
|
||||
// (Full-Score *byte* round-trip is pending item 5's whole-score codec.)
|
||||
eprintln!("[1b ] manifest + reducer-bookkeeping serialization stability");
|
||||
for seed in 0..n(64) {
|
||||
roundtrip::assert_manifest_roundtrip(&roundtrip::committed_manifest(seed));
|
||||
let mut rng = Rng::new(seed.wrapping_mul(0x0100_0193).wrapping_add(17));
|
||||
|
|
@ -67,8 +69,12 @@ fn main() {
|
|||
bundle_harness::run_manifest_selection(seed);
|
||||
}
|
||||
|
||||
// 5. Convergence (criterion 1): random sessions + the two-staff scenario.
|
||||
eprintln!("[5/8] convergence across delivery orders");
|
||||
// 5. Convergence (criterion 1): real-Score convergence through reduce_onto,
|
||||
// plus the reducer-bookkeeping projection convergence.
|
||||
eprintln!("[5/8] convergence across delivery orders (real Score + bookkeeping)");
|
||||
for seed in 0..n(64) {
|
||||
convergence::run_graph_convergence(6, seed.wrapping_mul(0x9E37_79B9).wrapping_add(11));
|
||||
}
|
||||
for seed in 0..n(500) {
|
||||
convergence::run_convergence(24, 8, seed.wrapping_mul(0x9E37_79B9));
|
||||
}
|
||||
|
|
@ -76,6 +82,10 @@ fn main() {
|
|||
convergence::run_two_staff_convergence(8, seed.wrapping_mul(0x9E37_79B9).wrapping_add(7));
|
||||
}
|
||||
|
||||
// 5b. Audit regression guards (every defect the Agent C audit surfaced).
|
||||
eprintln!("[5b ] audit defect regression guards");
|
||||
negative::run_all();
|
||||
|
||||
// 6. Reduction determinism (criterion 5): a large set reduced many ways, the
|
||||
// testkit's authoritative causal-order gate, + Agent C's own gate.
|
||||
let big = (1_000 * scale).max(1_000) as usize;
|
||||
|
|
|
|||
|
|
@ -3,18 +3,34 @@
|
|||
//! v0 acceptance criterion 1 (Convergence) and criterion 5 (Reduction
|
||||
//! determinism) — the determinism heart of Chapter 6.
|
||||
//!
|
||||
//! It drives the **real** [`epiphany_ops`] crate (Agent C has shipped): an
|
||||
//! [`OperationSet`] accepts the envelopes, [`OperationSet::reduce`] materializes
|
||||
//! them, and the materialized state's `canonical_bytes` is the artifact compared
|
||||
//! across delivery orders. Because the canonical reduction order
|
||||
//! It drives the **real** [`epiphany_ops`] crate (Agent C has shipped). There
|
||||
//! are two levels of convergence, and this harness asserts both:
|
||||
//!
|
||||
//! * **Real-Score convergence** ([`assert_graph_convergence`],
|
||||
//! [`run_graph_convergence`]) is the headline criterion 1: an edit session is
|
||||
//! reduced onto a real base [`epiphany_core::Score`] via
|
||||
//! [`OperationSet::reduce_onto`], and the entire materialized graph (the
|
||||
//! `Score` *and* its bookkeeping state) must be identical across delivery
|
||||
//! orders, pass `check_invariants`, and genuinely mutate the score.
|
||||
//! * **Reducer-bookkeeping convergence** ([`assert_convergence`],
|
||||
//! [`run_convergence`], [`run_two_staff_convergence`]) compares the canonical
|
||||
//! *bookkeeping projection* — [`OperationSet::reduce`] →
|
||||
//! `MaterializedState::canonical_bytes` — across delivery orders. This is the
|
||||
//! Chapter 6 §6.3 ledger (effects, conflicts, anomalies, tombstones,
|
||||
//! spellings, pending), not the full musical graph; it is retained as the
|
||||
//! byte-canonical determinism gate and the basis of criterion 5.
|
||||
//!
|
||||
//! Because the canonical reduction order
|
||||
//! ([`epiphany_ops::canonical_reduction_order`]) is a function of the operation
|
||||
//! *set* and not of delivery order, every delivery permutation must materialize
|
||||
//! byte-identically. The negative control in the tests proves the harness is not
|
||||
//! vacuous: a reducer that consumed *arrival* order instead would diverge, and
|
||||
//! this harness would catch it.
|
||||
//! identically at both levels. The negative control in the tests proves the
|
||||
//! harness is not vacuous: a reducer that consumed *arrival* order instead would
|
||||
//! diverge, and this harness would catch it.
|
||||
|
||||
use epiphany_core::OperationId;
|
||||
use epiphany_ops::{canonical_reduction_order, OperationEnvelope, OperationSet};
|
||||
use epiphany_core::{check_invariants, OperationId, Score, StaffInstanceId, VoiceId};
|
||||
use epiphany_ops::{
|
||||
canonical_reduction_order, GraphMaterialization, OperationEnvelope, OperationSet,
|
||||
};
|
||||
|
||||
use crate::rng::Rng;
|
||||
|
||||
|
|
@ -165,6 +181,106 @@ pub fn run_authoritative_reduction_gate(sets: usize, orders: usize, seed: u64) {
|
|||
}
|
||||
}
|
||||
|
||||
// === Real-Score convergence (acceptance criterion 1, graph level). ==========
|
||||
|
||||
/// Reduces `envelopes` onto `base` in the given delivery order via the real
|
||||
/// [`OperationSet::reduce_onto`], returning the full graph materialization (the
|
||||
/// `epiphany_core::Score` together with its canonical bookkeeping state).
|
||||
fn materialize_onto_in_order(
|
||||
base: &Score,
|
||||
envelopes: &[OperationEnvelope],
|
||||
) -> GraphMaterialization {
|
||||
let mut set = OperationSet::new();
|
||||
set.accept_all(envelopes.iter().cloned());
|
||||
set.reduce_onto(base)
|
||||
}
|
||||
|
||||
/// The number of events the given voice carries in `score`, or `None` if the
|
||||
/// voice is absent.
|
||||
fn voice_event_count(score: &Score, voice: VoiceId) -> Option<usize> {
|
||||
score
|
||||
.voices()
|
||||
.find_map(|(_, _, v)| (v.id == voice).then_some(v.events.len()))
|
||||
}
|
||||
|
||||
/// **Real-Score convergence (acceptance criterion 1).** Reduces the same
|
||||
/// operation set onto `base` under `orders` independent delivery permutations
|
||||
/// and asserts the entire [`GraphMaterialization`] — the real
|
||||
/// [`epiphany_core::Score`] *and* its bookkeeping state — is identical every
|
||||
/// time. Also asserts the materialized score satisfies every Chapter 5 graph
|
||||
/// invariant ([`check_invariants`]) and that the session is non-vacuous: the
|
||||
/// score actually changed and each targeted voice grew.
|
||||
pub fn assert_graph_convergence(
|
||||
base: &Score,
|
||||
envelopes: &[OperationEnvelope],
|
||||
targets: &[(StaffInstanceId, VoiceId)],
|
||||
orders: usize,
|
||||
rng: &mut Rng,
|
||||
) {
|
||||
let reference = materialize_onto_in_order(base, envelopes);
|
||||
|
||||
// The materialized real Score is structurally valid.
|
||||
let violations = check_invariants(&reference.score);
|
||||
assert!(
|
||||
violations.is_empty(),
|
||||
"materialized score violates graph invariants: {violations:?}"
|
||||
);
|
||||
|
||||
// Non-vacuity: the session genuinely mutated the score, and each targeted
|
||||
// voice grew (the generator inserts only at fresh positions, so no insert is
|
||||
// lost to promotion).
|
||||
assert!(
|
||||
reference.score != *base,
|
||||
"the edit session did not change the base score (vacuous convergence test)"
|
||||
);
|
||||
for &(_, voice) in targets {
|
||||
let before = voice_event_count(base, voice).expect("target voice exists in base");
|
||||
let after =
|
||||
voice_event_count(&reference.score, voice).expect("target voice survives reduction");
|
||||
assert!(
|
||||
after > before,
|
||||
"target voice {voice:?} did not grow under reduction ({before} -> {after})"
|
||||
);
|
||||
}
|
||||
|
||||
for k in 0..orders {
|
||||
let perm = rng.permutation(envelopes.len());
|
||||
let shuffled: Vec<OperationEnvelope> = perm.iter().map(|&i| envelopes[i].clone()).collect();
|
||||
let got = materialize_onto_in_order(base, &shuffled);
|
||||
assert_eq!(
|
||||
reference, got,
|
||||
"delivery permutation #{k} changed the materialized Score \
|
||||
(graph reduction is not order-independent)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects a `valid_score` base with at least two voices (scanning successive
|
||||
/// seeds), so the graph convergence gate genuinely edits two staves.
|
||||
fn two_voice_base(seed: u64) -> Score {
|
||||
let mut s = seed;
|
||||
for _ in 0..64 {
|
||||
let score = epiphany_core::generators::valid_score(s);
|
||||
if score.voices().count() >= 2 {
|
||||
return score;
|
||||
}
|
||||
s = s.wrapping_mul(0x9E37_79B9).wrapping_add(1);
|
||||
}
|
||||
epiphany_core::generators::valid_score(seed)
|
||||
}
|
||||
|
||||
/// **The self-contained real-Score convergence driver (criterion 1).** Builds a
|
||||
/// two-voice base score, authors a real ~50-bar edit session against its actual
|
||||
/// voices ([`crate::generators::graph_edit_session`]), and asserts graph-level
|
||||
/// convergence across `orders` delivery permutations. This is the graph
|
||||
/// counterpart of [`run_two_staff_convergence`] (its reducer-bookkeeping twin).
|
||||
pub fn run_graph_convergence(orders: usize, seed: u64) {
|
||||
let base = two_voice_base(seed);
|
||||
let mut rng = Rng::new(seed ^ 0x67A0_6FAC_E0B0_B0B0);
|
||||
let (targets, envelopes) = crate::generators::graph_edit_session(&base, &mut rng);
|
||||
assert_graph_convergence(&base, &envelopes, &targets, orders, &mut rng);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -190,6 +306,13 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_sessions_converge_on_the_real_score() {
|
||||
for seed in 0..8u64 {
|
||||
run_graph_convergence(4, seed.wrapping_mul(0x9E37_79B9).wrapping_add(7));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_and_singleton_sets_are_trivially_stable() {
|
||||
let mut rng = Rng::new(1);
|
||||
|
|
|
|||
|
|
@ -933,6 +933,101 @@ pub fn assert_two_staff_populated(envelopes: &[OperationEnvelope]) {
|
|||
assert!(live_events(n, 2 * n) > 0, "staff 1 has no live events");
|
||||
}
|
||||
|
||||
/// Whole-note position past which [`graph_edit_session`] inserts, chosen to
|
||||
/// clear `valid_score`'s base content (quarter-note events in `[0, 1)`).
|
||||
const GRAPH_SESSION_OFFSET: i64 = 4;
|
||||
|
||||
/// An `InsertEvent` payload targeting a **real** base voice (`staff_instance`
|
||||
/// must be the voice's actual container), minting `event`/`pitch` under
|
||||
/// [`OBJ_REPLICA`] as the `index`-th half-note past the base content. Unlike
|
||||
/// [`insert_at`], this addresses ids that already exist in a base [`graph`]
|
||||
/// score, so the payload survives [`OperationSet::reduce_onto`] (which rejects
|
||||
/// inserts into unknown voices) rather than only the base-free reducer.
|
||||
fn insert_into(
|
||||
staff_instance: StaffInstanceId,
|
||||
voice: VoiceId,
|
||||
event: u64,
|
||||
pitch: u64,
|
||||
index: u64,
|
||||
) -> OperationPayload {
|
||||
OperationPayload::Primitive(OperationKind::InsertEvent(InsertEventOp {
|
||||
voice,
|
||||
staff_instance,
|
||||
event: obj_event(event),
|
||||
// position = GRAPH_SESSION_OFFSET + index/2 (two half-notes per 4/4 bar).
|
||||
position: MusicalPosition(
|
||||
RationalTime::new(
|
||||
GRAPH_SESSION_OFFSET * EVENTS_PER_BAR as i64 + index as i64,
|
||||
2,
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
duration: MusicalDuration(RationalTime::new(1, 2).unwrap()),
|
||||
pitches: vec![obj_pitch(pitch)],
|
||||
}))
|
||||
}
|
||||
|
||||
/// The graph-level twin of [`two_staff_edit_session`]: a real ~50-bar edit
|
||||
/// session targeting the **actual** voices of `base`, for the
|
||||
/// [`OperationSet::reduce_onto`] convergence gate (acceptance criterion 1).
|
||||
///
|
||||
/// Two replicas alternately insert [`TWO_STAFF_EVENTS_PER_STAFF`] half-note
|
||||
/// events past the base content into the base's first two voices (so both
|
||||
/// staves are genuinely edited), then concurrently respell and delete over the
|
||||
/// shared minted id space, so the canonical reduction — not delivery order —
|
||||
/// decides the materialized `Score`. Returns the targeted voices alongside the
|
||||
/// envelopes (the convergence harness checks each one actually grew).
|
||||
///
|
||||
/// Requires a base with at least one voice; callers pass a base scanned for two
|
||||
/// (see `epiphany_core::generators::valid_score`).
|
||||
pub fn graph_edit_session(
|
||||
base: &epiphany_core::Score,
|
||||
rng: &mut Rng,
|
||||
) -> (Vec<(StaffInstanceId, VoiceId)>, Vec<OperationEnvelope>) {
|
||||
let targets: Vec<(StaffInstanceId, VoiceId)> = base
|
||||
.voices()
|
||||
.map(|(_, instance, voice)| (instance, voice.id))
|
||||
.take(2)
|
||||
.collect();
|
||||
assert!(
|
||||
!targets.is_empty(),
|
||||
"graph_edit_session requires a base score with at least one voice"
|
||||
);
|
||||
|
||||
let n = TWO_STAFF_EVENTS_PER_STAFF;
|
||||
let mut session = Session::new(2);
|
||||
for i in 0..n {
|
||||
for (ti, &(instance, voice)) in targets.iter().enumerate() {
|
||||
// Each target voice's consecutive half-notes alternate authoring
|
||||
// replicas; the two voices are edited by opposite replicas at any
|
||||
// given index. Positions are distinct within a voice (no
|
||||
// same-position collision → clean inserts), while cross-replica
|
||||
// causal sampling makes delivery order differ from reduction order.
|
||||
let r = (i as usize + ti) % 2;
|
||||
let object = ti as u64 * n + i;
|
||||
session.author(rng, r, insert_into(instance, voice, object, object, i));
|
||||
}
|
||||
}
|
||||
|
||||
let total = targets.len() as u64 * n;
|
||||
for _ in 0..80 {
|
||||
let r = rng.below(2) as usize;
|
||||
let payload = if rng.boolean() {
|
||||
OperationPayload::Primitive(OperationKind::DeleteEvent(DeleteEventOp {
|
||||
event: obj_event(rng.below(total)),
|
||||
tuplet_compensation: TupletCompensation::NotInTuplet,
|
||||
}))
|
||||
} else {
|
||||
OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp {
|
||||
pitch: obj_pitch(rng.below(total)),
|
||||
spelling: ContentHash([(rng.below(4) as u8) + 1; 32]),
|
||||
}))
|
||||
};
|
||||
session.author(rng, r, payload);
|
||||
}
|
||||
(targets, session.out)
|
||||
}
|
||||
|
||||
/// A `(base, mutated)` pair of operation sets whose operations have **identical
|
||||
/// identities, stamps, and causal contexts** but differ in payload *content*
|
||||
/// (one respelling's spelling). Both sets insert a pitch and respell it, so the
|
||||
|
|
|
|||
|
|
@ -29,11 +29,20 @@
|
|||
//! manifest-selection harness, driving the real [`epiphany_bundle`] through
|
||||
//! its public API and re-exporting its in-crate gates.
|
||||
//! * [`convergence`] — the CRDT convergence and reduction-determinism harnesses
|
||||
//! (criteria 1 and 5), driving the real [`epiphany_ops::OperationSet`] /
|
||||
//! [`epiphany_ops::canonical_reduction_order`] / reduce, and re-exporting Agent
|
||||
//! C's own determinism gate.
|
||||
//! (criteria 1 and 5). Criterion 1 proper is **real-Score** convergence
|
||||
//! ([`convergence::run_graph_convergence`]): an edit session reduced onto a
|
||||
//! real [`epiphany_core::Score`] via [`epiphany_ops::OperationSet::reduce_onto`]
|
||||
//! must materialize an identical graph under every delivery order. The
|
||||
//! byte-canonical **bookkeeping projection** convergence
|
||||
//! ([`convergence::assert_convergence`], over
|
||||
//! [`epiphany_ops::OperationSet::reduce`] →
|
||||
//! [`epiphany_ops::MaterializedState`]) is retained as a determinism gate and
|
||||
//! the basis of criterion 5. Re-exports Agent C's own determinism gate.
|
||||
//! * [`equivocation`] — the equivocation harness (criterion 3), driving the real
|
||||
//! [`epiphany_ops::OperationSlot`] model and re-exporting Agent C's gate.
|
||||
//! * [`negative`] — regression guards for every defect the Agent C framework
|
||||
//! audit surfaced (the M1 fixes), so a regression in `epiphany-ops` trips this
|
||||
//! suite directly rather than slipping past a generic convergence gate.
|
||||
//!
|
||||
//! * [`layout_stub`] — the layout round-trip harness (criterion 6). Agent E
|
||||
//! (`epiphany-layout-ir`, Chapters 7 & 9) has landed, so this module — once a
|
||||
|
|
@ -65,12 +74,19 @@
|
|||
//!
|
||||
//! | # | Criterion | Entry point |
|
||||
//! |---|-----------|-------------|
|
||||
//! | 1 | Convergence | [`convergence::assert_convergence`] |
|
||||
//! | 1 | Convergence (real Score) | [`convergence::run_graph_convergence`] |
|
||||
//! | 2 | Crash safety | [`bundle_harness::run_crash_recovery`] |
|
||||
//! | 3 | Equivocation | [`equivocation::assert_equivocation_order_independent`] |
|
||||
//! | 4 | Canonical serialization stability | [`roundtrip::run_roundtrip_corpus`] |
|
||||
//! | 4 | Canonical serialization stability (typed + container) | [`roundtrip::run_roundtrip_corpus`] |
|
||||
//! | 5 | Reduction determinism | [`convergence::assert_reduction_determinism`] |
|
||||
//! | 6 | Layout round-trip | [`layout_stub::round_trip`] |
|
||||
//!
|
||||
//! Criterion 1's reducer-bookkeeping counterpart ([`convergence::assert_convergence`])
|
||||
//! and criterion 4's bookkeeping-projection serialization
|
||||
//! ([`roundtrip::assert_reduction_serialization_stable`]) are retained under
|
||||
//! honest names. The full-`Score` **byte** round-trip is pending item 5 (Agent
|
||||
//! B): no whole-score codec exists yet, so that one gate is marked `#[ignore]`
|
||||
//! in `tests/acceptance.rs` rather than asserted falsely.
|
||||
|
||||
pub mod rng;
|
||||
|
||||
|
|
@ -80,6 +96,7 @@ pub mod roundtrip;
|
|||
|
||||
pub mod convergence;
|
||||
pub mod equivocation;
|
||||
pub mod negative;
|
||||
|
||||
pub mod bundle_harness;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,426 @@
|
|||
//! Negative / regression guards for the defects found in the Agent C audit
|
||||
//! (the M1 framework fixes). Item 4 of the v0 follow-up plan charters Agent F's
|
||||
//! suite to independently guard *every* defect the audit surfaced, so a
|
||||
//! regression in `epiphany-ops` trips this tripwire rather than slipping past a
|
||||
//! generic convergence gate.
|
||||
//!
|
||||
//! Each guard drives the **real** [`epiphany_ops`] public API and asserts the
|
||||
//! post-fix behavior. Where the pre-fix bug had a concrete, observable
|
||||
//! signature (an inverted order, an under-cut quarantine, an un-promoted
|
||||
//! collision), the guard also pins the *negative* outcome — the thing the buggy
|
||||
//! reducer would have produced — so the test is provably non-vacuous.
|
||||
//!
|
||||
//! The six audited defects:
|
||||
//!
|
||||
//! 1. A causal predecessor authored with a larger HLC than its successor sorted
|
||||
//! *after* it ([`assert_causal_order_dominates_inverted_hlc`]).
|
||||
//! 2. A missing predecessor expressed through the DVV *vector* (not a dot) was
|
||||
//! not detected ([`assert_missing_vector_predecessor_pends`]).
|
||||
//! 3. An HLC sequence `100, 200, 50` quarantined from counter 1, not the
|
||||
//! counter 0 that also forms a violating pair
|
||||
//! ([`assert_hlc_100_200_50_quarantines_from_zero`]).
|
||||
//! 4. A failed transaction left its members' generated conflicts behind
|
||||
//! ([`assert_failed_transaction_rolls_back_member_conflicts`]).
|
||||
//! 5. A *causally ordered* same-position insert was promoted as if concurrent
|
||||
//! ([`assert_causally_ordered_same_position_not_promoted`]).
|
||||
//! 6. Partial-duration interval overlaps were mishandled
|
||||
//! ([`assert_partial_overlap_promotes_but_adjacent_does_not`]).
|
||||
|
||||
use epiphany_core::{
|
||||
EventId, MusicalDuration, MusicalPosition, OperationId, PitchId, RationalTime, ReplicaId,
|
||||
StaffInstanceId, TransactionId, VoiceId, WallClockTime,
|
||||
};
|
||||
use epiphany_determinism::ContentHash;
|
||||
use epiphany_ops::{
|
||||
canonical_reduction_order, AuthorId, CausalContext, ConflictKind, DeleteEventOp,
|
||||
HybridLogicalClock, InsertEventOp, IntegrityAnomalyKind, NoOpReason, OperationEffect,
|
||||
OperationEnvelope, OperationKind, OperationPayload, OperationSet, OperationStamp,
|
||||
PendingReason, PreconditionFailureReason, RepairKind, RespellPitchOp, TransactionDescriptor,
|
||||
TupletCompensation,
|
||||
};
|
||||
|
||||
/// The replica that owns the synthetic object id space these scenarios edit.
|
||||
const OBJ: ReplicaId = ReplicaId(9);
|
||||
|
||||
fn op(replica: u64, counter: u64) -> OperationId {
|
||||
OperationId::new(ReplicaId(replica), counter)
|
||||
}
|
||||
|
||||
fn envelope(
|
||||
replica: u64,
|
||||
counter: u64,
|
||||
physical: i64,
|
||||
ctx: CausalContext,
|
||||
transaction: Option<TransactionId>,
|
||||
payload: OperationPayload,
|
||||
) -> OperationEnvelope {
|
||||
let id = op(replica, counter);
|
||||
OperationEnvelope {
|
||||
id,
|
||||
author: AuthorId(replica as u128),
|
||||
stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(physical), 0), id),
|
||||
causal_context: ctx,
|
||||
transaction,
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_span(
|
||||
voice: u64,
|
||||
event: u64,
|
||||
position: RationalTime,
|
||||
duration: RationalTime,
|
||||
) -> OperationPayload {
|
||||
OperationPayload::Primitive(OperationKind::InsertEvent(InsertEventOp {
|
||||
voice: VoiceId::new(OBJ, voice),
|
||||
staff_instance: StaffInstanceId::new(OBJ, 0),
|
||||
event: EventId::new(OBJ, event),
|
||||
position: MusicalPosition(position),
|
||||
duration: MusicalDuration(duration),
|
||||
pitches: vec![PitchId::new(OBJ, event)],
|
||||
}))
|
||||
}
|
||||
|
||||
fn insert(voice: u64, event: u64, pos: i64) -> OperationPayload {
|
||||
insert_span(
|
||||
voice,
|
||||
event,
|
||||
RationalTime::from_int(pos as i32),
|
||||
RationalTime::one(),
|
||||
)
|
||||
}
|
||||
|
||||
fn respell(pitch: u64, spelling: u8) -> OperationPayload {
|
||||
OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp {
|
||||
pitch: PitchId::new(OBJ, pitch),
|
||||
spelling: ContentHash([spelling; 32]),
|
||||
}))
|
||||
}
|
||||
|
||||
fn delete_event(event: u64) -> OperationPayload {
|
||||
OperationPayload::Primitive(OperationKind::DeleteEvent(DeleteEventOp {
|
||||
event: EventId::new(OBJ, event),
|
||||
tuplet_compensation: TupletCompensation::NotInTuplet,
|
||||
}))
|
||||
}
|
||||
|
||||
fn declare_tx(replica: u64, counter: u64, physical: i64, tx: TransactionId) -> OperationEnvelope {
|
||||
envelope(
|
||||
replica,
|
||||
counter,
|
||||
physical,
|
||||
CausalContext::new(),
|
||||
None,
|
||||
OperationPayload::Primitive(OperationKind::DeclareTransaction(TransactionDescriptor {
|
||||
id: tx,
|
||||
label: "edit".to_string(),
|
||||
category: None,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn promotes(effect: Option<&OperationEffect>) -> bool {
|
||||
matches!(
|
||||
effect,
|
||||
Some(OperationEffect::AppliedWithRepair { repairs })
|
||||
if repairs.iter().any(|r| matches!(r.kind, RepairKind::VoicePromoted { .. }))
|
||||
)
|
||||
}
|
||||
|
||||
// --- Defect 1: causal order vs. an inverted HLC. ----------------------------
|
||||
|
||||
/// A predecessor carrying a *larger* physical stamp than its dot-linked
|
||||
/// successor must still reduce first. A reducer that trusted the HLC tuple alone
|
||||
/// (the pre-fix bug) would invert the pair; the canonical order must not.
|
||||
pub fn assert_causal_order_dominates_inverted_hlc() {
|
||||
let predecessor = envelope(1, 0, 100, CausalContext::new(), None, insert(0, 100, 0));
|
||||
let successor = envelope(
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
CausalContext::new().with_dot(predecessor.id),
|
||||
None,
|
||||
insert(0, 101, 1),
|
||||
);
|
||||
|
||||
// Negative control: sorting by the HLC tuple alone *does* invert these.
|
||||
let mut by_hlc = [&successor, &predecessor];
|
||||
by_hlc.sort_by_key(|e| e.stamp.reduction_tuple());
|
||||
assert_eq!(
|
||||
by_hlc[0].id, successor.id,
|
||||
"negative control mis-constructed: HLC-only order should place the successor first"
|
||||
);
|
||||
|
||||
let ordered = canonical_reduction_order(&[&successor, &predecessor]);
|
||||
assert_eq!(
|
||||
ordered.iter().map(|e| e.id).collect::<Vec<_>>(),
|
||||
vec![predecessor.id, successor.id],
|
||||
"canonical reduction order must place the causal predecessor first despite its larger HLC"
|
||||
);
|
||||
}
|
||||
|
||||
// --- Defect 2: missing predecessor via the DVV vector. ----------------------
|
||||
|
||||
/// A dependent whose causal context covers a predecessor through the contiguous
|
||||
/// *vector* (`with_seen`), not a dot, must be held pending when that predecessor
|
||||
/// is absent. The pre-fix detector keyed only on dots and let this through.
|
||||
pub fn assert_missing_vector_predecessor_pends() {
|
||||
let present = envelope(1, 0, 1, CausalContext::new(), None, insert(0, 99, -1));
|
||||
let dependent = envelope(
|
||||
2,
|
||||
0,
|
||||
10,
|
||||
CausalContext::new().with_seen(ReplicaId(1), 2),
|
||||
None,
|
||||
insert(0, 100, 0),
|
||||
);
|
||||
let mut set = OperationSet::new();
|
||||
set.accept_all(vec![present.clone(), dependent.clone()]);
|
||||
let state = set.reduce();
|
||||
|
||||
assert_eq!(state.effects.len(), 1, "only the present operation applies");
|
||||
assert_eq!(state.effects[0].0, present.id);
|
||||
assert_eq!(
|
||||
state.pending,
|
||||
vec![(
|
||||
dependent.id,
|
||||
PendingReason::MissingCausalPredecessor { missing: op(1, 1) }
|
||||
)],
|
||||
"the operation must pend on the vector-covered but absent predecessor (1,1)"
|
||||
);
|
||||
}
|
||||
|
||||
// --- Defect 3: HLC 100, 200, 50 quarantines from counter 0. -----------------
|
||||
|
||||
/// The cut must begin at the *earliest* counter participating in any violating
|
||||
/// pair. With stamps `100, 200, 50` the violating pair is (counter 0 = 100,
|
||||
/// counter 2 = 50), so even the monotone counter 0 is quarantined. The pre-fix
|
||||
/// code cut from counter 1 (the first strict decrease) and left counter 0 live.
|
||||
pub fn assert_hlc_100_200_50_quarantines_from_zero() {
|
||||
let a = envelope(1, 0, 100, CausalContext::new(), None, insert(0, 100, 0));
|
||||
let b = envelope(1, 1, 200, CausalContext::new(), None, insert(0, 101, 1));
|
||||
let c = envelope(1, 2, 50, CausalContext::new(), None, insert(0, 102, 2));
|
||||
let mut set = OperationSet::new();
|
||||
set.accept_all(vec![a, b, c]);
|
||||
let state = set.reduce();
|
||||
|
||||
assert!(
|
||||
state.effects.is_empty(),
|
||||
"the entire offending replica stream must be excluded, including counter 0"
|
||||
);
|
||||
assert!(
|
||||
state.anomalies.iter().any(|an| matches!(
|
||||
an.kind,
|
||||
IntegrityAnomalyKind::ReplicaStreamQuarantined {
|
||||
replica,
|
||||
first_bad_counter,
|
||||
..
|
||||
} if replica == ReplicaId(1) && first_bad_counter == 0
|
||||
)),
|
||||
"the quarantine anomaly must report first_bad_counter == 0"
|
||||
);
|
||||
}
|
||||
|
||||
// --- Defect 4: failed transaction rolls back member-generated conflicts. -----
|
||||
|
||||
/// A transaction whose members would generate conflicts but whose block fails
|
||||
/// must leave *no* member-generated conflict behind — only the wholesale
|
||||
/// transaction conflict. The pre-fix snapshot did not restore the conflict
|
||||
/// registry, so the member's structural-collision conflict survived.
|
||||
pub fn assert_failed_transaction_rolls_back_member_conflicts() {
|
||||
let seed = envelope(2, 0, 1, CausalContext::new(), None, insert(0, 100, 0));
|
||||
let initial_spelling = envelope(
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
CausalContext::new().with_seen(ReplicaId(2), 0),
|
||||
None,
|
||||
respell(100, 1),
|
||||
);
|
||||
let tx = TransactionId::from_raw(89);
|
||||
let descriptor = declare_tx(1, 0, 10, tx);
|
||||
let tx_ctx = CausalContext::new().with_seen(ReplicaId(1), 0);
|
||||
let conflicting = envelope(1, 1, 11, tx_ctx.clone(), Some(tx), respell(100, 2));
|
||||
let failing = envelope(1, 2, 12, tx_ctx, Some(tx), delete_event(999));
|
||||
|
||||
let mut set = OperationSet::new();
|
||||
set.accept_all(vec![
|
||||
seed,
|
||||
initial_spelling,
|
||||
descriptor,
|
||||
conflicting,
|
||||
failing,
|
||||
]);
|
||||
let state = set.reduce();
|
||||
|
||||
assert_eq!(
|
||||
state.spellings.get(&PitchId::new(OBJ, 100)),
|
||||
Some(&ContentHash([1; 32])),
|
||||
"the pre-transaction spelling must survive the rollback"
|
||||
);
|
||||
assert_eq!(
|
||||
state.conflicts.records().len(),
|
||||
1,
|
||||
"only the wholesale transaction conflict should remain"
|
||||
);
|
||||
assert!(matches!(
|
||||
state.conflicts.records()[0].kind,
|
||||
ConflictKind::TransactionConflict { .. }
|
||||
));
|
||||
}
|
||||
|
||||
// --- Defect 5: causally ordered same-position insert is not promoted. --------
|
||||
|
||||
/// A same-position insert that *causally follows* the first (so it is not
|
||||
/// concurrent) must be a precondition failure, never a voice promotion.
|
||||
/// Promotion is for concurrent collisions only.
|
||||
pub fn assert_causally_ordered_same_position_not_promoted() {
|
||||
let first = envelope(1, 0, 10, CausalContext::new(), None, insert(0, 100, 0));
|
||||
let second = envelope(
|
||||
1,
|
||||
1,
|
||||
11,
|
||||
CausalContext::new().with_seen(ReplicaId(1), 0),
|
||||
None,
|
||||
insert(0, 101, 0),
|
||||
);
|
||||
let mut set = OperationSet::new();
|
||||
set.accept_all(vec![first, second.clone()]);
|
||||
let state = set.reduce();
|
||||
|
||||
let effect = state
|
||||
.effects
|
||||
.iter()
|
||||
.find(|(id, _)| *id == second.id)
|
||||
.map(|(_, e)| e);
|
||||
assert_eq!(
|
||||
effect,
|
||||
Some(&OperationEffect::NoOp {
|
||||
reason: NoOpReason::PreconditionFailedUnderReduction {
|
||||
reason: PreconditionFailureReason::EventDurationInvalid,
|
||||
},
|
||||
}),
|
||||
"a causally-ordered same-position insert must fail its precondition"
|
||||
);
|
||||
assert!(
|
||||
!promotes(effect),
|
||||
"a causally-ordered insert must not be promoted as if concurrent"
|
||||
);
|
||||
}
|
||||
|
||||
// --- Defect 6: partial-duration overlaps. -----------------------------------
|
||||
|
||||
/// Concurrent inserts whose intervals *partially* overlap collide (and promote
|
||||
/// the greater id), while concurrent inserts at *adjacent* half-open intervals
|
||||
/// do not. The pre-fix overlap test only compared start positions.
|
||||
pub fn assert_partial_overlap_promotes_but_adjacent_does_not() {
|
||||
// Partial overlap: [0, 1) and [1/2, 3/2).
|
||||
let first = envelope(
|
||||
1,
|
||||
0,
|
||||
10,
|
||||
CausalContext::new(),
|
||||
None,
|
||||
insert_span(0, 100, RationalTime::zero(), RationalTime::one()),
|
||||
);
|
||||
let second = envelope(
|
||||
2,
|
||||
0,
|
||||
10,
|
||||
CausalContext::new(),
|
||||
None,
|
||||
insert_span(
|
||||
0,
|
||||
200,
|
||||
RationalTime::new(1, 2).unwrap(),
|
||||
RationalTime::one(),
|
||||
),
|
||||
);
|
||||
let mut set = OperationSet::new();
|
||||
set.accept_all(vec![first, second.clone()]);
|
||||
let state = set.reduce();
|
||||
assert!(
|
||||
promotes(
|
||||
state
|
||||
.effects
|
||||
.iter()
|
||||
.find(|(id, _)| *id == second.id)
|
||||
.map(|(_, e)| e)
|
||||
),
|
||||
"a concurrent partial-interval overlap must promote the greater id"
|
||||
);
|
||||
|
||||
// Adjacent half-open intervals: [0, 1) and [1, 2) — no collision.
|
||||
let a = envelope(
|
||||
1,
|
||||
0,
|
||||
10,
|
||||
CausalContext::new(),
|
||||
None,
|
||||
insert_span(0, 100, RationalTime::zero(), RationalTime::one()),
|
||||
);
|
||||
let b = envelope(
|
||||
2,
|
||||
0,
|
||||
10,
|
||||
CausalContext::new(),
|
||||
None,
|
||||
insert_span(0, 200, RationalTime::one(), RationalTime::one()),
|
||||
);
|
||||
let mut adjacent = OperationSet::new();
|
||||
adjacent.accept_all(vec![a, b]);
|
||||
let adjacent_state = adjacent.reduce();
|
||||
assert!(
|
||||
adjacent_state
|
||||
.effects
|
||||
.iter()
|
||||
.all(|(_, e)| *e == OperationEffect::Applied),
|
||||
"adjacent half-open intervals must not collide"
|
||||
);
|
||||
}
|
||||
|
||||
/// Runs every audited-defect regression guard. The acceptance suite calls this
|
||||
/// as a single entry point (criterion-adjacent: the audit tripwire).
|
||||
pub fn run_all() {
|
||||
assert_causal_order_dominates_inverted_hlc();
|
||||
assert_missing_vector_predecessor_pends();
|
||||
assert_hlc_100_200_50_quarantines_from_zero();
|
||||
assert_failed_transaction_rolls_back_member_conflicts();
|
||||
assert_causally_ordered_same_position_not_promoted();
|
||||
assert_partial_overlap_promotes_but_adjacent_does_not();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn causal_order_dominates_inverted_hlc() {
|
||||
assert_causal_order_dominates_inverted_hlc();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_vector_predecessor_pends() {
|
||||
assert_missing_vector_predecessor_pends();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hlc_100_200_50_quarantines_from_zero() {
|
||||
assert_hlc_100_200_50_quarantines_from_zero();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_transaction_rolls_back_member_conflicts() {
|
||||
assert_failed_transaction_rolls_back_member_conflicts();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn causally_ordered_same_position_not_promoted() {
|
||||
assert_causally_ordered_same_position_not_promoted();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_overlap_promotes_but_adjacent_does_not() {
|
||||
assert_partial_overlap_promotes_but_adjacent_does_not();
|
||||
}
|
||||
}
|
||||
|
|
@ -9,15 +9,34 @@
|
|||
//! module (Agent E has landed). See the crate docs for the harness policy.
|
||||
|
||||
use epiphany_testkit::{
|
||||
bundle_harness, convergence, equivocation, fixtures, generators, layout_stub, roundtrip, Rng,
|
||||
bundle_harness, convergence, equivocation, fixtures, generators, layout_stub, negative,
|
||||
roundtrip, Rng,
|
||||
};
|
||||
|
||||
/// Criterion 1 — **Convergence.** Overlapping edits to a (modeled) 50-bar,
|
||||
/// two-staff score by two replicas converge to byte-identical materialized state
|
||||
/// regardless of envelope delivery order (Chapter 6's canonical reduction).
|
||||
/// Criterion 1 — **Convergence (real Score).** Overlapping edits to a real
|
||||
/// ~50-bar, two-voice base [`epiphany_core::Score`] by two replicas converge to
|
||||
/// an *identical materialized Score* — the real graph (arena, voices,
|
||||
/// tombstones, cross-cutting) together with its bookkeeping state — regardless
|
||||
/// of envelope delivery order, with every Chapter 5 invariant intact. Driven
|
||||
/// through Agent C's `OperationSet::reduce_onto`. This is the headline criterion;
|
||||
/// [`reducer_bookkeeping_convergence`] is its ledger-projection counterpart.
|
||||
#[test]
|
||||
fn criterion_1_convergence() {
|
||||
// The criterion's named shape: a two-staff, overlapping-edit session.
|
||||
for seed in 0..24u64 {
|
||||
convergence::run_graph_convergence(6, seed.wrapping_mul(0x9E37_79B9).wrapping_add(11));
|
||||
}
|
||||
}
|
||||
|
||||
/// **Reducer-bookkeeping convergence** (the former, weaker half of criterion 1,
|
||||
/// retained and honestly renamed). The canonical Chapter 6 §6.3 *bookkeeping
|
||||
/// projection* — `OperationSet::reduce` → `MaterializedState::canonical_bytes` —
|
||||
/// is byte-identical across delivery orders. That projection is the ledger
|
||||
/// (effects, conflicts, anomalies, tombstones, spellings, pending), **not** the
|
||||
/// full musical graph; real-Score convergence is `criterion_1_convergence`.
|
||||
/// Kept as a fast byte-level determinism gate (and the basis of criterion 5).
|
||||
#[test]
|
||||
fn reducer_bookkeeping_convergence() {
|
||||
// The named shape: a two-staff, overlapping-edit session (bookkeeping bytes).
|
||||
for seed in 0..16u64 {
|
||||
convergence::run_two_staff_convergence(8, seed.wrapping_mul(0x9E37_79B9).wrapping_add(11));
|
||||
}
|
||||
|
|
@ -27,6 +46,14 @@ fn criterion_1_convergence() {
|
|||
}
|
||||
}
|
||||
|
||||
/// **Audit regression guards.** Every defect the Agent C framework audit
|
||||
/// surfaced (the M1 fixes) is independently re-asserted here so a regression in
|
||||
/// `epiphany-ops` trips Agent F's suite directly. See [`negative`].
|
||||
#[test]
|
||||
fn audit_defect_regressions() {
|
||||
negative::run_all();
|
||||
}
|
||||
|
||||
/// Criterion 2 — **Crash safety.** A crash between any two syscalls in the
|
||||
/// commit path leaves the bundle openable — possibly at the previous
|
||||
/// generation, never corrupt (Chapter 8's atomic commit). Runs the testkit's
|
||||
|
|
@ -60,11 +87,18 @@ fn criterion_3_equivocation() {
|
|||
equivocation::ops_equivocation_fuzz(2_000, 0x1234_5678);
|
||||
}
|
||||
|
||||
/// Criterion 4 — **Canonical serialization stability.** The same canonical
|
||||
/// state serialized → loaded → re-serialized produces byte-identical bytes
|
||||
/// (Appendix D's canonical-serialization layer): the type-level round-trip
|
||||
/// corpus, Agent A's determinism gate (a slice), the bundle manifest/header, and
|
||||
/// a real score (operation-set → materialized bytes) round-trip through a bundle.
|
||||
/// Criterion 4 — **Canonical serialization stability (typed + container).** The
|
||||
/// same canonical value serialized → loaded → re-serialized produces
|
||||
/// byte-identical bytes (Appendix D's canonical-serialization layer): the
|
||||
/// type-level round-trip corpus over every `CanonicalEncode` type in Agents A
|
||||
/// and B, Agent A's determinism gate (a slice), and the real bundle
|
||||
/// manifest/header — including decoder rejection of corruption.
|
||||
///
|
||||
/// The **full-Score** byte round-trip is split out below: its bookkeeping
|
||||
/// projection ([`reducer_bookkeeping_serialization`]) and its reproducibility
|
||||
/// ([`full_score_materialization_is_reproducible`]) are exercised now; the
|
||||
/// whole-`Score` byte codec is pending item 5 (Agent B) —
|
||||
/// [`criterion_4_full_score_byte_roundtrip`].
|
||||
#[test]
|
||||
fn criterion_4_canonical_serialization_stability() {
|
||||
roundtrip::run_roundtrip_corpus(100_000, 0x00C0_FFEE_1234_5678);
|
||||
|
|
@ -81,7 +115,20 @@ fn criterion_4_canonical_serialization_stability() {
|
|||
roundtrip::assert_manifest_roundtrip(&rich);
|
||||
// The decoder actually validates: corruption is rejected.
|
||||
roundtrip::assert_manifest_decode_rejects_corruption(&rich);
|
||||
}
|
||||
}
|
||||
|
||||
/// **Reducer-bookkeeping serialization** (the former, narrower half of criterion
|
||||
/// 4, retained and honestly renamed). The canonical *bookkeeping projection*
|
||||
/// (`MaterializedState::canonical_bytes`) survives content-addressed storage in
|
||||
/// a real bundle and re-serializes byte-identically, and is musically sensitive
|
||||
/// (same identities + changed content → different bytes). This is the Chapter 6
|
||||
/// ledger, **not** the whole musical `Score`; the full-Score byte round-trip is
|
||||
/// [`criterion_4_full_score_byte_roundtrip`] (pending item 5).
|
||||
#[test]
|
||||
fn reducer_bookkeeping_serialization() {
|
||||
for seed in 0..48u64 {
|
||||
let mut rng = Rng::new(seed.wrapping_mul(0x0100_0193).wrapping_add(17));
|
||||
// The reduced canonical state survives content-addressed storage, and is
|
||||
// musically sensitive: same identities + changed content → different bytes.
|
||||
let session = generators::operation_envelopes(&mut rng, 40, 3, 6, 6);
|
||||
|
|
@ -93,6 +140,37 @@ fn criterion_4_canonical_serialization_stability() {
|
|||
roundtrip::assert_content_mutation_changes_serialization();
|
||||
}
|
||||
|
||||
/// **Full-Score materialization reproducibility** (achievable without the byte
|
||||
/// codec). Reducing the same edit session onto the same base `Score` twice —
|
||||
/// once in authored order, once shuffled — yields the *identical* materialized
|
||||
/// `epiphany_core::Score` and bookkeeping state. This is the determinism
|
||||
/// precondition any future whole-Score byte codec depends on, asserted today via
|
||||
/// structural equality of the real graph (`reduce_onto`).
|
||||
#[test]
|
||||
fn full_score_materialization_is_reproducible() {
|
||||
for seed in 0..16u64 {
|
||||
convergence::run_graph_convergence(4, seed.wrapping_mul(0x0100_0193).wrapping_add(23));
|
||||
}
|
||||
}
|
||||
|
||||
/// Criterion 4 (full-Score byte round-trip) — **pending item 5 (Agent B).** A
|
||||
/// whole-`epiphany_core::Score` / `GraphMaterialization` `encode → decode →
|
||||
/// re-encode` byte round-trip requires the whole-score canonical codec
|
||||
/// (`CanonicalEncode`/`CanonicalDecode for Score`), which does not exist yet:
|
||||
/// today only the bookkeeping `MaterializedState` and the A/B typed values have
|
||||
/// codecs. This gate is intentionally `#[ignore]`'d (visible as *ignored*, never
|
||||
/// falsely green) until item 5 lands the codec; then drop the attribute and
|
||||
/// assert the real byte cycle through a bundle snapshot.
|
||||
#[test]
|
||||
#[ignore = "pending item 5 (Agent B): whole-score codec (CanonicalEncode/Decode for Score) does not exist yet"]
|
||||
fn criterion_4_full_score_byte_roundtrip() {
|
||||
unimplemented!(
|
||||
"blocked on item 5: epiphany_core::Score has no canonical byte codec. \
|
||||
When it lands, reduce_onto a base, encode the Score, decode, and assert \
|
||||
a byte-identical re-encode through a real bundle snapshot."
|
||||
);
|
||||
}
|
||||
|
||||
/// Criterion 5 — **Reduction determinism.** A randomized 1,000-envelope set,
|
||||
/// reduced 10 times in 10 different orders, produces byte-identical materialized
|
||||
/// states *and* an identical canonical reduction order (Appendix D's
|
||||
|
|
|
|||
Loading…
Reference in New Issue