131 lines
5.5 KiB
Rust
131 lines
5.5 KiB
Rust
//! Agent K's merge gate: the v0 → v1 operation-payload migration is
|
|
//! **deterministic** and **equivalence-preserving** (QUICKSTART Agent K
|
|
//! acceptance: "v0 envelopes migrated to v1 reduce to byte-identical canonical
|
|
//! state as v0 envelopes did under v0 payloads").
|
|
//!
|
|
//! The v0 wire shape is reconstructed by [`project_v1_to_v0`] (the total inverse
|
|
//! direction), so the gate can drive the migration without a hand-built corpus
|
|
//! of historical v0 bytes:
|
|
//!
|
|
//! ```text
|
|
//! v1 corpus ──project──▶ v0 ──migrate(context)──▶ v1'
|
|
//! assert reduce(v1') == reduce(v1) (equivalence-preserving)
|
|
//! assert v1' == v1 (migration inverts projection)
|
|
//! assert migrate twice is byte-identical (deterministic)
|
|
//! ```
|
|
//!
|
|
//! and a **non-vacuity** guard proves the equivalence assertion is not trivially
|
|
//! satisfiable: the canonical reduction state is sensitive to a respell's
|
|
//! spelling content, so a migration that recovered the *wrong* spelling would be
|
|
//! caught.
|
|
|
|
use epiphany_core::{IdentityContext, ReplicaId, Score};
|
|
use epiphany_ops::{
|
|
migrate_v0_envelope, project_v1_to_v0, valuegen, OperationEnvelope, OperationKind,
|
|
OperationPayload, OperationSet,
|
|
};
|
|
|
|
use crate::generators::{content_mutation_pair, operation_envelopes};
|
|
use crate::rng::Rng;
|
|
|
|
/// A context [`Score`] carrying an explicit per-pitch spelling attachment for
|
|
/// every `RespellPitch` in the corpus. A respell's spelling is the one payload a
|
|
/// v0 projection cannot reconstruct from itself (it kept only a fingerprint); the
|
|
/// migration recovers it from exactly these attachments (P12-K1).
|
|
fn migration_context(corpus: &[OperationEnvelope]) -> Score {
|
|
let mut score = Score::empty(IdentityContext::new(ReplicaId(1)));
|
|
for env in corpus {
|
|
if let OperationPayload::Primitive(OperationKind::RespellPitch(op)) = &env.payload {
|
|
score
|
|
.spelling_attachments
|
|
.push(valuegen::explicit_spelling_attachment(
|
|
op.pitch,
|
|
op.spelling.clone(),
|
|
));
|
|
}
|
|
}
|
|
score
|
|
}
|
|
|
|
fn reduce_bytes(envs: &[OperationEnvelope]) -> Vec<u8> {
|
|
let mut set = OperationSet::new();
|
|
set.accept_all(envs.iter().cloned());
|
|
set.reduce().canonical_bytes()
|
|
}
|
|
|
|
fn migrate_all(v1: &[OperationEnvelope], ctx: &Score) -> Vec<OperationEnvelope> {
|
|
v1.iter()
|
|
.map(|env| {
|
|
migrate_v0_envelope(project_v1_to_v0(env), ctx)
|
|
.expect("the representative corpus migrates without irreversible payloads")
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// The migration equivalence + determinism gate over an `n_ops` random corpus.
|
|
pub fn run_migration_equivalence(n_ops: usize, seed: u64) {
|
|
let mut rng = Rng::new(seed);
|
|
let v1 = operation_envelopes(&mut rng, n_ops, 3, 6, 6);
|
|
let ctx = migration_context(&v1);
|
|
|
|
let migrated = migrate_all(&v1, &ctx);
|
|
|
|
// Equivalence-preserving: identical canonical reduction state.
|
|
assert_eq!(
|
|
reduce_bytes(&v1),
|
|
reduce_bytes(&migrated),
|
|
"v0->v1 migration changed the canonical reduction state (seed {seed})"
|
|
);
|
|
|
|
// Round-trip self-consistency over *this* corpus: because the corpus is
|
|
// built from the same `valuegen` helpers the migration reconstructs values
|
|
// with, `migrate(project(env)) == env` holds exactly here. This is a sharper
|
|
// check than reduction-equivalence for the corpus, but it is NOT a universal
|
|
// inverse: the v0 projection drops fields the migration cannot recover (e.g.
|
|
// a `ReplaceWithRest` rest's own voice — recovered from the deleted event's
|
|
// placement at reduction, not from the value). The spec-level property is the
|
|
// reduction-equivalence asserted above; widening `operation_envelopes` to emit
|
|
// such a payload would surface here and is M2's cue to revisit faithfulness.
|
|
assert_eq!(
|
|
v1, migrated,
|
|
"migration round-trip diverged from the original on this corpus (seed {seed})"
|
|
);
|
|
|
|
// Deterministic: migrating the same projection against the same context
|
|
// twice yields byte-identical envelopes.
|
|
let again = migrate_all(&v1, &ctx);
|
|
assert_eq!(
|
|
migrated, again,
|
|
"migration is not deterministic (seed {seed})"
|
|
);
|
|
}
|
|
|
|
/// Non-vacuity guard: prove the equivalence gate would *fail* if the migration
|
|
/// were wrong. A respell's spelling genuinely affects the canonical reduction
|
|
/// state, so a migration recovering a different spelling reduces to different
|
|
/// bytes — which the equivalence assertion above would catch.
|
|
pub fn assert_migration_gate_is_not_vacuous() {
|
|
// `base` inserts a pitch and respells it; `mutated` is identical except the
|
|
// respelling's spelling differs.
|
|
let (base, mutated) = content_mutation_pair();
|
|
|
|
// The migration round-trip on `base` is equivalence-preserving.
|
|
let ctx = migration_context(&base);
|
|
let migrated = migrate_all(&base, &ctx);
|
|
assert_eq!(
|
|
reduce_bytes(&base),
|
|
reduce_bytes(&migrated),
|
|
"migration of the controlled corpus is not equivalence-preserving"
|
|
);
|
|
|
|
// But the reduction is sensitive to the respelling's content: `base` and
|
|
// `mutated` reduce to *different* bytes. Had the migration recovered the
|
|
// wrong spelling, the equivalence assertion would have diverged just like
|
|
// this — so the gate is discriminating, not vacuous.
|
|
assert_ne!(
|
|
reduce_bytes(&base),
|
|
reduce_bytes(&mutated),
|
|
"non-vacuity control is mis-constructed: differing spellings must reduce differently"
|
|
);
|
|
}
|