organism-runtime 1.9.3

Curated embedded runtime for Organism — registry, readiness, and pipeline wiring
Documentation
//! Formation outcome records for learning and audit.

use converge_kernel::formation::{FormationKind, SuggestorCapability, SuggestorRole};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::compiler::{
    CompiledFormationPlan, FormationTemplateId, GovernanceClass, ProviderId, ReplayMode,
    SuggestorDescriptorId,
};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FormationRunScope {
    pub plan_id: Uuid,
    pub correlation_id: Uuid,
    pub tenant_id: Option<String>,
    pub tournament_id: Option<Uuid>,
    pub candidate_id: Option<Uuid>,
}

impl FormationRunScope {
    pub fn from_compiled_plan(plan: &CompiledFormationPlan) -> Self {
        Self {
            plan_id: plan.plan_id,
            correlation_id: plan.correlation_id,
            tenant_id: plan.tenant_id.clone(),
            tournament_id: None,
            candidate_id: None,
        }
    }

    pub fn with_tournament_id(mut self, tournament_id: Uuid) -> Self {
        self.tournament_id = Some(tournament_id);
        self
    }

    pub fn with_candidate_id(mut self, candidate_id: Uuid) -> Self {
        self.candidate_id = Some(candidate_id);
        self
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FormationOutcomeStatus {
    Planned,
    Converged,
    NeedsReview,
    CriteriaBlocked,
    BudgetExhausted,
    Failed,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct QualityScoreBps(u16);

impl QualityScoreBps {
    pub const MAX: u16 = 10_000;

    pub fn new(value: u16) -> Result<Self, QualityScoreError> {
        if value <= Self::MAX {
            Ok(Self(value))
        } else {
            Err(QualityScoreError::OutOfRange { value })
        }
    }

    pub fn as_u16(self) -> u16 {
        self.0
    }
}

#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum QualityScoreError {
    #[error("quality score must be between 0 and 10000 bps, got {value}")]
    OutOfRange { value: u16 },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BusinessQualitySignal {
    pub metric: String,
    pub score: QualityScoreBps,
    pub evidence: String,
}

impl BusinessQualitySignal {
    pub fn new(
        metric: impl Into<String>,
        score: QualityScoreBps,
        evidence: impl Into<String>,
    ) -> Self {
        Self {
            metric: metric.into(),
            score,
            evidence: evidence.into(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OutcomeRosterMember {
    pub suggestor_id: SuggestorDescriptorId,
    pub role: SuggestorRole,
    pub capabilities: Vec<SuggestorCapability>,
    pub replay_mode: ReplayMode,
    pub governance_class: GovernanceClass,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OutcomeProviderAssignment {
    pub suggestor_id: SuggestorDescriptorId,
    pub role: SuggestorRole,
    pub provider_id: ProviderId,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FormationOutcomeRecord {
    pub scope: FormationRunScope,
    pub template_id: FormationTemplateId,
    pub template_kind: FormationKind,
    pub roster: Vec<OutcomeRosterMember>,
    pub provider_assignments: Vec<OutcomeProviderAssignment>,
    pub status: FormationOutcomeStatus,
    pub stop_reason: Option<String>,
    pub gate_triggers: Vec<String>,
    pub quality_signal: Option<BusinessQualitySignal>,
    pub writeback_target: Option<String>,
}

impl FormationOutcomeRecord {
    pub fn from_compiled_plan(
        plan: &CompiledFormationPlan,
        status: FormationOutcomeStatus,
    ) -> Self {
        Self {
            scope: FormationRunScope::from_compiled_plan(plan),
            template_id: plan.template_id.clone(),
            template_kind: plan.template_kind,
            roster: plan
                .roster
                .iter()
                .map(|member| OutcomeRosterMember {
                    suggestor_id: member.suggestor_id.clone(),
                    role: member.role,
                    capabilities: member.capabilities.clone(),
                    replay_mode: member.replay_mode,
                    governance_class: member.governance_class,
                })
                .collect(),
            provider_assignments: plan
                .provider_assignments
                .iter()
                .map(|assignment| OutcomeProviderAssignment {
                    suggestor_id: assignment.suggestor_id.clone(),
                    role: assignment.role,
                    provider_id: assignment.provider_id.clone(),
                })
                .collect(),
            status,
            stop_reason: None,
            gate_triggers: Vec::new(),
            quality_signal: None,
            writeback_target: None,
        }
    }

    pub fn with_stop_reason(mut self, stop_reason: impl Into<String>) -> Self {
        self.stop_reason = Some(stop_reason.into());
        self
    }

    pub fn with_gate_trigger(mut self, gate_trigger: impl Into<String>) -> Self {
        self.gate_triggers.push(gate_trigger.into());
        self
    }

    pub fn with_quality_signal(mut self, signal: BusinessQualitySignal) -> Self {
        self.quality_signal = Some(signal);
        self
    }

    pub fn with_writeback_target(mut self, target: impl Into<String>) -> Self {
        self.writeback_target = Some(target.into());
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::compiler::{CompiledSuggestorRole, RoleProviderAssignment};

    fn id(n: u128) -> Uuid {
        Uuid::from_u128(n)
    }

    #[test]
    fn quality_score_rejects_values_above_100_percent() {
        assert_eq!(QualityScoreBps::new(10_000).unwrap().as_u16(), 10_000);
        assert!(matches!(
            QualityScoreBps::new(10_001),
            Err(QualityScoreError::OutOfRange { value: 10_001 })
        ));
    }

    #[test]
    fn outcome_record_copies_compiled_plan_context() {
        let plan = CompiledFormationPlan {
            plan_id: id(1),
            correlation_id: id(2),
            tenant_id: Some("tenant-a".to_string()),
            template_id: "vendor-selection-decide".into(),
            template_kind: FormationKind::Static,
            roster: vec![CompiledSuggestorRole {
                suggestor_id: "decision-synthesis".into(),
                role: SuggestorRole::Synthesis,
                capabilities: vec![SuggestorCapability::LlmReasoning],
                reads: Vec::new(),
                writes: Vec::new(),
                input_contracts: Vec::new(),
                output_contracts: Vec::new(),
                replay_mode: ReplayMode::Preferred,
                governance_class: GovernanceClass::RegulatedDecision,
            }],
            provider_assignments: vec![RoleProviderAssignment {
                suggestor_id: "decision-synthesis".into(),
                role: SuggestorRole::Synthesis,
                provider_id: "reasoning-llm".into(),
                requirements: converge_provider::BackendRequirements::reasoning_llm(),
            }],
            trace: Vec::new(),
            decisions: Vec::new(),
        };

        let record =
            FormationOutcomeRecord::from_compiled_plan(&plan, FormationOutcomeStatus::NeedsReview)
                .with_stop_reason("DPO approval required")
                .with_gate_trigger("dpo-gap-acceptance")
                .with_quality_signal(BusinessQualitySignal::new(
                    "audit_completeness",
                    QualityScoreBps::new(9_200).unwrap(),
                    "all score cells linked to source evidence",
                ))
                .with_writeback_target("decision://vendor-selection/demo");

        assert_eq!(record.scope.correlation_id, id(2));
        assert_eq!(record.scope.tenant_id.as_deref(), Some("tenant-a"));
        assert_eq!(record.roster[0].suggestor_id, "decision-synthesis");
        assert_eq!(record.provider_assignments[0].provider_id, "reasoning-llm");
        assert_eq!(record.gate_triggers, vec!["dpo-gap-acceptance"]);
        assert_eq!(
            record
                .quality_signal
                .as_ref()
                .map(|signal| signal.score.as_u16()),
            Some(9_200)
        );
    }
}