shepherd-core 6.6.0

The harness-agnostic shepherd engine: domain types, configuration schema, and run state. Knows nothing about any CLI, harness, or process.
//! Pure classification over facts opened and measured by a native adapter.

use super::{PathAuthority, Role};

#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    serde::Deserialize,
    serde::Serialize,
    strum::AsRefStr,
    strum::Display,
    strum::EnumCount,
    strum::EnumIs,
    strum::EnumString,
    strum::IntoStaticStr,
    strum::VariantNames,
)]
#[serde(rename_all = "kebab-case")]
#[strum(ascii_case_insensitive, serialize_all = "snake_case")]
pub enum PathClass {
    Production,
    NonCodeDeliverable,
    RunArtifact,
    NativeState,
}

#[derive(
    Clone,
    Copy,
    Debug,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    serde::Deserialize,
    serde::Serialize,
    strum::AsRefStr,
    strum::Display,
    strum::EnumCount,
    strum::EnumIs,
    strum::EnumString,
    strum::IntoStaticStr,
    strum::VariantNames,
)]
#[serde(rename_all = "kebab-case")]
#[strum(ascii_case_insensitive, serialize_all = "snake_case")]
pub enum PathFactKind {
    ExistingFile,
    ExistingDirectory,
    Missing,
}

#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields)]
pub struct TrustedPathFacts {
    pub path: PathAuthority,
    pub kind: PathFactKind,
    pub git_tracked: bool,
    pub workspace_member: bool,
    pub known_production: bool,
    pub known_non_code: bool,
    pub run_artifact: bool,
    pub native_state: bool,
    pub inherited_parent: Option<PathClass>,
    pub conflict: bool,
}

/// Derive one class from trusted descriptor, Git, workspace, and run-schema
/// facts. No field is a caller-selected class. Contradictions and unknowns are
/// production so a Worker cannot gain source authority through ambiguity.
#[must_use]
pub const fn classify_trusted_path(facts: &TrustedPathFacts) -> PathClass {
    if facts.native_state {
        return PathClass::NativeState;
    }
    if facts.conflict
        || facts.known_production
        || facts.workspace_member
        || (facts.git_tracked && !facts.known_non_code && !facts.run_artifact)
    {
        return PathClass::Production;
    }
    if facts.run_artifact {
        return PathClass::RunArtifact;
    }
    if facts.known_non_code {
        return PathClass::NonCodeDeliverable;
    }
    if matches!(facts.kind, PathFactKind::Missing) {
        return match facts.inherited_parent {
            Some(PathClass::NonCodeDeliverable) => PathClass::NonCodeDeliverable,
            Some(PathClass::RunArtifact) => PathClass::RunArtifact,
            Some(PathClass::Production | PathClass::NativeState) | None => PathClass::Production,
        };
    }
    PathClass::Production
}

impl Role {
    /// Role capability still composes with the native path fact. Neither side
    /// alone grants mutation authority.
    #[must_use]
    pub const fn may_write_class(self, class: PathClass) -> bool {
        match self {
            Self::Coder => matches!(class, PathClass::Production),
            Self::Worker => matches!(
                class,
                PathClass::NonCodeDeliverable | PathClass::RunArtifact
            ),
            Self::Engineer | Self::Conductor => matches!(class, PathClass::RunArtifact),
            Self::Auditor | Self::Critic | Self::Discovery | Self::Planter | Self::Shepherd => {
                false
            }
        }
    }
}