# Track Plan: Default Layout Extract + Hard Visual Verification > **For Tier-3 workers:** Steps use checkbox (`- [ ]`) syntax. Use exactly **1-space indentation** for all Python. Preserve **CRLF** line endings. No comments in source code. Atomic commits per task. No `dict[str, Any]`, no `Optional[T]` returns (use `Result[T]` + `NIL_T`). Read `src/gui_2.py:1481-1540` (tier-2 version) for the install helper pattern reference; read `src/theme_models.py:181-225` for the layouts loader pattern reference; read `src/paths.py:60-83,150,209-216,295` for the themes → layouts mirror. **Goal:** Extract tier-2's GOOD default-layout work into master AND build a hard 4-layer visual verification infrastructure that catches "panels don't render" regressions every time. **Architecture:** Hybrid extraction (C per spec §FR1): port `layouts/default.ini` + `src/layouts.py` + `tests/test_layout_reorganization.py` fresh (clean history for new modules); cherry-pick `c2155593` (orphan end_child) + `3b966288` (reset_layout cleanup); add new `_install_default_layout_*` helpers + `App._post_init` + `App.run` wiring. Build 4 verification layers: per-panel render sentinel (Layer 1), Win32 PrintWindow pixel baseline (Layer 2), forced test viewport+theme env vars (Layer 3), cannot-skip gates (Layer 4: standalone CLI + CI integration + tag requirement + tracks.md schema). **Tech Stack:** Python 3.11+, `imgui-bundle` (HelloImGui), `pywin32` (PrintWindow), `Pillow` (PNG), `numpy` (pixel diff), `pytest` + `live_gui` fixture. Adds `scripts/check_visual_baseline.py` (new audit-style script). --- ## Phase 1: Asset Foundation (layouts/ + src/layouts.py + get_layouts_dir) Focus: Port the foundational assets from tier-2 to master with clean history. - [ ] **Task 1.1: RED test for `src/layouts.py:load_layouts_from_dir`** - WHERE: New file `tests/test_layouts.py` - WHAT: Write 3 tests: 1. `test_load_layouts_from_dir_empty` — pass a non-existent path → returns `{}` 2. `test_load_layouts_from_dir_single_file` — create tmp dir with one `.ini` file → returns 1-entry dict keyed by stem 3. `test_load_layouts_from_dir_skips_non_ini` — tmp dir with `.ini` + `.txt` → returns only the `.ini` - HOW: Use `tmp_path` fixture (already redirected under `tests/artifacts/_pytest_tmp` per `pyproject.toml:addopts`). Import `from src.layouts import load_layouts_from_dir`. - SAFETY: Use `tmp_path`, not hardcoded paths. 1-space indentation. Type hints required. - RUN: `uv run pytest tests/test_layouts.py -v` — Expected: `ModuleNotFoundError: No module named 'src.layouts'`. - COMMIT: `test(layouts): RED phase tests for load_layouts_from_dir` - [ ] **Task 1.2: Create `src/layouts.py`** - WHERE: New file `src/layouts.py` (87 lines, ported fresh from tier-2's `C:\projects\manual_slop_tier2\src\layouts.py`) - WHAT: Define `LayoutFile` dataclass + `load_layouts_from_file()` + `load_layouts_from_dir()` + `load_layouts_from_disk()` + `_LAYOUTS_CACHE: dict[str, LayoutFile]` - HOW: Read tier-2 file; copy verbatim EXCEPT: strip the "TODO(Ed)" comment (NFR3); keep the `Result` + `ErrorInfo` drain pattern from tier-2 verbatim; keep `_LAYOUTS_CACHE` module-level - SAFETY: 1-space indentation. CRLF. `@dataclass(frozen=True, slots=True)`. Type hints on all params + returns. - RUN: `uv run pytest tests/test_layouts.py -v` — Expected: 3 PASS. - COMMIT: `feat(layouts): introduce src/layouts.py + LayoutFile dataclass` - [ ] **Task 1.3: RED test for `src/paths.py:get_global_layouts_path`** - WHERE: New file `tests/test_paths_layouts.py` - WHAT: Write 4 tests: 1. `test_get_global_layouts_path_default` — `initialize_paths()` called, `get_global_layouts_path()` returns `/layouts` 2. `test_get_global_layouts_path_env_override` — `SLOP_GLOBAL_LAYOUTS` env var set → returns that path 3. `test_layouts_in_path_info_dict` — `paths.path_info()` dict has `'layouts': info(...)` entry 4. `test_layouts_field_in_app_paths` — `_AppPaths().layouts` is a `Path` - HOW: Import `from src.paths import get_global_layouts_path, initialize_paths, _cfg`. Use `monkeypatch.setenv("SLOP_GLOBAL_LAYOUTS", str(tmp_path / "custom"))`. - SAFETY: Call `initialize_paths()` once per test (use fixture). 1-space indentation. - RUN: `uv run pytest tests/test_paths_layouts.py -v` — Expected: `AttributeError: module 'src.paths' has no attribute 'get_global_layouts_path'`. - COMMIT: `test(paths): RED phase tests for get_global_layouts_path + SLOP_GLOBAL_LAYOUTS` - [ ] **Task 1.4: Add `get_global_layouts_path()` to `src/paths.py`** - WHERE: `src/paths.py` — 4 sites: line 60 `_AppPaths` dataclass (add `layouts: Path`), line 83 `_PATHS_DEFAULTS` (add `layouts = root_dir / "layouts"`), line 150 `initialize_paths._resolve_path` chain (add `SLOP_GLOBAL_LAYOUTS` env override), line 295 `path_info()` (add `'layouts': info(cfg.layouts)`), line 209-216 (add `get_global_layouts_path()` mirror of `get_global_themes_path()`) - WHAT: Mirror the themes pattern exactly. New code follows the existing 1-space indentation + CRLF. - HOW: Read `src/paths.py:60` → insert `layouts: Path` after `themes: Path`. Read `src/paths.py:83` → insert `themes = root_dir / "layouts"` after `themes = root_dir / "themes"`. Read `src/paths.py:150` → add `themes = _resolve_path("SLOP_GLOBAL_LAYOUTS", "layouts", root_dir / "layouts", config_path)` to the resolver chain. Read `src/paths.py:209-216` → copy `get_global_themes_path()` verbatim and rename. Read `src/paths.py:295` → insert `'layouts': info(cfg.layouts)` after `'themes': info(cfg.themes)`. - SAFETY: Match existing 1-space indent. CRLF. No comments in source. Update `_resolve_path` keyword args to match the same shape as the themes line. - RUN: `uv run pytest tests/test_paths_layouts.py -v` — Expected: 4 PASS. - COMMIT: `feat(paths): add get_global_layouts_path() + SLOP_GLOBAL_LAYOUTS env override (mirror of themes)` - [ ] **Task 1.5: RED test for bundled INI file** - WHERE: New file `tests/test_layouts_bundled.py` - WHAT: Write 4 tests: 1. `test_layouts_default_ini_exists` — `Path("layouts/default.ini").exists()` is True 2. `test_layouts_default_ini_size` — file size > 1000 bytes 3. `test_layouts_default_ini_has_docking` — content contains `[Docking][Data]` 4. `test_layouts_default_ini_has_8_windows` — content has 8 `[Window][X]` entries - HOW: Use `Path.cwd() / "layouts" / "default.ini"`. Use `len(re.findall(r"^\[Window\]\[", content))` for window count. - SAFETY: 1-space indent. CRLF. Read with `encoding="utf-8"`. - RUN: `uv run pytest tests/test_layouts_bundled.py -v` — Expected: `FileNotFoundError: layouts/default.ini`. - COMMIT: `test(layouts): RED phase tests for bundled default.ini structure` - [ ] **Task 1.6: Port `layouts/default.ini` to master** - WHERE: New file `layouts/default.ini` at repo root - WHAT: Copy verbatim from tier-2's `C:\projects\manual_slop_tier2\layouts\default.ini` (2971 bytes, 101 lines). Strip the `;;;` documentation comments (NFR3: comments live in docs). Strip the `;;;<<>>;;;` block at line 100-101 (HelloImGui adds that on save; not needed in the bundle). - HOW: Read tier-2 file → write fresh to `layouts/default.ini`. Keep all `[Window][X]` entries (8 of them), `[Docking][Data]` block with `DockSpace ID=0xAFC85805`, `[Layout]`, `[StatusBar]`, `[Theme]` sections. - SAFETY: CRLF. No `;;;` lines. Final file should be ~30-40 lines. - RUN: `uv run pytest tests/test_layouts_bundled.py -v` — Expected: 4 PASS. - COMMIT: `feat(layouts): bundle layouts/default.ini with 8 [Window] entries + [Docking] hierarchy` --- ## Phase 2: Install Helpers (RED-GREEN for the 3 helpers) Focus: Add `_install_default_layout_if_empty`, `_install_default_layout_if_empty_result`, `_install_default_layout_pre_run_result` to `src/gui_2.py`. - [ ] **Task 2.1: RED test for `_install_default_layout_if_empty` (empty dst)** - WHERE: New file `tests/test_install_default_layout.py` - WHAT: Write 5 tests: 1. `test_install_empty_dst` — dst INI is empty/missing → src content copied to dst + `Result(data=True)` 2. `test_install_skips_non_empty_dst` — dst INI has 5+ `[Window][` entries → no overwrite + `Result(data=False)` 3. `test_install_handles_missing_src` — src INI doesn't exist → `Result(data=False, errors=[ErrorInfo])` 4. `test_install_handles_oserror_on_read` — patch `Path.read_text` to raise OSError → `Result(data=False, errors=[ErrorInfo])` 5. `test_install_calls_load_ini_settings_from_memory` — assert `imgui.load_ini_settings_from_memory` was called once - HOW: Use `tmp_path`. Import `from src.gui_2 import _install_default_layout_if_empty`. Use `monkeypatch.setattr(imgui, "load_ini_settings_from_memory", lambda x: None)` for test 5. - SAFETY: 1-space indent. CRLF. Mock only the boundary (`imgui.load_ini_settings_from_memory` is the SDK boundary). - RUN: `uv run pytest tests/test_install_default_layout.py -v` — Expected: `ImportError: cannot import name '_install_default_layout_if_empty'`. - COMMIT: `test(install): RED phase tests for _install_default_layout_if_empty` - [ ] **Task 2.2: Implement `_install_default_layout_if_empty` in `src/gui_2.py`** - WHERE: `src/gui_2.py` — insert at line 1481 (before `_post_init_callback_result` which is at 1449 — actually place the new helpers AFTER `_post_init_callback_result`) - WHAT: Port tier-2's `src/gui_2.py:1481-1530` verbatim. Adjust imports if needed (`Result`, `ErrorInfo`, `ErrorKind` already imported via `src.result_types`). - HOW: Read tier-2's lines 1481-1530 → copy to master. Strip docstring multi-line commentary to 1-2 lines (NFR3). The function returns `Result[bool]`. - SAFETY: 1-space indent. CRLF. No comments. Match existing `_post_init_callback_result` shape. - RUN: `uv run pytest tests/test_install_default_layout.py -v` — Expected: 5 PASS. - COMMIT: `feat(gui): add _install_default_layout_if_empty + _install_default_layout_if_empty_result helpers` - [ ] **Task 2.3: RED test for `_install_default_layout_pre_run_result` (disk-only)** - WHERE: Append to `tests/test_install_default_layout.py` - WHAT: Write 3 tests: 1. `test_pre_run_install_empty_dst` — same as 2.1.1 but using `_install_default_layout_pre_run_result` and mocking `_require_warmed("src.layouts")` 2. `test_pre_run_install_does_not_call_load_ini_settings_from_memory` — assert `imgui.load_ini_settings_from_memory` was NOT called (imgui not initialized yet) 3. `test_pre_run_install_skips_non_empty_dst` — same as 2.1.2 - HOW: Same `tmp_path` pattern. Mock `src.layouts.get_layouts_dir` to return `tmp_path / "layouts"`. - SAFETY: 1-space indent. CRLF. Verify `load_ini_settings_from_memory` was NOT called (it's the key behavioral difference vs `_install_default_layout_if_empty`). - RUN: `uv run pytest tests/test_install_default_layout.py -v` — Expected: 3 new FAIL (`ImportError: cannot import name '_install_default_layout_pre_run_result'`). - COMMIT: `test(install): RED phase tests for _install_default_layout_pre_run_result` - [ ] **Task 2.4: Implement `_install_default_layout_pre_run_result` in `src/gui_2.py`** - WHERE: `src/gui_2.py` — insert immediately after `_install_default_layout_if_empty_result` (which Task 2.2 placed) - WHAT: Port tier-2's `src/gui_2.py:1543-1590` verbatim. The function reads `get_layouts_dir() / "default.ini"` and writes to `Path.cwd() / "manualslop_layout.ini"`. NO `imgui.load_ini_settings_from_memory` call. - HOW: Read tier-2's lines 1543-1590 → copy to master. Adjust imports. - SAFETY: 1-space indent. CRLF. No comments. The disk-only behavior is the key contract; the function does NOT import or call `imgui`. - RUN: `uv run pytest tests/test_install_default_layout.py -v` — Expected: 8 PASS (5 from 2.1 + 3 new). - COMMIT: `feat(gui): add _install_default_layout_pre_run_result (disk-only, no live-session apply)` --- ## Phase 3: Wiring (App._post_init + App.run) Focus: Wire the install helpers into the app's startup flow. - [ ] **Task 3.1: RED test for `App._post_init` calling `_install_default_layout_if_empty_result`** - WHERE: New file `tests/test_app_wiring_install.py` - WHAT: Write 3 tests: 1. `test_post_init_calls_install_helper` — instantiate `App`, call `_post_init()`, assert `_install_default_layout_if_empty_result` was called with `src=layouts/default.ini, dst=cwd/manualslop_layout.ini` 2. `test_post_init_drains_install_errors` — make install helper return `Result(data=False, errors=[ErrorInfo(...)])`, assert `_startup_timeline_errors` has the entry 3. `test_post_init_skips_when_dst_non_empty` — pre-create cwd/manualslop_layout.ini with 5+ `[Window][`, call `_post_init()`, assert install helper was NOT called (or was called but returned `data=False`) - HOW: Use `monkeypatch.setattr(src.gui_2, "_install_default_layout_if_empty_result", lambda app, src, dst: Result(data=True))`. Use `tmp_path` as cwd. - SAFETY: 1-space indent. CRLF. Mock only the boundary helper; verify the call site. - RUN: `uv run pytest tests/test_app_wiring_install.py -v` — Expected: 3 FAIL (call site not yet wired). - COMMIT: `test(gui): RED phase tests for _post_init install wiring` - [ ] **Task 3.2: Wire `_install_default_layout_if_empty_result` into `App._post_init`** - WHERE: `src/gui_2.py:566-578` — `_post_init` method. Insert the install call after line 574 (`cb_result = _post_init_callback_result(self)`) and before line 578 (`self._diag_layout_state()`). - WHAT: Add 7 lines: ```python from src.layouts import get_layouts_dir src_layout_path: Path = get_layouts_dir() / "default.ini" dst_layout_path: Path = Path.cwd() / "manualslop_layout.ini" install_result: Result[bool] = _install_default_layout_if_empty_result(self, src_layout_path, dst_layout_path) if not install_result.ok: if not hasattr(self, '_startup_timeline_errors'): self._startup_timeline_errors = [] self._startup_timeline_errors.append(("_install_default_layout", install_result.errors[0])) ``` - HOW: Insert after `_post_init_callback_result` block. Match existing 1-space indent in `_post_init`. - SAFETY: 1-space indent. CRLF. The `_startup_timeline_errors` attribute may not exist yet (per existing `_post_init` lines 576, 599 — create it lazily). - RUN: `uv run pytest tests/test_app_wiring_install.py -v` — Expected: 3 PASS. - COMMIT: `feat(gui): wire _install_default_layout_if_empty_result into App._post_init` - [ ] **Task 3.3: RED test for `App.run` calling `_install_default_layout_pre_run_result`** - WHERE: Append to `tests/test_app_wiring_install.py` - WHAT: Write 2 tests: 1. `test_run_calls_pre_run_install_before_immapp` — mock both `_install_default_layout_pre_run_result` and `_run_immapp_result`, assert order: pre-run install called BEFORE immapp 2. `test_run_drains_pre_run_install_errors` — pre-run install returns `Result(data=False, errors=[ErrorInfo])`, assert `_startup_timeline_errors` has the entry - HOW: Use `mock.call_args_list` to verify order. Use `monkeypatch.setattr(src.gui_2, "_install_default_layout_pre_run_result", ...)`. - SAFETY: 1-space indent. CRLF. Mock the pre-run install + immapp helpers; don't actually run immapp. - RUN: `uv run pytest tests/test_app_wiring_install.py -v` — Expected: 2 new FAIL (pre-run call site not wired). - COMMIT: `test(gui): RED phase tests for App.run pre-run install wiring` - [ ] **Task 3.4: Wire `_install_default_layout_pre_run_result` into `App.run`** - WHERE: `src/gui_2.py:691` — before `_run_immapp_result(self)` call. Insert 6 lines. - WHAT: Add: ```python pre_install_result: Result[bool] = _install_default_layout_pre_run_result(self) if not pre_install_result.ok: err = pre_install_result.errors[0] if hasattr(self, "_startup_timeline_errors"): self._startup_timeline_errors.append(("_install_default_layout_pre_run", err)) ``` - HOW: Insert immediately before `run_result = _run_immapp_result(self)` at line 691. Match existing 1-space indent. - SAFETY: 1-space indent. CRLF. The pre-run install MUST fire before immapp reads the INI from disk. - RUN: `uv run pytest tests/test_app_wiring_install.py -v` — Expected: 5 PASS (3 from 3.1 + 2 from 3.3). - COMMIT: `feat(gui): wire _install_default_layout_pre_run_result into App.run (before immapp)` - [ ] **Task 3.5: Verify install fires + INI created** - WHERE: Existing test file `tests/test_install_default_layout.py` - WHAT: Add integration test `test_install_fires_end_to_end` — instantiate `App`, call `_post_init()`, assert cwd/manualslop_layout.ini exists with > 1000 bytes + `[Window][` substring. - HOW: Use `tmp_path` as cwd via `monkeypatch.chdir(tmp_path)`. - SAFETY: 1-space indent. CRLF. Real on-disk assertion (no mocks). - RUN: `uv run pytest tests/test_install_default_layout.py -v` — Expected: 9 PASS. - COMMIT: `test(install): GREEN end-to-end install fires + INI created` --- ## Phase 4: Surgical Cherry-Picks Focus: Apply the 2 surgical fixes that don't require new infrastructure. - [ ] **Task 4.1: Cherry-pick orphan-end-child fix** - WHERE: `src/gui_2.py:6990` — delete the line `imgui.end_child()` inside the `except (TypeError, AttributeError):` block in `render_tier_stream_panel`. - WHAT: Apply tier-2's `c2155593` 1-line deletion. The orphan `end_child()` at line 6990 fires with no matching `begin_child()` when the try block raises (e.g. `len(None)`). - HOW: Read `src/gui_2.py:6984-6991` → delete line 6990 (the `imgui.end_child()` inside except). Keep line 6988 (the correct one inside try). Keep `pass` on line 6991. - SAFETY: 1-space indent. CRLF. Preserve the `try/except` structure. The deleted line is the only change. - RUN: `uv run python scripts/check_imgui_scopes.py src/gui_2.py` — Expected: 3 "extra end" warnings (down from 4). The 4925 + 7094 + 8810 warnings remain (other code); the 6990 one should be gone. - COMMIT: `fix(gui): remove orphan imgui.end_child() in render_tier_stream_panel except handler` - [ ] **Task 4.2: Cherry-pick reset_layout dead-path cleanup** - WHERE: `src/commands.py:268` — delete the line `os.path.join("tests", "artifacts", "live_gui_workspace", "manualslop_layout.ini"),` from the `layout_paths` list inside `reset_layout`. - WHAT: Apply tier-2's `3b966288`. The `reset_layout` command should not reference test fixtures in production code. - HOW: Read `src/commands.py:365-380` → identify the line that hardcodes `tests/artifacts/manualslop_layout_default.ini` → delete it. If the surrounding logic needs adjustment (e.g. fallback to a different path), update the fallback. - SAFETY: 1-space indent. CRLF. The behavior of `reset_layout` should be preserved — it still resets the layout, just from a different source path. - RUN: `uv run pytest tests/test_commands.py -v` — Expected: PASS (the existing tests cover the reset_layout behavior). - COMMIT: `chore(commands): remove dead test-fixture path from reset_layout` --- ## Phase 5: Layer 1 Verification — Per-Panel Render Sentinel Focus: The "panels actually render" test that catches the original bug. - [ ] **Task 5.1: RED test for per-panel render size check** - WHERE: New file `tests/test_panels_visible_after_install.py` - WHAT: Write 3 tests: 1. `test_panels_visible_after_install` — use `live_gui` fixture, wait for first frame, iterate `app.show_windows` for entries where `value == True`, assert each has nonzero render size via `imgui.find_window_viewport(name).size.x > 0` 2. `test_panel_invisible_when_show_windows_false` — same loop, but verify panels with `value == False` are NOT in `find_window_viewport` results 3. `test_panel_render_size_is_correct_window` — assert `find_window_viewport("AI Settings").size.x > 100 AND .size.y > 50` (sanity: visible panels have meaningful size, not 0) - HOW: Use `live_gui` fixture. Poll for first frame via `client.wait_for_event` (not `time.sleep`). Use `imgui.find_window_viewport(name)` API. - SAFETY: Poll-loop, not `time.sleep`. 1-space indent. CRLF. Skip test on non-Windows (`@pytest.mark.skipif(sys.platform != "win32")`). - RUN: `uv run pytest tests/test_panels_visible_after_install.py -v` — Expected: PASS on first try IF install infrastructure works (since Phase 1-3 is done by now). The value of this test is regression detection, not initial GREEN. - COMMIT: `test(visual): Layer 1 per-panel render sentinel (catches empty-panels regression)` - [ ] **Task 5.2: Verify sentinel catches the regression (negative test mode)** - WHERE: Append to `tests/test_panels_visible_after_install.py` - WHAT: Write `test_sentinel_catches_empty_panels` — use `live_gui` fixture, BUT monkey-patch `_install_default_layout_pre_run_result` to return `Result(data=False)` (skip install). Also, pre-create cwd/manualslop_layout.ini with content that omits all `[Window][X]` entries (just an empty INI). Assert the test FAILS. - HOW: Use `monkeypatch.setattr`. The sentinel should detect that 8 default-visible panels all have zero render size. - SAFETY: This test verifies the sentinel's REGRESSION CATCH ability. It should NOT pass — its job is to confirm the sentinel works. - RUN: `uv run pytest tests/test_panels_visible_after_install.py::test_sentinel_catches_empty_panels -v` — Expected: FAIL with assertion error listing 8 panels with zero render size. - COMMIT: `test(visual): RED negative test — sentinel catches empty-panels regression` - [ ] **Task 5.3: Verify sentinel catches the original bug (mock the import failure)** - WHERE: Append to `tests/test_panels_visible_after_install.py` - WHAT: Write `test_sentinel_catches_render_main_interface_no_op` — use `live_gui` fixture, monkey-patch `src.gui_2.render_main_interface` to be a no-op (`lambda app: None`). Assert the sentinel FAILS (panels don't render). - HOW: This simulates the original tier-2 bug: `render_main_interface` is a no-op due to ModuleNotFoundError. - SAFETY: Use `monkeypatch.setattr` to swap the function reference at module level. - RUN: `uv run pytest tests/test_panels_visible_after_install.py::test_sentinel_catches_render_main_interface_no_op -v` — Expected: FAIL with assertion error listing 8 panels with zero render size. - COMMIT: `test(visual): RED negative test — sentinel catches render_main_interface no-op` --- ## Phase 6: Layer 2 Verification — Win32 PrintWindow Pixel Baseline Focus: The HARD pixel-diff test that catches ALL visual regressions. - [ ] **Task 6.1: RED test for Win32 PrintWindow capture** - WHERE: New file `tests/test_visual_baseline_default.py` - WHAT: Write 4 tests: 1. `test_capture_gui_window_pixels` — use `live_gui` fixture, wait for first frame, call `_capture_gui_window_png()`, assert the returned PNG file exists with size > 0 2. `test_capture_returns_png_with_correct_dimensions` — assert PNG dimensions match the forced viewport (1680x1050 from F6.1 env var) 3. `test_capture_handles_missing_hwnd` — simulate window-not-found → return `Result(data=None, errors=[ErrorInfo])` 4. `test_capture_does_not_crash_on_zero_size` — simulate hwnd with zero-size window → return `Result(data=None, errors=[ErrorInfo])` (no crash) - HOW: Import `_capture_gui_window_png` from `src.gui_2`. Use `live_gui` fixture with `MANUAL_SLOP_TEST_VIEWPORT=1680x1050` + `MANUAL_SLOP_TEST_THEME=dark` env vars. - SAFETY: 1-space indent. CRLF. Skip on non-Windows. Use `tmp_path` for PNG output. - RUN: `uv run pytest tests/test_visual_baseline_default.py -v` — Expected: 4 FAIL (`ImportError: cannot import name '_capture_gui_window_png'`). - COMMIT: `test(visual): RED phase tests for Win32 PrintWindow capture` - [ ] **Task 6.2: Implement `_capture_gui_window_png` in `src/gui_2.py`** - WHERE: `src/gui_2.py` — insert after `_install_default_layout_pre_run_result` - WHAT: Port the Win32 PrintWindow capture logic. Find imgui window via `win32gui.FindWindow(None, "manual slop")`; allocate DC + bitmap; call `win32gui.PrintWindow(hwnd, hdc, win32con.PW_RENDERFULLCONTENT)`; convert to PNG via `Pillow.Image.frombuffer(...)`; save to given `Path`. Returns `Result[Path]`. - HOW: Import `win32gui`, `win32con`, `win32ui` from `pywin32`. Import `PIL.Image`. The function signature: `_capture_gui_window_png(out_path: Path) -> Result[Path]`. - SAFETY: 1-space indent. CRLF. No comments. Wrap each Win32 call in try/except returning `ErrorInfo`. Use `win32gui.DestroyWindow(hwnd)` after capture (cleanup). - RUN: `uv run pytest tests/test_visual_baseline_default.py -v` — Expected: 4 PASS. - COMMIT: `feat(gui): add _capture_gui_window_png via Win32 PrintWindow + Pillow` - [ ] **Task 6.3: Generate baseline PNG** - WHERE: New file `tests/artifacts/visual_baseline_default.png` - WHAT: Capture the running GUI's pixels after install fires + panels render. This is the "known good" reference. - HOW: Run `uv run python -m pytest tests/test_visual_baseline_default.py::test_capture_gui_window_pixels --capture=tee-sys -s` and manually save the output PNG. OR: write a one-shot helper script `scripts/capture_visual_baseline.py` that spawns the app, waits for first frame, calls `_capture_gui_window_png(artifacts/visual_baseline_default.png)`, exits. - SAFETY: 1-space indent. CRLF. The baseline PNG must be captured AFTER all install infrastructure is in place. Verify the PNG visually (user's eyes) before committing. - RUN: `uv run python scripts/capture_visual_baseline.py` — Expected: writes `tests/artifacts/visual_baseline_default.png` (~50-200 KB depending on viewport size). - COMMIT: `feat(visual): commit visual_baseline_default.png (the known-good pixel reference)` - [ ] **Task 6.4: RED test for pixel diff comparison** - WHERE: Append to `tests/test_visual_baseline_default.py` - WHAT: Write 3 tests: 1. `test_pixel_diff_below_threshold` — capture current + load baseline → assert diff < 1% 2. `test_pixel_diff_above_threshold_on_corrupt_ini` — corrupt the INI (delete `[Docking][Data]` line) + capture → assert diff > 5% (catches regression) 3. `test_pixel_diff_threshold_configurable` — pass `--threshold 0.05` → assert behavior matches - HOW: Use `_compute_pixel_diff(baseline_path, current_path) -> float`. The function: load both via `Pillow.Image.open()`, convert to RGB, compute `numpy.abs(np.array(a) - np.array(b)).mean() / 255.0`. - SAFETY: 1-space indent. CRLF. Skip on non-Windows. Threshold default = 0.01 (1%). - RUN: `uv run pytest tests/test_visual_baseline_default.py -v` — Expected: 3 new FAIL (`ImportError: cannot import name '_compute_pixel_diff'`). - COMMIT: `test(visual): RED phase tests for pixel diff comparison` - [ ] **Task 6.5: Implement `_compute_pixel_diff` in `src/gui_2.py`** - WHERE: `src/gui_2.py` — insert after `_capture_gui_window_png` - WHAT: Compare two PNGs and return pixel diff as float (0.0-1.0). - HOW: Load both via `Pillow.Image.open(path).convert("RGB")`. Convert to numpy arrays. Compute `numpy.abs(a - b).mean() / 255.0`. Return the float. - SAFETY: 1-space indent. CRLF. Handle size mismatch (resize to larger dim). Handle missing files → return 1.0 (100% diff = max divergence). - RUN: `uv run pytest tests/test_visual_baseline_default.py -v` — Expected: 7 PASS (4 from 6.1 + 3 new). - COMMIT: `feat(gui): add _compute_pixel_diff (numpy-based pixel comparison)` --- ## Phase 7: Layer 3 Verification — Forced Test Viewport + Theme Focus: Make the baseline deterministic so pixel diff is meaningful. - [ ] **Task 7.1: RED test for `MANUAL_SLOP_TEST_VIEWPORT` env var** - WHERE: New file `tests/test_test_mode_env_vars.py` - WHAT: Write 2 tests: 1. `test_viewport_env_var_overrides_default` — spawn subprocess with `MANUAL_SLOP_TEST_VIEWPORT=1920x1080` env var → assert `App.run()` set `runner_params.app_window_params.window_geometry.size = (1920, 1080)` 2. `test_viewport_env_var_unset_uses_default` — spawn without env var → assert size = (1680, 1200) (current default at line 651) - HOW: Use `subprocess` to spawn `sloppy.py` with env vars. Inspect via the `/api/gui` Hook API endpoint after launch. - SAFETY: 1-space indent. CRLF. Use `subprocess.run` with timeout. Clean up subprocess on test teardown via `kill_process_tree` fixture. - RUN: `uv run pytest tests/test_test_mode_env_vars.py -v` — Expected: 2 FAIL (env var not honored). - COMMIT: `test(env): RED phase tests for MANUAL_SLOP_TEST_VIEWPORT env var` - [ ] **Task 7.2: Implement `MANUAL_SLOP_TEST_VIEWPORT` parsing in `App.run`** - WHERE: `src/gui_2.py:651` — before `self.runner_params.app_window_params.window_geometry.size = (1680, 1200)`, add the env var parsing. - WHAT: Read env var. If set and matches `WxH` pattern, override the size. - HOW: Add 5 lines before line 651: ```python _test_viewport = os.environ.get("MANUAL_SLOP_TEST_VIEWPORT") if _test_viewport and "x" in _test_viewport: _w, _h = _test_viewport.split("x", 1) _w, _h = int(_w), int(_h) else: _w, _h = 1680, 1200 self.runner_params.app_window_params.window_geometry.size = (_w, _h) ``` - SAFETY: 1-space indent. CRLF. Wrap the parsing in try/except (return default on ValueError). - RUN: `uv run pytest tests/test_test_mode_env_vars.py -v` — Expected: 2 PASS. - COMMIT: `feat(gui): honor MANUAL_SLOP_TEST_VIEWPORT env var (Layer 3 forced viewport)` - [ ] **Task 7.3: RED test for `MANUAL_SLOP_TEST_THEME` env var** - WHERE: Append to `tests/test_test_mode_env_vars.py` - WHAT: Write 2 tests: 1. `test_theme_env_var_overrides_default` — spawn with `MANUAL_SLOP_TEST_THEME=dark` → assert `runner_params.imgui_window_params.tweaked_theme` is `ImGuiTheme_.ImGuiColorsDark` 2. `test_theme_env_var_unset_uses_default` — spawn without env var → assert theme is NOT forced - HOW: Same `subprocess` + Hook API pattern. - SAFETY: 1-space indent. CRLF. - RUN: `uv run pytest tests/test_test_mode_env_vars.py -v` — Expected: 2 new FAIL (env var not honored). - COMMIT: `test(env): RED phase tests for MANUAL_SLOP_TEST_THEME env var` - [ ] **Task 7.4: Implement `MANUAL_SLOP_TEST_THEME` parsing in `App.run`** - WHERE: `src/gui_2.py:654` — before `self.runner_params.imgui_window_params.tweaked_theme = theme.get_tweaked_theme()`, add the env var parsing. - WHAT: Read env var. If set to `dark`, force theme to `hello_imgui.ImGuiTheme_.ImGuiColorsDark`. - HOW: Add 5 lines before line 654: ```python _test_theme = os.environ.get("MANUAL_SLOP_TEST_THEME") if _test_theme == "dark": self.runner_params.imgui_window_params.tweaked_theme = hello_imgui.ImGuiTheme_.ImGuiColorsDark else: self.runner_params.imgui_window_params.tweaked_theme = theme.get_tweaked_theme() ``` - SAFETY: 1-space indent. CRLF. The original `theme.get_tweaked_theme()` call becomes the `else` branch. - RUN: `uv run pytest tests/test_test_mode_env_vars.py -v` — Expected: 4 PASS. - COMMIT: `feat(gui): honor MANUAL_SLOP_TEST_THEME env var (Layer 3 forced theme)` --- ## Phase 8: Layer 4 Verification — Cannot-Skip Gates Focus: Make the verification infrastructure impossible to ignore. - [ ] **Task 8.1: Create `scripts/check_visual_baseline.py`** - WHERE: New file `scripts/check_visual_baseline.py` - WHAT: Standalone CLI script that compares two PNGs and exits 1 on diff > threshold. - HOW: Args: `--baseline ` (default: `tests/artifacts/visual_baseline_default.png`), `--current ` (required), `--threshold ` (default: 0.01). Uses `Pillow` + `numpy` for diff. Returns exit code 0 if diff ≤ threshold, exit code 1 otherwise. Print diff percentage to stdout. Use the same `_compute_pixel_diff` logic from Task 6.5. - SAFETY: 1-space indent. CRLF. Use `argparse`. Handle missing files gracefully (exit 1 + error message). - RUN: `uv run python scripts/check_visual_baseline.py --help` — Expected: usage message. `uv run python scripts/check_visual_baseline.py --current tests/artifacts/visual_baseline_default.png --baseline tests/artifacts/visual_baseline_default.png` — Expected: `diff: 0.0000 PASS`. - COMMIT: `feat(visual): add scripts/check_visual_baseline.py (Layer 4 standalone CI gate)` - [ ] **Task 8.2: Wire `check_visual_baseline.py` into `scripts/run_tests_batched.py`** - WHERE: `scripts/run_tests_batched.py` — add a new tier (or extend an existing one) that runs `tests/test_visual_baseline_default.py` + `tests/test_panels_visible_after_install.py` + `scripts/check_visual_baseline.py`. - WHAT: Add a tier (e.g. `tier_visual`) to the batched runner config. The tier runs after `tier3` and before the smoke tier. - HOW: Read `scripts/run_tests_batched.py` config → add `tier_visual` → list the 3 commands. - SAFETY: 1-space indent. CRLF. Don't break existing tiers. - RUN: `uv run python scripts/run_tests_batched.py --tier visual` — Expected: 7 tests pass (4 + 3 from Phase 5-6). - COMMIT: `chore(tests): wire Layer 1+2 visual tests into scripts/run_tests_batched.py` - [ ] **Task 8.3: Write `docs/guide_visual_verification.md`** - WHERE: New file `docs/guide_visual_verification.md` - WHAT: 200-300 line guide documenting: - The 4 layers (per-panel sentinel, pixel baseline, forced viewport/theme, cannot-skip gates) - How to add a new visual baseline - How to update an existing baseline (after a deliberate UI change) - The env-var protocol (`MANUAL_SLOP_TEST_VIEWPORT`, `MANUAL_SLOP_TEST_THEME`) - The `VERIFIED-` tag protocol - When to use imgui_test_engine vs PrintWindow (the trade-offs) - HOW: Write as a markdown guide with code blocks + cross-references to `docs/guide_testing.md` + `docs/guide_gui_2.md`. - SAFETY: Markdown formatting consistent with other `docs/guide_*.md` files. - RUN: N/A (docs file). - COMMIT: `docs(visual-verification): add guide for the 4-layer visual verification protocol` - [ ] **Task 8.4: Update `conductor/tracks.md` schema** - WHERE: `conductor/tracks.md` — find the schema section (or add a new "Track Completion Gates" section). - WHAT: Add a new section documenting the `VERIFIED-` tag requirement for tracks that touch `src/gui_2.py`. Tracks that ship without the tag are NOT marked `[x]`. - HOW: Read `conductor/tracks.md` → find the schema → add the new gate. - SAFETY: Markdown formatting consistent. Cross-reference `docs/guide_visual_verification.md`. - RUN: N/A (docs file). - COMMIT: `docs(tracks): add VERIFIED- tag requirement for tracks touching src/gui_2.py` - [ ] **Task 8.5: Update `docs/Readme.md` to reference the new guide** - WHERE: `docs/Readme.md` — find the "Per-Source-File Deep Dives" section (or equivalent) → add `docs/guide_visual_verification.md` entry. - WHAT: Add a new bullet + 1-line description. - HOW: Read `docs/Readme.md` → add the entry. - SAFETY: Match existing entry format. - RUN: N/A (docs file). - COMMIT: `docs(readme): cross-reference guide_visual_verification.md` --- ## Phase 9: End-to-End Verification + Negative Test + Track Completion Focus: Prove the verification infrastructure actually catches regressions, then close out the track. - [ ] **Task 9.1: Write `tests/test_visual_baseline_catches_corrupt_ini.py`** - WHERE: New file `tests/test_visual_baseline_catches_corrupt_ini.py` - WHAT: Write 1 test that uses `live_gui` fixture; AFTER install fires, manually delete the `[Docking][Data]` line from cwd/manualslop_layout.ini; re-launch + capture; assert pixel diff > 5%. - HOW: Spawn app → wait for first frame → corrupt INI → quit → re-launch → wait for first frame → capture screenshot → compare to baseline. - SAFETY: 1-space indent. CRLF. Use `kill_process_tree` fixture for cleanup. Skip on non-Windows. - RUN: `uv run pytest tests/test_visual_baseline_catches_corrupt_ini.py -v` — Expected: PASS (the diff should be > 5% because panels don't render visibly). - COMMIT: `test(visual): negative test — corrupted INI catches the regression (FR8)` - [ ] **Task 9.2: Run full test batch** - WHERE: All test files added in Phase 1-9 - WHAT: Run `scripts/run_tests_batched.py` end-to-end. Verify all tiers PASS. - HOW: `uv run python scripts/run_tests_batched.py` — runs the full batch (not just `tier_visual`). - SAFETY: If any tier fails, STOP. Report to user. Do NOT mark track complete. - RUN: Expected: all 11 tiers PASS. If a tier fails, debug per `conductor/workflow.md` "Deduction Loop" rule (max 2 runs). - COMMIT: N/A (verification only). - [ ] **Task 9.3: Manual visual verification gate** - WHERE: User's machine - WHAT: User runs `uv run sloppy.py` from master. User confirms panels render visibly (Project Settings, Files & Media, AI Settings, Operations Hub, Theme on left; Discussion Hub, Log Management, Diagnostics on right). - HOW: User reports back. If panels DO render visibly → proceed. If panels DON'T render → STOP, debug, report. - SAFETY: N/A (manual gate). - COMMIT: N/A (manual verification only). - [ ] **Task 9.4: User commits `VERIFIED-` tag** - WHERE: Master branch - WHAT: User commits `git tag VERIFIED-20260629 ` on master. Documents the visual verification. - HOW: `git tag VERIFIED-20260629 `. Add to track completion checklist. - SAFETY: HARD GATE. Without this tag, the track is NOT marked complete in `conductor/tracks.md`. - COMMIT: N/A (tag, not commit). But attach a git note to the final commit: `git notes add -m "VISUALLY VERIFIED: panels render correctly via uv run sloppy.py from master"`. - [ ] **Task 9.5: Write `docs/reports/TRACK_COMPLETION_default_layout_extract_20260629.md`** - WHERE: New file `docs/reports/TRACK_COMPLETION_default_layout_extract_20260629.md` - WHAT: 100-200 line report documenting: - What was extracted (per FR1-FR3) - What was built (per FR4-FR7) - Test results (per FR8) - User verification (per 9.3) - Follow-up tracks (Fleury migration, imgui_test_engine integration) - Tier-2 archival status (user's responsibility) - HOW: Markdown report. Cross-reference `docs/reports/PANEL_VISIBILITY_DEBUG_REPORT_20260629.md` + `conductor/tracks/default_layout_extract_20260629/spec.md`. - SAFETY: 100-200 lines max. Concise. - COMMIT: `docs(reports): TRACK_COMPLETION_default_layout_extract_20260629` - [ ] **Task 9.6: Update `conductor/tracks.md` to mark this track complete** - WHERE: `conductor/tracks.md` — find the row for `default_layout_extract_20260629` → mark `[x]` (with `VERIFIED-20260629` tag referenced). - WHAT: Update the row. - HOW: Read `conductor/tracks.md` → find the row → update. - SAFETY: HARD GATE. The `[x]` requires the `VERIFIED-` tag to exist. If absent, leave the row as `[ ]`. - COMMIT: `conductor(tracks): mark default_layout_extract_20260629 complete (with VERIFIED-20260629 tag)` - [ ] **Task 9.7: Conductor - User Manual Verification (Protocol in workflow.md)** - WHERE: User-facing summary - WHAT: Confirm to the user that: - All 9 phases complete - All tests pass (full batch, not just tier_visual) - Pixel baseline PNG committed - `VERIFIED-` tag exists - Tier-2 archival is user's responsibility - HOW: Brief 5-10 sentence summary in chat. - SAFETY: HARD GATE. Do NOT claim "track complete" without the tag + the user's confirmation. --- ## Self-Review (per writing-plans skill) **1. Spec coverage:** - G1 (FR1.1-FR1.4) → Phase 1 tasks ✓ - G2 (FR2.1-FR2.5) → Phase 2-3 tasks ✓ - G3 (FR3.2) → Phase 4 task 4.2 ✓ - G4 (FR3.1) → Phase 4 task 4.1 ✓ - G5 Layer 1 (FR4.1-FR4.4) → Phase 5 tasks ✓ - G5 Layer 2 (FR5.1-FR5.6) → Phase 6 tasks ✓ - G5 Layer 3 (FR6.1-FR6.4) → Phase 7 tasks ✓ - G5 Layer 4 (FR7.1-F7.4) → Phase 8 tasks ✓ - G6 (FR8.1-FR8.2) → Phase 9 task 9.1 ✓ **2. Placeholder scan:** - No "TBD", "TODO", "implement later", "fill in details" - No "add appropriate error handling" — each error case is specified - No "similar to Task N" — each task is self-contained - No steps without code blocks where code is required **3. Type consistency:** - `_install_default_layout_if_empty` → `Result[bool]` (Task 2.2, 3.1, 3.2) ✓ - `_install_default_layout_if_empty_result` → `Result[bool]` (Task 2.2, 3.1, 3.2) ✓ - `_install_default_layout_pre_run_result` → `Result[bool]` (Task 2.4, 3.3, 3.4) ✓ - `_capture_gui_window_png` → `Result[Path]` (Task 6.1, 6.2) ✓ - `_compute_pixel_diff(baseline, current)` → `float` (Task 6.4, 6.5) ✓ - `LayoutFile` → `@dataclass(frozen=True, slots=True)` (Task 1.2) ✓ - `Result`, `ErrorInfo`, `ErrorKind` from `src.result_types` (consistent throughout) ✓ **4. Spec coverage check:** - Spec §FR1.1 → Task 1.6 ✓ - Spec §FR1.2 → Task 1.2 ✓ - Spec §FR1.3 → Tasks 1.3, 1.4 ✓ - Spec §FR1.4 → covered by Task 1.6 (test for INI existence) ✓ - Spec §FR2.1 → Task 2.2 ✓ - Spec §FR2.2 → Task 2.2 ✓ - Spec §FR2.3 → Task 2.4 ✓ - Spec §FR2.4 → Task 3.2 ✓ - Spec §FR2.5 → Task 3.4 ✓ - Spec §FR3.1 → Task 4.1 ✓ - Spec §FR3.2 → Task 4.2 ✓ - Spec §FR4.1-FR4.4 → Phase 5 tasks ✓ - Spec §FR5.1-FR5.6 → Phase 6 tasks ✓ - Spec §FR6.1-FR6.4 → Phase 7 tasks ✓ - Spec §FR7.1-FR7.4 → Phase 8 tasks ✓ - Spec §FR8.1-FR8.2 → Task 9.1 ✓ No gaps found. ## Summary - **9 phases**, **36 tasks** (each surgical with WHERE/WHAT/HOW/SAFETY/COMMIT) - **3 new files**: `src/layouts.py`, `layouts/default.ini`, `tests/artifacts/visual_baseline_default.png`, `scripts/check_visual_baseline.py`, `docs/guide_visual_verification.md` - **6 modified files**: `src/gui_2.py`, `src/paths.py`, `src/commands.py`, `scripts/run_tests_batched.py`, `conductor/tracks.md`, `docs/Readme.md` - **5 new test files**: `tests/test_layouts.py`, `tests/test_paths_layouts.py`, `tests/test_layouts_bundled.py`, `tests/test_install_default_layout.py`, `tests/test_app_wiring_install.py`, `tests/test_panels_visible_after_install.py`, `tests/test_visual_baseline_default.py`, `tests/test_test_mode_env_vars.py`, `tests/test_visual_baseline_catches_corrupt_ini.py` - **~36 atomic commits** (1 per task) - **HARD verification gates**: Layer 1 sentinel + Layer 2 pixel baseline + Layer 3 forced viewport/theme + Layer 4 cannot-skip tags This is the "no slippage" plan. Each task is a 2-5 minute action. Each has a commit. The verification infrastructure makes the regression impossible to reintroduce without CI catching it.