Skip to main content

git_internal/internal/object/
types.rs

1//! Object type enumeration and AI Object Header Definition.
2//!
3//! This module defines the common metadata header shared by all AI process objects
4//! and the object type enumeration used across pack/object modules.
5
6use std::{
7    collections::HashMap,
8    fmt::{self, Display},
9};
10
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use uuid::Uuid;
14
15use super::integrity::{IntegrityHash, compute_integrity_hash};
16use crate::errors::GitError;
17
18/// Visibility of an AI process object.
19///
20/// Determines whether the object is accessible only within the project (Private)
21/// or can be shared externally (Public).
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23#[serde(rename_all = "snake_case")]
24pub enum Visibility {
25    Private,
26    Public,
27}
28
29/// In Git, each object type is assigned a unique integer value, which is used to identify the
30/// type of the object in Git repositories.
31///
32/// * `Blob` (1): A Git object that stores the content of a file.
33/// * `Tree` (2): A Git object that represents a directory or a folder in a Git repository.
34/// * `Commit` (3): A Git object that represents a commit in a Git repository, which contains
35///   information such as the author, committer, commit message, and parent commits.
36/// * `Tag` (4): A Git object that represents a tag in a Git repository, which is used to mark a
37///   specific point in the Git history.
38/// * `OffsetDelta` (6): A Git object that represents a delta between two objects, where the delta
39///   is stored as an offset to the base object.
40/// * `HashDelta` (7): A Git object that represents a delta between two objects, where the delta
41///   is stored as a hash of the base object.
42///
43/// By assigning unique integer values to each Git object type, Git can easily and efficiently
44/// identify the type of an object and perform the appropriate operations on it. when parsing a Git
45/// repository, Git can use the integer value of an object's type to determine how to parse
46/// the object's content.
47#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum ObjectType {
50    Commit = 1,
51    Tree,
52    Blob,
53    Tag,
54    OffsetZstdelta, // Private extension for Zstandard-compressed delta objects
55    OffsetDelta,
56    HashDelta,
57    ContextSnapshot,
58    Decision,
59    Evidence,
60    PatchSet,
61    Plan,
62    Provenance,
63    Run,
64    Task,
65    Intent,
66    ToolInvocation,
67    ContextPipeline,
68}
69
70const COMMIT_OBJECT_TYPE: &[u8] = b"commit";
71const TREE_OBJECT_TYPE: &[u8] = b"tree";
72const BLOB_OBJECT_TYPE: &[u8] = b"blob";
73const TAG_OBJECT_TYPE: &[u8] = b"tag";
74const CONTEXT_SNAPSHOT_OBJECT_TYPE: &[u8] = b"snapshot";
75const DECISION_OBJECT_TYPE: &[u8] = b"decision";
76const EVIDENCE_OBJECT_TYPE: &[u8] = b"evidence";
77const PATCH_SET_OBJECT_TYPE: &[u8] = b"patchset";
78const PLAN_OBJECT_TYPE: &[u8] = b"plan";
79const PROVENANCE_OBJECT_TYPE: &[u8] = b"provenance";
80const RUN_OBJECT_TYPE: &[u8] = b"run";
81const TASK_OBJECT_TYPE: &[u8] = b"task";
82const INTENT_OBJECT_TYPE: &[u8] = b"intent";
83const TOOL_INVOCATION_OBJECT_TYPE: &[u8] = b"invocation";
84const CONTEXT_PIPELINE_OBJECT_TYPE: &[u8] = b"pipeline";
85
86/// Display trait for Git objects type
87impl Display for ObjectType {
88    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
89        match self {
90            ObjectType::Blob => write!(f, "blob"),
91            ObjectType::Tree => write!(f, "tree"),
92            ObjectType::Commit => write!(f, "commit"),
93            ObjectType::Tag => write!(f, "tag"),
94            ObjectType::OffsetZstdelta => write!(f, "OffsetZstdelta"),
95            ObjectType::OffsetDelta => write!(f, "OffsetDelta"),
96            ObjectType::HashDelta => write!(f, "HashDelta"),
97            ObjectType::ContextSnapshot => write!(f, "snapshot"),
98            ObjectType::Decision => write!(f, "decision"),
99            ObjectType::Evidence => write!(f, "evidence"),
100            ObjectType::PatchSet => write!(f, "patchset"),
101            ObjectType::Plan => write!(f, "plan"),
102            ObjectType::Provenance => write!(f, "provenance"),
103            ObjectType::Run => write!(f, "run"),
104            ObjectType::Task => write!(f, "task"),
105            ObjectType::Intent => write!(f, "intent"),
106            ObjectType::ToolInvocation => write!(f, "invocation"),
107            ObjectType::ContextPipeline => write!(f, "pipeline"),
108        }
109    }
110}
111
112/// Display trait for Git objects type
113impl ObjectType {
114    /// Convert object type to 3-bit pack header type id.
115    ///
116    /// Git pack headers only carry 3 type bits (values 0..=7). AI object
117    /// types are not representable in this field and must not be written
118    /// as regular base objects in a pack entry.
119    pub fn to_pack_type_u8(&self) -> Result<u8, GitError> {
120        match self {
121            ObjectType::Commit => Ok(1),
122            ObjectType::Tree => Ok(2),
123            ObjectType::Blob => Ok(3),
124            ObjectType::Tag => Ok(4),
125            ObjectType::OffsetZstdelta => Ok(5),
126            ObjectType::OffsetDelta => Ok(6),
127            ObjectType::HashDelta => Ok(7),
128            _ => Err(GitError::PackEncodeError(format!(
129                "object type `{}` cannot be encoded in pack header type bits",
130                self
131            ))),
132        }
133    }
134
135    /// Decode 3-bit pack header type id to object type.
136    pub fn from_pack_type_u8(number: u8) -> Result<ObjectType, GitError> {
137        match number {
138            1 => Ok(ObjectType::Commit),
139            2 => Ok(ObjectType::Tree),
140            3 => Ok(ObjectType::Blob),
141            4 => Ok(ObjectType::Tag),
142            5 => Ok(ObjectType::OffsetZstdelta),
143            6 => Ok(ObjectType::OffsetDelta),
144            7 => Ok(ObjectType::HashDelta),
145            _ => Err(GitError::InvalidObjectType(format!(
146                "Invalid pack object type number: {number}"
147            ))),
148        }
149    }
150
151    /// Returns the loose-object type header bytes (e.g. `b"commit"`, `b"blob"`).
152    ///
153    /// Delta types (`OffsetDelta`, `HashDelta`, `OffsetZstdelta`) only
154    /// exist inside pack files and have no loose-object representation.
155    /// Passing a delta type is a logic error and returns `None`.
156    pub fn to_bytes(&self) -> Option<&[u8]> {
157        match self {
158            ObjectType::Commit => Some(COMMIT_OBJECT_TYPE),
159            ObjectType::Tree => Some(TREE_OBJECT_TYPE),
160            ObjectType::Blob => Some(BLOB_OBJECT_TYPE),
161            ObjectType::Tag => Some(TAG_OBJECT_TYPE),
162            ObjectType::ContextSnapshot => Some(CONTEXT_SNAPSHOT_OBJECT_TYPE),
163            ObjectType::Decision => Some(DECISION_OBJECT_TYPE),
164            ObjectType::Evidence => Some(EVIDENCE_OBJECT_TYPE),
165            ObjectType::PatchSet => Some(PATCH_SET_OBJECT_TYPE),
166            ObjectType::Plan => Some(PLAN_OBJECT_TYPE),
167            ObjectType::Provenance => Some(PROVENANCE_OBJECT_TYPE),
168            ObjectType::Run => Some(RUN_OBJECT_TYPE),
169            ObjectType::Task => Some(TASK_OBJECT_TYPE),
170            ObjectType::Intent => Some(INTENT_OBJECT_TYPE),
171            ObjectType::ToolInvocation => Some(TOOL_INVOCATION_OBJECT_TYPE),
172            ObjectType::ContextPipeline => Some(CONTEXT_PIPELINE_OBJECT_TYPE),
173            ObjectType::OffsetDelta | ObjectType::HashDelta | ObjectType::OffsetZstdelta => None,
174        }
175    }
176
177    /// Parses a string representation of a Git object type and returns an ObjectType value
178    pub fn from_string(s: &str) -> Result<ObjectType, GitError> {
179        match s {
180            "blob" => Ok(ObjectType::Blob),
181            "tree" => Ok(ObjectType::Tree),
182            "commit" => Ok(ObjectType::Commit),
183            "tag" => Ok(ObjectType::Tag),
184            "snapshot" => Ok(ObjectType::ContextSnapshot),
185            "decision" => Ok(ObjectType::Decision),
186            "evidence" => Ok(ObjectType::Evidence),
187            "patchset" => Ok(ObjectType::PatchSet),
188            "plan" => Ok(ObjectType::Plan),
189            "provenance" => Ok(ObjectType::Provenance),
190            "run" => Ok(ObjectType::Run),
191            "task" => Ok(ObjectType::Task),
192            "intent" => Ok(ObjectType::Intent),
193            "invocation" => Ok(ObjectType::ToolInvocation),
194            "pipeline" => Ok(ObjectType::ContextPipeline),
195            _ => Err(GitError::InvalidObjectType(s.to_string())),
196        }
197    }
198
199    /// Convert an object type to a byte array.
200    pub fn to_data(self) -> Result<Vec<u8>, GitError> {
201        match self {
202            ObjectType::Blob => Ok(vec![0x62, 0x6c, 0x6f, 0x62]), // blob
203            ObjectType::Tree => Ok(vec![0x74, 0x72, 0x65, 0x65]), // tree
204            ObjectType::Commit => Ok(vec![0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74]), // commit
205            ObjectType::Tag => Ok(vec![0x74, 0x61, 0x67]),        // tag
206            ObjectType::ContextSnapshot => Ok(vec![0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74]), // snapshot
207            ObjectType::Decision => Ok(vec![0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e]), // decision
208            ObjectType::Evidence => Ok(vec![0x65, 0x76, 0x69, 0x64, 0x65, 0x6e, 0x63, 0x65]), // evidence
209            ObjectType::PatchSet => Ok(vec![0x70, 0x61, 0x74, 0x63, 0x68, 0x73, 0x65, 0x74]), // patchset
210            ObjectType::Plan => Ok(vec![0x70, 0x6c, 0x61, 0x6e]), // plan
211            ObjectType::Provenance => Ok(vec![
212                0x70, 0x72, 0x6f, 0x76, 0x65, 0x6e, 0x61, 0x6e, 0x63, 0x65,
213            ]), // provenance
214            ObjectType::Run => Ok(vec![0x72, 0x75, 0x6e]),        // run
215            ObjectType::Task => Ok(vec![0x74, 0x61, 0x73, 0x6b]), // task
216            ObjectType::Intent => Ok(vec![0x69, 0x6e, 0x74, 0x65, 0x6e, 0x74]), // intent
217            ObjectType::ToolInvocation => Ok(vec![
218                0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
219            ]), // invocation
220            ObjectType::ContextPipeline => Ok(vec![0x70, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65]), // pipeline
221            _ => Err(GitError::InvalidObjectType(self.to_string())),
222        }
223    }
224
225    /// Convert an object type to a number.
226    pub fn to_u8(&self) -> u8 {
227        match self {
228            ObjectType::Commit => 1,
229            ObjectType::Tree => 2,
230            ObjectType::Blob => 3,
231            ObjectType::Tag => 4,
232            ObjectType::OffsetZstdelta => 5, // Type 5 is reserved in standard Git packs; we use it for Zstd delta objects.
233            ObjectType::OffsetDelta => 6,
234            ObjectType::HashDelta => 7,
235            ObjectType::ContextSnapshot => 8,
236            ObjectType::Decision => 9,
237            ObjectType::Evidence => 10,
238            ObjectType::PatchSet => 11,
239            ObjectType::Plan => 12,
240            ObjectType::Provenance => 13,
241            ObjectType::Run => 14,
242            ObjectType::Task => 15,
243            ObjectType::Intent => 16,
244            ObjectType::ToolInvocation => 17,
245            ObjectType::ContextPipeline => 18,
246        }
247    }
248
249    /// Convert a number to an object type.
250    pub fn from_u8(number: u8) -> Result<ObjectType, GitError> {
251        match number {
252            1 => Ok(ObjectType::Commit),
253            2 => Ok(ObjectType::Tree),
254            3 => Ok(ObjectType::Blob),
255            4 => Ok(ObjectType::Tag),
256            5 => Ok(ObjectType::OffsetZstdelta),
257            6 => Ok(ObjectType::OffsetDelta),
258            7 => Ok(ObjectType::HashDelta),
259            8 => Ok(ObjectType::ContextSnapshot),
260            9 => Ok(ObjectType::Decision),
261            10 => Ok(ObjectType::Evidence),
262            11 => Ok(ObjectType::PatchSet),
263            12 => Ok(ObjectType::Plan),
264            13 => Ok(ObjectType::Provenance),
265            14 => Ok(ObjectType::Run),
266            15 => Ok(ObjectType::Task),
267            16 => Ok(ObjectType::Intent),
268            17 => Ok(ObjectType::ToolInvocation),
269            18 => Ok(ObjectType::ContextPipeline),
270            _ => Err(GitError::InvalidObjectType(format!(
271                "Invalid object type number: {number}"
272            ))),
273        }
274    }
275
276    /// Returns `true` if this type is a base Git object that can appear
277    /// as a delta target in pack files. AI object types return `false`
278    /// because they cannot be encoded in pack files and should never
279    /// participate in delta window selection.
280    pub fn is_base(&self) -> bool {
281        matches!(
282            self,
283            ObjectType::Commit | ObjectType::Tree | ObjectType::Blob | ObjectType::Tag
284        )
285    }
286
287    /// Returns `true` if this type is an AI extension object (not representable
288    /// in the 3-bit Git pack header).
289    pub fn is_ai_object(&self) -> bool {
290        matches!(
291            self,
292            ObjectType::ContextSnapshot
293                | ObjectType::Decision
294                | ObjectType::Evidence
295                | ObjectType::PatchSet
296                | ObjectType::Plan
297                | ObjectType::Provenance
298                | ObjectType::Run
299                | ObjectType::Task
300                | ObjectType::Intent
301                | ObjectType::ToolInvocation
302                | ObjectType::ContextPipeline
303        )
304    }
305}
306
307/// Actor kind enum
308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
309#[serde(rename_all = "snake_case")]
310pub enum ActorKind {
311    Human,
312    Agent,
313    System,
314    McpClient,
315    #[serde(untagged)]
316    Other(String),
317}
318
319impl fmt::Display for ActorKind {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        match self {
322            ActorKind::Human => write!(f, "human"),
323            ActorKind::Agent => write!(f, "agent"),
324            ActorKind::System => write!(f, "system"),
325            ActorKind::McpClient => write!(f, "mcp_client"),
326            ActorKind::Other(s) => write!(f, "{}", s),
327        }
328    }
329}
330
331impl From<String> for ActorKind {
332    fn from(s: String) -> Self {
333        match s.as_str() {
334            "human" => ActorKind::Human,
335            "agent" => ActorKind::Agent,
336            "system" => ActorKind::System,
337            "mcp_client" => ActorKind::McpClient,
338            _ => ActorKind::Other(s),
339        }
340    }
341}
342
343impl From<&str> for ActorKind {
344    fn from(s: &str) -> Self {
345        match s {
346            "human" => ActorKind::Human,
347            "agent" => ActorKind::Agent,
348            "system" => ActorKind::System,
349            "mcp_client" => ActorKind::McpClient,
350            _ => ActorKind::Other(s.to_string()),
351        }
352    }
353}
354
355/// Actor reference (who created/triggered).
356#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
357pub struct ActorRef {
358    /// Kind: human/agent/system/mcp_client
359    kind: ActorKind,
360    /// Subject ID (user/agent name or client ID)
361    id: String,
362    /// Display name (optional)
363    display_name: Option<String>,
364    /// Auth context (optional, Libra usually empty)
365    auth_context: Option<String>,
366}
367
368impl ActorRef {
369    /// Create a new ActorRef with validation.
370    pub fn new(kind: impl Into<ActorKind>, id: impl Into<String>) -> Result<Self, String> {
371        let id_str = id.into();
372        if id_str.trim().is_empty() {
373            return Err("Actor ID cannot be empty".to_string());
374        }
375        Ok(Self {
376            kind: kind.into(),
377            id: id_str,
378            display_name: None,
379            auth_context: None,
380        })
381    }
382
383    /// Create an MCP client actor reference (MCP writes must use this).
384    pub fn new_for_mcp(id: impl Into<String>) -> Result<Self, String> {
385        Self::new(ActorKind::McpClient, id)
386    }
387
388    /// Validate that this actor is an MCP client.
389    pub fn ensure_mcp_client(&self) -> Result<(), String> {
390        if self.kind != ActorKind::McpClient {
391            return Err("MCP writes must use mcp_client actor kind".to_string());
392        }
393        Ok(())
394    }
395
396    pub fn kind(&self) -> &ActorKind {
397        &self.kind
398    }
399
400    pub fn id(&self) -> &str {
401        &self.id
402    }
403
404    pub fn display_name(&self) -> Option<&str> {
405        self.display_name.as_deref()
406    }
407
408    pub fn auth_context(&self) -> Option<&str> {
409        self.auth_context.as_deref()
410    }
411
412    pub fn set_display_name(&mut self, display_name: Option<String>) {
413        self.display_name = display_name;
414    }
415
416    pub fn set_auth_context(&mut self, auth_context: Option<String>) {
417        self.auth_context = auth_context;
418    }
419
420    /// Create a human actor reference.
421    pub fn human(id: impl Into<String>) -> Result<Self, String> {
422        Self::new(ActorKind::Human, id)
423    }
424
425    /// Create an agent actor reference.
426    pub fn agent(name: impl Into<String>) -> Result<Self, String> {
427        Self::new(ActorKind::Agent, name)
428    }
429
430    /// Create a system component actor reference.
431    pub fn system(component: impl Into<String>) -> Result<Self, String> {
432        Self::new(ActorKind::System, component)
433    }
434
435    /// Create an MCP client actor reference.
436    pub fn mcp_client(client_id: impl Into<String>) -> Result<Self, String> {
437        Self::new(ActorKind::McpClient, client_id)
438    }
439}
440
441/// Artifact reference (external content).
442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
443pub struct ArtifactRef {
444    /// Store type: local_fs/s3
445    store: String,
446    /// Storage key (e.g., path or object key)
447    key: String,
448    /// MIME type (optional)
449    content_type: Option<String>,
450    /// Size in bytes (optional)
451    size_bytes: Option<u64>,
452    /// Content hash (strongly recommended)
453    hash: Option<IntegrityHash>,
454    /// Expiration time (optional)
455    expires_at: Option<DateTime<Utc>>,
456}
457
458impl ArtifactRef {
459    pub fn new(store: impl Into<String>, key: impl Into<String>) -> Result<Self, String> {
460        let store = store.into();
461        let key = key.into();
462        if store.trim().is_empty() {
463            return Err("store cannot be empty".to_string());
464        }
465        if key.trim().is_empty() {
466            return Err("key cannot be empty".to_string());
467        }
468        Ok(Self {
469            store,
470            key,
471            content_type: None,
472            size_bytes: None,
473            hash: None,
474            expires_at: None,
475        })
476    }
477
478    pub fn store(&self) -> &str {
479        &self.store
480    }
481
482    pub fn key(&self) -> &str {
483        &self.key
484    }
485
486    pub fn content_type(&self) -> Option<&str> {
487        self.content_type.as_deref()
488    }
489
490    pub fn size_bytes(&self) -> Option<u64> {
491        self.size_bytes
492    }
493
494    pub fn hash(&self) -> Option<&IntegrityHash> {
495        self.hash.as_ref()
496    }
497
498    pub fn expires_at(&self) -> Option<DateTime<Utc>> {
499        self.expires_at
500    }
501
502    /// Calculate hash for the given content bytes.
503    pub fn compute_hash(content: &[u8]) -> IntegrityHash {
504        IntegrityHash::compute(content)
505    }
506
507    /// Set the hash directly.
508    pub fn with_hash(mut self, hash: IntegrityHash) -> Self {
509        self.hash = Some(hash);
510        self
511    }
512
513    /// Set the hash from a hex string.
514    pub fn with_hash_hex(mut self, hash: impl AsRef<str>) -> Result<Self, String> {
515        let hash = hash.as_ref().parse()?;
516        self.hash = Some(hash);
517        Ok(self)
518    }
519
520    pub fn set_content_type(&mut self, content_type: Option<String>) {
521        self.content_type = content_type;
522    }
523
524    pub fn set_size_bytes(&mut self, size_bytes: Option<u64>) {
525        self.size_bytes = size_bytes;
526    }
527
528    pub fn set_expires_at(&mut self, expires_at: Option<DateTime<Utc>>) {
529        self.expires_at = expires_at;
530    }
531
532    /// Verify if the provided content matches the stored checksum
533    #[must_use = "handle integrity verification result"]
534    pub fn verify_integrity(&self, content: &[u8]) -> Result<bool, String> {
535        let stored_hash = self
536            .hash
537            .as_ref()
538            .ok_or_else(|| "No hash stored in ArtifactRef".to_string())?;
539
540        Ok(IntegrityHash::compute(content) == *stored_hash)
541    }
542
543    /// Check if two artifacts have the same content based on checksum
544    #[must_use]
545    pub fn content_eq(&self, other: &Self) -> Option<bool> {
546        match (&self.hash, &other.hash) {
547            (Some(a), Some(b)) => Some(a == b),
548            _ => None,
549        }
550    }
551
552    /// Check if the artifact has expired
553    #[must_use]
554    pub fn is_expired(&self) -> bool {
555        if let Some(expires_at) = self.expires_at {
556            expires_at < Utc::now()
557        } else {
558            false
559        }
560    }
561}
562
563fn default_header_version() -> u32 {
564    1
565}
566
567/// Current header format version for newly created objects.
568pub const CURRENT_HEADER_VERSION: u32 = 1;
569
570/// Header shared by all AI Process Objects.
571///
572/// Contains standard metadata like ID, type, creator, and timestamps.
573///
574/// # Usage
575///
576/// Every AI object struct should flatten this header:
577///
578/// ```rust,ignore
579/// #[derive(Serialize, Deserialize)]
580/// pub struct AIObject {
581///     #[serde(flatten)]
582///     header: Header,
583///     // specific fields...
584/// }
585/// ```
586
587#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
588pub struct Header {
589    /// Global unique ID (UUID v7)
590    object_id: Uuid,
591    /// Object type (task/run/patchset/...)
592    object_type: ObjectType,
593    /// Format version of the Header struct itself.
594    /// Defaults to 1 when deserializing old data that lacks this field.
595    #[serde(default = "default_header_version")]
596    header_version: u32,
597    /// Per-object-type schema version for body fields.
598    schema_version: u32,
599    /// Creation time
600    created_at: DateTime<Utc>,
601    /// Last modification time.
602    ///
603    /// When deserializing legacy data that lacks this field, falls back
604    /// to `created_at` for deterministic behavior (see custom
605    /// `Deserialize` impl below).
606    updated_at: DateTime<Utc>,
607    /// Creator
608    created_by: ActorRef,
609    /// Visibility (fixed to private for Libra)
610    visibility: Visibility,
611    /// Search tags
612    #[serde(default)]
613    tags: HashMap<String, String>,
614    /// External ID mapping
615    #[serde(default)]
616    external_ids: HashMap<String, String>,
617    /// Content checksum (optional)
618    #[serde(default)]
619    checksum: Option<IntegrityHash>,
620}
621
622/// Custom `Deserialize` for [`Header`] so that a missing `updated_at`
623/// falls back to `created_at` instead of `Utc::now()`.  This avoids
624/// nondeterministic metadata when loading legacy objects that predate
625/// the `updated_at` field.
626impl<'de> Deserialize<'de> for Header {
627    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
628    where
629        D: serde::Deserializer<'de>,
630    {
631        #[derive(Deserialize)]
632        struct RawHeader {
633            object_id: Uuid,
634            object_type: ObjectType,
635            #[serde(default = "default_header_version")]
636            header_version: u32,
637            schema_version: u32,
638            created_at: DateTime<Utc>,
639            updated_at: Option<DateTime<Utc>>,
640            created_by: ActorRef,
641            visibility: Visibility,
642            #[serde(default)]
643            tags: HashMap<String, String>,
644            #[serde(default)]
645            external_ids: HashMap<String, String>,
646            #[serde(default)]
647            checksum: Option<IntegrityHash>,
648        }
649
650        let raw = RawHeader::deserialize(deserializer)?;
651        Ok(Header {
652            object_id: raw.object_id,
653            object_type: raw.object_type,
654            header_version: raw.header_version,
655            schema_version: raw.schema_version,
656            created_at: raw.created_at,
657            updated_at: raw.updated_at.unwrap_or(raw.created_at),
658            created_by: raw.created_by,
659            visibility: raw.visibility,
660            tags: raw.tags,
661            external_ids: raw.external_ids,
662            checksum: raw.checksum,
663        })
664    }
665}
666
667impl Header {
668    /// Create a new Header with default values.
669    ///
670    /// # Arguments
671    ///
672    /// * `object_type` - The specific type of the AI object.
673    /// * `created_by` - The actor (human/agent) creating this object.
674    pub fn new(object_type: ObjectType, created_by: ActorRef) -> Result<Self, String> {
675        let now = Utc::now();
676        Ok(Self {
677            object_id: Uuid::now_v7(),
678            object_type,
679            header_version: CURRENT_HEADER_VERSION,
680            schema_version: 1,
681            created_at: now,
682            updated_at: now,
683            created_by,
684            visibility: Visibility::Private,
685            tags: HashMap::new(),
686            external_ids: HashMap::new(),
687            checksum: None,
688        })
689    }
690
691    pub fn object_id(&self) -> Uuid {
692        self.object_id
693    }
694
695    pub fn object_type(&self) -> &ObjectType {
696        &self.object_type
697    }
698
699    pub fn header_version(&self) -> u32 {
700        self.header_version
701    }
702
703    pub fn schema_version(&self) -> u32 {
704        self.schema_version
705    }
706
707    pub fn created_at(&self) -> DateTime<Utc> {
708        self.created_at
709    }
710
711    pub fn updated_at(&self) -> DateTime<Utc> {
712        self.updated_at
713    }
714
715    pub fn created_by(&self) -> &ActorRef {
716        &self.created_by
717    }
718
719    pub fn visibility(&self) -> &Visibility {
720        &self.visibility
721    }
722
723    pub fn tags(&self) -> &HashMap<String, String> {
724        &self.tags
725    }
726
727    pub fn tags_mut(&mut self) -> &mut HashMap<String, String> {
728        &mut self.tags
729    }
730
731    pub fn external_ids(&self) -> &HashMap<String, String> {
732        &self.external_ids
733    }
734
735    pub fn external_ids_mut(&mut self) -> &mut HashMap<String, String> {
736        &mut self.external_ids
737    }
738
739    pub fn set_object_id(&mut self, object_id: Uuid) {
740        self.object_id = object_id;
741    }
742
743    pub fn set_object_type(&mut self, object_type: ObjectType) -> Result<(), String> {
744        self.object_type = object_type;
745        Ok(())
746    }
747
748    pub fn set_header_version(&mut self, header_version: u32) -> Result<(), String> {
749        if header_version == 0 {
750            return Err("header_version must be greater than 0".to_string());
751        }
752        self.header_version = header_version;
753        Ok(())
754    }
755
756    pub fn set_schema_version(&mut self, schema_version: u32) -> Result<(), String> {
757        if schema_version == 0 {
758            return Err("schema_version must be greater than 0".to_string());
759        }
760        self.schema_version = schema_version;
761        Ok(())
762    }
763
764    pub fn set_created_at(&mut self, created_at: DateTime<Utc>) {
765        self.created_at = created_at;
766    }
767
768    pub fn set_updated_at(&mut self, updated_at: DateTime<Utc>) {
769        self.updated_at = updated_at;
770    }
771
772    pub fn set_visibility(&mut self, visibility: Visibility) {
773        self.visibility = visibility;
774    }
775
776    /// Accessor for checksum
777    pub fn checksum(&self) -> Option<&IntegrityHash> {
778        self.checksum.as_ref()
779    }
780
781    /// Seal the header by calculating and setting the checksum of the provided object.
782    /// The checksum field is temporarily cleared to keep sealing idempotent.
783    /// Also updates `updated_at` to the current time, since sealing
784    /// represents a semantic modification of the object.
785    ///
786    /// This is typically called just before storing the object to ensure `checksum` matches content.
787    pub fn seal<T: Serialize>(&mut self, object: &T) -> Result<(), serde_json::Error> {
788        let previous_checksum = self.checksum.take();
789        match compute_integrity_hash(object) {
790            Ok(checksum) => {
791                self.checksum = Some(checksum);
792                self.updated_at = Utc::now();
793                Ok(())
794            }
795            Err(err) => {
796                self.checksum = previous_checksum;
797                Err(err)
798            }
799        }
800    }
801}
802
803#[cfg(test)]
804mod tests {
805    use chrono::{DateTime, Utc};
806    use uuid::Uuid;
807
808    use crate::internal::object::types::{
809        ActorKind, ActorRef, ArtifactRef, Header, IntegrityHash, ObjectType,
810    };
811
812    /// Verify ObjectType::Blob converts to its ASCII byte representation "blob".
813    #[test]
814    fn test_object_type_to_data() {
815        let blob = ObjectType::Blob;
816        let blob_bytes = blob.to_data().unwrap();
817        assert_eq!(blob_bytes, vec![0x62, 0x6c, 0x6f, 0x62]);
818    }
819
820    /// Verify parsing "tree" string returns ObjectType::Tree.
821    #[test]
822    fn test_object_type_from_string() {
823        assert_eq!(ObjectType::from_string("blob").unwrap(), ObjectType::Blob);
824        assert_eq!(ObjectType::from_string("tree").unwrap(), ObjectType::Tree);
825        assert_eq!(
826            ObjectType::from_string("commit").unwrap(),
827            ObjectType::Commit
828        );
829        assert_eq!(ObjectType::from_string("tag").unwrap(), ObjectType::Tag);
830        assert_eq!(
831            ObjectType::from_string("snapshot").unwrap(),
832            ObjectType::ContextSnapshot
833        );
834        assert_eq!(
835            ObjectType::from_string("decision").unwrap(),
836            ObjectType::Decision
837        );
838        assert_eq!(
839            ObjectType::from_string("evidence").unwrap(),
840            ObjectType::Evidence
841        );
842        assert_eq!(
843            ObjectType::from_string("patchset").unwrap(),
844            ObjectType::PatchSet
845        );
846        assert_eq!(ObjectType::from_string("plan").unwrap(), ObjectType::Plan);
847        assert_eq!(
848            ObjectType::from_string("provenance").unwrap(),
849            ObjectType::Provenance
850        );
851        assert_eq!(ObjectType::from_string("run").unwrap(), ObjectType::Run);
852        assert_eq!(ObjectType::from_string("task").unwrap(), ObjectType::Task);
853        assert_eq!(
854            ObjectType::from_string("invocation").unwrap(),
855            ObjectType::ToolInvocation
856        );
857
858        assert!(ObjectType::from_string("invalid_type").is_err());
859    }
860
861    /// Verify ObjectType::Commit converts to pack type number 1.
862    #[test]
863    fn test_object_type_to_u8() {
864        let commit = ObjectType::Commit;
865        let commit_number = commit.to_u8();
866        assert_eq!(commit_number, 1);
867    }
868
869    /// Verify pack type number 4 parses to ObjectType::Tag.
870    #[test]
871    fn test_object_type_from_u8() {
872        let tag_number = 4;
873        let tag = ObjectType::from_u8(tag_number).unwrap();
874        assert_eq!(tag, ObjectType::Tag);
875    }
876
877    #[test]
878    fn test_header_serialization() {
879        let actor = ActorRef::human("jackie").expect("actor");
880        let header = Header::new(ObjectType::Task, actor).expect("header");
881
882        let json = serde_json::to_string(&header).unwrap();
883        let deserialized: Header = serde_json::from_str(&json).unwrap();
884
885        assert_eq!(header.object_id(), deserialized.object_id());
886        assert_eq!(header.object_type(), deserialized.object_type());
887        assert_eq!(header.header_version(), deserialized.header_version());
888    }
889
890    #[test]
891    fn test_header_version_new_uses_current() {
892        let actor = ActorRef::human("jackie").expect("actor");
893        let header = Header::new(ObjectType::Task, actor).expect("header");
894        assert_eq!(
895            header.header_version(),
896            crate::internal::object::types::CURRENT_HEADER_VERSION
897        );
898    }
899
900    #[test]
901    fn test_header_version_defaults_on_missing() {
902        // Simulate old serialized data without header_version
903        let json = r#"{
904            "object_id": "01234567-89ab-cdef-0123-456789abcdef",
905            "object_type": "task",
906            "schema_version": 1,
907            "created_at": "2026-01-01T00:00:00Z",
908            "updated_at": "2026-01-01T00:00:00Z",
909            "created_by": {"kind": "human", "id": "jackie"},
910            "visibility": "private"
911        }"#;
912        let header: Header = serde_json::from_str(json).unwrap();
913        assert_eq!(header.header_version(), 1);
914    }
915
916    #[test]
917    fn test_header_version_setter_rejects_zero() {
918        let actor = ActorRef::human("jackie").expect("actor");
919        let mut header = Header::new(ObjectType::Task, actor).expect("header");
920        assert!(header.set_header_version(0).is_err());
921        assert!(header.set_header_version(3).is_ok());
922        assert_eq!(header.header_version(), 3);
923    }
924
925    #[test]
926    fn test_actor_ref() {
927        let actor = ActorRef::agent("coder").expect("actor");
928        assert_eq!(actor.kind(), &ActorKind::Agent);
929        assert_eq!(actor.id(), "coder");
930
931        let sys = ActorRef::system("scheduler").expect("system");
932        assert_eq!(sys.kind(), &ActorKind::System);
933
934        let client = ActorRef::mcp_client("vscode").expect("client");
935        assert_eq!(client.kind(), &ActorKind::McpClient);
936        assert!(client.ensure_mcp_client().is_ok());
937
938        let non_mcp = ActorRef::human("jackie").expect("actor");
939        assert!(non_mcp.ensure_mcp_client().is_err());
940    }
941
942    #[test]
943    fn test_actor_kind_serialization() {
944        let k = ActorKind::McpClient;
945        let s = serde_json::to_string(&k).unwrap();
946        assert_eq!(s, "\"mcp_client\"");
947
948        let k2: ActorKind = serde_json::from_str("\"system\"").unwrap();
949        assert_eq!(k2, ActorKind::System);
950    }
951
952    #[test]
953    fn test_header_checksum() {
954        let actor = ActorRef::human("jackie").expect("actor");
955        let mut header = Header::new(ObjectType::Task, actor).expect("header");
956        // Fix time for deterministic checksum
957        header.set_created_at(
958            DateTime::parse_from_rfc3339("2026-02-10T00:00:00Z")
959                .unwrap()
960                .with_timezone(&Utc),
961        );
962        header.set_object_id(Uuid::from_u128(0x00000000000000000000000000000001));
963
964        let checksum =
965            crate::internal::object::integrity::compute_integrity_hash(&header).expect("checksum");
966        assert_eq!(checksum.to_hex().len(), 64); // SHA256 length
967
968        // Ensure changes change checksum
969        header
970            .set_object_type(ObjectType::Run)
971            .expect("object_type");
972        let checksum2 =
973            crate::internal::object::integrity::compute_integrity_hash(&header).expect("checksum");
974        assert_ne!(checksum, checksum2);
975    }
976
977    #[test]
978    fn test_artifact_checksum() {
979        let content = b"hello world";
980        let hash = ArtifactRef::compute_hash(content);
981        // echo -n "hello world" | shasum -a 256
982        let expected_str = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
983        assert_eq!(hash.to_hex(), expected_str);
984
985        let artifact = ArtifactRef::new("s3", "key")
986            .expect("artifact")
987            .with_hash(hash);
988        assert_eq!(artifact.hash(), Some(&hash));
989
990        // Integrity check
991        assert!(artifact.verify_integrity(content).unwrap());
992        assert!(!artifact.verify_integrity(b"wrong").unwrap());
993
994        // Deduplication
995        let artifact2 = ArtifactRef::new("local", "other/path")
996            .expect("artifact")
997            .with_hash(IntegrityHash::compute(content));
998        assert_eq!(artifact.content_eq(&artifact2), Some(true));
999
1000        let artifact3 = ArtifactRef::new("s3", "diff")
1001            .expect("artifact")
1002            .with_hash(ArtifactRef::compute_hash(b"diff"));
1003        assert_eq!(artifact.content_eq(&artifact3), Some(false));
1004    }
1005
1006    #[test]
1007    fn test_invalid_checksum() {
1008        let result = ArtifactRef::new("s3", "key")
1009            .expect("artifact")
1010            .with_hash_hex("bad_hash");
1011        assert!(result.is_err());
1012    }
1013
1014    #[test]
1015    fn test_header_seal() {
1016        let actor = ActorRef::human("jackie").expect("actor");
1017        let mut header = Header::new(ObjectType::Task, actor).expect("header");
1018
1019        let content = serde_json::json!({"key": "value"});
1020        header.seal(&content).expect("seal");
1021
1022        assert!(header.checksum().is_some());
1023        let expected =
1024            crate::internal::object::integrity::compute_integrity_hash(&content).expect("checksum");
1025        assert_eq!(header.checksum().expect("checksum"), &expected);
1026    }
1027
1028    #[test]
1029    fn test_header_updated_at_on_seal() {
1030        let actor = ActorRef::human("jackie").expect("actor");
1031        let mut header = Header::new(ObjectType::Task, actor).expect("header");
1032
1033        let before = header.updated_at();
1034        let content = serde_json::json!({"key": "value"});
1035
1036        header.seal(&content).expect("seal");
1037
1038        let after = header.updated_at();
1039        assert!(after >= before);
1040    }
1041
1042    #[test]
1043    fn test_empty_actor_id() {
1044        let result = ActorRef::new(ActorKind::Human, "  ");
1045        assert!(result.is_err());
1046    }
1047
1048    #[test]
1049    fn test_artifact_expiration() {
1050        let mut artifact = ArtifactRef::new("s3", "key").expect("artifact");
1051        assert!(!artifact.is_expired());
1052
1053        artifact.set_expires_at(Some(Utc::now() - chrono::Duration::hours(1)));
1054        assert!(artifact.is_expired());
1055
1056        artifact.set_expires_at(Some(Utc::now() + chrono::Duration::hours(1)));
1057        assert!(!artifact.is_expired());
1058    }
1059}