Skip to content

ontometrics/circuit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

circuit

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.

The Wiener stance

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.

What circuit does

A Circuit bundles three things:

  1. A typed event taxonomy (CircuitEvent) with nested causal chains. Every failure has a kind (one of ValueUnavailable, BatchFailed, ServiceQuotaExceeded, StateMachineStuck, ContractViolated, ContextLost, Other) and an optional caused_by pointer to the upstream cause. Real-world failures are layered; the chain captures the story.

  2. 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.

  3. 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) returns Err(DeliveryError::...). It is impossible to silently fail to deliver a circuit event. The void is closed.

Example

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");

Sinks shipped in v0

  • InMemorySink — buffers events in a Vec, ideal for tests
  • ConsoleSink — writes one line per event to stderr
  • DiscordWebhookSink (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.

Platform sinks

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.

Why a separate crate

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 circuit both as a dependency (so its own scoring failures don't go silent) and as a subject of analysis (the closed-loop rule 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.

Naming

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.

License

Dual-licensed under MIT or Apache-2.0 at your option.

About

Closed-loop failure reporting for software systems. A Wiener-style 'circuit' for app failures — typed event taxonomy with causal chains, multiple redundant sinks, quorum policy requiring at least one user-facing receiver. Kills the silent-disappear bug class structurally.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages