use serde::{Deserialize, Serialize};
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AuditEvent {
pub v: u32,
pub ts: String,
pub entity: String,
pub entity_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
pub op: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<String>,
pub actor: String,
pub by: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<serde_json::Value>,
#[serde(default = "default_count", skip_serializing_if = "is_default_count")]
pub count: u64,
pub ctx: EventContext,
}
fn is_default_count(count: &u64) -> bool {
*count <= 1
}
fn default_count() -> u64 {
1
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntityType {
Task,
Change,
Module,
Wave,
Planning,
Config,
}
impl EntityType {
pub fn as_str(&self) -> &'static str {
match self {
EntityType::Task => "task",
EntityType::Change => "change",
EntityType::Module => "module",
EntityType::Wave => "wave",
EntityType::Planning => "planning",
EntityType::Config => "config",
}
}
}
impl std::fmt::Display for EntityType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Actor {
Cli,
Reconcile,
Ralph,
}
impl Actor {
pub fn as_str(&self) -> &'static str {
match self {
Actor::Cli => "cli",
Actor::Reconcile => "reconcile",
Actor::Ralph => "ralph",
}
}
}
impl std::fmt::Display for Actor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EventContext {
pub session_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub harness_session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub worktree: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub commit: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct WorktreeInfo {
pub path: std::path::PathBuf,
pub branch: Option<String>,
pub is_main: bool,
}
#[derive(Debug, Clone)]
pub struct TaggedAuditEvent {
pub event: AuditEvent,
pub source: WorktreeInfo,
}
pub mod ops {
pub const TASK_CREATE: &str = "create";
pub const TASK_STATUS_CHANGE: &str = "status_change";
pub const TASK_ADD: &str = "add";
pub const CHANGE_CREATE: &str = "create";
pub const CHANGE_ARCHIVE: &str = "archive";
pub const MODULE_CREATE: &str = "create";
pub const MODULE_CHANGE_ADDED: &str = "change_added";
pub const MODULE_CHANGE_COMPLETED: &str = "change_completed";
pub const WAVE_UNLOCK: &str = "unlock";
pub const PLANNING_DECISION: &str = "decision";
pub const PLANNING_BLOCKER: &str = "blocker";
pub const PLANNING_QUESTION: &str = "question";
pub const PLANNING_NOTE: &str = "note";
pub const PLANNING_FOCUS_CHANGE: &str = "focus_change";
pub const CONFIG_SET: &str = "set";
pub const CONFIG_UNSET: &str = "unset";
pub const RECONCILED: &str = "reconciled";
}
pub struct AuditEventBuilder {
entity: Option<EntityType>,
entity_id: Option<String>,
scope: Option<String>,
op: Option<String>,
from: Option<String>,
to: Option<String>,
actor: Option<Actor>,
by: Option<String>,
meta: Option<serde_json::Value>,
ctx: Option<EventContext>,
}
impl AuditEventBuilder {
pub fn new() -> Self {
Self {
entity: None,
entity_id: None,
scope: None,
op: None,
from: None,
to: None,
actor: None,
by: None,
meta: None,
ctx: None,
}
}
pub fn entity(mut self, entity: EntityType) -> Self {
self.entity = Some(entity);
self
}
pub fn entity_id(mut self, id: impl Into<String>) -> Self {
self.entity_id = Some(id.into());
self
}
pub fn scope(mut self, scope: impl Into<String>) -> Self {
self.scope = Some(scope.into());
self
}
pub fn op(mut self, op: impl Into<String>) -> Self {
self.op = Some(op.into());
self
}
pub fn from(mut self, from: impl Into<String>) -> Self {
self.from = Some(from.into());
self
}
pub fn to(mut self, to: impl Into<String>) -> Self {
self.to = Some(to.into());
self
}
pub fn actor(mut self, actor: Actor) -> Self {
self.actor = Some(actor);
self
}
pub fn by(mut self, by: impl Into<String>) -> Self {
self.by = Some(by.into());
self
}
pub fn meta(mut self, meta: serde_json::Value) -> Self {
self.meta = Some(meta);
self
}
pub fn ctx(mut self, ctx: EventContext) -> Self {
self.ctx = Some(ctx);
self
}
pub fn build(self) -> Option<AuditEvent> {
let entity = self.entity?;
let entity_id = self.entity_id?;
let op = self.op?;
let actor = self.actor?;
let by = self.by?;
let ctx = self.ctx?;
let ts = chrono::Utc::now()
.format("%Y-%m-%dT%H:%M:%S%.3fZ")
.to_string();
Some(AuditEvent {
v: SCHEMA_VERSION,
ts,
entity: entity.as_str().to_string(),
entity_id,
scope: self.scope,
op,
from: self.from,
to: self.to,
actor: actor.as_str().to_string(),
by,
meta: self.meta,
count: 1,
ctx,
})
}
}
impl Default for AuditEventBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[path = "event_tests.rs"]
mod event_tests;