Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .claude/agents/design-docs-reviewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
name: design-docs-reviewer
description: Reviews changes to docs/design/ (rules.md, decisions.md, mechanisms.md, and AGENTS.md itself, which carries the conventions the other three rest on) for the failure modes that this doc system's own tests cannot catch — unreproducible measurements, rules that contradict the decision entries they rest on, statements that contradict their own examples, and prose edits that change what the doc parsers see. Use before merging any PR that touches docs/design/, and after distilling a design session into those files.
tools: Bash, Read, Grep, Glob
---

You review changes to `docs/design/` for a Python name-parsing library
whose design docs are partly machine-checked. Your job is the part the
machines cannot do.

**You report findings. You never edit files.** No fixes, no
suggestions phrased as edits — the calling session applies them.

## The axes

`docs/design/AGENTS.md` holds the review axes and the conventions they
rest on: what the suite already enforces — so a finding one of those
guards would catch is a false positive — the dated-count rule, and the
corpus-population caution.

**Read that file and run every axis it lists.** Work from the file,
never from a summary, including one you have seen before: the list
grows, and individual axes get broadened in place.

It is the only copy on purpose. This agent used to restate the axes
and drifted three ways against it — a missing axis, a narrowed one,
and a corpus count whose correction was recorded there and never
reached the copy here (#473,
decisions.md#review-agent-single-source). Do not reintroduce a
summary.

## How to run

1. `git diff master...HEAD -- docs/design/` for the scope. Also
`git diff HEAD -- docs/design/` for uncommitted work; review often
runs before the commit.
2. Read the changed rules and every decision entry they cite in full.
Not the diff hunks — the surrounding rule and entry, because the
rule-vs-examples, rule-vs-decision and precedence axes need
context the hunk does not carry.
3. Recompute. Use `uv run --frozen python - <<'EOF'` for measurement
scripts and `uv run --frozen pytest -q` for the suite. A bare
`python`/`pytest` picks up the wrong environment here.
4. State which tree you measured on — `git rev-parse --short HEAD` and
whether the tree is dirty. Stale measurements have produced rounds
of already-fixed findings in this repo.

## Reporting

Report each finding as:

<file>:<line> — <one-line claim>
Why it is wrong: <the contradiction, quoted from both sides>
Concrete consequence: <what a reader or a future PR does wrong>
Confidence: CONFIRMED (measured) | PLAUSIBLE (reasoned)

Rank most severe first. Quote both sides of a contradiction verbatim —
a paraphrase of a doc claim is not evidence about the doc.

If an axis found nothing, say so in one line. A silent axis is
indistinguishable from an unrun one, and this repo has a recurring
inert-measurement failure mode.

**Do not report:** style, wording preferences, "consider adding",
missing content you cannot name a consequence for, or anything the
suite already enforces — AGENTS.md names those guards, and a finding
one of them would catch is a false positive. An empty report is a
fine outcome and is more useful than a padded one.
33 changes: 33 additions & 0 deletions .claude/commands/docs-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
description: Review docs/design/ changes for the failure modes the doc tests cannot catch
argument-hint: "[base-ref, default master]"
allowed-tools: Bash, Read, Grep, Glob, Agent
---

Review this branch's changes to `docs/design/` using the
`design-docs-reviewer` agent.

Base ref: `${1:-master}`

1. Check there is something to review:
`git diff ${1:-master}...HEAD --stat -- docs/design/` plus
`git diff HEAD --stat -- docs/design/` for uncommitted work. If
both are empty, say so and stop — do not review an empty diff.
2. Confirm the suite is green first, so the agent can assume the
machine-checked layer holds: `uv run --frozen pytest -q`. If it
fails, report that and stop; a red suite falsifies the
machine-checked layer docs/design/AGENTS.md tells the agent to
assume, and it will waste effort rediscovering known breakage.
3. Dispatch the `design-docs-reviewer` agent with the base ref and
the changed-file list. Let it run its own axes — do not
pre-summarize the diff for it, and do not tell it what you think
the issues are. Its value is reading what is written rather than
what was meant, and a primed agent loses exactly that.
4. Relay its findings verbatim in your reply — the agent's report is
not shown to the user. Add your own assessment of each: agree,
disagree with reasoning, or need-more-information. You are not
obliged to accept a finding, but you are obliged to say why you
reject one.

Do not apply fixes as part of this command. Report, then let the user
decide what lands.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ venv/
env/

# tools
.claude/
.claude/*
!.claude/agents/
!.claude/commands/
.codex/
.gemini/
.pytest_cache/
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Three committed contributor docs carry the parser's normative rules and their re

**Release-log claims.** Quantified or universal behavior claims in release bullets must come from the differential gate's classified summary, be verified against rules.md examples, or -- for a view the gate cannot see -- carry a recompute recipe stored with the design entry the bullet cites; never write one from memory. The gate compares the seven role fields and `_ambiguities`, so `capitalized()`, `initials()` and any future render view are invisible to it (decisions.md#R4, #R3) and the first two sources cannot reach a claim about one: a gate run is byte-identical across the change, and an example line witnesses an output without counting anything. A recipe names the corpus files, the policy sweep, and -- the part that is easy to omit and fatal -- THE COMPARATOR, which must be something the shipped tree is not: #408's first recipe said to compare `initials()` against a folded-first partition, which is what `initials()` now IS, so it reproduced 0 where the bullet claimed 660 and was the only stated provenance for the number. Run the recipe as written before shipping the bullet. Cross-version numbers (a released wheel, the pre-change tree) are dated snapshots under Counting claims, since nothing in the repository re-runs them. Per-rule ledger toml comments asserting PARSER behavior cite rule IDs under the excerpt discipline; free prose is for ledger mechanics only (owned by tools/differential/README.md).

**Working on docs/design/ has its own AGENTS.md.** `docs/design/AGENTS.md` carries the landing-a-design distillation checklist, the primary-source review rule, the dated-count convention, and the ten review axes. Claude Code loads it automatically when a session reads or edits anything under docs/design/; if your tool does not do nested discovery, read it yourself before touching those files or reviewing a change to them.
**Working on docs/design/ has its own AGENTS.md.** `docs/design/AGENTS.md` carries the landing-a-design distillation checklist, the primary-source review rule, the dated-count convention, and the review axes. Claude Code loads it automatically when a session reads or edits anything under docs/design/; if your tool does not do nested discovery, read it yourself before touching those files or reviewing a change to them.

**Triaging a design backlog: shape before value.** When a pile of open questions has to be ordered, relatedness will not partition it — in a parser nearly everything touches particles or suffixes. The line that does is whether a decision changes WHAT THE MODEL CAN EXPRESS or fills in a value in a shape already fixed. Settle the shape questions first, as a batch: they are few, they are usually independent of each other (so their own ordering does not matter), and each one collapses or re-frames a run of the value questions below it. The rest are leaves needing one measurement and one answer apiece, in any order. Worked example, 2026-08-16 (#386): 30 open issues and ~13 open design questions reduced to three shape decisions, which decided or re-framed fourteen of them — and two of the three turned out smaller than their issues claimed, because the issues had gone stale. Check the age of an issue's premises before believing its cost estimate.

Expand Down
5 changes: 3 additions & 2 deletions docs/design/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ rules.md states intended parsing behavior and is NORMATIVE; decisions.md is the

Two shapes, needing different instruments. CROSS-SESSION distillation is what the rule above is written for — the #381 arc harvested nine named sessions, and for each the source transcript is an artifact independent of the reviewing session, so "review your own sections" is a two-artifact comparison. SAME-SESSION design work (#386) has no such artifact: spec and docs were written in one context, so the same instruction degenerates into re-reading the working memory that produced any error, and the rationale inverts — when the source is NOW, the source is what got it wrong. There, compare against artifacts instead, in this measured order of yield: re-derive every number with a freshly written script (2 wrong claims on #386); re-read the written spec against a checklist for FIDELITY, not just presence (1); interrogate NAMED contested rule pairs one at a time (2). Unaided prose reading found none — so a general "coherence read" by the author is the one form to distrust, because a green report from the weakest instrument manufactures confidence rather than supplying it.

**Reviewing a docs/design change: the ten axes.** Run these before merging anything that touches rules.md, decisions.md, mechanisms.md — or THIS FILE, which carries the conventions those three rest on and has the same failure modes. Axes 3, 5 and 6 are rules.md-specific (they need rule statements, examples and `interacts:`); the other seven apply to any of the four. Say which axes you ran and which you skipped as inapplicable — a checklist that asks for something meaningless teaches the reader to skim it. Each was earned by a defect that survived every other check on #386, where the review passes surfaced ten issues in total — nine of them by these axes, one by the spec self-review above — and unaided prose reading surfaced none. The parenthetical is the case that earned the axis.
**Reviewing a docs/design change: the eleven axes.** Run these before merging anything that touches rules.md, decisions.md, mechanisms.md — or THIS FILE, which carries the conventions those three rest on and has the same failure modes. Axes 3, 5 and 6 are rules.md-specific (they need rule statements, examples and `interacts:`); the other eight apply to any of the four — axis 11 included, since an invariant over two inputs is prompted by what a rule implies but is RUN without reading one, which is the whole of its value. Say which axes you ran and which you skipped as inapplicable — a checklist that asks for something meaningless teaches the reader to skim it. Axes 1–9 were each earned by a defect that survived every other check on #386, where the review passes surfaced ten issues in total — nine of them by these axes, one by the spec self-review above — and unaided prose reading surfaced none. Axes 10 and 11 were earned the same way on other arcs — #291's, which PREDATES this list, and #461/#466's, which follows it. The parenthetical is the case that earned the axis.

Skip what the suite already enforces. `tests/v2/test_rules_doc.py` and `tests/v2/test_doc_citations.py` machine-check example lines and their `deviates:` today-values, boundaries, the one-of `implemented:`/`tracked:` rule, `implemented:` against the modules that actually cite the rule, `interacts:` and cross-doc anchor IDs, citation excerpts quoted verbatim, and legacy citation forms. Assume they pass; a finding one of them would catch is a false positive. Read those two files rather than trusting this sentence — it named five of the nine guards when written, and goes stale as guards are added.

Expand All @@ -28,5 +28,6 @@ Skip what the suite already enforces. `tests/v2/test_rules_doc.py` and `tests/v2
8. **Guard docstring vs. what the guard enforces**, especially when the promise spans two test modules.
9. **Prose is input to the doc parsers.** A line starting with `"` inside a rule block is an example; a comment's quoted values join the citation block above them.
10. **Re-run every capability claim.** Axis 1 recomputes numbers; this one recomputes CAN and CANNOT. A statement that the parser never or cannot produce some reading is measured by a parse run written now, never derived from a mechanism — most of all when it justifies building something. (#291 derived "a multi-word credential is unparseable" from "vocabulary is matched one token at a time" — which `given_name_titles` had been an exception to since 2026-07-19, a month before that arc, and `maiden_markers` is a second since #434 — and specified a vocabulary set, a matching unit, a predicate and a `Lexicon` field on it; `parse("John Smith, MD PhD").suffix` had been `'MD PhD'` since 1.4.0.)
11. **State the invariant that relates two DIFFERENT inputs.** Axes 1–10 compare a document to another document, to its own examples, or to the implementation, so every one of them can only find code that DISAGREES with a rule — none of them can find a rule that is simply wrong. When a rule implies that two spellings of the same name must parse alike, write that as an invariant over both inputs and run it: it consults no rule statement, so it is not blind that way. Distinct from an invariant over two VIEWS of one parse, like `test_the_family_partitions_into_particles_and_base` in tests/v2/test_cases.py — that one holds however wrong the rule is. (Two review rounds a month apart found real defects and missed the premise under them: #461 was backed out after the rule turned out wrong, #466 closed unmerged for the same reason, and in both, every reviewer checked the code against the rule and THE RULE was at fault. The worked invariant: the family-comma writing under the default order and the comma-less writing under FAMILY_FIRST must parse alike. Measured, it holds for the great majority of the particle vocabulary and fails exactly on the words that are also suffix vocabulary — recovering P6's contested-membership set from behavior, having read no rule. Composition, measurement and recompute recipe: decisions.md#two-input-invariants.)

Also: before reporting "N names move", report the size of the population that COULD move — a small count over a corpus blind to the shape is evidence about the corpus. For rules.md#P6: of 782 corpus names, 245 carry a comma, two of those end in a particle, and one of the two clears the words-to-spare guard. Running axis 1 over this very list caught an earlier wording of that sentence conflating the population with the movers, so the count above is the corrected one.
Also: before reporting "N names move", report the size of the population that COULD move — a small count over a corpus blind to the shape is evidence about the corpus. For rules.md#P6 the population is the comma-bearing names that END in a particle — a small fraction of the comma-bearing names, which are themselves under a third of the corpus; decisions.md#P6 carries the dated measurement and the recompute. Do not restate a corpus count here. Axis 1 over this very sentence once caught it conflating the population with the movers, and the corrected figures that replaced it were superseded within the month as the corpus grew — a standing count in this file is wrong twice over, once for the class and once for the digits.
Loading