Files
hq/02-DECISIONS/0064-a-build-edge-is-a-third-kind.md
T
jschoubben 10365f2eae Consolidate the design layer: one place per topic
Jochen: a jungle of specs that slightly contradict or patch each other, and
what matters is a working state rather than history. Both are fair and both are
mine.

Measured rather than assumed. 05-the-node-host and 09-the-node-lifecycle both
covered enrolment, the install commands, the unit file, the launcher and
reconcile -- I wrote 09 without taking anything out of 05, so the same things
were said twice and could drift apart.

Split by what each document IS. 05 is the component: what the host is, its
parts, the declaration vocabulary, the build order, how it is verified. 09 is
what happens to it: install, enrol, run, upgrade, retire. The whole "The
process" section left 05, and the unit file moved to 09 where installing is
described. 05 goes from 338 lines to 245 and now points at 09 rather than
restating it.

09 also carried a 105-line "Resolved" section -- six mechanisms framed as
"these were open and here is the answer". The content is needed; the framing is
history, and history is what makes a document read as a changelog rather than a
description. Renamed to what it actually is and the was-open phrasing removed.

Also added 10-delivery.md, which did not exist: four accepted decisions --
0054, 0063, 0064, 0065 -- had no design document at all, which is the specific
reason the delivery picture felt scattered. It is now one document covering
modules, the three edges, the core library, and how a change becomes a running
thing, with a table of what each property is designed against and what must
exist before it can be built.
2026-08-28 18:40:46 +02:00

4.6 KiB

status, date, deciders, reconstructed, extends
status date deciders reconstructed extends
accepted 2026-08-28 jochen false 0044-a-module-declares-presence-instantiation-and-exclusion.md

64. A build edge is a third kind, and it is the one delivery runs on

Context

ADR 0044 settled what a module declares, and research 011 established the edges: presence — the thing exists and is reachable — and instantiation — the provider is asked to make something for this consumer and hands back credentials.

Both are runtime edges. They answer the board needs a database from PostgreSQL.

ADR 0063 needs a different question answered: what has to be rebuilt when this changes? And that is not something either edge can say. The board being compiled against a shared library is not presence and not instantiation — nothing is provisioned, nothing hands back a credential, and the relationship is fixed inside the artifact rather than negotiated when it runs.

So the graph as designed cannot drive delivery, and finding that out is what stopped ADR 0063 being approved as written.

Decision

A build edge is a third kind: this artifact was compiled against that one.

It differs from the runtime edges in the way that matters, which is why it cannot be folded into them:

runtime edges build edge
when it is satisfied at provisioning, and again whenever it must be at build, once
what it binds a consumer to a provider that is running an artifact to another artifact
how it is repaired re-provision, re-grant rebuild — there is no other remedy
what changes it the mesh's decisions somebody's commit

An artifact's currency is its whole input closure

The correction this forces on ADR 0063, which said an artifact is stale when its source moved. That is half of it.

An artifact is out of date when its source moved, or when anything it was built against moved.

So what is recorded against an artifact is not a commit. It is a commit and the identity of every artifact it was built against — which is what makes is this current? answerable without building, and what makes the cascade computable.

It is derived, not declared

Nobody writes a build edge in a manifest. It is read from what the module actually imports, the way a package manager reads a lock file — because a declared list and the imports it describes drift, and the imports are the ones that are true. That is how-we-build's features are detected, not declared, applied to dependencies.

The runtime edges stay declared, and the asymmetry is not an inconsistency: a runtime edge is an intention somebody has about how the mesh should be wired, and nothing but a person can state it. A build edge is a fact about code that already exists.

Consequences

  • The rebuild set becomes computable, which is what ADR 0063 assumed and did not have: change a module, take its transitive inbound build edges, and that is what is stale. In order, because the edges are directed.
  • The graph measures design quality, not just build order. A module with many inbound build edges is one whose every change is expensive — and that is a fact about the design, readable before anything is built. The current shared library is exactly this and nobody could see it, because nothing drew the edges.
  • Fan-in becomes a thing that can be watched. A module acquiring inbound build edges over time is one turning into a hub, and it is visible while it is happening rather than after.
  • Reproducible builds would be worth much more than they look. If rebuilding unchanged source against unchanged inputs produced the same digest, a cascade would stop at the first module whose output did not change. Without that, one shared-library commit redeploys everything behaviourally unchanged. Not solved here, and it is the difference between a cascade and a storm.
  • Reading the edges needs a language-aware tool per language, which is a real cost and the reason declaring them looks tempting. It is still wrong for the reason above.

References