conductor(track): init default_layout_install_20260629 (empty INI -> install default; layouts/ at root + src/layouts.py; reset_layout path cleanup)

Bug: when cwd/manualslop_layout.ini is missing/empty after first-run,
post-deletion, or post-corrupt-INI, the GUI panels are not visible
despite show_windows[name] = True. Root cause is structural: imgui.begin
without [Window][name] + DockId in the INI produces a floating window
that gets clipped by the full-screen dockspace. Empirically confirmed:
8s of running produces a 585-byte INI containing only [Window][Debug##Default].

Fix shape (4 phases):
  Phase 1: relocate tests/artifacts/manualslop_layout_default.ini ->
           layouts/default.ini (at repo root, parallel to themes/ per
           user directive 'no configs in src/'); add src/paths.py
           'layouts' field + SLOP_GLOBAL_LAYOUTS env override (mirror
           themes pattern at line 60/83/150/210-216); add src/layouts.py
           loader module (mirror src/theme_models.py + src/theme_2.py
           contract; LayoutFile = @dataclass(frozen=True, slots=True)
           per the C11/Odin/Jai-in-Python value-type mandate).
  Phase 2: install-on-empty-INI in App._post_init. _install_default_layout_if_empty
           helper + drain helper, called BEFORE _diag_layout_state and
           BEFORE immapp.run. logs '[GUI] installed default layout: <src> -> <dst>'.
  Phase 3: drop hardcoded 'tests/artifacts/live_gui_workspace/...' path
           from src/commands.py:reset_layout line 369-376 (dead code in
           production; violates 'production code defaults to immediate
           directory' directive 2026-06-29).
  Phase 4: 3-test regression suite in tests/test_default_layout_install.py
           + 1 unit test in tests/test_reset_layout.py; user manual verify
           (delete INI, run sloppy.py standalone, see panels).

TDD red-first per task. Atomic per-task commits with git notes (per
conductor/workflow.md §Task Workflow step 9-10). No day estimates per
conductor/workflow.md §Tier 1 Track Initialization Rules.

Out of scope (deferred): panel_defs_fleury_migration - migrate the ~40
render_x functions to declarative PanelDef records per Ryan Fleury's
raddbg 'type view' / 'lens' pattern. Spec §Eventual Normalization Target
documents the design sketch + the transcripts at docs/transcripts/.
This track sets up layouts/ at repo root + src/layouts.py as the typed
loader so the future migration has somewhere to land.

Tracks.md row will be added in Phase 4 (Task 4.6) when the track ships.
This commit is contained in:
ed
2026-06-29 14:02:41 -04:00
parent 47564bb56a
commit 5ad062b13a
4 changed files with 491 additions and 0 deletions
@@ -0,0 +1,161 @@
## Phase 1: Move default layout + create layouts/ stack (parallel to themes/)
Focus: relocate `tests/artifacts/manualslop_layout_default.ini` to `layouts/default.ini` at repo root; add the parallel `src/paths.py` field, `get_layouts_dir()` accessor, and `src/layouts.py` loader module — exactly the themes pattern (`themes/` + `src/path.py:60,83,150` + `src/theme_models.py` + `src/theme_2.py`).
- [ ] Task 1.1: Verify bundled layout content + themes pattern baseline
- WHERE: `tests/artifacts/manualslop_layout_default.ini` (109 lines), `src/paths.py:60,83,150,210-216`, `src/theme_models.py:181-225`, `src/theme_2.py:340-346`, `themes/` at repo root
- WHAT: confirm files exist with the expected sizes and that the themes pattern is the canonical reference
- HOW: `git log --oneline -- tests/artifacts/manualslop_layout_default.ini` for provenance; `Get-Content src/paths.py | Select-String -Pattern "themes\s*[:=]|themes\s*=|root_dir/"` to map the path-resolution shape
- SAFETY: pure read; no behavior change
- [ ] Task 1.2: `git mv` asset to new home
- WHERE: `tests/artifacts/manualslop_layout_default.ini` → `layouts/default.ini` (new dir at repo root, parallel to `themes/`)
- WHAT: `git mv tests/artifacts/manualslop_layout_default.ini layouts/default.ini`
- HOW: PowerShell `git mv` preserves history; verify with `git status` after
- SAFETY: file rename, no content change; `layouts/` is gitignored? verify — `grep -i "layouts" .gitignore` should return nothing (or only `tests/artifacts/` excluding layouts/)
- [ ] Task 1.3: Update `tests/conftest.py:709` to read from `layouts/`
- WHERE: `tests/conftest.py:709` — `_default_layout_src = project_root / "tests" / "artifacts" / "manualslop_layout_default.ini"`
- WHAT: change to `_default_layout_src = project_root / "layouts" / "default.ini"`
- HOW: `manual-slop_edit_file`; preserve 1-space indentation per `conductor/code_styleguides/python.md`
- SAFETY: no semantic change to test behavior; same bundled content, new path
- [ ] Task 1.4: Add `layouts` field to `src/paths.py` config dataclass (mirror themes)
- WHERE: `src/paths.py:60` (`themes: Path = ...`) — add a `layouts: Path = ...` field right after
- WHAT: add the field declaration matching the `themes` shape exactly
- HOW: `manual-slop_edit_file`; 1-space indent
- SAFETY: additive — does not change existing fields
- [ ] Task 1.5: Resolve `layouts` default in `src/paths.py` (mirror themes)
- WHERE: `src/paths.py:83` (`themes = root_dir / "themes",`) — add `layouts = root_dir / "layouts",` immediately below
- WHAT: resolve the default path in the `initialize_paths`-style function
- HOW: `manual-slop_edit_file`; ensure the same closure/call-site shape as themes
- SAFETY: additive; existing themes path unchanged
- [ ] Task 1.6: Add `SLOP_GLOBAL_LAYOUTS` env + config override (mirror themes)
- WHERE: `src/paths.py:150` — add `_resolve_path("SLOP_GLOBAL_LAYOUTS", "layouts", root_dir / "layouts", config_path)` line in the same call shape
- WHAT: register the env var + config-file override for `layouts`, parallel to themes
- HOW: `manual-slop_edit_file`; exact-string preserve the existing `_resolve_path` call for themes
- SAFETY: additive; new env var only
- [ ] Task 1.7: Add `get_layouts_dir()` accessor to `src/paths.py` (mirror themes)
- WHERE: `src/paths.py:210-216` — add 2 functions (`get_layouts_dir() -> Path` + `get_layouts_project_config_path() -> Path` if themes has it) right after
- WHAT: accessor functions
- HOW: `manual-slop_edit_file`; preserve docstring format
- SAFETY: additive
- [ ] Task 1.8: Create `src/layouts.py` loader module (mirror `src/theme_models.py` + `src/theme_2.py`)
- WHERE: new file `src/layouts.py`
- WHAT: define `LayoutFile` `@dataclass(frozen=True, slots=True)` with `(name: str, raw_text: str, source_path: Path, scope: str)` fields; define `load_layouts_from_dir(path: Path, scope: str) -> dict[str, LayoutFile]` and `load_layouts_from_file(path: Path, scope: str) -> dict[str, LayoutFile]`; define `load_layouts_from_disk() -> None` that calls both with global + project paths; wrap parse errors in `Result` per `conductor/code_styleguides/error_handling.md`
- HOW: model after `src/theme_models.py:181-225` (`load_themes_from_dir`, `load_themes_from_toml`) + `src/theme_2.py:340-346` (`load_themes_from_disk`)
- SAFETY: new file, no existing code modification; uses `from __future__ import annotations` + `@dataclass(frozen=True, slots=True)` per `conductor/code_styleguides/data_oriented_design.md` §8.5
- [ ] Task 1.9: Add `src/layouts.py` to `tests/test_gui2_layout.py`-adjacent test inventory if present; for now, just verify import works
- WHERE: `tests/`
- WHAT: `uv run python -c "from src.layouts import load_layouts_from_disk; print(load_layouts_from_disk())"` to verify the module imports and returns a dict (empty by default since the test cwd has no `layouts/`)
- HOW: direct Python invocation
- SAFETY: pure inspection
- [ ] Task 1.10: Commit phase 1 with git note
- WHAT: `chore(layouts): introduce layouts/ directory + src/layouts.py (themes pattern); relocate default layout asset`
- HOW: standard atomic commit per `conductor/workflow.md` §Task Workflow; attach a 3-line git note explaining: relocation from tests/artifacts; parallel to themes; src/layouts.py mirrors src/theme_models.py + src/theme_2.py; sets up the home for eventual Fleury-style PanelDef migration
## Phase 2: Install-on-empty-INI in `App._post_init`
Focus: ship `layouts/default.ini` to `cwd/manualslop_layout.ini` when the file is missing/empty/small, before `immapp.run(...)` reads it.
- [ ] Task 2.1: Write failing test for install behavior
- WHERE: new file `tests/test_default_layout_install.py`
- WHAT: red phase — 3 tests:
1. `test_default_layout_installed_when_ini_missing` — `os.remove(cwd/manualslop_layout.ini)` before launch; `subprocess.Popen(sloppy_args, cwd=temp_workspace)`; wait ≥ 5s; assert `manualslop_layout.ini` exists with `[Window][Project Settings]` entry + a non-empty `DockId=` line
2. `test_default_layout_installed_when_ini_empty` — write a 5-byte stub INI before launch; same assertions as (1)
3. `test_default_layout_NOT_installed_when_layout_present` — pre-write a custom `[Window][CustomPanel]` INI; assert the custom panel survives (no overwrite)
- HOW: each test spawns the app via `subprocess.Popen(["uv", "run", "python", "-u", "sloppy.py", "--enable-test-hooks"], cwd=temp_workspace, stdout=log_file, stderr=log_file, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)` (mirrors the conftest at line 792), waits 5-8s, terminates via `kill_process_tree()` (per the conftest pattern at line 853), then asserts on the saved INI
- SAFETY: tests MUST NOT touch the repo-root `manualslop_layout.ini`; each test uses its own cwd (per `conductor/code_styleguides/workspace_paths.md`); temp workspace path = `Path("tests/artifacts/_default_layout_install_<pid>")`
- [ ] Task 2.2: Run phase 2.1 tests; confirm RED (fails for the right reason)
- WHERE: `tests/test_default_layout_install.py`
- HOW: `uv run pytest tests/test_default_layout_install.py -v --tb=short --timeout=120`
- Expected: 3 tests fail because no install logic exists yet; the temp-workspace INI is empty or absent post-launch
- [ ] Task 2.3: Implement `_install_default_layout_if_empty` helper
- WHERE: new module-level function `_install_default_layout_if_empty(src_ini: Path, dst_ini: Path) -> Result[bool]` near `_diag_layout_state` (`src/gui_2.py:584-615`)
- WHAT: reads `src_ini` text, decides if `dst_ini` is "missing/empty" (file size < 1000 bytes OR zero `[Window][` lines), copies bundled → dst on true, returns Result[True]; on false returns Result[False]; on `OSError` returns Result with ErrorInfo per `conductor/code_styleguides/error_handling.md`
- HOW: `shutil.copy2` for atomic copy; `sys.stderr.write(f"[GUI] installed default layout: {src_ini} -> {dst_ini}\n")` for the user-visible log
- SAFETY: thread-safe (no shared state); pure file I/O; 1-space indentation per project rule
- [ ] Task 2.4: Wire the helper into `App._post_init`
- WHERE: `src/gui_2.py:570-582` (`App._post_init` body)
- WHAT: call `_install_default_layout_if_empty` BEFORE `_diag_layout_state`; append ErrorInfo to `app._startup_timeline_errors` if `not result.ok`
- HOW: `install_result = _install_default_layout_if_empty_result(app, src_path, dst_path)`; if not ok, drain via `_startup_timeline_errors` per the existing pattern at line 580-582
- SAFETY: `_post_init` runs on the main thread (HelloImGui callback), no race
- [ ] Task 2.5: Add drain helper `_install_default_layout_if_empty_result`
- WHERE: `src/gui_2.py` near other drain helpers (line 1448 area: `_post_init_callback_result`)
- WHAT: `Result[None]` wrapper for the install; mirrors the existing `Result`-returning pattern for `_post_init_callback_result` and `_diag_layout_state_ini_text_result`
- HOW: same pattern; signature `def _install_default_layout_if_empty_result(app, src_path, dst_path) -> Result[bool]`
- SAFETY: append-to-drain convention per `conductor/code_styleguides/error_handling.md`
- [ ] Task 2.6: Verify phase 2.1 tests now pass
- WHERE: `tests/test_default_layout_install.py`
- HOW: `uv run pytest tests/test_default_layout_install.py -v --tb=short --timeout=120`
- Expected: all 3 pass; the post-launch INI has 7+ `[Window][X]` entries
- [ ] Task 2.7: Run adjacent test batch (`tests/test_gui*.py`) to confirm no regression
- WHERE: `tests/test_gui2_layout.py`, `tests/test_gui_diagnostics.py`, `tests/test_layout_reorganization.py`
- HOW: `uv run python scripts/run_tests_batched.py --tier test_gui*` (per `conductor/workflow.md` §"Tier 2 Autonomous Sandbox" — use the batched runner, never raw pytest for batched verification)
- Expected: prior batch_green preserved; no _post_init regressions
- [ ] Task 2.8: Commit phase 2 with git note
- WHAT: `fix(gui): install default layout when cwd/manualslop_layout.ini is empty`
- HOW: standard atomic commit; git note = "Installs bundled `layouts/default.ini` (resolved via the new src/layouts.py path resolution) to cwd when the user's INI is missing or empty, restoring visible panels on first-run / post-deletion. Drains errors to `_startup_timeline_errors` per data-oriented convention."
- [ ] Task 2.9: User Manual Verification
- PAUSE; await user `yes` after deleting `manualslop_layout.ini` and launching `sloppy.py` standalone — confirm panels are visible
## Phase 3: Remove hardcoded test-fixture path from production code
Focus: `src/commands.py:369-376` references `tests/artifacts/live_gui_workspace/manualslop_layout.ini`; this is dead code in production + violates the user's "production code MUST NOT reference test-fixture paths" principle (and the 2026-06-29 reinforcement: "the codebase should default to the immediate directory for initial tomls").
- [ ] Task 3.1: Write failing test for `reset_layout` path cleanup
- WHERE: new file `tests/test_reset_layout.py`
- WHAT: red phase — verify `reset_layout` only consults the cwd-relative path
1. `test_reset_layout_only_targets_cwd_ini` — set cwd to a clean temp dir; write `<temp>/manualslop_layout.ini`; create `<temp>/tests/artifacts/live_gui_workspace/manualslop_layout.ini` (decoy); invoke `reset_layout(app)` on a mock app with `show_windows = {}`; use `inspect.getsource(commands.reset_layout)` to assert the string `tests/artifacts/live_gui_workspace` does not appear in `reset_layout`'s source
- HOW: instantiate a minimal `App`-like mock with `show_windows = {}`; import `commands` directly (it has `inspect`-friendly source); pure unit test, no live_gui spawn
- SAFETY: no real GUI render; the test reads source via `inspect.getsource()`
- [ ] Task 3.2: Run phase 3.1 tests; confirm RED
- HOW: `uv run pytest tests/test_reset_layout.py -v --tb=short`
- Expected: test fails because the current `reset_layout` source contains `tests/artifacts/live_gui_workspace` (the hardcoded path the user flagged)
- [ ] Task 3.3: Remove the hardcoded path from `commands.reset_layout`
- WHERE: `src/commands.py:369-376`
- WHAT: `layout_paths = ["manualslop_layout.ini"]` (drop the `os.path.join("tests", ...)` line)
- HOW: `manual-slop_edit_file` with `old_string` containing both `layout_paths = [` and the `os.path.join(...)` line; replace with `layout_paths = ["manualslop_layout.ini"]`
- SAFETY: shrinks the function; no behavior change for end users (cwd-relative was the only functional path)
- [ ] Task 3.4: Update `commands.reset_layout` docstring
- WHERE: `src/commands.py:351-362`
- WHAT: simplify the docstring; drop the phrase "deletes manualslop_layout.ini so hello_imgui regenerates a fresh" if no longer accurate
- HOW: minimal edit via `manual-slop_edit_file`
- SAFETY: docstring only, no behavior change
- [ ] Task 3.5: Verify phase 3.1 tests now pass
- HOW: `uv run pytest tests/test_reset_layout.py -v --tb=short`
- Expected: 1 test passes; the `inspect.getsource` assertion holds
- [ ] Task 3.6: Run adjacent test_batch (`tests/test_commands*.py`)
- HOW: `uv run python scripts/run_tests_batched.py --filter test_commands*`
- Expected: no regression
- [ ] Task 3.7: Commit phase 3 with git note
- WHAT: `chore(commands): remove dead test-fixture path from reset_layout`
- HOW: standard atomic commit; git note = "Reset_layout referenced `tests/artifacts/live_gui_workspace/manualslop_layout.ini`, dead code in production. Removed per user directive 2026-06-29: production code must default to the immediate directory."
## Phase 4: Verification
Focus: full-batch confirmation; per-target test runs; cross-reference the original bug report.
- [ ] Task 4.1: Confirm spec acceptance criteria via test execution
- WHERE: `tests/test_default_layout_install.py`, `tests/test_reset_layout.py`, `tests/test_gui*.py`, `tests/test_commands*.py`
- HOW: `uv run python scripts/run_tests_batched.py --filter "test_default_layout_install|test_reset_layout|test_gui2_layout|test_gui_diagnostics|test_layout_reorganization|test_commands"`
- Acceptance:
- G1 (install on empty INI) — `test_default_layout_installed_when_ini_missing` passes
- G2 (overrides cleared on install) — `test_default_layout_installed_when_ini_empty` passes
- G3 (`reset_layout` path cleanup) — `test_reset_layout_only_targets_cwd_ini` passes
- G4 (regression test for visibility-after-empty) — all 3 `test_default_layout_install` tests pass
- G5 / G6 / G7 (layouts/ stack) — `tests/conftest.py:709` reads from new path; live_gui fixture unaffected; `src/layouts.py` importable
- G8 (conftest path update) — `tests/conftest.py:709` reads from `layouts/default.ini`
- [ ] Task 4.2: Empirical reproduction of the original bug
- WHERE: production cwd (no test harness)
- HOW: `Remove-Item manualslop_layout.ini -ErrorAction SilentlyContinue`; `uv run python sloppy.py`; observe via screenshot or VNC that Project Settings, Files & Media, AI Settings, Discussion Hub, Operations Hub, Theme, Log Management, Diagnostics are all visible
- SAFETY: requires user confirmation; this is the manual verification step per `conductor/workflow.md` §"Phase Completion Verification"
- [ ] Task 4.3: Checkpoint commit + verification git note
- WHAT: `conductor(checkpoint): end of Phase 4 (default_layout_install_20260629 complete)`
- HOW: empty commit allowed per `conductor/workflow.md` §"Phase Completion Verification"; attach a long-form verification report as git note documenting the 1 asset relocated (`tests/artifacts/manualslop_layout_default.ini` → `layouts/default.ini`), 2 new files (`layouts/default.ini`, `src/layouts.py`), 1 src command file modified (`commands.reset_layout`), 1 paths file modified (`src/paths.py` adds `layouts` field), 1 conftest updated (`tests/conftest.py:709`), 3 test files added (`tests/test_default_layout_install.py`, `tests/test_reset_layout.py`, etc.), and the empirical reproduction result
- [ ] Task 4.4: Append phase checkpoint + completion SHAs to `plan.md`
- WHERE: this file
- WHAT: append `[checkpoint: <7-char SHA>]` after each phase header + `[track complete: <7-char SHA>]` at end
- [ ] Task 4.5: Commit final plan update
- WHAT: `conductor(plan): mark default_layout_install_20260629 phases 1-4 complete`
- [ ] Task 4.6: Add row to `conductor/tracks.md`
- WHERE: `conductor/tracks.md` — Active Tracks table
- WHAT: add a row for `default_layout_install_20260629` with status "shipped 2026-06-29" after Phase 4 completes
- HOW: insert a new row following the table conventions; commit in the same Phase 4 commit batch