feat(nix): add a dev shell and a checks output (task 0039) #48

Merged
alexion merged 1 commits from task-0039-dev-shell-and-checks into main 2026-07-19 23:38:36 -04:00
Owner

Implements .claude/tasks/0039-dev-shell-and-checks.md.

What was built

Two further flake outputs, so nix develop gives a declarative answer to "what do I need to work on this" and nix flake check is not a silent no-op.

  • devShells.default carries Node, git, tea, and curl — enough for the build, the fast tier, the live end-to-end tier, and the benchmark harness's runner.
  • checks aliases the package, so nix flake check builds it and thereby runs both its verification phases (checkPhase, installCheckPhase).
  • forAllSystems now hands each output the system name alongside the package set, since the shell and the checks reach back into self.packages.

Verified by running it: nix develop yields Node 24.18.0 from the same store path as the package's; npm ci, npm run build, npm run test (410 passing) and npm run bench:run all work inside the shell; nix flake check passes, and fails with exit 1 when a deliberately failing assertion is added to the fast tier.

Deviations

The single Node reference became a passthru. The first cut read nodejs back off the derivation, which works only as a side effect of buildNpmPackage surfacing its arguments — nothing marked it load-bearing, so moving that inherit would have broken the shell silently at a distance. package.nix now declares passthru = { inherit nodejs; } with a comment saying what depends on it. passthru does not enter the derivation, so the store path is unchanged (verified: identical drv hash before and after).

curl was added to the shell. The benchmark's raw-api arm shells out to it.

The shell deliberately omits gitea-axi itself. provisionArmBin resolves each arm's binary by name off PATH, and the gitea-axi arm's must be the locally built dist/main.js so a run measures the working tree rather than whatever the flake last packaged — supplying the packaged one would silently substitute the wrong artifact. Exposing the built one was also rejected: tsc sets no executable bit on dist/main.js, so a shellHook would have had to chmod +x the build output on every shell entry. Both the flake and CLAUDE.md now state the boundary rather than implying the shell covers it.

CLAUDE.md's toolchain gotcha was stale by construction — it read "the repository has no dev shell yet (task 0039 adds one)". Rewritten to point at nix develop --command.

Review

Risk

Overall: MEDIUM

  • Blast radius: Low — two files; forAllSystems's signature change is internal to flake.nix and every call site is updated in the same diff.
  • Reversibility: Low — purely additive flake outputs plus a doc edit; git revert restores the prior state with no migration or published-schema residue.
  • Test coverage: Medium — the new checks output is itself the test wiring, but nothing asserts the dev shell contains the right tools.
  • Sensitive domain: Low — no auth/permissions logic; tea appears only as a shell package, not as a credential path change.
  • Size & complexity: Low — ~45 added lines, mostly comments, with no control flow beyond an attrset destructure.
  • Runtime criticality: Low — dev-only tooling and CI plumbing; the shipped packages.default derivation is untouched.

Unaddressed findings

Standards — nothing asserts the dev shell's contents. No test guards that the shell keeps carrying Node, git, tea, and curl; a regression would surface only when someone's workflow breaks. Left alone: a check derivation instantiating the shell to assert its PATH is exactly the per-stage granularity the task rules out, and the shell is dev-only.

Spec — nix flake check is a no-op when the package is already built. Because checks aliases the package, a cached store path means nothing rebuilds. This is inherent to aliasing rather than a defect — it is the same caching that makes any Nix check cheap, and the failure path was verified directly by forcing a rebuild with a failing test.

Implements [`.claude/tasks/0039-dev-shell-and-checks.md`](.claude/tasks/0039-dev-shell-and-checks.md). ## What was built Two further flake outputs, so `nix develop` gives a declarative answer to "what do I need to work on this" and `nix flake check` is not a silent no-op. - **`devShells.default`** carries Node, `git`, `tea`, and `curl` — enough for the build, the fast tier, the live end-to-end tier, and the benchmark harness's runner. - **`checks`** aliases the package, so `nix flake check` builds it and thereby runs both its verification phases (`checkPhase`, `installCheckPhase`). - **`forAllSystems`** now hands each output the system name alongside the package set, since the shell and the checks reach back into `self.packages`. Verified by running it: `nix develop` yields Node 24.18.0 from the same store path as the package's; `npm ci`, `npm run build`, `npm run test` (410 passing) and `npm run bench:run` all work inside the shell; `nix flake check` passes, and fails with exit 1 when a deliberately failing assertion is added to the fast tier. ## Deviations **The single Node reference became a `passthru`.** The first cut read `nodejs` back off the derivation, which works only as a side effect of `buildNpmPackage` surfacing its arguments — nothing marked it load-bearing, so moving that `inherit` would have broken the shell silently at a distance. `package.nix` now declares `passthru = { inherit nodejs; }` with a comment saying what depends on it. `passthru` does not enter the derivation, so the store path is unchanged (verified: identical drv hash before and after). **`curl` was added to the shell.** The benchmark's `raw-api` arm shells out to it. **The shell deliberately omits `gitea-axi` itself.** `provisionArmBin` resolves each arm's binary by name off `PATH`, and the `gitea-axi` arm's must be the locally built `dist/main.js` so a run measures the working tree rather than whatever the flake last packaged — supplying the packaged one would silently substitute the wrong artifact. Exposing the *built* one was also rejected: `tsc` sets no executable bit on `dist/main.js`, so a `shellHook` would have had to `chmod +x` the build output on every shell entry. Both the flake and CLAUDE.md now state the boundary rather than implying the shell covers it. **CLAUDE.md's toolchain gotcha was stale by construction** — it read "the repository has no dev shell yet (task 0039 adds one)". Rewritten to point at `nix develop --command`. ## Review ### Risk **Overall: MEDIUM** - Blast radius: Low — two files; `forAllSystems`'s signature change is internal to `flake.nix` and every call site is updated in the same diff. - Reversibility: Low — purely additive flake outputs plus a doc edit; `git revert` restores the prior state with no migration or published-schema residue. - Test coverage: Medium — the new `checks` output is itself the test wiring, but nothing asserts the dev shell contains the right tools. - Sensitive domain: Low — no auth/permissions logic; `tea` appears only as a shell package, not as a credential path change. - Size & complexity: Low — ~45 added lines, mostly comments, with no control flow beyond an attrset destructure. - Runtime criticality: Low — dev-only tooling and CI plumbing; the shipped `packages.default` derivation is untouched. ### Unaddressed findings **Standards — nothing asserts the dev shell's contents.** No test guards that the shell keeps carrying Node, `git`, `tea`, and `curl`; a regression would surface only when someone's workflow breaks. Left alone: a check derivation instantiating the shell to assert its `PATH` is exactly the per-stage granularity the task rules out, and the shell is dev-only. **Spec — `nix flake check` is a no-op when the package is already built.** Because `checks` aliases the package, a cached store path means nothing rebuilds. This is inherent to aliasing rather than a defect — it is the same caching that makes any Nix check cheap, and the failure path was verified directly by forcing a rebuild with a failing test.
alexion added 1 commit 2026-07-19 23:37:12 -04:00
feat(nix): add a dev shell and a checks output (task 0039)
All checks were successful
CI / test (pull_request) Successful in 54s
CI / test (push) Successful in 53s
6a9d4d4770
`nix develop` now yields the toolchain the repository actually needs — Node,
`git`, `tea`, and `curl` — giving a declarative answer to "what do I need to
work on this", which the repository previously specified nowhere.

The shell takes its Node from the package's `passthru` rather than naming
`pkgs.nodejs` a second time, so development and the shipped artifact cannot
drift onto different majors and cannot be set independently. `package.nix`
declares that `passthru` as an interface rather than leaving the shell to read
an incidental build attribute; it does not enter the derivation, so the store
path is unchanged.

The checks output aliases the package, so `nix flake check` builds it and
thereby runs both its verification phases instead of being a silent no-op. No
per-stage checks: the only stage adding coverage is the full typecheck, which
spans `test/` and `bench/` and would drag the benchmark harness into the
derivation's inputs, undoing the source filtering. It stays in CI.

The shell deliberately omits `gitea-axi` itself. The benchmark's arm resolves
that binary by name off PATH and must get the locally built `dist/main.js`, so
supplying the packaged one would silently substitute the wrong artifact.
alexion merged commit 6a9d4d4770 into main 2026-07-19 23:38:36 -04:00
alexion deleted branch task-0039-dev-shell-and-checks 2026-07-19 23:38:36 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: alexion/gitea-axi#48