1use serde::{Deserialize, Serialize};
8
9pub const SCHEMA_VERSION: u32 = 1;
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17pub struct AuditEvent {
18 pub v: u32,
20 pub ts: String,
22 pub entity: String,
24 pub entity_id: String,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub scope: Option<String>,
29 pub op: String,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub from: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub to: Option<String>,
37 pub actor: String,
39 pub by: String,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub meta: Option<serde_json::Value>,
44 #[serde(default = "default_count", skip_serializing_if = "is_default_count")]
46 pub count: u64,
47 pub ctx: EventContext,
49}
50
51fn is_default_count(count: &u64) -> bool {
52 *count <= 1
53}
54
55fn default_count() -> u64 {
56 1
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum EntityType {
63 Task,
65 Change,
67 Module,
69 Wave,
71 Planning,
73 Config,
75}
76
77impl EntityType {
78 pub fn as_str(&self) -> &'static str {
80 match self {
81 EntityType::Task => "task",
82 EntityType::Change => "change",
83 EntityType::Module => "module",
84 EntityType::Wave => "wave",
85 EntityType::Planning => "planning",
86 EntityType::Config => "config",
87 }
88 }
89}
90
91impl std::fmt::Display for EntityType {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.write_str(self.as_str())
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum Actor {
101 Cli,
103 Reconcile,
105 Ralph,
107}
108
109impl Actor {
110 pub fn as_str(&self) -> &'static str {
112 match self {
113 Actor::Cli => "cli",
114 Actor::Reconcile => "reconcile",
115 Actor::Ralph => "ralph",
116 }
117 }
118}
119
120impl std::fmt::Display for Actor {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 f.write_str(self.as_str())
123 }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
128pub struct EventContext {
129 pub session_id: String,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub harness_session_id: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub branch: Option<String>,
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub worktree: Option<String>,
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub commit: Option<String>,
143}
144
145#[derive(Debug, Clone, PartialEq)]
147pub struct WorktreeInfo {
148 pub path: std::path::PathBuf,
150 pub branch: Option<String>,
152 pub is_main: bool,
154}
155
156#[derive(Debug, Clone)]
158pub struct TaggedAuditEvent {
159 pub event: AuditEvent,
161 pub source: WorktreeInfo,
163}
164
165pub mod ops {
170 pub const TASK_CREATE: &str = "create";
173 pub const TASK_STATUS_CHANGE: &str = "status_change";
175 pub const TASK_ADD: &str = "add";
177
178 pub const CHANGE_CREATE: &str = "create";
181 pub const CHANGE_ARCHIVE: &str = "archive";
183
184 pub const MODULE_CREATE: &str = "create";
187 pub const MODULE_CHANGE_ADDED: &str = "change_added";
189 pub const MODULE_CHANGE_COMPLETED: &str = "change_completed";
191
192 pub const WAVE_UNLOCK: &str = "unlock";
195
196 pub const PLANNING_DECISION: &str = "decision";
199 pub const PLANNING_BLOCKER: &str = "blocker";
201 pub const PLANNING_QUESTION: &str = "question";
203 pub const PLANNING_NOTE: &str = "note";
205 pub const PLANNING_FOCUS_CHANGE: &str = "focus_change";
207
208 pub const CONFIG_SET: &str = "set";
211 pub const CONFIG_UNSET: &str = "unset";
213
214 pub const RECONCILED: &str = "reconciled";
217}
218
219pub struct AuditEventBuilder {
224 entity: Option<EntityType>,
225 entity_id: Option<String>,
226 scope: Option<String>,
227 op: Option<String>,
228 from: Option<String>,
229 to: Option<String>,
230 actor: Option<Actor>,
231 by: Option<String>,
232 meta: Option<serde_json::Value>,
233 ctx: Option<EventContext>,
234}
235
236impl AuditEventBuilder {
237 pub fn new() -> Self {
239 Self {
240 entity: None,
241 entity_id: None,
242 scope: None,
243 op: None,
244 from: None,
245 to: None,
246 actor: None,
247 by: None,
248 meta: None,
249 ctx: None,
250 }
251 }
252
253 pub fn entity(mut self, entity: EntityType) -> Self {
255 self.entity = Some(entity);
256 self
257 }
258
259 pub fn entity_id(mut self, id: impl Into<String>) -> Self {
261 self.entity_id = Some(id.into());
262 self
263 }
264
265 pub fn scope(mut self, scope: impl Into<String>) -> Self {
267 self.scope = Some(scope.into());
268 self
269 }
270
271 pub fn op(mut self, op: impl Into<String>) -> Self {
273 self.op = Some(op.into());
274 self
275 }
276
277 pub fn from(mut self, from: impl Into<String>) -> Self {
279 self.from = Some(from.into());
280 self
281 }
282
283 pub fn to(mut self, to: impl Into<String>) -> Self {
285 self.to = Some(to.into());
286 self
287 }
288
289 pub fn actor(mut self, actor: Actor) -> Self {
291 self.actor = Some(actor);
292 self
293 }
294
295 pub fn by(mut self, by: impl Into<String>) -> Self {
297 self.by = Some(by.into());
298 self
299 }
300
301 pub fn meta(mut self, meta: serde_json::Value) -> Self {
303 self.meta = Some(meta);
304 self
305 }
306
307 pub fn ctx(mut self, ctx: EventContext) -> Self {
309 self.ctx = Some(ctx);
310 self
311 }
312
313 pub fn build(self) -> Option<AuditEvent> {
318 let entity = self.entity?;
319 let entity_id = self.entity_id?;
320 let op = self.op?;
321 let actor = self.actor?;
322 let by = self.by?;
323 let ctx = self.ctx?;
324
325 let ts = chrono::Utc::now()
326 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
327 .to_string();
328
329 Some(AuditEvent {
330 v: SCHEMA_VERSION,
331 ts,
332 entity: entity.as_str().to_string(),
333 entity_id,
334 scope: self.scope,
335 op,
336 from: self.from,
337 to: self.to,
338 actor: actor.as_str().to_string(),
339 by,
340 meta: self.meta,
341 count: 1,
342 ctx,
343 })
344 }
345}
346
347impl Default for AuditEventBuilder {
348 fn default() -> Self {
349 Self::new()
350 }
351}
352
353#[cfg(test)]
354#[path = "event_tests.rs"]
355mod event_tests;