Both lines of work numbered from the same point, so four decision records and one design document existed twice with different content. The trunk keeps its numbers and this branch yields — the only rule that scales, because the trunk's are already cited by what merged before them. 0117 the bus is the only broker -> 0125 0118 a module declares its own seats -> 0126 0119 amqp is a provision, not the bus -> 0127 0120 the mesh bus is required -> 0128 0123 a seat carries its role's protocol -> 0129 0124 the predecessor is ending -> 0130 design 29, what a module declares -> design 32 Applied to the code repositories too, because a stale reference is worse when numbers collide than when they dangle: the reader lands on a real record that decided something else. Two reconciliations the merge forced, both real: **0110 was marked wholly superseded and was not.** Its successor says in as many words that everything 0110 decided about what a seat *is* stands untouched — and two records that landed on the trunk rest on exactly that part. So it is accepted again, extended rather than replaced, with a note saying which of its claims moved and where. **A seat's protocol becomes columns, not fields.** The trunk moved the seat set out of compiled code into a table the controller owns. This branch had added what a role accepts, emits and serves to the Go slice. The decision is unaffected and the mechanism is better for it: giving a role a protocol is now a write rather than a rebuild, which is the trunk's own argument applied to what this branch added. One check still fails and it fails on main too: a record resting on ADR 0112 while that is still 'proposed'. Left alone — it is not this merge's to answer.
102 lines
5.3 KiB
Markdown
102 lines
5.3 KiB
Markdown
---
|
|
topic: what runs on it
|
|
status: accepted
|
|
date: 2026-09-03
|
|
deciders: jochen
|
|
reconstructed: false
|
|
extends: 0040-what-a-module-is.md
|
|
---
|
|
|
|
# 41. Events are a relationship, the lighter sibling of provisioning
|
|
|
|
## Context
|
|
|
|
[ADR 0040](0040-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 0002](0002-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.
|
|
|
|
## Progressive insight
|
|
|
|
> **Progressive insight — 2026-09-26.** *"No provisioner and no per-consumer setup" was a fact
|
|
> about the transport, and the transport changed.* This record's table says an event's machinery is
|
|
> "nothing but the broker's topic routing", and the text that an event needs "no per-consumer setup
|
|
> — only a subscription". That was true of a topic exchange, where a binding cost nothing and the
|
|
> broker fanned out. On NATS
|
|
> ([ADR 0106](0106-the-bus-is-nats.md)) a subscription is a **durable consumer**: a real object
|
|
> with a name, an ack policy, a delivery limit and its own ack subject, created when a module is
|
|
> assigned and removed when it is not. Per-consumer setup exists, and the controller does it.
|
|
>
|
|
> The decision is untouched — events are declared on both sides, 1:many, credential-free, and
|
|
> still provisioning's lighter sibling; the lightness is now relative rather than absolute.
|
|
> [ADR 0126](0126-a-module-declares-its-own-seats.md) adds the relationship this record's two
|
|
> columns had no room for: work addressed to a role, where exactly one holder must act.
|
|
|
|
## References
|
|
|
|
- [ADR 0002](0002-nodes-communicate-over-a-broker.md) — the broker events ride.
|
|
- [ADR 0040](0040-what-a-module-is.md) — the relationships this extends.
|
|
- [ADR 0039](0039-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.
|