Skip to content

fix(maiden): a marker stops the particle chain that swallowed it - #409

Merged
derek73 merged 4 commits into
masterfrom
fix/399-maiden-marker-stops-chain
Aug 21, 2026
Merged

fix(maiden): a marker stops the particle chain that swallowed it#409
derek73 merged 4 commits into
masterfrom
fix/399-maiden-marker-stops-chain

Conversation

@derek73

@derek73 derek73 commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Closes #399.

Ursula von der Leyen geb. Albrecht lost the maiden name that the same words one particle chain apart kept:

Ursula Leyen geb. Albrecht          ->  family='Leyen'          maiden='Albrecht'
Ursula von der Leyen geb. Albrecht  ->  family='von der Leyen'  maiden='Albrecht'   (was: family='von der Leyen geb. Albrecht', maiden='')

The fix

Grouping's prefix chain absorbs everything up to the next prefix or suffix piece, and the maiden marker is consumed after the chain merges — by which point there is no lone marker piece left to find. The chain now stops at a marker the way it already stops at a suffix.

Only a non-leading particle ever reached the marker, which is why a leading single particle always worked (P4 chains nothing) while a leading run of two did not — the second particle's own chain fired.

The stop is gated on the consumer actually taking

This is the substance of the change, and it came out of review rather than the first cut. Sharing one marker-piece predicate settles what a marker piece is; it does not settle whether the consumer takes, because the consumer also needs a non-suffix piece after the marker. Stopping without one left the marker standing as its own piece, and a lone trailing piece takes a role field:

Jane van der Berg née    ->  middle='van der Berg'  family='née'     <- ungated
                         ->  family='van der Berg née'               <- gated

The marker became the surname and the real surname was demoted — the defect the stop exists to prevent, one field over. The gated reading is also what M2 already said should happen: a marker with nothing after it "is just a word", exactly as Jones néefamily='née'.

Three measured consequences of the ungated form, each closed by the gate:

Two shapes fixed without a second change

Limits, now stated rather than implicit

The bound reaches only a marker standing as a word of its own, so P3's connective join and P5's bound-given join each still absorb one first — Jane van der Berg née y Jones and van der Berg, abdul née Jones are unchanged. The deleted Accepted: clause covered a class of which this fixes one instance and leaves two; both survivors are back in M2 with examples.

Docs

  • rules.md#M2: the stop and its gate are in the statement, ahead of the examples — the citation checker cuts a statement at its first example line, so a rule stated after them can never be cited from code.
  • rules.md#P2: its statement enumerated the join's stops and named neither the marker nor the suffix. Both are named now, and its two code citations move with it.
  • decisions.md#M2: the false invariant is corrected; the quoted removal is verbatim so git log -S finds it; and the recorded reason for preferring the stop over a stage reorder is rewritten, because the example it cited does not discriminate — Anna Müller geb. von der Berg is identical before Ursula von der Leyen geb. Albrecht loses the maiden name — a particle chain swallows the marker where a suffix stops it #399, after it, and under a reconstructed reorder.
  • docs/release_log.rst gets its Behavior Changes bullet, which the first commit omitted.

Verification

Tests carry the whole verification. No corpus name has a particle ahead of a marker, so the differential gate is blind to this class and its clean exit says nothing about the change — all three baselines exit clean (1.4.0, 2.0.0, 2.1.0), the 1.4.0 baseline verified genuine rather than the mislabelled cached tree, over 751 names unchanged.

A synthetic Jane van der Berg 旧姓 Jones row was added to reach the CJK corpus builder — the only route a case row has into any corpus — and then removed. It used that corpus's provenance as a back door, and it pinned nothing: measured with the shipped lexicon it produces the same segments and pieces as the Latin spelling, since script_segment does not split at the boundary. #413 is the honest fix — the issues harvester matches only quoted names while this tracker writes them in backticks, so it misses 104, including Ursula von der Leyen geb. Albrecht from #399's own title.

20 case rows plus two piece-level tests in test_group.py. The piece level matters because that is where the two halves can disagree without any field looking wrong enough to fail — the misaligned version passed the entire suite. Removing the gate now fails 6 tests; dropping the marker predicate's lone-piece guard, previously an unkilled mutant, now fails 2.

Full suite green: 4293 passed. ruff and mypy clean, _group.py and _post_rules.py at 100% statement and branch coverage.

🤖 Generated with Claude Code

@derek73 derek73 added bug docs Documentation fixes and updates labels Aug 19, 2026
@derek73 derek73 self-assigned this Aug 19, 2026
`Ursula von der Leyen geb. Albrecht` lost the maiden name that
`Ursula Leyen geb. Albrecht` kept: grouping's prefix chain absorbed
everything up to the next prefix or suffix piece, and the marker is
consumed AFTER the chain merges, by which point there is no lone
marker piece left to find. rules.md carried that as an Accepted
limitation, but it was a consequence of stage order rather than a
judgment, so M2 now states the stop and the limitation is gone.

The chain's stop and the maiden handler's consumer share one
definition of a marker piece. A chain that stopped where the consumer
would not then take would strand the marker inside the family name --
the very defect the stop exists to fix.

Two shapes it fixes for free. Under a family-first order the marker
used to survive as an ordinary name word and take the leftover given
slot (`de la Cruz née Vega` -> given 'née'); dropped, it never reaches
the placement, which answers #399's open question with no second
change. And stopping the chain can leave a family of nothing but
particles, which #404's R2 reading already covers: `Jane de la née
Jones` reports family 'de la' with a non-empty base.

Tests are the whole verification here. No differential corpus name has
a particle ahead of a marker, so the gate is blind to this class and
its clean exit says nothing about it.

Closes #399

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@derek73
derek73 force-pushed the fix/399-maiden-marker-stops-chain branch from b633270 to 829de84 Compare August 19, 2026 08:36
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.55%. Comparing base (c43d0a3) to head (e35c870).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #409   +/-   ##
=======================================
  Coverage   98.55%   98.55%           
=======================================
  Files          44       44           
  Lines        2977     2981    +4     
=======================================
+ Hits         2934     2938    +4     
  Misses         43       43           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@derek73 derek73 added this to the v2.2 milestone Aug 19, 2026
Review found the first cut of #399 traded one stranding for a worse
one. Sharing a marker-piece predicate between the chain's stop and the
maiden consumer settles what a marker piece IS; it does not settle
whether the consumer TAKES, because the consumer also needs a
non-suffix piece after the marker. Stopping without one left the marker
standing as its own piece, and a lone trailing piece takes a role
field:

    Jane van der Berg née    was middle 'van der Berg', family 'née'

The marker became the surname and the real surname was demoted -- the
defect the stop exists to prevent, one field over. The stop now tests
the consumer's own condition, so the two halves cannot disagree.

Three measured consequences, each fixed by the gate. 'Jane van der Nee'
moved a bearer of the attested surname Nee out of the family, which M1
already warns about. 'Ursula von der Leyen geb.', an ordinary truncated
record, reported the marker as the surname. And 'St St née' stopped
reporting its particle-or-given fork, because group's emitter is
guarded on the chain having merged something and an ungated stop made
it merge nothing for a different reason than the guard assumes -- an A1
violation of the shape #405 tracks, and worse, since it removes a
report callers already see.

Pinned six ways: four case rows and two piece-level tests in
test_group.py. The piece level is where the two halves can disagree
without any field looking wrong enough to fail -- the misaligned
version passed the entire suite.

Also from review:

- Two limits the deleted Accepted clause used to cover are back in M2,
  scoped: the bound reaches only a lone marker, so P3's connective join
  and P5's bound-given join each still absorb one first. #399 fixed one
  instance of the join-swallow and left two.
- P2's statement enumerated the join's stops and named neither the
  marker nor the suffix. Both are stated now, and its two code
  citations move with it.
- The new normative sentence moved ahead of M2's examples, because the
  citation checker cuts a statement at its first example line -- stated
  after them, it could never be cited from code.
- decisions.md: the false invariant is corrected, the quoted removal is
  verbatim so `git log -S` finds it, and the recorded reason for
  preferring the stop over a stage reorder is rewritten. The example it
  cited does not discriminate: 'Anna Müller geb. von der Berg' is
  identical before #399, after it, and under a reconstructed reorder.
- A release_log.rst bullet, which the first commit omitted.
- Three case notes corrected: one claimed only the maiden field moved
  when the family moved too, one credited R2 for what P4 and P1 do, and
  one promised a pin the partition invariant does not deliver.
- 'Jane née and Jones Smith' pins the marker predicate's lone-piece
  guard, which was an unkilled mutant: dropping it left 4229 tests
  green while changing behaviour through a conjunction merge.
- The cross-script row puts a name of this shape in the CJK corpus, so
  the class is gate-visible for the first time, with its own ledger
  rule and recorded corpus claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two surviving join-swallows are recorded in M2 but named no issue,
unlike the fullwidth-colon limit beside them which points at #317. They
are #412 now. decisions.md's Open (M2) list also picks up #410 and
#411 -- the two empty-family paths a maiden name opens, both
pre-existing and both widened by the stop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…inery

The row existed to reach the CJK corpus builder, which is the only
route a case row has into any differential corpus -- corpus.jsonl
regenerates from v1's test banks at a pinned ref and corpus_issues.jsonl
harvests the tracker. Getting #399's class in front of the gate that
way was a back door: that corpus's provenance is CJK names, not Latin
mechanisms carrying one CJK token.

It also pinned nothing. Measured with the shipped lexicon,
"Jane van der Berg 旧姓 Jones" and "Jane van der Berg née Jones" produce
the same segments and the same pieces -- script_segment does not split
at the boundary -- so the cross-script spelling exercised no path the
Latin rows do not.

Removing it takes the CJK corpus entry, the ledger rule, its
_LATIN_ALTERNATION_SOURCES declaration and the recorded corpus claims
with it; those three files return to their master content exactly. All
three baselines still exit clean over 751 names.

The class is gate-invisible again, and decisions.md says so rather than
claiming coverage it does not have. #413 is the honest fix: the issues
harvester matches only quoted names, while this tracker writes them in
backticks, so it misses 104 names -- `Beethoven, Ludwig van` and
`Berg, Jan vd` among them, and `Ursula von der Leyen geb. Albrecht`,
which sat in #399's own title the whole time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@derek73
derek73 merged commit 95f7159 into master Aug 21, 2026
11 checks passed
@derek73
derek73 deleted the fix/399-maiden-marker-stops-chain branch August 21, 2026 04:36
derek73 added a commit that referenced this pull request Aug 22, 2026
Review found the reserve fix necessary but not sufficient, and two
prose claims measurably false.

Excluding the departing words is not enough on its own. Where a suffix
sits INSIDE the name the marker walk stops early, so the excluded span
is short, enough words survive it to clear the reserve, and the join
fires and takes the MARKER -- after which nothing leaves at all and the
count that authorised the join was reasoning about a name that never
came to be. `abd née Jones Jr Smith Berg` read given 'abd née'.

The join now declines outright when the piece it would absorb is a
marker, which is what P5 meant all along: it joins the bound word to a
name word, and a marker is not one. Tested against the marker directly
rather than through the span, because the two differ exactly where it
matters -- a marker with nothing but a suffix after it has no span at
all, yet the join would still have absorbed it (`Berg, abdul née PhD`
read given 'abdul née'). That closes the P5 half of #412 for real; the
first cut only narrowed it while claiming it closed.

The docstring's "the count and the removal cannot disagree" was the
same overstatement this session already shipped once. Sharing the span
helper settles what a maiden span IS; it does not settle what leaves,
because the two calls run at different times on different piece lists
and the join between them can invalidate the span. Forbidding the join
to absorb a marker is what closes that -- the sharing only rules out
disagreeing about the span itself.

The pre-existence claim was backwards. `abd Berg née Jones` reads
CORRECTLY at 2.1.0; `abd` acquired the defect only when #400 made it a
bound given-name word this cycle, so for that spelling this fixes a
regression introduced earlier in the same release. The pre-existing
witness is `abdul`/`abdel`/`abdal`, which read the defect at 2.0.0 and
2.1.0 alike. The issue body carries the same error and needs it too.

Six more rows, each closing a measured gap:

- `Abd Berg née Mary Jones`. Capping the excluded span at two pieces
  reproduced #411 exactly with the whole suite green -- every existing
  row had a one-word maiden name, so none could tell the span from the
  marker plus one. A particle-led maiden name does not serve; P2 makes
  it a single piece.
- The two shapes the new guard fixes, and `Berg, abd née Jones`, where
  a declining join after a family comma leaves S2's suffix reading and
  the name has no given name at all -- as `Berg, abd` alone always
  parsed. It reads alarmingly and was in no bullet and no row.
- The marker immediately after the bound word, and the Arabic script.

Plus a piece-level pin that the join never merges a marker, and the
bound-given shapes added to the #409 chain invariant's inputs -- that
invariant is keyed on the particle chain, and the distinction is now
exercised rather than assumed.

`Berg, abd née Jones` arrived UNEXPLAINED at 1.4.0 on four fields, the
subset test correctly rejecting both nearby rules, and is read once
here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug docs Documentation fixes and updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ursula von der Leyen geb. Albrecht loses the maiden name — a particle chain swallows the marker where a suffix stops it

1 participant