Skip to content

feat(differential): a fourth corpus from the rules doc's own examples - #415

Open
derek73 wants to merge 3 commits into
masterfrom
feat/414-rules-doc-corpus
Open

feat(differential): a fourth corpus from the rules doc's own examples#415
derek73 wants to merge 3 commits into
masterfrom
feat/414-rules-doc-corpus

Conversation

@derek73

@derek73 derek73 commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Closes #414.

docs/design/rules.md's 156 example names fed no differential corpus. 113 were in no corpus at all, and 12 had moved during the 2.2 cycle with nothing observing it.

Why the doc tests are not already this

test_rules_doc.py executes every example, which pins them harder than corpus membership pins anything — but against an expectation stored beside them. Change behavior deliberately and the expected value is edited in the same commit; the test goes green and says nothing. A doc example cannot warn about the change that edited it.

PR #409 is the worked example. It changed M2's

"Jane de la née Jones"      →  family="de la née Jones"

to family="de la". The doc test followed the edit. The name was in no corpus, so nothing independent observed the movement — the only reason it is explained anywhere is that I chose to explain it.

Parsing the same name against a released baseline can observe it, because no commit can edit 1.4.0.

The builder

build_rules_corpus.py, shaped after build_cjk_corpus.py: it selects through the doc's own parser rather than a second regex, and test_ledger_guards.py pins the checked-in file against the generator so a stale corpus fails the suite instead of silently narrowing the gate. Policy annotations are ignored and boundary strings kept, both following the CJK builder's stated reasoning.

Classifying the 12 is most of this diff, and it found four things

An unlogged behavior change. Mc Donald moves because #360 moved mc into the never-given particles. That commit is on master, in no release tag, and had no release-log bullet — a user-visible change heading for 2.2 with nothing in the changelog. Committed separately here.

Two ledger rules broad enough to absorb a regression, both harmless until a name of the right shape existed:

  • fix(#274) matched a parenthesized Nee. Under the default facade — which is what the differential parses with — no maiden delimiter is configured, so that word is a nickname and no marker is active at all. The rule stood ready to explain a maiden diff on a name with no marker in it.
  • The acronym rule's m\.?a\.? reached the real name John Smith M.A..

The vocabulary-presence and member-reach guards caught both. They are the absorption detectors, and this is the first corpus that gave them something to detect.

Two arrivals the ledger predicted in writing. fix(#379) said a vd-shaped name "should arrive unexplained and be read once"; fix(#367) named Sir de Mesnil as a future arrival. Both arrived. Both are now their own rules rather than widened literals — the guards allow one vocabulary alternation per rule, and widening both a title and a particle slot needs two, whose covers sets cannot both be recorded under one key.

Two pre-existing behaviors that had never had a corpus name: a nakaguro division meeting a glued hangul honorific, and two typographic quote spans read as one nickname set.

What review changed

The first cut had two rules doing the thing this ledger exists to prevent.

fix(#399) reached six corpus names and explained three. The other three were exactly the shapes its own comment says it must not claim — M2's accepted limits, two of them tracked at #412. The comment justified the reach with "the consumer needs a non-suffix word after the marker," which \s+\S does not express: PhD matches \S perfectly well. Now anchored to one word after the marker and no comma in the name: six → four.

fix(#274)'s reach went 4 → 16 when the corpus grew, and nine of the sixteen do not move. Seven arrived with this PR, four of them M2 boundary examples (Jones née, née Jones) that the vocabulary-presence guard cannot see — those names really do carry marker vocabulary. It now tests what the rule has always meant: a marker with a name word before it and one after it. 16 → 11, with all seven explained names inside the eleven.

Both still claim one static name each (Jane van der Berg née PhD, abd Allah). Excluding either needs the regex to ask a vocabulary question, which is a second alternation the guards permit only one of per rule — so they're named in the comments instead, the way fix(#379) names Nguyen, Van.

Two new guards, for holes the review demonstrated rather than argued:

  • _MUST_NOT_MATCH. A rule claiming exactly one corpus name can be widened with its claim count unmoved, because the names it newly reaches are outside the corpora. Six such widenings passed the entire suite — ^mc\s+\S+$ to ^mc, the vd rule to a bare \bvd\b, four more. The roster is a wall rather than a snapshot, so it's wrong at recording time too. It catches all six, plus reverting both narrowings.
  • A rule-count floor and per-rule contribution check. The staleness pin compares the file against the generator's current output, so a generator that silently selects less agrees with a regenerated file and both are wrong together. _CORPUS_FLOORS bounded that to a few names; this bounds the shapes it can't see.

Smaller fixes: the builder emitted "" (the [subject] example form carries no text, and rules.md's three collapsed into one empty string), 156 → 155; compare.py returned normally when a corpus file was deleted, since the glob just found fewer files; _CROSS_RULE_WINNERS gained the three names landing on the 1.4 catch-all and lost a header count stale twice over; and two release-log claims were wrong — los/las/das weren't particles at all before #360, so Maria das Neves moves from a non-leading position, and the #399 bullet's "no corpus name carries a particle ahead of a marker" stopped being true the moment this corpus landed.

Verification

All three baselines exit 0 over 864 names (751 → 864), the 1.4.0 baseline verified genuine rather than the mislabelled cached tree. Full suite green: 4403 passed, up from 4289 as the new corpus adds differential cases. ruff and mypy clean.

Every _CORPUS_CLAIMS growth was checked against the two absorption guards before recording, per the guard's own instruction.

🤖 Generated with Claude Code

derek73 and others added 2 commits August 20, 2026 22:01
6389f06 moved `mc` and `ste` into the never-given half of the particle
vocabulary and added `los`, `las` and `das`, which were missing from it
entirely. It is on master, in no release tag, and had no Behavior
Changes bullet -- so a user-visible change was heading for 2.2 with
nothing in the changelog.

Five name shapes move: `Mc Donald`, `Ste Marie`, `Los Santos`,
`Las Casas` and `Das Silva` each go from a given-plus-family reading to
a single folded family name, and the PARTICLE_OR_GIVEN ambiguity they
used to report goes with them.

Found by the rules-doc corpus (#414): `Mc Donald` is a rules.md#P1
example, and classifying its diff meant finding out which change had
moved it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/design/rules.md's 156 example names fed no differential corpus.
113 of them were in no corpus at all, and 12 had moved during the 2.2
cycle with nothing observing it.

The doc tests are not already this. They pin each example against an
expectation stored beside it, so a deliberate behaviour change edits
both in one commit and the suite stays green -- a doc example cannot
warn about the change that edited it. PR #409 is the worked example: it
changed M2's "Jane de la née Jones" from family 'de la née Jones' to
family 'de la', the doc test followed the edit, and nothing else
observed the movement. Parsing the same name against a RELEASED
baseline can, because no commit edits 1.4.0.

build_rules_corpus.py selects through the doc's own parser rather than
a second regex, the way build_cjk_corpus.py selects through the shipped
script table, and test_ledger_guards.py pins the checked-in file
against the generator.

Classifying the 12 is most of this diff, and it found four things:

- An unlogged behaviour change, committed separately: #360's
  never-given vocabulary move, which is what moves `Mc Donald`.
- Two ledger rules broad enough to absorb a regression, both harmless
  until a name of the right shape existed. fix(#274) matched a
  PARENTHESIZED `Nee` -- a nickname under the default facade the
  differential parses with, where no marker is active at all -- and
  stood ready to explain a maiden diff on a name with no marker in it.
  The acronym rule's `m\.?a\.?` reached the real name `John Smith
  M.A.`. The vocabulary-presence and member-reach guards caught both;
  they are the absorption detectors and this is the first corpus that
  gave them something to detect.
- Two arrivals the ledger predicted in writing. fix(#379) said a
  vd-shaped name "should arrive unexplained and be read once"; fix(#367)
  named `Sir de Mesnil` as a future arrival. Both arrived, and both are
  now their own rules -- separate rather than widened literals, because
  the guards allow one vocabulary alternation per rule and widening
  both a title and a particle slot needs two.
- Two pre-existing behaviours that had never had a corpus name: a
  nakaguro division meeting a glued hangul honorific, and two
  typographic quote spans read as one nickname set.

All three baselines exit 0 over 864 names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@derek73 derek73 added docs Documentation fixes and updates tooling labels Aug 21, 2026
@derek73 derek73 self-assigned this Aug 21, 2026
@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.55%. Comparing base (95f7159) to head (1044a69).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #415   +/-   ##
=======================================
  Coverage   98.55%   98.55%           
=======================================
  Files          44       44           
  Lines        2981     2981           
=======================================
  Hits         2938     2938           
  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.

Review found the first cut of #414 doing the thing this ledger exists
to prevent, in two rules at once.

fix(#399) reached six corpus names and explained three. The other
three were exactly the shapes its own comment says it must not claim:
the trailing-marker shape, the connective join and the post-comma
bound-given join -- M2's accepted limits, two of them tracked at #412.
The comment justified the reach with "the consumer needs a non-suffix
word after the marker", which `\s+\S` does not express: PhD matches
`\S` perfectly well. Anchored now to one word after the marker and no
comma in the name, which takes it to four.

fix(#274)'s reach went 4 -> 16 when the corpus grew, and nine of the
sixteen do not move. Seven arrived with this PR, four of them M2
BOUNDARY examples -- 'Jones nee', 'nee Jones' -- which the
vocabulary-presence guard cannot see, because those names really do
carry marker vocabulary. It now tests what the rule has always meant:
a marker with a name word before it and one after it. Reach 16 -> 11,
and all seven names it explains are inside the eleven.

Both rules still claim one static name each ('Jane van der Berg nee
PhD', 'abd Allah'). Excluding either needs the regex to ask a
vocabulary question, which is a second alternation the guards allow
only one of per rule -- so they are named in the comments instead,
the way fix(#379) names 'Nguyen, Van'.

Two new guards, for holes the review demonstrated rather than argued:

- _MUST_NOT_MATCH. A rule claiming exactly one corpus name can be
  widened with its claim count unmoved, because the names it newly
  reaches are outside the corpora. Six such widenings passed the whole
  suite: `^mc\s+\S+$` to `^mc`, the vd rule to a bare `\bvd\b`, and
  four more. The roster is a wall rather than a snapshot -- wrong at
  recording time too -- seeded from the names the rules' own comments
  argue about. It catches all six, plus reverting both narrowings.
- A rule-count floor and a per-rule contribution check on the rules
  doc. The staleness pin compares the file against the generator's
  CURRENT output, so a generator that silently selects less agrees
  with a regenerated file and both are wrong together. _CORPUS_FLOORS
  bounded that to a few names; these bound the shapes it cannot see.

Also from review:

- The builder emitted "" as a name: `[subject]` examples carry no
  text, and rules.md's three collapsed into one empty string the doc
  never wrote. Filtered, 156 -> 155.
- compare.py returned normally when a corpus file was DELETED -- the
  glob just found fewer files and the summary read like a full run.
  The floors already name every corpus that should exist, so main()
  asks them.
- _CROSS_RULE_WINNERS gained the three names that land on the 1.4
  catch-all with nothing recording it, and lost a header count that
  had gone stale twice over.
- The #360 bullet claimed non-leading names are untouched. True for mc
  and ste, which only changed halves; los, las and das were not
  particles at all, so 'Maria das Neves' moves too -- 6389f06's own
  headline example. Same for the ambiguity sentence: three of the six
  names never reported a fork.
- The #399 bullet's "no corpus name carries a particle ahead of a
  marker" stopped being true the moment this corpus landed.

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

docs Documentation fixes and updates tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rules.md's normative example names feed no differential corpus — 113 uncovered, and 12 moved this cycle unnoticed

1 participant