From be3411e180b195a3ce6cd48b9e05457821583e15 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 14 Aug 2026 02:15:32 +0200 Subject: [PATCH 1/2] feat(#3245): gate rustdoc in nix flake check, and clear the workspace Nothing in the gate read doc-comments: clippy doesn't check intra-doc links, cargo test doesn't, and no check built docs. So a [`Foo`] pointing at a renamed, moved or deleted item rendered as plain text and had no discoverer but a human happening to read the comment. That matters here more than in most repos, because the convention is to put a thing's authoritative description in one doc-comment and point at 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 sends the reader looking. Adds `docs-rustdoc` to nix/checks.nix: craneLib.cargoDoc over --workspace --no-deps --document-private-items, denying six rustdoc lints. Listed explicitly rather than -D warnings so a new lint appearing upstream cannot red the build on a class nobody has triaged. --document-private-items is load-bearing rather than thoroughness for its own sake: most of this workspace's doc-comments live on private items and //! module headers, so without it rustdoc checks a small fraction of the links and the gate sits green while the rot continues. Then fixes every error it reports, 40 to 0 across nine crates. The classes differ and so do the fixes: - public item, wrong scope -> qualify. Node and Node::parent are both public; the link failed only because scheduler.rs does not import Node. Six sites become [`crate::Node::parent`]. - private item -> downgrade to backticks. Nothing was made public to satisfy a lint; changing API surface to appease a doc check would be the tail wagging the dog. - genuinely dead -> [`JobBuilder::insert_into`] names a method that does not exist. Insertion is Scheduler::insert_job. - prose that looks like markup -> argv[0] parsed as a link, and // parsed as HTML tags. Note for future fixes: pub(crate) resolves in an intra-doc link, a plain private fn in a binary crate does not (wait_for_nodes resolved, connect_hint did not, same crate, same shape). The check does not ride the clippy/test artifact cache. It takes cargoArtifacts, 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 never documents. Measured at 6m47s cold; that reasoning is recorded in the check's own comment so the next reader does not re-derive it. Verified by running the check's exact command against the pre-cleanup tree first: 40 errors, build failed. A gate that cannot fail is not evidence, and building it before the cleanup makes that proof free. --- hive-agent/src/events.rs | 2 +- hive-agent/src/reminder_timer.rs | 2 +- hive-agent/src/stream_enrich.rs | 2 +- hive-agent/src/web_ui/mod.rs | 4 +-- hive-bash-mcp/src/runner.rs | 4 +-- hive-forge-notify/src/notify.rs | 2 +- hive-jobq/src/builder.rs | 6 ++-- hive-jobq/src/lib.rs | 6 ++-- hive-jobq/src/resources.rs | 6 ++-- hive-jobq/src/scheduler.rs | 20 +++++++------- hive-matrix-mcp/src/accounts.rs | 2 +- hive-matrix-mcp/src/handlers.rs | 6 ++-- hive-matrix-mcp/src/mcp.rs | 2 +- hive-matrix-mcp/src/wake.rs | 2 +- hive-priv-sock/src/lib.rs | 2 +- hive-sock-client/src/lib.rs | 2 +- hivectl/src/client.rs | 2 +- hivectl/src/dag_progress.rs | 6 ++-- nix/checks.nix | 47 ++++++++++++++++++++++++++++++++ swarm-controller/src/main.rs | 4 +-- 20 files changed, 88 insertions(+), 41 deletions(-) diff --git a/hive-agent/src/events.rs b/hive-agent/src/events.rs index e9a26990..c4f35fe6 100644 --- a/hive-agent/src/events.rs +++ b/hive-agent/src/events.rs @@ -678,7 +678,7 @@ impl Bus { } /// The effective context window for `model`: the API-reported window if a - /// turn has completed ([`api_context_window`]), else the per-model default + /// turn has completed (`api_context_window`), else the per-model default /// ([`context_window_tokens`]). Single accessor so the state + dashboard /// endpoints agree by construction. #[must_use] diff --git a/hive-agent/src/reminder_timer.rs b/hive-agent/src/reminder_timer.rs index f0f56819..394a5a3d 100644 --- a/hive-agent/src/reminder_timer.rs +++ b/hive-agent/src/reminder_timer.rs @@ -34,7 +34,7 @@ const REMINDER_BATCH_LIMIT: u64 = 100; const POLL_INTERVAL: Duration = Duration::from_secs(5); /// Same cap the broker used to enforce on `send`/`ask`/`remind` bodies -/// ([`hive-c0re::agent_config::limits::MESSAGE_MAX_BYTES`], not +/// (`hive-c0re`'s `agent_config::limits::MESSAGE_MAX_BYTES`, not /// reachable from here — hive-agent doesn't depend on hive-c0re). /// Duplicated rather than shared: this is the last remaining reminder /// caller of that constant once the c0re-side store is deleted (commit 6). diff --git a/hive-agent/src/stream_enrich.rs b/hive-agent/src/stream_enrich.rs index 2a877874..57b50b88 100644 --- a/hive-agent/src/stream_enrich.rs +++ b/hive-agent/src/stream_enrich.rs @@ -7,7 +7,7 @@ //! //! The sqlite event log stores raw (un-enriched) events — the DB never needs //! migration when the enrichment logic changes. Enrichment is applied at -//! SSE-emit time in [`crate::web_ui::stream`] so both the live tail +//! SSE-emit time in `crate::web_ui::stream` so both the live tail //! (`events/stream`) and the history replay (`events/history`) endpoints //! deliver the same enriched shape. //! diff --git a/hive-agent/src/web_ui/mod.rs b/hive-agent/src/web_ui/mod.rs index 86974404..1c65a448 100644 --- a/hive-agent/src/web_ui/mod.rs +++ b/hive-agent/src/web_ui/mod.rs @@ -292,7 +292,7 @@ enum SigintOutcome { /// directly (`Command::new(program).spawn()`, no shell in between), so the /// harness is always the immediate parent of any claude turn it started — /// scanning `/proc/*/status` for `PPid: ` plus `/proc/*/cmdline` -/// for an argv[0] of `claude` finds *that* specific process without needing +/// for an `argv[0]` of `claude` finds *that* specific process without needing /// the driver to surface its pid through any extra plumbing. Distinguishes /// the harness's own tracked turn from an unrelated `claude` someone is /// running interactively in the same container (a manually shelled-in @@ -301,7 +301,7 @@ enum SigintOutcome { /// **Matches on `cmdline`, not `status`'s `Name:` field.** The nixpkgs /// `claude-code` package wraps its real binary (`wrapProgram`-style: the /// executable on `PATH` is a thin `exec -a claude .../.claude-wrapped ...` -/// shim) — `exec -a` only overrides argv[0] as the process itself/`cmdline` +/// shim) — `exec -a` only overrides `argv[0]` as the process itself/`cmdline` /// see it, not the kernel's own `comm` (what `status`'s `Name:` line /// reports, set from the executed binary's own basename at `execve` time). /// So `Name:` shows `.claude-wrapped`, not `claude`, on a wrapped package — diff --git a/hive-bash-mcp/src/runner.rs b/hive-bash-mcp/src/runner.rs index 424bb9f9..8e7d609f 100644 --- a/hive-bash-mcp/src/runner.rs +++ b/hive-bash-mcp/src/runner.rs @@ -106,7 +106,7 @@ fn waiting_ids() -> &'static Mutex> { } /// True if an inline waiter is currently registered for `id`. Checked by -/// `run_task`'s completion handler under the same lock [`WaiterGuard`] +/// `run_task`'s completion handler under the same lock `WaiterGuard` /// (de)registers under — "is a waiter here" and "a waiter leaving" can /// never observe torn state relative to each other, unlike the flag this /// replaced. @@ -329,7 +329,7 @@ pub fn submit_task(cmd: String, timeout_secs: Option, name: Option) /// Inline wait: poll `read_task(id)` until terminal state or deadline. /// Returns the final task on success, or `None` if it never completed. /// -/// Registered as an inline waiter for `id` (via [`WaiterGuard`]) for the +/// Registered as an inline waiter for `id` (via `WaiterGuard`) for the /// polling loop only — deliberately **not** across the final fallback read /// below. A guard still held during that last read would let /// `run_task`'s completion handler see "waiter present" and skip the diff --git a/hive-forge-notify/src/notify.rs b/hive-forge-notify/src/notify.rs index 98ad8b54..fbeb60a6 100644 --- a/hive-forge-notify/src/notify.rs +++ b/hive-forge-notify/src/notify.rs @@ -17,7 +17,7 @@ //! //! **Multi-source**: always the internal Forgejo, plus github.com when the //! agent has a PAT. Each source polls independently behind -//! [`Source`](crate::source::Source); everything below is shared. Rationale +//! [`Source`]; everything below is shared. Rationale //! + host differences: [`docs/forge.md::Sources`](../../../docs/forge.md). //! //! Activation gates, self-notification filtering, body excerpt + diff --git a/hive-jobq/src/builder.rs b/hive-jobq/src/builder.rs index a2e207e9..7064f62a 100644 --- a/hive-jobq/src/builder.rs +++ b/hive-jobq/src/builder.rs @@ -12,7 +12,7 @@ //! inserted from outside this crate — `new()` and `insert_with` are both //! `pub(crate)`, and there is deliberately no `Default` impl, since a trait impl //! on a `pub` type is public regardless. There is no intermediate -//! node-description type to keep in sync with [`crate::Graph::insert`]'s signature — +//! node-description type to keep in sync with `Graph::insert`'s signature — //! so a job has no representation that can be passed around instead of being //! inserted. //! @@ -115,7 +115,7 @@ pub enum BuildError { /// Reject a job whose own declarations don't hold up — **before anything is /// inserted**, so no failure can leave a partial job behind. /// -/// This covers *every* rejection [`crate::Graph::insert`] can raise for a +/// This covers *every* rejection `Graph::insert` can raise for a /// builder-produced node, which is what makes the insert loop below infallible /// in practice: /// @@ -387,7 +387,7 @@ impl From<&NodeRef<'_, N, R>> for NodeGuid { impl NodeRef<'_, N, R> { /// This node's handle, for callers that want to hold the identity without /// the builder borrow (e.g. to look the id up after - /// [`JobBuilder::insert_into`]). + /// [`crate::scheduler::Scheduler::insert_job`]). #[must_use] pub fn guid(self) -> NodeGuid { self.guid diff --git a/hive-jobq/src/lib.rs b/hive-jobq/src/lib.rs index fdad544e..98e08c0a 100644 --- a/hive-jobq/src/lib.rs +++ b/hive-jobq/src/lib.rs @@ -321,7 +321,7 @@ pub struct Node { /// An error from inserting into or loading a [`Graph`] with a dangling id. /// /// A [`NodeId`] is only meaningful against the graph that minted it, so both -/// entry points — [`Graph::insert`] and deserialization — reject references to +/// entry points — `Graph::insert` and deserialization — reject references to /// nodes the graph does not contain. That is what lets internal iteration trust /// every id the graph holds. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] @@ -482,8 +482,8 @@ impl Graph { /// Insert without re-validating — **only** for a node the builder has /// already proved well-formed. /// - /// [`crate::builder::check_job_shape`] decides every rejection - /// [`Graph::insert`] could raise, before the first node lands. Re-checking + /// `check_job_shape` decides every rejection + /// `Graph::insert` could raise, before the first node lands. Re-checking /// here would not add safety: the insert loop mutates as it goes, so a /// rejection at node `i` would leave `0..i` in the graph — a loud error /// *after* the corruption rather than instead of it. Making the sink diff --git a/hive-jobq/src/resources.rs b/hive-jobq/src/resources.rs index 52198366..6754a6a1 100644 --- a/hive-jobq/src/resources.rs +++ b/hive-jobq/src/resources.rs @@ -7,7 +7,7 @@ //! available-once". Typical names: `build-slot` (capacity = number of build //! slots), `agent/` (capacity 1 — the per-agent lifecycle lock). //! -//! The one operation that matters is [`ResourceTable::try_acquire_all`]: it +//! The one operation that matters is `ResourceTable::try_acquire_all`: it //! takes *all* of a node's resource requests and either grants every one or //! grants none, touching nothing on failure. Because a node acquires all its //! resources atomically at start (never holds one while waiting for another), @@ -27,8 +27,8 @@ use std::hash::Hash; /// /// Configure known capacities with [`ResourceTable::set_capacity`]; any name /// left unconfigured has the default capacity (1). Acquire and release move -/// units atomically via [`ResourceTable::try_acquire_all`] / -/// [`ResourceTable::release_all`]. +/// units atomically via `ResourceTable::try_acquire_all` / +/// `ResourceTable::release_all`. #[derive(Debug, Clone)] pub struct ResourceTable { /// Configured capacities, keyed by resource. Missing ⇒ `default_capacity`. diff --git a/hive-jobq/src/scheduler.rs b/hive-jobq/src/scheduler.rs index 1ab6015d..3d4ae466 100644 --- a/hive-jobq/src/scheduler.rs +++ b/hive-jobq/src/scheduler.rs @@ -38,7 +38,7 @@ use crate::builder::{BuildError, JobBuilder, NodeGuid}; use crate::resources::ResourceTable; use crate::{Dep, Graph, GraphError, NodeId, State, TerminalState}; -/// The result of a node's own execution, reported to [`Scheduler::complete`]. +/// The result of a node's own execution, reported to `Scheduler::complete`. /// /// `Cancelled` is not an outcome a runner reports — it is scheduler-driven (an /// `AfterOk` dependency failed), so a runner only ever says `Done` or `Failed`. @@ -60,7 +60,7 @@ pub struct Scheduler { resources: ResourceTable, /// Fresh units each owner node acquired: `owner → [(resource, count)]`. /// Recorded against the node that *acquired* the units (never a borrower); - /// released back to the table once the owner and its whole [`Node::parent`] + /// released back to the table once the owner and its whole [`crate::Node::parent`] /// subtree are terminal. owned: HashMap>, /// Which branch currently borrows a given owner's grant: `(owner, resource) @@ -89,7 +89,7 @@ impl Scheduler { } /// Append a node under `parent` — e.g. a running node growing more work into - /// its own subtree. Delegates to [`Graph::insert`]; claim again afterwards + /// its own subtree. Delegates to `Graph::insert`; claim again afterwards /// to start it once it is runnable. /// /// # Errors @@ -114,7 +114,7 @@ impl Scheduler { /// being passed around as a spec. /// /// The one insertion entry point for a job. Nodes go straight into - /// [`Graph::insert_unchecked`]: [`crate::builder::check_job_shape`] has + /// `Graph::insert_unchecked`: `check_job_shape` has /// already decided every rejection the graph could raise, so re-validating /// per node could only report a problem *after* the earlier nodes were /// inserted. Claim again afterwards to start whatever became runnable. @@ -253,7 +253,7 @@ impl Scheduler { true } - /// The nearest [`Node::parent`] ancestor of `id` that *owns* (holds real + /// The nearest [`crate::Node::parent`] ancestor of `id` that *owns* (holds real /// units of) `name`, or `None` if none does (⇒ `id` must acquire it fresh). fn parent_ancestor_owning(&self, id: NodeId, name: &R) -> Option { let mut cur = self.graph.node(id).and_then(|n| n.parent); @@ -266,7 +266,7 @@ impl Scheduler { None } - /// Whether `ancestor` lies on `id`'s [`Node::parent`] chain (i.e. `id` is in + /// Whether `ancestor` lies on `id`'s [`crate::Node::parent`] chain (i.e. `id` is in /// `ancestor`'s subtree). `id` itself does not count as its own ancestor. fn parent_chain_contains(&self, id: NodeId, ancestor: NodeId) -> bool { let mut cur = self.graph.node(id).and_then(|n| n.parent); @@ -286,7 +286,7 @@ impl Scheduler { .is_some_and(|units| units.iter().any(|(n, _)| n == name)) } - /// Whether `root` and every node in its [`Node::parent`] subtree are + /// Whether `root` and every node in its [`crate::Node::parent`] subtree are /// terminal — the condition for releasing `root`'s owned grants (and for /// giving back a borrow whose branch-root is `root`). fn subtree_terminal(&self, root: NodeId) -> bool { @@ -298,7 +298,7 @@ impl Scheduler { } /// Report a running node's own logic result. On success the node is *not* - /// terminal until its sub-nodes ([`Node::parent`] children) all finish — it + /// terminal until its sub-nodes ([`crate::Node::parent`] children) all finish — it /// rests in [`State::Finishing`] until then, rolling up to [`State::Done`] /// (every child `Done`) or [`State::Failed`] (any child `Failed`/`Cancelled`). /// On failure it is `Failed` at once and its pending sub-nodes are cancelled @@ -324,7 +324,7 @@ impl Scheduler { self.release_ready(); } - /// [`Scheduler::complete`], plus whatever the node declared into the builder + /// `Scheduler::complete`, plus whatever the node declared into the builder /// it was handed while running. /// /// `grown`'s nodes are inserted **under `id`** and *before* the completion, @@ -440,7 +440,7 @@ impl Scheduler { /// After `start` became terminal, roll up every ancestor that was parked in /// `Finishing` awaiting its children: once all of an ancestor's children are /// terminal it transitions (Done / Failed), which may let *its* parent roll - /// up too, and so on up the [`Node::parent`] chain. + /// up too, and so on up the [`crate::Node::parent`] chain. fn roll_up_ancestors(&mut self, start: NodeId) { let mut cur = self.graph.node(start).and_then(|n| n.parent); while let Some(a) = cur { diff --git a/hive-matrix-mcp/src/accounts.rs b/hive-matrix-mcp/src/accounts.rs index d7cdb9ec..ebbb055b 100644 --- a/hive-matrix-mcp/src/accounts.rs +++ b/hive-matrix-mcp/src/accounts.rs @@ -50,7 +50,7 @@ impl AccountCfg { /// daemon-wide `HIVE_MATRIX_URL` — or `None` when neither is set. /// /// `None` is a real answer, not a failure: the account is skipped, the - /// same way [`discover_token_accounts_in`] already skips a discovered + /// same way `discover_token_accounts_in` already skips a discovered /// token whose homeserver sidecar is missing. #[must_use] pub fn homeserver(&self) -> Option { diff --git a/hive-matrix-mcp/src/handlers.rs b/hive-matrix-mcp/src/handlers.rs index 7f03a87e..1dc1e724 100644 --- a/hive-matrix-mcp/src/handlers.rs +++ b/hive-matrix-mcp/src/handlers.rs @@ -178,7 +178,7 @@ pub fn room_label(room: &matrix_sdk::Room) -> String { /// content (the agent must `read_room` then `mark_read` the latest event /// first), or `None` when the send may proceed. /// -/// Read-state is [`room_unread_state`] — the same predicate the wake path +/// Read-state is `room_unread_state` — the same predicate the wake path /// and `get_loose_ends` use, so "caught up" here means exactly what those /// surfaces mean. Reactions and `mark_read` are not gated — only /// message-posting tools (`send_message`, `send_reply`, `send_dm`) so an @@ -195,7 +195,7 @@ async fn unread_guard(client: &Client, room: &matrix_sdk::Room) -> Option Option { diff --git a/hive-priv-sock/src/lib.rs b/hive-priv-sock/src/lib.rs index 88399f1f..7324c248 100644 --- a/hive-priv-sock/src/lib.rs +++ b/hive-priv-sock/src/lib.rs @@ -446,7 +446,7 @@ pub enum PrivRequest { /// `forgejo` unix user. hive-priv executes: /// /// nixos-container run hive-forge -- runuser -u forgejo -- - /// forgejo --work-path /var/lib/forgejo admin + /// forgejo --work-path /var/lib/forgejo admin `` /// /// `args` must not contain null bytes, newlines, or shell metacharacters; /// hive-priv validates this before spawning the subprocess. diff --git a/hive-sock-client/src/lib.rs b/hive-sock-client/src/lib.rs index a4e536f8..3ae62432 100644 --- a/hive-sock-client/src/lib.rs +++ b/hive-sock-client/src/lib.rs @@ -48,7 +48,7 @@ pub enum Retry { /// backoff would stack sleeps on top of it and delay the rest of the /// batch. None, - /// Back off on [`RIDE_OUT_RESTART_BACKOFFS_MS`] (~60s total). For + /// Back off on `RIDE_OUT_RESTART_BACKOFFS_MS` (~60s total). For /// callers with no natural retry of their own, where a surfaced /// transient costs more than the wait. RideOutRestart, diff --git a/hivectl/src/client.rs b/hivectl/src/client.rs index 344b2ade..14bd4427 100644 --- a/hivectl/src/client.rs +++ b/hivectl/src/client.rs @@ -2,7 +2,7 @@ //! `/run/hyperhive/host.sock`. //! //! Connect failures are classified into an actionable message before they -//! reach the operator (see [`connect_hint`]) — the three ways this fails +//! reach the operator (see `connect_hint`) — the three ways this fails //! (not in `hive-admin`, no socket, nobody listening) need three different //! fixes, and the raw `Permission denied (os error 13)` names none of them. diff --git a/hivectl/src/dag_progress.rs b/hivectl/src/dag_progress.rs index fa25516b..4a07695a 100644 --- a/hivectl/src/dag_progress.rs +++ b/hivectl/src/dag_progress.rs @@ -2,11 +2,11 @@ //! //! Split out of `hivectl.rs` (which is already large): everything that //! polls the daemon's node queue (`HostRequest::QueueNodes`) and renders -//! progress lives here. [`wait_for_nodes`] is the entry point the command +//! progress lives here. [`crate::dag_progress::wait_for_nodes`] is the entry point the command //! handlers call; it dispatches to a live `indicatif` animation on a TTY //! and a plain line-on-change stream otherwise. //! -//! Consumes `hive-jobq-wire`'s generic [`GraphNode`]/`NodePayload`: a +//! Consumes `hive-jobq-wire`'s generic `GraphNode`/`NodePayload`: a //! node's kind (and, for a root, what submitted it) comes straight off //! `payload.label`; node-specific extras (`agent`) ride in //! `payload.data`'s opaque kvps — the shape `hive-c0re`'s @@ -19,7 +19,7 @@ //! `ids` is always a **batch**, not a single id: a hive-wide op (e.g. //! restarting every agent) submits one root per agent, and the whole //! batch is polled together in a single `QueueNodes` request per tick — -//! [`group_by_root`] splits the combined response back into per-root +//! `group_by_root` splits the combined response back into per-root //! groups rather than issuing one round-trip per id. use std::collections::{BTreeSet, HashMap, HashSet}; diff --git a/nix/checks.nix b/nix/checks.nix index c8ac66e6..56a1a935 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -64,6 +64,53 @@ in 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"; + # Every rustdoc lint that catches a *wrong* doc, denied. Kept as an + # explicit list rather than a blanket `-D warnings` so that adding a + # new rustdoc lint upstream can't silently red the build on a class + # nobody has triaged yet. + RUSTDOCFLAGS = builtins.concatStringsSep " " [ + "-D rustdoc::broken_intra_doc_links" + "-D rustdoc::private_intra_doc_links" + "-D rustdoc::invalid_html_tags" + "-D rustdoc::redundant_explicit_links" + "-D rustdoc::bare_urls" + "-D rustdoc::unescaped_backticks" + ]; + }; + # 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 diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index 6ff68409..a30bec24 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -5,7 +5,7 @@ //! //! Holds one piece of read-only state: the swarm's hive directory, loaded //! once at startup from an env var the NixOS module sets -//! (`services.hyperhive.swarm.controller`) — see [`load_hives`]. Still no +//! (`services.hyperhive.swarm.controller`) — see `load_hives`. Still no //! persistence and no writes; a config change means a redeploy, same as //! every other option this process reads. //! @@ -94,7 +94,7 @@ struct HiveEntry { #[derive(Clone)] struct AppState { - /// Loaded once at startup ([`load_hives`]); never mutated, so an + /// Loaded once at startup (`load_hives`); never mutated, so an /// `Arc` clone per request is the whole synchronization story. hives: Arc>, } From 8fed5c181327d5bd7cd295d4e4be856bdb5b4dd6 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 14 Aug 2026 02:19:45 +0200 Subject: [PATCH 2/2] 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. --- Cargo.toml | 17 +++++++++++++++++ nix/checks.nix | 18 ++++++------------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0ba429a1..184b75b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,23 @@ missing_panics_doc = "allow" module_name_repetitions = "allow" must_use_candidate = "allow" +[workspace.lints.rustdoc] +# Doc-link rot has no other discoverer: clippy does not read intra-doc +# links, `cargo test` does not, and nothing else builds docs. A `[`Foo`]` +# pointing at a renamed, moved or deleted item renders as plain text and +# misleads the next reader — worse than no link, since it names something +# and so sends them looking. +# +# Here rather than in `RUSTDOCFLAGS` on the CI check, so a plain local +# `cargo doc` fails the same way CI does. A gate you only meet in CI is a +# gate you meet too late. +broken_intra_doc_links = "deny" +private_intra_doc_links = "deny" +invalid_html_tags = "deny" +redundant_explicit_links = "deny" +bare_urls = "deny" +unescaped_backticks = "deny" + [workspace.dependencies] anyhow = "1" libc = "0.2" diff --git a/nix/checks.nix b/nix/checks.nix index 56a1a935..7181e8f7 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -97,18 +97,12 @@ in pname = "hyperhive-workspace"; version = "0.1.0"; cargoDocExtraArgs = "--workspace --no-deps --document-private-items"; - # Every rustdoc lint that catches a *wrong* doc, denied. Kept as an - # explicit list rather than a blanket `-D warnings` so that adding a - # new rustdoc lint upstream can't silently red the build on a class - # nobody has triaged yet. - RUSTDOCFLAGS = builtins.concatStringsSep " " [ - "-D rustdoc::broken_intra_doc_links" - "-D rustdoc::private_intra_doc_links" - "-D rustdoc::invalid_html_tags" - "-D rustdoc::redundant_explicit_links" - "-D rustdoc::bare_urls" - "-D rustdoc::unescaped_backticks" - ]; + # 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` +