A snapshot: read at commit 707a04c7ce8 on 2026-10-18T00:06:01Z by majordomus 0.29.0. Nothing on this page is read again. See it live
project.rust-public-api-quality@1
Every exported Rust item explains itself, shows itself running, and is exercised by something
What it is
- URI
majordomus://rule/project.rust-public-api-quality@1- Kind
rule- Identity
project.rust-public-api-quality@1- Path
.ai/repo/rules/project/rust-public-api-quality.v1.md- Provenance
- declared
- Media type
text/markdown- As a capability
- rule.project.rust-public-api-quality@1
Front matter
{
"id": "project.rust-public-api-quality",
"version": 1,
"kind": "rule",
"title": "Every exported Rust item explains itself, shows itself running, and is exercised by something",
"description": "The crate's exported surface is measured from its own syntax tree — documentation that says more than the signature, an executable example on everything that carries behaviour, and a module boundary that something exercises — and what the policy does not ask for is derived and printed rather than exempted by a list.",
"statement": "An item this crate exports is documented in words its signature does not already carry, shown running in an example the toolchain executes, and reached by a test; an item that is none of those is either not finished or not public.",
"status": "active",
"class": "blocking",
"depends_on": [
"project.no-claim-without-test@1",
"project.rust-command-tested-in-file@1"
],
"tags": [
"rust",
"documentation",
"testing",
"public-api"
],
"x-majordomus": {
"tests": [
"test/cases/96_rust_public_api_quality.sh"
]
}
}Content
---
id: project.rust-public-api-quality
version: 1
kind: rule
title: Every exported Rust item explains itself, shows itself running, and is exercised by something
description: The crate's exported surface is measured from its own syntax tree — documentation that says more than the signature, an executable example on everything that carries behaviour, and a module boundary that something exercises — and what the policy does not ask for is derived and printed rather than exempted by a list.
statement: An item this crate exports is documented in words its signature does not already carry, shown running in an example the toolchain executes, and reached by a test; an item that is none of those is either not finished or not public.
status: active
class: blocking
depends_on: [project.no-claim-without-test@1, project.rust-command-tested-in-file@1]
tags: [rust, documentation, testing, public-api]
x-majordomus:
tests: [test/cases/96_rust_public_api_quality.sh]
---
# Rationale
The compiler already refuses an exported item with no documentation: `#![warn(missing_docs)]`
in `lib.rs` and `-D warnings` in the gate make the absence a build failure. That catches
nothing about whether the documentation is worth reading, and it catches nothing at all
about examples, which is where the interesting failure lives.
Prose about behaviour drifts. It drifts silently, it drifts in the direction of what the
code used to do, and no gate in this repository could see it — because prose is not run.
An example is run: `cargo test --doc` compiles it against the real signatures and executes
it against the real behaviour, so an example that has gone stale is a red build rather than
a paragraph that misleads the next reader. That is the whole argument for this rule, and it
is the same argument `project.rust-command-tested-in-file` already makes for commands. This
rule generalises it from the command modules to the surface.
The second half is the boundary. A module is a conceptual unit — it owns something, it has
invariants, it has neighbours — and a module that does not say what it owns is a directory
with a `.rs` on the end. When this rule first measured the crate, every exported module had
a header and fewer than one in ten had an example in it, and a quarter of them had no test
beside them and no test naming them. Those are the modules where a change breaks nothing
visible, which is exactly where changes go wrong. The current numbers are not written here:
`majordomus quality report --summary` computes them.
The third part is what the rule does *not* demand, and this is deliberate. A field, a
variant, a constant, a static and a type alias are named and typed; an example of one is an
example of whatever reads it, and a rule that demanded one would be satisfied with
`assert!(true)` inside a fence — the ceremony this rule exists to refuse. The same is true
of an accessor whose body hands back what the receiver already holds. So the policy
classifies every item from its kind and its shape, derives what it may ask of it, and
**prints what it did not ask for, with the reason**. There is no exemption file, because an
exemption file is a place to add a line.
This rule does not replace `project.rust-command-tested-in-file`, and the two have different
subjects. That rule is about the modules the application *composes* — every one of them,
whatever its visibility — and asks for one assertion beside the declaration, in either
accepted form. This one is about what the crate *exports* to anybody, and asks for both an
example and a test. A capability module that is `pub(crate)` is held by the first and not by
the second, which is right: it is still a command, and it is not API.
A strict rule about the exported surface is also a rule about how large that surface is.
This crate is `publish = false`: its consumers are its own binary, its tests, its benches
and its doc examples, and nothing else. An item that no consumer needs costs a doc comment
and an example under this rule, and the right answer to that cost is usually `pub(crate)`.
That is the intended pressure and not a side effect of it.
# Required behaviour
Every item the crate exports — reachable from the crate root through `pub` modules, or
carried out of a private one by a `pub use` — carries documentation. Where the item carries
behaviour (a module, a type, a trait, a function, a method, an exported macro) that
documentation says something the signature does not, and includes at least one fenced block
that the toolchain compiles: no `ignore`, no fenced box of prose standing in for one.
An example counts when it is compiled, when it names the item it is documenting, and when
it asserts something that could be false. `assert!(true)`, `assert_eq!(1, 1)` and an
arithmetic identity are not assertions about the item; `unwrap`, `expect` and `?` are,
because a failure of either fails the doctest. `no_run` counts and `ignore` does not: the
difference is whether the toolchain compiled it.
Every exported module carries a `//!` header that explains the boundary rather than naming
it, at least one executable example of its lifecycle, and behavioural coverage: a
`#[cfg(test)]` test beside the declaration, or a test under `tests/` that names the module.
Either form counts, for the reason `project.rust-command-tested-in-file` gives — demanding
a particular one buys a token test beside a real example.
What the policy does not ask of an item is derived from that item's kind and shape and is
reported as an exemption with its reason. Nothing declares an exemption, and there is no
file in which to record one.
# Failure behaviour
`majordomus quality report` measures the crate and answers with a typed report: the counts,
and one finding per violation carrying a stable code, the rule, the file and line, the
symbol, why it matters and what to do. The codes are
`RUST_PUBLIC_MISSING_DOCS`, `RUST_PUBLIC_THIN_DOCS`, `RUST_PUBLIC_MISSING_EXAMPLE`,
`RUST_EXAMPLE_NOT_EXECUTABLE`, `RUST_EXAMPLE_PLACEHOLDER`,
`RUST_EXAMPLE_DOES_NOT_NAME_SUBJECT`, `RUST_MODULE_MISSING_DOCS`,
`RUST_MODULE_MISSING_EXAMPLE` and `RUST_MODULE_MISSING_BEHAVIOURAL_TEST`. The command exits
`10` when any of them stands, which is the code every other unmet contract in this
executable uses, and the JSON and the human rendering are two readings of one value, so
they cannot disagree about the outcome.
The measurement is a capability, so it is answered identically on the command line, over
HTTP, over MCP and in the Cockpit. The `rust-quality` gate in `.ai/repo/ci/gates.yaml` runs
it, and `scripts/rust-check` runs it in the same order a person does.
Enforcement is a ratchet while the debt shrinks. `.ai/repo/rust-quality-baseline.txt`
records the findings that stood when this rule landed; the gate fails for a finding that is
not in it, so the debt can shrink and cannot grow. That is the mechanism
`project.rust-command-tested-in-file` describes and used, for the same reason: a blocking
rule with no migration path is a rule that gets reverted. The baseline is written only by
`--write-baseline`, which is a deliberate act, and the rule is not finished until the file
is empty and removed — an empty exemption list invites somebody to add a line to it.
The validator does not live in `lib/`. That library belongs to the shipped tool and a
project rule that needs a check gets a gate, not a doctrine — `project.rust-command-tested-in-file`
makes the argument and this rule follows it. Where there is no Rust crate the check is
skipped and says so.
# Verification
`cargo test --manifest-path apps/majordomus-cli/Cargo.toml quality`, which measures fixture
crates built for the purpose: a documented, exampled and tested crate has no findings, and
one crate per way of failing produces exactly its own code and nothing else — no example,
an ignored example, a placeholder example, an example about something else, a module with
no test. The validator's own negative fixtures are what keep it from becoming ceremonial.
`apps/majordomus-cli/tests/quality.rs`, which runs the measurement against this crate
through the built registry and the real command tree, and asserts the ratchet: no finding
outside the baseline.
`test/cases/96_rust_public_api_quality.sh`, which builds the executable, measures a fixture
repository through it, and asserts the exit code and the JSON of both a clean and a failing
tree.