Skip to main content

opendev_context/compaction/
artifacts.rs

1//! Artifact index for tracking files touched during a session.
2
3use std::collections::HashMap;
4
5use chrono::Local;
6use serde::{Deserialize, Serialize};
7
8/// Tracks files touched during a session, surviving compaction.
9///
10/// Records file operations (create, modify, read, delete) with metadata
11/// so the agent retains awareness of workspace state post-compaction.
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
13pub struct ArtifactIndex {
14    pub entries: HashMap<String, ArtifactEntry>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ArtifactEntry {
19    pub file_path: String,
20    pub last_operation: String,
21    pub last_details: String,
22    pub created_at: String,
23    pub updated_at: String,
24    pub operation_count: u32,
25    pub operations_seen: Vec<String>,
26}
27
28impl ArtifactIndex {
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Record a file operation.
34    pub fn record(&mut self, file_path: &str, operation: &str, details: &str) {
35        let now = Local::now().to_rfc3339();
36        if let Some(existing) = self.entries.get_mut(file_path) {
37            existing.last_operation.clear();
38            existing.last_operation.push_str(operation);
39            existing.last_details.clear();
40            existing.last_details.push_str(details);
41            existing.updated_at = now;
42            existing.operation_count += 1;
43            if !existing.operations_seen.iter().any(|s| s == operation) {
44                existing.operations_seen.push(operation.to_owned());
45            }
46        } else {
47            let op = operation.to_owned();
48            self.entries.insert(
49                file_path.to_owned(),
50                ArtifactEntry {
51                    file_path: file_path.to_owned(),
52                    last_operation: op.clone(),
53                    last_details: details.to_owned(),
54                    created_at: now.clone(),
55                    updated_at: now,
56                    operation_count: 1,
57                    operations_seen: vec![op],
58                },
59            );
60        }
61    }
62
63    /// Format the artifact index as a compact summary for injection into compaction.
64    pub fn as_summary(&self) -> String {
65        if self.entries.is_empty() {
66            return String::new();
67        }
68        let mut lines = vec!["## Artifact Index (files touched this session)".to_string()];
69        for (path, entry) in &self.entries {
70            let ops = entry.operations_seen.join(", ");
71            let detail = if entry.last_details.is_empty() {
72                String::new()
73            } else {
74                format!(" — {}", entry.last_details)
75            };
76            lines.push(format!("- `{path}` [{ops}]{detail}"));
77        }
78        lines.join("\n")
79    }
80
81    pub fn len(&self) -> usize {
82        self.entries.len()
83    }
84
85    pub fn is_empty(&self) -> bool {
86        self.entries.is_empty()
87    }
88
89    /// Serialize the artifact index to a JSON value for session persistence.
90    pub fn to_json(&self) -> serde_json::Value {
91        serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
92    }
93
94    /// Deserialize an artifact index from a JSON value (loaded from session metadata).
95    pub fn from_json(value: &serde_json::Value) -> Option<Self> {
96        serde_json::from_value(value.clone()).ok()
97    }
98}
99
100#[cfg(test)]
101#[path = "artifacts_tests.rs"]
102mod tests;