use organism_pack::IntentBinding;
use organism_simulation::BudgetAmount;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadinessReport {
pub ready: bool,
pub confirmed: Vec<ReadinessConfirmation>,
pub gaps: Vec<ReadinessGap>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadinessConfirmation {
pub resource: String,
pub kind: ResourceKind,
pub detail: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadinessGap {
pub resource: String,
pub kind: ResourceKind,
pub severity: GapSeverity,
pub reason: String,
pub suggestion: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResourceKind {
Feature,
Credential,
Budget,
Service,
Pack,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GapSeverity {
Blocking,
Degraded,
Advisory,
}
pub trait ReadinessProbe: Send + Sync {
fn kind(&self) -> ResourceKind;
fn check(&self, binding: &IntentBinding) -> Vec<ReadinessItem>;
}
pub enum ReadinessItem {
Confirmed(ReadinessConfirmation),
Gap(ReadinessGap),
}
pub fn check(binding: &IntentBinding, probes: &[&dyn ReadinessProbe]) -> ReadinessReport {
let mut confirmed = Vec::new();
let mut gaps = Vec::new();
for probe in probes {
for item in probe.check(binding) {
match item {
ReadinessItem::Confirmed(c) => confirmed.push(c),
ReadinessItem::Gap(g) => gaps.push(g),
}
}
}
let ready = !gaps.iter().any(|g| g.severity == GapSeverity::Blocking);
ReadinessReport {
ready,
confirmed,
gaps,
}
}
pub struct CredentialProbe {
checks: Vec<(String, String)>,
}
impl CredentialProbe {
#[must_use]
pub fn new() -> Self {
Self { checks: Vec::new() }
}
#[must_use]
pub fn require(mut self, capability: impl Into<String>, env_var: impl Into<String>) -> Self {
self.checks.push((capability.into(), env_var.into()));
self
}
#[must_use]
pub fn with_standard_checks(self) -> Self {
self.require("vision", "ANTHROPIC_API_KEY")
.require("ocr", "MISTRAL_API_KEY")
.require("patent", "USPTO_API_KEY")
.require("social", "ANTHROPIC_API_KEY")
}
}
impl Default for CredentialProbe {
fn default() -> Self {
Self::new()
}
}
impl ReadinessProbe for CredentialProbe {
fn kind(&self) -> ResourceKind {
ResourceKind::Credential
}
fn check(&self, binding: &IntentBinding) -> Vec<ReadinessItem> {
let needed_capabilities: Vec<&str> = binding
.capabilities
.iter()
.map(|c| c.capability.as_str())
.collect();
let mut items = Vec::new();
for (capability, env_var) in &self.checks {
if !needed_capabilities.contains(&capability.as_str()) {
continue;
}
if std::env::var(env_var).is_ok() {
items.push(ReadinessItem::Confirmed(ReadinessConfirmation {
resource: capability.clone(),
kind: ResourceKind::Credential,
detail: format!("{env_var} is set"),
}));
} else {
items.push(ReadinessItem::Gap(ReadinessGap {
resource: capability.clone(),
kind: ResourceKind::Credential,
severity: GapSeverity::Blocking,
reason: format!("{env_var} is not set"),
suggestion: Some(format!("export {env_var}=<your-key>")),
}));
}
}
items
}
}
pub struct PackProbe<'a> {
registry: &'a super::registry::Registry,
}
impl<'a> PackProbe<'a> {
#[must_use]
pub fn new(registry: &'a super::registry::Registry) -> Self {
Self { registry }
}
}
impl ReadinessProbe for PackProbe<'_> {
fn kind(&self) -> ResourceKind {
ResourceKind::Pack
}
fn check(&self, binding: &IntentBinding) -> Vec<ReadinessItem> {
let mut items = Vec::new();
for pack_req in &binding.packs {
let registered = self
.registry
.packs()
.iter()
.any(|p| p.name == pack_req.pack_name);
if registered {
items.push(ReadinessItem::Confirmed(ReadinessConfirmation {
resource: pack_req.pack_name.clone(),
kind: ResourceKind::Pack,
detail: "registered in runtime".into(),
}));
} else {
let severity = if pack_req.confidence.as_f64() >= 0.8 {
GapSeverity::Blocking
} else {
GapSeverity::Degraded
};
items.push(ReadinessItem::Gap(ReadinessGap {
resource: pack_req.pack_name.clone(),
kind: ResourceKind::Pack,
severity,
reason: format!(
"pack '{}' needed ({:?}, confidence {:.0}%) but not registered",
pack_req.pack_name,
pack_req.source,
pack_req.confidence.as_f64() * 100.0
),
suggestion: Some(format!(
"registry.register_pack(\"{}\", ...)",
pack_req.pack_name
)),
}));
}
}
items
}
}
pub struct BudgetProbe {
pub token_budget: Option<u64>,
pub spend_budget: Option<BudgetAmount>,
}
impl BudgetProbe {
#[must_use]
pub fn new() -> Self {
Self {
token_budget: None,
spend_budget: None,
}
}
#[must_use]
pub fn with_token_budget(mut self, tokens: u64) -> Self {
self.token_budget = Some(tokens);
self
}
#[must_use]
pub fn with_spend_budget(mut self, dollars: impl Into<BudgetAmount>) -> Self {
self.spend_budget = Some(dollars.into());
self
}
}
impl Default for BudgetProbe {
fn default() -> Self {
Self::new()
}
}
impl ReadinessProbe for BudgetProbe {
fn kind(&self) -> ResourceKind {
ResourceKind::Budget
}
fn check(&self, binding: &IntentBinding) -> Vec<ReadinessItem> {
let mut items = Vec::new();
let needs_llm = binding
.capabilities
.iter()
.any(|c| ["vision", "ocr", "social"].contains(&c.capability.as_str()));
if needs_llm {
if let Some(tokens) = self.token_budget {
if tokens > 0 {
items.push(ReadinessItem::Confirmed(ReadinessConfirmation {
resource: "token_budget".into(),
kind: ResourceKind::Budget,
detail: format!("{tokens} tokens available"),
}));
} else {
items.push(ReadinessItem::Gap(ReadinessGap {
resource: "token_budget".into(),
kind: ResourceKind::Budget,
severity: GapSeverity::Blocking,
reason: "token budget exhausted".into(),
suggestion: Some("increase token budget or remove LLM capabilities".into()),
}));
}
}
if let Some(spend) = self.spend_budget {
if spend.as_f64() > 0.0 {
items.push(ReadinessItem::Confirmed(ReadinessConfirmation {
resource: "spend_budget".into(),
kind: ResourceKind::Budget,
detail: format!("${:.2} remaining", spend.as_f64()),
}));
} else {
items.push(ReadinessItem::Gap(ReadinessGap {
resource: "spend_budget".into(),
kind: ResourceKind::Budget,
severity: GapSeverity::Blocking,
reason: "spend budget exhausted".into(),
suggestion: Some("increase spend budget".into()),
}));
}
}
}
items
}
}
#[cfg(test)]
mod tests {
use super::*;
use organism_pack::DeclarativeBinding;
#[test]
fn reports_ready_when_no_gaps() {
let binding = DeclarativeBinding::new()
.pack("customers", "lead qualification")
.build();
let report = check(&binding, &[]);
assert!(report.ready);
assert!(report.gaps.is_empty());
}
#[test]
fn credential_probe_detects_missing_key() {
let binding = DeclarativeBinding::new()
.capability("vision", "scene understanding")
.build();
let probe =
CredentialProbe::new().require("vision", "ORGANISM_TEST_KEY_THAT_DOES_NOT_EXIST");
let report = check(&binding, &[&probe]);
assert!(!report.ready);
assert_eq!(report.gaps.len(), 1);
assert_eq!(report.gaps[0].resource, "vision");
assert_eq!(report.gaps[0].severity, GapSeverity::Blocking);
assert!(report.gaps[0].reason.contains("not set"));
}
#[test]
fn pack_probe_detects_unregistered_pack() {
let binding = DeclarativeBinding::new()
.pack("customers", "lead qualification")
.pack("legal", "contract review")
.build();
let mut registry = super::super::registry::Registry::new();
registry.register_pack_raw(super::super::registry::RegisteredPack {
name: "customers".into(),
description: "revenue ops".into(),
fact_prefixes: vec!["lead:".into()],
agent_names: vec![],
invariant_names: vec![],
agent_count: 8,
invariant_count: 2,
context_keys_read: vec![],
context_keys_written: vec![],
has_acceptance_invariants: false,
profile: organism_pack::PackProfile::default(),
});
let probe = PackProbe::new(®istry);
let report = check(&binding, &[&probe]);
assert!(!report.ready);
assert_eq!(report.confirmed.len(), 1);
assert_eq!(report.confirmed[0].resource, "customers");
assert_eq!(report.gaps.len(), 1);
assert_eq!(report.gaps[0].resource, "legal");
}
#[test]
fn budget_probe_blocks_on_zero_budget() {
let binding = DeclarativeBinding::new()
.capability("vision", "scene analysis")
.build();
let probe = BudgetProbe::new().with_token_budget(0);
let report = check(&binding, &[&probe]);
assert!(!report.ready);
assert!(report.gaps.iter().any(|g| g.resource == "token_budget"));
}
#[test]
fn budget_probe_confirms_available_budget() {
let binding = DeclarativeBinding::new()
.capability("ocr", "document reading")
.build();
let probe = BudgetProbe::new()
.with_token_budget(100_000)
.with_spend_budget(5.0);
let report = check(&binding, &[&probe]);
assert!(report.ready);
assert_eq!(report.confirmed.len(), 2);
}
}