--- topic: building it status: accepted date: 2026-09-16 deciders: jochen reconstructed: false extends: 0075-two-stores-and-which-provides-what.md --- # 76. The SDK is a published package, and the toolchain resolves it by version ## Context [ADR 0014](0014-no-npm-workspace.md) decided a module consumes its dependencies — the mesh's own shared library included — from the private registry. [ADR 0075](0075-two-stores-and-which-provides-what.md) decided the private registry is a `package-registry` provision, and that gitea provides it. What neither settled, and what [issue 053](../04-ISSUES/053-the-sdk-is-pinned-twice-and-the-two-disagree/00-report.md) left open, is the one build where the rule cannot simply be obeyed: **the first one.** The TypeScript toolchain image is built *from* the SDK — it carries the SDK so that every module compiled inside it resolves the shared library without each build fetching it. So the thing that compiles TypeScript and the thing that contains the SDK were the same object, and that object cannot be what builds the SDK. Stated as a question — "how does the SDK reach the registry before the toolchain exists, when the toolchain is what builds it?" — it reads as a paradox. It is not one. The paradox exists only because the toolchain *bakes a git-cloned copy* of the SDK. The SDK itself is plain TypeScript: it needs `node` and `tsc` and nothing the mesh makes. A public base image can build it. The circularity is a property of the workaround, not of the SDK. ## Decision **The SDK is an ordinary published package in the mesh's `package-registry`, consumed by version.** The git URL in the toolchain's manifest and the sibling-path lock beside it — the two halves of issue 053 — are both removed. A build resolves the SDK the way it resolves any dependency, with a lock that agrees with its manifest, so `npm ci` is the command and reproducibility is by construction rather than by the machine the build ran on. **The SDK is built with a public base image, not with the mesh's toolchain.** It is *not* one of the components the loop cannot build — the control plane, the registry, the builder, the catalogue ([`12-a-module-repository`](../03-DESIGN/01-to-be/12-a-module-repository.md)), which arrive by carrying an init builder because they are the loop's own machinery. The SDK is machinery for nothing; it is an ordinary dependency the loop builds and publishes like any other. The only constraint is narrow: it cannot be compiled *in the mesh toolchain*, because that toolchain is built from it. So it is compiled on a public base image instead — which needs nothing the mesh makes — and published before the toolchain that consumes it. It is not carried, because building it does not wait on a mesh existing first. **The toolchain base stays, thinned.** mesh-tools remains the image bundles are compiled in and the one place the SDK is resolved — but it `npm ci`s the SDK by version from the registry instead of baking a copy cloned from a git URL. Bundles keep borrowing its resolved dependencies; what changes is that the version they borrow is named and honest. This was the shape chosen over dropping the shared base entirely and having every bundle resolve the SDK itself: one resolution point, one place to be right about the version. **Genesis orders the publish before the first compile.** The package-registry provider is a public image (gitea), so it comes up needing no toolchain; the SDK is published into it; only then is the toolchain built, so the first `npm ci` has a registry to read from. Nothing in that chain is circular, because the only thing that needed the toolchain — baking the SDK — is gone. ## Consequences Each language's toolchain repeats the shape: its own SDK, built from that language's public base image, published to the same registry, resolved by version with that ecosystem's lockfile-honest install (`npm ci`, `cargo` against a vendored or registry source, `pip` against a pinned set). The warning in issue 053 — that whatever the TypeScript repository does the others will copy — is answered by making the copied thing the correct one. A change to the SDK is publish-then-consume, exactly as [ADR 0014](0014-no-npm-workspace.md) already priced it: publish the new SDK version, then bump the toolchain (and any module pinning it directly) to consume it. There is no shortcut that resolves an unpublished SDK, which is the property that was missing. mesh-tools is no longer an SDK carrier in the sense that mattered — it does not contain a copy whose provenance is a branch head somebody force-pushes. It contains a version. A mesh with no package-registry cannot build TypeScript. This is accepted and is not new: it is the same shape as a mesh that cannot reach a forge being unable to be raised ([ADR 0071](0071-where-genesis-gets-its-source.md)). Installing brings the registry up first. ## How this is checked | Rule | Checked by | |---|---| | The SDK a build compiles against is named, not cloned from a branch | The toolchain manifest pins `@novox/mesh-sdk` to a version, and the build runs `npm ci`, which refuses a lock that disagrees with the manifest. Issue 053's two checks become this one. | | The SDK builds without the mesh's own toolchain | The SDK's build recipe names a public base image. A recipe that named the mesh toolchain would reintroduce the cycle and is refused in review. | | The registry is up before the first compile | The genesis bed asserts the package-registry answers, and the SDK is published, before the base build runs. A base build that ran first would fail its `npm ci` with no registry, which is the positive control. | | A second language repeats the shape, not a new one | When a second SDK is added, its recipe is compared to this one: public base, publish by version, lockfile-honest install. |