Skip to content

Add Token.reading, the reading the parse took for each word (3.0, 4a) - #942

Merged
derek73 merged 8 commits into
masterfrom
feat/4a-token-reading
Oct 29, 2026
Merged

derek73 merged 8 commits into
masterfrom
feat/4a-token-reading

Conversation

@derek73

@derek73 derek73 commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

3.0 sub-phase 4a of the vocabulary work order (#754): every parsed token now carries Token.reading, the reading the parse took for it. ReadingKind gains PATRONYMIC. Reading.languages is out of scope. 3a's reading index (#941) has merged, and a follow-up adds the field together with its values from that index. Adding it unfilled now would be wrong data, since an empty languages says no file holds the reading.

What it adds

  • Token.reading: Reading | None, a field with compare=False and ADDED_IN = "3.0.0".
    • It uses the same Reading(kind, role, source) type that Ambiguity.alternative uses for the reading not taken.
    • reading.role is always the token's own role, and Token raises ValueError otherwise.
    • It is None for a token that replace() or revise() builds, and for a token loaded from a 2.x pickle.
    • capitalized() keeps each token's reading.
  • ReadingKind.PATRONYMIC. Nothing writes it yet; 4c's rules will.
>>> [str(t.reading) for t in parse("John Smith PhD Jones").tokens]
["Reading('name' as 'given', 'position')", "Reading('name' as 'family', 'position')",
 "Reading('credential' as 'suffix', 'vocabulary')", "Reading('credential' as 'suffix', 'position')"]

Where it is filled, and why there

The reading is read in _pipeline/_assemble.py, from each token's final role and the tags the deciding stages left:

  • classify's vocabulary and shape tags;
  • the joined mark that group and post_rules write;
  • post_rules' unjoined marks.

It is read there rather than written at each decision site because roles are only final at assemble. post_rules still moves words after assign (rules/P1's fold, P6's attachment, the policy folds), so a reading written earlier would need re-writing at every later move. That is the stale-mark class _types._remarked exists to recompute.

The suffix and name sources go through _vocab.suffix_kind and _vocab.name_source. These were split out of suffix_reading and name_reading, so the reading taken and Ambiguity.alternative use one predicate there.

Which reading each role takes is in decisions/vocabulary-model's new 2026-10-29 #754 entry, item 2, and _EXPECTED_READINGS in tests/v2/pipeline/test_assemble.py holds one row per branch.

Neutral

  • No role, tag or alternative of any parse moves. tools/differential/snapshot.jsonl is byte-identical, alternatives included.
  • The first draft did move the alternative for a bound given name read as a name: Smith, John Abu went from declared to vocabulary. Review caught it. Both shapes are now rows of _EXPECTED_ALTERNATIVES at their master values.

Cost

Measured with tools/perf/call_count.py on the reference row:

Python master this PR
3.11 305 306
3.15 286 287
  • The one added frame is suffix_kind for the row's III. _CALL_BASELINE (307 and 288) is not moved.
  • The cost is linear per token, not per parse. Measured 2026-10-29 as whole-parse frame deltas against a git archive of master, Python 3.11:
Token Added frames Example (master → this PR)
untagged non-suffix word (the commonest) 0 John Smith 149 → 149, Juan de la Vega 231 → 231
listed or shaped suffix +1 John Smith PhD 199 → 200
tagged name word that reaches name_source +1 van Gogh 161 → 162
family word glued to the next word +1 김민준 212 → 213
suffix connective about +2 John Smith, PhD and MD 298 → 302, with +1 each for PhD and MD
suffix word nothing tags or lists +3 (suffix_kind, _listed_suffix, _normalize), +4 in the whole-parse delta John Smith PhD Jones 267 → 272, ... Jones Brown 327 → 336
  • The 280 possible Readings are built once at import and shared, so no token pays to construct one.

Open questions

  1. A generation word and a post-nominal word read alike, until 4b.
    • The cause: Jr, Sr and Esq. all read post-nominal. The TOML gives jr a generation reading, but SUFFIX_WORDS compiles both kinds into one set, and classify tags them identically.
    • Why this answer: it is the shared predicate's own answer, and Ambiguity.alternative has given it since 2.7. The snapshot's one post-nominal alternative is Rinpoche's.
    • What was weighed: leaving the kind unset would have meant a parsed token with no reading, or a new "not yet known" kind. Either would disagree with that alternative.
    • What keeps it from shipping: test_a_suffix_word_reads_as_its_vocabulary_kind marks Jr and Sr as strict expected failures, with a reason naming 4b. The RFC's 4b row and its interleaving paragraph now say 4b reads a listed suffix word's kind from 3a's index.
    • The other option, a new Lexicon field, was set aside: the 2026-10-28 Lexicon surface entry makes the compiled sets internal.
  2. The seam for Reading.languages. A comment in assemble's loop marks where it joins. 3a's merged _reading_index.readings_of(text) returns the vocabulary's own kinds, so 4a's follow-up (or 4b) maps them onto ReadingKind, and it also gives the generation/post-nominal split above. This branch merges master (3a) and corrects 3a's RFC sentence, which said Add Token.reading, the reading the parse took for each word (3.0, 4a) #942 adds the field unfilled.
  3. A word that is both a bound given name and a particle (abu, أبو) reads P5's given name (name/vocabulary) where P5 joins it: in the given part, as the first word of its segment past the titles (Abu Bakar Smith, Smith, Abu Bakar, , Abu Bakar). Where the particle chain took it, it reads particle, in either part (John Abu Smith, and the given part of Salim Ali abu Bakar Hassan under FAMILY_FIRST_GIVEN_LAST). No tag tells the two joins apart, so position is the discriminator, matching where P5 joins. A tag written at P5's join would be the precise alternative, but changing a tag is not neutral.
  4. revise() builds tokens with no reading, since the caller forced their role. The alternative is to keep the sub-parse's reading re-pointed at the forced role; I left it out because the kind can then contradict the role (a title forced into family).
  5. Token's repr is unchanged. Readings are not shown, so reprs and doctests elsewhere hold still. Showing them is a later choice.

Docs

  • docs/usage.rst: a new "The reading each word took" section with doctests.
  • The release log's Unreleased Additions bullet.
  • The RFC's Phase 4 status line, its 4a and 4b rows, and the interleaving paragraph.
  • A new decisions/vocabulary-model entry.
  • Root AGENTS.md: the pickling and decision-site bullets, and assemble's Reads: line.
  • nameparser/_pipeline/AGENTS.md: a new "The reading taken is read at assemble" section.

The design-docs reviewer's six findings and a code review's four are fixed in the second commit. The orchestrator's review of 22afb30 is fixed in the last two commits: a joining particle reads particle, a suffix connective reads conjunction, the per-token costs above are measured, and the decisions entry covers a bound given name P5 did not join. Each fixed row was run against 22afb30 and fails there. A third round restores P5's given-name reading of abu (by position, the guard rows failing on 1e2ad95), reads a particle in a nickname as a name, sets the frame comment to the measured three, and words the suffix connective as the code reads it.

Checks

  • uv run pytest: 32322 passed, 5 xfailed (after merging master).
  • uv run pytest -n0 -m serial: 41 passed.
  • uv run --python 3.15 pytest: green.
  • uv run mypy, uv run ruff check and the Sphinx doctest build (0 failures): clean.

🤖 Generated with Claude Code

derek73 and others added 3 commits October 29, 2026 12:47
Every parsed token now carries a Reading: what the word was read as,
the field it landed in, and why. Assemble reads it from the final role
and the tags classify wrote, through the predicates that already name
Ambiguity.alternative (_vocab.suffix_kind and name_source), so the
reading taken and the reading not taken share one predicate. Neutral:
no field of any parse moves and the committed snapshot is unchanged.

ReadingKind gains PATRONYMIC, which no reading takes until 4c.
Token.reading carries ADDED_IN 3.0.0 and compare=False, so a 2.x
pickle loads with None (a 2.8.0 wheel fixture is added) and equality
keeps its 2.x meaning. replace() and revise() build tokens without one;
capitalized() carries each through. Reading.languages waits for 3a.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 4a review)

- name_source loses the bound-given arm, which moved Ambiguity.alternative
  for a bound given name read as a name (Smith, John Abu: declared ->
  vocabulary); assemble names the bound given name's reading itself, and
  _EXPECTED_ALTERNATIVES holds both shapes at their master values.
- A surname reads 'vocabulary' only where the segmentation split it off an
  unspaced name, matched on its folded text; spaced, the order placed it.
- A word a suffix run takes stays a credential by position; only the second
  word of a split credential (Ph. D.) takes the first's reading.
- Token rejects a reading of another field.
- Strict xfail for Jr and Sr reading as a generation, the 4b follow-up.
- Docs: the tags assemble reads are the deciding stages', not classify's
  alone; 280 shared readings, not 252; 4b needs 3a for a listed suffix
  word's kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@derek73 derek73 added this to the 3.0 milestone Oct 29, 2026
@derek73 derek73 added enhancement vocabulary Per-word vocabulary model and language packs labels Oct 29, 2026
@derek73 derek73 self-assigned this Oct 29, 2026
derek73 and others added 2 commits October 29, 2026 13:09
…#754, 4a review 2)

- A word that is a bound given name and a particle ('abu') reads 'particle'
  where the chain took it into the family (John Abu Smith, Smith, John Abu),
  and P5's given name only in the given part (Abu Bakar Salim); before, the
  bound-given arm ran first and read every one 'name', so Smith, John Abu's
  taken reading shared its kind with the report's alternative.
- A connective in a suffix run reads 'conjunction' (John Smith, PhD and MD),
  as in a title or name part, and skips the vocabulary lookup.
- The per-token frame costs in assemble's comment are the measured ones.
- The decisions entry says a listed bound given name P5 did not join still
  reads 'vocabulary'; _pipeline/AGENTS.md's capitalized() sentence reads
  right again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iew 2)

The role gate on the bound-given arm read 'abu' as P5's given name in any
given part, including one the particle chain made under
FAMILY_FIRST_GIVEN_LAST (Salim Ali abu Bakar Hassan), where 'de' in the
same place reads particle. No tag tells P5's join from the chain's, so a
word in both sets reads particle wherever it joins, and name, vocabulary
where it joins nothing (Abu Smith).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
derek73 and others added 3 commits October 29, 2026 13:19
Classify tags every listed suffix word, bracketed or not, so only an
acronym written with an interior period ('M.A' in 'John Smith (M.A)')
reaches the check untagged; codecov/patch flagged the dead arm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves docs/design/decisions/vocabulary-model.md by keeping 3a's
2026-10-29 #753 entry before 4a's, and says a follow-up adds
Reading.languages with its values (3a's RFC said #942 adds it unfilled).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eview 3)

f001e7e read every joined abu as a particle, P5's given-name join
included (Abu Bakar Smith, Smith, Abu Bakar, , Abu Bakar), where its twin
Abdul Rahman Smith reads name. A word in both sets is P5's given name in
the given part as the first word of its segment past the titles, which is
where P5 joins, and a particle where the chain took it, in either part
(John Abu Smith; Salim Ali abu Bakar Hassan under FAMILY_FIRST_GIVEN_LAST).

Also: a particle in a nickname reads as a name ('"Van" Smith'), the
unjoined marks being written in the name parts only; the frame comment
says three for an untagged suffix word, as measured; the decisions entry
says a connective in the suffix reads conjunction trailing as well as
joining.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@derek73
derek73 merged commit 6fd949e into master Oct 29, 2026
10 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement vocabulary Per-word vocabulary model and language packs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant