A particle standing alone in a name part is not acting as a particle - #406
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #406 +/- ##
==========================================
+ Coverage 98.54% 98.55% +0.01%
==========================================
Files 44 44
Lines 2954 2977 +23
==========================================
+ Hits 2911 2934 +23
Misses 43 43 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
derek73
force-pushed
the
feat/404-unjoined-particles
branch
2 times, most recently
from
August 19, 2026 07:44
f2f1ee4 to
c7c831f
Compare
"Anh Do" reported last 'Do' with family_base '' and initials 'A.' -- the surname vanished from the base and from the initials. Under FAMILY_FIRST "Del Toro" did the same with 'Del'. 43 of 751 corpus names x 3 orders had a non-empty family with an empty base. A particle earns its name by joining forward to the word it modifies. With nothing to join it is not doing a particle's work, so it reads as an ordinary name word: it anchors the base, leaves the particles view, and initials. POSITION decides that, not vocabulary -- which reverses R2's stated discriminator, "borne as an ordinary surname somewhere". That test would have needed a per-word adjudication of all 70 particles; this one needs none. The invariant it exists to hold, and now asserted over the whole case table: a non-empty family always has a non-empty base. A particle needs a base to attach to. MARKED, not untagged (mechanisms.md#MARK-DONT-STRIP): `particle` is stable API meaning "from the particle vocabulary wherever it lands", which stays true. UNJOINED_TAG records the decision, following FOLDED_TAG's precedent, and leaves #405's ambiguity emitter something to key on -- stripping would have destroyed the record for exactly the never-given half, whose only tag is `particle`. The v1 facade needed its own twin, since it re-derives particle-ness from the lexicon rather than reading tags; _split_last already had the guard, which is why only initials disagreed. Views-only: the seven role fields are byte-identical over all 751 corpus names in all three orders, so the differential is blind to this and reports 0 unexplained at every baseline. Tests are the whole verification. Closes #385 Closes #402 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
derek73
force-pushed
the
feat/404-unjoined-particles
branch
from
August 19, 2026 07:51
c7c831f to
24bba2c
Compare
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.
A family name made only of particle words reported no base, so the surname vanished from
family_baseand from the initials:43 of 751 corpus names × 3 orders had a non-empty family with an empty base. That is now 0, and asserted.
The rule
A particle earns its name by joining forward to the word it modifies. With nothing to join it is not doing a particle's work, so it reads as an ordinary name word — it anchors the base, leaves the particles view, and contributes an initial.
Position decides this, not vocabulary. That reverses
rules.md#R2's stated discriminator, which was "a family written wholly out of particle vocabulary still has a base where one of those words is itself borne as an ordinary surname". That test would have required adjudicating all 70 particles for whether each is borne as a surname specifically —vanis ambiguous because Vietnamese Văn is a given name,Dobecause Đỗ is a surname, and nothing in the vocabulary separates those two reasons today. The position test needs no per-word judgement at all.Where the particles do join a name word nothing changes:
"Juan de la Vega"keeps baseVega, particlesde la, initialsJ. V.The invariant
Asserted over the whole case table in
test_a_non_empty_family_always_has_a_base. Mutation-checked: deleting the marking pass fails it on two rows. It was inert when first written — no case row had an all-particle family — so three rows were added, which is what makes it bite.Marked, not stripped
particlestays on the token; a namespacedUNJOINED_TAGrecords the decision, followingFOLDED_TAG's precedent. Stripping was the tempting version — every consumer becomes correct for free — but it destroys the record asymmetrically:particleis the only tag a never-given particle carries, while an ambiguous one keepsvocab:particle-ambiguous. The half where a later fork is most interesting is the half that would lose its evidence, andParsedNamehas no lexicon to re-derive from. It would also be a breaking API change, sinceparticleis documented stable and means "from the particle vocabulary wherever it lands" — which stays true.This leaves #405's ambiguity emitter a positive predicate to key on.
Declined: the grouping half
#404 also proposed that a particle run joining nothing should not chain, splitting
"Jong van der"into middlevanplus familyder. Measured and rejected — it changes no view on its own, and it cancels the rule above where it matters:Grouping can decline to merge but cannot keep the words apart — roles re-assemble them, and two adjacent same-role pieces are one part at the field level. Recorded in
decisions.md#R2under Declined so nobody re-proposes it from first principles.Verification
Views-only:
title,given,middle,family,suffixare byte-identical over all 751 corpus names in all three orders. The differential harness compares only those seven roles, so it is blind to this change and reports 0 unexplained at every baseline — that is not evidence here, and the tests are.The v1 facade needed its own twin, since it re-derives particle-ness from the lexicon rather than reading tags.
_split_lastalready carried the guard, which is why only the initials disagreed; both surfaces now agree on every corpus name except 12 pre-existing conjunction cases (Amy E Maid, where v1 filtersEas a conjunction and v2 does not) — untouched here.3803 tests pass, mypy clean.
Closes #385
Closes #402
🤖 Generated with Claude Code