diff --git a/CHANGELOG.md b/CHANGELOG.md index a48b88fe..98cb3a29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,11 @@ sealed-member salt effects. The vector metadata marks these identities as E1 scaffolding identity, and API docs now state that deterministic member blinding defaults are reproducibility tools, not unlinkability boundaries. +- `warp-core` now begins the third braids/strands roadmap goalpost with + append-only braid membership history. `BraidMembershipEntry` and + `Braid::membership_history()` expose accepted `MemberWoven` facts as a + read-only projection over the braid event log, while `frontier()` remains the + current membership projection. - `warp-core` now enforces the v1 single-writer-head strand invariant through both `Strand::new(...)` and `StrandRegistry::insert(...)`, and runtime strand forking constructs the registered relation through the same constructor diff --git a/crates/warp-core/src/braid.rs b/crates/warp-core/src/braid.rs index 04caea71..e81ef73b 100644 --- a/crates/warp-core/src/braid.rs +++ b/crates/warp-core/src/braid.rs @@ -127,6 +127,19 @@ pub enum BraidEvent { }, } +/// One accepted braid membership fact, projected from append-only event history. +/// +/// This is a read model, not an admission token. Constructing this value does +/// not weave a member into a braid; only [`Braid::apply`] can admit +/// [`BraidEvent::MemberWoven`] into braid history. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BraidMembershipEntry { + /// Reference to the strand, which may be revealed or sealed. + pub member_ref: BraidMemberRef, + /// Monotonically increasing sequence number from the accepted weave event. + pub sequence_num: u64, +} + /// Evolving state of a coordination braid reconstructed from its event log. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Braid { @@ -304,7 +317,34 @@ impl Braid { self.status } + /// Returns the accepted append-only membership history. + /// + /// The event log is the source of truth. This projection includes only + /// accepted [`BraidEvent::MemberWoven`] facts, so rejected duplicate, + /// incoherent, or late member events are never visible here. + #[must_use] + pub fn membership_history(&self) -> Vec { + self.events + .iter() + .filter_map(|event| match event { + BraidEvent::MemberWoven { + member_ref, + sequence_num, + } => Some(BraidMembershipEntry { + member_ref: *member_ref, + sequence_num: *sequence_num, + }), + BraidEvent::BraidCreated { .. } + | BraidEvent::SettlementFinalized { .. } + | BraidEvent::BraidCollapsed { .. } => None, + }) + .collect() + } + /// Returns the current coordination frontier (active woven members). + /// + /// This is the current projection over [`Self::membership_history`], not a + /// replacement for historical membership views. #[must_use] pub fn frontier(&self) -> &[BraidMemberRef] { &self.members diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 6dc47ad9..9c346385 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -260,7 +260,9 @@ pub use proof::{ ObserverHonestyClaim, ProofEnvelope, ProofError, ProofKind, VerificationFailureCode, }; // --- Braid Log types --- -pub use braid::{Braid, BraidError, BraidEvent, BraidStatus, BraidTransitionKind}; +pub use braid::{ + Braid, BraidError, BraidEvent, BraidMembershipEntry, BraidStatus, BraidTransitionKind, +}; // --- Retained boundary shell family (θ_tick, θ_braid) --- pub use braid_shell::{ collapse_braid_shell, replay_braid_shell, BraidCoordinate, BraidMemberRef, BraidShell, diff --git a/crates/warp-core/tests/braid_public_api_tests.rs b/crates/warp-core/tests/braid_public_api_tests.rs index 647eef5d..1bcf9cf7 100644 --- a/crates/warp-core/tests/braid_public_api_tests.rs +++ b/crates/warp-core/tests/braid_public_api_tests.rs @@ -5,8 +5,8 @@ use warp_core::{ make_strand_id, AuthorityDomainId, AuthorityDomainRef, Braid, BraidError, BraidEvent, - BraidMemberRef, BraidStatus, BraidTransitionKind, OriginId, ProofEnvelope, ProofError, - ProofKind, + BraidMemberRef, BraidMembershipEntry, BraidStatus, BraidTransitionKind, OriginId, + ProofEnvelope, ProofError, ProofKind, }; fn authority_ref() -> AuthorityDomainRef { @@ -39,21 +39,17 @@ fn crate_root_exports_braid_lifecycle_error_and_member_types() -> Result<(), Bra } #[test] -fn public_braid_transition_failures_are_typed() { +fn public_braid_transition_failures_are_typed() -> Result<(), BraidError> { let member_ref = BraidMemberRef::Revealed(make_strand_id("typed-transition-member")); let mut braid = Braid::new([0xAC; 32], authority_ref()); - braid - .apply(BraidEvent::MemberWoven { - member_ref, - sequence_num: 0, - }) - .expect("first member weave"); - braid - .apply(BraidEvent::SettlementFinalized { - settlement_digest: [0x5E; 32], - }) - .expect("settlement finalization"); + braid.apply(BraidEvent::MemberWoven { + member_ref, + sequence_num: 0, + })?; + braid.apply(BraidEvent::SettlementFinalized { + settlement_digest: [0x5E; 32], + })?; assert_eq!( braid.apply(BraidEvent::MemberWoven { @@ -65,6 +61,7 @@ fn public_braid_transition_failures_are_typed() { status: BraidStatus::Finalized, }) ); + Ok(()) } #[test] @@ -80,6 +77,59 @@ fn public_braid_transition_display_is_human_facing() { ); } +#[test] +fn public_braid_membership_history_is_append_only_event_history() -> Result<(), BraidError> { + let first = BraidMemberRef::Revealed(make_strand_id("history-member-a")); + let second = BraidMemberRef::Revealed(make_strand_id("history-member-b")); + let late = BraidMemberRef::Revealed(make_strand_id("history-member-late")); + let mut braid = Braid::new([0xAD; 32], authority_ref()); + + braid.apply(BraidEvent::MemberWoven { + member_ref: first, + sequence_num: 0, + })?; + assert_eq!( + braid.apply(BraidEvent::MemberWoven { + member_ref: first, + sequence_num: 1, + }), + Err(BraidError::DuplicateMember { member_ref: first }) + ); + braid.apply(BraidEvent::MemberWoven { + member_ref: second, + sequence_num: 1, + })?; + braid.apply(BraidEvent::SettlementFinalized { + settlement_digest: [0x5E; 32], + })?; + assert_eq!( + braid.apply(BraidEvent::MemberWoven { + member_ref: late, + sequence_num: 2, + }), + Err(BraidError::InvalidTransition { + transition: BraidTransitionKind::WeaveMember, + status: BraidStatus::Finalized, + }) + ); + + assert_eq!( + braid.membership_history(), + vec![ + BraidMembershipEntry { + member_ref: first, + sequence_num: 0, + }, + BraidMembershipEntry { + member_ref: second, + sequence_num: 1, + }, + ] + ); + assert_eq!(braid.frontier(), &[first, second]); + Ok(()) +} + #[test] fn public_proof_validation_failures_are_typed() { let expected = [0x42; 32]; diff --git a/docs/design/braids-and-strands-hardening/goalpost-03-historical-membership-and-replay.md b/docs/design/braids-and-strands-hardening/goalpost-03-historical-membership-and-replay.md index 9ae4c281..85889b95 100644 --- a/docs/design/braids-and-strands-hardening/goalpost-03-historical-membership-and-replay.md +++ b/docs/design/braids-and-strands-hardening/goalpost-03-historical-membership-and-replay.md @@ -3,7 +3,7 @@ # Goalpost 3: Historical Membership And Replay -Status: planned. +Status: active. GP3-S1 implemented. Roadmap: [`../braids-and-strands-roadmap.md`](../braids-and-strands-roadmap.md) @@ -41,6 +41,38 @@ This goalpost includes: - Braid Flight Recorder artifact shape; - Causal X-Ray lower-mode output target. +## Implementation Design + +GP3-S1 establishes the membership-history source of truth without implementing +the later coordinate, diff, replay, or recorder surfaces. + +The implementation boundary is: + +```text +BraidEvent::MemberWoven +-> BraidMembershipEntry +-> Braid::membership_history() +-> Braid::frontier() +``` + +`BraidEvent` remains the authoritative append-only log. A +`BraidMembershipEntry` is a read projection over one accepted `MemberWoven` +event; it is not an admission token and constructing it does not weave a +member. `Braid::membership_history()` projects accepted weave events from the +log in event order. Rejected duplicate, incoherent, mixed-posture, or late +member events never enter the log and therefore never appear in membership +history. + +`Braid::frontier()` remains the current membership projection. Later slices +will add coordinate-based views and diffs over the same event-log facts instead +of treating current membership as the substrate. + +This preserves three boundaries: + +1. Admission happens through `Braid::apply(...)`. +2. Membership history is derived from accepted events. +3. Current frontier is one projection, not the historical model. + ## Non-Goals This goalpost does not include: diff --git a/docs/design/braids-and-strands-roadmap.md b/docs/design/braids-and-strands-roadmap.md index d0969322..4b8d392d 100644 --- a/docs/design/braids-and-strands-roadmap.md +++ b/docs/design/braids-and-strands-roadmap.md @@ -88,7 +88,7 @@ Design: Design: [`goalpost-03-historical-membership-and-replay.md`](braids-and-strands-hardening/goalpost-03-historical-membership-and-replay.md) -- [ ] GP3-S1: Promote append-only braid membership into an implementation +- [x] GP3-S1: Promote append-only braid membership into an implementation design. - [ ] GP3-S2: Add historical membership views by coordinate or event sequence. - [ ] GP3-S3: Add membership diff facts for added, ended, revealed, and