6.1 KiB
Known Start/End Casing Fix Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Fix incorrect pixel width calculation when using known_start/known_end in full name and single name dictionary solving.
Architecture: Extract a helper _case_unknown_portion() that applies capitalized casing with word-boundary awareness to the unknown portion of a name. Replace the blanket .lower() override in both solve functions.
Tech Stack: Python, PIL/Pillow fonts, pytest
Task 1: Add failing test for multi-word full name casing
Files:
- Modify:
tests/test_full_name_dictionary.py
Step 1: Write the failing test
Add to TestSolveFullNameDictionary:
@patch("unredact.pipeline.word_filter._get_associate_variants")
def test_known_start_multiword_casing(self, mock_variants):
"""Unknown portion of multi-word name should preserve word-boundary casing.
Name: 'john doe', known_start='jo', unknown='hn Doe' (not 'hn doe').
The width measurement must use 'hn Doe' not 'hn doe'.
"""
mock_variants.return_value = ["john doe"]
# Mock: measure "ohn Doe" with left context "o" from known_start
# font.getlength("ohn Doe") should be called, NOT "ohn doe"
font = _mock_font({
"ohn Doe": 42.0, # correct cased unknown
"ohn doe": 38.0, # wrong (current bug: all-lowercase)
})
results = solve_full_name_dictionary(
font, 42.0, 1.0, known_start="o", casing="capitalized",
)
texts = [r.text for r in results]
assert "John Doe" in texts
Step 2: Run test to verify it fails
Run: pytest tests/test_full_name_dictionary.py::TestSolveFullNameDictionary::test_known_start_multiword_casing -v
Expected: FAIL — currently measures "ohn doe" (38.0) which is outside tolerance of 42.0 ± 1.0
Task 2: Add helper _case_unknown_portion and fix solve_full_name_dictionary
Files:
- Modify:
unredact/pipeline/dictionary.py
Step 1: Add the helper function after _apply_casing
def _case_unknown_portion(unknown: str, known_start: str, casing: str) -> str:
"""Apply casing to the unknown portion of a name, respecting word boundaries.
When known_start is set and casing is 'capitalized', the unknown text
starts mid-word — its first fragment should be lowercase, but subsequent
words (after spaces) should be title-cased.
"""
if casing == "uppercase":
return unknown.upper()
if casing != "capitalized":
return unknown # lowercase — no change
if not known_start:
return unknown.title()
# If known_start ends with a space, unknown starts a new word
if known_start.endswith(" "):
return unknown.title()
# Unknown starts mid-word: first fragment lowercase, rest title-cased
parts = unknown.split(" ")
result = [parts[0].lower()]
for part in parts[1:]:
result.append(part.title() if part else "")
return " ".join(result)
Step 2: Replace lines 121-123 in solve_full_name_dictionary
Change:
unknown_display = _apply_casing(unknown_raw, casing)
if known_start and casing == "capitalized":
unknown_display = unknown_raw.lower()
To:
unknown_display = _case_unknown_portion(unknown_raw, known_start, casing)
Step 3: Fix effective_left to use cased known_start (line 125)
Change:
effective_left = known_start[-1] if known_start else left_context
To:
effective_left = _apply_casing(known_start, casing)[-1] if known_start else left_context
Step 4: Run the failing test to verify it passes
Run: pytest tests/test_full_name_dictionary.py -v
Expected: ALL PASS
Task 3: Add failing test for single name casing and fix solve_name_dictionary
Files:
- Modify:
tests/test_name_dictionary.py - Modify:
unredact/pipeline/dictionary.py
Step 1: Write the failing test
Add to TestSolveNameDictionary:
@patch("unredact.pipeline.word_filter._get_associate_firsts")
@patch("unredact.pipeline.word_filter._get_associate_lasts")
def test_known_start_capitalized_casing(self, mock_lasts, mock_firsts):
"""With known_start and capitalized casing, unknown part is mid-word lowercase."""
mock_firsts.return_value = ["john"]
mock_lasts.return_value = []
# known_start="j", unknown="ohn", should measure "ohn" (lowercase)
font = _mock_font({"ohn": 21.0})
results = solve_name_dictionary(
font, 21.0, 1.0, known_start="j", casing="capitalized",
)
texts = [r.text for r in results]
assert "John" in texts
Step 2: Run test to verify it passes (existing behavior is correct for single names)
Run: pytest tests/test_name_dictionary.py::TestSolveNameDictionary::test_known_start_capitalized_casing -v
Expected: PASS (single-word lowercase already works)
Step 3: Replace lines 218-225 in solve_name_dictionary
Change:
# Apply same casing to the unknown portion
if casing == "uppercase":
unknown_display = unknown.upper()
elif casing == "capitalized":
# Mid-word after known_start: lowercase. At word start: title case.
unknown_display = unknown.lower() if known_start else unknown.title()
else:
unknown_display = unknown
To:
unknown_display = _case_unknown_portion(unknown, known_start, casing)
Step 4: Fix effective_left to use cased known_start (line 229)
Change:
effective_left = known_start[-1] if known_start else left_context
To:
effective_left = _apply_casing(known_start, casing)[-1] if known_start else left_context
Step 5: Run all tests to verify nothing broke
Run: pytest tests/test_name_dictionary.py tests/test_full_name_dictionary.py -v
Expected: ALL PASS
Task 4: Commit
Step 1: Commit the changes
git add unredact/pipeline/dictionary.py tests/test_full_name_dictionary.py tests/test_name_dictionary.py
git commit -m "fix: known_start/end casing for multi-word name width calculation"