# Expert Review Panel: v1.4.1 Changes

**Date:** 2026-03-09
**Scope:** Only today's changes — transliteration feature, actions.py crash fix, PR #16 bound check fix
**Panel:** QA, MultiOS, CLI/TUI, Packaging, Performance, Python Code Quality

---

## Executive Summary

| Severity | Count | Status |
|----------|-------|--------|
| CRITICAL | 1 | Must fix before push |
| MEDIUM | 4 | Should fix before push |
| LOW | 6 | Optional improvements |

**Blocking issue:** CI will fail — tests assert `result.isascii()` but `anyascii` isn't in `dev` deps.

---

## CRITICAL Findings

### C1: Tests break in CI without anyascii [Packaging Expert]

**File:** `pyproject.toml:57-62` + `tests/test_utils/test_transliteration.py:45-72`

CI installs `pip install -e ".[dev]"`. The `dev` extra does NOT include `anyascii`. Five tests assert `result.isascii()` on transliterated output — these pass locally because `.venv` has anyascii installed, but will **fail in CI** where only `dev` deps are installed.

Affected tests: `test_transliterate_japanese`, `test_transliterate_korean`, `test_transliterate_chinese`, `test_transliterate_cyrillic`, `test_transliterate_mixed_keeps_ascii`

**Fix:** Add `anyascii>=0.3.0` to the `dev` extra in `pyproject.toml`.

**Panel consensus:** 6/6 AGREE. No debate — this is a straightforward omission.

---

## MEDIUM Findings

### M1: Toggle state not persisted across restarts [QA + Code Quality + CLI/TUI]

**File:** `lyrics_sidebar.py:464-470`

`toggle_transliteration()` flips `self._transliteration_enabled` in memory but never saves it. On restart, it reverts to `config.toml` default (`false`). Other toggle states (shuffle, repeat) persist via `session.json`.

**Panel debate:**

| Expert | Position | Score |
|--------|----------|-------|
| QA | Should persist in session.json (consistent with shuffle/repeat) | 8/10 |
| Code Quality | Design decision — ephemeral is valid if intentional | 6/10 |
| CLI/TUI | Users expect display preferences to survive restart | 8/10 |
| Performance | No opinion | — |
| MultiOS | No opinion | — |
| Packaging | No opinion | — |

**Consensus (4/4 voting):** Persist in `session.json`. Average score: 7.3/10 importance.

**Fix:** Add `"transliteration_enabled"` to `_save_session_state()` and restore in `_restore_session_state()`.

---

### M2: Scroll position and highlight reset on toggle [QA + CLI/TUI]

**File:** `lyrics_sidebar.py:359, 464-470`

When toggling transliteration, `_build_synced_view()` resets `current_line_index = -1` and destroys all widgets. The user sees a ~1s flash of unhighlighted lyrics before the next position tick re-syncs.

**Panel debate:**

| Expert | Position | Score |
|--------|----------|-------|
| QA | Save and restore current_line_index across rebuild | 7/10 |
| CLI/TUI | Force immediate position re-sync after rebuild | 8/10 |
| Performance | Immediate re-sync is ~0 cost, worth doing | 7/10 |

**Consensus (3/3 voting):** Force immediate position re-sync. Average score: 7.3/10.

**Fix:**
```python
def toggle_transliteration(self) -> None:
    self._transliteration_enabled = not self._transliteration_enabled
    if self._is_synced and self._synced_lines:
        self._build_synced_view()
        player = getattr(self.app, "player", None)
        if player and player.position is not None:
            self._on_position_change(player.position)
    elif self._unsynced_lines:
        self._build_unsynced_view()
```

---

### M3: AUR PKGBUILD missing anyascii optdepends [Packaging]

**File:** `aur/PKGBUILD:21-27`

Every other optional feature (MPRIS, Discord, Last.fm, Spotify) has an `optdepends` entry. `python-anyascii` is missing. Package exists in Arch's official `extra` repo.

**Panel consensus:** 6/6 AGREE. Straightforward omission.

**Fix:** Add `'python-anyascii: lyrics transliteration to ASCII'` to `optdepends` in both `aur/PKGBUILD` and `aur/.SRCINFO`.

---

### M4: View rebuild mounts widgets individually instead of batching [Performance]

**File:** `lyrics_sidebar.py:361-372, 380-390`

Each `_LyricLine` and transliteration `Static` is mounted one at a time via `scroll.mount(widget)`. For a 100-line song with transliteration, that's 200 individual mounts. Batching with `scroll.mount(*children)` would be a single layout pass.

**Panel debate:**

| Expert | Position | Score |
|--------|----------|-------|
| Performance | Batch mount — measurable improvement on toggle (50-150ms → single pass) | 7/10 |
| CLI/TUI | Batch mount is better Textual practice | 7/10 |
| QA | More moving parts in refactor, but clean win | 6/10 |
| Code Quality | Cleaner code, fewer side effects | 7/10 |

**Consensus (4/4 voting):** Batch mount. Average score: 6.75/10.

**Fix:** Collect all widgets in a list, then `scroll.mount(*children)` once. Apply to both `_build_synced_view()` and `_build_unsynced_view()`.

---

## LOW Findings

### L1: `has_non_ascii()` should use `str.isascii()` [Performance + Code Quality]

**File:** `transliteration.py:8-14`

Current: `text.encode("ascii")` + try/except. Creates throwaway bytes object, uses exception for control flow.
Better: `return bool(text) and not text.isascii()`. 9x faster for non-ASCII, single line, zero allocation.

**Consensus:** 5/6 AGREE. Trivial fix.

### L2: No tests for `_build_actions` function [QA]

**File:** `ui/popups/actions.py`

The crash was in production with zero test coverage. Fix is correct, but no regression test prevents future breakage.

**Consensus:** 4/6 AGREE (Performance, MultiOS abstain). Worth adding but not blocking.

### L3: Dead code — sidebar `handle_action` for TOGGLE_TRANSLITERATION [CLI/TUI + Code Quality]

**File:** `lyrics_sidebar.py:488-489`

The `TOGGLE_TRANSLITERATION` case in sidebar's `handle_action` is unreachable — the action is always handled by app-level dispatch at `app.py:916-920`. The sidebar's `handle_action` is only called for page widgets, not sidebars.

**Consensus:** 3/6 AGREE to remove. Others say it's harmless defense-in-depth.

### L4: No notification toast on toggle [CLI/TUI]

No `self.app.notify(...)` when transliteration is toggled. Compare: shuffle toggle shows a notification. Users get no feedback if anyascii isn't installed (toggle appears to do nothing).

**Consensus:** 4/6 AGREE. Good UX polish, especially for the "anyascii not installed" case.

### L5: `transliterate_line(None)` returns `None` [QA + Code Quality]

**File:** `transliteration.py:24`

Function signature says `str -> str` but `None` input returns `None`. Current callers guard against this, so no crash in practice.

**Consensus:** 3/6 AGREE to fix. Others say callers already protect it.

### L6: No test for session restore with filtered tracks [QA]

The bound check fix is correct, but there's no integration test for the scenario where `normalize_tracks` drops tracks and saved_index exceeds normalized length.

**Consensus:** 3/6 AGREE. Individual pieces are tested; integration test would be nice-to-have.

---

## Cross-Platform Assessment [MultiOS Expert]

| Change | Linux | macOS | Windows | Notes |
|--------|-------|-------|---------|-------|
| Transliteration utility | PASS | PASS | PASS | `str.encode("ascii")` is platform-agnostic |
| anyascii library | PASS | PASS | PASS | Pure Python, no C extensions |
| `[lyrics]` config section | PASS | PASS | PASS | Backward compatible, uses `encoding="utf-8"` |
| Terminal rendering | PASS | PASS | PASS | Output is pure ASCII by definition |
| actions.py fix | PASS | PASS | PASS | Python-level bug, not OS-specific |
| Session restore fix | PASS | PASS | PASS | Uses `encoding="utf-8"` for session.json |

**No platform-specific concerns.** All changes are clean across all 3 supported OSes.

---

## Agreed Action Items (Priority Order)

| # | Finding | Severity | All Agree? | Action |
|---|---------|----------|------------|--------|
| 1 | C1: anyascii missing from dev deps | CRITICAL | Yes (6/6) | Add to `dev` extra in pyproject.toml |
| 2 | M1: Toggle not persisted | MEDIUM | Yes (4/4) | Add to session.json save/restore |
| 3 | M2: Scroll/highlight reset on toggle | MEDIUM | Yes (3/3) | Force immediate position re-sync |
| 4 | M3: AUR optdepends missing | MEDIUM | Yes (6/6) | Add python-anyascii to PKGBUILD |
| 5 | M4: Individual widget mounts | MEDIUM | Yes (4/4) | Batch mount with `mount(*children)` |
| 6 | L1: Use `str.isascii()` | LOW | Yes (5/6) | One-line change |
| 7 | L4: No toggle notification | LOW | Yes (4/6) | Add notify toast |

Items L2, L3, L5, L6 are optional and can be deferred.

---

## Final Panel Verdict

**Ship-ready after fixing C1 + M1-M4.** No architectural concerns, no security issues, no platform-specific problems. The transliteration feature is well-designed with proper optional-dependency handling. The crash fix and bound check fix are both correct and minimal.
