diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a289806..4bb65913 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ evidence, keeping materialized projection bytes readable as schema/count facts while rejecting them as causal-history import material or WAL recovery authority without manifests and segment evidence. +- `warp-core` now exposes a versioned WSC causal-history export profile model + for `ref-only`, `self-contained`, and `CAS-addressed` profiles, including the + evidence each profile must carry and an explicit CAS byte-retention posture + that does not promote CAS hashes into causal authority. - `cargo xtask test-slice durable-runtime-wal` now runs the release-grade filesystem runtime WAL durability gate, joining filesystem ACK recovery, filesystem failure atomicity, CLI submission posture JSON, stale-claim, and diff --git a/crates/warp-core/src/wsc/mod.rs b/crates/warp-core/src/wsc/mod.rs index c9db447b..29c6ed53 100644 --- a/crates/warp-core/src/wsc/mod.rs +++ b/crates/warp-core/src/wsc/mod.rs @@ -76,10 +76,14 @@ pub use store::{ retention_records_from_wsc_envelope, retention_records_from_wsc_store, retention_records_to_wsc_envelope, topology_records_from_wsc_envelope, topology_records_from_wsc_store, topology_records_to_wsc_envelope, - validate_wsc_causal_history_store, InMemoryWscStore, WscReceiptCorrelationRecords, - WscRetentionRecords, WscStoreEnvelope, WscStoreEnvelopeId, WscStoreObstruction, - WscStoreObstructionKind, WscStorePort, WscStoreRecordKind, WscStoreSubject, - WscStoreWriteReceipt, WscTopologyRecords, + validate_wsc_causal_history_store, wsc_causal_history_export_profile, + wsc_causal_history_export_profiles, InMemoryWscStore, WscCausalHistoryCasAuthority, + WscCausalHistoryExportEvidence, WscCausalHistoryExportProfile, + WscCausalHistoryExportProfileKind, WscCausalHistoryExportValidationMaterial, + WscReceiptCorrelationRecords, WscRetentionRecords, WscStoreEnvelope, WscStoreEnvelopeId, + WscStoreObstruction, WscStoreObstructionKind, WscStorePort, WscStoreRecordKind, + WscStoreSubject, WscStoreWriteReceipt, WscTopologyRecords, + WSC_CAUSAL_HISTORY_EXPORT_PROFILE_VERSION, }; pub use validate::validate_wsc; pub use view::{AttachmentRef, WarpView, WscFile}; diff --git a/crates/warp-core/src/wsc/store.rs b/crates/warp-core/src/wsc/store.rs index 0bffa1fb..c1ffc2fc 100644 --- a/crates/warp-core/src/wsc/store.rs +++ b/crates/warp-core/src/wsc/store.rs @@ -125,6 +125,153 @@ impl WscStoreRecordKind { } } +/// Current version of the causal-history WSC export profile model. +pub const WSC_CAUSAL_HISTORY_EXPORT_PROFILE_VERSION: u16 = 1; + +/// Versioned causal-history WSC export profile kind. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum WscCausalHistoryExportProfileKind { + /// Reference-only export carrying graph facts and storage references. + RefOnly, + /// Self-contained export carrying embedded validation material. + SelfContained, + /// CAS-addressed export carrying byte hashes plus semantic references. + CasAddressed, +} + +impl WscCausalHistoryExportProfileKind { + /// Returns the stable profile label used in manifests and tests. + #[must_use] + pub const fn label(self) -> &'static str { + match self { + Self::RefOnly => "ref-only", + Self::SelfContained => "self-contained", + Self::CasAddressed => "CAS-addressed", + } + } +} + +/// Evidence requirement for a causal-history WSC export profile. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WscCausalHistoryExportEvidence { + /// The profile must carry this evidence. + Required, + /// The profile may carry this evidence, but consumers must not rely on it. + Optional, + /// The profile must not carry this evidence. + Forbidden, +} + +/// Validation material mode for a causal-history WSC export profile. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WscCausalHistoryExportValidationMaterial { + /// Validation depends on external WAL storage named by locators. + ExternalWalStorageRefs, + /// Validation can use embedded segment bytes or retained material. + EmbeddedSegmentBytesOrRetainedMaterial, + /// Validation names CAS bytes through content hashes and semantic refs. + CasHashesWithSemanticRefs, +} + +/// CAS authority posture for a causal-history WSC export profile. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WscCausalHistoryCasAuthority { + /// CAS names retained bytes only and is not causal authority. + ByteRetentionOnly, +} + +/// Versioned causal-history WSC export profile evidence requirements. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WscCausalHistoryExportProfile { + /// Profile model version. + pub version: u16, + /// Export profile kind. + pub kind: WscCausalHistoryExportProfileKind, + /// Required validation material mode. + pub validation_material: WscCausalHistoryExportValidationMaterial, + /// Projected graph facts requirement. + pub projected_graph_facts: WscCausalHistoryExportEvidence, + /// Segment locator requirement. + pub segment_locators: WscCausalHistoryExportEvidence, + /// Segment digest requirement. + pub segment_digests: WscCausalHistoryExportEvidence, + /// LSN range requirement. + pub lsn_ranges: WscCausalHistoryExportEvidence, + /// Commit anchor requirement. + pub commit_anchors: WscCausalHistoryExportEvidence, + /// Embedded segment bytes or retained material requirement. + pub embedded_segment_bytes_or_retained_material: WscCausalHistoryExportEvidence, + /// CAS content hash requirement. + pub cas_content_hashes: WscCausalHistoryExportEvidence, + /// Semantic reference requirement for CAS-addressed material. + pub semantic_refs: WscCausalHistoryExportEvidence, + /// CAS authority posture, when the profile uses CAS-addressed material. + pub cas_authority: Option, +} + +/// Returns the evidence requirements for one causal-history WSC export profile. +#[must_use] +pub const fn wsc_causal_history_export_profile( + kind: WscCausalHistoryExportProfileKind, +) -> WscCausalHistoryExportProfile { + match kind { + WscCausalHistoryExportProfileKind::RefOnly => WscCausalHistoryExportProfile { + version: WSC_CAUSAL_HISTORY_EXPORT_PROFILE_VERSION, + kind, + validation_material: WscCausalHistoryExportValidationMaterial::ExternalWalStorageRefs, + projected_graph_facts: WscCausalHistoryExportEvidence::Required, + segment_locators: WscCausalHistoryExportEvidence::Required, + segment_digests: WscCausalHistoryExportEvidence::Required, + lsn_ranges: WscCausalHistoryExportEvidence::Required, + commit_anchors: WscCausalHistoryExportEvidence::Required, + embedded_segment_bytes_or_retained_material: WscCausalHistoryExportEvidence::Forbidden, + cas_content_hashes: WscCausalHistoryExportEvidence::Forbidden, + semantic_refs: WscCausalHistoryExportEvidence::Forbidden, + cas_authority: None, + }, + WscCausalHistoryExportProfileKind::SelfContained => WscCausalHistoryExportProfile { + version: WSC_CAUSAL_HISTORY_EXPORT_PROFILE_VERSION, + kind, + validation_material: + WscCausalHistoryExportValidationMaterial::EmbeddedSegmentBytesOrRetainedMaterial, + projected_graph_facts: WscCausalHistoryExportEvidence::Required, + segment_locators: WscCausalHistoryExportEvidence::Optional, + segment_digests: WscCausalHistoryExportEvidence::Required, + lsn_ranges: WscCausalHistoryExportEvidence::Required, + commit_anchors: WscCausalHistoryExportEvidence::Required, + embedded_segment_bytes_or_retained_material: WscCausalHistoryExportEvidence::Required, + cas_content_hashes: WscCausalHistoryExportEvidence::Forbidden, + semantic_refs: WscCausalHistoryExportEvidence::Forbidden, + cas_authority: None, + }, + WscCausalHistoryExportProfileKind::CasAddressed => WscCausalHistoryExportProfile { + version: WSC_CAUSAL_HISTORY_EXPORT_PROFILE_VERSION, + kind, + validation_material: + WscCausalHistoryExportValidationMaterial::CasHashesWithSemanticRefs, + projected_graph_facts: WscCausalHistoryExportEvidence::Required, + segment_locators: WscCausalHistoryExportEvidence::Forbidden, + segment_digests: WscCausalHistoryExportEvidence::Required, + lsn_ranges: WscCausalHistoryExportEvidence::Required, + commit_anchors: WscCausalHistoryExportEvidence::Required, + embedded_segment_bytes_or_retained_material: WscCausalHistoryExportEvidence::Forbidden, + cas_content_hashes: WscCausalHistoryExportEvidence::Required, + semantic_refs: WscCausalHistoryExportEvidence::Required, + cas_authority: Some(WscCausalHistoryCasAuthority::ByteRetentionOnly), + }, + } +} + +/// Returns all supported causal-history WSC export profiles. +#[must_use] +pub const fn wsc_causal_history_export_profiles() -> [WscCausalHistoryExportProfile; 3] { + [ + wsc_causal_history_export_profile(WscCausalHistoryExportProfileKind::RefOnly), + wsc_causal_history_export_profile(WscCausalHistoryExportProfileKind::SelfContained), + wsc_causal_history_export_profile(WscCausalHistoryExportProfileKind::CasAddressed), + ] +} + /// Subject named by a WSC store obstruction. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum WscStoreSubject { diff --git a/crates/warp-core/tests/wsc_store_tests.rs b/crates/warp-core/tests/wsc_store_tests.rs index a1f512e3..fb80086d 100644 --- a/crates/warp-core/tests/wsc_store_tests.rs +++ b/crates/warp-core/tests/wsc_store_tests.rs @@ -23,8 +23,12 @@ use warp_core::wsc::{ retention_records_from_wsc_envelope, retention_records_from_wsc_store, retention_records_to_wsc_envelope, topology_records_from_wsc_envelope, topology_records_from_wsc_store, topology_records_to_wsc_envelope, - validate_wsc_causal_history_store, write_wsc_one_warp, InMemoryWscStore, OneWarpInput, - WscStoreEnvelope, WscStoreObstructionKind, WscStorePort, WscStoreRecordKind, WscStoreSubject, + validate_wsc_causal_history_store, write_wsc_one_warp, wsc_causal_history_export_profile, + wsc_causal_history_export_profiles, InMemoryWscStore, OneWarpInput, + WscCausalHistoryCasAuthority, WscCausalHistoryExportEvidence, + WscCausalHistoryExportProfileKind, WscCausalHistoryExportValidationMaterial, WscStoreEnvelope, + WscStoreObstructionKind, WscStorePort, WscStoreRecordKind, WscStoreSubject, + WSC_CAUSAL_HISTORY_EXPORT_PROFILE_VERSION, }; use warp_core::{ make_node_id, make_strand_id, make_type_id, make_warp_id, AuthorityDomainId, @@ -157,6 +161,157 @@ fn wsc_store_module_has_no_jedit_nouns() { assert!(!source.to_lowercase().contains("jedit")); } +#[test] +fn wsc_causal_history_export_profiles_cover_required_evidence() { + let profiles = wsc_causal_history_export_profiles(); + assert_eq!(profiles.len(), 3); + assert_eq!( + profiles.map(|profile| profile.kind), + [ + WscCausalHistoryExportProfileKind::RefOnly, + WscCausalHistoryExportProfileKind::SelfContained, + WscCausalHistoryExportProfileKind::CasAddressed, + ] + ); + assert!(profiles + .iter() + .all(|profile| profile.version == WSC_CAUSAL_HISTORY_EXPORT_PROFILE_VERSION)); + assert_eq!( + WscCausalHistoryExportProfileKind::RefOnly.label(), + "ref-only" + ); + assert_eq!( + WscCausalHistoryExportProfileKind::SelfContained.label(), + "self-contained" + ); + assert_eq!( + WscCausalHistoryExportProfileKind::CasAddressed.label(), + "CAS-addressed" + ); + + let ref_only = wsc_causal_history_export_profile(WscCausalHistoryExportProfileKind::RefOnly); + assert_eq!( + ref_only.validation_material, + WscCausalHistoryExportValidationMaterial::ExternalWalStorageRefs + ); + assert_eq!( + ref_only.projected_graph_facts, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + ref_only.segment_locators, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + ref_only.segment_digests, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + ref_only.lsn_ranges, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + ref_only.commit_anchors, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + ref_only.embedded_segment_bytes_or_retained_material, + WscCausalHistoryExportEvidence::Forbidden + ); + assert_eq!( + ref_only.cas_content_hashes, + WscCausalHistoryExportEvidence::Forbidden + ); + assert_eq!( + ref_only.semantic_refs, + WscCausalHistoryExportEvidence::Forbidden + ); + assert_eq!(ref_only.cas_authority, None); + + let self_contained = + wsc_causal_history_export_profile(WscCausalHistoryExportProfileKind::SelfContained); + assert_eq!( + self_contained.validation_material, + WscCausalHistoryExportValidationMaterial::EmbeddedSegmentBytesOrRetainedMaterial + ); + assert_eq!( + self_contained.projected_graph_facts, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + self_contained.segment_locators, + WscCausalHistoryExportEvidence::Optional + ); + assert_eq!( + self_contained.segment_digests, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + self_contained.lsn_ranges, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + self_contained.commit_anchors, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + self_contained.embedded_segment_bytes_or_retained_material, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + self_contained.cas_content_hashes, + WscCausalHistoryExportEvidence::Forbidden + ); + assert_eq!( + self_contained.semantic_refs, + WscCausalHistoryExportEvidence::Forbidden + ); + assert_eq!(self_contained.cas_authority, None); + + let cas_addressed = + wsc_causal_history_export_profile(WscCausalHistoryExportProfileKind::CasAddressed); + assert_eq!( + cas_addressed.validation_material, + WscCausalHistoryExportValidationMaterial::CasHashesWithSemanticRefs + ); + assert_eq!( + cas_addressed.projected_graph_facts, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + cas_addressed.segment_locators, + WscCausalHistoryExportEvidence::Forbidden + ); + assert_eq!( + cas_addressed.segment_digests, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + cas_addressed.lsn_ranges, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + cas_addressed.commit_anchors, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + cas_addressed.embedded_segment_bytes_or_retained_material, + WscCausalHistoryExportEvidence::Forbidden + ); + assert_eq!( + cas_addressed.cas_content_hashes, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + cas_addressed.semantic_refs, + WscCausalHistoryExportEvidence::Required + ); + assert_eq!( + cas_addressed.cas_authority, + Some(WscCausalHistoryCasAuthority::ByteRetentionOnly) + ); +} + #[test] fn accepted_submission_records_round_trip_through_wsc_envelope() { let duplicate = submission_acceptance(1, 11); @@ -632,10 +787,12 @@ fn topology_records_ignore_uncommitted_staged_wsc_envelope() { #[test] fn topology_records_reject_conflicting_duplicate_strand_fork() { let mut records = topology_records(); - let mut conflicting = match &records[0] { - TopologyIntentRecord::StrandFork(record) => record.clone(), - _ => panic!("expected strand fork fixture"), - }; + let mut conflicting = if let TopologyIntentRecord::StrandFork(record) = &records[0] { + Some(record.clone()) + } else { + None + } + .expect("expected strand fork fixture"); conflicting.child_worldline_id = worldline(99); records.push(TopologyIntentRecord::StrandFork(conflicting)); @@ -654,10 +811,12 @@ fn topology_records_reject_conflicting_duplicate_strand_fork() { fn topology_records_reject_idempotency_conflicts_for_each_record_family() { let records = topology_records(); - let fork = match &records[0] { - TopologyIntentRecord::StrandFork(record) => record.clone(), - _ => panic!("expected strand fork fixture"), - }; + let fork = if let TopologyIntentRecord::StrandFork(record) = &records[0] { + Some(record.clone()) + } else { + None + } + .expect("expected strand fork fixture"); let mut conflicting_fork = fork.clone(); conflicting_fork.strand_id = make_strand_id("wsc-topology-other-strand"); conflicting_fork.child_worldline_id = worldline(52); @@ -671,10 +830,12 @@ fn topology_records_reject_idempotency_conflicts_for_each_record_family() { WscStoreObstructionKind::DuplicateEnvelopeMismatch ); - let drop = match &records[1] { - TopologyIntentRecord::StrandDrop(record) => record.clone(), - _ => panic!("expected strand drop fixture"), - }; + let drop = if let TopologyIntentRecord::StrandDrop(record) = &records[1] { + Some(record.clone()) + } else { + None + } + .expect("expected strand drop fixture"); let mut conflicting_drop = drop.clone(); conflicting_drop.strand_id = make_strand_id("wsc-topology-other-drop"); conflicting_drop.child_worldline_id = worldline(53); @@ -688,10 +849,12 @@ fn topology_records_reject_idempotency_conflicts_for_each_record_family() { WscStoreObstructionKind::DuplicateEnvelopeMismatch ); - let braid_event = match &records[2] { - TopologyIntentRecord::BraidEvent(record) => record.clone(), - _ => panic!("expected braid event fixture"), - }; + let braid_event = if let TopologyIntentRecord::BraidEvent(record) = &records[2] { + Some(record.clone()) + } else { + None + } + .expect("expected braid event fixture"); let mut conflicting_braid_event = braid_event.clone(); conflicting_braid_event.event_index = 1; let obstruction = topology_records_to_wsc_envelope(&[ @@ -704,10 +867,12 @@ fn topology_records_reject_idempotency_conflicts_for_each_record_family() { WscStoreObstructionKind::DuplicateEnvelopeMismatch ); - let braid_shell = match &records[3] { - TopologyIntentRecord::BraidShell(record) => record.clone(), - _ => panic!("expected braid shell fixture"), - }; + let braid_shell = if let TopologyIntentRecord::BraidShell(record) = &records[3] { + Some(record.clone()) + } else { + None + } + .expect("expected braid shell fixture"); let mut conflicting_braid_shell = braid_shell.clone(); conflicting_braid_shell.shell_digest = [126; 32]; conflicting_braid_shell.material_digest = [127; 32]; @@ -724,10 +889,12 @@ fn topology_records_reject_idempotency_conflicts_for_each_record_family() { #[test] fn topology_records_reject_root_level_topology_attachment() { - let fork = match &topology_records()[0] { - TopologyIntentRecord::StrandFork(record) => record.clone(), - _ => panic!("expected strand fork fixture"), - }; + let fork = if let TopologyIntentRecord::StrandFork(record) = &topology_records()[0] { + Some(record.clone()) + } else { + None + } + .expect("expected strand fork fixture"); let envelope = topology_envelope_with_root_attachment(TopologyIntentRecord::StrandFork(fork)); let obstruction = topology_records_from_wsc_envelope(&envelope) @@ -905,7 +1072,7 @@ fn fixture_wsc_bytes(tick: u64) -> Vec { } fn topology_envelope_with_root_attachment(record: TopologyIntentRecord) -> WscStoreEnvelope { - let canonical = topology_records_to_wsc_envelope(&[record.clone()]) + let canonical = topology_records_to_wsc_envelope(std::slice::from_ref(&record)) .expect("canonical topology WSC envelope"); let payload = record.to_payload_bytes(); let root = make_node_id("echo/wsc-store/topology/root");