polyc-state-connect 2026.9.0

State plane transport adapter: capability-specific Connect clients and server-trait glue mapping the generated wire types onto the polyc-state kernel — typed outcomes, per-call admission, and the conformance surface the authenticated shell proves itself against (docs/proposals/separated-planes.md).
//! Immutable-metadata-specific outcomes carried as Connect details.

use connectrpc::{ConnectError, ErrorCode, ErrorDetail};
use polyc_proto::proto::polychrome::state::v1 as pb;
use polyc_state::{
    error::StateError,
    immutable::{Generation, ObjectError, ObjectId, Retention},
};

use crate::{
    error::{TransportFallback, decode_detail, from_connect_error as state_from_connect},
    immutable::wire::{classification, protection},
    wire::Kernel,
};

/// The detail type for outcomes added by immutable metadata.
pub const OBJECT_ERROR_DETAIL_TYPE: &str = "polychrome.state.v1.ObjectErrorDetail";

impl From<Kernel<&ObjectError>> for pb::ObjectErrorDetail {
    fn from(value: Kernel<&ObjectError>) -> Self {
        use pb::__buffa::oneof::object_error_detail::Outcome;
        let unknown = buffa::UnknownFields::default;
        let outcome = match value.0 {
            ObjectError::State(error) => Outcome::from(pb::StateErrorDetail::from(Kernel(error))),
            ObjectError::StaleGeneration {
                object,
                expected,
                current,
            } => Outcome::from(pb::StaleObjectGenerationDetail {
                object: object.as_str().to_owned(),
                expected: expected.get(),
                current: current.get(),
                __buffa_unknown_fields: unknown(),
            }),
            ObjectError::NotMonotonic {
                object,
                current,
                requested,
            } => Outcome::from(pb::NonMonotonicObjectGenerationDetail {
                object: object.as_str().to_owned(),
                current: current.get(),
                requested: requested.get(),
                __buffa_unknown_fields: unknown(),
            }),
            ObjectError::UnknownGeneration { object, generation } => {
                Outcome::from(pb::UnknownObjectGenerationDetail {
                    object: object.as_str().to_owned(),
                    generation: generation.get(),
                    __buffa_unknown_fields: unknown(),
                })
            }
            ObjectError::Retained {
                object,
                generation,
                retention,
            } => Outcome::from(pb::RetainedObjectGenerationDetail {
                object: object.as_str().to_owned(),
                generation: generation.get(),
                retention: buffa::MessageField::some(pb::ObjectRetention::from(Kernel(*retention))),
                __buffa_unknown_fields: unknown(),
            }),
            ObjectError::CurrentGeneration { object, generation } => {
                Outcome::from(pb::CurrentObjectGenerationDetail {
                    object: object.as_str().to_owned(),
                    generation: generation.get(),
                    __buffa_unknown_fields: unknown(),
                })
            }
            ObjectError::DescriptorConflict { object, generation } => {
                Outcome::from(pb::ObjectDescriptorConflictDetail {
                    object: object.as_str().to_owned(),
                    generation: generation.get(),
                    __buffa_unknown_fields: unknown(),
                })
            }
            ObjectError::ProtectionUnavailable {
                object,
                classification,
                declared,
            } => Outcome::from(pb::ObjectProtectionUnavailableDetail {
                object: object.as_str().to_owned(),
                classification: pb::ObjectClassification::from(Kernel(*classification)).into(),
                declared_protection: pb::ObjectAtRestProtection::from(Kernel(*declared)).into(),
                __buffa_unknown_fields: unknown(),
            }),
        };
        Self {
            outcome: Some(outcome),
            __buffa_unknown_fields: unknown(),
        }
    }
}

impl TryFrom<pb::ObjectErrorDetail> for Kernel<ObjectError> {
    type Error = pb::ObjectErrorDetail;

    fn try_from(value: pb::ObjectErrorDetail) -> Result<Self, Self::Error> {
        use pb::__buffa::oneof::object_error_detail::Outcome;
        let Some(outcome) = value.outcome.clone() else {
            return Err(value);
        };
        let decoded = match outcome {
            Outcome::State(detail) => {
                let Ok(error) = Kernel::<StateError>::try_from(*detail) else {
                    return Err(value);
                };
                ObjectError::State(error.into_inner())
            }
            Outcome::StaleGeneration(detail) => ObjectError::StaleGeneration {
                object: ObjectId::new(detail.object),
                expected: Generation::new(detail.expected),
                current: Generation::new(detail.current),
            },
            Outcome::NotMonotonic(detail) => ObjectError::NotMonotonic {
                object: ObjectId::new(detail.object),
                current: Generation::new(detail.current),
                requested: Generation::new(detail.requested),
            },
            Outcome::UnknownGeneration(detail) => ObjectError::UnknownGeneration {
                object: ObjectId::new(detail.object),
                generation: Generation::new(detail.generation),
            },
            Outcome::Retained(detail) => {
                let retention = detail
                    .retention
                    .into_option()
                    .and_then(|value| Kernel::<Retention>::try_from(value).ok())
                    .map_or(Retention::UntilReleased, Kernel::into_inner);
                ObjectError::Retained {
                    object: ObjectId::new(detail.object),
                    generation: Generation::new(detail.generation),
                    retention,
                }
            }
            Outcome::CurrentGeneration(detail) => ObjectError::CurrentGeneration {
                object: ObjectId::new(detail.object),
                generation: Generation::new(detail.generation),
            },
            Outcome::DescriptorConflict(detail) => ObjectError::DescriptorConflict {
                object: ObjectId::new(detail.object),
                generation: Generation::new(detail.generation),
            },
            Outcome::ProtectionUnavailable(detail) => {
                let Ok(classification) = classification("classification", detail.classification)
                else {
                    return Err(value);
                };
                let Ok(declared) = protection("declared_protection", detail.declared_protection)
                else {
                    return Err(value);
                };
                ObjectError::ProtectionUnavailable {
                    object: ObjectId::new(detail.object),
                    classification,
                    declared,
                }
            }
        };
        Ok(Self(decoded))
    }
}

/// Encodes the exact immutable-metadata outcome.
#[must_use]
pub fn to_connect_error(error: &ObjectError) -> ConnectError {
    let code = match error {
        ObjectError::State(state) => crate::error::code_for(state),
        ObjectError::UnknownGeneration { .. } => ErrorCode::NotFound,
        ObjectError::StaleGeneration { .. }
        | ObjectError::NotMonotonic { .. }
        | ObjectError::Retained { .. }
        | ObjectError::CurrentGeneration { .. }
        | ObjectError::DescriptorConflict { .. }
        | ObjectError::ProtectionUnavailable { .. } => ErrorCode::FailedPrecondition,
    };
    ConnectError::new(code, error.to_string()).with_detail(ErrorDetail::from_message(
        OBJECT_ERROR_DETAIL_TYPE,
        &pb::ObjectErrorDetail::from(Kernel(error)),
    ))
}

/// Decodes an immutable-metadata detail, or the shared State fallback.
#[must_use]
pub fn from_connect_error(error: &ConnectError, fallback: &TransportFallback) -> ObjectError {
    for detail in &error.details {
        if detail.type_url != OBJECT_ERROR_DETAIL_TYPE {
            continue;
        }
        let Some(encoded) = detail.value.as_deref() else {
            continue;
        };
        let Ok(bytes) = decode_detail(encoded) else {
            continue;
        };
        let Ok(message) = <pb::ObjectErrorDetail as buffa::Message>::decode_from_slice(&bytes)
        else {
            continue;
        };
        if let Ok(typed) = Kernel::<ObjectError>::try_from(message) {
            return typed.into_inner();
        }
    }
    ObjectError::State(state_from_connect(error, fallback))
}

#[cfg(test)]
mod tests {
    use super::*;
    use polyc_state::immutable::{AtRestProtection, Classification};

    #[test]
    fn every_family_variant_round_trips() {
        let variants = [
            ObjectError::StaleGeneration {
                object: ObjectId::new("o"),
                expected: Generation::new(1),
                current: Generation::new(2),
            },
            ObjectError::NotMonotonic {
                object: ObjectId::new("o"),
                current: Generation::new(2),
                requested: Generation::new(1),
            },
            ObjectError::UnknownGeneration {
                object: ObjectId::new("o"),
                generation: Generation::new(9),
            },
            ObjectError::Retained {
                object: ObjectId::new("o"),
                generation: Generation::new(1),
                retention: Retention::For(std::time::Duration::from_secs(4)),
            },
            ObjectError::CurrentGeneration {
                object: ObjectId::new("o"),
                generation: Generation::new(2),
            },
            ObjectError::DescriptorConflict {
                object: ObjectId::new("o"),
                generation: Generation::new(2),
            },
            ObjectError::ProtectionUnavailable {
                object: ObjectId::new("o"),
                classification: Classification::Restricted,
                declared: AtRestProtection::ManagedKey,
            },
        ];
        for original in variants {
            let wire = to_connect_error(&original);
            let back = from_connect_error(
                &wire,
                &TransportFallback::new(polyc_state::immutable::family(), 65_536, 1),
            );
            assert_eq!(back, original);
        }
    }
}