# Cover Refinements Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Improve cover headline rendering by filtering out obituaries, increasing font size, and using pixel-width truncation. **Architecture:** Three isolated changes that converge on the cover: (1) a new `_truncate_to_width` helper in `cover.py` replaces character-count truncation with Pillow `textbbox` measurement, combined with font/spacing bumps; (2) the three call sites that build headline lists gain an obituary filter; (3) tests validate both behaviors. **Tech Stack:** Python, Pillow (PIL), Flask, SQLAlchemy --- ### Task 1: Add pixel-width truncation helper and update font/spacing in cover.py **Files:** - Modify: `src/cover.py:73-114` (the `_draw_text_overlays` function) - Test: `tests/test_cover.py` - [ ] **Step 1: Write failing test for pixel-width truncation** Add a new test to `tests/test_cover.py` that verifies headlines are truncated based on pixel width, not character count. Import `_draw_text_overlays` and the helper. ```python # Add to imports at top of tests/test_cover.py: from PIL import ImageDraw, ImageFont from src.cover import _truncate_to_width, _get_font # Add this test: def test_truncate_to_width_respects_pixel_budget(): font = _get_font(18) short = "Short text" result = _truncate_to_width(short, font, 400) assert result == short, "Short text should not be truncated" long = "A" * 200 result = _truncate_to_width(long, font, 400) assert result.endswith("\u2026"), "Long text should end with ellipsis" assert len(result) < len(long), "Truncated text should be shorter" def test_truncate_to_width_fits_within_budget(): font = _get_font(18) long = "This is a really long headline that should definitely be truncated to fit" max_width = 448 result = _truncate_to_width(long, font, max_width) dummy = PILImage.new("RGBA", (1, 1)) draw = ImageDraw.Draw(dummy) bbox = draw.textbbox((0, 0), result, font=font) rendered_width = bbox[2] - bbox[0] assert rendered_width <= max_width, f"Rendered width {rendered_width} exceeds {max_width}" ``` - [ ] **Step 2: Run tests to verify they fail** Run: `python -m pytest tests/test_cover.py::test_truncate_to_width_respects_pixel_budget tests/test_cover.py::test_truncate_to_width_fits_within_budget -v` Expected: FAIL with `ImportError` (cannot import `_truncate_to_width`) - [ ] **Step 3: Implement `_truncate_to_width` and update `_draw_text_overlays`** In `src/cover.py`, add the new helper function after `_get_dominant_category` (after line 70): ```python def _truncate_to_width( text: str, font: ImageFont.FreeTypeFont, max_width: int ) -> str: dummy = PILImage.new("RGBA", (1, 1)) draw = ImageDraw.Draw(dummy) bbox = draw.textbbox((0, 0), text, font=font) if (bbox[2] - bbox[0]) <= max_width: return text ellipsis = "\u2026" for i in range(len(text) - 1, 0, -1): candidate = text[:i] + ellipsis bbox = draw.textbbox((0, 0), candidate, font=font) if (bbox[2] - bbox[0]) <= max_width: return candidate return ellipsis ``` Then update `_draw_text_overlays` — change line 83 and lines 101-114: ```python headline_font = _get_font(18) ``` And replace the headline strip block: ```python if headlines: max_headlines = min(len(headlines), 5) strip_height = 28 + max_headlines * 26 strip_top = height - strip_height draw.rectangle( [(0, strip_top), (width, height)], fill=(0, 0, 0, 170) ) max_width = width - 32 y = strip_top + 8 for headline in headlines[:max_headlines]: text = f"\u2022 {headline}" text = _truncate_to_width(text, headline_font, max_width) draw.text((16, y), text, fill="white", font=headline_font) y += 26 ``` - [ ] **Step 4: Run tests to verify they pass** Run: `python -m pytest tests/test_cover.py -v` Expected: ALL PASS (including the existing tests which verify image size/format) - [ ] **Step 5: Commit** ```bash git add src/cover.py tests/test_cover.py git commit -m "feat(cover): pixel-width headline truncation and larger font Replace character-count truncation (45 chars) with Pillow textbbox pixel-width measurement. Bump headline font 14→18, line spacing 22→26." ``` --- ### Task 2: Filter obituaries from cover headlines in publish.py **Files:** - Modify: `src/routes/publish.py:162-169` - [ ] **Step 1: Update headline and category list building to filter obituaries** In `src/routes/publish.py`, replace lines 162-169 in the `create_issue` function: ```python articles_for_issue = ( Article.query.filter(Article.id.in_(included_ids)) .order_by(Article.pub_date.asc()) .all() ) headlines = [ a.title for a in articles_for_issue if "Obituaries" not in json.loads(a.categories) ] categories_list = [] for a in articles_for_issue: categories_list.extend(json.loads(a.categories)) ``` This queries articles once (instead of twice), filters obituaries from headlines only, and keeps all categories for AI prompt selection. - [ ] **Step 2: Run existing tests** Run: `python -m pytest -v` Expected: ALL PASS - [ ] **Step 3: Commit** ```bash git add src/routes/publish.py git commit -m "feat(cover): filter obituaries from headlines in manual publish" ``` --- ### Task 3: Filter obituaries from cover headlines in issues.py **Files:** - Modify: `src/routes/issues.py:106-113` - [ ] **Step 1: Update the regenerate route's headline building** In `src/routes/issues.py`, replace lines 106-113 in the `regenerate` function: ```python articles_for_issue = ( Article.query.filter(Article.id.in_(article_ids)) .order_by(Article.pub_date.asc()) .all() ) headlines = [ a.title for a in articles_for_issue if "Obituaries" not in json.loads(a.categories) ] categories_list = [] for a in articles_for_issue: categories_list.extend(json.loads(a.categories)) ``` - [ ] **Step 2: Run existing tests** Run: `python -m pytest -v` Expected: ALL PASS - [ ] **Step 3: Commit** ```bash git add src/routes/issues.py git commit -m "feat(cover): filter obituaries from headlines in issue regenerate" ``` --- ### Task 4: Filter obituaries from cover headlines in scheduler.py **Files:** - Modify: `src/scheduler.py:78-83` - [ ] **Step 1: Update auto-publish headline building** In `src/scheduler.py`, replace lines 78-83 in the `_run_auto_publish` method: ```python article_ids = [a.id for a in articles] headlines = [ a.title for a in articles if "Obituaries" not in json.loads(a.categories) ] categories_list = [] for a in articles: categories_list.extend(json.loads(a.categories)) ``` - [ ] **Step 2: Run existing tests** Run: `python -m pytest -v` Expected: ALL PASS - [ ] **Step 3: Commit** ```bash git add src/scheduler.py git commit -m "feat(cover): filter obituaries from headlines in auto-publish" ``` --- ### Task 5: Add test for obituary filtering **Files:** - Test: `tests/test_cover.py` - [ ] **Step 1: Write an integration test that verifies obituaries are excluded** Since the obituary filtering happens at the call site (not inside `cover.py`), this test validates the filtering pattern used across all three call sites. Add to `tests/test_cover.py`: ```python import json def test_obituary_headlines_filtered(): """Verify the filtering pattern used by publish/issues/scheduler call sites.""" class FakeArticle: def __init__(self, title, categories): self.title = title self.categories = json.dumps(categories) articles = [ FakeArticle("Town Meeting Approves Budget", ["Government"]), FakeArticle("John Smith, 85, beloved teacher", ["Obituaries"]), FakeArticle("Harbor Festival This Weekend", ["Culture"]), FakeArticle("Jane Doe, 92, retired nurse", ["Obituaries"]), FakeArticle("Panthers Win Championship", ["Sports"]), ] headlines = [ a.title for a in articles if "Obituaries" not in json.loads(a.categories) ] assert len(headlines) == 3 assert "John Smith, 85, beloved teacher" not in headlines assert "Jane Doe, 92, retired nurse" not in headlines assert "Town Meeting Approves Budget" in headlines assert "Harbor Festival This Weekend" in headlines assert "Panthers Win Championship" in headlines ``` - [ ] **Step 2: Run the new test** Run: `python -m pytest tests/test_cover.py::test_obituary_headlines_filtered -v` Expected: PASS - [ ] **Step 3: Commit** ```bash git add tests/test_cover.py git commit -m "test: add obituary headline filtering test" ```