diff --git a/00-META/repos.md b/00-META/repos.md index 87e954b..994e245 100644 --- a/00-META/repos.md +++ b/00-META/repos.md @@ -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 diff --git a/02-DECISIONS/0044-what-the-sdk-holds-and-refuses.md b/02-DECISIONS/0044-what-the-sdk-holds-and-refuses.md new file mode 100644 index 0000000..5dde1f5 --- /dev/null +++ b/02-DECISIONS/0044-what-the-sdk-holds-and-refuses.md @@ -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.