Files
hq/02-DECISIONS/0044-what-the-sdk-holds-and-refuses.md
T
jschoubben 50d61398b6 ADR 0044 — what the SDK holds, and what it refuses
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
2026-09-03 21:41:08 +02:00

5.2 KiB

status, date, deciders, reconstructed, supersedes
status date deciders reconstructed supersedes
accepted 2026-09-03 jochen false 0030-the-repository-structure.md

44. What the SDK holds, and what it refuses

Context

ADR 0030 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) 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 — named the repositories; its mesh-sdk description ("types, not behaviour") is superseded by this record.
  • ADR 0015 — the decomposition this serves: code belongs to the boundary that owns it.
  • ADR 0041 — why the host mirrors the contracts instead of importing the SDK.