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    ToolInvocation,
66}
67
68const COMMIT_OBJECT_TYPE: &[u8] = b"commit";
69const TREE_OBJECT_TYPE: &[u8] = b"tree";
70const BLOB_OBJECT_TYPE: &[u8] = b"blob";
71const TAG_OBJECT_TYPE: &[u8] = b"tag";
72const CONTEXT_SNAPSHOT_OBJECT_TYPE: &[u8] = b"snapshot";
73const DECISION_OBJECT_TYPE: &[u8] = b"decision";
74const EVIDENCE_OBJECT_TYPE: &[u8] = b"evidence";
75const PATCH_SET_OBJECT_TYPE: &[u8] = b"patchset";
76const PLAN_OBJECT_TYPE: &[u8] = b"plan";
77const PROVENANCE_OBJECT_TYPE: &[u8] = b"provenance";
78const RUN_OBJECT_TYPE: &[u8] = b"run";
79const TASK_OBJECT_TYPE: &[u8] = b"task";
80const TOOL_INVOCATION_OBJECT_TYPE: &[u8] = b"invocation";
81
82/// Display trait for Git objects type
83impl Display for ObjectType {
84    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
85        match self {
86            ObjectType::Blob => write!(f, "blob"),
87            ObjectType::Tree => write!(f, "tree"),
88            ObjectType::Commit => write!(f, "commit"),
89            ObjectType::Tag => write!(f, "tag"),
90            ObjectType::OffsetZstdelta => write!(f, "OffsetZstdelta"),
91            ObjectType::OffsetDelta => write!(f, "OffsetDelta"),
92            ObjectType::HashDelta => write!(f, "HashDelta"),
93            ObjectType::ContextSnapshot => write!(f, "snapshot"),
94            ObjectType::Decision => write!(f, "decision"),
95            ObjectType::Evidence => write!(f, "evidence"),
96            ObjectType::PatchSet => write!(f, "patchset"),
97            ObjectType::Plan => write!(f, "plan"),
98            ObjectType::Provenance => write!(f, "provenance"),
99            ObjectType::Run => write!(f, "run"),
100            ObjectType::Task => write!(f, "task"),
101            ObjectType::ToolInvocation => write!(f, "invocation"),
102        }
103    }
104}
105
106/// Display trait for Git objects type
107impl ObjectType {
108    /// Convert object type to 3-bit pack header type id.
109    ///
110    /// Git pack headers only carry 3 type bits (values 0..=7). AI object
111    /// types are not representable in this field and must not be written
112    /// as regular base objects in a pack entry.
113    pub fn to_pack_type_u8(&self) -> Result<u8, GitError> {
114        match self {
115            ObjectType::Commit => Ok(1),
116            ObjectType::Tree => Ok(2),
117            ObjectType::Blob => Ok(3),
118            ObjectType::Tag => Ok(4),
119            ObjectType::OffsetZstdelta => Ok(5),
120            ObjectType::OffsetDelta => Ok(6),
121            ObjectType::HashDelta => Ok(7),
122            _ => Err(GitError::PackEncodeError(format!(
123                "object type `{}` cannot be encoded in pack header type bits",
124                self
125            ))),
126        }
127    }
128
129    /// Decode 3-bit pack header type id to object type.
130    pub fn from_pack_type_u8(number: u8) -> Result<ObjectType, GitError> {
131        match number {
132            1 => Ok(ObjectType::Commit),
133            2 => Ok(ObjectType::Tree),
134            3 => Ok(ObjectType::Blob),
135            4 => Ok(ObjectType::Tag),
136            5 => Ok(ObjectType::OffsetZstdelta),
137            6 => Ok(ObjectType::OffsetDelta),
138            7 => Ok(ObjectType::HashDelta),
139            _ => Err(GitError::InvalidObjectType(format!(
140                "Invalid pack object type number: {number}"
141            ))),
142        }
143    }
144
145    pub fn to_bytes(&self) -> &[u8] {
146        match self {
147            ObjectType::Commit => COMMIT_OBJECT_TYPE,
148            ObjectType::Tree => TREE_OBJECT_TYPE,
149            ObjectType::Blob => BLOB_OBJECT_TYPE,
150            ObjectType::Tag => TAG_OBJECT_TYPE,
151            ObjectType::ContextSnapshot => CONTEXT_SNAPSHOT_OBJECT_TYPE,
152            ObjectType::Decision => DECISION_OBJECT_TYPE,
153            ObjectType::Evidence => EVIDENCE_OBJECT_TYPE,
154            ObjectType::PatchSet => PATCH_SET_OBJECT_TYPE,
155            ObjectType::Plan => PLAN_OBJECT_TYPE,
156            ObjectType::Provenance => PROVENANCE_OBJECT_TYPE,
157            ObjectType::Run => RUN_OBJECT_TYPE,
158            ObjectType::Task => TASK_OBJECT_TYPE,
159            ObjectType::ToolInvocation => TOOL_INVOCATION_OBJECT_TYPE,
160            _ => panic!("can put compute the delta hash value"),
161        }
162    }
163
164    /// Parses a string representation of a Git object type and returns an ObjectType value
165    pub fn from_string(s: &str) -> Result<ObjectType, GitError> {
166        match s {
167            "blob" => Ok(ObjectType::Blob),
168            "tree" => Ok(ObjectType::Tree),
169            "commit" => Ok(ObjectType::Commit),
170            "tag" => Ok(ObjectType::Tag),
171            "snapshot" => Ok(ObjectType::ContextSnapshot),
172            "decision" => Ok(ObjectType::Decision),
173            "evidence" => Ok(ObjectType::Evidence),
174            "patchset" => Ok(ObjectType::PatchSet),
175            "plan" => Ok(ObjectType::Plan),
176            "provenance" => Ok(ObjectType::Provenance),
177            "run" => Ok(ObjectType::Run),
178            "task" => Ok(ObjectType::Task),
179            "invocation" => Ok(ObjectType::ToolInvocation),
180            _ => Err(GitError::InvalidObjectType(s.to_string())),
181        }
182    }
183
184    /// Convert an object type to a byte array.
185    pub fn to_data(self) -> Result<Vec<u8>, GitError> {
186        match self {
187            ObjectType::Blob => Ok(vec![0x62, 0x6c, 0x6f, 0x62]), // blob
188            ObjectType::Tree => Ok(vec![0x74, 0x72, 0x65, 0x65]), // tree
189            ObjectType::Commit => Ok(vec![0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74]), // commit
190            ObjectType::Tag => Ok(vec![0x74, 0x61, 0x67]),        // tag
191            ObjectType::ContextSnapshot => Ok(vec![0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74]), // snapshot
192            ObjectType::Decision => Ok(vec![0x64, 0x65, 0x63, 0x69, 0x73, 0x69, 0x6f, 0x6e]), // decision
193            ObjectType::Evidence => Ok(vec![0x65, 0x76, 0x69, 0x64, 0x65, 0x6e, 0x63, 0x65]), // evidence
194            ObjectType::PatchSet => Ok(vec![0x70, 0x61, 0x74, 0x63, 0x68, 0x73, 0x65, 0x74]), // patchset
195            ObjectType::Plan => Ok(vec![0x70, 0x6c, 0x61, 0x6e]), // plan
196            ObjectType::Provenance => Ok(vec![
197                0x70, 0x72, 0x6f, 0x76, 0x65, 0x6e, 0x61, 0x6e, 0x63, 0x65,
198            ]), // provenance
199            ObjectType::Run => Ok(vec![0x72, 0x75, 0x6e]),        // run
200            ObjectType::Task => Ok(vec![0x74, 0x61, 0x73, 0x6b]), // task
201            ObjectType::ToolInvocation => Ok(vec![
202                0x69, 0x6e, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
203            ]), // invocation
204            _ => Err(GitError::InvalidObjectType(self.to_string())),
205        }
206    }
207
208    /// Convert an object type to a number.
209    pub fn to_u8(&self) -> u8 {
210        match self {
211            ObjectType::Commit => 1,
212            ObjectType::Tree => 2,
213            ObjectType::Blob => 3,
214            ObjectType::Tag => 4,
215            ObjectType::OffsetZstdelta => 5, // Type 5 is reserved in standard Git packs; we use it for Zstd delta objects.
216            ObjectType::OffsetDelta => 6,
217            ObjectType::HashDelta => 7,
218            ObjectType::ContextSnapshot => 8,
219            ObjectType::Decision => 9,
220            ObjectType::Evidence => 10,
221            ObjectType::PatchSet => 11,
222            ObjectType::Plan => 12,
223            ObjectType::Provenance => 13,
224            ObjectType::Run => 14,
225            ObjectType::Task => 15,
226            ObjectType::ToolInvocation => 16,
227        }
228    }
229
230    /// Convert a number to an object type.
231    pub fn from_u8(number: u8) -> Result<ObjectType, GitError> {
232        match number {
233            1 => Ok(ObjectType::Commit),
234            2 => Ok(ObjectType::Tree),
235            3 => Ok(ObjectType::Blob),
236            4 => Ok(ObjectType::Tag),
237            5 => Ok(ObjectType::OffsetZstdelta),
238            6 => Ok(ObjectType::OffsetDelta),
239            7 => Ok(ObjectType::HashDelta),
240            8 => Ok(ObjectType::ContextSnapshot),
241            9 => Ok(ObjectType::Decision),
242            10 => Ok(ObjectType::Evidence),
243            11 => Ok(ObjectType::PatchSet),
244            12 => Ok(ObjectType::Plan),
245            13 => Ok(ObjectType::Provenance),
246            14 => Ok(ObjectType::Run),
247            15 => Ok(ObjectType::Task),
248            16 => Ok(ObjectType::ToolInvocation),
249            _ => Err(GitError::InvalidObjectType(format!(
250                "Invalid object type number: {number}"
251            ))),
252        }
253    }
254
255    pub fn is_base(&self) -> bool {
256        match self {
257            ObjectType::Commit => true,
258            ObjectType::Tree => true,
259            ObjectType::Blob => true,
260            ObjectType::Tag => true,
261            ObjectType::HashDelta => false,
262            ObjectType::OffsetZstdelta => false,
263            ObjectType::OffsetDelta => false,
264            ObjectType::ContextSnapshot => true,
265            ObjectType::Decision => true,
266            ObjectType::Evidence => true,
267            ObjectType::PatchSet => true,
268            ObjectType::Plan => true,
269            ObjectType::Provenance => true,
270            ObjectType::Run => true,
271            ObjectType::Task => true,
272            ObjectType::ToolInvocation => true,
273        }
274    }
275}
276
277/// Actor kind enum
278#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
279#[serde(rename_all = "snake_case")]
280pub enum ActorKind {
281    Human,
282    Agent,
283    System,
284    McpClient,
285    #[serde(untagged)]
286    Other(String),
287}
288
289impl fmt::Display for ActorKind {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        match self {
292            ActorKind::Human => write!(f, "human"),
293            ActorKind::Agent => write!(f, "agent"),
294            ActorKind::System => write!(f, "system"),
295            ActorKind::McpClient => write!(f, "mcp_client"),
296            ActorKind::Other(s) => write!(f, "{}", s),
297        }
298    }
299}
300
301impl From<String> for ActorKind {
302    fn from(s: String) -> Self {
303        match s.as_str() {
304            "human" => ActorKind::Human,
305            "agent" => ActorKind::Agent,
306            "system" => ActorKind::System,
307            "mcp_client" => ActorKind::McpClient,
308            _ => ActorKind::Other(s),
309        }
310    }
311}
312
313impl From<&str> for ActorKind {
314    fn from(s: &str) -> Self {
315        match s {
316            "human" => ActorKind::Human,
317            "agent" => ActorKind::Agent,
318            "system" => ActorKind::System,
319            "mcp_client" => ActorKind::McpClient,
320            _ => ActorKind::Other(s.to_string()),
321        }
322    }
323}
324
325/// Actor reference (who created/triggered).
326#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
327pub struct ActorRef {
328    /// Kind: human/agent/system/mcp_client
329    kind: ActorKind,
330    /// Subject ID (user/agent name or client ID)
331    id: String,
332    /// Display name (optional)
333    display_name: Option<String>,
334    /// Auth context (optional, Libra usually empty)
335    auth_context: Option<String>,
336}
337
338impl ActorRef {
339    /// Create a new ActorRef with validation.
340    pub fn new(kind: impl Into<ActorKind>, id: impl Into<String>) -> Result<Self, String> {
341        let id_str = id.into();
342        if id_str.trim().is_empty() {
343            return Err("Actor ID cannot be empty".to_string());
344        }
345        Ok(Self {
346            kind: kind.into(),
347            id: id_str,
348            display_name: None,
349            auth_context: None,
350        })
351    }
352
353    /// Create an MCP client actor reference (MCP writes must use this).
354    pub fn new_for_mcp(id: impl Into<String>) -> Result<Self, String> {
355        Self::new(ActorKind::McpClient, id)
356    }
357
358    /// Validate that this actor is an MCP client.
359    pub fn ensure_mcp_client(&self) -> Result<(), String> {
360        if self.kind != ActorKind::McpClient {
361            return Err("MCP writes must use mcp_client actor kind".to_string());
362        }
363        Ok(())
364    }
365
366    pub fn kind(&self) -> &ActorKind {
367        &self.kind
368    }
369
370    pub fn id(&self) -> &str {
371        &self.id
372    }
373
374    pub fn display_name(&self) -> Option<&str> {
375        self.display_name.as_deref()
376    }
377
378    pub fn auth_context(&self) -> Option<&str> {
379        self.auth_context.as_deref()
380    }
381
382    pub fn set_display_name(&mut self, display_name: Option<String>) {
383        self.display_name = display_name;
384    }
385
386    pub fn set_auth_context(&mut self, auth_context: Option<String>) {
387        self.auth_context = auth_context;
388    }
389
390    /// Create a human actor reference.
391    pub fn human(id: impl Into<String>) -> Result<Self, String> {
392        Self::new(ActorKind::Human, id)
393    }
394
395    /// Create an agent actor reference.
396    pub fn agent(name: impl Into<String>) -> Result<Self, String> {
397        Self::new(ActorKind::Agent, name)
398    }
399
400    /// Create a system component actor reference.
401    pub fn system(component: impl Into<String>) -> Result<Self, String> {
402        Self::new(ActorKind::System, component)
403    }
404
405    /// Create an MCP client actor reference.
406    pub fn mcp_client(client_id: impl Into<String>) -> Result<Self, String> {
407        Self::new(ActorKind::McpClient, client_id)
408    }
409}
410
411/// Artifact reference (external content).
412#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
413pub struct ArtifactRef {
414    /// Store type: local_fs/s3
415    store: String,
416    /// Storage key (e.g., path or object key)
417    key: String,
418    /// MIME type (optional)
419    content_type: Option<String>,
420    /// Size in bytes (optional)
421    size_bytes: Option<u64>,
422    /// Content hash (strongly recommended)
423    hash: Option<IntegrityHash>,
424    /// Expiration time (optional)
425    expires_at: Option<DateTime<Utc>>,
426}
427
428impl ArtifactRef {
429    pub fn new(store: impl Into<String>, key: impl Into<String>) -> Result<Self, String> {
430        let store = store.into();
431        let key = key.into();
432        if store.trim().is_empty() {
433            return Err("store cannot be empty".to_string());
434        }
435        if key.trim().is_empty() {
436            return Err("key cannot be empty".to_string());
437        }
438        Ok(Self {
439            store,
440            key,
441            content_type: None,
442            size_bytes: None,
443            hash: None,
444            expires_at: None,
445        })
446    }
447
448    pub fn store(&self) -> &str {
449        &self.store
450    }
451
452    pub fn key(&self) -> &str {
453        &self.key
454    }
455
456    pub fn content_type(&self) -> Option<&str> {
457        self.content_type.as_deref()
458    }
459
460    pub fn size_bytes(&self) -> Option<u64> {
461        self.size_bytes
462    }
463
464    pub fn hash(&self) -> Option<&IntegrityHash> {
465        self.hash.as_ref()
466    }
467
468    pub fn expires_at(&self) -> Option<DateTime<Utc>> {
469        self.expires_at
470    }
471
472    /// Calculate hash for the given content bytes.
473    pub fn compute_hash(content: &[u8]) -> IntegrityHash {
474        IntegrityHash::compute(content)
475    }
476
477    /// Set the hash directly.
478    pub fn with_hash(mut self, hash: IntegrityHash) -> Self {
479        self.hash = Some(hash);
480        self
481    }
482
483    /// Set the hash from a hex string.
484    pub fn with_hash_hex(mut self, hash: impl AsRef<str>) -> Result<Self, String> {
485        let hash = hash.as_ref().parse()?;
486        self.hash = Some(hash);
487        Ok(self)
488    }
489
490    pub fn set_content_type(&mut self, content_type: Option<String>) {
491        self.content_type = content_type;
492    }
493
494    pub fn set_size_bytes(&mut self, size_bytes: Option<u64>) {
495        self.size_bytes = size_bytes;
496    }
497
498    pub fn set_expires_at(&mut self, expires_at: Option<DateTime<Utc>>) {
499        self.expires_at = expires_at;
500    }
501
502    /// Verify if the provided content matches the stored checksum
503    #[must_use = "handle integrity verification result"]
504    pub fn verify_integrity(&self, content: &[u8]) -> Result<bool, String> {
505        let stored_hash = self
506            .hash
507            .as_ref()
508            .ok_or_else(|| "No hash stored in ArtifactRef".to_string())?;
509
510        Ok(IntegrityHash::compute(content) == *stored_hash)
511    }
512
513    /// Check if two artifacts have the same content based on checksum
514    #[must_use]
515    pub fn content_eq(&self, other: &Self) -> Option<bool> {
516        match (&self.hash, &other.hash) {
517            (Some(a), Some(b)) => Some(a == b),
518            _ => None,
519        }
520    }
521
522    /// Check if the artifact has expired
523    #[must_use]
524    pub fn is_expired(&self) -> bool {
525        if let Some(expires_at) = self.expires_at {
526            expires_at < Utc::now()
527        } else {
528            false
529        }
530    }
531}
532
533/// Header shared by all AI Process Objects.
534///
535/// Contains standard metadata like ID, type, creator, and timestamps.
536///
537/// # Usage
538///
539/// Every AI object struct should flatten this header:
540///
541/// ```rust,ignore
542/// #[derive(Serialize, Deserialize)]
543/// pub struct MyObject {
544///     #[serde(flatten)]
545///     header: Header,
546///     // specific fields...
547/// }
548/// ```
549#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
550pub struct Header {
551    /// Global unique ID (UUID v7)
552    object_id: Uuid,
553    /// Object type (task/run/patchset/...)
554    object_type: ObjectType,
555    /// Model version
556    schema_version: u32,
557    /// Repository identifier
558    repo_id: Uuid,
559    /// Creation time
560    created_at: DateTime<Utc>,
561    /// Creator
562    created_by: ActorRef,
563    /// Visibility (fixed to private for Libra)
564    visibility: Visibility,
565    /// Search tags
566    #[serde(default)]
567    tags: HashMap<String, String>,
568    /// External ID mapping
569    #[serde(default)]
570    external_ids: HashMap<String, String>,
571    /// Content checksum (optional)
572    #[serde(default)]
573    checksum: Option<IntegrityHash>,
574}
575
576impl Header {
577    /// Create a new Header with default values.
578    ///
579    /// # Arguments
580    ///
581    /// * `object_type` - The specific type of the AI object.
582    /// * `repo_id` - The UUID of the repository this object belongs to.
583    /// * `created_by` - The actor (human/agent) creating this object.
584    pub fn new(
585        object_type: ObjectType,
586        repo_id: Uuid,
587        created_by: ActorRef,
588    ) -> Result<Self, String> {
589        Ok(Self {
590            object_id: Uuid::now_v7(),
591            object_type,
592            schema_version: 1,
593            repo_id,
594            created_at: Utc::now(),
595            created_by,
596            visibility: Visibility::Private,
597            tags: HashMap::new(),
598            external_ids: HashMap::new(),
599            checksum: None,
600        })
601    }
602
603    pub fn object_id(&self) -> Uuid {
604        self.object_id
605    }
606
607    pub fn object_type(&self) -> &ObjectType {
608        &self.object_type
609    }
610
611    pub fn schema_version(&self) -> u32 {
612        self.schema_version
613    }
614
615    pub fn repo_id(&self) -> Uuid {
616        self.repo_id
617    }
618
619    pub fn created_at(&self) -> DateTime<Utc> {
620        self.created_at
621    }
622
623    pub fn created_by(&self) -> &ActorRef {
624        &self.created_by
625    }
626
627    pub fn visibility(&self) -> &Visibility {
628        &self.visibility
629    }
630
631    pub fn tags(&self) -> &HashMap<String, String> {
632        &self.tags
633    }
634
635    pub fn tags_mut(&mut self) -> &mut HashMap<String, String> {
636        &mut self.tags
637    }
638
639    pub fn external_ids(&self) -> &HashMap<String, String> {
640        &self.external_ids
641    }
642
643    pub fn external_ids_mut(&mut self) -> &mut HashMap<String, String> {
644        &mut self.external_ids
645    }
646
647    pub fn set_object_id(&mut self, object_id: Uuid) {
648        self.object_id = object_id;
649    }
650
651    pub fn set_object_type(&mut self, object_type: ObjectType) -> Result<(), String> {
652        self.object_type = object_type;
653        Ok(())
654    }
655
656    pub fn set_schema_version(&mut self, schema_version: u32) -> Result<(), String> {
657        if schema_version == 0 {
658            return Err("schema_version must be greater than 0".to_string());
659        }
660        self.schema_version = schema_version;
661        Ok(())
662    }
663
664    pub fn set_created_at(&mut self, created_at: DateTime<Utc>) {
665        self.created_at = created_at;
666    }
667
668    pub fn set_visibility(&mut self, visibility: Visibility) {
669        self.visibility = visibility;
670    }
671
672    /// Accessor for checksum
673    pub fn checksum(&self) -> Option<&IntegrityHash> {
674        self.checksum.as_ref()
675    }
676
677    /// Seal the header by calculating and setting the checksum of the provided object.
678    /// The checksum field is temporarily cleared to keep sealing idempotent.
679    ///
680    /// This is typically called just before storing the object to ensure `checksum` matches content.
681    pub fn seal<T: Serialize>(&mut self, object: &T) -> Result<(), serde_json::Error> {
682        let previous = self.checksum.take();
683        match compute_integrity_hash(object) {
684            Ok(checksum) => {
685                self.checksum = Some(checksum);
686                Ok(())
687            }
688            Err(err) => {
689                self.checksum = previous;
690                Err(err)
691            }
692        }
693    }
694}
695
696#[cfg(test)]
697mod tests {
698    use chrono::{DateTime, Utc};
699    use uuid::Uuid;
700
701    use crate::internal::object::types::{
702        ActorKind, ActorRef, ArtifactRef, Header, IntegrityHash, ObjectType,
703    };
704
705    /// Verify ObjectType::Blob converts to its ASCII byte representation "blob".
706    #[test]
707    fn test_object_type_to_data() {
708        let blob = ObjectType::Blob;
709        let blob_bytes = blob.to_data().unwrap();
710        assert_eq!(blob_bytes, vec![0x62, 0x6c, 0x6f, 0x62]);
711    }
712
713    /// Verify parsing "tree" string returns ObjectType::Tree.
714    #[test]
715    fn test_object_type_from_string() {
716        assert_eq!(ObjectType::from_string("blob").unwrap(), ObjectType::Blob);
717        assert_eq!(ObjectType::from_string("tree").unwrap(), ObjectType::Tree);
718        assert_eq!(
719            ObjectType::from_string("commit").unwrap(),
720            ObjectType::Commit
721        );
722        assert_eq!(ObjectType::from_string("tag").unwrap(), ObjectType::Tag);
723        assert_eq!(
724            ObjectType::from_string("snapshot").unwrap(),
725            ObjectType::ContextSnapshot
726        );
727        assert_eq!(
728            ObjectType::from_string("decision").unwrap(),
729            ObjectType::Decision
730        );
731        assert_eq!(
732            ObjectType::from_string("evidence").unwrap(),
733            ObjectType::Evidence
734        );
735        assert_eq!(
736            ObjectType::from_string("patchset").unwrap(),
737            ObjectType::PatchSet
738        );
739        assert_eq!(ObjectType::from_string("plan").unwrap(), ObjectType::Plan);
740        assert_eq!(
741            ObjectType::from_string("provenance").unwrap(),
742            ObjectType::Provenance
743        );
744        assert_eq!(ObjectType::from_string("run").unwrap(), ObjectType::Run);
745        assert_eq!(ObjectType::from_string("task").unwrap(), ObjectType::Task);
746        assert_eq!(
747            ObjectType::from_string("invocation").unwrap(),
748            ObjectType::ToolInvocation
749        );
750
751        assert!(ObjectType::from_string("invalid_type").is_err());
752    }
753
754    /// Verify ObjectType::Commit converts to pack type number 1.
755    #[test]
756    fn test_object_type_to_u8() {
757        let commit = ObjectType::Commit;
758        let commit_number = commit.to_u8();
759        assert_eq!(commit_number, 1);
760    }
761
762    /// Verify pack type number 4 parses to ObjectType::Tag.
763    #[test]
764    fn test_object_type_from_u8() {
765        let tag_number = 4;
766        let tag = ObjectType::from_u8(tag_number).unwrap();
767        assert_eq!(tag, ObjectType::Tag);
768    }
769
770    #[test]
771    fn test_header_serialization() {
772        let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
773        let actor = ActorRef::human("jackie").expect("actor");
774        let header = Header::new(ObjectType::Task, repo_id, actor).expect("header");
775
776        let json = serde_json::to_string(&header).unwrap();
777        let deserialized: Header = serde_json::from_str(&json).unwrap();
778
779        assert_eq!(header.object_id(), deserialized.object_id());
780        assert_eq!(header.object_type(), deserialized.object_type());
781        assert_eq!(header.repo_id(), deserialized.repo_id());
782    }
783
784    #[test]
785    fn test_actor_ref() {
786        let actor = ActorRef::agent("coder").expect("actor");
787        assert_eq!(actor.kind(), &ActorKind::Agent);
788        assert_eq!(actor.id(), "coder");
789
790        let sys = ActorRef::system("scheduler").expect("system");
791        assert_eq!(sys.kind(), &ActorKind::System);
792
793        let client = ActorRef::mcp_client("vscode").expect("client");
794        assert_eq!(client.kind(), &ActorKind::McpClient);
795        assert!(client.ensure_mcp_client().is_ok());
796
797        let non_mcp = ActorRef::human("jackie").expect("actor");
798        assert!(non_mcp.ensure_mcp_client().is_err());
799    }
800
801    #[test]
802    fn test_actor_kind_serialization() {
803        let k = ActorKind::McpClient;
804        let s = serde_json::to_string(&k).unwrap();
805        assert_eq!(s, "\"mcp_client\"");
806
807        let k2: ActorKind = serde_json::from_str("\"system\"").unwrap();
808        assert_eq!(k2, ActorKind::System);
809    }
810
811    #[test]
812    fn test_header_checksum() {
813        let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
814        let actor = ActorRef::human("jackie").expect("actor");
815        let mut header = Header::new(ObjectType::Task, repo_id, actor).expect("header");
816        // Fix time for deterministic checksum
817        header.set_created_at(
818            DateTime::parse_from_rfc3339("2026-02-10T00:00:00Z")
819                .unwrap()
820                .with_timezone(&Utc),
821        );
822        header.set_object_id(Uuid::from_u128(0x00000000000000000000000000000001));
823
824        let checksum =
825            crate::internal::object::integrity::compute_integrity_hash(&header).expect("checksum");
826        assert_eq!(checksum.to_hex().len(), 64); // SHA256 length
827
828        // Ensure changes change checksum
829        header
830            .set_object_type(ObjectType::Run)
831            .expect("object_type");
832        let checksum2 =
833            crate::internal::object::integrity::compute_integrity_hash(&header).expect("checksum");
834        assert_ne!(checksum, checksum2);
835    }
836
837    #[test]
838    fn test_artifact_checksum() {
839        let content = b"hello world";
840        let hash = ArtifactRef::compute_hash(content);
841        // echo -n "hello world" | shasum -a 256
842        let expected_str = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
843        assert_eq!(hash.to_hex(), expected_str);
844
845        let artifact = ArtifactRef::new("s3", "key")
846            .expect("artifact")
847            .with_hash(hash);
848        assert_eq!(artifact.hash(), Some(&hash));
849
850        // Integrity check
851        assert!(artifact.verify_integrity(content).unwrap());
852        assert!(!artifact.verify_integrity(b"wrong").unwrap());
853
854        // Deduplication
855        let artifact2 = ArtifactRef::new("local", "other/path")
856            .expect("artifact")
857            .with_hash(IntegrityHash::compute(content));
858        assert_eq!(artifact.content_eq(&artifact2), Some(true));
859
860        let artifact3 = ArtifactRef::new("s3", "diff")
861            .expect("artifact")
862            .with_hash(ArtifactRef::compute_hash(b"diff"));
863        assert_eq!(artifact.content_eq(&artifact3), Some(false));
864    }
865
866    #[test]
867    fn test_invalid_checksum() {
868        let result = ArtifactRef::new("s3", "key")
869            .expect("artifact")
870            .with_hash_hex("bad_hash");
871        assert!(result.is_err());
872    }
873
874    #[test]
875    fn test_header_seal() {
876        let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
877        let actor = ActorRef::human("jackie").expect("actor");
878        let mut header = Header::new(ObjectType::Task, repo_id, actor).expect("header");
879
880        let content = serde_json::json!({"key": "value"});
881        header.seal(&content).expect("seal");
882
883        assert!(header.checksum().is_some());
884        let expected =
885            crate::internal::object::integrity::compute_integrity_hash(&content).expect("checksum");
886        assert_eq!(header.checksum().expect("checksum"), &expected);
887    }
888
889    #[test]
890    fn test_empty_actor_id() {
891        let result = ActorRef::new(ActorKind::Human, "  ");
892        assert!(result.is_err());
893    }
894
895    #[test]
896    fn test_artifact_expiration() {
897        let mut artifact = ArtifactRef::new("s3", "key").expect("artifact");
898        assert!(!artifact.is_expired());
899
900        artifact.set_expires_at(Some(Utc::now() - chrono::Duration::hours(1)));
901        assert!(artifact.is_expired());
902
903        artifact.set_expires_at(Some(Utc::now() + chrono::Duration::hours(1)));
904        assert!(!artifact.is_expired());
905    }
906}