refactor(pipeline): piece-level predicates get their own module (#439) - #441
Merged
Conversation
Pure relocation. No behavior change, no ledger entry, no release-log bullet -- the differential is byte-identical because no parse moves. Ten objects leave _group.py verbatim, comment blocks included: the rules.md citations live in those comments and have to travel with the code they cite. _is_title_piece, _PERIOD_ABBREV, _is_leading_title, _leading_titles, _is_suffix_piece, _segment_holds_no_name, Peel, _peel_walk, _trailing_start, _peel_trailing. _vocab.py was the other candidate and rules itself out in its own docstring -- "Text-level tests used by more than one stage; token/piece-level predicates live with their stage." That contract is deliberate (is_wholly_suffix documents two departures from it), so the piece layer gets its own module rather than bending the text layer's. The split is by what the question TAKES, which is the rule mechanisms.md#ONE-PREDICATE-PER-QUESTION states. _trailing_start travels with the peel although only _peel_walk and _peel_trailing cross a stage boundary: decisions.md describes the S2 peel as those three together, and splitting a documented unit across modules to satisfy a usage rule would be worse than carrying one group-only function along. Five predicates stay in _group, being group's alone: _is_prefix_piece, _is_conj_piece, _is_rootname, _is_maiden_marker_piece, and the local closures. group imports back what it still uses; nothing imports _pieces from below, so there is no cycle. Three couplings the move had to carry, none of which grep would have found from the function names alone: - rules.md H2, H3 and S2 name the module their citation sits in, and test_doc_citations asserts set EQUALITY against citing modules, so their implemented: lines move to _pieces.py. - test_regex_sync tracks _PERIOD_ABBREV by module path in three places (the import, the assertion, and the declared-copy roster). - test_layering iterates ALLOWED.items(), so a module absent from that dict is never checked AT ALL. _pieces.py is registered with a tighter contract than the generic stage allowance -- _state and _vocab only -- which makes the module docstring's claim executable. Verified non-vacuous: the entry rejects an added nameparser._render import on a scratch copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The same-PR amendment rule landing on text I merged yesterday. Both statements were true when written and the move makes them false: - _group's docstring said it houses the shared piece predicates and explained why they were there. They are not there now, and what remains is group's own. - mechanisms.md#ONE-PREDICATE-PER-QUESTION said a predicate over pieces and tags goes to _group. It goes to _pieces; group was where they collected BEFORE the module existed, which is the accumulation the entry describes rather than the destination it should recommend. The entry's Lives in gains the layering test, because that is now where each module's contract is written down -- and a piece predicate that grows a dependency on a stage shows up there as a widened entry rather than as a review comment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #441 +/- ##
=======================================
Coverage 98.59% 98.59%
=======================================
Files 44 45 +1
Lines 3064 3068 +4
=======================================
+ Hits 3021 3025 +4
Misses 43 43 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
test_layering_contract iterates ALLOWED.items(), so its coverage is whatever someone remembered to key. A module absent from that dict is not checked loosely -- it is not checked at all, and nothing notices: the suite stays green and the new module may import whatever it likes. Registering _pieces.py in the commit before this one fixed that one module and left the mechanism intact for the next. Scoped to _pipeline/ because that is what this contract governs. Package-wide, 16 of 45 modules are unkeyed -- config/ vocabulary data, util.py, _version.py -- and they were never in scope; a whole-package assertion would be asserting the wrong thing. Latent rather than active when closed: all 13 _pipeline modules were keyed, so #439's _pieces.py would have been the first to slip through. That measurement is in the docstring rather than in an assertion, because asserting the count is the constant-content pattern AGENTS.md forbids -- it would fail on every legitimate new module, which is the opposite of what this guard wants. Verified non-vacuous by unregistering _pieces.py on a scratch copy: this test fails and test_layering_contract still PASSES, which is the silent gap it closes, demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four reviewers. The move itself survived every mechanical check: ten objects byte-identical, object sets conserved, no new cycle, and 0 of 1069 corpus names differ across all seven role fields plus str(). All ten are killed by the behavior suite -- zero mutation survivors. The findings are the guards around the move and the prose describing it. THE GUARD I SHIPPED WAS DISARMABLE BY ONE CHARACTER. Path.glob on a missing directory returns empty rather than raising, so a typo in the directory literal or the pattern left test_every_pipeline_module_is_ keyed_in_allowed asserting `set() - keyed` -- true for every possible ALLOWED. A completeness check measuring nothing, which is the shape of the bug it exists to prevent. The directory now comes from the imported subpackage (a typo is an AttributeError) and a known member is asserted, which also covers a mistyped glob. The same registry shape one file over, and this one was ACTIVE rather than latent: test_regex_sync's _MODULES was missing six pipeline modules (_assemble, _classify, _extract, _script_segment, _segment, _state), so they were never scanned for undeclared hand copies of a config regex. Measured on a scratch copy: a new module carrying a wrong copy of the mac pattern passed all 20 tests. Added, and given the symmetric guard -- which catches it now, where before only the layering twin did. Nothing had slipped through; the six carry no hand copies. Three of the five prefixes in the _pieces ALLOWED entry were dead permission carried over from the stage allowance. _lexicon is the sharp one: a title predicate reaching for _title_key is the likeliest next widening, and the entry pre-authorised exactly what its comment claimed to watch for. Narrowed to what the module imports. Prose. Every present-tense "assign imports group" is now FALSE -- that import is precisely what this PR removed; _assign no longer names _group at all. Past-tensed, with the rule that replaced it named instead. Also: _peel_trailing's docstring justified its own placement by that import (the one sentence in the moved block that could not survive verbatim); _assign's two "group's _leading_titles"/"group's _peel_trailing"; _vocab's "piece-level predicates live with their stage", which #439 repealed and which _pieces was quoting AS CORROBORATION; the tags attributed to _vocab when classify and group write them; a "three group sites" count the move made two; group's header claiming H2 and H3 after their citations left; and AGENTS.md's _PERIOD_ABBREV, stale by two hops. S2 regains _group in implemented:, via a citation at the site that acts on it -- group holds thirteen call sites of the peel trio deciding where the family and maiden names stop, and dropping off S2's map because the citation moved was the map getting worse. Counts: mine were falsified three times this session, and my own first correction here said "five predicates" over a list of six. These name the PRs and let the reader count. mechanisms.md's Lives in no longer claims a qualifier that stopped selecting its list, and records that is_trailing_numeral_suffix lost its stage caller to this very move. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Closes #439.
Ten objects leave
_group.pyfor a newnameparser/_pipeline/_pieces.py. No behavior change — the differential is byte-identical at every baseline, because no parse moves.Why a new module rather than
_vocab.pyThe issue offered both.
_vocab.pyrules itself out in its own docstring:That contract is deliberate —
is_wholly_suffixdocuments two explicit departures from it — so the piece layer gets its own module rather than bending the text layer's. The split is by what the question takes, which is whatmechanisms.md#ONE-PREDICATE-PER-QUESTIONstates:_vocabanswers how a word reads,_pieceshow a piece reads.What moved, and what didn't
Moved (10):
_is_title_piece,_PERIOD_ABBREV,_is_leading_title,_leading_titles,_is_suffix_piece,_segment_holds_no_name,Peel,_peel_walk,_trailing_start,_peel_trailing— verbatim, comment blocks included, because therules.mdcitations live in those comments and have to travel with the code they cite.Stayed (group's own):
_is_prefix_piece,_is_conj_piece,_is_rootname,_is_maiden_marker_piece._trailing_starttravels with the peel even though only_peel_walkand_peel_trailingcross a stage boundary:decisions.mddescribes the S2 peel as those three together, and splitting a documented unit across modules to satisfy a usage rule would be worse than carrying one group-only function along._groupimports back what it still uses. Nothing imports_piecesfrom below, so there is no cycle.Three couplings the move had to carry
None of these are findable by grepping the function names, and each would have failed CI on its own:
rules.mdH2, H3 and S2 name the module their citation sits in, andtest_doc_citationsasserts set equality against citing modules — so theirimplemented:lines move to_pieces.py.test_regex_synctracks_PERIOD_ABBREVby module path in three places: the import, the assertion, and the declared-copy roster.test_layeringiteratesALLOWED.items(), so a module absent from that dict is never checked at all. A new pipeline module escapes the layering contract silently.On (3),
_pieces.pyis registered with a tighter contract than the generic stage allowance —_stateand_vocabonly — which makes the module docstring's claim executable rather than merely asserted. Verified non-vacuous: the entry rejects an addednameparser._renderimport on a scratch copy. (The first probe used_assignand only produced a circular-import error at collection, which proves the cycle but not the assertion; the acyclic probe is the one that shows the guard firing.)And the gap itself is now closed, not just stepped around. Registering
_pieces.pyfixed one module and left the mechanism intact for the next, sotest_every_pipeline_module_is_keyed_in_allowedasserts that every_pipeline/*.pyis keyed. Scoped there because that is what the contract governs — package-wide, 16 of 45 modules are unkeyed (config/vocabulary data,util.py,_version.py) and were never in scope.It was latent rather than active: all 13 pipeline modules were keyed, so
_pieces.pywould have been the first to slip through. That measurement lives in the docstring rather than an assertion, because asserting the count is the constant-content patternAGENTS.mdforbids — it would fail on every legitimate new module.Verified by unregistering
_pieces.pyon a scratch copy: the new test fails andtest_layering_contractstill passes — the silent gap, demonstrated.Docs the move falsified
The same-PR amendment rule landing on text merged yesterday in #440. Both statements were true when written:
_group's docstring said it houses the shared piece predicates and explained why.mechanisms.md#ONE-PREDICATE-PER-QUESTIONsaid a piece-and-tag predicate goes to_group.It goes to
_piecesnow;_groupis where they collected before the module existed, which is the accumulation the entry describes rather than the destination it should recommend. The entry'sLives inalso gains the layering test, since that is now where each module's contract is written down.Deliberately not done
No renames. The moved names keep their leading underscore, though
_vocab's convention would drop it for a shared module's public surface. That would touch ~60 call sites across three files and dilute a diff whose value is being a verifiable pure move — a follow-up if you want it.Verification
uv run pytestuv run mypyuv run ruff check_group.pygoes from 1052 to 843 lines.🤖 Generated with Claude Code