223 lines
7.6 KiB
Rust
223 lines
7.6 KiB
Rust
//! 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());
|
|
}
|
|
}
|