Skip to content

Finish v2.2.0: trim the release notes, close #433 and #356, sweep the docs - #477

Merged
derek73 merged 10 commits into
masterfrom
claude/v2-2-0-release-finalization-de07b4
Sep 1, 2026
Merged

Finish v2.2.0: trim the release notes, close #433 and #356, sweep the docs#477
derek73 merged 10 commits into
masterfrom
claude/v2-2-0-release-finalization-de07b4

Conversation

@derek73

@derek73 derek73 commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Closes the last two open v2.2 issues, cuts the release notes down to what a caller needs to decide, and runs the release checklist's staleness sweep.

The release notes

The 2.2.0 entry had grown to 12,442 words against 2,222 for 2.1.0 and 2,931 for 2.0.0 — 5.6x the previous minor across fewer bullets, so the bloat was entirely per-bullet (150–771 words each, against 50–150 in 2.1.0). Bullets had become mini design documents: blast radius by baseline, per-field redistribution, gate-blindness disclaimers, recompute recipes, review history.

All of that already lives in docs/design/decisions.md, in more detail — R3 carries #408's parity counts, recompute recipe and gate-blindness note; parse-cost carries the +67 calls and the per-PR attribution; P2 carries #367's grouping argument. So this was deletion plus a pointer, not a migration. Pointers name the entry (the `R3` entry of docs/design/decisions.md), never a line number, following the four bullets that already did.

Each bullet keeps: how a name parsed before, what it does now, the limits a caller would trip over, the 1.4.0 direction, and the issue — which carries the full measurement. 12,442 → 4,927 words.

Closes #433

customize.rst and rules.md's suffix Background both said, truly, that a multi-word vocabulary entry can never match — and neither said the next thing, whose absence is what does the damage: adjacent suffix tokens are reassembled after matching, so a multi-word credential parses as its component words. parse("John Smith, MD PhD").suffix has been 'MD PhD' since 1.4.0.

The inference from "a multi-word ENTRY is inert" to "a multi-word CREDENTIAL is unparseable" survived #291, an approved design spec and a full implementation plan specifying a new vocabulary set, matching unit, predicate and Lexicon field. None of them ran the parse.

Both examples verified non-vacuous: on the default vocabulary "John Smith, LEED AP" gives suffix '' and family 'John Smith'.

Closes #356

Six live items from PR #354's five-agent review, none touching parse behaviour:

  • __all__ on the two migration destinations. The 1.x shims got one in Rename the vocabulary data modules to the 2.0 terminology, with a 2.x bridge (#293) #354; particles and bound_given_names did not, so from nameparser.config.particles import * bound assert_normalized and BOUND_GIVEN_NAMES — the latter owned by another module and imported only for a disjointness assert — as though they were vocabulary. This is the one caller-visible change in the PR and it has a release-log bullet.
  • __dir__ no longer raises KeyError. It read sys.modules[module], so dir() on a module dropped from sys.modules raised — not among the things dir() may do to a caller. It degrades to the closure's aliases.
  • ALIASES is a NamedTuple (for m, n, _, _ in ALIASES appeared six times and reads correctly only if you remember which end is which; _LatinCopy is the precedent), _RETIRED_NAMES is derived from it rather than hand-written a second time in the same file — verified identical to the five rows it replaces.
  • The star-import test asserts values, not only names, and the from-import attribution test runs over all five aliases rather than the one prefixes row — the two that kept their module are a different shape, their table sitting at the bottom of a module mid-execution during its own import.

Both fixes mutation-checked rather than merely covered: routing prefixes.PREFIXES at NON_GIVEN_NAME_PARTICLES fails the new value assertion while the pre-existing name assertion passes it (the bound set is identical either way); restoring the sys.modules subscript fails the new dir() test on all four shims.

Six further items were already fixed since the issue was filed — verified rather than assumed, and listed in the commit message.

The docs sweep

Three stale behavior claims, two of them broken by #296 in this same cycle — which is the shape the sweep exists for, since prose describes behavior in words and grepping the changed symbol finds none of it.

site claim measured
concepts.rst Dr. is "a title before a name and a suffix after it" "John Smith Dr." → family 'Dr.'
usage.rst post-comma PhD lands in title "田中さん, PhD" → suffix 'さん, PhD'
_group.py Reads: "Policy.extra_suffix_delimiters — no other Policy field" reads policy.lenient_comma_suffixes

concepts.rst's was the worked example for why the fields are really pre- and post-nominal; replaced with Lt., one of the twelve deliberate duals #296 kept. _group.py's undeclared read arrived with the #430/#432 review round (986e9d6) and the header was never updated — a false universal in a Reads: block is worse than an omission, being the thing the next person checks against instead of the code.

Sites verified accurate and left alone are listed in that commit, so the sweep is on record as having covered them.

Verification

  • 6,196 tests pass, 156 skipped, 9 xfailed
  • mypy: no issues in 109 source files; ruff: clean
  • sphinx-build -b html clean; -b doctest 108 doctests, 0 failures
  • Differential gate exits 0 at all three baselines, unexplained: 0 — 1104 names, 226 / 198 / 106 intentional diffs at 1.4.0 / 2.0.0 / 2.1.0
  • No open Dependabot PRs on uv.lock

🤖 Generated with Claude Code

derek73 and others added 4 commits August 31, 2026 15:20
The 2.2.0 section had grown to 12,442 words against 2,222 for 2.1.0 and
2,931 for 2.0.0 -- 5.6x the previous minor across fewer bullets, so the
bloat was entirely per-bullet (150-771 words each, against 50-150 in
2.1.0). Bullets had become mini design documents: blast radius by
baseline, per-field redistribution, gate-blindness disclaimers, recompute
recipes, and review history.

All of that already lives in docs/design/decisions.md, in more detail --
R3 carries #408's parity counts, recompute recipe and the gate-blindness
note; parse-cost carries the +67 calls and the per-PR attribution; P2
carries #367's grouping argument. So this is deletion plus a pointer, not
a migration. Pointers name the entry ("the R3 entry of
docs/design/decisions.md"), never a line number, following the four
bullets that already did.

What each bullet keeps: how a name parsed before, what it does now, the
limits a caller would trip over, the 1.4.0 direction, and the issue --
which carries the full measurement. 12,442 -> 4,927 words.

Also replaces the roz restoration recipe's dataclasses.replace spelling
with Lexicon.default().add(maiden_markers={"roz"}), the idiom
customize.rst documents; verified to restore the reading exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y is not

customize.rst and rules.md's suffix Background both said, truly, that a
multi-word vocabulary entry can never match -- and neither said the next
thing, whose absence is what does the damage: adjacent suffix tokens are
reassembled after matching (_vocab.is_wholly_suffix), so a multi-word
credential parses as its component words. parse("John Smith, MD PhD")
has read suffix 'MD PhD' since 1.4.0.

The inference from "a multi-word ENTRY is inert" to "a multi-word
CREDENTIAL is unparseable" survived #291, an approved design spec and a
full implementation plan specifying a new vocabulary set, matching unit,
predicate and Lexicon field. None of them ran the parse. No shipped doc
ever stated the false claim -- a true limitation sitting next to an
unstated capability is the shape that produced it, so stating the
capability is the fix.

The recipe is a teaching example rather than a pinning one, so it earns
its doctest under the recipe carve-out. Both examples verified
non-vacuous: on the default vocabulary "John Smith, LEED AP" gives
suffix '' and family 'John Smith'.

Closes #433

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…354

Six items from PR #354's five-agent review, none touching behaviour.

The two migration DESTINATIONS never got the __all__ their 1.x shims
gained in that PR, so `from nameparser.config.particles import *` bound
`assert_normalized` and BOUND_GIVEN_NAMES -- the latter owned by another
module and imported only for a disjointness assert -- as though they
were vocabulary. Both now declare one, in source order for autodoc, per
the note in suffixes.py.

alias_getattr's __dir__ read `sys.modules[module]`, so dir() on a module
dropped from sys.modules raised KeyError, which is not among the things
dir() may do to a caller. It degrades to the closure's aliases instead.

In the test file: ALIASES becomes a NamedTuple (six `for m, n, _, _ in
ALIASES` sites read correctly only if you remember which end is which;
_LatinCopy in test_ledger_guards.py is the precedent); _RETIRED_NAMES is
derived from it rather than hand-written a second time in the same file,
verified identical to the five rows it replaces;
test_star_import_binds_exactly_the_live_and_retired_names asserts VALUES
as well as names; and the from-import attribution test runs over all
five aliases rather than the one prefixes row -- the two that KEPT their
module are a different shape, their table sitting at the bottom of a
module mid-execution during its own import.

Both fixes mutation-checked rather than merely covered. Routing
prefixes.PREFIXES at NON_GIVEN_NAME_PARTICLES fails the new value
assertion while the pre-existing name assertion passes it, the bound set
being identical either way; restoring the sys.modules subscript fails
the new dir() test on all four shims.

Already fixed since the issue was filed, verified rather than assumed:
customize.rst names REGEXES/CAPITALIZATION_EXCEPTIONS as the freeze
exceptions and says TITLES was not renamed; AGENTS.md carries the
measured esq claim, the 2.0 note on the delimiter sentinel, and the
per-read-location warning semantics; and no gitignored-spec citation
survives in the package (test_doc_citations enforces it). AGENTS.md's
join line sits under the v1 parser walkthrough, where
join_on_conjunctions and lastname_pieces are v1's own identifiers.

Closes #356

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The release-checklist sweep (AGENTS.md step 0), run against what 2.2
itself changed. Two of the three were broken by #296 in this same
cycle, which is the shape the sweep exists for: prose describes
behavior in words, so grepping the changed SYMBOL finds none of it.

concepts.rst said `Dr.` is "a title before a name and a suffix after
it", the worked example for why the fields are really pre- and
post-nominal. #296 took `dr` out of the suffix vocabulary, so
"John Smith Dr." gives family 'Dr.' (measured). Replaced with `Lt.`,
one of the twelve deliberate duals that audit kept: "Lt. Smith" is
title 'Lt.' and "John Smith LT" is suffix 'LT'.

usage.rst's CJK credentials section said the post-comma credential
lands in `title` for `PhD`. #296 took `phd` out of TITLES: measured,
"田中さん, PhD" gives suffix 'さん, PhD'. Three landings collapse to two,
so the sentence says so -- `given` for `V.`, `PhD` and `Ph. D.` in
`suffix`.

_group.py's stage header declared Policy.extra_suffix_delimiters "-- no
other Policy field" while reading policy.lenient_comma_suffixes at the
segment_suffix_reading call. That read arrived with the #430/#432
review round (986e9d6) and the header was never updated. A false
universal in a Reads: block is worse than an omission: it is the thing
the next person checks against instead of the code.

Verified accurate and left alone, so the sweep is on record as having
covered them: modules.rst documents every live config module and
correctly omits the two retired shims; every Policy field appears in
customize.rst; the maiden and maiden_delimiters docs already carry the
#335 behavior; migrate.rst's 2.2 rename section is current including
the per-read-location warning semantics; concepts.rst's eight-token and
Span(0,3) example, its STABLE_TAGS list, its nickname_delimiters,
Ma-comma and "Joao da Silva do Amaral de Souza" claims, and
usage.rst's "John Smith PhD MD" -> 'PhD, MD' all measure as written.
No open Dependabot PRs on uv.lock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@derek73 derek73 added docs Documentation fixes and updates breaking-change Backwards-incompatible API change tests labels Sep 1, 2026
@derek73 derek73 self-assigned this Sep 1, 2026
@derek73 derek73 added this to the v2.2 milestone Sep 1, 2026
@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.66%. Comparing base (865e6bf) to head (fe1bccb).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #477   +/-   ##
=======================================
  Coverage   98.66%   98.66%           
=======================================
  Files          45       45           
  Lines        3214     3218    +4     
=======================================
+ Hits         3171     3175    +4     
  Misses         43       43           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@derek73 derek73 removed the breaking-change Backwards-incompatible API change label Sep 1, 2026
derek73 and others added 5 commits August 31, 2026 19:28
Derek's call. Moved out of Breaking Changes to the end of Behavior
Changes, and the bullet now says outright that no parse changes. What
`__all__` removed from `from nameparser.config.particles import *` is
`assert_normalized` and another module's BOUND_GIVEN_NAMES -- names
nobody imports these modules for, bound only because the destinations
never got the `__all__` their 1.x shims did. Backwards-incompatible in
the letter, not in anything a caller wrote.

The PR's breaking-change label came off with it; the two were one
classification and had to move together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Derek's ask: make the log scannable, so a reader can decide from the
lead whether a bullet is worth reading. The existing first sentences
were already written as identifiers, so they are what got bolded.

Applied to bullets of 25 words or more, which scopes it by itself:
2.2.0, 2.1.0 and 2.0.0 come out near-fully bolded while the 1.x history
is mostly untouched, its bullets being one-liners where the whole bullet
IS the identifier and bolding it would defeat the purpose. 134 leads in
all. The list-table rows fall below the threshold and are left alone.

INLINE MARKUP DOES NOT NEST IN RST, and it fails silently -- measured
through docutils rather than assumed: a ``literal``, a :role:`target`,
an *emphasis* or a `link <url>`_ inside ** ** renders its own
punctuation verbatim, with NO warning, so the page would have shipped
visible double backticks. Every lead is reduced to plain text before
being wrapped; a role keeps its last dotted segment
(:data:`~...BOUND_GIVEN_NAMES` -> BOUND_GIVEN_NAMES). The identifiers
appear again in each bullet's body with their markup intact.

Two bullets whose first sentence ran past the limit (#467, #360) were
split by hand rather than left unbolded, preserving every fact.

Also fixes two bold call-outs written earlier in this same branch that
had literals inside them and carried the same silent defect.

Verified on the RENDERED HTML, not the source: zero <strong> spans
contain a stray backtick or asterisk across all 181 of them, and
``add(**entries)`` still renders correctly -- ** inside an inline
literal is plain text to RST, which is why the source-level check
reports it and the build does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Derek caught the 1.3.0 bullets ending mid-parenthesis:

  **Add middle_name_as_last flag ... with no middle-name concept (e.g.**
   Arabic patronymic chaining) (#133)

The splitter masked ``literals`` before looking for a sentence end but
knew nothing about plain-text abbreviations, so "e.g." read as one.
Fourteen leads were truncated, every one of them in 1.3.x, where "(e.g.
X)" is the house idiom for an example.

Fixed at the cause rather than in the fourteen places: the splitter now
walks EVERY candidate cut in order and takes the first VALID one, where
valid means the lead does not end in a known abbreviation and does not
stop inside an unpaired bracket or quote. Where no cut qualifies the
bullet is left unbolded, which is the right answer for these: each is a
single sentence, so its "lead" would have been the whole bullet.

123 leads rather than 134; the 11 that dropped out are exactly the ones
that were being truncated. Better plain than cut off mid-parenthesis.

Verified on the rendered HTML across all three failure classes, not just
the one reported -- of 170 <strong> spans, zero contain stray markup,
zero end in an abbreviation, zero hold an unbalanced bracket or quote.
The first two of those checks did not exist before this commit, which is
why the defect shipped: I had checked the nesting class and assumed the
splitting was right.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Derek's read, and the specific cuts are his:

- the z-domu migration call-out ("if you stored z and domu separately,
  remove both") -- nobody did that, so it is advice to no one
- "a clause holding nothing but the marker stays a nickname"
- the roz lead's reason clause ("where it had been eating English given
  names") -- the removal is the point
- "what stops working is the abbreviation itself, for Czech and Slovak
  callers", with its worked example and 1.4.0 comparison

And the style problem, which was real: "What does NOT move: a lone
marker in brackets is just a word ..." promises a list of things that do
not move and then delivers a statement about what something IS. Four
long sentences to establish what "Jane Smith (née)" does, which is not
worth the reading. Replaced by the one clause a caller acts on -- a
markerless clause is still a nickname, which is what maiden_delimiters
remains for.

Then the same standard applied across the section, 32 cuts in all. The
classes: a non-change nobody would have assumed otherwise ("Names
without the comma are untouched", "Jack Mc Donald is byte-identical", "a
conjunction written as a word of its own is untouched"); an edge case
too narrow to earn its sentence (the trailing bare marker, "Smith née
Jones PhD"); rationale the reader does not need to act ("it was never a
reliable way to change a default ..."); corpus and vocabulary trivia
("eight more corpus names", "membership goes from 28 words to 33"); and
CI forensics that belong in the decisions entry the bullet already
cites. 4927 -> 4456 words.

Every cut is a deletion of a whole sentence or clause; the 21 in-place
rewordings were diffed word by word against the intended list, and no
fact left the file that was not deliberately dropped.

ONE SELF-INFLICTED SCARE, recorded because the check is the lesson: a
blanket ' +\.' -> '.' tidy meant to clean up after the deletions ate the
space in `from ... import` (x3) and in two deliberate examples, `"John .
Smith"` and `"J. . V."` -- an ellipsis and a spaced initial both carry a
legitimate space before a period. Caught by diffing every changed line
word-for-word rather than by reading the result. Reverted and redone
with the tidy narrowed to collapsing double spaces.

Verified: 169 bold leads with zero stray markup, zero abbreviation
truncations, zero unbalanced brackets; sphinx html and doctest clean;
6196 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`juan e-f smith` was a constructed shape -- `e-f` is nobody's name. It
is now `Ortega-y-Gasset`, which is a hyphenated last name (Derek's ask),
a real one, and routinely hyphenated in library catalogues.

Measured on the pre-#458 tree (git archive of 805bcf8^ into a scratch
checkout, so nothing in this worktree moved):

  'jose ortega-y-gasset'  ->  'Jose Ortega-y-Gasset'
  'JOSE ORTEGA-Y-GASSET'  ->  'Jose Ortega-Y-Gasset'

and both give 'Jose Ortega-Y-Gasset' here -- the same disagreement `e-f`
showed, on a name someone actually has.

WHY IT HAS TO BE THIS KIND OF HYPHENATED NAME, which is worth stating
because the obvious simplification does not work: an ORDINARY
hyphenated surname cannot witness this at all. Measured across both
trees, 'bonham-carter', 'lloyd-webber', 'day-lewis', 'marie-smith' and
'jean-luc' are byte-identical before and after, in either case. The
defect was case repair re-deciding conjunction-or-initial per word of a
token's text, so the token has to CONTAIN a conjunction for there to be
anything to get wrong. `y` is what makes the example work.

The spaced form is now named in the bullet as the contrast, because a
reader seeing 'Ortega-Y-Gasset' will ask: 'jose ortega y gasset' still
repairs to 'Jose Ortega y Gasset', the conventional Spanish rendering,
since there `y` is a token of its own and IS the conjunction.

The claim is gate-invisible (decisions.md#R4 -- compare.py never calls
capitalized()), so it is pinned by a unit test rather than left to
prose. Mutation-checked: removing the UNCLASSIFIED_TAG gate in
_render.py, which is the pre-#458 behaviour, fails both the new test and
the existing `e-f` one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Derek on the old opener: "finishes the 2.0 rename at the layer it never
reached" -- which layer, and why not just say so? The next sentence
answered the riddle, so the riddle bought nothing. And a Lexicon that
"has spoken of" particles is odd personification; it has USED them.

Rewritten to lead with what the release IS, and the clusters are
measured rather than asserted. Across the 37 bullets: surname particles
11, maiden-name clauses 7, Arabic bound given names 6, credentials after
a comma 6, the rename 3, render views 2, tooling and docs 2. Behavior
Changes holds 34 bullets, of which #475 and #356 are not parsing fixes,
so "about thirty" is 32.

Two things the count corrected in the framing we started from:
name_order is the primary driver of FOUR bullets (#359, #467, #395,
#355), not of most of the release -- but all four sit inside the particle
thread, which IS the largest, so the story holds even though the
mechanism is narrower than it felt. And name_order is 2.0's, not 2.1's;
2.1 added script_orders, the East-Asian script-driven one. What is new
here is what DECLARING it is evidence for on Latin-script names, which
is the one thing in the release a reader cannot reconstruct from the
bullets, so it is the sentence that stayed.

The constants are now named in the text (PREFIXES, BOUND_FIRST_NAMES,
FIRST_NAME_TITLES) instead of the layer being alluded to, and the
gate-is-the-source-of-record sentence went with the rest of the fluff --
each bullet already cites the issue that carries its measurement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@derek73
derek73 merged commit 6f9159d into master Sep 1, 2026
11 checks passed
@derek73
derek73 deleted the claude/v2-2-0-release-finalization-de07b4 branch September 1, 2026 05:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation fixes and updates tests

Projects

None yet

1 participant