Skip to main content

oxidized_state/
schema.rs

1//! Schema definitions for AIVCS SurrealDB tables
2//!
3//! Tables:
4//! - commits: Version control commits (graph nodes)
5//! - branches: Branch pointers to commit IDs
6//! - agents: Registered agent metadata
7//! - memories: Agent memory/context snapshots
8
9use chrono::{DateTime, Utc};
10
11/// Module for serializing chrono DateTime to SurrealDB datetime format
12mod surreal_datetime {
13    use chrono::{DateTime, Utc};
14    use serde::{self, Deserialize, Deserializer, Serializer};
15    use surrealdb::sql::Datetime as SurrealDatetime;
16
17    pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
18    where
19        S: Serializer,
20    {
21        let sd = SurrealDatetime::from(*date);
22        serde::Serialize::serialize(&sd, serializer)
23    }
24
25    pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
26    where
27        D: Deserializer<'de>,
28    {
29        let sd = SurrealDatetime::deserialize(deserializer)?;
30        Ok(DateTime::from(sd))
31    }
32}
33
34/// Module for serializing optional chrono DateTime to SurrealDB datetime format
35mod surreal_datetime_opt {
36    use chrono::{DateTime, Utc};
37    use serde::{self, Deserialize, Deserializer, Serializer};
38    use surrealdb::sql::Datetime as SurrealDatetime;
39
40    pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
41    where
42        S: Serializer,
43    {
44        match date {
45            Some(d) => {
46                let sd = SurrealDatetime::from(*d);
47                serde::Serialize::serialize(&Some(sd), serializer)
48            }
49            None => serde::Serialize::serialize(&None::<SurrealDatetime>, serializer),
50        }
51    }
52
53    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
54    where
55        D: Deserializer<'de>,
56    {
57        let sd = Option::<SurrealDatetime>::deserialize(deserializer)?;
58        Ok(sd.map(DateTime::from))
59    }
60}
61use serde::{Deserialize, Serialize};
62use sha2::{Digest, Sha256};
63use uuid::Uuid;
64
65/// Composite Commit ID - hash of (Logic + State + Environment)
66///
67/// A commit in AIVCS is a tuple of:
68/// 1. Logic: The Rust binaries/scripts hash
69/// 2. State: The agent state snapshot hash
70/// 3. Environment: The Nix Flake hash
71#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
72pub struct CommitId {
73    /// The composite hash
74    pub hash: String,
75    /// Logic hash component
76    pub logic_hash: Option<String>,
77    /// State hash component
78    pub state_hash: String,
79    /// Environment (Nix) hash component
80    pub env_hash: Option<String>,
81}
82
83impl CommitId {
84    /// Create a new CommitId from state only (Phase 1 MVP)
85    pub fn from_state(state: &[u8]) -> Self {
86        let mut hasher = Sha256::new();
87        hasher.update(state);
88        let state_hash = hex::encode(hasher.finalize());
89
90        // Consistent with new(None, state_hash, None)
91        Self::new(None, &state_hash, None)
92    }
93
94    /// Create a full composite CommitId (Phase 2+)
95    pub fn new(logic_hash: Option<&str>, state_hash: &str, env_hash: Option<&str>) -> Self {
96        let mut hasher = Sha256::new();
97
98        // Use markers and separators to prevent hash collisions
99        // Logic component
100        hasher.update(b"L");
101        if let Some(lh) = logic_hash {
102            hasher.update(b"S");
103            hasher.update(lh.as_bytes());
104        } else {
105            hasher.update(b"N");
106        }
107        hasher.update(b"\0");
108
109        // State component (always present)
110        hasher.update(b"S:");
111        hasher.update(state_hash.as_bytes());
112        hasher.update(b"\0");
113
114        // Environment component
115        hasher.update(b"E");
116        if let Some(eh) = env_hash {
117            hasher.update(b"S");
118            hasher.update(eh.as_bytes());
119        } else {
120            hasher.update(b"N");
121        }
122
123        let composite = hex::encode(hasher.finalize());
124
125        CommitId {
126            hash: composite,
127            logic_hash: logic_hash.map(String::from),
128            state_hash: state_hash.to_string(),
129            env_hash: env_hash.map(String::from),
130        }
131    }
132
133    /// Get short hash (first 8 characters)
134    pub fn short(&self) -> String {
135        self.hash.chars().take(8).collect()
136    }
137}
138
139impl std::fmt::Display for CommitId {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        write!(f, "{}", self.hash)
142    }
143}
144
145/// Commit record stored in SurrealDB
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct CommitRecord {
148    /// SurrealDB record ID
149    pub id: Option<surrealdb::sql::Thing>,
150    /// The commit ID (composite hash)
151    pub commit_id: CommitId,
152    /// Parent commit IDs (empty for root commits)
153    pub parent_ids: Vec<String>,
154    /// Commit message
155    pub message: String,
156    /// Author/agent that created the commit
157    pub author: String,
158    /// Timestamp of commit creation
159    #[serde(with = "surreal_datetime")]
160    pub created_at: DateTime<Utc>,
161    /// Branch name (if this is a branch head)
162    pub branch: Option<String>,
163}
164
165impl CommitRecord {
166    /// Create a new commit record
167    pub fn new(commit_id: CommitId, parent_ids: Vec<String>, message: &str, author: &str) -> Self {
168        CommitRecord {
169            id: None,
170            commit_id,
171            parent_ids,
172            message: message.to_string(),
173            author: author.to_string(),
174            created_at: Utc::now(),
175            branch: None,
176        }
177    }
178}
179
180/// Snapshot record - the actual agent state data
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct SnapshotRecord {
183    /// SurrealDB record ID
184    pub id: Option<surrealdb::sql::Thing>,
185    /// The commit ID this snapshot belongs to
186    pub commit_id: String,
187    /// Serialized agent state (JSON)
188    pub state: serde_json::Value,
189    /// Size in bytes
190    pub size_bytes: u64,
191    /// Timestamp
192    #[serde(with = "surreal_datetime")]
193    pub created_at: DateTime<Utc>,
194}
195
196impl SnapshotRecord {
197    /// Create a new snapshot record
198    pub fn new(commit_id: &str, state: serde_json::Value) -> Self {
199        let size = serde_json::to_string(&state)
200            .map(|s| s.len() as u64)
201            .unwrap_or(0);
202
203        SnapshotRecord {
204            id: None,
205            commit_id: commit_id.to_string(),
206            state,
207            size_bytes: size,
208            created_at: Utc::now(),
209        }
210    }
211}
212
213/// Branch record - pointer to a commit
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct BranchRecord {
216    /// SurrealDB record ID
217    pub id: Option<surrealdb::sql::Thing>,
218    /// Branch name (e.g., "main", "feature/experiment-1")
219    pub name: String,
220    /// Current head commit ID
221    pub head_commit_id: String,
222    /// Is this the default branch?
223    pub is_default: bool,
224    /// Created timestamp
225    #[serde(with = "surreal_datetime")]
226    pub created_at: DateTime<Utc>,
227    /// Last updated timestamp
228    #[serde(with = "surreal_datetime")]
229    pub updated_at: DateTime<Utc>,
230}
231
232impl BranchRecord {
233    /// Create a new branch record
234    pub fn new(name: &str, head_commit_id: &str, is_default: bool) -> Self {
235        let now = Utc::now();
236        BranchRecord {
237            id: None,
238            name: name.to_string(),
239            head_commit_id: head_commit_id.to_string(),
240            is_default,
241            created_at: now,
242            updated_at: now,
243        }
244    }
245}
246
247/// Agent record - registered agent metadata
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct AgentRecord {
250    /// SurrealDB record ID
251    pub id: Option<surrealdb::sql::Thing>,
252    /// Agent UUID
253    pub agent_id: Uuid,
254    /// Agent name
255    pub name: String,
256    /// Agent type/kind
257    pub agent_type: String,
258    /// Configuration (JSON)
259    pub config: serde_json::Value,
260    /// Created timestamp
261    #[serde(with = "surreal_datetime")]
262    pub created_at: DateTime<Utc>,
263}
264
265impl AgentRecord {
266    /// Create a new agent record
267    pub fn new(name: &str, agent_type: &str, config: serde_json::Value) -> Self {
268        AgentRecord {
269            id: None,
270            agent_id: Uuid::new_v4(),
271            name: name.to_string(),
272            agent_type: agent_type.to_string(),
273            config,
274            created_at: Utc::now(),
275        }
276    }
277}
278
279/// Memory record - agent memory/context for RAG
280#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct MemoryRecord {
282    /// SurrealDB record ID
283    pub id: Option<surrealdb::sql::Thing>,
284    /// The commit ID this memory belongs to
285    pub commit_id: String,
286    /// Memory key/namespace
287    pub key: String,
288    /// Memory content (text for embedding)
289    pub content: String,
290    /// Optional embedding vector (for semantic search)
291    pub embedding: Option<Vec<f32>>,
292    /// Metadata
293    pub metadata: serde_json::Value,
294    /// Created timestamp
295    #[serde(with = "surreal_datetime")]
296    pub created_at: DateTime<Utc>,
297}
298
299impl MemoryRecord {
300    /// Create a new memory record
301    pub fn new(commit_id: &str, key: &str, content: &str) -> Self {
302        MemoryRecord {
303            id: None,
304            commit_id: commit_id.to_string(),
305            key: key.to_string(),
306            content: content.to_string(),
307            embedding: None,
308            metadata: serde_json::json!({}),
309            created_at: Utc::now(),
310        }
311    }
312
313    /// Set embedding vector
314    pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
315        self.embedding = Some(embedding);
316        self
317    }
318
319    /// Set metadata
320    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
321        self.metadata = metadata;
322        self
323    }
324}
325
326/// Graph edge - represents commit relationships (parent -> child)
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct GraphEdge {
329    /// Child commit ID
330    pub child_id: String,
331    /// Parent commit ID
332    pub parent_id: String,
333    /// Edge type (normal, merge, fork)
334    pub edge_type: EdgeType,
335    /// Created timestamp
336    #[serde(with = "surreal_datetime")]
337    pub created_at: DateTime<Utc>,
338}
339
340/// Type of graph edge
341#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
342#[serde(rename_all = "lowercase")]
343pub enum EdgeType {
344    /// Normal parent-child relationship
345    Normal,
346    /// Merge commit (multiple parents)
347    Merge,
348    /// Fork/branch point
349    Fork,
350}
351
352impl GraphEdge {
353    /// Create a new normal graph edge
354    pub fn new(child_id: &str, parent_id: &str) -> Self {
355        GraphEdge {
356            child_id: child_id.to_string(),
357            parent_id: parent_id.to_string(),
358            edge_type: EdgeType::Normal,
359            created_at: Utc::now(),
360        }
361    }
362
363    /// Create a merge edge
364    pub fn merge(child_id: &str, parent_id: &str) -> Self {
365        GraphEdge {
366            child_id: child_id.to_string(),
367            parent_id: parent_id.to_string(),
368            edge_type: EdgeType::Merge,
369            created_at: Utc::now(),
370        }
371    }
372}
373
374// ---------------------------------------------------------------------------
375// RunLedger Records — Execution Run Persistence
376// ---------------------------------------------------------------------------
377
378/// Run record - execution run metadata and state
379#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct RunRecord {
381    /// SurrealDB record ID
382    pub id: Option<surrealdb::sql::Thing>,
383    /// Unique run ID (UUID string)
384    pub run_id: String,
385    /// Agent spec digest (SHA256)
386    pub spec_digest: String,
387    /// Git SHA at time of run (optional)
388    pub git_sha: Option<String>,
389    /// Agent name
390    pub agent_name: String,
391    /// Arbitrary tags (JSON)
392    pub tags: serde_json::Value,
393    /// Run status: "RUNNING" | "COMPLETED" | "FAILED" | "CANCELLED"
394    pub status: String,
395    /// Total events recorded
396    pub total_events: u64,
397    /// Final state digest (if completed)
398    pub final_state_digest: Option<String>,
399    /// Duration in milliseconds
400    pub duration_ms: u64,
401    /// Whether run succeeded
402    pub success: bool,
403    /// Created timestamp
404    #[serde(with = "surreal_datetime")]
405    pub created_at: DateTime<Utc>,
406    /// Completed timestamp (if terminal)
407    #[serde(default, with = "surreal_datetime_opt")]
408    pub completed_at: Option<DateTime<Utc>>,
409}
410
411impl RunRecord {
412    /// Create a new run record in "running" state
413    pub fn new(
414        run_id: String,
415        spec_digest: String,
416        git_sha: Option<String>,
417        agent_name: String,
418        tags: serde_json::Value,
419    ) -> Self {
420        RunRecord {
421            id: None,
422            run_id,
423            spec_digest,
424            git_sha,
425            agent_name,
426            tags,
427            status: "RUNNING".to_string(),
428            total_events: 0,
429            final_state_digest: None,
430            duration_ms: 0,
431            success: false,
432            created_at: Utc::now(),
433            completed_at: None,
434        }
435    }
436
437    /// Mark run as completed
438    pub fn complete(
439        mut self,
440        total_events: u64,
441        final_state_digest: Option<String>,
442        duration_ms: u64,
443    ) -> Self {
444        self.status = "COMPLETED".to_string();
445        self.total_events = total_events;
446        self.final_state_digest = final_state_digest;
447        self.duration_ms = duration_ms;
448        self.success = true;
449        self.completed_at = Some(Utc::now());
450        self
451    }
452
453    /// Mark run as failed
454    pub fn fail(mut self, total_events: u64, duration_ms: u64) -> Self {
455        self.status = "FAILED".to_string();
456        self.total_events = total_events;
457        self.duration_ms = duration_ms;
458        self.success = false;
459        self.completed_at = Some(Utc::now());
460        self
461    }
462
463    /// Mark run as cancelled
464    pub fn cancel(mut self, total_events: u64, duration_ms: u64) -> Self {
465        self.status = "CANCELLED".to_string();
466        self.total_events = total_events;
467        self.duration_ms = duration_ms;
468        self.success = false;
469        self.completed_at = Some(Utc::now());
470        self
471    }
472}
473
474/// Run event record - single event in execution
475#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct RunEventRecord {
477    /// SurrealDB record ID
478    pub id: Option<surrealdb::sql::Thing>,
479    /// Run ID this event belongs to
480    pub run_id: String,
481    /// Monotonic sequence number within run (1-indexed)
482    pub seq: u64,
483    /// Event kind (e.g. "graph_started", "node_entered", "tool_called")
484    pub kind: String,
485    /// Event payload (JSON)
486    pub payload: serde_json::Value,
487    /// Event timestamp
488    #[serde(with = "surreal_datetime")]
489    pub timestamp: DateTime<Utc>,
490}
491
492impl RunEventRecord {
493    /// Create a new run event record
494    pub fn new(run_id: String, seq: u64, kind: String, payload: serde_json::Value) -> Self {
495        RunEventRecord {
496            id: None,
497            run_id,
498            seq,
499            kind,
500            payload,
501            timestamp: Utc::now(),
502        }
503    }
504}
505
506/// Release record - agent release and version management
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct ReleaseRecordSchema {
509    /// SurrealDB record ID
510    pub id: Option<surrealdb::sql::Thing>,
511    /// Release/Agent name
512    pub name: String,
513    /// Spec digest being released
514    pub spec_digest: String,
515    /// Version label (e.g. "v1.2.3")
516    pub version_label: Option<String>,
517    /// Who or what promoted this release
518    pub promoted_by: String,
519    /// Release notes
520    pub notes: Option<String>,
521    /// Created timestamp
522    #[serde(with = "surreal_datetime")]
523    pub created_at: DateTime<Utc>,
524}
525
526impl ReleaseRecordSchema {
527    /// Create a new release record
528    pub fn new(
529        name: String,
530        spec_digest: String,
531        version_label: Option<String>,
532        promoted_by: String,
533        notes: Option<String>,
534    ) -> Self {
535        ReleaseRecordSchema {
536            id: None,
537            name,
538            spec_digest,
539            version_label,
540            promoted_by,
541            notes,
542            created_at: Utc::now(),
543        }
544    }
545}
546
547// ---------------------------------------------------------------------------
548// Decision and Memory Provenance Records — Decision Learning and Lineage
549// ---------------------------------------------------------------------------
550
551/// Decision record - captures agent decisions with rationale and outcomes
552#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct DecisionRecord {
554    /// SurrealDB record ID
555    pub id: Option<surrealdb::sql::Thing>,
556    /// Unique decision ID (UUID string)
557    pub decision_id: String,
558    /// Associated commit ID
559    pub commit_id: String,
560    /// Task/context this decision was about
561    pub task: String,
562    /// What was decided/action taken
563    pub action: String,
564    /// Why this decision was made
565    pub rationale: String,
566    /// Alternative options considered (JSON array)
567    pub alternatives: serde_json::Value,
568    /// Confidence level (0.0-1.0)
569    pub confidence: f32,
570    /// Decision outcome
571    pub outcome: Option<String>, // JSON serialized DecisionOutcome enum
572    /// Decision timestamp
573    #[serde(with = "surreal_datetime")]
574    pub timestamp: DateTime<Utc>,
575    /// When outcome was recorded (if any)
576    #[serde(default, with = "surreal_datetime_opt")]
577    pub outcome_at: Option<DateTime<Utc>>,
578}
579
580impl DecisionRecord {
581    /// Create a new decision record
582    pub fn new(
583        decision_id: String,
584        commit_id: String,
585        task: String,
586        action: String,
587        rationale: String,
588        confidence: f32,
589    ) -> Self {
590        DecisionRecord {
591            id: None,
592            decision_id,
593            commit_id,
594            task,
595            action,
596            rationale,
597            alternatives: serde_json::json!([]),
598            confidence,
599            outcome: None,
600            timestamp: Utc::now(),
601            outcome_at: None,
602        }
603    }
604
605    /// Add alternatives to consider
606    pub fn with_alternatives(mut self, alternatives: Vec<String>) -> Self {
607        self.alternatives = serde_json::json!(alternatives);
608        self
609    }
610
611    /// Record the decision outcome
612    pub fn with_outcome(mut self, outcome: String) -> Self {
613        self.outcome = Some(outcome);
614        self.outcome_at = Some(Utc::now());
615        self
616    }
617}
618
619/// Provenance source for memory records
620#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
621#[serde(rename_all = "snake_case")]
622pub enum ProvenanceSourceType {
623    /// From a run execution trace
624    RunTrace,
625    /// From a state snapshot
626    StateSnapshot,
627    /// From user annotation
628    UserAnnotation,
629    /// Derived from another memory
630    MemoryDerivation,
631}
632
633/// Memory provenance record - tracks lineage of memories
634#[derive(Debug, Clone, Serialize, Deserialize)]
635pub struct MemoryProvenanceRecord {
636    /// SurrealDB record ID
637    pub id: Option<surrealdb::sql::Thing>,
638    /// The memory ID this provenance describes
639    pub memory_id: String,
640    /// Source type (JSON serialized ProvenanceSourceType)
641    pub source_type: String,
642    /// Source details (JSON: run_id, event_idx, commit_id, user_id, parent_id, etc.)
643    pub source_data: serde_json::Value,
644    /// Parent memory ID if derived
645    pub derived_from: Option<String>,
646    /// Created timestamp
647    #[serde(with = "surreal_datetime")]
648    pub created_at: DateTime<Utc>,
649    /// When this provenance became invalid/stale
650    #[serde(default, with = "surreal_datetime_opt")]
651    pub invalidated_at: Option<DateTime<Utc>>,
652}
653
654impl MemoryProvenanceRecord {
655    /// Create provenance for a run trace source
656    pub fn from_run_trace(memory_id: String, run_id: String, event_idx: usize) -> Self {
657        MemoryProvenanceRecord {
658            id: None,
659            memory_id,
660            source_type: ProvenanceSourceType::RunTrace.to_string(),
661            source_data: serde_json::json!({ "run_id": run_id, "event_idx": event_idx }),
662            derived_from: None,
663            created_at: Utc::now(),
664            invalidated_at: None,
665        }
666    }
667
668    /// Create provenance for a state snapshot source
669    pub fn from_snapshot(memory_id: String, commit_id: String) -> Self {
670        MemoryProvenanceRecord {
671            id: None,
672            memory_id,
673            source_type: ProvenanceSourceType::StateSnapshot.to_string(),
674            source_data: serde_json::json!({ "commit_id": commit_id }),
675            derived_from: None,
676            created_at: Utc::now(),
677            invalidated_at: None,
678        }
679    }
680
681    /// Create provenance for user annotation
682    pub fn from_user_annotation(memory_id: String, user_id: String) -> Self {
683        MemoryProvenanceRecord {
684            id: None,
685            memory_id,
686            source_type: ProvenanceSourceType::UserAnnotation.to_string(),
687            source_data: serde_json::json!({ "user_id": user_id }),
688            derived_from: None,
689            created_at: Utc::now(),
690            invalidated_at: None,
691        }
692    }
693
694    /// Create provenance for derived memory
695    pub fn from_derivation(memory_id: String, parent_id: String, derivation: String) -> Self {
696        MemoryProvenanceRecord {
697            id: None,
698            memory_id,
699            source_type: ProvenanceSourceType::MemoryDerivation.to_string(),
700            source_data: serde_json::json!({ "derivation": derivation }),
701            derived_from: Some(parent_id),
702            created_at: Utc::now(),
703            invalidated_at: None,
704        }
705    }
706
707    /// Mark this provenance as invalidated
708    pub fn invalidate(mut self) -> Self {
709        self.invalidated_at = Some(Utc::now());
710        self
711    }
712}
713
714impl core::fmt::Display for ProvenanceSourceType {
715    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
716        match self {
717            ProvenanceSourceType::RunTrace => write!(f, "run_trace"),
718            ProvenanceSourceType::StateSnapshot => write!(f, "state_snapshot"),
719            ProvenanceSourceType::UserAnnotation => write!(f, "user_annotation"),
720            ProvenanceSourceType::MemoryDerivation => write!(f, "memory_derivation"),
721        }
722    }
723}
724
725#[cfg(test)]
726mod tests {
727    use super::*;
728
729    #[test]
730    fn test_commit_id_from_state() {
731        let state = b"test state data";
732        let commit_id = CommitId::from_state(state);
733
734        assert!(!commit_id.hash.is_empty());
735        assert_eq!(commit_id.hash.len(), 64); // SHA256 hex = 64 chars
736        assert!(commit_id.logic_hash.is_none());
737        assert!(commit_id.env_hash.is_none());
738    }
739
740    #[test]
741    fn test_commit_id_deterministic() {
742        let state = b"same state";
743        let id1 = CommitId::from_state(state);
744        let id2 = CommitId::from_state(state);
745
746        assert_eq!(id1.hash, id2.hash);
747    }
748
749    #[test]
750    fn test_commit_id_different_states() {
751        let id1 = CommitId::from_state(b"state 1");
752        let id2 = CommitId::from_state(b"state 2");
753
754        assert_ne!(id1.hash, id2.hash);
755    }
756
757    #[test]
758    fn test_commit_id_short() {
759        let commit_id = CommitId::from_state(b"test");
760        assert_eq!(commit_id.short().len(), 8);
761    }
762
763    #[test]
764    fn test_composite_commit_id() {
765        let commit_id = CommitId::new(Some("logic-hash"), "state-hash", Some("env-hash"));
766
767        assert!(!commit_id.hash.is_empty());
768        assert_eq!(commit_id.logic_hash, Some("logic-hash".to_string()));
769        assert_eq!(commit_id.env_hash, Some("env-hash".to_string()));
770    }
771
772    #[test]
773    fn test_commit_id_collision_prevention() {
774        // Test that swapping components results in different hashes
775        let id1 = CommitId::new(Some("ab"), "cd", None);
776        let id2 = CommitId::new(Some("a"), "bcd", None);
777        assert_ne!(id1.hash, id2.hash);
778
779        // Test that None vs "none" string doesn't collide if we use prefixes correctly
780        // (Wait, we use "none" for None, so if state_hash was "none" and logic_hash was None,
781        // it might collide if we don't have prefixes)
782        let id3 = CommitId::new(None, "state", None);
783        let id4 = CommitId::new(Some("none"), "state", None);
784        assert_ne!(id3.hash, id4.hash);
785    }
786
787    #[test]
788    fn test_snapshot_record_size() {
789        let state = serde_json::json!({"key": "value", "nested": {"a": 1}});
790        let snapshot = SnapshotRecord::new("commit-123", state);
791
792        assert!(snapshot.size_bytes > 0);
793    }
794
795    #[test]
796    fn test_run_record_new() {
797        let run = RunRecord::new(
798            "run-123".to_string(),
799            "spec-digest-abc".to_string(),
800            Some("abc123".to_string()),
801            "test-agent".to_string(),
802            serde_json::json!({"env": "test"}),
803        );
804
805        assert_eq!(run.run_id, "run-123");
806        assert_eq!(run.status, "RUNNING");
807        assert_eq!(run.total_events, 0);
808        assert!(!run.success);
809    }
810
811    #[test]
812    fn test_run_record_complete() {
813        let run = RunRecord::new(
814            "run-123".to_string(),
815            "spec-digest-abc".to_string(),
816            Some("abc123".to_string()),
817            "test-agent".to_string(),
818            serde_json::json!({}),
819        )
820        .complete(5, Some("state-digest-xyz".to_string()), 1000);
821
822        assert_eq!(run.status, "COMPLETED");
823        assert_eq!(run.total_events, 5);
824        assert!(run.success);
825        assert!(run.completed_at.is_some());
826    }
827
828    #[test]
829    fn test_run_record_fail() {
830        let run = RunRecord::new(
831            "run-123".to_string(),
832            "spec-digest-abc".to_string(),
833            None,
834            "test-agent".to_string(),
835            serde_json::json!({}),
836        )
837        .fail(2, 500);
838
839        assert_eq!(run.status, "FAILED");
840        assert_eq!(run.total_events, 2);
841        assert!(!run.success);
842        assert!(run.completed_at.is_some());
843    }
844
845    #[test]
846    fn test_run_event_record() {
847        let event = RunEventRecord::new(
848            "run-123".to_string(),
849            1,
850            "graph_started".to_string(),
851            serde_json::json!({"graph_id": "g1"}),
852        );
853
854        assert_eq!(event.run_id, "run-123");
855        assert_eq!(event.seq, 1);
856        assert_eq!(event.kind, "graph_started");
857    }
858
859    #[test]
860    fn test_release_record() {
861        let release = ReleaseRecordSchema::new(
862            "my-agent".to_string(),
863            "spec-digest-abc".to_string(),
864            Some("v1.0.0".to_string()),
865            "alice".to_string(),
866            Some("Initial release".to_string()),
867        );
868
869        assert_eq!(release.name, "my-agent");
870        assert_eq!(release.version_label, Some("v1.0.0".to_string()));
871    }
872
873    #[test]
874    fn test_decision_record_new() {
875        let decision = DecisionRecord::new(
876            "dec-123".to_string(),
877            "commit-abc".to_string(),
878            "task-optimize".to_string(),
879            "use_parallel".to_string(),
880            "Improves throughput".to_string(),
881            0.85,
882        );
883
884        assert_eq!(decision.decision_id, "dec-123");
885        assert_eq!(decision.commit_id, "commit-abc");
886        assert_eq!(decision.task, "task-optimize");
887        assert_eq!(decision.action, "use_parallel");
888        assert_eq!(decision.confidence, 0.85);
889        assert!(decision.outcome.is_none());
890    }
891
892    #[test]
893    fn test_decision_record_with_alternatives() {
894        let decision = DecisionRecord::new(
895            "dec-456".to_string(),
896            "commit-def".to_string(),
897            "task-retry".to_string(),
898            "exponential_backoff".to_string(),
899            "Reduces thundering herd".to_string(),
900            0.75,
901        )
902        .with_alternatives(vec!["linear_backoff".to_string(), "no_retry".to_string()]);
903
904        let alts: Vec<String> = serde_json::from_value(decision.alternatives).unwrap();
905        assert_eq!(alts.len(), 2);
906        assert!(alts.contains(&"linear_backoff".to_string()));
907    }
908
909    #[test]
910    fn test_decision_record_with_outcome() {
911        let outcome_json = serde_json::json!({
912            "status": "success",
913            "benefit": 1.23,
914            "duration_ms": 5000
915        });
916        let decision = DecisionRecord::new(
917            "dec-789".to_string(),
918            "commit-ghi".to_string(),
919            "task-cache".to_string(),
920            "redis_cache".to_string(),
921            "Faster lookups".to_string(),
922            0.9,
923        )
924        .with_outcome(outcome_json.to_string());
925
926        assert!(decision.outcome.is_some());
927        assert!(decision.outcome_at.is_some());
928    }
929
930    #[test]
931    fn test_memory_provenance_from_run_trace() {
932        let prov = MemoryProvenanceRecord::from_run_trace(
933            "mem-123".to_string(),
934            "run-456".to_string(),
935            42,
936        );
937
938        assert_eq!(prov.memory_id, "mem-123");
939        assert_eq!(prov.source_type, ProvenanceSourceType::RunTrace.to_string());
940        assert!(prov.derived_from.is_none());
941        assert!(prov.invalidated_at.is_none());
942
943        let source_data: serde_json::Value = prov.source_data;
944        assert_eq!(source_data["run_id"], "run-456");
945        assert_eq!(source_data["event_idx"], 42);
946    }
947
948    #[test]
949    fn test_memory_provenance_from_snapshot() {
950        let prov =
951            MemoryProvenanceRecord::from_snapshot("mem-789".to_string(), "commit-abc".to_string());
952
953        assert_eq!(prov.memory_id, "mem-789");
954        assert_eq!(
955            prov.source_type,
956            ProvenanceSourceType::StateSnapshot.to_string()
957        );
958        assert_eq!(prov.source_data["commit_id"], "commit-abc");
959    }
960
961    #[test]
962    fn test_memory_provenance_from_derivation() {
963        let prov = MemoryProvenanceRecord::from_derivation(
964            "mem-new".to_string(),
965            "mem-parent".to_string(),
966            "summarize".to_string(),
967        );
968
969        assert_eq!(prov.memory_id, "mem-new");
970        assert_eq!(prov.derived_from, Some("mem-parent".to_string()));
971        assert_eq!(
972            prov.source_type,
973            ProvenanceSourceType::MemoryDerivation.to_string()
974        );
975        assert_eq!(prov.source_data["derivation"], "summarize");
976    }
977
978    #[test]
979    fn test_memory_provenance_invalidation() {
980        let prov = MemoryProvenanceRecord::from_user_annotation(
981            "mem-123".to_string(),
982            "user-456".to_string(),
983        );
984
985        assert!(prov.invalidated_at.is_none());
986
987        let invalidated = prov.invalidate();
988        assert!(invalidated.invalidated_at.is_some());
989    }
990
991    #[test]
992    fn test_provenance_source_type_display() {
993        assert_eq!(ProvenanceSourceType::RunTrace.to_string(), "run_trace");
994        assert_eq!(
995            ProvenanceSourceType::StateSnapshot.to_string(),
996            "state_snapshot"
997        );
998        assert_eq!(
999            ProvenanceSourceType::UserAnnotation.to_string(),
1000            "user_annotation"
1001        );
1002        assert_eq!(
1003            ProvenanceSourceType::MemoryDerivation.to_string(),
1004            "memory_derivation"
1005        );
1006    }
1007}