Reconcile: adopt initialization's consolidated HQ as canonical, re-home this session's new work #24

Merged
jschoubben merged 177 commits from reconcile-init-into-main into main 2026-09-05 10:27:11 +00:00
5 changed files with 205 additions and 5 deletions
Showing only changes of commit 9d091c81e0 - Show all commits
@@ -17,6 +17,14 @@ touches:
# 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
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-control` | 2 | the control plane and its contexts |
| `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/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
against each artifact does it, and that record becomes load-bearing: wrong, and the mesh either
rebuilds forever or never rebuilds at all.
- **Rebuild storms.** One change to a shared library makes everything out of date at once. The
ordering that today's *levels* provide has to come from the module graph
([research 011](../01-RESEARCH/011-the-module-graph/00-overview.md)), which is designed and not
built.
- **Rebuild storms are real and mostly behaviourally empty.** One shared-library commit
invalidates nearly everything, and most of those rebuilds produce artifacts that do the same
thing they did before — so **the fleet is redeployed for no change in behaviour.** Reproducible
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
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
@@ -101,6 +103,18 @@ examined.
the fault this record is removing, reintroduced in a new place. **This is the real risk and it
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
- **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.