hyperhive/nix/checks.nix
atlas 8fed5c1813 refactor(#3245): move the rustdoc lints into the workspace lint table
mara asked whether these can live in Cargo.toml. They can, and they
should: [workspace.lints.rustdoc] sits alongside the existing clippy
table, every crate already opts in via `[lints] workspace = true`, and
the toolchain is well past the 1.74 that introduced lint tables.

The reason it is better than RUSTDOCFLAGS on the check: a plain local
`cargo doc` now fails exactly the way CI does. Setting the lints only in
the nix derivation would have made CI the one place the gate exists,
which is the same "you meet it too late" problem the gate was written to
solve.

The check keeps --workspace --no-deps --document-private-items and drops
the RUSTDOCFLAGS block entirely.

Verified by mutation rather than assumption, with no RUSTDOCFLAGS set
anywhere: clean tree exits 0; reintroducing one broken link makes
`cargo doc -p hive-jobq` exit 101 with `error: public documentation for
Outcome links to private item Scheduler::complete`. An error rather than
a warning is the proof the deny came from the lint table.
2026-08-14 02:30:55 +02:00

209 lines
9.5 KiB
Nix

# Flake checks: formatting, the clippy gate, the workspace test run,
# the nix-options docs eval, the hivectl CLI-reference freshness check,
# and the module-eval property table. Imported per system from flake.nix.
{
pkgs,
craneLib,
rust,
self,
system,
treefmt-eval,
nixosSystem,
}:
let
inherit (rust) cleanSrc cargoArtifacts nativeBuildInputs;
in
{
formatting = treefmt-eval.config.build.check self;
# The only check here that covers **nix**. Every other one is a Rust
# derivation, so a `.nix`-only diff moves no hash and the whole set is
# cache hits — green without evaluating what changed. See the file's
# header for what belongs in it and what needs something that executes
# rather than evaluates.
module-eval = import ./module-eval.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
};
# Clippy via crane's first-class `cargoClippy` builder. Reuses the
# shared `cargoArtifacts` (deps already built) and runs
# `cargo clippy --workspace --all-targets` directly.
#
# `-D warnings` makes every rustc/clippy warning a hard CI gate.
# Pedantic is gated too, deliberately: the workspace lint table
# (Cargo.toml) sets `pedantic = deny`, so pedantic lints are errors
# both locally and here. We want that — a toolchain bump that adds a
# new pedantic lint reds the build until the code is updated, rather
# than sliding in unnoticed. (The lint table allows a few noisy
# pedantic lints explicitly, e.g. `must_use_candidate`; those keep
# their allow via higher priority.)
clippy = craneLib.cargoClippy {
src = cleanSrc;
inherit cargoArtifacts nativeBuildInputs;
pname = "hyperhive-workspace";
version = "0.1.0";
cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings";
};
# `cargo test --workspace` lifted out of the package builds so the
# `hyperhive-assets` dep (which `hive-agent::prompt::tests`
# needs via `HIVE_ASSETS_DIR` to assert against the actual
# production prompt template) is scoped to this one check
# instead of bleeding into the binary derivations' input
# hash. Net: editing `hive-agent/prompts/system.md` still
# rebuilds this test check (correct — the tests assert
# against its wording), but `packages.default` and the
# per-container toplevels stay fully cached.
cargo-test = craneLib.cargoTest {
src = cleanSrc;
inherit cargoArtifacts nativeBuildInputs;
pname = "hyperhive-workspace";
version = "0.1.0";
cargoTestExtraArgs = "--workspace";
HIVE_ASSETS_DIR = "${self.packages.${system}.assets}/share/hyperhive";
};
# Rustdoc gate. Builds the workspace's docs and turns rustdoc's own
# lints into hard failures, so a `[`Foo`]` pointing at a renamed,
# moved or deleted item reds the PR instead of silently rendering as
# plain text.
#
# Why this needs to exist at all: nothing else reads doc-comments.
# Clippy doesn't check intra-doc links, `cargo test` doesn't, and no
# other check builds docs — so a dangling pointer had no discoverer
# but a human happening to read the comment. That matters more here
# than in most repos, because the convention is to put a thing's
# authoritative description in one doc-comment and link to it from
# everywhere else: the design leans on the pointers being real, and a
# dangling link is worse than no link, since it names something and
# so sends the reader looking.
#
# `--document-private-items` is load-bearing, not thoroughness for its
# own sake: most of this workspace's doc-comments live on private
# items and `//!` module headers. Without it rustdoc checks a small
# fraction of the links and the gate would sit green while the rot
# continued.
#
# ⚠️ This does NOT reuse the `cargoArtifacts` the way `clippy` and
# `cargo-test` do — it takes them, but rustdoc needs its own flavour
# of dependency metadata, which `cargo build` does not produce. So a
# `--no-deps` docs build still *compiles* dependencies it will never
# document (matrix-sdk dominates), and this check costs its own build
# rather than riding the others' cache.
docs-rustdoc = craneLib.cargoDoc {
src = cleanSrc;
inherit cargoArtifacts nativeBuildInputs;
pname = "hyperhive-workspace";
version = "0.1.0";
cargoDocExtraArgs = "--workspace --no-deps --document-private-items";
# The lints themselves are NOT set here — they live in
# `[workspace.lints.rustdoc]` in the root Cargo.toml, alongside the
# clippy table, and every crate inherits them via `[lints] workspace
# = true`. That way a plain local `cargo doc` fails exactly the way
# this check does; setting them as `RUSTDOCFLAGS` here would make CI
# the only place the gate exists.
};
# Nix options docs evaluation. Cheap: pulls in `nixosOptionsDoc` +
# the host module's stub eval, no rust or frontend deps. CI fails
# fast if a module change breaks option declarations or the doc
# rendering. Reuses the `packages.<system>.docs` derivation so the
# per-system eval of nix/docs/default.nix happens once.
inherit (self.packages.${system}) docs;
# Frontend build. Builds the `hyperhive-frontend` npm package, whose
# fixed-output npm-deps derivation pins `npmDepsHash`
# (nix/packages/frontend.nix). No other check exercises that FOD:
# `cargo-test` forces `.#assets`, but `assets` carries no frontend
# dependency, so without this check a stale `npmDepsHash` after a
# `package-lock.json` bump that forgets to recompute it sails through
# every PR and only breaks when the host toplevel is built on deploy.
# Reusing the already-defined `packages.<system>.frontend` derivation
# makes such a hash mismatch a red PR instead of a red main.
#
# `swarm-ui` alongside it for the same FOD-staleness reason (shares
# the same lockfile / `npmDepsHash`, see nix/packages/swarm-ui.nix) —
# plus, since it's not in `packages.default`'s dependency closure like
# `frontend` is, this is also the only thing that actually *builds*
# it in CI rather than just evaluating it (`nix flake check` doesn't
# build every `packages.<system>.*` output on its own).
inherit (self.packages.${system}) frontend swarm-ui;
# TypeScript type-check for `swarm-ui` (`npm run typecheck`, i.e.
# `tsc --noEmit`). esbuild only strips types without checking them
# (see `frontend/packages/swarm-ui/build.mjs`'s own comment), so
# without this a real type error would still build clean and pass
# every other check. Argus flagged the gap on this PR's review;
# wiring it in now per mara's follow-up ask on the same PR, rather
# than leaving it as a someday-issue.
#
# A separate `buildNpmPackage` derivation rather than folding into
# `swarm-ui` itself: this one's whole job is to *fail loudly* on a
# type error, and `npm ci` + `tsc` needs `typescript` in
# `node_modules`, which `swarm-ui.nix`'s actual build doesn't need
# (esbuild transpiles TS without it) — no reason to make the real
# build depend on the dev-only type-checker.
swarm-ui-typecheck = pkgs.buildNpmPackage {
pname = "hyperhive-swarm-ui-typecheck";
version = "0.0.0";
src = ../frontend;
# Same lockfile as `frontend`/`swarm-ui` above — recompute in
# lockstep with those two whenever `frontend/package-lock.json`
# changes (`prefetch-npm-deps frontend/package-lock.json`).
npmDepsHash = "sha256-LIwW5Nn9cSqJHCm1czPIVxQdP5OqWk7U/4kvkwaj2ts=";
buildPhase = ''
runHook preBuild
npm run typecheck --workspace=packages/swarm-ui
runHook postBuild
'';
dontNpmInstall = true;
installPhase = ''
runHook preInstall
mkdir -p $out
touch $out/ok
runHook postInstall
'';
};
# `hivectl` CLI reference freshness check. The committed
# markdown at `docs/tools/hivectl-cli.md` is the rendered
# output of the hidden `hivectl markdown-docs` subcommand
# (clap-markdown walks the binary's own command tree). This
# check regenerates it from the built binary and fails if the
# committed copy drifted — so a verb / flag / help-string edit
# that forgets to refresh the doc is caught in CI. Reuses the
# already-built `packages.<system>.default` (no extra compile).
# Regenerate locally with:
# nix build .#default
# ./result/bin/hivectl markdown-docs > docs/tools/hivectl-cli.md
hivectl-docs = pkgs.runCommand "hivectl-docs-fresh" { nativeBuildInputs = [ pkgs.diffutils ]; } ''
${self.packages.${system}.default}/bin/hivectl markdown-docs > generated.md
if ! diff -u ${../docs/tools/hivectl-cli.md} generated.md; then
echo "" >&2
echo "ERROR: docs/tools/hivectl-cli.md is out of date regenerate it:" >&2
echo " nix build .#default && ./result/bin/hivectl markdown-docs > docs/tools/hivectl-cli.md" >&2
exit 1
fi
touch "$out"
'';
# `swarmctl` CLI reference freshness check — same shape as
# `hivectl-docs` above, `swarmctl markdown-docs` (clap-markdown over
# its own command tree) instead. Reuses `packages.<system>.swarmctl`
# (already built as its own package, out of `daemonBins` — see
# nix/packages/default.nix's comment on it).
swarmctl-docs = pkgs.runCommand "swarmctl-docs-fresh" { nativeBuildInputs = [ pkgs.diffutils ]; } ''
${self.packages.${system}.swarmctl}/bin/swarmctl markdown-docs > generated.md
if ! diff -u ${../docs/tools/swarmctl-cli.md} generated.md; then
echo "" >&2
echo "ERROR: docs/tools/swarmctl-cli.md is out of date regenerate it:" >&2
echo " nix build .#swarmctl && ./result/bin/swarmctl markdown-docs > docs/tools/swarmctl-cli.md" >&2
exit 1
fi
touch "$out"
'';
}