Closed-loop failure reporting for software systems.
circuit is a small Rust crate that takes a stance from Norbert Wiener's Cybernetics (1948) and applies it to the way software reports its own failures.
Wiener's central claim — the move that founded the field of cybernetics — is that effective control of a system requires the controlled system to feed information back to the controller. A thermostat is the textbook case: a heater that doesn't know the room temperature can heat or not heat, but it cannot regulate. Feedback is what turns actuation into control.
Wiener's deeper point, the one that gets lost when cybernetics is reduced to "feedback loops," is that a signal is feedback only if it changes the controller's behavior. A thermometer that nobody is reading is not feedback. A telemetry event that lands in a dashboard nobody watches is not feedback. A LogShipper.event(...) call that goes to a sink with no monitored consumer is not feedback. They are all open-loop emissions — signals into a void.
The bug class that follows is the canonical software failure of the modern era: the surface keeps reassuring the user that everything is fine while the underlying state has stopped advancing. The user only learns from indirect evidence — lost data, missed alarms, a session that mysteriously never accumulated. The app appears to work. The control loop was never closed.
circuit exists to make loop-closing a first-class concept the code is responsible for.
A Circuit bundles three things:
-
A typed event taxonomy (
CircuitEvent) with nested causal chains. Every failure has a kind (one ofValueUnavailable,BatchFailed,ServiceQuotaExceeded,StateMachineStuck,ContractViolated,ContextLost,Other) and an optionalcaused_bypointer to the upstream cause. Real-world failures are layered; the chain captures the story. -
A set of sinks (
CircuitSink). Discord webhooks, on-disk records, in-app banners, system notifications — each declared user-facing or not. Adding a new transport means implementing one trait. -
A delivery policy. By default a circuit requires:
- A quorum of acknowledgments (
min_acks, default 1): at least N sinks must accept the event. - At least one user-facing sink (
require_user_facing, default true): the cybernetic rule. The user is the controller of last resort; an event that doesn't reach a user-facing sink is missing its most important consumer.
If either constraint is violated,
circuit.report(&event)returnsErr(DeliveryError::...). It is impossible to silently fail to deliver a circuit event. The void is closed. - A quorum of acknowledgments (
use circuit::{Circuit, CircuitEvent, CircuitEventKind, Severity,
ValueUnavailableReason};
use circuit::sink::{ConsoleSink, InMemorySink};
let circuit = Circuit::builder()
.sink(ConsoleSink::new("dev-console"))
.sink(InMemorySink::new("user-banner").user_facing())
.build();
// Today's D-Minder bug, expressed as a nested circuit event:
let event = CircuitEvent::new(
CircuitEventKind::ContractViolated {
contract: "timer-actively-tracking-vitamin-d".into(),
expected: "rate × elapsed accumulating".into(),
actual: "session terminated after one minute".into(),
},
Severity::Critical,
)
.caused_by(
CircuitEvent::new(
CircuitEventKind::ValueUnavailable {
what: "vitamin_d_rate".into(),
reason: ValueUnavailableReason::NotYetComputed,
},
Severity::Error,
)
.caused_by(
CircuitEvent::new(
CircuitEventKind::ContextLost {
operation: "ios26-foreground-resume".into(),
what: "rate-recalc-async-deps".into(),
},
Severity::Warn,
),
),
);
circuit.report(&event).expect("delivered to at least one user-facing sink");InMemorySink— buffers events in aVec, ideal for testsConsoleSink— writes one line per event to stderrDiscordWebhookSink(feature:discord, default on) — POSTs to a Discord channel webhook with the event kind, severity glyph, causal chain, and fields. Anyone with the webhook URL can post to that channel; treat it as a secret.
iOS and Android sinks (system notifications, in-app banners) live in their own crates and conform to CircuitSink via FFI. The crate intentionally does not pull in platform code — it stays cross-compilable.
circuit is the cybernetic substrate. Each consumer treats it as a load-bearing primitive:
- D-Minder (iOS + Android) — runtime failure reporting from the app to ops and to the user's screen.
- intratom — uses
circuitboth as a dependency (so its own scoring failures don't go silent) and as a subject of analysis (theclosed-looprule in intratom flags code that emits failure-like signals without routing them through a circuit). - analog — when an operation's post-condition stops being true, the failure reports through a
circuit. - baker and other side projects — same pattern.
A circuit is the simplest closed-loop object physics gives us — current flows, returns, the loop is complete. The metaphor is honest: a signal that doesn't return to where it can affect the controller is not a circuit. It is a broken wire.
Dual-licensed under MIT or Apache-2.0 at your option.