Issue 009 (fixed) + Issue 008 (resolved via ADR 0053) — module-runtime config & provider contract #21
+1
-1
@@ -31,7 +31,7 @@ target, not the present.
|
||||
| `mesh-substrate` | 1 | the four pinned services, as declarations |
|
||||
| `mesh-control` | 2 | the control plane and its contexts |
|
||||
| `mesh-surfaces` | 3 | tools, web, cli |
|
||||
| `mesh-sdk` | — | contracts shared across tiers |
|
||||
| `mesh-sdk` | — | the stable spine modules build against — the tool-serving harness, the messaging/event framework, the contracts and core primitives. Holds nothing per-module and nothing volatile ([ADR 0044](../02-DECISIONS/0044-what-the-sdk-holds-and-refuses.md)). |
|
||||
| `mesh-lab` | — | **exists.** The lab — scenario lifecycle, networking, placement. Ships to nobody; runs on a workstation. |
|
||||
|
||||
Tier 4's shape is open, and deliberately so: see ADR 0030 and
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
status: accepted
|
||||
date: 2026-09-03
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
supersedes: 0030-the-repository-structure.md
|
||||
---
|
||||
|
||||
# 44. What the SDK holds, and what it refuses
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0030](0030-the-repository-structure.md) named `mesh-sdk` "contracts shared across tiers:
|
||||
types, not behaviour." That line is superseded here, because it draws the boundary in the wrong
|
||||
place. The boundary that matters is not *types versus behaviour* — it is **how often the thing
|
||||
changes**.
|
||||
|
||||
The current SDK is the cautionary tale, and its failure is precise. `hal/sdk` holds all the
|
||||
code, including a per-module API client for every service (`clients/plex.ts`, `clients/gitea.ts`,
|
||||
…) and a per-module tool implementation for each (`tools/plex.ts`, …). Every module depends on
|
||||
the SDK, so **every edit to any of that per-module code rebuilds every module** — the cascade.
|
||||
The SDK is under constant maintenance precisely because it became the place all the volatile
|
||||
per-module logic accumulated.
|
||||
|
||||
The root cause is worth stating exactly, because the fix follows from it: the pressure was never
|
||||
to share a client *between* modules. It was to share a client between one module's *own features*
|
||||
— plex's tools, its health check and its hooks all wanted the same `PlexClient` — and the only
|
||||
place to share code across a module's features was the global SDK. So **intra-module sharing
|
||||
leaked out as inter-module coupling.**
|
||||
|
||||
## Decision
|
||||
|
||||
The SDK holds the **stable spine** that modules build against, and earns its place by rarely
|
||||
changing. The test for membership is change-frequency, not kind.
|
||||
|
||||
### What it holds
|
||||
|
||||
- The **tool-serving harness** — the worker and registration mechanism, and the tool-definition
|
||||
type. *How* a tool is declared and served is settled; it does not change when an individual
|
||||
tool does.
|
||||
- The **messaging and event framework** — the broker client, the event consumer, the envelope.
|
||||
- The **contracts** — the manifest, declaration, provision and link shapes.
|
||||
- **Core primitives** — sealing and crypto, semver, the shared resolution helpers.
|
||||
|
||||
These change rarely and deliberately. When one of them does change, a rebuild of everything is
|
||||
the *correct* outcome, because the contract every module shares has genuinely changed.
|
||||
|
||||
### What it must not hold — the more important half
|
||||
|
||||
- **A module's API client.** A Plex client, a Gitea client, a MinIO client belong in their
|
||||
module. They change when that service's API or the module's use of it changes, which is often,
|
||||
and which has nothing to do with any other module.
|
||||
- **A module's tool implementations.** Same reason, same place: in the module.
|
||||
- **Anything volatile** — anything that changes when one service's features change.
|
||||
|
||||
The rule, stated so it can be applied without re-deriving it:
|
||||
|
||||
> If editing a thing recompiles unrelated modules **and** it changes often, it does not belong
|
||||
> in the SDK.
|
||||
|
||||
Both conditions are load-bearing. A rare change that cascades is fine — that is a contract, and
|
||||
the cascade is correct. A frequent change that stays local is fine — that is a module minding its
|
||||
own business. Only **frequent *and* cascading** is the disease, and per-module clients and tools
|
||||
are its carriers.
|
||||
|
||||
### Where per-module shared code lives instead
|
||||
|
||||
Code shared among a module's *own* features lives **in the module**. The default is the plainest
|
||||
thing that works: an ordinary shared file the features import — `plex/client.ts`, imported by
|
||||
`plex/tools/`. Within one module, features are files importing sibling files; no package
|
||||
boundary, no ceremony.
|
||||
|
||||
A **module-local SDK** (a sub-package with its own version) is warranted only for the few modules
|
||||
whose shared surface is large enough to version on its own. It is the exception, not the shape.
|
||||
|
||||
Either form gives the property the global SDK could not: editing a module's shared code rebuilds
|
||||
**that module and nothing else**.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The cascade becomes **structurally impossible for module logic**. There is no longer an edge
|
||||
from one module's internals to another, so the only thing that can rebuild everything is a real
|
||||
change to a shared contract in the SDK — which is rare, and when it happens, is right.
|
||||
- The SDK is small and stable **by construction**, not by discipline. Its size is no longer a
|
||||
thing anyone has to police.
|
||||
- **Converting a module from the current system is partly a de-coupling, not just a move.** Its
|
||||
client and its tools are pulled *out* of the shared SDK and *into* the module. A conversion
|
||||
that copied `clients/plex.ts` into the SDK's replacement would rebuild the exact mistake.
|
||||
- The host still does not import the SDK. It depends on nothing
|
||||
([ADR 0041](0041-the-host-depends-on-nothing.md)) and **mirrors** the contracts rather than
|
||||
importing them, exactly as its apply-shapes table already does deliberately. The SDK is shared
|
||||
by the tiers that *can* share code; the host is not one of them.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0030](0030-the-repository-structure.md) — named the repositories; its `mesh-sdk`
|
||||
description ("types, not behaviour") is superseded by this record.
|
||||
- [ADR 0015](0015-mesh-brokers-nodes-host-agents-think.md) — the decomposition this serves: code
|
||||
belongs to the boundary that owns it.
|
||||
- [ADR 0041](0041-the-host-depends-on-nothing.md) — why the host mirrors the contracts instead of
|
||||
importing the SDK.
|
||||
Reference in New Issue
Block a user