//! Tier capability matrix (ADR-029). //! //! Defines which operations and surfaces a project gains access to at each //! tier. Used by the CLI (`ore tier status`), REST API (`/tier/status`), //! GraphQL (`tierStatus`), and MCP (`ontoref_tier_status`). use serde::Serialize; use crate::tier::Tier; /// A named capability gated on a minimum tier. #[derive(Debug, Clone, Serialize)] pub struct TierCapability { pub id: &'static str, pub label: &'static str, pub min_tier: Tier, pub description: &'static str, } static CAPABILITIES: &[TierCapability] = &[ TierCapability { id: "ncl-export", label: "NCL export & query", min_tier: Tier::Tier0, description: "Export any .ncl file as typed JSON via daemon cache or direct nickel call", }, TierCapability { id: "describe", label: "Describe / self-knowledge", min_tier: Tier::Tier0, description: "describe project, capabilities, config, connections, state — full self-knowledge surface", }, TierCapability { id: "adrs", label: "Architecture Decision Records", min_tier: Tier::Tier0, description: "Author, query, and validate ADRs as typed NCL with per-constraint pass/fail", }, TierCapability { id: "reflection-modes", label: "Reflection modes (DAG workflows)", min_tier: Tier::Tier0, description: "Define and execute DAG-validated operational modes via the reflection layer", }, TierCapability { id: "positioning", label: "Positioning layer (ADR-035)", min_tier: Tier::Tier0, description: "Marketing as queryable protocol surface: value-props, audiences, campaigns", }, TierCapability { id: "migrations", label: "Protocol migrations", min_tier: Tier::Tier0, description: "Track and apply protocol evolution migrations across project adoption", }, TierCapability { id: "substrate", label: "Substrate (commit layer + state root)", min_tier: Tier::Tier1, description: "Content-addressed ontology commits with state root — enables signed attestations and audit trails (ADR-023)", }, TierCapability { id: "signed-artifacts", label: "Signed artifacts & witnesses", min_tier: Tier::Tier1, description: "Cryptographically signed witnesses on any authoritative-state mutation", }, TierCapability { id: "ops-dispatch", label: "Ops dispatch (typed mutations)", min_tier: Tier::Tier2, description: "All state mutations route through declared domain operations — the agent action boundary (ADR-024)", }, TierCapability { id: "catalogued-ops", label: "Catalogued operations (ADR-026)", min_tier: Tier::Tier2, description: "Ops registered in the artifact catalog with validators, preconditions, and SLA declarations", }, TierCapability { id: "runtime-mutation", label: "Runtime-only state mutation", min_tier: Tier::Tier2, description: "NCL files become render targets; all writes go through the ops runtime — human and AI paths unified", }, TierCapability { id: "witness-verification", label: "Witness verification at boundary", min_tier: Tier::Tier2, description: "Every op produces a verifiable witness; validators reject ill-typed state without a valid witness chain", }, ]; /// Capability split for a given tier: what is available vs locked. #[derive(Debug, Serialize)] pub struct CapabilityView { pub available: Vec, pub locked: Vec, } /// A serialisable reference into the static capability table. #[derive(Debug, Clone, Serialize)] pub struct CapabilityRef { pub id: &'static str, pub label: &'static str, pub min_tier: Tier, pub description: &'static str, } impl From<&'static TierCapability> for CapabilityRef { fn from(c: &'static TierCapability) -> Self { CapabilityRef { id: c.id, label: c.label, min_tier: c.min_tier, description: c.description, } } } /// Returns the capability split for the given tier. pub fn capability_view(current: Tier) -> CapabilityView { let (available, locked): (Vec<_>, Vec<_>) = CAPABILITIES .iter() .partition(|c| c.min_tier <= current); CapabilityView { available: available.into_iter().map(CapabilityRef::from).collect(), locked: locked.into_iter().map(CapabilityRef::from).collect(), } } /// The tier immediately following the given one, if any. pub fn next_tier(current: Tier) -> Option { match current { Tier::Tier0 => Some(Tier::Tier1), Tier::Tier1 => Some(Tier::Tier2), Tier::Tier2 => None, } } /// Forward projection: what the next tier adds, if a next tier exists. #[derive(Debug, Serialize)] pub struct TierProjection { pub from: Tier, pub to: Option, pub adds: Vec, /// Human-readable narrative summarising the value proposition of the /// next tier. pub narrative: &'static str, } /// Compute the forward projection from the current tier. pub fn tier_projection(current: Tier) -> TierProjection { let to = next_tier(current); let (adds, narrative) = match to { Some(Tier::Tier1) => ( CAPABILITIES .iter() .filter(|c| c.min_tier == Tier::Tier1) .map(CapabilityRef::from) .collect(), "Tier-1 adopts the substrate layer (ADR-023): content-addressed ontology commits, \ a state root, and signed artifacts. Every mutation becomes auditable and \ reproducible. Prerequisite for ops-dispatch at Tier-2.", ), Some(Tier::Tier2) => ( CAPABILITIES .iter() .filter(|c| c.min_tier == Tier::Tier2) .map(CapabilityRef::from) .collect(), "Tier-2 adopts the operations layer (ADR-024): all state mutations route through \ declared domain operations. NCL files become render targets. The runtime is the \ only mutation path; witnesses are cryptographically verifiable; validators reject \ ill-typed state.", ), None => (vec![], "Already at the highest tier. No further evolution is defined."), Some(_) => unreachable!(), }; TierProjection { from: current, to, adds, narrative } } #[cfg(test)] mod tests { use super::*; #[test] fn tier0_has_six_available() { let v = capability_view(Tier::Tier0); assert_eq!(v.available.len(), 6); assert!(!v.locked.is_empty()); } #[test] fn tier2_all_available() { let v = capability_view(Tier::Tier2); assert_eq!(v.locked.len(), 0); assert_eq!(v.available.len(), CAPABILITIES.len()); } #[test] fn next_tier_chain() { assert_eq!(next_tier(Tier::Tier0), Some(Tier::Tier1)); assert_eq!(next_tier(Tier::Tier1), Some(Tier::Tier2)); assert_eq!(next_tier(Tier::Tier2), None); } #[test] fn projection_from_tier0_adds_substrate_capabilities() { let p = tier_projection(Tier::Tier0); assert_eq!(p.to, Some(Tier::Tier1)); assert!(p.adds.iter().any(|c| c.id == "substrate")); assert!(p.adds.iter().any(|c| c.id == "signed-artifacts")); } #[test] fn projection_from_tier2_is_terminal() { let p = tier_projection(Tier::Tier2); assert!(p.to.is_none()); assert!(p.adds.is_empty()); } }