diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index c4389756..29cef2fc 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -252,7 +252,8 @@ pub use warp_state::{WarpInstance, WarpState}; pub use witnessed_suffix::{ evaluate_witnessed_suffix_admission, WitnessedSuffixAdmissionContext, WitnessedSuffixAdmissionOutcome, WitnessedSuffixAdmissionRequest, - WitnessedSuffixAdmissionResponse, WitnessedSuffixLocalAdmissionPosture, WitnessedSuffixShell, + WitnessedSuffixAdmissionResponse, WitnessedSuffixLocalAdmissionPosture, + WitnessedSuffixLocalAdmissionPostureError, WitnessedSuffixShell, }; pub use worldline::{ ApplyError, AtomWrite, AtomWriteSet, HashTriplet, OutputFrameSet, WorldlineId, diff --git a/crates/warp-core/src/witnessed_suffix.rs b/crates/warp-core/src/witnessed_suffix.rs index 42fc35e1..d8b70acd 100644 --- a/crates/warp-core/src/witnessed_suffix.rs +++ b/crates/warp-core/src/witnessed_suffix.rs @@ -8,6 +8,7 @@ use blake3::Hasher; use echo_wasm_abi::{encode_cbor, kernel_port as abi}; +use thiserror::Error; use crate::attachment::{AttachmentOwner, AttachmentPlane}; use crate::clock::WorldlineTick; @@ -139,22 +140,48 @@ pub trait WitnessedSuffixAdmissionContext { ) -> WitnessedSuffixLocalAdmissionPosture; } +/// Error returned when constructing a canonical local admission posture fails. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)] +pub enum WitnessedSuffixLocalAdmissionPostureError { + /// A provenance coordinate appeared more than once in a posture vector. + #[error("duplicate witnessed suffix local admission provenance ref: {provenance_ref:?}")] + DuplicateProvenanceRef { + /// Duplicate provenance coordinate. + provenance_ref: ProvenanceRef, + }, +} + /// Local posture reported by the read-only admission context. +/// +/// Prefer [`Self::admissible`], [`Self::staged`], or [`Self::plural`] for +/// ordinary construction so ref vectors are sorted canonically and duplicate +/// provenance refs are rejected before reaching an ABI-visible response. Direct +/// enum construction remains available for raw-shape tests and defensive +/// evaluator inputs. #[derive(Clone, Debug, PartialEq, Eq)] pub enum WitnessedSuffixLocalAdmissionPosture { /// Local evidence says the suffix is admissible. Admissible { /// Target-local provenance coordinates produced or expected by admission. + /// + /// Use [`WitnessedSuffixLocalAdmissionPosture::admissible`] for normal + /// construction. admitted_refs: Vec, }, /// Local evidence says the suffix should be retained for later judgment. Staged { /// Source or target coordinates retained while staged. + /// + /// Use [`WitnessedSuffixLocalAdmissionPosture::staged`] for normal + /// construction. staged_refs: Vec, }, /// Local evidence preserves lawful plurality. Plural { /// Candidate coordinates that remain lawful plural outcomes. + /// + /// Use [`WitnessedSuffixLocalAdmissionPosture::plural`] for normal + /// construction. candidate_refs: Vec, }, /// Local evidence reports deterministic adverse admission law. @@ -170,6 +197,51 @@ pub enum WitnessedSuffixLocalAdmissionPosture { }, } +impl WitnessedSuffixLocalAdmissionPosture { + /// Builds an admissible posture with canonical admitted refs. + pub fn admissible( + admitted_refs: Vec, + ) -> Result { + Ok(Self::Admissible { + admitted_refs: canonical_unique_provenance_refs(admitted_refs)?, + }) + } + + /// Builds a staged posture with canonical staged refs. + pub fn staged( + staged_refs: Vec, + ) -> Result { + Ok(Self::Staged { + staged_refs: canonical_unique_provenance_refs(staged_refs)?, + }) + } + + /// Builds a plural posture with canonical candidate refs. + pub fn plural( + candidate_refs: Vec, + ) -> Result { + Ok(Self::Plural { + candidate_refs: canonical_unique_provenance_refs(candidate_refs)?, + }) + } + + /// Builds a conflict posture from named conflict evidence. + #[must_use] + pub fn conflict( + reason: ConflictReason, + source_ref: ProvenanceRef, + evidence_digest: Hash, + overlap_revalidation: Option, + ) -> Self { + Self::Conflict { + reason, + source_ref, + evidence_digest, + overlap_revalidation, + } + } +} + /// Evaluates one witnessed suffix admission request against local evidence. /// /// Performs deterministic local validation before returning the classified @@ -374,6 +446,24 @@ fn canonical_provenance_refs(mut refs: Vec) -> Vec refs } +fn canonical_unique_provenance_refs( + mut refs: Vec, +) -> Result, WitnessedSuffixLocalAdmissionPostureError> { + refs.sort_unstable(); + + for window in refs.windows(2) { + if window[0] == window[1] { + return Err( + WitnessedSuffixLocalAdmissionPostureError::DuplicateProvenanceRef { + provenance_ref: window[0], + }, + ); + } + } + + Ok(refs) +} + fn obstructed_response( request: &WitnessedSuffixAdmissionRequest, source_shell_digest: Hash, diff --git a/crates/warp-core/src/witnessed_suffix_tests.rs b/crates/warp-core/src/witnessed_suffix_tests.rs index 6670d1ef..df977a74 100644 --- a/crates/warp-core/src/witnessed_suffix_tests.rs +++ b/crates/warp-core/src/witnessed_suffix_tests.rs @@ -9,8 +9,8 @@ use crate::{ StrandBasisReport, StrandDivergenceFootprint, StrandOverlapRevalidation, StrandRevalidationState, WarpId, WitnessedSuffixAdmissionContext, WitnessedSuffixAdmissionOutcome, WitnessedSuffixAdmissionRequest, - WitnessedSuffixAdmissionResponse, WitnessedSuffixLocalAdmissionPosture, WitnessedSuffixShell, - WorldlineId, WorldlineTick, + WitnessedSuffixAdmissionResponse, WitnessedSuffixLocalAdmissionPosture, + WitnessedSuffixLocalAdmissionPostureError, WitnessedSuffixShell, WorldlineId, WorldlineTick, }; fn worldline(seed: u8) -> WorldlineId { @@ -127,6 +127,8 @@ impl WitnessedSuffixAdmissionContext for TargetBasisEchoAdmissionContext { &self, request: &WitnessedSuffixAdmissionRequest, ) -> WitnessedSuffixLocalAdmissionPosture { + // This trait method cannot return Result, so this fixture uses the raw + // shape only where the resolved basis is already deterministic. WitnessedSuffixLocalAdmissionPosture::Admissible { admitted_refs: vec![request.target_basis], } @@ -141,6 +143,38 @@ fn clean_context(posture: WitnessedSuffixLocalAdmissionPosture) -> FakeAdmission } } +fn admissible_posture( + refs: Vec, +) -> Result { + WitnessedSuffixLocalAdmissionPosture::admissible(refs) +} + +fn staged_posture( + refs: Vec, +) -> Result { + WitnessedSuffixLocalAdmissionPosture::staged(refs) +} + +fn plural_posture( + refs: Vec, +) -> Result { + WitnessedSuffixLocalAdmissionPosture::plural(refs) +} + +fn conflict_posture( + reason: ConflictReason, + source_ref: ProvenanceRef, + evidence_digest: Hash, + overlap_revalidation: Option, +) -> WitnessedSuffixLocalAdmissionPosture { + WitnessedSuffixLocalAdmissionPosture::conflict( + reason, + source_ref, + evidence_digest, + overlap_revalidation, + ) +} + #[test] fn witnessed_suffix_core_request_converts_to_abi_shape() { let request = request(); @@ -258,11 +292,112 @@ fn witnessed_suffix_core_response_converts_obstructed_outcome_to_abi() { } #[test] -fn witnessed_suffix_evaluator_admits_clean_suffix() { +fn witnessed_suffix_local_posture_admissible_constructor_canonicalizes_refs() { + assert_eq!( + WitnessedSuffixLocalAdmissionPosture::admissible(vec![ + provenance_ref(30, 12), + provenance_ref(30, 10), + ]), + Ok(WitnessedSuffixLocalAdmissionPosture::Admissible { + admitted_refs: vec![provenance_ref(30, 10), provenance_ref(30, 12)], + }) + ); +} + +#[test] +fn witnessed_suffix_local_posture_staged_constructor_canonicalizes_refs() { + assert_eq!( + WitnessedSuffixLocalAdmissionPosture::staged(vec![ + provenance_ref(32, 12), + provenance_ref(32, 11), + ]), + Ok(WitnessedSuffixLocalAdmissionPosture::Staged { + staged_refs: vec![provenance_ref(32, 11), provenance_ref(32, 12)], + }) + ); +} + +#[test] +fn witnessed_suffix_local_posture_plural_constructor_canonicalizes_refs() { + assert_eq!( + WitnessedSuffixLocalAdmissionPosture::plural(vec![ + provenance_ref(34, 13), + provenance_ref(33, 12), + ]), + Ok(WitnessedSuffixLocalAdmissionPosture::Plural { + candidate_refs: vec![provenance_ref(33, 12), provenance_ref(34, 13)], + }) + ); +} + +#[test] +fn witnessed_suffix_local_posture_constructors_reject_duplicate_refs() { + let duplicate_ref = provenance_ref(30, 10); + + for duplicate_result in [ + WitnessedSuffixLocalAdmissionPosture::admissible(vec![duplicate_ref, duplicate_ref]), + WitnessedSuffixLocalAdmissionPosture::staged(vec![duplicate_ref, duplicate_ref]), + WitnessedSuffixLocalAdmissionPosture::plural(vec![duplicate_ref, duplicate_ref]), + ] { + assert_eq!( + duplicate_result, + Err( + WitnessedSuffixLocalAdmissionPostureError::DuplicateProvenanceRef { + provenance_ref: duplicate_ref, + } + ) + ); + } +} + +#[test] +fn witnessed_suffix_local_posture_constructors_reject_duplicates_after_sorting() { + let duplicate_ref = provenance_ref(30, 10); + let intervening_ref = provenance_ref(31, 10); + + assert_eq!( + WitnessedSuffixLocalAdmissionPosture::admissible(vec![ + duplicate_ref, + intervening_ref, + duplicate_ref, + ]), + Err( + WitnessedSuffixLocalAdmissionPostureError::DuplicateProvenanceRef { + provenance_ref: duplicate_ref, + }, + ) + ); +} + +#[test] +fn witnessed_suffix_local_posture_conflict_constructor_names_all_evidence() { + let overlap_revalidation = StrandOverlapRevalidation::Conflict { + overlapping_slots: vec![node_slot("constructor-overlap-a")], + }; + + let posture = WitnessedSuffixLocalAdmissionPosture::conflict( + ConflictReason::ParentFootprintOverlap, + provenance_ref(35, 14), + [36; 32], + Some(overlap_revalidation.clone()), + ); + + assert_eq!( + posture, + WitnessedSuffixLocalAdmissionPosture::Conflict { + reason: ConflictReason::ParentFootprintOverlap, + source_ref: provenance_ref(35, 14), + evidence_digest: [36; 32], + overlap_revalidation: Some(overlap_revalidation), + } + ); +} + +#[test] +fn witnessed_suffix_evaluator_admits_clean_suffix( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let request = request(); - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Admissible { - admitted_refs: vec![provenance_ref(30, 10)], - }); + let context = clean_context(admissible_posture(vec![provenance_ref(30, 10)])?); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -277,19 +412,19 @@ fn witnessed_suffix_evaluator_admits_clean_suffix() { } if target_worldline_id == worldline(11) && admitted_refs == vec![provenance_ref(30, 10)] )); + Ok(()) } #[test] -fn witnessed_suffix_evaluator_stages_boundary_only_suffix() { +fn witnessed_suffix_evaluator_stages_boundary_only_suffix( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let request = WitnessedSuffixAdmissionRequest { source_suffix: shell_with_entries(Vec::new()), target_worldline_id: worldline(11), target_basis: provenance_ref(12, 9), basis_report: None, }; - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Staged { - staged_refs: vec![provenance_ref(5, 1)], - }); + let context = clean_context(staged_posture(vec![provenance_ref(5, 1)])?); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -298,10 +433,12 @@ fn witnessed_suffix_evaluator_stages_boundary_only_suffix() { WitnessedSuffixAdmissionOutcome::Staged { staged_refs, .. } if staged_refs == vec![provenance_ref(5, 1)] )); + Ok(()) } #[test] -fn witnessed_suffix_evaluator_obstructs_empty_suffix_without_boundary_witness() { +fn witnessed_suffix_evaluator_obstructs_empty_suffix_without_boundary_witness( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let mut request = WitnessedSuffixAdmissionRequest { source_suffix: shell_with_entries(Vec::new()), target_worldline_id: worldline(11), @@ -309,9 +446,7 @@ fn witnessed_suffix_evaluator_obstructs_empty_suffix_without_boundary_witness() basis_report: None, }; request.source_suffix.boundary_witness = None; - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Staged { - staged_refs: Vec::new(), - }); + let context = clean_context(staged_posture(Vec::new())?); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -322,17 +457,17 @@ fn witnessed_suffix_evaluator_obstructs_empty_suffix_without_boundary_witness() .. } )); + Ok(()) } #[test] -fn witnessed_suffix_evaluator_allows_equal_start_and_end_ticks() { +fn witnessed_suffix_evaluator_allows_equal_start_and_end_ticks( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let mut request = request(); request.source_suffix.source_suffix_end_tick = Some(request.source_suffix.source_suffix_start_tick); request.source_suffix.source_entries = vec![provenance_ref(3, 2)]; - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Admissible { - admitted_refs: vec![provenance_ref(30, 10)], - }); + let context = clean_context(admissible_posture(vec![provenance_ref(30, 10)])?); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -345,10 +480,12 @@ fn witnessed_suffix_evaluator_allows_equal_start_and_end_ticks() { } if target_worldline_id == worldline(11) && admitted_refs == vec![provenance_ref(30, 10)] )); + Ok(()) } #[test] -fn witnessed_suffix_evaluator_stages_when_target_basis_is_boundary_witness() { +fn witnessed_suffix_evaluator_stages_when_target_basis_is_boundary_witness( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let boundary_witness = provenance_ref(5, 1); let request = WitnessedSuffixAdmissionRequest { source_suffix: shell_with_entries(Vec::new()), @@ -359,9 +496,7 @@ fn witnessed_suffix_evaluator_stages_when_target_basis_is_boundary_witness() { let context = FakeAdmissionContext { expected_shell_digest: Some([6; 32]), resolved_target_basis: Some(boundary_witness), - posture: WitnessedSuffixLocalAdmissionPosture::Staged { - staged_refs: vec![boundary_witness], - }, + posture: staged_posture(vec![boundary_witness])?, }; let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -372,15 +507,15 @@ fn witnessed_suffix_evaluator_stages_when_target_basis_is_boundary_witness() { WitnessedSuffixAdmissionOutcome::Staged { staged_refs, .. } if staged_refs == vec![boundary_witness] )); + Ok(()) } #[test] -fn witnessed_suffix_evaluator_preserves_plural_outcome() { +fn witnessed_suffix_evaluator_preserves_plural_outcome( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let request = request(); let candidates = vec![provenance_ref(33, 12), provenance_ref(34, 13)]; - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Plural { - candidate_refs: candidates.clone(), - }); + let context = clean_context(plural_posture(candidates.clone())?); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -392,17 +527,18 @@ fn witnessed_suffix_evaluator_preserves_plural_outcome() { .. } if candidate_refs == candidates )); + Ok(()) } #[test] fn witnessed_suffix_evaluator_conflicts_with_adverse_admission_law() { let request = request(); - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Conflict { - reason: ConflictReason::ParentFootprintOverlap, - source_ref: provenance_ref(35, 14), - evidence_digest: [36; 32], - overlap_revalidation: None, - }); + let context = clean_context(conflict_posture( + ConflictReason::ParentFootprintOverlap, + provenance_ref(35, 14), + [36; 32], + None, + )); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -423,12 +559,12 @@ fn witnessed_suffix_evaluator_preserves_conflict_overlap_revalidation() { let overlap_revalidation = StrandOverlapRevalidation::Conflict { overlapping_slots: vec![node_slot("overlap-a"), node_slot("overlap-b")], }; - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Conflict { - reason: ConflictReason::ParentFootprintOverlap, - source_ref: provenance_ref(35, 14), - evidence_digest: [36; 32], - overlap_revalidation: Some(overlap_revalidation.clone()), - }); + let context = clean_context(conflict_posture( + ConflictReason::ParentFootprintOverlap, + provenance_ref(35, 14), + [36; 32], + Some(overlap_revalidation.clone()), + )); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -474,14 +610,13 @@ fn witnessed_suffix_evaluator_classifies_against_resolved_target_basis() { } #[test] -fn witnessed_suffix_evaluator_preserves_matching_request_basis_report() { +fn witnessed_suffix_evaluator_preserves_matching_request_basis_report( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let mut request = request(); let report = basis_report(provenance_ref(12, 9)); request.basis_report = Some(report.clone()); request.source_suffix.basis_report = Some(basis_report(provenance_ref(12, 9))); - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Admissible { - admitted_refs: vec![provenance_ref(30, 10)], - }); + let context = clean_context(admissible_posture(vec![provenance_ref(30, 10)])?); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -492,16 +627,16 @@ fn witnessed_suffix_evaluator_preserves_matching_request_basis_report() { .. } if actual == report )); + Ok(()) } #[test] -fn witnessed_suffix_evaluator_falls_back_to_matching_source_basis_report() { +fn witnessed_suffix_evaluator_falls_back_to_matching_source_basis_report( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let mut request = request(); let report = basis_report(provenance_ref(12, 9)); request.source_suffix.basis_report = Some(report.clone()); - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Staged { - staged_refs: vec![provenance_ref(5, 1)], - }); + let context = clean_context(staged_posture(vec![provenance_ref(5, 1)])?); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -512,15 +647,15 @@ fn witnessed_suffix_evaluator_falls_back_to_matching_source_basis_report() { .. } if actual == report )); + Ok(()) } #[test] -fn witnessed_suffix_evaluator_obstructs_stale_basis_report() { +fn witnessed_suffix_evaluator_obstructs_stale_basis_report( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let mut request = request(); request.basis_report = Some(basis_report(provenance_ref(99, 99))); - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Admissible { - admitted_refs: vec![provenance_ref(30, 10)], - }); + let context = clean_context(admissible_posture(vec![provenance_ref(30, 10)])?); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -531,10 +666,11 @@ fn witnessed_suffix_evaluator_obstructs_stale_basis_report() { .. } )); + Ok(()) } #[test] -fn witnessed_suffix_evaluator_normalizes_admitted_refs() { +fn witnessed_suffix_evaluator_normalizes_raw_admitted_refs() { let request = request(); let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Admissible { admitted_refs: vec![provenance_ref(30, 12), provenance_ref(30, 10)], @@ -550,7 +686,7 @@ fn witnessed_suffix_evaluator_normalizes_admitted_refs() { } #[test] -fn witnessed_suffix_evaluator_normalizes_staged_refs() { +fn witnessed_suffix_evaluator_normalizes_raw_staged_refs() { let request = request(); let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Staged { staged_refs: vec![provenance_ref(32, 12), provenance_ref(32, 11)], @@ -566,7 +702,7 @@ fn witnessed_suffix_evaluator_normalizes_staged_refs() { } #[test] -fn witnessed_suffix_evaluator_normalizes_plural_candidate_refs() { +fn witnessed_suffix_evaluator_normalizes_raw_plural_candidate_refs() { let request = request(); let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Plural { candidate_refs: vec![provenance_ref(34, 13), provenance_ref(33, 12)], @@ -582,14 +718,13 @@ fn witnessed_suffix_evaluator_normalizes_plural_candidate_refs() { } #[test] -fn witnessed_suffix_evaluator_obstructs_mismatched_digest() { +fn witnessed_suffix_evaluator_obstructs_mismatched_digest( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let request = request(); let context = FakeAdmissionContext { expected_shell_digest: Some([99; 32]), resolved_target_basis: Some(provenance_ref(12, 9)), - posture: WitnessedSuffixLocalAdmissionPosture::Admissible { - admitted_refs: vec![provenance_ref(30, 10)], - }, + posture: admissible_posture(vec![provenance_ref(30, 10)])?, }; let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -601,18 +736,17 @@ fn witnessed_suffix_evaluator_obstructs_mismatched_digest() { .. } )); + Ok(()) } #[test] -fn witnessed_suffix_evaluator_obstructs_missing_local_source_digest_without_reusing_request_digest() -{ +fn witnessed_suffix_evaluator_obstructs_missing_local_source_digest_without_reusing_request_digest( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let request = request(); let context = FakeAdmissionContext { expected_shell_digest: None, resolved_target_basis: Some(provenance_ref(12, 9)), - posture: WitnessedSuffixLocalAdmissionPosture::Admissible { - admitted_refs: vec![provenance_ref(30, 10)], - }, + posture: admissible_posture(vec![provenance_ref(30, 10)])?, }; let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -629,17 +763,17 @@ fn witnessed_suffix_evaluator_obstructs_missing_local_source_digest_without_reus .. } if evidence_digest == response.source_shell_digest )); + Ok(()) } #[test] -fn witnessed_suffix_evaluator_obstructs_unknown_target_basis() { +fn witnessed_suffix_evaluator_obstructs_unknown_target_basis( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let request = request(); let context = FakeAdmissionContext { expected_shell_digest: Some([6; 32]), resolved_target_basis: None, - posture: WitnessedSuffixLocalAdmissionPosture::Admissible { - admitted_refs: vec![provenance_ref(30, 10)], - }, + posture: admissible_posture(vec![provenance_ref(30, 10)])?, }; let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -651,16 +785,16 @@ fn witnessed_suffix_evaluator_obstructs_unknown_target_basis() { .. } )); + Ok(()) } #[test] -fn witnessed_suffix_evaluator_obstructs_inconsistent_bounds() { +fn witnessed_suffix_evaluator_obstructs_inconsistent_bounds( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let mut request = request(); request.source_suffix.source_suffix_start_tick = tick(5); request.source_suffix.source_suffix_end_tick = Some(tick(4)); - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Admissible { - admitted_refs: vec![provenance_ref(30, 10)], - }); + let context = clean_context(admissible_posture(vec![provenance_ref(30, 10)])?); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -671,17 +805,17 @@ fn witnessed_suffix_evaluator_obstructs_inconsistent_bounds() { .. } )); + Ok(()) } #[test] -fn witnessed_suffix_evaluator_obstructs_source_entry_outside_suffix_bounds() { +fn witnessed_suffix_evaluator_obstructs_source_entry_outside_suffix_bounds( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { for outside_tick in [1, 5] { let mut request = request(); let offending_ref = provenance_ref(3, outside_tick); request.source_suffix.source_entries = vec![offending_ref]; - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Admissible { - admitted_refs: vec![provenance_ref(30, 10)], - }); + let context = clean_context(admissible_posture(vec![provenance_ref(30, 10)])?); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -694,16 +828,16 @@ fn witnessed_suffix_evaluator_obstructs_source_entry_outside_suffix_bounds() { } if source_ref == offending_ref )); } + Ok(()) } #[test] -fn witnessed_suffix_evaluator_obstructs_source_entry_from_foreign_worldline() { +fn witnessed_suffix_evaluator_obstructs_source_entry_from_foreign_worldline( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let mut request = request(); let offending_ref = provenance_ref(4, 3); request.source_suffix.source_entries = vec![offending_ref]; - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Admissible { - admitted_refs: vec![provenance_ref(30, 10)], - }); + let context = clean_context(admissible_posture(vec![provenance_ref(30, 10)])?); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -715,16 +849,16 @@ fn witnessed_suffix_evaluator_obstructs_source_entry_from_foreign_worldline() { .. } if source_ref == offending_ref )); + Ok(()) } #[test] -fn witnessed_suffix_evaluator_obstructs_out_of_order_source_entries() { +fn witnessed_suffix_evaluator_obstructs_out_of_order_source_entries( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let mut request = request(); let offending_ref = provenance_ref(3, 3); request.source_suffix.source_entries = vec![provenance_ref(3, 4), offending_ref]; - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Admissible { - admitted_refs: vec![provenance_ref(30, 10)], - }); + let context = clean_context(admissible_posture(vec![provenance_ref(30, 10)])?); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -736,16 +870,16 @@ fn witnessed_suffix_evaluator_obstructs_out_of_order_source_entries() { .. } if source_ref == offending_ref )); + Ok(()) } #[test] -fn witnessed_suffix_evaluator_obstructs_duplicate_source_entries() { +fn witnessed_suffix_evaluator_obstructs_duplicate_source_entries( +) -> Result<(), WitnessedSuffixLocalAdmissionPostureError> { let mut request = request(); let duplicate_ref = provenance_ref(3, 3); request.source_suffix.source_entries = vec![duplicate_ref, duplicate_ref]; - let context = clean_context(WitnessedSuffixLocalAdmissionPosture::Admissible { - admitted_refs: vec![provenance_ref(30, 10)], - }); + let context = clean_context(admissible_posture(vec![provenance_ref(30, 10)])?); let response = evaluate_witnessed_suffix_admission(&request, &context); @@ -757,6 +891,7 @@ fn witnessed_suffix_evaluator_obstructs_duplicate_source_entries() { .. } if source_ref == duplicate_ref )); + Ok(()) } #[test] diff --git a/docs/design/0012-witnessed-suffix-posture-canonicalization/design.md b/docs/design/0012-witnessed-suffix-posture-canonicalization/design.md new file mode 100644 index 00000000..0d02c37c --- /dev/null +++ b/docs/design/0012-witnessed-suffix-posture-canonicalization/design.md @@ -0,0 +1,132 @@ + + + +# 0012 — Witnessed suffix posture canonicalization + +_Add named canonical construction for witnessed suffix local admission +postures._ + +Legend: [PLATFORM](../../method/legends/PLATFORM.md) + +Depends on: + +- [Witnessed suffix admission evaluator](../../design/witnessed-suffix-admission-evaluator.md) +- [Continuum runtime and CAS readings](../../design/continuum-runtime-and-cas-readings.md) +- [0011 — Optic and observer runtime doctrine](../0011-optic-observer-runtime-doctrine/design.md) + +Source card: + +- `docs/method/backlog/inbox/PLATFORM_witnessed-suffix-admission-hardening.md` + +## Why this cycle exists + +PR #323 made the witnessed suffix local admission evaluator deterministic and +honest around source suffix evidence, basis resolution, and output posture +ordering. The evaluator now canonicalizes posture vectors before returning ABI +visible outcomes. + +That is not enough once future call sites construct +`WitnessedSuffixLocalAdmissionPosture` directly. Raw enum construction can +reintroduce caller-order leakage or duplicate provenance refs before the +evaluator gets a chance to normalize the shape. This cycle starts by making +that missing named construction path executable as RED. + +GREEN 1 adds the named constructors and migrates ordinary test fixtures to use +them. Raw enum construction remains only where a test is asserting raw shape or +evaluator normalization of raw posture input. + +## Human users / jobs / hills + +### Primary human users + +Maintainers reviewing witnessed suffix admission, settlement, and Continuum +runtime work. + +### Human jobs + +1. Review the named constructor API and duplicate-ref error contract. +2. Confirm the slice does not reopen transport, sync, ABI, or broad Continuum + design. + +### Human hill + +A maintainer can review the canonical posture construction contract and the +small helper implementation without transport, sync, or ABI noise. + +## Agent users / jobs / hills + +### Primary agent users + +Future coding agents extending witnessed suffix admission call sites. + +### Agent jobs + +1. Use named posture constructors instead of hand-building posture vectors. +2. Programmatically determine that posture refs are canonicalized and duplicate + refs are rejected. + +### Agent hill + +An agent can run a single warp-core witnessed suffix test target and verify the +canonical constructor contract. + +## Human playback + +1. The human reads the constructor contract tests in + `crates/warp-core/src/witnessed_suffix_tests.rs`. +2. The tests name `admissible`, `staged`, `plural`, and `conflict` constructor + paths on `WitnessedSuffixLocalAdmissionPosture`. +3. The human can verify the GREEN 1 implementation without inspecting transport + or ABI code. + +## Agent playback + +1. The agent runs `cargo test -p warp-core --lib witnessed_suffix`. +2. The command passes the constructor contract examples and existing evaluator + defense tests. +3. The agent determines the slice remains local to warp-core witnessed suffix + posture construction. + +## Implementation outline + +1. RED: add tests for named canonical construction of admissible, staged, + plural, and conflict local postures. +2. RED: move the backlog card into this design packet. +3. GREEN 1: add the named constructors and duplicate-ref error. +4. GREEN 1: keep ordinary fixtures on the canonical constructor path; leave raw + enum construction only for raw-shape and evaluator-defense tests. + +## Tests to write first + +- constructor canonicalizes admissible refs +- constructor canonicalizes staged refs +- constructor canonicalizes plural candidate refs +- constructors reject duplicate refs with a named error +- constructors reject duplicate refs after canonical sorting +- conflict constructor names reason, source ref, digest, and overlap evidence + +## Risks / unknowns + +- The constructor names may change during GREEN. If so, update only the tests + and design wording in the same narrow slice. +- Duplicate handling may need a wider posture validation policy if empty ref + vectors also become invalid. This RED only demands duplicate rejection. +- Direct enum construction remains available for tests that deliberately assert + raw shape or raw evaluator input. Ordinary clean fixtures should prefer named + constructors. + +## Postures + +- **Accessibility:** Not applicable; this is Rust API/test surface only. +- **Localization:** Not applicable; no user-facing strings are introduced. +- **Agent inspectability:** The design and tests name exact constructor paths, + duplicate handling, and raw-construction exceptions so future agents can + extend call sites without broad repo exploration. + +## Non-goals + +- Do not change transport, sync, import execution, or ABI shape. +- Do not redesign witnessed suffix admission. +- Do not implement Continuum proof, IPA, or commitment machinery. +- Do not audit every admission or settlement vector in this slice. +- Do not weaken the evaluator's existing obstruction posture. diff --git a/docs/method/backlog/inbox/PLATFORM_witnessed-suffix-admission-hardening.md b/docs/method/backlog/inbox/PLATFORM_witnessed-suffix-admission-hardening.md deleted file mode 100644 index 40ce576b..00000000 --- a/docs/method/backlog/inbox/PLATFORM_witnessed-suffix-admission-hardening.md +++ /dev/null @@ -1,73 +0,0 @@ - - - -# Witnessed suffix admission hardening - -Status: inbox. - -Source: PR #323 retrospective follow-up. - -## Why now - -PR #323 hardened the witnessed suffix local admission evaluator around -deterministic posture output, canonical source entries, resolved-basis -classification, and missing local digest evidence. - -Those fixes should not turn into a wider PR-review workstream, but the review -left useful backlog fuel. Future call sites may construct -`WitnessedSuffixLocalAdmissionPosture` outside the current tests. Obstruction -digest domains may grow beyond the local evaluator family. Other -admission-shaped responses may still expose caller-order leakage at an ABI -boundary. Test fixtures that look clean can also accidentally encode malformed -causal coordinates. - -This card captures that follow-up work without reopening the current evaluator -scope. - -## What it should look like - -- Add canonical constructors or validation helpers for - `WitnessedSuffixLocalAdmissionPosture` before additional call sites build - postures directly. -- Add an explicit design paragraph for canonical source suffix invariants: - - every source entry belongs to the shell source worldline - - provenance refs are in canonical order - - entries sit inside the claimed suffix bounds - - the suffix carries either witness-backed entries or boundary witness - evidence sufficient for an honest obstruction -- Document obstruction digest domains and how their domain/version strings - relate to future Continuum evidence hashes. -- Audit admission and settlement response vectors for ABI-visible caller-order - leakage. -- Strengthen fixture discipline so positive "clean" fixtures cannot encode - malformed causal coordinates by accident. Malformed coordinates should appear - only in negative tests that name the invariant they violate. - -## Done looks like - -- Posture construction has a named canonical path, or direct construction is - intentionally constrained and documented. -- The witnessed suffix design docs name the canonical source suffix invariants - explicitly. -- Obstruction digest domain/version policy is documented before more digest - families appear. -- ABI-visible admission and settlement vectors are audited, with canonicalized - vectors fixed or caller-law ordering documented. -- Test fixture helpers make valid causal coordinates the default. - -## Repo evidence - -- `crates/warp-core/src/witnessed_suffix.rs` -- `crates/warp-core/src/witnessed_suffix_tests.rs` -- `crates/echo-wasm-abi/src/witnessed_suffix_tests.rs` -- `docs/design/witnessed-suffix-admission-evaluator.md` -- `docs/design/continuum-runtime-and-cas-readings.md` -- PR #323: `https://github.com/flyingrobots/echo/pull/323` - -## Non-goals - -- Do not reopen PR #323's evaluator implementation without a new RED. -- Do not redesign the witnessed suffix ABI. -- Do not add Continuum proof, IPA, or commitment machinery. -- Do not change transport, sync, or import execution behavior. -- Do not weaken the evaluator's existing obstruction posture.