Compare commits

...
Author SHA1 Message Date
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
atlas
be3411e180 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
  <args>/<hex>/<name> 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.
2026-08-14 02:30:55 +02:00
21 changed files with 99 additions and 41 deletions

View file

@ -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"

View file

@ -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]

View file

@ -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).

View file

@ -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.
//!

View file

@ -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: <our own pid>` 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 —

View file

@ -106,7 +106,7 @@ fn waiting_ids() -> &'static Mutex<HashMap<String, u32>> {
}
/// 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<u64>, name: Option<String>)
/// 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

View file

@ -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 +

View file

@ -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<N, R> From<&NodeRef<'_, N, R>> for NodeGuid {
impl<N, R> 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

View file

@ -321,7 +321,7 @@ pub struct Node<N, R> {
/// 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<N, R> Graph<N, R> {
/// 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

View file

@ -7,7 +7,7 @@
//! available-once". Typical names: `build-slot` (capacity = number of build
//! slots), `agent/<name>` (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<R> {
/// Configured capacities, keyed by resource. Missing ⇒ `default_capacity`.

View file

@ -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<N, R: Clone + Eq + Hash> {
resources: ResourceTable<R>,
/// 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<NodeId, Vec<(R, u32)>>,
/// Which branch currently borrows a given owner's grant: `(owner, resource)
@ -89,7 +89,7 @@ impl<N, R: Clone + Eq + Hash> Scheduler<N, R> {
}
/// 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<N, R: Clone + Eq + Hash> Scheduler<N, R> {
/// 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<N, R: Clone + Eq + Hash> Scheduler<N, R> {
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<NodeId> {
let mut cur = self.graph.node(id).and_then(|n| n.parent);
@ -266,7 +266,7 @@ impl<N, R: Clone + Eq + Hash> Scheduler<N, R> {
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<N, R: Clone + Eq + Hash> Scheduler<N, R> {
.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<N, R: Clone + Eq + Hash> Scheduler<N, R> {
}
/// 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<N, R: Clone + Eq + Hash> Scheduler<N, R> {
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<N, R: Clone + Eq + Hash> Scheduler<N, R> {
/// 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 {

View file

@ -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<String> {

View file

@ -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<Daemon
/// Fetch the single most recent timeline event in `room`, of any type
/// (redactions/state/reactions included — identity is what
/// [`room_unread_state`] needs, not content). `None` for a genuinely
/// `room_unread_state` needs, not content). `None` for a genuinely
/// empty room or on any request failure.
async fn latest_event(
client: &Client,
@ -909,7 +909,7 @@ pub async fn download_file(
///
/// **Latency note**: each unread room triggers a live `/messages`
/// network request to the matrix homeserver to determine its latest
/// event (via [`room_unread_state`]). This adds per-room round-trip
/// event (via `room_unread_state`). This adds per-room round-trip
/// latency to `get_loose_ends` and the wake-signal path. Acceptable in
/// practice (rooms with unread are few; request is best-effort), but
/// worth bearing in mind if latency becomes a concern.

View file

@ -550,7 +550,7 @@ async fn unread_summary_handler(
/// Run the MCP server over HTTP (rmcp streamable-http transport) on
/// `addr`, dispatching against `registry`. Also serves a small
/// non-MCP `/unread-summary` status endpoint (see
/// [`unread_summary_handler`]).
/// `unread_summary_handler`).
///
/// Sole transport — there is no stdio mode. Long-lived so claude
/// reconnects to the stable URL each turn instead of respawning a

View file

@ -20,7 +20,7 @@ use hive_sock_client::{Retry, notify};
const TODO_SOCKET_RETRY: Retry = Retry::None;
/// The harness-served in-agent socket (`HIVE_AGENT_SOCKET`) where todo ops
/// go — distinct from the host-served control socket used by [`send_wake`].
/// go — distinct from the host-served control socket used by `send_wake`.
/// `None` when unset/empty, in which case todo sends are a best-effort
/// no-op (a standalone daemon without the harness socket).
fn agent_socket() -> Option<std::path::PathBuf> {

View file

@ -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 <args>
/// forgejo --work-path /var/lib/forgejo admin `<args>`
///
/// `args` must not contain null bytes, newlines, or shell metacharacters;
/// hive-priv validates this before spawning the subprocess.

View file

@ -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,

View file

@ -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.

View file

@ -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};

View file

@ -64,6 +64,47 @@ 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";
# 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

View file

@ -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<Vec<HiveEntry>>,
}