lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 47addd903de6f67989dfaf53afaba148e6449721
parent adcf4016531edcd7dcb26c22c21479b3834d39ad
Author: triesap <tyson@radroots.org>
Date:   Thu, 16 Jul 2026 08:41:07 +0000

runtime: add Phase 1 prototype paths

- expose producer, buyer, and route coordinator path fixtures
- connect path steps to shared actions, objects, outbox, visibility, and authority
- cover required Phase 1 prototype paths in tests

Diffstat:
Mcrates/mobile_core/src/runtime/product_surface.rs | 346+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 346 insertions(+), 0 deletions(-)

diff --git a/crates/mobile_core/src/runtime/product_surface.rs b/crates/mobile_core/src/runtime/product_surface.rs @@ -218,6 +218,14 @@ pub enum OutboxBehavior { #[derive(uniffi::Enum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] +pub enum PrototypePathKind { + ProducerFoodToRoute, + BuyerCommitmentToRoute, + RouteCoordinatorAssignment, +} + +#[derive(uniffi::Enum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] pub enum OutboxState { NotQueued, Draft, @@ -406,6 +414,31 @@ pub struct SearchResultSummary { pub sync_state: SyncState, } +#[derive(uniffi::Record, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrototypePathStep { + pub id: String, + pub label: String, + pub context: ActiveContext, + pub action_type: Option<AddActionType>, + pub object_ref: Option<ObjectRef>, + pub authority_gate: AuthorityGate, + pub visibility: VisibilityClass, + pub outbox_state: OutboxState, + pub sync_state: SyncState, +} + +#[derive(uniffi::Record, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrototypePath { + pub id: String, + pub kind: PrototypePathKind, + pub title: String, + pub actor: WorkflowActor, + pub context: ActiveContext, + pub steps: Vec<PrototypePathStep>, +} + pub const CANONICAL_CONTEXT_TYPES: [ContextType; 10] = [ ContextType::Regional, ContextType::Network, @@ -1146,6 +1179,240 @@ pub fn fixture_search_results( .collect() } +fn prototype_path_step( + id: impl Into<String>, + label: impl Into<String>, + context: ActiveContext, + action_type: Option<AddActionType>, + object_ref: Option<ObjectRef>, + domain: AuthorityDomain, + action: AuthorityAction, + visibility: VisibilityClass, + outbox_state: OutboxState, + sync_state: SyncState, +) -> PrototypePathStep { + PrototypePathStep { + id: id.into(), + label: label.into(), + context: context.clone(), + action_type, + object_ref, + authority_gate: fixture_authority_gate(context.actor, context, domain, action), + visibility, + outbox_state, + sync_state, + } +} + +pub fn fixture_prototype_paths() -> Vec<PrototypePath> { + let farm = context_for_type(ContextType::Farm); + let buyer = context_for_type(ContextType::Buyer); + let route = context_for_type(ContextType::Route); + let route_partner = context_for_type(ContextType::RoutePartner); + let food_ref = object_ref(ObjectKind::Food, "food_harvest_001", "Summer squash lot"); + let route_ref = object_ref( + ObjectKind::Route, + "route_thursday_001", + "Thursday network loop", + ); + let buyer_request_ref = object_ref( + ObjectKind::BuyerPacket, + "buyer_commitment_001", + "Kitchen commitment packet", + ); + let exception_ref = object_ref( + ObjectKind::Exception, + "route_blocker_001", + "Missing pickup confirmation", + ); + + vec![ + PrototypePath { + id: "producer_food_to_route".to_string(), + kind: PrototypePathKind::ProducerFoodToRoute, + title: "Producer food to route".to_string(), + actor: farm.actor, + context: farm.clone(), + steps: vec![ + prototype_path_step( + "producer_today", + "Farm Today", + farm.clone(), + None, + Some(farm.context_ref.clone()), + AuthorityDomain::FarmWorkspaceOperations, + AuthorityAction::Search, + farm.visibility_scope, + OutboxState::NotQueued, + SyncState::Online, + ), + prototype_path_step( + "producer_add_food", + "Add Food", + farm.clone(), + Some(AddActionType::Food), + Some(food_ref.clone()), + AuthorityDomain::FarmWorkspaceOperations, + AuthorityAction::Submit, + VisibilityClass::NetworkVisible, + OutboxState::Draft, + SyncState::Offline, + ), + prototype_path_step( + "producer_add_to_route", + "Add to Route", + farm.clone(), + Some(AddActionType::RouteNeed), + Some(route_ref.clone()), + AuthorityDomain::FarmWorkspaceOperations, + AuthorityAction::Share, + VisibilityClass::RouteScoped, + OutboxState::Queued, + SyncState::Syncing, + ), + prototype_path_step( + "producer_food_page", + "Food page", + farm.clone(), + None, + Some(food_ref.clone()), + AuthorityDomain::FarmWorkspaceOperations, + AuthorityAction::NavigateRelatedObject, + VisibilityClass::NetworkVisible, + OutboxState::Shared, + SyncState::Synced, + ), + prototype_path_step( + "producer_route_page", + "Route page", + route.clone(), + None, + Some(route_ref.clone()), + AuthorityDomain::RouteCoordination, + AuthorityAction::NavigateRelatedObject, + VisibilityClass::RouteScoped, + OutboxState::NotQueued, + SyncState::Online, + ), + ], + }, + PrototypePath { + id: "buyer_commitment_to_route".to_string(), + kind: PrototypePathKind::BuyerCommitmentToRoute, + title: "Buyer commitment to route".to_string(), + actor: buyer.actor, + context: buyer.clone(), + steps: vec![ + prototype_path_step( + "buyer_today", + "Buyer Today", + buyer.clone(), + None, + Some(buyer.context_ref.clone()), + AuthorityDomain::BuyerWorkspace, + AuthorityAction::Search, + buyer.visibility_scope, + OutboxState::NotQueued, + SyncState::Online, + ), + prototype_path_step( + "buyer_request", + "Add Buyer Request", + buyer.clone(), + Some(AddActionType::BuyerRequest), + Some(buyer_request_ref.clone()), + AuthorityDomain::BuyerWorkspace, + AuthorityAction::Submit, + VisibilityClass::BuyerScoped, + OutboxState::Draft, + SyncState::Offline, + ), + prototype_path_step( + "buyer_commitment", + "Confirm Commitment", + buyer.clone(), + Some(AddActionType::BuyerCommitment), + Some(buyer_request_ref), + AuthorityDomain::BuyerWorkspace, + AuthorityAction::Approve, + VisibilityClass::BuyerScoped, + OutboxState::Queued, + SyncState::Syncing, + ), + prototype_path_step( + "buyer_route", + "Route", + route.clone(), + None, + Some(route_ref.clone()), + AuthorityDomain::RouteCoordination, + AuthorityAction::NavigateRelatedObject, + VisibilityClass::RouteScoped, + OutboxState::Shared, + SyncState::Synced, + ), + ], + }, + PrototypePath { + id: "route_coordinator_assignment".to_string(), + kind: PrototypePathKind::RouteCoordinatorAssignment, + title: "Route coordinator assignment".to_string(), + actor: route.actor, + context: route.clone(), + steps: vec![ + prototype_path_step( + "route_today", + "Route Today", + route.clone(), + None, + Some(route_ref.clone()), + AuthorityDomain::RouteCoordination, + AuthorityAction::Search, + VisibilityClass::RouteScoped, + OutboxState::NotQueued, + SyncState::Online, + ), + prototype_path_step( + "route_page", + "Route page", + route.clone(), + None, + Some(route_ref), + AuthorityDomain::RouteCoordination, + AuthorityAction::NavigateRelatedObject, + VisibilityClass::RouteScoped, + OutboxState::NotQueued, + SyncState::Online, + ), + prototype_path_step( + "route_resolve_blocker", + "Resolve blocker", + route.clone(), + Some(AddActionType::Exception), + Some(exception_ref), + AuthorityDomain::RouteCoordination, + AuthorityAction::Close, + VisibilityClass::RouteScoped, + OutboxState::Conflict, + SyncState::Failed, + ), + prototype_path_step( + "route_assign_partner", + "Assign RoutePartner", + route, + Some(AddActionType::RouteNeed), + Some(route_partner.context_ref.clone()), + AuthorityDomain::RouteCoordination, + AuthorityAction::Assign, + VisibilityClass::RouteScoped, + OutboxState::Queued, + SyncState::Syncing, + ), + ], + }, + ] +} + pub fn fixture_outbox_items() -> Vec<OutboxItem> { let context = context_for_type(ContextType::Farm); CANONICAL_OUTBOX_STATES @@ -1271,6 +1538,11 @@ impl RadrootsRuntime { fixture_search_results(query, context_id) } + pub fn phase1_prototype_paths(&self) -> Vec<PrototypePath> { + let _ = self; + fixture_prototype_paths() + } + pub fn phase1_outbox_retry_decision(&self, item: OutboxItem) -> OutboxRetryDecision { let _ = self; fixture_outbox_retry_decision(item) @@ -1476,6 +1748,80 @@ mod tests { } #[test] + fn prototype_paths_cover_required_phase_1_actor_routes() { + let paths = fixture_prototype_paths(); + assert_eq!(paths.len(), 3); + assert!( + paths + .iter() + .any(|path| path.kind == PrototypePathKind::ProducerFoodToRoute + && path.actor == WorkflowActor::ProducerAdmin + && path.steps.iter().any(|step| step.label == "Add Food") + && path.steps.iter().any(|step| step.label == "Add to Route")) + ); + assert!(paths.iter().any(|path| { + path.kind == PrototypePathKind::BuyerCommitmentToRoute + && path.actor == WorkflowActor::BuyerSourcingLead + && path + .steps + .iter() + .any(|step| step.label == "Confirm Commitment") + })); + assert!(paths.iter().any(|path| { + path.kind == PrototypePathKind::RouteCoordinatorAssignment + && path.actor == WorkflowActor::RouteCoordinator + && path + .steps + .iter() + .any(|step| step.label == "Assign RoutePartner") + })); + } + + #[test] + fn prototype_paths_connect_actions_objects_outbox_and_authority() { + let paths = fixture_prototype_paths(); + let steps: Vec<&PrototypePathStep> = + paths.iter().flat_map(|path| path.steps.iter()).collect(); + + assert!( + steps + .iter() + .any(|step| step.action_type == Some(AddActionType::Food)) + ); + assert!(steps.iter().any(|step| { + step.object_ref + .as_ref() + .is_some_and(|object_ref| object_ref.object_type == ObjectKind::Food) + })); + assert!(steps.iter().any(|step| { + step.object_ref + .as_ref() + .is_some_and(|object_ref| object_ref.object_type == ObjectKind::Route) + })); + assert!(steps.iter().any(|step| { + step.object_ref + .as_ref() + .is_some_and(|object_ref| object_ref.object_type == ObjectKind::RoutePartner) + })); + assert!( + steps + .iter() + .any(|step| step.outbox_state == OutboxState::Queued) + ); + assert!( + steps + .iter() + .any(|step| step.outbox_state == OutboxState::Conflict) + ); + assert!(steps.iter().all(|step| step.authority_gate.is_required)); + assert!( + steps + .iter() + .all(|step| step.visibility != VisibilityClass::SecretNeverShared) + ); + } + + #[test] fn serde_names_preserve_product_vocabulary() { assert_eq!( serde_json::to_value(WorkflowActor::NetworkMember).expect("serialize actor"),