//! Immutable orientation values. Durable selection and conflict handling live in `klbr_runtime::Store`.
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::Path;
use crate::digest::sha256_hex;
pub const STEERING_FORM_TEMPLATE: &str =
include_str!("../../python/klbr-runtime/src/klbr/templates/steering-form.md");
pub const STEERING_SITUATE_TEMPLATE: &str =
include_str!("../../python/klbr-runtime/src/klbr/templates/steering-situate.md");
pub const STEERING_ELABORATE_TEMPLATE: &str =
include_str!("../../python/klbr-runtime/src/klbr/templates/steering-elaborate.md");
pub const STEERING_REPAIR_TEMPLATE: &str =
include_str!("../../python/klbr-runtime/src/klbr/templates/steering-repair.md");
pub use klbr_runtime::protocol::{
AdoptionActor, ApplicationBoundary, CarrierAuthor, CarrierMode, CarrierPlacement, CarrierRole,
OrientationScope, TargetSelector,
};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OrientationCandidate {
pub candidate_id: String,
pub text: String,
pub carrier_hash: String,
pub carrier_role: CarrierRole,
pub carrier_placement: CarrierPlacement,
pub scope: OrientationScope,
pub mode: CarrierMode,
pub target: String,
pub target_selector: TargetSelector,
pub carrier_author: CarrierAuthor,
pub parent_revision: Option<String>,
pub source_refs: Vec<String>,
pub staged_at_ms: i64,
}
impl OrientationCandidate {
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn new(
candidate_id: impl Into<String>,
text: impl Into<String>,
scope: OrientationScope,
mode: CarrierMode,
target: impl Into<String>,
target_selector: TargetSelector,
carrier_author: CarrierAuthor,
parent_revision: Option<String>,
source_refs: Vec<String>,
) -> Self {
let text = text.into();
Self {
candidate_id: candidate_id.into(),
carrier_hash: sha256_hex(text.as_bytes()),
text,
carrier_role: CarrierRole::default(),
carrier_placement: CarrierPlacement::default(),
scope,
mode,
target: target.into(),
target_selector,
carrier_author,
parent_revision,
source_refs,
staged_at_ms: chrono::Utc::now().timestamp_millis(),
}
}
#[must_use]
pub fn semantic_payload(&self) -> Value {
let mut value = serde_json::to_value(self).expect("orientation candidate serializes");
value
.as_object_mut()
.expect("candidate is an object")
.remove("staged_at_ms");
value
}
pub fn prepare_adoption(
&self,
active: Option<&OrientationRevision>,
expected_parent: Option<&str>,
adoption_actor: AdoptionActor,
operation_id: &str,
apply_at: ApplicationBoundary,
context_epoch: u64,
) -> Result<(OrientationRevision, AdoptReceipt), OrientationError> {
if self.parent_revision.as_deref() != expected_parent {
return Err(OrientationError::CandidateParentMismatch {
candidate_parent: self.parent_revision.clone(),
expected: expected_parent.map(str::to_owned),
});
}
if self.mode == CarrierMode::Situate && self.scope.is_global() {
return Err(OrientationError::SituatedCannotUpdateGlobal);
}
if self.mode == CarrierMode::Elaborate {
if let Some(active) = active.filter(|r| Some(r.revision_id.as_str()) == expected_parent)
{
if active.target != self.target {
return Err(OrientationError::ElaborationTargetMismatch {
expected: active.target.clone(),
actual: self.target.clone(),
});
}
}
}
if self.mode == CarrierMode::RepairReturn
&& !self.source_refs.iter().any(|r| {
["trace:", "receipt:", "event:"]
.iter()
.any(|p| r.starts_with(p))
})
{
return Err(OrientationError::InsufficientEvidenceTrace);
}
let revision_id = format!("rev-{}", self.candidate_id);
let revision = OrientationRevision {
revision_id: revision_id.clone(),
candidate_id: self.candidate_id.clone(),
text: self.text.clone(),
carrier_hash: self.carrier_hash.clone(),
carrier_role: self.carrier_role,
carrier_placement: self.carrier_placement,
scope: self.scope.clone(),
mode: self.mode,
target: self.target.clone(),
target_selector: self.target_selector.clone(),
carrier_author: self.carrier_author.clone(),
adoption_actor,
parent_revision: expected_parent.map(str::to_owned),
source_refs: self.source_refs.clone(),
operation_id: operation_id.into(),
context_epoch,
adopted_at_ms: chrono::Utc::now().timestamp_millis(),
};
let receipt = AdoptReceipt {
kind: "orientation.adopted".into(),
operation_id: operation_id.into(),
candidate_id: self.candidate_id.clone(),
revision_id: revision_id.clone(),
scope: self.scope.clone(),
status: "committed_pending_request".into(),
apply_at,
carrier_ref: format!("orientation-revision:{revision_id}"),
carrier_hash: self.carrier_hash.clone(),
carrier_role: self.carrier_role,
carrier_placement: self.carrier_placement,
effect_on_behavior: "not_evaluated".into(),
context_epoch,
};
Ok((revision, receipt))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OrientationRevision {
pub revision_id: String,
pub candidate_id: String,
pub text: String,
pub carrier_hash: String,
pub carrier_role: CarrierRole,
pub carrier_placement: CarrierPlacement,
pub scope: OrientationScope,
pub mode: CarrierMode,
pub target: String,
pub target_selector: TargetSelector,
pub carrier_author: CarrierAuthor,
pub adoption_actor: AdoptionActor,
pub parent_revision: Option<String>,
pub source_refs: Vec<String>,
pub operation_id: String,
pub context_epoch: u64,
pub adopted_at_ms: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdoptReceipt {
pub kind: String,
pub operation_id: String,
pub candidate_id: String,
pub revision_id: String,
pub scope: OrientationScope,
pub status: String,
pub apply_at: ApplicationBoundary,
pub carrier_ref: String,
pub carrier_hash: String,
pub carrier_role: CarrierRole,
pub carrier_placement: CarrierPlacement,
pub effect_on_behavior: String,
pub context_epoch: u64,
}
#[derive(Debug, thiserror::Error)]
pub enum OrientationError {
#[error("stale-parent write conflict: expected {expected:?}, active is {actual:?}")]
StaleParentConflict {
expected: Option<String>,
actual: Option<String>,
},
#[error("situated continuation cannot update global orientation")]
SituatedCannotUpdateGlobal,
#[error("elaboration target must remain {expected:?}, got {actual:?}")]
ElaborationTargetMismatch { expected: String, actual: String },
#[error("repair mode requires a trace, receipt, or event source reference")]
InsufficientEvidenceTrace,
#[error("candidate parent {candidate_parent:?} does not match adoption parent {expected:?}")]
CandidateParentMismatch {
candidate_parent: Option<String>,
expected: Option<String>,
},
#[error("unsupported carrier placement: {0}")]
UnsupportedCarrierPlacement(String),
#[error("unsupported carrier role: {0}")]
UnsupportedCarrierRole(String),
#[error("unsupported orientation application boundary: {0}")]
UnsupportedApplicationBoundary(String),
#[error("failed write cannot report adopted or installed: {0}")]
WriteFailed(String),
}
/// Loads an editable release template, falling back to the compiled default.
pub fn load_steering_template(mode: CarrierMode, release_dir: Option<&Path>) -> (String, String) {
let (filename, fallback) = match mode {
CarrierMode::Form => ("steering-form.md", STEERING_FORM_TEMPLATE),
CarrierMode::Situate => ("steering-situate.md", STEERING_SITUATE_TEMPLATE),
CarrierMode::Elaborate => ("steering-elaborate.md", STEERING_ELABORATE_TEMPLATE),
CarrierMode::RepairReturn => ("steering-repair.md", STEERING_REPAIR_TEMPLATE),
};
let text = release_dir
.into_iter()
.flat_map(|root| {
[
root.join(filename),
root.join("templates").join(filename),
root.join("klbr/templates").join(filename),
]
})
.find_map(|path| std::fs::read_to_string(path).ok())
.unwrap_or_else(|| fallback.into());
let hash = sha256_hex(text.as_bytes());
(text, hash)
}