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:
@@ -17,6 +17,14 @@ touches:
|
|||||||
|
|
||||||
# 011 — The module graph
|
# 011 — The module graph
|
||||||
|
|
||||||
|
> **A third edge was found after this graduated.** This effort established *presence* and
|
||||||
|
> *instantiation*, and both are **runtime** edges — they answer *what does this need in order to
|
||||||
|
> run*. Delivery needs a different question answered — *what has to be rebuilt when this changes*
|
||||||
|
> — and that is a **build** edge, fixed inside an artifact rather than negotiated when it runs.
|
||||||
|
> Recorded by [ADR 0064](../../02-DECISIONS/0064-a-build-edge-is-a-third-kind.md), which also
|
||||||
|
> notes what this effort's three entities turn out to be good for
|
||||||
|
> ([ADR 0065](../../02-DECISIONS/0065-the-core-library-is-the-meshs-domain.md)).
|
||||||
|
|
||||||
## What is being investigated
|
## What is being investigated
|
||||||
|
|
||||||
Whether the catalogue's missing structure is a **graph** — modules declaring what they need,
|
Whether the catalogue's missing structure is a **graph** — modules declaring what they need,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ the rule connecting them is stated here.
|
|||||||
| `novox/mesh-substrate` | 1 | the four pinned services, as declarations |
|
| `novox/mesh-substrate` | 1 | the four pinned services, as declarations |
|
||||||
| `novox/mesh-control` | 2 | the control plane and its contexts |
|
| `novox/mesh-control` | 2 | the control plane and its contexts |
|
||||||
| `novox/mesh-surfaces` | 3 | tools, web, cli — thin, no logic |
|
| `novox/mesh-surfaces` | 3 | tools, web, cli — thin, no logic |
|
||||||
| `novox/mesh-sdk` | — | contracts shared across tiers: types, not behaviour |
|
| `novox/mesh-sdk` | — | ~~contracts shared across tiers: types, not behaviour~~ → **the mesh's own domain** ([ADR 0065](0065-the-core-library-is-the-meshs-domain.md)) |
|
||||||
| `novox/mesh-lab` | — | the lab: scenario lifecycle, networking, placement |
|
| `novox/mesh-lab` | — | the lab: scenario lifecycle, networking, placement |
|
||||||
| `novox/hq` | — | this repository. Company-scoped ([ADR 0028](0028-hq-is-company-scoped.md)) |
|
| `novox/hq` | — | this repository. Company-scoped ([ADR 0028](0028-hq-is-company-scoped.md)) |
|
||||||
|
|
||||||
|
|||||||
@@ -87,10 +87,12 @@ examined.
|
|||||||
- **"Is this artifact current?" must be answerable without building it.** A commit recorded
|
- **"Is this artifact current?" must be answerable without building it.** A commit recorded
|
||||||
against each artifact does it, and that record becomes load-bearing: wrong, and the mesh either
|
against each artifact does it, and that record becomes load-bearing: wrong, and the mesh either
|
||||||
rebuilds forever or never rebuilds at all.
|
rebuilds forever or never rebuilds at all.
|
||||||
- **Rebuild storms.** One change to a shared library makes everything out of date at once. The
|
- **Rebuild storms are real and mostly behaviourally empty.** One shared-library commit
|
||||||
ordering that today's *levels* provide has to come from the module graph
|
invalidates nearly everything, and most of those rebuilds produce artifacts that do the same
|
||||||
([research 011](../01-RESEARCH/011-the-module-graph/00-overview.md)), which is designed and not
|
thing they did before — so **the fleet is redeployed for no change in behaviour.** Reproducible
|
||||||
built.
|
builds would stop the cascade at the first module whose output did not move; without them, the
|
||||||
|
storm is in the declarations rather than the builds
|
||||||
|
([ADR 0064](0064-a-build-edge-is-a-third-kind.md)).
|
||||||
- **The run identity people actually use is lost.** *Did my change go out?* is answerable today
|
- **The run identity people actually use is lost.** *Did my change go out?* is answerable today
|
||||||
by opening a pipeline. With convergence there is no run to open, and **something has to replace
|
by opening a pipeline. With convergence there is no run to open, and **something has to replace
|
||||||
that** — a query over the two comparisons above — or this will be worse to live with than what
|
that** — a query over the two comparisons above — or this will be worse to live with than what
|
||||||
@@ -101,6 +103,18 @@ examined.
|
|||||||
the fault this record is removing, reintroduced in a new place. **This is the real risk and it
|
the fault this record is removing, reintroduced in a new place. **This is the real risk and it
|
||||||
is not solved here.**
|
is not solved here.**
|
||||||
|
|
||||||
|
## What must exist first
|
||||||
|
|
||||||
|
Stated as a list because this record cannot be implemented without them, and saying so is better
|
||||||
|
than discovering it:
|
||||||
|
|
||||||
|
1. **The module graph, including build edges** ([ADR 0064](0064-a-build-edge-is-a-third-kind.md)).
|
||||||
|
Designed, not built. Without it there is no rebuild set and no ordering.
|
||||||
|
2. **A recorded input closure per artifact** — its commit and the identity of everything it was
|
||||||
|
built against — so *is this current?* is answerable without building.
|
||||||
|
3. **Something that notices a reconciler is not converging.** Below, and the one that is a risk
|
||||||
|
rather than a cost.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- **Detection stops being correctness and becomes latency.** The specific faults the as-is
|
- **Detection stops being correctness and becomes latency.** The specific faults the as-is
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
status: proposed
|
||||||
|
date: 2026-08-28
|
||||||
|
deciders: jochen
|
||||||
|
reconstructed: false
|
||||||
|
extends: 0064-a-build-edge-is-a-third-kind.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# 65. The core library is the mesh's domain, and types ship with their modules
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR 0030](0030-the-repository-structure.md) describes the shared library as *"contracts shared
|
||||||
|
across tiers: types, not behaviour"* — a guard against what the current one became, which is a
|
||||||
|
package holding too much code and, with it, everybody's dependencies.
|
||||||
|
|
||||||
|
**The guard is aimed at the wrong thing.** *Types, not behaviour* sounds safe and does not
|
||||||
|
address the fault: a library everything depends on is a hub whether it holds types or code. The
|
||||||
|
fan-in is what makes a change expensive, and *types not behaviour* leaves the fan-in exactly
|
||||||
|
where it was.
|
||||||
|
|
||||||
|
[ADR 0064](0064-a-build-edge-is-a-third-kind.md) makes this measurable rather than a matter of
|
||||||
|
taste: a module's cost is its **inbound build edges**, and a type hub has as many as a code hub.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### Types ship with the module they belong to
|
||||||
|
|
||||||
|
A type is part of a module's contract, so it travels with the module. A consumer needing
|
||||||
|
`inventory`'s types depends on **`inventory`** — a real edge, narrow and visible — instead of both
|
||||||
|
depending on a hub where the relationship cannot be seen.
|
||||||
|
|
||||||
|
**This trades one wide edge for several narrow ones, and that is the improvement.** Under
|
||||||
|
[ADR 0064](0064-a-build-edge-is-a-third-kind.md) a change to one module's types now invalidates
|
||||||
|
its actual consumers, rather than everything that touched the hub.
|
||||||
|
|
||||||
|
### The core library holds the mesh's own domain, and that is the test
|
||||||
|
|
||||||
|
Not *shared code*. A drawer labelled shared is a drawer everything goes in, which is how the
|
||||||
|
current one grew.
|
||||||
|
|
||||||
|
> **The core library holds what is true of the mesh regardless of which context you are in.**
|
||||||
|
|
||||||
|
[Research 011](../01-RESEARCH/011-the-module-graph/00-overview.md) already found what that is:
|
||||||
|
**a module, a node, and an assignment.** Those three are the mesh's domain — every context speaks
|
||||||
|
about them, and none of them belongs to one context.
|
||||||
|
|
||||||
|
The test, applied to a candidate: *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 — that is
|
||||||
|
delivery's. A grant does not — that is provisioning's.
|
||||||
|
|
||||||
|
**Domain-driven is the point rather than the label.** The current library is what happens when
|
||||||
|
the organising idea is *who else might want this*: the answer is always yes, so everything is
|
||||||
|
admitted. *Is this the mesh's domain* has a defensible no.
|
||||||
|
|
||||||
|
### Why this stays small on its own
|
||||||
|
|
||||||
|
A domain model changes when what the mesh **is** changes, which is rare. A shared-code drawer
|
||||||
|
changes whenever anybody writes something reusable, which is constantly. So the core library
|
||||||
|
inherits the property the graph is supposed to reveal — **few changes, many dependants** — rather
|
||||||
|
than fighting for it.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **[ADR 0030](0030-the-repository-structure.md)'s description of the shared library is
|
||||||
|
superseded.** *Types, not behaviour* is replaced by *the mesh's domain*, and its types move to
|
||||||
|
the modules that own them. The rest of 0030 — the naming rule and the repository list — stands.
|
||||||
|
- **A module now publishes its own contract**, which it does not do today. That is real work and
|
||||||
|
it is the same work as making a module a self-contained artifact, so it is not additional.
|
||||||
|
- **The check is not the one that was proposed and is better.** *The build output contains no
|
||||||
|
runtime code* would have enforced *types, not behaviour* — a rule now withdrawn. What replaces
|
||||||
|
it is **inbound build edges**, which is a measurement rather than a prohibition: the core
|
||||||
|
library should have many, and anything else acquiring many is turning into a hub. That is
|
||||||
|
visible while it happens rather than after.
|
||||||
|
- **Fewer things will be shared, and some code will be written twice.** That is the trade and it
|
||||||
|
should be said plainly: the current library exists because sharing felt free. Under this it has
|
||||||
|
a name, an owner and a visible edge, and two similar functions in two modules is often the
|
||||||
|
better answer — `how-we-build` §8 already says three similar lines beat a premature
|
||||||
|
abstraction.
|
||||||
|
- **Nothing here says how a module publishes its types**, and the answer differs per language.
|
||||||
|
That belongs with delivery.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR 0030](0030-the-repository-structure.md) — the description this replaces.
|
||||||
|
- [ADR 0064](0064-a-build-edge-is-a-third-kind.md) — what makes fan-in measurable.
|
||||||
|
- [Research 011](../01-RESEARCH/011-the-module-graph/00-overview.md) — module, node, assignment.
|
||||||
Reference in New Issue
Block a user