From 50d61398b62ac091147c0f6d53e34f72b5708598 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 3 Sep 2026 21:41:08 +0200 Subject: [PATCH] =?UTF-8?q?ADR=200044=20=E2=80=94=20what=20the=20SDK=20hol?= =?UTF-8?q?ds,=20and=20what=20it=20refuses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supersedes ADR 0030's 'types, not behaviour' line for mesh-sdk. The boundary is change-frequency, not kind: the SDK holds the stable spine (tool-serving harness, messaging/event framework, contracts, core primitives) and refuses per-module clients, per-module tool code, and anything volatile — because those are what turned hal/sdk into constant maintenance and made every edit rebuild every module. States the rule (frequent AND cascading is the disease), why the root cause was intra-module feature-sharing leaking into inter-module coupling, and where per-module shared code lives instead (in the module — a shared file, or a module-local sdk for the few large ones). Updates repos.md's canonical mesh-sdk description to match; leaves 0030 untouched (immutable). Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF --- 00-META/repos.md | 2 +- .../0044-what-the-sdk-holds-and-refuses.md | 101 ++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 02-DECISIONS/0044-what-the-sdk-holds-and-refuses.md 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.