A build edge, a core library that is a domain, and 0063 corrected

Three things from walking a real dev cycle through 0063, all of which Jochen
caught by pushing on where I had glossed.

0064 -- a build edge is a third kind. Research 011 established presence and
instantiation, and both are RUNTIME edges: they answer what a module needs in
order to run. Delivery needs a different question -- what has to be rebuilt when
this changes -- and that relationship is fixed inside an artifact rather than
negotiated when it runs. So the graph as designed could not drive delivery,
which is the real reason 0063 was not approvable.

It is derived rather than declared, read from what a module actually imports,
because a declared list and the imports it describes drift and the imports are
the true ones. The runtime edges stay declared, and that asymmetry is not an
inconsistency: a runtime edge is an intention somebody has, a build edge is a
fact about code that exists.

It also makes design quality measurable. A module with many inbound build edges
is one whose every change is expensive, and the current shared library is
exactly that -- nobody could see it because nothing drew the edges.

0065 -- the core library is the mesh's domain. Jochen disagreed with 0030's
"types, not behaviour" and was right: that guard is aimed at the wrong thing. A
library everything depends on is a hub whether it holds types or code, and the
fan-in is what makes a change expensive. So types ship with the module that
owns them -- trading one wide edge for several narrow ones -- and the core
library holds what is true of the mesh regardless of context, which research
011 already found: a module, a node, an assignment.

The test is "would this still mean the same thing in a context that had never
heard of the one it came from". A node does; a pipeline stage does not.
Domain-driven is the point rather than the label: "who else might want this"
always answers yes, which is how the current one grew.

And it changes the check for the better. "The build output contains no runtime
code" would have enforced a rule now withdrawn. Inbound build edges is a
measurement rather than a prohibition, and it is visible while a hub is forming
rather than after.

0063 revised on both counts, plus a third: I had written "the lab judges it" as
though that were a step. A lab run takes tens of seconds, occupies a VM, and
fails for environmental reasons -- and a shared-library change produces dozens.
One expensive non-deterministic gate fails both ways, and neither failure looks
like itself. Verdicts are now tiered, and a run that failed environmentally is
explicitly not a verdict.

0063 also now carries what must exist before it can be implemented, rather than
leaving that to be discovered.
This commit is contained in:
2026-08-28 18:25:28 +02:00
parent 4ab8a0507f
commit 9d091c81e0
5 changed files with 205 additions and 5 deletions
@@ -0,0 +1,91 @@
---
status: proposed
date: 2026-08-28
deciders: jochen
reconstructed: false
extends: 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](0044-a-module-declares-presence-instantiation-and-exclusion.md) settled what a module
declares, and [research 011](../01-RESEARCH/011-the-module-graph/00-overview.md) 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](0063-delivery-is-reconciliation-not-a-pipeline.md) 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](0063-delivery-is-reconciliation-not-a-pipeline.md),
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](0063-delivery-is-reconciliation-not-a-pipeline.md) 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
- [ADR 0044](0044-a-module-declares-presence-instantiation-and-exclusion.md) — the two runtime
edges this joins.
- [ADR 0063](0063-delivery-is-reconciliation-not-a-pipeline.md) — what needs this to work.
- [Research 011](../01-RESEARCH/011-the-module-graph/00-overview.md) — the graph this extends.