Merge pull request 'ADR 0255: the bar takes blocks as data from the module that knows the hardware' (#205) from feat/the-bar-takes-blocks into main

This commit was merged in pull request #205.
This commit is contained in:
2026-10-08 12:44:40 +00:00
3 changed files with 229 additions and 1 deletions
@@ -93,6 +93,10 @@ reports, `graphical-session`, still gates the display server itself.
> included, is a dependency on the seat that receives it. What a contribution contains still follows
> the tool's grain.
> **Extended — 2026-10-08, by [ADR 0255](0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md).** A seat may receive a kind as data in a shape the seat
> defines, rendered by its holder's template: `node-bar` receives blocks that way. A contribution may
> also be placed only where the machine reports a capability.
**5. The display server's module writes the session's start.** It writes a block at the start of
`~/.xinitrc`, in this order:
@@ -0,0 +1,207 @@
---
topic: what runs on it
status: accepted
date: 2026-10-08
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md
---
# 255. The bar takes blocks as data from the module that knows the hardware, placed where the machine has it
## Context
The bar's module claims `node-bar` ([ADR 0208](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md)) and
writes both bars' files whole. When it was adopted from the workstations on 2026-10-04, three kinds of
block were left out because they were not the bar's to know:
- the battery, which only the laptop has;
- the GPU's load, an AMD block on the laptop, commented out for NVIDIA on the workstation;
- three Bluetooth headsets by hardware address.
The bar's own documentation said that a block which follows a machine's hardware belongs to the module
that knows that hardware. It also said such a module had no way in. ADR 0208 §4 lets a module
contribute to a holder's file "in the tool's own grain", and
[ADR 0212](0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md) made that one
general form: a seat, a kind the seat receives, and text in the tool's own grammar, which the controller
places and never reads. `node-bar` received nothing. Since then the laptop's bar has shown no battery.
Measured on the laptop on 2026-10-08, the bar's memory block read 96 %. It counted the cache the kernel
gives back when asked as used. The `memory-pressure` module, which judges the same machine, read 73 %.
Three facts shaped the decision:
- **The text form would tie every contributor to one bar.** A module giving the bar a battery under
ADR 0212 would write i3status-rust's own grammar. A second bar module, waybar or polybar, could not
read it, and the seat exists so that its holder can be replaced.
- **The mesh already separates data from format once.** A module's `facts` give a path and a template.
The controller renders the mesh's roster through the template. The data is the mesh's and the format
is the module's.
- **What a machine has is already reported.** The node-engine reports detected capabilities, each with
what it saw and how. The controller uses them to refuse a module a machine cannot run. A battery is
not among them yet.
## Considered Options
1. **The seat receives blocks as data, and its holder renders them.** A contribution says what the block
shows in terms no bar owns. The holder turns it into its own grammar. Chosen by the operator on
2026-10-08: "option 1 is exactly how it should work, other modules can 'provide' block information in
some form or another (use a generic boundary layer)".
2. **Blocks that hide themselves where they do not apply.** i3status-rust's common `if_command` option
(a test for the battery's directory, say) hides a block at run time on a machine without the
hardware. Rejected: the knowledge stays in the bar, every bar asks the same question again at run
time, and another bar module would have to carry the same tests in its own grammar.
3. **A text contribution in the bar's grammar, under ADR 0212 as it stands.** Rejected for the reason
above: the contributor would write one bar's grammar, and the seat could not change hands.
4. **A provision the bar requires.** Rejected: a provision is a service one module serves another
([ADR 0040](0040-what-a-module-is.md)) and is resolved to one provider. A bar takes pieces from any
number of modules on its own machine, and that is what a seat's contributions already are.
On the source of the battery, the operator ruled on the same day that generic information must not come
from a vendor's module. A battery is any laptop's. The laptop model's module was the first candidate
and is not the source.
## Decision
**1. A seat may receive a kind as data.** The seat defines the kind's shape: its fields, their values,
and which fields place a piece. A contribution of that kind gives `data` in that shape instead of
`content`. The holder's claim gives a template for the kind in `renders`: a template over one piece,
written in its tool's grammar. The controller renders each piece through it and places the result.
Each piece is preceded by a comment naming its module, as in ADR 0212 §3. The template is given one
function, `quote`, which writes a value as a JSON string with DEL escaped too: a valid basic string in
TOML and in most tools' grammars. A kind given as text stays as ADR 0212 decided.
**2. The holder places a data kind by its placing fields.** The placeholder is
`${contribution:<seat>:<kind>:<field>=<value>,…}`. Within one placeholder, pieces go in the order the
shape names, then in module order. Across all of its files, the holder places every combination of
the placing fields exactly once. Its template must render every example the seat defines. If a
combination is missing, a contribution is lost. If it is placed twice, the contribution is written
twice. Both are refused at registration. A holder that places none of the kind is not refused. It was
written before the kind existed, and its contributions wait until it places them.
A piece the holder's template renders to nothing, or fails on, is left out of that machine's
declaration and named, with its module and why. This happens to a `shows` the template does not know
yet. The rest of the machine's declaration is sent. Push and plan say which pieces were left out, and
the merge gate refuses a change that leaves out a piece that was placed before.
**3. A contribution of an offered kind may name a capability the machine must report:
`if-capability`.** The controller composes it only for a machine whose node-engine reported that
capability. Where nothing is known, there is nothing. The machine's facts decide at composition. The
tool does not decide at run time.
- The name must be one the node-engine detects. A name nothing detects would leave the piece out on
every machine without a word, so it is refused at registration.
- Only a kind the seat offers (§4) may say it. A kind a holder depends on, such as what a backup keeps
or a key's trigger, is never left out for want of something, so `if-capability` is refused there.
**4. A kind may be offered.** A contribution of an offered kind does not depend on the seat
(an exception to ADR 0212 §4). Where nobody holds the seat, nothing is placed, and the contributor
runs there all the same.
**5. `node-bar` receives `block`, offered, as data.** A block has these fields:
- `bar`: `bottom` or `top`;
- `place`: `resources`, beside the processor, memory and disks, or `status`, beside the sound and
before the clock;
- `order`: a whole number from 0 to 99, 50 when absent;
- `shows`: `battery` (option `device`) or `command` (options `command` and `interval`, one line the
command prints, every interval seconds);
- `options`: the options of what it shows.
Nothing in a block belongs to one bar. A new kind of block is added to the seat's shape, and every
holder's template must render it before the shape is accepted.
**6. Generic hardware is shown by the module that holds the generic seat, conditioned on the machine's
facts.** A vendor's module contributes only what is that vendor's alone. The power module holds
`node-power` on every machine, so it contributes:
- the battery block, `if-capability: battery`;
- the power-draw block, `if-capability: power-meter`.
The laptop model's module contributes no block. Something only its vendor's daemon knows would be its
block, such as the charge limit asusd holds. None is contributed yet.
**7. The node-engine reports two more capabilities, read from the kernel:**
- `battery`: a battery that powers the machine. A device's battery, such as a mouse's or a headset's,
is not one, and neither is an empty bay.
- `power-meter`: something here measures the power drawn, the same meters the power module's sampler
reads. That is a system battery, a processor package's powercap energy counter (RAPL, on Intel and
AMD), or a discrete graphics device that reports its own power. Each package is counted once by its
name, and integrated graphics, which report the package again, are not counted.
**8. The power drawn is measured honestly, and the block says what was measured.**
- **On battery:** the battery's discharge rate. That is the whole machine, and the block says
`machine`.
- **On mains, or on a machine without a battery:** the processor package's power, as a rate of
change of its energy counter. A discrete graphics device's own power is added where the device
reports it. The block says `CPU` or `CPU+GPU`, never the machine.
- An APU's integrated graphics report the whole package, so they are not counted a second time.
- A package Intel shows twice (through its MSRs and through MMIO) is read once.
- A sleeping GPU, AMD or NVIDIA, is neither read nor woken to be asked.
- A counter that gives more than 1000 W was reset, after a resume for example. That sample gives no
reading rather than a number that is not power.
- **The machine at the wall** can only be measured by a metered plug or a UPS. It is not measured.
**9. The energy counter stays root's.** Read often, it leaks what the processor is doing (the
PLATYPUS attack, CVE-2020-8694), which is why the kernel makes it root's. No permission is loosened.
- The power module runs a sampler as root. It reads the counter every five seconds and publishes
only the averaged rate, as one line anyone may read.
- The bar's command prints that line. It says the power is not measured when the line is missing
or older than thirty seconds.
- **The sampler runs wherever the power module is assigned, servers included.** A server has the
energy counter too, so conditioning the sampler on `power-meter` would not keep it off one. The mesh
has no way to condition a resource on another seat (the bar) being held on the machine, and building
one for a process that reads a few kernel files every five seconds is not worth a new mechanism now.
**10. The memory block shows what programs hold,** not counting the cache. That is the figure
`memory-pressure` judges the same machine by.
## Consequences
- The laptop's bar gains the battery and the power draw. A workstation's bar gains the power draw, and its
processor's part is labelled as such. A server has no bar, and nothing is placed there.
- Replacing the bar's module costs one template. No contributor changes.
- `node-bar` is the first seat to receive data. `node-hotkeys`, `node-display-session` and
`node-backup` keep text.
- **Version skew:** an older controller refuses a manifest with `data`, `if-capability` or `renders`.
The controller is delivered before the catalogue, as a delivery group's order rules already require
([ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md) §4).
The node-engine is delivered first of all. Until a machine's node-engine reports `battery` and
`power-meter`, that machine's bar shows neither block, and nothing fails.
- **What got harder:**
- The controller now runs a holder's template, and a template can be wrong. Its examples are rendered
at registration, and a value it prints but does not have is refused, not written as `<no value>`.
- The kind's shape is code in the controller's seat table, as the seats' verbs are. A new block kind
is a change to the controller and to every holder's template.
- **Left open:**
- the machine's draw at the wall, from a metered plug or a UPS;
- a GPU-load block, which belongs to a generic module that does not exist;
- a vendor-only block, such as the laptop's charge limit;
- the sampler on a machine without a bar, until a resource can be conditioned on a seat being held;
- the headsets, which are one person's devices.
## How it is checked
| Rule | Checked by |
|---|---|
| A data contribution fits its seat's shape (fields, values, required options, order 0–99), and text and data are not mixed | the catalogue check; the controller's `TestABlockOutsideTheShapeIsRefused` |
| A holder places every combination of the placing fields once, with a template that renders every example | the catalogue check; `TestAHolderPlacesEveryCombinationOnceWithATemplateThatRendersEveryExample` |
| Pieces are rendered by the holder's template, in place and order, each named by module | `TestABlockIsRenderedByTheHolderInItsPlaceAndOrderNamedByModule` |
| `if-capability` places a contribution only where the machine reported the capability | `TestAContributionIfACapabilityIsPlacedOnlyWhereTheMachineReportsIt`, `TestResolveCarriesTheMachinesCapabilities` |
| `if-capability` names a capability the node-engine detects, on an offered kind only | the catalogue check; `TestIfACapabilityNamesOneTheNodeEngineDetectsOnAnOfferedKindOnly` |
| A piece the holder cannot render is left out and named, and the machine gets the rest | `TestAPieceTheHolderCannotRenderIsLeftOutAndNamedNotTheMachine`; the merge gate refuses a new one |
| The catalogue's bar renders every example of the shape | `TestTheCataloguesBarRendersEveryExampleOfTheShape` |
| `quote` gives a TOML basic string of the same text | `TestQuoteIsATomlBasicStringOfTheSameText` |
| An offered kind makes no dependency on its seat | `TestAnOfferedBlockMakesNoDependencyOnTheBar` |
| `battery` and `power-meter` are read from the kernel, and a device's battery is not the machine's | the node-engine's `power_test.go` |
| The power draw says `machine` only on battery, counts each package once, never counts an APU's graphics twice, never reads a sleeping GPU, and gives no reading for a reset counter | the power module's `draw_test.go` |
| The sampler publishes once per interval and stops when told; the bar's script shows only a line under thirty seconds old | `TestTheLoopPublishesEveryIntervalAndStopsWhenCancelled`, `TestTheBarsScriptShowsOnlyAFreshLine` |
| Only a rate leaves root, readable by anyone | the power module's `TestOnlyTheRateIsPublishedReadableByAnyone` |
| The bar's template renders a battery and a quoted command, each bar places both places once, and the memory block uses what programs hold | the i3status-rust module's `blocks_test.go` |
| On the machines | the merge gate's composition of every machine, and after rollout the bar's `i3status_rust_blocks` tool on the laptop and a workstation |
## References
- [ADR 0208](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md) §4, which this extends
- [ADR 0210](0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md),
[ADR 0212](0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md) §1, §3 and §4, whose
general form this widens
- [ADR 0211](0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md), the power seat
- [To-be 42](../03-DESIGN/01-to-be/42-the-machines-modules-in-order.md)
@@ -2,7 +2,7 @@
layer: to-be
status: in-progress
code: [mesh-catalog, mesh-controller, mesh-host]
updated: 2026-10-04
updated: 2026-10-08
decisions:
- 02-DECISIONS/0215-the-machines-message-bus-is-a-node-seat-and-is-never-restarted-live.md
- 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md
@@ -19,6 +19,7 @@ decisions:
- 02-DECISIONS/0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md
- 02-DECISIONS/0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md
- 02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md
- 02-DECISIONS/0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md
---
# 42. The machines' modules, in order
@@ -118,6 +119,22 @@ laptop model's vendor keys become its contribution. The window-manager fragments
clipboard, the wallpaper, the bar and the laptop model become `config` contributions to
`node-display-session`.
**Added 2026-10-08.** `node-bar` receives blocks as data ([ADR 0255](../../02-DECISIONS/0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md)). A block
says which bar, which place (`resources` or `status`), an order, and what it shows (`battery`, or one
line a `command` prints). `i3status-rust` renders it with the template its claim gives and places
both places of both bars once. The bar writes no block that depends on the hardware:
- `power` contributes the battery where the node-engine reports `battery`, and the power draw where it
reports `power-meter`. On battery the draw is the machine's discharge; otherwise it is the processor
package's power from its energy counter, plus a discrete GPU's own, and says so. A root sampler
publishes only the rate, so the counter stays root's.
- The laptop model's module contributes no block. A vendor-only figure, such as its charge limit, would
be its block.
- The bar's memory block shows what programs hold, the figure `memory-pressure` judges by.
The node-engine is delivered first, so the machines report the two capabilities. The controller comes
next, because it parses `data`, `if-capability` and `renders`. The catalogue comes last. Left open: the
draw at the wall (a metered plug or a UPS), a GPU-load block from a generic module, and the headsets.
## Phase 3 — one machine model
The laptop's hardware module (vendor daemon, GPU mode, charge limit, logind, brightness and vendor keys)