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
36 changes: 22 additions & 14 deletions AGENTS.md

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ You can also pass a name string to see how it will be parsed:

CI runs tests against every supported Python version (see the matrix in `.github/workflows/python-package.yml`) via GitHub Actions on every push and pull request.

Changing the Word Lists
-------------------------

The default vocabulary (titles, suffixes, particles and the rest) lives in
`nameparser/vocab/*.toml`, one file per language tag and one entry per word,
listing what the word can be read as. The parser reads a module compiled from
those files, so after editing one, rebuild it:

uv run python tools/vocab/build.py

The build checks the files and refuses, with a message, a mistake it can
detect: a typo in a kind or property, an unnormalized spelling, a reading a
word is recorded as refused. `tools/vocab/query.py` looks words up
(`--word`, `--kind`, `--issue`, `--excluded`).

Writing Tests
----------------

Expand Down
2 changes: 1 addition & 1 deletion docs/design/decisions/3-0-reevaluations.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Promoted 2026-08-15 from session memory (Derek's 2026-07-30 ask; promotion appro
- (A) Parking lot from the 2026-07-06 config-model discussion, 3.0-shaped by design: scoped config override via contextvars (`with nameparser.config.use(c):`, the decimal.localcontext pattern — fixes test pollution and config wars while keeping the shared default), and a requests-Session-style Parser(constants) API (rejected then as a full rewrite; the shared CONSTANTS survives 2.x because it is the only config channel that reaches parses inside code users don't own — #262's contract framing).
- (B) The 1.3.0-era legacy-pickle property-key skip (suffixes_prefixes_titles in old blobs) rides in the shim's __setstate__ and dies with it.
- (B) The FAMILY_COMMA doctrine (rule W3): inherited from v1's lastname-comma but correct on its own terms — an explicit comma is stronger evidence than script.
- (A) 2026-08-17 — PARTICLE VOCABULARY AS LOCALE PACKS. Raised while reversing the order-independence decision above: every argument in the leading-particle thread is a language judgement wearing a position heuristic. `de` and `do` lead surnames in Vietnamese, `von` is German, `dos` Portuguese, `das` both Portuguese and a borne Bengali surname — and the per-word comments in nameparser/config/particles.py cite exactly these traditions as their justification. Locale packs already exist (ru, tr_az, zh, ja) and already carry vocabulary and rotations, so particles are the obvious next tenant: a caller who knows the tradition could opt into a tuned set instead of the parser splitting the difference globally. Not 2.x work — it needs the packs to carry vocabulary OVERRIDES rather than additions, and it would give C-i's positional qualifier a per-locale answer rather than one global one.
- (A) 2026-08-17 — PARTICLE VOCABULARY AS LOCALE PACKS. Raised while reversing the order-independence decision above: every argument in the leading-particle thread is a language judgement wearing a position heuristic. `de` and `do` lead surnames in Vietnamese, `von` is German, `dos` Portuguese, `das` both Portuguese and a borne Bengali surname — and the per-word comments in nameparser/config/particles.py (reading notes in nameparser/vocab/und.toml since 2026-10-18) cite exactly these traditions as their justification. Locale packs already exist (ru, tr_az, zh, ja) and already carry vocabulary and rotations, so particles are the obvious next tenant: a caller who knows the tradition could opt into a tuned set instead of the parser splitting the difference globally. Not 2.x work — it needs the packs to carry vocabulary OVERRIDES rather than additions, and it would give C-i's positional qualifier a per-locale answer rather than one global one.
- (A) 2026-08-18 — LOCALE CONTROL OF P3's SINGLE-LETTER CARVE-OUT, and the sharper requirement it puts on the entry above: a pack would have to carry per-word POLICY, not just membership. Which single letters a tradition wants joined differs by language — Spanish writes "y" and would want "e" read as an initial, Portuguese wants the reverse, Dutch would want every single letter read as an initial. One global rule serves all three today: a single ALPHABETIC character in a name of fewer than four pieces stays a name word, and a bare Latin capital never joins (#383). It is v1's Google Code issue 11 fix ("john e smith") generalized to a vocabulary v1 did not have — the set now holds six single-letter conjunctions (y, e, и, і, й, و), and the carve-out reaches all of them because it counts characters, while the capital veto reaches only the Latin ones because it tests a Latin shape. That asymmetry is the visible seam and it is why #383 is open. A pack that could only ADD or REPLACE vocabulary could not express any of it; the join threshold and the initial veto would have to be per-word, per-pack values. Measured while documenting P3: "&" is single-character but not a letter, so it joins at any length ("Juan & Garcia" is one part while "Juan y Garcia" is three).
- (A) 2026-09-13 — SHOULD A LOCALE PACK BE ABLE TO REMOVE BASE VOCABULARY? `Locale.lexicon` is UNIONED onto the base and never removes — the field's own comment says so — which makes `conjunctions_ambiguous` (decisions/P3, 2026-09-13) expressible by a pack in ONE DIRECTION ONLY: an `nl` pack can add "y", and a `pt` pack cannot remove "e", so a Portuguese caller has to write `Parser(lexicon=Lexicon.default().remove(conjunctions_ambiguous={"e"}))` by hand. Accepted for 2.x and parked here rather than filed, because 3.0 is when a wider set of locale packs gets designed and this is part of that work (Derek, 2026-09-13). It is the 2026-08-17 PARTICLE VOCABULARY AS LOCALE PACKS bullet's "overrides rather than additions" requirement meeting a second tenant, which is what makes it a shape question rather than one pack's inconvenience.
- (A) 2026-09-13 — THE DIFFERENTIAL HARNESS CANNOT SEE A LOCALE PACK'S EFFECT AT ALL. `compare.py` builds one `Parser` per name ORDER and nothing else, and a `Case.locale` row projects into the corpora as a bare name string — no corpus row carries a locale key, verified over every `tools/differential/corpus*.jsonl` file on 2026-09-13 — so every corpus name is parsed under the DEFAULT `Lexicon` on both sides. A pack's effect on a name can therefore never diff, at any baseline. That is fine while packs only ADD vocabulary the default already carries most of, and stops being fine the moment the bullet above is answered yes. The shape of the fix is a `locale` carried per corpus ENTRY the way `shape` already carries an order, resolved at the baselines that ship the pack and skipped below them. Parked with the bullet above rather than filed, for the same reason: the two are one piece of work (Derek, 2026-09-13).
Expand Down
2 changes: 1 addition & 1 deletion docs/design/decisions/P3-connectives.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
- 2026-09-15 #289/#516 — AMENDS the out-of-scope list above: #289 and #516 are no longer out of scope, having shipped in 2.4 on the fact this entry's own fork computes. The fact is now a `ParseState` field rather than a classify local, taken over the SAME own-words span this entry defines, and `_pieces.own_words` is where that span lives for both readers. #492 stays out of scope and stays deferred. See decisions/S6.
- 2026-09-23 #492/#478 — AMENDS the out-of-scope list above again: both have landed, in RENDER only. #492: R5's gate leaves the suffixes out (decisions/R5, 2026-09-23); this entry's fork is untouched and still counts a cased suffix, which is the R5/parser split that entry records, and render still does not import `is_one_case`. #478: a connective part inside a hyphenated word keeps its lowercase (decisions/R4, 2026-09-23); the spaced-form claim it rested on is unchanged, and still depends on `y` staying out of the subset.

- 2026-09-13 #383/#479 — `conjunctions_ambiguous` is deliberately NOT registered in `_SUBSET_FIELDS`, so an orphan marker entry raises nothing. An orphan there is INERT rather than harmful: the classify fork tests membership in `conjunctions` before it reads the subset, and the emitter tests it again, so a marker entry whose base word is gone is never consulted by either. AGENTS.md's invariants rule guards harm, not no-ops, and the precedent is this file's #given-name-titles Declined entry, where two attempts at a check each cost a working configuration to forbid a condition that costs nothing. What holds the SHIPPED constant to the subset relation is an import-time assert in `nameparser/config/conjunctions.py` (which also holds it to cased single letters, the only entries the fork can read), and the v1 shim's intersection, which is a provable no-op. So `remove(conjunctions={"e"})` simply works, leaving a stale marker entry behind that does nothing. Pinned by `test_removing_a_conjunction_leaves_its_ambiguous_marker_alone` in tests/v2/test_lexicon.py and by the orphan behavioral pin in tests/v2/pipeline/test_classify.py, both of which cite this entry.
- 2026-09-13 #383/#479 — `conjunctions_ambiguous` is deliberately NOT registered in `_SUBSET_FIELDS`, so an orphan marker entry raises nothing. An orphan there is INERT rather than harmful: the classify fork tests membership in `conjunctions` before it reads the subset, and the emitter tests it again, so a marker entry whose base word is gone is never consulted by either. AGENTS.md's invariants rule guards harm, not no-ops, and the precedent is this file's #given-name-titles Declined entry, where two attempts at a check each cost a working configuration to forbid a condition that costs nothing. What holds the SHIPPED constant to the subset relation is an import-time assert in `nameparser/config/conjunctions.py` (which also holds it to cased single letters, the only entries the fork can read; since 2026-10-18 the subset holds by construction and the letter check is a vocabulary build check, decisions/vocabulary-model), and the v1 shim's intersection, which is a provable no-op. So `remove(conjunctions={"e"})` simply works, leaving a stale marker entry behind that does nothing. Pinned by `test_removing_a_conjunction_leaves_its_ambiguous_marker_alone` in tests/v2/test_lexicon.py and by the orphan behavioral pin in tests/v2/pipeline/test_classify.py, both of which cite this entry.

- 2026-09-13 #383/#479 — AMENDS the 2026-08-27 #445 entry under M4 below, which calls `initial` "the `initial` shape tag": since this change the tag is no longer purely a shape claim, because a marked single-letter connective in a one-case name carries it without being initial-SHAPED. rules/M4's statement now says "read as an initial" for that reason, and M4's carve-out itself is unchanged — it still reads the tag classify recorded rather than a predicate of its own.

Expand Down
Loading
Loading