Finish v2.2.0: trim the release notes, close #433 and #356, sweep the docs - #477
Merged
Conversation
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>
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 —R3carries #408's parity counts, recompute recipe and gate-blindness note;parse-costcarries the +67 calls and the per-PR attribution;P2carries #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.rstandrules.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").suffixhas 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
Lexiconfield. 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;particlesandbound_given_namesdid not, sofrom nameparser.config.particles import *boundassert_normalizedandBOUND_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 raisesKeyError. It readsys.modules[module], sodir()on a module dropped fromsys.modulesraised — not among the thingsdir()may do to a caller. It degrades to the closure's aliases.ALIASESis aNamedTuple(for m, n, _, _ in ALIASESappeared six times and reads correctly only if you remember which end is which;_LatinCopyis the precedent),_RETIRED_NAMESis derived from it rather than hand-written a second time in the same file — verified identical to the five rows it replaces.prefixesrow — 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.PREFIXESatNON_GIVEN_NAME_PARTICLESfails the new value assertion while the pre-existing name assertion passes it (the bound set is identical either way); restoring thesys.modulessubscript fails the newdir()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.
concepts.rstDr.is "a title before a name and a suffix after it""John Smith Dr."→ family'Dr.'usage.rstPhDlands intitle"田中さん, PhD"→ suffix'さん, PhD'_group.pyPolicy.extra_suffix_delimiters— no other Policy field"policy.lenient_comma_suffixesconcepts.rst's was the worked example for why the fields are really pre- and post-nominal; replaced withLt., 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 aReads: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
mypy: no issues in 109 source files;ruff: cleansphinx-build -b htmlclean;-b doctest108 doctests, 0 failuresunexplained: 0— 1104 names, 226 / 198 / 106 intentional diffs at 1.4.0 / 2.0.0 / 2.1.0uv.lock🤖 Generated with Claude Code