field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

commit 80a22209a357abf6c31ecd405e68ab1ddd8eaeaa
parent ebd589a3048c8c94e9c8715c2b0b412b6de9845f
Author: triesap <tyson@radroots.org>
Date:   Thu, 16 Jul 2026 09:24:47 +0000

runtime: add Phase 1 stewardship access items

- model admin-lite stewardship and member access states
- keep full group management explicitly deferred
- cover NetworkSteward private-access boundaries in tests

Diffstat:
Mcore/crates/tera_core/src/runtime/product_surface.rs | 477+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 477 insertions(+), 0 deletions(-)

diff --git a/core/crates/tera_core/src/runtime/product_surface.rs b/core/crates/tera_core/src/runtime/product_surface.rs @@ -267,6 +267,20 @@ pub enum ProofProvenanceReviewState { #[derive(uniffi::Enum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] +pub enum StewardshipAccessItemKind { + AccessRequestReview, + RoleApproval, + RoutePartnerInvite, + RoutePoolMetadata, + PublicModeration, + InviteAcceptance, + RequestAccess, + AccessDenied, + GroupManagementDeferred, +} + +#[derive(uniffi::Enum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] pub enum OutboxState { NotQueued, Draft, @@ -534,6 +548,25 @@ pub struct ProofProvenanceArtifact { pub sync_state: SyncState, } +#[derive(uniffi::Record, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StewardshipAccessItem { + pub id: String, + pub kind: StewardshipAccessItemKind, + pub title: String, + pub actor: WorkflowActor, + pub context: ActiveContext, + pub target_ref: ObjectRef, + pub required_authority: AuthorityGate, + pub visibility: VisibilityClass, + pub is_admin_lite: bool, + pub is_phase_2_deferred: bool, + pub grants_private_access: bool, + pub outbox_state: OutboxState, + pub sync_state: SyncState, + pub detail_lines: Vec<String>, +} + pub const CANONICAL_CONTEXT_TYPES: [ContextType; 10] = [ ContextType::Regional, ContextType::Network, @@ -727,6 +760,18 @@ pub const CANONICAL_PROOF_PROVENANCE_REVIEW_STATES: [ProofProvenanceReviewState; ProofProvenanceReviewState::Published, ]; +pub const CANONICAL_STEWARDSHIP_ACCESS_ITEM_KINDS: [StewardshipAccessItemKind; 9] = [ + StewardshipAccessItemKind::AccessRequestReview, + StewardshipAccessItemKind::RoleApproval, + StewardshipAccessItemKind::RoutePartnerInvite, + StewardshipAccessItemKind::RoutePoolMetadata, + StewardshipAccessItemKind::PublicModeration, + StewardshipAccessItemKind::InviteAcceptance, + StewardshipAccessItemKind::RequestAccess, + StewardshipAccessItemKind::AccessDenied, + StewardshipAccessItemKind::GroupManagementDeferred, +]; + fn object_ref( object_type: ObjectKind, object_id: impl Into<String>, @@ -2129,6 +2174,296 @@ pub fn fixture_proof_provenance_artifacts( } } +struct StewardshipAccessItemFixture { + id: &'static str, + kind: StewardshipAccessItemKind, + title: &'static str, + actor: WorkflowActor, + context: ActiveContext, + target_ref: ObjectRef, + domain: AuthorityDomain, + action: AuthorityAction, + visibility: VisibilityClass, + is_admin_lite: bool, + is_phase_2_deferred: bool, + grants_private_access: bool, + outbox_state: OutboxState, + sync_state: SyncState, + detail_lines: Vec<&'static str>, +} + +fn stewardship_access_item(fixture: StewardshipAccessItemFixture) -> StewardshipAccessItem { + StewardshipAccessItem { + id: fixture.id.to_string(), + kind: fixture.kind, + title: fixture.title.to_string(), + actor: fixture.actor, + context: fixture.context.clone(), + target_ref: fixture.target_ref, + required_authority: fixture_authority_gate( + fixture.actor, + fixture.context, + fixture.domain, + fixture.action, + ), + visibility: fixture.visibility, + is_admin_lite: fixture.is_admin_lite, + is_phase_2_deferred: fixture.is_phase_2_deferred, + grants_private_access: fixture.grants_private_access, + outbox_state: fixture.outbox_state, + sync_state: fixture.sync_state, + detail_lines: fixture + .detail_lines + .into_iter() + .map(str::to_string) + .collect(), + } +} + +fn all_stewardship_access_items() -> Vec<StewardshipAccessItem> { + let steward = context_for_type(ContextType::NetworkSteward); + let network = context_for_type(ContextType::Network); + let route = context_for_type(ContextType::Route); + let member = context_for_type(ContextType::Regional); + let access_request_ref = object_ref( + ObjectKind::AccessMembership, + "access_request_farm_team_001", + "Farm team access request", + ); + let role_ref = object_ref( + ObjectKind::AccessMembership, + "role_route_partner_candidate_001", + "Route partner candidate role", + ); + let route_partner_ref = object_ref( + ObjectKind::RoutePartner, + "route_partner_invite_001", + "Thursday route partner invite", + ); + let route_pool_ref = object_ref( + ObjectKind::Route, + "route_pool_metadata_001", + "Route pool metadata", + ); + let public_update_ref = object_ref( + ObjectKind::Update, + "community_update_review_001", + "Community update moderation", + ); + let invite_ref = object_ref( + ObjectKind::MemberInvite, + "member_invite_001", + "Network invite", + ); + let request_ref = object_ref( + ObjectKind::AccessMembership, + "access_request_self_001", + "Request network access", + ); + let denied_ref = object_ref( + ObjectKind::Farm, + "private_farm_denied_001", + "Private farm context", + ); + let group_ref = object_ref( + ObjectKind::AccessMembership, + "phase2_group_management_001", + "Group management", + ); + + vec![ + stewardship_access_item(StewardshipAccessItemFixture { + id: "steward_review_access_request", + kind: StewardshipAccessItemKind::AccessRequestReview, + title: "Review access request", + actor: WorkflowActor::NetworkSteward, + context: steward.clone(), + target_ref: access_request_ref, + domain: AuthorityDomain::RelayGroupAccess, + action: AuthorityAction::Approve, + visibility: VisibilityClass::WorkspacePrivate, + is_admin_lite: true, + is_phase_2_deferred: false, + grants_private_access: false, + outbox_state: OutboxState::Queued, + sync_state: SyncState::Online, + detail_lines: vec![ + "NetworkSteward reviews scoped membership requests.", + "Approval grants role context, not private workspace access.", + ], + }), + stewardship_access_item(StewardshipAccessItemFixture { + id: "steward_approve_role", + kind: StewardshipAccessItemKind::RoleApproval, + title: "Approve role", + actor: WorkflowActor::NetworkSteward, + context: steward.clone(), + target_ref: role_ref, + domain: AuthorityDomain::RelayGroupAccess, + action: AuthorityAction::Approve, + visibility: VisibilityClass::WorkspacePrivate, + is_admin_lite: true, + is_phase_2_deferred: false, + grants_private_access: false, + outbox_state: OutboxState::Shared, + sync_state: SyncState::Synced, + detail_lines: vec![ + "Role approval remains constrained to the requested context.", + "Private farm, buyer, route, proof, and trace data require their own authorities.", + ], + }), + stewardship_access_item(StewardshipAccessItemFixture { + id: "steward_invite_route_partner", + kind: StewardshipAccessItemKind::RoutePartnerInvite, + title: "Invite RoutePartner", + actor: WorkflowActor::NetworkSteward, + context: steward.clone(), + target_ref: route_partner_ref, + domain: AuthorityDomain::RelayGroupAccess, + action: AuthorityAction::Share, + visibility: VisibilityClass::NetworkVisible, + is_admin_lite: true, + is_phase_2_deferred: false, + grants_private_access: false, + outbox_state: OutboxState::Queued, + sync_state: SyncState::Syncing, + detail_lines: vec![ + "RoutePartner invite can be issued without route stop details.", + "Assignment still belongs to route coordination.", + ], + }), + stewardship_access_item(StewardshipAccessItemFixture { + id: "steward_route_pool_metadata", + kind: StewardshipAccessItemKind::RoutePoolMetadata, + title: "Set route pool metadata", + actor: WorkflowActor::NetworkSteward, + context: steward.clone(), + target_ref: route_pool_ref, + domain: AuthorityDomain::NetworkStewardship, + action: AuthorityAction::Assign, + visibility: VisibilityClass::NetworkVisible, + is_admin_lite: true, + is_phase_2_deferred: false, + grants_private_access: false, + outbox_state: OutboxState::Draft, + sync_state: SyncState::Offline, + detail_lines: vec![ + "Stewardship metadata describes route pool availability.", + "Concrete route assignment remains route-coordinator authority.", + ], + }), + stewardship_access_item(StewardshipAccessItemFixture { + id: "steward_public_moderation", + kind: StewardshipAccessItemKind::PublicModeration, + title: "Moderate public/community state", + actor: WorkflowActor::NetworkSteward, + context: steward.clone(), + target_ref: public_update_ref, + domain: AuthorityDomain::PublicPublishing, + action: AuthorityAction::Correct, + visibility: VisibilityClass::PublicCommunity, + is_admin_lite: true, + is_phase_2_deferred: false, + grants_private_access: false, + outbox_state: OutboxState::AwaitingAuthority, + sync_state: SyncState::Online, + detail_lines: vec![ + "Public/community moderation is allowed where publishing authority permits it.", + "Moderation never exposes private proof, buyer, farm, or route-stop data.", + ], + }), + stewardship_access_item(StewardshipAccessItemFixture { + id: "member_accept_invite", + kind: StewardshipAccessItemKind::InviteAcceptance, + title: "Accept invite", + actor: WorkflowActor::NetworkMember, + context: network.clone(), + target_ref: invite_ref, + domain: AuthorityDomain::RelayGroupAccess, + action: AuthorityAction::Submit, + visibility: VisibilityClass::NetworkVisible, + is_admin_lite: false, + is_phase_2_deferred: false, + grants_private_access: false, + outbox_state: OutboxState::Queued, + sync_state: SyncState::Online, + detail_lines: vec![ + "Invite acceptance connects membership without full group management.", + ], + }), + stewardship_access_item(StewardshipAccessItemFixture { + id: "member_request_access", + kind: StewardshipAccessItemKind::RequestAccess, + title: "Request access", + actor: WorkflowActor::NetworkMember, + context: member, + target_ref: request_ref, + domain: AuthorityDomain::RelayGroupAccess, + action: AuthorityAction::Submit, + visibility: VisibilityClass::NetworkVisible, + is_admin_lite: false, + is_phase_2_deferred: false, + grants_private_access: false, + outbox_state: OutboxState::Draft, + sync_state: SyncState::Offline, + detail_lines: vec!["Access requests are queued as explicit membership work."], + }), + stewardship_access_item(StewardshipAccessItemFixture { + id: "member_access_denied", + kind: StewardshipAccessItemKind::AccessDenied, + title: "Access denied", + actor: WorkflowActor::NetworkMember, + context: network, + target_ref: denied_ref, + domain: AuthorityDomain::FarmWorkspaceOperations, + action: AuthorityAction::Search, + visibility: VisibilityClass::FarmPrivate, + is_admin_lite: false, + is_phase_2_deferred: false, + grants_private_access: false, + outbox_state: OutboxState::NotQueued, + sync_state: SyncState::Online, + detail_lines: vec![ + "Denied state preserves the blocked context label without revealing private data.", + ], + }), + stewardship_access_item(StewardshipAccessItemFixture { + id: "phase2_group_management_deferred", + kind: StewardshipAccessItemKind::GroupManagementDeferred, + title: "Group management deferred", + actor: WorkflowActor::NetworkSteward, + context: route, + target_ref: group_ref, + domain: AuthorityDomain::NetworkStewardship, + action: AuthorityAction::Approve, + visibility: VisibilityClass::WorkspacePrivate, + is_admin_lite: true, + is_phase_2_deferred: true, + grants_private_access: false, + outbox_state: OutboxState::NotQueued, + sync_state: SyncState::Unknown, + detail_lines: vec![ + "Full group creation and management are explicitly deferred to Phase 2.", + ], + }), + ] +} + +pub fn fixture_stewardship_access_items(context_id: Option<String>) -> Vec<StewardshipAccessItem> { + let Some(context_id) = context_id else { + return all_stewardship_access_items(); + }; + let matching: Vec<StewardshipAccessItem> = all_stewardship_access_items() + .into_iter() + .filter(|item| item.context.context_ref.object_id == context_id) + .collect(); + if matching.is_empty() { + all_stewardship_access_items() + } else { + matching + } +} + pub fn fixture_outbox_items() -> Vec<OutboxItem> { let context = context_for_type(ContextType::Farm); CANONICAL_OUTBOX_STATES @@ -2275,6 +2610,14 @@ impl RadrootsRuntime { fixture_proof_provenance_artifacts(context_id) } + pub fn phase1_stewardship_access_items( + &self, + context_id: Option<String>, + ) -> Vec<StewardshipAccessItem> { + let _ = self; + fixture_stewardship_access_items(context_id) + } + pub fn phase1_outbox_retry_decision(&self, item: OutboxItem) -> OutboxRetryDecision { let _ = self; fixture_outbox_retry_decision(item) @@ -2343,6 +2686,7 @@ mod tests { assert_eq!(CANONICAL_ROUTE_EXECUTION_STEP_KINDS.len(), 6); assert_eq!(CANONICAL_PROOF_PROVENANCE_ARTIFACT_KINDS.len(), 4); assert_eq!(CANONICAL_PROOF_PROVENANCE_REVIEW_STATES.len(), 8); + assert_eq!(CANONICAL_STEWARDSHIP_ACCESS_ITEM_KINDS.len(), 9); } #[test] @@ -2386,6 +2730,7 @@ mod tests { CANONICAL_ROUTE_EXECUTION_FLOW_KINDS.len() ); assert_eq!(fixture_proof_provenance_artifacts(None).len(), 6); + assert_eq!(fixture_stewardship_access_items(None).len(), 9); } #[test] @@ -2869,6 +3214,138 @@ mod tests { } #[test] + fn stewardship_access_items_cover_admin_lite_and_member_access_states() { + let items = fixture_stewardship_access_items(None); + for kind in CANONICAL_STEWARDSHIP_ACCESS_ITEM_KINDS { + assert!( + items.iter().any(|item| item.kind == kind), + "missing {kind:?}" + ); + } + + assert!(items.iter().any(|item| { + item.kind == StewardshipAccessItemKind::InviteAcceptance + && item.actor == WorkflowActor::NetworkMember + })); + assert!(items.iter().any(|item| { + item.kind == StewardshipAccessItemKind::RequestAccess + && item.actor == WorkflowActor::NetworkMember + })); + assert!(items.iter().any(|item| { + item.kind == StewardshipAccessItemKind::AccessDenied + && !item.required_authority.is_allowed + && item.visibility == VisibilityClass::FarmPrivate + })); + assert!(items.iter().any(|item| { + item.kind == StewardshipAccessItemKind::GroupManagementDeferred + && item.is_phase_2_deferred + })); + } + + #[test] + fn network_steward_can_perform_admin_lite_actions() { + let items: Vec<StewardshipAccessItem> = fixture_stewardship_access_items(None) + .into_iter() + .filter(|item| item.actor == WorkflowActor::NetworkSteward && item.is_admin_lite) + .collect(); + assert!(items.len() >= 6); + + for kind in [ + StewardshipAccessItemKind::AccessRequestReview, + StewardshipAccessItemKind::RoleApproval, + StewardshipAccessItemKind::RoutePartnerInvite, + StewardshipAccessItemKind::RoutePoolMetadata, + StewardshipAccessItemKind::PublicModeration, + ] { + let item = items + .iter() + .find(|item| item.kind == kind) + .unwrap_or_else(|| panic!("missing {kind:?}")); + assert!(item.required_authority.is_allowed); + assert!(!item.grants_private_access); + assert!(!item.is_phase_2_deferred); + } + + let route_pool = items + .iter() + .find(|item| item.kind == StewardshipAccessItemKind::RoutePoolMetadata) + .expect("route pool item"); + assert_eq!( + route_pool.required_authority.domain, + AuthorityDomain::NetworkStewardship + ); + assert_eq!( + route_pool.required_authority.action, + AuthorityAction::Assign + ); + + let moderation = items + .iter() + .find(|item| item.kind == StewardshipAccessItemKind::PublicModeration) + .expect("public moderation item"); + assert_eq!( + moderation.required_authority.domain, + AuthorityDomain::PublicPublishing + ); + } + + #[test] + fn phase_2_group_management_remains_deferred() { + let deferred = fixture_stewardship_access_items(None) + .into_iter() + .find(|item| item.kind == StewardshipAccessItemKind::GroupManagementDeferred) + .expect("deferred group management item"); + assert!(deferred.is_phase_2_deferred); + assert!(deferred.is_admin_lite); + assert!(!deferred.grants_private_access); + assert_eq!(deferred.outbox_state, OutboxState::NotQueued); + } + + #[test] + fn network_steward_does_not_automatically_gain_private_workspace_access() { + let steward = context_for_type(ContextType::NetworkSteward); + for (domain, context_type) in [ + (AuthorityDomain::FarmWorkspaceOperations, ContextType::Farm), + (AuthorityDomain::BuyerWorkspace, ContextType::Buyer), + (AuthorityDomain::RouteCoordination, ContextType::Route), + (AuthorityDomain::RouteExecution, ContextType::RoutePartner), + (AuthorityDomain::TraceProof, ContextType::TraceRecords), + (AuthorityDomain::Receipt, ContextType::PickupPoint), + ] { + let context = context_for_type(context_type); + let gate = fixture_authority_gate( + WorkflowActor::NetworkSteward, + context, + domain, + AuthorityAction::Search, + ); + assert!( + !gate.is_allowed, + "NetworkSteward unexpectedly gained {domain:?}" + ); + } + + assert!( + fixture_authority_gate( + WorkflowActor::NetworkSteward, + steward.clone(), + AuthorityDomain::RelayGroupAccess, + AuthorityAction::Approve, + ) + .is_allowed + ); + assert!( + fixture_authority_gate( + WorkflowActor::NetworkSteward, + steward, + AuthorityDomain::NetworkStewardship, + AuthorityAction::Assign, + ) + .is_allowed + ); + } + + #[test] fn serde_names_preserve_product_vocabulary() { assert_eq!( serde_json::to_value(WorkflowActor::NetworkMember).expect("serialize actor"),