The spec was drafted while the working tree was on tier2/post_module_taxonomy_de_cruft_20260627, but the track targets master. 2 line numbers were from the cruft branch, not master: - src/commands.py reset_layout: spec said :342-378 + :371; master is :248-275 + :268 - src/command_palette.py: spec said 208 lines; master is 165 lines Also added a Branch State Warning section documenting: - main working tree is on tier2/post_module_taxonomy_de_cruft_20260627 (NOT master) - module_taxonomy_refactor_20260627 + post_module_taxonomy_de_cruft_20260627 are NOT merged to master - this track does NOT depend on those cruft tracks - master worktree at C:\projects\manual_slop_master is the editing surface All other line numbers (App._post_init:566, App.run:619, _run_immapp_result:691, _post_init_callback_result:1449, render_persona_editor_window:3433, orphan end_child:6990, paths.py themes:60/83/150/209-216/295) verified correct against master.
42 KiB
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. Nodict[str, Any], noOptional[T]returns (useResult[T]+NIL_T). Readsrc/gui_2.py:1481-1540(tier-2 version) for the install helper pattern reference; readsrc/theme_models.py:181-225for the layouts loader pattern reference; readsrc/paths.py:60-83,150,209-216,295for 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:
test_load_layouts_from_dir_empty— pass a non-existent path → returns{}test_load_layouts_from_dir_single_file— create tmp dir with one.inifile → returns 1-entry dict keyed by stemtest_load_layouts_from_dir_skips_non_ini— tmp dir with.ini+.txt→ returns only the.ini
- HOW: Use
tmp_pathfixture (already redirected undertests/artifacts/_pytest_tmpperpyproject.toml:addopts). Importfrom 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
- WHERE: New file
-
Task 1.2: Create
src/layouts.py- WHERE: New file
src/layouts.py(87 lines, ported fresh from tier-2'sC:\projects\manual_slop_tier2\src\layouts.py) - WHAT: Define
LayoutFiledataclass +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+ErrorInfodrain pattern from tier-2 verbatim; keep_LAYOUTS_CACHEmodule-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
- WHERE: New file
-
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:
test_get_global_layouts_path_default—initialize_paths()called,get_global_layouts_path()returns<root_dir>/layoutstest_get_global_layouts_path_env_override—SLOP_GLOBAL_LAYOUTSenv var set → returns that pathtest_layouts_in_path_info_dict—paths.path_info()dict has'layouts': info(...)entrytest_layouts_field_in_app_paths—_AppPaths().layoutsis aPath
- HOW: Import
from src.paths import get_global_layouts_path, initialize_paths, _cfg. Usemonkeypatch.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
- WHERE: New file
-
Task 1.4: Add
get_global_layouts_path()tosrc/paths.py- WHERE:
src/paths.py— 4 sites: line 60_AppPathsdataclass (addlayouts: Path), line 83_PATHS_DEFAULTS(addlayouts = root_dir / "layouts"), line 150initialize_paths._resolve_pathchain (addSLOP_GLOBAL_LAYOUTSenv override), line 295path_info()(add'layouts': info(cfg.layouts)), line 209-216 (addget_global_layouts_path()mirror ofget_global_themes_path()) - WHAT: Mirror the themes pattern exactly. New code follows the existing 1-space indentation + CRLF.
- HOW: Read
src/paths.py:60→ insertlayouts: Pathafterthemes: Path. Readsrc/paths.py:83→ insertthemes = root_dir / "layouts"afterthemes = root_dir / "themes". Readsrc/paths.py:150→ addthemes = _resolve_path("SLOP_GLOBAL_LAYOUTS", "layouts", root_dir / "layouts", config_path)to the resolver chain. Readsrc/paths.py:209-216→ copyget_global_themes_path()verbatim and rename. Readsrc/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_pathkeyword 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)
- WHERE:
-
Task 1.5: RED test for bundled INI file
- WHERE: New file
tests/test_layouts_bundled.py - WHAT: Write 4 tests:
test_layouts_default_ini_exists—Path("layouts/default.ini").exists()is Truetest_layouts_default_ini_size— file size > 1000 bytestest_layouts_default_ini_has_docking— content contains[Docking][Data]test_layouts_default_ini_has_8_windows— content has 8[Window][X]entries
- HOW: Use
Path.cwd() / "layouts" / "default.ini". Uselen(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
- WHERE: New file
-
Task 1.6: Port
layouts/default.inito master- WHERE: New file
layouts/default.iniat 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;;;<<<SplitIds>>>;;;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 withDockSpace 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
- WHERE: New file
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:
test_install_empty_dst— dst INI is empty/missing → src content copied to dst +Result(data=True)test_install_skips_non_empty_dst— dst INI has 5+[Window][entries → no overwrite +Result(data=False)test_install_handles_missing_src— src INI doesn't exist →Result(data=False, errors=[ErrorInfo])test_install_handles_oserror_on_read— patchPath.read_textto raise OSError →Result(data=False, errors=[ErrorInfo])test_install_calls_load_ini_settings_from_memory— assertimgui.load_ini_settings_from_memorywas called once
- HOW: Use
tmp_path. Importfrom src.gui_2 import _install_default_layout_if_empty. Usemonkeypatch.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_memoryis 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
- WHERE: New file
-
Task 2.2: Implement
_install_default_layout_if_emptyinsrc/gui_2.py- WHERE:
src/gui_2.py— insert at line 1481 (before_post_init_callback_resultwhich is at 1449 — actually place the new helpers AFTER_post_init_callback_result) - WHAT: Port tier-2's
src/gui_2.py:1481-1530verbatim. Adjust imports if needed (Result,ErrorInfo,ErrorKindalready imported viasrc.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_resultshape. - 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
- WHERE:
-
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:
test_pre_run_install_empty_dst— same as 2.1.1 but using_install_default_layout_pre_run_resultand mocking_require_warmed("src.layouts")test_pre_run_install_does_not_call_load_ini_settings_from_memory— assertimgui.load_ini_settings_from_memorywas NOT called (imgui not initialized yet)test_pre_run_install_skips_non_empty_dst— same as 2.1.2
- HOW: Same
tmp_pathpattern. Mocksrc.layouts.get_layouts_dirto returntmp_path / "layouts". - SAFETY: 1-space indent. CRLF. Verify
load_ini_settings_from_memorywas 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
- WHERE: Append to
-
Task 2.4: Implement
_install_default_layout_pre_run_resultinsrc/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-1590verbatim. The function readsget_layouts_dir() / "default.ini"and writes toPath.cwd() / "manualslop_layout.ini". NOimgui.load_ini_settings_from_memorycall. - 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)
- WHERE:
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_initcalling_install_default_layout_if_empty_result- WHERE: New file
tests/test_app_wiring_install.py - WHAT: Write 3 tests:
test_post_init_calls_install_helper— instantiateApp, call_post_init(), assert_install_default_layout_if_empty_resultwas called withsrc=layouts/default.ini, dst=cwd/manualslop_layout.initest_post_init_drains_install_errors— make install helper returnResult(data=False, errors=[ErrorInfo(...)]), assert_startup_timeline_errorshas the entrytest_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 returneddata=False)
- HOW: Use
monkeypatch.setattr(src.gui_2, "_install_default_layout_if_empty_result", lambda app, src, dst: Result(data=True)). Usetmp_pathas 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
- WHERE: New file
-
Task 3.2: Wire
_install_default_layout_if_empty_resultintoApp._post_init- WHERE:
src/gui_2.py:566-578—_post_initmethod. 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:
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_resultblock. Match existing 1-space indent in_post_init. - SAFETY: 1-space indent. CRLF. The
_startup_timeline_errorsattribute may not exist yet (per existing_post_initlines 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
- WHERE:
-
Task 3.3: RED test for
App.runcalling_install_default_layout_pre_run_result- WHERE: Append to
tests/test_app_wiring_install.py - WHAT: Write 2 tests:
test_run_calls_pre_run_install_before_immapp— mock both_install_default_layout_pre_run_resultand_run_immapp_result, assert order: pre-run install called BEFORE immapptest_run_drains_pre_run_install_errors— pre-run install returnsResult(data=False, errors=[ErrorInfo]), assert_startup_timeline_errorshas the entry
- HOW: Use
mock.call_args_listto verify order. Usemonkeypatch.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
- WHERE: Append to
-
Task 3.4: Wire
_install_default_layout_pre_run_resultintoApp.run- WHERE:
src/gui_2.py:691— before_run_immapp_result(self)call. Insert 6 lines. - WHAT: Add:
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)
- WHERE:
-
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— instantiateApp, call_post_init(), assert cwd/manualslop_layout.ini exists with > 1000 bytes +[Window][substring. - HOW: Use
tmp_pathas cwd viamonkeypatch.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
- WHERE: Existing test file
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 lineimgui.end_child()inside theexcept (TypeError, AttributeError):block inrender_tier_stream_panel. - WHAT: Apply tier-2's
c21555931-line deletion. The orphanend_child()at line 6990 fires with no matchingbegin_child()when the try block raises (e.g.len(None)). - HOW: Read
src/gui_2.py:6984-6991→ delete line 6990 (theimgui.end_child()inside except). Keep line 6988 (the correct one inside try). Keeppasson line 6991. - SAFETY: 1-space indent. CRLF. Preserve the
try/exceptstructure. 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
- WHERE:
-
Task 4.2: Cherry-pick reset_layout dead-path cleanup
- WHERE:
src/commands.py:268— delete the lineos.path.join("tests", "artifacts", "live_gui_workspace", "manualslop_layout.ini"),from thelayout_pathslist insidereset_layout. - WHAT: Apply tier-2's
3b966288. Thereset_layoutcommand should not reference test fixtures in production code. - HOW: Read
src/commands.py:365-380→ identify the line that hardcodestests/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_layoutshould 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
- WHERE:
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:
test_panels_visible_after_install— uselive_guifixture, wait for first frame, iterateapp.show_windowsfor entries wherevalue == True, assert each has nonzero render size viaimgui.find_window_viewport(name).size.x > 0test_panel_invisible_when_show_windows_false— same loop, but verify panels withvalue == Falseare NOT infind_window_viewportresultstest_panel_render_size_is_correct_window— assertfind_window_viewport("AI Settings").size.x > 100 AND .size.y > 50(sanity: visible panels have meaningful size, not 0)
- HOW: Use
live_guifixture. Poll for first frame viaclient.wait_for_event(nottime.sleep). Useimgui.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)
- WHERE: New file
-
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— uselive_guifixture, BUT monkey-patch_install_default_layout_pre_run_resultto returnResult(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
- WHERE: Append to
-
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— uselive_guifixture, monkey-patchsrc.gui_2.render_main_interfaceto 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_interfaceis a no-op due to ModuleNotFoundError. - SAFETY: Use
monkeypatch.setattrto 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
- WHERE: Append to
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:
test_capture_gui_window_pixels— uselive_guifixture, wait for first frame, call_capture_gui_window_png(), assert the returned PNG file exists with size > 0test_capture_returns_png_with_correct_dimensions— assert PNG dimensions match the forced viewport (1680x1050 from F6.1 env var)test_capture_handles_missing_hwnd— simulate window-not-found → returnResult(data=None, errors=[ErrorInfo])test_capture_does_not_crash_on_zero_size— simulate hwnd with zero-size window → returnResult(data=None, errors=[ErrorInfo])(no crash)
- HOW: Import
_capture_gui_window_pngfromsrc.gui_2. Uselive_guifixture withMANUAL_SLOP_TEST_VIEWPORT=1680x1050+MANUAL_SLOP_TEST_THEME=darkenv vars. - SAFETY: 1-space indent. CRLF. Skip on non-Windows. Use
tmp_pathfor 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
- WHERE: New file
-
Task 6.2: Implement
_capture_gui_window_pnginsrc/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; callwin32gui.PrintWindow(hwnd, hdc, win32con.PW_RENDERFULLCONTENT); convert to PNG viaPillow.Image.frombuffer(...); save to givenPath. ReturnsResult[Path]. - HOW: Import
win32gui,win32con,win32uifrompywin32. ImportPIL.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. Usewin32gui.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
- WHERE:
-
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 -sand manually save the output PNG. OR: write a one-shot helper scriptscripts/capture_visual_baseline.pythat 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: writestests/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)
- WHERE: New file
-
Task 6.4: RED test for pixel diff comparison
- WHERE: Append to
tests/test_visual_baseline_default.py - WHAT: Write 3 tests:
test_pixel_diff_below_threshold— capture current + load baseline → assert diff < 1%test_pixel_diff_above_threshold_on_corrupt_ini— corrupt the INI (delete[Docking][Data]line) + capture → assert diff > 5% (catches regression)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 viaPillow.Image.open(), convert to RGB, computenumpy.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
- WHERE: Append to
-
Task 6.5: Implement
_compute_pixel_diffinsrc/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. Computenumpy.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)
- WHERE:
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_VIEWPORTenv var- WHERE: New file
tests/test_test_mode_env_vars.py - WHAT: Write 2 tests:
test_viewport_env_var_overrides_default— spawn subprocess withMANUAL_SLOP_TEST_VIEWPORT=1920x1080env var → assertApp.run()setrunner_params.app_window_params.window_geometry.size = (1920, 1080)test_viewport_env_var_unset_uses_default— spawn without env var → assert size = (1680, 1200) (current default at line 651)
- HOW: Use
subprocessto spawnsloppy.pywith env vars. Inspect via the/api/guiHook API endpoint after launch. - SAFETY: 1-space indent. CRLF. Use
subprocess.runwith timeout. Clean up subprocess on test teardown viakill_process_treefixture. - 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
- WHERE: New file
-
Task 7.2: Implement
MANUAL_SLOP_TEST_VIEWPORTparsing inApp.run- WHERE:
src/gui_2.py:651— beforeself.runner_params.app_window_params.window_geometry.size = (1680, 1200), add the env var parsing. - WHAT: Read env var. If set and matches
WxHpattern, override the size. - HOW: Add 5 lines before line 651:
_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)
- WHERE:
-
Task 7.3: RED test for
MANUAL_SLOP_TEST_THEMEenv var- WHERE: Append to
tests/test_test_mode_env_vars.py - WHAT: Write 2 tests:
test_theme_env_var_overrides_default— spawn withMANUAL_SLOP_TEST_THEME=dark→ assertrunner_params.imgui_window_params.tweaked_themeisImGuiTheme_.ImGuiColorsDarktest_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
- WHERE: Append to
-
Task 7.4: Implement
MANUAL_SLOP_TEST_THEMEparsing inApp.run- WHERE:
src/gui_2.py:654— beforeself.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 tohello_imgui.ImGuiTheme_.ImGuiColorsDark. - HOW: Add 5 lines before line 654:
_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 theelsebranch. - 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)
- WHERE:
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 <path>(default:tests/artifacts/visual_baseline_default.png),--current <path>(required),--threshold <float>(default: 0.01). UsesPillow+numpyfor diff. Returns exit code 0 if diff ≤ threshold, exit code 1 otherwise. Print diff percentage to stdout. Use the same_compute_pixel_difflogic 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)
- WHERE: New file
-
Task 8.2: Wire
check_visual_baseline.pyintoscripts/run_tests_batched.py- WHERE:
scripts/run_tests_batched.py— add a new tier (or extend an existing one) that runstests/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 aftertier3and before the smoke tier. - HOW: Read
scripts/run_tests_batched.pyconfig → addtier_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
- WHERE:
-
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-<YYYYMMDD>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_*.mdfiles. - RUN: N/A (docs file).
- COMMIT:
docs(visual-verification): add guide for the 4-layer visual verification protocol
- WHERE: New file
-
Task 8.4: Update
conductor/tracks.mdschema- WHERE:
conductor/tracks.md— find the schema section (or add a new "Track Completion Gates" section). - WHAT: Add a new section documenting the
VERIFIED-<YYYYMMDD>tag requirement for tracks that touchsrc/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-<date> tag requirement for tracks touching src/gui_2.py
- WHERE:
-
Task 8.5: Update
docs/Readme.mdto reference the new guide- WHERE:
docs/Readme.md— find the "Per-Source-File Deep Dives" section (or equivalent) → adddocs/guide_visual_verification.mdentry. - 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
- WHERE:
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_guifixture; 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_treefixture 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)
- WHERE: New file
-
Task 9.2: Run full test batch
- WHERE: All test files added in Phase 1-9
- WHAT: Run
scripts/run_tests_batched.pyend-to-end. Verify all tiers PASS. - HOW:
uv run python scripts/run_tests_batched.py— runs the full batch (not justtier_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.pyfrom 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-<date>tag- WHERE: Master branch
- WHAT: User commits
git tag VERIFIED-20260629 <final-commit-sha>on master. Documents the visual verification. - HOW:
git tag VERIFIED-20260629 <sha>. 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
- WHERE: New file
-
Task 9.6: Update
conductor/tracks.mdto mark this track complete- WHERE:
conductor/tracks.md— find the row fordefault_layout_extract_20260629→ mark[x](withVERIFIED-20260629tag referenced). - WHAT: Update the row.
- HOW: Read
conductor/tracks.md→ find the row → update. - SAFETY: HARD GATE. The
[x]requires theVERIFIED-<date>tag to exist. If absent, leave the row as[ ]. - COMMIT:
conductor(tracks): mark default_layout_extract_20260629 complete (with VERIFIED-20260629 tag)
- WHERE:
-
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-<date>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,ErrorKindfromsrc.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.