Files
huskies/server/src/pipeline_state/subscribers.rs
T

167 lines
5.6 KiB
Rust
Raw Normal View History

2026-05-13 23:18:59 +00:00
//! 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};
2026-05-13 23:18:59 +00:00
// ── 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=<ISO8601> id=<story_id> from=<from_stage> to=<to_stage> event=<event_label>`
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));
}
}
2026-05-14 23:39:56 +00:00
/// 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() {}
2026-05-13 23:18:59 +00:00
/// 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.
2026-04-29 10:41:32 +00:00
/// 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)
);
}
}
2026-04-29 10:41:32 +00:00
/// 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
);
}
}
2026-04-29 10:41:32 +00:00
/// 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
);
}
}
2026-04-29 10:41:32 +00:00
/// 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) {
2026-05-13 16:43:19 +00:00
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)
);
}
}
}
2026-04-29 10:41:32 +00:00
/// 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
);
}
}