Files
unredact/docs/plans/2026-02-22-known-start-end-casing-fix-impl.md
T
2026-02-23 21:17:22 -06:00

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"