← the Atlas

nix build ≠ typecheck

reference · evergreen ·implemented ·

A talk-mode diagnosis of #1049 — a green `nix build` exits 0 with broken server types; the fix is a cached `checks.typecheck` flake derivation that closes the local false-confidence gap and deletes a CI node.

Talk-mode response · diagnosis confirmed against source · design reviewed (Lowy + Hickey) and revised.

What I verified

Claim in the issueStatusSource
nix build only runs the Vite client bundle, no tsctruedefault.nix:87-95buildPhase = pnpm --filter kolu-client build only
Server ships as .ts, run under tsx (transpile-only)truedefault.nix:154-155tsx … server/src/index.ts
typescript pruned from the production closuretruedefault.nix:109 — in installPhase (after build, before cp -r . $out)
No checkPhase / doCheck on the derivationtruedefault.nix:59-126
The real gate is just check / CIpartlyCI did gate: the DAG then included a standalone typecheckpnpm typecheck node (deleted by #1056 in favour of the flake check). just check (justfile:67-68) is the local inner loop.

So the path that gives false confidence:

edit server .tsnix build .#defaultno tsc runsexit 0“safe to deploy” ✗

Recommendation — a checks.typecheck derivation

Add a typecheck derivation to the root flake.nix that reuses pnpmDeps, runs pnpm -r typecheck, and touch $out on success. Three things fall out of this:

The trap to avoid — fileset drift must-fix

The build’s src fileset (default.nix:15-38) is a hand-maintained allowlist of package dirs — and it omits packages/tests. If the typecheck derivation reused that fileset, it would typecheck a different, smaller set than just check (which sees the whole working tree). A package added to pnpm-workspace.yaml but forgotten in the fileset would type-error while the derivation exits 0 — reintroducing the exact false-green #1049 is about, just relocated.

Pin to one platform cost

devour-flake builds checks.${system} for all three systems. TypeScript typecheck is platform-independent, so attaching it to all three triples CI cost for identical results. Pin it to one — following the existing precedent nix/home/example/flake.nix:70 (checks.${linuxSystem}.vm-test) and nix/home/example/flake.nix:114 (checks.${darwinSystem}).

As built, this was reversed too. #1056 runs the type gates on every system, deliberately: the build environment (nodejs/pnpm and the platform-resolved deps pnpmConfigHook installs) differs per platform, so each platform’s tsc/astro check is its own proof — a darwin-only type error wouldn’t surface from a linux-only check (rationale comment in flake.nix:66-72).

Concrete change (single PR — infra, not user-visible, no phasing)

1 · flake.nix — add the check output

+# typecheck the whole workspace as a cached, devour-flake-gated proof.
+# Own broad fileset (all of ./packages) — NOT default.nix's narrow shipping
+# allowlist — so a new package can never silently escape the gate (#1049).
+# Pinned to one system: tsc is platform-independent (cf. nix/home/example).
+checks.${linuxSystem}.typecheck = import ./nix/typecheck.nix { inherit pkgs; };

A small nix/typecheck.nix leaf: mkDerivation reusing pnpmDeps + pnpmConfigHook, buildPhase = "pnpm -r typecheck", installPhase = "touch $out". No node-gyp rebuild — tsc --noEmit needs .d.ts, not the compiled .node.

2 · ci/mod.just — delete the now-redundant recipe

-default: nix home-manager e2e smoke fmt typecheck biome unit surface-example-build pnpm-hash-fresh
+default: nix home-manager e2e smoke fmt biome unit surface-example-build pnpm-hash-fresh

-typecheck: install
-    {{ nix_shell }} pnpm typecheck

And one comment at the nix: recipe (ci/mod.just:63) noting that checks.typecheck (built here via devour-flake) replaced the standalone recipe — so the temporal coupling (“the nix node is now load-bearing for typecheck”) is documented, not folklore.

3 · default.nix — one comment at the buildPhase

 buildPhase = ''
   runHook preBuild
   ...
+  # NOTE: this does NOT typecheck — only the Vite client bundle is built and
+  # the server runs under tsx. The type gate is `nix build .#checks…typecheck`
+  # (run by CI's `nix` node). A green `nix build .#default` is not a type-proof.
   pnpm --filter kolu-client build
   runHook postBuild
 '';

Leave just check (justfile:67-68) exactly as-is — it stays the fast local inner loop. No pre-push hook needed: making nix build a type-proof and keeping the CI gate is sufficient; a hook would be a fourth locus for the same concept.

What I deliberately rejected