Run interrogate + ruff D over the target's public surface, then judge every
hit: is this a real public symbol a consumer types against, or something
trivial a docstring wouldn't help (a dunder, an obvious one-line property, an
overload stub)? A plain linter flags every missing docstring the same way —
the judgment pass here is what tells a load-bearing gap from noise.
Two passes. audit.py in this skill's directory is pass one —
mechanical: run ruff D1xx and interrogate, parse both into findings rows. It
never judges document vs skip; every row it emits starts bucket: document
(a ruff D1xx hit is a real missing-docstring candidate by construction, same
reasoning error-handling's parser uses for a caught-and-dropped exception).
The judgment pass below reads those rows and reassigns the real bucket.
Buckets & categories
document — a real gap: public API a consumer builds against, with
nothing trivial about it. Write the docstring.
skip — flagged, but a docstring genuinely adds nothing: a trivial
dunder (__repr__, __eq__), an obvious one-line @property, an
@overload stub whose implementation carries the real docstring.
unsure — flagged, and it isn't a clean fit for either bucket above:
a symbol whose purpose isn't obvious from its name/shape alone, but nothing
concrete marks it trivial either.
category is the ruff D code's target, slugged into a small closed set:
missing-module-docstring (D100), missing-package-docstring (D104),
missing-class-docstring (D101/D106), missing-method-docstring (D102),
missing-function-docstring (D103), missing-init-docstring (D107),
missing-magic-method-docstring (D105). extra.coverage carries
interrogate's overall coverage percentage for the whole scanned scope —
the same number on every row, not per-symbol.
Run
Scope tight. Audit $ARGUMENTS if given; with no argument, scope
defaults per ~/.agents/skills/all-audits/SKILL.md's Scope section.
Target the package's public modules — skip tests,
vendored/generated/dependency trees (node_modules, dist, .venv,
vendor, build output, lockfiles), and any .git/ or worktrees/ tree.
Pass one — run ruff D1xx and interrogate over the SAME scope, parse
them.
scope="$(realpath "${ARGUMENTS:-.}")"
uvx ruff check --select D100,D101,D102,D103,D104,D105,D106,D107 --output-format json "$scope" > /tmp/ruff-out.json
uvx interrogate -v "$scope" \
-e "$scope/node_modules" -e "$scope/.venv" -e "$scope/dist" -e "$scope/vendor" \
-e "$scope/.git" -e "$scope/build" -e "$scope/worktrees" \
> /tmp/interrogate-out.txt
python3 ~/.agents/skills/docstring-coverage/audit.py /tmp/ruff-out.json /tmp/interrogate-out.txt
Only the D1xx "missing docstring" codes are selected — not the D2xx/D4xx
style-convention codes (blank-line placement, summary formatting). Those
are ruff's own job to enforce as a lint gate, not this audit's; several of
them (D203/D211) actively conflict with each other, which is why they stay
out of scope here.
audit.py's parse_coverage(ruff_json, interrogate_text) -> list[dict]
is the tested seam (~/.agents/skills/docstring-coverage/fixtures/ + answer-key.md back
it, mirroring ~/.agents/skills/dead-code/fixtures/) — pure, no subprocess inside it, fed
ruff's captured JSON and interrogate's captured text. main() wraps it:
reads the two file paths as argv, prints one JSON row per ruff D1xx hit.
This is a candidate list, not a verdict — every row still needs the
judgment pass. Private symbols (leading _) and already-documented
symbols never appear — ruff's own D1xx rule semantics exclude them, so
there's nothing to hand-filter.
Pass two — judge every row. For each row, read the flagged symbol in
context: is it public API a consumer types against, or something trivial
a docstring wouldn't help? Reassign bucket per the rules above, and
rewrite summary / failure to say why — "public function taking two
args and returning a computed value, no docstring at all" for a
document, "trivial __repr__ override, name and body are
self-explanatory" for a skip.
Write the findings log and render the summary — the default
deliverable. See
~/.agents/skills/all-audits/harness/AUDIT-RUN.md for the shared
write-and-deliver step (tmpdir resolution, findings.jsonl +
report.html, opening, and the final print). This audit
touches no code — writing the docstrings is a separate, opt-in step the
user asks for by name. This skill's own bucket names and metabar:
- Log — one JSONL line per ruff D1xx hit.
bucket is document /
skip / unsure. category is the D-code slug (see above).
extra.coverage carries interrogate's overall coverage percentage.
- Summary — the verdict, a
N flagged · D document · S skip · U unsure · coverage NN.N% metabar, findings grouped by bucket then
category with counts. No per-hit cards. Call out the skip finds in a
vt-callout — the ones a naive tool-only read would have wrongly told
the user to document.
Verify against the fixture
~/.agents/skills/docstring-coverage/fixtures/sample.py carries one undocumented public
function (undocumented_public, no docstring — ruff D103 flags it), one
documented public function (documented_public — not flagged), and one
private function (_helper, no docstring but not public — not flagged by
D1xx). ~/.agents/skills/docstring-coverage/fixtures/answer-key.md has the captured ruff +
interrogate output and the expected finding. Running this skill over
~/.agents/skills/docstring-coverage/fixtures/ should reproduce that table: one row,
undocumented_public at line 9, bucket: document, extra.coverage: 50.0.
1---2name: docstring-coverage3description: Find public functions and classes with no docstring on a typed (py.typed) library, so consumers get docs on the surface they build against.4---56Run interrogate + ruff `D` over the target's public surface, then judge every7hit: is this a real public symbol a consumer types against, or something8trivial a docstring wouldn't help (a dunder, an obvious one-line property, an9overload stub)? A plain linter flags every missing docstring the same way —10the judgment pass here is what tells a load-bearing gap from noise.1112**Two passes.** `audit.py` in this skill's directory is pass one —13mechanical: run ruff D1xx and interrogate, parse both into findings rows. It14never judges document vs skip; every row it emits starts `bucket: document`15(a ruff D1xx hit is a real missing-docstring candidate by construction, same16reasoning error-handling's parser uses for a caught-and-dropped exception).17The judgment pass below reads those rows and reassigns the real bucket.1819## Buckets & categories2021- **`document`** — a real gap: public API a consumer builds against, with22 nothing trivial about it. Write the docstring.23- **`skip`** — flagged, but a docstring genuinely adds nothing: a trivial24 dunder (`__repr__`, `__eq__`), an obvious one-line `@property`, an25 `@overload` stub whose implementation carries the real docstring.26- **`unsure`** — flagged, and it isn't a clean fit for either bucket above:27 a symbol whose purpose isn't obvious from its name/shape alone, but nothing28 concrete marks it trivial either.2930`category` is the ruff D code's target, slugged into a small closed set:31`missing-module-docstring` (D100), `missing-package-docstring` (D104),32`missing-class-docstring` (D101/D106), `missing-method-docstring` (D102),33`missing-function-docstring` (D103), `missing-init-docstring` (D107),34`missing-magic-method-docstring` (D105). `extra.coverage` carries35interrogate's overall coverage percentage for the whole scanned scope —36the same number on every row, not per-symbol.3738## Run39401. **Scope tight.** Audit `$ARGUMENTS` if given; with no argument, scope41 defaults per `~/.agents/skills/all-audits/SKILL.md`'s Scope section.42 Target the package's public modules — skip tests,43 vendored/generated/dependency trees (`node_modules`, `dist`, `.venv`,44 `vendor`, build output, lockfiles), and any `.git/` or `worktrees/` tree.45462. **Pass one — run ruff D1xx and interrogate over the SAME scope, parse47 them.**48 ```sh49 scope="$(realpath "${ARGUMENTS:-.}")"50 uvx ruff check --select D100,D101,D102,D103,D104,D105,D106,D107 --output-format json "$scope" > /tmp/ruff-out.json51 uvx interrogate -v "$scope" \52 -e "$scope/node_modules" -e "$scope/.venv" -e "$scope/dist" -e "$scope/vendor" \53 -e "$scope/.git" -e "$scope/build" -e "$scope/worktrees" \54 > /tmp/interrogate-out.txt55 python3 ~/.agents/skills/docstring-coverage/audit.py /tmp/ruff-out.json /tmp/interrogate-out.txt56 ```57 Only the D1xx "missing docstring" codes are selected — not the D2xx/D4xx58 style-convention codes (blank-line placement, summary formatting). Those59 are ruff's own job to enforce as a lint gate, not this audit's; several of60 them (D203/D211) actively conflict with each other, which is why they stay61 out of scope here.6263 `audit.py`'s `parse_coverage(ruff_json, interrogate_text) -> list[dict]`64 is the tested seam (`~/.agents/skills/docstring-coverage/fixtures/` + `answer-key.md` back65 it, mirroring `~/.agents/skills/dead-code/fixtures/`) — pure, no subprocess inside it, fed66 ruff's captured JSON and interrogate's captured text. `main()` wraps it:67 reads the two file paths as argv, prints one JSON row per ruff D1xx hit.68 This is a candidate list, not a verdict — every row still needs the69 judgment pass. Private symbols (leading `_`) and already-documented70 symbols never appear — ruff's own D1xx rule semantics exclude them, so71 there's nothing to hand-filter.72733. **Pass two — judge every row.** For each row, read the flagged symbol in74 context: is it public API a consumer types against, or something trivial75 a docstring wouldn't help? Reassign `bucket` per the rules above, and76 rewrite `summary` / `failure` to say *why* — "public function taking two77 args and returning a computed value, no docstring at all" for a78 `document`, "trivial `__repr__` override, name and body are79 self-explanatory" for a `skip`.80814. **Write the findings log and render the summary — the default82 deliverable.** See83 `~/.agents/skills/all-audits/harness/AUDIT-RUN.md` for the shared84 write-and-deliver step (tmpdir resolution, `findings.jsonl` +85 `report.html`, opening, and the final print). This audit86 touches no code — writing the docstrings is a separate, opt-in step the87 user asks for by name. This skill's own bucket names and metabar:8889 - **Log** — one JSONL line per ruff D1xx hit. `bucket` is `document` /90 `skip` / `unsure`. `category` is the D-code slug (see above).91 `extra.coverage` carries interrogate's overall coverage percentage.92 - **Summary** — the verdict, a `N flagged · D document · S skip · U93 unsure · coverage NN.N%` metabar, findings grouped by bucket then94 category with counts. No per-hit cards. Call out the `skip` finds in a95 `vt-callout` — the ones a naive tool-only read would have wrongly told96 the user to document.9798## Verify against the fixture99100`~/.agents/skills/docstring-coverage/fixtures/sample.py` carries one undocumented public101function (`undocumented_public`, no docstring — ruff D103 flags it), one102documented public function (`documented_public` — not flagged), and one103private function (`_helper`, no docstring but not public — not flagged by104D1xx). `~/.agents/skills/docstring-coverage/fixtures/answer-key.md` has the captured ruff +105interrogate output and the expected finding. Running this skill over106`~/.agents/skills/docstring-coverage/fixtures/` should reproduce that table: one row,107`undocumented_public` at line 9, `bucket: document`, `extra.coverage: 50.0`.