Add mesh-knowledge, the knowledge base's first slice (hq issue 425, ADR 0290)
mesh/merge-gate pass: builds new: modules/mesh-knowledge, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compo…
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds new: modules/mesh-knowledge, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compo…
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
An agent learns what a decision says only by reading whole records. This module holds every decision record and research effort whole in a database in the store, with clauses that carry their own status and source, typed links read from both ends, and every version. It answers ask, get and holds within a stated bound, and takes a record through reserve, propose (refused on any of the 29 per-record rules), review by someone other than the author, and acceptance, on the operator's proven warrant when the record needs it. The 4 whole-set rules make the module unhealthy when they fail. The import reads hq at a commit through the records module, never overwrites, and changes nothing when run again. Prose decisions get drafted clauses that a second judgement must confirm before they are served.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
331e8d0bbef401cee83c28aef27db9e823a901c7
|
||||
Binary file not shown.
@@ -0,0 +1,680 @@
|
||||
# Glossary — the words of the mesh, by domain, and the ones it stopped using
|
||||
|
||||
One name per thing. This page is the authority; where an older record says something else, that
|
||||
record keeps its words and this page says how to read them. It exists because the words kept drifting
|
||||
in conversation — "control plane", "controller" and "master" for one thing, "substrate" and
|
||||
"foundation" for another — and a mesh you cannot name precisely is a mesh two people describe differently. The rule and
|
||||
its check are [ADR 0244](../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md);
|
||||
the domains are drawn in [to-be 49](../03-DESIGN/01-to-be/49-the-mesh-in-domains.md).
|
||||
|
||||
## How to read this page
|
||||
|
||||
A **domain** is an area of the mesh that owns a set of concepts: inside it each concept has one word,
|
||||
and the domain decides what that word means. Other domains use the word as it is defined here and never
|
||||
change its meaning. Every word is defined once, in the domain that owns it; a section lists the words
|
||||
it uses from other domains under *Uses*, with no second definition.
|
||||
|
||||
Two fixed lines follow an entry where they apply, and `00-META/checks/words.py` reads them:
|
||||
|
||||
- *Not:* followed by struck-through words — the words this entry replaced. A struck word with no scope
|
||||
is retired everywhere; *(hq)* retires it in this repository's prose only; *(tools)* in the
|
||||
descriptions of the mesh's tools and seat verbs only. A retired word may be quoted, never used.
|
||||
- *Identifier until renamed:* followed by a name in a code span — code that still carries an old name.
|
||||
It may stand in a code span and nowhere else until the code is renamed, and then the line goes.
|
||||
|
||||
Nothing else on this page is struck through. A word that means different things in different domains
|
||||
is listed under [Homonyms](#homonyms), with the qualified form each domain uses.
|
||||
|
||||
## Words every domain uses
|
||||
|
||||
- **mesh** — the whole: the nodes, the modules assigned to them, the controller that decides and the
|
||||
records it keeps. One mesh per operator; a second mesh is a second everything.
|
||||
- **domain** — an area of the mesh that owns a set of concepts and the one word for each, as described
|
||||
in the section above ([ADR 0244](../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)).
|
||||
ADR 0006 called seven of them the controller's *contexts*; they carry over as domains under the same
|
||||
names, except *observability*, which is now **Health and repair**. Where a domain's records live in
|
||||
the controller, the domain owns that store alone ([ADR 0008](../02-DECISIONS/0008-a-context-owns-its-store.md)).
|
||||
*Context* in that sense is not used in new writing; the word stays ordinary English, so this is
|
||||
checked by review, not by `words.py`.
|
||||
- **machine** — any computer: hardware, a virtual machine, before, during or outside its membership of
|
||||
the mesh. Prose about the computer itself — its disks, its kernel, the network it sits on, what was on
|
||||
it before the mesh — says machine.
|
||||
- **node** — a machine the mesh has adopted and owns. A machine becomes a node when it joins
|
||||
([ADR 0004](../02-DECISIONS/0004-a-node-and-how-it-joins.md)), and from then on it runs the node-engine
|
||||
and is either **adopted** (the mesh holds what it found there as found) or **converged** (the mesh made
|
||||
it what it is) — [ADR 0100](../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md).
|
||||
Prose about a member of the mesh says node. A node is just a machine that has joined; being one
|
||||
implies nothing about what it runs.
|
||||
- **module** — the unit the mesh assigns: one named thing, defined by its manifest, that a node runs —
|
||||
a service, a container, files, packages, its own code — and the tools it serves
|
||||
([ADR 0040](../02-DECISIONS/0040-what-a-module-is.md)). Everything configurable on a node is a module.
|
||||
- **seat** — a named role at a scope (node / site / mesh), held by a module assignment, from a **closed
|
||||
set** the mesh defines: a claim naming a seat outside the set is refused. A seat may **deliver a
|
||||
provision**, and its holder is then the mesh's answer for it when several modules provide it
|
||||
([ADR 0126](../02-DECISIONS/0126-a-module-declares-its-own-seats.md)). The set, with who holds each
|
||||
seat, is the overview of what a mesh has ([26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)).
|
||||
A seat carries **verbs** — the tools every holder must serve. A verb added to a held seat is first
|
||||
promised as optional, then served, then required ([ADR 0246](../02-DECISIONS/0246-a-seats-new-verb-is-promised-before-it-is-required.md)).
|
||||
A node seat may also name **the paths its holder owns**, such as `/etc/hosts`: a claim whose module does
|
||||
not declare them is refused, and only a claimant declares them ([ADR 0287](../02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md)).
|
||||
- **operator** — the person who runs the mesh, and the one the mesh talks to.
|
||||
*Not:* ~~master~~ (hq)
|
||||
- **person** — any human, as against the mesh acting unattended: *a person's word* releases a held
|
||||
delivery, *a person* deletes a consumer's data. The operator is one; the word says that a human, not
|
||||
the mesh, acted.
|
||||
|
||||
## Module — what a module is and declares
|
||||
|
||||
**Purpose.** To say what one module is, in the one document every other domain reads.
|
||||
**Recorded in** the catalogue, one manifest per module. **Upstream of** every other domain; its words
|
||||
are a published vocabulary, fixed by the manifest's schema. **Decided by** ADR 0040, 0126, 0174, 0188,
|
||||
0207, 0236, 0262.
|
||||
**Uses:** module, seat, provision (Provisioning), data class (Data), health (Health and repair).
|
||||
|
||||
- **manifest** — the file a module is defined by (`module.json`): what it is, what it provides and
|
||||
requires, the seats it claims, the resources it declares, its tools, its data and its health. Say
|
||||
manifest in prose, not the file's name.
|
||||
*Not:* ~~module definition~~ (hq)
|
||||
- **resource** — one thing a manifest declares a node must have: a file, a directory, a service, a
|
||||
container, a package, a bundle, an archive. A module **declares** resources; the verb *declares* is
|
||||
this domain's and means what a manifest states.
|
||||
- **claim** — a module taking a spot on a seat: `claims: [{name, scope}]` in a manifest. A mesh-scoped
|
||||
exclusive claim is how the mesh says "there is one of me". A foundation seat is named after the role
|
||||
it guards: the `mesh-controller`, `postgres` and `nats` modules claim the `mesh-controller`,
|
||||
`mesh-store` and `mesh-broker` seats ([ADR 0079](../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md),
|
||||
[ADR 0116](../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md)).
|
||||
- **setting** — a value a manifest declares and an assignment gives, one of the two ways a node varies
|
||||
a module ([ADR 0174](../02-DECISIONS/0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)).
|
||||
What a "flavor" once varied is a setting or a separate module.
|
||||
*Not:* ~~flavor~~
|
||||
- **preference** — a setting whose manifest gives it a default, because its value is true of the
|
||||
software and only tuned per node: a font size, a width
|
||||
([ADR 0262](../02-DECISIONS/0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md)).
|
||||
A setting with no default is the operator's own value: a domain, an identity.
|
||||
*Not:* ~~tunable~~ (hq)
|
||||
- **kept region** — a marked block in a managed file the mesh writes *into*, where the operator's own
|
||||
lines survive every send and are given back when the module goes (ADR 0174). The other way a node
|
||||
varies a module.
|
||||
- **bundle** — the artifact a module's own code is built into — its tools, a seat's implementation, a
|
||||
daemon — in any language the mesh has a toolchain for; never an image. A **tools bundle** speaks MCP to
|
||||
the tool runner. One module may declare several
|
||||
([ADR 0188](../02-DECISIONS/0188-a-modules-own-code-is-bundles-in-any-language-and-a-tools-bundle-speaks-mcp-to-the-runtime.md)).
|
||||
- **tool** — one capability a module serves through the tool runner, addressed `<module>.<tool>`. A
|
||||
seat's **verb** is the same thing carried by a seat rather than a module.
|
||||
- **mesh-sdk** — the library a module's own code is written against, including the harness that serves a
|
||||
tools bundle to the tool runner. The harness is part of it, not a library of its own.
|
||||
*Not:* ~~tools-sdk~~
|
||||
- **invokes** — the manifest word for the tools a module calls, `<module>.<tool>` each or `*` for every
|
||||
one. A grant on the publish side and nothing else; a module that declares none calls nothing.
|
||||
- **upgrade policy** — how a module's new builds reach its nodes: `roll` (one node first, judged, then
|
||||
the rest), `together`, or `record` (register the build, send it nowhere)
|
||||
([ADR 0236](../02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md)).
|
||||
The controller's verb `upgrade` shows it.
|
||||
|
||||
## Core — the mesh's own machinery
|
||||
|
||||
**Purpose.** To keep the controller, the bus, the store and every node's engine running and agreed, so
|
||||
every other domain has something to run on. **Recorded by** the controller and the store.
|
||||
**Upstream of** every domain but Module; the others use the bus and the store as they are.
|
||||
**Decided by** ADR 0005, 0006, 0067, 0106, 0141, 0175, 0227, 0229, 0289.
|
||||
**Uses:** node, module, seat.
|
||||
|
||||
- **controller** — the component that decides what each node should be, holds the mesh's records, and
|
||||
tells nodes over the bus. The relationship is *controller and nodes*, and no node is subordinate: a
|
||||
node applies its declaration on its own and survives the control-node dying.
|
||||
*Not:* ~~control plane~~, ~~slave~~ (hq), ~~mesh-control~~ (hq)
|
||||
- **mesh-controller** — the module that runs the controller. It claims the `mesh-controller` seat at
|
||||
mesh scope, which is what makes it singular.
|
||||
- **control-node** — the one node that also holds the `mesh-controller` seat. There is exactly one per
|
||||
mesh. It is not a separate kind of node — it is a node that additionally runs the controller (and,
|
||||
today, the foundation). Lose it and the other nodes keep running what they were last told; they simply
|
||||
cannot be told anything new.
|
||||
- **node-engine** — the program on every node that applies what the controller declares: it receives
|
||||
the node's declaration, writes the files, runs the services and containers, and reports what it did.
|
||||
It is the engine, not a module: it owns no file's content, and every file it writes belongs to the
|
||||
module that declared it. *Agent* is avoided because that word means a coding agent here. A record
|
||||
written before the rename keeps the old name. "The host" is retired in this repository's prose only:
|
||||
in the tools' descriptions it is also an SSH server's or a container runtime's own word, so a
|
||||
description that means the node-engine by it is found by review.
|
||||
*Not:* ~~host agent~~, ~~the host~~ (hq), ~~node host~~
|
||||
*Identifier until renamed:* `mesh-host` — the repository, its binary and its service unit; the rename is
|
||||
[issue 354](../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)
|
||||
- **tool runner** — the one program on each node that loads every assigned module's tools bundle and
|
||||
serves every tool and every held seat's verb on the subjects the memberships issue; the node-engine
|
||||
supervises it as a process beside the services it runs, never a container
|
||||
([ADR 0175](../02-DECISIONS/0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md),
|
||||
which called it "node tools"). Its mode on loopback is the mesh MCP server.
|
||||
*Not:* ~~node tools~~, ~~tool runtime~~ (hq)
|
||||
*Identifier until renamed:* `node-tools` — the module, its unit and its bus account; the rename is
|
||||
[issue 354](../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md)
|
||||
- **foundation** — the store and the bus, raised at genesis before any module system exists. The
|
||||
foundation is not a third thing beside the store and the bus — it *is* those two, named together.
|
||||
*Not:* ~~substrate~~
|
||||
- **genesis** — raising the foundation and the controller on an empty mesh, by the one installation
|
||||
done by hand ([ADR 0067](../02-DECISIONS/0067-genesis-is-a-pivot.md)).
|
||||
- **store** — the one postgres server. It holds the controller's own databases (`inventory`,
|
||||
`identity`, `licences`, each owned by one domain, ADR 0008) and every module's own database. One server,
|
||||
many databases — never one shared mesh database. Bare *store* means this and nothing else
|
||||
(see [Homonyms](#homonyms)).
|
||||
- **bus** — the mesh's own nervous system: NATS, one per mesh, carrying every link the mesh has —
|
||||
control, declarations, builds, events, tool calls ([ADR 0106](../02-DECISIONS/0106-the-bus-is-nats.md)).
|
||||
A module reaches it by requiring `mesh-bus` ([ADR 0128](../02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md));
|
||||
one that does not require it has no account on it. Held by the `mesh-broker` seat, which is named
|
||||
after the role rather than the server, so the server can change without the seat doing so. The `nats`
|
||||
module holds it.
|
||||
- **the deprecated broker** — the lavinmq module. It was the mesh's bus and is not any more. It keeps
|
||||
running as an **ordinary provider** of the `amqp` provision, for modules that need a message broker
|
||||
of their own the way something needs a database
|
||||
([ADR 0131](../02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)) — it claims no
|
||||
seat, is not foundation, is never raised at genesis, and a mesh that never installs it is complete.
|
||||
Say *the deprecated broker*, not *the compatibility broker* (it serves the mesh's own modules, not only
|
||||
the predecessor's) and not *the AMQP broker* (naming it after a protocol invites describing the bus by
|
||||
contrast with it, which is backwards).
|
||||
- **layer** — one of the four levels the mesh is built in, from the bottom: the node-engine, the
|
||||
foundation, the controller, the surfaces. The records' `topic: the tiers` and `repos.md` say *tier* for
|
||||
this; new prose says layer (see [Homonyms](#homonyms)).
|
||||
- **lease** and **epoch** — which controller may send (the lease, held in the bus and renewed), and the
|
||||
order of what it sent (the epoch, which a node reads before it accepts a declaration)
|
||||
([ADR 0229](../02-DECISIONS/0229-the-cores-order-is-a-lease-the-store-remembers-and-an-epoch-a-machine-is-sent-once-it-reads-one.md)).
|
||||
|
||||
- **record mirror** — a key-value bucket on the bus that the controller writes from one kind of its
|
||||
records (nodes, assignments, walks, builds, settings), after each change commits in its store and only
|
||||
when the value differs, with one event per write. The store stays the record; the self-check compares
|
||||
the two and raises `mirror-drifted` on any difference
|
||||
([ADR 0289](../02-DECISIONS/0289-the-controller-mirrors-its-records-into-buckets-written-only-on-a-change-and-audited-against-its-store.md)). Shortened to *mirror* where the
|
||||
text is about the controller's records; the builder's *mirrored* `upstream` image (Change and delivery) is
|
||||
a different thing, a copy of an outside image in the artifact store.
|
||||
|
||||
## Placement — what runs on which node
|
||||
|
||||
**Purpose.** To decide, send and apply what each node runs, and to say why.
|
||||
**Recorded by** the controller's `inventory` database. **Upstream of** Provisioning, Connectivity, Data
|
||||
and Health and repair. **Decided by** ADR 0100, 0126, 0176, 0181, 0207, 0221, 0252, 0261.
|
||||
**Uses:** node, machine, module, seat, setting (Module), manifest (Module).
|
||||
|
||||
- **assignment** — a module put on a node, with the settings that node gives it.
|
||||
- **pending assignment** — an assignment of a module whose build is still running, kept, and made by the
|
||||
controller when that build registers the module, or ended with why when it does not
|
||||
([ADR 0261](../02-DECISIONS/0261-an-assignment-made-while-its-modules-build-runs-is-kept-pending-and-the-controller-makes-it.md)).
|
||||
- **scope** — where a seat or a claim holds: `node`, `site` or `mesh`. `node` is also the scope's
|
||||
value in a manifest.
|
||||
- **capacity** — how many holders a seat takes. A capacity-1 seat is exclusive. A higher-capacity seat
|
||||
is a **bench**: several holders coexist. A **replicated** bench has one holder per node, each on record
|
||||
and each answering the same, like `mesh-dns-resolver`
|
||||
([ADR 0223](../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md)). A
|
||||
**kinded** bench has holders that are different modules, each claiming one **kind**, and a verb's
|
||||
subject carries the kind; `channel` and `intake` are the only ones (ADR 0234).
|
||||
- **installed / holding** — a module may be assigned (its package installed, its files placed) without
|
||||
holding the seat its family declares; *holding* is being the one — the login shell, the display session
|
||||
— on that node ([ADR 0176](../02-DECISIONS/0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md)).
|
||||
The module that holds a seat is its **holder**.
|
||||
- **depends on a seat** — a module needing a seat held on its node by some module, without holding it.
|
||||
Derived from the resources it declares, never stated: a `service` depends on `node-service-manager`, a
|
||||
`package` on `node-package-manager`, a `container` on `node-container-runtime`
|
||||
([ADR 0207](../02-DECISIONS/0207-a-module-depends-on-the-node-seats-that-apply-its-resources.md)).
|
||||
Not a claim: a module claims a seat it holds and declares resources.
|
||||
- **declaration** — what the controller sends one node: every resource of every module assigned there,
|
||||
composed. The noun is this domain's; what a manifest says is that it *declares* (Module), and a manifest
|
||||
is never called a declaration. The controller's verb `plan` previews a node's declaration.
|
||||
- **send** — the controller giving a node its declaration. The controller's verb `push` asks for a
|
||||
send (of one node, or of every node); prose says send. A git push and a phone's push notification are
|
||||
other things (see [Homonyms](#homonyms)).
|
||||
- **apply** — the node-engine making its node match its declaration; **reconcile** is doing so again
|
||||
until nothing differs.
|
||||
- **operator account** — the login name of the person who works on a node, stated on the node record;
|
||||
empty for a node nobody logs into. Everything the mesh places under a person's home is resolved
|
||||
against this account's home and owned by it
|
||||
([ADR 0181](../02-DECISIONS/0181-the-operator-account-is-a-node-fact-and-a-home-is-a-placement-root.md)).
|
||||
Not a name a manifest carries.
|
||||
- **agent account** — the login every coding agent on a node runs as, when the node names one of its own;
|
||||
stated on the node record at the controller's terminal, never by a verb or a setting. Empty means agents
|
||||
run as the operator account. It cannot become root without a person, and the node-engine judges that on
|
||||
every look ([ADR 0266](../02-DECISIONS/0266-on-the-control-node-agents-run-under-an-account-of-their-own-that-cannot-become-root-without-a-person.md)).
|
||||
- **account group** — a group of the machine's group database that a module puts an account in, by
|
||||
declaring the account with that group and nothing else of it. Several modules may each add one to the
|
||||
same account; the mesh takes back only an account group it added
|
||||
([ADR 0252](../02-DECISIONS/0252-a-module-puts-an-account-in-a-group-and-the-mesh-says-when-a-new-login-is-needed.md)).
|
||||
Never *membership*, which is the bus's word.
|
||||
|
||||
## Provisioning — one module serving another
|
||||
|
||||
**Purpose.** To resolve which module serves a provision for which consumer, and to wire the two.
|
||||
**Recorded by** the controller. **Upstream of** Data; downstream of Module, Placement and Identity.
|
||||
**Decided by** ADR 0027, 0084, 0138, 0225, 0230, 0263.
|
||||
**Uses:** module, seat, credential (Identity and access).
|
||||
|
||||
- **provision** — a service one module `provides` and others `require`; the mesh resolves a provider
|
||||
and wires the two with an endpoint and a credential. A provision is a service you offer, a seat is a
|
||||
role you occupy, and the two meet where a seat delivers a provision: occupying the seat is what makes
|
||||
a module *the* provider of it.
|
||||
- **provider** and **consumer** — the module that provides a provision, and the module that requires
|
||||
it. Which of several providers serves a consumer is resolved
|
||||
([ADR 0084](../02-DECISIONS/0084-which-provider-serves-a-consumer.md)).
|
||||
- **pin** — a person's choice of provider for a consumer, which resolution then respects.
|
||||
- **endpoint** — where a consumer reaches its provider; an assignment binds it and says how far it
|
||||
reaches ([ADR 0138](../02-DECISIONS/0138-an-assignment-binds-an-endpoint-and-says-how-far-it-reaches.md)).
|
||||
- **retire** — a provider stops serving a consumer the mesh no longer asks for; what it kept is deleted
|
||||
only by a person ([ADR 0230](../02-DECISIONS/0230-a-consumer-the-mesh-stops-asking-for-is-retired-and-deleted-only-by-a-person.md)).
|
||||
- **share** — a folder one node offers to the mesh's other nodes over the private network: the provision
|
||||
`nfs-share`, provided by the holder of the node seat `node-nfs-server`, one share per folder
|
||||
([ADR 0263](../02-DECISIONS/0263-a-share-is-a-provision-a-node-mounts-it-on-demand-and-only-nodes-that-ask-get-it.md)).
|
||||
- **mount** — a share, or an occasional source, made visible at a path on one node by the holder of the
|
||||
node seat `node-mounts`, as a mount unit and an automount unit, mounted on access (ADR 0263).
|
||||
- **occasional source** — a share on a device that is not a node and comes and goes, such as a USB disk;
|
||||
its absence is a state, *not present*, never a fault (ADR 0263).
|
||||
|
||||
## Identity and access — who may do what
|
||||
|
||||
**Purpose.** To issue, hold, rotate and check the credentials and grants by which modules, nodes, agents
|
||||
and the operator act. **Recorded by** the controller's `identity` and `licences` databases and the vault.
|
||||
**Upstream of** Provisioning, Change and delivery, and Operator and conversation.
|
||||
**Decided by** ADR 0085, 0113, 0183, 0225, 0234.
|
||||
**Uses:** module, node, operator, consumer (Provisioning).
|
||||
|
||||
- **credential** — anything that proves an identity: a password, a key, a token. A **pair credential**
|
||||
is the two ends of one credential, the consumer's and the provider's.
|
||||
- **secret** — a credential or other value the vault keeps sealed and the node-engine unseals into a
|
||||
file at apply ([ADR 0085](../02-DECISIONS/0085-a-secret-is-a-provision.md)).
|
||||
- **secret family** — a module's own secrets declared as one name ending in `-*`; each member is the prefix
|
||||
and a name (`smb-password-<source>`, one per source the module's settings name), given by its full name
|
||||
and never made by the mesh
|
||||
([ADR 0283](../02-DECISIONS/0283-a-part-that-waits-for-the-operators-secret-or-setting-is-said-as-waiting-passes-the-first-node-gate-and-names-the-act.md)).
|
||||
- **vault** — the module that owns every secret (`mesh-vault`,
|
||||
[24 — The secrets vault](../03-DESIGN/01-to-be/24-the-secrets-vault.md)).
|
||||
- **grant** — what an identity may call or reach, bounded by the provisions it requires
|
||||
([ADR 0225](../02-DECISIONS/0225-a-consumers-identity-is-bounded-by-the-provision-it-requires.md)).
|
||||
- **bus account** and **membership** — a module's or a node's account on the bus, and the subjects it
|
||||
is issued. The bus server calls its accounts *users*; the mesh says bus account.
|
||||
- **rotate** — replacing a credential without a consumer holding one the provider does not know.
|
||||
- **licence** — a model-access account the licence manager holds and **binds** to a consumer, the
|
||||
coding agent on a node ([ADR 0183](../02-DECISIONS/0183-the-anthropic-licence-manager-is-a-module-and-hands-tokens-to-the-agent-over-the-bus.md)).
|
||||
Taking a consumer off a licence is **unbinding** it; the licence manager's verb for it is still
|
||||
`release`, an identifier to rename.
|
||||
- **proof** — what an authorising answer carries: a verified sender, a one-time code, a security key's
|
||||
touch (ADR 0234). How much proof a kind of ask needs is its **assurance level**.
|
||||
- **controller's terminal** — a command line the controller runs as typed by its operator, not asked through a
|
||||
verb, so a change that is the terminal's alone is taken (issue 339, issue 340) — or, since
|
||||
[ADR 0277](../02-DECISIONS/0277-a-trusted-setting-is-proposed-through-a-verb-and-set-on-the-operators-warrant-and-a-modules-own-secret-is-asked-for-and-typed-at-the-desk.md),
|
||||
proposed through a verb and set on the operator's warrant. In the controller's code it is a
|
||||
line run without `MESH_VERB`. The operator reaches it with `mesh-cli` from their own account on a node where no
|
||||
agent can run as that account: the control-node, and a node whose agents run under an account of their own
|
||||
([ADR 0272](../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md)).
|
||||
|
||||
## Change and delivery — a commit on its way to the nodes
|
||||
|
||||
**Purpose.** To take one commit from its pull request to every node that should run it, and to put it
|
||||
back when it fails. **Recorded by** the `mesh-delivery` module (deliveries) and the controller (the
|
||||
planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
|
||||
**Decided by** ADR 0162, 0218, 0236, 0237, 0238, 0239, 0250, 0267, 0270, 0276.
|
||||
**Uses:** module, node, declaration and send (Placement), health (Health and repair), person.
|
||||
|
||||
- **merge check** — the two statuses a pull request carries before it may merge: the **merge gate**
|
||||
(`mesh/merge-gate`, the build seat's composed check of the modules the change touches) and the
|
||||
**repository check** (`mesh/repo-check`, the repository's own `merge-check.sh`)
|
||||
([ADR 0238](../02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md)).
|
||||
- **build seat** — the seat whose holder builds modules, `node-build-agent`; its holder on a node is the
|
||||
**builder**. Any node may hold the seat, so there is no build node by nature
|
||||
([ADR 0237](../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md)).
|
||||
*Not:* ~~build machine~~
|
||||
- **build queue** — the controller's queue of builds to run; one entry in it is a **build request**.
|
||||
The queue's verbs still say *ask* for an entry; the conversation's ask is another thing
|
||||
(see [Homonyms](#homonyms)).
|
||||
- **package** — what code resolves when it is **compiled**: an npm/cargo/pypi dependency, by **version**.
|
||||
Served by the **package-registry** (gitea). Only a builder talks to it.
|
||||
- **artifact** — anything a build produces and the mesh delivers to a node by **digest**: an `image`, a
|
||||
mirrored `upstream` image, a `bundle` of the module's own code, an `archive`. Served by the
|
||||
**artifact store**, an OCI registry that holds every kind as content-addressed blobs
|
||||
([ADR 0156](../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)).
|
||||
An image is one kind of artifact, and a module is not an image. A package and an artifact are two
|
||||
protocols, not one store being weak ([ADR 0075](../02-DECISIONS/0075-two-stores-and-which-provides-what.md)).
|
||||
- **build source** — the files one artifact is built from, in one or more repositories: for a Go program
|
||||
its import closure (the directories of its packages, its Go module files) and its recipe; for any other
|
||||
artifact the paths its manifest names, by default its module's own directory. Each artifact of a build
|
||||
has its own, and a change rebuilds the artifacts whose build source it touches
|
||||
([ADR 0267](../02-DECISIONS/0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md)).
|
||||
Older records (ADR 0015, ADR 0111) and designs say *build source* for the repository a module
|
||||
is built from; this entry narrows it to the files within, and that use reads as *the repository holding
|
||||
the build source*. Sharing a repository is not a dependency, and no build source is a whole repository
|
||||
holding another module's files.
|
||||
- **catalogue** — what the mesh holds of every module, at which commit; and the repository the
|
||||
modules live in. `mesh-catalog` and `catalog_modules` are identifiers and keep their spelling.
|
||||
*Not:* ~~catalog~~ (hq)
|
||||
- **delivery** — one commit in one repository on its way to the nodes, from its pull request's head
|
||||
being announced to one of its final states: one pull request, one status, one note
|
||||
([ADR 0239](../02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)).
|
||||
From its merge on it is delivered by the walk of the batch that answers it, and ends with that walk
|
||||
([ADR 0276](../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md)).
|
||||
Its states are one table (`proposed`, `checked`, `ready` or `rejected`, `published`, `delivering`,
|
||||
`held`, and the final six: `delivered`, `failed`, `superseded`, `stopped`, `retired` and `covered`;
|
||||
[ADR 0250](../02-DECISIONS/0250-a-merge-heard-only-from-the-buss-history-is-retired-and-owed-work-the-forge-cannot-do-ends.md),
|
||||
[ADR 0270](../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)). Not *change* (a diff). *Pipeline* is the predecessor's build-and-deploy
|
||||
mechanism, which the as-is designs describe; it is never a word for a delivery.
|
||||
*Not:* ~~deployment~~ (hq)
|
||||
- **covered** — the end of a delivery that a later merge containing it delivered: every module it moves was
|
||||
sent to every machine running it by the walk of a later merge into the same trunk, delivered on its own
|
||||
passing check, whose merge commit the forge says contains this one. Not *superseded*, which is a delivery
|
||||
whose walk a newer delivery took over before it ended; nor *delivered*, which its own walk or an empty plan
|
||||
ends in ([ADR 0270](../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)).
|
||||
- **delivery group** — two or more deliveries sharing a pull request head branch name across the mesh's
|
||||
repositories, delivered as one unit in an order declared (`after:`) or inferred by the planner. One
|
||||
level: a group holds deliveries, never groups. Its state is derived from its members, never set.
|
||||
- **delivery plan** — what a delivery does to the mesh: its **build plan** (the modules it moves and
|
||||
their dependents, in tiers), its **deploy plan** (per node, what it receives and what waits for a
|
||||
person) and its verdict (the composed nodes, the replays). Computed by the controller's planner from a
|
||||
diffset; the controller's verb `delivery-plan` shows it. *Deploy* is used in this one place.
|
||||
*Not:* ~~change plan~~, ~~release plan~~
|
||||
- **merge window** — the rolling wait in which the controller collects merges before it plans them: each
|
||||
merge restarts it, and it closes when none arrived for its length (90 seconds by default) or at the latest
|
||||
10 minutes after its first merge ([ADR 0276](../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md)).
|
||||
- **batch** — the merges one merge window collected, planned once as one walk. For a repository merged more
|
||||
than once in it, the walk takes the latest commit, and the earlier merges are answered by it. Not *group*,
|
||||
which is a delivery group.
|
||||
- **mesh-delivery** — the module that owns deliveries and delivery groups, holding the mesh-scoped seat
|
||||
of the same name. It records and decides; the controller sends, judges and rolls back when it is asked.
|
||||
- **walk** — the controller's sending of one batch's builds across nodes, at most one trunk commit per
|
||||
repository, tier by tier, one node first and judged at the first-node gate, then the rest (ADR 0236,
|
||||
[ADR 0276](../02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md)). One walk is open at a time; it names every merge it answers. A primitive the delivering stage
|
||||
asks for, not an object anyone manages. A **tier** is one step of a walk: a module, then the modules
|
||||
built against it. The controller's verb `plans` lists the walks. Walking is not a noun of its own:
|
||||
a module *rolls out* by its `roll` policy.
|
||||
*Not:* ~~rollout~~ (hq)
|
||||
- **first-node gate** — the judgement on a delivery's first node: healthy three times, no witness put it
|
||||
back, no condition raised since the send (ADR 0236 §2). Not *the gate* bare (see [Homonyms](#homonyms)).
|
||||
- **core module** and **leaf module** — a core module is one on the controller's own path (the controller,
|
||||
the node-engine, the tool runner, the bus, mesh-delivery) or the mesh's resolver, whose judgement protects
|
||||
the mesh's ability to reach a node at all; a leaf module is every other module. Decided by
|
||||
[ADR 0282](../02-DECISIONS/0282-a-fix-to-a-leaf-module-runs-healthy-on-every-node-within-five-minutes-of-its-merge-and-a-core-module-within-ten.md).
|
||||
Not a *core component* (ADR 0236's controller, node-engine, tool runner and bus, each with a health probe):
|
||||
the core modules add mesh-delivery and the resolver.
|
||||
- **delivery time** — from a delivery's merge to every node of its deploy plan that is heard from running its
|
||||
modules' builds and reporting them healthy. Its **delivery budget** is what it is held to: five minutes for
|
||||
a leaf module, ten for a core module (ADR 0282). Not the walk's time, which ends at the send.
|
||||
- **release** — a person's word that a held delivery goes on; the verb `mesh-delivery.release`. Used in
|
||||
this sense only.
|
||||
- **rollback** — putting a node back on the build it ran before, by something other than the build being
|
||||
judged (a witness, ADR 0236).
|
||||
- **the lab**, **scenario**, **replay** — a real mesh raised to run a change against before it reaches
|
||||
nodes; what a lab run declares; and a recorded failure run again to prove a fix (ADR 0016, ADR 0237).
|
||||
|
||||
## Health and repair — what is wrong, and putting it right
|
||||
|
||||
**Purpose.** To notice what is wrong with the mesh, say it once, repair what may be repaired unattended,
|
||||
and record what was done by hand. **Recorded by** the controller's condition store.
|
||||
**Upstream of** Change and delivery (the first-node gate) and Operator and conversation (a condition
|
||||
becomes a message). **Decided by** ADR 0227, 0231, 0240, 0252, 0283.
|
||||
**Uses:** node, module, node-engine (Core).
|
||||
|
||||
- **health** — a module's or a core component's statement of how it is alive and ready, which the
|
||||
node-engine judges ([ADR 0240](../02-DECISIONS/0240-a-module-says-how-it-is-healthy-and-the-node-engine-judges-it.md)).
|
||||
- **relogin needed** — an account's health when the group database lists it in an account group and its
|
||||
running session, started before, does not have that group yet. Said by the node-engine as the module's
|
||||
resource of kind `account`; it clears at the first look after a new login (ADR 0252).
|
||||
- **waiting** — a resource's health when its module's tool check says it waits for the operator: a named own
|
||||
secret or setting of the module, not given yet, without which a part cannot work. Not a fault: the
|
||||
controller checks it and says it as the condition `needs-operator`, naming the act, and the first-node gate
|
||||
reads it as *waits for a person*
|
||||
([ADR 0283](../02-DECISIONS/0283-a-part-that-waits-for-the-operators-secret-or-setting-is-said-as-waiting-passes-the-first-node-gate-and-names-the-act.md)).
|
||||
- **probe** — one look at one thing's health, run by whoever owns the verdict; what a probe returns is a
|
||||
**finding**, before it becomes a condition.
|
||||
- **signal** and **watchdog** — something that must keep happening (a heartbeat, a report after a send),
|
||||
and the watcher that raises a condition when it stops (to-be 45 §3).
|
||||
- **condition** — one open fact about something the mesh owns that is wrong, with a key and a severity;
|
||||
raised and cleared only by observation
|
||||
([ADR 0231](../02-DECISIONS/0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md)).
|
||||
What a wrapped dashboard raises is its own; the mesh's notion is a condition.
|
||||
*Not:* ~~alert~~ (hq)
|
||||
- **self-check** — the controller's own examination of the mesh, run on demand and on a schedule
|
||||
(to-be 45 §4). Its verb is `doctor`, an identifier; prose says self-check.
|
||||
*Not:* ~~doctor~~ (hq)
|
||||
- **healer** — a registered response to one condition kind: its **repair** (the ordinary path again),
|
||||
its **budget**, its back-off and its brake. A healer may not withdraw, delete or recreate data.
|
||||
- **drill** — something broken on purpose to see the mesh raise and clear its condition; a drill is
|
||||
never counted as a repair.
|
||||
- **rehearsal** — a question the operator starts at the controller's terminal (`rehearse`) and answers on a
|
||||
channel, to see an answer travel the whole way: its approval performs nothing and is recorded as the
|
||||
operator's decision ([ADR 0259](../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md)).
|
||||
Not a drill: nothing is broken.
|
||||
- **hand-act** — a repair a person made by hand, recorded so the mesh knows it happened.
|
||||
- **witness** — what rolls a core component back when its new build does not become healthy: never the
|
||||
component itself (to-be 45 §8).
|
||||
|
||||
## Data — what the mesh keeps, and getting it back
|
||||
|
||||
**Purpose.** To know every item of data a module holds, how precious it is, and to keep it recoverable.
|
||||
**Recorded by** the manifests' `data` declarations and every node's `node-backup` seat.
|
||||
**Upstream of** Change and delivery and Health and repair. **Decided by** ADR 0232, 0233, 0235.
|
||||
**Uses:** module, node, provider and consumer (Provisioning), person.
|
||||
|
||||
- **data class** — how precious an item of data is, ranked by the operator: `irreplaceable`,
|
||||
`valuable`, `rebuildable`, `cache`; and `none` for a provision that keeps nothing of anybody's
|
||||
([ADR 0233](../02-DECISIONS/0233-a-module-declares-the-data-it-holds-and-the-mesh-protects-and-watches-it-from-that.md)).
|
||||
- **backup** — a copy kept elsewhere by the node's `node-backup` holder; **restore point** — one backup
|
||||
at one moment; **restore** — bringing it back beside the live data, never over it.
|
||||
- **stream snapshot** — the bus's own backup of each stream, taken by the module holding `mesh-broker`
|
||||
([ADR 0235](../02-DECISIONS/0235-the-bus-is-backed-up-by-its-own-snapshot-of-each-stream.md)).
|
||||
- **sticky binding** — a consumer's tie to the data a provider keeps for it, which moves only by a
|
||||
person ([ADR 0232](../02-DECISIONS/0232-a-binding-to-a-consumers-data-moves-only-by-a-person.md)).
|
||||
|
||||
## Connectivity — how nodes and modules reach one another
|
||||
|
||||
**Purpose.** To make every node and module reachable by name where it should be, and unreachable where
|
||||
it should not. **Recorded by** the controller. **Upstream of** Provisioning and Health and repair.
|
||||
**Decided by** ADR 0007, 0117, 0138, 0223, 0226, 0247.
|
||||
**Uses:** node, machine, assignment and endpoint.
|
||||
|
||||
- **private network** — the mesh's own encrypted network between its nodes, on which every node has an
|
||||
address and a mesh name ([ADR 0226](../02-DECISIONS/0226-the-private-network-is-assigned-by-its-own-name-and-the-proxy-names-its-public-issuer.md)).
|
||||
The network the machine sits on without it is the **underlay**.
|
||||
*Not:* ~~overlay~~ (hq)
|
||||
- **anchor** and **hub** — the roles a node plays for the private network: the anchor is reachable from
|
||||
outside and every node reaches it; a hub relays for nodes that cannot reach each other directly.
|
||||
- **resolver** — a seat holder that answers the mesh's names; a node lists only the mesh's resolvers
|
||||
(ADR 0223), or, where it holds one, its own resolver, which asks them. **uplink** — a node's
|
||||
connection to the outside network, a seat ([ADR 0117](../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md)). **hostname** — a node's own name,
|
||||
a seat.
|
||||
- **split DNS** — resolving names by domain on one machine: a VPN's domains through the VPN's servers
|
||||
over its link, every other name through the mesh's resolvers. The provision `split-dns`, provided by
|
||||
the holder of the node seat `node-resolver`, the machine's **own resolver**, which exists only where
|
||||
something requires it ([ADR 0247](../02-DECISIONS/0247-a-machine-with-a-vpn-client-routes-names-by-domain-through-a-resolver-of-its-own.md)).
|
||||
- **proxy** and **public name** — the module that answers a public name and forwards it to an
|
||||
endpoint on the private network.
|
||||
- **packet filter** — what the mesh enforces on a node about which packets pass, the
|
||||
`node-packet-filter` seat and its verb `rules`. A **found firewall** is a program the mesh found on a
|
||||
machine and keeps retired or in force; *firewall* says that, and a wrapped program's firewall is its own.
|
||||
- **intrusion prevention** and **ban** — refusing a source that misbehaved, and the refusal itself.
|
||||
- **reach** — how far an assignment's endpoint may be reached from: the node, the private network, or
|
||||
the outside (ADR 0138).
|
||||
|
||||
## Operator and conversation — the person the mesh works for
|
||||
|
||||
**Purpose.** To let the mesh and its operator talk: tell, ask, answer, and act only on an answer it can
|
||||
trust. **Recorded by** the router (asks, warrants and the operator's identities).
|
||||
**Downstream of** Health and repair, Change and delivery, and Identity and access. An authorising
|
||||
answer acts in whichever domain asked, through that domain's own verb; the conversation owns the
|
||||
asking, never the act. **Decided by** ADR 0152, 0175, 0234, 0259, 0272.
|
||||
**Uses:** operator, person, proof (Identity and access), release (Change and delivery).
|
||||
|
||||
- **mesh MCP server** — the endpoint on a node's loopback through which every agent and person on that
|
||||
node reaches the mesh: the MCP server named `mesh`, with its five tools `mesh_overview`,
|
||||
`mesh_machine`, `mesh_search`, `mesh_describe` and `mesh_call`. It is the tool runner's loopback mode,
|
||||
not a module of its own. "Console" suggested a terminal or a shell, and the module `mesh-console` it
|
||||
once named no longer exists; "the tool bridge" and "the brain" named the predecessor's program.
|
||||
*Not:* ~~console~~ (hq), ~~mesh-console~~, ~~tool bridge~~
|
||||
- **mesh-cli** — the module on every node that places the operator's command, and the command itself:
|
||||
`/usr/local/bin/mesh-cli <command line>` asks the controller to run one of its command lines. It asks the
|
||||
node-engine on its own machine, which says to the controller which account is asking; the controller runs the
|
||||
line as the **controller's terminal** or as an ordinary call
|
||||
([ADR 0272](../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md)).
|
||||
- **nox** — `mesh-cli`'s alias, in interactive zsh only. A script, `sudo` and a document meant to work everywhere
|
||||
say `mesh-cli`. The operator also calls the mesh itself *nox mesh*; on this page and in the records *nox* is the
|
||||
alias alone. It is the one command the operator types: every line the mesh addresses to the operator — a
|
||||
condition's words, a verb's answer, a README, a design — names a `nox` line, never a daemon's (the controller's
|
||||
or the node-engine's binary followed by a command), which `words.py` and the catalogue's check refuse
|
||||
([ADR 0272](../02-DECISIONS/0272-the-operator-reaches-the-controller-with-mesh-cli-on-every-node-and-a-node-is-the-terminal-only-where-no-agent-runs-as-its-operator.md),
|
||||
its insight of 2026-10-09).
|
||||
- **channel / intake** — the two kinded benches the mesh talks to its operator through: `channel`
|
||||
sends, `intake` turns what arrives into one envelope. A holder of either declares **capabilities** from
|
||||
the fixed vocabulary `channel-capabilities/1`. Not *notifier* (that is the desktop's node seat, one
|
||||
holder of kind `desktop`) and not *bot* (that is one service's account).
|
||||
- **router** — the module that holds the `operator-channel` seat, orders the channels and checks every
|
||||
sender ([ADR 0234](../02-DECISIONS/0234-the-mesh-holds-a-conversation-with-its-operator.md)).
|
||||
- **headline / verdict / explanation / resolved line** — what a condition says to the operator in plain
|
||||
words: a few words naming the thing and what is wrong; "Nothing for you to do." or "Needs you:" and the
|
||||
one thing the operator can do themselves (its **needs**); one or two sentences on what happened and what
|
||||
it means; the line said when it clears. Its **actions** are the answers a notification offers. Carried by
|
||||
the condition beside its **summary**, which keeps the plans, commits and verbs for whoever looks closer
|
||||
([ADR 0253](../02-DECISIONS/0253-a-condition-says-itself-to-the-operator-in-plain-words-beside-its-summary.md)).
|
||||
The router may override "Nothing for you to do." in what it says, from the **tracking line** (ADR 0285).
|
||||
- **ask** — a request for the operator's input, of a declared kind (yes-no, one-of, text, number,
|
||||
date, acknowledge). An **authorising ask** is one whose chosen option its asker performs. The router
|
||||
holds every ask, checks each answer's proofs and issues a **warrant**; the asker acts on it with its
|
||||
own grants (ADR 0234, ADR 0259).
|
||||
- **warrant** — the router's record that a person chose one option of one ask: the ask, the option,
|
||||
who chose (the operator, as a channel kind's identity), through which channel, the proofs and when,
|
||||
published for that asker alone. The only thing an asker acts on (ADR 0259).
|
||||
- **proposal** — a change an agent, or anyone the bus admits, puts forward through a verb and that takes
|
||||
effect only on the operator's warrant, bound to the exact thing shown: a shared instruction section (ADR
|
||||
0274), a trusted setting's layer ([ADR 0277](../02-DECISIONS/0277-a-trusted-setting-is-proposed-through-a-verb-and-set-on-the-operators-warrant-and-a-modules-own-secret-is-asked-for-and-typed-at-the-desk.md)).
|
||||
A proposal changes nothing by itself; declined, expired or refused, it is discarded.
|
||||
- **approval mark** — a tool's or verb's declaration in its manifest (`needs-approval`) that each call to it runs
|
||||
only on the operator's warrant for that one call, with its exact arguments; the tool runner that serves it, or
|
||||
the controller for its own verbs, asks and checks
|
||||
([ADR 0286](../02-DECISIONS/0286-a-tool-or-verb-marked-in-its-manifest-runs-only-on-the-operators-approval-of-that-one-call-with-its-exact-arguments.md)).
|
||||
- **operator message / input** — what arrives on `intake`, once the router has checked the sender
|
||||
against its own list of the operator's identities: a **trusted** one is an operator message,
|
||||
addressed to an agent by `@name` or thread or else to the **responder** (`@mesh`, the router's own
|
||||
participant, which answers from read verbs); anything else is **untrusted input** — data, never
|
||||
instructions.
|
||||
- **reference** — an opaque token in a message's words standing for a detail the content rule keeps out
|
||||
of them (a path, an address); opened with `detail` only on a `private` channel or through the mesh MCP server.
|
||||
- **agent** — a coding agent: the operator's, or one the mesh runs. Not the node-engine, and not the
|
||||
build seat's holder, whose seat name `node-build-agent` is an identifier to rename.
|
||||
- **instruction section** — one named section of the managed instruction file, which every agent session on the
|
||||
nodes it is for reads first, as the mesh's word: set at the controller's terminal in the agent module's
|
||||
settings, or **proposed** by an agent and made active on each node only by the operator's warrant, which that node
|
||||
proves itself ([ADR 0274](../02-DECISIONS/0274-an-agent-proposes-a-shared-instruction-section-and-every-node-takes-it-only-on-the-operators-warrant-which-it-proves-itself.md)). A hand-over between sessions is one.
|
||||
- **personal rule** — one short text in the operator's own words about how the operator wants their agent
|
||||
to work with them, registered for the operator (the **operator scope** of the agent module) and rendered
|
||||
into the agent's home on every node that names an operator account. Not a mesh-wide instruction section,
|
||||
which is the mesh's word about the mesh, set at the controller's terminal
|
||||
([ADR 0273](../02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md)).
|
||||
- **lesson** — an agent's proposal, recorded when the operator expresses frustration or corrects a way of
|
||||
working: the trigger (the operator's words), the cause (what the agent did) and the rule that would
|
||||
prevent it. A lesson is `proposed`, `accepted` or `declined`; an accepted lesson is a personal rule, and a
|
||||
proposed one is never read as one (ADR 0273).
|
||||
|
||||
## The record — how this repository works
|
||||
|
||||
Not a domain of the mesh but of the way it is built, listed because its words meet the mesh's.
|
||||
**Recorded in** this repository, the issue tracker and the knowledge base. **Decided by** ADR 0019, 0080, 0244, 0275,
|
||||
0284, 0290, 0291.
|
||||
|
||||
- **research effort**, **graduation**, **hand-off**, **playbook** — an investigation in `01-RESEARCH/`;
|
||||
its closing into a decision and a design; a design given to a code repository; a documented workflow
|
||||
in `00-META/process/`.
|
||||
- **decision record** — one numbered file in `02-DECISIONS/`, never rewritten: **superseded** by a later
|
||||
record, or corrected by a marked **progressive insight**. Always qualified in this repository: bare
|
||||
*record* has other meanings (see [Homonyms](#homonyms)).
|
||||
- **clause** — one numbered statement a decision record makes, with its own status (`proposed`, `in force`,
|
||||
`changed`, `superseded`): a later record may change one clause and leave the rest
|
||||
([ADR 0290](../02-DECISIONS/0290-a-decision-record-lives-whole-in-the-knowledge-base-made-of-clauses.md)). Not a *rule*, which is a line of
|
||||
`00-META/how-we-build.md`.
|
||||
- **knowledge base** — the role that holds the decision records and research efforts whole (number, status, topic,
|
||||
typed links, clauses and reasoning, with every version), takes them as proposals, has them reviewed and accepted,
|
||||
and answers what holds: the seat `knowledge-base`, held by the module `mesh-knowledge`
|
||||
([55 — The knowledge base](../03-DESIGN/01-to-be/55-the-knowledge-base.md)). Not **the record**, the literal reader
|
||||
of this repository.
|
||||
- **design** — a document in `03-DESIGN/`: **as-is** (what runs) or **to-be** (what is being built).
|
||||
- **issue** — a question about the mesh that someone must answer, numbered and tracked by the issue tracker, which
|
||||
holds it whole: its report, diagnosis and resolution included ([ADR 0291](../02-DECISIONS/0291-an-issue-lives-whole-in-the-issue-tracker.md));
|
||||
the folders in `04-ISSUES/` are imported there and removed.
|
||||
An **incident** is a past event that taught a rule.
|
||||
- **issue tracker** — the role that holds the mesh's issues and hands out their numbers: the seat `issue-tracker`,
|
||||
held by the module `mesh-issues` ([53 — The issue tracker](../03-DESIGN/01-to-be/53-the-issue-tracker.md)).
|
||||
Not hits's *item*; bare *ticket* is a homonym (see [Homonyms](#homonyms)).
|
||||
*Not:* ~~ticketing system~~ (hq), ~~ticket system~~ (hq)
|
||||
- **duplicate check** — the issue tracker's judgement, when an issue is opened, of whether it asks a question an
|
||||
older issue not resolved already asks; a certain duplicate is closed `wontfix` with its report kept on the older
|
||||
issue ([ADR 0278](../02-DECISIONS/0278-the-issue-tracker-checks-every-new-issue-for-a-duplicate-through-a-judgement-the-agent-makes-without-tools.md)).
|
||||
Its pass over the backlog is the verb `groom`.
|
||||
- **triage** — the issue tracker's judgement of an opened issue, made by the agent with no tools: whether it is a
|
||||
duplicate (the **duplicate check**), its **severity** and what it **needs**, a suggested owner, and the issues it
|
||||
depends on or relates to; it fills only what nobody gave ([ADR 0279](../02-DECISIONS/0279-the-issue-tracker-triages-every-new-issue-and-an-issue-is-ready-when-what-it-depends-on-is-delivered.md)).
|
||||
- **ready** — of an issue: every issue it depends on is wontfix, or resolved with its fix delivered.
|
||||
- **worker** — of an issue: the agent or person, with its node (who@node), that moved it to `diagnosing` or `fixing`
|
||||
and works on it; another is refused while the worker is current, unless it gives a reason to **take it over**. A
|
||||
worker with nothing done on the issue for a day is **stale**. **Fixing** is the status an issue is at while its fix
|
||||
is worked on; an issue is resolved only from it ([ADR 0284](../02-DECISIONS/0284-an-agent-moves-an-issue-to-fixing-under-its-own-name-and-an-issue-is-resolved-only-from-fixing.md)).
|
||||
Not a *claim* or a *holder*, which are a module's on a seat.
|
||||
- **tracks** — of an issue and a condition: the issue links the condition's key with a link of kind `condition`. The
|
||||
**tracking line** is the line the router says in every message about a condition: the issues that track it and
|
||||
where each stands, or that none does ([ADR 0285](../02-DECISIONS/0285-an-issue-tracks-a-condition-by-its-key-and-every-message-about-the-condition-says-the-issue-and-where-it-stands.md)).
|
||||
- **hq check** — one of `00-META/checks/`, run by `merge-check.sh` as this repository's check.
|
||||
|
||||
## Homonyms
|
||||
|
||||
A word below means different things in different domains. In a governing document it is never bare;
|
||||
it is always the qualified form. **Checked by review, not by `words.py`:** a word list cannot tell one
|
||||
sense from another, so the reviewer looks for the bare word in the diff. Where a homonym is settled by
|
||||
renaming one sense, the old sense moves to a *Not:* line and becomes mechanical.
|
||||
|
||||
| Word | Sense | Say | Domain |
|
||||
|---|---|---|---|
|
||||
| plan | what the controller would send one node | that node's **declaration** (verb `plan`) | Placement |
|
||||
| | what a delivery would do to the mesh | **delivery plan** | Change and delivery |
|
||||
| | the sending of one batch of commits across nodes | **walk** (verb `plans`) | Change and delivery |
|
||||
| | a step a person starts, like the bus's | **planned step** | Core |
|
||||
| membership | the subjects a bus account is issued | **membership** | Provisioning |
|
||||
| | an account in a group of the machine | **account group** | Placement |
|
||||
| push | the controller giving nodes their declarations | **send** (verb `push`) | Placement |
|
||||
| | a git push | **git push** | The record |
|
||||
| | a phone notification | **push notification** | Operator and conversation |
|
||||
| release | a person letting a held delivery go on | **release** | Change and delivery |
|
||||
| | taking a consumer off a licence | **unbind** (verb `release`, to rename) | Identity and access |
|
||||
| gate | the judgement on a delivery's first node | **first-node gate** | Change and delivery |
|
||||
| | the pull request status | **merge gate** | Change and delivery |
|
||||
| | a failed step stopping its module's later steps (ADR 0136) | *a failed step holds its module* | Placement |
|
||||
| tier | a level of the mesh | **layer** | Core |
|
||||
| | a step of a walk | **tier** | Change and delivery |
|
||||
| | how much proof an ask needs | **assurance level** | Identity and access |
|
||||
| ask | a request for the operator's input | **ask** | Operator and conversation |
|
||||
| | an entry in the build queue | **build request** | Change and delivery |
|
||||
| store | the one database server | **store** | Core |
|
||||
| | the OCI registry | **artifact store** | Change and delivery |
|
||||
| | a node's own copy of its last declaration | **last declaration** | Placement |
|
||||
| record | a numbered decision | **decision record** | The record |
|
||||
| | the literal reader of this repository, searched first for a symptom | **the record** (the `records` module) | Operator and conversation |
|
||||
| | the upgrade policy that sends nowhere | `record` | Module |
|
||||
| knowledge base | the module holding the decision records and research efforts whole | **knowledge base** | The record |
|
||||
| | the predecessor's operational memory, in older playbooks (issue 430) | none: it no longer exists | — |
|
||||
| check | a pull request's two statuses | **merge check**, **merge gate**, **repository check** | Change and delivery |
|
||||
| | a delivery group's verdict | **composed check** | Change and delivery |
|
||||
| | the controller's own examination | **self-check** | Health and repair |
|
||||
| | a file in `00-META/checks/` | **hq check** | The record |
|
||||
| agent | a coding agent | **agent** | Operator and conversation |
|
||||
| | the build seat's holder | **builder** | Change and delivery |
|
||||
| the user | an account of a wrapped program, or of the bus server | **its account**, **bus account** | Identity and access |
|
||||
| | a human | **operator** or **person** | words every domain uses |
|
||||
| firewall | what the mesh enforces | **packet filter** | Connectivity |
|
||||
| | what was found on a machine | **found firewall** | Connectivity |
|
||||
| ticket | an answer's one-time token (ADR 0258) | **answer ticket** | Operator and conversation |
|
||||
| | a question someone must answer | **issue** | The record |
|
||||
| deploy | the per-node part of a delivery plan | **deploy plan** | Change and delivery |
|
||||
| | a wrapped program's own deploy | its own word | — |
|
||||
|
||||
## How this page is kept
|
||||
|
||||
A new word for an existing thing lands here first, in the domain that owns it, in the same change that
|
||||
introduces it in code. A word moves to another domain only with a decision record. A retired word goes
|
||||
on a *Not:* line and nowhere else on this page, and `words.py` then fails on it in running prose. A
|
||||
record under `02-DECISIONS/` keeps whatever word it was written with — those are immutable — so a word
|
||||
retired here may still appear there, and the *Not:* lines are how to read it.
|
||||
|
||||
**How it is checked** ([ADR 0244](../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)):
|
||||
`python3 00-META/checks/words.py`, run by `merge-check.sh` on every pull request, fails on a retired
|
||||
word or a bare identifier in running prose in `00-META/`, `03-DESIGN/`, `AGENTS.md`, `README.md`, and
|
||||
research and issues dated from 2026-10-07 — code spans, quotations and link targets excepted — and on a
|
||||
head word that heads two entries or is also retired. The words retired with no scope or with *(tools)*
|
||||
are copied into the catalogue as `retired-words`, whose own repository check holds the descriptions of
|
||||
the mesh's tools to them; a change here that retires such a word changes that copy in the same delivery.
|
||||
Homonyms are checked by review.
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
---
|
||||
status: graduated
|
||||
initiated: 2026-10-10
|
||||
touches:
|
||||
- 02-DECISIONS/0019-how-this-repository-works.md
|
||||
- 02-DECISIONS/0080-the-development-cycle-is-checked.md
|
||||
- 00-META/process/03-issues.md
|
||||
- 00-META/glossary.md
|
||||
- the runtime's state verbs and the SDK's state interface
|
||||
became:
|
||||
- 03-DESIGN/01-to-be/53-the-issue-tracker.md
|
||||
- 02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md
|
||||
---
|
||||
|
||||
# 042 — The mesh keeps its own issues
|
||||
|
||||
## What
|
||||
|
||||
Whether the mesh should keep its issues in a module of its own, on the bus, instead of only as
|
||||
numbered folders in this repository's `04-ISSUES/`, and what the smallest useful first version of
|
||||
that module is. The operator's ask of 2026-10-10: work on a ticketing system on the bus, consolidate
|
||||
every current issue into it, start small and extend the module along the way.
|
||||
|
||||
The effort answers six questions, in [01-findings.md](01-findings.md):
|
||||
|
||||
1. The smallest useful first slice: what the module holds, which tools it serves, which events it
|
||||
emits, and what it explicitly leaves out.
|
||||
2. Where the truth lives during and after the move: the folders here, the module, or a split
|
||||
between them, and what each choice does to the public-repository rule, playbook 03 and the hq
|
||||
checks.
|
||||
3. How the 357 issue folders are imported and consolidated (duplicates, still-open ones, grouping).
|
||||
4. How the controller's conditions, the router's asks and deliveries relate to issues.
|
||||
5. How the operator sees issues: through the mesh's tools first, through a view onto the bus later.
|
||||
6. The open questions for the operator, and the slices after the first.
|
||||
|
||||
The operator also named an existing project as inspiration: *hits*, a headless issue tracker built
|
||||
as NATS micro services on JetStream, with an MCP server for agents. Its model is studied in the
|
||||
findings, along with whether to reuse its code or build on its model.
|
||||
|
||||
## Why
|
||||
|
||||
Numbers collide. Playbook 03 tells an author to take the next free number across `main` and every
|
||||
open pull request, and [issue 155](../../04-ISSUES/155-two-records-may-share-a-number-and-nothing-says-so/00-report.md)
|
||||
records the second collision reaching `main` with every check passing. `cycle.py` now refuses two
|
||||
records sharing a number, which catches a collision after it happens and does not prevent one. On
|
||||
`main` at the time of writing, 31 commits mention a renumbering. On 2026-10-07 three decision numbers
|
||||
collided in one evening. On 2026-10-10 an open pull request still carries a branch named for issue
|
||||
349 and a title naming issue 350, because 349 was taken on `main` first. The operator's answer on
|
||||
2026-10-07 was that numbering belongs to the mesh's own issue tracker.
|
||||
|
||||
Status is also hard to see. Of 357 issue folders on `main`, 73 are not closed (27 `open`, 45
|
||||
`located`, 1 `diagnosing`). Finding them means reading frontmatter across the repository.
|
||||
Nothing connects a condition the controller raises to the issue that would fix it: on 2026-10-10 a
|
||||
condition was silenced with the words "hq issue to follow", and nothing records whether one followed.
|
||||
|
||||
## What it touches
|
||||
|
||||
- [ADR 0019](../../02-DECISIONS/0019-how-this-repository-works.md) (statuses live in frontmatter) and
|
||||
[ADR 0080](../../02-DECISIONS/0080-the-development-cycle-is-checked.md) (the cycle is checked), with
|
||||
playbook [03](../../00-META/process/03-issues.md) and `00-META/checks/cycle.py`.
|
||||
- [ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
|
||||
(a module keeps its state in key-value buckets it declares),
|
||||
[ADR 0197](../../02-DECISIONS/0197-every-tool-announces-itself-on-the-bus-in-the-nats-services-protocol.md)
|
||||
(tools announce themselves on the bus) and
|
||||
[ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) (events are declared).
|
||||
- The conditions of [ADR 0231](../../02-DECISIONS/0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md)
|
||||
and the asks of [ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md).
|
||||
- The records module, which indexes this repository for agents.
|
||||
|
||||
**Graduated 2026-10-10** into [ADR 0275](../../02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)
|
||||
and [to-be 53](../../03-DESIGN/01-to-be/53-the-issue-tracker.md). The operator delegated the five open questions; each
|
||||
was decided as recommended. `touches:` names what the decision changes.
|
||||
|
||||
## Words
|
||||
|
||||
The glossary's word for one of these is **issue** (*The record*: "a report in `04-ISSUES/`"). This
|
||||
effort keeps that word for the thing the module holds, and proposes **issue tracker** for the module
|
||||
that holds them, with the module named `issues`. It does not use *ticket*, the operator's word in
|
||||
the ask, or *item*, the word hits uses. If the glossary entry is widened (an issue is then held by
|
||||
the issue tracker, and its narrative may live in `04-ISSUES/`), *ticket* would go on its *Not:* line.
|
||||
That change belongs to the decision this effort graduates into, not to the effort itself.
|
||||
|
||||
## Open questions for the operator
|
||||
|
||||
Listed in [01-findings.md § 7](01-findings.md#7-open-questions-for-the-operator), each with a
|
||||
recommendation.
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
# 01 — Findings
|
||||
|
||||
Measured on 2026-10-10 against `main` of this repository, the open pull requests on the forge, the
|
||||
controller's open conditions, the module SDK's source, and a clone of the hits repository at its
|
||||
release v0.7.0. Each section ends with what this effort recommends; section 7 lists what needs the
|
||||
operator.
|
||||
|
||||
## 0. What exists today, measured
|
||||
|
||||
| Fact | Measured |
|
||||
|---|---|
|
||||
| Issue folders on `main` | 357, numbered up to 362; numbers 283 and 284 were never used, and 338, 341 and 350 sit on open pull requests (363 too) |
|
||||
| Status in frontmatter | 283 `resolved`, 1 `wontfix`, 45 `located`, 27 `open`, 1 `diagnosing` |
|
||||
| Frontmatter keys | `status`, `opened`, `located-in` on all 357; `fixed-by` 356; `amended-design` 344; `replay` 22; `replay-none` 44 |
|
||||
| Size | 1.9 MiB of markdown in all; median folder 4.6 KiB, largest 25 KiB; 244 folders hold only a report, 111 also a diagnosis |
|
||||
| Links between issues | 168 folders link another issue's folder (265 links); 193 name another issue in prose |
|
||||
| `located-in` | free text: the controller is written `mesh-controller`, `cmd/mesh-controller`, `mesh-control` and as file paths; 17 issues name nothing |
|
||||
| Renumberings | 31 commits on `main` mention one; one open pull request still carries the old number in its branch name |
|
||||
| The forge's own issues | none in this repository or the controller's; [issue 178](../../04-ISSUES/178-a-routed-name-resolves-to-a-provider-merely-told-it/00-report.md) was filed there once and moved here, because "its numbers collide with hq's pull request numbers in conversation" |
|
||||
| Conditions | 10 open; 5 of kind `healer-wanted` (a hand repair repeated, "an automatic repair is wanted"); one silenced with "hq issue to follow" |
|
||||
|
||||
Two things follow. The collisions come from allocating a number by reading a repository that many
|
||||
branches write at once; nothing short of one allocator removes them. And the frontmatter is a small,
|
||||
regular data set (six keys, 1.9 MiB) that fits easily in the bus's key-value store under the limits
|
||||
[ADR 0201](../../02-DECISIONS/0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
|
||||
§6 sets (256 KiB a value, 64 MiB a bucket).
|
||||
|
||||
## 1. The first slice
|
||||
|
||||
**A Go module `issues`, one instance on the control-node, serving one seat held once for the mesh.**
|
||||
Its state is in buckets it declares (ADR 0201), its tools announce themselves on the bus
|
||||
([ADR 0197](../../02-DECISIONS/0197-every-tool-announces-itself-on-the-bus-in-the-nats-services-protocol.md)),
|
||||
and every change is an event it declares under `emits`
|
||||
([ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md)). The module that delivers pull
|
||||
requests already has this shape: Go, one mesh seat, two state buckets, declared events.
|
||||
|
||||
**What one issue holds in v1**, the frontmatter of today plus what the folders carry in prose:
|
||||
|
||||
- `number` (the key), `title`, `status` (`open`, `diagnosing`, `located`, `resolved`, `wontfix`),
|
||||
`opened`, `closed`;
|
||||
- `located-in`, from a registered list of repositories and modules rather than free text;
|
||||
- `fixed-by` (pull requests or commits), `amended-design`, `replay` or `replay-none`;
|
||||
- `notes`, appended and never edited: who wrote, when, and the text;
|
||||
- `links`, typed: to another issue (`duplicates`, `relates-to`), to a pull request, a decision
|
||||
record, a design, a condition key or a delivery;
|
||||
- `record`: the path of the issue's folder in this repository, when it has one (section 2).
|
||||
|
||||
**Storage.** One bucket `issues` with one key per issue, keeping a short history per key, and a
|
||||
`counter` key for the next number. The events (`opened`, `changed`, `noted`, `linked`) carry what
|
||||
changed and who changed it, so they are the audit trail, and a view can watch the bucket. Section 6
|
||||
compares this with a stream as the source of record.
|
||||
|
||||
**Tools** (verbs of the seat, found with `mesh_search`):
|
||||
|
||||
| Tool | Does |
|
||||
|---|---|
|
||||
| `open` | takes the next number atomically and writes the issue; refuses a title or report that is empty |
|
||||
| `get` | one issue, whole |
|
||||
| `list` | by status, by `located-in`, by a phrase in title or notes; compact rows, newest first |
|
||||
| `move` | changes status, enforcing today's cycle rules at the write: `located` needs `located-in`; `resolved` needs `fixed-by`; a core issue needs `replay` or `replay-none`; `wontfix` needs a reason |
|
||||
| `note` | appends to the trail |
|
||||
| `link` / `unlink` | adds or removes a typed link |
|
||||
| `import` | reads this repository's issue folders and writes them as issues with their own numbers (section 3); idempotent, reporting what it would change before it does |
|
||||
|
||||
**The caller is the bus credential, not an argument.** The module records who acted from the
|
||||
identity the runtime gives it with each call, so an agent cannot write as someone else.
|
||||
|
||||
**Atomic numbering needs compare-and-set.** The SDK's state interface (v0.1.9) offers `get`, `put`,
|
||||
`delete`, `keys` and `watch`, and no write that is refused when the key changed since it was read.
|
||||
Whether the runtime's `mesh/state.put` takes an expected revision was not checked; the tool runner's
|
||||
code was not read for this effort. Two ways out: add `create` and an `update` that takes the revision
|
||||
read to the state interface (small, and every module with a counter gains it), or run exactly one
|
||||
instance and number inside it. **Recommended: the first.** One instance is also right for v1, but the
|
||||
counter should not depend on it.
|
||||
|
||||
> **Correction — 2026-10-10.** The finding above is wrong about the SDK. Version v0.1.9, the one read, had no
|
||||
> compare-and-set, but the SDK's Go state interface already had it from go/v0.1.10: `Create` (writes a key only if it
|
||||
> does not exist) and `Update` (writes only at the revision the caller read), with the runtime's matching state
|
||||
> verbs, both from [ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md).
|
||||
> What the tracker's counter needed and slice 1 added is `Increment`, in go/v0.1.12, which reads a counter and writes
|
||||
> the next number with `Create` or `Update` at the revision it read, retrying a bounded number of times. The
|
||||
> recommendation stands; the work it named was smaller than stated. ADR 0275 carries the same correction.
|
||||
|
||||
**Explicitly left out of v1:** decision and research numbers (slice 2), any change to playbook 03 or
|
||||
the hq checks (slice 2), duplicate finding beyond links (slice 3), opening an issue from a condition
|
||||
(slice 4), a web view (slice 5), full-text or semantic search, priorities, claims, notifications, and
|
||||
the forge's own issues.
|
||||
|
||||
## 2. Where the truth lives
|
||||
|
||||
| | (a) Split: the tracker holds the issue, hq holds the narrative | (b) Everything moves | (c) The tracker mirrors hq |
|
||||
|---|---|---|---|
|
||||
| Number | tracker | tracker | hq, so collisions stay |
|
||||
| Status, `located-in`, `fixed-by`, links | tracker | tracker | hq frontmatter, copied |
|
||||
| Report and diagnosis | hq folder for a design or governance question; notes in the tracker for the rest | tracker notes | hq |
|
||||
| Public-repository rule | holds: hq keeps only scrubbed narrative, the tracker is private and may name nodes | the public record of design questions is lost | holds |
|
||||
| Playbook 03 | changes: open in the tracker first, write a folder only for a design or governance question | replaced | unchanged |
|
||||
| `cycle.py` | its issue rules move into the tracker's `move`, refused at the write instead of at merge | same | unchanged, and a second copy to keep in step |
|
||||
| Records module | still finds the narrative; the tracker's `list` and `get` serve status | finds no issues | unchanged |
|
||||
|
||||
Option (c) keeps the collisions and adds a second copy that can drift, so it answers neither of the
|
||||
operator's problems. Option (b) loses the public record that hq exists to be
|
||||
([ADR 0019](../../02-DECISIONS/0019-how-this-repository-works.md)), and with it the
|
||||
symptom-to-component memory that playbook 03 says is never deleted.
|
||||
|
||||
**Recommended: (a).** Every issue gets a number from the tracker, including ones that never need a
|
||||
document here (a node's quirk, an operational follow-up, which playbook 03's table already sends
|
||||
elsewhere). A design or governance issue also gets a folder `04-ISSUES/NNN-slug/` with the tracker's
|
||||
number, whose frontmatter shrinks to a pointer (`issue: NNN`) and whose prose stays reviewed in pull
|
||||
requests. Status lives in one place, the tracker. This reverses ADR 0019's "statuses live only in
|
||||
frontmatter" for issues and moves part of ADR 0080's check, so it needs a decision record that
|
||||
supersedes those parts, written when this effort graduates.
|
||||
|
||||
**Numbering continues.** The tracker's counter starts above the highest number on `main` and on open
|
||||
branches at the moment of the switch (today 363, so the first new issue is 364). Numbers 283 and 284
|
||||
stay unused, and a number never moves again.
|
||||
|
||||
**During the move**, hq frontmatter stays authoritative until the import has run and been checked
|
||||
against it, and one pull request then removes the status keys from every report and changes playbook
|
||||
03 and `cycle.py` together. There is no period in which both are written.
|
||||
|
||||
## 3. Importing and consolidating the 357 issues
|
||||
|
||||
**Import.** `import` reads the folders through the records module (`records_list`, `records_read`),
|
||||
over the bus, rather than from a checkout. Each folder becomes an issue with its own number, its
|
||||
frontmatter as fields, its title from the heading, the commit it was read at, and its path as
|
||||
`record`. Folder-to-folder links (265) become `relates-to` links. The four open pull requests'
|
||||
issues (338, 341, 350, 363) are reserved as numbers and imported when they merge.
|
||||
|
||||
**Consolidating** means four concrete passes, each producing a list for the operator or an agent to
|
||||
confirm, never applying itself:
|
||||
|
||||
1. **Close what is fixed.** A `located` or `open` issue whose `located-in` repository has a merged
|
||||
pull request naming its number is a candidate for `resolved`. hits's auditor (section 6) checks
|
||||
exactly this against git and is the model.
|
||||
2. **Merge duplicates.** Candidates are pairs that link each other and share a `located-in`, and
|
||||
pairs whose titles share most words. A confirmed duplicate is closed `wontfix` with a
|
||||
`duplicates` link and the reason, keeping its number and folder.
|
||||
3. **Normalise `located-in`** onto the registered list (`mesh-controller`, `mesh-catalog`, the
|
||||
node-engine's repository, a module's name), keeping the file paths as a note.
|
||||
4. **Group the 73 not closed** by domain (the glossary's domains, ADR 0244) and by `located-in`, so
|
||||
the operator sees, for example, how many open issues the delivery domain carries.
|
||||
|
||||
## 4. Conditions, asks and deliveries
|
||||
|
||||
A condition is one open fact, raised and cleared only by observation (ADR 0231); an issue is a
|
||||
question someone must answer. They are different things, and a condition must not be closed by
|
||||
closing an issue. But a condition can say that a fix is owed: five of today's ten are `healer-wanted`,
|
||||
and one silence says "hq issue to follow".
|
||||
|
||||
Recommended for a later slice (slice 4): an issue links a condition by its key; the controller shows
|
||||
the link on the condition; and for `healer-wanted` the controller opens one issue per condition key
|
||||
when it first raises it, linked both ways, instead of an agent remembering to. A silence whose reason
|
||||
names an owed issue should carry its number. Asks
|
||||
([ADR 0259](../../02-DECISIONS/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md))
|
||||
stay with the router: the tracker asks nothing in v1. Deliveries relate through pull requests: when
|
||||
the forge says a pull request merged, the tracker notes it on every issue the pull request names, and
|
||||
offers `resolved` (slice 4). Nothing in v1 depends on these.
|
||||
|
||||
## 5. How the operator sees issues
|
||||
|
||||
First through the mesh's tools: `list` returns rows short enough to read in a chat, `get` one issue
|
||||
whole, and every agent session reaches them through the mesh MCP server. The responder (`@mesh`,
|
||||
ADR 0234) answers from read verbs, so "what is open in delivery" works on a channel once the seat
|
||||
exists. Later a view onto the bus: the `issues` bucket watched over the bus (a browser reaches it through a WebSocket listener, which this effort did not check for),
|
||||
read-only, which the operator named as a future direction for every module's state. Nothing in v1
|
||||
blocks it, because the state is already a bucket and every change an event.
|
||||
|
||||
## 6. hits: what it is, and what to take from it
|
||||
|
||||
**What it is.** A headless issue tracker in Go (module `github.com/impire-io/hits`), with no web
|
||||
interface and no HTTP interface: NATS micro services on JetStream, a command line for people and a
|
||||
stdio MCP server for agents. One author, 81 commits between 2026-09-02 and 2026-09-14, release
|
||||
v0.7.0, about 9,700 lines of Go outside tests and 21 test files, tested against an embedded NATS
|
||||
server. Its model descends from a method like this repository's: a sibling `hits-hq` with numbered
|
||||
playbooks and decisions.
|
||||
|
||||
**Data model.** An *item* has a type (`bug`, `task`, `improvement`), a status (`open`, `diagnosing`,
|
||||
`located`, `blocked`, `resolved`, `wontfix`), a priority, `located-in` (from a registered list of
|
||||
*projects*), `fixed-by` (pull request, commit or action), `amended-design`, append-only notes, typed
|
||||
links (`duplicates`, `relates-to`), a claim, a block, and a tombstone for a filing mistake. Its
|
||||
statuses and fields are this repository's frontmatter almost key for key. Projects, *initiatives*
|
||||
(groups of projects that also scope the numbering) and *releases* are registered lists that are
|
||||
never reused once retired.
|
||||
|
||||
**How it stores.** The source of record is a stream `hits-ops` with subjects
|
||||
`hits.ops.item.<id>` (and `project`, `initiative`, `release`). Every write appends an operation,
|
||||
published with the per-subject expected last sequence, so two writers to one item cannot both win.
|
||||
A bucket `hits-state` holds the folded snapshot of each item, the registered lists and one counter
|
||||
per initiative. The counter is advanced by compare-and-set and an item is numbered only after it is
|
||||
validated, so a refused create spends no number. On start it rebuilds the bucket from the stream.
|
||||
|
||||
**Services and subjects.** One micro service `hits` with 23 endpoints under `hits.api.*` (`create`,
|
||||
`get`, `edit`, `transition`, `claim`, `release`, `block`, `unblock`, `link`, `unlink`, `note`,
|
||||
`tombstone`, and register, list and retire for projects, initiatives and releases); three index
|
||||
services, each a projection of the stream: full text (`hits.search.query`, a local index on disk),
|
||||
a graph (`hits.graph.neighbors`, `hits.graph.walk`) and an optional semantic search that needs an
|
||||
outside embedding provider.
|
||||
|
||||
**MCP tools.** 27, one per endpoint: `create_item`, `get_item`, `edit_item`, `transition_item`,
|
||||
`claim_item`, `release_item`, `block_item`, `unblock_item`, `link_items`, `unlink_items`,
|
||||
`note_item`, `tombstone_item`, the project, initiative and release tools, and `search_items`,
|
||||
`semantic_search`, `graph_neighbors`, `graph_walk`. One server serves one agent under one actor.
|
||||
Its command line also has an auditor that checks every item's `fixed-by` against git and finds
|
||||
merged pull requests whose items are still open.
|
||||
|
||||
| | Adopt as it is | Adapt to the mesh |
|
||||
|---|---|---|
|
||||
| Statuses, fields, append-only notes, typed links, tombstone | yes: they are already ours | — |
|
||||
| Stream of operations as the record, bucket as its projection | the pattern | the stream and bucket declared in the manifest and created by the controller (ADR 0201 §3), not created by the service on start |
|
||||
| Expected-sequence writes; compare-and-set counter; number only after validation | yes | needs compare-and-set in the state interface (section 1) |
|
||||
| The auditor | the idea, as a tool | reads the forge through the gitea module, not a local clone |
|
||||
| Subjects `hits.api.*`, its own MCP server | no | the seat's verbs, served by the tool runner and found with `mesh_search` |
|
||||
| Actor named by the caller | no | the caller is the bus credential the mesh issued |
|
||||
| Contexts, OAuth device flow, semantic search | no | the mesh issues credentials; no outside provider |
|
||||
| *item*, *project*, *initiative*, *release* | no | *issue*; a repository or module; no initiatives in v1; *release* is already a homonym here (glossary) |
|
||||
|
||||
**Reuse its code, or build on its model?** Its licence is the Sustainable Use License 1.0: use and
|
||||
modification are allowed for internal business, non-commercial or personal use, and distribution only
|
||||
free of charge and for non-commercial purposes. It is not an open-source licence. Running it on a
|
||||
personal mesh is allowed; vendoring it into the catalogue binds the catalogue to that licence for as
|
||||
long as the code is there. Its services create their own stream and bucket on start, which ADR 0201
|
||||
§3 forbids a module to do; its subjects carry no prefix and assume one tracker per bus account; its
|
||||
words clash with the glossary. Changing those means a fork of a two-week-old, single-author project.
|
||||
v1 here is the item store without the indexes: roughly the size of the module that delivers pull
|
||||
requests.
|
||||
|
||||
**Recommended: build our own module on hits's model,** and say so in the module's README. Do not
|
||||
depend on or vendor its code. Revisit only if its licence changes, or if its author agrees to a
|
||||
licence for this use.
|
||||
|
||||
## 7. Open questions for the operator
|
||||
|
||||
1. **Should the tracker hold every issue's number and status, while hq keeps a folder only for design
|
||||
and governance issues, pointing at the tracker by number (option (a) in section 2)?**
|
||||
Recommended: yes. It ends the collisions, keeps the public record, and moves the cycle rules into
|
||||
the write, where they refuse instead of report.
|
||||
2. **Should we build our own `issues` module on hits's model rather than run or vendor hits?**
|
||||
Recommended: build our own. hits's licence limits distribution, its services create their own bus
|
||||
resources (against ADR 0201), and its words clash with ours. Its model, its compare-and-set
|
||||
numbering and its auditor are worth copying.
|
||||
3. **Should the tracker also hand out decision and research numbers?** Recommended: yes, in slice 2.
|
||||
Decision numbers collided three times on 2026-10-07; the same counter mechanism serves any
|
||||
sequence, and the tracker then reserves the number when a branch starts.
|
||||
4. **Should the word stay *issue*, with the module named `issues` and *ticket* retired?**
|
||||
Recommended: yes. *Issue* is the glossary's word for this, and one word per thing (ADR 0244).
|
||||
5. **Should the controller open an issue by itself for a `healer-wanted` condition, or only link
|
||||
one an agent or the operator opened?** Recommended: link only, until slice 4 has run for a week;
|
||||
then open by itself for `healer-wanted`, one issue per condition key.
|
||||
|
||||
## 8. Slices after the first
|
||||
|
||||
| Slice | Delivers | Done when the operator sees |
|
||||
|---|---|---|
|
||||
| 1 | The `issues` module: open, get, list, move, note, link, import; compare-and-set in the state interface | the next issue number comes from the tracker and the 73 not closed are listed by one call |
|
||||
| 2 | Decision and research numbers from the tracker; playbook 03 and `cycle.py` changed; status keys removed from every report | two agents opening at once get two numbers, and no hq pull request is renumbered |
|
||||
| 3 | Consolidation: the four passes of section 3, and an audit tool for `fixed-by` against the forge | the not-closed list shrinks by what was already fixed or duplicated, each with its reason |
|
||||
| 4 | Conditions and pull requests linked; `healer-wanted` opens an issue | a condition shows its issue, and a merge notes the issues it names |
|
||||
| 5 | A read-only view onto the `issues` bucket | the operator reads issues in a browser without an agent |
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
---
|
||||
status: graduated
|
||||
initiated: 2026-10-10
|
||||
touches:
|
||||
- 02-DECISIONS/0019-how-this-repository-works.md
|
||||
- 02-DECISIONS/0080-the-development-cycle-is-checked.md
|
||||
- 02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
|
||||
- 02-DECISIONS/0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md
|
||||
- 02-DECISIONS/0274-an-agent-proposes-a-shared-instruction-section-and-every-node-takes-it-only-on-the-operators-warrant-which-it-proves-itself.md
|
||||
- 02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md
|
||||
- 03-DESIGN/01-to-be/53-the-issue-tracker.md
|
||||
- 00-META/process/
|
||||
- 00-META/checks/
|
||||
- 00-META/glossary.md
|
||||
- the records module's tools
|
||||
became:
|
||||
- 02-DECISIONS/0290-a-decision-record-lives-whole-in-the-knowledge-base-made-of-clauses.md
|
||||
- 02-DECISIONS/0291-an-issue-lives-whole-in-the-issue-tracker.md
|
||||
- 03-DESIGN/01-to-be/55-the-knowledge-base.md
|
||||
---
|
||||
|
||||
# 045 — A knowledge base the mesh writes: decisions, guidelines, policies
|
||||
|
||||
## What
|
||||
|
||||
Whether the mesh should **write** its own knowledge (decision records, guidelines, policies, designs and
|
||||
playbooks) through a module's verbs, instead of only **reading** hand-written markdown, and whether it
|
||||
should hold light content beside them: meeting notes, architectural drawings and blog posts.
|
||||
|
||||
The operator's purpose, given on 2026-10-11: the record becomes **shared knowledge that agents reach
|
||||
through tools**, so that an agent gets what was decided as an answer instead of reading markdown files,
|
||||
and a session no longer has to start in this repository's folder to know it. A session still starts here
|
||||
for other work, such as writing a record. The operator later put the light content last, and decided that
|
||||
an issue's content belongs in the issue tracker rather than in this repository.
|
||||
|
||||
Today the record is read-only. The `records` module reads this repository and serves it to sessions
|
||||
([ADR 0153](../../02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)).
|
||||
Everything in it is written by hand as markdown and reaches `main` through a pull request; the hq checks
|
||||
in `00-META/checks/` judge it only after it is written. The issue tracker has already taken issues out
|
||||
of this path: it numbers and tracks them in a module of its own, and hq keeps only the narrative
|
||||
([ADR 0275](../../02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)).
|
||||
|
||||
## Why
|
||||
|
||||
Numbers collide, and the checks find a collision only after it happens. On 2026-10-10, research number
|
||||
043 was taken by one open pull request while a second session held an unpushed branch claiming the same
|
||||
number for a different effort. On 2026-10-07 three decision numbers collided in one evening. A
|
||||
record written by a verb could take its number from one counter, the way the issue tracker does.
|
||||
|
||||
Reading is costly too. Answering one question about what holds today means reading several records
|
||||
whole, and the generated decision index is larger than one tool answer may be.
|
||||
|
||||
Some knowledge also has no home. This repository is public, so it may not name nodes or describe
|
||||
operations; the mesh's knowledge about its own installation lives in agents' private notes, in no
|
||||
shared and searchable place.
|
||||
|
||||
## The two options to weigh
|
||||
|
||||
- **A. Git stays the store.** The `records` module gains write verbs (propose, amend as a progressive
|
||||
insight, supersede, retire), each of which makes a branch and a pull request through the forge. Numbers
|
||||
come from the issue tracker's counters, as a further sequence beside `issue`
|
||||
([to-be 53](../../03-DESIGN/01-to-be/53-the-issue-tracker.md) §4).
|
||||
- **B. A store is the truth, and hq is its rendered public export**, as the issue tracker already is for
|
||||
issues (ADR 0275).
|
||||
|
||||
## The questions
|
||||
|
||||
1. **Types and lifecycles.** Which kinds of knowledge exist: decision, guideline, policy, design,
|
||||
playbook, and any others? What lifecycle does each have (proposed, accepted, superseded, retired;
|
||||
to-be and as-is for a design), and which transitions does each kind allow?
|
||||
2. **Numbering from one counter.** One sequence per kind, or one for all? How is a number reserved for
|
||||
a draft that is later abandoned, given that a number is never reused? How are today's collisions
|
||||
(043 is the latest example) resolved on import?
|
||||
3. **Typed links.** Which links between records are needed: supersedes, amends, implements, cites,
|
||||
decided-by, and others? Which of them does frontmatter express today (`became:`, `decisions:`,
|
||||
`fixed-by:`), and which exist only as prose links that no check reads?
|
||||
4. **Approval.** A decision or a policy changes how the mesh is run. Should accepting one require the
|
||||
operator's warrant on a channel that proves who answered, as a shared instruction section already
|
||||
does ([ADR 0274](../../02-DECISIONS/0274-an-agent-proposes-a-shared-instruction-section-and-every-node-takes-it-only-on-the-operators-warrant-which-it-proves-itself.md))?
|
||||
Which kinds need it, and which may an agent write on its own?
|
||||
5. **Public and private knowledge.** Node names and operational knowledge have no home today. Does the
|
||||
knowledge base hold private records beside public ones, with a rule for which may be exported? How is
|
||||
a record checked so that nothing private reaches the public repository?
|
||||
6. **Checks as refusals at write.** `records.py`, `cycle.py`, `words.py` and `index.py` judge a pull
|
||||
request after it is written. Which of their rules can become refusals of the write verb itself, and
|
||||
which only make sense across the whole record?
|
||||
7. **Import.** How are the decision records (274 on `main` on 2026-10-11, numbered up to 0289), the designs and the playbooks imported without
|
||||
losing their history, their numbers or their links? Is the import repeatable, as the issue tracker's
|
||||
is?
|
||||
8. **What git gives today.** Review on a pull request, line comments, a history of every change with
|
||||
its author, a diff, a revert, and the forge's required checks. What of that would a store have to
|
||||
rebuild under option B, and what would option A lose by making every write a pull request?
|
||||
|
||||
## Words
|
||||
|
||||
The glossary has words for **decision record**, **design**, **playbook** and **the record** (the
|
||||
`records` module), but none for **guideline** or **policy** in this sense, and *policy* is already used
|
||||
for a module's **upgrade policy**. This effort uses "guideline" and "policy" as plain descriptions, and
|
||||
"knowledge base" for the whole, until a decision settles the words through the glossary's own rules.
|
||||
|
||||
## What it touches
|
||||
|
||||
How this repository works (ADR 0019) and how the development cycle is checked (ADR 0080), the
|
||||
`records` module (ADR 0153) and the generated decision index (ADR 0248), the issue tracker and its
|
||||
counters (ADR 0275, to-be 53), approval on a proving channel (ADR 0274), the playbooks in
|
||||
`00-META/process/`, the hq checks in `00-META/checks/`, and the glossary.
|
||||
|
||||
## Documents
|
||||
|
||||
- [01 — Findings](01-findings.md): what exists, measured on 2026-10-11; where the truth lives (the store
|
||||
for each record's structure and rules, this repository for its reasoning); issues whole in the issue
|
||||
tracker; the order of work; and what is still open.
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
# 01 — Findings
|
||||
|
||||
Measured on 2026-10-11 against `main` of this repository, the records module and the issue tracker as they
|
||||
answer through the mesh MCP server, and the hq checks in `00-META/checks/`. Section 0 is what exists; section 1 is where the truth lives, which
|
||||
decides the rest; sections 2 to 9 answer the overview's questions and the operator's additions; section 10 is
|
||||
the order of work, and section 11 what is still open.
|
||||
|
||||
## The purpose, in the operator's words
|
||||
|
||||
The operator gave the purpose on 2026-10-11: the decisions and the rest of the record should become **shared
|
||||
knowledge that agents reach through tools**, instead of reading markdown files, and a session should no longer
|
||||
have to start in this repository's folder to know what was decided. A session still starts here for other work,
|
||||
such as writing a record. Later that day the operator widened the scope to **light records**
|
||||
(meeting notes, architectural drawings, blog posts) and then put them last; asked for a structured store of
|
||||
the decisions and research efforts; and decided that an issue's content belongs in the issue tracker, not in
|
||||
this repository.
|
||||
|
||||
That purpose moves the weight of the overview's questions. Where the truth is stored (option A or B) matters
|
||||
less than **what an answer looks like**: an agent needs the rule that holds now, with the records it rests on,
|
||||
in one call and a few kilobytes.
|
||||
|
||||
An acceptance sentence for the whole effort: *a session started in any folder on any node asks "what holds
|
||||
about the merge window, and why" and gets, in one call of under 4 KiB, the rules in force, the records they
|
||||
come from, what changed them since, and the commit it was read at.*
|
||||
|
||||
## 0. What exists today, measured
|
||||
|
||||
| Fact | Measured |
|
||||
|---|---|
|
||||
| Records on `main` | 274 decision records, 44 research efforts (119 files), 55 to-be and 16 as-is designs, 8 playbooks, 361 issue narratives (479 files): 951 files, and the glossary |
|
||||
| Size | 6.9 MB of markdown, about 1.7 million tokens at four bytes a token; decision records alone 2.6 MB (about 650,000 tokens) |
|
||||
| One question, today | "what holds about the merge window": `records_search` gives 26 places in 11 documents; reading the five records and the design they point to is 85 KB, about 21,000 tokens |
|
||||
| The decision index | `records_decisions` answers 77 KB in one piece; the agent's tool-answer limit refused it inline, and it had to be read from a file in parts |
|
||||
| What `records_search` does | a literal, case-insensitive phrase match giving document, line and heading; no ranking, no notion of which record is still in force |
|
||||
| Frontmatter of a decision | `topic`, `status`, `date`, `deciders` on all 274; `extends` on 229 (182 with the folder prefix, 47 without); `supersedes-in-part` on 15 (21 links), `supersedes` on 8, `superseded-by` on 9, `rests-on` on 2, `amended` on 1 |
|
||||
| Status of a decision | 265 `accepted`, 9 `superseded`; no record is marked as changed in part, though 21 links say so |
|
||||
| Supersession said only in prose | 6 records say they supersede, amend or replace another and carry no field for it; 7 records carry a "superseded" marker in the body, of which only 1 has the field or the status |
|
||||
| Progressive insights | 39 records carry the dated marker, 55 markers in all; 2 more use an older form |
|
||||
| Change after writing | 189 of 271 traceable decision records were changed after the commit that created them; 34 paths were changed five times or more |
|
||||
| Pace | commits touching decision records: 87 in the last ten days of August, 160 in September, 274 in the first eleven days of October |
|
||||
| Number collisions | 16 decision numbers were taken twice by sessions working at once (2026-09-25 to 10-10), each repaired by a renumbering commit; 15 numbers on `main` are gaps; no research number collided on `main` (the 043 collision of the overview never reached it) |
|
||||
| A decision's rules | 263 records have a `Decision` section; only 37 of them write it as numbered rules with a bold lead (198 rules in all); the rest write prose |
|
||||
| Drawings | no file but markdown in the repository; no diagram source; 22 designs carry diagrams drawn in text characters |
|
||||
| Knowledge with no home | the repository is public, and its README says what only makes sense to someone who knows this installation "is either a note in the wrong place or a disclosure"; one agent's private notes hold 89 files for one repository on one node, and issue 367 (a session's memory stays on the node where it was learned) is open |
|
||||
| The hq checks | 33 rules: 25 judge one record, 4 the whole record, 4 the prose (section 7) |
|
||||
|
||||
Three things follow.
|
||||
|
||||
1. **The cost of an answer is the problem, not where the files are.** 85 KB for one question, and an index
|
||||
too large for one tool answer, mean a session reads in parts or reads less than it should. Moving the
|
||||
markdown into a store without changing the shape of the answer would keep that cost.
|
||||
2. **The data that makes an answer cheap is missing or loose.** What a record decided is prose in 226 of 263
|
||||
records. Which records changed which is half in fields, half in prose: 6 records say it only in prose, and
|
||||
6 more carry a body marker that no field or status agrees with.
|
||||
Status says `accepted` for records that 21 later links change in part.
|
||||
3. **The records change after they are written, and that is reviewed.** 189 of 271 decision records were
|
||||
edited after creation, every edit through a pull request with a human approval. Whatever is built must keep
|
||||
that review for what governs the mesh.
|
||||
|
||||
## 1. Where the truth lives: what a clean start would choose
|
||||
|
||||
The operator asked on 2026-10-11 which option this effort would choose with no prior setup. The answer
|
||||
decides the rest, so it comes first.
|
||||
|
||||
**The unit the mesh reasons about is a rule, not a file.** A decision is a handful of rules. A later decision
|
||||
changes one of them: ADR 0282 replaces one rule of ADR 0276 and leaves the others standing. Git and markdown
|
||||
know files only, so that change can be written only as prose. The earlier record keeps `accepted`, and every
|
||||
reader must read both records to learn what holds. Section 0's loose links all have this one cause: 21 partial
|
||||
supersessions with no reverse, 6 said only in prose, 6 markers no field agrees with.
|
||||
|
||||
**A store can hold what a repository cannot.** In a store, each rule is an entry with its own status and links
|
||||
("in force", "changed by rule 2 of 0282"). A write that leaves the record inconsistent is refused when it is
|
||||
made, not found by a check afterwards. Numbers come from one counter. "What holds now" is one query.
|
||||
|
||||
**What git does best is review of prose.** The reasoning behind a decision (its context, the options weighed,
|
||||
its consequences) is read and commented on by people. Agents rarely ask for it. They ask what holds.
|
||||
|
||||
So a clean start divides **by layer, not by kind**:
|
||||
|
||||
- **the store is the truth for the structure** of every governing record: its number, title, kind, topic,
|
||||
status, its rules one by one, and its typed links, at the level of the rule where a link is about a rule;
|
||||
- **this repository keeps the reasoning** as markdown under the store's number, reviewed in pull requests.
|
||||
|
||||
That is the division the issue tracker already made for issues
|
||||
([ADR 0275](../../02-DECISIONS/0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)):
|
||||
the tracker holds number, status and links, and hq held the narrative. Section 9 takes issues one step further,
|
||||
on the operator's word: their narrative leaves this repository too.
|
||||
|
||||
Option A (git stays the truth, and a module's write verbs open pull requests, with numbers from a counter) was weighed and is not chosen. It keeps review, but it
|
||||
keeps the rule-level links in prose and the refusals after the fact, which is the cause of section 0's mess.
|
||||
Option B in full (the reasoning moves into the store too) is not chosen either: it would rebuild review, line
|
||||
comments, history and diff for prose, which git already gives.
|
||||
|
||||
## 2. Kinds and their lifecycles
|
||||
|
||||
| Kind | Lifecycle | Notes |
|
||||
|---|---|---|
|
||||
| decision | proposed → accepted → superseded; a rule can be changed or superseded on its own; a dated progressive insight corrects a fact | 274 records |
|
||||
| research | active → graduated or abandoned | 44 efforts |
|
||||
| design | proposed → designed → in-progress → implemented, in one of two layers (to-be, as-is) | 71 documents; a later slice (section 10) |
|
||||
| playbook | in force, or retired | 8, with no frontmatter today; a later slice |
|
||||
| glossary entry | in force, or retired with its replacement | one file today; a later slice |
|
||||
|
||||
The first slice holds decisions and research, the two the operator named.
|
||||
|
||||
A **guideline** or a **policy**, which the overview named, has no record of its own today. What the operator
|
||||
calls a policy is written as a decision; what an agent should do is a shared instruction section (ADR 0274) or
|
||||
a personal rule ([ADR 0273](../../02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md)).
|
||||
No new kind is proposed for either.
|
||||
|
||||
**Light records** (meeting notes, drawings, blog posts) were added to the scope by the operator on 2026-10-11
|
||||
and put last by the operator the same day. They govern nothing, need no review to exist, and are private by
|
||||
default; a drawing's file belongs in an object-store bucket
|
||||
([ADR 0288](../../02-DECISIONS/0288-the-object-store-stays-on-the-maintained-fork-and-garage-is-chosen-only-after-the-consumers-s3-features-are-counted.md))
|
||||
with a text description an agent reads. They are a later slice (section 10) and shape nothing in the first.
|
||||
|
||||
## 3. Numbering from one counter
|
||||
|
||||
The 16 concurrent collisions all come from reading the next number off a repository that many branches write
|
||||
at once. The issue tracker hands out numbers by compare-and-set on a counter key
|
||||
([to-be 53](../../03-DESIGN/01-to-be/53-the-issue-tracker.md) §4); the knowledge base asks it for every number.
|
||||
|
||||
- **One sequence per kind**: decision, research, and later design. Numbers keep their meaning in conversation
|
||||
("ADR 0276").
|
||||
- **A reserved number is spent.** A proposal that is abandoned keeps its number with status `withdrawn`, who
|
||||
reserved it and when. Gaps already exist (15 on `main`) and harm nothing; a reused number would.
|
||||
- **The counter starts above the highest number on `main` and on every open branch**, as the tracker's did.
|
||||
- **The counters stay where ADR 0275 put them.** Its rule 8 decided that the issue tracker hands out decision
|
||||
and research numbers too, in its second slice; to-be 53 §4 so far names only the issue counter. The knowledge
|
||||
base's `reserve` asks the tracker for the number, so one counter holds every sequence and no accepted record
|
||||
is superseded for it.
|
||||
|
||||
## 4. Rules and typed links
|
||||
|
||||
**A rule** is one short statement a decision makes, numbered within its record ("0276 rule 1"), with its own
|
||||
status: `in force`, `changed by` a rule of a later record, or `superseded`. 37 of 263 decision sections already
|
||||
write numbered rules with a bold lead (198 rules); the other 226 write prose, from which the import drafts the
|
||||
rules (section 7).
|
||||
|
||||
The links the first slice holds, each stored in both directions:
|
||||
|
||||
| Link | From → to | Today |
|
||||
|---|---|---|
|
||||
| supersedes | record → record | `supersedes` / `superseded-by`, 9 of 9 mirrored |
|
||||
| changes | rule or record → rule | `supersedes-in-part` on 15 records, one direction; 6 more only in prose |
|
||||
| extends | record → record | `extends` on 229, two spellings |
|
||||
| became | research → decision or design | `became` |
|
||||
| decided by | design → decision | `decisions` |
|
||||
| corrects | progressive insight → record | the dated marker, 55 in 39 records |
|
||||
| cites | any → any | prose links: 1,785 decision to decision, 469 decision to issue |
|
||||
|
||||
"What holds about X" is then: the rules matching X whose status is `in force`, each with its record and the
|
||||
changes that brought it to its present wording.
|
||||
|
||||
## 5. Approval
|
||||
|
||||
A decision governs how the mesh is built and run, so it needs a human approval before it holds.
|
||||
|
||||
- **Proposing** reserves a number and writes the structure with status `proposed`. Any agent may propose.
|
||||
- **Accepting** happens when the pull request carrying the record's reasoning merges with its human approval
|
||||
and both required checks. The knowledge base sees the merge on the bus and moves the record and its rules to
|
||||
`accepted` and `in force`, and every rule it changes to `changed by`. The merge is the warrant; a second
|
||||
approval beside it would split the evidence.
|
||||
- **A progressive insight** is a correction of fact and goes the same way: a pull request on the reasoning,
|
||||
and the structure recorded on merge.
|
||||
- A **structural write without a pull request** (a link added, a rule's wording fixed) is refused for an
|
||||
accepted record. Changing what an accepted record says is a new decision, as it is today.
|
||||
|
||||
## 6. Public and private
|
||||
|
||||
This repository is public and stays so. The reasoning of a decision or research effort stays here, public.
|
||||
The structure in the store is derived from public records and may be exported here as generated pages, like
|
||||
the decision index of [ADR 0248](../../02-DECISIONS/0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md),
|
||||
generated when read and never stored.
|
||||
|
||||
What changes is the issues' narrative (section 9): it moves into the tracker, which is the mesh's own and not
|
||||
public. The public repository loses those narratives. ADR 0275 kept them public on purpose, citing *the mesh
|
||||
remembers*; the tracker remembers them as well, but only for the mesh. Whether a resolved issue's narrative is
|
||||
ever exported publicly is a question for the decision that supersedes that part of ADR 0275.
|
||||
|
||||
Knowledge about this installation (one agent's 89 private notes, issue 367) stays out of scope. Agents' lessons
|
||||
and rules belong to the agent module's register (ADR 0273).
|
||||
|
||||
## 7. Checks as refusals at write, and the import
|
||||
|
||||
Of the 33 rules the hq checks enforce, **25 judge one record** and become refusals of the knowledge base's
|
||||
write verbs: a number taken twice, a supersession not mirrored, a cited record not accepted, a record without a
|
||||
topic, a research effort graduated without `became`. Two need a look beyond the record: a write that
|
||||
supersedes must check who cites what it supersedes. **4 judge the whole record** and **4 judge the prose**;
|
||||
those stay checks on this repository, run on every pull request. As frontmatter leaves this repository
|
||||
(section 8), the 25 leave `records.py` and `cycle.py` with it, and a check is added that a record's reasoning
|
||||
names a number the knowledge base holds.
|
||||
|
||||
**The import** reads this repository at a commit through the records module and writes each decision and
|
||||
research effort at its own number: status, topic, date, deciders, links, and its rules. Like the tracker's, it
|
||||
creates what is absent, never overwrites, never advances a counter, and a second run changes nothing and says
|
||||
so. Two things a reading cannot recover alone:
|
||||
|
||||
- **Loose links.** The 6 supersessions said only in prose, the 6 markers no field agrees with, the 47 bare
|
||||
`extends` targets and the 21 one-way partial supersessions are repaired in this repository first, by one
|
||||
pull request, so the import reads clean input.
|
||||
- **The rules of 226 decisions written as prose.** The import drafts them through the agent's judgement
|
||||
([ADR 0278](../../02-DECISIONS/0278-the-issue-tracker-checks-every-new-issue-for-a-duplicate-through-a-judgement-the-agent-makes-without-tools.md))
|
||||
and stores them as `drafted`, not `in force`. A topic's drafts are confirmed by the operator or a reviewer in
|
||||
one pass. An answer marks a drafted rule as drafted, and never presents it as the record's word.
|
||||
|
||||
## 8. What leaves this repository, and what stays
|
||||
|
||||
| | Today, in this repository | After |
|
||||
|---|---|---|
|
||||
| a decision's number, status, topic, links | frontmatter | the knowledge base; the file keeps the number in its name and heading |
|
||||
| a decision's rules | prose in the `Decision` section | the knowledge base; the section stays as prose for the reasoning |
|
||||
| a decision's context, options, consequences | markdown | unchanged |
|
||||
| a research effort's status and `became` | frontmatter | the knowledge base |
|
||||
| a research effort's documents | markdown | unchanged |
|
||||
| the decision index | generated from frontmatter when read | generated from the knowledge base when read |
|
||||
| an issue's report, diagnosis, resolution | `04-ISSUES/NNN-slug/` for 361 issues | the issue tracker (section 9) |
|
||||
|
||||
Frontmatter leaves a file only after the import has proven it holds the same values, one kind at a time, as
|
||||
ADR 0275 did for issues' status.
|
||||
|
||||
## 9. Issues live in the issue tracker, whole
|
||||
|
||||
The operator decided on 2026-10-11 that an issue's content belongs in the issue tracker, not in this
|
||||
repository. Measured on `main`: 361 issue folders, 1.9 MB; a folder's median is 4.4 KB and the largest
|
||||
25.9 KB, well under a tracker value's 256 KiB. Every folder has `00-report.md`; 107 have a diagnosis, 9 a
|
||||
resolution, 1 a progress file. Since ADR 0275, new issues live in the tracker already; the highest issue folder
|
||||
is 364.
|
||||
|
||||
What the tracker needs to hold it:
|
||||
|
||||
- **an issue's documents**: the report it already holds, plus a diagnosis and a resolution as named documents
|
||||
of the issue, each with its author and date, revised in place with history (notes stay append-only);
|
||||
- **an import** of the 361 folders, repeatable, keeping each issue's number and dates;
|
||||
- **links** from records: 250 files in this repository mention a path into `04-ISSUES/` 765 times, counted
|
||||
as every `04-ISSUES/NNN` path in the text (markdown links alone are fewer, about 660). Those mentions become
|
||||
plain "issue N" references that the knowledge base and the tracker resolve, rewritten in one pull request
|
||||
once the import is proven; the link check in `records.py` then refuses a new link into `04-ISSUES/`.
|
||||
|
||||
This supersedes part of ADR 0275 (hq keeps the narrative of a design or governance issue), so it needs its own
|
||||
decision record.
|
||||
|
||||
## 10. Slices
|
||||
|
||||
1. **Clean the input** (this repository, one pull request): repair the loose links of section 7, so the import
|
||||
reads clean records. In review as hq pull request 305, which declares every partial supersession in both
|
||||
records and adds the `partial-supersession` check. Beside it, a shared instruction section for every
|
||||
session: take a decision or research number above every one on `main` and on open branches until the
|
||||
counter exists, and name a partial supersession in a field, never only in prose.
|
||||
2. **The knowledge base, decisions and research**: a Go module serving one seat held once for the mesh, on
|
||||
the control-node beside the issue tracker. The import, the rules and links, and the answers; numbers come from the issue tracker (section 3):
|
||||
|
||||
| Verb | Answers |
|
||||
|---|---|
|
||||
| `ask` | the records and rules that bear on a question, ranked: number, kind, status, title, and whether each rule still holds or what changed it |
|
||||
| `get` | one record: its rules, its links both ways, what changed it since; its reasoning's text only when asked |
|
||||
| `holds` | for a topic or a record, the rules in force now, each with the record it comes from |
|
||||
| `reserve` | the next number of a kind, from the issue tracker's counter (section 3), spent whether or not the record lands |
|
||||
| `propose` | a new record's structure, status `proposed` |
|
||||
| `import` | this repository's decisions and research at a commit, repeatable |
|
||||
|
||||
Storage is a database in the store, provided to the module: the structure is small, but `ask` needs ranked
|
||||
full-text search, which the store's server does and a bucket does not. Every answer is bounded in size.
|
||||
3. **Frontmatter leaves this repository**, kind by kind, once the import is proven equal.
|
||||
4. **Issues whole in the tracker** (section 9): documents, import, links rewritten, folders removed.
|
||||
5. **Designs, playbooks and glossary entries** into the knowledge base the same way.
|
||||
6. **Light records**: notes, posts and drawings.
|
||||
|
||||
How a session reaches it: the knowledge base's tools reach every session through the mesh MCP server, and one
|
||||
shared instruction section, approved by ADR 0274's route, tells every session to ask the knowledge base what
|
||||
holds before reading files. A session needs this repository's folder only to write a record's reasoning.
|
||||
|
||||
Proved by the acceptance sentence at the top: the merge window question, asked from a session outside this
|
||||
repository, answered in one call under 4 KiB, naming ADR 0276's rules in force and the one ADR 0282 changed.
|
||||
|
||||
## 11. What needs the operator
|
||||
|
||||
Decided by the operator on 2026-10-11: the store is the truth for structure and this repository for reasoning
|
||||
(section 1); light records come last; issues live whole in the tracker (section 9).
|
||||
|
||||
Still open:
|
||||
|
||||
1. **Drafted rules (section 7)**: the 226 prose decisions get their rules drafted by the agent's judgement and
|
||||
confirmed a topic at a time. Who confirms: the operator, or an independent reviewing agent with the operator
|
||||
seeing a summary?
|
||||
2. **Issues' public narrative (section 6)**: once issues leave this repository, is a resolved design or
|
||||
governance issue ever exported here publicly, or does it stay the mesh's alone?
|
||||
|
||||
## Words
|
||||
|
||||
The glossary has **decision record**, **design**, **playbook** and **the record** (the records module). This
|
||||
effort proposes, for a decision to settle through the glossary's own rules: **knowledge base** for the module
|
||||
and its seat; **rule** for one statement a decision makes, with its own status; **governing record** for a kind
|
||||
reviewed before it holds; and **note**, **post** and **drawing** for the light kinds. *Policy* stays the word
|
||||
for a module's upgrade policy, and *guideline* is not proposed, since section 2 finds no record of that kind.
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
---
|
||||
topic: building it
|
||||
status: accepted
|
||||
date: 2026-04-03
|
||||
deciders: jochen
|
||||
reconstructed: true
|
||||
---
|
||||
|
||||
# 11. Managed files are generated onto nodes and never edited there
|
||||
|
||||
> Reconstructed after the fact from the evidence cited below.
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0006](0006-the-substrate-and-the-control-plane.md) put every binding in the mesh
|
||||
database. But the things that consume those bindings — environment files, service
|
||||
definitions, daemon configuration, firewall rules — are files on a node's disk, because that
|
||||
is what the software reading them requires.
|
||||
|
||||
So the same value exists twice: authoritatively in the database, and materialised in a file.
|
||||
Any edit to the file is a change to a copy. Before this decision, environment values could be
|
||||
pushed from a node back into the database, which made the direction ambiguous in both
|
||||
directions at once.
|
||||
|
||||
## Considered options
|
||||
|
||||
1. **Bidirectional sync** — a node's edits flow back to the database. Rejected, and removed.
|
||||
Two writers and no arbiter: whichever synced last wins, and neither is authority.
|
||||
2. **Files are authoritative; the database is a cache of them.** Rejected — it inverts
|
||||
ADR 0013 and returns to state that cannot be reconciled across nodes.
|
||||
3. **Strictly one-directional: the database is written, files are generated.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Every managed file is **derived**. A synchroniser regenerates it from the mesh database
|
||||
whenever the underlying values change. The write path is the mesh tool that owns the value;
|
||||
the file is an output.
|
||||
|
||||
This applies to generated environment files, service definitions, managed configuration, and
|
||||
anything else a synchroniser lists as its own.
|
||||
|
||||
An edit to a managed file survives until the next synchronisation and is then overwritten,
|
||||
without a warning, taking whatever it was fixing with it.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **A file edited on a node is a bug with a delay on it.** This is now one of the mesh's core
|
||||
values, and it is a consequence of this decision rather than a stance taken independently.
|
||||
- To change a value you must know which tool owns it. That is a real cost, paid every time,
|
||||
and the reason the mesh provides a way to ask whether a given file is managed.
|
||||
- Debugging by editing a file no longer works, and fails in the most confusing way available:
|
||||
it works, and then stops working later for no locally visible reason.
|
||||
- Recovery is cheap. A node's entire managed surface can be regenerated from the database.
|
||||
- Values resolve by precedence — database override, then existing file value, then generated,
|
||||
then manifest default — which means an unset override does not clobber a generated
|
||||
password. The subtlety is real and has caused its own confusion.
|
||||
|
||||
## References
|
||||
|
||||
- `Extract hal/env-sync module, remove syncEnvToDb`, 2026-04-03 — the commit that removed the
|
||||
node-to-database direction.
|
||||
- Knowledge base: `conventions/no-direct-file-mutation`, `provisioning` (resolution priority).
|
||||
- Regeneration gaps: `troubleshooting/changed-manifest-default-not-rerendered`,
|
||||
`troubleshooting/config-removed-from-manifest-not-pruned`.
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: superseded
|
||||
superseded-by: 0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
|
||||
date: 2026-09-26
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
supersedes: 02-DECISIONS/0125-the-bus-is-the-only-broker.md
|
||||
---
|
||||
|
||||
# 127. AMQP is a provision, not the bus
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0125](0125-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 0125](0125-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 0126](0126-a-module-declares-its-own-seats.md) — seats, including the one the NATS server
|
||||
now holds alone.
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-09-27
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
supersedes: 0127-amqp-is-a-provision-not-the-bus.md
|
||||
---
|
||||
|
||||
# 131. Everything on the mesh speaks to the broker seat, and AMQP is not a provision
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0127](0127-amqp-is-a-provision-not-the-bus.md) settled the old broker as an ordinary provider
|
||||
of an ordinary provision, `amqp`, kept for whatever wanted a message broker of its own. The day the
|
||||
bus moved was the day that framing was tested, and it failed in a way that took the control plane
|
||||
down for an evening.
|
||||
|
||||
Three things came out of the wreckage. **The protocol had leaked into the seat's contract**: for a
|
||||
module to hold `mesh-broker`, it had to provide what the seat delivers, and what it delivered was
|
||||
`amqp` — so the module that will carry the bus on NATS could not hold the seat that names the bus,
|
||||
while the module the mesh was leaving could. **A consumer of `amqp` is not asking for AMQP.** The two
|
||||
modules requiring it wanted the mesh's messaging — to emit an event, to hear a topic — and named the
|
||||
wire protocol only because that was the word available. **And AMQP and NATS are not interchangeable
|
||||
at the wire.** A provision named after a protocol can only ever be answered by that protocol, so once
|
||||
the bus is NATS an `amqp` provision has one possible provider, and it is the thing being retired.
|
||||
|
||||
The operator's position, stated during the outage: modules depend on the broker *seat*, not on a
|
||||
protocol; AMQP is obsolete as anything the mesh's core knows about; a module that depends on `amqp`
|
||||
is wrong; and everything should reach the mesh's bus and be able to emit events and consume topics
|
||||
through it.
|
||||
|
||||
## Decision
|
||||
|
||||
**A module that needs messaging uses the mesh's bus, and the mesh's bus is whatever holds
|
||||
`mesh-broker`.** Emitting an event and consuming a topic go through the sdk, which is handed the
|
||||
bus by the mesh with the module's own credential. No manifest names a wire protocol to get it.
|
||||
|
||||
**`amqp` is neither a provision nor a requirement.** Registration refuses a manifest that provides
|
||||
it or requires it. The `mesh-broker` seat delivers `mesh-bus`, and its holder is the module that
|
||||
provides `mesh-bus` — today the nats module, and only it.
|
||||
|
||||
**The old broker's module and the two modules that required it leave the catalogue.** They are
|
||||
removed, not converted: one was a proof that a grant worked end to end, the other forwards mail off a
|
||||
queue, and both are re-done against the bus if wanted, as new modules under this record.
|
||||
|
||||
**The controller's AMQP transport is deleted once every node reports on the new bus**, and the
|
||||
switch that selects a transport goes with it — one bus, so nothing to select.
|
||||
|
||||
The predecessor's own broker is outside the mesh and not this record's concern
|
||||
([ADR 0130](0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)): what the predecessor's
|
||||
tooling loses when it stops is accepted there.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Keep 0127: AMQP stays an ordinary provision with the old broker as its provider.** Rejected. It
|
||||
is what put the protocol into the seat's contract, it is why the seat could be left with no valid
|
||||
holder mid-change, and it keeps two transports in the control plane indefinitely for the benefit of
|
||||
two modules that did not want AMQP in the first place.
|
||||
2. **Bridge it: the old broker's module also provides `mesh-bus`, so both can hold the seat during the
|
||||
change.** Rejected. It makes the retiring broker a legitimate mesh bus for exactly as long as
|
||||
nobody removes the line, which in practice is forever, and it leaves `amqp` as a thing the core
|
||||
still knows the name of.
|
||||
3. **The seat is the dependency; the protocol is nobody's business but the holder's.** Adopted.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **The change of holder is a handover, and it needs a command.** Nothing today moves a seat from
|
||||
one assignment to another as one act, and a seat the control plane dereferences cannot be empty
|
||||
in between — that emptiness is the outage this record comes from. The command takes a seat and the
|
||||
assignment taking it over. Designed and built before the cutover, under
|
||||
[28 — Building the bus](../03-DESIGN/01-to-be/28-building-the-bus.md).
|
||||
- **The seat's row moves to `mesh-bus` before the new holder registers, and that is safe.** The
|
||||
control plane composes its own bus address through the seat *by name*
|
||||
(`${seat:mesh-broker:…}`), and the overview derives holders by name; only registration and the
|
||||
provision-to-seat resolution read what a seat delivers. So the row can change under the current
|
||||
holder without unseating it, the new holder can then register its claim, and the handover happens
|
||||
when both are running. Verified in the code during the outage, not assumed.
|
||||
- **Registration gains two refusals**: a manifest providing `amqp`, and one requiring it.
|
||||
- **The `rollout check` stops saying the old broker stays.** It said so under 0127; it now lists
|
||||
unassigning it as the last step of the move.
|
||||
- **What got harder**: a third party that genuinely wants an AMQP broker on a mesh node runs one as
|
||||
any application module, with no provision and no seat, and nothing on the mesh routes to it. That
|
||||
is the cost of the mesh not knowing the word.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| No manifest provides or requires `amqp` | a registration test refusing each, naming this record; and a whole-catalogue test asserting no registered manifest names it |
|
||||
| `mesh-broker` delivers `mesh-bus`, and only a `mesh-bus` provider may hold it | the existing registration test for a delivering seat, with the row's value read from the store (mesh-controller#89) |
|
||||
| The seat's row can change without unseating the holder | a test composing the control plane's own address and the overview under a row that the current holder does not satisfy |
|
||||
| The rollout does not leave the old broker running | `rollout check` output, asserted in its test |
|
||||
| The AMQP transport is gone | the package does not compile with it referenced; the switch variable is refused as unknown at start |
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-01
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||
---
|
||||
|
||||
# 162. A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue
|
||||
|
||||
> **Superseded in part — 2026-10-10, by [ADR 0276](0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md).**
|
||||
> "A merge produces a tiered plan" no longer holds: merges are collected in a rolling merge window, and each
|
||||
> batch is planned once, as one walk with at most one commit per repository (issue 362). The plan the mesh
|
||||
> keeps, its tiers and the one dependency relation stand as decided here.
|
||||
|
||||
> **Progressive insight — 2026-10-02.** The context below says a dependent is *built by whichever build machine is running — the only one there could be*. That was a fact of the day, not of the decision: since [ADR 0190](0190-a-seats-work-is-shared-by-its-holders-and-building-is-the-first-such-role.md) a tier's asks are taken by every machine holding the build seat. The plan and its tiers are unchanged.
|
||||
|
||||
> **Superseded in part — 2026-10-08, by [ADR 0267](0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md).**
|
||||
> In decision 1, the `packages` kind ("the module's build reads the other's repository") and the *code*
|
||||
> dependency ("B packages A's source … rebuilt whenever A changes, in the same tier") no longer hold: a file
|
||||
> two modules build from moves both through their build sources, and no edge is drawn for sharing a
|
||||
> repository. `stands-on` and `declared` name an artifact at each end. The build and runtime dependencies,
|
||||
> `built-by`, and decisions 2 to 4 stand as decided here.
|
||||
|
||||
## Context
|
||||
|
||||
A merge on the forge reaches the controller as an event, and the controller asks the build
|
||||
machine for what that merge changed. Until today that meant the modules whose recorded source is
|
||||
that repository; since this afternoon it also means everything standing on what moved
|
||||
([issue 186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md)).
|
||||
Both are done inside the handler that received the event: it asks one build, waits for it, asks the
|
||||
next, and returns when the last is done. Three things followed from that shape on 2026-10-01:
|
||||
|
||||
- The controller hears nothing else for the length of the work — twenty-five minutes for the runtime
|
||||
image and its forty-three dependents ([issue 184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md)).
|
||||
- A controller replaced mid-merge loses the rest of the merge: the redelivered announcement reads as
|
||||
history, and the dependents are asked by hand.
|
||||
- Nothing is deployed between builds. A merge that changes the build machine and something the build
|
||||
machine builds asks for both in order, but the second is built by whichever build machine is running
|
||||
— the old one, unless somebody pushed in between. The order the dependents are sorted in exists for
|
||||
the artifacts; it says nothing about what must be *running*.
|
||||
|
||||
And the knowledge the order is computed from is scattered: a manifest's `build.on`, the artifacts a
|
||||
build was made against, the repositories a build read, and the fact that every source-built module is
|
||||
built by the build machine, each read by a different function in the merge handler.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. A module's dependencies are one relation in the catalogue.** `depends-on` edges, each with the
|
||||
kind of dependency on it: `stands-on` (the module's artifact is built on the other's), `packages` (the
|
||||
module's build reads the other's repository), `built-by` (the module is built by the holder of the
|
||||
build-machine seat), and `declared` (a manifest's `build.on`). The relation is answered by one query
|
||||
of the catalogue — the controller's inventory today, the catalogue seat's tool when something outside
|
||||
the controller needs it — and nothing else computes an edge. The edges are derived from facts
|
||||
recorded at two moments and written by nobody: registration records the manifest (`declared`, and
|
||||
`built-by` for anything with a source), a build's take-in records what the image was built on and
|
||||
which repositories it read (`stands-on`, `packages`). A module's first build places it by its
|
||||
declared edges alone; from its second it is placed by what was true.
|
||||
|
||||
**The kinds are three dependencies, not one.** A *code* dependency — B packages A's source — means
|
||||
B is rebuilt whenever A changes, in the same tier: B's build needs nothing of A's first. A *build*
|
||||
dependency — B stands on A's artifact, or declares it — means B is rebuilt after A is *built*, the
|
||||
next tier, and nothing need be deployed in between. A *runtime* dependency — B is built by A — means
|
||||
B is rebuilt only after A is built *and running*, the next tier with a gate on the machines' reports.
|
||||
One cycle is real and resolved by the kinds themselves: the runtime image is built by the build
|
||||
machine, and the build machine stands on the runtime image; the image comes first, built by the
|
||||
build machine that is running, which is the only one there could be — a `built-by` edge never orders
|
||||
a module after a build machine that stands on it. A provision is not a dependency of this relation:
|
||||
a consumer binds to its provider through what the push renders, and a change to the provider's image
|
||||
changes nothing in the consumer's; a consumer whose build does read a provider's source declares it.
|
||||
"A was deployed, so restart B" is the push's domain — B is replaced when what it reads changed — and
|
||||
not the plan's.
|
||||
|
||||
**2. A merge produces a plan, and the plan is a record.** The controller takes the modules the merge
|
||||
changed and everything reachable from them along `depends-on` edges, and sorts that set into tiers:
|
||||
tier 0 depends on nothing else in the set, tier 1 only on tier 0, and so on. The plan — the merge it
|
||||
answers, the tiers, and each module's state — is written to the store before any build is asked. The
|
||||
handler asks tier 0 and returns. Every build's outcome, taken in by the same handler that takes every
|
||||
outcome in, advances the plan it belongs to; a controller replaced mid-plan resumes it from the store.
|
||||
|
||||
**3. A tier is done when it is built, and when what the next tier needs from it is running.** A
|
||||
module whose roll-out policy says *roll out* is sent to its machines when it moves, as today. The next
|
||||
tier is asked only once every module in this tier is built and every rolled-out module of this tier
|
||||
that a later tier is `built-by` has been applied by the machines running it — the machines' reports
|
||||
say so. A module whose policy says *record* is built and not waited for. So a merge
|
||||
touching the build machine and the controller builds the build machine, waits until it is the build
|
||||
machine that is running, and only then asks for the controller's build.
|
||||
|
||||
**4. A plan is read where the mesh is read.** `status` lists every open plan: the merge, the tier it
|
||||
is at of how many, what it is waiting for and since when; `builds` lists the asked beside the built.
|
||||
A plan that has waited past a bound is named red there, which is the first fact of
|
||||
[issue 187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)'s list.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The merge handler returns in milliseconds; the receive loop is never held by a build again. Issue
|
||||
184's remaining cause — a handler that waits for its own work — is removed rather than worked
|
||||
around; the bus's heartbeats stop being dropped under a merge.
|
||||
- A controller roll in the middle of a plan costs nothing: the plan is in the store and the asks are
|
||||
in the queue (mesh-controller 194).
|
||||
- A release across repositories is a plan whose edges cross repositories; the order a person kept in
|
||||
a work-order file is the order the tiers give. Issue 186's third fault is answered by the plan,
|
||||
not by a separate release record.
|
||||
- The explicit `build --on <base>` stays as the way to ask for the same plan by hand.
|
||||
- A module's `build.on` remains the one place a manifest states a dependency the store cannot see.
|
||||
|
||||
## How this is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| Dependencies are one relation, each edge with its kind | an inventory test over a fixture catalogue: a runtime image, a module on it, a module packaging the controller's source, and the build machine; the four kinds come back from one call |
|
||||
| A merge's set is sorted into tiers along the three kinds: a code dependency in the same tier, a build dependency after its base is built, a runtime dependency after the build machine; the build machine's own base first | a unit test on the tiering over the mesh's real shape: the runtime image, the build machine on it, modules built by it, a plugin declared on one, the proxy packaging the controller, an unrelated module left out; a cycle is one last tier and said |
|
||||
| Only a runtime dependency gates on deployment, and only for a module that rolls out | the same test's gate cases |
|
||||
| The plan is written before any build is asked, and the handler returns | a controller test: a merge announcement produces a plan row with its tiers and one asked build per tier-0 module, and the handler is back before any outcome |
|
||||
| An outcome advances its plan; a complete tier asks the next; a tier with a rolled-out base waits for the machines' reports | store-backed tests over a two-tier plan: the first outcome marks built; the tier's roll-out gate holds until the report; the next tier is asked after |
|
||||
| A controller restarted mid-plan resumes it | a test that opens a plan, drops the handler, and advances from the store alone |
|
||||
| `status` lists open plans and names one that waits past the bound | the status JSON test with a fixture plan |
|
||||
| Live | a catalogue merge touching a base and a dependent: the plan's tiers in `status`, the base rolled before the dependent is asked |
|
||||
|
||||
## Built and proven live, 2026-10-01
|
||||
|
||||
> **Progressive insight — 2026-10-01.** The decision stands; these are the facts of its building.
|
||||
|
||||
Built in mesh-controller 197 (the relation, the plan record, the driver, `status`), 198 (`plans`),
|
||||
199 (a `built-by` edge orders and gates but never widens — the first live plan had taken the whole
|
||||
catalogue along for a controller change; `plans stop`), 200. The first merge handled by the finished
|
||||
machinery, at 18:56Z, was a controller change and produced the plan this record describes: tier 0
|
||||
the build machine; tier 1 the controller and the proxy that packages its source. The handler
|
||||
returned at once; the build machine was built, rolled, and the plan read *tier 0 built; waiting for
|
||||
builder on novox to be applied* until the machine reported; then tier 1 was asked, both built, and
|
||||
the plan read done — three minutes, read through the console with `plans`, the receive loop taking
|
||||
reports throughout. What the day between decision and proof taught is in issues
|
||||
[184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md),
|
||||
[186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md) and
|
||||
[188](../04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md).
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0157](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
|
||||
- [Design 30 — The mesh updates itself on a push](../03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md)
|
||||
- Issues [184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md), [186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md), [187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-10-07
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md
|
||||
---
|
||||
|
||||
# 240. A module says how it is healthy, and the node-engine judges it
|
||||
|
||||
## Context
|
||||
|
||||
The release gate ([ADR 0236](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md)
|
||||
§2) judges a catalogue module on its first machine by what the mesh sees from outside: the build reported
|
||||
applied, no witness put it back, no condition raised since the send about the machine or the module there,
|
||||
and its tools served. None of that looks at what the module *runs*. To-be 45 names the gap in its Phase 4:
|
||||
*container state in the node-engine's report, without which a container that crash-loops after its compose
|
||||
applied is seen only through what it breaks.*
|
||||
|
||||
Measured on the live mesh for [research 032](../01-RESEARCH/032-a-module-says-how-it-is-healthy/00-overview.md)
|
||||
([evidence](../01-RESEARCH/032-a-module-says-how-it-is-healthy/01-evidence.md)), 2026-10-07:
|
||||
|
||||
- **125 catalogue modules; 68 run something long-lived** — 49 a container that stays up, 19 a service
|
||||
unit stated `running` and no such container. 50 run only their bundle in the node tools, 7 only files,
|
||||
directories and packages.
|
||||
- **No manifest can declare a health check, and nothing reads one.** The node-engine's report carries no
|
||||
container or unit state; no probe of the self-check reads one.
|
||||
- **19 of 73 long-running catalogue containers have an image that ships a check** (7 of 45 container
|
||||
modules). The mesh never reads it. **Two of those were wrong in the mesh's hands**: the studio and the
|
||||
flow editor read *unhealthy* while working, because their checks ask an address the program does not
|
||||
bind in the mesh's configuration. Read without proof, they would have rolled back two good builds.
|
||||
- **Every restart count is 0**, because a recreate loses it; the runtime's event history on the home
|
||||
server is about a minute long, pushed out by its own check executions.
|
||||
- **Of seven recent incidents**, liveness alone would have caught the agent server's crash loop (about a
|
||||
hundred restarts, found by a person reading its log for something else); an HTTP readiness check the
|
||||
web application that accepted TCP and answered nothing for eleven hours
|
||||
([issue 145](../04-ISSUES/145-a-machine-reads-healthy-while-its-modules-cannot-reach-each-other/00-report.md));
|
||||
only the module's own check the identity provider's refused administrator
|
||||
([issue 179](../04-ISSUES/179-an-adopted-identity-providers-admin-never-took-the-minted-secret/00-report.md)).
|
||||
A provisioner runtime that restarted until the overlay was up and then worked
|
||||
([issue 058](../04-ISSUES/058-a-provisioner-runtime-crash-loops-until-the-overlay-is-up/00-report.md))
|
||||
is the warning: a restart count without a start period reads churn that stops as a crash loop.
|
||||
- **One sample is not a finding.** A single unanswered question raised an urgent alert nobody could read
|
||||
([issue 277](../04-ISSUES/277-one-unanswered-question-was-an-urgent-alert-nobody-could-read/00-report.md));
|
||||
the self-check now raises such a finding on the second look (to-be 45 §4).
|
||||
|
||||
The mesh now rolls a module out on its own and puts the previous build back when the first machine is not
|
||||
healthy. That promise is only as good as "healthy" is, and for a module it is judged today from the outside.
|
||||
|
||||
**Checked against GENESIS.** *Failure must be loud* — a crash loop behind every passing check is work that
|
||||
reported success and did nothing. *Anything requiring a human to notice it will be noticed late* — eleven
|
||||
hours, and a hundred restarts, each found by a person. *Evidence over assertion* — "applied" is an
|
||||
assertion about a declaration; "it answers" is a measurement. *The mesh notices when something is wrong
|
||||
before you do* ([effect](../00-META/effect.md)). *Long-lived user services rather than an orchestrator*
|
||||
([context](../00-META/context.md)) is why this record judges and reports, and restarts nothing on health:
|
||||
an orchestrator's liveness restart is the part it does not take. *The mesh is a guest on a personal node*
|
||||
bounds the cost: the checks' floors below. Nothing here conflicts with GENESIS.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**Who runs the checks.**
|
||||
|
||||
1. *The container runtime's own check, read by the node-engine.* Rejected as the whole answer: containers
|
||||
only — nothing for the 19 service-only modules or anything only a module's tool knows; every look is an
|
||||
execution inside the container, which on the home server already pushes every lifecycle event out of
|
||||
the runtime's history; an image check the module never stated is a check nobody owns, and two of 19 were
|
||||
wrong. Kept as one *kind*, adopted by name and proved.
|
||||
2. *The module's own tool answers "healthy", and that is the judgement.* Rejected as the judge: a bundle is
|
||||
hosted by the node tools, so a tool that answers proves the bundle up, not the server; and a component
|
||||
that judges itself is what [ADR 0227](0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md)
|
||||
rule 8 refuses for the core. Kept as one kind, for function no endpoint shows, and only beside a check
|
||||
the module does not run itself.
|
||||
3. *The node-engine runs everything itself, including commands, on its own schedule.* Rejected in part: a
|
||||
command that only makes sense inside the container is better timed by the runtime's own retries and
|
||||
start period than by an execution per look from outside.
|
||||
4. **The node-engine owns every check and every verdict, and runs each kind where it is cheapest.**
|
||||
Chosen: HTTP, TCP and unit checks it makes itself; a command it hands to the runtime as that
|
||||
container's check and reads; a tool it asks through the node tools. One runner and one reader per
|
||||
machine, for every hosting form, and it keeps what the runtime forgets.
|
||||
|
||||
**Where the result goes.**
|
||||
|
||||
1. *Only a field of the report.* Rejected alone: a report follows an apply, so a container that goes bad at
|
||||
03:00 waits for the next one.
|
||||
2. *Only an event on each change.* Rejected alone: events are lost or replayed; an event is a sample, not
|
||||
a state.
|
||||
3. *A verb the controller calls per module when it judges.* Rejected: a pull per module per judging, and a
|
||||
machine slow to answer reads as unhealthy.
|
||||
4. **State in the report, its change on the bus, and a condition the controller raises on the second
|
||||
look.** Chosen. It is how the core already says its own state; the gate, the self-check, the healers
|
||||
([ADR 0231](0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md)) and
|
||||
the operator's conversation ([ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md)) all
|
||||
already act on conditions.
|
||||
|
||||
**What is judged.**
|
||||
|
||||
1. *Liveness only.* Rejected: no declarations needed, but it misses issue 145 (the port was open and the
|
||||
program running) and issue 179.
|
||||
2. *Readiness only, where declared.* Rejected: nothing is judged until every module has declared, and
|
||||
liveness alone would have caught the worst incident of the window.
|
||||
3. **Liveness for every long-running resource at once, readiness where declared, the declaration required
|
||||
over a migration.** Chosen: the gate means something from the first build.
|
||||
|
||||
**What is done with an unhealthy module.**
|
||||
|
||||
1. *Restart it, as an orchestrator's liveness probe does.* Rejected for this record: the evidence holds no
|
||||
case where a restart would have fixed anything — the crash loop was restarting already — and a restart
|
||||
hides the failure the gate is meant to see. Whether a healer restarts what stays unhealthy is left to a
|
||||
record of its own under ADR 0231.
|
||||
2. **Nothing restarts on health; the condition reaches a person or a healer.** Chosen.
|
||||
|
||||
**A provider down.** With 12 consumers of the database provision and 36 of a route:
|
||||
|
||||
1. *Ignore it.* Rejected: twelve conditions for one fault, and twelve gates failed for something none of
|
||||
them did.
|
||||
2. *Judge providers first, consumers only once their providers are healthy.* Rejected: a consumer broken on
|
||||
its own is not said while its provider is down, which is when it is most needed.
|
||||
3. **A consumer's check names the provision it exercises; while that provision's provider is unhealthy on
|
||||
the record, the consumer's finding is held under the provider's condition and its gate waits.** Chosen.
|
||||
It is how [issue 281](../04-ISSUES/281-a-tier-sent-one-module-at-a-time-blamed-a-module-for-its-machine/00-report.md)
|
||||
already treats a machine-level fault: what is the machine's is never pinned on a module.
|
||||
|
||||
**Where the declaration sits.**
|
||||
|
||||
1. *One per module.* Rejected: a module of eleven containers (the mail module) could not say which is wrong.
|
||||
2. **On each long-running resource**, beside the resource's other fields. Chosen.
|
||||
|
||||
**The migration.**
|
||||
|
||||
1. *Required at once.* Rejected: 68 modules to change before the next merge, and nothing judged until all
|
||||
are.
|
||||
2. *Optional for ever.* Rejected: 38 of 45 container modules would stay at liveness, and a field that is
|
||||
optional for ever is a field half the catalogue never gets.
|
||||
3. **Liveness at once; the declaration required by a date, counted down.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. Liveness is judged for every long-running resource, with no declaration.** A container that stays up,
|
||||
a process that stays up and a service stated `running` are *alive* when running and not restarted more than
|
||||
once within the settle window after their grace period. The node-engine observes this itself on each tick
|
||||
and keeps the restarts it counted across recreates and across its own restarts; it never relies on the
|
||||
runtime's restart count or event history. A resource held still by an open maintenance step
|
||||
([ADR 0189](0189-the-store-keeps-what-the-records-name.md)) is neither alive nor dead: it is said as held,
|
||||
and judged again when the step ends.
|
||||
|
||||
**2. A module declares how each long-running resource is ready**, in a field named `health` on that
|
||||
resource: one kind — the image's own check adopted by name, an HTTP request to a declared endpoint and the
|
||||
status expected, a TCP connect to a declared endpoint, a command in the container, the unit's own
|
||||
readiness, or one of the module's tools — with an interval (default 30 s, **not under 10 s**), a timeout
|
||||
under the interval, a number of failing looks in a row before it is unhealthy (default 3, **not under 2**),
|
||||
and a grace period after a start (default 60 s), in which failure does not count. The grace and the failing
|
||||
looks together are at most five minutes, so a resource broken from its start is said within the gate's
|
||||
bound. An endpoint is named by its `listens` name, never by a port or an address, so a check follows the
|
||||
machine's ports as the endpoint does. A check of function that no endpoint shows is a module's own tool, and
|
||||
only in addition to a check the module does not run itself.
|
||||
|
||||
**3. The node-engine runs every check and owns every verdict.** HTTP, TCP and unit checks it makes itself;
|
||||
a command in the container it hands to the runtime as that container's check and reads the state; an
|
||||
adopted image check it reads the same way; a tool it asks through the node tools. Nothing else on the machine
|
||||
judges a module, and nothing else sets a container's check.
|
||||
|
||||
**4. The state goes in the report, its change on the bus, and a condition is the controller's.** Each
|
||||
report carries, per module and long-running resource, a state — healthy, unhealthy, starting, held,
|
||||
unknown — since when, the failing streak and the restarts counted; each change is emitted as an event, and a
|
||||
state that is not healthy is said again while it lasts. The controller keeps the last state per machine,
|
||||
raises `module.<module>.<machine>.unhealthy` when two consecutive statements say so, and clears it on the
|
||||
first that does not. **The gate needs no new rule:** ADR 0236 §2's *a module's own health holds* now reads
|
||||
the module's stated health — a judging is healthy only when every long-running resource of the module on that
|
||||
machine is healthy, so a resource still starting is not yet a pass — and its *no condition raised since the
|
||||
send* holds the module on this condition. Every start begins in `starting`, so a condition from before the
|
||||
send clears at the new build's start and anything after it is the new build's. At the gate's bound the
|
||||
build is put back, as ADR 0236 §3 says.
|
||||
|
||||
> **The mechanism changed — 2026-10-10, by [ADR 0283](0283-a-part-that-waits-for-the-operators-secret-or-setting-is-said-as-waiting-passes-the-first-node-gate-and-names-the-act.md).** The four states, the
|
||||
> condition and the gate's reading stand. A fifth state, `waiting`, now sits beside them: a tool check
|
||||
> (rule 2) may answer *waits* as well as healthy or not, naming an own secret or a setting the operator has
|
||||
> yet to give, and one look that says so is enough, inside the grace too. The controller checks the wait,
|
||||
> says it as `needs-operator` rather than `unhealthy`, and the gate reads it as ADR 0254's *waits for a
|
||||
> person*: a pass.
|
||||
|
||||
**5. A provider down is said once, at the provider.** A check names the provision it exercises. While
|
||||
that provision's provider for this consumer is unhealthy on the record, the consumer's finding is held under
|
||||
the provider's condition — listed there as waiting on it, raised as nothing of its own — and the consumer's
|
||||
gate waits rather than fails. What a consumer finds while its provider is healthy is its own.
|
||||
|
||||
> **The mechanism changed — 2026-10-11, by [ADR 0292](0292-a-consumer-of-the-part-a-provider-waits-on-is-held-under-the-wait-and-passes-its-gate-as-a-wait-for-a-person.md).** The hold under an unhealthy provider, and its consumer's gate
|
||||
> waiting, stand. A provider that only waits for the operator (ADR 0283) now holds the consumers of a provision one
|
||||
> of its waits covers, listed at its `needs-operator` condition, which stays a warning; their gate passes as a wait
|
||||
> for a person, carrying the wait. A consumer no wait covers is said on its own.
|
||||
|
||||
**6. Nothing is restarted for being unhealthy.** The runtime restarts what exits, as now. A condition
|
||||
reaches a person through the operator's conversation, or a healer; a healer that restarts on health is its
|
||||
own decision.
|
||||
|
||||
**7. A declaration is proved before it is trusted.** `module check` refuses a `health` field that names an
|
||||
endpoint the module does not declare, an interval, timeout or count outside its bounds, or a tool the module
|
||||
does not serve. A bed from mesh-lab, run by the catalogue's check on the build seat, starts every resource
|
||||
whose declaration or image changed and requires its check to say healthy within its grace; an image check
|
||||
adopted by name is proved the same way, because two of nineteen were wrong. This proves the check's
|
||||
wiring — that it can see the program working — not the change, which the live mesh and the first machine's
|
||||
gate still judge ([ADR 0149](0149-the-live-mesh-is-the-test-bed.md) stands).
|
||||
|
||||
**8. Every catalogue module that runs something long-lived declares one.** A catalogue-wide count of the
|
||||
long-running resources without `health` may only go down. `module check` warns from this decision, and
|
||||
refuses a long-running resource without `health` once the count reaches zero, or six weeks after liveness is
|
||||
first judged live, whichever is first. A module running nothing long-lived — its bundle only, or files and
|
||||
packages — declares none: the node tools serving its tools is its liveness, as the gate judges today.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Liveness alone, from the first build, would have caught the crash loop inside the gate's ten minutes.**
|
||||
Readiness would have caught the silent web application in about a minute instead of eleven hours. The
|
||||
identity provider's refused administrator needs the module's own tool, which it already has (ADR 0224 §5).
|
||||
- **The node-engine grows a small scheduler, a field of its report and an event; the controller a condition
|
||||
kind and its two-look rule; the gate a reading of state it already had a place for.** A machine of 45
|
||||
containers spends tens of milliseconds a look reading state, and about 3 s a minute on in-container
|
||||
commands at the default interval.
|
||||
- **A module whose first machine was unhealthy before the send no longer passes the gate for being
|
||||
unchanged in its unhealthiness** — the start resets it to `starting`, and the new build is judged on its
|
||||
own.
|
||||
- **What a check names becomes load-bearing.** A check naming the wrong provision hides a consumer's own
|
||||
fault under its provider; the bed and the first machine's gate are what catch that.
|
||||
- **Harder:** a slow starter — a module that needs more than five minutes to be ready — cannot say so
|
||||
within these bounds and fails its gate. That is accepted until one exists; it would be a change to the
|
||||
bound, recorded.
|
||||
- **A provider's health and its consumers' failures meet in two places**: this record's condition (the
|
||||
provider's resource is unhealthy) and ADR 0224's standing (the provider keeps failing a consumer). They
|
||||
are different facts, both kept; the hold of rule 5 reads only this record's.
|
||||
- **Data is not health.** Whether a module's data is there, measured and backed up stays
|
||||
[ADR 0233](0233-a-module-declares-the-data-it-holds-and-the-mesh-protects-and-watches-it-from-that.md)'s,
|
||||
watched by the self-check's D13.
|
||||
|
||||
## What this does not decide
|
||||
|
||||
- Whether a healer restarts what stays unhealthy (rule 6 leaves it to its own record, under ADR 0231).
|
||||
- Health for scheduled work — whether the last scheduled run succeeded belongs to the record of the
|
||||
scheduled step.
|
||||
- Health of what a module's events do ([issue 276](../04-ISSUES/276-a-handler-that-did-its-work-was-offered-it-five-times/00-report.md))
|
||||
— the event contract's, and the bus watchdog's.
|
||||
- The core's own definitions (ADR 0236 §1, to-be 45 §8), which stand; a core component may later declare its
|
||||
own through the same field.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| 1 Liveness without declaration | node-engine tests over a fake runtime and service manager: a container recreated keeps its counted restarts, and so does the engine restarted; a restart inside grace is not counted; two restarts within the settle window after grace make it unhealthy; a resource under a maintenance step is held. A mesh-lab replay of the crash loop (a container whose program exits at start) fails its gate within the bound |
|
||||
| 2 The field and its bounds | `module check` refuses each out-of-range part and each endpoint named by port or address, a test per refusal; a catalogue-wide test parses every `health` field |
|
||||
| 3 The engine owns the verdict | a node-engine test that a declared command becomes the container's check and nothing else sets one; a test that an HTTP check dials the endpoint's current port after a port change |
|
||||
| 4 Report, event, condition, gate | controller tests: one unhealthy statement raises nothing and is listed unconfirmed; two raise; a healthy one clears; the gate fails a judging while a resource is starting or unhealthy and holds a module on this condition (the existing gate test, extended); a condition from before the send clears at the new build's start. A replay of issue 145 — a database made unreachable — raises the consumer's condition within two looks |
|
||||
| 5 Said once at the provider | a controller test with one unhealthy database provider and three consumers failing: one condition, at the provider, the consumers listed as waiting; their gates wait, not fail; a consumer failing while its provider is healthy is raised on its own |
|
||||
| 6 No restart on health | a node-engine test that an unhealthy container is not restarted, recreated or stopped |
|
||||
| 7 Proved before trusted | the bed's run of every changed declaration in the catalogue's check; the replay of the studio's false *unhealthy* (a check that asks an address the program does not bind) fails the bed, not a machine |
|
||||
| 8 Every long-running module declares | the catalogue-wide count of long-running resources without `health`, compared with the number kept in the catalogue: a merge may lower it and never raise it; after the date, `module check` refuses |
|
||||
| live | every machine's report carries a state for every long-running resource; `conditions` raises `module.<module>.<machine>.unhealthy` for a module stopped on purpose and clears it when it runs |
|
||||
|
||||
## References
|
||||
|
||||
- [Research 032](../01-RESEARCH/032-a-module-says-how-it-is-healthy/00-overview.md) — the evidence, the
|
||||
options and the recommendation this record takes.
|
||||
- [To-be 48](../03-DESIGN/01-to-be/48-a-module-says-how-it-is-healthy.md), the design.
|
||||
- [ADR 0236](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md)
|
||||
§2, whose module health this gives a content;
|
||||
[ADR 0227](0227-the-core-holds-nine-rules-each-checked-and-is-built-to-them-in-six-phases.md) rules 5, 6
|
||||
and 8 and [to-be 45](../03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md) §2, §4 and §8 — the
|
||||
condition, the second look, the witness that is never the component;
|
||||
[ADR 0224](0224-a-provider-that-keeps-failing-a-consumer-is-a-problem-the-controller-reports.md),
|
||||
[ADR 0231](0231-a-healer-acts-on-what-observation-raised-and-only-observation-says-it-worked.md),
|
||||
[ADR 0233](0233-a-module-declares-the-data-it-holds-and-the-mesh-protects-and-watches-it-from-that.md),
|
||||
[ADR 0234](0234-the-mesh-holds-a-conversation-with-its-operator.md),
|
||||
[ADR 0189](0189-the-store-keeps-what-the-records-name.md),
|
||||
[ADR 0149](0149-the-live-mesh-is-the-test-bed.md),
|
||||
[ADR 0237](0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md).
|
||||
- Issues 058, 145, 179, 181, 276, 277, 281.
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-08
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
supersedes-in-part:
|
||||
- 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
|
||||
extends: 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
|
||||
---
|
||||
|
||||
# 249. A delivery group's order rules have a precedence, and a declared order outranks them all
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)
|
||||
decision 4 orders a delivery group's members by a line `after: <repository>` and by three inferred rules:
|
||||
*built by* (a member that moves a module goes before a member whose modules are built by it or stand on
|
||||
it), *version skew* (the controller before a manifest change) and *engine before controller* (the
|
||||
node-engine before the controller, from ADR 0236's order for its walks). It says nothing about two rules
|
||||
ordering one pair in opposite directions. The planner treated every such pair as a cycle, and a declared
|
||||
order against an inferred one as a cycle too.
|
||||
|
||||
On 2026-10-08 the group `feat/a-machine-joins-through-the-tunnel` held three pull requests, each `ready`
|
||||
on its own: the controller's, the node-engine's and the lab's
|
||||
([issue 309](../04-ISSUES/309-two-order-rules-ordered-one-pair-both-ways/00-report.md)). The controller's
|
||||
repository also holds the build agent, and its pull request moved it. Every source-built module is built
|
||||
by the build agent, so *built by* put the controller's member first. *Engine before controller* put the
|
||||
node-engine's member first. The group was rejected: "its order contradicts itself", naming the two
|
||||
members and neither rule. The person merged it by hand, the controller first, since that member only added
|
||||
a verb the new node-engine calls. That is the order *built by* gave, and the order
|
||||
[ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md) asks for: a verb is promised before
|
||||
it is required.
|
||||
|
||||
The rules are not of one kind. *Built by* and *version skew* are read from the graph and from what the
|
||||
change does: which module builds which, and whether a manifest changes. *Engine before controller* is
|
||||
read from two module names alone. It holds when the controller starts sending something the node-engine
|
||||
must first accept. It is wrong when the node-engine starts asking for something the controller must first
|
||||
serve, as here. The rule cannot tell the two apart.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **A precedence among the rules, and a declared order above them all.** Chosen.
|
||||
2. **Narrow *built by* so it never applies through the build agent to the node-engine.** It would end
|
||||
this case. But a builder that changes how the node-engine is built does have to go first, and the next
|
||||
pair of rules that disagree would be refused the same way. Rejected.
|
||||
3. **Drop *engine before controller*.** ADR 0236's reason still holds when the node-engine is what must
|
||||
accept something new. Rejected.
|
||||
4. **Keep refusing, and name the two rules.** Then every group that moves the controller's repository and
|
||||
the node-engine together is refused, and the person must still merge by hand. A declared order could not
|
||||
help, because a declared order against an inferred one was also a cycle. Rejected.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **When rules order one pair of members in opposite directions, the rule of higher precedence decides.**
|
||||
From highest to lowest:
|
||||
- *declared*: an `after:` line a person wrote;
|
||||
- *built by* and *version skew*: what the graph and the change say;
|
||||
- *engine before controller*: the default for walks, read from module names alone.
|
||||
2. **A declared order outranks every inferred rule.** This replaces ADR 0239 decision 4's sentence "a
|
||||
declared order that contradicts an inferred one is a cycle". A person who knows the order the rules
|
||||
cannot see writes one line, and the group follows it.
|
||||
3. **The order says which rule won.** Each pair carries its rule and the rules it won over. The delivery's
|
||||
`show`, `groups` and plan note print them, for example "the controller's member before the node-engine's:
|
||||
built by, over engine before controller".
|
||||
4. **Rules of one rank in opposite directions are still refused.** Two `after:` lines naming each other,
|
||||
or *built by* both ways, have no precedence between them. The group is rejected. The reason names both
|
||||
members, both rules and the direction each gives, and says how to declare the order. A cycle through
|
||||
three members or more is refused naming the pairs in it.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The group of issue 309 is ordered without a person: the controller's member first.
|
||||
- A group can no longer be refused because a person's `after:` disagrees with an inferred rule. The person
|
||||
can now overrule *version skew*, and a manifest change merged before the controller that parses it fails
|
||||
at the gate, not silently. The rule the person overruled is printed on the pair.
|
||||
- *Engine before controller* now decides only where nothing in the graph or the pull requests does.
|
||||
- The ranks are a list in the controller's planner. A new inferred rule must be given a rank when it is
|
||||
added.
|
||||
|
||||
**How it is checked.** The controller's tests `TestAnOrderRuleYieldsToAStrongerOne` and
|
||||
`TestAGroupIsOrderedByTheGraphAndWhatItsPullRequestsSay` check that *built by* wins over *engine before
|
||||
controller*, that a declared line wins over both and over *version skew*, that each pair names what it won
|
||||
over, and that equal-rank rules both ways are refused with both rules named and the `after:` lines to
|
||||
write. mesh-delivery's `TestAGroupSaysWhichOrderRuleWonOrWhyNoneDid` and
|
||||
`TestACycleWithoutAContradictionNamesItsPairs` check that the refusal and the won-over rules reach
|
||||
`groups` and the group's state. The replay R309 in mesh-lab's register fails before the fix and passes on it.
|
||||
|
||||
## References
|
||||
|
||||
- [Issue 309](../04-ISSUES/309-two-order-rules-ordered-one-pair-both-ways/00-report.md), its report and
|
||||
diagnosis.
|
||||
- [ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)
|
||||
decision 4, the sentence this replaces.
|
||||
- [ADR 0236](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md),
|
||||
*Rollout order*, where *engine before controller* comes from.
|
||||
- [ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md), a verb promised before it is
|
||||
required.
|
||||
- [Design 47](../03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md), *A delivery group*.
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-10
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
supersedes-in-part:
|
||||
- 02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md
|
||||
- 02-DECISIONS/0218-a-plan-sends-grants-before-code-rolls-out-one-machine-first-and-a-newer-merge-takes-over-an-older-plan.md
|
||||
- 02-DECISIONS/0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md
|
||||
- 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
|
||||
extends: 02-DECISIONS/0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md
|
||||
---
|
||||
|
||||
# 276. Merges are assembled in a rolling window, and each batch is delivered by one walk with one commit per repository
|
||||
|
||||
> **Superseded in part — 2026-10-10, by [ADR 0282](0282-a-fix-to-a-leaf-module-runs-healthy-on-every-node-within-five-minutes-of-its-merge-and-a-core-module-within-ten.md).**
|
||||
> Decision 4's "one walk at a time", and decision 1's window for every merge, no longer hold: a lone merge is
|
||||
> cut at once, and walks that share no repository, module or edge of the module graph run at once, one
|
||||
> first-node judgement per node at a time. Two walks of one repository never at once, no walk superseded once
|
||||
> it started, and the window for a merge that waits for its delivery group or arrives while a batch assembles
|
||||
> stand as decided here.
|
||||
|
||||
## Context
|
||||
|
||||
Shortly after midnight on 2026-10-10 two pull requests of the catalogue were merged 18 seconds apart, the
|
||||
claude-code change at 00:20:52 and the dunst change at 00:21:09
|
||||
([issue 362](../04-ISSUES/362-two-merges-seconds-apart-left-their-delivery-waiting-for-a-person/00-report.md)).
|
||||
The controller opened one walk for each merge, as [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md)
|
||||
decided ("a merge produces a tiered plan"). The second walk superseded the first, as
|
||||
[ADR 0218](0218-a-plan-sends-grants-before-code-rolls-out-one-machine-first-and-a-newer-merge-takes-over-an-older-plan.md)
|
||||
decision 3 decided, and both were planned at the dunst commit, as the fix for
|
||||
[issue 349](../04-ISSUES/349-a-merge-acted-on-late-superseded-the-merge-after-it-and-built-the-older-commit/00-report.md)
|
||||
decided. mesh-delivery then tied the surviving walk to the dunst delivery, which the supersession had already
|
||||
ended, and tied no walk to the claude-code delivery. Nothing gave the walk its word. The change waited 58
|
||||
minutes, until the operator started the walk by hand with `plans go`.
|
||||
|
||||
The diagnosis found two parts that each behave as written and disagree on what a walk's commit means. That
|
||||
is the narrow cause. The wide one is that the mesh plans one walk per merge, while merges arrive in bursts:
|
||||
|
||||
- **A burst is the normal case.** The operator merges a batch of reviewed pull requests at once, often
|
||||
across repositories (a catalogue change with the controller change it needs). Every merge opened a walk,
|
||||
and every walk after the first superseded the one before. Three supersession paths now exist (ADR 0218,
|
||||
issue 254, issue 349), each with its own bookkeeping, and issue 362 is a fourth way for them to disagree.
|
||||
- **A walk superseded mid-gate wastes the gate and confuses its record.** A first machine judged healthy
|
||||
for a build that the next walk replaces a minute later proves nothing about the build that stays. On
|
||||
2026-10-09 a controller merge landing while another walk was at its gate rolled everything back; the rule
|
||||
"one controller rollout at a time" was kept since then by the person merging, not by the mesh.
|
||||
- **ADR 0239 made a delivery "one commit … from commit to delivered" and walked it alone**, while its own
|
||||
decision 4 delivers a delivery group across repositories as one unit. The walk the commit gets is
|
||||
decided by when the controller heard its merge, which is not something the person merging controls.
|
||||
|
||||
On 2026-10-10 the operator decided the model, in their words:
|
||||
|
||||
> "a delivery doesn't really 'run from commit to delivered', we've already discussed that a delivery plan
|
||||
> might start with a set of commits spanning multiple repo's, however, only 1 commit per repo is allowed
|
||||
> … we assemble all commits that are coming in with a 1 minute rolling window, so if a new commit comes
|
||||
> in, less then 1 or 2 minutes after the last one, the builder again waits 1 or 2 minutes, until no new
|
||||
> commit came in, then it looks at the 'group of commits' and decides if they all fit into 1 delivery
|
||||
> plan … This is a hard problem to tackle, but it will improve our build setup I believe."
|
||||
|
||||
> "the 'actively assembling plan' is very interesting, the controller can say 'Still waiting for 13
|
||||
> seconds for new commits, these are the currently grouped commits, plan is yet to be calculated'."
|
||||
|
||||
On two commits of one repository in one window, the operator chose the later commit over splitting:
|
||||
"i like your idea better, I just thought it would be more complex, but if it's reasonably doable, go for
|
||||
it".
|
||||
|
||||
**Checked against GENESIS.** *Failure must be loud*: an assembling batch and a waiting walk each have a
|
||||
bound and a condition. *The mesh owns the truth*: the window, the batch and the walk are kept by the
|
||||
controller and shown by its `plans`, not inferred from timing by a person. Nothing here conflicts with
|
||||
GENESIS.
|
||||
|
||||
**Words.** The glossary had no word for the merges one window collects, nor for the window. This record
|
||||
adds **merge window** and **batch** to it (Change and delivery), with this record. *Group* is not used for
|
||||
either, because **delivery group** already names deliveries that share a head branch name.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**What a walk answers.**
|
||||
|
||||
1. **One walk per merge, as today, with the bookkeeping repaired.** The walk names the merges it answers,
|
||||
and mesh-delivery ties a superseding walk to every live delivery it contains. Rejected as the model,
|
||||
kept as a repair (decision 8): it makes the 362 shape deliver, but every burst still opens and
|
||||
supersedes walks, a walk can still be superseded at its gate, and a cross-repository change is still
|
||||
walked as separate walks in whatever order the merges were heard.
|
||||
2. **A rolling merge window, and one walk per batch** (chosen). Merges are collected while they keep
|
||||
arriving; the batch they form is planned once, as one walk with at most one commit per repository.
|
||||
3. **Every walk started by hand.** The operator named it as the alternative ("this does mean we need to
|
||||
manually start all delivery plans, or, even better, …"). Rejected: a person in every delivery is the
|
||||
wait ADR 0239 exists to remove, and a merge that passed its checks must reach its machines without a
|
||||
person.
|
||||
|
||||
**Two commits of one repository in one batch.**
|
||||
|
||||
1. **Split into two walks in merge order** (the operator's first proposal). The second walk waits for the
|
||||
first. Rejected: every machine running the module builds, is sent and is judged on the earlier commit,
|
||||
and then again on the later one minutes after; the gate judges a build that is about to be replaced;
|
||||
and the batch's walk stops being one unit.
|
||||
2. **One walk with the later commit** (chosen). On a linear trunk the later merge commit contains the
|
||||
earlier one, so building it delivers both. One build, one gate, one rollout. The earlier merge is
|
||||
answered by its successor and ends `covered` when the walk is delivered. Only when that walk fails are
|
||||
the earlier merges walked on their own, to find which one brought the failure.
|
||||
|
||||
**Who keeps the window.**
|
||||
|
||||
1. **mesh-delivery**, which owns deliveries (ADR 0239). Rejected: a walk on the controller's own path (the
|
||||
controller, the node-engine, the node tools, the bus, mesh-delivery itself) must not wait for
|
||||
mesh-delivery, and with no holder of the seat on record the controller plans alone. The window would
|
||||
then exist twice, once in each.
|
||||
2. **The controller** (chosen). It hears every merge (the bus, and the catch-up that reads back what the
|
||||
bus missed), it owns the planner and the walk, and it already plans alone for its own path. mesh-delivery
|
||||
keeps what it owns: each commit's delivery record, the order of a delivery group, and the word that lets
|
||||
a waiting walk start.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **A rolling merge window.** The controller collects every merge into a branch a module follows into the
|
||||
open **batch**. The window is rolling: each merge restarts it. It closes when no merge arrived for the
|
||||
window's length, 90 seconds by default (the controller's setting `merge-window`), and at the latest 10
|
||||
minutes after the batch's first merge (`merge-window-at-most`), so a busy period never starves delivery.
|
||||
The batch, its merges and its times are kept in the controller's store, so a restarted controller
|
||||
resumes the window where it stood rather than losing or doubling a merge.
|
||||
2. **A batch is planned once, as one walk with at most one commit per repository.** When the window closes,
|
||||
the planner computes one delivery plan for the batch: every module its commits move, their dependents,
|
||||
in tiers along the module graph, across repositories. For a repository with two or more merges in the
|
||||
batch the walk takes the latest commit of its branch, which contains the earlier ones. **Every merge
|
||||
belongs to exactly one walk**: the walk names every merge it answers, its own commits and the earlier
|
||||
ones they contain. A merge is never dropped and never walked twice.
|
||||
3. **The earlier merge of a repository is answered by its successor.** Its delivery follows the walk and
|
||||
ends `covered` when the walk is delivered, naming the merge that carried it (the glossary's word,
|
||||
[ADR 0270](0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md);
|
||||
here the containment is the trunk's order inside one batch, so the forge is not asked). **When that walk
|
||||
fails**, the walk's own commits fail, and each earlier merge it answered is walked on its own commit, the
|
||||
newest first, one walk each, in the next batches: the first that is delivered ends `delivered`, and the
|
||||
search stops there. A walk that failed never marks a merge delivered.
|
||||
4. **One walk at a time, and nothing supersedes a walk that started.** A batch becomes a walk only when no
|
||||
walk is open. Merges that arrive while a walk runs join the next batch, whatever its window says. A walk
|
||||
that has not started, because it waits for mesh-delivery's word, is folded into the next batch when that
|
||||
batch is planned, and the new walk answers every merge the folded one answered. This is the only
|
||||
supersession left; ADR 0218 decision 3 and the issue 349 rule of planning a late merge at a later commit
|
||||
are replaced by it. A merge heard after a later merge of its repository and branch was put in a walk is
|
||||
answered by that walk: by the batch still assembling, by the open walk, or, when that walk was delivered,
|
||||
it ends `covered` at once.
|
||||
|
||||
> **The mechanism changed — 2026-10-10, by [ADR 0282](0282-a-fix-to-a-leaf-module-runs-healthy-on-every-node-within-five-minutes-of-its-merge-and-a-core-module-within-ten.md).**
|
||||
> What stands: the window for a merge whose delivery group has a member not yet merged or that arrives while a
|
||||
> batch assembles (decision 1), two walks of one repository never at once, and no walk superseded once it
|
||||
> started. What moved: a lone merge is cut at once without the window, and walks that share no repository,
|
||||
> module or module-graph edge run at once, one first-node judgement per machine at a time. Until ADR 0282's
|
||||
> second and fourth slices are built, the window for every merge and one walk at a time hold.
|
||||
5. **The order of a delivery group holds inside a walk** (ADR 0249 unchanged). The pairs its order rules
|
||||
give (declared, built by, version skew, engine before controller, each with the rule that won) become
|
||||
edges of the walk's tiers beside the module graph: the modules of a member ordered after another are in
|
||||
a later tier than that member's. A contradiction still rejects the group before anything merges.
|
||||
mesh-delivery gives a walk its word only when every delivery it carries is at its turn.
|
||||
6. **A delivery is one commit's record, and its walk is the batch's.** A delivery keeps its pull request,
|
||||
its status, its note and its check, as ADR 0239 decided. From its merge on it is delivered by the walk
|
||||
that answers it, and every delivery a walk carries ends with that walk's result: `delivered` (its own
|
||||
commit) or `covered` (an earlier commit of the same repository) when the walk is done, `failed` when it
|
||||
fails, `stopped` when it is stopped. The walk's machine steps are recorded on each delivery it carries.
|
||||
7. **The assembling state is shown.** The controller's `plans` lists the open batch first, as the operator
|
||||
asked: "assembling: 13 s left (at the latest 00:31:09); grouped: novox/mesh-catalog@48bda475 (answers
|
||||
553b7191), novox/mesh-controller@a6bc0931; plan not yet calculated". It says the batch on the bus as
|
||||
`plan-moved` with the state `assembling`, so mesh-delivery's `deliveries` and `show` say of a merged
|
||||
delivery that its batch is assembling and how long is left, and of one whose batch waits that it waits
|
||||
behind which walk.
|
||||
8. **The bookkeeping is repaired as well** (the narrow cause of issue 362), because decision 4 keeps one
|
||||
supersession path. A walk names every merge it answers, and a superseded walk names the walk that took it
|
||||
over. mesh-delivery ties a walk to every live delivery whose merge it answers, follows a walk to the one
|
||||
that took it over, and **never ties a walk to a delivery that has ended**. A walk waiting for the word
|
||||
that no live delivery holds is either adopted as a delivery of its own or listed at once by `stalled`.
|
||||
9. **Every wait has a bound and a condition.** A batch still assembling a minute past
|
||||
`merge-window-at-most` while no walk is open raises a condition: the controller failed to cut it. A
|
||||
batch waiting behind an open walk longer than that walk's bound raises one naming the walk. A walk
|
||||
waiting for its word past 30 minutes is S16's, as before, and its words name the merges it answers. A
|
||||
merged delivery no walk answers within ten minutes is held with the true reason (which open walks of its
|
||||
repository exist and that none names its merge, or that the controller holds none) and listed by
|
||||
`stalled` at once, so probe D14 raises it for the operator.
|
||||
|
||||
> **Decided during the build, 2026-10-10** (on the operator's delegation). Decision 2 batches every merge of the
|
||||
> window together, and decision 4 starts a walk on the controller's own path without mesh-delivery's word. A
|
||||
> batch holding both a merge on that path and a catalogue merge would therefore start the catalogue's deliveries
|
||||
> without their word: their turn in a delivery group, their holds, and the order of decision 5 would be skipped.
|
||||
> So **a merge on the controller's own path never shares a batch with a merge that waits for the delivery's
|
||||
> word.** Such a merge goes into a batch of its own, with its own window; the open catalogue batch closes and is
|
||||
> cut by the normal rules, and queues behind the own-path walk like any other. Catalogue merges heard during the
|
||||
> own-path batch's window join the next catalogue batch. An own-path walk being cut folds no catalogue walk that
|
||||
> waits for its word: that walk's merges are batched again behind it, and the walk names that batch as the one
|
||||
> that took it over, so no catalogue delivery ever skips its turn. Checked by the controller's test
|
||||
> `TestAMergeOnTheControllersPathNeverSharesABatch`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **A merge waits up to 90 seconds before it is planned**, and up to 10 minutes in a busy period. A walk
|
||||
takes minutes to build and longer to judge, so the wait is small beside it, and it removes the builds,
|
||||
sends and gates that a superseded walk wasted.
|
||||
- **A cross-repository change merged within a window is walked as one unit**, tiered by the graph and its
|
||||
group's order. A person no longer has to merge in order and wait in between.
|
||||
- **One walk at a time is mechanical.** A controller merge can no longer land in the middle of another
|
||||
walk's gate; it waits for the next batch.
|
||||
- **A failing walk fails every delivery it carried**, including one whose own modules passed before the
|
||||
failure. Each delivery's machine steps say how far it got, and decision 3's search finds the culprit only
|
||||
among the merges of one repository. A failure in a walk that carried several repositories is the
|
||||
operator's to read. This is the price of one unit, and the operator chose it.
|
||||
- **What got harder**: the controller's merge handling holds state across merges (the open batch) and is
|
||||
restart-safe; the walk's record names several repositories and commits instead of one, and every reader of
|
||||
it (`plans`, S3, S16, `plan-moved`, mesh-delivery) changes with it. The store gains the batch and the
|
||||
merges a walk answers, as a numbered migration and in the consolidated schema.
|
||||
|
||||
> **Progressive insight — 2026-10-10.** The controller has no consolidated schema file: a fresh install runs its
|
||||
> numbered migrations from the first, embedded in the build, so the numbered migration (0089) is the whole of
|
||||
> the schema change. The bullet above said "as a numbered migration and in the consolidated schema"; that second
|
||||
> half describes mesh-delivery's kind of store, not the controller's. The decision stands.
|
||||
- **The replay of issue 362** in the lab raises a mesh, merges two commits of one repository 18 seconds
|
||||
apart, and passes when one walk delivers both without a person; it fails on the commit before the change.
|
||||
|
||||
## How it is checked
|
||||
|
||||
- The controller's tests: two merges of one repository 18 seconds apart make one batch and one walk at the
|
||||
later commit, answering both; merges in three repositories within the window make one walk; a merge at
|
||||
second 80 restarts the window; the window closes at `merge-window-at-most` however busy; a merge arriving
|
||||
while a walk runs joins the next batch; a walk that has not started is folded into the next one; `plans`
|
||||
shows the assembling state; the batch survives a restart; the two conditions of decision 9 are raised past
|
||||
their bounds and not before.
|
||||
- mesh-delivery's tests: both deliveries of one walk end `delivered` and `covered` with its done record; a
|
||||
failed walk marks none delivered and walks the earlier merge alone; a walk is never tied to an ended
|
||||
delivery; a walk is given its word only when every delivery it carries is at its turn; a group's order is
|
||||
unchanged; a held delivery with no walk names the true reason and is listed by `stalled`.
|
||||
- Each of those tests fails with its part of the change removed.
|
||||
- mesh-lab's replay R362 (decision's consequence above), proved to fail before the change and pass after it.
|
||||
|
||||
## References
|
||||
|
||||
- [Issue 362](../04-ISSUES/362-two-merges-seconds-apart-left-their-delivery-waiting-for-a-person/00-report.md),
|
||||
its diagnosis, and the replay R362 in mesh-lab.
|
||||
- [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) (a merge produces a plan),
|
||||
[ADR 0218](0218-a-plan-sends-grants-before-code-rolls-out-one-machine-first-and-a-newer-merge-takes-over-an-older-plan.md)
|
||||
decision 3 (a newer merge takes over an older plan), [ADR 0238](0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md)
|
||||
decision 2 (one commit, one change plan), [ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md)
|
||||
(a delivery walked by its own commit's walk), each superseded in the part this record names.
|
||||
- [ADR 0249](0249-a-delivery-groups-order-rules-have-a-precedence-and-a-declared-order-outranks-them-all.md),
|
||||
whose order rules this record keeps and uses as tier edges.
|
||||
- [ADR 0270](0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md),
|
||||
whose `covered` ends an earlier merge of a repository.
|
||||
- [Issue 349](../04-ISSUES/349-a-merge-acted-on-late-superseded-the-merge-after-it-and-built-the-older-commit/00-report.md),
|
||||
whose rule decision 4 replaces.
|
||||
- To-be [47](../03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md) and
|
||||
[10](../03-DESIGN/01-to-be/10-delivery.md), amended with this record.
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-10
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md
|
||||
supersedes-in-part:
|
||||
- 02-DECISIONS/0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md
|
||||
---
|
||||
|
||||
# 282. A fix to a leaf module runs healthy on every node within five minutes of its merge, and a core module within ten
|
||||
|
||||
## Context
|
||||
|
||||
On 2026-10-10 the operator asked, in their words: "we definitely need to speed up our bug fixing process so a
|
||||
bugfix can land deployed and rolled out in less than 5 minutes". Issue 382 in the issue tracker (a small fix took
|
||||
45 minutes to reach a node) names the causes it saw: a 90-second merge window, one walk at a time, about four to
|
||||
five minutes per tier, and three judgement passes for every module. Issue 338 (a module built from a shared
|
||||
repository moves on every merge to it) names a fourth: every merge of the controller walks two catalogue modules
|
||||
in two more tiers.
|
||||
|
||||
### What was measured
|
||||
|
||||
Every walk the controller keeps (`delivery-walks`) records, per module, when its build was asked and finished,
|
||||
when its first node was sent it, when the first-node gate judged it and when the rest were sent. Read on
|
||||
2026-10-10 for the 51 walks that finished between 2026-10-09 13:55 and 2026-10-10 17:32 (UTC), each walk's time
|
||||
from its merge to its builds being sent to every node it targets splits into these phases. A walk is a **core**
|
||||
walk when it moves a module of the controller's own path (the controller, the node-engine, the tool runner, the
|
||||
bus, mesh-delivery) or the mesh's resolver, and a **leaf** walk otherwise. Nine walks that failed or were folded
|
||||
into another are not counted.
|
||||
|
||||
| Phase, in minutes | Leaf: median | Leaf: worst | Leaf: share | Core: median | Core: worst | Core: share |
|
||||
|---|---|---|---|---|---|---|
|
||||
| the merge window: merge to walk opened (a wait over 2.2 counted as 1.8 of window, the rest as waiting) | 0.5 | 2.1 | 9 % | 0.4 | 2.1 | 7 % |
|
||||
| waiting behind another walk | 0.3 | 59.1 | 47 % | 0.0 | 1.8 | 1 % |
|
||||
| build | 0.4 | 1.6 | 5 % | 1.2 | 4.1 | 14 % |
|
||||
| built to sent to the first node | 0.3 | 0.6 | 3 % | 0.5 | 3.0 | 5 % |
|
||||
| first-node judgement, the longest of each tier | 2.6 | 6.3 | 27 % | 7.2 | 14.9 | 63 % |
|
||||
| first nodes of one tier judged one after another | 0.0 | 4.2 | 8 % | 0.0 | 0.0 | 0 % |
|
||||
| judged to sent to the rest | 0.0 | 0.9 | 2 % | 0.5 | 5.1 | 6 % |
|
||||
| between tiers | 0.0 | 0.6 | 0 % | 0.0 | 1.0 | 3 % |
|
||||
| **merge to sent to every node** | **6.0** | **66.0** | | **9.7** | **20.5** | |
|
||||
|
||||
- **Leaf walks** (20): median 6.0 minutes, 90th percentile 14.8, worst 66.0; **7 of 20 within five minutes**.
|
||||
The worst is the incident of issue 362 (58 minutes waiting for a person's word), which ADR 0276 already
|
||||
repaired; without it the worst wait behind another walk is 13.3 minutes. Since the merge window went live on
|
||||
2026-10-10 at about 12:40, a lone merge waits 1.6 to 2.1 minutes for its window (median 1.8), and **4 of the 9
|
||||
leaf walks since then waited behind another walk**, for 1.8, 4.9, 9.5 and 13.3 minutes, each behind a walk of
|
||||
the controller's own path.
|
||||
- **Core walks** (31): median 9.7, 90th percentile 17.0, worst 20.5; 4 of 31 within five minutes. They are two
|
||||
populations. **13 core walks had one tier**: median 5.6, 90th percentile 7.2, worst 8.8. **18 walks
|
||||
were controller merges that also walked the build agent and the route proxy** in tiers of their own (issue
|
||||
338): median 14.2, worst 20.5, none within ten minutes.
|
||||
- **The first-node judgement** is three healthy readings, at least forty seconds apart and two minutes after
|
||||
the send (ADR 0236 decision 2). It took a median of 2.4 minutes for a core module other than the controller (10
|
||||
judgements), 3.6 minutes for the controller (19 judgements, worst 4.3, its readiness includes its
|
||||
self-check), and 2.6 minutes per tier of a leaf walk. **Five of the 20 leaf walks judged two first nodes one
|
||||
after the other** within one tier (a module first on one node, another first on a second), adding 3.0 to 4.2
|
||||
minutes each.
|
||||
- **The build is small**: a median of 0.4 minutes for a leaf walk and 0.6 minutes for a core walk of one tier.
|
||||
- **The rest are sent at once already**: a leaf walk sends the rest a median of 0.0 minutes after its judgement.
|
||||
The walk ends when the rest are *sent*, not when they run the build. Applying a send takes a median of 10 to
|
||||
17 seconds per node (the controller's `durations`, seven days, every node), so the measured times are short of
|
||||
"healthy on every node" by about a quarter of a minute.
|
||||
- **The merge gate does not build the module.** Its record for a controller pull request on 2026-10-10 shows it
|
||||
clones the repositories, composes every node's declaration with the change, compiles the controller only to
|
||||
judge the change with itself (3.2 seconds), and runs the repository's `merge-check.sh` (6 minutes 13 seconds,
|
||||
before the merge). No artifact is built that the merge could reuse; the merge's build is the first.
|
||||
|
||||
Three limits of the measurement: a walk opens only when the walk before it ends, so time beyond the window's
|
||||
own 1.6 to 2.1 minutes is counted as waiting; the walk keeps only the last judgement pass, so the time of a
|
||||
first pass is estimated below, not measured; and the check before the merge is not counted, because the
|
||||
operator's target starts at the merge.
|
||||
|
||||
**Checked against GENESIS.** *Anything requiring a human to notice it will be noticed late*: a fix that takes
|
||||
fifteen minutes to land is noticed by the operator waiting for it. *Failure must be loud*: a delivery over its
|
||||
budget raises a condition. *Evidence over assertion*: every lever below is weighed by what it would have saved
|
||||
in the walks above. Nothing here conflicts with GENESIS.
|
||||
|
||||
**Words.** The glossary has no word for the time from a merge to its modules running healthy everywhere, nor for
|
||||
the time it is held to, nor for the two kinds of module. This record proposes **delivery time**, **delivery
|
||||
budget**, **core module** and **leaf module** for the Change and delivery domain, with this record.
|
||||
|
||||
## Considered Options
|
||||
|
||||
Each lever is weighed by what it saves in the 20 leaf walks measured, against what it costs.
|
||||
|
||||
1. **No merge window for a lone merge** (chosen). Saves the window's 1.8 minutes on every lone merge. The window
|
||||
exists to put the parts of one change, merged together across repositories, into one walk (ADR 0276). A
|
||||
merge whose delivery group has no member still unmerged has no part to wait for.
|
||||
2. **Independent walks in parallel** (chosen). Saves the wait behind another walk: 1.8 to 13.3 minutes in 4 of
|
||||
the 9 leaf walks since the window went live, 34.7 minutes over the 19 leaf walks outside the issue 362
|
||||
incident. Its cost is two builds judged on one node at once, which the rules in decision 3 keep apart.
|
||||
3. **Judgement passes by the module's class** (chosen). One healthy reading takes an estimated minute after the
|
||||
send (a send applies in 10 to 17 seconds, and the controller acted on a build in a median of 0.3 minutes),
|
||||
against 2.6 minutes for three. Saves about 1.5 minutes per tier of a leaf walk. The cost is a crash that shows
|
||||
only after the first reading, which a second reading forty seconds later catches for a module with a
|
||||
long-running resource. A module whose every resource is a tools bundle has no process of its own to crash: the
|
||||
tool runner serves its tools or does not.
|
||||
4. **The first nodes of one tier judged at once** (chosen; not in issue 382, found by the measurement). Saves
|
||||
3.0 to 4.2 minutes in 5 of the 20 leaf walks.
|
||||
5. **Reuse the merge gate's build of the pull request's head** when the merged tree equals the checked one.
|
||||
Rejected: the merge gate builds no artifact today (see Context), so there is nothing to reuse. Building every
|
||||
head's artifacts at its check, to save the merge's build, would save at most the build's median 0.4 minutes
|
||||
for a leaf walk, cost a build on every push to a pull request, and need ADR 0238 decision 1 ("anything built to
|
||||
check it is never registrable") reopened.
|
||||
6. **A build cache.** Rejected as a lever of this record: the build is 5 % of a leaf walk's time, and the
|
||||
artifact that did not change is already not rebuilt (ADR 0267's build source).
|
||||
7. **Sending the rest at once after the first passes.** Already so: the rest are sent a median of 0.0 minutes
|
||||
after the judgement. Nothing to gain.
|
||||
8. **The plan says when it is expected to land** (chosen). Saves no time. It tells the operator, while waiting,
|
||||
when to look, from the same measurements.
|
||||
9. **One budget for every module, five minutes.** Rejected: a core module's judgement is what protects the
|
||||
mesh's ability to reach a node at all. A broken node-engine, tool runner, bus or resolver cannot be put back
|
||||
from outside the node, and the controller judges every other build. The controller's judgement alone takes 3.6
|
||||
minutes; its three readings are kept.
|
||||
|
||||
With options 1 to 4 applied to the 20 measured leaf walks (no window, no wait, one judgement of about a minute
|
||||
per tier with its first nodes at once, plus a quarter of a minute for the rest to apply), the median would be 2.1
|
||||
minutes and the worst 5.3; **19 of 20 within five minutes**, against 7 of 20 measured. Keeping three readings
|
||||
and applying only options 1, 2 and 4 gives a median of 3.9 and a worst of 9.6 (18 of 20).
|
||||
|
||||
## Decision
|
||||
|
||||
1. **The target, and how it is measured.** A delivery's **delivery time** runs from its merge to the moment
|
||||
every node of its deploy plan that is heard from runs its modules' builds and reports them healthy. A node not
|
||||
heard from (a laptop asleep) is listed and not waited for, as ADR 0236 leaves it. **A leaf module's delivery
|
||||
budget is five minutes. A core module's is ten.** A delivery held for a person (a module that records, a hold,
|
||||
a bus step) is shown and not held to a budget: its wait is the person's, and the controller's
|
||||
condition for a walk left waiting (S16) and its probe of stalled deliveries (D14) already speak for it.
|
||||
- A **core module** is one on the controller's own path (the controller, the node-engine, the tool runner, the
|
||||
bus, mesh-delivery) or the mesh's resolver. A **leaf module** is every other module. A delivery's class is
|
||||
that of the slowest-budgeted module it moves.
|
||||
- The ten minutes for a core module are the measurement's: the 13 core walks of one tier took a median of
|
||||
5.6 minutes and at worst 8.8. A controller merge that also walks the build agent and the route proxy (median
|
||||
14.2) meets it once issue 338's path precision is built: that work is tracked by issue 338 and is the
|
||||
planner's, not this record's.
|
||||
2. **No merge window for a lone merge.** When a merge arrives, no batch is assembling, and its delivery belongs to
|
||||
no delivery group with a member not yet merged, the controller cuts its batch at once. A merge of a group still
|
||||
waiting for members, or a merge arriving while a batch assembles, waits for the window as ADR 0276 decided.
|
||||
3. **Independent walks run at once.** A batch becomes a walk without waiting for an open walk when it shares with
|
||||
every open walk no repository, no module, and no edge of the module graph between their modules. Where they
|
||||
meet, they wait for each other:
|
||||
- **one first-node judgement per node at a time**: a walk's first node is chosen among its module's nodes
|
||||
where no other walk is judging; a send to a node where another walk's build is being judged waits for that
|
||||
verdict, so ADR 0236 decision 4a (no build reaches a node without a judgement) holds as written;
|
||||
- **a core walk keeps its ground**: no leaf build is sent to a node where a core build is being judged, and
|
||||
while a new controller is judged, no leaf judgement counts a reading; those resume when it passes or is put
|
||||
back. A core send never waits for a leaf judgement: a leaf judgement open on that node is set aside without
|
||||
a verdict and starts again, from its first reading, after the core build's verdict, so a core delivery is
|
||||
not held past its budget by a leaf one;
|
||||
- two walks of one repository never run at once (a repository's later merge contains its earlier ones, ADR 0276 decision 2), and a merge into a repository with
|
||||
an open walk joins that repository's next batch.
|
||||
|
||||
This replaces ADR 0276 decision 4's "one walk at a time"; its rule that a walk which started is never
|
||||
superseded stands.
|
||||
4. **Judgement by class.** The first-node gate's readings, each at least forty seconds after the one before:
|
||||
- a **core module**: three, the first at least two minutes after the send, as ADR 0236 decision 2 decided;
|
||||
- a **leaf module with a long-running resource** (a service, a container, a process the node-engine runs):
|
||||
two, the first after the node-engine states every such resource healthy (ADR 0240);
|
||||
- a **leaf module whose every resource is a tools bundle**: one, once the tool runner serves its tools.
|
||||
|
||||
Everything else ADR 0236 decided of the first-node gate stands: no witness put the build back, no condition
|
||||
raised since the send, ten minutes at the most, and a rollback when it fails.
|
||||
5. **One tier's first nodes are judged at once**, each by its own readings, and the tier passes when every one
|
||||
has passed.
|
||||
6. **Every walk keeps its phases, and every delivery its delivery time.**
|
||||
- The controller records in the walk, and says in `plan-moved`, each phase of the table above with its start
|
||||
and end, each judgement reading with its time, and the first healthy report of each node of the rest. Its
|
||||
`durations` gains the kind `walk-phase`, per class.
|
||||
- mesh-delivery records each delivery's delivery time, its class and its phases from the walk. A new verb,
|
||||
`times`, answers each class's budget, the last seven days' median and worst delivery time, and every
|
||||
delivery over its budget with where its time went; `deliveries` heads its list with one line of the same,
|
||||
and `show` gives a delivery's phases.
|
||||
- The controller's `status` shows that line, read from `times` by the self-check, beside what is wrong.
|
||||
7. **A delivery over its budget is loud.** The self-check's probe reads `times` and raises the warning
|
||||
`delivery.<class>.over-budget` when a delivery's time exceeds its class's budget, naming the delivery and its
|
||||
longest phase. It clears when the next delivery of that class lands within its budget.
|
||||
8. **The plan says when it will land.** `plans` and mesh-delivery's `show` give an open batch and an open walk an
|
||||
expected landing time: now plus the remaining phases at the last seven days' median for its class, and once a
|
||||
walk is judging, plus the readings still owed.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **A leaf fix lands within five minutes when nothing else is in its way**, by the measurement's own estimate:
|
||||
build 0.4, send 0.3, one or two readings at about a minute each, the rest a quarter of a minute.
|
||||
- **A burst of unrelated merges no longer queues**; it walks in parallel, and one node judges one walk at a time.
|
||||
The controller holds several open walks, and every reader of a walk (`plans`, the walk's conditions, `plan-moved`,
|
||||
mesh-delivery) reads a list where it read one.
|
||||
- **A leaf module is judged on less evidence.** A build that fails after its last reading reaches the rest. That
|
||||
is bounded: the rest's first healthy report is recorded, a failure there is a condition on that node, and the
|
||||
build that broke it is put back the ordinary way. The trade is the operator's stated choice of speed for leaf
|
||||
modules; core modules keep every reading.
|
||||
- **A core walk still holds its ground.** A leaf delivery whose node is judging a core build waits up to that
|
||||
judgement (3.6 minutes for the controller) and may exceed its budget; the condition says so, and the week's
|
||||
numbers show how often.
|
||||
- **The core budget depends on issue 338.** Until the planner moves only the modules whose build source a merge
|
||||
touched, a controller merge exceeds ten minutes and raises the condition each time. That is intended: the
|
||||
condition counts what issue 338 costs.
|
||||
- **What got harder:** the walk's record grows by the phases and readings, kept in the controller's store as a
|
||||
numbered migration; the scheduler of walks gains the independence test and the per-node judgement rule, each a
|
||||
place where two walks can disagree, and each tested.
|
||||
- **On acceptance**, ADR 0236 (decision 2, three readings for every module) and ADR 0276 (decision 4, one walk at
|
||||
a time; decision 1, the window for every merge) each gain a note saying the mechanism moved here, and to-be 47
|
||||
lists this record among its decisions.
|
||||
|
||||
## How it is checked
|
||||
|
||||
| Rule | Checked by |
|
||||
|---|---|
|
||||
| a lone merge is cut at once; a group's member and a merge during assembly wait | the controller's tests: a lone merge asks its build with no window; a merge whose group has an unmerged member waits for the window; a second merge within the window joins the batch |
|
||||
| independent walks run at once, dependent ones wait | the controller's tests: two catalogue merges of unrelated modules walk at once; two of one repository, two sharing a module, and two joined by a graph edge do not; two walks never judge on one node at once; a send to a node under another walk's judgement waits for its verdict; no leaf reading counts while a new controller is judged |
|
||||
| judgement by class | the controller's tests: a tools-only leaf passes on one reading, a leaf with a service on two forty seconds apart, a core module on three two minutes after the send; each fails and is put back as before |
|
||||
| one tier's first nodes at once | the controller's test: a tier whose modules are first on two nodes judges both at once and passes when both pass |
|
||||
| every phase and the delivery time are recorded | the controller's test of the walk record and `plan-moved`; mesh-delivery's tests of the delivery time, its class, `times` and `show` |
|
||||
| a delivery over budget is loud | the probe's test: a leaf delivery of 5 minutes 10 seconds raises `delivery.leaf.over-budget` naming its longest phase; the next within budget clears it |
|
||||
| the target holds on the live mesh | `mesh-delivery.times`: the week's leaf median within five minutes and the condition raised by no more than one leaf delivery in ten; the same measurement as this record's Context, repeated a week after the last part is built |
|
||||
|
||||
## References
|
||||
|
||||
- Issue 382 (a small fix takes 45 minutes to reach a node) and issue 338 (a module built from a shared repository
|
||||
moves on every merge to it), in the issue tracker.
|
||||
- [ADR 0236](0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md):
|
||||
the first-node gate's three readings, kept for core modules.
|
||||
- [ADR 0238](0238-a-commit-is-the-build-at-hand-one-commit-one-change-plan-checked-off-the-trunk-and-published-only-on-it.md):
|
||||
a check's build is never registrable, which option 5 would have reopened.
|
||||
- [ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md):
|
||||
mesh-delivery owns the delivery, and so its delivery time.
|
||||
- [ADR 0267](0267-an-artifact-is-rebuilt-only-when-its-build-source-changes-and-a-module-moves-only-along-build-dependencies-on-artifacts.md):
|
||||
an unchanged artifact is not rebuilt.
|
||||
- [ADR 0276](0276-merges-are-assembled-in-a-rolling-window-and-each-batch-is-delivered-by-one-walk-with-one-commit-per-repository.md):
|
||||
the merge window and the walk this record changes in part.
|
||||
- [Research 044](../01-RESEARCH/044-what-leaves-the-controller-next/00-overview.md), whose question 6 asks for
|
||||
the time from merge to a node running the fix as a baseline; the table above is that baseline.
|
||||
- To-be [47](../03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md), amended with this record.
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
---
|
||||
topic: how we work
|
||||
status: accepted
|
||||
date: 2026-10-11
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0153-the-record-is-read-by-a-module-and-the-console-lists-it.md
|
||||
---
|
||||
|
||||
# 290. A decision record lives whole in the knowledge base, made of clauses
|
||||
|
||||
> **Rewritten — 2026-10-11, on the operator's word.** This record was merged an hour earlier as *The knowledge base
|
||||
> holds each record's structure and clauses, and hq keeps its reasoning*: the store held the structure, and this
|
||||
> repository the reasoning, reviewed in pull requests. The operator asked why one record should live in two places,
|
||||
> and asked for this record to be updated in place rather than superseded. Nothing had been built on the earlier
|
||||
> version. It is in this repository's history at commit `08b1077b`.
|
||||
|
||||
> **Updated — 2026-10-11, on the operator's word.** Rule 9 left open whether this repository keeps a generated
|
||||
> public copy of accepted decision records and research efforts (issue 435). The operator answered: no copy is needed. Rule 9 and the consequences
|
||||
> say so.
|
||||
|
||||
## Context
|
||||
|
||||
[Research 045](../01-RESEARCH/045-a-knowledge-base-the-mesh-writes/00-overview.md) measured, on 2026-10-11, how an
|
||||
agent learns what the mesh decided ([findings](../01-RESEARCH/045-a-knowledge-base-the-mesh-writes/01-findings.md)).
|
||||
The operator gave the purpose the same day: the decisions and research should become shared knowledge that agents
|
||||
reach through tools, instead of reading markdown files, and a session should no longer have to start in this
|
||||
repository's folder to know what was decided.
|
||||
|
||||
**An answer costs too much.** "What holds about the merge window" took 26 search hits in 11 documents and about 85 KB
|
||||
of reading, some 21,000 tokens. The decision index, generated by the records module
|
||||
([ADR 0248](0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md)), is 77 KB, more than one
|
||||
tool answer may be. The records module matches a phrase literally and knows nothing of which record still holds
|
||||
([ADR 0153](0153-the-record-is-read-by-a-module-and-the-console-lists-it.md)).
|
||||
|
||||
**What a decision decided is not data.** 226 of 263 decision records write their `Decision` section as prose; 37
|
||||
write it as numbered statements with a bold lead (198 in all). A later record that changes one statement of an older
|
||||
one could say so only in prose or by naming the whole record: 21 `supersedes-in-part` links had no reverse, and 6
|
||||
more records said it in prose alone, until hq pull request 305 declared all of them in both records and added a check.
|
||||
|
||||
**The checks run after the fact.** Of the 33 rules the hq checks enforce, 25 judge one record and 4 judge its prose;
|
||||
all 29 could refuse a bad record when it is written. Only 4 need the whole record set.
|
||||
|
||||
**Numbers collide.** 16 decision numbers were taken twice by sessions working at once between 2026-09-25 and
|
||||
2026-10-10. [ADR 0275](0275-the-mesh-numbers-and-tracks-its-issues-in-a-module-of-its-own-and-hq-keeps-only-the-narrative-of-a-design-or-governance-issue.md)
|
||||
rule 8 already decided that the issue tracker hands out decision and research numbers; issue 424 tracks building it.
|
||||
|
||||
### Against GENESIS
|
||||
|
||||
- **The mesh owns the truth** ([mission](../00-META/mission.md)): a record lives in one place, written through one
|
||||
module, not in a file every branch may edit.
|
||||
- **The mesh brokers everything the work needs, knowledge included**: an agent asks the mesh what holds, by
|
||||
capability, instead of knowing which folder to read.
|
||||
- **The mesh remembers** ([effect](../00-META/effect.md)): what was decided, and why, is available to whoever needs it
|
||||
next, on any node, in any folder, with every earlier version kept.
|
||||
- **Failure must be loud**: a write that would leave the records inconsistent is refused when it is made.
|
||||
|
||||
## Considered Options
|
||||
|
||||
### Where a record lives
|
||||
|
||||
1. **hq stays the truth; a module indexes it** (the research's option A). Rejected: a link about one statement of a
|
||||
decision stays prose, and every inconsistency is found after it is written.
|
||||
2. **Divided: the knowledge base holds a record's structure and clauses; hq keeps its reasoning.** Rejected by the
|
||||
operator on 2026-10-11: one record in two places, an agent sent to a file for the *why*, and the structure written
|
||||
twice until the frontmatter left hq.
|
||||
3. **The knowledge base holds a record whole: structure, clauses and reasoning, with every version.** Adopted. It is
|
||||
the division the operator made for issues ([ADR 0291](0291-an-issue-lives-whole-in-the-issue-tracker.md)), applied
|
||||
to decisions and research.
|
||||
|
||||
### The unit
|
||||
|
||||
1. **The record.** Rejected: a later record changes one statement of an older one far more often than the whole.
|
||||
2. **The clause: one numbered statement a decision makes, with its own status.** Adopted. The research called it a
|
||||
*rule*; *rule* already names a line of [how-we-build](../00-META/how-we-build.md), and one word names one thing
|
||||
([ADR 0244](0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md)).
|
||||
|
||||
### How a record is reviewed, once it is not a pull request
|
||||
|
||||
1. **The operator approves every record.** Rejected: records are reviewed today by an independent agent, and the
|
||||
operator is asked only for real decisions; making every record wait for one person slows what works.
|
||||
2. **An independent agent reviews it, the write-time checks pass, and a record that needs the operator waits for the
|
||||
operator's Approve on a channel that proves who answered.** Adopted. It is how a pull request here is merged
|
||||
today, and the operator's Approve already reaches the mesh that way
|
||||
([ADR 0274](0274-an-agent-proposes-a-shared-instruction-section-and-every-node-takes-it-only-on-the-operators-warrant-which-it-proves-itself.md)).
|
||||
|
||||
### Who confirms the clauses drafted for the 226 prose decisions (the research's open question 1)
|
||||
|
||||
1. **The operator, topic by topic.** Rejected: 226 records is days of reading for one person, for statements already
|
||||
decided and reviewed once.
|
||||
2. **Nobody: drafted clauses are served as they are.** Rejected: a summary nobody checked would speak as the record's
|
||||
word, which is the work-that-reports-success-and-did-nothing fault.
|
||||
3. **A second judgement, independent of the one that drafted them, confirms each record's clauses against its text;
|
||||
the operator is told per topic and may send any record back.** Adopted, and confirmed by the operator on
|
||||
2026-10-11.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **The knowledge base is a Go module, `mesh-knowledge`, with one instance on the control-node.** It declares and
|
||||
claims one seat held once for the mesh, `knowledge-base`
|
||||
([ADR 0126](0126-a-module-declares-its-own-seats.md)). Its first kinds are the **decision record** and the
|
||||
**research effort**.
|
||||
2. **The knowledge base is the truth for a record whole**: its number, kind, title, topic, status, date, deciders, its
|
||||
typed links (held in both directions), its clauses, and its reasoning (context, options weighed, consequences,
|
||||
how it is checked; a research effort's documents). **Every version is kept**, with who wrote it and when, and any
|
||||
two versions can be compared. Once the import has run and been checked, no record is written in this repository.
|
||||
3. **A decision record is made of clauses.** A clause is one numbered statement of what was decided, with a status:
|
||||
`proposed`, `in force`, `changed` by a clause of a later record, or `superseded`. A link that is about a clause
|
||||
names the clause, not only the record. A record is written as clauses from the start. Until the knowledge base
|
||||
takes writes, a record written in this repository from 2026-10-11 on writes its `Decision` section as numbered
|
||||
clauses with a bold lead, and `records.py` refuses an accepted one that does not.
|
||||
4. **A record is proposed, reviewed and accepted in the knowledge base.** `propose` writes it with a number from
|
||||
`reserve`, and refuses it outright when one of the 29 rules that judge one record or its prose fails. An
|
||||
independent agent reviews it (`review`), and its approval moves the record and its clauses to `accepted` and
|
||||
`in force`, and every clause it changes or supersedes accordingly. A record that says it needs the operator waits
|
||||
for the operator's Approve, asked on a channel that proves who answered, and the warrant is kept with it
|
||||
([ADR 0259](0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md)).
|
||||
An accepted record changes only by a new record, or by a dated correction of a fact, written as a new version and marked as one;
|
||||
the 4 rules that judge the whole record set run over it after every acceptance and raise a condition when one
|
||||
fails.
|
||||
5. **Numbers come from the issue tracker's counters**, as ADR 0275 rule 8 decided: `reserve` asks the tracker for the
|
||||
next decision or research number, and a reserved number is spent whether or not its record is accepted. Until the
|
||||
tracker hands them out, tracked by issue 424, a session takes the highest number on `main` and on every open
|
||||
branch, plus one, as the shared instruction section *writing-decision-records* says.
|
||||
6. **An answer is the shape agents need, bounded.** The verbs are `ask` (records and clauses bearing on a question,
|
||||
ranked, each saying whether it holds or what changed it), `get` (one record: clauses, links both ways, what changed
|
||||
it since; its reasoning only when asked), `holds` (the clauses in force for a topic or a record), `links`,
|
||||
`history` (a record's versions and the difference between two), `reserve`, `propose`, `review`, `import` and
|
||||
`return` (send drafted clauses back to be drafted again). `ask`, `get` and `holds` stay under a size bound they
|
||||
state; the default is 4 KiB.
|
||||
7. **State is in a database in the store**, provided to the module, not in buckets: `ask` needs ranked full-text
|
||||
search, and a record's versions are a record set to search, not current state to watch
|
||||
([ADR 0201](0201-a-module-keeps-its-current-state-in-key-value-buckets-it-declares-and-reaches-through-the-runtime.md)
|
||||
is not narrowed). **Every change is an event the module declares**
|
||||
([ADR 0041](0041-events-are-a-relationship.md)), carrying what changed and who changed it.
|
||||
8. **The import reads this repository at a commit and can run again.** It writes every decision record and research
|
||||
effort whole, at its own number, with the commit as its first version's source; it creates what is absent, never
|
||||
overwrites, never advances a counter, and a second run changes nothing and says so. A record whose clauses are
|
||||
written as numbered statements gets them as `written`. A prose record gets them `drafted` by the agent's judgement
|
||||
([ADR 0278](0278-the-issue-tracker-checks-every-new-issue-for-a-duplicate-through-a-judgement-the-agent-makes-without-tools.md)),
|
||||
then `confirmed` by a second judgement that sees only the record and the drafts; the operator is told per topic and
|
||||
may send a record back. An answer marks a drafted or confirmed clause as such, and never serves a drafted clause as
|
||||
in force.
|
||||
9. **This repository stops holding decision records and research efforts** once the import is checked against it,
|
||||
record by record: one pull request removes them, rewrites the mentions of their paths to "ADR NNNN" and "research
|
||||
NNN", and changes playbooks 01 and 02, tracked by issue 426. This repository keeps no copy of them, generated or
|
||||
otherwise; the knowledge base is the only place a decision record or research effort is read. The records module
|
||||
stays the literal reader of what this repository still holds.
|
||||
10. **It is built in slices, each tracked by an issue.** The first is the module for decisions and research: import,
|
||||
clauses, answers, and proposing and reviewing a record, tracked by issue 425. The records leave this repository
|
||||
under issue 426. Designs, playbooks and glossary entries follow the same way, tracked by issue 428. Meeting notes,
|
||||
drawings and blog posts come last and are not decided here, tracked by issue 429. The design is
|
||||
[to-be 55](../03-DESIGN/01-to-be/55-the-knowledge-base.md).
|
||||
|
||||
## Consequences
|
||||
|
||||
- An agent on any node asks what holds and why, and gets the clauses in force, the records they come from and what
|
||||
changed them, in one call, instead of reading several records whole. No session needs this repository's folder to
|
||||
read or write a decision.
|
||||
- A later record that changes one statement of an older one says which, and the older clause's status changes when
|
||||
the later record is accepted.
|
||||
- Review moves from a pull request to the knowledge base's `review`, and the forge's line comments and diff are
|
||||
replaced by the record's versions and `history`. A record is checked when it is written, not when it is merged.
|
||||
- Until the first slice takes writes, records are still written here as pull requests, with numbered clauses, and the
|
||||
import takes them in with the rest.
|
||||
- What got harder: one more module the mesh's own process depends on. Reading or writing a decision needs the mesh to
|
||||
be reachable; a node that cannot reach the control-node says so rather than guessing. Once the records leave it
|
||||
(issue 426), the public repository no longer shows the mesh's decisions or research.
|
||||
|
||||
## How it is checked
|
||||
|
||||
- **Clauses in a record written here** (rule 3): `records.py`, check `clauses`, on every pull request of this
|
||||
repository, until the records leave it.
|
||||
- **What the module does** (rules 2, 4, 6, 7, 8): the module's tests and its live acceptance, listed in
|
||||
[to-be 55](../03-DESIGN/01-to-be/55-the-knowledge-base.md) §9, built under issue 425.
|
||||
- **Nothing lost when the records leave** (rule 9): every record compared with its file at the import's commit before
|
||||
the files are removed, the result written on issue 426.
|
||||
- **Numbers** (rule 5): the issue tracker's tests of its counters, built under issue 424; until then the
|
||||
`numbering` and duplicate checks of `records.py` and `cycle.py` find a collision on `main`.
|
||||
|
||||
## References
|
||||
|
||||
- [Research 045](../01-RESEARCH/045-a-knowledge-base-the-mesh-writes/00-overview.md) and its
|
||||
[findings](../01-RESEARCH/045-a-knowledge-base-the-mesh-writes/01-findings.md).
|
||||
- [To-be 55 — The knowledge base](../03-DESIGN/01-to-be/55-the-knowledge-base.md).
|
||||
- hq pull request 305: every partial supersession declared in both records, and the `partial-supersession` check.
|
||||
- Issues 424, 425, 426, 428, 429 and 435 (answered: no copy in this repository).
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
# 02-DECISIONS
|
||||
|
||||
Architecture decision records — the "why" trail behind the rules in
|
||||
[`00-META`](../00-META/) and the specifications in [`03-DESIGN`](../03-DESIGN/).
|
||||
|
||||
**Numbered `02` because a decision precedes the design it authorises.** Research concludes,
|
||||
the decision is recorded here, and only then is the design written. Following the folder
|
||||
numbers walks the process in the order it happens.
|
||||
|
||||
One file per decision, numbered, never deleted. A superseded record has its `status:` changed
|
||||
and gains a pointer to what replaced it — **its reasoning is never rewritten**. The reasoning that
|
||||
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.
|
||||
|
||||
## A pointer back from what a record changes
|
||||
|
||||
A new record naming an old one is not enough. **Where a record changes a mechanism an older record
|
||||
states — without reversing the decision, so no supersession — the older record gets a dated note
|
||||
saying where its mechanism now lives.** A reader arrives at the old record by following a citation,
|
||||
and finds text that is still the decision and no longer the method; nothing in it says a later record
|
||||
moved the method, and the new record is not in their hands.
|
||||
|
||||
> **The mechanism changed — YYYY-MM-DD, by ADR NNNN.** What still stands, what moved,
|
||||
> and why.
|
||||
|
||||
Three examples of the shape, all found by being missed: ADR 0066 still described a routed name being
|
||||
written into every container after 0148 replaced that with resolution; ADR 0047 still said a module's
|
||||
code runs in a container after 0150 made it a supervised process; and ADR 0016 still read as though the
|
||||
lab were the test bed after 0149 said the live mesh is. Each was a citation leading to the wrong
|
||||
answer, in a record that was not wrong about anything it decided.
|
||||
|
||||
**This is not machine-checked, and it cannot be from `extends:` alone.** 102 records extend another and
|
||||
87 name a parent that does not mention them, which is correct: extending usually means building on a
|
||||
context, and a one-directional pointer is the right shape for that. What needs a note is the narrower
|
||||
case where the parent's own text has gone stale, and which case that is, is a judgement — so it is a
|
||||
rule for the author and the reviewer, and the diff is where it is caught. Making it mechanical would
|
||||
mean a record declaring the relationship in its frontmatter, which is a change to the record schema and
|
||||
has not been decided.
|
||||
|
||||
**A supersession in part is the declared case, and it is machine-checked.** A record that replaces part of
|
||||
an older one names it under `supersedes-in-part:`, and the older record carries, directly under its
|
||||
heading and newest first, a note in the form
|
||||
|
||||
> **Superseded in part — YYYY-MM-DD, by ADR NNNN** (a link to it). What no longer holds, and what stands
|
||||
> as decided here.
|
||||
|
||||
dated by the newer record's `date:`. `00-META/checks/records.py` (`partial-supersession`) fails when
|
||||
either half is missing, when the note's date is not the newer record's, and on a note in the older form
|
||||
*Narrowed, not replaced*. Where the note sits is the reviewer's to see in the diff. Saying "this narrows" or "this replaces" an older record in prose alone is not
|
||||
enough: on 2026-10-11, 6 records did only that, and 11 of 21 declared links had no note on the record
|
||||
they changed.
|
||||
|
||||
The records run in the order the decisions were taken, oldest first.
|
||||
|
||||
**Every decision is a record.** There is no ledger and no index file — if a decision is worth
|
||||
recording it is worth a record, and if it is not worth a record it is not recorded
|
||||
([ADR 0019](0019-how-this-repository-works.md)). A "decision" small enough to be one line is
|
||||
almost always a **rule**, and a rule belongs in
|
||||
[`00-META/how-we-build.md`](../00-META/how-we-build.md), where it is enforced and keeps the
|
||||
incident that earned it.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
```yaml
|
||||
---
|
||||
status: proposed | accepted | superseded
|
||||
date: YYYY-MM-DD # when the decision was taken, not when it was written down
|
||||
deciders: name
|
||||
reconstructed: true|false # true when the record was written after the fact from evidence
|
||||
superseded-by: # 02-DECISIONS/NNNN-....md, when status is superseded
|
||||
extends: # 02-DECISIONS/NNNN-....md, when this record widens an earlier one
|
||||
supersedes: # a list of 02-DECISIONS/NNNN-....md this record replaces whole
|
||||
supersedes-in-part: # a list of 02-DECISIONS/NNNN-....md of which this record replaces a part
|
||||
---
|
||||
```
|
||||
|
||||
## Body
|
||||
|
||||
```
|
||||
# N. Title in plain language
|
||||
|
||||
## Context what was true, with evidence
|
||||
## Considered Options numbered, each with why it was rejected
|
||||
## Decision what was decided
|
||||
## Consequences what follows, including what got harder
|
||||
## References commits, pull requests, knowledge-base entries, prior art
|
||||
```
|
||||
|
||||
State evidence, not assertion. *"Zero of 124 modules declare `brain` as a dependency"*
|
||||
outranks *"the dependency rule is not followed"*.
|
||||
|
||||
## Reconstructed records
|
||||
|
||||
Records 0001–0014 were written on 2026-08-23, after the decisions they describe. Records 0015 onward were taken as records. Those
|
||||
decisions were taken in implementation rather than in a document; the records state what was
|
||||
decided and the evidence it was decided from, and each carries `reconstructed: true` and says
|
||||
so in its first lines.
|
||||
|
||||
A reconstructed record is not a transcript. Where the deliberation is not recoverable, the
|
||||
options section states what the alternatives were and why the chosen one won on the evidence
|
||||
available — not a discussion that did not happen. Where a date is not establishable it says so
|
||||
rather than guessing.
|
||||
|
||||
## Reading order
|
||||
|
||||
**A number identifies a record and never changes.** Records are referenced from outside this
|
||||
repository — code comments, commit messages — so a number that moves invalidates them silently.
|
||||
Renumbering once cost 96 references across two code repositories, and that is why the numbers
|
||||
are now fixed.
|
||||
|
||||
So the folder is in creation order, and **the reading order lives here**: six topics, in the
|
||||
order somebody would learn the system. Each record names its own in `topic:`.
|
||||
|
||||
1. **What the mesh is** — `topic: the mesh`. What it is for, and the parts it is made of.
|
||||
2. **Its tiers, from the bottom up** — `topic: the tiers`. The foundation, the node and the controller, and
|
||||
what each one provides to the one above.
|
||||
3. **What runs on them, and how it gets there** — `topic: what runs on it`. Modules, seats and
|
||||
provisions, and how they reach a node.
|
||||
4. **How it is built** — `topic: building it`. The code, its repositories, and what is made from
|
||||
it for a node.
|
||||
5. **How it is checked** — `topic: checking it`. What judges a change, and what proves a decision holds.
|
||||
6. **How we work** — `topic: how we work`. This repository, its playbooks, and how decisions and
|
||||
designs are made.
|
||||
|
||||
**The list of records under each topic is generated when it is read, and never stored here**
|
||||
([ADR 0248](0248-the-decision-index-is-generated-when-it-is-read-and-never-stored.md)). A stored list
|
||||
was one more line in this file for every decision, so two decisions open at once always conflicted
|
||||
([issue 297](../04-ISSUES/297-two-open-decisions-always-conflict/00-report.md)). Read it with:
|
||||
|
||||
```
|
||||
python3 00-META/checks/index.py --print the records, by topic, in reading order
|
||||
```
|
||||
|
||||
or the `hq-status` skill, or the records module's `records_decisions` from anywhere on the mesh.
|
||||
`index.py` checks that every record's topic is one of the six above, and fails if this file stores the
|
||||
list again.
|
||||
@@ -0,0 +1 @@
|
||||
d1edcf544b384502fded36c1605571ae74b07ed5
|
||||
Reference in New Issue
Block a user