lenso-service 0.1.24

Public contracts for Lenso Providers and Autonomous Services.
Documentation
use std::collections::{BTreeMap, BTreeSet};

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use utoipa::ToSchema;

use crate::extraction_input_digest;

pub const SUPPORT_ENVELOPE_PROTOCOL: &str = "lenso.support-envelope.v1";

#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum SupportEnvelopeDecision {
    Passed,
    Blocked,
}

#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum SupportEnvelopeIssueCode {
    ScalePointMissing,
    TopologyInvalid,
    MeasurementIncomplete,
    BudgetExceeded,
    EnvironmentDrift,
    SaturationUnknown,
    HiddenCentralDependency,
    CleanupIncomplete,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SupportEnvelopeIssue {
    pub code: SupportEnvelopeIssueCode,
    pub message: String,
    pub remediation: String,
    pub next_actions: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SupportScalePoint {
    pub service_count: u32,
    pub workload_count: u32,
    pub store_count: u32,
    pub contract_count: u32,
    pub workflow_count: u32,
    pub tenant_count: u32,
    pub topology_digest: String,
    pub environment_digest: String,
    pub compatible_baseline_digest: String,
    pub environment_verification: bool,
    pub environment_drift_detected: bool,
    pub measurement_digests: BTreeMap<String, String>,
    pub budgets_passed: bool,
    pub repeated_run_count: u32,
    pub variance_basis_points: u32,
    pub system_plane_data_plane_requests: u64,
    pub runtime_console_data_plane_requests: u64,
    pub telemetry_data_plane_requests: u64,
    pub policy_data_plane_requests: u64,
    pub registry_data_plane_requests: u64,
    pub saturation_signal: String,
    pub bottlenecks: Vec<String>,
    pub cleanup_complete: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SupportEnvelopeInput {
    pub support_manifest_digest: String,
    pub adapter_versions: BTreeMap<String, String>,
    pub points: Vec<SupportScalePoint>,
    pub recommended_service_limit: u32,
    pub variance_tolerance_basis_points: u32,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SupportEnvelope {
    pub protocol: String,
    pub envelope_id: String,
    pub envelope_digest: String,
    pub support_manifest_digest: String,
    pub adapter_versions: BTreeMap<String, String>,
    pub points: Vec<SupportScalePoint>,
    pub recommended_service_limit: u32,
    pub decision: SupportEnvelopeDecision,
    pub issues: Vec<SupportEnvelopeIssue>,
    pub next_actions: Vec<String>,
}

#[must_use]
pub fn evaluate_support_envelope(mut input: SupportEnvelopeInput) -> SupportEnvelope {
    input.points.sort_by_key(|point| point.service_count);
    let mut issues = Vec::new();
    let counts = input
        .points
        .iter()
        .map(|point| point.service_count)
        .collect::<BTreeSet<_>>();
    if counts != BTreeSet::from([3, 10, 20]) {
        issues.push(issue(
            SupportEnvelopeIssueCode::ScalePointMissing,
            "Support evidence must include the three-, ten-, and twenty-Service scale points.",
            "Run the same pinned profile at all declared support points.",
            "Collect the missing scale point before publishing the envelope.",
        ));
    }
    if input.recommended_service_limit != 20
        || !valid_digest(&input.support_manifest_digest)
        || input.adapter_versions.is_empty()
        || input.variance_tolerance_basis_points == 0
    {
        issues.push(issue(
            SupportEnvelopeIssueCode::TopologyInvalid,
            "Support envelope metadata does not describe the bounded 3–20 Service product scope.",
            "Bind the envelope to exact adapters and the reviewed M6 limit.",
            "Correct the envelope metadata.",
        ));
    }
    for point in &input.points {
        if point.workload_count < point.service_count
            || point.store_count != point.service_count
            || point.contract_count < point.service_count
            || point.workflow_count == 0
            || point.tenant_count == 0
            || !valid_digest(&point.topology_digest)
            || !valid_digest(&point.environment_digest)
            || !valid_digest(&point.compatible_baseline_digest)
            || !point.environment_verification
        {
            issues.push(issue(
                SupportEnvelopeIssueCode::TopologyInvalid,
                format!(
                    "The {}-Service point does not represent distinct Service workloads and Stores.",
                    point.service_count
                ),
                "Scale logical Services, background work, tenants, Contracts, and Workflows together.",
                "Correct the scale fixture and repeat the profile.",
            ));
        }
        let required_measurements = [
            "startup",
            "rollout",
            "direct_calls",
            "events",
            "inbox_outbox",
            "workflows",
            "timers",
            "compensation",
            "story_federation",
            "policy",
            "console",
            "failure_recovery",
            "connections",
            "backlog",
            "resources",
            "evidence_freshness",
        ];
        if required_measurements
            .iter()
            .any(|name| !point.measurement_digests.contains_key(*name))
            || point
                .measurement_digests
                .values()
                .any(|digest| !valid_digest(digest))
            || point.repeated_run_count < 3
            || point.variance_basis_points > input.variance_tolerance_basis_points
        {
            issues.push(issue(
                SupportEnvelopeIssueCode::MeasurementIncomplete,
                format!(
                    "The {}-Service point lacks complete repeated evidence.",
                    point.service_count
                ),
                "Record load, resource, failure, recovery, and convergence evidence with variance.",
                "Repeat the pinned environment runs.",
            ));
        }
        if point.environment_drift_detected {
            issues.push(issue(
                SupportEnvelopeIssueCode::EnvironmentDrift,
                format!(
                    "The {}-Service point drifted from its compatible pinned baseline.",
                    point.service_count
                ),
                "Report infrastructure drift separately from product budget regressions.",
                "Restore the pinned environment and repeat the profile.",
            ));
        }
        if !point.budgets_passed {
            issues.push(issue(
                SupportEnvelopeIssueCode::BudgetExceeded,
                format!(
                    "The {}-Service point exceeded its reviewed budget.",
                    point.service_count
                ),
                "Keep the result environment-specific and diagnose the limiting resource.",
                "Correct the bottleneck or lower the reviewed support limit.",
            ));
        }
        if point.saturation_signal.trim().is_empty() || point.bottlenecks.is_empty() {
            issues.push(issue(
                SupportEnvelopeIssueCode::SaturationUnknown,
                format!(
                    "The {}-Service point does not identify saturation or bottlenecks.",
                    point.service_count
                ),
                "State which resource limits the point and how it is observed.",
                "Add the evidence-backed saturation signal.",
            ));
        }
        if point.system_plane_data_plane_requests > 0
            || point.runtime_console_data_plane_requests > 0
            || point.telemetry_data_plane_requests > 0
            || point.policy_data_plane_requests > 0
            || point.registry_data_plane_requests > 0
        {
            issues.push(issue(
                SupportEnvelopeIssueCode::HiddenCentralDependency,
                "A scale point depends on a central coordination or observability service for established traffic.",
                "Keep those surfaces outside the Data Plane after convergence.",
                "Withhold the central surfaces and repeat the scale point.",
            ));
        }
        if !point.cleanup_complete {
            issues.push(issue(
                SupportEnvelopeIssueCode::CleanupIncomplete,
                "Scale-point cleanup is incomplete.",
                "Remove or isolate disposable Stores, streams, identities, and Workloads.",
                "Finish cleanup before accepting the envelope.",
            ));
        }
    }
    let decision = if issues.is_empty() {
        SupportEnvelopeDecision::Passed
    } else {
        SupportEnvelopeDecision::Blocked
    };
    let next_actions = if issues.is_empty() {
        vec!["Publish the observed 3–20 Service envelope as a bounded GA claim.".into()]
    } else {
        issues
            .iter()
            .flat_map(|issue| issue.next_actions.iter().cloned())
            .collect()
    };
    let mut envelope = SupportEnvelope {
        protocol: SUPPORT_ENVELOPE_PROTOCOL.to_owned(),
        envelope_id: String::new(),
        envelope_digest: String::new(),
        support_manifest_digest: input.support_manifest_digest,
        adapter_versions: input.adapter_versions,
        points: input.points,
        recommended_service_limit: input.recommended_service_limit,
        decision,
        issues,
        next_actions,
    };
    envelope.envelope_digest = digest_without_identity(&envelope);
    envelope.envelope_id = format!("support-envelope:{}", &envelope.envelope_digest[7..23]);
    envelope
}

#[must_use]
pub fn support_envelope_schema() -> Value {
    let mut schema = serde_json::to_value(schemars::schema_for!(SupportEnvelope))
        .expect("support envelope schema serializes");
    schema["$id"] = Value::String(
        "https://contracts.lenso.local/ga/lenso.support-envelope.v1.schema.json".to_owned(),
    );
    schema
}

fn issue(
    code: SupportEnvelopeIssueCode,
    message: impl Into<String>,
    remediation: impl Into<String>,
    next_action: impl Into<String>,
) -> SupportEnvelopeIssue {
    SupportEnvelopeIssue {
        code,
        message: message.into(),
        remediation: remediation.into(),
        next_actions: vec![next_action.into()],
    }
}

fn valid_digest(value: &str) -> bool {
    value.strip_prefix("sha256:").is_some_and(|digest| {
        digest.len() == 64 && digest.bytes().all(|byte| byte.is_ascii_hexdigit())
    })
}

fn digest_without_identity(envelope: &SupportEnvelope) -> String {
    let mut canonical = envelope.clone();
    canonical.envelope_id.clear();
    canonical.envelope_digest.clear();
    extraction_input_digest(&serde_json::to_vec(&canonical).expect("support envelope serializes"))
}

#[must_use]
pub fn support_envelope_integrity_is_valid(envelope: &SupportEnvelope) -> bool {
    valid_digest(&envelope.envelope_digest)
        && envelope.envelope_digest == digest_without_identity(envelope)
        && envelope.envelope_id == format!("support-envelope:{}", &envelope.envelope_digest[7..23])
}