From 177031fec5694c10b336425a42b43654fb8b5f63 Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 9 Jul 2026 08:55:52 -0700 Subject: [PATCH 1/6] feat(warp-core): add retained evidence boundary posture --- CHANGELOG.md | 4 + crates/warp-core/src/lib.rs | 5 +- crates/warp-core/src/retained_evidence.rs | 431 +++++++++++++++++- .../tests/retained_evidence_ref_tests.rs | 205 ++++++++- 4 files changed, 638 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3db89c27..6901b0d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ### Added +- `warp-core` now exposes `RetainedEvidenceBoundaryPosture` with boundary + layer, origin, proof strength, access, completeness, and obstruction axes so + retained evidence refs can be projected without conflating citation, reveal + permission, redaction, unsupported evidence kinds, or missing retention. - `warp-core` now exposes a recovered WAL evidence segment catalog derived from `RecoveryScanReport`, with a non-authoritative live cache in `TrustedRuntimeWal` that marks cache-update failures as rebuild posture diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index d0d840ce..d4c044fc 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -279,7 +279,10 @@ pub use playback::{ CursorId, CursorRole, PlaybackCursor, PlaybackMode, SeekError, SeekThen, StepResult, }; pub use retained_evidence::{ - RetainedEvidenceCoordinate, RetainedEvidencePosture, RetainedEvidenceRef, RetainedEvidenceRole, + RetainedEvidenceAccess, RetainedEvidenceBoundaryPosture, RetainedEvidenceCompleteness, + RetainedEvidenceCoordinate, RetainedEvidenceLayer, RetainedEvidenceOrigin, + RetainedEvidencePosture, RetainedEvidenceProofStrength, RetainedEvidenceRef, + RetainedEvidenceRole, }; // --- Sealed membership capability types --- pub use sealed_membership::{ diff --git a/crates/warp-core/src/retained_evidence.rs b/crates/warp-core/src/retained_evidence.rs index 5965a8ef..36bc9ec5 100644 --- a/crates/warp-core/src/retained_evidence.rs +++ b/crates/warp-core/src/retained_evidence.rs @@ -8,12 +8,16 @@ use blake3::Hasher; -use crate::contract_obstruction::ContractObstruction; +use crate::contract_obstruction::{ + ContractObstruction, ContractObstructionKind, ContractObstructionSubject, +}; use crate::contract_registry::{ContractEvidenceIdentity, ContractOperationKind}; use crate::ident::Hash; const RETAINED_EVIDENCE_COORDINATE_ID_DOMAIN: &[u8] = b"echo:retained-evidence-coordinate-id:v1\0"; const RETAINED_EVIDENCE_REF_ID_DOMAIN: &[u8] = b"echo:retained-evidence-ref-id:v1\0"; +const RETAINED_EVIDENCE_BOUNDARY_POSTURE_ID_DOMAIN: &[u8] = + b"echo:retained-evidence-boundary-posture-id:v1\0"; /// Semantic role of retained contract evidence. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -45,6 +49,155 @@ impl RetainedEvidenceRole { } } +/// Boundary layer occupied by retained evidence. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RetainedEvidenceLayer { + /// Coordinates and anchors needed to reintegrate the evidence with causal + /// history. + ReintegrationCore, + /// Proof-bearing material that certifies admission or observation + /// lawfulness. + WitnessCore, + /// Operational receipt or explanation shell. + ReceiptShell, +} + +impl RetainedEvidenceLayer { + fn tag(self) -> u8 { + match self { + Self::ReintegrationCore => 0, + Self::WitnessCore => 1, + Self::ReceiptShell => 2, + } + } +} + +/// Origin posture for retained evidence at a boundary. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RetainedEvidenceOrigin { + /// Native Echo retained evidence. + Native, + /// Evidence translated through a substrate/refinement boundary. + Translated, + /// Fixture or test-only evidence. + Fixture, + /// Evidence derived from other retained support. + Derived, + /// Opaque evidence whose origin cannot be refined locally. + Opaque, +} + +impl RetainedEvidenceOrigin { + fn tag(self) -> u8 { + match self { + Self::Native => 0, + Self::Translated => 1, + Self::Fixture => 2, + Self::Derived => 3, + Self::Opaque => 4, + } + } +} + +/// Proof strength carried by retained evidence. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RetainedEvidenceProofStrength { + /// No accepted proof is carried. + None, + /// Digest-only evidence. + DigestOnly, + /// Signature-backed evidence. + Signature, + /// Replay-certificate-backed evidence. + ReplayCertificate, + /// Merkle opening or equivalent inclusion proof. + MerkleOpening, + /// Zero-knowledge proof-backed evidence. + ZkProof, + /// Composite proof bundle. + Composite, +} + +impl RetainedEvidenceProofStrength { + fn tag(self) -> u8 { + match self { + Self::None => 0, + Self::DigestOnly => 1, + Self::Signature => 2, + Self::ReplayCertificate => 3, + Self::MerkleOpening => 4, + Self::ZkProof => 5, + Self::Composite => 6, + } + } +} + +/// Access posture for retained evidence at the current observer boundary. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RetainedEvidenceAccess { + /// The current observer may reveal the retained bytes. + Revealable, + /// The current observer may cite the evidence but not reveal bytes. + CitationOnly, + /// The evidence is explicitly redacted at this boundary. + Redacted, + /// Authority denies the requested access. + AuthorityBlocked, + /// Required decryption or unsealing key is unavailable. + KeyUnavailable, + /// The participant/runtime does not support this evidence kind. + Unsupported, +} + +impl RetainedEvidenceAccess { + fn tag(self) -> u8 { + match self { + Self::Revealable => 0, + Self::CitationOnly => 1, + Self::Redacted => 2, + Self::AuthorityBlocked => 3, + Self::KeyUnavailable => 4, + Self::Unsupported => 5, + } + } +} + +/// Completeness posture for retained evidence support. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RetainedEvidenceCompleteness { + /// Obligated support is complete for this boundary. + Complete, + /// Some support is present, but the obligation is only partially met. + Partial, + /// Loss, redaction, or unavailability was declared rather than hidden. + DeclaredLost, + /// Evidence is stale for the requested basis or context. + Stale, + /// No semantic coordinate descriptor exists. + MissingCoordinate, + /// A descriptor exists, but the retained content is absent. + MissingContent, + /// Retained content is present but corrupt or hash-invalid. + Corrupt, + /// The boundary cannot admit the evidence for the requested purpose. + Obstructed, +} + +impl RetainedEvidenceCompleteness { + fn tag(self) -> u8 { + match self { + Self::Complete => 0, + Self::Partial => 1, + Self::DeclaredLost => 2, + Self::Stale => 3, + Self::MissingCoordinate => 4, + Self::MissingContent => 5, + Self::Corrupt => 6, + Self::Obstructed => 7, + } + } +} + /// Semantic coordinate for retained contract evidence. /// /// The coordinate names what the bytes answer. It is intentionally separate @@ -201,6 +354,219 @@ impl RetainedEvidencePosture { } } +/// Observer-facing retained evidence boundary posture. +/// +/// This is the projection surface above local byte retention. It keeps +/// semantic identity, witness-ladder layer, evidence origin, proof strength, +/// access, completeness, and obstruction posture distinct. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RetainedEvidenceBoundaryPosture { + /// Semantic coordinate the posture answers for. + pub coordinate: RetainedEvidenceCoordinate, + /// Exact retained reference when a descriptor is known. + pub reference: Option, + /// Witness-ladder layer occupied by the evidence. + pub layer: RetainedEvidenceLayer, + /// Evidence origin posture. + pub origin: RetainedEvidenceOrigin, + /// Proof strength posture. + pub proof_strength: RetainedEvidenceProofStrength, + /// Access posture for the current observer boundary. + pub access: RetainedEvidenceAccess, + /// Completeness posture for the current support obligation. + pub completeness: RetainedEvidenceCompleteness, + /// Typed obstruction when this posture obstructs the boundary claim. + pub obstruction: Option, +} + +impl RetainedEvidenceBoundaryPosture { + /// Builds available retained evidence that may be cited but not revealed. + #[must_use] + pub fn available_citation( + reference: RetainedEvidenceRef, + layer: RetainedEvidenceLayer, + origin: RetainedEvidenceOrigin, + proof_strength: RetainedEvidenceProofStrength, + ) -> Self { + Self::available_with_access( + reference, + layer, + origin, + proof_strength, + RetainedEvidenceAccess::CitationOnly, + ) + } + + /// Builds available retained evidence that may be revealed. + #[must_use] + pub fn available_revealable( + reference: RetainedEvidenceRef, + layer: RetainedEvidenceLayer, + origin: RetainedEvidenceOrigin, + proof_strength: RetainedEvidenceProofStrength, + ) -> Self { + Self::available_with_access( + reference, + layer, + origin, + proof_strength, + RetainedEvidenceAccess::Revealable, + ) + } + + /// Builds available retained evidence with an explicit access posture. + #[must_use] + pub fn available_with_access( + reference: RetainedEvidenceRef, + layer: RetainedEvidenceLayer, + origin: RetainedEvidenceOrigin, + proof_strength: RetainedEvidenceProofStrength, + access: RetainedEvidenceAccess, + ) -> Self { + Self { + coordinate: reference.coordinate.clone(), + reference: Some(reference), + layer, + origin, + proof_strength, + access, + completeness: RetainedEvidenceCompleteness::Complete, + obstruction: None, + } + } + + /// Builds redacted retained evidence posture. Redaction is declared loss, + /// not missing content. + #[must_use] + pub fn redacted( + reference: RetainedEvidenceRef, + layer: RetainedEvidenceLayer, + origin: RetainedEvidenceOrigin, + proof_strength: RetainedEvidenceProofStrength, + ) -> Self { + Self { + coordinate: reference.coordinate.clone(), + reference: Some(reference), + layer, + origin, + proof_strength, + access: RetainedEvidenceAccess::Redacted, + completeness: RetainedEvidenceCompleteness::DeclaredLost, + obstruction: None, + } + } + + /// Builds missing-coordinate boundary posture using the existing + /// `MissingRetention` obstruction for true local retained-coordinate absence. + #[must_use] + pub fn missing_coordinate( + coordinate: RetainedEvidenceCoordinate, + layer: RetainedEvidenceLayer, + origin: RetainedEvidenceOrigin, + proof_strength: RetainedEvidenceProofStrength, + access: RetainedEvidenceAccess, + ) -> Self { + Self { + obstruction: Some(coordinate.missing_retention_obstruction()), + coordinate, + reference: None, + layer, + origin, + proof_strength, + access, + completeness: RetainedEvidenceCompleteness::MissingCoordinate, + } + } + + /// Builds missing-content boundary posture using the existing + /// `MissingRetention` obstruction for true local retained-content absence. + #[must_use] + pub fn missing_content( + reference: RetainedEvidenceRef, + layer: RetainedEvidenceLayer, + origin: RetainedEvidenceOrigin, + proof_strength: RetainedEvidenceProofStrength, + access: RetainedEvidenceAccess, + ) -> Self { + Self { + obstruction: Some(reference.missing_retention_obstruction()), + coordinate: reference.coordinate.clone(), + reference: Some(reference), + layer, + origin, + proof_strength, + access, + completeness: RetainedEvidenceCompleteness::MissingContent, + } + } + + /// Builds posture for an unsupported proof/evidence kind. This is an + /// admission obstruction, not missing retention. + #[must_use] + pub fn unsupported_evidence_kind( + coordinate: RetainedEvidenceCoordinate, + layer: RetainedEvidenceLayer, + origin: RetainedEvidenceOrigin, + proof_strength: RetainedEvidenceProofStrength, + ) -> Self { + let obstruction = + ContractObstruction::admission_obstruction(ContractObstructionSubject::Retention { + retention_id: coordinate.coordinate_id(), + }) + .with_contract(coordinate.contract.clone()); + Self { + coordinate, + reference: None, + layer, + origin, + proof_strength, + access: RetainedEvidenceAccess::Unsupported, + completeness: RetainedEvidenceCompleteness::Obstructed, + obstruction: Some(obstruction), + } + } + + /// Stable id for the full observer-facing boundary posture. + #[must_use] + pub fn boundary_posture_id(&self) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(RETAINED_EVIDENCE_BOUNDARY_POSTURE_ID_DOMAIN); + hasher.update(&self.coordinate.coordinate_id()); + match &self.reference { + Some(reference) => { + hasher.update(&[1]); + hasher.update(&reference.evidence_ref_id()); + } + None => { + hasher.update(&[0]); + } + } + hasher.update(&[self.layer.tag()]); + hasher.update(&[self.origin.tag()]); + hasher.update(&[self.proof_strength.tag()]); + hasher.update(&[self.access.tag()]); + hasher.update(&[self.completeness.tag()]); + match &self.obstruction { + Some(obstruction) => { + hasher.update(&[1]); + update_obstruction(&mut hasher, obstruction); + } + None => { + hasher.update(&[0]); + } + } + hasher.finalize().into() + } + + /// Returns true only when this boundary posture grants byte revelation. + #[must_use] + pub fn grants_reveal(&self) -> bool { + self.access == RetainedEvidenceAccess::Revealable + && self.completeness == RetainedEvidenceCompleteness::Complete + && self.obstruction.is_none() + } +} + fn update_contract_identity(hasher: &mut Hasher, contract: &ContractEvidenceIdentity) { hasher.update(contract.package_id.as_bytes()); hasher.update(&contract.echo_abi_version.to_le_bytes()); @@ -228,3 +594,66 @@ fn contract_operation_kind_tag(kind: ContractOperationKind) -> u8 { ContractOperationKind::Query => 1, } } + +fn update_obstruction(hasher: &mut Hasher, obstruction: &ContractObstruction) { + hasher.update(&[contract_obstruction_kind_tag(obstruction.kind)]); + update_obstruction_subject(hasher, &obstruction.subject); + match &obstruction.contract { + Some(contract) => { + hasher.update(&[1]); + update_contract_identity(hasher, contract); + } + None => { + hasher.update(&[0]); + } + } +} + +fn contract_obstruction_kind_tag(kind: ContractObstructionKind) -> u8 { + match kind { + ContractObstructionKind::UnsupportedOperation => 0, + ContractObstructionKind::UnsupportedQuery => 1, + ContractObstructionKind::AdmissionObstruction => 2, + ContractObstructionKind::RuntimeFault => 3, + ContractObstructionKind::MissingRetention => 4, + ContractObstructionKind::StaleBasis => 5, + ContractObstructionKind::ResidualReading => 6, + ContractObstructionKind::BudgetExceeded => 7, + } +} + +fn update_obstruction_subject(hasher: &mut Hasher, subject: &ContractObstructionSubject) { + match subject { + ContractObstructionSubject::Unspecified => { + hasher.update(&[0]); + } + ContractObstructionSubject::Operation { op_id } => { + hasher.update(&[1]); + hasher.update(&op_id.to_le_bytes()); + } + ContractObstructionSubject::Query { query_id } => { + hasher.update(&[2]); + hasher.update(&query_id.to_le_bytes()); + } + ContractObstructionSubject::Submission { submission_id } => { + hasher.update(&[3]); + hasher.update(submission_id); + } + ContractObstructionSubject::Ticket { ticket_digest } => { + hasher.update(&[4]); + hasher.update(ticket_digest); + } + ContractObstructionSubject::Reading { reading_id } => { + hasher.update(&[5]); + hasher.update(reading_id); + } + ContractObstructionSubject::Retention { retention_id } => { + hasher.update(&[6]); + hasher.update(retention_id); + } + ContractObstructionSubject::SchedulerFault { fault_id } => { + hasher.update(&[7]); + hasher.update(fault_id.as_bytes()); + } + } +} diff --git a/crates/warp-core/tests/retained_evidence_ref_tests.rs b/crates/warp-core/tests/retained_evidence_ref_tests.rs index 5b8f3dfb..3237200d 100644 --- a/crates/warp-core/tests/retained_evidence_ref_tests.rs +++ b/crates/warp-core/tests/retained_evidence_ref_tests.rs @@ -8,11 +8,13 @@ use warp_core::causal_wal::{ RecoveredRetentionIndexError, RetainedMaterialKind, RetainedMaterialRecord, }; use warp_core::{ - make_head_id, ContractEvidenceIdentity, ContractObstructionKind, ContractObstructionSubject, - ContractOperationKind, GlobalTick, InstalledContractPackageId, IntentOutcome, - IntentOutcomeDecision, IntentOutcomeObservation, ReceiptCorrelationRecord, - RetainedEvidenceCoordinate, RetainedEvidencePosture, RetainedEvidenceRef, RetainedEvidenceRole, - TickReceiptRejection, WorldlineId, WorldlineTick, WriterHeadKey, + make_head_id, ContractEvidenceIdentity, ContractObstruction, ContractObstructionKind, + ContractObstructionSubject, ContractOperationKind, GlobalTick, InstalledContractPackageId, + IntentOutcome, IntentOutcomeDecision, IntentOutcomeObservation, ReceiptCorrelationRecord, + RetainedEvidenceAccess, RetainedEvidenceBoundaryPosture, RetainedEvidenceCompleteness, + RetainedEvidenceCoordinate, RetainedEvidenceLayer, RetainedEvidenceOrigin, + RetainedEvidencePosture, RetainedEvidenceProofStrength, RetainedEvidenceRef, + RetainedEvidenceRole, TickReceiptRejection, WorldlineId, WorldlineTick, WriterHeadKey, }; fn hash(seed: u8) -> [u8; 32] { @@ -348,6 +350,199 @@ fn available_retained_evidence_is_not_an_obstruction() { assert_eq!(posture, RetainedEvidencePosture::Available(reference)); } +#[test] +fn native_and_fixture_retained_evidence_do_not_alias() { + let reference = RetainedEvidenceRef::new( + coordinate(RetainedEvidenceRole::Witness, 40), + content_hash(b"same witness bytes"), + 18, + ); + let native = RetainedEvidenceBoundaryPosture::available_citation( + reference.clone(), + RetainedEvidenceLayer::WitnessCore, + RetainedEvidenceOrigin::Native, + RetainedEvidenceProofStrength::Signature, + ); + let fixture = RetainedEvidenceBoundaryPosture::available_citation( + reference, + RetainedEvidenceLayer::WitnessCore, + RetainedEvidenceOrigin::Fixture, + RetainedEvidenceProofStrength::Signature, + ); + + assert_ne!(native, fixture); + assert_ne!(native.boundary_posture_id(), fixture.boundary_posture_id()); + assert_eq!(native.origin, RetainedEvidenceOrigin::Native); + assert_eq!(fixture.origin, RetainedEvidenceOrigin::Fixture); +} + +#[test] +fn redacted_retained_evidence_is_not_missing_content() { + let reference = RetainedEvidenceRef::new( + coordinate(RetainedEvidenceRole::ContractReceipt, 41), + content_hash(b"receipt shell bytes"), + 19, + ); + let redacted = RetainedEvidenceBoundaryPosture::redacted( + reference, + RetainedEvidenceLayer::ReceiptShell, + RetainedEvidenceOrigin::Native, + RetainedEvidenceProofStrength::ReplayCertificate, + ); + + assert_eq!(redacted.access, RetainedEvidenceAccess::Redacted); + assert_eq!( + redacted.completeness, + RetainedEvidenceCompleteness::DeclaredLost + ); + assert_ne!( + redacted + .obstruction + .as_ref() + .map(|obstruction| obstruction.kind), + Some(ContractObstructionKind::MissingRetention) + ); +} + +#[test] +fn unsupported_evidence_kind_obstructs_not_missing_retention() { + let coord = coordinate(RetainedEvidenceRole::Witness, 42); + let unsupported = RetainedEvidenceBoundaryPosture::unsupported_evidence_kind( + coord.clone(), + RetainedEvidenceLayer::WitnessCore, + RetainedEvidenceOrigin::Translated, + RetainedEvidenceProofStrength::ZkProof, + ); + + assert_eq!(unsupported.access, RetainedEvidenceAccess::Unsupported); + assert_eq!( + unsupported.completeness, + RetainedEvidenceCompleteness::Obstructed + ); + assert_eq!( + unsupported + .obstruction + .as_ref() + .map(|obstruction| &obstruction.subject), + Some(&ContractObstructionSubject::Retention { + retention_id: coord.coordinate_id() + }) + ); + assert_ne!( + unsupported + .obstruction + .as_ref() + .map(|obstruction| obstruction.kind), + Some(ContractObstructionKind::MissingRetention) + ); +} + +#[test] +fn boundary_posture_id_binds_obstruction_contract_evidence() { + let coord = coordinate(RetainedEvidenceRole::Witness, 46); + let base_obstruction = + ContractObstruction::admission_obstruction(ContractObstructionSubject::Retention { + retention_id: coord.coordinate_id(), + }); + let first = RetainedEvidenceBoundaryPosture { + coordinate: coord.clone(), + reference: None, + layer: RetainedEvidenceLayer::WitnessCore, + origin: RetainedEvidenceOrigin::Translated, + proof_strength: RetainedEvidenceProofStrength::Composite, + access: RetainedEvidenceAccess::AuthorityBlocked, + completeness: RetainedEvidenceCompleteness::Obstructed, + obstruction: Some(base_obstruction.clone().with_contract(contract( + 46, + 47, + ContractOperationKind::Mutation, + ))), + }; + let second = RetainedEvidenceBoundaryPosture { + obstruction: Some(base_obstruction.with_contract(contract( + 48, + 49, + ContractOperationKind::Mutation, + ))), + ..first.clone() + }; + + assert_ne!(first.obstruction, second.obstruction); + assert_ne!(first.boundary_posture_id(), second.boundary_posture_id()); +} + +#[test] +fn retained_content_hash_does_not_identify_semantic_evidence() { + let content_hash = content_hash(b"shared cold proof bytes"); + let first = RetainedEvidenceRef::new( + coordinate(RetainedEvidenceRole::ReadingPayload, 43), + content_hash, + 23, + ); + let second = RetainedEvidenceRef::new( + coordinate(RetainedEvidenceRole::ReadingPayload, 44), + content_hash, + 23, + ); + + let first_posture = RetainedEvidenceBoundaryPosture::available_citation( + first, + RetainedEvidenceLayer::ReintegrationCore, + RetainedEvidenceOrigin::Native, + RetainedEvidenceProofStrength::MerkleOpening, + ); + let second_posture = RetainedEvidenceBoundaryPosture::available_citation( + second, + RetainedEvidenceLayer::ReintegrationCore, + RetainedEvidenceOrigin::Native, + RetainedEvidenceProofStrength::MerkleOpening, + ); + + assert_eq!( + first_posture + .reference + .as_ref() + .map(|reference| reference.content_hash), + second_posture + .reference + .as_ref() + .map(|reference| reference.content_hash) + ); + assert_ne!( + first_posture.coordinate.coordinate_id(), + second_posture.coordinate.coordinate_id() + ); + assert_ne!( + first_posture.boundary_posture_id(), + second_posture.boundary_posture_id() + ); +} + +#[test] +fn available_retained_evidence_ref_does_not_grant_reveal() { + let reference = RetainedEvidenceRef::new( + coordinate(RetainedEvidenceRole::ReadingEnvelope, 45), + content_hash(b"reading envelope"), + 16, + ); + let citation = RetainedEvidenceBoundaryPosture::available_citation( + reference.clone(), + RetainedEvidenceLayer::ReintegrationCore, + RetainedEvidenceOrigin::Native, + RetainedEvidenceProofStrength::DigestOnly, + ); + let revealable = RetainedEvidenceBoundaryPosture::available_revealable( + reference, + RetainedEvidenceLayer::ReintegrationCore, + RetainedEvidenceOrigin::Native, + RetainedEvidenceProofStrength::DigestOnly, + ); + + assert_eq!(citation.access, RetainedEvidenceAccess::CitationOnly); + assert!(!citation.grants_reveal()); + assert!(revealable.grants_reveal()); +} + #[test] fn query_identity_does_not_imply_payload_retention() { let query_reading_id = hash(10); From ce89f5eff8231e6bdb093ccc7c02b54fa3adb550 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 10 Jul 2026 06:20:25 -0700 Subject: [PATCH 2/6] Fix: obstruct retained evidence access posture --- crates/warp-core/src/retained_evidence.rs | 43 +++++++++++++++---- .../tests/retained_evidence_ref_tests.rs | 39 +++++++++++++++++ 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/crates/warp-core/src/retained_evidence.rs b/crates/warp-core/src/retained_evidence.rs index 36bc9ec5..2e0929f3 100644 --- a/crates/warp-core/src/retained_evidence.rs +++ b/crates/warp-core/src/retained_evidence.rs @@ -423,15 +423,40 @@ impl RetainedEvidenceBoundaryPosture { proof_strength: RetainedEvidenceProofStrength, access: RetainedEvidenceAccess, ) -> Self { - Self { - coordinate: reference.coordinate.clone(), - reference: Some(reference), - layer, - origin, - proof_strength, - access, - completeness: RetainedEvidenceCompleteness::Complete, - obstruction: None, + match access { + RetainedEvidenceAccess::Revealable | RetainedEvidenceAccess::CitationOnly => Self { + coordinate: reference.coordinate.clone(), + reference: Some(reference), + layer, + origin, + proof_strength, + access, + completeness: RetainedEvidenceCompleteness::Complete, + obstruction: None, + }, + RetainedEvidenceAccess::Redacted => { + Self::redacted(reference, layer, origin, proof_strength) + } + RetainedEvidenceAccess::AuthorityBlocked + | RetainedEvidenceAccess::KeyUnavailable + | RetainedEvidenceAccess::Unsupported => { + let obstruction = ContractObstruction::admission_obstruction( + ContractObstructionSubject::Retention { + retention_id: reference.evidence_ref_id(), + }, + ) + .with_contract(reference.coordinate.contract.clone()); + Self { + coordinate: reference.coordinate.clone(), + reference: Some(reference), + layer, + origin, + proof_strength, + access, + completeness: RetainedEvidenceCompleteness::Obstructed, + obstruction: Some(obstruction), + } + } } } diff --git a/crates/warp-core/tests/retained_evidence_ref_tests.rs b/crates/warp-core/tests/retained_evidence_ref_tests.rs index 3237200d..55d6b77b 100644 --- a/crates/warp-core/tests/retained_evidence_ref_tests.rs +++ b/crates/warp-core/tests/retained_evidence_ref_tests.rs @@ -437,6 +437,45 @@ fn unsupported_evidence_kind_obstructs_not_missing_retention() { ); } +#[test] +fn obstructing_available_access_does_not_claim_complete_support() { + let reference = RetainedEvidenceRef::new( + coordinate(RetainedEvidenceRole::Witness, 47), + content_hash(b"blocked witness bytes"), + 21, + ); + let blocked = RetainedEvidenceBoundaryPosture::available_with_access( + reference.clone(), + RetainedEvidenceLayer::WitnessCore, + RetainedEvidenceOrigin::Native, + RetainedEvidenceProofStrength::Signature, + RetainedEvidenceAccess::AuthorityBlocked, + ); + + assert_eq!(blocked.access, RetainedEvidenceAccess::AuthorityBlocked); + assert_eq!( + blocked.completeness, + RetainedEvidenceCompleteness::Obstructed + ); + assert_eq!( + blocked + .obstruction + .as_ref() + .map(|obstruction| obstruction.kind), + Some(ContractObstructionKind::AdmissionObstruction) + ); + assert_eq!( + blocked + .obstruction + .as_ref() + .map(|obstruction| &obstruction.subject), + Some(&ContractObstructionSubject::Retention { + retention_id: reference.evidence_ref_id() + }) + ); + assert!(!blocked.grants_reveal()); +} + #[test] fn boundary_posture_id_binds_obstruction_contract_evidence() { let coord = coordinate(RetainedEvidenceRole::Witness, 46); From d57e2df022a7777fdb6c6d90f25215f7a01fd222 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 10 Jul 2026 06:23:43 -0700 Subject: [PATCH 3/6] Fix: require retained evidence ref for reveal --- crates/warp-core/src/retained_evidence.rs | 1 + .../tests/retained_evidence_ref_tests.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/crates/warp-core/src/retained_evidence.rs b/crates/warp-core/src/retained_evidence.rs index 2e0929f3..99afe43c 100644 --- a/crates/warp-core/src/retained_evidence.rs +++ b/crates/warp-core/src/retained_evidence.rs @@ -588,6 +588,7 @@ impl RetainedEvidenceBoundaryPosture { pub fn grants_reveal(&self) -> bool { self.access == RetainedEvidenceAccess::Revealable && self.completeness == RetainedEvidenceCompleteness::Complete + && self.reference.is_some() && self.obstruction.is_none() } } diff --git a/crates/warp-core/tests/retained_evidence_ref_tests.rs b/crates/warp-core/tests/retained_evidence_ref_tests.rs index 55d6b77b..608b7ca5 100644 --- a/crates/warp-core/tests/retained_evidence_ref_tests.rs +++ b/crates/warp-core/tests/retained_evidence_ref_tests.rs @@ -582,6 +582,22 @@ fn available_retained_evidence_ref_does_not_grant_reveal() { assert!(revealable.grants_reveal()); } +#[test] +fn revealable_boundary_posture_without_reference_does_not_grant_reveal() { + let posture = RetainedEvidenceBoundaryPosture { + coordinate: coordinate(RetainedEvidenceRole::ReadingEnvelope, 48), + reference: None, + layer: RetainedEvidenceLayer::ReintegrationCore, + origin: RetainedEvidenceOrigin::Native, + proof_strength: RetainedEvidenceProofStrength::DigestOnly, + access: RetainedEvidenceAccess::Revealable, + completeness: RetainedEvidenceCompleteness::Complete, + obstruction: None, + }; + + assert!(!posture.grants_reveal()); +} + #[test] fn query_identity_does_not_imply_payload_retention() { let query_reading_id = hash(10); From b62519945652d57175dec9b60f2a505cc87d1d1e Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 10 Jul 2026 06:53:18 -0700 Subject: [PATCH 4/6] Fix: obstruct redacted retained evidence --- crates/warp-core/src/retained_evidence.rs | 7 ++++++- .../tests/retained_evidence_ref_tests.rs | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/warp-core/src/retained_evidence.rs b/crates/warp-core/src/retained_evidence.rs index 99afe43c..34c33957 100644 --- a/crates/warp-core/src/retained_evidence.rs +++ b/crates/warp-core/src/retained_evidence.rs @@ -469,6 +469,11 @@ impl RetainedEvidenceBoundaryPosture { origin: RetainedEvidenceOrigin, proof_strength: RetainedEvidenceProofStrength, ) -> Self { + let obstruction = + ContractObstruction::admission_obstruction(ContractObstructionSubject::Retention { + retention_id: reference.evidence_ref_id(), + }) + .with_contract(reference.coordinate.contract.clone()); Self { coordinate: reference.coordinate.clone(), reference: Some(reference), @@ -477,7 +482,7 @@ impl RetainedEvidenceBoundaryPosture { proof_strength, access: RetainedEvidenceAccess::Redacted, completeness: RetainedEvidenceCompleteness::DeclaredLost, - obstruction: None, + obstruction: Some(obstruction), } } diff --git a/crates/warp-core/tests/retained_evidence_ref_tests.rs b/crates/warp-core/tests/retained_evidence_ref_tests.rs index 608b7ca5..de9e7460 100644 --- a/crates/warp-core/tests/retained_evidence_ref_tests.rs +++ b/crates/warp-core/tests/retained_evidence_ref_tests.rs @@ -384,7 +384,7 @@ fn redacted_retained_evidence_is_not_missing_content() { 19, ); let redacted = RetainedEvidenceBoundaryPosture::redacted( - reference, + reference.clone(), RetainedEvidenceLayer::ReceiptShell, RetainedEvidenceOrigin::Native, RetainedEvidenceProofStrength::ReplayCertificate, @@ -395,6 +395,22 @@ fn redacted_retained_evidence_is_not_missing_content() { redacted.completeness, RetainedEvidenceCompleteness::DeclaredLost ); + assert_eq!( + redacted + .obstruction + .as_ref() + .map(|obstruction| obstruction.kind), + Some(ContractObstructionKind::AdmissionObstruction) + ); + assert_eq!( + redacted + .obstruction + .as_ref() + .map(|obstruction| &obstruction.subject), + Some(&ContractObstructionSubject::Retention { + retention_id: reference.evidence_ref_id() + }) + ); assert_ne!( redacted .obstruction From bd6f3b0f3c2df2c73e56217764d8a6686ab72a37 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 10 Jul 2026 06:57:05 -0700 Subject: [PATCH 5/6] Fix: bind retained reveal to coordinate --- crates/warp-core/src/retained_evidence.rs | 5 ++++- .../tests/retained_evidence_ref_tests.rs | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/warp-core/src/retained_evidence.rs b/crates/warp-core/src/retained_evidence.rs index 34c33957..74cf037f 100644 --- a/crates/warp-core/src/retained_evidence.rs +++ b/crates/warp-core/src/retained_evidence.rs @@ -593,7 +593,10 @@ impl RetainedEvidenceBoundaryPosture { pub fn grants_reveal(&self) -> bool { self.access == RetainedEvidenceAccess::Revealable && self.completeness == RetainedEvidenceCompleteness::Complete - && self.reference.is_some() + && matches!( + self.reference.as_ref(), + Some(reference) if reference.coordinate == self.coordinate + ) && self.obstruction.is_none() } } diff --git a/crates/warp-core/tests/retained_evidence_ref_tests.rs b/crates/warp-core/tests/retained_evidence_ref_tests.rs index de9e7460..5ee1f417 100644 --- a/crates/warp-core/tests/retained_evidence_ref_tests.rs +++ b/crates/warp-core/tests/retained_evidence_ref_tests.rs @@ -614,6 +614,27 @@ fn revealable_boundary_posture_without_reference_does_not_grant_reveal() { assert!(!posture.grants_reveal()); } +#[test] +fn revealable_boundary_posture_requires_reference_coordinate_match() { + let reference = RetainedEvidenceRef::new( + coordinate(RetainedEvidenceRole::ReadingEnvelope, 49), + content_hash(b"mismatched reading envelope"), + 27, + ); + let posture = RetainedEvidenceBoundaryPosture { + coordinate: coordinate(RetainedEvidenceRole::ReadingEnvelope, 50), + reference: Some(reference), + layer: RetainedEvidenceLayer::ReintegrationCore, + origin: RetainedEvidenceOrigin::Native, + proof_strength: RetainedEvidenceProofStrength::DigestOnly, + access: RetainedEvidenceAccess::Revealable, + completeness: RetainedEvidenceCompleteness::Complete, + obstruction: None, + }; + + assert!(!posture.grants_reveal()); +} + #[test] fn query_identity_does_not_imply_payload_retention() { let query_reading_id = hash(10); From 65d88357293a4b39e4aede12a6630481836b7a29 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 10 Jul 2026 06:59:18 -0700 Subject: [PATCH 6/6] Fix: avoid retained evidence test shadowing --- crates/warp-core/tests/retained_evidence_ref_tests.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/warp-core/tests/retained_evidence_ref_tests.rs b/crates/warp-core/tests/retained_evidence_ref_tests.rs index 5ee1f417..07bb7973 100644 --- a/crates/warp-core/tests/retained_evidence_ref_tests.rs +++ b/crates/warp-core/tests/retained_evidence_ref_tests.rs @@ -528,15 +528,15 @@ fn boundary_posture_id_binds_obstruction_contract_evidence() { #[test] fn retained_content_hash_does_not_identify_semantic_evidence() { - let content_hash = content_hash(b"shared cold proof bytes"); + let shared_hash = content_hash(b"shared cold proof bytes"); let first = RetainedEvidenceRef::new( coordinate(RetainedEvidenceRole::ReadingPayload, 43), - content_hash, + shared_hash, 23, ); let second = RetainedEvidenceRef::new( coordinate(RetainedEvidenceRole::ReadingPayload, 44), - content_hash, + shared_hash, 23, );