Skip to main content

lean_ctx_protocol/context_checkpoint/
v2.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use super::*;
4
5/// Canonical typed live-state checkpoint.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct ContextCheckpointV1 {
8    /// Schema version of the checkpoint contract.
9    pub schema_version: u32,
10    /// Parent, branch, and device identity.
11    pub identity: ContextCheckpointIdentityV1,
12    /// Work-graph lineage references.
13    pub lineage: ContextCheckpointLineageV1,
14    /// Portable live state.
15    pub live_state: ContextCheckpointLiveStateV1,
16    /// Binding to a P6 carrier envelope, when the checkpoint travels in one.
17    pub carrier: Option<ContextCheckpointCarrierBindingV1>,
18    /// Engine version that produced the checkpoint.
19    pub engine_version: SemanticVersion,
20    /// Canonical UTC creation timestamp.
21    pub created_at: UtcTimestamp,
22    /// Canonical UTC timestamp of the latest portable semantic update.
23    pub updated_at: UtcTimestamp,
24    /// Metadata for an encrypted downstream projection, when present.
25    pub encryption_metadata: Option<ContextCheckpointEncryptionMetadataV1>,
26}
27
28#[derive(Deserialize)]
29#[serde(deny_unknown_fields)]
30struct ContextCheckpointWireV1 {
31    #[serde(deserialize_with = "deserialize_schema_version")]
32    schema_version: u32,
33    identity: ContextCheckpointIdentityV1,
34    lineage: ContextCheckpointLineageV1,
35    live_state: ContextCheckpointLiveStateV1,
36    carrier: Option<ContextCheckpointCarrierBindingV1>,
37    engine_version: SemanticVersion,
38    created_at: UtcTimestamp,
39    #[serde(default)]
40    updated_at: LegacyRequiredField<UtcTimestamp>,
41    #[serde(default)]
42    encryption_metadata: Option<ContextCheckpointEncryptionMetadataV1>,
43}
44
45#[derive(Default)]
46enum LegacyRequiredField<T> {
47    #[default]
48    Missing,
49    Present(T),
50}
51
52impl<'de, T> Deserialize<'de> for LegacyRequiredField<T>
53where
54    T: Deserialize<'de>,
55{
56    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
57    where
58        D: Deserializer<'de>,
59    {
60        T::deserialize(deserializer).map(Self::Present)
61    }
62}
63
64impl<'de> Deserialize<'de> for ContextCheckpointV1 {
65    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66    where
67        D: Deserializer<'de>,
68    {
69        let wire = ContextCheckpointWireV1::deserialize(deserializer)?;
70        let checkpoint = Self {
71            schema_version: wire.schema_version,
72            identity: wire.identity,
73            lineage: wire.lineage,
74            live_state: wire.live_state,
75            carrier: wire.carrier,
76            engine_version: wire.engine_version,
77            updated_at: match wire.updated_at {
78                LegacyRequiredField::Missing => wire.created_at.clone(),
79                LegacyRequiredField::Present(updated_at) => updated_at,
80            },
81            created_at: wire.created_at,
82            encryption_metadata: wire.encryption_metadata,
83        };
84        checkpoint.validate().map_err(D::Error::custom)?;
85        Ok(checkpoint)
86    }
87}
88
89impl ContextCheckpointV1 {
90    /// Schema version represented by this type.
91    pub const SCHEMA_VERSION: u32 = 1;
92
93    /// Schema identity represented by this type.
94    pub const SCHEMA_ID: &'static str = CONTEXT_CHECKPOINT_SCHEMA_ID;
95
96    /// Construct a validated checkpoint.
97    ///
98    /// This is the only way to obtain a `ContextCheckpointV1` that is known
99    /// valid at construction time; every digest and signing API revalidates
100    /// because the fields remain publicly assignable.
101    pub fn try_new(
102        identity: ContextCheckpointIdentityV1,
103        lineage: ContextCheckpointLineageV1,
104        live_state: ContextCheckpointLiveStateV1,
105        carrier: Option<ContextCheckpointCarrierBindingV1>,
106        engine_version: SemanticVersion,
107        created_at: UtcTimestamp,
108    ) -> Result<Self, ValidationError> {
109        let checkpoint = Self {
110            schema_version: Self::SCHEMA_VERSION,
111            identity,
112            lineage,
113            live_state,
114            carrier,
115            engine_version,
116            updated_at: created_at.clone(),
117            created_at,
118            encryption_metadata: None,
119        };
120        checkpoint.validate()?;
121        Ok(checkpoint)
122    }
123
124    /// Attach optional portable metadata through a fallible API.
125    pub fn with_optional_metadata(
126        mut self,
127        encryption_metadata: Option<ContextCheckpointEncryptionMetadataV1>,
128    ) -> Result<Self, ValidationError> {
129        self.encryption_metadata = encryption_metadata;
130        self.validate()?;
131        Ok(self)
132    }
133
134    /// Set the portable semantic update timestamp.
135    pub fn with_updated_at(mut self, updated_at: UtcTimestamp) -> Result<Self, ValidationError> {
136        self.updated_at = updated_at;
137        self.validate()?;
138        Ok(self)
139    }
140
141    /// Attach all optional portable continuation state through a fallible API.
142    pub fn with_portable_state(
143        mut self,
144        session_state: Option<ContextCheckpointSessionStateV1>,
145        learning_state: Option<ContextCheckpointLearningStateV1>,
146        encryption_metadata: Option<ContextCheckpointEncryptionMetadataV1>,
147    ) -> Result<Self, ValidationError> {
148        self.live_state.session_state = session_state;
149        self.live_state.learning_state = learning_state;
150        self.encryption_metadata = encryption_metadata;
151        self.validate()?;
152        Ok(self)
153    }
154
155    /// Validate every invariant, including the cross-field bindings.
156    pub fn validate(&self) -> Result<(), ValidationError> {
157        validate_schema_version(self.schema_version)?;
158        self.identity.validate()?;
159        self.lineage.validate()?;
160        self.live_state.validate()?;
161        if self.live_state.task.task_id != self.lineage.task_id {
162            return Err(ValidationError::new(
163                "live_state task_id must equal lineage task_id",
164            ));
165        }
166        if self.live_state.task.plan_id != self.lineage.plan_id {
167            return Err(ValidationError::new(
168                "live_state plan_id must equal lineage plan_id",
169            ));
170        }
171        if let Some(carrier) = &self.carrier {
172            carrier.validate()?;
173            if carrier.workspace_id != self.lineage.workspace_id {
174                return Err(ValidationError::new(
175                    "carrier workspace_id must equal lineage workspace_id",
176                ));
177            }
178        }
179        if let Some(encryption_metadata) = &self.encryption_metadata {
180            encryption_metadata.validate()?;
181        }
182        if let Some(session) = &self.live_state.session_state {
183            if session.identity.tenant_id.as_ref() != Some(&self.lineage.tenant_id)
184                || session.identity.project_id != self.lineage.project_id
185                || session.identity.workspace_id.as_ref() != Some(&self.lineage.workspace_id)
186                || session.identity.task_id != self.lineage.task_id
187            {
188                return Err(ValidationError::new(
189                    "session identity must match checkpoint tenant, project, workspace, and task",
190                ));
191            }
192            if self.lineage.plan_id.is_none() {
193                return Err(ValidationError::new(
194                    "session checkpoints require an explicit plan_id",
195                ));
196            }
197            if session.state.active_plan_id != self.lineage.plan_id {
198                return Err(ValidationError::new(
199                    "session active_plan_id must equal checkpoint lineage plan_id",
200                ));
201            }
202        }
203        if self.updated_at < self.created_at {
204            return Err(ValidationError::new(
205                "updated_at must not precede created_at",
206            ));
207        }
208        Ok(())
209    }
210
211    /// Return canonical compact JSON bytes with lexicographically sorted keys.
212    pub fn canonical_bytes(&self) -> Result<Vec<u8>, ValidationError> {
213        self.validate()?;
214        canonical_bytes_of(self, "checkpoint")
215    }
216
217    /// Return the canonical JSON encoding as UTF-8 text.
218    pub fn canonical_json(&self) -> Result<String, ValidationError> {
219        String::from_utf8(self.canonical_bytes()?)
220            .map_err(|error| ValidationError::new(format!("checkpoint canonical UTF-8: {error}")))
221    }
222
223    /// Decode exactly canonical JSON and validate every invariant.
224    pub fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, ValidationError> {
225        if bytes.len() > MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES {
226            return Err(ValidationError::new(format!(
227                "checkpoint exceeds the {MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES} byte encoded limit"
228            )));
229        }
230        let checkpoint = serde_json::from_slice::<Self>(bytes)
231            .map_err(|error| ValidationError::new(format!("decode checkpoint: {error}")))?;
232        if checkpoint.canonical_bytes()? != bytes {
233            return Err(ValidationError::new(
234                "checkpoint JSON is not canonical UTF-8, compactness or key order",
235            ));
236        }
237        Ok(checkpoint)
238    }
239
240    /// Migrate a canonical legacy-v1 checkpoint that predates mandatory tenant binding.
241    ///
242    /// The caller must supply the authenticated tenant context; this function never
243    /// guesses or derives a tenant from untrusted checkpoint fields. Payloads already
244    /// carrying a tenant must use [`Self::from_canonical_bytes`] instead.
245    pub fn from_legacy_v1_canonical_bytes(
246        bytes: &[u8],
247        authenticated_tenant_id: TenantId,
248    ) -> Result<Self, ValidationError> {
249        if bytes.len() > MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES {
250            return Err(ValidationError::new(format!(
251                "checkpoint exceeds the {MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES} byte encoded limit"
252            )));
253        }
254        let mut value: Value = serde_json::from_slice(bytes)
255            .map_err(|error| ValidationError::new(format!("decode legacy checkpoint: {error}")))?;
256        let canonical_legacy = serde_json::to_vec(&value).map_err(|error| {
257            ValidationError::new(format!("canonicalize legacy checkpoint: {error}"))
258        })?;
259        if canonical_legacy != bytes {
260            return Err(ValidationError::new(
261                "legacy checkpoint JSON is not canonical UTF-8, compactness or key order",
262            ));
263        }
264        let lineage = value
265            .get_mut("lineage")
266            .and_then(Value::as_object_mut)
267            .ok_or_else(|| ValidationError::new("legacy checkpoint lineage must be an object"))?;
268        if lineage.contains_key("tenant_id") {
269            return Err(ValidationError::new(
270                "legacy migration rejects checkpoints that already carry tenant_id",
271            ));
272        }
273        lineage.insert(
274            "tenant_id".to_owned(),
275            Value::String(authenticated_tenant_id.as_str().to_owned()),
276        );
277        let checkpoint = serde_json::from_value::<Self>(value)
278            .map_err(|error| ValidationError::new(format!("migrate legacy checkpoint: {error}")))?;
279        checkpoint.validate()?;
280        Ok(checkpoint)
281    }
282
283    /// Domain-separated content identity of the whole checkpoint.
284    pub fn digest(&self) -> Result<Sha256Digest, ValidationError> {
285        let bytes = self.canonical_bytes()?;
286        digest_with_domain(CONTEXT_CHECKPOINT_DIGEST_DOMAIN, &bytes)
287    }
288
289    /// Domain-separated digest inputs consumed by downstream slices.
290    pub fn digest_inputs(&self) -> Result<ContextCheckpointDigestInputsV1, ValidationError> {
291        self.validate()?;
292        Ok(ContextCheckpointDigestInputsV1 {
293            identity_digest: digest_with_domain(
294                CONTEXT_CHECKPOINT_IDENTITY_DIGEST_DOMAIN,
295                &canonical_bytes_of(&self.identity, "checkpoint identity")?,
296            )?,
297            lineage_digest: digest_with_domain(
298                CONTEXT_CHECKPOINT_LINEAGE_DIGEST_DOMAIN,
299                &canonical_bytes_of(&self.lineage, "checkpoint lineage")?,
300            )?,
301            live_state_digest: digest_with_domain(
302                CONTEXT_CHECKPOINT_LIVE_STATE_DIGEST_DOMAIN,
303                &canonical_bytes_of(&self.live_state, "checkpoint live state")?,
304            )?,
305            checkpoint_digest: self.digest()?,
306        })
307    }
308
309    /// Build the sealed payload a downstream signer may cover.
310    pub fn signing_payload(&self) -> Result<ContextCheckpointSigningPayloadV1, ValidationError> {
311        let canonical = self.canonical_bytes()?;
312        let mut bytes =
313            Vec::with_capacity(CONTEXT_CHECKPOINT_SIGNATURE_DOMAIN.len() + canonical.len());
314        bytes.extend_from_slice(CONTEXT_CHECKPOINT_SIGNATURE_DOMAIN);
315        bytes.extend_from_slice(&canonical);
316        let digest = digest_with_domain(CONTEXT_CHECKPOINT_SIGNATURE_DOMAIN, &canonical)?;
317        Ok(ContextCheckpointSigningPayloadV1 { bytes, digest })
318    }
319}
320
321/// Canonical checkpoint contract carrying authoritative artifact lineage.
322///
323/// V2 is intentionally a separate type and schema.  V1 readers therefore
324/// reject V2 bytes instead of silently discarding the artifact references that
325/// make a checkpoint auditable.
326#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct ContextCheckpointV2 {
328    pub schema_version: u32,
329    pub identity: ContextCheckpointIdentityV1,
330    pub lineage: ContextCheckpointLineageV2,
331    pub live_state: ContextCheckpointLiveStateV1,
332    pub carrier: Option<ContextCheckpointCarrierBindingV1>,
333    pub engine_version: SemanticVersion,
334    pub created_at: UtcTimestamp,
335    pub updated_at: UtcTimestamp,
336    pub encryption_metadata: Option<ContextCheckpointEncryptionMetadataV1>,
337}
338
339#[derive(Deserialize)]
340#[serde(deny_unknown_fields)]
341struct ContextCheckpointWireV2 {
342    schema_version: u32,
343    identity: ContextCheckpointIdentityV1,
344    lineage: ContextCheckpointLineageV2,
345    live_state: ContextCheckpointLiveStateV1,
346    carrier: Option<ContextCheckpointCarrierBindingV1>,
347    engine_version: SemanticVersion,
348    created_at: UtcTimestamp,
349    updated_at: UtcTimestamp,
350    #[serde(default)]
351    encryption_metadata: Option<ContextCheckpointEncryptionMetadataV1>,
352}
353
354impl<'de> Deserialize<'de> for ContextCheckpointV2 {
355    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
356    where
357        D: Deserializer<'de>,
358    {
359        let wire = ContextCheckpointWireV2::deserialize(deserializer)?;
360        let checkpoint = Self {
361            schema_version: wire.schema_version,
362            identity: wire.identity,
363            lineage: wire.lineage,
364            live_state: wire.live_state,
365            carrier: wire.carrier,
366            engine_version: wire.engine_version,
367            created_at: wire.created_at,
368            updated_at: wire.updated_at,
369            encryption_metadata: wire.encryption_metadata,
370        };
371        checkpoint.validate().map_err(D::Error::custom)?;
372        Ok(checkpoint)
373    }
374}
375
376impl ContextCheckpointV2 {
377    /// Schema version represented by this type.
378    pub const SCHEMA_VERSION: u32 = 2;
379
380    /// Schema identity represented by this type.
381    pub const SCHEMA_ID: &'static str = CONTEXT_CHECKPOINT_V2_SCHEMA_ID;
382
383    /// Attach structurally validated but unverified artifact refs to a V1 checkpoint.
384    ///
385    /// This wire-level operation does not prove that the supplied digests belong
386    /// to authoritative task, plan, or receipt documents and therefore does not
387    /// expose a signing payload. Production callers must use the core
388    /// `build_checkpoint_v2` boundary, which verifies those owners first.
389    pub fn from_v1_unverified(
390        checkpoint: ContextCheckpointV1,
391        artifact_lineage: ContextCheckpointArtifactLineageV1,
392    ) -> Result<Self, ValidationError> {
393        checkpoint.validate()?;
394        artifact_lineage.validate()?;
395        let migrated = Self {
396            schema_version: Self::SCHEMA_VERSION,
397            identity: checkpoint.identity,
398            lineage: ContextCheckpointLineageV2 {
399                project_id: checkpoint.lineage.project_id,
400                workspace_id: checkpoint.lineage.workspace_id,
401                tenant_id: checkpoint.lineage.tenant_id,
402                task_id: checkpoint.lineage.task_id,
403                plan_id: checkpoint.lineage.plan_id,
404                receipt_ids: checkpoint.lineage.receipt_ids,
405                artifact_lineage,
406                context_ir_digest: checkpoint.lineage.context_ir_digest,
407                hosted_index_digest: checkpoint.lineage.hosted_index_digest,
408                evidence_refs: checkpoint.lineage.evidence_refs,
409                knowledge_refs: checkpoint.lineage.knowledge_refs,
410                gotcha_refs: checkpoint.lineage.gotcha_refs,
411                snapshot_refs: checkpoint.lineage.snapshot_refs,
412            },
413            live_state: checkpoint.live_state,
414            carrier: checkpoint.carrier,
415            engine_version: checkpoint.engine_version,
416            created_at: checkpoint.created_at,
417            updated_at: checkpoint.updated_at,
418            encryption_metadata: checkpoint.encryption_metadata,
419        };
420        migrated.validate()?;
421        Ok(migrated)
422    }
423
424    /// Validate every V2 invariant, including all V1 live-state bindings.
425    pub fn validate(&self) -> Result<(), ValidationError> {
426        if self.schema_version != Self::SCHEMA_VERSION {
427            return Err(ValidationError::new(format!(
428                "unsupported checkpoint V2 schema_version {}; expected {}",
429                self.schema_version,
430                Self::SCHEMA_VERSION
431            )));
432        }
433        self.lineage.validate()?;
434        self.legacy_checkpoint()?.validate()
435    }
436
437    fn legacy_checkpoint(&self) -> Result<ContextCheckpointV1, ValidationError> {
438        Ok(ContextCheckpointV1 {
439            schema_version: ContextCheckpointV1::SCHEMA_VERSION,
440            identity: self.identity.clone(),
441            lineage: self.lineage.legacy(),
442            live_state: self.live_state.clone(),
443            carrier: self.carrier.clone(),
444            engine_version: self.engine_version.clone(),
445            created_at: self.created_at.clone(),
446            updated_at: self.updated_at.clone(),
447            encryption_metadata: self.encryption_metadata.clone(),
448        })
449    }
450
451    /// Return canonical compact JSON bytes with lexicographically sorted keys.
452    pub fn canonical_bytes(&self) -> Result<Vec<u8>, ValidationError> {
453        self.validate()?;
454        canonical_bytes_of(self, "checkpoint V2")
455    }
456
457    /// Return canonical JSON text.
458    pub fn canonical_json(&self) -> Result<String, ValidationError> {
459        String::from_utf8(self.canonical_bytes()?)
460            .map_err(|error| ValidationError::new(format!("checkpoint V2 UTF-8: {error}")))
461    }
462
463    /// Decode exactly canonical V2 JSON and validate every invariant.
464    pub fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, ValidationError> {
465        if bytes.len() > MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES {
466            return Err(ValidationError::new(format!(
467                "checkpoint V2 exceeds the {MAX_CONTEXT_CHECKPOINT_ENCODED_BYTES} byte encoded limit"
468            )));
469        }
470        let checkpoint = serde_json::from_slice::<Self>(bytes)
471            .map_err(|error| ValidationError::new(format!("decode checkpoint V2: {error}")))?;
472        if checkpoint.canonical_bytes()? != bytes {
473            return Err(ValidationError::new(
474                "checkpoint V2 JSON is not canonical UTF-8, compactness or key order",
475            ));
476        }
477        Ok(checkpoint)
478    }
479
480    /// Domain-separated content identity of the complete V2 checkpoint.
481    pub fn digest(&self) -> Result<Sha256Digest, ValidationError> {
482        digest_with_domain(
483            CONTEXT_CHECKPOINT_V2_DIGEST_DOMAIN,
484            &self.canonical_bytes()?,
485        )
486    }
487
488    /// Domain-separated digest inputs, including the V2 artifact lineage.
489    pub fn digest_inputs(&self) -> Result<ContextCheckpointDigestInputsV1, ValidationError> {
490        self.validate()?;
491        Ok(ContextCheckpointDigestInputsV1 {
492            identity_digest: digest_with_domain(
493                CONTEXT_CHECKPOINT_IDENTITY_DIGEST_DOMAIN,
494                &canonical_bytes_of(&self.identity, "checkpoint V2 identity")?,
495            )?,
496            lineage_digest: digest_with_domain(
497                CONTEXT_CHECKPOINT_V2_LINEAGE_DIGEST_DOMAIN,
498                &canonical_bytes_of(&self.lineage, "checkpoint V2 lineage")?,
499            )?,
500            live_state_digest: digest_with_domain(
501                CONTEXT_CHECKPOINT_LIVE_STATE_DIGEST_DOMAIN,
502                &canonical_bytes_of(&self.live_state, "checkpoint V2 live state")?,
503            )?,
504            checkpoint_digest: self.digest()?,
505        })
506    }
507}
508
509impl Serialize for ContextCheckpointV2 {
510    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
511    where
512        S: serde::Serializer,
513    {
514        self.validate().map_err(serde::ser::Error::custom)?;
515        #[derive(Serialize)]
516        struct Wire<'a> {
517            schema_version: u32,
518            identity: &'a ContextCheckpointIdentityV1,
519            lineage: &'a ContextCheckpointLineageV2,
520            live_state: &'a ContextCheckpointLiveStateV1,
521            carrier: &'a Option<ContextCheckpointCarrierBindingV1>,
522            engine_version: &'a SemanticVersion,
523            created_at: &'a UtcTimestamp,
524            updated_at: &'a UtcTimestamp,
525            #[serde(skip_serializing_if = "Option::is_none")]
526            encryption_metadata: &'a Option<ContextCheckpointEncryptionMetadataV1>,
527        }
528        Wire {
529            schema_version: self.schema_version,
530            identity: &self.identity,
531            lineage: &self.lineage,
532            live_state: &self.live_state,
533            carrier: &self.carrier,
534            engine_version: &self.engine_version,
535            created_at: &self.created_at,
536            updated_at: &self.updated_at,
537            encryption_metadata: &self.encryption_metadata,
538        }
539        .serialize(serializer)
540    }
541}
542
543impl Serialize for ContextCheckpointV1 {
544    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
545    where
546        S: serde::Serializer,
547    {
548        self.validate().map_err(serde::ser::Error::custom)?;
549        #[derive(Serialize)]
550        struct Wire<'a> {
551            schema_version: u32,
552            identity: &'a ContextCheckpointIdentityV1,
553            lineage: &'a ContextCheckpointLineageV1,
554            live_state: &'a ContextCheckpointLiveStateV1,
555            carrier: &'a Option<ContextCheckpointCarrierBindingV1>,
556            engine_version: &'a SemanticVersion,
557            created_at: &'a UtcTimestamp,
558            updated_at: &'a UtcTimestamp,
559            #[serde(skip_serializing_if = "Option::is_none")]
560            encryption_metadata: &'a Option<ContextCheckpointEncryptionMetadataV1>,
561        }
562        Wire {
563            schema_version: self.schema_version,
564            identity: &self.identity,
565            lineage: &self.lineage,
566            live_state: &self.live_state,
567            carrier: &self.carrier,
568            engine_version: &self.engine_version,
569            created_at: &self.created_at,
570            updated_at: &self.updated_at,
571            encryption_metadata: &self.encryption_metadata,
572        }
573        .serialize(serializer)
574    }
575}
576
577/// Domain-separated digests derived from one validated checkpoint.
578///
579/// The fields are private: a value of this type can only originate from
580/// [`ContextCheckpointV1::digest_inputs`], which validates first.
581#[derive(Debug, Clone, PartialEq, Eq)]
582pub struct ContextCheckpointDigestInputsV1 {
583    identity_digest: Sha256Digest,
584    lineage_digest: Sha256Digest,
585    live_state_digest: Sha256Digest,
586    checkpoint_digest: Sha256Digest,
587}
588
589impl ContextCheckpointDigestInputsV1 {
590    /// Digest over the parent, branch, device, and sequence identity.
591    pub fn identity_digest(&self) -> &Sha256Digest {
592        &self.identity_digest
593    }
594
595    /// Digest over the project, workspace, task, plan, and receipt lineage.
596    pub fn lineage_digest(&self) -> &Sha256Digest {
597        &self.lineage_digest
598    }
599
600    /// Digest over the portable live state.
601    pub fn live_state_digest(&self) -> &Sha256Digest {
602        &self.live_state_digest
603    }
604
605    /// Digest binding every part of the checkpoint together.
606    pub fn checkpoint_digest(&self) -> &Sha256Digest {
607        &self.checkpoint_digest
608    }
609}
610
611/// Sealed signature payload for one validated checkpoint.
612///
613/// The fields are private: a value of this type can only originate from
614/// [`ContextCheckpointV1::signing_payload`], so an invalid checkpoint can never
615/// reach a signer.
616#[derive(Debug, Clone, PartialEq, Eq)]
617pub struct ContextCheckpointSigningPayloadV1 {
618    bytes: Vec<u8>,
619    digest: Sha256Digest,
620}
621
622impl ContextCheckpointSigningPayloadV1 {
623    /// Borrow the domain-prefixed bytes a signature must cover.
624    pub fn bytes(&self) -> &[u8] {
625        &self.bytes
626    }
627
628    /// Borrow the domain-separated digest of the covered bytes.
629    pub fn digest(&self) -> &Sha256Digest {
630        &self.digest
631    }
632}
633
634/// Seam for the downstream projection slice.
635pub trait ContextCheckpointProjectionV1 {
636    /// Projection produced from a validated checkpoint.
637    type Projection;
638    /// Error surfaced by the projection slice.
639    type Error;
640
641    /// Project a validated checkpoint into a downstream carrier shape.
642    fn project(&self, checkpoint: &ContextCheckpointV1) -> Result<Self::Projection, Self::Error>;
643}
644
645/// Seam for the downstream branch-merge slice.
646pub trait ContextCheckpointMergePolicyV1 {
647    /// Error surfaced by the merge slice.
648    type Error;
649
650    /// Merge two checkpoint branches against their common ancestor.
651    fn merge(
652        &self,
653        ancestor: &ContextCheckpointV1,
654        left: &ContextCheckpointV1,
655        right: &ContextCheckpointV1,
656    ) -> Result<ContextCheckpointV1, Self::Error>;
657}
658
659/// Seam for the downstream migration slice.
660pub trait ContextCheckpointMigrationV1 {
661    /// Error surfaced by the migration slice.
662    type Error;
663
664    /// Migrate encoded checkpoint bytes of an older schema to this domain.
665    fn migrate(&self, bytes: &[u8]) -> Result<ContextCheckpointV1, Self::Error>;
666}
667
668/// Seam for the downstream signing and trust slice.
669pub trait ContextCheckpointSignerV1 {
670    /// Signature representation produced by the signer.
671    type Signature;
672    /// Error surfaced by the signing slice.
673    type Error;
674
675    /// Sign a sealed checkpoint payload.
676    fn sign(
677        &self,
678        payload: &ContextCheckpointSigningPayloadV1,
679    ) -> Result<Self::Signature, Self::Error>;
680}