167 lines
5.6 KiB
Rust
167 lines
5.6 KiB
Rust
//! 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=<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));
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
);
|
|
}
|
|
}
|