lean-ctx-protocol 0.5.0

Open wire contracts for lean-ctx: events, observations, assignments, evidence gaps.
Documentation
// SPDX-License-Identifier: Apache-2.0

use super::*;
use crate::{
    AgentId, ContextSessionPhaseV1, ContextSessionRecoveryStateV1, RunId, SessionId, TraceId,
};
use serde_json::json;

const CHECKPOINT_ID: &str = "11111111-2222-4333-8444-555555555555";
const PARENT_ID: &str = "22222222-3333-4444-8555-666666666666";
const WORKSPACE_ID: &str = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee";

/// Canonical JSON produced by [`fixture`]; any drift is a wire break.
const GOLDEN_JSON: &str = r#"{"carrier":{"carrier_schema_id":"leanctx.context-checkpoint/v2","envelope_digest":"sha256:8888888888888888888888888888888888888888888888888888888888888888","logical_state_schema_id":"leanctx.workspace.state/v1","state_digest":"sha256:7777777777777777777777777777777777777777777777777777777777777777","workspace_id":"aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"},"created_at":"2026-09-06T12:00:00Z","engine_version":"4.0.0","identity":{"branch_id":"main","checkpoint_id":"11111111-2222-4333-8444-555555555555","device_id":"device-a","device_sequence":7,"parent_checkpoint_id":"22222222-3333-4444-8555-666666666666"},"lineage":{"context_ir_digest":"sha256:1111111111111111111111111111111111111111111111111111111111111111","evidence_refs":["evidence:alpha","evidence:beta"],"gotcha_refs":["gotcha:alpha"],"hosted_index_digest":"sha256:2222222222222222222222222222222222222222222222222222222222222222","knowledge_refs":["knowledge:alpha"],"plan_id":"plan-a","project_id":"project-a","receipt_ids":["receipt-a","receipt-b"],"snapshot_refs":["snapshot:alpha"],"task_id":"task-a","workspace_id":"aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"},"live_state":{"decisions":[{"decision_id":"decision-a","evidence_refs":["evidence:alpha"],"rationale":"Prevents replay across projects","statement":"Bind lineage into the checkpoint digest","status":"accepted"},{"decision_id":"decision-b","evidence_refs":[],"rationale":"Fail closed rather than filter later","statement":"Reject machine-local material at construction","status":"proposed"}],"files":[{"content_digest":"sha256:3333333333333333333333333333333333333333333333333333333333333333","role":"modified","source_id":"source-a"},{"content_digest":"sha256:4444444444444444444444444444444444444444444444444444444444444444","role":"reference","source_id":"source-b"}],"findings":["Carrier verifier pins seven logical-state keys"],"handoff_summary":"Typed domain complete; projections remain downstream","next_steps":["Run the deterministic gates","Write the report"],"package_pins":[{"package_digest":"sha256:6666666666666666666666666666666666666666666666666666666666666666","package_id":"package-a","version":"1.2.3"}],"policy_pins":[{"policy_digest":"sha256:5555555555555555555555555555555555555555555555555555555555555555","policy_id":"policy-a"}],"profile_id":"profile-a","progress":{"completed_steps":3,"confidence_milliunits":820,"summary":"Domain types landed; gates pending","total_steps":8},"schema_version":1,"task":{"plan_id":"plan-a","status":"in_progress","task_id":"task-a","title":"Converge the checkpoint domain"}},"schema_version":1}"#;

/// Domain-separated digests over [`GOLDEN_JSON`] and its parts.
///
/// These were reproduced by an independent SHA-256 implementation over the
/// same domain prefixes, so they pin the wire form and not just this code.
const GOLDEN_CHECKPOINT_DIGEST: &str =
    "sha256:c4678a9e1d053b6db66f6c75e5a40bb3bea2b085a2d3345b56180d819b9e9dec";
const GOLDEN_IDENTITY_DIGEST: &str =
    "sha256:c333698fa48bcf4056498c7029967732265ff7e2431bb7dc2a17b072fc94b122";
const GOLDEN_LINEAGE_DIGEST: &str =
    "sha256:c81bf99b0589aaceb301a0fe004adc7334447955bef852170ad670ab93cc56a9";
const GOLDEN_LIVE_STATE_DIGEST: &str =
    "sha256:b3d743e997c4f2527b5a2f370f45b8c88fa71ebaf784a6222e0db5b330cd0e55";
const GOLDEN_SIGNING_DIGEST: &str =
    "sha256:4c4d755f301ca9054f112642552fc301eed801cc616898db14c9edab95556edc";

fn digest_of(seed: &str) -> Sha256Digest {
    Sha256Digest::new(format!("sha256:{}", seed.repeat(64 / seed.len()))).expect("valid digest")
}

fn text(value: &str) -> ContextCheckpointTextV1 {
    ContextCheckpointTextV1::new(value).expect("valid text")
}

fn reference(value: &str) -> ProtocolReference {
    ProtocolReference::new(value).expect("valid reference")
}

fn identity() -> ContextCheckpointIdentityV1 {
    ContextCheckpointIdentityV1::try_new(
        ContextCheckpointIdV1::new(CHECKPOINT_ID).expect("valid checkpoint id"),
        Some(ContextCheckpointIdV1::new(PARENT_ID).expect("valid parent id")),
        Some(ContextCheckpointDeviceIdV1::new("device-a").expect("valid parent device")),
        Some(ContextCheckpointBranchIdV1::new("main").expect("valid parent branch")),
        Some(6),
        ContextCheckpointBranchIdV1::new("main").expect("valid branch"),
        ContextCheckpointDeviceIdV1::new("device-a").expect("valid device"),
        7,
    )
    .expect("valid identity")
}

fn lineage() -> ContextCheckpointLineageV1 {
    ContextCheckpointLineageV1 {
        project_id: ProjectId::new("project-a").expect("valid project"),
        workspace_id: WorkspaceId::new(WORKSPACE_ID).expect("valid workspace"),
        tenant_id: TenantId::new("tenant-a").expect("valid tenant"),
        task_id: TaskId::new("task-a").expect("valid task"),
        plan_id: Some(PlanId::new("plan-a").expect("valid plan")),
        receipt_ids: vec![
            ReceiptId::new("receipt-a").expect("valid receipt"),
            ReceiptId::new("receipt-b").expect("valid receipt"),
        ],
        context_ir_digest: Some(digest_of("1")),
        hosted_index_digest: Some(digest_of("2")),
        evidence_refs: vec![reference("evidence:alpha"), reference("evidence:beta")],
        knowledge_refs: vec![reference("knowledge:alpha")],
        gotcha_refs: vec![reference("gotcha:alpha")],
        snapshot_refs: vec![reference("snapshot:alpha")],
    }
}

fn live_state() -> ContextCheckpointLiveStateV1 {
    ContextCheckpointLiveStateV1 {
        schema_version: ContextCheckpointLiveStateV1::SCHEMA_VERSION,
        task: ContextCheckpointTaskV1 {
            task_id: TaskId::new("task-a").expect("valid task"),
            title: text("Converge the checkpoint domain"),
            status: ContextCheckpointTaskStatusV1::InProgress,
            plan_id: Some(PlanId::new("plan-a").expect("valid plan")),
        },
        progress: ContextCheckpointProgressV1 {
            completed_steps: 3,
            total_steps: 8,
            confidence_milliunits: 820,
            summary: text("Domain types landed; gates pending"),
        },
        decisions: vec![
            ContextCheckpointDecisionV1 {
                decision_id: DecisionId::new("decision-a").expect("valid decision"),
                statement: text("Bind lineage into the checkpoint digest"),
                rationale: text("Prevents replay across projects"),
                status: ContextCheckpointDecisionStatusV1::Accepted,
                evidence_refs: vec![reference("evidence:alpha")],
            },
            ContextCheckpointDecisionV1 {
                decision_id: DecisionId::new("decision-b").expect("valid decision"),
                statement: text("Reject machine-local material at construction"),
                rationale: text("Fail closed rather than filter later"),
                status: ContextCheckpointDecisionStatusV1::Proposed,
                evidence_refs: Vec::new(),
            },
        ],
        findings: vec![text("Carrier verifier pins seven logical-state keys")],
        next_steps: vec![
            text("Run the deterministic gates"),
            text("Write the report"),
        ],
        handoff_summary: text("Typed domain complete; projections remain downstream"),
        files: vec![
            ContextCheckpointFileV1 {
                source_id: SourceId::new("source-a").expect("valid source"),
                role: ContextCheckpointFileRoleV1::Modified,
                content_digest: digest_of("3"),
            },
            ContextCheckpointFileV1 {
                source_id: SourceId::new("source-b").expect("valid source"),
                role: ContextCheckpointFileRoleV1::Reference,
                content_digest: digest_of("4"),
            },
        ],
        profile_id: Some(ProfileId::new("profile-a").expect("valid profile")),
        policy_pins: vec![ContextCheckpointPolicyPinV1 {
            policy_id: PolicyId::new("policy-a").expect("valid policy"),
            policy_digest: digest_of("5"),
        }],
        package_pins: vec![ContextCheckpointPackagePinV1 {
            package_id: PackageId::new("package-a").expect("valid package"),
            version: SemanticVersion::new("1.2.3").expect("valid version"),
            package_digest: digest_of("6"),
        }],
        session_state: None,
        learning_state: None,
    }
}

fn carrier() -> ContextCheckpointCarrierBindingV1 {
    ContextCheckpointCarrierBindingV1 {
        carrier_schema_id: CONTEXT_CHECKPOINT_CARRIER_SCHEMA_ID.to_owned(),
        logical_state_schema_id: CONTEXT_CHECKPOINT_CARRIER_LOGICAL_STATE_SCHEMA_ID.to_owned(),
        workspace_id: WorkspaceId::new(WORKSPACE_ID).expect("valid workspace"),
        state_digest: digest_of("7"),
        envelope_digest: digest_of("8"),
    }
}

fn session_state() -> ContextCheckpointSessionStateV1 {
    ContextCheckpointSessionStateV1::try_new(
        SessionIdentityV1 {
            session_id: SessionId::new("session-a").expect("valid session"),
            task_id: TaskId::new("task-a").expect("valid task"),
            run_id: RunId::new("run-a").expect("valid run"),
            trace_id: TraceId::new("trace-a").expect("valid trace"),
            parent_task_id: None,
            agent_id: AgentId::new("agent-a").expect("valid agent"),
            project_id: ProjectId::new("project-a").expect("valid project"),
            workspace_id: Some(WorkspaceId::new(WORKSPACE_ID).expect("valid workspace")),
            tenant_id: Some(TenantId::new("tenant-a").expect("valid tenant")),
            project_root_ref: reference("project:root"),
            project_revision_ref: Some(reference("revision:abc123")),
            created_at: UtcTimestamp::new("2026-09-06T11:00:00Z").expect("valid timestamp"),
        },
        ContextSessionStateV1 {
            phase: ContextSessionPhaseV1::Executing,
            revision: 3,
            next_event_sequence: 4,
            active_plan_id: Some(PlanId::new("plan-a").expect("valid plan")),
            receipt_id: None,
            last_checkpoint_digest: Some(digest_of("a")),
            recovery_state: ContextSessionRecoveryStateV1::Resumable,
            abort_reason: None,
        },
    )
    .expect("valid session state")
}

fn learning_state() -> ContextCheckpointLearningStateV1 {
    ContextCheckpointLearningStateV1::try_new(reference("learning:alpha"), digest_of("9"), 2)
        .expect("valid learning state")
}

fn encryption_metadata() -> ContextCheckpointEncryptionMetadataV1 {
    ContextCheckpointEncryptionMetadataV1::try_new(
        ContextCheckpointEncryptionAlgorithmV1::Aes256Gcm,
        reference("kms:key-a"),
        digest_of("0"),
    )
    .expect("valid encryption metadata")
}

fn fixture() -> ContextCheckpointV1 {
    ContextCheckpointV1::try_new(
        identity(),
        lineage(),
        live_state(),
        Some(carrier()),
        SemanticVersion::new("4.0.0").expect("valid engine version"),
        UtcTimestamp::new("2026-09-06T12:00:00Z").expect("valid timestamp"),
    )
    .expect("fixture must be valid")
}

fn rich_fixture() -> ContextCheckpointV1 {
    fixture()
        .with_portable_state(
            Some(session_state()),
            Some(learning_state()),
            Some(encryption_metadata()),
        )
        .expect("rich fixture must be valid")
}

fn canonical_value(checkpoint: &ContextCheckpointV1) -> Value {
    serde_json::from_slice(&checkpoint.canonical_bytes().expect("canonical bytes"))
        .expect("canonical JSON")
}

fn decode(value: &Value) -> Result<ContextCheckpointV1, ValidationError> {
    let bytes = serde_json::to_vec(&sort_json(value.clone())).expect("encode");
    ContextCheckpointV1::from_canonical_bytes(&bytes)
}

include!("tests_core.rs");
include!("tests_extended.rs");