107 lines
5.7 KiB
Markdown
107 lines
5.7 KiB
Markdown
---
|
|
topic: building it
|
|
status: accepted
|
|
date: 2026-09-03
|
|
deciders: jochen
|
|
reconstructed: false
|
|
---
|
|
|
|
# 39. What the SDK holds, and what it refuses
|
|
|
|
_Reconciliation note (2026-09-05): supersedes the earlier "repository structure" decision, which the consolidation folded; no standalone record remains to point at, so body references to it now point at the nearest surviving record, [ADR 0015](0015-applications-live-in-their-own-repository.md)._
|
|
|
|
## Context
|
|
|
|
The earlier "repository structure" decision (folded in consolidation; see the reconciliation note
|
|
above, and [ADR 0015](0015-applications-live-in-their-own-repository.md) as the nearest survivor)
|
|
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 0005](0005-the-node-host.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
|
|
|
|
- The earlier "repository structure" decision — named the repositories; its `mesh-sdk`
|
|
description ("types, not behaviour") is superseded by this record (folded in consolidation;
|
|
nearest survivor [ADR 0015](0015-applications-live-in-their-own-repository.md)).
|
|
- [ADR 0001](0001-mesh-brokers-nodes-host-agents-think.md) — the decomposition this serves: code
|
|
belongs to the boundary that owns it.
|
|
- [ADR 0005](0005-the-node-host.md) — why the host mirrors the contracts instead of
|
|
importing the SDK.
|