1use serde::{Deserialize, Serialize};
2use std::path::{Path, PathBuf};
3
4const STORE_FILENAME: &str = "autonomy_drivers_v1.json";
5
6const MAX_EVENTS: usize = 128;
8const MAX_DECISIONS_PER_EVENT: usize = 16;
9const MAX_TOOL_CHARS: usize = 64;
10const MAX_ACTION_CHARS: usize = 64;
11const MAX_REASON_CODE_CHARS: usize = 48;
12const MAX_REASON_CHARS: usize = 220;
13const MAX_DETAIL_CHARS: usize = 512;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum AutonomyPhaseV1 {
18 PreCall,
19 PostRead,
20 PostCall,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum AutonomyDriverKindV1 {
26 Preload,
27 Prefetch,
28 Dedup,
29 Response,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum AutonomyVerdictV1 {
35 Run,
36 Skip,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct AutonomyDriverDecisionV1 {
41 pub driver: AutonomyDriverKindV1,
42 pub verdict: AutonomyVerdictV1,
43 pub reason_code: String,
44 pub reason: String,
45 #[serde(default)]
46 pub detail: Option<String>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct AutonomyDriverEventV1 {
51 pub seq: u64,
52 pub created_at: String,
53 pub phase: AutonomyPhaseV1,
54 pub role: String,
55 pub profile: String,
56 pub tool: String,
57 #[serde(default)]
58 pub action: Option<String>,
59 pub decisions: Vec<AutonomyDriverDecisionV1>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct AutonomyDriversV1 {
64 pub schema_version: u32,
65 pub created_at: String,
66 pub updated_at: String,
67 pub next_seq: u64,
68 #[serde(default)]
69 pub events: Vec<AutonomyDriverEventV1>,
70}
71
72impl Default for AutonomyDriversV1 {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78fn store_path() -> Option<PathBuf> {
79 crate::core::data_dir::lean_ctx_data_dir()
80 .ok()
81 .map(|d| d.join(STORE_FILENAME))
82}
83
84impl AutonomyDriversV1 {
85 pub fn new() -> Self {
86 let now = chrono::Utc::now().to_rfc3339();
87 Self {
88 schema_version: crate::core::contracts::AUTONOMY_DRIVERS_V1_SCHEMA_VERSION,
89 created_at: now.clone(),
90 updated_at: now,
91 next_seq: 1,
92 events: Vec::new(),
93 }
94 }
95
96 pub fn load() -> Self {
97 let Some(path) = store_path() else {
98 return Self::new();
99 };
100 let content = std::fs::read_to_string(&path).unwrap_or_default();
101 serde_json::from_str::<Self>(&content).unwrap_or_else(|_| Self::new())
102 }
103
104 pub fn save(&self) -> Result<(), String> {
105 let Some(path) = store_path() else {
106 return Err("no data dir".to_string());
107 };
108 let json = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?;
109 let json = crate::core::redaction::redact_text(&json);
111 crate::config_io::write_atomic_with_backup(&path, &json)?;
112 Ok(())
113 }
114
115 pub fn record(&mut self, mut ev: AutonomyDriverEventV1) {
116 ev.seq = self.next_seq;
117 self.next_seq = self.next_seq.saturating_add(1);
118 self.updated_at = chrono::Utc::now().to_rfc3339();
119
120 bound_event_in_place(&mut ev);
121 self.events.push(ev);
122 self.prune_in_place();
123 }
124
125 pub fn latest(&self) -> Option<&AutonomyDriverEventV1> {
126 self.events.last()
127 }
128
129 fn prune_in_place(&mut self) {
130 if self.events.len() <= MAX_EVENTS {
131 return;
132 }
133 let overflow = self.events.len() - MAX_EVENTS;
134 self.events.drain(0..overflow);
135 }
136}
137
138fn bound_event_in_place(ev: &mut AutonomyDriverEventV1) {
139 ev.tool = truncate(&ev.tool, MAX_TOOL_CHARS);
140 if let Some(a) = ev.action.clone() {
141 let t = truncate(&a, MAX_ACTION_CHARS);
142 ev.action = if t.trim().is_empty() { None } else { Some(t) };
143 }
144 if ev.decisions.len() > MAX_DECISIONS_PER_EVENT {
145 ev.decisions.truncate(MAX_DECISIONS_PER_EVENT);
146 }
147 for d in &mut ev.decisions {
148 d.reason_code = truncate(&d.reason_code, MAX_REASON_CODE_CHARS);
149 d.reason = truncate(&d.reason, MAX_REASON_CHARS);
150 if let Some(detail) = d.detail.clone() {
151 let t = truncate(&detail, MAX_DETAIL_CHARS);
152 d.detail = if t.trim().is_empty() { None } else { Some(t) };
153 }
154 }
155}
156
157fn truncate(s: &str, max: usize) -> String {
158 let s = s.trim();
159 if s.len() <= max {
160 return s.to_string();
161 }
162 let mut out = s[..max].to_string();
163 out.push('…');
164 out
165}
166
167pub fn write_project_autonomy_drivers_v1(
168 project_root: &Path,
169 drivers: &AutonomyDriversV1,
170 filename: Option<&str>,
171) -> Result<PathBuf, String> {
172 let proofs_dir = project_root.join(".lean-ctx").join("proofs");
173 std::fs::create_dir_all(&proofs_dir).map_err(|e| e.to_string())?;
174
175 let ts = chrono::Utc::now().format("%Y-%m-%d_%H%M%S");
176 let name = filename.map_or_else(
177 || format!("autonomy-drivers-v1_{ts}.json"),
178 std::string::ToString::to_string,
179 );
180 let path = proofs_dir.join(name);
181
182 let json = serde_json::to_string_pretty(drivers).map_err(|e| e.to_string())?;
183 let json = crate::core::redaction::redact_text(&json);
184 crate::config_io::write_atomic(&path, &json)?;
185 Ok(path)
186}
187
188pub fn format_compact_event(ev: &AutonomyDriverEventV1) -> String {
189 let mut parts = Vec::new();
190 for d in &ev.decisions {
191 let driver = match d.driver {
192 AutonomyDriverKindV1::Preload => "preload",
193 AutonomyDriverKindV1::Prefetch => "prefetch",
194 AutonomyDriverKindV1::Dedup => "dedup",
195 AutonomyDriverKindV1::Response => "response",
196 };
197 let verdict = match d.verdict {
198 AutonomyVerdictV1::Run => "run",
199 AutonomyVerdictV1::Skip => "skip",
200 };
201 parts.push(format!("{driver}={verdict}({})", d.reason_code));
202 }
203 format!(
204 "[autonomy:{}] {}",
205 match ev.phase {
206 AutonomyPhaseV1::PreCall => "pre",
207 AutonomyPhaseV1::PostRead => "read",
208 AutonomyPhaseV1::PostCall => "post",
209 },
210 parts.join(", ")
211 )
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn record_bounded_and_seq_increments() {
220 let mut s = AutonomyDriversV1::new();
221 for i in 0..(MAX_EVENTS + 5) {
222 s.record(AutonomyDriverEventV1 {
223 seq: 0,
224 created_at: "2026-01-01T00:00:00Z".to_string(),
225 phase: AutonomyPhaseV1::PreCall,
226 role: "coder".to_string(),
227 profile: "exploration".to_string(),
228 tool: format!("tool{i}"),
229 action: None,
230 decisions: vec![AutonomyDriverDecisionV1 {
231 driver: AutonomyDriverKindV1::Preload,
232 verdict: AutonomyVerdictV1::Skip,
233 reason_code: "disabled".to_string(),
234 reason: "disabled".to_string(),
235 detail: None,
236 }],
237 });
238 }
239 assert!(s.events.len() <= MAX_EVENTS);
240 assert_eq!(s.events.last().unwrap().seq, s.next_seq - 1);
241 }
242
243 #[test]
244 fn compact_format_includes_phase_and_drivers() {
245 let ev = AutonomyDriverEventV1 {
246 seq: 1,
247 created_at: "2026-01-01T00:00:00Z".to_string(),
248 phase: AutonomyPhaseV1::PostCall,
249 role: "coder".to_string(),
250 profile: "exploration".to_string(),
251 tool: "ctx_read".to_string(),
252 action: Some("full".to_string()),
253 decisions: vec![AutonomyDriverDecisionV1 {
254 driver: AutonomyDriverKindV1::Response,
255 verdict: AutonomyVerdictV1::Run,
256 reason_code: "output_large".to_string(),
257 reason: "output large".to_string(),
258 detail: None,
259 }],
260 };
261 let s = format_compact_event(&ev);
262 assert!(s.contains("autonomy:post"));
263 assert!(s.contains("response=run"));
264 }
265}