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    /// Path to the success rubric for this evaluation.
404    pub rubric_path: Option<String>,
405    /// Whether this run is a production-promotion gate.
406    #[serde(default)]
407    pub is_promotion_gate: bool,
408    /// Current maturity phase of the agent (1-4).
409    #[serde(default)]
410    pub agent_phase: u8,
411    /// Created timestamp
412    #[serde(with = "surreal_datetime")]
413    pub created_at: DateTime<Utc>,
414    /// Completed timestamp (if terminal)
415    #[serde(default, with = "surreal_datetime_opt")]
416    pub completed_at: Option<DateTime<Utc>>,
417}
418
419impl RunRecord {
420    /// Create a new run record in "running" state
421    #[allow(clippy::too_many_arguments)]
422    pub fn new(
423        run_id: String,
424        spec_digest: String,
425        git_sha: Option<String>,
426        agent_name: String,
427        tags: serde_json::Value,
428        rubric_path: Option<String>,
429        is_promotion_gate: bool,
430        agent_phase: u8,
431    ) -> Self {
432        RunRecord {
433            id: None,
434            run_id,
435            spec_digest,
436            git_sha,
437            agent_name,
438            tags,
439            status: "RUNNING".to_string(),
440            total_events: 0,
441            final_state_digest: None,
442            duration_ms: 0,
443            success: false,
444            rubric_path,
445            is_promotion_gate,
446            agent_phase,
447            created_at: Utc::now(),
448            completed_at: None,
449        }
450    }
451
452    /// Mark run as completed
453    pub fn complete(
454        mut self,
455        total_events: u64,
456        final_state_digest: Option<String>,
457        duration_ms: u64,
458    ) -> Self {
459        self.status = "COMPLETED".to_string();
460        self.total_events = total_events;
461        self.final_state_digest = final_state_digest;
462        self.duration_ms = duration_ms;
463        self.success = true;
464        self.completed_at = Some(Utc::now());
465        self
466    }
467
468    /// Mark run as failed
469    pub fn fail(mut self, total_events: u64, duration_ms: u64) -> Self {
470        self.status = "FAILED".to_string();
471        self.total_events = total_events;
472        self.duration_ms = duration_ms;
473        self.success = false;
474        self.completed_at = Some(Utc::now());
475        self
476    }
477
478    /// Mark run as cancelled
479    pub fn cancel(mut self, total_events: u64, duration_ms: u64) -> Self {
480        self.status = "CANCELLED".to_string();
481        self.total_events = total_events;
482        self.duration_ms = duration_ms;
483        self.success = false;
484        self.completed_at = Some(Utc::now());
485        self
486    }
487}
488
489/// Run event record - single event in execution
490#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct RunEventRecord {
492    /// SurrealDB record ID
493    pub id: Option<surrealdb::sql::Thing>,
494    /// Run ID this event belongs to
495    pub run_id: String,
496    /// Monotonic sequence number within run (1-indexed)
497    pub seq: u64,
498    /// Event kind (e.g. "graph_started", "node_entered", "tool_called")
499    pub kind: String,
500    /// Event payload (JSON)
501    pub payload: serde_json::Value,
502    /// Event timestamp
503    #[serde(with = "surreal_datetime")]
504    pub timestamp: DateTime<Utc>,
505}
506
507impl RunEventRecord {
508    /// Create a new run event record
509    pub fn new(run_id: String, seq: u64, kind: String, payload: serde_json::Value) -> Self {
510        RunEventRecord {
511            id: None,
512            run_id,
513            seq,
514            kind,
515            payload,
516            timestamp: Utc::now(),
517        }
518    }
519}
520
521/// Release record - agent release and version management
522#[derive(Debug, Clone, Serialize, Deserialize)]
523pub struct ReleaseRecordSchema {
524    /// SurrealDB record ID
525    pub id: Option<surrealdb::sql::Thing>,
526    /// Release/Agent name
527    pub name: String,
528    /// Spec digest being released
529    pub spec_digest: String,
530    /// Version label (e.g. "v1.2.3")
531    pub version_label: Option<String>,
532    /// Who or what promoted this release
533    pub promoted_by: String,
534    /// Release notes
535    pub notes: Option<String>,
536    /// Created timestamp
537    #[serde(with = "surreal_datetime")]
538    pub created_at: DateTime<Utc>,
539}
540
541impl ReleaseRecordSchema {
542    /// Create a new release record
543    pub fn new(
544        name: String,
545        spec_digest: String,
546        version_label: Option<String>,
547        promoted_by: String,
548        notes: Option<String>,
549    ) -> Self {
550        ReleaseRecordSchema {
551            id: None,
552            name,
553            spec_digest,
554            version_label,
555            promoted_by,
556            notes,
557            created_at: Utc::now(),
558        }
559    }
560}
561
562// ---------------------------------------------------------------------------
563// Decision and Memory Provenance Records — Decision Learning and Lineage
564// ---------------------------------------------------------------------------
565
566/// Decision record - captures agent decisions with rationale and outcomes
567#[derive(Debug, Clone, Serialize, Deserialize)]
568pub struct DecisionRecord {
569    /// SurrealDB record ID
570    pub id: Option<surrealdb::sql::Thing>,
571    /// Unique decision ID (UUID string)
572    pub decision_id: String,
573    /// Associated commit ID
574    pub commit_id: String,
575    /// Task/context this decision was about
576    pub task: String,
577    /// What was decided/action taken
578    pub action: String,
579    /// Why this decision was made
580    pub rationale: String,
581    /// Alternative options considered
582    pub alternatives: Vec<String>,
583    /// Confidence level (0.0-1.0)
584    pub confidence: f32,
585    /// Decision outcome
586    pub outcome: Option<String>, // JSON serialized DecisionOutcome enum
587    /// Decision timestamp
588    #[serde(with = "surreal_datetime")]
589    pub timestamp: DateTime<Utc>,
590    /// When outcome was recorded (if any)
591    #[serde(default, with = "surreal_datetime_opt")]
592    pub outcome_at: Option<DateTime<Utc>>,
593}
594
595impl DecisionRecord {
596    /// Create a new decision record
597    pub fn new(
598        decision_id: String,
599        commit_id: String,
600        task: String,
601        action: String,
602        rationale: String,
603        confidence: f32,
604    ) -> Self {
605        DecisionRecord {
606            id: None,
607            decision_id,
608            commit_id,
609            task,
610            action,
611            rationale,
612            alternatives: Vec::new(),
613            confidence,
614            outcome: None,
615            timestamp: Utc::now(),
616            outcome_at: None,
617        }
618    }
619
620    /// Add alternatives to consider
621    pub fn with_alternatives(mut self, alternatives: Vec<String>) -> Self {
622        self.alternatives = alternatives;
623        self
624    }
625
626    /// Record the decision outcome
627    pub fn with_outcome(mut self, outcome: String) -> Self {
628        self.outcome = Some(outcome);
629        self.outcome_at = Some(Utc::now());
630        self
631    }
632}
633
634/// Provenance source for memory records
635#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
636#[serde(rename_all = "snake_case")]
637pub enum ProvenanceSourceType {
638    /// From a run execution trace
639    RunTrace,
640    /// From a state snapshot
641    StateSnapshot,
642    /// From user annotation
643    UserAnnotation,
644    /// Derived from another memory
645    MemoryDerivation,
646}
647
648/// Memory provenance record - tracks lineage of memories
649#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct MemoryProvenanceRecord {
651    /// SurrealDB record ID
652    pub id: Option<surrealdb::sql::Thing>,
653    /// The memory ID this provenance describes
654    pub memory_id: String,
655    /// Source type (JSON serialized ProvenanceSourceType)
656    pub source_type: String,
657    /// Source details (JSON: run_id, event_idx, commit_id, user_id, parent_id, etc.)
658    pub source_data: serde_json::Value,
659    /// Parent memory ID if derived
660    pub derived_from: Option<String>,
661    /// Created timestamp
662    #[serde(with = "surreal_datetime")]
663    pub created_at: DateTime<Utc>,
664    /// When this provenance became invalid/stale
665    #[serde(default, with = "surreal_datetime_opt")]
666    pub invalidated_at: Option<DateTime<Utc>>,
667}
668
669impl MemoryProvenanceRecord {
670    /// Create provenance for a run trace source
671    pub fn from_run_trace(memory_id: String, run_id: String, event_idx: usize) -> Self {
672        MemoryProvenanceRecord {
673            id: None,
674            memory_id,
675            source_type: ProvenanceSourceType::RunTrace.to_string(),
676            source_data: serde_json::json!({ "run_id": run_id, "event_idx": event_idx }),
677            derived_from: None,
678            created_at: Utc::now(),
679            invalidated_at: None,
680        }
681    }
682
683    /// Create provenance for a state snapshot source
684    pub fn from_snapshot(memory_id: String, commit_id: String) -> Self {
685        MemoryProvenanceRecord {
686            id: None,
687            memory_id,
688            source_type: ProvenanceSourceType::StateSnapshot.to_string(),
689            source_data: serde_json::json!({ "commit_id": commit_id }),
690            derived_from: None,
691            created_at: Utc::now(),
692            invalidated_at: None,
693        }
694    }
695
696    /// Create provenance for user annotation
697    pub fn from_user_annotation(memory_id: String, user_id: String) -> Self {
698        MemoryProvenanceRecord {
699            id: None,
700            memory_id,
701            source_type: ProvenanceSourceType::UserAnnotation.to_string(),
702            source_data: serde_json::json!({ "user_id": user_id }),
703            derived_from: None,
704            created_at: Utc::now(),
705            invalidated_at: None,
706        }
707    }
708
709    /// Create provenance for derived memory
710    pub fn from_derivation(memory_id: String, parent_id: String, derivation: String) -> Self {
711        MemoryProvenanceRecord {
712            id: None,
713            memory_id,
714            source_type: ProvenanceSourceType::MemoryDerivation.to_string(),
715            source_data: serde_json::json!({ "derivation": derivation }),
716            derived_from: Some(parent_id),
717            created_at: Utc::now(),
718            invalidated_at: None,
719        }
720    }
721
722    /// Mark this provenance as invalidated
723    pub fn invalidate(mut self) -> Self {
724        self.invalidated_at = Some(Utc::now());
725        self
726    }
727}
728
729impl core::fmt::Display for ProvenanceSourceType {
730    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
731        match self {
732            ProvenanceSourceType::RunTrace => write!(f, "run_trace"),
733            ProvenanceSourceType::StateSnapshot => write!(f, "state_snapshot"),
734            ProvenanceSourceType::UserAnnotation => write!(f, "user_annotation"),
735            ProvenanceSourceType::MemoryDerivation => write!(f, "memory_derivation"),
736        }
737    }
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743
744    #[test]
745    fn test_commit_id_from_state() {
746        let state = b"test state data";
747        let commit_id = CommitId::from_state(state);
748
749        assert!(!commit_id.hash.is_empty());
750        assert_eq!(commit_id.hash.len(), 64); // SHA256 hex = 64 chars
751        assert!(commit_id.logic_hash.is_none());
752        assert!(commit_id.env_hash.is_none());
753    }
754
755    #[test]
756    fn test_commit_id_deterministic() {
757        let state = b"same state";
758        let id1 = CommitId::from_state(state);
759        let id2 = CommitId::from_state(state);
760
761        assert_eq!(id1.hash, id2.hash);
762    }
763
764    #[test]
765    fn test_commit_id_different_states() {
766        let id1 = CommitId::from_state(b"state 1");
767        let id2 = CommitId::from_state(b"state 2");
768
769        assert_ne!(id1.hash, id2.hash);
770    }
771
772    #[test]
773    fn test_commit_id_short() {
774        let commit_id = CommitId::from_state(b"test");
775        assert_eq!(commit_id.short().len(), 8);
776    }
777
778    #[test]
779    fn test_composite_commit_id() {
780        let commit_id = CommitId::new(Some("logic-hash"), "state-hash", Some("env-hash"));
781
782        assert!(!commit_id.hash.is_empty());
783        assert_eq!(commit_id.logic_hash, Some("logic-hash".to_string()));
784        assert_eq!(commit_id.env_hash, Some("env-hash".to_string()));
785    }
786
787    #[test]
788    fn test_commit_id_collision_prevention() {
789        // Test that swapping components results in different hashes
790        let id1 = CommitId::new(Some("ab"), "cd", None);
791        let id2 = CommitId::new(Some("a"), "bcd", None);
792        assert_ne!(id1.hash, id2.hash);
793
794        // Test that None vs "none" string doesn't collide if we use prefixes correctly
795        // (Wait, we use "none" for None, so if state_hash was "none" and logic_hash was None,
796        // it might collide if we don't have prefixes)
797        let id3 = CommitId::new(None, "state", None);
798        let id4 = CommitId::new(Some("none"), "state", None);
799        assert_ne!(id3.hash, id4.hash);
800    }
801
802    #[test]
803    fn test_snapshot_record_size() {
804        let state = serde_json::json!({"key": "value", "nested": {"a": 1}});
805        let snapshot = SnapshotRecord::new("commit-123", state);
806
807        assert!(snapshot.size_bytes > 0);
808    }
809
810    #[test]
811    fn test_run_record_new() {
812        let run = RunRecord::new(
813            "run-123".to_string(),
814            "spec-digest-abc".to_string(),
815            Some("abc123".to_string()),
816            "test-agent".to_string(),
817            serde_json::json!({"env": "test"}),
818            None,
819            false,
820            1,
821        );
822
823        assert_eq!(run.run_id, "run-123");
824        assert_eq!(run.status, "RUNNING");
825        assert_eq!(run.total_events, 0);
826        assert!(!run.success);
827    }
828
829    #[test]
830    fn test_run_record_complete() {
831        let run = RunRecord::new(
832            "run-123".to_string(),
833            "spec-digest-abc".to_string(),
834            Some("abc123".to_string()),
835            "test-agent".to_string(),
836            serde_json::json!({}),
837            None,
838            false,
839            1,
840        )
841        .complete(5, Some("state-digest-xyz".to_string()), 1000);
842
843        assert_eq!(run.status, "COMPLETED");
844        assert_eq!(run.total_events, 5);
845        assert!(run.success);
846        assert!(run.completed_at.is_some());
847    }
848
849    #[test]
850    fn test_run_record_fail() {
851        let run = RunRecord::new(
852            "run-123".to_string(),
853            "spec-digest-abc".to_string(),
854            None,
855            "test-agent".to_string(),
856            serde_json::json!({}),
857            None,
858            false,
859            1,
860        )
861        .fail(2, 500);
862
863        assert_eq!(run.status, "FAILED");
864        assert_eq!(run.total_events, 2);
865        assert!(!run.success);
866        assert!(run.completed_at.is_some());
867    }
868
869    #[test]
870    fn test_run_event_record() {
871        let event = RunEventRecord::new(
872            "run-123".to_string(),
873            1,
874            "graph_started".to_string(),
875            serde_json::json!({"graph_id": "g1"}),
876        );
877
878        assert_eq!(event.run_id, "run-123");
879        assert_eq!(event.seq, 1);
880        assert_eq!(event.kind, "graph_started");
881    }
882
883    #[test]
884    fn test_release_record() {
885        let release = ReleaseRecordSchema::new(
886            "my-agent".to_string(),
887            "spec-digest-abc".to_string(),
888            Some("v1.0.0".to_string()),
889            "alice".to_string(),
890            Some("Initial release".to_string()),
891        );
892
893        assert_eq!(release.name, "my-agent");
894        assert_eq!(release.version_label, Some("v1.0.0".to_string()));
895    }
896
897    #[test]
898    fn test_decision_record_new() {
899        let decision = DecisionRecord::new(
900            "dec-123".to_string(),
901            "commit-abc".to_string(),
902            "task-optimize".to_string(),
903            "use_parallel".to_string(),
904            "Improves throughput".to_string(),
905            0.85,
906        );
907
908        assert_eq!(decision.decision_id, "dec-123");
909        assert_eq!(decision.commit_id, "commit-abc");
910        assert_eq!(decision.task, "task-optimize");
911        assert_eq!(decision.action, "use_parallel");
912        assert_eq!(decision.confidence, 0.85);
913        assert!(decision.outcome.is_none());
914    }
915
916    #[test]
917    fn test_decision_record_with_alternatives() {
918        let decision = DecisionRecord::new(
919            "dec-456".to_string(),
920            "commit-def".to_string(),
921            "task-retry".to_string(),
922            "exponential_backoff".to_string(),
923            "Reduces thundering herd".to_string(),
924            0.75,
925        )
926        .with_alternatives(vec!["linear_backoff".to_string(), "no_retry".to_string()]);
927
928        assert_eq!(decision.alternatives.len(), 2);
929        assert!(decision
930            .alternatives
931            .contains(&"linear_backoff".to_string()));
932    }
933
934    #[test]
935    fn test_decision_record_with_outcome() {
936        let outcome_json = serde_json::json!({
937            "status": "success",
938            "benefit": 1.23,
939            "duration_ms": 5000
940        });
941        let decision = DecisionRecord::new(
942            "dec-789".to_string(),
943            "commit-ghi".to_string(),
944            "task-cache".to_string(),
945            "redis_cache".to_string(),
946            "Faster lookups".to_string(),
947            0.9,
948        )
949        .with_outcome(outcome_json.to_string());
950
951        assert!(decision.outcome.is_some());
952        assert!(decision.outcome_at.is_some());
953    }
954
955    #[test]
956    fn test_memory_provenance_from_run_trace() {
957        let prov = MemoryProvenanceRecord::from_run_trace(
958            "mem-123".to_string(),
959            "run-456".to_string(),
960            42,
961        );
962
963        assert_eq!(prov.memory_id, "mem-123");
964        assert_eq!(prov.source_type, ProvenanceSourceType::RunTrace.to_string());
965        assert!(prov.derived_from.is_none());
966        assert!(prov.invalidated_at.is_none());
967
968        let source_data: serde_json::Value = prov.source_data;
969        assert_eq!(source_data["run_id"], "run-456");
970        assert_eq!(source_data["event_idx"], 42);
971    }
972
973    #[test]
974    fn test_memory_provenance_from_snapshot() {
975        let prov =
976            MemoryProvenanceRecord::from_snapshot("mem-789".to_string(), "commit-abc".to_string());
977
978        assert_eq!(prov.memory_id, "mem-789");
979        assert_eq!(
980            prov.source_type,
981            ProvenanceSourceType::StateSnapshot.to_string()
982        );
983        assert_eq!(prov.source_data["commit_id"], "commit-abc");
984    }
985
986    #[test]
987    fn test_memory_provenance_from_derivation() {
988        let prov = MemoryProvenanceRecord::from_derivation(
989            "mem-new".to_string(),
990            "mem-parent".to_string(),
991            "summarize".to_string(),
992        );
993
994        assert_eq!(prov.memory_id, "mem-new");
995        assert_eq!(prov.derived_from, Some("mem-parent".to_string()));
996        assert_eq!(
997            prov.source_type,
998            ProvenanceSourceType::MemoryDerivation.to_string()
999        );
1000        assert_eq!(prov.source_data["derivation"], "summarize");
1001    }
1002
1003    #[test]
1004    fn test_memory_provenance_invalidation() {
1005        let prov = MemoryProvenanceRecord::from_user_annotation(
1006            "mem-123".to_string(),
1007            "user-456".to_string(),
1008        );
1009
1010        assert!(prov.invalidated_at.is_none());
1011
1012        let invalidated = prov.invalidate();
1013        assert!(invalidated.invalidated_at.is_some());
1014    }
1015
1016    #[test]
1017    fn test_provenance_source_type_display() {
1018        assert_eq!(ProvenanceSourceType::RunTrace.to_string(), "run_trace");
1019        assert_eq!(
1020            ProvenanceSourceType::StateSnapshot.to_string(),
1021            "state_snapshot"
1022        );
1023        assert_eq!(
1024            ProvenanceSourceType::UserAnnotation.to_string(),
1025            "user_annotation"
1026        );
1027        assert_eq!(
1028            ProvenanceSourceType::MemoryDerivation.to_string(),
1029            "memory_derivation"
1030        );
1031    }
1032}