byteor_runtime/
compatibility.rs

1use std::collections::BTreeMap;
2use std::path::Path;
3
4use byteor_app_kit::approval::{
5    approval_spec_hash_from_canonical_spec_kv, required_approval_capabilities_for_spec_kv,
6};
7use byteor_app_kit::capabilities::{
8    ExecutionTarget, PolicyClassId, ReplaySupport, RuntimeCapabilityDocument, RuntimeEnvironment,
9    StageCatalogEntry, StageKeyKind,
10};
11use byteor_core::config::Environment;
12use serde::Serialize;
13
14use crate::{canonical_encode_spec_kv_v1, default_runtime_capability_document, read_spec_kv_v1};
15
16/// Schema version for runtime compatibility reports.
17pub(crate) const COMPATIBILITY_REPORT_VERSION: u32 = 1;
18
19/// How a stage requirement was satisfied in the report.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
21#[serde(rename_all = "snake_case")]
22pub enum StageRequirementSource {
23    /// Supported as an intrinsic built-in stage op.
24    BuiltIn,
25    /// Matched a stable catalog key exactly.
26    CatalogStable,
27    /// Matched a parameterized catalog family.
28    CatalogPrefix,
29    /// No supporting catalog entry was found.
30    Unknown,
31}
32
33/// One stage-related compatibility requirement derived from a spec.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
35#[serde(rename_all = "camelCase")]
36pub struct StageCompatibilityRequirement {
37    /// Stable stage identifier or family-expanded stage key.
38    pub key: String,
39    /// How the requirement was satisfied.
40    pub source: StageRequirementSource,
41    /// Whether the runtime can execute this stage requirement.
42    pub supported: bool,
43    /// Policy class, when known.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub policy_class: Option<PolicyClassId>,
46    /// Replay posture for this stage requirement.
47    pub replay_support: ReplaySupport,
48    /// Approval capability ids implied by this stage requirement.
49    #[serde(default, skip_serializing_if = "Vec::is_empty")]
50    pub approval_capabilities: Vec<String>,
51}
52
53/// Machine-readable compatibility report for a authored spec against a runtime surface.
54#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
55#[serde(rename_all = "camelCase")]
56pub struct CompatibilityReport {
57    /// Report schema version.
58    pub schema_version: u32,
59    /// Runtime product identity that produced the report.
60    pub product: String,
61    /// Requested environment posture.
62    pub environment: RuntimeEnvironment,
63    /// Canonical spec hash.
64    pub spec_hash: String,
65    /// Lowered execution target.
66    pub target: ExecutionTarget,
67    /// Whether the spec is executable on this runtime surface.
68    pub compatible: bool,
69    /// Whether deploy-time approval is required in the requested environment.
70    pub requires_approval: bool,
71    /// Approval capabilities required by the authored spec.
72    #[serde(default, skip_serializing_if = "Vec::is_empty")]
73    pub required_approval_capabilities: Vec<String>,
74    /// Effective replay posture after considering all stage requirements.
75    pub replay_support: ReplaySupport,
76    /// Stage-level requirements derived from the spec.
77    pub stage_requirements: Vec<StageCompatibilityRequirement>,
78    /// Stage keys the runtime does not recognize.
79    #[serde(default, skip_serializing_if = "Vec::is_empty")]
80    pub unknown_stage_keys: Vec<String>,
81    /// Model-specific run modes available for the target.
82    pub supported_run_modes: Vec<String>,
83    /// Human-readable reasons for incompatibility or deployment blockers.
84    #[serde(default, skip_serializing_if = "Vec::is_empty")]
85    pub reasons: Vec<String>,
86}
87
88/// Build a compatibility report for a spec against the default runtime capability surface.
89pub fn compatibility_report_for_spec(
90    spec_path: &Path,
91    env: Environment,
92) -> Result<CompatibilityReport, String> {
93    let spec = read_spec_kv_v1(spec_path)?;
94    let canonical_spec_kv = canonical_encode_spec_kv_v1(spec_path)?;
95    Ok(compatibility_report_for_document(
96        &default_runtime_capability_document("byteor-runtime"),
97        &spec,
98        &canonical_spec_kv,
99        env,
100    ))
101}
102
103/// Build a compatibility report for a spec against a supplied capability document.
104pub fn compatibility_report_for_document(
105    document: &RuntimeCapabilityDocument,
106    spec: &byteor_pipeline_spec::PipelineSpecV1,
107    canonical_spec_kv: &str,
108    env: Environment,
109) -> CompatibilityReport {
110    let target = match spec {
111        byteor_pipeline_spec::PipelineSpecV1::SingleRing(_) => ExecutionTarget::SingleRing,
112        byteor_pipeline_spec::PipelineSpecV1::LaneGraph(_) => ExecutionTarget::LaneGraph,
113    };
114    let environment = runtime_environment(env);
115    let required_approval_capabilities = required_approval_capabilities_for_spec_kv(canonical_spec_kv);
116    let requires_approval = !required_approval_capabilities.is_empty()
117        && matches!(environment, RuntimeEnvironment::Staging | RuntimeEnvironment::Prod);
118
119    let mut requirements = collect_stage_requirements(document, spec);
120    requirements.sort_by(|left, right| left.key.cmp(&right.key));
121
122    let unknown_stage_keys: Vec<String> = requirements
123        .iter()
124        .filter(|requirement| !requirement.supported)
125        .map(|requirement| requirement.key.clone())
126        .collect();
127
128    let mut reasons = Vec::new();
129    if !document.supported_targets.contains(&target) {
130        reasons.push(format!(
131            "target {:?} is not supported by {}",
132            target, document.product
133        ));
134    }
135    if !unknown_stage_keys.is_empty() {
136        reasons.push(format!(
137            "unknown stage keys: {}",
138            unknown_stage_keys.join(", ")
139        ));
140    }
141    if requires_approval {
142        reasons.push(format!(
143            "approval required in {} for capabilities: {}",
144            environment_label(environment),
145            required_approval_capabilities.join(", ")
146        ));
147    }
148
149    let replay_support = requirements.iter().fold(ReplaySupport::Allowed, |current, requirement| {
150        max_replay_support(current, requirement.replay_support)
151    });
152
153    CompatibilityReport {
154        schema_version: COMPATIBILITY_REPORT_VERSION,
155        product: document.product.clone(),
156        environment,
157        spec_hash: approval_spec_hash_from_canonical_spec_kv(canonical_spec_kv),
158        target,
159        compatible: document.supported_targets.contains(&target) && unknown_stage_keys.is_empty(),
160        requires_approval,
161        required_approval_capabilities,
162        replay_support,
163        stage_requirements: requirements,
164        unknown_stage_keys,
165        supported_run_modes: supported_run_modes(target),
166        reasons,
167    }
168}
169
170fn collect_stage_requirements(
171    document: &RuntimeCapabilityDocument,
172    spec: &byteor_pipeline_spec::PipelineSpecV1,
173) -> Vec<StageCompatibilityRequirement> {
174    let mut requirements = BTreeMap::<String, StageCompatibilityRequirement>::new();
175
176    match spec {
177        byteor_pipeline_spec::PipelineSpecV1::SingleRing(single_ring) => {
178            for stage in &single_ring.stages {
179                match &stage.op {
180                    byteor_pipeline_spec::StageOpV1::Identity => {
181                        requirements.entry("builtin:identity".to_string()).or_insert_with(|| {
182                            builtin_requirement("builtin:identity")
183                        });
184                    }
185                    byteor_pipeline_spec::StageOpV1::AddU8 { .. } => {
186                        requirements.entry("builtin:add_u8".to_string()).or_insert_with(|| {
187                            builtin_requirement("builtin:add_u8")
188                        });
189                    }
190                    byteor_pipeline_spec::StageOpV1::ResolverKey { stage } => {
191                        requirements
192                            .entry(stage.clone())
193                            .or_insert_with(|| catalog_requirement(document, stage));
194                    }
195                }
196            }
197        }
198        byteor_pipeline_spec::PipelineSpecV1::LaneGraph(lane_graph) => {
199            for role in &lane_graph.roles {
200                if let byteor_pipeline_spec::RoleCfgV1::Stage(stage_role) = role {
201                    let stage = &stage_role.stage;
202                    requirements
203                        .entry(stage.clone())
204                        .or_insert_with(|| catalog_requirement(document, stage));
205                }
206            }
207        }
208    }
209
210    requirements.into_values().collect()
211}
212
213fn builtin_requirement(key: &str) -> StageCompatibilityRequirement {
214    StageCompatibilityRequirement {
215        key: key.to_string(),
216        source: StageRequirementSource::BuiltIn,
217        supported: true,
218        policy_class: Some(PolicyClassId::PureTransform),
219        replay_support: ReplaySupport::Allowed,
220        approval_capabilities: Vec::new(),
221    }
222}
223
224fn catalog_requirement(
225    document: &RuntimeCapabilityDocument,
226    stage_key: &str,
227) -> StageCompatibilityRequirement {
228    match match_stage_catalog_entry(document, stage_key) {
229        Some((entry, source)) => StageCompatibilityRequirement {
230            key: stage_key.to_string(),
231            source,
232            supported: true,
233            policy_class: Some(entry.policy_class),
234            replay_support: entry.replay_support,
235            approval_capabilities: entry.approval_capabilities.clone(),
236        },
237        None => StageCompatibilityRequirement {
238            key: stage_key.to_string(),
239            source: StageRequirementSource::Unknown,
240            supported: false,
241            policy_class: None,
242            replay_support: ReplaySupport::Unsupported,
243            approval_capabilities: Vec::new(),
244        },
245    }
246}
247
248fn match_stage_catalog_entry<'a>(
249    document: &'a RuntimeCapabilityDocument,
250    stage_key: &str,
251) -> Option<(&'a StageCatalogEntry, StageRequirementSource)> {
252    document.stage_catalog.iter().find_map(|entry| match entry.key_kind {
253        StageKeyKind::Stable if entry.key == stage_key => {
254            Some((entry, StageRequirementSource::CatalogStable))
255        }
256        StageKeyKind::Prefix => {
257            let prefix = entry.key.trim_end_matches('*');
258            if stage_key.starts_with(prefix) {
259                Some((entry, StageRequirementSource::CatalogPrefix))
260            } else {
261                None
262            }
263        }
264        _ => None,
265    })
266}
267
268fn runtime_environment(env: Environment) -> RuntimeEnvironment {
269    match env {
270        Environment::Dev => RuntimeEnvironment::Dev,
271        Environment::Staging => RuntimeEnvironment::Staging,
272        Environment::Prod => RuntimeEnvironment::Prod,
273    }
274}
275
276fn environment_label(env: RuntimeEnvironment) -> &'static str {
277    match env {
278        RuntimeEnvironment::Dev => "dev",
279        RuntimeEnvironment::Staging => "staging",
280        RuntimeEnvironment::Prod => "prod",
281    }
282}
283
284fn supported_run_modes(target: ExecutionTarget) -> Vec<String> {
285    match target {
286        ExecutionTarget::SingleRing => vec![
287            "shm_supervised".to_string(),
288            "one_shot".to_string(),
289            "adapter_loop".to_string(),
290        ],
291        ExecutionTarget::LaneGraph => vec![
292            "shm_supervised".to_string(),
293            "multi_process".to_string(),
294        ],
295    }
296}
297
298fn max_replay_support(left: ReplaySupport, right: ReplaySupport) -> ReplaySupport {
299    match (left, right) {
300        (ReplaySupport::Unsupported, _) | (_, ReplaySupport::Unsupported) => ReplaySupport::Unsupported,
301        (ReplaySupport::DryRunOnly, _) | (_, ReplaySupport::DryRunOnly) => ReplaySupport::DryRunOnly,
302        _ => ReplaySupport::Allowed,
303    }
304}