Merge pull request 'The bus in five steps: a decomposition, a breakdown, and progressive insight' (#134) from feat/the-bus-in-five-steps into main
This commit was merged in pull request #134.
This commit is contained in:
@@ -284,6 +284,59 @@ def check_numbering(failures, records):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_progressive_insights(failures, records):
|
||||||
|
"""A correction made inside a record is marked and dated, or it is a silent rewrite.
|
||||||
|
|
||||||
|
A record may be corrected in place when a *fact* in it went stale and the decision still
|
||||||
|
stands (`02-DECISIONS/README.md`, "Progressive insight"). The whole safety of that allowance
|
||||||
|
is that the correction is legible in the record rather than only in a diff nobody reads, so
|
||||||
|
the form is what is checked here: every mention of an insight is the marker, the marker
|
||||||
|
carries an ISO date, and that date is not earlier than the decision's own — an insight
|
||||||
|
predating the decision it corrects is a copied marker, not a correction.
|
||||||
|
|
||||||
|
What this cannot check is an edit made with no marker at all. Nothing mechanical can; that
|
||||||
|
one is the reviewer's, reading the diff. The check keeps the *marked* path honest so that an
|
||||||
|
unmarked change stands out as the anomaly it is.
|
||||||
|
"""
|
||||||
|
phrase = re.compile(r"progressive insight", re.I)
|
||||||
|
marker = re.compile(r"\*\*Progressive insights?\s*[\u2014\u2013-]\s*(\d{4}-\d{2}-\d{2})\.?\*\*")
|
||||||
|
loose = re.compile(r"\*\*[^*]*[Pp]rogressive insights?[^*]*\*\*")
|
||||||
|
iso = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||||
|
|
||||||
|
for number, record in sorted(records.items()):
|
||||||
|
text = record["text"]
|
||||||
|
if not phrase.search(text):
|
||||||
|
continue
|
||||||
|
decided = str(record["front"].get("date", ""))
|
||||||
|
good = [(m.start(), m.end(), m.group(1)) for m in marker.finditer(text)]
|
||||||
|
|
||||||
|
for m in loose.finditer(text):
|
||||||
|
if any(s <= m.start() and m.end() <= e for s, e, _ in good):
|
||||||
|
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))
|
||||||
|
|
||||||
|
for _, _, stamp in good:
|
||||||
|
if decided and iso.match(decided) and stamp < decided:
|
||||||
|
failures.add("insights", rel(record["path"]),
|
||||||
|
"a progressive insight dated %s predates the decision (%s)"
|
||||||
|
% (stamp, decided))
|
||||||
|
|
||||||
|
covered = [(s, e) for s, e, _ in good]
|
||||||
|
for m in phrase.finditer(text):
|
||||||
|
if any(s <= m.start() and m.end() <= e for s, e in covered):
|
||||||
|
continue
|
||||||
|
line = text.rfind("\n", 0, m.start()) + 1
|
||||||
|
if text[line:m.start()].lstrip().startswith("#"):
|
||||||
|
continue
|
||||||
|
if loose.search(text, line, text.find("\n", m.end()) + 1 or len(text)):
|
||||||
|
continue
|
||||||
|
failures.add("insights", rel(record["path"]),
|
||||||
|
"'progressive insight' appears unmarked; a correction is marked and "
|
||||||
|
"dated, or it is a silent rewrite")
|
||||||
|
|
||||||
|
|
||||||
def check_status_against_code(failures):
|
def check_status_against_code(failures):
|
||||||
"""A design document naming specific code may not still call itself `designed`.
|
"""A design document naming specific code may not still call itself `designed`.
|
||||||
|
|
||||||
@@ -325,6 +378,7 @@ def main():
|
|||||||
check_numbering(failures, records)
|
check_numbering(failures, records)
|
||||||
check_topics(failures, records)
|
check_topics(failures, records)
|
||||||
check_status_against_code(failures)
|
check_status_against_code(failures)
|
||||||
|
check_progressive_insights(failures, records)
|
||||||
print(f"records: {len(records)} decision records checked")
|
print(f"records: {len(records)} decision records checked")
|
||||||
return failures.report()
|
return failures.report()
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,10 @@
|
|||||||
A design changes only through a decision.
|
A design changes only through a decision.
|
||||||
|
|
||||||
1. Write the decision record. If it reverses an earlier one, the earlier record's `status:`
|
1. Write the decision record. If it reverses an earlier one, the earlier record's `status:`
|
||||||
becomes `superseded-by: 02-DECISIONS/NNNN-....md` — **its text is never edited**.
|
becomes `superseded-by: 02-DECISIONS/NNNN-....md` — **its reasoning is never rewritten**. If the
|
||||||
|
earlier record is sound and only a *fact* in it went stale, that is a **progressive insight**,
|
||||||
|
corrected in place and marked in the record rather than superseded
|
||||||
|
([`02-DECISIONS/README.md`](../../02-DECISIONS/README.md)).
|
||||||
2. Edit the to-be design document and set `updated:` to today.
|
2. Edit the to-be design document and set `updated:` to today.
|
||||||
3. If the amendment came from an issue, set that issue's `amended-design:` to the document
|
3. If the amendment came from an issue, set that issue's `amended-design:` to the document
|
||||||
path.
|
path.
|
||||||
@@ -54,4 +57,5 @@ Implementation state is a third axis, independent of both design and decision.
|
|||||||
|
|
||||||
- Do not move a to-be document into `00-as-is/`. Write the as-is document; both stand.
|
- Do not move a to-be document into `00-as-is/`. Write the as-is document; both stand.
|
||||||
- Do not edit an as-is document to describe an intention. That is what the to-be layer is for.
|
- Do not edit an as-is document to describe an intention. That is what the to-be layer is for.
|
||||||
- Do not change a decision record's meaning. Supersede it.
|
- Do not change a decision record's meaning. Supersede it. Correcting a fact it got wrong, while
|
||||||
|
the decision stands, is a progressive insight — marked and dated in the record, never silent.
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
---
|
||||||
|
topic: the mesh
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-26
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 02-DECISIONS/0106-the-bus-is-nats.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 116. The bus is built in five steps, and the protocol moves with it
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0106](0106-the-bus-is-nats.md) decided the bus is NATS and described the change as one thing:
|
||||||
|
built beside the migration, cut over in one rollout after its core. The architecture was written as
|
||||||
|
[design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) and revised once after review. What neither
|
||||||
|
says is how the work is divided, and three gaps follow from that.
|
||||||
|
|
||||||
|
**The whole of the build is one point.** Design 25 §9 numbers four items. The first — "the `nats`
|
||||||
|
module, the controller's and host's link on NATS, the runtime's client — built and proven in the
|
||||||
|
lab" — is every line of code the change requires; the other three are the rollout. A step of that
|
||||||
|
size ends at nothing provable until it ends at everything, which is the failure
|
||||||
|
[design 22](../03-DESIGN/01-to-be/22-the-work-ahead.md) opens by naming: *a phase that ends at a
|
||||||
|
claim is a phase that went missing without anything complaining.*
|
||||||
|
|
||||||
|
**There is no adoption path.** Design 25 §5 says the broker "is raised at genesis like the store,
|
||||||
|
adopted as a module in the same phase" — which describes a mesh being raised from nothing. The mesh
|
||||||
|
this is for is already running, and a running node gets a foundation module by adoption in place
|
||||||
|
([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)), not by genesis. §9 goes
|
||||||
|
straight from "built beside" to "one rollout" and never crosses that gap.
|
||||||
|
|
||||||
|
**The wire changes and the protocol specification does not know it.** Design 25 §8 says a module
|
||||||
|
sees nothing new. That is true of the SDK's contract — `request`, `handle`, `publish`, `subscribe`
|
||||||
|
— and false of the wire underneath it.
|
||||||
|
[ADR 0074](0074-the-wire-is-specified-not-the-types.md) settled that what an SDK implements is a
|
||||||
|
*specified wire*, checked by fixtures that must match byte for byte, precisely because two
|
||||||
|
implementations that disagree about an envelope do not fail to compile. That specification is
|
||||||
|
[design 19](../03-DESIGN/01-to-be/19-the-module-protocol.md), and it is written entirely in AMQP:
|
||||||
|
exchanges, a durable per-consumer queue named `<node>.<module>.events`, a shared `serve.<key>`
|
||||||
|
queue — sixteen occurrences of an AMQP term across the document. Design 25 does not cite design 19
|
||||||
|
anywhere, and design 19 does not cite ADR 0106. So the record that says *what two implementations
|
||||||
|
may not disagree about* still describes the bus being replaced.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
1. **Keep ADR 0106's shape — build it all, cut over once.** Rejected: not for its rollout, which
|
||||||
|
is right, but because it leaves the build a single step of unknown length with no intermediate
|
||||||
|
anyone can run. The three gaps above were found by dividing it; they were invisible while it
|
||||||
|
was one item.
|
||||||
|
2. **Cut over incrementally by traffic kind** — events to NATS first, then tools, then control,
|
||||||
|
the mesh on two buses meanwhile. Rejected: ADR 0106 already rejected two buses, and this is
|
||||||
|
that with extra steps. The store-window guarantee
|
||||||
|
([ADR 0083](0083-one-push-leaves-the-mesh-consistent.md)) is exactly the one that cannot cross
|
||||||
|
a seam, and control is exactly the traffic that carries it.
|
||||||
|
3. **Five steps, each ending at something provable, the cutover still one rollout.** The build is
|
||||||
|
divided; the bus still moves once. Adopted.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The bus is built in five steps. Each ends at something a lab bed proves, and no step's proof
|
||||||
|
waits for the one after it. The cutover remains a single rollout** — dividing the build does not
|
||||||
|
divide the bus.
|
||||||
|
|
||||||
|
**Step 1 — genesis raises the broker.** The `nats` module, and genesis placing it in the
|
||||||
|
foundation. This is built and proven even though the mesh it is for will never travel this path,
|
||||||
|
because
|
||||||
|
genesis is where the foundation is *defined*: the only place the mesh comes from nothing, and the
|
||||||
|
definition every other path is measured against. A genesis path that exists only on paper is one
|
||||||
|
nobody discovers is wrong until there is a second mesh.
|
||||||
|
|
||||||
|
**Step 2 — adoption puts the broker in the seat.** A mesh already running receives the broker by
|
||||||
|
adoption in place, and the seat it claims is **`mesh-broker`** — unchanged.
|
||||||
|
[ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) named the foundation seats
|
||||||
|
after the server's *role* rather than the product for exactly this case, and ADR 0106 restated it:
|
||||||
|
the broker module changes, the seat does not. A seat named after the product would have to be
|
||||||
|
renamed by every change the seat exists to survive.
|
||||||
|
|
||||||
|
**Step 3 — the protocol gets a NATS binding.** ADR 0074 stands unamended: the mesh defines a module
|
||||||
|
protocol, an SDK is an implementation of it in one language and nothing more, the protocol is split
|
||||||
|
per capability, and conformance is executable fixtures per capability rather than prose. What
|
||||||
|
changes is what the specification specifies. Design 19's wire section is rewritten from exchanges
|
||||||
|
and queues to subjects and streams; the conformance suite is built — first against the bus the
|
||||||
|
mesh has, then restated on NATS; every SDK claims the capabilities it passes, and a language may
|
||||||
|
arrive with connection and events alone.
|
||||||
|
|
||||||
|
**The SDK gains no conveniences in the process.** [ADR 0039](0039-what-the-sdk-holds-and-refuses.md)
|
||||||
|
refuses frequent-and-cascading code in the shared library, and ADR 0074 restates it — an SDK is
|
||||||
|
"not a convenience layer, not a place for helpers to accumulate." A new transport is the moment
|
||||||
|
that pressure is highest and the reason to hold hardest: the predecessor's shared library is the
|
||||||
|
cautionary tale ADR 0039 opens with, and it did not become that in one decision. Code shared among
|
||||||
|
a module's own features stays in that module.
|
||||||
|
|
||||||
|
**Step 4 — the core speaks NATS.** The controller's link, the host's link and the tool runtime's
|
||||||
|
client, and with them the flows that are today carried by something other than the bus: a build
|
||||||
|
source's change reaching the builder, an installation, a module's own reports. Each is a
|
||||||
|
conversion with a named before and after, not a rewrite. Observation — heartbeats, conditions,
|
||||||
|
key-value state — is [research 017](../01-RESEARCH/017-a-mesh-that-heals-itself/00-overview.md)'s
|
||||||
|
and stays there; that effort already reserves it for after the move, and this step does not
|
||||||
|
pre-empt its design.
|
||||||
|
|
||||||
|
**Step 5 — deployment.** The rollout ADR 0106 decided, unchanged: the controller, every host and
|
||||||
|
every runtime move together, each node confirmed to have heard before AMQP stops. What this step
|
||||||
|
adds is that steps 1 to 4 *ship ahead of it* without moving any node's bus — the module exists in
|
||||||
|
the catalogue, the protocol is specified, the code is written and beds pass, and the running mesh
|
||||||
|
is still on AMQP throughout. The bus moves on one day, at the end, once.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Design 25 §9 is replaced by these five steps** and §10's beds are attributed to the step each
|
||||||
|
proves, so no proof waits for the last step.
|
||||||
|
- **Design 19 is stale in its wire section from today** and says so in its own frontmatter and
|
||||||
|
opening until step 3 rewrites it. A specification that describes the bus being replaced is worse
|
||||||
|
than an absent one, because it reads as current.
|
||||||
|
- **`nats-broker` is not a seat.** The module is `nats`; the seat is `mesh-broker`.
|
||||||
|
- **Steps 1 to 4 leave every node on AMQP.** A step can be abandoned, or reordered after step 2,
|
||||||
|
without a rollback — the cost of being wrong is bounded until step 5.
|
||||||
|
- **What got harder:** five steps mean five proofs rather than one, and step 3 rewrites a design
|
||||||
|
other designs cite, so their references are checked when it lands. Dividing the work does not
|
||||||
|
reduce it.
|
||||||
|
|
||||||
|
## How it is checked
|
||||||
|
|
||||||
|
- **Per step, a bed, and the bed named in design 25 §10 against the step it belongs to.** Step 1:
|
||||||
|
a mesh raised from nothing has the server standing, its streams asserted and every account
|
||||||
|
composed from the manifests — no mesh traffic on it yet. Step 2: the broker adopted into a mesh
|
||||||
|
already running, a second holder of `mesh-broker` refused at resolution, and nothing routed to it.
|
||||||
|
Step 3: the conformance suite passing per capability, on NATS, for every implementation that
|
||||||
|
claims it — and a module built before the binding serving its tools unchanged. Step 4: each
|
||||||
|
converted flow proved against the behaviour it replaced, **and the full genesis bed** — a mesh
|
||||||
|
enrolling, holding a push, and rolling out an upgrade on NATS. Step 5: the cutover bed, then the
|
||||||
|
rollout with every node reporting.
|
||||||
|
- **Step 3 is not done when the code runs.** It is done when the fixtures match byte for byte
|
||||||
|
across implementations, which is ADR 0074's own test and the only one that catches two SDKs
|
||||||
|
quietly ignoring each other.
|
||||||
|
- **A step that cannot name what its bed proves is not a step**, and is divided further before it
|
||||||
|
is started.
|
||||||
|
|
||||||
|
## Progressive insights
|
||||||
|
|
||||||
|
Corrections of fact made in this record after it was accepted, under the rule in
|
||||||
|
[`README.md`](README.md). The decision — five steps, each proved, one rollout — is untouched by
|
||||||
|
both; each corrects something this record asserted about the *state of the code*, which nobody had
|
||||||
|
measured when it was written.
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-09-26.** *There is no conformance suite to recapture.* This record
|
||||||
|
> said step 3's "fixtures are recaptured on NATS", and design 19's wire was described as specified
|
||||||
|
> and conformed. Measuring the four repositories found no conformance fixtures in any of them:
|
||||||
|
> [design 22](../03-DESIGN/01-to-be/22-the-work-ahead.md)'s Phase 1.2, which would have built the
|
||||||
|
> suite, is still open. Step 3 therefore **builds** it, and builds it first against the bus the
|
||||||
|
> mesh has — a suite born on the new bus certifies whatever the new bus happens to do. The step's
|
||||||
|
> place in the order, and ADR 0074's model it implements, are unchanged.
|
||||||
|
|
||||||
|
> **Progressive insight — 2026-09-26.** *The full genesis bed belongs to step 4, not step 1.* This
|
||||||
|
> record attributed "a mesh raised on NATS from genesis" to step 1, and described that step as "the
|
||||||
|
> `nats` module and a mesh raised on it from nothing". A bed that enrols a node, holds a push and
|
||||||
|
> rolls out an upgrade needs the controller and the host to speak NATS — which is step 3's
|
||||||
|
> implementations and step 4's flows. A step whose proof cannot run is exactly the failure this
|
||||||
|
> record was written to prevent, so step 1 now ends at the server standing from genesis, correctly
|
||||||
|
> configured and carrying nothing, and the full bed is named under step 4. The five steps, their
|
||||||
|
> names, their order and the single rollout are unchanged; only where two beds run has moved.
|
||||||
|
>
|
||||||
|
> Recorded here rather than superseded because the decision this record makes — that the work is
|
||||||
|
> divided and each division is proved — is what *produced* the correction: the dependency was
|
||||||
|
> invisible while the build was one item.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0106](0106-the-bus-is-nats.md) — the decision this divides.
|
||||||
|
- [ADR 0074](0074-the-wire-is-specified-not-the-types.md) — the protocol and its conformance suite;
|
||||||
|
step 3 is its NATS binding, not a replacement.
|
||||||
|
- [ADR 0039](0039-what-the-sdk-holds-and-refuses.md) — why step 3 adds no helpers.
|
||||||
|
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) — why the seat is
|
||||||
|
`mesh-broker`.
|
||||||
|
- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md) — the adoption step 2 uses.
|
||||||
|
- [design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md), [design 19](../03-DESIGN/01-to-be/19-the-module-protocol.md) — the two documents this changes.
|
||||||
+40
-2
@@ -8,8 +8,45 @@ the decision is recorded here, and only then is the design written. Following th
|
|||||||
numbers walks the process in the order it happens.
|
numbers walks the process in the order it happens.
|
||||||
|
|
||||||
One file per decision, numbered, never deleted. A superseded record has its `status:` changed
|
One file per decision, numbered, never deleted. A superseded record has its `status:` changed
|
||||||
and gains a pointer to what replaced it — **its text is never edited**. The reasoning that was
|
and gains a pointer to what replaced it — **its reasoning is never rewritten**. The reasoning that
|
||||||
rejected is the expensive half to rediscover.
|
was rejected is the expensive half to rediscover.
|
||||||
|
|
||||||
|
## Progressive insight
|
||||||
|
|
||||||
|
A record is a decision, not a snapshot of everything that was true the day it was written, and
|
||||||
|
those two fail differently. **A fact a record asserted can turn out to be wrong while the decision
|
||||||
|
it supports stays right** — a count taken before anyone measured, a file named that does not
|
||||||
|
exist, a proof attributed to a step that cannot run it. Superseding a record for that buries a
|
||||||
|
correct decision under a second one, and teaches every reader to first work out which of two
|
||||||
|
records is live. Done a few times, the reading order stops being one.
|
||||||
|
|
||||||
|
So: **a correction of fact that leaves the decision standing is made in the record, in place,
|
||||||
|
marked and dated.**
|
||||||
|
|
||||||
|
> **Progressive insight — YYYY-MM-DD.** What was found, what the record said before, and what it
|
||||||
|
> says instead.
|
||||||
|
|
||||||
|
Three conditions, all of which hold:
|
||||||
|
|
||||||
|
- **It corrects a fact, not a judgement.** That a suite does not exist is a fact. That building it
|
||||||
|
is the wrong order is a judgement, and judgements supersede.
|
||||||
|
- **It adds; it never quietly replaces.** Where body text changes, the note says what stood there
|
||||||
|
before, so a reader who followed a citation to the old wording can find out what happened to it.
|
||||||
|
A correction nobody can see is indistinguishable from a record that was always right, which is
|
||||||
|
the failure the immutability rule exists to prevent.
|
||||||
|
- **The decision, the options weighed and the consequences stand untouched.** If the correction
|
||||||
|
changes what was decided, which alternatives were rejected, or a consequence another record
|
||||||
|
relies on, it is not an insight — write the superseding record.
|
||||||
|
|
||||||
|
**What still supersedes**, without exception: reversing a decision, changing its scope, rejecting
|
||||||
|
an option it accepted, or making a consequence false that a later record cites. The test is not
|
||||||
|
how large the edit looks in a diff; it is whether a reader who acted on the old text would now be
|
||||||
|
wrong about *what was decided* rather than about *a detail the decision did not rest on*.
|
||||||
|
|
||||||
|
**How this is checked.** `00-META/checks/records.py` requires every insight to be marked in the
|
||||||
|
form above and dated no earlier than the record's own `date:` — an unmarked edit is a rule
|
||||||
|
violation the reviewer looks for in the diff, and a marked one is legible in the record itself.
|
||||||
|
The git history is the backstop, not the record of intent; the note is the record of intent.
|
||||||
|
|
||||||
The records run in the order the decisions were taken, oldest first.
|
The records run in the order the decisions were taken, oldest first.
|
||||||
|
|
||||||
@@ -95,6 +132,7 @@ python3 00-META/checks/index.py fail if stale
|
|||||||
- **0104** — [A provision may be answered by an adapter to the predecessor](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md)
|
- **0104** — [A provision may be answered by an adapter to the predecessor](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md)
|
||||||
- **0105** — [The mesh adopts the predecessor's tunnel in place](0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md)
|
- **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)
|
- **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)
|
||||||
|
|
||||||
### Its tiers, from the bottom up
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ code:
|
|||||||
- mesh-sdk src
|
- mesh-sdk src
|
||||||
- mesh-tools src/broker-amqp.ts
|
- mesh-tools src/broker-amqp.ts
|
||||||
- mesh-controller internal/link
|
- mesh-controller internal/link
|
||||||
updated: 2026-09-21
|
updated: 2026-09-26
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
||||||
|
- 02-DECISIONS/0106-the-bus-is-nats.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/0074-the-wire-is-specified-not-the-types.md
|
||||||
- 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
|
- 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
|
||||||
- 02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
|
- 02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
|
||||||
@@ -23,6 +25,27 @@ language and nothing more ([ADR 0074](../../02-DECISIONS/0074-the-wire-is-specif
|
|||||||
This is a specification, so it says what is required rather than how anything is arranged. Where it
|
This is a specification, so it says what is required rather than how anything is arranged. Where it
|
||||||
describes current behaviour that is *not yet* specified-and-conformed, it says so.
|
describes current behaviour that is *not yet* specified-and-conformed, it says so.
|
||||||
|
|
||||||
|
> **The wire below is the bus being replaced.** *2026-09-26.*
|
||||||
|
> [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md) moved the mesh's bus to NATS. Everything
|
||||||
|
> in this document that names an exchange, a queue or a routing key — the event exchanges, the
|
||||||
|
> durable `<node>.<module>.events` queue, the shared `serve.<key>` queue — describes the transport
|
||||||
|
> being retired, and the conformance fixtures were captured against it.
|
||||||
|
>
|
||||||
|
> What does **not** change is this document's model, which is the part ADR 0074 decided: a floor
|
||||||
|
> plus independent capabilities, an SDK that implements what it claims and is legitimate when it
|
||||||
|
> claims less, identity taken from the sealed credential rather than the environment, at-least-once
|
||||||
|
> with dedup on `x-event-id`, and conformance as executable fixtures per capability rather than
|
||||||
|
> prose. The envelope keeps its shape ([ADR 0042](../../02-DECISIONS/0042-the-shape-of-an-event-on-the-wire.md));
|
||||||
|
> it becomes the message body.
|
||||||
|
>
|
||||||
|
> Rewriting the wire sections onto the subjects and streams of
|
||||||
|
> [design 25](25-the-bus-on-nats.md) §2–§3, and recapturing the fixtures there, is **step 3 of
|
||||||
|
> [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md)**. Until that lands, read
|
||||||
|
> the sections below for what two implementations may not disagree *about*, and design 25 for what
|
||||||
|
> they will disagree about it *on*. A specification that silently described a retired transport
|
||||||
|
> would be worse than an absent one, because it reads as current — hence this note rather than a
|
||||||
|
> quiet edit.
|
||||||
|
|
||||||
## The shape of it
|
## The shape of it
|
||||||
|
|
||||||
A **floor** every implementation needs, and three **capabilities** that are independent of each
|
A **floor** every implementation needs, and three **capabilities** that are independent of each
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ code:
|
|||||||
- mesh-host internal/link (to be replaced)
|
- mesh-host internal/link (to be replaced)
|
||||||
- mesh-tools src/broker-amqp.ts (to be replaced)
|
- mesh-tools src/broker-amqp.ts (to be replaced)
|
||||||
- mesh-catalog modules/nats (to be written)
|
- mesh-catalog modules/nats (to be written)
|
||||||
updated: 2026-09-24
|
- mesh-sdk src (the protocol's NATS binding, step 3)
|
||||||
|
updated: 2026-09-26
|
||||||
decisions:
|
decisions:
|
||||||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
- 02-DECISIONS/0106-the-bus-is-nats.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
|
||||||
|
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
||||||
- 02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
|
- 02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
|
||||||
- 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
|
- 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
|
||||||
- 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
|
- 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
|
||||||
@@ -221,64 +226,154 @@ bridged. It is three things:
|
|||||||
|
|
||||||
Nothing is built of this before §10's bed passes; the MCP surface is a thin adapter over (2).
|
Nothing is built of this before §10's bed passes; the MCP surface is a thin adapter over (2).
|
||||||
|
|
||||||
## 8. What a module sees
|
## 8. What a module sees, and what the wire does
|
||||||
|
|
||||||
Nothing new. `publish` on an envelope becomes a publish on `mesh.events.<module>.<event>`;
|
**The contract a module is written against does not change.** `publish` on an envelope becomes a
|
||||||
`subscribe` with a pattern becomes a durable JetStream consumer on the matching subject filter;
|
publish on `mesh.events.<module>.<event>`; `subscribe` with a pattern becomes a durable JetStream
|
||||||
`request`/`handle` become a NATS request and a queue-group subscription on
|
consumer on the matching subject filter; `request`/`handle` become a NATS request and a queue-group
|
||||||
`mesh.tools.<module>.<tool>`. The envelope's shape ([ADR 0042](../../02-DECISIONS/0042-the-shape-of-an-event-on-the-wire.md))
|
subscription on `mesh.tools.<module>.<tool>`. The envelope's shape
|
||||||
is unchanged; it is the message body. A module built today runs on the new runtime without a
|
([ADR 0042](../../02-DECISIONS/0042-the-shape-of-an-event-on-the-wire.md)) is unchanged; it is the
|
||||||
rebuild — that is the test of ADR 0039, and it is in §10.
|
message body. A module built today runs on the new runtime without a rebuild — that is the test of
|
||||||
|
ADR 0039, and it is in §10.
|
||||||
|
|
||||||
## 9. Moving from the bus the mesh has
|
**The wire underneath it changes completely, and that is a specification, not an implementation
|
||||||
|
detail.** Revision, second review ([ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md)):
|
||||||
|
an earlier draft of this section said "nothing new" and stopped there, which read as though the
|
||||||
|
change were contained inside the runtime. It is not.
|
||||||
|
[ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md) settled that an SDK is an
|
||||||
|
implementation of a *specified wire*, checked by fixtures that must match byte for byte — because
|
||||||
|
two implementations that disagree about an envelope do not fail to compile, they ignore each other
|
||||||
|
while both keep running. That specification is
|
||||||
|
[design 19](19-the-module-protocol.md), and it is written in exchanges, a durable per-consumer queue
|
||||||
|
named `<node>.<module>.events`, and a shared `serve.<key>` queue. Every one of those is gone here.
|
||||||
|
|
||||||
Per ADR 0106: built beside, cut over once, after the core.
|
So design 19 is rewritten from exchanges and queues to the subjects and streams of §2 and §3, its
|
||||||
|
fixtures are recaptured on NATS, and each SDK re-claims the capabilities it passes. That is step 3
|
||||||
|
of §9, and until it lands design 19 says in its own opening that its wire section describes the bus
|
||||||
|
being replaced. What is *not* rewritten: ADR 0074's model — protocol split per capability, an SDK
|
||||||
|
that implements the floor and events alone is legitimate, conformance executable per capability —
|
||||||
|
and ADR 0039's refusal. A new transport is when the pressure to grow the shared library is highest;
|
||||||
|
nothing is added to it here.
|
||||||
|
|
||||||
1. The `nats` module, the controller's and host's link on NATS, the runtime's client — built and
|
## 9. Moving from the bus the mesh has: five steps
|
||||||
proven in the lab (§10) while the migration continues on AMQP. Modules converted meanwhile
|
|
||||||
target the sdk contract and are untouched by this.
|
|
||||||
2. The cutover is one rollout, previewed: the controller assigns `nats` to the hub (raised beside
|
|
||||||
the AMQP broker on its own ports), composes every node's and module's account into it, then
|
|
||||||
rolls out the controller, every host and every runtime built for NATS. Each node's host connects
|
|
||||||
to the new bus as it comes up and reports; the controller confirms every node heard before it
|
|
||||||
stops listening on AMQP. The predecessor's clients never notice: their broker is the
|
|
||||||
compatibility module and stays.
|
|
||||||
3. The AMQP-side mesh accounts are removed from the compatibility broker; it keeps only the
|
|
||||||
predecessor's users. The bus's port settings follow ADR 0100 like any port.
|
|
||||||
4. The compatibility broker retires when its retirement condition holds.
|
|
||||||
|
|
||||||
What is not done: no dual-bus period for the mesh's own traffic, no bridge, no module rebuilt.
|
Per ADR 0106 the bus moves once. Per
|
||||||
|
[ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md) the *build* is five steps,
|
||||||
|
each ending at something §10 proves, so that no part of this waits on the whole of it. Dividing the
|
||||||
|
build does not divide the bus: steps 1 to 4 leave every node on AMQP, and step 5 is still one
|
||||||
|
rollout.
|
||||||
|
|
||||||
|
**Step 1 — genesis raises the broker.** The `nats` module of §5, and a mesh raised on it from
|
||||||
|
nothing. This is built and proven although the mesh it is for will never travel this path: genesis
|
||||||
|
is where the foundation is *defined*, the one place the mesh comes from nothing, and the definition
|
||||||
|
every other path is measured against. A genesis path that exists only on paper is one nobody finds
|
||||||
|
wrong until there is a second mesh. *Ends at: the genesis bed.*
|
||||||
|
|
||||||
|
**Step 2 — adoption puts the broker in its seat.** A mesh already running does not get a foundation
|
||||||
|
module by being raised again; it adopts one in place
|
||||||
|
([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)). The
|
||||||
|
server is raised beside the AMQP broker on its own ports, carrying no mesh traffic yet, and the
|
||||||
|
`nats` module is adopted onto it. The seat it claims is **`mesh-broker`**, unchanged — the
|
||||||
|
foundation seats are named after the server's role rather than the product
|
||||||
|
([ADR 0079](../../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md)) for
|
||||||
|
exactly this case, and a seat named after the product would need renaming by every change the seat
|
||||||
|
exists to survive. *Ends at: the adoption bed.*
|
||||||
|
|
||||||
|
**Step 3 — the protocol gets its NATS binding.** §8's other half: design 19 rewritten from
|
||||||
|
exchanges and queues to subjects and streams, its conformance fixtures recaptured on NATS, each SDK
|
||||||
|
re-claiming the capabilities it passes. ADR 0074's model is unamended and ADR 0039's refusal holds —
|
||||||
|
no helper layer arrives with the new transport. A language may still arrive in pieces: connection
|
||||||
|
and events first, tools and provisioning when something needs them. *Ends at: the conformance suite,
|
||||||
|
per capability, per implementation.*
|
||||||
|
|
||||||
|
**Step 4 — the core speaks NATS.** The controller's link, the host's link, the tool runtime's
|
||||||
|
client — and with them the flows carried today by something other than the bus: a build source's
|
||||||
|
change reaching the builder, an installation, a module's own reports. Each is a conversion with a
|
||||||
|
named before and after. Observation — heartbeats, conditions, key-value state — belongs to
|
||||||
|
[research 017](../../01-RESEARCH/017-a-mesh-that-heals-itself/00-overview.md), which already
|
||||||
|
reserves it for after the move; this step does not pre-empt its design. Modules converted meanwhile
|
||||||
|
target the sdk contract and are untouched by any of it. *Ends at: each converted flow proved against
|
||||||
|
the behaviour it replaced.*
|
||||||
|
|
||||||
|
**Step 5 — the rollout.** Unchanged from ADR 0106 and previewed: the controller composes every
|
||||||
|
node's and module's account into the server standing since step 2, then rolls out the controller,
|
||||||
|
every host and every runtime built for NATS. Each node's host connects to the new bus as it comes up
|
||||||
|
and reports; the controller confirms every node heard before it stops listening on AMQP. The
|
||||||
|
predecessor's clients never notice — their broker is the compatibility module and stays. Then the
|
||||||
|
AMQP-side mesh accounts are removed from it, leaving only the predecessor's users, and it retires
|
||||||
|
when its retirement condition holds. The bus's port settings follow ADR 0100 like any port.
|
||||||
|
*Ends at: the cutover bed, then the rollout itself.*
|
||||||
|
|
||||||
|
What is not done, at any step: no dual-bus period for the mesh's own traffic, no bridge, no module
|
||||||
|
rebuilt.
|
||||||
|
|
||||||
|
**The work of these five, broken down and measured, is [design 28](28-building-the-bus.md)** —
|
||||||
|
including the two places their dependencies put a bed later than the step that names it.
|
||||||
|
|
||||||
## 10. How it is checked
|
## 10. How it is checked
|
||||||
|
|
||||||
Two lab beds, both required green before any node's bus moves.
|
**A bed per step, and each is green before the step after it starts** — the division in §9 is only
|
||||||
|
real if the proofs divide with it. A step that cannot name what its bed proves is not a step, and is
|
||||||
|
divided further before it is started.
|
||||||
|
|
||||||
**The bus bed** — a mesh raised on NATS from genesis:
|
**Step 1 — the genesis-broker bed**, a mesh raised from nothing, the server standing on it. The
|
||||||
|
mesh does not yet *live* on this bus — nothing speaks it until step 3's implementations exist — so
|
||||||
|
what this bed proves is the server, its configuration and the permissions, each of which the server
|
||||||
|
itself enforces and a plain client can therefore check:
|
||||||
|
- the four streams exist, asserted idempotently on a second start, with the retention of §3;
|
||||||
|
- every account and permission in the composed file is derived from the manifests' `emits` and
|
||||||
|
`consumes` and nothing else, with each user's own ack subject and its own inbox prefix;
|
||||||
|
- a user cannot publish outside its `emits` nor subscribe outside its `consumes` — refused by the
|
||||||
|
server, not by convention;
|
||||||
|
- a user cannot ack another user's delivery, and cannot subscribe another's inbox prefix — refused
|
||||||
|
by the server, not by the client's own good behaviour;
|
||||||
|
- the monitoring port is refused from anything but the private network;
|
||||||
|
- the `nats` container is not recreated when only its composed configuration file changes, and a
|
||||||
|
change to that file is live (a new user can connect, a revoked one cannot) within one
|
||||||
|
watcher-poll interval, without a restart.
|
||||||
|
|
||||||
|
**Step 2 — the adoption bed**, a mesh already running that has never had this server:
|
||||||
|
- the server is raised beside the AMQP broker on its own ports and the `nats` module is adopted
|
||||||
|
onto it in place, holding the data and the configuration it was raised with;
|
||||||
|
- the seat it claims is `mesh-broker`, and a second assignment of it anywhere in the mesh is
|
||||||
|
refused at resolution — *one per mesh*, as ADR 0079 requires;
|
||||||
|
- every node stays on AMQP throughout and nothing routes to the adopted server. This is the
|
||||||
|
check that makes steps 3 and 4 safe to run against a live mesh: adoption that quietly carried
|
||||||
|
traffic would be step 5 arriving early and unrehearsed.
|
||||||
|
|
||||||
|
**Step 3 — conformance, per capability, per implementation:**
|
||||||
|
- the fixtures of design 19, recaptured on NATS, produced and consumed byte for byte by every SDK
|
||||||
|
that claims the capability — the test ADR 0074 set, and the only one that catches two
|
||||||
|
implementations quietly ignoring each other;
|
||||||
|
- an SDK implementing connection and events alone passes those two and claims nothing more,
|
||||||
|
rather than failing as a whole;
|
||||||
|
- a module built before this design serves its tools unchanged on the new runtime — the test of
|
||||||
|
ADR 0039;
|
||||||
|
- the shared library gained nothing but the binding: its surface is the protocol and the
|
||||||
|
primitives, and a helper that arrived with the transport is a review failure, not a detail.
|
||||||
|
|
||||||
|
**Step 4 — the mesh living on it.** The implementations exist from step 3, so this is where a mesh
|
||||||
|
can first be raised on NATS and run:
|
||||||
- a node enrols over TLS with a claimed token, and the enrolment user cannot read a declaration;
|
- a node enrols over TLS with a claimed token, and the enrolment user cannot read a declaration;
|
||||||
- a push composes; the store is stopped; the push is held (nak with delay), the store returns, the
|
- a push composes; the store is stopped; the push is held (nak with delay), the store returns, the
|
||||||
push applies, nothing was lost or duplicated;
|
push applies, nothing was lost or duplicated;
|
||||||
- a node that was away gets exactly the newest declaration, and a replayed older one is refused
|
- a node that was away gets exactly the newest declaration, and a replayed older one is refused
|
||||||
by sequence;
|
by sequence;
|
||||||
- an upgrade rolls out to two nodes;
|
- an upgrade rolls out to two nodes;
|
||||||
- a module's tool is invoked from another node and from a person's client, each with an account
|
|
||||||
that can invoke it, and refused from one that cannot;
|
|
||||||
- a module's account cannot publish outside its `emits` nor subscribe outside its `consumes` —
|
|
||||||
refused by the server;
|
|
||||||
- a module acks a delivery from its own durable consumer, and is refused acking another module's;
|
|
||||||
- a user subscribes another module's or person's inbox prefix and is refused by the server, not
|
|
||||||
by the client's own good behaviour;
|
|
||||||
- an event whose consumer keeps failing dead-letters after `max-deliver`;
|
|
||||||
- an enrolment request held by a `nak`-with-delay cycle still reaches the enrolling node's inbox
|
- an enrolment request held by a `nak`-with-delay cycle still reaches the enrolling node's inbox
|
||||||
once the controller answers — proving the reply travels in the payload and not the transport
|
once the controller answers — proving the reply travels in the payload and not the transport
|
||||||
field a consumer's ack has already claimed;
|
field a consumer's ack has already claimed;
|
||||||
- the `nats` container is not recreated when only its composed configuration file changes, and
|
- an event whose consumer keeps failing dead-letters after `max-deliver`;
|
||||||
a change to that file is live (a new user can connect, a revoked one cannot) within one
|
- a module's tool is invoked from another node and from a person's client, each with an account
|
||||||
watcher-poll interval, without a restart;
|
that can invoke it, and refused from one that cannot;
|
||||||
- a module built before this design serves its tools unchanged on the new runtime.
|
- a build source's change reaches the builder over the bus, and the build that follows is the one
|
||||||
|
the change asked for;
|
||||||
|
- an installation completes over the bus, with the same outcome the path it replaces produced;
|
||||||
|
- a node that was unreachable catches up on its reports rather than losing them.
|
||||||
|
|
||||||
**The cutover bed** — a mesh on AMQP with a predecessor stand-in on the compatibility broker moves
|
**Step 5 — the cutover bed:** a mesh on AMQP with a predecessor stand-in on the compatibility
|
||||||
its bus in one rollout; every node reports on NATS afterwards; the stand-in's client on AMQP is
|
broker moves its bus in one rollout; every node reports on NATS afterwards; the stand-in's client
|
||||||
still connected throughout.
|
on AMQP is still connected throughout.
|
||||||
|
|
||||||
Unit tests hold the controller to composing accounts from `emits`/`consumes` and nothing else, to
|
Unit tests hold the controller to composing accounts from `emits`/`consumes` and nothing else, to
|
||||||
creating the four streams and asserting them idempotently, and to spending a token exactly once;
|
creating the four streams and asserting them idempotently, and to spending a token exactly once;
|
||||||
@@ -287,7 +382,15 @@ to mapping the sdk contract onto subjects exactly as §8 says.
|
|||||||
|
|
||||||
## 11. Open, for the review
|
## 11. Open, for the review
|
||||||
|
|
||||||
**Closed by this revision** (first review, recorded in `MIGRATION-LOG.md`, 2026-09-24): the
|
**Closed by the second revision** (2026-09-26, [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md)):
|
||||||
|
the build was one undivided item (§9 is now five steps, each with its own bed in §10); there was no
|
||||||
|
adoption path for a mesh already running (§9 step 2); and §8 said a module sees "nothing new"
|
||||||
|
without distinguishing the sdk's contract, which does not change, from the specified wire, which
|
||||||
|
changes entirely and is design 19's (§8, §9 step 3). Two smaller corrections: the seat is
|
||||||
|
`mesh-broker` and not the product's name, and the shared library gains no conveniences with the new
|
||||||
|
transport.
|
||||||
|
|
||||||
|
**Closed by the first revision** (recorded in `MIGRATION-LOG.md`, 2026-09-24): the
|
||||||
`reload-on`/container mismatch (§5), the eaten reply subject on a CONTROL-stream message (§2, §6),
|
`reload-on`/container mismatch (§5), the eaten reply subject on a CONTROL-stream message (§2, §6),
|
||||||
the missing ack permission (§4), and the un-scoped reply inbox under one account (§4). Each is
|
the missing ack permission (§4), and the un-scoped reply inbox under one account (§4). Each is
|
||||||
named where it was wrong, not silently fixed, so a reader comparing against the first version can
|
named where it was wrong, not silently fixed, so a reader comparing against the first version can
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
---
|
||||||
|
layer: to-be
|
||||||
|
status: proposed
|
||||||
|
code: []
|
||||||
|
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/0074-the-wire-is-specified-not-the-types.md
|
||||||
|
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
|
||||||
|
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
||||||
|
- 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 28. Building the bus
|
||||||
|
|
||||||
|
**The work of [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md)'s five steps,
|
||||||
|
in the order its dependencies allow, with what each ends at.**
|
||||||
|
[Design 25](25-the-bus-on-nats.md) is the architecture and stays the authority on *what is built*;
|
||||||
|
this document holds only the order, the sizes and the proofs, and it is wrong the moment it
|
||||||
|
disagrees with design 25 rather than the other way round.
|
||||||
|
|
||||||
|
Each step ends at something runnable. A step that cannot name what its bed proves is not a step,
|
||||||
|
and is divided further before it is started.
|
||||||
|
|
||||||
|
## How this is built, and when it is run
|
||||||
|
|
||||||
|
**Written as code with unit tests, committed per change, and taken to the lab once the pieces that
|
||||||
|
would change the outcome are in place.** The mistake this avoids is the one
|
||||||
|
[design 22](22-the-work-ahead.md) records: running a long bed against a mesh mid-transformation and
|
||||||
|
debugging paths the next step deletes. Where a fault can be reasoned out of the code path, it is —
|
||||||
|
reading, not running.
|
||||||
|
|
||||||
|
So the beds below are acceptance tests at the end of assembled work, not the tool for finding each
|
||||||
|
bug, and a step's bed is run when that step is finished rather than while it is being written.
|
||||||
|
|
||||||
|
## What the work is, measured
|
||||||
|
|
||||||
|
Counted 2026-09-26, non-test source only. The point of counting is that none of this is unknown
|
||||||
|
territory: every piece has a shape already standing beside it.
|
||||||
|
|
||||||
|
| Piece | Today | Size | Becomes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| the controller's link | Go, one package | ~1 800 lines | the same package on NATS |
|
||||||
|
| the host's link | Go, one package, mirroring the contracts rather than importing them | ~1 000 lines | the same, on NATS |
|
||||||
|
| the tool runtime's client | TypeScript, one file | ~390 lines | the same, on NATS |
|
||||||
|
| the sdk's messaging surface | TypeScript: messaging, events, tools, contracts, primitives | ~360 lines across five | **unchanged**, see below |
|
||||||
|
| the broker module | the adopted AMQP broker: client, tools, provisioner, bootstrap, image, manifest | ~340 lines of module code | the `nats` module, same shape |
|
||||||
|
| the beds | 39 lab scenarios, including a broker bed, an adoption bed, a genesis bed and a store-window bed | — | four analogues and one new |
|
||||||
|
|
||||||
|
**Two measurements are worth stating on their own, because they change what the steps are.**
|
||||||
|
|
||||||
|
**The sdk speaks no AMQP, and never did.** The word appears in its source three times, in three
|
||||||
|
comments; its messaging module says in as many words that it "carries the contract, not a specific
|
||||||
|
AMQP client build." [ADR 0039](../../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md) put the
|
||||||
|
client in the runtime, and the payoff is collected here: **no module is rebuilt for this change, and
|
||||||
|
the sdk's own diff is three comments.** That is the whole reason a bus can be replaced under a live
|
||||||
|
mesh at all.
|
||||||
|
|
||||||
|
**The wire therefore has three implementations, not two, and no suite pins any of them.**
|
||||||
|
[ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md) spoke of "the existing
|
||||||
|
two implementations" — Go and TypeScript. Measured, the Go side is *two separate packages* that
|
||||||
|
mirror rather than share (the host imports nothing, by
|
||||||
|
[ADR 0005](../../02-DECISIONS/0005-the-node-host.md)), so the count is the controller's link, the
|
||||||
|
host's link, and the runtime's client. And a search for conformance fixtures finds none anywhere in
|
||||||
|
the four repositories: design 22's Phase 1.2 — the suite — has not been built.
|
||||||
|
|
||||||
|
> **This corrected the record.** ADR 0116 said step 3's fixtures were *recaptured* on NATS. There
|
||||||
|
> is nothing to recapture, so step 3 **builds** the suite, and its first job is to pin the wire the
|
||||||
|
> mesh has before changing it — a suite written only against the new bus certifies whatever the new
|
||||||
|
> bus happens to do. A fact went stale while the decision stood, which is a **progressive insight**
|
||||||
|
> ([`02-DECISIONS/README.md`](../../02-DECISIONS/README.md)): it is marked and dated in ADR 0116
|
||||||
|
> itself rather than left to be discovered here.
|
||||||
|
|
||||||
|
## The order the work actually allows
|
||||||
|
|
||||||
|
**The five steps are chunks of capability; the build order is not simply 1 to 5, and pretending
|
||||||
|
otherwise would put two beds where they cannot run.** Three edges decide it:
|
||||||
|
|
||||||
|
- **A specification precedes the implementations it governs.** ADR 0074's whole argument is that
|
||||||
|
agreement is specified and checked, not hoped for. So the wire's NATS binding is written *before*
|
||||||
|
the three implementations are, even though it is step 3 — and its conformance half can only
|
||||||
|
*finish* once two implementations exist to disagree.
|
||||||
|
- **A mesh cannot be raised on a bus nothing speaks.** A bed that raises a mesh on NATS from
|
||||||
|
genesis — enrolling a node, holding a push while the store restarts, rolling out an upgrade —
|
||||||
|
needs the controller and the host to speak NATS already. That is the implementations, and they
|
||||||
|
arrive with step 3.
|
||||||
|
- **Adoption needs the module and nothing else.** Step 2 puts a correctly configured server into a
|
||||||
|
running mesh that continues to ignore it, which depends on no link at all.
|
||||||
|
|
||||||
|
So step 1's bed proves *the server, from genesis, configured* — not a mesh living on it. The full
|
||||||
|
genesis bed is step 4's, where it can first run.
|
||||||
|
|
||||||
|
> **This corrected the record too.** ADR 0116 first attributed "a mesh raised on NATS from genesis"
|
||||||
|
> to step 1. That bed cannot run until the links exist, and a step whose proof cannot run is the
|
||||||
|
> exact failure the record was written to prevent — so step 1 now ends at the server standing,
|
||||||
|
> correctly configured and carrying nothing, and the full bed is named under step 4. The five
|
||||||
|
> steps, their names, their order and the single rollout are unchanged; only where two beds run has
|
||||||
|
> moved. Marked and dated in ADR 0116 as a progressive insight, with what the record said before.
|
||||||
|
|
||||||
|
```
|
||||||
|
step 1 module, genesis places it ──┐
|
||||||
|
step 2 adoption into a running mesh ──┤ neither needs a link
|
||||||
|
│
|
||||||
|
step 3 the wire specified ──► three implementations ──► the suite
|
||||||
|
│
|
||||||
|
step 4 the flows, and the full genesis bed
|
||||||
|
│
|
||||||
|
step 5 the rollout
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1 — the module, and genesis raises it
|
||||||
|
|
||||||
|
**Why here.** Everything else needs a server to talk to, and genesis is where the foundation is
|
||||||
|
defined. The mesh this is for will never travel this path — it is already running, and takes step 2
|
||||||
|
— but genesis is the definition every other path is measured against, and one that exists only on
|
||||||
|
paper is wrong until there is a second mesh to find out.
|
||||||
|
|
||||||
|
- [ ] 1.1 the `nats` module: manifest, image, one container, its client, TLS and monitoring ports,
|
||||||
|
JetStream on a named volume — the shape of design 25 §5, and the same shape the broker module
|
||||||
|
beside it already has
|
||||||
|
- [ ] 1.2 the composed configuration as a **directory** resource, and the entrypoint that watches
|
||||||
|
the one file and signals the server itself — design 25 §5's correction, kept inside the module
|
||||||
|
because a container has no reload and a recreate would drop every connection the mesh has
|
||||||
|
- [ ] 1.3 the controller composes that file: accounts, permissions, TLS, JetStream — permissions
|
||||||
|
derived from `emits` and `consumes` and nothing else, plus each user's own ack subject and its
|
||||||
|
own inbox prefix (design 25 §4)
|
||||||
|
- [ ] 1.4 the four streams, created at genesis and asserted idempotently on start, by the controller
|
||||||
|
as their only writer
|
||||||
|
- [ ] 1.5 genesis raises it as foundation, claiming the seat **`mesh-broker`** — the seat is the
|
||||||
|
server's role, not the product
|
||||||
|
- [ ] 1.6 the genesis-broker bed
|
||||||
|
|
||||||
|
**Done when.** A mesh raised from nothing has the server standing with the streams asserted and
|
||||||
|
every account and permission composed from the manifests; a user cannot publish outside its
|
||||||
|
`emits`, subscribe outside its `consumes`, ack another user's delivery or subscribe another's inbox
|
||||||
|
prefix; the monitoring port is refused from anything but the private network; a change to the
|
||||||
|
composed file is live within one watcher interval without a restart, and the container is not
|
||||||
|
recreated by it.
|
||||||
|
|
||||||
|
**The permission checks belong here rather than later** because the server enforces them itself — a
|
||||||
|
plain client proves them, no link required — and they are the whole of what ADR 0043 asks for. No
|
||||||
|
mesh traffic is on the bus yet; that is step 4's bed, not this one's.
|
||||||
|
|
||||||
|
## Step 2 — adoption puts it in the seat
|
||||||
|
|
||||||
|
**Why here.** It needs only step 1's module, it is the path the mesh that exists will actually take,
|
||||||
|
and it is what makes steps 3 and 4 safe to develop against a live mesh. A running mesh does not get
|
||||||
|
a foundation module by being raised again; it adopts one in place
|
||||||
|
([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
|
||||||
|
|
||||||
|
- [ ] 2.1 the server raised beside the existing broker on its own ports, carrying nothing
|
||||||
|
- [ ] 2.2 the `nats` module adopted onto it in place, holding the data and configuration it was
|
||||||
|
raised with
|
||||||
|
- [ ] 2.3 the seat claim, and the resolver's refusal of a second holder mesh-wide
|
||||||
|
- [ ] 2.4 the adoption bed
|
||||||
|
|
||||||
|
**Done when.** A mesh already running has the server adopted, holding `mesh-broker`; a second
|
||||||
|
assignment anywhere is refused at resolution — *one per mesh*; and every node is still on the old
|
||||||
|
bus with nothing routed to the new one. **That last check is the point of the step**: adoption that
|
||||||
|
quietly carried traffic would be step 5 arriving early and unrehearsed.
|
||||||
|
|
||||||
|
## Step 3 — the protocol on NATS
|
||||||
|
|
||||||
|
**Why here.** The implementations cannot be written against an unwritten wire, and this is the step
|
||||||
|
that decides what "agreeing" means for everything after it. It is the largest step and the one that
|
||||||
|
pays for itself furthest away.
|
||||||
|
|
||||||
|
- [ ] 3.1 **the suite first, on the bus the mesh has** — fixtures for the envelope and its required
|
||||||
|
headers, the contributions file, a served tool call and a grant, capturing what the three
|
||||||
|
implementations do *today*. Written first because a suite born on the new bus certifies
|
||||||
|
whatever the new bus happens to do
|
||||||
|
- [ ] 3.2 design 19 rewritten from exchanges, queues and routing keys to the subjects and streams of
|
||||||
|
design 25 §2–§3, per capability, with ADR 0074's model untouched: floor plus capabilities, an
|
||||||
|
implementation legitimate when it claims less, identity from the sealed credential, dedup on
|
||||||
|
`x-event-id`
|
||||||
|
- [ ] 3.3 the fixtures restated on NATS, and the capability each implementation claims
|
||||||
|
- [ ] 3.4 the controller's link on NATS
|
||||||
|
- [ ] 3.5 the host's link on NATS — mirroring, still importing nothing
|
||||||
|
- [ ] 3.6 the tool runtime's client on NATS, behind the unchanged sdk contract
|
||||||
|
- [ ] 3.7 the sdk's three stale comments, and nothing else in it
|
||||||
|
|
||||||
|
**Done when.** The fixtures are produced and consumed byte for byte by every implementation that
|
||||||
|
claims the capability, and a module built before any of this serves its tools unchanged on the new
|
||||||
|
runtime. **The step is not done when the code runs** — two implementations that disagree about an
|
||||||
|
envelope do not fail to compile, they ignore each other while both keep running, which is the
|
||||||
|
failure ADR 0074 exists to catch.
|
||||||
|
|
||||||
|
**And the shared library gained nothing but the binding.** A new transport is when the pressure to
|
||||||
|
add conveniences is highest, and ADR 0039's rule does not bend for it: a helper that arrives with
|
||||||
|
the bus is a review failure, not a detail. Code shared among a module's own features stays in that
|
||||||
|
module.
|
||||||
|
|
||||||
|
## Step 4 — the core speaks it
|
||||||
|
|
||||||
|
**Why here.** The links exist from step 3, so the flows that are not on the bus at all can move onto
|
||||||
|
it, and the beds that need a mesh living on NATS can finally run.
|
||||||
|
|
||||||
|
- [ ] 4.1 **the full genesis bed** — a mesh raised on NATS from nothing and living on it: a node
|
||||||
|
enrols over TLS with a claimed token and the enrolment user cannot read a declaration; a push
|
||||||
|
is held while the store restarts and applies after, nothing lost or duplicated; a node that
|
||||||
|
was away gets exactly the newest declaration and refuses a replayed older one by sequence; an
|
||||||
|
upgrade rolls out to two nodes; an event dead-letters after `max-deliver`; and an enrolment
|
||||||
|
held by a `nak`-with-delay cycle still reaches the enrolling node, proving the reply travels
|
||||||
|
in the payload and not the transport field the consumer's ack has claimed. The server-enforced
|
||||||
|
permissions were proved at step 1 and are not re-proved here
|
||||||
|
- [ ] 4.2 a build source's change reaches the builder over the bus, and the build that follows is
|
||||||
|
the one the change asked for
|
||||||
|
- [ ] 4.3 an installation completes over the bus, with the same outcome as the path it replaces
|
||||||
|
- [ ] 4.4 a person's client: the account, the client that speaks the bus, and the tool surface over
|
||||||
|
it (design 25 §7) — a module's tool invoked from another node and from a person, refused from
|
||||||
|
an account that may not
|
||||||
|
- [ ] 4.5 reports and catch-up: a node that was unreachable catches up rather than losing them
|
||||||
|
|
||||||
|
**Done when.** Each converted flow is proved against the behaviour it replaced, and the full genesis
|
||||||
|
bed is green. **Observation is not in this step** — heartbeats, conditions and key-value state are
|
||||||
|
[research 017](../../01-RESEARCH/017-a-mesh-that-heals-itself/00-overview.md)'s, that effort already
|
||||||
|
reserves them for after the move, and a flow built ahead of its design would be rebuilt.
|
||||||
|
|
||||||
|
## Step 5 — the rollout
|
||||||
|
|
||||||
|
**Why here.** It is the only step that moves a node's bus, and it moves every node's at once.
|
||||||
|
|
||||||
|
- [ ] 5.1 the cutover bed: a mesh on AMQP with a predecessor stand-in on the compatibility broker
|
||||||
|
moves its bus in one rollout, every node reporting on NATS afterwards, the stand-in's own
|
||||||
|
client still connected throughout
|
||||||
|
- [ ] 5.2 the rollout: accounts composed, then the controller, every host and every runtime
|
||||||
|
together; every node confirmed heard before AMQP stops
|
||||||
|
- [ ] 5.3 the mesh's accounts removed from the compatibility broker, leaving the predecessor's users
|
||||||
|
- [ ] 5.4 the compatibility broker retires when its condition holds — no client connected for the
|
||||||
|
period the operator sets
|
||||||
|
|
||||||
|
**Done when.** Every node reports on NATS and the predecessor's clients never noticed.
|
||||||
|
|
||||||
|
## The through-line
|
||||||
|
|
||||||
|
The order is dependency, not preference. **Steps 1 to 4 leave every node on AMQP**, so the cost of
|
||||||
|
being wrong is bounded until the last step: a step may be abandoned, or reordered after step 2,
|
||||||
|
without a rollback. The server stands before anything speaks to it; the wire is specified before it
|
||||||
|
is implemented three times; the flows move once there is something to move them onto; and the bus
|
||||||
|
itself moves once, at the end, on one day.
|
||||||
|
|
||||||
|
## What is deliberately not here
|
||||||
|
|
||||||
|
- **Observation** — research 017's, after the move, by its own design.
|
||||||
|
- **Leaf nodes** — design 25 §11 keeps this out of scope and says so; a leaf per machine is a later
|
||||||
|
question, noted so it is not forgotten.
|
||||||
|
- **The predecessor's world.** It is AMQP, it cannot move, and it does not need to: its broker is
|
||||||
|
the compatibility module until its last client is gone.
|
||||||
|
|
||||||
|
## How this list is kept true
|
||||||
|
|
||||||
|
A task is ticked when its change is committed, not when it is written. A step is done when its bed
|
||||||
|
is green, not when its tasks are ticked. If a step's tasks are all ticked and its bed has not run,
|
||||||
|
the step is **in progress** and this document says so — that gap is the thing the whole shape is
|
||||||
|
built to make visible.
|
||||||
@@ -34,8 +34,10 @@ document is written and this one's status becomes `implemented`.
|
|||||||
| [`22-the-work-ahead.md`](22-the-work-ahead.md) | Everything decided and not yet built, in dependency order, each phase ending at a run | [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md), [ADR 0075](../../02-DECISIONS/0075-two-stores-and-which-provides-what.md), [ADR 0014](../../02-DECISIONS/0014-no-npm-workspace.md) |
|
| [`22-the-work-ahead.md`](22-the-work-ahead.md) | Everything decided and not yet built, in dependency order, each phase ending at a run | [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md), [ADR 0075](../../02-DECISIONS/0075-two-stores-and-which-provides-what.md), [ADR 0014](../../02-DECISIONS/0014-no-npm-workspace.md) |
|
||||||
| [`23-choosing-a-provider.md`](23-choosing-a-provider.md) | Which of several providers of a kind serves a consumer, and when a module carries its own instead | [ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md), [ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) |
|
| [`23-choosing-a-provider.md`](23-choosing-a-provider.md) | Which of several providers of a kind serves a consumer, and when a module carries its own instead | [ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md), [ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) |
|
||||||
| [`24-the-secrets-vault.md`](24-the-secrets-vault.md) | The module that owns a secret — a `secret` provision, and the boundary of what it owns | [ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), [ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md), [ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md) |
|
| [`24-the-secrets-vault.md`](24-the-secrets-vault.md) | The module that owns a secret — a `secret` provision, and the boundary of what it owns | [ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), [ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md), [ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md) |
|
||||||
|
| [`25-the-bus-on-nats.md`](25-the-bus-on-nats.md) | **Proposed.** The architecture of the mesh's bus on NATS: what rides which subject under which guarantee and whose account, how a node joins, how a person reaches a tool, and how the mesh moves from the bus it has | [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md), [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md), [ADR 0043](../../02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md), [ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md) |
|
||||||
| [`26-the-seats.md`](26-the-seats.md) | **Proposed.** What a mesh can have one of, who fills each, and a seat's holder answering for the provision it delivers — including the `git` seat a build's source can live on | [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md), [ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md), [ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md) |
|
| [`26-the-seats.md`](26-the-seats.md) | **Proposed.** What a mesh can have one of, who fills each, and a seat's holder answering for the provision it delivers — including the `git` seat a build's source can live on | [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md), [ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md), [ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md) |
|
||||||
| [`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 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md) |
|
| [`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 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) |
|
||||||
|
|
||||||
## Not yet written
|
## Not yet written
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,12 @@ vocabulary — *controller* (not "control plane"), *foundation* (not "substrate"
|
|||||||
docs (`layer`, `status`, `code`, `updated`), issue reports (`status`, `located-in`,
|
docs (`layer`, `status`, `code`, `updated`), issue reports (`status`, `located-in`,
|
||||||
`fixed-by`, `amended-design`) and decision records (`status`, `date`, `deciders`).
|
`fixed-by`, `amended-design`) and decision records (`status`, `date`, `deciders`).
|
||||||
Never create a central status file; cross-cutting views are generated from frontmatter.
|
Never create a central status file; cross-cutting views are generated from frontmatter.
|
||||||
- **`02-DECISIONS/` records are immutable.** Supersede with a new record; never edit meaning. Fixing a
|
- **`02-DECISIONS/` records hold their meaning.** Supersede with a new record rather than rewriting
|
||||||
broken link or path is allowed.
|
what was decided, the options weighed, or a consequence another record relies on. Fixing a broken
|
||||||
|
link or path is allowed, and so is a **progressive insight** — a correction of *fact* that leaves
|
||||||
|
the decision standing, made in place, marked and dated in the record's own words
|
||||||
|
([`02-DECISIONS/README.md`](02-DECISIONS/README.md)). A fact going stale is not the decision going
|
||||||
|
wrong, and superseding a sound record for one buries it.
|
||||||
- **Design docs are prose and diagrams only** — no code. A manifest field may be named; a
|
- **Design docs are prose and diagrams only** — no code. A manifest field may be named; a
|
||||||
manifest may not be pasted.
|
manifest may not be pasted.
|
||||||
- **Two layers, never mixed.** [`03-DESIGN/00-as-is/`](03-DESIGN/00-as-is/) describes the mesh
|
- **Two layers, never mixed.** [`03-DESIGN/00-as-is/`](03-DESIGN/00-as-is/) describes the mesh
|
||||||
|
|||||||
Reference in New Issue
Block a user