commit a5e09c07f6f773f1399913402741202b43314387
parent 9dd15f54ae78fe91c2b062fc11e803efa5fa98b3
Author: triesap <tyson@radroots.org>
Date: Fri, 7 Aug 2026 08:14:46 +0000
feat(mobile): focus phase one social domain
- Replace broad field operations with exact card and Add taxonomies.
- Add validated local context, stable identities, ranking, and cursors.
- Classify admitted events without content or media-dependent drift.
- Expose focused catalogs and context creation through mobile FFI.
Diffstat:
9 files changed, 1986 insertions(+), 3612 deletions(-)
diff --git a/core/crates/tera_core/Cargo.toml b/core/crates/tera_core/Cargo.toml
@@ -28,10 +28,15 @@ mobile-social = [
[dependencies]
radroots_sdk = { workspace = true, features = ["memory"] }
+radroots_event = { workspace = true, default-features = false, features = ["std"] }
+radroots_event_codec = { workspace = true, default-features = false, features = ["json", "std"] }
chrono = { workspace = true }
+hex = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
+sha2 = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
+nostr = { workspace = true, features = ["std"] }
tokio = { workspace = true, features = ["macros", "rt"] }
diff --git a/core/crates/tera_core/src/runtime/product_surface.rs b/core/crates/tera_core/src/runtime/product_surface.rs
@@ -1,3640 +1,108 @@
-//! Phase 1 product-surface contracts for native Radroots clients.
+//! Focused Phase 1 social-product domain for native Radroots clients.
//!
-//! These DTOs are the shared Rust vocabulary for the iOS `Today | Add`
-//! surface. They deliberately model workflow actors, visibility, authority,
-//! Today cards, Add actions, object pages, and outbox state separately from
-//! low-level Nostr protocol roles or legacy trade/listing APIs.
-
-use serde::{Deserialize, Serialize};
+//! This module owns presentation-neutral product semantics. Protocol parsing
+//! and admission remain in lower event crates; persistence and live query
+//! composition remain in the runtime slices that consume these types.
+
+mod context;
+mod cursor;
+mod identity;
+mod model;
+mod projection;
+mod ranking;
+
+pub use context::{
+ ContextAdmission, ContextRank, LocalNetwork, LocalNetworkAdmission, LocalNetworkError,
+ LocalityEvidence,
+};
+pub use cursor::{CursorError, CursorScope, TodayCursor, TodayCursorPosition};
+pub use identity::{CARD_ID_SCHEMA_VERSION, CardId, CardIdError, CardSourceIdentity};
+pub use model::{
+ AddCommandType, CANONICAL_ADD_COMMAND_TYPES, CANONICAL_CARD_ADD_PARITY,
+ CANONICAL_TODAY_CARD_TYPES, CardAddParity, CardLifecycleState, ClassifiedCard, MediaReference,
+ MediaVerificationState, ProfileSummary, SupportingProfile, ThreadReference, TodayCardType,
+};
+pub use projection::{ProductEventClassification, ProductEventExclusion, classify_admitted_event};
+pub use ranking::{RankError, TODAY_RANK_SCHEMA_VERSION, TimeRelevance, TodayRank, TodayRankInput};
use super::RadrootsRuntime;
-/// Phase 1 workflow actors are product authority roles. Low-level protocol
-/// roles such as Farmer, Buyer, Seller, and Service are compatibility roles and
-/// are not sufficient authority for Phase 1 workflows.
-pub const WORKFLOW_ACTOR_COMPATIBILITY_NOTE: &str = "Phase 1 workflow actors are product authority roles; low-level protocol \
- roles such as Farmer, Buyer, Seller, and Service are compatibility roles \
- and are not sufficient authority.";
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum ContextType {
- Regional,
- Network,
- Farm,
- Buyer,
- Route,
- RoutePartner,
- PickupPoint,
- TraceRecords,
- Hub,
- NetworkSteward,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum WorkflowActor {
- NetworkMember,
- ProducerAdmin,
- FarmTeamMember,
- HubOperator,
- TraceLead,
- BuyerSourcingLead,
- BuyerReceiver,
- PickupPointCoordinator,
- RouteCoordinator,
- RoutePartner,
- NetworkSteward,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum VisibilityClass {
- LocalDraft,
- FarmPrivate,
- WorkspacePrivate,
- NetworkVisible,
- RouteScoped,
- BuyerScoped,
- PublicCommunity,
- PublicProvenance,
- SecretNeverShared,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-pub enum AuthorityDomain {
- #[serde(rename = "Relay/group access")]
- RelayGroupAccess,
- #[serde(rename = "Farm/workspace operations authority")]
- FarmWorkspaceOperations,
- #[serde(rename = "Buyer workspace authority")]
- BuyerWorkspace,
- #[serde(rename = "Route coordination authority")]
- RouteCoordination,
- #[serde(rename = "Route execution authority")]
- RouteExecution,
- #[serde(rename = "Receipt authority")]
- Receipt,
- #[serde(rename = "Trace/proof authority")]
- TraceProof,
- #[serde(rename = "Public publishing authority")]
- PublicPublishing,
- #[serde(rename = "Network stewardship authority")]
- NetworkStewardship,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum AuthorityAction {
- Submit,
- Publish,
- Share,
- Assign,
- Approve,
- Correct,
- Close,
- Retry,
- Search,
- NavigateRelatedObject,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum ObjectKind {
- Region,
- Network,
- Farm,
- BuyerWorkspace,
- Route,
- RoutePartner,
- PickupPoint,
- Hub,
- Food,
- Ask,
- Event,
- Place,
- Task,
- Proof,
- Exception,
- Provenance,
- Update,
- AccessMembership,
- BuyerPacket,
- RouteStop,
- Draft,
- OutboxItem,
- MemberInvite,
- Correction,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum ObjectPageFamily {
- Network,
- NetworkRoute,
- FarmWorkspace,
- FarmPublicProfile,
- BuyerWorkspace,
- PickupPointPlace,
- Food,
- Event,
- RouteStop,
- Proof,
- BuyerPacket,
- PublicProvenance,
- Exception,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum TodayCardType {
- Route,
- Food,
- Ask,
- Event,
- Place,
- Task,
- Proof,
- Exception,
- Provenance,
- Update,
- AccessMembership,
- SyncOutbox,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum AddActionType {
- Photo,
- Note,
- Ask,
- Scan,
- Food,
- Harvest,
- BuyerRequest,
- BuyerCommitment,
- RouteNeed,
- RouteStop,
- PickupEvent,
- Place,
- Proof,
- Exception,
- PublicUpdate,
- Provenance,
- MemberInvite,
- Correction,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum AddFlowState {
- NotStarted,
- Draft,
- Editing,
- ValidationFailed,
- ReadyToSubmit,
- Submitted,
- Queued,
- Syncing,
- NeedsApproval,
- Approved,
- Published,
- Shared,
- Confirmed,
- Failed,
- Conflict,
- Discarded,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum OutboxBehavior {
- LocalOnly,
- QueueWhenOffline,
- RequireOnline,
- PublishWhenAuthorized,
- ShareWhenAuthorized,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum PrototypePathKind {
- ProducerFoodToRoute,
- BuyerCommitmentToRoute,
- RouteCoordinatorAssignment,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum RouteExecutionFlowKind {
- RoutePartnerAssignedStops,
- BuyerReceiptConfirmation,
- ExceptionRecovery,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum RouteExecutionStepKind {
- AssignedRoute,
- PickupConfirmation,
- DropoffConfirmation,
- ReceiptConfirmation,
- ExceptionReport,
- RecoveryAction,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum ProofProvenanceArtifactKind {
- ProofCompleteness,
- BuyerPacketDraft,
- BuyerPacketShared,
- PublicProvenancePreview,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum ProofProvenanceReviewState {
- MissingProof,
- Complete,
- Draft,
- ReadyToShare,
- Shared,
- RedactionRequired,
- ReadyToPublish,
- Published,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum StewardshipAccessItemKind {
- AccessRequestReview,
- RoleApproval,
- RoutePartnerInvite,
- RoutePoolMetadata,
- PublicModeration,
- InviteAcceptance,
- RequestAccess,
- AccessDenied,
- GroupManagementDeferred,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum OutboxState {
- NotQueued,
- Draft,
- Queued,
- Syncing,
- AwaitingAuthority,
- Published,
- Shared,
- Failed,
- Conflict,
- Discarded,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "PascalCase")]
-pub enum SyncState {
- Unknown,
- Online,
- Offline,
- Syncing,
- Synced,
- Stale,
- Failed,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct ObjectRef {
- pub object_type: ObjectKind,
- pub object_id: String,
- pub display_label: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct EventRef {
- pub event_id: String,
- pub relay_url: Option<String>,
- pub kind: Option<u32>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct ActiveContext {
- pub context_type: ContextType,
- pub context_ref: ObjectRef,
- pub actor: WorkflowActor,
- pub display_label: String,
- pub visibility_scope: VisibilityClass,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct AuthorityGate {
- pub domain: AuthorityDomain,
- pub action: AuthorityAction,
- pub actor: WorkflowActor,
- pub context: ActiveContext,
- pub is_required: bool,
- pub is_allowed: bool,
- pub reason: Option<String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct ObjectPageSummary {
- pub object_ref: ObjectRef,
- pub family: ObjectPageFamily,
- pub primary_context: ActiveContext,
- pub title: String,
- pub subtitle: Option<String>,
- pub visibility: VisibilityClass,
- pub visibility_label: String,
- pub required_authority: AuthorityGate,
- pub sync_state: SyncState,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct TodayCardAction {
- pub id: String,
- pub label: String,
- pub action_type: Option<AddActionType>,
- pub target_object: Option<ObjectRef>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct TodayCard {
- pub id: String,
- pub card_type: TodayCardType,
- pub source_object_refs: Vec<ObjectRef>,
- pub source_event_refs: Vec<EventRef>,
- pub primary_context: ActiveContext,
- pub actor: WorkflowActor,
- pub visibility: VisibilityClass,
- pub visibility_label: String,
- pub title: String,
- pub status_line: String,
- pub detail_lines: Vec<String>,
- pub pills: Vec<String>,
- pub primary_action: TodayCardAction,
- pub secondary_action: Option<TodayCardAction>,
- pub ranking_reason: String,
- pub ranking_features: Vec<String>,
- pub sync_state: SyncState,
- pub outbox_state: OutboxState,
- pub is_stale: bool,
- pub is_offline: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct RelatedObjectRequirement {
- pub object_type: ObjectKind,
- pub relationship_label: String,
- pub is_required: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct ValidationRequirement {
- pub id: String,
- pub label: String,
- pub is_blocking: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct AddAction {
- pub action_type: AddActionType,
- pub display_label: String,
- pub allowed_context_types: Vec<ContextType>,
- pub required_authority: AuthorityGate,
- pub default_visibility: VisibilityClass,
- pub allowed_visibility_options: Vec<VisibilityClass>,
- pub created_or_updated_object_type: ObjectKind,
- pub related_object_requirements: Vec<RelatedObjectRequirement>,
- pub validation_requirements: Vec<ValidationRequirement>,
- pub supports_offline: bool,
- pub supports_draft: bool,
- pub outbox_behavior: OutboxBehavior,
- pub primary_submit_label: String,
- pub completion_state: AddFlowState,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct OutboxItem {
- pub id: String,
- pub action_type: AddActionType,
- pub context: ActiveContext,
- pub object_refs: Vec<ObjectRef>,
- pub event_refs: Vec<EventRef>,
- pub visibility: VisibilityClass,
- pub authority_gate: AuthorityGate,
- pub flow_state: AddFlowState,
- pub outbox_state: OutboxState,
- pub sync_state: SyncState,
- pub queued_at_unix: Option<u64>,
- pub last_attempt_at_unix: Option<u64>,
- pub retry_count: u32,
- pub last_error: Option<String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct OutboxRetryDecision {
- pub item_id: String,
- pub is_retryable: bool,
- pub authority_gate: AuthorityGate,
- pub reason: Option<String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct SearchResultSummary {
- pub id: String,
- pub object_ref: ObjectRef,
- pub primary_context: ActiveContext,
- pub title: String,
- pub subtitle: Option<String>,
- pub visibility: VisibilityClass,
- pub visibility_label: String,
- pub required_authority: AuthorityGate,
- pub sync_state: SyncState,
-}
-
-#[derive(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(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>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct RouteExecutionStep {
- pub id: String,
- pub kind: RouteExecutionStepKind,
- pub label: String,
- pub actor: WorkflowActor,
- pub context: ActiveContext,
- pub route_ref: ObjectRef,
- pub object_ref: Option<ObjectRef>,
- pub required_authority: AuthorityGate,
- pub visibility: VisibilityClass,
- pub supports_offline: bool,
- pub supports_partial_receipt: bool,
- pub uses_receipt_token: bool,
- pub outbox_state: OutboxState,
- pub sync_state: SyncState,
- pub detail_lines: Vec<String>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct RouteExecutionFlow {
- pub id: String,
- pub kind: RouteExecutionFlowKind,
- pub title: String,
- pub actor: WorkflowActor,
- pub context: ActiveContext,
- pub route_ref: ObjectRef,
- pub steps: Vec<RouteExecutionStep>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct ProofProvenanceArtifact {
- pub id: String,
- pub kind: ProofProvenanceArtifactKind,
- pub review_state: ProofProvenanceReviewState,
- pub title: String,
- pub actor: WorkflowActor,
- pub context: ActiveContext,
- pub object_ref: ObjectRef,
- pub source_object_refs: Vec<ObjectRef>,
- pub required_authority: AuthorityGate,
- pub visibility: VisibilityClass,
- pub is_public_preview: bool,
- pub requires_redaction_review: bool,
- pub can_publish: bool,
- pub public_summary_lines: Vec<String>,
- pub redacted_field_labels: Vec<String>,
- pub outbox_state: OutboxState,
- pub sync_state: SyncState,
-}
-
-#[derive(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,
- ContextType::Farm,
- ContextType::Buyer,
- ContextType::Route,
- ContextType::RoutePartner,
- ContextType::PickupPoint,
- ContextType::TraceRecords,
- ContextType::Hub,
- ContextType::NetworkSteward,
-];
-
-pub const CANONICAL_WORKFLOW_ACTORS: [WorkflowActor; 11] = [
- WorkflowActor::NetworkMember,
- WorkflowActor::ProducerAdmin,
- WorkflowActor::FarmTeamMember,
- WorkflowActor::HubOperator,
- WorkflowActor::TraceLead,
- WorkflowActor::BuyerSourcingLead,
- WorkflowActor::BuyerReceiver,
- WorkflowActor::PickupPointCoordinator,
- WorkflowActor::RouteCoordinator,
- WorkflowActor::RoutePartner,
- WorkflowActor::NetworkSteward,
-];
-
-pub const CANONICAL_VISIBILITY_CLASSES: [VisibilityClass; 9] = [
- VisibilityClass::LocalDraft,
- VisibilityClass::FarmPrivate,
- VisibilityClass::WorkspacePrivate,
- VisibilityClass::NetworkVisible,
- VisibilityClass::RouteScoped,
- VisibilityClass::BuyerScoped,
- VisibilityClass::PublicCommunity,
- VisibilityClass::PublicProvenance,
- VisibilityClass::SecretNeverShared,
-];
-
-pub const CANONICAL_AUTHORITY_DOMAINS: [AuthorityDomain; 9] = [
- AuthorityDomain::RelayGroupAccess,
- AuthorityDomain::FarmWorkspaceOperations,
- AuthorityDomain::BuyerWorkspace,
- AuthorityDomain::RouteCoordination,
- AuthorityDomain::RouteExecution,
- AuthorityDomain::Receipt,
- AuthorityDomain::TraceProof,
- AuthorityDomain::PublicPublishing,
- AuthorityDomain::NetworkStewardship,
-];
-
-pub const CANONICAL_TODAY_CARD_TYPES: [TodayCardType; 12] = [
- TodayCardType::Route,
- TodayCardType::Food,
- TodayCardType::Ask,
- TodayCardType::Event,
- TodayCardType::Place,
- TodayCardType::Task,
- TodayCardType::Proof,
- TodayCardType::Exception,
- TodayCardType::Provenance,
- TodayCardType::Update,
- TodayCardType::AccessMembership,
- TodayCardType::SyncOutbox,
-];
-
-pub const TODAY_CARD_RANKING_PRIORITY: [TodayCardType; 12] = [
- TodayCardType::Exception,
- TodayCardType::SyncOutbox,
- TodayCardType::Route,
- TodayCardType::Proof,
- TodayCardType::Food,
- TodayCardType::Task,
- TodayCardType::Provenance,
- TodayCardType::Ask,
- TodayCardType::Event,
- TodayCardType::Place,
- TodayCardType::Update,
- TodayCardType::AccessMembership,
-];
-
-pub const CANONICAL_ADD_ACTION_TYPES: [AddActionType; 18] = [
- AddActionType::Photo,
- AddActionType::Note,
- AddActionType::Ask,
- AddActionType::Scan,
- AddActionType::Food,
- AddActionType::Harvest,
- AddActionType::BuyerRequest,
- AddActionType::BuyerCommitment,
- AddActionType::RouteNeed,
- AddActionType::RouteStop,
- AddActionType::PickupEvent,
- AddActionType::Place,
- AddActionType::Proof,
- AddActionType::Exception,
- AddActionType::PublicUpdate,
- AddActionType::Provenance,
- AddActionType::MemberInvite,
- AddActionType::Correction,
-];
-
-pub const CANONICAL_ADD_FLOW_STATES: [AddFlowState; 16] = [
- AddFlowState::NotStarted,
- AddFlowState::Draft,
- AddFlowState::Editing,
- AddFlowState::ValidationFailed,
- AddFlowState::ReadyToSubmit,
- AddFlowState::Submitted,
- AddFlowState::Queued,
- AddFlowState::Syncing,
- AddFlowState::NeedsApproval,
- AddFlowState::Approved,
- AddFlowState::Published,
- AddFlowState::Shared,
- AddFlowState::Confirmed,
- AddFlowState::Failed,
- AddFlowState::Conflict,
- AddFlowState::Discarded,
-];
-
-pub const CANONICAL_OBJECT_PAGE_FAMILIES: [ObjectPageFamily; 13] = [
- ObjectPageFamily::Network,
- ObjectPageFamily::NetworkRoute,
- ObjectPageFamily::FarmWorkspace,
- ObjectPageFamily::FarmPublicProfile,
- ObjectPageFamily::BuyerWorkspace,
- ObjectPageFamily::PickupPointPlace,
- ObjectPageFamily::Food,
- ObjectPageFamily::Event,
- ObjectPageFamily::RouteStop,
- ObjectPageFamily::Proof,
- ObjectPageFamily::BuyerPacket,
- ObjectPageFamily::PublicProvenance,
- ObjectPageFamily::Exception,
-];
-
-pub const CANONICAL_OUTBOX_STATES: [OutboxState; 10] = [
- OutboxState::NotQueued,
- OutboxState::Draft,
- OutboxState::Queued,
- OutboxState::Syncing,
- OutboxState::AwaitingAuthority,
- OutboxState::Published,
- OutboxState::Shared,
- OutboxState::Failed,
- OutboxState::Conflict,
- OutboxState::Discarded,
-];
-
-pub const CANONICAL_SYNC_STATES: [SyncState; 7] = [
- SyncState::Unknown,
- SyncState::Online,
- SyncState::Offline,
- SyncState::Syncing,
- SyncState::Synced,
- SyncState::Stale,
- SyncState::Failed,
-];
-
-pub const CANONICAL_ROUTE_EXECUTION_FLOW_KINDS: [RouteExecutionFlowKind; 3] = [
- RouteExecutionFlowKind::RoutePartnerAssignedStops,
- RouteExecutionFlowKind::BuyerReceiptConfirmation,
- RouteExecutionFlowKind::ExceptionRecovery,
-];
-
-pub const CANONICAL_ROUTE_EXECUTION_STEP_KINDS: [RouteExecutionStepKind; 6] = [
- RouteExecutionStepKind::AssignedRoute,
- RouteExecutionStepKind::PickupConfirmation,
- RouteExecutionStepKind::DropoffConfirmation,
- RouteExecutionStepKind::ReceiptConfirmation,
- RouteExecutionStepKind::ExceptionReport,
- RouteExecutionStepKind::RecoveryAction,
-];
-
-pub const CANONICAL_PROOF_PROVENANCE_ARTIFACT_KINDS: [ProofProvenanceArtifactKind; 4] = [
- ProofProvenanceArtifactKind::ProofCompleteness,
- ProofProvenanceArtifactKind::BuyerPacketDraft,
- ProofProvenanceArtifactKind::BuyerPacketShared,
- ProofProvenanceArtifactKind::PublicProvenancePreview,
-];
-
-pub const CANONICAL_PROOF_PROVENANCE_REVIEW_STATES: [ProofProvenanceReviewState; 8] = [
- ProofProvenanceReviewState::MissingProof,
- ProofProvenanceReviewState::Complete,
- ProofProvenanceReviewState::Draft,
- ProofProvenanceReviewState::ReadyToShare,
- ProofProvenanceReviewState::Shared,
- ProofProvenanceReviewState::RedactionRequired,
- ProofProvenanceReviewState::ReadyToPublish,
- 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>,
- label: impl Into<String>,
-) -> ObjectRef {
- ObjectRef {
- object_type,
- object_id: object_id.into(),
- display_label: label.into(),
- }
-}
-
-fn context_fixture_parts(
- context_type: ContextType,
-) -> (
- ObjectKind,
- &'static str,
- &'static str,
- WorkflowActor,
- VisibilityClass,
-) {
- match context_type {
- ContextType::Regional => (
- ObjectKind::Region,
- "region_floripa",
- "Floripa regional food network",
- WorkflowActor::NetworkMember,
- VisibilityClass::PublicCommunity,
- ),
- ContextType::Network => (
- ObjectKind::Network,
- "network_floripa",
- "Floripa local food network",
- WorkflowActor::NetworkMember,
- VisibilityClass::NetworkVisible,
- ),
- ContextType::Farm => (
- ObjectKind::Farm,
- "farm_floripa_001",
- "Floripa Farm",
- WorkflowActor::ProducerAdmin,
- VisibilityClass::FarmPrivate,
- ),
- ContextType::Buyer => (
- ObjectKind::BuyerWorkspace,
- "buyer_workspace_001",
- "Kitchen buyer workspace",
- WorkflowActor::BuyerSourcingLead,
- VisibilityClass::BuyerScoped,
- ),
- ContextType::Route => (
- ObjectKind::Route,
- "route_thursday_001",
- "Thursday network loop",
- WorkflowActor::RouteCoordinator,
- VisibilityClass::RouteScoped,
- ),
- ContextType::RoutePartner => (
- ObjectKind::RoutePartner,
- "route_partner_001",
- "Assigned route partner",
- WorkflowActor::RoutePartner,
- VisibilityClass::RouteScoped,
- ),
- ContextType::PickupPoint => (
- ObjectKind::PickupPoint,
- "pickup_point_001",
- "Neighborhood pickup point",
- WorkflowActor::PickupPointCoordinator,
- VisibilityClass::NetworkVisible,
- ),
- ContextType::TraceRecords => (
- ObjectKind::Proof,
- "trace_records_001",
- "Trace and records",
- WorkflowActor::TraceLead,
- VisibilityClass::WorkspacePrivate,
- ),
- ContextType::Hub => (
- ObjectKind::Hub,
- "hub_001",
- "Floripa hub",
- WorkflowActor::HubOperator,
- VisibilityClass::WorkspacePrivate,
- ),
- ContextType::NetworkSteward => (
- ObjectKind::AccessMembership,
- "network_stewardship_001",
- "Network stewardship",
- WorkflowActor::NetworkSteward,
- VisibilityClass::WorkspacePrivate,
- ),
- }
-}
-
-fn context_for_type(context_type: ContextType) -> ActiveContext {
- let (object_type, object_id, label, actor, visibility_scope) =
- context_fixture_parts(context_type);
- ActiveContext {
- context_type,
- context_ref: object_ref(object_type, object_id, label),
- actor,
- display_label: label.to_string(),
- visibility_scope,
- }
-}
-
-fn authority_allowed(actor: WorkflowActor, domain: AuthorityDomain) -> bool {
- matches!(
- (actor, domain),
- (
- WorkflowActor::NetworkMember,
- AuthorityDomain::RelayGroupAccess
- ) | (
- WorkflowActor::ProducerAdmin,
- AuthorityDomain::FarmWorkspaceOperations
- ) | (WorkflowActor::ProducerAdmin, AuthorityDomain::TraceProof)
- | (
- WorkflowActor::ProducerAdmin,
- AuthorityDomain::PublicPublishing
- )
- | (
- WorkflowActor::FarmTeamMember,
- AuthorityDomain::FarmWorkspaceOperations
- )
- | (
- WorkflowActor::HubOperator,
- AuthorityDomain::FarmWorkspaceOperations
- )
- | (WorkflowActor::HubOperator, AuthorityDomain::RouteExecution)
- | (WorkflowActor::TraceLead, AuthorityDomain::TraceProof)
- | (
- WorkflowActor::BuyerSourcingLead,
- AuthorityDomain::BuyerWorkspace
- )
- | (WorkflowActor::BuyerReceiver, AuthorityDomain::Receipt)
- | (
- WorkflowActor::PickupPointCoordinator,
- AuthorityDomain::Receipt
- )
- | (
- WorkflowActor::PickupPointCoordinator,
- AuthorityDomain::RouteExecution
- )
- | (
- WorkflowActor::RouteCoordinator,
- AuthorityDomain::RouteCoordination
- )
- | (WorkflowActor::RoutePartner, AuthorityDomain::RouteExecution)
- | (
- WorkflowActor::NetworkSteward,
- AuthorityDomain::RelayGroupAccess
- )
- | (
- WorkflowActor::NetworkSteward,
- AuthorityDomain::PublicPublishing
- )
- | (
- WorkflowActor::NetworkSteward,
- AuthorityDomain::NetworkStewardship
- )
- )
-}
-
-pub fn fixture_authority_gate(
- actor: WorkflowActor,
- context: ActiveContext,
- domain: AuthorityDomain,
- action: AuthorityAction,
-) -> AuthorityGate {
- let is_allowed = authority_allowed(actor, domain);
- AuthorityGate {
- domain,
- action,
- actor,
- context,
- is_required: true,
- is_allowed,
- reason: if is_allowed {
- None
- } else {
- Some("fixture authority denies this actor/domain pair".to_string())
- },
- }
-}
-
-pub fn fixture_active_contexts() -> Vec<ActiveContext> {
- CANONICAL_CONTEXT_TYPES
- .into_iter()
- .map(context_for_type)
- .collect()
-}
-
-fn context_by_object_id(context_id: Option<String>) -> Option<ActiveContext> {
- let context_id = context_id?;
- fixture_active_contexts()
- .into_iter()
- .find(|context| context.context_ref.object_id == context_id)
-}
-
-fn card_action_for(card_type: TodayCardType) -> Option<AddActionType> {
- match card_type {
- TodayCardType::Route => Some(AddActionType::RouteStop),
- TodayCardType::Food => Some(AddActionType::Food),
- TodayCardType::Ask => Some(AddActionType::Ask),
- TodayCardType::Event => Some(AddActionType::PickupEvent),
- TodayCardType::Place => Some(AddActionType::Place),
- TodayCardType::Task => Some(AddActionType::Note),
- TodayCardType::Proof => Some(AddActionType::Proof),
- TodayCardType::Exception => Some(AddActionType::Exception),
- TodayCardType::Provenance => Some(AddActionType::Provenance),
- TodayCardType::Update => Some(AddActionType::PublicUpdate),
- TodayCardType::AccessMembership => Some(AddActionType::MemberInvite),
- TodayCardType::SyncOutbox => None,
- }
-}
-
-fn object_kind_for_card(card_type: TodayCardType) -> ObjectKind {
- match card_type {
- TodayCardType::Route => ObjectKind::Route,
- TodayCardType::Food => ObjectKind::Food,
- TodayCardType::Ask => ObjectKind::Ask,
- TodayCardType::Event => ObjectKind::Event,
- TodayCardType::Place => ObjectKind::Place,
- TodayCardType::Task => ObjectKind::Task,
- TodayCardType::Proof => ObjectKind::Proof,
- TodayCardType::Exception => ObjectKind::Exception,
- TodayCardType::Provenance => ObjectKind::Provenance,
- TodayCardType::Update => ObjectKind::Update,
- TodayCardType::AccessMembership => ObjectKind::AccessMembership,
- TodayCardType::SyncOutbox => ObjectKind::OutboxItem,
- }
-}
-
-fn ranking_reason_for(card_type: TodayCardType) -> &'static str {
- match card_type {
- TodayCardType::Exception => "blocking exception",
- TodayCardType::SyncOutbox => "required sync action",
- TodayCardType::Route => "time-window route operation",
- TodayCardType::Proof => "route readiness and proof gap",
- TodayCardType::Food => "commitment window",
- TodayCardType::Task => "assigned task",
- TodayCardType::Provenance => "provenance candidate",
- TodayCardType::Ask => "food availability and ask",
- TodayCardType::Event => "event window",
- TodayCardType::Place => "place context",
- TodayCardType::Update => "community update",
- TodayCardType::AccessMembership => "network access state",
- }
-}
-
-fn ranking_features_for(card_type: TodayCardType) -> Vec<String> {
- match card_type {
- TodayCardType::Exception => vec!["blocking".to_string(), "recovery".to_string()],
- TodayCardType::SyncOutbox => vec!["outbox".to_string(), "retry".to_string()],
- TodayCardType::Route => vec!["time_window".to_string(), "route".to_string()],
- TodayCardType::Proof => vec!["proof_gap".to_string(), "trace".to_string()],
- TodayCardType::Food => vec!["commitment".to_string(), "availability".to_string()],
- TodayCardType::Task => vec!["assigned".to_string(), "work".to_string()],
- TodayCardType::Provenance => vec!["candidate".to_string(), "public_review".to_string()],
- TodayCardType::Ask => vec!["ask".to_string(), "network_need".to_string()],
- TodayCardType::Event => vec!["event".to_string(), "calendar".to_string()],
- TodayCardType::Place => vec!["place".to_string(), "local_context".to_string()],
- TodayCardType::Update => vec!["update".to_string(), "community".to_string()],
- TodayCardType::AccessMembership => vec!["access".to_string(), "membership".to_string()],
- }
-}
-
-fn status_line_for(card_type: TodayCardType) -> &'static str {
- match card_type {
- TodayCardType::Exception => "Needs review",
- TodayCardType::SyncOutbox => "Retry required",
- TodayCardType::Route => "Route window open",
- TodayCardType::Proof => "Proof gap detected",
- TodayCardType::Food => "Commitment window active",
- TodayCardType::Task => "Assigned work ready",
- TodayCardType::Provenance => "Candidate ready for review",
- TodayCardType::Ask => "Network need available",
- TodayCardType::Event => "Upcoming gathering",
- TodayCardType::Place => "Place context updated",
- TodayCardType::Update => "Community update ready",
- TodayCardType::AccessMembership => "Membership state available",
- }
-}
-
-fn detail_lines_for(card_type: TodayCardType) -> Vec<String> {
- match card_type {
- TodayCardType::Exception => vec![
- "Resolve the blocker before related route work continues.".to_string(),
- "Recovery path remains scoped to the active context.".to_string(),
- ],
- TodayCardType::SyncOutbox => {
- vec!["Queued work will re-check context, visibility, and authority.".to_string()]
- }
- TodayCardType::Route => {
- vec!["Review stops, timing, proof gaps, and assigned route partner state.".to_string()]
- }
- TodayCardType::Proof => {
- vec!["Trace record needs proof completion before publication.".to_string()]
- }
- TodayCardType::Food => {
- vec!["Food availability is connected to commitments and routes.".to_string()]
- }
- TodayCardType::Task => vec!["Complete the assigned task from this context.".to_string()],
- TodayCardType::Provenance => {
- vec!["Public provenance preview requires redaction review.".to_string()]
- }
- TodayCardType::Ask => vec!["Respond to a scoped network ask.".to_string()],
- TodayCardType::Event => vec!["Gathering context is visible to the network.".to_string()],
- TodayCardType::Place => {
- vec!["Place details are ready for local network review.".to_string()]
- }
- TodayCardType::Update => vec!["Share a context-aware network update.".to_string()],
- TodayCardType::AccessMembership => {
- vec!["Review member access for this context.".to_string()]
- }
- }
-}
-
-pub fn fixture_today_cards(context_id: Option<String>) -> Vec<TodayCard> {
- let contexts = fixture_active_contexts();
- let filter_context = context_by_object_id(context_id);
- TODAY_CARD_RANKING_PRIORITY
- .into_iter()
- .enumerate()
- .map(|(index, card_type)| {
- let context = filter_context
- .clone()
- .unwrap_or_else(|| contexts[index % contexts.len()].clone());
- let object_kind = object_kind_for_card(card_type);
- let object_id = format!("phase1_{:?}_001", object_kind).to_lowercase();
- let object = object_ref(object_kind, object_id, format!("{:?} fixture", card_type));
- let action_type = card_action_for(card_type);
- TodayCard {
- id: format!("today_{:?}_001", card_type).to_lowercase(),
- card_type,
- source_object_refs: vec![object.clone()],
- source_event_refs: Vec::new(),
- primary_context: context.clone(),
- actor: context.actor,
- visibility: context.visibility_scope,
- visibility_label: format!("{:?}", context.visibility_scope),
- title: format!("{:?} fixture card", card_type),
- status_line: status_line_for(card_type).to_string(),
- detail_lines: detail_lines_for(card_type),
- pills: vec![
- format!("{:?}", card_type),
- ranking_reason_for(card_type).to_string(),
- ],
- primary_action: TodayCardAction {
- id: format!("primary_{:?}", card_type).to_lowercase(),
- label: action_type
- .map(|action| format!("{:?}", action))
- .unwrap_or_else(|| "Review".to_string()),
- action_type,
- target_object: Some(object),
- },
- secondary_action: None,
- ranking_reason: ranking_reason_for(card_type).to_string(),
- ranking_features: ranking_features_for(card_type),
- sync_state: if matches!(card_type, TodayCardType::SyncOutbox) {
- SyncState::Failed
- } else {
- SyncState::Online
- },
- outbox_state: if matches!(card_type, TodayCardType::SyncOutbox) {
- OutboxState::Failed
- } else {
- OutboxState::NotQueued
- },
- is_stale: matches!(card_type, TodayCardType::Proof),
- is_offline: matches!(card_type, TodayCardType::SyncOutbox),
- }
- })
- .collect()
-}
-
-fn add_action_object_kind(action_type: AddActionType) -> ObjectKind {
- match action_type {
- AddActionType::Photo | AddActionType::Note | AddActionType::PublicUpdate => {
- ObjectKind::Update
- }
- AddActionType::Ask | AddActionType::BuyerRequest | AddActionType::RouteNeed => {
- ObjectKind::Ask
- }
- AddActionType::Scan | AddActionType::Proof => ObjectKind::Proof,
- AddActionType::Food | AddActionType::Harvest | AddActionType::BuyerCommitment => {
- ObjectKind::Food
- }
- AddActionType::RouteStop | AddActionType::PickupEvent => ObjectKind::RouteStop,
- AddActionType::Place => ObjectKind::Place,
- AddActionType::Exception => ObjectKind::Exception,
- AddActionType::Provenance => ObjectKind::Provenance,
- AddActionType::MemberInvite => ObjectKind::AccessMembership,
- AddActionType::Correction => ObjectKind::Correction,
- }
-}
-
-fn add_action_authority(action_type: AddActionType) -> (AuthorityDomain, AuthorityAction) {
- match action_type {
- AddActionType::PublicUpdate | AddActionType::Provenance => {
- (AuthorityDomain::PublicPublishing, AuthorityAction::Publish)
- }
- AddActionType::BuyerRequest | AddActionType::BuyerCommitment => {
- (AuthorityDomain::BuyerWorkspace, AuthorityAction::Submit)
- }
- AddActionType::RouteNeed | AddActionType::RouteStop | AddActionType::PickupEvent => {
- (AuthorityDomain::RouteCoordination, AuthorityAction::Submit)
- }
- AddActionType::Proof | AddActionType::Scan => {
- (AuthorityDomain::TraceProof, AuthorityAction::Submit)
- }
- AddActionType::MemberInvite => (AuthorityDomain::RelayGroupAccess, AuthorityAction::Share),
- AddActionType::Correction => (AuthorityDomain::TraceProof, AuthorityAction::Correct),
- _ => (
- AuthorityDomain::FarmWorkspaceOperations,
- AuthorityAction::Submit,
- ),
- }
-}
-
-fn default_visibility_for_action(action_type: AddActionType) -> VisibilityClass {
- match action_type {
- AddActionType::PublicUpdate => VisibilityClass::PublicCommunity,
- AddActionType::Provenance => VisibilityClass::PublicProvenance,
- AddActionType::BuyerRequest | AddActionType::BuyerCommitment => {
- VisibilityClass::BuyerScoped
- }
- AddActionType::RouteNeed | AddActionType::RouteStop | AddActionType::PickupEvent => {
- VisibilityClass::RouteScoped
- }
- AddActionType::Photo | AddActionType::Note | AddActionType::Scan => {
- VisibilityClass::LocalDraft
- }
- _ => VisibilityClass::FarmPrivate,
- }
-}
-
-pub fn fixture_add_actions(context_id: Option<String>) -> Vec<AddAction> {
- let context =
- context_by_object_id(context_id).unwrap_or_else(|| context_for_type(ContextType::Farm));
- CANONICAL_ADD_ACTION_TYPES
- .into_iter()
- .map(|action_type| {
- let (domain, action) = add_action_authority(action_type);
- AddAction {
- action_type,
- display_label: format!("{:?}", action_type),
- allowed_context_types: vec![context.context_type],
- required_authority: fixture_authority_gate(
- context.actor,
- context.clone(),
- domain,
- action,
- ),
- default_visibility: default_visibility_for_action(action_type),
- allowed_visibility_options: vec![
- VisibilityClass::LocalDraft,
- default_visibility_for_action(action_type),
- ],
- created_or_updated_object_type: add_action_object_kind(action_type),
- related_object_requirements: vec![RelatedObjectRequirement {
- object_type: context.context_ref.object_type,
- relationship_label: "primary context".to_string(),
- is_required: true,
- }],
- validation_requirements: vec![ValidationRequirement {
- id: "fixture_required_fields".to_string(),
- label: "Required fields are present".to_string(),
- is_blocking: true,
- }],
- supports_offline: true,
- supports_draft: true,
- outbox_behavior: OutboxBehavior::QueueWhenOffline,
- primary_submit_label: "Submit".to_string(),
- completion_state: AddFlowState::ReadyToSubmit,
- }
- })
- .collect()
-}
-
-fn object_kind_for_page(family: ObjectPageFamily) -> ObjectKind {
- match family {
- ObjectPageFamily::Network => ObjectKind::Network,
- ObjectPageFamily::NetworkRoute => ObjectKind::Route,
- ObjectPageFamily::FarmWorkspace | ObjectPageFamily::FarmPublicProfile => ObjectKind::Farm,
- ObjectPageFamily::BuyerWorkspace => ObjectKind::BuyerWorkspace,
- ObjectPageFamily::PickupPointPlace => ObjectKind::PickupPoint,
- ObjectPageFamily::Food => ObjectKind::Food,
- ObjectPageFamily::Event => ObjectKind::Event,
- ObjectPageFamily::RouteStop => ObjectKind::RouteStop,
- ObjectPageFamily::Proof => ObjectKind::Proof,
- ObjectPageFamily::BuyerPacket => ObjectKind::BuyerPacket,
- ObjectPageFamily::PublicProvenance => ObjectKind::Provenance,
- ObjectPageFamily::Exception => ObjectKind::Exception,
- }
-}
-
-pub fn fixture_object_page_summaries(context_id: Option<String>) -> Vec<ObjectPageSummary> {
- let context =
- context_by_object_id(context_id).unwrap_or_else(|| context_for_type(ContextType::Network));
- CANONICAL_OBJECT_PAGE_FAMILIES
- .into_iter()
- .map(|family| {
- let object_kind = object_kind_for_page(family);
- ObjectPageSummary {
- object_ref: object_ref(
- object_kind,
- format!("phase1_{:?}_page_001", family).to_lowercase(),
- format!("{:?} fixture page", family),
- ),
- family,
- primary_context: context.clone(),
- title: format!("{:?} fixture page", family),
- subtitle: Some("fixture-backed object summary".to_string()),
- visibility: context.visibility_scope,
- visibility_label: format!("{:?}", context.visibility_scope),
- required_authority: fixture_authority_gate(
- context.actor,
- context.clone(),
- AuthorityDomain::RelayGroupAccess,
- AuthorityAction::NavigateRelatedObject,
- ),
- sync_state: SyncState::Online,
- }
- })
- .collect()
-}
-
-fn visibility_allows_search_result(visibility: VisibilityClass) -> bool {
- matches!(
- visibility,
- VisibilityClass::NetworkVisible
- | VisibilityClass::PublicCommunity
- | VisibilityClass::PublicProvenance
- )
-}
-
-fn object_kind_allows_search_result(object_kind: ObjectKind, visibility: VisibilityClass) -> bool {
- match object_kind {
- ObjectKind::BuyerPacket | ObjectKind::RouteStop => false,
- ObjectKind::Proof => visibility == VisibilityClass::PublicProvenance,
- _ => true,
- }
-}
-
-pub fn fixture_search_results(
- query: Option<String>,
- context_id: Option<String>,
-) -> Vec<SearchResultSummary> {
- let normalized_query = query.unwrap_or_default().trim().to_lowercase();
- fixture_object_page_summaries(context_id)
- .into_iter()
- .filter(|page| page.required_authority.is_allowed)
- .filter(|page| visibility_allows_search_result(page.visibility))
- .filter(|page| {
- object_kind_allows_search_result(page.object_ref.object_type, page.visibility)
- })
- .filter(|page| {
- normalized_query.is_empty()
- || page.title.to_lowercase().contains(&normalized_query)
- || format!("{:?}", page.family)
- .to_lowercase()
- .contains(&normalized_query)
- })
- .map(|page| SearchResultSummary {
- id: format!("search_{}", page.object_ref.object_id),
- object_ref: page.object_ref,
- primary_context: page.primary_context,
- title: page.title,
- subtitle: page.subtitle,
- visibility: page.visibility,
- visibility_label: page.visibility_label,
- required_authority: page.required_authority,
- sync_state: page.sync_state,
- })
- .collect()
-}
-
-struct PrototypePathStepFixture {
- id: &'static str,
- label: &'static str,
- context: ActiveContext,
- action_type: Option<AddActionType>,
- object_ref: Option<ObjectRef>,
- domain: AuthorityDomain,
- action: AuthorityAction,
- visibility: VisibilityClass,
- outbox_state: OutboxState,
- sync_state: SyncState,
-}
-
-fn prototype_path_step(fixture: PrototypePathStepFixture) -> PrototypePathStep {
- let PrototypePathStepFixture {
- id,
- label,
- context,
- action_type,
- object_ref,
- domain,
- action,
- visibility,
- outbox_state,
- sync_state,
- } = fixture;
- PrototypePathStep {
- id: id.to_string(),
- label: label.to_string(),
- 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(PrototypePathStepFixture {
- id: "producer_today",
- label: "Farm Today",
- context: farm.clone(),
- action_type: None,
- object_ref: Some(farm.context_ref.clone()),
- domain: AuthorityDomain::FarmWorkspaceOperations,
- action: AuthorityAction::Search,
- visibility: farm.visibility_scope,
- outbox_state: OutboxState::NotQueued,
- sync_state: SyncState::Online,
- }),
- prototype_path_step(PrototypePathStepFixture {
- id: "producer_add_food",
- label: "Add Food",
- context: farm.clone(),
- action_type: Some(AddActionType::Food),
- object_ref: Some(food_ref.clone()),
- domain: AuthorityDomain::FarmWorkspaceOperations,
- action: AuthorityAction::Submit,
- visibility: VisibilityClass::NetworkVisible,
- outbox_state: OutboxState::Draft,
- sync_state: SyncState::Offline,
- }),
- prototype_path_step(PrototypePathStepFixture {
- id: "producer_add_to_route",
- label: "Add to Route",
- context: farm.clone(),
- action_type: Some(AddActionType::RouteNeed),
- object_ref: Some(route_ref.clone()),
- domain: AuthorityDomain::FarmWorkspaceOperations,
- action: AuthorityAction::Share,
- visibility: VisibilityClass::RouteScoped,
- outbox_state: OutboxState::Queued,
- sync_state: SyncState::Syncing,
- }),
- prototype_path_step(PrototypePathStepFixture {
- id: "producer_food_page",
- label: "Food page",
- context: farm.clone(),
- action_type: None,
- object_ref: Some(food_ref.clone()),
- domain: AuthorityDomain::FarmWorkspaceOperations,
- action: AuthorityAction::NavigateRelatedObject,
- visibility: VisibilityClass::NetworkVisible,
- outbox_state: OutboxState::Shared,
- sync_state: SyncState::Synced,
- }),
- prototype_path_step(PrototypePathStepFixture {
- id: "producer_route_page",
- label: "Route page",
- context: route.clone(),
- action_type: None,
- object_ref: Some(route_ref.clone()),
- domain: AuthorityDomain::RouteCoordination,
- action: AuthorityAction::NavigateRelatedObject,
- visibility: VisibilityClass::RouteScoped,
- outbox_state: OutboxState::NotQueued,
- sync_state: 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(PrototypePathStepFixture {
- id: "buyer_today",
- label: "Buyer Today",
- context: buyer.clone(),
- action_type: None,
- object_ref: Some(buyer.context_ref.clone()),
- domain: AuthorityDomain::BuyerWorkspace,
- action: AuthorityAction::Search,
- visibility: buyer.visibility_scope,
- outbox_state: OutboxState::NotQueued,
- sync_state: SyncState::Online,
- }),
- prototype_path_step(PrototypePathStepFixture {
- id: "buyer_request",
- label: "Add Buyer Request",
- context: buyer.clone(),
- action_type: Some(AddActionType::BuyerRequest),
- object_ref: Some(buyer_request_ref.clone()),
- domain: AuthorityDomain::BuyerWorkspace,
- action: AuthorityAction::Submit,
- visibility: VisibilityClass::BuyerScoped,
- outbox_state: OutboxState::Draft,
- sync_state: SyncState::Offline,
- }),
- prototype_path_step(PrototypePathStepFixture {
- id: "buyer_commitment",
- label: "Confirm Commitment",
- context: buyer.clone(),
- action_type: Some(AddActionType::BuyerCommitment),
- object_ref: Some(buyer_request_ref),
- domain: AuthorityDomain::BuyerWorkspace,
- action: AuthorityAction::Approve,
- visibility: VisibilityClass::BuyerScoped,
- outbox_state: OutboxState::Queued,
- sync_state: SyncState::Syncing,
- }),
- prototype_path_step(PrototypePathStepFixture {
- id: "buyer_route",
- label: "Route",
- context: route.clone(),
- action_type: None,
- object_ref: Some(route_ref.clone()),
- domain: AuthorityDomain::RouteCoordination,
- action: AuthorityAction::NavigateRelatedObject,
- visibility: VisibilityClass::RouteScoped,
- outbox_state: OutboxState::Shared,
- sync_state: 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(PrototypePathStepFixture {
- id: "route_today",
- label: "Route Today",
- context: route.clone(),
- action_type: None,
- object_ref: Some(route_ref.clone()),
- domain: AuthorityDomain::RouteCoordination,
- action: AuthorityAction::Search,
- visibility: VisibilityClass::RouteScoped,
- outbox_state: OutboxState::NotQueued,
- sync_state: SyncState::Online,
- }),
- prototype_path_step(PrototypePathStepFixture {
- id: "route_page",
- label: "Route page",
- context: route.clone(),
- action_type: None,
- object_ref: Some(route_ref),
- domain: AuthorityDomain::RouteCoordination,
- action: AuthorityAction::NavigateRelatedObject,
- visibility: VisibilityClass::RouteScoped,
- outbox_state: OutboxState::NotQueued,
- sync_state: SyncState::Online,
- }),
- prototype_path_step(PrototypePathStepFixture {
- id: "route_resolve_blocker",
- label: "Resolve blocker",
- context: route.clone(),
- action_type: Some(AddActionType::Exception),
- object_ref: Some(exception_ref),
- domain: AuthorityDomain::RouteCoordination,
- action: AuthorityAction::Close,
- visibility: VisibilityClass::RouteScoped,
- outbox_state: OutboxState::Conflict,
- sync_state: SyncState::Failed,
- }),
- prototype_path_step(PrototypePathStepFixture {
- id: "route_assign_partner",
- label: "Assign RoutePartner",
- context: route,
- action_type: Some(AddActionType::RouteNeed),
- object_ref: Some(route_partner.context_ref.clone()),
- domain: AuthorityDomain::RouteCoordination,
- action: AuthorityAction::Assign,
- visibility: VisibilityClass::RouteScoped,
- outbox_state: OutboxState::Queued,
- sync_state: SyncState::Syncing,
- }),
- ],
- },
- ]
-}
-
-struct RouteExecutionStepFixture {
- id: &'static str,
- kind: RouteExecutionStepKind,
- label: &'static str,
- actor: WorkflowActor,
- context: ActiveContext,
- object_ref: Option<ObjectRef>,
- domain: AuthorityDomain,
- action: AuthorityAction,
- visibility: VisibilityClass,
- supports_offline: bool,
- supports_partial_receipt: bool,
- uses_receipt_token: bool,
- outbox_state: OutboxState,
- sync_state: SyncState,
- detail_lines: Vec<&'static str>,
-}
-
-fn route_execution_step(
- route_ref: ObjectRef,
- fixture: RouteExecutionStepFixture,
-) -> RouteExecutionStep {
- RouteExecutionStep {
- id: fixture.id.to_string(),
- kind: fixture.kind,
- label: fixture.label.to_string(),
- actor: fixture.actor,
- context: fixture.context.clone(),
- route_ref,
- object_ref: fixture.object_ref,
- required_authority: fixture_authority_gate(
- fixture.actor,
- fixture.context,
- fixture.domain,
- fixture.action,
- ),
- visibility: fixture.visibility,
- supports_offline: fixture.supports_offline,
- supports_partial_receipt: fixture.supports_partial_receipt,
- uses_receipt_token: fixture.uses_receipt_token,
- outbox_state: fixture.outbox_state,
- sync_state: fixture.sync_state,
- detail_lines: fixture
- .detail_lines
- .into_iter()
- .map(str::to_string)
- .collect(),
- }
-}
-
-fn all_route_execution_flows() -> Vec<RouteExecutionFlow> {
- let route = context_for_type(ContextType::Route);
- let route_partner = context_for_type(ContextType::RoutePartner);
- let buyer_receiver = ActiveContext {
- context_type: ContextType::Buyer,
- context_ref: object_ref(
- ObjectKind::BuyerWorkspace,
- "buyer_receiver_workspace_001",
- "Kitchen receiving workspace",
- ),
- actor: WorkflowActor::BuyerReceiver,
- display_label: "Kitchen receiving workspace".to_string(),
- visibility_scope: VisibilityClass::BuyerScoped,
- };
- let route_ref = object_ref(
- ObjectKind::Route,
- "route_thursday_001",
- "Thursday network loop",
- );
- let stop_pickup_ref = object_ref(
- ObjectKind::RouteStop,
- "route_stop_pickup_001",
- "Floripa Farm pickup",
- );
- let stop_dropoff_ref = object_ref(
- ObjectKind::RouteStop,
- "route_stop_dropoff_001",
- "Kitchen drop-off",
- );
- let pickup_proof_ref = object_ref(
- ObjectKind::Proof,
- "proof_pickup_001",
- "Pickup confirmation proof",
- );
- let dropoff_proof_ref = object_ref(
- ObjectKind::Proof,
- "proof_dropoff_001",
- "Drop-off confirmation proof",
- );
- let receipt_ref = object_ref(
- ObjectKind::Proof,
- "receipt_kitchen_001",
- "Kitchen receipt confirmation",
- );
- let exception_ref = object_ref(
- ObjectKind::Exception,
- "exception_short_case_001",
- "Short case divergence",
- );
-
- vec![
- RouteExecutionFlow {
- id: "route_partner_assigned_stops".to_string(),
- kind: RouteExecutionFlowKind::RoutePartnerAssignedStops,
- title: "Assigned route stops".to_string(),
- actor: WorkflowActor::RoutePartner,
- context: route_partner.clone(),
- route_ref: route_ref.clone(),
- steps: vec![
- route_execution_step(
- route_ref.clone(),
- RouteExecutionStepFixture {
- id: "assigned_route",
- kind: RouteExecutionStepKind::AssignedRoute,
- label: "Assigned route",
- actor: WorkflowActor::RoutePartner,
- context: route_partner.clone(),
- object_ref: Some(route_ref.clone()),
- domain: AuthorityDomain::RouteExecution,
- action: AuthorityAction::Search,
- visibility: VisibilityClass::RouteScoped,
- supports_offline: true,
- supports_partial_receipt: false,
- uses_receipt_token: false,
- outbox_state: OutboxState::NotQueued,
- sync_state: SyncState::Online,
- detail_lines: vec![
- "RoutePartner sees the assigned route and assigned stops only.",
- "Buyer packet and private buyer workspace data are not exposed.",
- ],
- },
- ),
- route_execution_step(
- route_ref.clone(),
- RouteExecutionStepFixture {
- id: "pickup_confirmation",
- kind: RouteExecutionStepKind::PickupConfirmation,
- label: "Confirm pickup",
- actor: WorkflowActor::RoutePartner,
- context: route_partner.clone(),
- object_ref: Some(pickup_proof_ref),
- domain: AuthorityDomain::RouteExecution,
- action: AuthorityAction::Submit,
- visibility: VisibilityClass::RouteScoped,
- supports_offline: true,
- supports_partial_receipt: false,
- uses_receipt_token: false,
- outbox_state: OutboxState::Queued,
- sync_state: SyncState::Offline,
- detail_lines: vec![
- "Photo, note, scan, or signature proof can queue offline.",
- "The stop remains scoped to the assigned route.",
- ],
- },
- ),
- route_execution_step(
- route_ref.clone(),
- RouteExecutionStepFixture {
- id: "dropoff_confirmation",
- kind: RouteExecutionStepKind::DropoffConfirmation,
- label: "Confirm drop-off",
- actor: WorkflowActor::RoutePartner,
- context: route_partner,
- object_ref: Some(dropoff_proof_ref),
- domain: AuthorityDomain::RouteExecution,
- action: AuthorityAction::Submit,
- visibility: VisibilityClass::RouteScoped,
- supports_offline: true,
- supports_partial_receipt: false,
- uses_receipt_token: false,
- outbox_state: OutboxState::Syncing,
- sync_state: SyncState::Syncing,
- detail_lines: vec![
- "Drop-off proof syncs when relay connectivity returns.",
- "Receipt confirmation remains separate from route execution.",
- ],
- },
- ),
- route_execution_step(
- route_ref.clone(),
- RouteExecutionStepFixture {
- id: "assigned_pickup_stop",
- kind: RouteExecutionStepKind::AssignedRoute,
- label: "Pickup stop",
- actor: WorkflowActor::RoutePartner,
- context: context_for_type(ContextType::RoutePartner),
- object_ref: Some(stop_pickup_ref),
- domain: AuthorityDomain::RouteExecution,
- action: AuthorityAction::NavigateRelatedObject,
- visibility: VisibilityClass::RouteScoped,
- supports_offline: true,
- supports_partial_receipt: false,
- uses_receipt_token: false,
- outbox_state: OutboxState::NotQueued,
- sync_state: SyncState::Synced,
- detail_lines: vec!["Assigned stop detail is available offline."],
- },
- ),
- route_execution_step(
- route_ref.clone(),
- RouteExecutionStepFixture {
- id: "assigned_dropoff_stop",
- kind: RouteExecutionStepKind::AssignedRoute,
- label: "Drop-off stop",
- actor: WorkflowActor::RoutePartner,
- context: context_for_type(ContextType::RoutePartner),
- object_ref: Some(stop_dropoff_ref),
- domain: AuthorityDomain::RouteExecution,
- action: AuthorityAction::NavigateRelatedObject,
- visibility: VisibilityClass::RouteScoped,
- supports_offline: true,
- supports_partial_receipt: false,
- uses_receipt_token: false,
- outbox_state: OutboxState::NotQueued,
- sync_state: SyncState::Synced,
- detail_lines: vec!["Assigned stop detail is available offline."],
- },
- ),
- ],
- },
- RouteExecutionFlow {
- id: "buyer_receipt_confirmation".to_string(),
- kind: RouteExecutionFlowKind::BuyerReceiptConfirmation,
- title: "Buyer receipt confirmation".to_string(),
- actor: WorkflowActor::BuyerReceiver,
- context: buyer_receiver.clone(),
- route_ref: route_ref.clone(),
- steps: vec![route_execution_step(
- route_ref.clone(),
- RouteExecutionStepFixture {
- id: "receiver_receipt",
- kind: RouteExecutionStepKind::ReceiptConfirmation,
- label: "Confirm full or partial receipt",
- actor: WorkflowActor::BuyerReceiver,
- context: buyer_receiver,
- object_ref: Some(receipt_ref),
- domain: AuthorityDomain::Receipt,
- action: AuthorityAction::Submit,
- visibility: VisibilityClass::BuyerScoped,
- supports_offline: true,
- supports_partial_receipt: true,
- uses_receipt_token: true,
- outbox_state: OutboxState::Queued,
- sync_state: SyncState::Offline,
- detail_lines: vec![
- "BuyerReceiver can confirm full or partial receipt.",
- "A scoped receipt token can be used without exposing buyer workspace data.",
- ],
- },
- )],
- },
- RouteExecutionFlow {
- id: "route_exception_recovery".to_string(),
- kind: RouteExecutionFlowKind::ExceptionRecovery,
- title: "Exception recovery".to_string(),
- actor: WorkflowActor::RoutePartner,
- context: context_for_type(ContextType::RoutePartner),
- route_ref: route_ref.clone(),
- steps: vec![
- route_execution_step(
- route_ref.clone(),
- RouteExecutionStepFixture {
- id: "report_exception",
- kind: RouteExecutionStepKind::ExceptionReport,
- label: "Report divergence",
- actor: WorkflowActor::RoutePartner,
- context: context_for_type(ContextType::RoutePartner),
- object_ref: Some(exception_ref.clone()),
- domain: AuthorityDomain::RouteExecution,
- action: AuthorityAction::Submit,
- visibility: VisibilityClass::RouteScoped,
- supports_offline: true,
- supports_partial_receipt: false,
- uses_receipt_token: false,
- outbox_state: OutboxState::Conflict,
- sync_state: SyncState::Failed,
- detail_lines: vec![
- "Short, damaged, late, or missing-item divergence becomes an exception card.",
- "The exception stays route-scoped until resolved or escalated.",
- ],
- },
- ),
- route_execution_step(
- route_ref,
- RouteExecutionStepFixture {
- id: "resolve_exception",
- kind: RouteExecutionStepKind::RecoveryAction,
- label: "Resolve recovery path",
- actor: WorkflowActor::RouteCoordinator,
- context: route,
- object_ref: Some(exception_ref),
- domain: AuthorityDomain::RouteCoordination,
- action: AuthorityAction::Close,
- visibility: VisibilityClass::RouteScoped,
- supports_offline: false,
- supports_partial_receipt: true,
- uses_receipt_token: false,
- outbox_state: OutboxState::AwaitingAuthority,
- sync_state: SyncState::Online,
- detail_lines: vec![
- "RouteCoordinator chooses correction, partial receipt, replacement, or closure.",
- "Recovery keeps route execution, receipt, and buyer data boundaries separate.",
- ],
- },
- ),
- ],
- },
- ]
-}
-
-pub fn fixture_route_execution_flows(context_id: Option<String>) -> Vec<RouteExecutionFlow> {
- let Some(context_id) = context_id else {
- return all_route_execution_flows();
- };
- let matching: Vec<RouteExecutionFlow> = all_route_execution_flows()
- .into_iter()
- .filter(|flow| {
- flow.context.context_ref.object_id == context_id
- || flow
- .steps
- .iter()
- .any(|step| step.context.context_ref.object_id == context_id)
- })
- .collect();
- if matching.is_empty() {
- all_route_execution_flows()
- } else {
- matching
- }
-}
-
-struct ProofProvenanceArtifactFixture {
- id: &'static str,
- kind: ProofProvenanceArtifactKind,
- review_state: ProofProvenanceReviewState,
- title: &'static str,
- actor: WorkflowActor,
- context: ActiveContext,
- object_ref: ObjectRef,
- source_object_refs: Vec<ObjectRef>,
- domain: AuthorityDomain,
- action: AuthorityAction,
- visibility: VisibilityClass,
- is_public_preview: bool,
- requires_redaction_review: bool,
- can_publish: bool,
- public_summary_lines: Vec<&'static str>,
- redacted_field_labels: Vec<&'static str>,
- outbox_state: OutboxState,
- sync_state: SyncState,
-}
-
-fn proof_provenance_artifact(fixture: ProofProvenanceArtifactFixture) -> ProofProvenanceArtifact {
- ProofProvenanceArtifact {
- id: fixture.id.to_string(),
- kind: fixture.kind,
- review_state: fixture.review_state,
- title: fixture.title.to_string(),
- actor: fixture.actor,
- context: fixture.context.clone(),
- object_ref: fixture.object_ref,
- source_object_refs: fixture.source_object_refs,
- required_authority: fixture_authority_gate(
- fixture.actor,
- fixture.context,
- fixture.domain,
- fixture.action,
- ),
- visibility: fixture.visibility,
- is_public_preview: fixture.is_public_preview,
- requires_redaction_review: fixture.requires_redaction_review,
- can_publish: fixture.can_publish,
- public_summary_lines: fixture
- .public_summary_lines
- .into_iter()
- .map(str::to_string)
- .collect(),
- redacted_field_labels: fixture
- .redacted_field_labels
- .into_iter()
- .map(str::to_string)
- .collect(),
- outbox_state: fixture.outbox_state,
- sync_state: fixture.sync_state,
- }
-}
-
-fn private_provenance_redaction_labels() -> Vec<&'static str> {
- vec![
- "private trace JSON",
- "private buyer details",
- "private evidence",
- "private route stops",
- "worker notes",
- ]
-}
-
-fn all_proof_provenance_artifacts() -> Vec<ProofProvenanceArtifact> {
- let trace = context_for_type(ContextType::TraceRecords);
- let farm = context_for_type(ContextType::Farm);
- let buyer = context_for_type(ContextType::Buyer);
- let proof_ref = object_ref(
- ObjectKind::Proof,
- "proof_route_loop_001",
- "Route loop proof set",
- );
- let producer_proof_ref = object_ref(
- ObjectKind::Proof,
- "proof_farm_lot_001",
- "Farm lot proof set",
- );
- let buyer_packet_ref = object_ref(
- ObjectKind::BuyerPacket,
- "buyer_packet_kitchen_001",
- "Kitchen buyer packet",
- );
- let public_provenance_ref = object_ref(
- ObjectKind::Provenance,
- "public_provenance_squash_001",
- "Summer squash provenance preview",
- );
- 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 receipt_ref = object_ref(
- ObjectKind::Proof,
- "receipt_kitchen_001",
- "Kitchen receipt confirmation",
- );
-
- vec![
- proof_provenance_artifact(ProofProvenanceArtifactFixture {
- id: "trace_proof_completeness",
- kind: ProofProvenanceArtifactKind::ProofCompleteness,
- review_state: ProofProvenanceReviewState::MissingProof,
- title: "Trace proof completeness",
- actor: WorkflowActor::TraceLead,
- context: trace.clone(),
- object_ref: proof_ref.clone(),
- source_object_refs: vec![route_ref.clone(), receipt_ref.clone()],
- domain: AuthorityDomain::TraceProof,
- action: AuthorityAction::Approve,
- visibility: VisibilityClass::WorkspacePrivate,
- is_public_preview: false,
- requires_redaction_review: false,
- can_publish: false,
- public_summary_lines: vec![
- "Internal proof set needs one receipt confirmation before publication review.",
- ],
- redacted_field_labels: Vec::new(),
- outbox_state: OutboxState::NotQueued,
- sync_state: SyncState::Online,
- }),
- proof_provenance_artifact(ProofProvenanceArtifactFixture {
- id: "producer_proof_completeness",
- kind: ProofProvenanceArtifactKind::ProofCompleteness,
- review_state: ProofProvenanceReviewState::Complete,
- title: "Producer proof completeness",
- actor: WorkflowActor::ProducerAdmin,
- context: farm.clone(),
- object_ref: producer_proof_ref.clone(),
- source_object_refs: vec![food_ref.clone(), route_ref.clone()],
- domain: AuthorityDomain::TraceProof,
- action: AuthorityAction::Approve,
- visibility: VisibilityClass::FarmPrivate,
- is_public_preview: false,
- requires_redaction_review: false,
- can_publish: false,
- public_summary_lines: vec![
- "Authorized producer review confirms farm lot proof completeness.",
- ],
- redacted_field_labels: Vec::new(),
- outbox_state: OutboxState::Shared,
- sync_state: SyncState::Synced,
- }),
- proof_provenance_artifact(ProofProvenanceArtifactFixture {
- id: "buyer_packet_draft",
- kind: ProofProvenanceArtifactKind::BuyerPacketDraft,
- review_state: ProofProvenanceReviewState::Draft,
- title: "Buyer packet draft",
- actor: WorkflowActor::BuyerSourcingLead,
- context: buyer.clone(),
- object_ref: buyer_packet_ref.clone(),
- source_object_refs: vec![food_ref.clone(), receipt_ref.clone()],
- domain: AuthorityDomain::BuyerWorkspace,
- action: AuthorityAction::Submit,
- visibility: VisibilityClass::BuyerScoped,
- is_public_preview: false,
- requires_redaction_review: false,
- can_publish: false,
- public_summary_lines: vec!["Buyer packet draft is scoped to the buyer workspace."],
- redacted_field_labels: Vec::new(),
- outbox_state: OutboxState::Draft,
- sync_state: SyncState::Offline,
- }),
- proof_provenance_artifact(ProofProvenanceArtifactFixture {
- id: "buyer_packet_shared",
- kind: ProofProvenanceArtifactKind::BuyerPacketShared,
- review_state: ProofProvenanceReviewState::Shared,
- title: "Buyer packet shared",
- actor: WorkflowActor::BuyerSourcingLead,
- context: buyer,
- object_ref: buyer_packet_ref,
- source_object_refs: vec![producer_proof_ref.clone(), receipt_ref.clone()],
- domain: AuthorityDomain::BuyerWorkspace,
- action: AuthorityAction::Share,
- visibility: VisibilityClass::BuyerScoped,
- is_public_preview: false,
- requires_redaction_review: false,
- can_publish: false,
- public_summary_lines: vec!["Buyer packet has been shared with authorized receivers."],
- redacted_field_labels: Vec::new(),
- outbox_state: OutboxState::Shared,
- sync_state: SyncState::Synced,
- }),
- proof_provenance_artifact(ProofProvenanceArtifactFixture {
- id: "public_provenance_redaction_review",
- kind: ProofProvenanceArtifactKind::PublicProvenancePreview,
- review_state: ProofProvenanceReviewState::RedactionRequired,
- title: "Public provenance redaction review",
- actor: WorkflowActor::ProducerAdmin,
- context: farm.clone(),
- object_ref: public_provenance_ref.clone(),
- source_object_refs: vec![food_ref.clone(), producer_proof_ref.clone()],
- domain: AuthorityDomain::PublicPublishing,
- action: AuthorityAction::Publish,
- visibility: VisibilityClass::PublicProvenance,
- is_public_preview: true,
- requires_redaction_review: true,
- can_publish: false,
- public_summary_lines: vec![
- "Summer squash was grown by Floripa Farm for the Thursday network loop.",
- "Public preview includes farm, food, harvest window, and network-level route summary.",
- ],
- redacted_field_labels: private_provenance_redaction_labels(),
- outbox_state: OutboxState::AwaitingAuthority,
- sync_state: SyncState::Online,
- }),
- proof_provenance_artifact(ProofProvenanceArtifactFixture {
- id: "public_provenance_ready",
- kind: ProofProvenanceArtifactKind::PublicProvenancePreview,
- review_state: ProofProvenanceReviewState::ReadyToPublish,
- title: "Public provenance ready",
- actor: WorkflowActor::ProducerAdmin,
- context: farm,
- object_ref: public_provenance_ref,
- source_object_refs: vec![food_ref, producer_proof_ref],
- domain: AuthorityDomain::PublicPublishing,
- action: AuthorityAction::Publish,
- visibility: VisibilityClass::PublicProvenance,
- is_public_preview: true,
- requires_redaction_review: false,
- can_publish: true,
- public_summary_lines: vec![
- "Summer squash provenance is ready for public community publication.",
- "Preview includes only redacted farm, food, harvest window, and network summary fields.",
- ],
- redacted_field_labels: private_provenance_redaction_labels(),
- outbox_state: OutboxState::Queued,
- sync_state: SyncState::Syncing,
- }),
- ]
-}
-
-pub fn fixture_proof_provenance_artifacts(
- context_id: Option<String>,
-) -> Vec<ProofProvenanceArtifact> {
- let Some(context_id) = context_id else {
- return all_proof_provenance_artifacts();
- };
- let matching: Vec<ProofProvenanceArtifact> = all_proof_provenance_artifacts()
- .into_iter()
- .filter(|artifact| artifact.context.context_ref.object_id == context_id)
- .collect();
- if matching.is_empty() {
- all_proof_provenance_artifacts()
- } else {
- matching
- }
-}
-
-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
- .into_iter()
- .enumerate()
- .map(|(index, outbox_state)| OutboxItem {
- id: format!("outbox_fixture_{index:02}"),
- action_type: AddActionType::PublicUpdate,
- context: context.clone(),
- object_refs: vec![object_ref(
- ObjectKind::Update,
- format!("draft_update_{index:02}"),
- "Public update draft",
- )],
- event_refs: Vec::new(),
- visibility: VisibilityClass::PublicCommunity,
- authority_gate: fixture_authority_gate(
- context.actor,
- context.clone(),
- AuthorityDomain::PublicPublishing,
- AuthorityAction::Retry,
- ),
- flow_state: if matches!(outbox_state, OutboxState::Draft) {
- AddFlowState::Draft
- } else {
- AddFlowState::Queued
- },
- outbox_state,
- sync_state: CANONICAL_SYNC_STATES[index % CANONICAL_SYNC_STATES.len()],
- queued_at_unix: Some(1_799_971_200 + index as u64),
- last_attempt_at_unix: None,
- retry_count: index as u32,
- last_error: if matches!(outbox_state, OutboxState::Failed) {
- Some("fixture failure".to_string())
- } else {
- None
- },
- })
- .collect()
-}
-
-fn outbox_state_allows_retry(state: OutboxState) -> bool {
- matches!(state, OutboxState::Failed | OutboxState::Conflict)
-}
-
-fn outbox_visibility_allows_retry(visibility: VisibilityClass) -> bool {
- !matches!(
- visibility,
- VisibilityClass::LocalDraft | VisibilityClass::SecretNeverShared
- )
-}
-
-pub fn fixture_outbox_retry_decision(item: OutboxItem) -> OutboxRetryDecision {
- let authority_gate = fixture_authority_gate(
- item.context.actor,
- item.context.clone(),
- item.authority_gate.domain,
- AuthorityAction::Retry,
- );
- let state_allows_retry = outbox_state_allows_retry(item.outbox_state);
- let visibility_allows_retry = outbox_visibility_allows_retry(item.visibility);
- let is_retryable = state_allows_retry && visibility_allows_retry && authority_gate.is_allowed;
- let reason = if is_retryable {
- None
- } else if !state_allows_retry {
- Some(format!(
- "{:?} is not a retryable outbox state",
- item.outbox_state
- ))
- } else if !visibility_allows_retry {
- Some(format!(
- "{:?} visibility cannot be retried",
- item.visibility
- ))
- } else {
- authority_gate.reason.clone()
- };
-
- OutboxRetryDecision {
- item_id: item.id,
- is_retryable,
- authority_gate,
- reason,
- }
-}
-
impl RadrootsRuntime {
- pub fn phase1_active_contexts(&self) -> Vec<ActiveContext> {
- let _ = self;
- fixture_active_contexts()
+ /// Returns the exact five Phase 1 Today card types in contract order.
+ pub fn phase1_card_types(&self) -> Vec<TodayCardType> {
+ CANONICAL_TODAY_CARD_TYPES.to_vec()
}
- pub fn phase1_today_cards(&self, context_id: Option<String>) -> Vec<TodayCard> {
- let _ = self;
- fixture_today_cards(context_id)
+ /// Returns the exact five Phase 1 Add commands in card-parity order.
+ pub fn phase1_add_command_types(&self) -> Vec<AddCommandType> {
+ CANONICAL_ADD_COMMAND_TYPES.to_vec()
}
- pub fn phase1_add_actions(&self, context_id: Option<String>) -> Vec<AddAction> {
- let _ = self;
- fixture_add_actions(context_id)
+ /// Returns the closed one-to-one Today/Add mapping.
+ pub fn phase1_card_add_parity(&self) -> Vec<CardAddParity> {
+ CANONICAL_CARD_ADD_PARITY.to_vec()
}
- pub fn phase1_object_page_summaries(
+ /// Constructs a validated local query/composer context.
+ pub fn phase1_local_network(
&self,
- context_id: Option<String>,
- ) -> Vec<ObjectPageSummary> {
- let _ = self;
- fixture_object_page_summaries(context_id)
- }
-
- pub fn phase1_outbox_snapshot(&self) -> Vec<OutboxItem> {
- let _ = self;
- fixture_outbox_items()
- }
-
- pub fn phase1_search_results(
- &self,
- query: Option<String>,
- context_id: Option<String>,
- ) -> Vec<SearchResultSummary> {
- let _ = self;
- fixture_search_results(query, context_id)
- }
-
- pub fn phase1_prototype_paths(&self) -> Vec<PrototypePath> {
- let _ = self;
- fixture_prototype_paths()
- }
-
- pub fn phase1_route_execution_flows(
- &self,
- context_id: Option<String>,
- ) -> Vec<RouteExecutionFlow> {
- let _ = self;
- fixture_route_execution_flows(context_id)
- }
-
- pub fn phase1_proof_provenance_artifacts(
- &self,
- context_id: Option<String>,
- ) -> Vec<ProofProvenanceArtifact> {
- let _ = self;
- 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)
- }
-
- pub fn phase1_check_authority(
- &self,
- actor: WorkflowActor,
- context: ActiveContext,
- domain: AuthorityDomain,
- action: AuthorityAction,
- ) -> AuthorityGate {
- let _ = self;
- fixture_authority_gate(actor, context, domain, action)
+ id: String,
+ label: String,
+ relay_urls: Vec<String>,
+ locality: Option<String>,
+ followed_authors: Vec<String>,
+ generation: u64,
+ ) -> Result<LocalNetwork, crate::RadrootsAppError> {
+ LocalNetwork::new(
+ id,
+ label,
+ relay_urls,
+ locality,
+ followed_authors,
+ generation,
+ )
+ .map_err(|error| crate::RadrootsAppError::runtime(error.to_string()))
}
}
#[cfg(test)]
-#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
- fn object_ref(object_type: ObjectKind, object_id: &str, display_label: &str) -> ObjectRef {
- ObjectRef {
- object_type,
- object_id: object_id.to_string(),
- display_label: display_label.to_string(),
- }
- }
-
- fn active_context() -> ActiveContext {
- ActiveContext {
- context_type: ContextType::Farm,
- context_ref: object_ref(ObjectKind::Farm, "farm_123", "Root & Rad Farm"),
- actor: WorkflowActor::ProducerAdmin,
- display_label: "Root & Rad Farm".to_string(),
- visibility_scope: VisibilityClass::FarmPrivate,
- }
- }
-
- fn authority_gate() -> AuthorityGate {
- AuthorityGate {
- domain: AuthorityDomain::FarmWorkspaceOperations,
- action: AuthorityAction::Submit,
- actor: WorkflowActor::ProducerAdmin,
- context: active_context(),
- is_required: true,
- is_allowed: true,
- reason: None,
- }
- }
-
#[test]
- fn runtime_wrappers_expose_the_complete_phase_1_surface() {
+ fn runtime_exposes_only_the_locked_card_and_add_catalogs() {
let runtime = RadrootsRuntime::new().expect("runtime");
- let contexts = runtime.phase1_active_contexts();
- let context = contexts.first().expect("context").clone();
- let context_id = Some(context.context_ref.object_id.clone());
-
- assert!(!runtime.phase1_today_cards(context_id.clone()).is_empty());
- assert!(!runtime.phase1_add_actions(context_id.clone()).is_empty());
- assert!(
- !runtime
- .phase1_object_page_summaries(context_id.clone())
- .is_empty()
- );
- assert!(!runtime.phase1_outbox_snapshot().is_empty());
- assert!(
- !runtime
- .phase1_search_results(Some("farm".to_owned()), context_id.clone())
- .is_empty()
- );
- assert!(!runtime.phase1_prototype_paths().is_empty());
- assert!(
- !runtime
- .phase1_route_execution_flows(context_id.clone())
- .is_empty()
- );
- assert!(
- !runtime
- .phase1_proof_provenance_artifacts(context_id.clone())
- .is_empty()
- );
- assert!(
- !runtime
- .phase1_stewardship_access_items(context_id)
- .is_empty()
- );
-
- let item = runtime
- .phase1_outbox_snapshot()
- .into_iter()
- .find(|item| item.outbox_state == OutboxState::Failed)
- .expect("failed outbox fixture");
- let decision = runtime.phase1_outbox_retry_decision(item);
- assert!(decision.is_retryable);
-
- let gate = runtime.phase1_check_authority(
- context.actor,
- context.clone(),
- AuthorityDomain::RelayGroupAccess,
- AuthorityAction::Search,
- );
- assert_eq!(gate.context, context);
-
- assert!(!runtime.phase1_search_results(None, None).is_empty());
-
- let flows = fixture_route_execution_flows(None);
- let direct_context = flows[0].context.context_ref.object_id.clone();
- assert!(!fixture_route_execution_flows(Some(direct_context)).is_empty());
- let step_context = flows
- .iter()
- .flat_map(|flow| {
- flow.steps
- .iter()
- .map(move |step| (&flow.context.context_ref.object_id, step))
- })
- .find(|(flow_context, step)| {
- flow_context.as_str() != step.context.context_ref.object_id
- })
- .map(|(_, step)| step.context.context_ref.object_id.clone())
- .expect("step-only context");
- assert!(!fixture_route_execution_flows(Some(step_context)).is_empty());
- assert_eq!(
- fixture_route_execution_flows(Some("unknown_context".to_owned())).len(),
- flows.len()
- );
-
- let proof_context = fixture_proof_provenance_artifacts(None)[0]
- .context
- .context_ref
- .object_id
- .clone();
- assert!(!fixture_proof_provenance_artifacts(Some(proof_context)).is_empty());
- let stewardship_context = fixture_stewardship_access_items(None)[0]
- .context
- .context_ref
- .object_id
- .clone();
- assert!(!fixture_stewardship_access_items(Some(stewardship_context)).is_empty());
- }
-
- #[test]
- fn canonical_vocabularies_match_phase_1_spec_counts() {
- assert_eq!(CANONICAL_CONTEXT_TYPES.len(), 10);
- assert_eq!(CANONICAL_WORKFLOW_ACTORS.len(), 11);
- assert_eq!(CANONICAL_VISIBILITY_CLASSES.len(), 9);
- assert_eq!(CANONICAL_AUTHORITY_DOMAINS.len(), 9);
- assert_eq!(CANONICAL_TODAY_CARD_TYPES.len(), 12);
- assert_eq!(TODAY_CARD_RANKING_PRIORITY.len(), 12);
- assert_eq!(CANONICAL_ADD_ACTION_TYPES.len(), 18);
- assert_eq!(CANONICAL_ADD_FLOW_STATES.len(), 16);
- assert_eq!(CANONICAL_OBJECT_PAGE_FAMILIES.len(), 13);
- assert_eq!(CANONICAL_OUTBOX_STATES.len(), 10);
- assert_eq!(CANONICAL_SYNC_STATES.len(), 7);
- assert_eq!(CANONICAL_ROUTE_EXECUTION_FLOW_KINDS.len(), 3);
- 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]
- fn today_card_ranking_priority_covers_every_card_type() {
- for card_type in CANONICAL_TODAY_CARD_TYPES {
- assert!(TODAY_CARD_RANKING_PRIORITY.contains(&card_type));
- }
- assert_eq!(TODAY_CARD_RANKING_PRIORITY[0], TodayCardType::Exception);
- assert_eq!(TODAY_CARD_RANKING_PRIORITY[1], TodayCardType::SyncOutbox);
- assert_eq!(TODAY_CARD_RANKING_PRIORITY[2], TodayCardType::Route);
-
- let cards = fixture_today_cards(None);
- assert_eq!(cards[0].card_type, TodayCardType::Exception);
- assert_eq!(cards[0].ranking_reason, "blocking exception");
- assert_eq!(cards[1].card_type, TodayCardType::SyncOutbox);
- assert_eq!(cards[1].outbox_state, OutboxState::Failed);
- assert!(cards[1].is_offline);
- }
-
- #[test]
- fn fixture_backed_projection_apis_cover_required_surface() {
- assert_eq!(
- fixture_active_contexts().len(),
- CANONICAL_CONTEXT_TYPES.len()
- );
- assert_eq!(
- fixture_today_cards(None).len(),
- CANONICAL_TODAY_CARD_TYPES.len()
- );
- assert_eq!(
- fixture_add_actions(None).len(),
- CANONICAL_ADD_ACTION_TYPES.len()
- );
- assert_eq!(
- fixture_object_page_summaries(None).len(),
- CANONICAL_OBJECT_PAGE_FAMILIES.len()
- );
- assert_eq!(fixture_outbox_items().len(), CANONICAL_OUTBOX_STATES.len());
- assert_eq!(
- fixture_route_execution_flows(None).len(),
- 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]
- fn object_page_fixtures_carry_routeable_refs_and_navigation_authority() {
- let pages = fixture_object_page_summaries(None);
- assert_eq!(pages.len(), CANONICAL_OBJECT_PAGE_FAMILIES.len());
-
- for (page, family) in pages.iter().zip(CANONICAL_OBJECT_PAGE_FAMILIES) {
- assert_eq!(page.family, family);
- assert_eq!(page.object_ref.object_type, object_kind_for_page(family));
- assert_eq!(
- page.required_authority.action,
- AuthorityAction::NavigateRelatedObject
- );
- assert_eq!(
- page.required_authority.domain,
- AuthorityDomain::RelayGroupAccess
- );
- assert_eq!(
- page.required_authority.context.context_ref.object_id,
- page.primary_context.context_ref.object_id
- );
- assert!(!page.object_ref.object_id.is_empty());
- assert!(!page.title.is_empty());
- }
- }
-
- #[test]
- fn authority_visibility_fixtures_cover_every_actor_and_visibility_class() {
- let context = active_context();
- for actor in CANONICAL_WORKFLOW_ACTORS {
- let gate = fixture_authority_gate(
- actor,
- context.clone(),
- AuthorityDomain::RelayGroupAccess,
- AuthorityAction::Search,
- );
- assert_eq!(gate.actor, actor);
- assert_eq!(gate.action, AuthorityAction::Search);
- }
-
- for visibility in CANONICAL_VISIBILITY_CLASSES {
- let result_allowed = visibility_allows_search_result(visibility);
- if matches!(
- visibility,
- VisibilityClass::LocalDraft
- | VisibilityClass::FarmPrivate
- | VisibilityClass::WorkspacePrivate
- | VisibilityClass::RouteScoped
- | VisibilityClass::BuyerScoped
- | VisibilityClass::SecretNeverShared
- ) {
- assert!(!result_allowed);
- }
- }
- }
-
- #[test]
- fn search_results_filter_private_and_unauthorized_surfaces() {
- let network = context_for_type(ContextType::Network);
- let results = fixture_search_results(
- Some("fixture".to_string()),
- Some(network.context_ref.object_id),
- );
- assert!(!results.is_empty());
- assert!(
- results
- .iter()
- .all(|result| result.required_authority.is_allowed)
- );
- assert!(
- results
- .iter()
- .all(|result| visibility_allows_search_result(result.visibility))
- );
- assert!(
- results
- .iter()
- .all(|result| object_kind_allows_search_result(
- result.object_ref.object_type,
- result.visibility
- ))
- );
- assert!(
- !results
- .iter()
- .any(|result| result.object_ref.object_type == ObjectKind::BuyerPacket)
- );
- assert!(
- !results
- .iter()
- .any(|result| result.object_ref.object_type == ObjectKind::RouteStop)
- );
-
- let farm = context_for_type(ContextType::Farm);
- let denied = fixture_search_results(
- Some("fixture".to_string()),
- Some(farm.context_ref.object_id),
- );
- assert!(denied.is_empty());
- }
-
- #[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 route_execution_flows_cover_partner_receipt_and_exception_paths() {
- let flows = fixture_route_execution_flows(None);
- assert_eq!(flows.len(), 3);
-
- for kind in CANONICAL_ROUTE_EXECUTION_FLOW_KINDS {
- assert!(
- flows.iter().any(|flow| flow.kind == kind),
- "missing {kind:?}"
- );
- }
-
- let steps: Vec<&RouteExecutionStep> =
- flows.iter().flat_map(|flow| flow.steps.iter()).collect();
- for kind in CANONICAL_ROUTE_EXECUTION_STEP_KINDS {
- assert!(
- steps.iter().any(|step| step.kind == kind),
- "missing {kind:?}"
- );
- }
-
- assert!(steps.iter().any(|step| {
- step.kind == RouteExecutionStepKind::PickupConfirmation
- && step.supports_offline
- && step
- .object_ref
- .as_ref()
- .is_some_and(|object_ref| object_ref.object_type == ObjectKind::Proof)
- }));
- assert!(steps.iter().any(|step| {
- step.kind == RouteExecutionStepKind::DropoffConfirmation
- && step.supports_offline
- && step
- .object_ref
- .as_ref()
- .is_some_and(|object_ref| object_ref.object_type == ObjectKind::Proof)
- }));
- assert!(
- steps
- .iter()
- .any(|step| step.kind == RouteExecutionStepKind::ExceptionReport
- && step.outbox_state == OutboxState::Conflict)
- );
- assert!(
- fixture_today_cards(None)
- .iter()
- .any(|card| card.card_type == TodayCardType::Exception)
- );
- }
-
- #[test]
- fn route_partner_execution_flow_is_assigned_route_scoped_only() {
- let flow = fixture_route_execution_flows(None)
- .into_iter()
- .find(|flow| flow.kind == RouteExecutionFlowKind::RoutePartnerAssignedStops)
- .expect("route partner flow");
- assert_eq!(flow.actor, WorkflowActor::RoutePartner);
- assert_eq!(flow.context.actor, WorkflowActor::RoutePartner);
-
- for step in &flow.steps {
- assert_eq!(step.actor, WorkflowActor::RoutePartner);
- assert_eq!(step.visibility, VisibilityClass::RouteScoped);
- assert_eq!(
- step.required_authority.domain,
- AuthorityDomain::RouteExecution
- );
- assert!(step.required_authority.is_allowed);
- assert_ne!(step.required_authority.domain, AuthorityDomain::Receipt);
- assert_ne!(
- step.required_authority.domain,
- AuthorityDomain::BuyerWorkspace
- );
- assert!(matches!(
- step.object_ref
- .as_ref()
- .map(|object_ref| object_ref.object_type),
- Some(ObjectKind::Route | ObjectKind::RouteStop | ObjectKind::Proof)
- ));
- }
- }
-
- #[test]
- fn buyer_receipt_flow_supports_partial_receipt_token_without_route_execution() {
- let flow = fixture_route_execution_flows(None)
- .into_iter()
- .find(|flow| flow.kind == RouteExecutionFlowKind::BuyerReceiptConfirmation)
- .expect("buyer receipt flow");
- assert_eq!(flow.actor, WorkflowActor::BuyerReceiver);
- assert_eq!(flow.context.actor, WorkflowActor::BuyerReceiver);
- assert_eq!(flow.steps.len(), 1);
-
- let receipt = &flow.steps[0];
- assert_eq!(receipt.kind, RouteExecutionStepKind::ReceiptConfirmation);
- assert_eq!(receipt.required_authority.domain, AuthorityDomain::Receipt);
- assert_eq!(receipt.required_authority.action, AuthorityAction::Submit);
- assert!(receipt.required_authority.is_allowed);
- assert!(receipt.supports_partial_receipt);
- assert!(receipt.uses_receipt_token);
- assert_ne!(
- receipt.required_authority.domain,
- AuthorityDomain::RouteExecution
- );
- assert_eq!(receipt.visibility, VisibilityClass::BuyerScoped);
- }
-
- #[test]
- fn route_exception_recovery_separates_reporting_from_coordination() {
- let flow = fixture_route_execution_flows(None)
- .into_iter()
- .find(|flow| flow.kind == RouteExecutionFlowKind::ExceptionRecovery)
- .expect("exception recovery flow");
- let report = flow
- .steps
- .iter()
- .find(|step| step.kind == RouteExecutionStepKind::ExceptionReport)
- .expect("report step");
- let recovery = flow
- .steps
- .iter()
- .find(|step| step.kind == RouteExecutionStepKind::RecoveryAction)
- .expect("recovery step");
-
- assert_eq!(report.actor, WorkflowActor::RoutePartner);
+ assert_eq!(runtime.phase1_card_types(), CANONICAL_TODAY_CARD_TYPES);
assert_eq!(
- report.required_authority.domain,
- AuthorityDomain::RouteExecution
+ runtime.phase1_add_command_types(),
+ CANONICAL_ADD_COMMAND_TYPES
);
- assert_eq!(report.outbox_state, OutboxState::Conflict);
- assert!(report.supports_offline);
- assert_eq!(recovery.actor, WorkflowActor::RouteCoordinator);
+ assert_eq!(runtime.phase1_card_add_parity(), CANONICAL_CARD_ADD_PARITY);
assert_eq!(
- recovery.required_authority.domain,
- AuthorityDomain::RouteCoordination
- );
- assert_eq!(recovery.required_authority.action, AuthorityAction::Close);
- assert!(recovery.supports_partial_receipt);
- assert!(recovery.required_authority.is_allowed);
- }
-
- #[test]
- fn proof_provenance_artifacts_cover_required_kinds_and_states() {
- let artifacts = fixture_proof_provenance_artifacts(None);
- for kind in CANONICAL_PROOF_PROVENANCE_ARTIFACT_KINDS {
- assert!(
- artifacts.iter().any(|artifact| artifact.kind == kind),
- "missing {kind:?}"
- );
- }
-
- assert!(artifacts.iter().any(|artifact| {
- artifact.kind == ProofProvenanceArtifactKind::ProofCompleteness
- && artifact.review_state == ProofProvenanceReviewState::MissingProof
- }));
- assert!(artifacts.iter().any(|artifact| {
- artifact.kind == ProofProvenanceArtifactKind::ProofCompleteness
- && artifact.review_state == ProofProvenanceReviewState::Complete
- }));
- assert!(artifacts.iter().any(|artifact| {
- artifact.kind == ProofProvenanceArtifactKind::BuyerPacketDraft
- && artifact.review_state == ProofProvenanceReviewState::Draft
- }));
- assert!(artifacts.iter().any(|artifact| {
- artifact.kind == ProofProvenanceArtifactKind::BuyerPacketShared
- && artifact.review_state == ProofProvenanceReviewState::Shared
- }));
- assert!(artifacts.iter().any(|artifact| {
- artifact.kind == ProofProvenanceArtifactKind::PublicProvenancePreview
- && artifact.review_state == ProofProvenanceReviewState::RedactionRequired
- }));
- assert!(artifacts.iter().any(|artifact| {
- artifact.kind == ProofProvenanceArtifactKind::PublicProvenancePreview
- && artifact.review_state == ProofProvenanceReviewState::ReadyToPublish
- }));
- }
-
- #[test]
- fn trace_lead_and_authorized_producer_can_review_proof_completeness() {
- let proof_artifacts: Vec<ProofProvenanceArtifact> =
- fixture_proof_provenance_artifacts(None)
- .into_iter()
- .filter(|artifact| artifact.kind == ProofProvenanceArtifactKind::ProofCompleteness)
- .collect();
- assert_eq!(proof_artifacts.len(), 2);
- assert!(proof_artifacts.iter().any(|artifact| {
- artifact.actor == WorkflowActor::TraceLead
- && artifact.required_authority.domain == AuthorityDomain::TraceProof
- && artifact.required_authority.action == AuthorityAction::Approve
- && artifact.required_authority.is_allowed
- }));
- assert!(proof_artifacts.iter().any(|artifact| {
- artifact.actor == WorkflowActor::ProducerAdmin
- && artifact.required_authority.domain == AuthorityDomain::TraceProof
- && artifact.required_authority.action == AuthorityAction::Approve
- && artifact.required_authority.is_allowed
- }));
- }
-
- #[test]
- fn buyer_packets_are_private_and_public_provenance_is_distinct() {
- let artifacts = fixture_proof_provenance_artifacts(None);
- let buyer_packets: Vec<&ProofProvenanceArtifact> = artifacts
- .iter()
- .filter(|artifact| {
- matches!(
- artifact.kind,
- ProofProvenanceArtifactKind::BuyerPacketDraft
- | ProofProvenanceArtifactKind::BuyerPacketShared
+ runtime
+ .phase1_local_network(
+ "nearby".into(),
+ "Near me".into(),
+ vec!["wss://relay.example".into()],
+ Some("u10h".into()),
+ vec!["a".repeat(64)],
+ 1,
)
- })
- .collect();
- assert_eq!(buyer_packets.len(), 2);
- assert!(buyer_packets.iter().all(|artifact| {
- artifact.object_ref.object_type == ObjectKind::BuyerPacket
- && artifact.visibility == VisibilityClass::BuyerScoped
- && !artifact.is_public_preview
- && !artifact.can_publish
- }));
-
- let public_previews: Vec<&ProofProvenanceArtifact> = artifacts
- .iter()
- .filter(|artifact| {
- artifact.kind == ProofProvenanceArtifactKind::PublicProvenancePreview
- })
- .collect();
- assert_eq!(public_previews.len(), 2);
- assert!(public_previews.iter().all(|artifact| {
- artifact.object_ref.object_type == ObjectKind::Provenance
- && artifact.visibility == VisibilityClass::PublicProvenance
- && artifact.is_public_preview
- && artifact.required_authority.domain == AuthorityDomain::PublicPublishing
- }));
- }
-
- #[test]
- fn public_provenance_publication_requires_authority_and_redaction_review() {
- let artifacts = fixture_proof_provenance_artifacts(None);
- let review = artifacts
- .iter()
- .find(|artifact| {
- artifact.id == "public_provenance_redaction_review"
- && artifact.kind == ProofProvenanceArtifactKind::PublicProvenancePreview
- })
- .expect("redaction review artifact");
- assert!(review.required_authority.is_allowed);
- assert_eq!(review.required_authority.action, AuthorityAction::Publish);
- assert!(review.requires_redaction_review);
- assert!(!review.can_publish);
- assert_eq!(review.outbox_state, OutboxState::AwaitingAuthority);
-
- let ready = artifacts
- .iter()
- .find(|artifact| artifact.id == "public_provenance_ready")
- .expect("ready artifact");
- assert!(ready.required_authority.is_allowed);
- assert!(!ready.requires_redaction_review);
- assert!(ready.can_publish);
- assert_eq!(
- ready.review_state,
- ProofProvenanceReviewState::ReadyToPublish
- );
- }
-
- #[test]
- fn public_provenance_never_leaks_private_trace_or_buyer_fields() {
- let public_previews: Vec<ProofProvenanceArtifact> =
- fixture_proof_provenance_artifacts(None)
- .into_iter()
- .filter(|artifact| {
- artifact.kind == ProofProvenanceArtifactKind::PublicProvenancePreview
- })
- .collect();
- assert!(!public_previews.is_empty());
-
- let blocked = private_provenance_redaction_labels();
- for artifact in public_previews {
- for label in &blocked {
- assert!(
- artifact
- .redacted_field_labels
- .iter()
- .any(|redacted| redacted == label)
- );
- }
-
- let public_text = artifact.public_summary_lines.join(" ").to_lowercase();
- for label in &blocked {
- assert!(
- !public_text.contains(&label.to_lowercase()),
- "{label} leaked into public summary"
- );
- }
- assert!(
- !artifact
- .source_object_refs
- .iter()
- .any(|object_ref| object_ref.object_type == ObjectKind::BuyerPacket)
- );
- assert!(
- !artifact
- .source_object_refs
- .iter()
- .any(|object_ref| object_ref.object_type == ObjectKind::RouteStop)
- );
- }
- }
-
- #[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"),
- "NetworkMember"
- );
- assert_eq!(
- serde_json::to_value(VisibilityClass::PublicProvenance).expect("serialize visibility"),
- "PublicProvenance"
- );
- assert_eq!(
- serde_json::to_value(AuthorityDomain::RelayGroupAccess).expect("serialize authority"),
- "Relay/group access"
- );
- assert_eq!(
- serde_json::to_value(AddActionType::BuyerCommitment).expect("serialize action"),
- "BuyerCommitment"
- );
- }
-
- #[test]
- fn product_surface_records_round_trip_through_json() {
- let card = TodayCard {
- id: "card_route_gap_001".to_string(),
- card_type: TodayCardType::Proof,
- source_object_refs: vec![object_ref(ObjectKind::Route, "route_123", "Thursday loop")],
- source_event_refs: vec![EventRef {
- event_id: "event_abc".to_string(),
- relay_url: Some("wss://relay.example".to_string()),
- kind: Some(1),
- }],
- primary_context: active_context(),
- actor: WorkflowActor::ProducerAdmin,
- visibility: VisibilityClass::RouteScoped,
- visibility_label: "route crew".to_string(),
- title: "Proof needed for Thursday loop".to_string(),
- status_line: "missing pickup confirmation".to_string(),
- detail_lines: vec!["2 stops need proof".to_string()],
- pills: vec!["blocking".to_string(), "route".to_string()],
- primary_action: TodayCardAction {
- id: "add_proof".to_string(),
- label: "Add proof".to_string(),
- action_type: Some(AddActionType::Proof),
- target_object: Some(object_ref(ObjectKind::Route, "route_123", "Thursday loop")),
- },
- secondary_action: None,
- ranking_reason: "blocking exception".to_string(),
- ranking_features: vec!["proof_gap".to_string()],
- sync_state: SyncState::Online,
- outbox_state: OutboxState::NotQueued,
- is_stale: false,
- is_offline: false,
- };
-
- let json = serde_json::to_string(&card).expect("serialize card");
- let decoded: TodayCard = serde_json::from_str(&json).expect("decode card");
- assert_eq!(decoded, card);
- assert!(json.contains("\"cardType\":\"Proof\""));
- assert!(json.contains("\"visibility\":\"RouteScoped\""));
- }
-
- #[test]
- fn add_action_and_outbox_contract_carry_authority_and_visibility() {
- let add_action = AddAction {
- action_type: AddActionType::PublicUpdate,
- display_label: "Public update".to_string(),
- allowed_context_types: vec![ContextType::Network, ContextType::Farm],
- required_authority: authority_gate(),
- default_visibility: VisibilityClass::PublicCommunity,
- allowed_visibility_options: vec![
- VisibilityClass::NetworkVisible,
- VisibilityClass::PublicCommunity,
- ],
- created_or_updated_object_type: ObjectKind::Update,
- related_object_requirements: vec![RelatedObjectRequirement {
- object_type: ObjectKind::Farm,
- relationship_label: "posted by".to_string(),
- is_required: true,
- }],
- validation_requirements: vec![ValidationRequirement {
- id: "non_empty_body".to_string(),
- label: "Body is required".to_string(),
- is_blocking: true,
- }],
- supports_offline: true,
- supports_draft: true,
- outbox_behavior: OutboxBehavior::PublishWhenAuthorized,
- primary_submit_label: "Publish".to_string(),
- completion_state: AddFlowState::ReadyToSubmit,
- };
- let outbox_item = OutboxItem {
- id: "outbox_001".to_string(),
- action_type: add_action.action_type,
- context: active_context(),
- object_refs: vec![object_ref(
- ObjectKind::Update,
- "draft_001",
- "Public update draft",
- )],
- event_refs: Vec::new(),
- visibility: add_action.default_visibility,
- authority_gate: add_action.required_authority.clone(),
- flow_state: AddFlowState::Queued,
- outbox_state: OutboxState::AwaitingAuthority,
- sync_state: SyncState::Offline,
- queued_at_unix: Some(1_799_971_200),
- last_attempt_at_unix: None,
- retry_count: 0,
- last_error: None,
- };
-
- assert_eq!(
- outbox_item.authority_gate.domain,
- AuthorityDomain::FarmWorkspaceOperations
- );
- assert_eq!(outbox_item.visibility, VisibilityClass::PublicCommunity);
- assert_eq!(outbox_item.flow_state, AddFlowState::Queued);
- assert_eq!(outbox_item.sync_state, SyncState::Offline);
- }
-
- #[test]
- fn outbox_retry_decision_rechecks_state_visibility_and_authority() {
- let failed = fixture_outbox_items()
- .into_iter()
- .find(|item| item.outbox_state == OutboxState::Failed)
- .expect("failed outbox fixture");
-
- let retryable = fixture_outbox_retry_decision(failed.clone());
- assert!(retryable.is_retryable);
- assert_eq!(retryable.item_id, failed.id);
- assert_eq!(retryable.authority_gate.action, AuthorityAction::Retry);
- assert!(retryable.reason.is_none());
-
- let mut queued = failed.clone();
- queued.outbox_state = OutboxState::Queued;
- let queued_decision = fixture_outbox_retry_decision(queued);
- assert!(!queued_decision.is_retryable);
- assert!(
- queued_decision
- .reason
- .as_deref()
- .expect("queued reason")
- .contains("not a retryable outbox state")
- );
-
- let mut secret = failed.clone();
- secret.visibility = VisibilityClass::SecretNeverShared;
- let secret_decision = fixture_outbox_retry_decision(secret);
- assert!(!secret_decision.is_retryable);
- assert!(
- secret_decision
- .reason
- .as_deref()
- .expect("secret reason")
- .contains("visibility cannot be retried")
- );
-
- let mut denied = failed;
- denied.authority_gate.domain = AuthorityDomain::BuyerWorkspace;
- let denied_decision = fixture_outbox_retry_decision(denied);
- assert!(!denied_decision.is_retryable);
- assert!(!denied_decision.authority_gate.is_allowed);
- assert!(denied_decision.reason.is_some());
- }
-
- #[test]
- fn compatibility_note_quarantines_low_level_roles() {
- assert!(WORKFLOW_ACTOR_COMPATIBILITY_NOTE.contains("Farmer"));
- assert!(WORKFLOW_ACTOR_COMPATIBILITY_NOTE.contains("Buyer"));
- assert!(WORKFLOW_ACTOR_COMPATIBILITY_NOTE.contains("not sufficient authority"));
- }
-
- #[test]
- fn authority_fixture_allows_and_denies_by_actor_domain_pair() {
- let context = active_context();
- let allowed = fixture_authority_gate(
- WorkflowActor::ProducerAdmin,
- context.clone(),
- AuthorityDomain::FarmWorkspaceOperations,
- AuthorityAction::Submit,
- );
- assert!(allowed.is_allowed);
- assert_eq!(allowed.reason, None);
-
- let denied = fixture_authority_gate(
- WorkflowActor::NetworkMember,
- context,
- AuthorityDomain::FarmWorkspaceOperations,
- AuthorityAction::Submit,
+ .expect("network")
+ .id,
+ "nearby"
+ );
+ assert!(
+ runtime
+ .phase1_local_network(
+ "nearby".into(),
+ "Near me".into(),
+ Vec::new(),
+ None,
+ Vec::new(),
+ 1,
+ )
+ .is_err()
);
- assert!(!denied.is_allowed);
- assert!(denied.reason.is_some());
}
}
diff --git a/core/crates/tera_core/src/runtime/product_surface/context.rs b/core/crates/tera_core/src/runtime/product_surface/context.rs
@@ -0,0 +1,338 @@
+use std::collections::BTreeSet;
+
+use radroots_event::id::RelayUrl;
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+const CONTEXT_TEXT_MAX_BYTES: usize = 256;
+const RELAY_URL_MAX_BYTES: usize = 2_048;
+
+/// A validated local query/composer context. It has no Nostr event identity.
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct LocalNetwork {
+ pub id: String,
+ pub label: String,
+ pub relay_urls: Vec<String>,
+ pub locality: Option<String>,
+ pub followed_authors: Vec<String>,
+ pub generation: u64,
+}
+
+#[derive(Clone, Debug, Error, Eq, PartialEq)]
+pub enum LocalNetworkError {
+ #[error("local network {field} is invalid")]
+ InvalidText { field: &'static str },
+ #[error("local network requires at least one relay")]
+ MissingRelay,
+ #[error("local network relay URL is invalid")]
+ InvalidRelay,
+ #[error("local network relay URLs must be unique")]
+ DuplicateRelay,
+ #[error("local network followed author is invalid")]
+ InvalidAuthor,
+ #[error("local network followed authors must be unique")]
+ DuplicateAuthor,
+}
+
+impl LocalNetwork {
+ pub fn new(
+ id: String,
+ label: String,
+ relay_urls: Vec<String>,
+ locality: Option<String>,
+ followed_authors: Vec<String>,
+ generation: u64,
+ ) -> Result<Self, LocalNetworkError> {
+ validate_text(&id, "id")?;
+ validate_text(&label, "label")?;
+ if let Some(locality) = locality.as_deref() {
+ validate_text(locality, "locality")?;
+ }
+ if relay_urls.is_empty() {
+ return Err(LocalNetworkError::MissingRelay);
+ }
+ let mut relays = BTreeSet::new();
+ for relay in &relay_urls {
+ if relay.is_empty()
+ || relay.len() > RELAY_URL_MAX_BYTES
+ || !relay.starts_with("wss://")
+ || RelayUrl::parse(relay).is_err()
+ {
+ return Err(LocalNetworkError::InvalidRelay);
+ }
+ if !relays.insert(relay) {
+ return Err(LocalNetworkError::DuplicateRelay);
+ }
+ }
+ let mut authors = BTreeSet::new();
+ for author in &followed_authors {
+ if author.len() != 64
+ || !author
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ return Err(LocalNetworkError::InvalidAuthor);
+ }
+ if !authors.insert(author) {
+ return Err(LocalNetworkError::DuplicateAuthor);
+ }
+ }
+ Ok(Self {
+ id,
+ label,
+ relay_urls,
+ locality,
+ followed_authors,
+ generation,
+ })
+ }
+
+ /// Applies the locked locality policy to the selected local context.
+ pub const fn admit(&self, evidence: LocalityEvidence) -> LocalNetworkAdmission {
+ match evidence {
+ LocalityEvidence::Match => LocalNetworkAdmission::Included(ContextAdmission {
+ rank: ContextRank::LocalityMatch,
+ reason: "locality_match",
+ }),
+ LocalityEvidence::Missing => LocalNetworkAdmission::Included(ContextAdmission {
+ rank: ContextRank::MissingLocalityFallback,
+ reason: "locality_missing_fallback",
+ }),
+ LocalityEvidence::Nonmatch => LocalNetworkAdmission::Excluded {
+ reason: "locality_nonmatch",
+ },
+ }
+ }
+}
+
+fn validate_text(value: &str, field: &'static str) -> Result<(), LocalNetworkError> {
+ if value.is_empty()
+ || value.trim() != value
+ || value.len() > CONTEXT_TEXT_MAX_BYTES
+ || value.chars().any(char::is_control)
+ {
+ return Err(LocalNetworkError::InvalidText { field });
+ }
+ Ok(())
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "PascalCase")]
+pub enum LocalityEvidence {
+ Match,
+ Missing,
+ Nonmatch,
+}
+
+/// The only admitted context-rank values.
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
+#[serde(rename_all = "PascalCase")]
+pub enum ContextRank {
+ MissingLocalityFallback = 1,
+ LocalityMatch = 2,
+}
+
+impl ContextRank {
+ pub const fn value(self) -> u8 {
+ self as u8
+ }
+
+ pub const fn from_value(value: u8) -> Option<Self> {
+ match value {
+ 1 => Some(Self::MissingLocalityFallback),
+ 2 => Some(Self::LocalityMatch),
+ _ => None,
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ContextAdmission {
+ pub rank: ContextRank,
+ pub reason: &'static str,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum LocalNetworkAdmission {
+ Included(ContextAdmission),
+ Excluded { reason: &'static str },
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn network() -> LocalNetwork {
+ LocalNetwork::new(
+ "local-network".into(),
+ "Near me".into(),
+ vec!["wss://relay.example".into()],
+ Some("u10h".into()),
+ vec!["a".repeat(64)],
+ 7,
+ )
+ .expect("network")
+ }
+
+ #[test]
+ fn locality_policy_has_exact_rank_and_exclusion_outcomes() {
+ assert_eq!(
+ network().admit(LocalityEvidence::Match),
+ LocalNetworkAdmission::Included(ContextAdmission {
+ rank: ContextRank::LocalityMatch,
+ reason: "locality_match",
+ })
+ );
+ assert_eq!(
+ network().admit(LocalityEvidence::Missing),
+ LocalNetworkAdmission::Included(ContextAdmission {
+ rank: ContextRank::MissingLocalityFallback,
+ reason: "locality_missing_fallback",
+ })
+ );
+ assert!(matches!(
+ network().admit(LocalityEvidence::Nonmatch),
+ LocalNetworkAdmission::Excluded { .. }
+ ));
+ }
+
+ #[test]
+ fn local_network_fields_are_bounded_and_unique() {
+ assert_eq!(network().generation, 7);
+ for invalid in [
+ LocalNetwork::new(
+ "".into(),
+ "label".into(),
+ vec!["wss://r".into()],
+ None,
+ vec![],
+ 0,
+ ),
+ LocalNetwork::new("id".into(), "label".into(), vec![], None, vec![], 0),
+ LocalNetwork::new(
+ "id".into(),
+ "label".into(),
+ vec!["".into()],
+ None,
+ vec![],
+ 0,
+ ),
+ LocalNetwork::new(
+ "id".into(),
+ "label".into(),
+ vec![format!("wss://{}", "r".repeat(RELAY_URL_MAX_BYTES))],
+ None,
+ vec![],
+ 0,
+ ),
+ LocalNetwork::new(
+ "id".into(),
+ "label".into(),
+ vec!["wss://relay example".into()],
+ None,
+ vec![],
+ 0,
+ ),
+ LocalNetwork::new(
+ "id".into(),
+ "label".into(),
+ vec!["wss://relay\u{7f}".into()],
+ None,
+ vec![],
+ 0,
+ ),
+ LocalNetwork::new(
+ "id".into(),
+ "label".into(),
+ vec!["https://r".into()],
+ None,
+ vec![],
+ 0,
+ ),
+ LocalNetwork::new(
+ "id".into(),
+ "label".into(),
+ vec!["wss://".into()],
+ None,
+ vec![],
+ 0,
+ ),
+ LocalNetwork::new(
+ "id".into(),
+ "label".into(),
+ vec!["wss://user@relay.example".into()],
+ None,
+ vec![],
+ 0,
+ ),
+ LocalNetwork::new(
+ "id".into(),
+ "label".into(),
+ vec!["wss://relay.example#fragment".into()],
+ None,
+ vec![],
+ 0,
+ ),
+ LocalNetwork::new(
+ "id".into(),
+ "label".into(),
+ vec!["wss://r".into(), "wss://r".into()],
+ None,
+ vec![],
+ 0,
+ ),
+ LocalNetwork::new(
+ "id".into(),
+ "label".into(),
+ vec!["wss://r".into()],
+ None,
+ vec!["A".repeat(64)],
+ 0,
+ ),
+ LocalNetwork::new(
+ "id".into(),
+ "label".into(),
+ vec!["wss://r".into()],
+ None,
+ vec!["a".repeat(63)],
+ 0,
+ ),
+ LocalNetwork::new(
+ "id".into(),
+ "label".into(),
+ vec!["wss://r".into()],
+ None,
+ vec!["a".repeat(64), "a".repeat(64)],
+ 0,
+ ),
+ LocalNetwork::new(
+ " id ".into(),
+ "label".into(),
+ vec!["wss://r".into()],
+ None,
+ vec![],
+ 0,
+ ),
+ LocalNetwork::new(
+ "i".repeat(CONTEXT_TEXT_MAX_BYTES + 1),
+ "label".into(),
+ vec!["wss://r".into()],
+ None,
+ vec![],
+ 0,
+ ),
+ LocalNetwork::new(
+ "id".into(),
+ "la\u{7f}bel".into(),
+ vec!["wss://r".into()],
+ None,
+ vec![],
+ 0,
+ ),
+ ] {
+ assert!(invalid.is_err());
+ }
+ }
+}
diff --git a/core/crates/tera_core/src/runtime/product_surface/cursor.rs b/core/crates/tera_core/src/runtime/product_surface/cursor.rs
@@ -0,0 +1,438 @@
+use sha2::{Digest, Sha256};
+use thiserror::Error;
+
+use super::{CardId, ContextRank, TODAY_RANK_SCHEMA_VERSION, TodayRank};
+use crate::runtime::product_surface::ranking::TODAY_RANK_ALGORITHM_VERSION;
+
+const CURSOR_PREFIX: &str = "rrtc1:";
+const CURSOR_DOMAIN: &[u8] = b"radroots.today-cursor.v1\0";
+const CURSOR_SCHEMA_VERSION: u16 = 1;
+const MAX_CONTEXT_ID_BYTES: usize = 256;
+const FIXED_PAYLOAD_BYTES: usize = 2 + 2 + 2 + 2 + 8 + 8 + 32 + 8 + 1 + 1 + 8 + 32;
+const DIGEST_BYTES: usize = 32;
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct CursorScope {
+ pub context_id: String,
+ pub context_generation: u64,
+ pub as_of: u64,
+ pub store_generation: [u8; 32],
+ pub projection_generation: u64,
+}
+
+impl CursorScope {
+ pub fn new(
+ context_id: String,
+ context_generation: u64,
+ as_of: u64,
+ store_generation: [u8; 32],
+ projection_generation: u64,
+ ) -> Result<Self, CursorError> {
+ validate_context_id(&context_id)?;
+ Ok(Self {
+ context_id,
+ context_generation,
+ as_of,
+ store_generation,
+ projection_generation,
+ })
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct TodayCursorPosition {
+ pub rank: TodayRank,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct TodayCursor(String);
+
+#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
+pub enum CursorError {
+ #[error("today cursor context id is invalid")]
+ InvalidContext,
+ #[error("today cursor encoding is malformed")]
+ Malformed,
+ #[error("today cursor integrity check failed")]
+ Integrity,
+ #[error("today cursor version is unsupported")]
+ Version,
+ #[error("today cursor belongs to another context")]
+ ContextMismatch,
+ #[error("today cursor belongs to another frozen snapshot")]
+ SnapshotMismatch,
+ #[error("today cursor belongs to a retired store or projection generation")]
+ Stale,
+ #[error("today cursor position is invalid")]
+ InvalidPosition,
+}
+
+impl TodayCursor {
+ pub fn encode(scope: &CursorScope, position: TodayCursorPosition) -> Result<Self, CursorError> {
+ if position.rank.schema_version != TODAY_RANK_SCHEMA_VERSION
+ || position.rank.algorithm_version != TODAY_RANK_ALGORITHM_VERSION
+ {
+ return Err(CursorError::Version);
+ }
+ if position.rank.time_relevance_rank > 4 {
+ return Err(CursorError::InvalidPosition);
+ }
+ let context_bytes = scope.context_id.as_bytes();
+ let mut payload = Vec::with_capacity(FIXED_PAYLOAD_BYTES + context_bytes.len());
+ payload.extend_from_slice(&CURSOR_SCHEMA_VERSION.to_be_bytes());
+ payload.extend_from_slice(&TODAY_RANK_SCHEMA_VERSION.to_be_bytes());
+ payload.extend_from_slice(&TODAY_RANK_ALGORITHM_VERSION.to_be_bytes());
+ payload.extend_from_slice(
+ &u16::try_from(context_bytes.len())
+ .expect("validated context length fits u16")
+ .to_be_bytes(),
+ );
+ payload.extend_from_slice(context_bytes);
+ payload.extend_from_slice(&scope.context_generation.to_be_bytes());
+ payload.extend_from_slice(&scope.as_of.to_be_bytes());
+ payload.extend_from_slice(&scope.store_generation);
+ payload.extend_from_slice(&scope.projection_generation.to_be_bytes());
+ payload.push(position.rank.context_rank.value());
+ payload.push(position.rank.time_relevance_rank);
+ payload.extend_from_slice(&position.rank.effective_at.to_be_bytes());
+ payload.extend_from_slice(position.rank.card_id.as_bytes());
+ let digest = cursor_digest(&payload);
+ payload.extend_from_slice(&digest);
+ Ok(Self(format!("{CURSOR_PREFIX}{}", hex::encode(payload))))
+ }
+
+ pub fn decode(value: &str, expected: &CursorScope) -> Result<TodayCursorPosition, CursorError> {
+ let encoded = value
+ .strip_prefix(CURSOR_PREFIX)
+ .ok_or(CursorError::Malformed)?;
+ if encoded.len() % 2 != 0
+ || !encoded
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ return Err(CursorError::Malformed);
+ }
+ let bytes = hex::decode(encoded).map_err(|_| CursorError::Malformed)?;
+ if bytes.len() < FIXED_PAYLOAD_BYTES + DIGEST_BYTES {
+ return Err(CursorError::Malformed);
+ }
+ let (payload, observed_digest) = bytes.split_at(bytes.len() - DIGEST_BYTES);
+ if cursor_digest(payload).as_slice() != observed_digest {
+ return Err(CursorError::Integrity);
+ }
+ decode_payload(payload, expected)
+ }
+
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+fn decode_payload(
+ payload: &[u8],
+ expected: &CursorScope,
+) -> Result<TodayCursorPosition, CursorError> {
+ let mut decoder = Decoder::new(payload);
+ let cursor_version = decoder.u16()?;
+ let rank_schema_version = decoder.u16()?;
+ let rank_algorithm_version = decoder.u16()?;
+ if cursor_version != CURSOR_SCHEMA_VERSION
+ || rank_schema_version != TODAY_RANK_SCHEMA_VERSION
+ || rank_algorithm_version != TODAY_RANK_ALGORITHM_VERSION
+ {
+ return Err(CursorError::Version);
+ }
+ let context_len = usize::from(decoder.u16()?);
+ let context_id =
+ core::str::from_utf8(decoder.bytes(context_len)?).map_err(|_| CursorError::Malformed)?;
+ validate_context_id(context_id)?;
+ let context_generation = decoder.u64()?;
+ let as_of = decoder.u64()?;
+ let store_generation = decoder.array_32()?;
+ let projection_generation = decoder.u64()?;
+ let context_rank = ContextRank::from_value(decoder.u8()?).ok_or(CursorError::Malformed)?;
+ let time_relevance_rank = decoder.u8()?;
+ if time_relevance_rank > 4 {
+ return Err(CursorError::Malformed);
+ }
+ let effective_at = decoder.u64()?;
+ let card_id =
+ CardId::parse(&hex::encode(decoder.array_32()?)).map_err(|_| CursorError::Malformed)?;
+ if !decoder.is_finished() {
+ return Err(CursorError::Malformed);
+ }
+ if context_id != expected.context_id || context_generation != expected.context_generation {
+ return Err(CursorError::ContextMismatch);
+ }
+ if as_of != expected.as_of {
+ return Err(CursorError::SnapshotMismatch);
+ }
+ if store_generation != expected.store_generation
+ || projection_generation != expected.projection_generation
+ {
+ return Err(CursorError::Stale);
+ }
+ Ok(TodayCursorPosition {
+ rank: TodayRank {
+ schema_version: rank_schema_version,
+ algorithm_version: rank_algorithm_version,
+ context_rank,
+ time_relevance_rank,
+ effective_at,
+ card_id,
+ },
+ })
+}
+
+fn validate_context_id(value: &str) -> Result<(), CursorError> {
+ if value.is_empty()
+ || value.len() > MAX_CONTEXT_ID_BYTES
+ || value.trim() != value
+ || value.chars().any(char::is_control)
+ {
+ return Err(CursorError::InvalidContext);
+ }
+ Ok(())
+}
+
+fn cursor_digest(payload: &[u8]) -> [u8; 32] {
+ let mut digest = Sha256::new();
+ digest.update(CURSOR_DOMAIN);
+ digest.update(payload);
+ digest.finalize().into()
+}
+
+struct Decoder<'a> {
+ remaining: &'a [u8],
+}
+
+impl<'a> Decoder<'a> {
+ const fn new(value: &'a [u8]) -> Self {
+ Self { remaining: value }
+ }
+
+ fn bytes(&mut self, length: usize) -> Result<&'a [u8], CursorError> {
+ if self.remaining.len() < length {
+ return Err(CursorError::Malformed);
+ }
+ let (value, remaining) = self.remaining.split_at(length);
+ self.remaining = remaining;
+ Ok(value)
+ }
+
+ fn u8(&mut self) -> Result<u8, CursorError> {
+ Ok(self.bytes(1)?[0])
+ }
+
+ fn u16(&mut self) -> Result<u16, CursorError> {
+ Ok(u16::from_be_bytes(
+ self.bytes(2)?.try_into().expect("exact length"),
+ ))
+ }
+
+ fn u64(&mut self) -> Result<u64, CursorError> {
+ Ok(u64::from_be_bytes(
+ self.bytes(8)?.try_into().expect("exact length"),
+ ))
+ }
+
+ fn array_32(&mut self) -> Result<[u8; 32], CursorError> {
+ Ok(self.bytes(32)?.try_into().expect("exact length"))
+ }
+
+ const fn is_finished(&self) -> bool {
+ self.remaining.is_empty()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn scope() -> CursorScope {
+ CursorScope::new("nearby".into(), 4, 2_000_000_000, [7; 32], 9).expect("scope")
+ }
+
+ fn position() -> TodayCursorPosition {
+ TodayCursorPosition {
+ rank: TodayRank {
+ schema_version: TODAY_RANK_SCHEMA_VERSION,
+ algorithm_version: TODAY_RANK_ALGORITHM_VERSION,
+ context_rank: ContextRank::LocalityMatch,
+ time_relevance_rank: 3,
+ effective_at: 1_999_999_000,
+ card_id: CardId::parse(&"a".repeat(64)).expect("card"),
+ },
+ }
+ }
+
+ fn payload(cursor: &TodayCursor) -> Vec<u8> {
+ let bytes =
+ hex::decode(cursor.as_str().strip_prefix(CURSOR_PREFIX).expect("prefix")).expect("hex");
+ bytes[..bytes.len() - DIGEST_BYTES].to_vec()
+ }
+
+ fn signed_payload(mut payload: Vec<u8>) -> String {
+ payload.extend_from_slice(&cursor_digest(&payload));
+ format!("{CURSOR_PREFIX}{}", hex::encode(payload))
+ }
+
+ #[test]
+ fn cursor_vector_round_trips_and_is_fixed() {
+ let cursor = TodayCursor::encode(&scope(), position()).expect("cursor");
+ assert_eq!(
+ cursor.as_str(),
+ "rrtc1:00010001000100066e6561726279000000000000000400000000773594000707070707070707070707070707070707070707070707070707070707070707000000000000000902030000000077359018aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaedf305be41633dfc2f7d621e067c3d33a71c3548c6a1fcf68a6707a1d8664b11"
+ );
+ assert_eq!(
+ TodayCursor::decode(cursor.as_str(), &scope()).expect("decode"),
+ position()
+ );
+ }
+
+ #[test]
+ fn cursor_rejects_tamper_context_snapshot_and_stale_generations() {
+ let cursor = TodayCursor::encode(&scope(), position()).expect("cursor");
+ let mut tampered = cursor.as_str().as_bytes().to_vec();
+ *tampered.last_mut().expect("byte") = b'0';
+ assert_eq!(
+ TodayCursor::decode(core::str::from_utf8(&tampered).expect("utf8"), &scope()),
+ Err(CursorError::Integrity)
+ );
+ let other_context =
+ CursorScope::new("other".into(), 4, 2_000_000_000, [7; 32], 9).expect("scope");
+ assert_eq!(
+ TodayCursor::decode(cursor.as_str(), &other_context),
+ Err(CursorError::ContextMismatch)
+ );
+ let other_context_generation =
+ CursorScope::new("nearby".into(), 5, 2_000_000_000, [7; 32], 9).expect("scope");
+ assert_eq!(
+ TodayCursor::decode(cursor.as_str(), &other_context_generation),
+ Err(CursorError::ContextMismatch)
+ );
+ let other_snapshot =
+ CursorScope::new("nearby".into(), 4, 2_000_000_001, [7; 32], 9).expect("scope");
+ assert_eq!(
+ TodayCursor::decode(cursor.as_str(), &other_snapshot),
+ Err(CursorError::SnapshotMismatch)
+ );
+ let stale = CursorScope::new("nearby".into(), 4, 2_000_000_000, [8; 32], 9).expect("scope");
+ assert_eq!(
+ TodayCursor::decode(cursor.as_str(), &stale),
+ Err(CursorError::Stale)
+ );
+ let stale_projection =
+ CursorScope::new("nearby".into(), 4, 2_000_000_000, [7; 32], 10).expect("scope");
+ assert_eq!(
+ TodayCursor::decode(cursor.as_str(), &stale_projection),
+ Err(CursorError::Stale)
+ );
+ }
+
+ #[test]
+ fn malformed_and_versioned_cursor_inputs_fail_closed() {
+ assert_eq!(
+ TodayCursor::decode("nope", &scope()),
+ Err(CursorError::Malformed)
+ );
+ for malformed in ["rrtc1:0", "rrtc1:GG", "rrtc1:00"] {
+ assert_eq!(
+ TodayCursor::decode(malformed, &scope()),
+ Err(CursorError::Malformed)
+ );
+ }
+ assert_eq!(
+ TodayCursor::decode(
+ &TodayCursor::encode(&scope(), position())
+ .expect("cursor")
+ .as_str()
+ .to_uppercase(),
+ &scope()
+ ),
+ Err(CursorError::Malformed)
+ );
+ assert!(CursorScope::new("".into(), 0, 0, [0; 32], 0).is_err());
+ assert!(CursorScope::new("x".repeat(257), 0, 0, [0; 32], 0).is_err());
+ assert!(CursorScope::new(" nearby ".into(), 0, 0, [0; 32], 0).is_err());
+ assert!(CursorScope::new("near\u{7f}by".into(), 0, 0, [0; 32], 0).is_err());
+ let cursor = TodayCursor::encode(&scope(), position()).expect("cursor");
+ for version_offset in [1, 3, 5] {
+ let mut unsupported = payload(&cursor);
+ unsupported[version_offset] = 2;
+ assert_eq!(
+ TodayCursor::decode(&signed_payload(unsupported), &scope()),
+ Err(CursorError::Version)
+ );
+ }
+ let mut invalid_utf8 = payload(&cursor);
+ invalid_utf8[8] = 0xff;
+ assert_eq!(
+ TodayCursor::decode(&signed_payload(invalid_utf8), &scope()),
+ Err(CursorError::Malformed)
+ );
+ let mut invalid_context = payload(&cursor);
+ invalid_context[8] = b' ';
+ assert_eq!(
+ TodayCursor::decode(&signed_payload(invalid_context), &scope()),
+ Err(CursorError::InvalidContext)
+ );
+ let mut trailing = payload(&cursor);
+ trailing.push(0);
+ assert_eq!(
+ TodayCursor::decode(&signed_payload(trailing), &scope()),
+ Err(CursorError::Malformed)
+ );
+ let mut invalid_context_rank = payload(&cursor);
+ invalid_context_rank[70] = 3;
+ assert_eq!(
+ TodayCursor::decode(&signed_payload(invalid_context_rank), &scope()),
+ Err(CursorError::Malformed)
+ );
+ let mut invalid_time_rank = payload(&cursor);
+ invalid_time_rank[71] = 5;
+ assert_eq!(
+ TodayCursor::decode(&signed_payload(invalid_time_rank), &scope()),
+ Err(CursorError::Malformed)
+ );
+ let mut truncated_field = vec![0; FIXED_PAYLOAD_BYTES];
+ truncated_field[1] = 1;
+ truncated_field[3] = 1;
+ truncated_field[5] = 1;
+ truncated_field[6] = 1;
+ assert_eq!(
+ TodayCursor::decode(&signed_payload(truncated_field), &scope()),
+ Err(CursorError::Malformed)
+ );
+ let invalid_version = TodayCursorPosition {
+ rank: TodayRank {
+ schema_version: 2,
+ ..position().rank
+ },
+ };
+ assert_eq!(
+ TodayCursor::encode(&scope(), invalid_version),
+ Err(CursorError::Version)
+ );
+ let invalid_algorithm = TodayCursorPosition {
+ rank: TodayRank {
+ algorithm_version: 2,
+ ..position().rank
+ },
+ };
+ assert_eq!(
+ TodayCursor::encode(&scope(), invalid_algorithm),
+ Err(CursorError::Version)
+ );
+ let invalid_rank = TodayCursorPosition {
+ rank: TodayRank {
+ time_relevance_rank: 5,
+ ..position().rank
+ },
+ };
+ assert_eq!(
+ TodayCursor::encode(&scope(), invalid_rank),
+ Err(CursorError::InvalidPosition)
+ );
+ }
+}
diff --git a/core/crates/tera_core/src/runtime/product_surface/identity.rs b/core/crates/tera_core/src/runtime/product_surface/identity.rs
@@ -0,0 +1,185 @@
+use core::fmt;
+
+use radroots_event::EventId;
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+use thiserror::Error;
+
+use super::TodayCardType;
+
+pub const CARD_ID_SCHEMA_VERSION: u16 = 1;
+const CARD_ID_DOMAIN: &[u8] = b"radroots.today-card.v1\0";
+
+/// Canonical source identity used to derive a stable card identifier.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum CardSourceIdentity {
+ Event(EventId),
+ Address {
+ kind: u32,
+ author_pubkey: String,
+ identifier: String,
+ },
+}
+
+#[derive(Clone, Debug, Error, Eq, PartialEq)]
+pub enum CardIdError {
+ #[error("card address kind must be parameterized replaceable")]
+ InvalidAddressKind,
+ #[error("card address author must be canonical lowercase hexadecimal")]
+ InvalidAuthor,
+ #[error("card address identifier is invalid")]
+ InvalidIdentifier,
+ #[error("card identifier must be 64 lowercase hexadecimal characters")]
+ InvalidCardId,
+}
+
+impl CardSourceIdentity {
+ pub fn address(
+ kind: u32,
+ author_pubkey: impl Into<String>,
+ identifier: impl Into<String>,
+ ) -> Result<Self, CardIdError> {
+ if !(30_000..40_000).contains(&kind) {
+ return Err(CardIdError::InvalidAddressKind);
+ }
+ let author_pubkey = author_pubkey.into();
+ if author_pubkey.len() != 64
+ || !author_pubkey
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ return Err(CardIdError::InvalidAuthor);
+ }
+ let identifier = identifier.into();
+ if identifier.is_empty()
+ || identifier.len() > 512
+ || identifier.chars().any(char::is_control)
+ {
+ return Err(CardIdError::InvalidIdentifier);
+ }
+ Ok(Self::Address {
+ kind,
+ author_pubkey,
+ identifier,
+ })
+ }
+
+ pub fn canonical_string(&self) -> String {
+ match self {
+ Self::Event(event_id) => format!("event:{}", event_id.to_hex()),
+ Self::Address {
+ kind,
+ author_pubkey,
+ identifier,
+ } => format!("address:{kind}:{author_pubkey}:{identifier}"),
+ }
+ }
+}
+
+/// Lowercase SHA-256 stable identity for one top-level Today card.
+#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct CardId([u8; 32]);
+
+impl CardId {
+ pub fn derive(card_type: TodayCardType, source: &CardSourceIdentity) -> Self {
+ let mut digest = Sha256::new();
+ digest.update(CARD_ID_DOMAIN);
+ digest.update(card_type.label().as_bytes());
+ digest.update(b"\0");
+ digest.update(source.canonical_string().as_bytes());
+ Self(digest.finalize().into())
+ }
+
+ pub fn parse(value: &str) -> Result<Self, CardIdError> {
+ if value.len() != 64
+ || !value
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ return Err(CardIdError::InvalidCardId);
+ }
+ let mut bytes = [0; 32];
+ hex::decode_to_slice(value, &mut bytes).map_err(|_| CardIdError::InvalidCardId)?;
+ Ok(Self(bytes))
+ }
+
+ pub const fn as_bytes(&self) -> &[u8; 32] {
+ &self.0
+ }
+
+ pub fn to_hex(self) -> String {
+ hex::encode(self.0)
+ }
+}
+
+impl fmt::Display for CardId {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(&hex::encode(self.0))
+ }
+}
+
+impl Serialize for CardId {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(&self.to_hex())
+ }
+}
+
+impl<'de> Deserialize<'de> for CardId {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?;
+ Self::parse(&value).map_err(serde::de::Error::custom)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn stable_card_id_vectors_cover_regular_and_addressable_sources() {
+ let event = EventId::parse("a".repeat(64)).expect("event");
+ assert_eq!(
+ CardId::derive(TodayCardType::Update, &CardSourceIdentity::Event(event)).to_hex(),
+ "36bf89dc7a6759143986b1f339870ec792f7bee865c9738b0adb50ac9c5197be"
+ );
+ let address = CardSourceIdentity::address(31_923, "b".repeat(64), "farmers-market-2026")
+ .expect("address");
+ assert_eq!(
+ CardId::derive(TodayCardType::Event, &address).to_hex(),
+ "75f6127161783c583368b6c76ed779ae5e02cd7bc9f71ef55ff39e32a59274fc"
+ );
+ let replacement = CardId::derive(TodayCardType::Event, &address);
+ assert_eq!(replacement, CardId::derive(TodayCardType::Event, &address));
+ }
+
+ #[test]
+ fn card_identity_rejects_noncanonical_addresses_and_ids() {
+ assert!(CardSourceIdentity::address(1, "a".repeat(64), "id").is_err());
+ assert!(CardSourceIdentity::address(40_000, "a".repeat(64), "id").is_err());
+ assert!(CardSourceIdentity::address(30_402, "a".repeat(63), "id").is_err());
+ assert!(CardSourceIdentity::address(30_402, "A".repeat(64), "id").is_err());
+ assert!(CardSourceIdentity::address(30_402, "a".repeat(64), "").is_err());
+ assert!(CardSourceIdentity::address(30_402, "a".repeat(64), "i".repeat(513)).is_err());
+ assert!(CardSourceIdentity::address(30_402, "a".repeat(64), "bad\nid").is_err());
+ let opaque = CardSourceIdentity::address(31_923, "a".repeat(64), " market day ")
+ .expect("opaque d value");
+ assert!(opaque.canonical_string().ends_with(": market day "));
+ assert!(CardId::parse(&"a".repeat(63)).is_err());
+ assert!(CardId::parse(&"A".repeat(64)).is_err());
+
+ let card = CardId::parse(&"c".repeat(64)).expect("card");
+ assert_eq!(card.to_string(), "c".repeat(64));
+ let encoded = serde_json::to_string(&card).expect("serialize");
+ assert_eq!(
+ serde_json::from_str::<CardId>(&encoded).expect("deserialize"),
+ card
+ );
+ assert!(serde_json::from_str::<CardId>(&format!("\"{}\"", "G".repeat(64))).is_err());
+ }
+}
diff --git a/core/crates/tera_core/src/runtime/product_surface/model.rs b/core/crates/tera_core/src/runtime/product_surface/model.rs
@@ -0,0 +1,197 @@
+use serde::{Deserialize, Serialize};
+
+use super::{CardId, ContextRank, TodayRank};
+
+/// The closed Phase 1 top-level Today taxonomy.
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
+#[serde(rename_all = "PascalCase")]
+pub enum TodayCardType {
+ Update,
+ PhotoUpdate,
+ Ask,
+ Event,
+ FoodAvailability,
+}
+
+impl TodayCardType {
+ pub const fn label(self) -> &'static str {
+ match self {
+ Self::Update => "Update",
+ Self::PhotoUpdate => "PhotoUpdate",
+ Self::Ask => "Ask",
+ Self::Event => "Event",
+ Self::FoodAvailability => "FoodAvailability",
+ }
+ }
+}
+
+/// The closed Phase 1 Add command taxonomy.
+#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
+#[serde(rename_all = "PascalCase")]
+pub enum AddCommandType {
+ CreateUpdate,
+ CreatePhotoUpdate,
+ CreateAsk,
+ CreateEvent,
+ CreateFoodAvailability,
+}
+
+/// One exact top-level card to Add-command mapping.
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CardAddParity {
+ pub card_type: TodayCardType,
+ pub add_command_type: AddCommandType,
+}
+
+pub const CANONICAL_TODAY_CARD_TYPES: [TodayCardType; 5] = [
+ TodayCardType::Update,
+ TodayCardType::PhotoUpdate,
+ TodayCardType::Ask,
+ TodayCardType::Event,
+ TodayCardType::FoodAvailability,
+];
+
+pub const CANONICAL_ADD_COMMAND_TYPES: [AddCommandType; 5] = [
+ AddCommandType::CreateUpdate,
+ AddCommandType::CreatePhotoUpdate,
+ AddCommandType::CreateAsk,
+ AddCommandType::CreateEvent,
+ AddCommandType::CreateFoodAvailability,
+];
+
+pub const CANONICAL_CARD_ADD_PARITY: [CardAddParity; 5] = [
+ CardAddParity {
+ card_type: TodayCardType::Update,
+ add_command_type: AddCommandType::CreateUpdate,
+ },
+ CardAddParity {
+ card_type: TodayCardType::PhotoUpdate,
+ add_command_type: AddCommandType::CreatePhotoUpdate,
+ },
+ CardAddParity {
+ card_type: TodayCardType::Ask,
+ add_command_type: AddCommandType::CreateAsk,
+ },
+ CardAddParity {
+ card_type: TodayCardType::Event,
+ add_command_type: AddCommandType::CreateEvent,
+ },
+ CardAddParity {
+ card_type: TodayCardType::FoodAvailability,
+ add_command_type: AddCommandType::CreateFoodAvailability,
+ },
+];
+
+/// Supporting standard profiles that enrich the product without creating cards.
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "PascalCase")]
+pub enum SupportingProfile {
+ Profile,
+ Reply,
+ Comment,
+ Deletion,
+}
+
+/// Local media verification never changes the canonical card type.
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "PascalCase")]
+pub enum MediaVerificationState {
+ Pending,
+ Verified,
+ Failed,
+ Unavailable,
+}
+
+/// Structural media metadata plus its separate local verification state.
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct MediaReference {
+ pub url: String,
+ pub sha256: Option<String>,
+ pub media_type: Option<String>,
+ pub width: Option<u32>,
+ pub height: Option<u32>,
+ pub byte_size: Option<u64>,
+ pub alt: Option<String>,
+ pub verification: MediaVerificationState,
+}
+
+/// Tolerant profile attribution attached to cards and Me results.
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ProfileSummary {
+ pub author_pubkey: String,
+ pub name: Option<String>,
+ pub display_name: Option<String>,
+ pub picture: Option<MediaReference>,
+}
+
+/// Thread enrichment identity; replies and comments never become top-level cards.
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ThreadReference {
+ pub profile: SupportingProfile,
+ pub root: String,
+ pub parent_event_id: String,
+}
+
+/// Current rendering state derived from standard event semantics.
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "PascalCase")]
+pub enum CardLifecycleState {
+ Active,
+ Sold,
+ Past,
+}
+
+/// Verified, visible, context-admitted source facts for one top-level card.
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ClassifiedCard {
+ pub schema_version: u16,
+ pub card_id: CardId,
+ pub card_type: TodayCardType,
+ pub source_event_id: String,
+ pub source_address: Option<String>,
+ pub author_pubkey: String,
+ pub contract_id: String,
+ pub content: String,
+ pub authored_at: u64,
+ pub context_rank: ContextRank,
+ pub inclusion_reason: String,
+ pub media: Vec<MediaReference>,
+ pub lifecycle: CardLifecycleState,
+ pub rank: Option<TodayRank>,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn card_and_add_taxonomies_are_exact_and_serialized_stably() {
+ assert_eq!(CANONICAL_TODAY_CARD_TYPES.len(), 5);
+ assert_eq!(CANONICAL_ADD_COMMAND_TYPES.len(), 5);
+ for (index, parity) in CANONICAL_CARD_ADD_PARITY.iter().enumerate() {
+ assert_eq!(parity.card_type, CANONICAL_TODAY_CARD_TYPES[index]);
+ assert_eq!(parity.add_command_type, CANONICAL_ADD_COMMAND_TYPES[index]);
+ }
+ assert_eq!(
+ serde_json::to_string(&CANONICAL_TODAY_CARD_TYPES).expect("cards"),
+ r#"["Update","PhotoUpdate","Ask","Event","FoodAvailability"]"#
+ );
+ }
+
+ #[test]
+ fn media_state_is_independent_from_card_type() {
+ for state in [
+ MediaVerificationState::Pending,
+ MediaVerificationState::Verified,
+ MediaVerificationState::Failed,
+ MediaVerificationState::Unavailable,
+ ] {
+ assert!(!serde_json::to_string(&state).expect("state").is_empty());
+ }
+ }
+}
diff --git a/core/crates/tera_core/src/runtime/product_surface/projection.rs b/core/crates/tera_core/src/runtime/product_surface/projection.rs
@@ -0,0 +1,469 @@
+use radroots_event::food::availability::FoodAvailabilityStatus;
+use radroots_event_codec::{
+ admission::RadrootsAdmittedEvent, decode::post::RadrootsPostClassification,
+};
+use serde::{Deserialize, Serialize};
+
+use super::{
+ CardId, CardLifecycleState, CardSourceIdentity, ClassifiedCard, ContextAdmission,
+ LocalNetworkAdmission, MediaReference, MediaVerificationState, SupportingProfile,
+ TodayCardType,
+};
+
+const CLASSIFIED_CARD_SCHEMA_VERSION: u16 = 1;
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub enum ProductEventClassification {
+ Card(Box<ClassifiedCard>),
+ Supporting(SupportingProfile),
+ Excluded(ProductEventExclusion),
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "PascalCase")]
+pub enum ProductEventExclusion {
+ LocalityNonmatch,
+ UnsupportedProfile,
+ InvalidSourceIdentity,
+}
+
+/// Classifies an already signature/id-verified and standard-profile-admitted event.
+///
+/// The caller must supply the result of the selected LocalNetwork admission.
+/// Replacement and deletion are applied by storage before the event reaches
+/// this boundary. No content prose or remote media retrieval influences type.
+pub fn classify_admitted_event(
+ admitted: &RadrootsAdmittedEvent,
+ context: LocalNetworkAdmission,
+) -> ProductEventClassification {
+ let context = match context {
+ LocalNetworkAdmission::Included(context) => context,
+ LocalNetworkAdmission::Excluded { .. } => {
+ return ProductEventClassification::Excluded(ProductEventExclusion::LocalityNonmatch);
+ }
+ };
+
+ match admitted {
+ RadrootsAdmittedEvent::Profile(_) => supporting(SupportingProfile::Profile),
+ RadrootsAdmittedEvent::Reply(_) => supporting(SupportingProfile::Reply),
+ RadrootsAdmittedEvent::Comment(_) => supporting(SupportingProfile::Comment),
+ RadrootsAdmittedEvent::DeletionRequest(_) => supporting(SupportingProfile::Deletion),
+ RadrootsAdmittedEvent::RootPost(event) => {
+ let card_type = match event.projection().classification() {
+ RadrootsPostClassification::Update => TodayCardType::Update,
+ RadrootsPostClassification::PhotoUpdate => TodayCardType::PhotoUpdate,
+ RadrootsPostClassification::Ask => TodayCardType::Ask,
+ RadrootsPostClassification::ThreadExcluded => {
+ return ProductEventClassification::Excluded(
+ ProductEventExclusion::UnsupportedProfile,
+ );
+ }
+ _ => {
+ return ProductEventClassification::Excluded(
+ ProductEventExclusion::UnsupportedProfile,
+ );
+ }
+ };
+ card(
+ admitted,
+ card_type,
+ context,
+ post_media(event.projection()),
+ CardLifecycleState::Active,
+ )
+ }
+ RadrootsAdmittedEvent::FoodAvailability(event) => {
+ let lifecycle = match event.projection().status() {
+ FoodAvailabilityStatus::Active => CardLifecycleState::Active,
+ FoodAvailabilityStatus::Sold => CardLifecycleState::Sold,
+ };
+ card(
+ admitted,
+ TodayCardType::FoodAvailability,
+ context,
+ food_media(event.projection()),
+ lifecycle,
+ )
+ }
+ RadrootsAdmittedEvent::ContractValidated(event) => match event.contract_id() {
+ "radroots.calendar.date_event.v1" | "radroots.calendar.time_event.v1" => card(
+ admitted,
+ TodayCardType::Event,
+ context,
+ calendar_media(event.event().tags_as_vec()),
+ CardLifecycleState::Active,
+ ),
+ _ => ProductEventClassification::Excluded(ProductEventExclusion::UnsupportedProfile),
+ },
+ _ => ProductEventClassification::Excluded(ProductEventExclusion::UnsupportedProfile),
+ }
+}
+
+const fn supporting(profile: SupportingProfile) -> ProductEventClassification {
+ ProductEventClassification::Supporting(profile)
+}
+
+fn card(
+ admitted: &RadrootsAdmittedEvent,
+ card_type: TodayCardType,
+ context: ContextAdmission,
+ media: Vec<MediaReference>,
+ lifecycle: CardLifecycleState,
+) -> ProductEventClassification {
+ let event = admitted.event();
+ let source = match card_type {
+ TodayCardType::Update | TodayCardType::PhotoUpdate | TodayCardType::Ask => {
+ CardSourceIdentity::Event(*event.id())
+ }
+ TodayCardType::Event | TodayCardType::FoodAvailability => {
+ let identifier = event
+ .tags_as_vec()
+ .into_iter()
+ .find(|tag| tag.first().map(String::as_str) == Some("d"))
+ .and_then(|tag| tag.get(1).cloned());
+ let Some(identifier) = identifier else {
+ return ProductEventClassification::Excluded(
+ ProductEventExclusion::InvalidSourceIdentity,
+ );
+ };
+ let Ok(source) =
+ CardSourceIdentity::address(event.kind_u32(), event.author().to_hex(), identifier)
+ else {
+ return ProductEventClassification::Excluded(
+ ProductEventExclusion::InvalidSourceIdentity,
+ );
+ };
+ source
+ }
+ };
+ let source_address = match &source {
+ CardSourceIdentity::Event(_) => None,
+ CardSourceIdentity::Address {
+ kind,
+ author_pubkey,
+ identifier,
+ } => Some(format!("{kind}:{author_pubkey}:{identifier}")),
+ };
+ ProductEventClassification::Card(Box::new(ClassifiedCard {
+ schema_version: CLASSIFIED_CARD_SCHEMA_VERSION,
+ card_id: CardId::derive(card_type, &source),
+ card_type,
+ source_event_id: event.id_hex(),
+ source_address,
+ author_pubkey: event.author().to_hex(),
+ contract_id: admitted.contract_id().to_owned(),
+ content: event.content().to_owned(),
+ authored_at: event.created_at_u64(),
+ context_rank: context.rank,
+ inclusion_reason: context.reason.to_owned(),
+ media,
+ lifecycle,
+ rank: None,
+ }))
+}
+
+fn post_media(
+ projection: &radroots_event_codec::decode::post::RadrootsInboundPostProjection,
+) -> Vec<MediaReference> {
+ projection
+ .imeta()
+ .iter()
+ .filter_map(|media| {
+ let dimensions = media.dimensions();
+ Some(MediaReference {
+ url: media.url()?.to_owned(),
+ sha256: media.sha256().map(str::to_owned),
+ media_type: media.media_type().map(str::to_owned),
+ width: dimensions.map(|value| value.width()),
+ height: dimensions.map(|value| value.height()),
+ byte_size: media.size(),
+ alt: media.alt().map(str::to_owned),
+ verification: MediaVerificationState::Unavailable,
+ })
+ })
+ .collect()
+}
+
+fn food_media(
+ projection: &radroots_event_codec::decode::food_availability::RadrootsInboundFoodAvailabilityProjection,
+) -> Vec<MediaReference> {
+ projection
+ .images()
+ .iter()
+ .filter_map(|media| {
+ let dimensions = media.dimensions();
+ Some(MediaReference {
+ url: media.url()?.to_owned(),
+ sha256: blossom_digest(media.url()?),
+ media_type: None,
+ width: dimensions.map(|value| value.width()),
+ height: dimensions.map(|value| value.height()),
+ byte_size: None,
+ alt: None,
+ verification: MediaVerificationState::Unavailable,
+ })
+ })
+ .collect()
+}
+
+fn calendar_media(tags: Vec<Vec<String>>) -> Vec<MediaReference> {
+ tags.into_iter()
+ .find(|tag| tag.first().map(String::as_str) == Some("image"))
+ .and_then(|tag| tag.get(1).cloned())
+ .map(|url| MediaReference {
+ sha256: blossom_digest(&url),
+ url,
+ media_type: None,
+ width: None,
+ height: None,
+ byte_size: None,
+ alt: None,
+ verification: MediaVerificationState::Unavailable,
+ })
+ .into_iter()
+ .collect()
+}
+
+fn blossom_digest(url: &str) -> Option<String> {
+ let path = url.split_once("://")?.1.split_once('/')?.1;
+ let candidate = path.split(['.', '/', '?', '#']).next()?;
+ (candidate.len() == 64
+ && candidate
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)))
+ .then(|| candidate.to_owned())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use nostr::secp256k1::Message;
+ use nostr::{Keys, SECP256K1};
+ use radroots_event::{
+ Event, envelope::EventEnvelopeParts, wire::compute_canonical_nip01_event_id,
+ };
+ use radroots_event_codec::{admission::admit_verified_event, verify::verify_nip01_event};
+
+ const SECRET: &str = "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
+
+ fn admitted(kind: u32, tags: Vec<Vec<&str>>, content: &str) -> RadrootsAdmittedEvent {
+ admitted_owned(
+ kind,
+ tags.into_iter()
+ .map(|tag| tag.into_iter().map(str::to_owned).collect())
+ .collect(),
+ content,
+ )
+ }
+
+ fn admitted_owned(kind: u32, tags: Vec<Vec<String>>, content: &str) -> RadrootsAdmittedEvent {
+ let keys = Keys::parse(SECRET).expect("key");
+ let author = keys.public_key().to_string();
+ let created_at = 2_000_000_000;
+ let id = compute_canonical_nip01_event_id(&author, created_at, kind, &tags, content)
+ .expect("id");
+ let message = Message::from_digest(*id.as_bytes());
+ let signature = SECP256K1.sign_schnorr_no_aux_rand(
+ &message,
+ &nostr::secp256k1::Keypair::from_secret_key(SECP256K1, keys.secret_key()),
+ );
+ let event = Event::new(EventEnvelopeParts {
+ id: id.to_hex(),
+ author,
+ created_at,
+ kind,
+ tags,
+ content: content.into(),
+ sig: signature.to_string(),
+ })
+ .expect("event");
+ let verified = verify_nip01_event(event).expect("verified");
+ admit_verified_event(verified).expect("admitted")
+ }
+
+ const fn context() -> LocalNetworkAdmission {
+ LocalNetworkAdmission::Included(ContextAdmission {
+ rank: super::super::ContextRank::MissingLocalityFallback,
+ reason: "locality_missing_fallback",
+ })
+ }
+
+ fn card_type(event: RadrootsAdmittedEvent) -> TodayCardType {
+ match classify_admitted_event(&event, context()) {
+ ProductEventClassification::Card(card) => card.card_type,
+ other => panic!("expected card, got {other:?}"),
+ }
+ }
+
+ #[test]
+ fn exact_five_card_classifier_precedence_is_protocol_structural() {
+ assert_eq!(
+ card_type(admitted(1, vec![], "ordinary note")),
+ TodayCardType::Update
+ );
+ let digest_tag = format!("x {}", "a".repeat(64));
+ assert_eq!(
+ card_type(admitted(
+ 1,
+ vec![vec![
+ "imeta",
+ "url https://media.example/a.jpg",
+ &digest_tag,
+ "m image/jpeg",
+ "dim 10x20",
+ "size 123",
+ "alt field photo"
+ ]],
+ "photo https://media.example/a.jpg",
+ )),
+ TodayCardType::PhotoUpdate
+ );
+ assert_eq!(
+ card_type(admitted(
+ 1,
+ vec![vec!["t", " RADROOTS-ASK "], vec!["imeta", "broken"]],
+ "Anyone have carrots",
+ )),
+ TodayCardType::Ask
+ );
+ assert_eq!(
+ card_type(admitted(
+ 31_923,
+ vec![
+ vec!["d", "market-2026"],
+ vec!["title", "Saturday market"],
+ vec!["start", "2000000100"],
+ vec!["end", "2000000200"],
+ vec!["D", "23148"],
+ ],
+ "Farm market",
+ )),
+ TodayCardType::Event
+ );
+ assert_eq!(
+ card_type(admitted(
+ 30_402,
+ vec![
+ vec!["d", "carrots"],
+ vec!["title", "Carrots"],
+ vec!["summary", "Fresh bunches"],
+ vec!["published_at", "1999999999"],
+ vec!["location", "Saanich"],
+ vec!["price", "3", "CAD"],
+ vec!["radroots:price_unit", "lb"],
+ vec!["status", "active"],
+ ],
+ "Carrots available",
+ )),
+ TodayCardType::FoodAvailability
+ );
+ }
+
+ #[test]
+ fn ordinary_standard_kind_one_needs_no_product_marker() {
+ let event = admitted(1, vec![vec!["t", "gardening"]], "Seedlings are ready");
+ let ProductEventClassification::Card(card) = classify_admitted_event(&event, context())
+ else {
+ panic!("standard kind-1 must remain admitted");
+ };
+ assert_eq!(card.card_type, TodayCardType::Update);
+ assert_eq!(
+ card.context_rank,
+ super::super::ContextRank::MissingLocalityFallback
+ );
+ }
+
+ #[test]
+ fn supporting_profiles_never_become_cards_and_nonmatches_are_excluded() {
+ let profile = admitted(0, vec![], r#"{"name":"Farm"}"#);
+ let reply = admitted(1, vec![vec!["e", &"a".repeat(64), "", "root"]], "Reply");
+ let author = Keys::parse(SECRET).expect("key").public_key().to_string();
+ let root_id = "a".repeat(64);
+ let comment = admitted_owned(
+ 1_111,
+ vec![
+ vec!["E".into(), root_id.clone(), String::new(), author.clone()],
+ vec!["K".into(), "30402".into()],
+ vec!["P".into(), author.clone()],
+ vec!["e".into(), root_id.clone(), String::new(), author.clone()],
+ vec!["k".into(), "30402".into()],
+ vec!["p".into(), author],
+ ],
+ "Comment",
+ );
+ let deletion = admitted(5, vec![vec!["e", &root_id]], "Superseded");
+ for (event, expected) in [
+ (profile, SupportingProfile::Profile),
+ (reply, SupportingProfile::Reply),
+ (comment, SupportingProfile::Comment),
+ (deletion, SupportingProfile::Deletion),
+ ] {
+ assert_eq!(
+ classify_admitted_event(&event, context()),
+ ProductEventClassification::Supporting(expected)
+ );
+ }
+
+ let nip98 = admitted(
+ 27_235,
+ vec![
+ vec!["u", "https://media.example/upload"],
+ vec!["method", "GET"],
+ ],
+ "{}",
+ );
+ assert_eq!(
+ classify_admitted_event(&nip98, context()),
+ ProductEventClassification::Excluded(ProductEventExclusion::UnsupportedProfile)
+ );
+
+ let update = admitted(1, vec![], "ordinary note");
+ assert_eq!(
+ classify_admitted_event(
+ &update,
+ LocalNetworkAdmission::Excluded {
+ reason: "locality_nonmatch"
+ }
+ ),
+ ProductEventClassification::Excluded(ProductEventExclusion::LocalityNonmatch)
+ );
+ }
+
+ #[test]
+ fn addressable_replacements_keep_stable_card_identity() {
+ let first = admitted(
+ 30_402,
+ vec![
+ vec!["d", "carrots"],
+ vec!["title", "Carrots"],
+ vec!["summary", "Fresh"],
+ vec!["published_at", "1999999999"],
+ vec!["location", "Saanich"],
+ vec!["price", "3", "CAD"],
+ vec!["radroots:price_unit", "lb"],
+ vec!["status", "active"],
+ ],
+ "First",
+ );
+ let second = admitted(
+ 30_402,
+ vec![
+ vec!["d", "carrots"],
+ vec!["title", "Carrots"],
+ vec!["summary", "Fresh"],
+ vec!["published_at", "1999999999"],
+ vec!["location", "Saanich"],
+ vec!["price", "4", "CAD"],
+ vec!["radroots:price_unit", "lb"],
+ vec!["status", "active"],
+ ],
+ "Second",
+ );
+ let ids = [first, second].map(|event| match classify_admitted_event(&event, context()) {
+ ProductEventClassification::Card(card) => card.card_id,
+ other => panic!("expected card, got {other:?}"),
+ });
+ assert_eq!(ids[0], ids[1]);
+ }
+}
diff --git a/core/crates/tera_core/src/runtime/product_surface/ranking.rs b/core/crates/tera_core/src/runtime/product_surface/ranking.rs
@@ -0,0 +1,259 @@
+use core::cmp::Ordering;
+
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+use thiserror::Error;
+
+use super::{CardId, ContextRank, TodayCardType};
+
+pub const TODAY_RANK_SCHEMA_VERSION: u16 = 1;
+pub const TODAY_RANK_ALGORITHM_VERSION: u16 = 1;
+const RANK_DIGEST_DOMAIN: &[u8] = b"radroots.today-rank.v1\0";
+const UPCOMING_EVENT_WINDOW_SECONDS: u64 = 7 * 24 * 60 * 60;
+
+/// Exact time inputs used by the deliberately small Phase 1 ranking function.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum TimeRelevance {
+ Published,
+ Event { start: u64, end: Option<u64> },
+ FoodAvailability { active: bool },
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct TodayRankInput {
+ pub card_type: TodayCardType,
+ pub context_rank: ContextRank,
+ pub as_of: u64,
+ pub effective_at: u64,
+ pub time: TimeRelevance,
+ pub card_id: CardId,
+}
+
+#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
+pub enum RankError {
+ #[error("card type and time-relevance input do not match")]
+ MismatchedTimeProfile,
+ #[error("event end must be later than its start")]
+ InvalidEventRange,
+}
+
+/// Lexicographic Today order key.
+///
+/// Its [`Ord`] implementation sorts directly into feed order: higher context,
+/// time relevance, and effective time first, then lower card ID.
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TodayRank {
+ pub schema_version: u16,
+ pub algorithm_version: u16,
+ pub context_rank: ContextRank,
+ pub time_relevance_rank: u8,
+ pub effective_at: u64,
+ pub card_id: CardId,
+}
+
+impl TodayRank {
+ pub fn derive(input: TodayRankInput) -> Result<Self, RankError> {
+ let time_relevance_rank = time_relevance_rank(input)?;
+ Ok(Self {
+ schema_version: TODAY_RANK_SCHEMA_VERSION,
+ algorithm_version: TODAY_RANK_ALGORITHM_VERSION,
+ context_rank: input.context_rank,
+ time_relevance_rank,
+ effective_at: input.effective_at,
+ card_id: input.card_id,
+ })
+ }
+
+ pub fn digest(self) -> [u8; 32] {
+ let mut digest = Sha256::new();
+ digest.update(RANK_DIGEST_DOMAIN);
+ digest.update(self.schema_version.to_be_bytes());
+ digest.update(self.algorithm_version.to_be_bytes());
+ digest.update([self.context_rank.value()]);
+ digest.update([self.time_relevance_rank]);
+ digest.update(self.effective_at.to_be_bytes());
+ digest.update(self.card_id.as_bytes());
+ digest.finalize().into()
+ }
+
+ pub fn digest_hex(self) -> String {
+ hex::encode(self.digest())
+ }
+}
+
+impl Ord for TodayRank {
+ fn cmp(&self, other: &Self) -> Ordering {
+ other
+ .context_rank
+ .cmp(&self.context_rank)
+ .then_with(|| other.time_relevance_rank.cmp(&self.time_relevance_rank))
+ .then_with(|| other.effective_at.cmp(&self.effective_at))
+ .then_with(|| self.card_id.cmp(&other.card_id))
+ }
+}
+
+impl PartialOrd for TodayRank {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+fn time_relevance_rank(input: TodayRankInput) -> Result<u8, RankError> {
+ match (input.card_type, input.time) {
+ (
+ TodayCardType::Update | TodayCardType::PhotoUpdate | TodayCardType::Ask,
+ TimeRelevance::Published,
+ ) => Ok(1),
+ (TodayCardType::FoodAvailability, TimeRelevance::FoodAvailability { active }) => {
+ Ok(if active { 3 } else { 0 })
+ }
+ (TodayCardType::Event, TimeRelevance::Event { start, end }) => {
+ if end.is_some_and(|end| end <= start) {
+ return Err(RankError::InvalidEventRange);
+ }
+ if end.is_some_and(|end| input.as_of >= end) {
+ return Ok(0);
+ }
+ if input.as_of >= start {
+ return Ok(4);
+ }
+ if start.saturating_sub(input.as_of) <= UPCOMING_EVENT_WINDOW_SECONDS {
+ Ok(3)
+ } else {
+ Ok(2)
+ }
+ }
+ _ => Err(RankError::MismatchedTimeProfile),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn id(value: char) -> CardId {
+ CardId::parse(&value.to_string().repeat(64)).expect("card id")
+ }
+
+ fn input(card_type: TodayCardType, time: TimeRelevance) -> TodayRankInput {
+ TodayRankInput {
+ card_type,
+ context_rank: ContextRank::LocalityMatch,
+ as_of: 2_000_000_000,
+ effective_at: 1_999_999_900,
+ time,
+ card_id: id('a'),
+ }
+ }
+
+ #[test]
+ fn time_relevance_boundaries_are_exact() {
+ assert_eq!(
+ TodayRank::derive(input(TodayCardType::Update, TimeRelevance::Published))
+ .expect("update")
+ .time_relevance_rank,
+ 1
+ );
+ assert_eq!(
+ TodayRank::derive(input(
+ TodayCardType::FoodAvailability,
+ TimeRelevance::FoodAvailability { active: true }
+ ))
+ .expect("food")
+ .time_relevance_rank,
+ 3
+ );
+ assert_eq!(
+ TodayRank::derive(input(
+ TodayCardType::FoodAvailability,
+ TimeRelevance::FoodAvailability { active: false }
+ ))
+ .expect("sold food")
+ .time_relevance_rank,
+ 0
+ );
+ for (start, end, expected) in [
+ (1_999_999_900, Some(2_000_000_100), 4),
+ (1_999_999_900, None, 4),
+ (2_000_604_800, Some(2_000_604_900), 3),
+ (2_000_604_801, None, 2),
+ (1_999_999_000, Some(2_000_000_000), 0),
+ ] {
+ assert_eq!(
+ TodayRank::derive(input(
+ TodayCardType::Event,
+ TimeRelevance::Event { start, end }
+ ))
+ .expect("event")
+ .time_relevance_rank,
+ expected
+ );
+ }
+ }
+
+ #[test]
+ fn tuple_sorts_in_locked_feed_order_and_has_a_fixed_digest() {
+ let exact = TodayRank::derive(input(TodayCardType::Update, TimeRelevance::Published))
+ .expect("rank");
+ assert_eq!(
+ exact.digest_hex(),
+ "c7792876c8177f6f5420cc0f9aa84fb3c478f0bc6555c94ea5a7288502d6e4db"
+ );
+ let fallback = TodayRank {
+ context_rank: ContextRank::MissingLocalityFallback,
+ time_relevance_rank: 4,
+ effective_at: exact.effective_at + 100,
+ card_id: id('e'),
+ ..exact
+ };
+ let lower_time = TodayRank {
+ time_relevance_rank: 0,
+ effective_at: exact.effective_at + 200,
+ card_id: id('d'),
+ ..exact
+ };
+ let older = TodayRank {
+ effective_at: exact.effective_at - 1,
+ card_id: id('c'),
+ ..exact
+ };
+ let tie_high_id = TodayRank {
+ effective_at: exact.effective_at + 1,
+ card_id: id('b'),
+ ..exact
+ };
+ let tie_low_id = TodayRank {
+ effective_at: exact.effective_at + 1,
+ card_id: id('a'),
+ ..exact
+ };
+ let mut values = vec![fallback, lower_time, older, tie_high_id, tie_low_id];
+ values.sort();
+ assert_eq!(
+ values,
+ vec![tie_low_id, tie_high_id, older, lower_time, fallback]
+ );
+ }
+
+ #[test]
+ fn mismatched_and_invalid_time_inputs_fail_closed() {
+ assert_eq!(
+ TodayRank::derive(input(
+ TodayCardType::Update,
+ TimeRelevance::FoodAvailability { active: true }
+ )),
+ Err(RankError::MismatchedTimeProfile)
+ );
+ assert_eq!(
+ TodayRank::derive(input(
+ TodayCardType::Event,
+ TimeRelevance::Event {
+ start: 10,
+ end: Some(10),
+ }
+ )),
+ Err(RankError::InvalidEventRange)
+ );
+ }
+}
diff --git a/core/crates/tera_core/tests/package_boundary.rs b/core/crates/tera_core/tests/package_boundary.rs
@@ -7,6 +7,12 @@ const INFO: &str = include_str!("../src/runtime/info.rs");
const KEY_MANAGEMENT: &str = include_str!("../src/runtime/key_management.rs");
const NOSTR: &str = include_str!("../src/runtime/nostr.rs");
const PRODUCT_SURFACE: &str = include_str!("../src/runtime/product_surface.rs");
+const PRODUCT_CONTEXT: &str = include_str!("../src/runtime/product_surface/context.rs");
+const PRODUCT_CURSOR: &str = include_str!("../src/runtime/product_surface/cursor.rs");
+const PRODUCT_IDENTITY: &str = include_str!("../src/runtime/product_surface/identity.rs");
+const PRODUCT_MODEL: &str = include_str!("../src/runtime/product_surface/model.rs");
+const PRODUCT_PROJECTION: &str = include_str!("../src/runtime/product_surface/projection.rs");
+const PRODUCT_RANKING: &str = include_str!("../src/runtime/product_surface/ranking.rs");
const SDK: &str = include_str!("../src/runtime/sdk.rs");
#[test]
@@ -21,6 +27,15 @@ fn core_owns_no_uniffi_or_process_global_logging_policy() {
("src/runtime/key_management.rs", KEY_MANAGEMENT),
("src/runtime/nostr.rs", NOSTR),
("src/runtime/product_surface.rs", PRODUCT_SURFACE),
+ ("src/runtime/product_surface/context.rs", PRODUCT_CONTEXT),
+ ("src/runtime/product_surface/cursor.rs", PRODUCT_CURSOR),
+ ("src/runtime/product_surface/identity.rs", PRODUCT_IDENTITY),
+ ("src/runtime/product_surface/model.rs", PRODUCT_MODEL),
+ (
+ "src/runtime/product_surface/projection.rs",
+ PRODUCT_PROJECTION,
+ ),
+ ("src/runtime/product_surface/ranking.rs", PRODUCT_RANKING),
("src/runtime/sdk.rs", SDK),
] {
assert!(