use std::path::{Path, PathBuf};
use clawft_types::placement::{AttrValue, Capability, CapabilityState, Provenance};
use super::body::MODEL_MARKER_PREFIX;
use super::check::{CheckMode, ModelState};
use super::registry::ModelRegistry;
use crate::node_facts::probe::{cap, str_list};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StoreTier {
Internal,
External,
}
#[derive(Debug, Clone)]
pub struct TierResolver {
external_roots: Vec<PathBuf>,
}
impl TierResolver {
pub fn system_default() -> Self {
let roots: &[&str] = if cfg!(target_os = "macos") {
&["/Volumes"]
} else {
&["/media", "/mnt", "/run/media"]
};
Self::with_external_roots(roots.iter().map(PathBuf::from).collect())
}
pub fn with_external_roots(external_roots: Vec<PathBuf>) -> Self {
Self { external_roots }
}
pub fn classify(&self, path: &Path) -> (StoreTier, Option<PathBuf>) {
for root in &self.external_roots {
if let Ok(rest) = path.strip_prefix(root)
&& let Some(first) = rest.components().next()
{
return (StoreTier::External, Some(root.join(first)));
}
}
(StoreTier::Internal, None)
}
pub fn mounted(&self, mount: &Path) -> bool {
mount.is_dir()
}
}
impl Default for TierResolver {
fn default() -> Self {
Self::system_default()
}
}
pub fn model_capabilities(registry: &ModelRegistry, tiers: &TierResolver) -> Vec<Capability> {
let mut out = Vec::new();
let mut detached = false;
for (id, entry) in registry.entries() {
if entry.hidden {
continue;
}
let Ok(check) = registry.check(&id, CheckMode::Stat) else {
continue;
};
let untrusted = registry.untrusted(&id, &entry);
if matches!(check.state, ModelState::Refused { .. }) || entry.refused.is_some() || untrusted {
if let Some(c) = cap("model.present", Provenance::Probed) {
out.push(
c.with_attr("shards", str_list::<&str>(&[]))
.with_state(CapabilityState::Degraded),
);
}
continue;
}
let (tier, mount) = tiers.classify(&entry.root);
let drive_gone = tier == StoreTier::External
&& mount.as_deref().is_some_and(|m| !tiers.mounted(m));
detached |= drive_gone;
let complete = check.present_shards.len() == check.total_shards;
let state = match (&check.state, complete) {
(ModelState::Ready, true) if !drive_gone => CapabilityState::Available,
_ => CapabilityState::Degraded,
};
let mut shards: Vec<String> = Vec::new();
if state == CapabilityState::Available || drive_gone {
shards.push(format!("{MODEL_MARKER_PREFIX}{id}"));
}
if drive_gone && let Ok(body) = entry.body() {
shards.extend(body.shard_hashes().map(String::from));
} else {
shards.extend(check.present_shards.iter().cloned());
}
if shards.is_empty() {
continue;
}
if let Some(c) = cap("model.present", Provenance::Probed) {
out.push(c.with_attr("shards", str_list(&shards)).with_state(state));
}
}
if detached
&& let Some(c) = cap("store.tier.external", Provenance::Probed)
{
out.push(
c.with_attr("mounted", AttrValue::Bool(false))
.with_state(CapabilityState::Degraded),
);
}
out
}