Skip to content

de Mesnil Juan should read family de Mesnil plus given Juan — the fold takes the particle run, not the rest of the name #390

Description

@derek73

rules.md#P1 states that a leading never-given particle makes the
particle run and the one name word it attaches to the family, with any
further name words read by position. The parser still folds every
remaining name word in, so the rule and the code disagree.

Behavior, all three orders

                today                       intended
GIVEN_FIRST     family='de Mesnil Juan'     family='de Mesnil'  given='Juan'
FAMILY_FIRST    family='de Mesnil Juan'     family='de Mesnil'  given='Juan'
FF_GIVEN_LAST   family='de Mesnil Juan'     family='de Mesnil'  given='Juan'

name_order deliberately has no effect on this input class, and
that is the decision rather than an oversight. A leading never-given
particle is evidence about how the name is written — the Latin-script
analogue of rules.md#W4, where a wholly-hangul name reads
family-first under every declared order because the script settles it.
decisions.md#O4 draws the line the same way: "Words no vocabulary
has claimed read by position"
, so the declared order governs the
unclaimed remainder, which is most inputs but not this one.

The alternative — particle group takes position one, name_order
names the position — is Declined at decisions.md#P1: it makes
de la Vega read given='de la Vega' with no family under
GIVEN_FIRST, breaking a corpus name and v1 parity, unless a further
rule forces a particle-headed lone group into the family, at which
point it produces this issue's reading anyway.

Where the decision lives

Decided 2026-08-16 in #386 as part of the order-precedence keystone,
recorded at decisions.md#P1 (the 2026-08-16 #364 entry). #364
asked the question and closed once it was answered; this issue is the
implementation, so the two deviates: #364 markers on rules.md#P1
have a live target.

"Takes everything" was never argued for — it was the shape of v1's
handle_non_first_name_prefix, not a decision.

Measured scope

One corpus name moves, over all 782 names of the three differential
corpora with no prefilter:

de Mesnil Garcia   family='de Mesnil Garcia'  →  family='de Mesnil', given='Garcia'

Two lookalikes do not move: de la Vega is one group start to
finish, and de Mesnil Jr. has two name words because Jr. is a
suffix.

Measurement trap, from mechanisms.md: the particle run chains
through any particle, not only the never-given ones. A detector
walking only the never-given run splits de la Vega after de la and
reports 50 false movers.

Implementation note

The fold is in nameparser/_pipeline/_post_rules.py and today reads
for i in givens + middles: _retag(tokens, i, Role.FAMILY) — every
remaining name word. It needs the particle's own group instead, which
state.pieces already carries. The DEVIATION #364 comment above it
comes out in the same diff.

Sibling

#365 is the same rule reached under FAMILY_FIRST, where the particle
strands in middle. Both close together: once grouping is
order-independent, the middle position needs no third fold site.

Verification

Activity

  1. self-assigned this
    on Aug 17, 2026
  2. derek73 commented on Aug 17, 2026

    @derek73
    OwnerAuthor

    First attempt closed unmerged — three routes still violate P1, and one regresses

    PR #391 implemented the claim in assign, before positions are handed
    out. That part works for the shape this issue names:

    de Mesnil Juan   →  family='de Mesnil'  given='Juan'   (all three orders)
    

    It passed 3459 tests, tools/differential exit 0 at 1.4.0/2.0.0/2.1.0,
    ruff and mypy. A /pr-review-toolkit:review-pr pass with three agents
    then found eight issues, all verified by measurement. Recording them
    here so the next attempt starts from them rather than rediscovering
    them.

    Blocking: de los Santos regresses to a wrong surname

    de los Santos   2.1.0: last='de los Santos'   attempt: given='Santos'  family='de los'
    de las Casas    2.1.0: last='de las Casas'    attempt: given='Casas'   family='de las'
    

    los, las, das, el are not in the particle vocabulary — only
    de, la, der, den are. So the claim takes de + los and hands
    the real surname to given.

    This is the important one, because it inverts the bundle's sequencing.
    "Takes everything" is insensitive to vocabulary gaps; "takes the group"
    is not. The old sweep was masking an incomplete particle set, and
    narrowing the claim converts that gap into user-visible breakage on a
    very common Hispanic name shape. #360 has to land first, with C-i
    applied to the unexamined articles (los/las are articles, borne as
    no one's name, so C-i puts them in the never-given half).

    The differential could not see it: no de los name exists in any of the
    three corpora. Corpus-blindness hiding a regression rather than a small
    count.

    Blocking: a multi-particle run still folds the whole name

    de la Cruz Maria    →  family='de la Cruz Maria'   (P1: family='de la Cruz', given='Maria')
    de la Vega Juan     →  family='de la Vega Juan'    (P1: family='de la Vega',  given='Juan')
    

    Cause is rules.md#P4: the leading particle chains nothing, so de is
    its own piece and the next particle chains everything after it —
    pieces = [['de'], ['la','Vega','Juan']]. Taking pieces[:2] takes the
    whole name. de Mesnil Juan works only because it has no second
    particle. So the implementation takes the particle run plus one
    piece
    , where P1 says one word; the two differ exactly when the run
    spans more than one piece.

    Blocking: the family-comma path never reaches the claim

    Smith, de Mesnil Juan   →  family='Smith de Mesnil Juan'   (P1: family='Smith de Mesnil', given='Juan')
    

    _assign_main is not called for FAMILY_COMMA, so the old
    whole-remainder sweep survives in post_rules for segment 1.

    All three agents converged independently on the last two, which is the
    strongest signal in the set.

    #365 does NOT close with this

    The attempt claimed it did. It does not — verified:

    Mesnil Garcia de   FF    given='Garcia' middle='de' family='Mesnil'   ← stranded
                       FFGL                            family='Mesnil Garcia de'  ← folded
    

    The surviving post_rules site is still ROLE-keyed, which is #365's
    actual defect. Relocating the leading half does not reach it.

    Other verified findings

    • PARTICLE_OR_GIVEN emits a false detail. de Mesnil van MD Gogh
      reports "leading 'van' may be a family-name particle" — van is the
      third piece. Cause: name_pieces is rebound to the leftover before
      the emitter reads name_pieces[0], silently changing its meaning.
      _ambiguities is a compared differential field, so this is a latent
      unexplained diff.
    • FF and FFGL diverge at two leftovers and nothing tests it.
      de Mesnil Juan Carlos → FFGL swaps given/middle. The
      _name_positions(order, count + 1) / drop-FAMILY line is pinned only
      in its degenerate one-leftover case, where all three orders coincide.
    • A replacement test was inert. Dr. de Mesnil cannot exercise the
      title peel under the module's fixture lexicon (titles={"mr","sir"} —
      dr absent), so de Mesnil chains into one piece, the claim never
      fires, and the test passes with the feature disabled. Mutation-verified.
      Mr. is the title that works here.
    • _CROSS_RULE_WINNERS needs a row for de Mesnil Garcia: without
      the new ledger rule it falls to the fields-only fix(suffix-routing)
      catch-all with the gate still at 0 unexplained.
    • ~8 prose sites contradict the new behavior and the same-PR
      amendment rule puts them in the diff: no release_log.rst bullet, the
      existing particles_ambiguous has no effect on the parse under name_order=FAMILY_FIRST #359 bullet now false, nameparser/config/particles.py
      docstrings (autodoc-rendered API reference), docs/customize.rst,
      docs/usage.rst, AGENTS.md:198, mechanisms.md#PIECES still naming
      _post_rules.py as the pieces reader, and both stage docstrings.
      Every doctest keeps passing because every example is a two-word name.

    What the next attempt needs

    1. Mc Donald and Ste Marie read the particle as the given name #360 first. The claim cannot be narrowed while the particle set
      has gaps.
    2. The claim belongs in grouping, not assign. Two of the three
      blocking routes need a chained piece SPLIT (de la Vega Juan) and
      the third needs the FAMILY_COMMA branch — neither is reachable from
      where assign hands out positions.
    3. Keep the deviates: #364 markers on rules.md#P1 until all three
      routes hold. Removing them while the comma and multi-particle routes
      still sweep is what let the doc claim a rule the parser does not have.

    Branch fix/390-fold-takes-particle-group is pushed if any of it is
    worth salvaging; master is untouched.

  3. derek73 commented on Aug 18, 2026

    @derek73
    OwnerAuthor

    Second attempt also closed unmerged (PR #394) — and the rework strategy is now clearer

    #394 put the claim in grouping, which fixed the three routes #391 could
    not reach. It still regressed four shapes and stated several things in
    the normative docs that are false. A three-agent review found them; all
    were verified by measurement against master.

    What #394 got right, and should be kept

    Regressions from master

                           master                     #394
    de and Mesnil Juan     given='de and Mesnil'      family='de and Mesnil Juan'
    de y Mesnil Juan       family='de y Mesnil Juan'  family='de y'          <- zero name words
    St de Mesnil Juan      (n/a)                      family='de Mesnil Juan' <- the whole-name fold
    de abdul Rahman Juan   family='de abdul Rahman'   family='de abdul'      <- splits a P5 bound pair
    

    False statements no test could see

    • rules.md#P4: "A title before either changes nothing" — three TITLES
      members (st, do, freiherr) change everything.
    • rules.md#P2: "The final group reads as the family name" — false for
      every name the PR moves (de Mesnil van der Berg → family='de Mesnil', given='van der Berg'). P2 got no amendment and has no
      interacts: line, so nothing points back at P1/P4.
    • The guard comment said "three conditions" over four conjuncts, and
      credited not title for st/do/freiherr — which are in the
      AMBIGUOUS half, so a different conjunct already excludes them.
    • "v1 gave family='Smith de Mesnil'" — v1.4.0 actually gives
      last='de Mesnil Smith'. Wrong in decisions.md and a test comment.
    • Ledger prose about fix(suffix-routing) being "below" was
      copy-pasted into the 2.0.0 and 2.1.0 ledgers, which have no
      fields-only catch-all at all.

    The core new code was unpinned

    Verified by mutation on a copy: replacing the
    _name_positions(order, count + 1) minus FAMILY construction with a
    hardcoded [GIVEN] + [MIDDLE]*(n-1) — discarding name_order for the
    leftover entirely — passes the whole suite. So does removing the
    prefix(leading) conjunct, which is also inert (no input distinguishes
    it). The two-leftover shape (de Mesnil Juan Carlos), where the orders
    actually diverge, appears in no test, no case row and no rule example.

    Also a THIRD fixture-trap casualty: Dr. de MD Mesnil cannot exercise
    the leading chain, because dr is absent from test_post_rules.py's
    reduced _LEX.

    The rework, corrected

    My first proposal was to walk tokens and ignore piece boundaries. That
    is wrong, and Derek's correction is why: the conjunction rule means a
    "word" in other rules can be several words.
    Vega y Santos is one
    unit, so de la Vega y Santos Juan → family='de la Vega y Santos',
    given='Juan' is CORRECT — a review finding against it was a false
    positive. Ignoring piece boundaries would split it.

    The formulation that explains every case, including both regressions:

    The family group is the particle run plus exactly ONE NAME WORD,
    where a conjunction-joined run counts as one word. Stop extending as
    soon as the group contains a name word; do not extend at all if it
    already does; claim nothing if it never does.

    de                        run has no name word            -> given='de'          (P1 Accepted)
    de Mesnil Juan            [de] has none, take Mesnil      -> family='de Mesnil'
    de and Mesnil Juan        [de and Mesnil] ALREADY has one -> stop, given='Juan'
    de y Mesnil Juan          [de y] has none, take Mesnil    -> family='de y Mesnil'
    de la Vega y Santos Juan  Vega y Santos is ONE unit       -> family='de la Vega y Santos'
    

    Both #394 regressions are this rule not being applied: it extended past
    a piece that already had a name word, and it stopped on a piece that
    had none. Stated this way the conjunction merge and the title overlap
    stop being special cases.

    Whatever lands next also needs, per the review:

    • a test at TWO leftovers across all three orders (the only place
      name_order matters for the remainder)
    • a negative control for each guard conjunct, or fewer conjuncts
    • rules.md#P2 amended or marked — its "final group is the family"
      sentence does not survive this change
    • the stale DEVIATION #364 comment in _post_rules.py removed
    • doc examples that use the DEFAULT vocabulary, not the reduced _LEX

    Branch fix/390-retry-claim-in-grouping is pushed if any of it is
    worth salvaging. master is untouched.

  4. derek73 commented on Aug 18, 2026

    @derek73
    OwnerAuthor

    Superseded by #395.

    The direction changed after two failed attempts: the stopping point is now keyed on name_order rather than on position alone. Declaring a family-first order is precisely an assertion that what follows the family is not more surname, so the default order keeps the greedy reading (de Mesnil Juan → family='de Mesnil Juan', the same shape as pennie von bergen wessels) and only family-first orders stop at the first name word.

    That makes this issue's title false — it asserts the bounded reading for the default order. decisions.md#P1 carries a 2026-08-17 entry superseding the "no name_order moves that stopping point" sentence this issue was written under.

    Everything the two attempts established — the guard traps, the coverage gaps, the fixture trap, the docs needing amendment — is carried forward to #395.

  5. added this to the v2.2 milestone on Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions