diff --git a/00-META/glossary.md b/00-META/glossary.md index 04bd1286..cf622668 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -305,13 +305,17 @@ and the operator act. **Recorded by** the controller's `identity` and `licences` **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. +**Decided by** ADR 0162, 0218, 0236, 0237, 0238, 0239, 0250, 0267, 0270, 0276, 0323. **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). + (ADR 0238). Both judge the merge of the pull request's head into a named **trunk commit**, made in the + build seat's own clone and never pushed (ADR 0323). +- **trunk commit** — the commit of the trunk a merge check's verdict was judged against: the trunk's head when + the build seat cloned it. The verdict holds for that commit only, and goes out of date when the trunk moves + (ADR 0323). - **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). diff --git a/03-DESIGN/01-to-be/06-the-controller.md b/03-DESIGN/01-to-be/06-the-controller.md index d2e59aa4..cbaaa56a 100644 --- a/03-DESIGN/01-to-be/06-the-controller.md +++ b/03-DESIGN/01-to-be/06-the-controller.md @@ -3,8 +3,9 @@ layer: to-be status: in-progress code: - mesh-controller -updated: 2026-10-10 +updated: 2026-10-12 decisions: + - "ADR 0315" - "ADR 0001" - "ADR 0005" - "ADR 0006" @@ -64,11 +65,20 @@ applications** — first-party, shipped with everything else, and running on the anything else does. A task does not need to know a node exists, and *being ours does not make something infrastructure*. `ai` is folded into `config`: a provider licence is an ordinary grant. -**`record` is an open question rather than an eighth entry.** Contexts integrate through it -(ADR 0008), which makes it load-bearing, -and *where it lives* is unresolved, tracked by issue 414 since -research 006 closed — putting it in the foundation risks recreating the circularity the tier design just -removed. Listing it here would settle by naming what has not been settled by arguing. +**`record` is not an eighth entry: it is the bus** (2026-10-12, ADR 0315). Contexts integrate through +it (ADR 0008), and where it lives was left open by research 006, for fear that putting it in the foundation +would recreate the circularity the tier design removed. ADR 0315 writes down what later records built: the +record is the bus's, the EVENTS stream, where each module says the events it declares (ADR 0041), and the +key-value buckets each module declares for its current state, which another module names in `reads` to read +(ADR 0201). Asking another context is a call to its verbs over the bus, request and reply, and is not the +record; a secret, and whatever a context keeps out of its buckets, is asked for. The controller's own records +are read from its record mirrors (ADR 0289) or asked of its verbs, never from its store. The record lives in +the foundation's bus, not in the store, which stays each context's private database reached only with that +context's own credential, so no store holds a record shared between contexts and the circularity does not +return. No module owns the record whole: each event and each bucket belongs to the module that declares it, +which alone writes it. *How it is checked:* where each rule was decided — the runtime's refusal of a bucket +not issued or a write to one only read (ADR 0201), one credential per context's store (ADR 0008), and the +mirrors' events counted live and compared with the store by probe D17 (ADR 0289). **One of the seven is built.** `inventory` owns a database of that name and holds the node records; the rest do not exist. What it takes to run any of them — the language, and what must already be @@ -225,8 +235,8 @@ every public name. - ~~**The contexts themselves.**~~ **Decided** — seven, by ADR 0006. - What remains open is narrower and named there: **where the record lives**, which research 006 - leaves unresolved because the foundation is the one place it must not go. + What remained open, **where the record lives**, is decided by ADR 0315: on the bus, in the EVENTS + stream and the buckets each module declares, never in a store (above). - **How far it may be split.** One deployable today. Splitting a context out costs the single interface a surface depends on (research 011). diff --git a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md index 7fe64154..8550f27f 100644 --- a/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md +++ b/03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md @@ -2,8 +2,11 @@ layer: to-be status: in-progress code: [mesh-controller internal/catalogue] -updated: 2026-10-08 +updated: 2026-10-12 decisions: + - "ADR 0312" + - "ADR 0319" + - "ADR 0309" - "ADR 0262" - "ADR 0225" - "ADR 0112" @@ -151,6 +154,24 @@ ADR 0107). never created, owned or removed by the mesh. The module requires read or read-write access. Where the data is, is an operator value on the assignment. +*A read-write access writes through the path's own group (2026-10-12, ADR 0312, extending ADR 0051; the +build is tracked by issue 537):* when a module declares a `read-write` access, the node-engine reads the group +that owns the path as it applies, and makes the identity the module's code runs as a member of it: a +container's process gains it as a supplementary group, and an account that runs the module's tools is put in it +as ADR 0252 puts an account in a group. The module then writes the path as the path's group may, and as nothing +else. The path itself is never changed: no owner, group or mode is set on it or under it, and a read-write +access whose path's group cannot write is stated unhealthy, saying that the operator decides. A `read` access +grants no group. A path owned by group 0, or by a group that would let the identity act as root, is refused by +name. A group the node-engine granted is taken back when no declared read-write access asks for it any more, +never one the identity held before (ADR 0252 §3). A container is recreated with the group, and a service of +the machine's own service manager that runs the module's tools as an account is restarted once the account +holds it; a unit of the account's own service manager, or a login session, waits for a new login as ADR 0252 +§5 and ADR 0254 say. The tool runner's account so gains the groups of every read-write access of the modules it +serves: the boundary is the account's, not the module's. *How it is checked:* the node-engine's tests ADR 0312 +lists (the group given and the path's owner, group and mode unchanged; a read access granting nothing; an +unwritable group unhealthy and group 0 refused; the group taken back; the container recreated and the system +unit restarted holding the group). + **A port** is what ADR 0038 already decided: the module says which port its software uses, and the node-engine answers with where the machine put it. @@ -212,6 +233,26 @@ gains a setting by shipping its current value as the default, and `settings` lis source: the default, the mesh, or the node. The first module to use it is the notifier, whose font size and width are preferences. *How it is checked:* the table in ADR 0262. +*A value several modules share is set once (2026-10-12, ADR 0309, changing ADR 0164 §7 and §8 and ADR 0262 +§3, §7, §8 and §10, and ADR 0319, changing ADR 0164 §4 and §9 and ADR 0309 §2, §3, §5 and §6; the build is +tracked by issue 415):* a module keeps every setting it takes, and a preference may also name a mesh-wide +**shared name**, such as `desktop.font-size`. Unset in every layer of its own module, the preference +reads the shared value, and only when that is unset too its default; read from the bottom, last +winning: the default, the shared value, the module's mesh-wide layer, the node's. So a person sets +the interface font size once and every module that names it follows, while any one module or node +can still be set apart in its own layer. A shared value is a fourth place of change beside the three +of ADR 0164 (a new default no longer reaches an assignment whose shared value is set), and the plan +of a change to it names every assignment, of every module that reads it, +whose effective value moves. It is set and cleared through the `settings` verb without a module, and +both `settings` listings name the shared name as a value's source beside the default, the mesh and the +node. **A shared value reaches no trusted setting** (ADR 0319): a trusted setting may not declare a +shared name, and registration refuses one that does, so a shared value is never trusted and needs no +warrant; a trusted setting is set in its own module's layer, on the operator's warrant (ADR 0277) or at +the terminal, as before. A trusted setting that a stored manifest still declares with a shared name +never reads the shared value, and both listings say why beside it. Whether a shared value is a +number, a colour or a string waits for the typed-settings work (issues 346 and 410). *How it is +checked:* the tables in ADR 0309 and ADR 0319. + *The operator's value travels only where it is asked for (2026-09-30, [issue 173](../../04-ISSUES/173-a-modules-settings-reach-every-fact-it-contributes/00-report.md)):* a setting overrides a key a contribution or a served fact declares and adds none; a file keeps taking diff --git a/03-DESIGN/01-to-be/32-what-a-module-declares.md b/03-DESIGN/01-to-be/32-what-a-module-declares.md index de6cb6a1..691f146e 100644 --- a/03-DESIGN/01-to-be/32-what-a-module-declares.md +++ b/03-DESIGN/01-to-be/32-what-a-module-declares.md @@ -12,8 +12,9 @@ code: - mesh-tools src/main.ts - mesh-catalog modules/mesh-catalog - mesh-tools node-tools/internal/runtime (a module's state, ADR 0201) -updated: 2026-10-07 +updated: 2026-10-12 decisions: + - "ADR 0307" - "ADR 0240" - "ADR 0233" - "ADR 0201" @@ -260,6 +261,17 @@ them on the bundle's channel — refusing, with the reason, a name it was not is bucket it only reads. A watch hands the current values first, then every change: a bundle that starts late, or starts again, has the whole of the state before it has any of the news. +*A key's kept values are read too (2026-10-12, ADR 0307, extending ADR 0201).* A bucket keeps as many past +values per key as its owner declared, and the runtime serves them: `history`, given a state and a key, answers +every value the bucket still keeps for that key, oldest first, each with its revision, whether it was a put or +a delete, and when it was written; an empty answer for a key it holds nothing for. It is a read like `get`: +served to the owner and to every reader, refused with the reason to a module the bucket was not issued to, and +it writes nothing. The Go SDK's `State(name)` carries it as `History(key)`, beside `Get`. Its first user is the +issue tracker's switch to its log (ADR 0300 §6), which carries every value its bucket still keeps; that work is +tracked by issue 502. *How it is checked:* the runtime's tests (several values and a delete answered oldest +first, with revision, operation and time; an empty answer for an unknown key; a reader served, a module the +bucket was not issued to refused, nothing written) and the SDK's tests of `History`. + The owner says how many past values a key keeps and how long a value lives, as a seat says how long its backlog survives (§3); the mesh caps a value's size and a bucket's. **A bucket outlives its module's assignment** — what a module stored is data diff --git a/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md b/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md index 287efc33..2a5c8705 100644 --- a/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md +++ b/03-DESIGN/01-to-be/41-the-shell-and-the-accounts-environment.md @@ -2,8 +2,9 @@ layer: to-be status: in-progress code: [mesh-host, mesh-controller, mesh-catalog] -updated: 2026-10-09 +updated: 2026-10-12 decisions: + - "ADR 0310" - "ADR 0268" - "ADR 0252" - "ADR 0203" @@ -88,6 +89,7 @@ WP4 the service manager's module, finished (mesh-catalog) independ WP5 assign and prove (operator-gated) needs WP1–WP3 merged and rolled WP6 a module puts the account in a group (mesh-host, mesh-controller, mesh-catalog) issue 247 WP7 the control-node withholds execute (mesh-controller, mesh-catalog) ADR 0268 +WP8 a node's files and accounts are read (mesh-sdk, mesh-catalog, mesh-controller) ADR 0310, issue 418 ``` WP1, WP2 and WP4 are independent, and are built in parallel on one feature branch per repository @@ -334,3 +336,33 @@ out, the control-node's value is set to `withhold` and zsh is pushed to it. Then 2. the control-node's `zsh_config` answers `execute` as not served, and says why; 3. `mesh_search` for `node-login-shell execute` names the other three nodes and not the control-node; 4. a call to the control-node's `node-login-shell.execute` is refused, and the other nodes still answer. + +## WP8 — A node's files and accounts are read through `node-reads` + +*`mesh-sdk`, `mesh-catalog`, `mesh-controller`. ADR 0310, extending ADR 0268; tracked by issue 418.* + +ADR 0268 §5 has narrow read tools take the common reads over from `execute`, so that an approval is asked for an +act and not for a read, and ADR 0286 §44 sets the approval mark on `execute` only once those reads have a tool on +every node. Two had none: reading or searching a file, and looking up an account or a group. + +- **A node seat, `node-reads`, on every node**, the control-node included, held by the tool runner; the + controller holds the seat's definition beside the other node seats. +- **Three reads:** a file read bounded in size, a search by fixed string bounded in matches and in depth, and a + lookup of an account or a group. Each verb says what it replaces (ADR 0245): `cat`, `grep`, `getent passwd`, + `getent group` and `id`. The bounds are set by the build and stated in each verb's description. +- **It reads as an account of its own**, which the mesh creates on every node, in no group that holds secrets, + with no login shell and no home of the operator's, so the node's file modes bound what it sees. `/proc` and + `/sys` are read as far as that account may. +- **Secret places are refused by name** before any read: the vault's directories, every module's own-secret + directory and the known credential files, whatever the file modes would allow. +- **One content rule.** The rule the operator channel applies to what it sends moves into `mesh-sdk`, and both + the operator channel and every `node-reads` answer apply it. + +Once the seat answers on every node, the approval mark on `node-login-shell.execute` is set as ADR 0286 §44 says, +under the same issue. + +**Proof.** The rows of ADR 0310's *how it is checked*: the controller's seat listing names a holder of +`node-reads` on every node; the tool runner's tests of the bounds, the account and the refusal by name; one +`mesh-sdk` test of the content rule and one in each of its users; `mesh_search` for `cat`, `grep` and +`getent passwd` finding the verbs. Live, the kernel's RAID status file is read through the seat on the home +server, and the agent's credentials file on the laptop is refused. diff --git a/03-DESIGN/01-to-be/42-the-machines-modules-in-order.md b/03-DESIGN/01-to-be/42-the-machines-modules-in-order.md index a021773a..174fe209 100644 --- a/03-DESIGN/01-to-be/42-the-machines-modules-in-order.md +++ b/03-DESIGN/01-to-be/42-the-machines-modules-in-order.md @@ -2,8 +2,9 @@ layer: to-be status: in-progress code: [mesh-catalog, mesh-controller, mesh-host] -updated: 2026-10-10 +updated: 2026-10-12 decisions: + - "ADR 0303" - "ADR 0215" - "ADR 0173" - "ADR 0040" @@ -120,6 +121,20 @@ not write into its include directory. Each contribution is a dependency on the s so assigning one of them without a window manager is refused. The first versions, which still write the include files themselves, move to contributions once the controller derives the dependency. +**A package may be declared by several modules** (2026-10-11, ADR 0303, changing ADR 0210 §1, §10 and §11 +for packages). Two modules on one node that each need a program may each declare its package, and it is +present while any of them declares it. Declaring a package asserts only that it is present: the node-engine +installs one the machine lacks, never upgrades it, and never removes it because a declaration went away, so a +second declaration changes nothing on the machine and taking one module off leaves the package to the others. +A module that needs a program another seat holder installs may declare that package itself, or still depend on +that seat. One module declaring a package present while another declares it absent (ADR 0180) on one node is +refused at assignment and at composition, naming both. A tool's configuration files and the contributions to +its seat keep their one owner, as above: only the package itself may be declared by several modules. *How it +is checked:* the controller's tests (two declarations of one package are assigned; present against absent is +refused, naming both modules) and the node-engine's tests ADR 0303 requires (a package declared twice is +installed once, one no module declares any more is not removed, an installed one is reported unchanged and not +upgraded). + **Added 2026-10-04.** `triggerhappy` holds `node-hotkeys` on both workstations (ADR 0212). The laptop model's vendor keys become its contribution. The window-manager fragments of the launcher, the clipboard, the wallpaper, the bar and the laptop model become `config` contributions to diff --git a/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md b/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md index 4f169223..fe551841 100644 --- a/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md +++ b/03-DESIGN/01-to-be/45-a-core-that-cannot-fail-silently.md @@ -4,7 +4,10 @@ status: in-progress code: [mesh-controller, mesh-host, mesh-tools, mesh-catalog, mesh-sdk, mesh-lab] updated: 2026-10-12 decisions: + - "ADR 0325" + - "ADR 0323" - "ADR 0318" + - "ADR 0314" - "ADR 0293" - "ADR 0289" - "ADR 0285" @@ -79,6 +82,7 @@ Every kind of state the core keeps, and the one component that writes it. Anyone | a machine's declaration | controller (lease holder) | the bus, last per subject | read | | a hand-over asked of a machine ([issue 356](../../04-ISSUES/356-the-operators-command-ran-a-stale-engine-and-a-hand-over-had-no-channel/00-report.md)) | controller, at its terminal | the machine, beside its state | the engine verifies the mesh's signature and records it, or refuses | | a fresh setuid search asked of a machine ([issue 361](../../04-ISSUES/361-the-control-nodes-agent-account-is-never-judged/00-report.md)) | controller, at its terminal | the machine, which throws its last search away | the engine verifies the mesh's signature and starts it, or refuses | +| the take of a shared folder asked of a machine (ADR 0325 §8, issue 567; subject `mesh.node..ask.access-act`) | controller, on the operator's warrant | the machine, which records the act | the engine verifies the mesh's signature and takes it, or refuses | | a machine's applied state and its report | the node-engine's apply queue (§6) | the machine; the report on the bus | the reconcile and a delivery *enqueue*, never apply | | the controller lease | the controller instance holding it | key-value `mesh-controller_lease` | a candidate waits | | plans and their tiers | controller (lease holder), compare-and-set on the plan's revision | the controller's store | read through `plans` | @@ -551,6 +555,22 @@ the controller's replay R301 and its tests of the rule). > its cause, so the exception is read from the verbs. Nothing decided changes: ADR 0230's acts stay > exempt, and pushes by hand — what roll-out by default (ADR 0236) exists to end — still count. +**A condition no healer can fix becomes an issue** (2026-10-12, ADR 0314, extending ADR 0285; the build is +tracked by issue 409). A condition no healer can fix is one whose kind no healer answers, one a healer passes +over because it does not apply, or one handed to the operator after a healer's budget was spent. When such a +condition is raised, passed over or handed on, and no issue links its key (ADR 0285), the issue tracker opens +one itself, from the controller's condition events, with the condition's summary, its evidence and its key, and +links the key at once; the controller sets no link. Every message about the condition then names the issue +through its tracking line. Agent sessions take such an issue as any other: a claiming move that names the +worker, a branch, review and the required checks; no standing agent acts on a condition by itself. What an +agent does about it is written in the issue's notes, and every act it makes by hand on the mesh is also written +to the hand-act log, which S15 reads. ADR 0275 §9's week stands: the tracker opens no issue for a condition +before 2026-10-17 20:17 UTC, and until then such a condition is linked by whoever opens an issue for it. *How +it is checked:* the tracker's tests feeding condition events (one issue opened and linked for each of the three +cases, none for a condition a healer acts on or one already linked, none before the week ends); live, the +messenger's preview of such a condition says "Tracked as issue N", and the issue's notes and the hand-act log +hold what the agent did. + ## 8. Staged core upgrades and rollback (rule 8) **Health, per core component**, as probes in the registry (§4): @@ -680,6 +700,19 @@ what is not an ordinary send, and the verdict — posted on the pull request. Th a state machine, kept, followed by the release and written to the commit as a note; which parts are built is Phase 5's. +Revision, ADR 0323 (2026-10-12). **The gate judges what merging makes, not the head.** The build seat takes +the trunk the pull request merges into, its head at that moment as the check's **trunk commit**, and makes in +its own clone the merge of the head into it, as the forge's default merge would; both layers judge that +merge, and a head that does not merge cleanly fails, naming the files in conflict. The merge is only checked: +never pushed, never registered. The verdict names the head and the trunk commit in both statuses' words +("head … merged into trunk …"), in mesh-delivery's `checks` and `show` and in the kept delivery plan, and +names the commits of the other repositories the check read; their move is issue 568's question. When the +trunk moves, the forge's holder puts both statuses of every open pull request into it back to pending with +the reason and runs nothing; the merge tool refuses a verdict whose trunk commit is not the trunk's head, +asks that head's check once, says how long the last check of the repository took, and never merges by +itself (checked by the builder's `TestReplay341`, replayed as R341, and the forge module's `trunk` and +`guard` tests). + **The replays.** mesh-lab carries a scripted scenario for each core incident, asserting the rule's outcome, run on every merge to mesh-controller, `mesh-host` and mesh-tools: @@ -901,4 +934,3 @@ in [to-be 47](47-delivery-from-commit-to-delivered.md), and their progress is tr - Routing by presence, quiet hours, answering back through a channel, and an external dead-man service — decided by ADR 0234, designed in [to-be 46](46-the-conversation-with-the-operator.md). -- A condition that needs judgement handed to an agent as work — tracked by issue 409, since research 017 closed. diff --git a/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md b/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md index 05b9f72b..da34cd72 100644 --- a/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md +++ b/03-DESIGN/01-to-be/46-the-conversation-with-the-operator.md @@ -2,8 +2,9 @@ layer: to-be status: in-progress code: [mesh-controller, mesh-tools, mesh-sdk, mesh-catalog] -updated: 2026-10-10 +updated: 2026-10-12 decisions: + - "ADR 0308" - "ADR 0286" - "ADR 0277" - "ADR 0259" @@ -289,6 +290,40 @@ Presence facts are events on the bus. The router keeps them as **current state o machine and per intake kind, overwritten — never as a history, and never in a message's words. A consumer that wants a timeline of the operator's day is a decision of its own. +### While a delivery walks + +*2026-10-12, ADR 0308; the build is tracked by issue 532.* + +A delivery restarts what it moves, and those restarts raise conditions that clear once it is done; a router +that restarted used to say again what it had been told to keep quiet (issue 531). So the router holds what a +walk touches and tells the walk's result once. + +- **A walk names what it touches.** The controller's walk events say, when a walk starts and again when it + ends with its outcome (delivered, failed or stopped), the modules it moves and the machines it sends them to. + What it touches is those modules on those machines, and every module a moved tool runner or node-engine + carries there. +- **The router holds what an open walk touches.** A message whose subject is a touched module or machine is + held instead of sent, kept with when it was held, and `history` says it was held. A message several open + walks touch is held until the last of them ends or its bound passes. +- **Some messages are never held:** an ask (§6), a secret given or replaced, a message about anything no open + walk touches, and a walk's own failure. +- **A walk's end is told once:** one message with its outcome, the modules and machines it reached and how + long it took, then each held message whose condition is still open, one line each. One whose condition + cleared during the walk is only counted. A walk delivered with nothing held and nothing open is one line, or + nothing on a channel set to say only what needs the operator. +- **A hold has a bound:** twice the largest delivery budget in force among the walk's deliveries + ([to-be 47](47-delivery-from-commit-to-delivered.md)): 20 minutes for a core module, 10 for leaf modules at + a five-minute budget and 6 at a three-minute one; 15 minutes, not doubled, for a walk none of whose + deliveries has a budget. Past it, every held message still open is sent with a line saying the walk still + runs and since when, and the cleared ones are counted. A walk that hangs never silences the mesh. +- **A restart says nothing it has said:** a router that restarts says again only what is open and was never + answered, never a silenced message or one withheld for its content. + +*How it is checked:* the controller's tests of the walk events, and the router's tests of holding, never +holding, the end message, the bound and the restart, as ADR 0308 lists them; live, the next delivery of the +tool runner gives the operator one message at its end, and the channel's `history` shows what was held and +released. + ### The content rule The router's, applied before anything reaches any holder; §9 says what it allows. A `private` holder diff --git a/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md b/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md index 3625ebea..8e7be134 100644 --- a/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md +++ b/03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md @@ -2,8 +2,11 @@ layer: to-be status: in-progress code: [mesh-controller, mesh-catalog modules/mesh-delivery] -updated: 2026-10-11 +updated: 2026-10-12 decisions: + - "ADR 0323" + - "ADR 0313" + - "ADR 0306" - "ADR 0299" - "ADR 0298" - "ADR 0282" @@ -79,7 +82,7 @@ allows. | State | Means | Bound | H2 after the bound | |---|---|---|---| -| proposed | a head was announced and is being checked | 1 hour | nothing: the check's own watchdog (S6) speaks | +| proposed | a head was announced and is being checked | 1 hour from when its check is asked; none while its verdict is out of date and no check is asked | nothing: the check's own watchdog (S6) speaks | | checked | a verdict arrived | 1 minute | nothing: it is decided at once | | ready | it passed; it waits to be merged | none | — | | rejected | it failed its check | none | — | @@ -88,7 +91,32 @@ allows. | delivering | its walk runs; its builds are asked and registered tier by tier | 2 hours | `delivered`, `failed` or `superseded`, as the walk's record says | | delivered, failed, superseded, stopped, retired, covered | final | — | — | -The transitions are those of ADR 0239 decision 2 and ADR 0270. A delivery that is +**A verdict holds for one trunk commit** (ADR 0323). The check judges the head merged into the trunk's head +at the moment the build seat clones it, the **trunk commit**, and the verdict names both. When another merge +moves the trunk, a `ready` delivery goes back to `proposed` on the observed event `trunk moved`, with both +commits in the transition's reason; a `rejected` one stays `rejected`, its statuses saying the verdict is out +of date. Nothing is asked for it: a recheck, a new head or the merge tool asks the check again. The row from +`proposed` to `checked` takes only a verdict that names the trunk's head as its trunk commit, so a verdict for +an older trunk is kept and shown and does not count, and the rows reach a fixed point. Checked by +mesh-delivery's `TestATrunkMoveSendsAReadyDeliveryBackToProposed` and the table's own walk. + +What this leaves as it is, said so nobody reads more into it: + +- **mesh-delivery's trunk head is the last merge it heard** into that trunk while the delivery was open. A push + to the trunk that is not a merge it hears does not move it; the forge's holder, which reads the branch, and + the merge tool, which compares commits when asked, still catch it. +- **A verdict that names no trunk commit**, from a build seat older than ADR 0323, counts in mesh-delivery as it + did: such a delivery stays `ready`. The forge reads the same verdict out of date once its trunk moves, and the + merge tool refuses it and asks the check. The two agree once the head is judged again. +- **A verdict no build judges** (a change that touches no module, in a repository with no `merge-check.sh` or + not the mesh's) names the base branch's head the forge announced with the head as its trunk commit. +- **In a busy repository the merge tool can refuse, ask, and refuse again**: another merge may move the trunk + while the asked check runs, and the new verdict is then out of date too. Each refusal asks once more; the + merge goes through once a check finishes before the next merge. +- **A check that was asked and never answers** is listed by `stalled` an hour after the ask, and is asked again + with `mesh-delivery.recheck`, which the merge tool's refusal names. + +The transitions are those of ADR 0239 decision 2, ADR 0270 and ADR 0323. A delivery that is merged while not `ready` goes to `held`. Its check did not pass, and only a person decides that it goes on. A delivery merged with no walk answering it within ten minutes is held too, with the true reason, and listed by `stalled` at once. Nothing waits silently. @@ -102,7 +130,11 @@ delivery merged with no walk answering it within ten minutes is held too, with t forge says that later merge commit contains it (`gitea_contains`, asked once per pair and kept). Merge order is only the first filter, and a forge that cannot answer means not yet covered. `mesh/delivery` says `covered: a later merge that contains it was delivered` as a success. A module that runs on one - machine only gets no "rest" step and is never covered; + machine only is covered too: its walk records the empty rest as sent once its first machine passes + (ADR 0270, as corrected 2026-10-12); +- *a stop whose walk ended without building its modules* (issue 474) stays `stopped`, a final state, and + is read as covered once a later merge covers it by the same test: `mesh/delivery` then says `covered after + its stop` with the covering merge, once, as a success. No state or transition is added (issue 480); - the covered row is tried before `go`, from `held` and from `ready` past the ten minutes. **History is retired, not held** (ADR 0250). @@ -217,6 +249,14 @@ shown and held to no budget. Measured on 51 walks on 2026-10-09 and 10, a leaf w (7 of 20 within five), a core walk of one tier 5.6 (worst 8.8), and a controller walk dragging two catalogue tiers 14.2 (issue 338). +*A wider measure, around the delivery budget (2026-10-12, ADR 0313, extending ADR 0282).* The delivery budget +above runs from the merge; the operator's own target is wider, the **fix time**, which runs from an issue's +first move to `fixing` to the moment its last fix is delivered. Each issue's class carries a **fix budget** too +(five minutes leaf, ten core, the delivery budget's own figures), and the tracker splits a fix time into four +phases — implementing, review, checks, and merge to running (the delivery time above) — so a fix over its +budget is said loud, naming its longest phase, and clears at the next fix of its class delivered within budget. +The delivery budget stays as written: a measure inside the fix budget, not replaced by it. + **What changes in the walk.** - **A lone merge is cut at once**: no batch assembling, and no member of its delivery group still unmerged. @@ -232,6 +272,32 @@ shown and held to no budget. Measured on 51 walks on 2026-10-09 and 10, a leaf w - **Every phase is kept**: the window, the wait, the build, the send, each reading, the rest's first healthy report; the plan shows an expected landing time from the last seven days' medians. +*Walks beside each other (2026-10-12, ADR 0306, extending ADR 0304, which narrowed the bullets above to what +a change shares).* Building the walks at once found what those bullets left open, and ADR 0306 decides it: + +- **A walk's builds are its own, never another's backlog.** A send composes a module whose build another open + walk owns at the build its node was last sent, and neither carries nor judges it: that build is moved and + judged only by its own walk, so a failed walk puts back only its own builds. What waits on a node with no open + walk to move it is still backlog, carried and judged by the next gated send there (this changes ADR 0236 §17 + for builds an open walk owns). +- **A delivery group still assembles.** A merge whose delivery group has a member not yet merged waits for the + merge window whether or not a batch is assembling, so a group's members walk in one batch, in their order. +- **Related means everything the walks would move.** A module in common is any module a walk plans to build or + send, the dependents of what its merges moved included; a change whose modules are not known yet is related to + every change. Two related walks are neither cut beside each other nor judged beside each other on one node. +- **A starved judgement is said.** A leaf judgement set aside by core builds three times in a row raises a + warning naming the walk and the core walks that set it aside. +- **A node's own fault fails every judgement on it.** A node found unwell as a whole (a failed apply, an + unhealthy network or engine) fails every judgement open on it, leaf and core, each walk putting back only its + own builds. +- **A rollback beside another walk's judgement** behaves as it does today; whether it should wait is tracked by + issue 528. + +*How it is checked:* the controller's tests ADR 0306 lists, one per clause: an unsent build of walk A left out +of walk B's send and not taken up as backlog while A is open; a group's second member joining its first's batch; +overlapping dependents keeping two walks apart; the warning on the third set-aside; a failed apply failing every +open judgement on its node. + **Who builds what.** | Part | Owner | What it adds | @@ -268,9 +334,15 @@ only once it is built. first, then *built by* and *version skew*, then *engine before controller*. The pair shows the rule that won and the rules it won over. Rules of one rank both ways are a contradiction. The group is `rejected`, and the reason names both members, both rules and the `after:` line that would settle it. -- **Check**: the members' heads composed together, one gate over every machine with all their definitions. It - is asked by mesh-delivery through `delivery-check` when the group forms or a member's head moves, and - posted on every member's head as `mesh/delivery-group`. +- **Check**: the members' heads composed together, one gate over every machine with all their definitions, + each head merged into its own trunk's head first and named with that trunk commit (ADR 0323). It is asked + by mesh-delivery through `delivery-check` when the group forms or a member's head moves, and posted on + every member's head as `mesh/delivery-group`. A member rebased or pushed is a new head with its own check: + it takes its old head's place in the group, even once the group is delivering, and the composed check runs + again for the new heads instead of holding the group. A merge into a member's trunk that is not one of the + group's members puts the composed check out of date: the group reads `checking` with the reason, and the + composed check runs again once a member is judged again. Checked by mesh-delivery's + `TestAGroupsComposedCheckGoesOutOfDateOnAMergeThatIsNotItsMember` and `TestARebasedMemberIsComposedAgainNotHeld`. - **Delivering**: in order. A member waits in `published` until the one before is `delivered` or `covered`. A member published while its group's composed check is asked and not answered waits there for the answer; one held because that check had not passed is *lifted* back to `published` once it passes or warns for the @@ -307,7 +379,7 @@ In this order, each by its owner: | Verb | Answers or does | |---|---| -| `deliveries` | every delivery not final, and the final ones of the last day, one line each; filtered by state, repository or group | +| `deliveries` | every delivery not final, the final ones of the last day, and every final one `stalled` still lists, one line each; filtered by state, repository or group | | `show` | one delivery or group whole: plan, transitions, machine steps, order, the walk | | `groups` | every group with its members in order and its derived state | | `what-if` | the delivery plan a diffset would have, asked of the controller's planner, kept nowhere | diff --git a/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md b/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md index ca2e748f..06dad5ed 100644 --- a/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md +++ b/03-DESIGN/01-to-be/51-the-registries-keep-what-is-named.md @@ -6,8 +6,9 @@ code: - mesh-catalog modules/artifact-store-tools (the store's tools), modules/distribution (its unbuilt code removed) - mesh-catalog modules/gitea (package retention) - mesh-catalog modules/docker (image pruning) -updated: 2026-10-08 +updated: 2026-10-12 decisions: + - "ADR 0311" - "ADR 0251" - "ADR 0189" - "ADR 0257" @@ -110,7 +111,8 @@ ADR 0257: failed one, and the controller keeps a row per copy in `artifact_mirrored`. The copies the existing build records already named were recorded by the migration that made the table. - **Kept by the kept builds that stood on it** (`stood-on`), and by nothing else: a definition never - keeps a copy. Otherwise a copy is eligible, like any artifact the mesh made. + keeps a copy. Otherwise a copy is eligible, like any artifact the mesh made. *(A copy of a pinned + third-party image is the exception, below: ADR 0311 §3.)* - **Held whole.** A copy counts as held only when its index and every platform the index names are held; otherwise it is copied again. A copy a build copies again is no longer recorded as let go of. - **An index goes with its platforms only in a confirmed collect.** Before its first delete, it reads @@ -122,6 +124,35 @@ ADR 0257: `unrecorded_mirrors`, with what it alone marks. The controller's `mirrors` verb records the ones a person names, and the sweep then decides about them. +### Pinned third-party images + +*2026-10-12, ADR 0311, extending ADR 0257; the build is tracked by issue 421.* A third-party image a module +pins was held only upstream, and an image withdrawn there was found only when a node that lacked it tried to +deploy (issue 113). So the mesh holds a copy of what its modules pin, and checks both sources: + +- **Every pinned third-party image is copied** into the artifact store when it is first used, into the + repository of its upstream image (`upstream//`, as above). +- **Its record names the pin.** The copy's row names, as its reason, the module whose definition pinned the + digest (changing ADR 0257 §6, which named only a build or a person's reason). +- **Kept while a module pins it.** A copy is kept, with the reason `pinned`, while any module's current + definition pins its digest; neither the sweep after a build nor the nightly clean-up lets it go. For these + copies a definition does keep a copy, and one is eligible only when no kept build stood on it and no current + definition pins it (changing ADR 0257 §8, §9 and §20). `artifact_store_references` shows them with the reason + `pinned`. +- **A node pulls from the mesh first**, from the artifact store's copy, and from the upstream only second. +- **A scheduled check pulls every pinned image as an empty node would**, once from the copy and once from the + upstream. An upstream that no longer serves while the copy does raises a warning condition naming the module + and the image; neither serving raises an urgent one; each clears once the image is served again. An upstream + that serves while the copy does not is not decided yet (issue 421); a node then still pulls from the upstream. +- **A module may declare its upstream**, the object store's module first, with its maintained fork, and a view + shows each declared upstream's last release and open advisories, so ADR 0288's triggers of no release for six + months and of an advisory unpatched for 30 days can be read there. + +*How it is checked:* the table in ADR 0311 — the controller's copy and its row, the collection test of keeping +by pin, the node-engine's pull from the store with the upstream unreachable, the check's two conditions, the +manifest test of a declared upstream, the view against a recorded answer, and live, the object store's pinned +image held in the store. + ### The controller's verbs for the store - **`artifacts`** reads only. It answers every reference the mesh recorded making, with its state and, diff --git a/03-DESIGN/01-to-be/55-the-knowledge-base.md b/03-DESIGN/01-to-be/55-the-knowledge-base.md index 677b82d1..924dbcb0 100644 --- a/03-DESIGN/01-to-be/55-the-knowledge-base.md +++ b/03-DESIGN/01-to-be/55-the-knowledge-base.md @@ -2,9 +2,10 @@ layer: to-be status: designed code: [] -updated: 2026-10-11 +updated: 2026-10-12 decisions: - "ADR 0290" + - "ADR 0321" --- # 55 — The knowledge base @@ -125,8 +126,17 @@ approval. On `approve`: On acceptance the record and its clauses move to `accepted` and `in force`, every clause its `changes` and `supersedes` name moves to `changed` or `superseded`, and `accepted` and `changed` events are emitted. Then the 4 -rules that judge the whole record set run (an accepted decision cited by nothing, and the like); a failure raises a -condition naming the rule and the records, and does not undo the acceptance. +rules that judge the whole record set run (an accepted decision cited by nothing, and the like). A failure does not +undo the acceptance. It is a fault of the records, which their authors repair, not of the module: it is told to the +operator once, as a warning naming the rule and the records, and cleared once the rule holds again. The module's +health names it and stays healthy, because health says only whether the module works, and a finding never holds a +walk at the module's first-node gate (the operator's decision of 2026-10-12 on issue 504, as issue 508 decided for a proposal never +reviewed). + +The rule `cited` is also judged ahead, in the review, before the judgement: a decision that names no home and that no +accepted record links is not accepted. Its review, by the knowledge base itself, asks changes naming the rule (issue +504). It is not refused at `propose`, because no record can link a number only reserved: a record that cites a new +decision is proposed after it and accepted, and the new decision is proposed again. An accepted record is changed only by a new record, or by a progressive insight: a new version marked as one, which is reviewed like a proposal. @@ -148,6 +158,14 @@ per clause whether the record says exactly that; an accepted clause becomes `con same way. When every record of a topic has been through both, the operator is told once per topic, with the counts confirmed and still drafted. `return` with a record's number and a reason sends it back to be drafted again. +**A drafted clause may be changed, and a linked clause keeps its number** (ADR 0321, issue 494). A later record's +`changes` may name a drafted clause, confirmed or not, as it may one in force or changed; only a superseded or proposed +clause is refused. On that record's acceptance the drafted clause is marked `changed`, naming the clause that changed +it, keeps its number and stays `drafted`: it is still never served as in force, and `get` and `ask` say both. Because +a redraft numbers a record's clauses anew, `return` refuses a record any of whose clauses another record changed or +links by number, in a version served or proposed, naming each such link; a record that changes the drafted clause is +the way to correct it. A record no other record links by clause is returned as before. + ## 8. The import `import` takes a commit of this repository (and `dry-run`) and writes every decision record and research effort @@ -184,10 +202,12 @@ repository keeps no copy of the records, generated or otherwise (issue 435, answ |---|---| | A proposal that breaks a per-record rule is refused, naming every reason | the module's table test, one case per rule of § 4 | | A record is accepted only after the knowledge base's own judgement approves it, and, when it needs the operator, only on the warrant | the module's tests: a proposal whose judgement answers changes, or cannot be had, stays proposed; a `needs operator` record unaccepted without the warrant, accepted with it | -| A whole-set rule that fails is loud | the module's test: an acceptance that leaves a decision cited by nothing raises the condition | +| A decision is not accepted cited by nothing | the module's test: a decision with no home and no record linking it gets changes from the review, with no judgement asked; with a home, or once a record citing it is accepted, it is accepted | +| A whole-set rule that fails is loud, and leaves the module healthy | the module's test: a correction that leaves a decision cited by nothing tells the operator once, as a warning naming the rule and the record, not again after a restart, and clears it once a record cites it; the module's health stays healthy throughout | | Every version is kept and comparable | the module's test: three versions, `history` lists them, the difference of the first and third is exact | | The import changes nothing on a second run, and loses nothing | the module's test over a fixture repository; and the comparison of § 8 on issue 426 | | A drafted clause is never served as in force | the module's test: `holds` over a record with only drafted clauses answers none, and `ask` marks them | +| A drafted clause may be changed, and a linked clause is never renumbered | the module's tests: a record changing a drafted clause is accepted, and the clause is then `changed`, keeps its number, stays drafted and is not served; `return` of a record whose clause another record changed or links by number is refused naming the link, and writes nothing | | An answer stays within its bound | the module's test over the full import of `main` at a recorded commit: every `ask`, `get` and `holds` of a fixed question list | | A record written in this repository meanwhile writes its clauses | `records.py`, check `clauses`, on every hq pull request, until the records left this repository (issue 426); since then `records.py`, check `no-records`, refuses any record written here | | Live | the acceptance sentence, asked from a session outside this repository |