//! Concrete subscriber stubs for the event bus, plus the production audit-log subscriber. use super::Stage; use super::events::{TransitionFired, TransitionSubscriber}; #[allow(unused_imports)] use super::{event_label, stage_dir_name, stage_label}; // ── Audit log subscriber ───────────────────────────────────────────────────── /// Format a `TransitionFired` event as a structured one-line audit log entry. /// /// Fields are in stable `key=value` order separated by spaces: /// `audit ts= id= from= to= event=` pub fn format_audit_entry(f: &TransitionFired) -> String { format!( "audit ts={} id={} from={} to={} event={}", f.at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true), f.story_id, stage_label(&f.before), stage_label(&f.after), event_label(&f.event), ) } /// Subscriber that writes structured one-line audit entries for every stage transition. pub struct AuditLogSubscriber; impl TransitionSubscriber for AuditLogSubscriber { fn name(&self) -> &'static str { "audit-log" } fn on_transition(&self, f: &TransitionFired) { crate::slog!("{}", format_audit_entry(f)); } } /// Reconcile: no-op for the audit log subscriber. /// /// The audit log records live transitions only. Replaying historical CRDT state at /// reconcile time would produce misleading entries (wrong timestamps, duplicate lines). /// Eventual consistency of the audit log is not required — missed events are simply /// absent from the log, which is acceptable. pub(crate) fn reconcile_audit_log() {} /// Spawn a background task that writes a structured audit log entry for every pipeline transition. /// /// Subscribes to the transition broadcast channel. Every `TransitionFired` event produces /// one line via [`format_audit_entry`] and writes it to the shared log buffer. pub fn spawn_audit_log_subscriber() { let sub = AuditLogSubscriber; let mut rx = super::events::subscribe_transitions(); tokio::spawn(async move { loop { match rx.recv().await { Ok(fired) => sub.on_transition(&fired), Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { crate::slog_warn!( "[audit-log] Subscriber lagged, skipped {n} event(s); \ some transitions may be absent from the audit log." ); } Err(tokio::sync::broadcast::error::RecvError::Closed) => break, } } }); } // ── Subscriber stubs (real dispatch uses these as the interface) ───────────── // // These are ready to wire into the event bus but not yet connected to the // actual subsystems. Suppress dead_code until consumers are migrated. /// Subscriber that logs pipeline transitions to the Matrix bot channel. #[allow(dead_code)] pub struct MatrixBotSubscriber; #[allow(dead_code)] impl TransitionSubscriber for MatrixBotSubscriber { fn name(&self) -> &'static str { "matrix-bot" } fn on_transition(&self, f: &TransitionFired) { crate::slog!( "[pipeline/matrix-bot] #{}: {} → {}", f.story_id, stage_label(&f.before), stage_label(&f.after) ); } } /// Subscriber that re-renders the filesystem `work/` directory on stage transitions. #[allow(dead_code)] pub struct FileRendererSubscriber; #[allow(dead_code)] impl TransitionSubscriber for FileRendererSubscriber { fn name(&self) -> &'static str { "filesystem" } fn on_transition(&self, f: &TransitionFired) { crate::slog!( "[pipeline/filesystem] re-rendering work/{}/{}", stage_dir_name(&f.after), f.story_id ); } } /// Subscriber that writes stage updates to the pipeline-items data store. #[allow(dead_code)] pub struct PipelineItemsSubscriber; #[allow(dead_code)] impl TransitionSubscriber for PipelineItemsSubscriber { fn name(&self) -> &'static str { "pipeline-items" } fn on_transition(&self, f: &TransitionFired) { crate::slog!( "[pipeline/items] UPDATE stage = '{}' WHERE id = '{}'", stage_dir_name(&f.after), f.story_id ); } } /// Subscriber that promotes eligible backlog items when a story completes or is archived. #[allow(dead_code)] pub struct AutoAssignSubscriber; #[allow(dead_code)] impl TransitionSubscriber for AutoAssignSubscriber { fn name(&self) -> &'static str { "auto-assign" } fn on_transition(&self, f: &TransitionFired) { if matches!( f.after, Stage::Done { .. } | Stage::Archived { .. } | Stage::Abandoned { .. } | Stage::Superseded { .. } | Stage::Rejected { .. } ) { crate::slog!( "[pipeline/auto-assign] story {} reached {}; checking for promotable backlog items", f.story_id, stage_label(&f.after) ); } } } /// Subscriber that broadcasts stage transitions to all connected WebSocket clients. #[allow(dead_code)] pub struct WebUiBroadcastSubscriber; #[allow(dead_code)] impl TransitionSubscriber for WebUiBroadcastSubscriber { fn name(&self) -> &'static str { "web-ui-broadcast" } fn on_transition(&self, f: &TransitionFired) { crate::slog!( "[pipeline/web-ui] broadcasting #{} transition to connected clients", f.story_id ); } }