use serde_json::Value;
use crate::core::StoreError;
pub trait Upcaster: Send + Sync + std::fmt::Debug {
fn current_version(&self, kind: &str) -> u16;
fn upcast(&self, kind: &str, version: u16, payload: Value) -> Result<Value, StoreError>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Identity;
impl Upcaster for Identity {
fn current_version(&self, _kind: &str) -> u16 {
4
}
fn upcast(&self, kind: &str, version: u16, payload: Value) -> Result<Value, StoreError> {
match version {
4 => Ok(payload),
1..=3 => Err(StoreError::Corrupt {
seq: 0,
detail: format!(
"record {kind} is v{version}: this journal predates the current format. \
Every one of these changes is dangerous precisely because the older \
record still *parses*. v1→v2: `RunAdmitted.policy` became \
`policy_bundle` and `Declassified` became `Released`, so the policy \
digest goes silently absent and a resumed run reports that no policy \
governed it. v3→v4: `RunAdmitted.agent` became `capability` and gained \
`governed_by`, so a v3 record names a capability in a field about \
identity and reports every run as ungoverned. The whole journal is \
refused rather than the affected kinds, because a journal containing \
one contains the others. No lift is offered: the old records do not \
carry the identity the new shape requires, and inventing one would \
fabricate provenance. Start a fresh journal — the project is \
pre-release and the cut is deliberate"
),
}),
_ => Err(StoreError::Corrupt {
seq: 0,
detail: format!(
"record {kind} v{version} is newer than this build understands (v4) — \
readers must be deployed before writers"
),
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn a_pre_cut_journal_is_refused_rather_than_misread() {
let err = Identity
.upcast("RunAdmitted", 1, json!({ "agent": "a", "input": null }))
.unwrap_err();
let text = err.to_string();
assert!(
text.contains("policy_bundle") && text.contains("fresh journal"),
"the refusal must name what changed and what to do about it: {text}"
);
}
#[test]
fn future_versions_are_refused_not_guessed() {
let err = Identity.upcast("EffectDone", 7, json!({})).unwrap_err();
assert!(matches!(err, StoreError::Corrupt { .. }));
}
#[test]
fn current_version_passes_through() {
let v = Identity.current_version("EffectDone");
assert_eq!(
Identity.upcast("EffectDone", v, json!({"a": 1})).unwrap(),
json!({"a": 1})
);
}
#[test]
fn upcasting_is_pure() {
let v = Identity.current_version("StepStarted");
let a = Identity
.upcast("StepStarted", v, json!({"skill": "x"}))
.unwrap();
let b = Identity
.upcast("StepStarted", v, json!({"skill": "x"}))
.unwrap();
assert_eq!(a, b);
}
}