You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(mechanisms,cases,customize,differential): the fork criterion, measured
The VOCABULARY-EXERCISES-FORKS entry named four forks for
maiden_markers. Measured, the set has none: it is read at exactly two
sites, both plain whole-token membership after _normalize, and all
sixteen members collapse to one structural class across bare,
bracketed and lone-bracketed shapes with the marker text normalised
out. Two of the four named forks were not parser behavior at all --
the substring split is _carries, a test guard that deliberately asks
wider, and edge-period normalization is _normalize, which discriminates
spellings across every vocabulary rather than members of this one.
That makes the entry's argument stronger, and it now says so: rows on
this set earn their place on clause SHAPE, never on which member
appeared. The unit is the INPUT, matching cases.py's wording rather
than the entry's old "one member and not another"; a row plainly earns
its place with no member fork behind it.
Three repairs beyond that. The delimiter enumeration claimed
exhaustiveness and missed WORD_INTERNAL_DELIMITERS, which splits the
same-character class in two -- 'Jane Smith"' reports
UNBALANCED_DELIMITER and "Jane Smith'" reports nothing -- while
listing ASCII against fullwidth, which is not a fork: swapping the
pair across all 45 corpus names holding a paren changes no field and
no ambiguity kind. The entry blurred the line Derek drew, so it states
it: a delimiter pair the library SHIPS is caller-visible behavior and
deleting one reddens three tests, while vocabulary a caller configures
is data. And a row may earn its place a second way, with no fork
behind it, by pinning a name a release note advertises by name.
Its counts are gone rather than corrected. They were present-tense
prose over a drifting wordlist, which root AGENTS.md forbids, this PR
moved 17 to 16 by removing roz, and the descriptive figure was wrong
anyway -- five members carry rows, so eleven have none.
Two merge blockers. A cases.py note carried "leaves the entire suite
green" beside the correcting sentence that says the suite catches it;
the previous commit appended the truth and left the falsehood
standing. And neither recompute one-liner ran as pasted: both bodies
were indented inside the string handed to python -c, so both raised
IndentationError. I had run them flush-left in my own shell, which is
the inert-measurement shape they exist to prevent. Both are dedented,
carry a note saying why, and were re-run by reconstructing them from
the source and pasting.
The #445 marker went on the bracketed rules only. The bare spelling is
claimed at 1.4.0 by fix(#274), which also declares `family`, so a
"keep the family" fix would land inside it with the gate green -- the
same absorption on the same field at the same baseline. Marked there
too, with the names to re-measure listed.
Also: three ledger regexes gained (?i) in the previous commit without
being mentioned, a real widening (the class now matches NEE and NÉE)
bounded by _Claim's digest; customize.rst said suffix-shaped content
"is read as a suffix" when S1 drops the brackets and parses the
content as if written bare; _policy.py's autodoc'd docstring never got
that clarification; and a 63/9 count is replaced by its recompute
command, since 9 does not reproduce under its own definition.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/design/mechanisms.md
+15-1Lines changed: 15 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -141,7 +141,21 @@ Contract statement. A user-supplied pattern is rejected as over-matching by prob
141
141
142
142
### VOCABULARY-EXERCISES-FORKS — pin the branch, not the wordlist
143
143
144
-
Contract statement. A vocabulary set is data; the code that reads it is what has behavior, so a case row earns its place by exercising a FORK — a branch the parser takes for one member and not another — never by covering a member. Sixteen maiden markers are not sixteen behaviors: født, fødd and född are one class, and a row for each pins nothing the first does not. It follows that "N of M entries have no test" is not a finding; it reads as one because it carries a number. The finding is "this fork has no test", and the forks are findable by reading the code that consults the set and enumerating the branches it takes — for maiden markers, the substring-versus-token split (ASCII against not), the edge-period normalization that makes geb. and geb one word, the native-script whole-token case that cannot collide with Latin text, and a member borne as an ordinary name (decisions.md#vocabulary-collisions). Four classes, four rows; the other twelve entries are data. The same reduction applies to the delimiter pairs, where the forks are same-character against distinct-character open and close (N2) and ASCII against fullwidth, not eleven pairs. Coverage measured against a wordlist grows with every entry added and narrows nothing, which is the tell: a criterion that inflates with the data is measuring the data.
144
+
Contract statement. A vocabulary set a caller configures is data; the code that reads it is what has behavior, so a case row earns its place by exercising a FORK — a branch the parser takes for one INPUT and not another — never by covering one more member. The unit is the input, not the member, because most forks are keyed on the shape of the surrounding name rather than on which word appeared. Find them by reading the sites that consult the set and enumerating the branches they take, and expect that enumeration to come back empty: `maiden_markers` is read at exactly two sites (`_classify.py`'s tagging and `_extract.py`'s `_maiden_marked`), both plain whole-token membership after `_normalize`, and every shipped member parses to one structural class in bare, bracketed and lone-bracketed shapes with the marker text normalised out — zero member forks. What earns the maiden rows is clause SHAPE: marker first against marker later, a word after the marker against none, a marker a separator divides from the name against one glued to it. Two adjacent things are NOT forks of this set and do not want rows: the substring-versus-token split is `_carries` in test_ledger_guards.py, a test guard deliberately asking a wider question than the parser (tools/differential/README.md says so), and edge-period normalization lives in `_lexicon._normalize`, applying to every token of every vocabulary, so it discriminates spellings rather than members. Only "a member borne as an ordinary name" is member-keyed (decisions.md#vocabulary-collisions), and it has its own rows. The reduction does NOT carry across to delimiter pairs, and the difference is the line to hold: a pair the library SHIPS is caller-visible behavior, and vocabulary a caller configures is data — deleting `("«", "»")` from the defaults reddens three tests, so cases.py's one-row-per-shipped-pair convention is load-bearing and this criterion is not permission to delete those rows. Within the pairs the code does fork, on whether open and close are the same character (N2) and again inside that class, where `WORD_INTERNAL_DELIMITERS` carves the apostrophe out: `Jane Smith"` reports UNBALANCED_DELIMITER and `Jane Smith'` reports nothing. ASCII against fullwidth is not one of them — swapping `( )` for `( )` across every corpus name holding a paren changes no field and no ambiguity kind. A row may also earn its place a second way, with no fork behind it: pinning a name a release note advertises BY NAME, which is a promise to a reader rather than a branch in the parser. It follows that "N of M entries have no test" is not a finding; it reads as one because it carries a number, and it inflates with every entry added, which is the tell that it measures the data. The finding is "this fork has no test". State the argument without present-tense counts over a drifting wordlist — see docs/design/AGENTS.md — and where a count is genuinely wanted, ship the one-liner instead:
145
+
146
+
```
147
+
uv run python -c "
148
+
from nameparser import parse
149
+
from nameparser.config.maiden_markers import MAIDEN_MARKERS
150
+
shapes = ['Jane Smith {m} Jones', 'Jane Smith ({m} Jones)', 'Jane Smith ({m})']
151
+
cls = {}
152
+
for m in sorted(MAIDEN_MARKERS):
153
+
sig = tuple(tuple((f, (getattr(parse(s.format(m=m)), f) or '').replace(m, '<M>'))
154
+
for f in ('given', 'middle', 'family', 'suffix', 'nickname', 'maiden'))
0 commit comments