Skip to content

Commit 127aa83

Browse files
derek73claude
andcommitted
docs: correct docs/AGENTS.md against its review
Its own cited evidence was partly wrong: the table "model" claimed rows linked that did not (now true, after the previous commit), the opening row counts were off, the chancellor incident predates this PR (0ab19fd), and "the fork is reported either way" was cited as true of the dotted field when the review showed it is not. The recipe/pin rule now states the line this PR actually drew (a claim the prose makes earns a doctest), the row threshold is per description cell, and Verifying says what the exit code hides: an unresolved :ref: or title link prints a WARNING and exits 0, and CI's HTML step has no -W. Adds three lessons from the review round: splitting breaks claims as merging does, check that nothing was dropped, and spell examples so they type-check; and says how to find the code behind a code claim (the stage Reads: lines). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4bde4b1 commit 127aa83

1 file changed

Lines changed: 14 additions & 8 deletions

File tree

‎docs/AGENTS.md‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
This file covers the Sphinx user documentation: `usage.rst`, `customize.rst`, `locales.rst`, `migrate.rst`, `concepts.rst` and the rest of `docs/*.rst`. `docs/design/` is not user documentation and has its own `docs/design/AGENTS.md`; the mechanics of doctests (shared namespace, optional extras, which doctest runner to trust) are in the root `AGENTS.md` under Gotchas → Doctests, and are not repeated here.
44

5-
The rules below were distilled from PR #588, which restructured `customize.rst` after its Policy table rows had grown to 25–35 lines each. Every rule names the failure it prevents, because each of those failures happened.
5+
The rules below were distilled from PR #588, which restructured `customize.rst` after three of its Policy table rows had grown to 24–38 source lines each (measured on master, 2026-10-02), and from the review rounds on that PR. Two rules carry lessons from earlier docs work, cited where they appear. Every rule names the failure it prevents, because each of those failures happened.
66

77
## Structure
88

9-
**A table is an index.** A row states what the field does and its default, in a sentence or two, and links to a section with `:ref:`. Detail, boundary cases and examples go in the section. A row past about five lines is the signal that it has stopped being an index. Rows grow by accretion — each fix adds its boundary case where the field is already described — so when you have a new case to document, add it to the section, never to the row. `customize.rst`'s Policy table is the model: its longer-reaching fields (`name_order`, the delimiters, the `unlisted_*` pair) are short rows that link to sections, while a field that needs no more than its row (`middle_as_family`) has no section at all.
9+
**A table is an index.** A row states what the field does and its default, in a sentence or two, and links to a section with `:ref:`. Detail, boundary cases and examples go in the section. A description cell past about five lines is the signal that the row has stopped being an index. Rows grow by accretion — each fix adds its boundary case where the field is already described — so when you have a new case to document, add it to the section, never to the row. `customize.rst`'s Policy table is the model: every row whose field has a section links to it, and a field that needs no more than its row (`middle_as_family`) has no section at all. A row that has a section and does not link to it is a dead end — #588's first pass found five.
1010

1111
**One topic per paragraph; subheadings for a section with several.** A paragraph that answers more than one question gets split, and a section holding more than one task gets subheadings named for what the reader came to do ("Routing a pair to maiden names", "Teaching the splitter a surname"), not for the mechanism. Keep an existing section title when you add subheadings under it: other pages link to titles (`` `Words that are also ordinary names`_ ``), and renaming breaks them.
1212

@@ -20,21 +20,27 @@ The rules below were distilled from PR #588, which restructured `customize.rst`
2020

2121
## Examples
2222

23-
**A recipe is a doctest; an edge case is a unit test.** A *recipe* — how to do the thing the reader came to do — belongs in the docs as a `.. doctest::` block, which teaches and is CI-checked at once. An example that only *pins* edge-case behavior teaches nothing and costs maintenance; state the behavior in one sentence and pin it in a unit test. An example sitting as prose inside a table cell is the worst of both: it teaches from the wrong place and nothing checks it.
23+
**A recipe is a doctest; an edge case is a unit test.** A *recipe* — how to do the thing the reader came to do — belongs in the docs as a `.. doctest::` block, which teaches and is CI-checked at once. The line between the two is whether the prose states the behavior: a claim the section makes to the reader (`edd` repairs to `EDD`, not a mask) earns a doctest beside it, which is what keeps the claim true; an edge case the prose has no reason to mention earns one sentence at most and a unit test, never a doctest (PR #216). An example sitting as prose inside a table cell is the worst of both: it teaches from the wrong place and nothing checks it.
2424

25-
**An example must be non-vacuous.** Show the baseline before the customization, and check that they differ: an `add(titles={"chancellor"})` example once shipped where `chancellor` was already a default title, so the add was a no-op and the doctest passed demonstrating nothing. Prefer a word absent from the defaults for a teachable reason (`dean`, which is also a given name).
25+
**An example must be non-vacuous.** Show the baseline before the customization, and check that they differ: an `add(titles={"chancellor"})` example once shipped (fixed in 0ab19fdb) where `chancellor` was already a default title, so the add was a no-op and the doctest passed demonstrating nothing. Prefer a word absent from the defaults for a teachable reason (`dean`, which is also a given name).
2626

27-
**An example must run warning-free.** Readers paste examples, so a warning the doctest build prints is one every reader gets. Fix the example, not the build: a `Lexicon.empty()` used only to drop titles also dropped the Korean surnames and drew the segmenterless warning, and the fix was an example that empties only the field it is about. Run `uv run sphinx-build -q -b doctest docs <tmp>` and expect no output at all.
27+
**An example must run warning-free.** Readers paste examples, so a warning the doctest build prints is one every reader gets. Fix the example, not the build: a `Lexicon.empty()` used only to drop titles also dropped the Korean surnames and drew the segmenterless warning, and the fix was an example that empties only the field it is about. Run the doctest build under Verifying below and expect no output at all.
28+
29+
**Spell examples so they type-check.** nameparser ships `py.typed`, so readers' examples run under mypy: write `()` and `frozenset()` rather than `{}` and bare set literals for `Policy` fields (the root `AGENTS.md`'s validation bullet covers the same rule for messages).
2830

2931
**Give doctest variables distinctive names** (`cred`, `caps_off`, `name_jr`), never `name`, `lex` or `policy` in a new block unless you mean to reuse the binding above — all blocks on a page share one namespace (root `AGENTS.md`, Gotchas → Doctests).
3032

3133
## Claims
3234

33-
**Measure every claim before you move it.** Restructuring is rewriting, and rewriting a claim is making one. Run each example and each stated behavior against the tree before the prose moves, not after. Combining sentences is where claims break: two rows each true of their own field become one intro that sounds true of both. #588's first draft did this twice — "the fork is reported either way" was true of dotted acronyms and false of all-caps words (`Smith, XYZ` reports nothing), and a comparison to the listed acronym `MA` was wrong (`Jack MA` is a suffix, `Jack X.Y.Z.` is not).
35+
**Measure every claim before you move it.** Restructuring is rewriting, and rewriting a claim is making one. Run each example and each stated behavior against the tree before the prose moves, not after. Combining sentences is where claims break: two rows each true of their own field become one intro that sounds true of both. #588's first draft did this twice — "the fork is reported either way" sounded true of both fields and was false of all-caps words (`Smith, XYZ` reports nothing), and a comparison to the listed acronym `MA` was wrong (`Jack MA` is a suffix, `Jack X.Y.Z.` is not). It was not even true of the dotted field it came from: a review found `John Smith, X.Y.Z.` takes the credential silently. Narrowing is the usual repair ("whatever pair encloses it" became "any configured pair": square brackets are not a default pair).
36+
37+
**Splitting breaks claims too.** Moving a sentence under one subheading makes it read as that subsection's alone. The rule for when the two-letter initials exception gives way landed under "Dotted acronyms" and stopped covering capitals, which it does (`John Smith, PhD MJ`).
38+
39+
**Check that nothing was dropped.** After a restructuring, list the distinct facts in each old passage (`git show master:docs/<file>`) and find each one in the new text — moved, reworded with the same meaning, or deliberately cut and said so in the commit. #588's review found version notes ("since 2.4") and two examples lost in the move.
3440

3541
**Check a claim end to end, through the call the reader makes.** "One `phd` key covers `Phd`" was true of the key match and false of `capitalized()`'s output, which leaves mixed case alone unless `force=True`. Verify against what the reader would observe, not an intermediate step.
3642

37-
**A claim about code needs the code.** "An empty `honorific_tails` stops the peel at its first guard" is checked by reading the guard, not by parsing a name.
43+
**A claim about code needs the code.** "An empty `honorific_tails` stops the peel at its first guard" is checked by reading the guard, not by parsing a name. Find it through the stage headers: each `nameparser/_pipeline/*.py` module declares the `Policy`/`Lexicon` fields it reads on a `Reads:` line, so grepping those for the field names the stage to read.
3844

3945
## Linking
4046

@@ -51,4 +57,4 @@ uv run sphinx-build -q -b doctest docs /tmp/doctest
5157
uv run sphinx-build -q -b html docs /tmp/html
5258
```
5359

54-
Both should print nothing. The HTML build reports an unresolved `:ref:` as a warning there, which is how a renamed heading or a missing label shows up.
60+
Both should print nothing. The HTML build prints an unresolved `:ref:` or an unknown title link (`` `Some heading`_ ``) as a WARNING and still exits 0, and CI's HTML step has no `-W`, so CI will not catch it either — reading the output is the only check. A doctest that emits a warning likewise prints and passes.

0 commit comments

Comments
 (0)