lean-ctx 3.9.18

Context Runtime for AI Agents with CCP. 71 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24+ AI tools. Reduces LLM token consumption by up to 99%.
Documentation
use super::event::{
    CustomerApproval, EvidenceClass, MECHANISM_COMPRESSION, MeasurementMethod, SavingsEvent,
    SettlementStatus, compute_hash,
};
use super::store::{self, GENESIS};
use serde_json::{Value, json};
use std::path::PathBuf;

fn legacy_json() -> Value {
    json!({
        "ts": "2026-06-01T12:00:00+00:00",
        "tool": "ctx_read",
        "model_id": "claude-3.5-sonnet",
        "tokenizer": "o200k_base",
        "baseline_tokens": 1000,
        "actual_tokens": 300,
        "saved_tokens": 700,
        "bounce_adjustment": 0,
        "unit_price_per_m_usd": 3.0,
        "saved_usd": 0.0021,
        "repo_hash": "repo",
        "agent_id": "local",
        "prev_hash": "",
        "entry_hash": ""
    })
}

fn event(value: Value) -> SavingsEvent {
    serde_json::from_value(value).expect("fixture must deserialize")
}

fn assert_legacy_defaults(ev: &SavingsEvent) {
    assert_eq!(ev.mechanism, MECHANISM_COMPRESSION);
    assert!(ev.version.is_empty());
    assert_p5_defaults(ev);
}

fn assert_p5_defaults(ev: &SavingsEvent) {
    assert!(ev.intent_tag.is_none());
    assert!(ev.outcome.is_none());
    assert!(ev.model_original.is_none());
    assert!(ev.model_routed.is_none());
    assert!(ev.routing_savings.is_none());
    assert!(ev.response_original_tokens.is_none());
    assert!(ev.response_delivered_tokens.is_none());
    assert!(ev.agent_chain_id.is_none());
    assert!(ev.chain_depth.is_none());
    assert!(ev.measurement_method.is_none());
    assert!(ev.evidence_class.is_none());
    assert!(ev.confidence.is_none());
    assert!(ev.quality_signal.is_none());
    assert!(ev.attribution_group.is_none());
    assert!(ev.attribution_id.is_none());
    assert!(ev.baseline_ref.is_none());
    assert!(ev.price_version.is_none());
    assert!(ev.customer_approval.is_none());
    assert!(ev.settlement_status.is_none());
}

fn full_event() -> SavingsEvent {
    let mut value = legacy_json();
    value["mechanism"] = json!("compression");
    value["version"] = json!("5.0.0");
    value["intent_tag"] = json!("code_generation");
    value["outcome"] = json!("used");
    value["model_original"] = json!("claude-opus-4.5");
    value["model_routed"] = json!("claude-sonnet-4");
    value["routing_savings"] = json!(42);
    value["response_original_tokens"] = json!(800);
    value["response_delivered_tokens"] = json!(500);
    value["agent_chain_id"] = json!("chain-1");
    value["chain_depth"] = json!(2);
    value["measurement_method"] = json!("direct_count");
    value["evidence_class"] = json!("measured");
    value["confidence"] = json!(0.99);
    value["quality_signal"] = json!("accepted");
    value["attribution_group"] = json!("group-a");
    value["attribution_id"] = json!("blake3-scope");
    value["baseline_ref"] = json!("baseline-1");
    value["price_version"] = json!("pricing-2026-06");
    value["customer_approval"] = json!("approved");
    value["settlement_status"] = json!("settled");
    event(value)
}

fn linked(mut ev: SavingsEvent, prev: &str, version: u8) -> SavingsEvent {
    ev.prev_hash = prev.to_string();
    let content = match version {
        1 => ev.canonical_content_legacy(),
        3 => ev.canonical_content_v3(),
        4 => ev.canonical_content_v4(),
        5 => ev.canonical_content(),
        _ => unreachable!("test fixture version"),
    };
    ev.entry_hash = compute_hash(prev, &content);
    ev
}

fn temp_path(tag: &str) -> PathBuf {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_or(0, |duration| duration.as_nanos());
    std::env::temp_dir().join(format!("lean-ctx-migration-{tag}-{nanos}.jsonl"))
}

#[test]
fn v1_ledger_still_loads() {
    let ev = event(legacy_json());
    assert_legacy_defaults(&ev);
}

#[test]
fn v2_ledger_still_loads() {
    let mut value = legacy_json();
    value["mechanism"] = json!("compression");
    let ev = event(value);
    assert_legacy_defaults(&ev);
}

#[test]
fn v3_ledger_still_loads() {
    let mut value = legacy_json();
    value["mechanism"] = json!("routing");
    let ev = event(value);
    assert_eq!(ev.mechanism, "routing");
    assert!(ev.version.is_empty());
    assert_p5_defaults(&ev);
}

#[test]
fn v4_ledger_still_loads() {
    let mut value = legacy_json();
    value["version"] = json!("4.0.0");
    let ev = event(value);
    assert_eq!(ev.version, "4.0.0");
    assert_p5_defaults(&ev);
}

#[test]
fn v5_ledger_includes_p5() {
    let parsed: SavingsEvent =
        serde_json::from_str(&serde_json::to_string(&full_event()).expect("serialize full event"))
            .expect("roundtrip full event");
    assert_eq!(parsed, full_event());
    assert_eq!(
        parsed.measurement_method,
        Some(MeasurementMethod::DirectCount)
    );
    assert_eq!(parsed.evidence_class, Some(EvidenceClass::Measured));
    assert_eq!(parsed.customer_approval, Some(CustomerApproval::Approved));
    assert_eq!(parsed.settlement_status, Some(SettlementStatus::Settled));
}

#[test]
fn mixed_version_chain_verifies() {
    let v1 = linked(event(legacy_json()), GENESIS, 1);
    let v3 = linked(
        event({
            let mut value = legacy_json();
            value["mechanism"] = json!("routing");
            value
        }),
        &v1.entry_hash,
        3,
    );
    let v4 = linked(
        event({
            let mut value = legacy_json();
            value["version"] = json!("4.0.0");
            value
        }),
        &v3.entry_hash,
        4,
    );
    let v5 = linked(full_event(), &v4.entry_hash, 5);

    assert!(v1.hash_matches(GENESIS));
    assert!(v3.hash_matches(&v1.entry_hash));
    assert!(v4.hash_matches(&v3.entry_hash));
    assert!(v5.hash_matches(&v4.entry_hash));
}

#[test]
fn p5_fields_omitted_in_json_when_none() {
    let json = serde_json::to_string(&event(legacy_json())).expect("serialize legacy event");
    for field in [
        "intent_tag",
        "outcome",
        "model_original",
        "model_routed",
        "routing_savings",
        "response_original_tokens",
        "response_delivered_tokens",
        "agent_chain_id",
        "chain_depth",
        "measurement_method",
        "evidence_class",
        "confidence",
        "quality_signal",
        "attribution_group",
        "attribution_id",
        "baseline_ref",
        "price_version",
        "customer_approval",
        "settlement_status",
    ] {
        assert!(
            !json.contains(&format!("\"{field}\"")),
            "field {field} present"
        );
    }
}

#[test]
fn rechain_preserves_p5_fields() {
    let path = temp_path("rechain");
    store::append(&path, full_event()).expect("append first event");
    let mut second = full_event();
    second.intent_tag = Some("summarization".into());
    second.attribution_id = Some("scope-2".into());
    store::append(&path, second).expect("append second event");
    let before = store::load(&path);

    assert_eq!(store::rechain(&path).expect("rechain ledger"), 2);
    let after = store::load(&path);
    assert_eq!(after.len(), before.len());
    for (old, new) in before.iter().zip(after.iter()) {
        assert_eq!(new.intent_tag, old.intent_tag);
        assert_eq!(new.measurement_method, old.measurement_method);
        assert_eq!(new.evidence_class, old.evidence_class);
        assert_eq!(new.attribution_id, old.attribution_id);
        assert_eq!(new.customer_approval, old.customer_approval);
        assert_eq!(new.settlement_status, old.settlement_status);
    }
    assert!(store::verify(&path).valid);
    let _ = std::fs::remove_file(path);
}