fix(check-new-line-breaks): split at a lowercase-opening sentence boundary - #425
fix(check-new-line-breaks): split at a lowercase-opening sentence boundary#425d-morrison wants to merge 8 commits into
Conversation
…ndary The sentence splitter's lookahead required the next sentence to start with an uppercase letter or markup, so a line like `it went red. renv restored the lockfile.` read as one sentence and shipped unflagged --- the shape our prose writes most, since it routinely opens a sentence with a lowercase package or repo name. Add a second regex branch (_SENT_BREAK_LOWER_RE) that accepts a lowercase follower, guarded by a two-lowercase-letter lookbehind (?<=[a-z][a-z]) so a single-letter initial (U.S.), a dotted abbreviation (a.m.), a decimal or version (v2.1), and an ellipsis (wait... foo) are still left intact. Its closing class omits the emphasis markers */_ so mid-sentence emphasis (**critical.** yet) stays on one line, preserving #397's guard rail. Closes #389 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Note: the red |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…oc fixes Finding 1 (blocking): the lowercase branch's closing-character class (`"')]) re-opened the exact over-split that dropping */_ from the uppercase branch (#397) closed --- e.g. `He said "stop that." and then left.` split falsely, as did README.md:190 and three changelog fragments. Drop the closer class entirely; the branch now requires the terminal punctuation to be immediately followed by whitespace, which keeps quotes, parens, emphasis, and ellipses on one line. Finding 2 (doc): the ellipsis exclusion was misattributed to the lookbehind (wait ends in two lowercase letters, so the lookbehind IS satisfied) --- it is the single-dot-consumption + \s+ requirement. Corrected in the code comment, CLAUDE.md, and the changelog fragment. Finding 3 (regression): the lowercase branch made the case-sensitive _ABBREV_RE reachable, so `3 sec. then` split falsely. Add re.IGNORECASE and drop 'No' from the list (a lowercase 'no.' is the English word and should still split). Finding 4 (tests): the decimal/version tests passed via the pre-existing \s+ requirement, not the lookbehind they were grouped under. Relabel them and add a digit-ending-token test (plan9. really) that genuinely exercises the lookbehind (mutation-verified), plus quoted-fragment, lowercase-abbrev, and no-as-a-word tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round 1 — ARD summary (all 4 findings Addressed)Thorough review, thank you — all four were real. Fixed in
Every fix was empirically verified against the real module, and each new guard-rail test was mutation-checked to confirm it goes red when its guard is removed. Full suite: 79 passed. All 4 threads resolved. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…ntroduced Round 1's finding-3 fix dropped 'No' from _ABBREVS to let a lowercase 'no.' split. But _ABBREV_RE runs before BOTH sentence-break branches, so that also un-protected 'No.' on the pre-existing uppercase branch: 'Item No. Three ...' false-split. Same bug class round 1 caught (an abbreviation edit for one branch regressing the other), reintroduced by the fix. Replace the blanket re.IGNORECASE with a scoped rule: protect each abbreviation in its conventional case AND its all-lowercase form, but not its all-caps form (so 'filed with the SEC. The case ...' still splits -- resolving the secondary IGNORECASE-over-widening finding too), and keep 'No' as a case-sensitive exception so 'No.' (number) stays protected while a lowercase 'no.' (word) splits. Add regression tests for both, mutation-verified. Also cross-reference the ai-config#1212 follow-up from CLAUDE.md's duplicate- regex note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round 2 — ARD summaryBoth round-2 findings Addressed in
Both got mutation-verified regression tests ( On the other soft observation (the "shape our prose writes most" phrasing appearing in three files): left as-is deliberately — CLAUDE.md, the changelog, and the code comment serve different audiences, and the phrase is a qualitative framing rather than the measured |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…rcase branch
Round 2's fix registered every abbreviation's lowercase form up front, which --
since _ABBREV_RE runs before BOTH branches -- also protected them on the
pre-existing uppercase branch, silently un-splitting genuine boundaries like
'It took 300 ms. The next run ...' (third occurrence of the same cross-branch
bug class).
The disambiguator is the follower's case: a lowercase unit before a lowercase
word ('3 sec. then') is mid-sentence, but before an uppercase word ('300 ms.
The') is a genuine boundary that must split. So protect conventional-case
abbreviations on both branches (unchanged) and protect the lowercase forms in a
SECOND pass (_ABBREV_LOWER_RE) applied only after the uppercase branch has run,
so they suppress the lowercase branch without reaching the uppercase one.
- Excludes 'no' (the word splits); adds min/hr/hrs (common bare time units in
this repo's timeout/duration prose) to cut a realistic false positive.
- Fixes finding 2: stale 'matched case-insensitively' docstring on
test_lowercase_abbreviation_before_lowercase_does_not_split.
- Adds test_lowercase_abbreviation_before_uppercase_does_split (mutation-
verified) and test_added_time_unit_before_lowercase_does_not_split.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round 3 — ARD summaryBoth round-3 findings Addressed in
New regression tests ( The min/Jan/Inc completeness point is now a disclosed limitation in the code comment and CLAUDE.md: the lowercase list is curated, not exhaustive, so an unlisted lowercase abbreviation before a lowercase word can still false-split on this warn-only check. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
… code change) Round 4 found no functional regressions -- the two-pass abbreviation mechanism is sound -- but the round-3 comment rewrite carried factual errors in the exact self-referential documentation this file relies on to prevent future regressions. All three fixes are comment/prose only; splitting behavior is unchanged. Each attribution is now mutation-verified rather than reasoned: 1. Ellipsis (wait... foo) is blocked by the LOOKBEHIND, not the immediate- whitespace guard: the only dot with a following space is the third, and its two preceding chars are both dots, so the lookbehind fails there. Corrected in the code comment, CLAUDE.md, the changelog, and the ellipsis test's docstring (all previously said the opposite). 2. Decimal/version: an INTERNAL dot (0.9012, v2.1 mid-token) is blocked by the \s+ requirement (no following space), but a version at a CLAUSE END (v2.1.) is blocked by the lookbehind (the trailing . does have a space). The blanket 'left intact by \s+' claim was imprecise. Added test_version_at_clause_end_does_not_split (mutation-verified). 3. Disclosed-limitation wording: the residual false-positive set is any unlisted abbreviation ending in two lowercase letters regardless of case (Inc., Prof., Mon.), not just lowercase abbreviations -- the lookbehind inspects trailing characters only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round 4 — ARD summaryRound 4 found no functional regressions (the two-pass mechanism is confirmed sound). All three findings were comment/documentation-accuracy issues in the self-referential "map future widenings are read against" block. Addressed in
No code or test behavior changed. Full suite: 84 passed. All 3 threads resolved. To avoid a further round on this exact class: every guard attribution in the comment, CLAUDE.md, and test docstrings is now backed by a mutation check (remove the guard, confirm the case starts splitting), rather than by plausible-sounding reasoning. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…c only) Round 5 confirmed all five prior regressions fixed and the regex/abbreviation logic fully sound. One doc-accuracy error remained: the new guard-2 comment inverted #397's history, claiming it 'dropped */_' to close 'an over-split'. Per #397 itself and CLAUDE.md:714 (unchanged), #397 was the closing class OMITTING */_, which swallowed '**bold.**' sentence ends (a false negative / under-split); it was fixed by ADDING those characters. Corrected at all three sites (check-new-line-breaks.py, CLAUDE.md, the test docstring) to the accurate framing: the uppercase branch safely carries closers because its uppercase-follower lookahead refuses a mid-construct lowercase continuation; the lowercase branch's follower is lowercase, so a closer there would over-split -- which is why this branch has no closing class. No code change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round 5 — ARD summaryRound 5 confirmed the regex/abbreviation logic is fully sound (all five prior regressions fixed, no ReDoS, no cross-branch leak, corpus scan clean). One doc-accuracy error remained: I had inverted #397's history at three sites — claiming it "dropped Corrected in |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…gelog wording Round 6 verdict was Ready for merge; these clear the two non-blocking items. - DRY: extract _abbrev_pattern() shared by _ABBREV_RE and _ABBREV_LOWER_RE. Both compiled patterns are byte-identical to before (verified), so behavior is unchanged -- _ABBREV_RE in particular stays exactly as it was. - Coverage: add test_incidental_lowercase_abbrev_before_lowercase_does_not_split for ms. + lowercase, the direction the reviewer noted was untested (ms enters the list incidentally as Ms.lower(), documented in the test). - Changelog: reword so ms. is not presented as a deliberately-curated addition; use the deliberate min. as the example instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round 6 — ARD summaryRound 6 verdict was Ready for merge; this clears the two non-blocking items so the PR is fully clean. Addressed in
85 passed, thread resolved. No functional change; the regex/abbreviation logic verified sound in round 5 is untouched. |
|
Claude finished review — View run Code review — #425This is a confirming review for round 7. The most recent prior review (round 6, comment, run) concluded Ready for merge with one non-blocking finding (the What I checked, all empirically against the actual code on disk (which reflects the PR's merged head) rather than by inspection alone:
One process note, not a PR defect: this session's working-tree No new issues found, and the prior round's one non-blocking item and DRY suggestion are both resolved in this commit. VerdictReady for merge |
Closes #389
Problem
check-new-line-breaks.py's sentence splitter required the next sentence tobegin with an uppercase letter or markup (
(?=[A-Z"'`*[])`), so a lineopening its second sentence with a bare lowercase word read as one sentence and
shipped unflagged:
That is exactly the shape our prose writes most, since it routinely opens a
sentence with a lowercase package or repo name (
renv,serodynamics,dplyr). The failure direction is a false negative — nothing turns red — so aclean report was weaker evidence than it looked.
Fix
Add a second regex branch,
_SENT_BREAK_LOWER_RE, for the lowercase-followercase, applied after the existing (unchanged) uppercase branch:
The two-lowercase-letter lookbehind
(?<=[a-z][a-z])is the guard theuppercase lookahead gave the other branch for free: it splits only when the
previous sentence ends in a genuine word (≥2 trailing lowercase letters), which
rejects every documented false-positive shape:
v2.1,0.9012.)U.S. economy.follows a single uppercase lettera.m. sharp.follows.m, not two letterswait... foo.follows..Option a. reallyais a one-letter tokenIt is **critical.** yet*/_(preserves #397's guard)Tests
Adds 7 tests: 2 positive (the reprex cases, confirmed to fail against
pre-fix code) and 5 negative guard-rail cases. Full suite: 75 passed.
Follow-up
The same regex is duplicated in ai-config's
scripts/semantic-line-breaks.py(the reformatter this check is the detector half of); filed
Morrison-Lab/ai-config#1212 to port this fix there.