Skip to main content

lean_ctx/core/execution_ledger/
migration.rs

1//! One-way migration helpers for legacy Savings Ledger observations.
2//!
3//! Migration only reads the source ledger.  The source file is never opened for
4//! writing and its signed/hash-chained bytes are not rewritten or re-chained.
5
6use std::path::Path;
7
8use super::Result;
9use super::event::ExecutionEvent;
10use super::store::ExecutionLedgerStore;
11
12const LEGACY_TASK_ID: &str = "legacy-savings-ledger";
13const LEGACY_TRACE_ID: &str = "legacy-savings-ledger";
14
15/// Migrates the configured Savings Ledger into the configured execution ledger.
16///
17/// Legacy savings entries did not always have task identity.  Such entries are
18/// attached to the stable compatibility task/trace IDs above. The original
19/// entry hash is carried in the generated invocation identity and reference.
20pub fn migrate_from_savings_ledger() -> Result<usize> {
21    let Some(source_path) = crate::core::savings_ledger::store::default_path() else {
22        return Ok(0);
23    };
24    let destination = ExecutionLedgerStore::from_default()?;
25    migrate_from_savings_ledger_at(&source_path, &destination, LEGACY_TASK_ID, LEGACY_TRACE_ID)
26}
27
28/// Migrates one source file into an explicitly selected destination store.
29///
30/// The return value is the number of Savings Ledger entries converted, not the
31/// number of execution events appended (each source entry produces one
32/// model-observation event under the stable compatibility plan).
33pub fn migrate_from_savings_ledger_at(
34    source_path: &Path,
35    destination: &ExecutionLedgerStore,
36    task_id: &str,
37    trace_id: &str,
38) -> Result<usize> {
39    let source_events = crate::core::savings_ledger::store::load(source_path);
40    if source_events.is_empty() {
41        return Ok(0);
42    }
43    destination.append(ExecutionEvent::TaskStarted {
44        task_id: task_id.to_owned(),
45        trace_id: trace_id.to_owned(),
46        envelope_ref: "legacy:savings-ledger".to_owned(),
47        timestamp: "1970-01-01T00:00:00Z".to_owned(),
48        sequence_number: 0,
49        prev_hash: String::new(),
50    })?;
51    const LEGACY_PLAN_ID: &str = "legacy-savings-plan";
52    destination.append_if_new(ExecutionEvent::PlanCreated {
53        task_id: task_id.to_owned(),
54        trace_id: trace_id.to_owned(),
55        plan_id: LEGACY_PLAN_ID.to_owned(),
56        plan_ref: "legacy:savings-ledger".to_owned(),
57        timestamp: "1970-01-01T00:00:00Z".to_owned(),
58        sequence_number: 0,
59        prev_hash: String::new(),
60    })?;
61    let mut migrated = 0;
62
63    for source in &source_events {
64        let model_added = destination.append_if_new(ExecutionEvent::ModelInvoked {
65            task_id: task_id.to_owned(),
66            trace_id: trace_id.to_owned(),
67            plan_id: LEGACY_PLAN_ID.to_owned(),
68            invocation_id: format!("legacy-savings-invocation:{}", source.entry_hash),
69            invocation_ref: format!("savings-ledger:sha256:{}", source.entry_hash),
70            model: source.model_id.clone(),
71            provider: "savings-ledger".to_owned(),
72            tokens_in: source.actual_tokens,
73            tokens_out: source.response_delivered_tokens.unwrap_or(0),
74            latency_ms: 0,
75            timestamp: source.ts.clone(),
76            sequence_number: 0,
77            prev_hash: String::new(),
78        })?;
79        migrated += usize::from(model_added);
80    }
81
82    Ok(migrated)
83}