diff --git a/02-DECISIONS/0046-events-are-a-relationship.md b/02-DECISIONS/0046-events-are-a-relationship.md new file mode 100644 index 0000000..4c5cb97 --- /dev/null +++ b/02-DECISIONS/0046-events-are-a-relationship.md @@ -0,0 +1,84 @@ +--- +status: accepted +date: 2026-09-03 +deciders: jochen +reconstructed: false +extends: 0045-what-a-module-is.md +--- + +# 46. Events are a relationship, the lighter sibling of provisioning + +## Context + +[ADR 0045](0045-what-a-module-is.md) names two relationships between modules — seats and +provide/require (provisioning). A third is latent in the mesh and worth making first-class: the +broker every node already runs ([ADR 0001](0001-nodes-communicate-over-a-broker.md)) can carry a +module's activity as **events**, which any other module reacts to. A logger that writes an audit +trail, a module that acts when another module acts, observability — all of it is one mechanism, and +today it is ambient rather than declared. + +## Decision + +**A module emits events and consumes events, and both are declared** — parallel to `provides` / +`requires`, so the mesh knows the event graph the same way it knows the provisioning graph. + +### Events are provisioning's lighter sibling + +| | provisioning | events | +|---|---|---| +| shape | **1:1**, a provider creates a resource *for* one consumer | **1:many**, a module emits, any number listen | +| credential | yes — sealed, per consumer | none — it is broadcast | +| machinery | a provisioner (the reconcile adapter) | nothing but the broker's topic routing | +| declared as | `provides` / `requires` | `emits` / `consumes` | + +Because an event is broadcast and credential-free, there is no provisioner and no per-consumer +setup — only a subscription. That is why it is the *lighter* relationship, and why most +inter-module reaction should be an event, not a provision. + +### An event carries what an audit needs + +Every event carries its **type** (a dotted topic key, so listeners match by prefix), its **source** +module, the **node** it came from, and the **time**. A body follows. The metadata is not optional: +a reaction may only need the body, but an audit trail needs to know who did what, where and when, +and an event that cannot answer that is not auditable. + +### The audit logger is just a consumer of everything + +A logger that records the whole mesh's activity is **not a privileged component** — it is an +ordinary module that consumes `#` (every event) and writes them down. It holds no special access; +it only listens widely. That it falls out of the model with no new machinery is the check that the +model is right. + +### `consumes` is validated like `requires` + +A `consumes` for an event that **nothing** `emits` is a dangling edge, and the mesh refuses it +before deploy — the same rule that catches a `requires` for a resource nothing provides +([research 011](../01-RESEARCH/011-the-module-graph/00-overview.md)). A listener waiting for an +event that can never arrive is a silent failure, and this repository's whole discipline is against +silent failure. + +### One runtime serves all three + +The per-node module runtime that serves a module's tools also wires its `consumes` (subscribe, +dispatch to the handler) and lets its code `emit`. Tools are *invoked* (request/reply), resources +are *provisioned* (1:1, credentialed), events are *emitted and consumed* (1:many, broadcast) — +three relationships, one broker, one runtime, all declared on the manifest. + +## Consequences + +- The mesh gains a declared **event graph** alongside the provisioning graph — visible, validated, + reasoned over. +- **Reaction becomes the default coordination**: a module acts on another's event without either + knowing the other, and without a credentialed link. Coupling drops. +- An **audit trail** is a module, not a platform feature — and can be swapped, extended or run more + than once (a file logger and a queryable one) with no change to anything that emits. +- The runtime must dispatch a module's event handlers as well as its tools; that generalisation is + small (both arrive by importing the module's entrypoint) but it is real work. + +## References + +- [ADR 0001](0001-nodes-communicate-over-a-broker.md) — the broker events ride. +- [ADR 0045](0045-what-a-module-is.md) — the relationships this extends. +- [ADR 0044](0044-what-the-sdk-holds-and-refuses.md) — `emit`/`on` are stable sdk surface; the + broker binding and the runtime are not. +- [research 011](../01-RESEARCH/011-the-module-graph/00-overview.md) — the graph these edges join.