diff --git a/00-META/checks/records.py b/00-META/checks/records.py index dd1f5c1..3f12a2e 100644 --- a/00-META/checks/records.py +++ b/00-META/checks/records.py @@ -302,7 +302,9 @@ def check_progressive_insights(failures, records): # Both patterns stay on one line: a bold run does not span paragraphs, and `[^*]*` across # newlines will happily join an unrelated `**` far above to the marker below, reporting the # whole span between them. It did exactly that the first time this ran. - marker = re.compile(r"\*\*Progressive insights?[ \t]*[\u2014\u2013-][ \t]*(\d{4}-\d{2}-\d{2})\.?\*\*") + # Trailing words after the date are allowed — "— 2026-09-26, correcting the one above." — so + # an insight can say what it relates to. Only the date's presence and position are fixed. + marker = re.compile(r"\*\*Progressive insights?[ \t]*[\u2014\u2013-][ \t]*(\d{4}-\d{2}-\d{2})[^*\n]*\*\*") loose = re.compile(r"\*\*[^*\n]*[Pp]rogressive insights?[^*\n]*\*\*") iso = re.compile(r"^\d{4}-\d{2}-\d{2}$") @@ -316,6 +318,10 @@ def check_progressive_insights(failures, records): for m in loose.finditer(text): if any(s <= m.start() and m.end() <= e for s, e, _ in good): continue + # A bold run carrying a link is discussing an insight — usually another record's — + # rather than marking one. A marker never needs to cite anything. + if "](" in m.group(0): + continue failures.add("insights", rel(record["path"]), "a progressive insight is not in the dated marked form " "'**Progressive insight \u2014 YYYY-MM-DD.**': %s" % m.group(0)) diff --git a/02-DECISIONS/0106-the-bus-is-nats.md b/02-DECISIONS/0106-the-bus-is-nats.md index a12110d..4c8d3e2 100644 --- a/02-DECISIONS/0106-the-bus-is-nats.md +++ b/02-DECISIONS/0106-the-bus-is-nats.md @@ -88,6 +88,17 @@ compatibility broker beside it moves its bus in one rollout with every node repo > decision — the bus is NATS, the AMQP broker becomes the predecessor's compatibility broker and > retires with the last of them — is unchanged. +> **Progressive insight — 2026-09-26, correcting the one above.** *The broker is not a +> compatibility module at all, and the sentence does not become true.* The insight above said +> [ADR 0117](0117-the-bus-is-the-only-broker.md) would make "one purpose — the predecessor's +> clients" true by moving the mesh's own modules off it. +> [ADR 0119](0119-amqp-is-a-provision-not-the-bus.md) supersedes that: nothing moves off, because +> a module may legitimately need an AMQP broker as a backing service the way it needs a database. +> The broker becomes **an ordinary provider module** — no seat, not foundation, not raised at +> genesis, and with no retirement condition, because the day its last client disappears is not a +> day anything is waiting for. What this record decided — the mesh's bus is NATS — is untouched +> by both; what was wrong was the sentence describing what happens to the old server, twice. + ## References - [research 014](../01-RESEARCH/014-the-bus-on-nats/00-overview.md) diff --git a/02-DECISIONS/0117-the-bus-is-the-only-broker.md b/02-DECISIONS/0117-the-bus-is-the-only-broker.md index 65be793..a00f555 100644 --- a/02-DECISIONS/0117-the-bus-is-the-only-broker.md +++ b/02-DECISIONS/0117-the-bus-is-the-only-broker.md @@ -1,6 +1,7 @@ --- topic: the mesh -status: accepted +status: superseded +superseded-by: 02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md date: 2026-09-26 deciders: jochen reconstructed: false diff --git a/02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md b/02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md new file mode 100644 index 0000000..9ca50b9 --- /dev/null +++ b/02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md @@ -0,0 +1,103 @@ +--- +topic: the mesh +status: accepted +date: 2026-09-26 +deciders: jochen +reconstructed: false +supersedes: 02-DECISIONS/0117-the-bus-is-the-only-broker.md +--- + +# 119. AMQP is a provision, not the bus + +## Context + +[ADR 0117](0117-the-bus-is-the-only-broker.md) decided that the bus is the only broker, and went +one step further than it had grounds for: it also decided that the `amqp` **interface** — a module +requiring a message broker of its own — "is not carried forward" and "retires with the +compatibility broker rather than gaining a successor", with the two modules declaring it converted +to the bus in step 4. + +The operator's correction: **AMQP is deprecated as the mesh's transport, not abolished as a +service.** The broker module keeps running and keeps answering `amqp` requirements. It is no +longer a core part of the mesh — *"it's just a module like mssql now."* + +**What 0117 conflated** is two different reasons a module might ask for a broker, which look +identical in a manifest: + +1. **To talk to other modules.** Wrong under one bus, and the thing 0117 was right to refuse: a + private broker used as inter-module transport is a second bus, with every guarantee crossing a + seam and no scoping the mesh can see. +2. **Because it genuinely needs an AMQP broker**, the way something needs a database — a queue for + its own internals, or interop with software that speaks AMQP and nothing else. That is a + backing service, and the mesh has a word for backing services already. + +0117 saw the first and legislated against both. The second is ordinary, and forbidding it would +make the mesh unable to run a large class of perfectly normal software while claiming that as +architecture. + +## Considered Options + +1. **Keep 0117 as written** — retire the interface, convert the two modules. Rejected by the + operator, and wrongly reasoned besides: it treats "needs an AMQP broker" as always a mistake. +2. **Keep the broker as the predecessor's compatibility module**, as ADR 0106 framed it, with a + retirement condition. Rejected: it is not single-purpose and its clients are not only the + predecessor's, so the retirement condition describes a day that will not come. +3. **The broker is an ordinary provider module of an ordinary provision.** Adopted. + +## Decision + +**The mesh's bus is NATS and only NATS.** Everything 0117 decided about *the bus* stands: one bus, +a module's messaging is subjects on it scoped by what it declares, no module is handed a bus of +its own, and the `mesh-broker` seat is the NATS server's. + +**`amqp` remains a provision a module may require**, answered by the broker module the way +`postgres-database` is answered by the store module or a database is answered by mssql. It is not +deprecated as an interface; the software behind it is simply no longer the mesh's nervous system. + +**The broker module stops being foundation.** It claims no seat — `mesh-broker` is the NATS +server's — it is not raised at genesis, nothing in the mesh requires it, and a mesh that never +installs it is a complete mesh. It is installed when something wants it, like any other provider. + +**The rule that survives, stated so it can be applied:** *inter-module communication goes over the +bus.* A module may hold a broker, a database or a cache as a backing service; it may not use one +as a channel to another module. The line is not which software is involved, it is whether a second +module is on the other end. + +**Neither `amqp-ping` nor `amqp-email-forwarder` needs converting.** 0117 put that work in step 4; +it is removed. They require a backing service and a provider answers. + +## Consequences + +- **The "compatibility broker" framing is wrong and goes.** There is no `lavinmq-compat`, no + single purpose and no retirement condition. Design 25 §5 is corrected. +- **[ADR 0106](0106-the-bus-is-nats.md)'s progressive insight was itself wrong** and is corrected + by a second one there. It said 0117 would make 0106's "one purpose — the predecessor's clients" + sentence true by moving the mesh's modules off. Nothing moves off; the sentence is simply not + what the broker is. +- **The seat change stands**, for a better reason than 0117 gave: not because a broker cannot be + provisioned, but because *this* broker is not the mesh's bus. The broker module drops its + `mesh-broker` claim and the `nats` module takes it. +- **Step 4 loses two conversions**; step 1 and the WBS are otherwise unaffected. +- **What got harder:** the rule is now a judgement rather than a prohibition. "Is this a backing + service or a channel to another module?" has to be asked in review, where 0117 could have + answered it with a parser. That is the honest cost of allowing the legitimate case. + +## How it is checked + +- **A module's own messaging needs no `requires`.** The check from 0117, unchanged: a module + declaring only `emits` and `consumes` reaches its subjects and is refused every other. +- **The broker holds no seat.** A manifest test: the broker module claims nothing, and a mesh + raised without it is complete — genesis names it nowhere. +- **`amqp` resolves like any provision.** A resolution test: a module requiring it is answered by + the provider, refused when none is assigned, and neither case touches the bus. +- **What cannot be checked mechanically**, and is said rather than implied: that a module holding + a broker is not using it to reach another module. Review, not a parser. + +## References + +- [ADR 0117](0117-the-bus-is-the-only-broker.md) — superseded; its ruling on the bus is kept + whole and only its ruling on the interface is reversed. +- [ADR 0106](0106-the-bus-is-nats.md) — the bus is NATS; its compatibility-broker framing is + corrected here. +- [ADR 0118](0118-a-module-declares-its-own-seats.md) — seats, including the one the NATS server + now holds alone. diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index fcbe797..abf0392 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -133,7 +133,8 @@ python3 00-META/checks/index.py fail if stale - **0105** — [The mesh adopts the predecessor's tunnel in place](0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md) - **0106** — [The bus is NATS](0106-the-bus-is-nats.md) - **0116** — [The bus is built in five steps, and the protocol moves with it](0116-the-bus-is-built-in-five-steps.md) -- **0117** — [The bus is the only broker](0117-the-bus-is-the-only-broker.md) +- **0117** — [The bus is the only broker](0117-the-bus-is-the-only-broker.md) *(superseded)* +- **0119** — [AMQP is a provision, not the bus](0119-amqp-is-a-provision-not-the-bus.md) ### Its tiers, from the bottom up diff --git a/03-DESIGN/01-to-be/25-the-bus-on-nats.md b/03-DESIGN/01-to-be/25-the-bus-on-nats.md index 7ecfe38..fc7e274 100644 --- a/03-DESIGN/01-to-be/25-the-bus-on-nats.md +++ b/03-DESIGN/01-to-be/25-the-bus-on-nats.md @@ -10,7 +10,7 @@ code: updated: 2026-09-26 decisions: - 02-DECISIONS/0106-the-bus-is-nats.md - - 02-DECISIONS/0117-the-bus-is-the-only-broker.md + - 02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md - 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md - 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md - 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md @@ -214,11 +214,25 @@ current. Nothing is declared as `reload-on` or `restart-on` for this resource at Its guard is the same rule as the AMQP broker's: the monitoring port is refused from anything but the private network. It is raised at genesis like the store, adopted as a module in the same -phase. The predecessor's AMQP broker remains a module of its own, `lavinmq-compat`, with a -retirement condition: no client connected for a period the operator sets. +phase. + +**The AMQP broker is an ordinary module, not a compatibility layer.** Revision, second review +([ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md)): earlier text here, and +ADR 0106 before it, called it `lavinmq-compat` — one purpose, the predecessor's clients, and a +retirement condition of no client connected for a period the operator sets. It is none of those. +A module may legitimately need an AMQP broker as a **backing service**, the way it needs a +database, and the provider that answers that is an ordinary module like any other: no seat, not +foundation, never raised at genesis, installed when something wants it and absent from a mesh +that does not. There is no retirement condition, because the day its last client disappears is +not a day anything is waiting for. + +What is deprecated is AMQP as **the mesh's transport**, which is this whole document. The rule +that remains is about direction rather than software: *inter-module communication goes over the +bus.* A module may hold a broker, a database or a cache for itself; it may not use one as a +channel to another module. **And with one purpose, which it did not have when this was written.** Revision, second review -([ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md)): two modules of the new mesh +([ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md))): two modules of the new mesh required a broker *of their own* from it — a private vhost per consumer, the analog of database-per-login, which is a different thing from the mesh's bus and was never examined here. **There is one bus, and no module is handed a broker as a resource.** A module's messaging is @@ -436,7 +450,7 @@ find what changed and why. **Still open:** - ~~Whether EVENTS should be one stream or one per emitting module.~~ **Closed** - ([ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md)): one stream, and not as a + ([ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md))): one stream, and not as a preference — the streams the bus is made of are composed as configuration before any module runs, because a provisioner is itself a module that needs a bus account to start. Bootstrapping decides it. diff --git a/03-DESIGN/01-to-be/28-building-the-bus.md b/03-DESIGN/01-to-be/28-building-the-bus.md index 1bea5a2..5d68c57 100644 --- a/03-DESIGN/01-to-be/28-building-the-bus.md +++ b/03-DESIGN/01-to-be/28-building-the-bus.md @@ -9,7 +9,7 @@ updated: 2026-09-26 decisions: - 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md - 02-DECISIONS/0106-the-bus-is-nats.md - - 02-DECISIONS/0117-the-bus-is-the-only-broker.md + - 02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md - 02-DECISIONS/0118-a-module-declares-its-own-seats.md - 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md - 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md diff --git a/03-DESIGN/01-to-be/29-what-a-module-declares.md b/03-DESIGN/01-to-be/29-what-a-module-declares.md index 7349d78..51498ce 100644 --- a/03-DESIGN/01-to-be/29-what-a-module-declares.md +++ b/03-DESIGN/01-to-be/29-what-a-module-declares.md @@ -5,7 +5,7 @@ code: [] updated: 2026-09-26 decisions: - 02-DECISIONS/0118-a-module-declares-its-own-seats.md - - 02-DECISIONS/0117-the-bus-is-the-only-broker.md + - 02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md - 02-DECISIONS/0106-the-bus-is-nats.md - 02-DECISIONS/0041-events-are-a-relationship.md - 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md @@ -317,7 +317,7 @@ it as an ordinary module once the registry exists. So there are exactly two things the normal path cannot make, both at genesis, both ending the moment the mesh can mint for itself: **the bus's own accounts** (§the bootstrap argument in -[ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md) — a provisioner is a module and +[ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md)) — a provisioner is a module and needs an account before it can run) and **the vault's own credential**. Any third exception is a design failure, and naming these two is what makes a third one visible. diff --git a/03-DESIGN/01-to-be/README.md b/03-DESIGN/01-to-be/README.md index 41f9bde..abd5b86 100644 --- a/03-DESIGN/01-to-be/README.md +++ b/03-DESIGN/01-to-be/README.md @@ -39,7 +39,7 @@ document is written and this one's status becomes `implemented`. | [`27-a-module-requires-the-mesh-resolves.md`](27-a-module-requires-the-mesh-resolves.md) | **Proposed.** One concept for everything a module needs: a requirement with a contract, answered by one of four kinds of provider, resolved at assignment or refused. Retires settings, placeholders, facts and paths in definitions | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md), [ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)) | | [`28-building-the-bus.md`](28-building-the-bus.md) | **Proposed.** The five steps of the bus work in the order their dependencies allow, each ending at a bed — with the surface measured, so no step's size is a guess | [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md), [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md), [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md) | -| [`29-what-a-module-declares.md`](29-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md), [ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) | +| [`29-what-a-module-declares.md`](29-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md), [ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) | ## Not yet written