epiphany/crates/epiphany-testkit/src/gminor.rs

350 lines
13 KiB
Rust

//! The `[7f]` conformance gate (G-minor, `spec/PLAN_GMINOR_SCHEMA_MINOR.md`
//! §4, pin 11): an independent oracle over the manifest's carried schema
//! minor, built from **known, decodable, in-tree artifacts**.
//!
//! `epiphany-textproj` deliberately never decodes edit-barrier bytes (pin 8:
//! it has no `epiphany-layout-ir` dependency), so it cannot itself check
//! whether a manifest's carried `SchemaVersion` matches what its declared
//! edit barriers require. This module is the independent check that *can*:
//! `epiphany-testkit` depends on `epiphany-layout-ir`, so it can decode
//! `ExtensionDeclaration::edit_barriers`, walk every barrier's
//! `prohibited_operation_kinds`, and recompute the exact aggregate minor the
//! manifest should carry (`epiphany_layout_ir::barrier::edit_barriers_introduced_minor`).
//!
//! **What this gate is not.** It is not evidence that `epiphany-textproj`
//! validates arbitrary hand-edited documents — pin 11's ruled design (a) is
//! that `textproj` stays a *preserving* producer, carrying whatever
//! `SchemaVersion` a document declares verbatim, and a hand-edited document
//! whose barrier bytes changed while its carried version did not is
//! undetectable at that layer by construction. This gate validates a
//! separate, narrower claim: that *this crate's own fixtures*, built directly
//! against `epiphany-bundle`, are exactly and correctly stamped. An
//! undecodable barrier blob (a foreign extension's bytes, a corrupt encoding)
//! is reported as **not-checkable**, never silently counted as a pass.
use epiphany_bundle::{
Bundle, DocumentId, ExtensionDeclaration, ExtensionId, FileUuid, Manifest, MemStore,
SchemaVersion, SemVer,
};
use epiphany_layout_ir::{
decode_edit_barriers, edit_barriers_introduced_minor, encode_edit_barriers, BarrierCondition,
BarrierScope, EditBarrier,
};
use epiphany_ops::OperationKindTag;
/// The oracle's verdict for one manifest.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GminorVerdict {
/// Every barrier blob decoded; `expected` is the recomputed aggregate,
/// `actual` is the carried superblock value, and `matches` is whether
/// they are **exactly equal** — not `>=` (pin 11.1: equality alone
/// catches over-stamping after a contributing barrier is removed).
Checked {
expected: SchemaVersion,
actual: SchemaVersion,
matches: bool,
},
/// At least one barrier blob failed to decode (a foreign extension, or
/// deliberately corrupt bytes). The exact epoch cannot be established, so
/// this is reported as not-checkable — **never** as a pass (pin 11.3).
NotCheckable(String),
}
/// A single edit barrier naming `tags`, with an otherwise-trivial
/// whole-score, unconditional scope — the minimum shape needed to exercise
/// `prohibited_operation_kinds`.
fn barrier(tags: &[OperationKindTag]) -> EditBarrier {
EditBarrier {
scope: BarrierScope::WholeScore,
affected_object_kinds: Vec::new(),
prohibited_operation_kinds: tags.to_vec(),
condition: BarrierCondition::Always,
}
}
fn extension_declaration(id_byte: u8, barriers: &[EditBarrier]) -> ExtensionDeclaration {
ExtensionDeclaration {
extension_id: ExtensionId([id_byte; 16]),
version: SemVer::new(1, 0, 0),
required: false,
preserved_chunk_roots: Vec::new(),
affected_object_kinds: Vec::new(),
edit_barriers: encode_edit_barriers(barriers),
}
}
/// Builds a bundle whose manifest declares `extensions` and is stamped at
/// `stamped_version` — the two independent knobs every fixture below varies.
fn build_bundle(
seed: u8,
extensions: Vec<ExtensionDeclaration>,
stamped_version: SchemaVersion,
) -> Bundle<MemStore> {
let mut manifest = Manifest::empty(DocumentId([seed; 16]));
manifest.extension_declarations = extensions;
Bundle::create_versioned(
MemStore::new(),
FileUuid([seed; 16]),
manifest,
stamped_version,
crate::production_caps(),
)
.expect("fixture manifest is emittable")
}
/// The independent oracle (pin 11): recomputes the exact aggregate minor a
/// bundle's manifest should carry from its decodable edit barriers, and
/// compares it **by equality** to the carried superblock value.
pub fn check(bundle: &Bundle<MemStore>) -> GminorVerdict {
let mut all_barriers = Vec::new();
for declaration in &bundle.manifest().extension_declarations {
match decode_edit_barriers(&declaration.edit_barriers) {
Ok(mut decoded) => all_barriers.append(&mut decoded),
Err(error) => {
return GminorVerdict::NotCheckable(format!(
"extension {:?}: edit_barriers did not decode: {error:?}",
declaration.extension_id
))
}
}
}
let epoch_max = edit_barriers_introduced_minor(&all_barriers);
let expected = SchemaVersion::for_major_at_epoch(0, epoch_max);
let actual = bundle.superblock().manifest_schema_version;
GminorVerdict::Checked {
expected,
actual,
matches: expected == actual,
}
}
/// Fixture: a manifest naming barrier tag 31 (`CreateInstrument`, epoch 8),
/// correctly stamped at exactly that epoch. Positive: the oracle must find
/// `matches: true`.
pub fn fixture_correctly_stamped_tag_31() -> Bundle<MemStore> {
let declarations = vec![extension_declaration(
1,
&[barrier(&[OperationKindTag::CreateInstrument])],
)];
build_bundle(1, declarations, SchemaVersion::new(0, 8))
}
/// Fixture: a manifest naming only baseline tags, correctly stamped at the
/// baseline `{0, 1}`. Positive.
pub fn fixture_baseline_only() -> Bundle<MemStore> {
let declarations = vec![extension_declaration(
2,
&[barrier(&[
OperationKindTag::InsertEvent,
OperationKindTag::DeleteEvent,
])],
)];
build_bundle(2, declarations, SchemaVersion::V0)
}
/// Negative fixture (pin 11.2, required #1): a blob naming tag 31 (epoch 8),
/// but the manifest carries the **baseline** version — under-stamped. The
/// oracle must find `matches: false`.
pub fn fixture_understamped() -> Bundle<MemStore> {
let declarations = vec![extension_declaration(
3,
&[barrier(&[OperationKindTag::CreateInstrument])],
)];
build_bundle(3, declarations, SchemaVersion::V0)
}
/// Negative fixture (pin 11.2, required #2): two barriers, one naming tag 31
/// (epoch 8, the sole max contributor) and one baseline-only. The barrier
/// contributing the maximum is then **removed** (only the baseline barrier
/// remains), but the manifest retains the old aggregate `{0, 8}` — exactly
/// the over-stamp pin 6.5 warns "blindly retaining the previous aggregate"
/// produces. The oracle must find `matches: false`.
pub fn fixture_overstamped_after_barrier_removal() -> Bundle<MemStore> {
let declarations = vec![extension_declaration(
4,
&[barrier(&[OperationKindTag::InsertEvent])],
)];
build_bundle(4, declarations, SchemaVersion::new(0, 8))
}
/// Fixture: a manifest declaring an extension whose `edit_barriers` blob is
/// deliberately corrupt (not a valid canonical `EditBarrier` set encoding).
/// The oracle must report `NotCheckable`, never a pass (pin 11.3).
pub fn fixture_undecodable_barrier_blob() -> Bundle<MemStore> {
let declaration = ExtensionDeclaration {
extension_id: ExtensionId([5; 16]),
version: SemVer::new(1, 0, 0),
required: false,
preserved_chunk_roots: Vec::new(),
affected_object_kinds: Vec::new(),
// Not a canonical edit-barrier-set encoding: garbage bytes that a
// real foreign/corrupt extension could plausibly carry.
edit_barriers: vec![0xFF, 0x00, 0x13, 0x37, 0xAB],
};
build_bundle(5, vec![declaration], SchemaVersion::V0)
}
/// Runs the whole `[7f]` gate: every fixture, checked against its expected
/// verdict shape. Returns `(checked, not_checkable)` — the two counts the
/// conformance suite reports (pin 11.4: describing the gate's actual reach
/// honestly, not implying it validates arbitrary edits).
pub fn run_gate() -> (usize, usize) {
let mut checked = 0usize;
let mut not_checkable = 0usize;
let positive_correct = check(&fixture_correctly_stamped_tag_31());
assert_eq!(
positive_correct,
GminorVerdict::Checked {
expected: SchemaVersion::new(0, 8),
actual: SchemaVersion::new(0, 8),
matches: true,
},
"a manifest naming tag 31, correctly stamped at minor 8, must check as matching"
);
checked += 1;
let positive_baseline = check(&fixture_baseline_only());
assert_eq!(
positive_baseline,
GminorVerdict::Checked {
expected: SchemaVersion::V0,
actual: SchemaVersion::V0,
matches: true,
},
"a manifest naming only baseline tags must check as matching its baseline stamp"
);
checked += 1;
let negative_understamped = check(&fixture_understamped());
assert_eq!(
negative_understamped,
GminorVerdict::Checked {
expected: SchemaVersion::new(0, 8),
actual: SchemaVersion::V0,
matches: false,
},
"an under-stamped manifest (tag 31 present, baseline carried) must be caught"
);
checked += 1;
let negative_overstamped = check(&fixture_overstamped_after_barrier_removal());
assert_eq!(
negative_overstamped,
GminorVerdict::Checked {
expected: SchemaVersion::V0,
actual: SchemaVersion::new(0, 8),
matches: false,
},
"an over-stamped manifest (max contributor removed, old aggregate retained) must be caught"
);
checked += 1;
match check(&fixture_undecodable_barrier_blob()) {
GminorVerdict::NotCheckable(_) => not_checkable += 1,
other => panic!("an undecodable barrier blob must report NotCheckable, got {other:?}"),
}
(checked, not_checkable)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn s8_manifest_naming_tag_31_stamps_8_baseline_tags_stamp_baseline() {
assert_eq!(
check(&fixture_correctly_stamped_tag_31()),
GminorVerdict::Checked {
expected: SchemaVersion::new(0, 8),
actual: SchemaVersion::new(0, 8),
matches: true,
}
);
assert_eq!(
check(&fixture_baseline_only()),
GminorVerdict::Checked {
expected: SchemaVersion::V0,
actual: SchemaVersion::V0,
matches: true,
}
);
}
#[test]
fn s15_the_gate_fails_the_understamped_fixture() {
let verdict = check(&fixture_understamped());
assert_eq!(
verdict,
GminorVerdict::Checked {
expected: SchemaVersion::new(0, 8),
actual: SchemaVersion::V0,
matches: false,
}
);
}
#[test]
fn s16_the_gate_fails_the_overstamped_fixture() {
let verdict = check(&fixture_overstamped_after_barrier_removal());
assert_eq!(
verdict,
GminorVerdict::Checked {
expected: SchemaVersion::V0,
actual: SchemaVersion::new(0, 8),
matches: false,
}
);
}
#[test]
fn s17_an_undecodable_blob_is_reported_not_checkable_never_a_pass() {
assert!(matches!(
check(&fixture_undecodable_barrier_blob()),
GminorVerdict::NotCheckable(_)
));
}
#[test]
fn the_gate_reports_four_checked_and_one_not_checkable() {
assert_eq!(run_gate(), (4, 1));
}
#[test]
fn correctly_stamped_fixtures_match() {
assert!(matches!(
check(&fixture_correctly_stamped_tag_31()),
GminorVerdict::Checked { matches: true, .. }
));
assert!(matches!(
check(&fixture_baseline_only()),
GminorVerdict::Checked { matches: true, .. }
));
}
#[test]
fn understamped_fixture_is_caught() {
assert!(matches!(
check(&fixture_understamped()),
GminorVerdict::Checked { matches: false, .. }
));
}
#[test]
fn overstamped_fixture_is_caught() {
assert!(matches!(
check(&fixture_overstamped_after_barrier_removal()),
GminorVerdict::Checked { matches: false, .. }
));
}
#[test]
fn undecodable_barrier_blob_is_not_checkable_not_a_pass() {
assert!(matches!(
check(&fixture_undecodable_barrier_blob()),
GminorVerdict::NotCheckable(_)
));
}
}