ontoref-code/crates/ontoref-ops/src/capabilities.rs

224 lines
7.6 KiB
Rust
Raw Permalink Normal View History

//! 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<CapabilityRef>,
pub locked: Vec<CapabilityRef>,
}
/// 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<Tier> {
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<Tier>,
pub adds: Vec<CapabilityRef>,
/// 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());
}
}