use crate::lock::ItemKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VisibilityClass {
Exported,
Local,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ItemVisibility {
pub class: VisibilityClass,
pub is_explicit_override: bool,
pub warning: Option<VisibilityWarning>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VisibilityWarning {
EffectfulItemExported { kind: ItemKind, name: String },
}
pub fn default_visibility(kind: ItemKind) -> VisibilityClass {
match kind {
ItemKind::Agent | ItemKind::Skill | ItemKind::BootstrapDoc => VisibilityClass::Exported,
ItemKind::Hook | ItemKind::McpServer => VisibilityClass::Local,
}
}
pub fn resolve_visibility(
kind: ItemKind,
name: &str,
explicit_exported: Option<bool>,
) -> ItemVisibility {
let default = default_visibility(kind);
match explicit_exported {
None => ItemVisibility {
class: default,
is_explicit_override: false,
warning: None,
},
Some(true) => {
let warning = if default == VisibilityClass::Local {
Some(VisibilityWarning::EffectfulItemExported {
kind,
name: name.to_owned(),
})
} else {
None
};
ItemVisibility {
class: VisibilityClass::Exported,
is_explicit_override: default == VisibilityClass::Local,
warning,
}
}
Some(false) => {
ItemVisibility {
class: VisibilityClass::Local,
is_explicit_override: default == VisibilityClass::Exported,
warning: None,
}
}
}
}
pub fn can_cross_package_boundary(visibility: &ItemVisibility) -> bool {
visibility.class == VisibilityClass::Exported
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lock::ItemKind;
#[test]
fn agents_are_exported_by_default() {
assert_eq!(
default_visibility(ItemKind::Agent),
VisibilityClass::Exported
);
}
#[test]
fn skills_are_exported_by_default() {
assert_eq!(
default_visibility(ItemKind::Skill),
VisibilityClass::Exported
);
}
#[test]
fn bootstrap_docs_are_exported_by_default() {
assert_eq!(
default_visibility(ItemKind::BootstrapDoc),
VisibilityClass::Exported
);
}
#[test]
fn hooks_are_local_by_default() {
assert_eq!(default_visibility(ItemKind::Hook), VisibilityClass::Local);
}
#[test]
fn mcp_servers_are_local_by_default() {
assert_eq!(
default_visibility(ItemKind::McpServer),
VisibilityClass::Local
);
}
#[test]
fn resolve_agent_with_no_override_is_exported() {
let v = resolve_visibility(ItemKind::Agent, "coder", None);
assert_eq!(v.class, VisibilityClass::Exported);
assert!(!v.is_explicit_override);
assert!(v.warning.is_none());
}
#[test]
fn resolve_hook_with_no_override_is_local() {
let v = resolve_visibility(ItemKind::Hook, "pre-commit", None);
assert_eq!(v.class, VisibilityClass::Local);
assert!(!v.is_explicit_override);
assert!(v.warning.is_none());
}
#[test]
fn resolve_hook_exported_true_emits_warning() {
let v = resolve_visibility(ItemKind::Hook, "pre-commit", Some(true));
assert_eq!(v.class, VisibilityClass::Exported);
assert!(v.is_explicit_override);
assert!(matches!(
v.warning,
Some(VisibilityWarning::EffectfulItemExported {
kind: ItemKind::Hook,
..
})
));
}
#[test]
fn resolve_mcp_exported_true_emits_warning() {
let v = resolve_visibility(ItemKind::McpServer, "my-server", Some(true));
assert_eq!(v.class, VisibilityClass::Exported);
assert!(v.is_explicit_override);
assert!(v.warning.is_some());
}
#[test]
fn resolve_skill_exported_false_is_local_with_override() {
let v = resolve_visibility(ItemKind::Skill, "planning", Some(false));
assert_eq!(v.class, VisibilityClass::Local);
assert!(v.is_explicit_override);
assert!(v.warning.is_none());
}
#[test]
fn can_cross_boundary_is_true_for_exported() {
let v = resolve_visibility(ItemKind::Agent, "coder", None);
assert!(can_cross_package_boundary(&v));
}
#[test]
fn can_cross_boundary_is_false_for_local() {
let v = resolve_visibility(ItemKind::Hook, "pre-commit", None);
assert!(!can_cross_package_boundary(&v));
}
#[test]
fn multi_hop_exported_agent_remains_visible() {
let v = resolve_visibility(ItemKind::Agent, "deep-agent", None);
assert!(can_cross_package_boundary(&v));
assert!(can_cross_package_boundary(&v));
}
}