Plan: Library-API Docs Burn-down (#463)
On this page
B6 of the Backlog Cleanup Campaign. #463’s first
two goals are already met (panic-class lints via ADR-030 / epic &62; unused_crate_dependencies
via the cargo machete gate, #464); this plan executes the remaining library doc-lints.
Decomposed per the split-large-issues rule into epic &68 library-api-docs-burndown with
14 child issues (#940–#953). Each batch table row is a living Status table (cargo xtask
plan-lint scans the Status column; canonical tokens only).
At a glance
-
What: roll
![warn(missing_docs)]+![warn(unreachable_pub)]across the library crates and document/fix every emission. -
Scope: 3,240
missing_docsitems across 24 crates + 128unreachable_pubitems (127 canopy-web, 1 canopy-api). Two service crates excluded frommissing_docsby documented policy; tool libs included per the product owner. -
Shape: 14–15 MRs (M0 pilot → M9 contracts → tool libs → service-crate cleanup; M12/seed may split a/b), one child issue per MR. Multi-session; each MR is its own ~10–13 min pre-push battery.
-
How: per-MR a parallel agent-draft + skeptic-verify workflow writes the docs; then
cargo clippy -D warnings+ the battery + ship.
Context
Measured 2026-06-28 via cargo rustc -p <crate> --lib — --force-warn <lint> (deps excluded), per
crate: missing_docs = 3,980 items across 26 of the 39 library crates (the other 13 already carry
the attr and are fully documented); unreachable_pub = 128.
Service-crate exclusion (documented policy → formalized here). canopy-eligibility/src/lib.rs:2-5
states a general policy: "Service-level crates do not enforce missing_docs — the HTTP API is the
public contract; the library crate exists only so integration tests can reach into the orchestrator."
canopy-web/src/lib.rs:3-6 says the same in effect ("this lib export is narrow") but does not name
the policy. So: apply it to both and make it explicit — M13 adds the matching comment to
canopy-web/src/lib.rs. Net: exclude canopy-web (645) + canopy-eligibility (95) from
missing_docs (−740 → 3,240); canopy-web still gets its unreachable_pub demotion (which enforces
the "narrow lib export" the policy describes).
Tool libs included (product-owner decision): canopy-seed (891) + canopy-cli (74) — no documented
exclusion.
Already done (13 documented libs, unchanged): canopy-api, -auth, -common, -db, -mq, -policy, -reference, -rules-client, -secrets, -signing, -store, -test-lib, -typst.
Doc-authoring standard (match the 13 documented crates)
-
Placement:
![warn(missing_docs)]and![warn(unreachable_pub)]go immediately after the crate//!doc block and before the first item — including crates that are just a list ofpub mod(e.g.canopy-contracts-rules/src/lib.rs). Model:canopy-common/src/lib.rs:8.warn, neverdeny; nocfg(test)carve-out. -
Granularity: a
///on every public item and every public field / enum variant (canopy-api/src/lib.rs:45-62,canopy-reference/src/types.rs:15-26). -
Style: one-line summary, sentence fragment, no trailing period, no backticks-around-types on line one; optional multi-line elaboration with domain context + ADR/issue cites (
canopy-contracts-persons/src/income.rs:17-33). -
J8 (anti-tautology): the pre-commit J8 gate rejects docs that restate the name — write intent. For genuinely self-evident homogeneous clusters (e.g. geographic enums) use the existing escape
#[allow(missing_docs)] // <reason>(precedent:canopy-reference/src/fips.rs:13), sparingly, at the type level — never a blanket dodge. A lone self-evident field gets a short real doc adding its unit/constraint/nullability. -
OpenAPI coupling: types deriving
utoipa::ToSchemaembed their doc comment verbatim as the schemadescription. Confirmed OpenAPI-feeding crates in scope: all contracts-* + canopy-composition + canopy-overpayments. Any such batch MUST regenerate the snapshots. Per-batch self-check:grep -l <CrateType> docs/modules/ROOT/openapi/*.json; the pre-push api-docs drift gate is the backstop.
GitLab structure (epic &68, children #940–#953)
-
#463 stays open as the originating umbrella (rescoped); the final batch MR (M13) closes it — the only MR that names #463 in a close keyword.
-
Each MR
Closesits own child issue on merge (campaign model — no withholding); each close gets the mandatory comment (impl SHA + bare merge SHA + changed crates + checked criteria).
MR batches
The n after each crate is its measured missing_docs count (these sum to 3,240); the
unreachable_pub column is emissions to fix — both attrs are added to every in-scope crate
regardless. Doc-only diffs are additive/low-risk, so the campaign "<500 LOC" guideline is relaxed for
the larger single crates; batches may be re-grouped.
| MR (issue) | Crates (missing_docs n) | unreachable_pub | Wt | OpenAPI | Status |
|---|---|---|---|---|---|
M0 (#940) |
validators(11), rules(24), facts(0), crypto-shred(0), plugin-macros(0); + |
api:1 |
3 |
rules |
Done (2026-06-28) — !710 |
M1 (#941) |
contracts eligibility(40), notices(55), enrollment(59) |
— |
5 |
yes |
Done (2026-06-28) — !711 |
M2 (#942) |
contracts wic(67), renewals(71), security(73) |
— |
5 |
yes |
Done (2026-06-28) — !712 |
M3 (#943) |
contracts caps(94), appeals(112) |
— |
5 |
yes |
Done (2026-06-28) — !713 |
M4 (#944) |
contracts verification(117), tanf(134) |
— |
5 |
yes |
Done (2026-06-28) — !714 |
M5 (#945) |
contracts reporting(171) |
— |
5 |
yes |
Done (2026-06-28) — !715 |
M6 (#946) |
contracts snap(204) |
— |
5 |
yes |
Done (2026-06-28) — !716 |
M7 (#947) |
contracts medicaid(206) |
— |
5 |
yes |
Done (2026-06-29) — !717 |
M8 (#948) |
contracts persons(230) |
— |
5 |
yes |
Done (2026-06-29) — !718 |
M9 (#949) |
contracts applications(328) |
— |
8 |
yes |
Done (2026-06-29) — !719 |
M10 (#950) |
composition(230), overpayments(49) |
— |
5 |
yes (both) |
Done (2026-06-29) — !720 |
M11 (#951) |
canopy-cli(74) |
— |
3 |
no |
Done (2026-06-29) — !721 |
M12 (#952) |
canopy-seed(891) — split a/b (model.rs=797 ≫450 LOC ⇒ M12a model.rs, M12b the remaining 10 files=94); shipped as 2 MRs against the single #952 (one shippable unit ⇒ no redundant child, per gitlab-issue-mr-standards: M12a |
— |
8 |
no |
Done (2026-06-29) — !722 (model.rs) + !723 (rest + lints) |
M13 (#953) |
service crates: canopy-web demote 127 + attrs + exclusion comment; canopy-eligibility attr (no missing_docs); closes #463 |
web:127 |
3 |
no |
Done (2026-06-29) — !724 |
Coverage: M0–M12 list each of the 24 in-scope crates exactly once and sum to 3,240; M13 is the
service-crate unreachable_pub cleanup. Sequencing: pilot (de-risk) → contracts (highest ROI:
OpenAPI + cross-crate consumers) → shared/tool libs → service cleanup last.
Per-MR execution loop
Branch chore/cleanup-docs-<slug> (campaign convention).
-
Enumerate: per crate,
cargo rustc -p <crate> --lib — --force-warn missing_docs 2>&1 | grep "missing documentation"(and--force-warn unreachable_pubfor M0/M13) → the exactfile:lineset. -
Draft (Workflow
pipelineover the batch’s.rsfiles — distinct files ⇒ conflict-free parallel edits): each Draft agent documents its file’s undocumented public items per the standard + J8; a Verify agent re-reads that file’s diff, kills tautologies, adds missed fields. For M13, agents demote the enumeratedpub→pub(crate)/pub(super). -
Enable the lints: add both attrs to each in-scope crate’s
lib.rs(the 13 documented libs get onlyunreachable_pub, M0; service crates get onlyunreachable_pub, M13). -
Verify zero residual:
cargo clippy -p <crate> --all-targets — -D warningsexits 0 with no output, andRUSTDOCFLAGS="-D rustdoc::broken_intra_doc_links" cargo doc --no-deps -p <crate>is clean (matches the pre-push gate,.githooks/pre-push). -
OpenAPI (batches whose crates feed snapshots — M0 rules, M1–M9, M10):
cargo xtask dev refresh && cargo xtask api-docs --update && cargo fmt --all && cargo fmt --check --all&&(fail-fast); never; echo $?. Commit the snapshot diff; doc-text-only diffs are expected — flag any semantic schema change in the MR description. -
Battery + ship: full pre-push battery → fresh J1–J8 subagent on the staged diff (report to user; fix any J8 tautology in-place + re-stage) → commit (signed, human author,
Co-Authored-Bythe session model) → push → MR (Closes #<child>; M13 alsoCloses #463). -
Post-merge: close the child with the mandatory comment; tick the epic task list; flip the batch
Status→Done (YYYY-MM-DD) — !<MR>;git branch -d+git remote prune origin.
M13 demotion safety: a grep for external users is a hint; the authority is the compiler —
cargo check --workspace --all-targets (in the battery) fails if any demoted item had an external
user, so a green workspace check is the proof.
Verification
-
Per MR: step 4 (clippy
-D warnings+ intra-doc-linkcargo docboth clean) + OpenAPI regen’d iff a feeding crate changed + full battery green + MR links its child. -
Final: all 24 in-scope libs carry
![warn(missing_docs)]; all 39 libs carry; workspacecargo clippy --all-targets — -D warningsgreen; #463 closed; epic &68 fully checked; campaign B6 row →Done.
Out of scope
-
missing_docson canopy-web + canopy-eligibility — documented service-crate policy (formalized on web in M13). -
B3b (#466 mutants), B7 (#484) — separate, decision-blocked.
-
missing_docs_in_private_items(1,321, transition-allowed inCargo.toml) — owned by epic &62 M4. -
Any logic/refactor bug found while documenting → file a
fix:issue +/relate; never fold into a doc MR.