# Modernization Debt Log

Status: live
Last updated: 2026-06-17

Every shortcut, temporary adapter, retained vendored dependency, skipped
platform gate, compatibility shim, or incomplete automation path must be
recorded here before it lands. Entries must be specific enough for a future
agent or engineer to remove them without reconstructing context from chat.

## Entry Rules

- Add an entry before merging the shortcut.
- Reference the debt id in code comments, TODOs, ADRs, or roadmap notes.
- Include an owner, reason, validation command, and removal condition.
- Do not close an entry until the removal condition is met and validated.
- Prefer deleting shortcuts over expanding this log.

## Reductions

- 2026-06-17: `DEBT-0039` was narrowed again. `App::dialog_browse()` in
  `src/app_dialogs_workflow.cpp` now delegates retained browse-dialog button
  wiring and selected-path open execution through
  `src/legacy_document_open_services.*`, so the app dialog shell no longer
  owns that document-open handoff inline while retained dialog creation,
  unsaved-project prompting, project-open execution, and title/layer refresh
  still remain.
- 2026-06-17: `DEBT-0003` was narrowed again. `App::update()` in
  `src/legacy_app_runtime_shell_services.cpp` no longer owns retained
  app-frame layout-update and canvas-toolbar refresh execution inline;
  `src/legacy_app_frame_services.*` now owns that frame-update execution
  behind an explicit `App&` plus `AppFrameUpdatePlan` seam, while draw-time
  execution, UI observer walking, and broader app-frame ownership still
  remain.
- 2026-06-17: `DEBT-0003` was narrowed again. `App::tick()` and
  `App::resize()` in `src/app_events.cpp` no longer own retained app-frame
  tick and surface-resize execution inline; `src/legacy_app_frame_services.*`
  now owns the tick/resize execution against explicit `App&` plus frame plans,
  while broader event dispatch and runtime/platform frame ownership still
  remain.
- 2026-06-17: `DEBT-0045` was narrowed again.
  `src/app_layout_ui_state.cpp` no longer owns retained floating/docked panel
  save/restore serialization inline; that panel-persistence family now lives in
  `src/legacy_app_ui_state_services.*`, while RTL direction execution and
  broader preference/runtime ownership still remain.
- 2026-06-17: `DEBT-0035` was narrowed again.
  `src/app_layout_main_toolbar.cpp` no longer owns the retained main-toolbar
  button wiring inline; `src/legacy_main_toolbar_binding_services.*` now owns
  the per-button bindings behind explicit `App&` plus toolbar-root
  dependencies, while the app-layout file remains a thin layout-root adapter
  and retained toolbar execution still lives in
  `src/legacy_app_shell_services.*`.
- 2026-06-17: `DEBT-0031`/`DEBT-0030` were narrowed again.
  `src/app_layout_file_menu.cpp` no longer owns the retained File-menu popup,
  export-submenu popup, or button wiring inline; that binding now lives in
  `src/legacy_file_menu_binding_services.*`, while retained file/export command
  execution still lives in `src/legacy_app_shell_services.*`.
- 2026-06-17: `DEBT-0034` was narrowed again.
  `src/app_layout_about_layer_menu.cpp` no longer owns the retained About-menu
  popup creation, version-label setup, or button wiring inline; that binding
  now lives in `src/legacy_about_menu_binding_services.*`, while retained About
  command execution and diagnostics still live in
  `src/legacy_app_shell_services.*`.
- 2026-06-17: `DEBT-0033` was narrowed again.
  `src/app_layout_tools_menu.cpp` no longer owns the retained Tools > Panels
  submenu popup flow or panel-item wiring inline; that binding now lives in
  `src/legacy_tools_menu_binding_services.*`, while retained Tools command
  execution and options-menu preference wiring still remain in the app shell.
- 2026-06-17: `DEBT-0063` was narrowed again.
  `src/app_layout_sidebar.cpp` no longer owns the retained color-popup open /
  close wiring inline; that binding now lives in
  `src/legacy_sidebar_color_popup_services.*` with explicit `App&`,
  popup-root, trigger-button, and panel dependencies, while retained
  stroke/grid/layer popup families still remain on the sidebar shell.
- 2026-06-17: `DEBT-0038` was narrowed again. The retained cloud upload and
  download background execution in `src/legacy_cloud_services.cpp` now routes
  through `AppRuntime::canvas_async_task` instead of a file-static worker
  singleton, while retained prompt/progress lifetime, OpenGL context guards,
  thumbnail loading, and transfer execution still remain in the cloud bridge.
- 2026-06-17: `DEBT-0048` was narrowed again. The retained ABR/PPBR import path
  in `src/legacy_brush_package_import_services.cpp` now uses
  `AppRuntime::canvas_async_task` instead of a file-static worker singleton,
  while the legacy preset panel remains the importer/storage owner and broader
  brush asset/UI ownership transfer still remains open.
- 2026-06-17: `DEBT-0044` was narrowed again. The retained asynchronous
  timelapse export path in `src/legacy_document_export_services.cpp` now routes
  through `AppRuntime::canvas_async_task` instead of a file-static worker
  singleton, while retained `App::rec_export`, animation MP4 execution, and
  mobile/Web completion paths still remain in the video-export bridge.
- 2026-06-17: `DEBT-0036` was narrowed again. `src/canvas_layer.cpp` now
  routes retained `Layer` / `LayerFrame` render queueing through
  `src/renderer_gl/render_runtime_dispatch.h` instead of direct
  `App::I->render_task*` calls; retained `Canvas::I` draw-state use and the
  broader canvas renderer execution remain.
- 2026-06-17: `DEBT-0034` was narrowed again.
  `LegacyAboutMenuServices::run_performance_test(...)` in
  `src/legacy_app_shell_services.cpp` now delegates the retained stroke/timing
  workflow to `src/legacy_app_shell_performance_test_services.*`, so the About
  menu adapter no longer owns the inline render-task/canvas execution body.
- 2026-06-17: `DEBT-0040`/`DEBT-0042` were narrowed again. `App::dialog_save()`
  and `App::dialog_save_ver()` in `src/app_dialogs_workflow.cpp` now delegate
  Save As / Save Version acceptance through
  `src/legacy_document_session_services.*`, so the app dialog shell no longer
  owns the retained plan/target lookup entrypoints for those save paths.
- 2026-06-17: `DEBT-0047` was narrowed again. The desktop async PPBR export
  path in `src/legacy_brush_package_export_services.cpp` now uses
  `AppRuntime::canvas_async_task` plus named completion helpers instead of a
  file-static worker singleton, while retained preset-panel execution and
  dialog-owned request extraction still remain.
- 2026-06-17: `DEBT-0036` was narrowed again. Retained `Shape`,
  `src/shader.cpp`, and `src/font.cpp` now queue render-thread create/update/
  destroy work through `src/renderer_gl/render_runtime_dispatch.h` instead of
  calling `App::I` directly, and `Shader` render-thread entry points now assert
  that explicit runtime contract while shader-program and mesh ownership remain
  retained.
- 2026-06-17: `DEBT-0036` was narrowed again. `NodeStrokePreview` no longer
  owns static worker-thread, queue, or render-mutex state directly;
  `src/legacy_node_stroke_preview_runtime_services.*` now owns the retained
  preview worker lifecycle and queue helpers, leaving the node as a thinner
  adapter while the retained preview runtime still reaches app/render helpers
  internally.
- 2026-06-17: `DEBT-0029` was narrowed again.
  `LegacyFileMenuServices::pick_image_for_import()` in
  `src/legacy_app_shell_services.cpp` now delegates retained import execution
  to `src/legacy_document_image_import_services.*`, so the File-menu adapter no
  longer defines the live `DocumentImageImportServices` implementation inline;
  legacy `Image` loading and retained canvas/import-mode execution still remain.
- 2026-06-17: `DEBT-0003` was narrowed again. `src/app_runtime.cpp` and
  `src/app_runtime.h` now synchronize render/UI worker running state, reject
  cross-thread work once those workers stop, and drain queued work through
  explicit runtime-owned post/shutdown semantics consumed by
  `src/legacy_app_runtime_shell_services.cpp`; broader singleton reach, app
  shell ownership, and remaining runtime service adoption still remain.
- 2026-06-17: `DEBT-0017` was narrowed again.
  `cmake/PanoPainterSources.cmake` no longer compiles
  `${PP_PLATFORM_WEB_SOURCES}` into `PP_PANOPAINTER_APP_SOURCES`, so root app
  source ownership no longer mixes live Web platform implementation files into
  the retained app group; broader platform entrypoint/package cleanup remains.
- 2026-06-17: `DEBT-0043` was narrowed again.
  `src/legacy_canvas_document_io_services.cpp` now delegates the live
  equirectangular export family through `src/legacy_document_export_services.*`
  instead of carrying that orchestration inline with the broader canvas
  document-I/O shell; retained export directory creation, Web handoff, and
  remaining legacy `Canvas` export execution still remain.
- 2026-06-17: `DEBT-0003` was narrowed again.
  `src/platform_windows/windows_runtime_flow.*` now owns the live Win32
  startup/session composition flow, including the bound app/session preflight
  and the existing `convert` / `-vrmode` mode switch, so
  `src/platform_windows/windows_runtime_shell.cpp` is down to a thin entry
  wrapper and no longer repeats manual app/session teardown on each exit path.
- 2026-06-17: `DEBT-0017` was narrowed again. The orphaned
  `src/platform_legacy/legacy_platform_services.*` shim and its
  `legacy_platform_fallback_behavior.h` helper were deleted after the root app,
  WebGL entrypoint, and retained Android package graphs all moved off that
  fallback path.
- 2026-06-17: `DEBT-0017` was narrowed again. The retained Android package
  CMake projects now compile `src/platform_android/android_platform_services.cpp`
  instead of `src/platform_legacy/legacy_platform_services.cpp`, matching the
  Android entrypoint's existing concrete `create_platform_services(...)` path.
  The retained standard package link gate still fails, but the same package
  build also fails after restoring the old legacy source list, so that linker
  failure is currently tracked as preexisting noise rather than a regression
  from this slice.
- 2026-06-17: `DEBT-0017`/`DEBT-0055` were narrowed again.
  `src/platform_legacy/legacy_platform_services.*` is no longer compiled into
  the root `panopainter_app` source graph. The retained shim still exists on
  disk for non-root compatibility consumers, but the root Windows/Android/Apple
  modernization matrix no longer links it into the live app target.
- 2026-06-17: `DEBT-0017`/`DEBT-0050`/`DEBT-0053`/`DEBT-0057` were narrowed
  again. `webgl/src/main.cpp` now binds a concrete
  `src/platform_web/web_platform_services.*` `PlatformServices` instance
  directly, so the live WebGL entrypoint no longer depends on
  `src/platform_legacy/legacy_platform_services.*` for render-context,
  prepared-file, picker, app-close, or default-canvas behavior.
- 2026-06-17: `DEBT-0003` was narrowed again.
  `src/platform_windows/windows_runtime_shell.cpp` now binds a local
  `MainWindowSession` object for the live Win32 session, and the window handle,
  title buffer, and sandbox flag now live on that explicit runtime-owned
  session instead of the retired retained accessor pocket.
- 2026-06-17: `DEBT-0016`/`DEBT-0017`/`DEBT-0050`/`DEBT-0051`/`DEBT-0053`/
  `DEBT-0057` were narrowed again. `src/platform_web/web_platform_services.*`
  now owns the concrete Web `PlatformServices` implementation and
  `webgl/src/main.cpp` now binds that owned service directly, so
  `src/platform_legacy/legacy_platform_services.*` no longer carries the
  touched Web render-context, app-close, picker, storage-path,
  persistent-storage, default-canvas, prepared-file, or default-render-target
  branches, and `src/platform_legacy/legacy_platform_state.*` is gone.
- 2026-06-17: `DEBT-0016`/`DEBT-0017`/`DEBT-0051` were narrowed again.
  `src/platform_linux/linux_platform_services.*` now owns the concrete Linux
  `PlatformServices` implementation and `linux/src/main.cpp` now binds that
  owned service directly, so `src/platform_legacy/legacy_platform_services.*`
  no longer carries the touched Linux storage-path, GLFW render-context,
  app-close, desktop picker, default-render-target, or FPS-reporting branches.
- 2026-06-17: `DEBT-0016`/`DEBT-0017` were narrowed again.
  `src/platform_android/android_platform_services.*` now owns the concrete
  Android `PlatformServices` implementation and
  `android/src/cpp/main.cpp` now binds that owned service directly, so
  `src/platform_legacy/legacy_platform_services.*` no longer carries the
  Android bridge/configuration surface or the touched Android execution
  branches.
- 2026-06-17: `DEBT-0016`/`DEBT-0017`/`DEBT-0050`/`DEBT-0051`/`DEBT-0052`/
  `DEBT-0053` were narrowed again. `src/platform_apple/apple_platform_services.*`
  now owns the concrete Apple `PlatformServices` implementation and
  `create_apple_platform_services()`, the Apple entrypoints bind that owned
  service directly, and `src/platform_legacy/legacy_platform_services.*` no
  longer carries the touched Apple execution branches or Apple provider
  configuration surface.
- 2026-06-17: `DEBT-0016`/`DEBT-0017`/`DEBT-0051`/`DEBT-0052`/`DEBT-0053`
  were narrowed again. `PanoPainter-OSX/main.cpp` and
  `PanoPainter/GameViewController.m` now bind owned legacy
  `PlatformServices` instances into `App`, and
  `src/platform_legacy/legacy_platform_services.*` now takes an injected Apple
  document-service provider instead of hardcoding direct calls to
  `active_legacy_apple_document_platform_services()` in the touched path.
- 2026-06-17: `DEBT-0016`/`DEBT-0017` were narrowed again.
  `src/platform_legacy/legacy_platform_services.*` now takes an explicit
  Android bridge through `create_platform_services(...)`, and
  `android/src/cpp/main.cpp` now seeds Android-owned clipboard, keyboard,
  JNI-thread attach/detach, async render-context, and file-picker callbacks
  into that adapter instead of leaving those JNI/EGL entrypoints declared
  directly inside the cross-platform fallback layer.
- 2026-06-17: `DEBT-0017`/`DEBT-0050`/`DEBT-0053`/`DEBT-0057` were narrowed
  again. `src/platform_legacy/legacy_platform_services.*` now takes an
  explicit `WebPlatformServices*` dependency through
  `create_platform_services(...)`, `webgl/src/main.cpp` now threads its owned
  Web service directly into that legacy adapter, and
  `src/platform_legacy/legacy_platform_state.*` no longer carries the retained
  `active_legacy_web_platform_services()` / `try_*legacy_web*` dispatch layer;
  retained non-Windows adapter execution still exists, but the live WebGL path
  no longer depends on a legacy Web fallback singleton-style bridge.
- 2026-06-17: `DEBT-0017` was narrowed again.
  `src/platform_legacy/legacy_platform_services.*` no longer exposes the dead
  `pp::platform::legacy::platform_services()` fallback singleton; Linux,
  WebGL, and Android already bind owned `create_platform_services(...)`
  instances at their entrypoints, so the retained non-Windows debt is now
  limited to the adapter implementation itself rather than a live global
  accessor surface.
- 2026-06-16: `DEBT-0003` was narrowed again. `main.cpp` main-thread queued
  task state now lives behind a narrow retained helper instead of
  `RetainedState.main_tasklist` / `main_task_mutex` directly; broader Win32
  bootstrap/runtime ownership remains.
- 2026-06-16: `DEBT-0037` was narrowed again. `App::update_rec_frames()` in
  `src/app.cpp` now delegates recording label refresh through
  `update_legacy_recording_frame_label(...)` in
  `src/legacy_recording_services.cpp` instead of building the label directly;
  retained recording label lookup, encoder-state reads, and MP4 execution
  remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` non-`draw_merged`
  per-frame layer draw callback setup now routes through
  `make_legacy_canvas_draw_merge_layer_frame_draw(...)` in
  `src/legacy_canvas_draw_merge_services.h` instead of keeping that callback
  body inline in `NodeCanvas::draw()`; broader canvas draw orchestration and
  retained GL resource ownership remain.
- 2026-06-16: `DEBT-0003` was narrowed again. The Win32 async GL/context lock
  state now lives in `src/platform_windows/windows_platform_services.cpp`
  instead of `src/main.cpp` retained state, and `main.cpp` only seeds the
  created `HDC`/`HGLRC` into that platform-owned helper during initialization
  and context recreation; broader Win32 bootstrap/runtime ownership remains.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` merged-path
  per-plane merged-texture draw callback setup now routes through
  `make_legacy_canvas_draw_merge_layer_texture_draw(...)` in
  `src/legacy_canvas_draw_merge_services.h` instead of keeping that callback
  setup inline in `NodeCanvas::draw()`; broader canvas draw orchestration and
  retained GL resource ownership remain.
- 2026-06-16: `DEBT-0037` was narrowed again. `App::rec_loop()` in
  `src/app.cpp` now delegates recording worker iteration orchestration into
  `process_legacy_recording_worker_iteration(...)` in
  `src/legacy_recording_services.cpp` instead of owning the wait/plan/encode
  shell itself; retained recording loop control, readback call sites, and MP4
  execution remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` checkerboard
  background-plane callback setup now routes through
  `make_legacy_canvas_draw_merge_background_checkerboard_plane(...)` in
  `src/legacy_canvas_draw_merge_services.h` instead of keeping that callback
  inline in `NodeCanvas::draw()`; broader canvas draw orchestration and
  retained GL resource ownership remain.
- 2026-06-16: `DEBT-0003` was narrowed again. Canvas async
  import/export/save/open background work now runs through an
  `AppRuntime`-owned queue/worker in `src/app_runtime.h/.cpp`, and
  `src/canvas.cpp` no longer defines a retained static canvas async worker;
  broader app task ownership and retained runtime singleton reach remain.
- 2026-06-16: `DEBT-0037` was narrowed again. `App::rec_loop()` in
  `src/app.cpp` now routes its wait plus iteration plan/encode shell through a
  local helper instead of carrying that orchestration inline; retained
  recording loop control, readback call sites, and MP4 execution remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` grid-mode draw
  callback setup now routes through
  `make_legacy_canvas_draw_merge_grid_modes_draw(...)` in
  `src/legacy_canvas_draw_merge_services.h` instead of keeping that callback
  loop inline in `NodeCanvas::draw()`; broader canvas draw orchestration and
  retained GL resource ownership remain.
- 2026-06-16: `DEBT-0003` was narrowed again. Prepared-file background work
  now runs through an `AppRuntime`-owned queue/worker in
  `src/app_runtime.h/.cpp`, and `src/app_events.cpp` no longer defines a
  retained static prepared-file worker; broader app task ownership and canvas
  async worker ownership remain.
- 2026-06-16: `DEBT-0037` was narrowed again. `App::rec_loop()` in
  `src/app.cpp` now routes its worker-iteration pointer lookup plus
  `plan_recording_worker_iteration(...)` setup through a local helper instead
  of carrying that setup inline; retained recording loop control, readback call
  sites, and MP4 execution remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` current-mode draw
  callback setup now routes through
  `make_legacy_canvas_draw_merge_current_modes_draw(...)` in
  `src/legacy_canvas_draw_merge_services.h` instead of keeping that callback
  loop inline in `NodeCanvas::draw()`; broader canvas draw orchestration and
  retained GL resource ownership remain.
- 2026-06-16: `DEBT-0037` was narrowed again. `App::rec_loop()` in
  `src/app.cpp` now routes its coherent frame encode/update chunk through a
  local helper instead of carrying dirty-stroke clearing, equirect PBO
  creation, the worker-side `yield`, `ImageRef` creation, encode, and frame
  label refresh inline; retained recording loop control, readback call sites,
  and MP4 execution remain.
- 2026-06-16: `DEBT-0003` was narrowed again. The prepared-file worker in
  `src/app_events.cpp` and the canvas async worker in `src/canvas.cpp` now sit
  behind named retained local worker-state helpers instead of bare static
  worker accessors; retained app/canvas singleton reach and broader runtime
  ownership remain.
- 2026-06-16: `DEBT-0003` was narrowed again. `src/main.cpp` now keeps Win32
  window handles, GL/task mutex state, stylus timers, splash-dialog state, and
  VR worker state behind one retained local state object instead of separate
  file-scope globals; retained entrypoint orchestration, direct `App::I`
  mutation, and broader platform/runtime coupling remain.
- 2026-06-16: `DEBT-0017` was narrowed again. Retained GLFW window hooks and
  the non-Linux fallback storage-path return in
  `src/platform_legacy/legacy_platform_services.cpp` now route through local
  retained-state helpers instead of direct method-body `App::I` reads; the
  retained platform fallback adapter and broader platform-to-app singleton
  reach remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` heightmap draw
  callback setup now routes through
  `make_legacy_canvas_draw_merge_heightmap_draw(...)` in
  `src/legacy_canvas_draw_merge_services.h` instead of keeping that callback
  body inline in `NodeCanvas::draw()`; broader canvas draw orchestration and
  retained GL resource ownership remain.
- 2026-06-16: `DEBT-0003` was narrowed again. The Windows main-loop run-state
  and VR worker coordination flags in `src/main.cpp` now use `std::atomic`
  instead of unsynchronized globals; retained Win32 entrypoint ownership,
  global app singleton reach, and broader runtime coupling remain.
- 2026-06-16: `DEBT-0017` was narrowed again. Retained Apple ObjC handles plus
  storage paths now live behind one local helper in
  `src/platform_legacy/legacy_platform_services.cpp`, and the iOS SonarPen
  bridge now starts through that retained Apple state instead of reading
  `App::I` inside the bridge body; the retained Apple fallback adapter and
  broader platform-to-app singleton reach remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` smoothing-mask face
  shader setup plus per-face draw execution now route through
  `execute_legacy_canvas_draw_merge_smask_faces(...)` in
  `src/legacy_canvas_draw_merge_services.h` instead of spelling that pass out
  inline in `NodeCanvas::draw()`; broader canvas draw orchestration and
  retained GL resource ownership remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` merged-path
  per-plane merged-texture draw execution now routes through
  `execute_legacy_canvas_draw_merge_layer_texture(...)` instead of spelling out
  the sampler bind, `TextureAlpha` setup, texture bind, draw, and unbind
  inline in `NodeCanvas::draw()`; broader canvas draw orchestration remains.
- 2026-06-16: `DEBT-0003` was narrowed again. Windows VR device ownership in
  `src/main.cpp` now uses `std::unique_ptr<Vive>` instead of a raw global
  `Vive*`; retained global VR state, Win32 message-loop ownership, and broader
  app/runtime coupling remain.
- 2026-06-16: `DEBT-0017` was narrowed again. Linux/Web GLFW render-context
  acquire/present hooks and Linux app-close now route through retained local
  GLFW callback hooks in `src/platform_legacy/legacy_platform_services.cpp`
  instead of direct method-body `App::I` access; retained fallback platform
  ownership and broader singleton reach remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` cache-to-screen
  checkerboard-plane callback setup now routes through
  `make_legacy_canvas_draw_merge_cache_to_screen_checkerboard_plane(...)` in
  `src/legacy_canvas_draw_merge_services.h` instead of building the full MVP
  callback body inline in `NodeCanvas::draw()`; broader canvas draw
  orchestration remains.
- 2026-06-16: `DEBT-0003` was narrowed again. `LogRemote` now owns its network
  logging worker as `std::jthread` with explicit stop requests in `src/log.*`
  instead of raw `std::thread` ownership; retained global logger singleton,
  network logging policy, and queue execution remain.
- 2026-06-16: `DEBT-0017` was narrowed again. Apple crash-test, app-close, and
  iOS SonarPen hooks now route through explicit bridge callbacks in
  `src/platform_apple/apple_platform_services.*` consumed by
  `src/platform_legacy/legacy_platform_services.cpp` instead of direct `App::I`
  calls in those legacy platform methods; the retained Apple fallback adapter
  and broader platform singleton reach remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` merged-path and
  non-blend checkerboard background setup now route through
  `execute_legacy_canvas_draw_merge_background_setup(...)` in
  `src/legacy_canvas_draw_merge_services.h` instead of keeping that shared
  background loop inline in both branches of `NodeCanvas::draw()`; broader
  canvas draw orchestration remains.
- 2026-06-16: `DEBT-0003` was narrowed again. The Windows splash-dialog worker
  and HMD renderer worker in `src/main.cpp` now use `std::jthread` with
  explicit stop requests instead of raw `std::thread` ownership; retained
  global VR state, Win32 message-loop ownership, and broader app runtime
  coupling remain.
- 2026-06-16: `DEBT-0017` was narrowed again. Apple render-context
  acquire/release/present hooks and iOS main-render-target binding now route
  through explicit bridge callbacks in
  `src/platform_apple/apple_platform_services.*` consumed by
  `src/platform_legacy/legacy_platform_services.cpp` instead of direct `App::I`
  calls in those legacy platform methods; the retained Apple fallback adapter
  and broader platform singleton reach remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` non-`draw_merged`
  per-layer/per-plane retained draw execution now routes through
  `execute_legacy_canvas_draw_merge_layer_plane(...)` in
  `src/legacy_canvas_draw_merge_services.h` instead of keeping the erase,
  temporary paint/composite, regular texture, and optional blend path fully
  inline in `NodeCanvas::draw()`; retained layer traversal and broader canvas
  renderer orchestration remain.
- 2026-06-16: `DEBT-0003` was narrowed again. `AppRuntime` now owns its
  render/UI worker threads as `std::jthread` with explicit stop requests in
  `src/app_runtime.cpp` and `src/app_runtime.h` instead of raw `std::thread`
  ownership; retained app task call sites, singleton composition, and broader
  runtime/platform coupling remain.
- 2026-06-16: `DEBT-0017`/`DEBT-0053` were narrowed again. iOS virtual-keyboard
  visibility and prepared-file save handoff now route through explicit Apple
  bridge callbacks in `src/platform_apple/apple_platform_services.*` consumed
  by `src/platform_legacy/legacy_platform_services.cpp` instead of direct
  `App::I` calls in those legacy platform methods; the retained Apple fallback
  adapter and broader platform singleton reach remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` smoothing-mask
  overlay draw, smoothing-mask face pass, grid keepalive draw, heightmap draw,
  and current-mode draw now route through
  `execute_legacy_canvas_draw_merge_post_draw(...)` in
  `src/legacy_canvas_draw_merge_services.h` instead of living inline in
  `NodeCanvas::draw()`; broader canvas draw orchestration and retained GL
  resource ownership remain.
- 2026-06-16: `DEBT-0036` was narrowed again. The retained
  `NodePanelGrid::bake_uvs()` worker now uses scoped `std::jthread` ownership
  instead of a raw local `std::thread`; retained bake execution, progress-loop
  polling, and grid rendering ownership remain.
- 2026-06-16: `DEBT-0017` was narrowed again.
  `LegacyPlatformServices::prepare_storage_paths()` now routes Apple storage
  path setup through a local helper instead of reading `App::I` directly in
  the method body; the retained Apple fallback adapter and broader
  platform-to-app singleton reach remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeStrokePreview` background
  preview execution now owns its worker as `std::jthread` with explicit stop,
  unblock, and join semantics instead of a raw `std::thread`; retained queue
  ownership, static renderer resources, and preview execution flow remain.
- 2026-06-16: `DEBT-0037` was narrowed again. Recording worker ownership now
  uses `std::jthread` through `App::rec_thread` and
  `src/legacy_recording_services.cpp`, with explicit stop requests before the
  existing notify/join flow; retained recording loop control, progress
  lifetime, and export execution remain.
- 2026-06-16: `DEBT-0017`/`DEBT-0051`/`DEBT-0052`/`DEBT-0055` were narrowed
  again. `active_apple_document_platform_services()` in
  `src/platform_legacy/legacy_platform_services.cpp` now captures explicit
  Apple view/app handles for its bridge callbacks instead of closing over
  `App::I` inside those lambdas, while the rest of the retained platform
  fallback still owns broader singleton reach and platform execution.
- 2026-06-16: `DEBT-0043`/`DEBT-0040`/`DEBT-0042` were narrowed again. The
  retained `Canvas` async import/export/save/open entrypoints in
  `src/canvas.cpp` no longer launch detached threads; they now use an owned
  local `std::jthread` queue and return completion callbacks to the UI thread,
  while retained progress-node mutation, render-task orchestration, and canvas
  storage/export ownership remain.
- 2026-06-16: `DEBT-0044` was narrowed again. The retained asynchronous
  timelapse export path in `src/legacy_document_export_services.cpp` no longer
  launches a detached worker; it now uses a service-owned `std::jthread`
  queue and returns the success dialog to the UI thread, while retained
  recording/export execution still stays behind the legacy bridge.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` cache-to-screen
  checkerboard plus cache-texture composite now route through
  `legacy_canvas_draw_merge_services.h` instead of living inline in
  `NodeCanvas::draw()`; broader canvas draw orchestration remains retained.
- 2026-06-16: `DEBT-0051`/`DEBT-0052`/`DEBT-0055` were narrowed again.
  `src/platform_apple/apple_platform_services.*` no longer reaches `App::I`
  for clipboard, display/share, cursor-visibility, or save-ui-state behavior;
  those calls now flow through narrow injected Apple bridge callbacks from
  `src/platform_legacy/legacy_platform_services.cpp`, while retained Apple
  bridge construction and broader platform singleton reach remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` density-resolve
  display execution now routes through
  `legacy_canvas_draw_merge_services.h` instead of living inline in
  `NodeCanvas::draw()`; the cache-to-screen composite block and broader canvas
  draw orchestration remain retained.
- 2026-06-16: `DEBT-0053` was narrowed again. `App::pick_file_save(...)` no
  longer launches a detached worker for background prepared-file writes; it
  now uses a service-owned `std::jthread` queue and posts prepared-file save
  completion back to the UI thread, while retained platform save/download
  handoff execution remains.
- 2026-06-16: `DEBT-0036` was narrowed again. The retained grid lightmap
  launch in `src/legacy_grid_ui_services.cpp` no longer uses a detached
  worker thread; it now uses a service-owned `std::jthread` queue with
  UI-thread state handoff, while retained bake execution and grid rendering
  ownership remain.
- 2026-06-16: `DEBT-0048` was narrowed again. The retained ABR/PPBR import
  bridge in `src/legacy_brush_package_import_services.cpp` no longer launches
  detached worker threads; it now uses a service-owned `std::jthread` queue,
  while retained preset ownership, progress UI, and storage mutation remain.
- 2026-06-16: `DEBT-0047` was narrowed again. The retained PPBR export bridge
  in `src/legacy_brush_package_export_services.cpp` no longer launches a
  detached desktop worker; it now uses a service-owned `std::jthread` queue
  and returns dialog close plus success-message work to the UI thread, while
  retained dialog data extraction, preset export ownership, and mobile/Web
  completion remain.
- 2026-06-16: `DEBT-0036` was narrowed again. `NodeStrokePreview` final
  composite plus preview-copy execution now routes through
  `legacy_node_stroke_preview_execution_services.h` instead of living inline
  in `draw_stroke_immediate()`; retained live-pass sequencing, GL resource
  ownership, and node-owned draw execution remain.
- 2026-06-15: `DEBT-0042` was narrowed again. The retained Save Version dialog
  wiring in `src/app_dialogs.cpp` now routes through a focused helper instead
  of living inline in `App::dialog_save_ver()`; the remaining document-session
  bridge debt stays concentrated in close prompts, save dialogs, app document
  field mutation, and keyboard/dialog cleanup.
- 2026-06-15: `DEBT-0042` was narrowed again. The retained Save dialog button
  wiring in `src/app_dialogs.cpp` now routes through a focused helper instead
  of living inline in `App::dialog_save()`; the remaining document-session
  bridge debt stays concentrated in close prompts, save-version routing, app
  document field mutation, and keyboard/dialog cleanup.
- 2026-06-15: `DEBT-0042` was narrowed again. The retained Save Version
  execution in `src/legacy_document_session_services.cpp` now routes through a
  focused helper instead of living inline in
  `LegacyDocumentVersionSaveServices::save_document_version()`; the remaining
  document-session bridge debt stays concentrated in close prompts, save
  dialogs, overwrite prompts, and keyboard/dialog cleanup.
- 2026-06-15: `DEBT-0040` was narrowed again. The retained close-unsaved
  prompt wiring in `src/legacy_document_session_services.cpp` now routes
  through a focused helper instead of living inline in
  `show_unsaved_close_prompt()`; the remaining document-session bridge debt
  stays concentrated in save-before-workflow prompts, save-version routing,
  app document field mutation, and keyboard/dialog cleanup.
- 2026-06-15: `DEBT-0040` was narrowed again. The retained save-before-
  continue workflow prompt wiring in `src/legacy_document_session_services.cpp`
  now routes through a focused helper instead of living inline in
  `prompt_save_before_continue()`; the remaining document-session bridge debt
  stays concentrated in close prompts, native close requests, save-version
  routing, app document field mutation, and keyboard/dialog cleanup.
- 2026-06-15: `DEBT-0040`/`DEBT-0041`/`DEBT-0042` were narrowed again. The
  retained overwrite-prompt wiring in `src/legacy_document_session_services.cpp`
  now routes through a focused helper instead of living inline in the new
  document and save-file prompt methods; the remaining document-session bridge
  debt stays concentrated in close prompts, native close requests, save-version
  routing, app document field mutation, and keyboard/dialog cleanup.
- 2026-06-15: `DEBT-0039` was narrowed again. The retained ABR/PPBR import
  prompt wiring in `src/legacy_document_open_services.cpp` now routes through
  a focused helper instead of living inline in each document-open import
  prompt; the remaining document-open bridge debt stays concentrated in
  unsaved-project prompts, project-open execution, layer refresh, title
  updates, and retained history services.
- 2026-06-15: `DEBT-0039` was narrowed again. The retained document-browse
  button wiring in `App::dialog_browse()` now routes through a focused helper
  in `src/app_dialogs.cpp` instead of living inline in the retained browse
  dialog method; the remaining document-open bridge debt stays concentrated in
  unsaved-project prompts, project-open execution, layer refresh, title
  updates, and retained history services.
- 2026-06-15: `DEBT-0039` was narrowed again. The retained unsaved-project
  discard prompt wiring in `LegacyDocumentOpenServices::prompt_discard_unsaved_project()`
  now routes through a focused helper in `src/legacy_document_open_services.cpp`
  instead of living inline in the retained document-open service method; the
  remaining document-open bridge debt stays concentrated in project-open
  execution, layer refresh, title updates, and retained history services.
- 2026-06-15: `DEBT-0039` was narrowed again. The retained project-open
  success/failure handling in `open_legacy_project()` now routes through a
  focused helper in `src/legacy_document_open_services.cpp` instead of living
  inline in the retained document-open helper; the remaining document-open
  bridge debt stays concentrated in layer refresh, title updates, and retained
  history services.
- 2026-06-15: `DEBT-0039` was narrowed again. The retained project-open
  layer/title update work and failure-path error dialog in
  `handle_open_legacy_project_result()` now route through focused helpers in
  `src/legacy_document_open_services.cpp` instead of living inline in the
  retained document-open helper; the remaining document-open bridge debt stays
  concentrated in retained history services.
- 2026-06-15: `DEBT-0039` was narrowed again. The retained project-open
  history-planning call in `open_legacy_project()` now routes through a focused
  helper in `src/legacy_document_open_services.cpp` instead of living inline in
  the retained document-open helper; the remaining document-open bridge debt
  stays concentrated in retained history services.
- 2026-06-15: `DEBT-0039`/`DEBT-0040` were narrowed again. The retained
  history adapter now reuses the shared `src/legacy_history_services.*` helper
  from both document-open and document-session bridges instead of living
  inline in those bridge files; the remaining bridge debt stays concentrated in
  retained history services.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud downloaded-
  project open camera reset and layer-clear setup in
  `execute_legacy_downloaded_project_open()` now routes through a focused
  helper in `src/legacy_document_open_services.cpp` instead of living inline
  in the document-open bridge; the remaining cloud bridge debt stays
  concentrated in retained prompt/progress lifetime, OpenGL context guarding,
  and the still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud downloaded-
  project open, layer refresh, and action-history reset in
  `execute_legacy_downloaded_project_open()` now route through a focused
  helper in `src/legacy_document_open_services.cpp` instead of living inline
  in the document-open bridge; the remaining cloud bridge debt stays
  concentrated in retained prompt/progress lifetime, OpenGL context guarding,
  and the still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud publish
  prompt setup in `show_cloud_publish_prompt()` now routes through a focused
  helper in `src/legacy_cloud_services.cpp` instead of living inline in the
  retained prompt/setup body; the remaining cloud bridge debt stays
  concentrated in retained prompt/progress lifetime, OpenGL context guarding,
  and the still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud publish
  detached-thread launch in `show_cloud_publish_prompt()` now routes through a
  focused helper in `src/legacy_cloud_services.cpp` instead of living inline
  in the retained prompt/setup body; the remaining cloud bridge debt stays
  concentrated in retained prompt/progress lifetime, OpenGL context guarding,
  and the still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud publish
  transfer-and-success body in `execute_cloud_publish_worker()` now routes
  through a focused helper in `src/legacy_cloud_services.cpp` instead of
  living inline in the retained worker body; the remaining cloud bridge debt
  stays concentrated in retained prompt/progress lifetime, OpenGL context
  guarding, and the still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud download
  flow in `execute_cloud_download_thread()` now routes through a focused
  helper in `src/legacy_cloud_services.cpp` instead of living inline in the
  retained worker body; the remaining cloud bridge debt stays concentrated in
  retained prompt/progress lifetime, OpenGL context guarding, and the
  still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud download
  post-transfer open/close sequence in `execute_cloud_download_thread()` now
  routes through a focused helper in `src/legacy_cloud_services.cpp` instead
  of living inline in the retained worker body; the remaining cloud bridge
  debt stays concentrated in retained prompt/progress lifetime, OpenGL
  context guarding, and the still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud download
  URL construction and progress-callback wiring in
  `execute_cloud_download_thread()` now routes through a focused helper in
  `src/legacy_cloud_services.cpp` instead of living inline in the retained
  worker body; the remaining cloud bridge debt stays concentrated in retained
  prompt/progress lifetime, OpenGL context guarding, and the still-retained
  transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud download
  progress-dialog creation path in `execute_cloud_download_thread()` now
  routes through a focused helper in `src/legacy_cloud_services.cpp` instead
  of living inline in the retained worker body; the remaining cloud bridge
  debt stays concentrated in retained prompt/progress lifetime, OpenGL
  context guarding, and the still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud download
  thread launch in `LegacyCloudServices::start_download()` now routes through
  a focused helper in `src/legacy_cloud_services.cpp` instead of living
  inline in the retained service method; the remaining cloud bridge debt stays
  concentrated in retained prompt/progress lifetime, OpenGL context guarding,
  and the still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud browser
  dialog creation path in `LegacyCloudServices::show_browser()` now routes
  through a focused helper in `src/legacy_cloud_services.cpp` instead of
  living inline in the retained service method; the remaining cloud bridge
  debt stays concentrated in retained prompt/progress lifetime, OpenGL
  context guarding, and the still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud publish
  prompt path in `LegacyCloudServices::prompt_publish()` now routes through a
  focused helper in `src/legacy_cloud_services.cpp` instead of living inline
  in the retained service method; the remaining cloud bridge debt stays
  concentrated in retained prompt/progress lifetime, OpenGL context guarding,
  and the still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud
  save-required warning path in `LegacyCloudServices::show_save_required_warning()`
  now routes through a focused helper in `src/legacy_cloud_services.cpp`
  instead of living inline in the retained service method; the remaining
  cloud bridge debt stays concentrated in retained prompt/progress lifetime,
  OpenGL context guarding, and the still-retained transfer-thread execution
  model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud bulk-upload
  per-file execution loop in `LegacyCloudServices::upload_all_bulk_files()`
  now routes through a focused helper in `src/legacy_cloud_services.cpp`
  instead of living inline in the retained service method; the remaining
  cloud bridge debt stays concentrated in retained prompt/progress lifetime,
  OpenGL context guarding, and the still-retained transfer-thread execution
  model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud bulk-upload
  progress lifetime wiring in `LegacyCloudServices::begin_bulk_upload()` and
  `end_bulk_upload()` now routes through focused helpers in
  `src/legacy_cloud_services.cpp` instead of living inline in the retained
  service methods; the remaining cloud bridge debt stays concentrated in
  retained prompt/progress lifetime, OpenGL context guarding, and the
  still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud-browser
  OK-button wiring in `LegacyCloudServices::show_browser()` now routes through
  a focused helper in `src/legacy_cloud_services.cpp` instead of living
  inline in the retained service method; the remaining cloud bridge debt stays
  concentrated in retained prompt/progress lifetime, OpenGL context guarding,
  and the still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud-publish
  prompt button wiring in `LegacyCloudServices::prompt_publish()` now routes
  through a focused helper in `src/legacy_cloud_services.cpp` instead of
  living inline in the retained service method; the remaining cloud bridge
  debt stays concentrated in retained prompt/progress lifetime, OpenGL context
  guarding, and the still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud-publish
  worker body in `LegacyCloudServices::prompt_publish()` now routes through a
  focused helper in `src/legacy_cloud_services.cpp` instead of living inline
  in the retained service method; the remaining cloud bridge debt stays
  concentrated in retained prompt/progress lifetime, OpenGL context guarding,
  and the still-retained transfer-thread execution model.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud-download
  detached worker body in `LegacyCloudServices::start_download()` now routes
  through a focused helper in `src/legacy_cloud_services.cpp` instead of
  living inline in the retained service method; the remaining cloud bridge
  debt stays concentrated in retained prompt/progress lifetime, OpenGL context
  guarding, and transfer-thread execution.
- 2026-06-15: `DEBT-0038` was narrowed again. The retained cloud-browser
  thumbnail fetch, decode, and texture-apply path in
  `NodeDialogCloud::load_thumbs_thread()` now routes through a focused helper
  in `src/node_dialog_cloud.cpp` instead of living inline in the retained
  loader body; the remaining cloud bridge debt stays concentrated in retained
  prompt/progress lifetime, OpenGL context guarding, and transfer-thread
  execution.
- 2026-06-15: `DEBT-0038` was narrowed again. The cloud-browser slot
  creation and selection wiring in `NodeDialogCloud::load_thumbs_thread()`
  now route through a focused helper in `src/node_dialog_cloud.*` instead of
  living inline in the retained loader body; the remaining cloud bridge debt
  stays concentrated in retained prompt/progress lifetime, OpenGL context
  guarding, `NodeDialogCloud` thumbnail execution, and transfer-thread
  execution.
- 2026-06-15: `DEBT-0038` was narrowed again. The cloud-browser file-list
  request/response handling in `NodeDialogCloud::load_thumbs_thread()` now
  routes through a focused helper in `src/node_dialog_cloud.*` instead of
  living inline in the retained loader body; the remaining cloud bridge debt
  stays concentrated in retained prompt/progress lifetime, OpenGL context
  guarding, `NodeDialogCloud` thumbnail execution, and transfer-thread
  execution.
- 2026-06-15: `DEBT-0038` was narrowed again. The initial cloud-browser
  loading placeholder setup in `NodeDialogCloud::load_thumbs_thread()` now
  routes through a focused helper in `src/node_dialog_cloud.*` instead of
  living inline in the retained loader body; the remaining cloud bridge debt
  stays concentrated in retained prompt/progress lifetime, OpenGL context
  guarding, `NodeDialogCloud` network/thumbnail execution, and transfer-thread
  execution.
- 2026-06-15: `DEBT-0038` was narrowed again. Retained cloud downloaded-project
  post-open reconciliation now routes through `src/legacy_document_open_services.*`
  instead of living inline in `src/legacy_cloud_services.cpp`; the remaining
  cloud bridge debt stays concentrated in retained prompt/progress lifetime,
  OpenGL context guarding, `NodeDialogCloud`, and transfer-thread execution.
- 2026-06-15: `DEBT-0038` was narrowed again. Retained save-before-upload
  execution now routes through `src/legacy_document_session_services.*`
  instead of `src/legacy_cloud_services.cpp` calling
  `Canvas::I->project_save_thread(...)` directly; the remaining cloud bridge
  debt stays concentrated in retained prompt/progress lifetime, OpenGL context
  guarding, downloaded-project open, layer refresh, and action-history reset.
- 2026-06-15: `DEBT-0038` was narrowed again. Retained cloud upload
  post-transfer response/error handling now routes through a dedicated helper
  in `src/legacy_cloud_services.cpp` instead of living inline in
  `execute_cloud_upload_transfer(...)`; the remaining cloud bridge debt stays
  concentrated in save-before-upload execution, retained prompt/progress
  lifetime, OpenGL context guarding, downloaded-project open, layer refresh,
  and action-history reset.
- 2026-06-15: `DEBT-0038` was narrowed again. Retained cloud upload form
  construction now routes through a dedicated helper in
  `src/legacy_cloud_services.cpp` instead of living inline in
  `execute_cloud_upload_transfer(...)`; the remaining cloud bridge debt stays
  concentrated in save-before-upload execution, upload response/error handling,
  retained prompt/progress lifetime, OpenGL context guarding,
  downloaded-project open, layer refresh, and action-history reset.
- 2026-06-15: `DEBT-0038` was narrowed again. Retained upload/download CURL
  execution, TLS-bypass policy consumption, and progress callback dispatch now
  live in `src/legacy_cloud_services.*` instead of `App::upload` and
  `App::download`; the remaining cloud bridge debt is now concentrated in
  save-before-upload execution, upload form/response handling, retained
  prompt/progress lifetime, OpenGL context guarding, downloaded-project open,
  layer refresh, and action-history reset.
- 2026-06-15: `DEBT-0038`/`DEBT-0063` were narrowed again. The cloud browser
  dialog in `src/legacy_cloud_services.cpp` now opens through
  `src/legacy_ui_overlay_services.*` and participates in the checked overlay
  lifetime seam instead of direct raw `layout[main_id]->add_child(...)`
  ownership; remaining cloud debt is now concentrated in retained worker
  threads, network transfer helpers, project-open refresh, and dialog-internal
  legacy behavior.
- 2026-06-15: `DEBT-0035`/`DEBT-0063` were narrowed again. The
  main-toolbar settings dialog in `src/legacy_app_shell_services.cpp` now opens
  through `src/legacy_ui_overlay_services.*` and participates in the checked
  overlay lifetime seam instead of direct raw `layout[main_id]->add_child(...)`
  ownership; cloud-browser and other remaining retained dialog families still
  stay open under the same debts.
- 2026-06-15: `DEBT-0058`/`DEBT-0063` were narrowed again. App-owned
  progress/message/input dialogs created through
  `src/legacy_ui_overlay_services.*` now open through checked overlay handles
  when the main layout anchor is available instead of only attaching raw root
  children; toolbar settings, cloud browser, and other remaining retained
  dialog families still stay open under the same debt until they move to the
  same lifetime model.
- 2026-06-16: `DEBT-0049` was narrowed again. `pp_assets_brush_package_tests`
  now names the legacy PPBR compatibility fixture set explicitly: `0.0`, `0.1`,
  `0.2`, `1.1`, `2.1`, and rejected `1.2`. The live parser still preserves the
  legacy tolerance until a stricter supported version matrix is proven safe.
- 2026-06-15: `DEBT-0063` was narrowed again. Sidebar stroke/color/layer/grid
  popups, file/about/layer menu popups in `src/app_layout.cpp`, quick
  brush/color popup+ticker overlays in `src/legacy_quick_ui_services.cpp`,
  combo-box popup open/close in `src/node_combobox.cpp`, and picker
  outside-click teardown in `src/node_dialog_picker.cpp` now route through
  checked overlay handles or explicit handle-safe close callbacks instead of
  raw attach-and-destroy popup ownership; remaining popup families still stay
  open under the same debt until the last retained dialog/menu surfaces are
  converted.
- 2026-06-15: `DEBT-0058`/`DEBT-0063` were narrowed again. App-owned About,
  Changelog, User Manual, New Document, Open, Browse, Save, Resize, Layer
  Rename, and PPBR Export dialogs now open and close through checked overlay
  handles in `src/app_dialogs.cpp`, and the corresponding retained dialog nodes
  no longer self-destroy their cancel/OK buttons through
  `bind_legacy_click_destroys_node(...)`; menu/layout-owned popup families still
  remain on separate retained cleanup paths.
- 2026-06-17: `DEBT-0058`/`DEBT-0063` were narrowed again. The retained
  usermanual, changelog, and about openers in `src/app_dialogs_info_openers.cpp`
  now delegate retained dialog construction and overlay close wiring through
  `src/legacy_info_dialog_services.*` with explicit `App&` plus overlay-anchor
  dependencies, so the app dialog shell no longer owns that info-dialog family
  inline while `open_whatsnew_dialog()` still owns the retained whats-new flow
  inline.
- 2026-06-17: `DEBT-0058`/`DEBT-0063` were narrowed again. The retained New
  Document and Save dialog openers in `src/app_dialogs_workflow.cpp` now
  delegate retained dialog construction and button wiring through
  `src/legacy_document_session_services.*` with explicit `App&` ownership, so
  the app dialog shell no longer owns that document-session dialog family
  inline while browse, open, and resize flows still remain there.
- 2026-06-17: `DEBT-0035` was narrowed again. `App::title_update()` and the
  draw-toolbar/stroke-popup binding families now delegate through
  `src/legacy_app_status_services.*`,
  `src/legacy_draw_toolbar_binding_services.*`, and
  `src/legacy_sidebar_stroke_popup_services.*`, so
  `src/app_layout.cpp`, `src/app_layout_draw_toolbar.cpp`, and part of
  `src/app_layout_sidebar.cpp` are thinner adapters while retained app-shell
  execution and remaining popup families still stay open under the same debt.
- 2026-06-15: `DEBT-0036` was narrowed again. `NodeStrokePreview` now drops the
  retained pass-sequence, mix-execution, final-composite-request, background
  capture, and preview-copy wrapper structs/functions in favor of direct
  retained helper dispatch plus renderer-facing preview copy helpers; the live
  preview path still owns concrete framebuffer, texture-binding, and draw
  execution around those helpers.
- 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::stroke_commit()` now
  routes the retained pre-dispatch state capture through local commit helpers;
  the live path still owns concrete layer/action mutation until the retained
  commit adapter is fully split from `Canvas`.
- 2026-06-14: `DEBT-0063` was narrowed again. Open/Browse
  delete-confirmation now uses handle-based open/close in
  `src/node_dialog_open.cpp` and `src/node_dialog_browse.cpp`, with
  `attach_legacy_overlay_node_to_root` fallback removed; migration still pending in
  remaining panel families.
- 2026-06-14: `DEBT-0063` was narrowed again. `src/node_combobox.cpp` now uses
  overlay handle open/close (`open_legacy_overlay_node_with_handle` and
  `close_legacy_overlay_node`) for popup lifecycle, with the older attach/close
  overlay path removed in this file.
- 2026-06-14: `DEBT-0063` was narrowed again. `src/node_panel_stroke.cpp` now
  routes the stroke preset/brush/dual-brush/pattern popup open-close flow through
  checked overlay handles and explicit partial-open cleanup; migration is still
  pending in remaining panel families.
- 2026-06-16: `DEBT-0063` was narrowed again. `src/node_panel_stroke.cpp` now
  routes stroke popup close/reset through a shared overlay helper, and
  `src/node_popup_menu.cpp` now keeps the popup alive through selection dispatch
  with scoped ownership instead of raw `this` callbacks.
- 2026-06-14: `DEBT-0063` was narrowed again. `src/node_panel_brush.cpp` now
  routes brush popup-menu open-close flow through checked overlay handles and
  handle-based close on selection.
- 2026-06-14: `DEBT-0063` was narrowed again. `src/legacy_quick_ui_services.cpp`
  now converts quick brush/color popup lifecycle from raw attach+bind-destroy to
  checked-overlay open/close (`open_legacy_overlay_node_with_handle` and
  `close_legacy_overlay_node`) with explicit handle invalidation on closure.
- 2026-06-14: `DEBT-0063` was narrowed again. `src/node_popup_menu.h/.cpp`
  now routes popup-menu close through checked overlay handles and removes direct
  `close_legacy_popup_overlay(*this)` usage.
- 2026-06-14: `DEBT-0063` was narrowed again. `src/node_panel_layer.h/.cpp` now
  uses shared ownership for current-layer/m_layers tracking, guards stale-layer
  callback/mutation paths, and routes popup outside-click close through
  `close_legacy_popup_panel`; migration remains open in other panel/dialog
  families.
- 2026-06-14: `DEBT-0063` was narrowed again. `src/node_dialog_picker.cpp` now
  inlines outside-click/autohide popup close through capture release, parent
  detach, and guarded `on_popup_close` dispatch instead of routing that path
  through `close_legacy_popup_panel(*this, on_popup_close)`.
- 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::stroke_draw()` now
  routes the retained main-pass request assembly through helper APIs and keeps
  the live callsite focused on branch selection and pass dispatch.
- 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::stroke_draw()` now
  routes the main-pass destination texture binding declaration through
  `Canvas::make_stroke_draw_main_pass_request`; `Canvas::stroke_draw()` now only
  assembles execution inputs and dispatches the retained main-pass callback chain.
- 2026-06-14: `DEBT-0064` was narrowed again. Stroke-preview result copy now
  routes through `pp::paint_renderer::copy_stroke_preview_result_to_texture(...)`
  in `src/paint_renderer/compositor.h`, and the node-level copy wrapper,
  final-composite wrapper, and pass-sequence wrapper are gone. The compositor
  test-local `Texture2D::bind()` shim is also gone, while live preview still
  owns concrete texture binding and remaining OpenGL copy execution.
- 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::stroke_commit()` now
  routes the retained request-dispatch invocation through
  `execute_canvas_stroke_commit_dispatch(...)`; the wrapper still owns the
  state capture and helper dispatch.
- 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::stroke_commit()` now
  routes the retained sequence plan assembly through
  `make_canvas_stroke_commit_sequence_plan(...)`; the wrapper still owns the
  request dispatch and commit sequencing.
- 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::stroke_commit()` now
  routes the retained request build and execution through
  `execute_canvas_stroke_commit_request(...)`; the wrapper still owns the
  sequence plan assembly and dispatch.
- 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::draw_merge_final_plane_composite()`
  now routes the retained final-plane composite execution through
  `make_canvas_draw_merge_final_plane_composite_execution(...)`; the wrapper
  still owns request assembly and dispatch.
- 2026-06-14: `DEBT-0036` was narrowed again. `execute_canvas_draw_merge_plane_dispatch()`
  now routes the per-plane framebuffer setup and unbind handling through
  `execute_canvas_draw_merge_plane_setup(...)` plus
  `execute_canvas_draw_merge_plane_final_composite(...)`; the per-plane wrapper
  still owns the plane-selection loop and branch dispatch.
- 2026-06-14: `DEBT-0036` was narrowed again. `execute_canvas_draw_merge_plane_iteration()`
  now routes the per-plane dispatch wrapper through
  `execute_canvas_draw_merge_plane_dispatch(...)`; the plane loop still owns
  the plane-selection guard and helper dispatch.
- 2026-06-14: `DEBT-0036` was narrowed again. `execute_canvas_draw_merge_branch_body()`
  now routes the branch dispatch object assembly through
  `make_canvas_draw_merge_branch_dispatch(...)`; the branch body still owns the
  selection guard and dispatch wiring.
- 2026-06-15: `DEBT-0036` was narrowed again. `execute_canvas_draw_merge_branch_body()`
  now drops the unused `draw_checkerboard` flag from the branch helper chain,
  shrinking the helper API mismatch while `Canvas::draw_merge_branch_orchestration()`
  still owns the remaining branch orchestration.
- 2026-06-13: `STR-016` completed in `b9ed78e1`. The per-layer composite block
  moved out of `Canvas::draw_merge()`; archival record of the completed extraction.
- 2026-06-15: `DEBT-0036` was narrowed again. `Canvas::draw_merge_branch_orchestration()`
  now drops the unused `draw_checkerboard` parameter from the internal branch
  wrapper, and `Canvas::draw_merge_temporary_paint_branch()` now drops the
  unused `copy_blend_destination` parameter from the temporary-paint wrapper,
  but those changes are follow-on cleanup after `STR-016`, not blockers on the
  completed extraction.
- 2026-06-15: `DEBT-0036` was narrowed again. The stale
  `make_canvas_draw_merge_branch_dispatch(...)` forward declaration in
  `src/canvas.cpp` now matches the 7-argument implementation, and the
  remaining `Canvas::draw_merge()` inline-default comment in `src/canvas.cpp`
  now matches the header's `SIXPLETTE(true)` default; both are historical
  cleanup notes after `STR-016`, not reasons the task stayed blocked.
- 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::draw_merge_branch_orchestration()`
  now routes the temporary erase, temporary paint, texture, and blend dispatch
  bodies through retained helpers inside `execute_canvas_draw_merge_branch_body(...)`;
  the branch body still owns the selection guard and helper dispatch.
- 2026-06-14: `DEBT-0036` was narrowed again. `execute_canvas_draw_merge_branch_body()`
  now routes the temporary erase, texture, and blend dispatch bodies through
  `make_canvas_draw_merge_temporary_erase_dispatch(...)`,
  `make_canvas_draw_merge_layer_texture_dispatch(...)`, and
  `make_canvas_draw_merge_layer_blend_dispatch(...)`; the branch body still
  owns the selection guard and execution dispatch.
- 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::draw_merge_temporary_paint_branch()`
  now routes the retained temporary-paint request construction through
  `make_canvas_draw_merge_temporary_paint_request(...)`; the temporary-paint
  wrapper still owns the branch-selection guard and execution dispatch.
- 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::draw_merge_branch_orchestration()`
  now routes the retained branch execution body through
  `execute_canvas_draw_merge_branch_body(...)`; the branch wrapper still owns
  the branch-selection guard and dispatch wiring.
- 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::draw_merge()` now
  routes the final-composite gate through
  `execute_canvas_draw_merge_plane_final_composite(...)`; the plane-loop helper
  still owns the per-plane framebuffer setup and branch dispatch.
- 2026-06-14: `DEBT-0036` was narrowed again. `Canvas::draw_merge()` now
  routes its plane iteration and branch orchestration through
  `execute_canvas_draw_merge_plane_iteration(...)`; the canvas path still owns
  the per-plane framebuffer setup and retained branch execution callbacks.
- 2026-06-14: `DEBT-0036` was narrowed again. `execute_stroke_preview_final_composite_pass()`
  now routes the retained final-composite request construction through
  `make_stroke_preview_final_composite_request(...)`; the preview path still
  owns the concrete shader setup, sampler binding, input binding, and draw
  callback.
- 2026-06-14: `DEBT-0036` was narrowed again. `NodeStrokePreview::stroke_draw_mix()`
  now routes the retained preview mix request construction through
  `make_stroke_preview_mix_execution_request(...)`; the preview path still
  owns the concrete mixer framebuffer, sample/input binding, and draw callback.
- 2026-06-14: `DEBT-0036` was narrowed again. `execute_stroke_preview_sample_pass()`
  now routes the retained sample-request construction through
  `make_stroke_preview_sample_request(...)`; the preview path still owns the
  concrete destination binding, copy callback, and brush draw dispatch.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw_mix()` now
  routes the combined setup, request bundle, and shell execution through
  `make_legacy_canvas_stroke_mix_pass_shell(...)` plus
  `execute_legacy_canvas_stroke_mix_pass_shell(...)`; the live path still
  owns the concrete framebuffer bind and GL capability toggles.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw_mix()` now
  routes the combined setup and request bundle through
  `make_legacy_canvas_stroke_mix_pass_shell(...)`; the live path still owns
  the concrete framebuffer bind and GL capability toggles.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw_mix()` now
  routes the retained mix-pass shell through
  `execute_legacy_canvas_stroke_mix_pass_shell(...)`; the live path still owns
  the concrete framebuffer bind and GL capability toggles.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw_mix()` now
  routes the full retained mix-pass shell through
  `execute_legacy_canvas_stroke_mix_pass_with_setup(...)`; the live path
  still owns the concrete framebuffer bind and GL capability toggles.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw_mix()` now
  routes the setup/end shell through
  `make_legacy_canvas_stroke_mix_pass_setup(...)`; the live path still owns
  the concrete framebuffer bind and GL capability toggles.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw_mix()` now
  routes its retained mix-pass execution shell through
  `execute_legacy_canvas_stroke_mix_pass_with_setup(...)` with a concrete
  request builder; the live path still owns the OpenGL setup and per-plane
  texture wiring.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw_mix()` now
  routes retained mix-pass request wiring through
  `make_legacy_canvas_stroke_mix_pass_request(...)`; the legacy path still
  owns the concrete framebuffer setup and per-plane GL callbacks.
- 2026-06-13: `PLT-004` was narrowed again. `LegacyPlatformServices::display_file()`
  and `share_file()` now delegate Apple file actions through
  `AppleDocumentPlatformServices`, leaving the legacy adapter with only the
  cross-platform dispatch shell.
- 2026-06-13: `PLT-005` was narrowed again. Linux rendered-frame title updates
  now route through `src/platform_linux/linux_platform_services.*`; the legacy
  platform adapter still owns the surrounding cross-platform dispatch shell.
- 2026-06-13: `PLT-006` was narrowed again. macOS cursor visibility now routes
  through `src/platform_apple/apple_platform_services.*`; the legacy platform
  adapter still owns the surrounding cross-platform dispatch shell.
- 2026-06-13: `PLT-006` was narrowed again. macOS cursor visibility now routes
  through the Apple service boundary instead of the catch-all legacy adapter;
  the Apple path still owns the OS-specific cursor toggle.
- 2026-06-13: `PLT-007` was narrowed again. macOS UI-state saving now routes
  through the Apple service boundary instead of the catch-all legacy adapter;
  the Apple path still owns the OS-specific UI-state save call.
- 2026-06-13: `PLT-007` was narrowed again. macOS UI-state saving now routes
  through `src/platform_apple/apple_platform_services.*` instead of the
  catch-all legacy adapter; the Apple path still owns the OS-specific save call.
- 2026-06-13: `PLT-008` was narrowed again. Apple clipboard get/set now routes
  through `src/platform_apple/apple_platform_services.*` instead of the
  catch-all legacy adapter; the Apple path still owns the OS-specific clipboard
  calls.
- 2026-06-13: `PLT-008` was narrowed again. Apple clipboard get/set now routes
  through the Apple service boundary instead of the catch-all legacy adapter;
  the Apple path still owns the OS-specific clipboard calls.
- 2026-06-13: `PLT-009` was narrowed again. Apple clipboard helper methods now
  route through `src/platform_apple/apple_platform_services.*` instead of the
  catch-all legacy adapter; the Apple path still owns the OS-specific clipboard
  calls.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw_mix()`
  now calls the retained mix-pass executor directly instead of a local wrapper
  shell helper; the live path still owns the concrete mixer framebuffer setup
  and GL capability toggles.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw_mix()`
  now routes the remaining mixer framebuffer/capability shell through a local
  helper, leaving the call site with only mix-pass planning and helper
  invocation.
- 2026-06-13: `LATER-003` was narrowed again. The retained stroke mix shell
  now has direct executor regression coverage in
  `retained_stroke_mix_pass_shell_builder_preserves_combined_wiring`.
- 2026-06-13: `LATER-003` was narrowed again. The retained stroke mix shell
  now also has isolated executor regression coverage in
  `retained_stroke_mix_pass_shell_executor_preserves_combined_wiring`.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw_mix()`
  no longer computes an unused mix-plane plan in the live shell path.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw_mix()`
  now routes the remaining framebuffer setup callbacks through a local helper,
  leaving the method with only shell assembly and executor dispatch.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw_mix()`
  now keeps only retained shell assembly and executor dispatch at the callsite.
- 2026-06-13: `DEBT-0036` was narrowed again. `Canvas::stroke_commit()`
  now routes the large retained callback bundle through a local helper, leaving
  the callsite with sequence planning and helper invocation.
- 2026-06-13: `DEBT-0036` was narrowed again. `Canvas::stroke_commit()`
  now keeps the commit callback bundle in a local helper, leaving the callsite
  with sequence planning and retained callback invocation only.
- 2026-06-13: `DEBT-0036` was narrowed again. `Canvas::stroke_commit()`
  now routes the commit-input texture role switch through a local helper,
  leaving the callsite with sequence planning plus retained callback wiring.
- 2026-06-13: `DEBT-0036` was narrowed again. `Canvas::stroke_commit()`
  now routes the commit-input texture role switch through a retained service
  helper, leaving the callsite with sequence planning and concrete face
  bindings only.
- 2026-06-13: `DEBT-0036` was narrowed again. `Canvas::stroke_commit()`
  now routes the face-indexed commit-input binding wrapper through the retained
  service boundary, leaving `Canvas` with only concrete face bindings.
- 2026-06-13: `DEBT-0036` was narrowed again. `Canvas::stroke_commit()`
  now routes the retained commit request assembly through
  `make_legacy_canvas_stroke_commit_request(...)`, leaving the callsite with
  only concrete faces, sequence, and callbacks.
- 2026-06-13: `DEBT-0036` was narrowed again. `Canvas::stroke_commit()`
  now routes the retained sequence invocation through a local helper,
  leaving the callsite with only the built commit state.
- 2026-06-13: `DEBT-0036` was narrowed again. `NodeStrokePreview::draw_stroke_immediate()`
  now routes final composite execution and preview copy-back through a retained
  local wrapper, leaving the call site with only sequence wiring.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw()` now
  routes dual-brush tip dispatch through retained helper overloads, so the
  face-index callback wiring is no longer built inline in the dual-pass
  branch.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw()` now
  routes pad destination texture dispatch through retained helper overloads,
  so the face-index callback wiring is no longer built inline in the pad
  branch.
- 2026-06-13: `LATER-003` was narrowed again. `Canvas::stroke_draw()` now
  routes its main live-pass bind, execute, and unbind sequence through
  `execute_legacy_canvas_stroke_main_pass(...)`; the retained adapter still
  owns the concrete sampler and texture callbacks.
- 2026-06-13: RND-005 was completed. `desktop-gpu` now has a second
  deterministic OpenGL readback fixture and the CTest registration uses the
  configured target path.
- 2026-06-13: RND-006 was completed. `desktop-gpu` now has a compositor-named
  deterministic OpenGL readback fixture in `pp_renderer_gl_gpu_readback_tests`.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::draw_stroke_immediate()`
  now routes final-composite setup and preview copy-back through retained
  helpers in `legacy_node_stroke_preview_execution_services.h`; the retained
  path still owns the concrete texture objects and pass-order wiring.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::draw_stroke_immediate()`
  now routes main-pass texture binding through
  `bind_legacy_node_stroke_preview_main_pass_textures(...)`; the retained path
  still owns the concrete texture objects and mixer binding callbacks.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::draw_stroke_immediate()`
  no longer duplicates main-pass setup in the outer `prepare_main_pass` block;
  the retained main-live-pass helper now owns blend-uniform and texture binding
  setup, while the sequence wrapper keeps only structural pass ordering.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::draw_stroke_immediate()`
  now routes the main live-pass setup, frame execution, and preview copy-back
  through `execute_legacy_node_stroke_preview_main_live_pass(...)`; the
  retained path still owns the concrete frame mutation, sample shader setup,
  and final mixer unbind.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::draw_stroke_immediate()`
  now routes the main live-pass request assembly through
  `make_stroke_draw_immediate_main_live_pass_request(...)`; the retained path
  still owns the concrete frame mutation, sample shader setup, and final mixer
  unbind.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::draw_stroke_immediate()`
  now routes the dual-pass live body through
  `execute_stroke_draw_immediate_dual_pass(...)`; the retained path still owns
  the concrete frame mutation, shader setup, and preview copy behavior.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::draw_stroke_immediate()`
  now routes the pass-sequence request assembly through
  `make_stroke_draw_immediate_pass_sequence_request(...)`; the retained path
  still owns the concrete preview copy behavior and final-state restoration.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` now routes
  the remaining per-layer branch orchestration through
  `execute_legacy_canvas_draw_merge_layer_composite(...)`; the retained path
  still owns the concrete temporary-stroke, layer-texture, and layer-blend
  callback bodies.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_commit()` now
  routes the remaining service wiring through `make_canvas_stroke_commit_request(...)`;
  the retained path still owns the concrete history mutation and layer dirty
  box updates.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_commit()` now
  routes action bookkeeping and dirty-box capture/mutation through helper
  functions; the retained callback builder now only forwards concrete canvas
  state.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_commit()` now
  builds its retained callback table through
  `make_legacy_canvas_stroke_commit_callbacks(...)`; the legacy executor still
  owns ordering, history capture, and dilate sequencing, and the concrete
  Canvas state wiring remains open in the legacy path.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` now routes
  per-plane merge-target clear, blend-state gating, and optional checkerboard
  prepass through `execute_legacy_canvas_draw_merge_plane_setup(...)`;
  per-plane iteration, per-layer branch selection, and concrete framebuffer
  ownership remain in the legacy Canvas path.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` now routes
  the standard per-layer texture-alpha pass through
  `execute_legacy_canvas_draw_merge_layer_texture(...)`; per-plane iteration,
  temporary-stroke branch selection, and concrete layer RTT ownership remain in
  the legacy Canvas path.
- 2026-06-14: DEBT-0036 was narrowed again. `NodeStrokePreview::stroke_draw_mix()`
  now routes the mix-pass execution request through
  `make_stroke_draw_mix_execution_request(...)`; the retained path still owns
  the concrete framebuffer, viewport, and draw ordering.
- 2026-06-14: DEBT-0036 was narrowed again. `NodeStrokePreview::make_stroke_draw_immediate_main_live_pass_request()`
  now routes sample shader setup through
  `execute_stroke_draw_immediate_main_live_sample_pass(...)`; the retained path
  still owns the concrete preview frame mutation and copy behavior.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` temporary
  composite setup now routes through `execute_canvas_draw_merge_temporary_composite(...)`;
  setup, sampler, texture, draw, and unbind callbacks still remain retained in
  the legacy Canvas path.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` temporary
  erase and paint branch wiring now routes through
  `execute_legacy_canvas_draw_merge_temporary_composite(...)`; the retained
  path still owns the concrete setup, sampler, texture, draw, and unbind
  callbacks.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` now routes
  the remaining temporary erase and paint callback bundle through
  `execute_legacy_canvas_draw_merge_temporary_composite(...)`; branch
  selection remains in `Canvas`.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` temporary
  erase branch execution now routes through
  `make_legacy_canvas_draw_merge_temporary_erase_composite(...)`; the live
  path still owns the concrete layer RTT, mixer RTT, sampler, and plane
  callbacks.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` temporary
  paint branch execution now routes through
  `make_legacy_canvas_draw_merge_temporary_paint_composite(...)`; the live
  path still owns the concrete layer RTT, mixer RTT, sampler, and plane
  callbacks.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` now routes
  the layer-composite shell through a local wrapper around
  `execute_legacy_canvas_draw_merge_layer_composite(...)`; the final branch
  selection remains in `Canvas`.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` now routes
  the layer texture, layer blend, and final-plane composite callbacks through
  local wrappers around the retained helpers; only branch selection glue
  remains in the legacy Canvas path.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` remaining
  branch orchestration now routes through `draw_merge_branch_orchestration(...)`;
  the retained path still owns the concrete per-branch framebuffer, sampler,
  and texture wiring.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` final-plane
  composite now routes through `draw_merge_final_plane_composite(...)`; the
  retained path still owns the concrete merged-texture copy, checkerboard
  redraw, and final blend/draw wiring.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::draw_stroke_immediate()`
  now routes retained preview feedback/material/composite planning plus stroke
  shader uniform assembly through
  `plan_legacy_node_stroke_preview_pass_orchestration(...)`; focused compositor
  coverage now locks the retained destination-feedback fallback, composite-slot
  intent, and pattern/dual shader-uniform handoff while brush mutation, retained
  `Stroke` population, and live GL callback execution remain in the preview
  node.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::draw_stroke_immediate()`
  now routes retained preview stroke max-size fallback, dual-preview max-size
  derivation, pattern-scale flips, and Bezier preview-point generation through
  `plan_legacy_node_stroke_preview_stroke_setup(...)`; compositor coverage now
  also locks the retained stroke-setup curve/pressure intent and
  pass-sequence validation short-circuit behavior. Brush object mutation,
  camera wiring, retained `Stroke` population, and all live GL execution
  remain in the legacy preview node.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` per-layer
  blend composite now routes merge-RTT unbind, shader setup, optional
  destination-copy feedback, draw, and cleanup ordering through
  `execute_legacy_canvas_draw_merge_layer_blend(...)`; layer iteration,
  temporary-stroke branch selection, framebuffer copies, sampler/texture
  object callbacks, and final merged-plane ownership remain retained in
  `Canvas`.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` erase live
  temporary-stroke composite now routes retained setup, sampler bind, texture
  bind, draw, and texture unbind ordering through
  `execute_legacy_canvas_stroke_temporary_composite(...)`; broader final
  composite ownership remains in the legacy Canvas path.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::stroke_draw_mix()`
  now routes retained mix-pass shader setup plus framebuffer/state/input/draw
  ordering through `execute_legacy_node_stroke_preview_mix_pass(...)`; the
  preview node keeps only the concrete GL save/restore, texture-object bind,
  and plane-draw callbacks.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` live-pass
  sampler bind/unbind plus semantic texture-input dispatch now route through
  retained stroke execution helpers; concrete GL object mapping, framebuffer
  ownership, shader timing, and final draw execution remain retained in
  `Canvas`.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` live-pass
  face callback orchestration now routes through
  `execute_legacy_canvas_stroke_live_pass_with_face_framebuffers(...)`; the
  retained path still owns the concrete framebuffer array and per-face GL
  callbacks.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` dual-pass
  face execution now routes through
  `execute_legacy_canvas_stroke_dual_pass_frame_callbacks(...)`; the retained
  path still owns the concrete dual-brush shader, sampler, and framebuffer
  wiring.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` dual-pass
  frame orchestration now routes through a retained helper; the retained path
  still owns the concrete shader setup, brush-tip dispatch, and frame callback
  execution.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` dual-pass
  frame callback body now routes through `stroke_draw_dual_pass_frame_pass(...)`;
  the retained path still owns the concrete dual-pass request shell and frame
  execution wiring.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` dual-pass
  request assembly now routes through `make_stroke_draw_dual_pass_request(...)`;
  the retained path still owns the concrete request execution shell and frame
  callbacks.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` dual-pass
  request assembly now routes through a retained helper object; the retained
  path still owns the concrete request shell and frame callbacks.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` pad-face
  orchestration now routes through `stroke_draw_pad_face_orchestration(...)`;
  the retained path still owns the concrete pad shader setup, destination
  dispatch, and callback wiring.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` pad-face
  callback body now routes through `stroke_draw_pad_face_callback_body(...)`;
  the retained path still owns the concrete pad shader setup, face selection,
  and copy wiring.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` temporary
  paint branch now routes through `draw_merge_temporary_paint_branch(...)`;
  the retained path still owns the concrete temporary-paint setup, texture
  binding, and draw ordering.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` remaining
  branch orchestration now routes through `draw_merge_branch_orchestration(...)`;
  the retained path still owns the concrete per-branch framebuffer, sampler,
  and texture wiring.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` main-pass
  live face orchestration now routes through
  `execute_legacy_canvas_stroke_live_pass_with_face_framebuffers(...)`; the
  retained path still owns the concrete shader, sampler, and framebuffer
  wiring.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` main-pass
  face loop orchestration now routes through
  `execute_legacy_canvas_stroke_main_pass_frame_callbacks(...)`; the retained
  path still owns the concrete shader, sampler, and framebuffer wiring.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` pad-face
  orchestration now routes through
  `execute_legacy_canvas_stroke_pad_face_callbacks(...)`; the retained path
  still owns the concrete brush shape, destination dispatch, and framebuffer
  wiring.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` pad-face
  callback body now routes through `Canvas::stroke_draw_pad_pass(...)`; the
  retained path still owns the concrete pad shader setup and face callback
  execution.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw_samples()`
  request assembly now routes through `make_stroke_draw_samples_request(...)`;
  the retained path still owns the concrete destination-copy dispatch and
  brush upload/draw wiring.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw_samples()`
  destination-texture dispatch now routes through
  `make_stroke_draw_samples_destination_texture_dispatch(...)`; the retained
  path still owns the concrete destination-copy callback wiring and request
  execution.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw_samples()`
  now routes polygon triangulation, sample-point assembly, and retained
  destination-copy / upload / draw helper handoff through
  `execute_legacy_canvas_stroke_sample_polygon(...)`; direct GL callback
  wiring and remaining live draw ownership remain retained in `Canvas`.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw_mix()` now
  routes visible-plane filtering, retained sampler/texture-slot binding, and
  final plane draw ordering through `execute_legacy_canvas_stroke_mix_pass(...)`;
  mixer framebuffer/state setup and per-plane shader material/MVP preparation
  remain retained in `Canvas`.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw_samples()`
  now routes face-indexed destination bind/copy/unbind and brush upload/draw
  through `execute_legacy_canvas_stroke_face_sample_polygon(...)`; the
  retained sample executor owns the face-aware dispatch contract while
  `Canvas` keeps only concrete GL object callbacks.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge()` non-erase
  live temporary-stroke composite now routes setup, sampler bind, texture
  bind, draw, and texture unbind ordering through
  `execute_legacy_canvas_stroke_temporary_composite(...)`; erase-path and
  broader final composite ownership remain retained in `Canvas`.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::draw_stroke_immediate()`
  now routes dual-pass/background/main-pass/final-composite/copy-back ordering
  through `execute_legacy_node_stroke_preview_pass_sequence(...)`; the
  remaining local preview ownership is concentrated around `stroke_draw_mix()`
  setup/execution.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::stroke_draw_mix()`
  now routes mixer framebuffer bind/unbind, viewport/scissor/blend state,
  texture-slot binding, and final plane draw through one local helper;
  material planning and shader uniform setup remain retained in the preview
  node.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::stroke_draw_samples`
  now routes destination bind/unbind, framebuffer copy callback wrapping,
  sample-point assembly, and brush-vertex upload/draw through one local helper;
  mixer-pass state execution and higher-level pass orchestration remain retained
  in the preview node.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` main, pad,
  and dual live-pass texture-input binding/unbinding intent now routes through
  shared retained stroke execution helpers; sampler binding, concrete GL object
  mapping, framebuffer ownership, and final draw execution remain retained in
  `Canvas`.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview` live-pass
  sampler binding, dual/main pass texture binding, checkerboard/background
  capture wrapping, and final preview copy-back now route through shared local
  helpers; mixer state execution and per-sample GL ordering remain retained in
  the preview node.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview` dual-pass and
  main-pass frame-loop execution plus full-frame copy-back now route through
  shared local helpers; checkerboard/background capture ordering, texture-unit
  binding, mixer ownership, and final composite semantics remain retained in
  the preview node.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` main and
  dual live-pass per-face framebuffer begin/end execution plus pad-face array
  assembly now route through `legacy_canvas_stroke_execution_services.h`;
  shader activation timing, texture/sampler binding, framebuffer ownership, pad
  execution, and final OpenGL draw remain retained in `Canvas`.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` main and
  dual live-pass dirty semantics now route through
  `execute_legacy_canvas_stroke_live_pass_with_dirty_tracking(...)` in
  `legacy_canvas_stroke_execution_services.h`; shader timing, sampler/texture
  binding, framebuffer ownership, pad execution, and final OpenGL draw remain
  retained in `Canvas`.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview` final
  composite sampler/input binding and slot intent now route through one local
  preview helper; mixer execution, per-sample stroke callbacks, framebuffer
  copies, and final OpenGL draw ownership remain retained in the preview node.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` pad-pass
  destination bind/copy/unbind ordering now routes through
  `legacy_canvas_stroke_execution_services.h`; shader setup, pad color
  selection, framebuffer ownership, and final OpenGL draw remain retained in
  `Canvas`.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` current and
  dual live sample frame/face traversal plus dirty tracking now route through
  `legacy_canvas_stroke_execution_services.h`; shader timing, sampler/texture
  binding, framebuffer ownership, pad execution, and final OpenGL draw remain
  retained in `Canvas`.
- 2026-06-13: DEBT-0036 preview-adapter hardening grew again.
  `pp_paint_renderer_compositor_tests` now covers
  `legacy_node_stroke_preview_execution_services.h` framebuffer-feedback
  fallback clamping plus retained preview composite slot intent, while live
  preview sample execution, mixer passes, texture binding, and final draw
  ordering remain retained.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::stroke_draw_compute`
  now routes preview quad frame planning through
  `legacy_canvas_stroke_execution_services.h`. Preview stroke sampling, mixer
  execution, texture binding, and final OpenGL draw ordering remain retained.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` current and
  dual stroke per-face framebuffer/sample callback ordering now routes through
  the retained stroke execution helper; framebuffer ownership, shader uniform
  timing, sampler/texture binding, and live draw execution remain retained.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` current and
  dual stroke dirty-box mutation now routes through the retained stroke
  execution helper; framebuffer binding, shader uniform timing, sampler/texture
  binding, and live draw execution remain retained.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_commit` retained
  commit input texture/sampler binding, erase/composite draw dispatch,
  committed-copy-to-dilate-source, and dilate draw now route through
  `legacy_canvas_stroke_commit_services.h`; Canvas still owns history
  readback, `ActionStroke` population, layer dirty-box mutation, and retained
  RTT/framebuffer ownership.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` pad-pass
  dirty-face iteration, pad-region planning, and NDC quad assembly now route
  through a retained stroke execution helper callback boundary; Canvas still
  owns framebuffer copies, brush-shape uploads, and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` current and
  dual stroke frame-face traversal now routes through the retained stroke
  execution helper; framebuffer binding, shader uniform timing, dirty-box
  mutation, sampler/texture binding, and live draw execution remain retained.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw_compute`
  frame planning now routes brush-quad construction, mixer feedback bounds,
  2D/3D projection selection intent, and frame assembly through the retained
  stroke execution helper; legacy projection geometry, stroke samples, and live
  draw execution remain retained.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` pad-region
  planning now routes through the retained stroke execution helper wrapping
  `pp_paint_renderer`; pad color selection, dirty-face iteration,
  framebuffer copies, quad upload, and draw execution remain retained.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw` face
  dirty-box planning now routes through a retained stroke execution helper
  wrapping `pp_paint_renderer`, retained stroke commit step dispatch clamps
  malformed step counts to the fixed plan array, and compositor coverage now
  exercises malformed retained commit plans plus all-input stroke-preview
  composite planning. Live stroke rasterization, callback execution, texture
  binding, and history mutation remain retained.
- 2026-06-13: DEBT-0036 was narrowed again. Remaining live shader setup
  outside retained helper headers now routes through retained helper surfaces
  for canvas modes, equirect layer export, `NodeCanvas` debug dirty bounds,
  atlas image drawing, and text drawing; those paths still own geometry,
  framebuffer flow, texture/sampler binding, blend/depth state, readback, and
  draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. Remaining simple color, hue,
  color-quad, grid heightmap, and pen/line preview shader setup in UI nodes
  and canvas modes now routes through retained helper surfaces; those paths
  still own geometry, texture/sampler binding, blend/depth state, readback,
  and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. Scrollbar, text-input, and VR
  frame/cursor/controller shader setup now routes through retained UI/preview
  helpers; those paths still own geometry, blend/depth state, sampler/texture
  binding, and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. Depth export `TextureColorize`
  shader setup now routes through `legacy_canvas_draw_merge_services.h`; the
  export path still owns sampler/texture binding, layer color selection, and
  draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeCanvas` smoothing-mask
  `TextureMask` setup now routes through `legacy_canvas_draw_merge_services.h`;
  the node still owns smoothing-mask texture binding, per-face MVP updates,
  blend state, and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. Slider `Color` and `ColorHue`
  shader setup now routes through `legacy_ui_overlay_services.h`; the slider
  nodes still own geometry, hue direction, border drawing, and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. Retained `TextureAlpha` shader
  setup now routes through `legacy_canvas_draw_merge_services.h` for Canvas
  draw-merge layer redraws, depth export merged-layer rendering, NodeCanvas
  cached/live layer redraws, and desktop VR layer redraws; those paths still
  own sampler/texture binding, render-task ordering, per-frame alpha updates,
  and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeCanvas` density-resolve and
  desktop VR UI `Texture` shader setup now route through
  `legacy_canvas_draw_merge_services.h`; those paths still own render target,
  sampler/texture binding, viewport/state restoration, and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. `CanvasModeTransform::on_Draw`
  and `CanvasModeFloodFill::on_Draw` retained `Texture` shader setup now route
  through `legacy_canvas_draw_merge_services.h`; the mode paths still own
  active texture selection, sampler/texture binding, and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::draw`
  retained preview `Texture` shader setup now routes through
  `legacy_canvas_draw_merge_services.h`; the node still owns preview texture
  binding, sampler binding, and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeViewport` retained preview
  draw and `NodePanelGrid` sun-overlay `Texture` shader setup now route
  through `legacy_canvas_draw_merge_services.h`; the node paths still own
  texture/sampler binding, viewport/state setup, and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::import_layer` retained
  object-draw `Texture` shader setup now routes through
  `legacy_canvas_draw_merge_services.h`; import_layer still owns texture upload,
  sampler/texture binding, draw callbacks, and object geometry.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeImage` and
  `NodeImageTexture` retained plain image `Texture` shader setup now routes
  through `legacy_canvas_draw_merge_services.h`; the nodes still own
  texture/sampler binding, atlas setup, blend state, and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::thumbnail_generate`
  checkerboard and final `Texture` shader setup now route through
  `legacy_canvas_draw_merge_services.h`; thumbnail generation still owns
  destination copies, sampler/texture binding, readback, and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_objects` retained
  object composite `Texture` shader setup now routes through
  `legacy_canvas_draw_merge_services.h`; draw_objects still owns temporary
  face readback, sampler/texture binding, draw execution, and dirty-box updates.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::thumbnail_generate`
  thumbnail layer `TextureBlend` shader setup now routes through
  `legacy_canvas_draw_merge_services.h`; thumbnail generation still owns
  destination feedback copies, sampler/texture binding, grid redraw, readback,
  and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. Desktop VR checkerboard background
  shader setup now routes through `legacy_canvas_draw_merge_services.h`; VR
  still owns render state transitions, per-plane transforms, and draw
  execution.
- 2026-06-13: DEBT-0036 was narrowed again. `CanvasModeMaskCut::apply`
  retained `CompDraw` shader setup now routes through
  `legacy_canvas_stroke_composite_services.h`; mask-cut still owns render-task
  ordering, framebuffer copy bounds, sampler/texture binding, and draw
  execution.
- 2026-06-13: DEBT-0036 was narrowed again. Desktop VR retained temporary erase
  and draw shader setup now route through the shared stroke erase/composite
  helpers; VR still owns sampler/texture binding, per-eye/view transforms,
  temporary stroke texture selection, and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_commit` and
  `Canvas::draw_merge` retained `CompErase` shader setup now route through
  `legacy_canvas_stroke_erase_services.h`; Canvas still owns texture binding,
  mask RTT binding, dirty/layer mutation, framebuffer feedback, and draw
  execution.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeCanvas` live temporary-erase
  `CompErase` shader setup now routes through
  `legacy_canvas_stroke_erase_services.h`; the retained node path still owns
  per-onion-frame alpha updates, texture binding, temporary erase texture
  selection, and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeCanvas` live temporary-stroke
  `CompDraw` shader setup now routes through
  `legacy_canvas_stroke_composite_services.h`; the retained node path still
  owns per-onion-frame alpha updates, texture binding, temporary stroke texture
  selection, and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview` preview
  background checkerboard shader setup now routes through
  `legacy_canvas_draw_merge_services.h`, preserving the existing colorize
  condition and preview MVP. The retained preview path still owns background
  capture ordering, texture copies, stroke setup, and draw execution.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge` non-stroke
  `TextureBlend` shader setup now routes through
  `legacy_canvas_draw_merge_services.h`, preserving the optional `TexBG`
  uniform only when copy-based blend destination feedback is active. Canvas
  still owns draw-merge layer iteration, framebuffer copies, sampler/texture
  binding, and draw ordering.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeCanvas` non-stroke
  `TextureBlend` shader setup now routes through
  `legacy_canvas_draw_merge_services.h`, including the optional destination
  texture uniform only when blend destination copies are used. NodeCanvas still
  owns panorama layer traversal, onion-frame drawing, blend destination copies,
  sampler/texture binding, framebuffer lifetime, and draw ordering.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeCanvas` now reuses
  `legacy_canvas_draw_merge_services.h` for retained checkerboard grid shader
  setup and the final cached-layer texture redraw setup. NodeCanvas still owns
  panorama layer traversal, onion-frame drawing, blend destination copies,
  sampler/texture binding, and draw ordering.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw_mix` now
  reuses `legacy_canvas_stroke_composite_services.h` for mixer-pass retained
  `kShader::CompDraw` binding and blend uniform writes, with the helper now
  preserving caller-specific texture slot uniforms. Mixer framebuffer,
  viewport/scissor/capability state, sampler binding, texture binding, and draw
  execution remain retained Canvas code.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw_mix()` now
  routes retained mix-shell execution through a local wrapper around
  `execute_legacy_canvas_stroke_mix_pass_shell(...)`; the live path still owns
  the concrete mixer framebuffer setup and GL capability toggles.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw_mix()` now
  has regression coverage for retained mixer-state callback ordering through
  `execute_legacy_canvas_stroke_mix_pass(...)`; the live path still owns the
  concrete mixer framebuffer setup and GL capability toggles.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` main-pass
  texture dispatch now has regression coverage through
  `make_legacy_canvas_stroke_main_pass_texture_dispatch(...)`; the live path
  still owns the concrete texture objects and sampler state.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` live-pass
  sampler dispatch now has regression coverage through
  `make_legacy_canvas_stroke_live_pass_sampler_dispatch(...)`; the live path
  still owns the concrete sampler objects and binding state.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` dual-pass
  brush-tip dispatch now uses a retained helper object, with regression
  coverage proving the helper order; the live path still owns the concrete
  brush-tip texture object and dual-pass branch wiring.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` pad copy
  behavior now uses a retained helper for copy-region wiring; the pad branch
  still owns the concrete framebuffer and texture-object callbacks.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` dual-pass
  shader setup now uses a retained wrapper helper; the dual-pass branch still
  owns the concrete shader selection and framebuffer wiring.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_draw()` pad
  destination dispatch now reuses a retained helper object, with regression
  coverage proving the helper order; the pad branch still owns the concrete
  texture-object wiring and copy timing.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge` checkerboard
  background shader setup and final merged-texture redraw setup now route
  through `legacy_canvas_draw_merge_services.h`. The retained Canvas path still
  owns draw-merge layer iteration, blend-gate branching, framebuffer copies,
  sampler/texture binding, and draw ordering.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::draw_merge` end-of-plane
  merged-texture copy, optional checkerboard redraw, and final merged-texture
  composite ordering now route through
  `execute_legacy_canvas_draw_merge_final_plane_composite(...)`; the retained
  Canvas path still owns per-plane iteration plus the concrete framebuffer,
  sampler, texture, and draw callbacks.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::layer_merge` now reuses
  `legacy_canvas_stroke_composite_services.h` for retained layer-merge
  `kShader::CompDraw` binding and source/destination blend uniform writes.
  Render-task ordering, dirty face/box mutation, framebuffer copies, sampler
  binding, texture binding, and draw execution remain in the retained Canvas
  layer-merge path.
- 2026-06-13: DEBT-0036 was narrowed again. `Canvas::stroke_commit` now routes
  its retained per-face commit order through
  `execute_legacy_canvas_stroke_commit_sequence`, consuming the tested
  `CanvasStrokeCommitSequencePlan` while keeping history `ActionStroke`
  mutation, layer dirty state, RTT/framebuffer binding, shader execution, and
  sampler/texture binding inside Canvas callbacks. The adapter remains retained
  until stroke commit execution is owned by the renderer backend implementation.
- 2026-06-13: DEBT-0036 was narrowed again. `pp_paint_renderer` now owns a
  tested `CanvasStrokeCommitSequencePlan` for `Canvas::stroke_commit`
  readback, dirty-state, scratch-copy, erase/composite draw, committed-copy,
  dilate order, and commit texture slot roles. A retained
  `legacy_canvas_stroke_commit_services.h` adapter skeleton consumes the
  semantic plan through callbacks, but the live Canvas commit body still owns
  history/layer mutation, RTT/framebuffer binding, sampler binding, and final
  OpenGL execution until the adapter is wired.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::stroke_draw_mix`
  now reuses `legacy_canvas_stroke_composite_services.h` for mixer-pass
  `kShader::CompDraw` binding and composite/pattern/dual uniform writes.
  Mixer framebuffer, scissor/capability state, texture binding, and draw/copy
  ordering remain retained legacy execution.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview::stroke_draw_mix`
  now routes retained mix-pass material planning, pattern scale/offset
  derivation, and composite/pattern/dual uniform payload assembly through
  `plan_legacy_node_stroke_preview_mix_pass(...)`, with semantic adapter
  coverage in `pp_paint_renderer_compositor_tests`. Mixer framebuffer
  ownership, retained shader execution, texture binding, and final draw/copy
  ordering remain local legacy preview execution.
- 2026-06-13: DEBT-0036 was narrowed again. `NodeStrokePreview` final preview
  background capture, composite input binding/draw, and preview texture copy now
  route through `legacy_canvas_stroke_preview_services.h`, with semantic preview
  composite ordering and texture-slot intent covered by `pp_paint_renderer`
  tests. Static preview RTT/texture ownership, checkerboard shader setup,
  framebuffer copies, and retained GL callbacks remain in legacy preview code.
- 2026-06-13: DEBT-0036 was narrowed again. Stroke sample copy bounds, live
  face dirty-box accumulation, and preview padding region math now live as
  tested `pp_paint_renderer` planners and are consumed by retained Canvas and
  stroke sample execution adapters. Canvas/preview still own GL ordering,
  RTT/texture binding, history mutation, and final dirty state storage.
- 2026-06-12: DEBT-0036 was narrowed again. `Canvas::stroke_commit` now reuses
  `legacy_canvas_stroke_composite_services.h` for commit-time final stroke
  `kShader::CompDraw` binding and composite/pattern/dual uniform writes.
  Texture/framebuffer binding, copy ordering, layer dirty mutation, and
  non-stroke composite callers remain retained legacy execution.
- 2026-06-12: DEBT-0036 was narrowed again. Canvas stroke padding and commit
  dilate shader setup now route retained `kShader::StrokePad` and
  `kShader::StrokeDilate` binding/uniform writes through
  `legacy_canvas_stroke_edge_services.h`. Quad expansion, dirty-box policy,
  texture/framebuffer binding and copies, and draw ordering remain retained
  legacy execution.
- 2026-06-12: DEBT-0036 was narrowed again. Canvas live draw-merge and
  `NodeStrokePreview` final stroke composite `kShader::CompDraw` setup now
  share `legacy_canvas_stroke_composite_services.h` for retained shader binding
  and composite/pattern/dual uniform writes. Texture/RTT binding, samplers,
  draw calls, commit-time compositing, and non-stroke `CompDraw` callers remain
  retained legacy execution.
- 2026-06-12: DEBT-0036 was narrowed again. Canvas and `NodeStrokePreview`
  live stroke passes now route retained `kShader::Stroke` binding plus setup,
  blend, pattern, and per-sample uniform writes through
  `legacy_canvas_stroke_shader_services.h`. RTT/texture ownership,
  dirty-box policy, composite/pad/checkerboard shaders, and retained callback
  execution remain in the legacy callers until the stroke backend is owned by
  renderer services.
- 2026-06-12: DEBT-0036 was narrowed again. Canvas and `NodeStrokePreview`
  stroke sample execution now delegate optional destination copy, brush vertex
  upload, brush-shape draw, and destination unbind through
  `execute_legacy_canvas_stroke_sample`. Shader setup, RTT/texture ownership,
  dirty-box policy, and retained callback execution remain in the legacy callers
  until a renderer-owned stroke backend replaces the adapter.
- 2026-06-12: DEBT-0036 was narrowed again. `NodeStrokePreview` now consumes
  the same `CanvasStrokeMaterialPlan` boundary for preview dual-brush,
  each-sample pattern, and composite material decisions. Preview GL draw calls,
  texture binding details, and pattern scale/offset uniforms remain retained.
- 2026-06-12: DEBT-0036 was narrowed again. Live `Canvas::stroke_draw`,
  stroke commit, and draw-merge paths now consume `CanvasStrokeMaterialPlan`
  through `plan_legacy_canvas_stroke_material` for destination feedback,
  each-sample pattern, dual-brush, and final composite material decisions.
  Retained OpenGL draw calls and `NodeStrokePreview` still duplicate execution
  and preview material wiring.
- 2026-06-12: DEBT-0036 was narrowed again. Live `Canvas::stroke_draw`
  destination feedback now consumes a named `CanvasStrokeRasterizationPlan`
  through `plan_legacy_canvas_stroke_rasterization`, and `pp_paint_renderer`
  owns pure material/pass planning for stroke pattern, mixer, dual-brush, and
  final composite texture/uniform intent. Retained OpenGL stroke execution
  still lives in `Canvas`, but feedback/material decisions now have tested
  renderer-facing plan boundaries.
- 2026-06-12: DEBT-0036 was narrowed again. The opt-in `desktop-gpu`
  preset now owns a real OpenGL readback golden gate through
  `pp_renderer_gl_gpu_readback_tests`, validating a deterministic 1x1 clear
  and `glReadPixels` result against exact RGBA bytes. The first context helper
  is Windows/WGL-only and skips clearly on platforms without a helper; macOS
  and Linux GPU context helpers plus broader golden coverage remain open.
- 2026-06-12: DEBT-0060 was closed. Retained Android standard/Quest/Focus
  package CMake files no longer generate or prepend a patched `nanort.h`
  overlay. Android package configure now applies the tracked nanort source
  compatibility patch, Android/GLES runtime dispatch avoids desktop debug
  callback symbols, and the retained package source lists include the extracted
  overlay/preference adapters needed by recent app-service boundaries.
- 2026-06-12: DEBT-0058 and DEBT-0063 were narrowed. App-level
  progress/message/input dialogs now route through a pure
  `pp::app::AppDialogFactory`; retained `NodeProgressBar`, `NodeMessageBox`,
  and `NodeInputBox` construction and root attachment are centralized behind
  the legacy dialog/overlay factory layer.
- 2026-06-12: DEBT-0017, DEBT-0051, and DEBT-0055 were narrowed. Apple
  document browse roots, file/image/save/directory picker dispatch, macOS
  empty-selection filtering, working-directory picker policy, and display-path
  formatting now live in `src/platform_apple/apple_platform_services.*`; the
  retained legacy platform adapter delegates Apple document-platform calls to
  that boundary while `App` still stores Apple platform handles directly.
- 2026-06-12: DEBT-0010, DEBT-0036, and DEBT-0043 were narrowed. Payload-complete
  depth export now generates deterministic image/depth PNG payloads through
  pure `pp_paint_renderer` and writes them through the app-core two-payload
  writer before retained fallback. Retained OpenGL depth rendering/readback is
  now fallback-only for unsupported targets, incomplete readback, or writer
  failure; the pure path currently uses the extracted fixed perspective export
  view rather than captured live legacy camera state.
- 2026-06-12: DEBT-0062 was closed. The generated VS 2026 fmt overlay was
  removed from root CMake, reused build trees delete the stale
  `compat/fmt-vs2026` directory, and VS 2026 retained fmt sources now use a
  generated forced-include compatibility header that includes the STL
  configuration once and undefines `_SECURE_SCL` before fmt selects its removed
  checked-array-iterator branch.
- 2026-06-12: DEBT-0039, DEBT-0040, and DEBT-0042 were narrowed. Document-open,
  close, save, save-before-workflow, Save As, Save Version, and new-document
  history effects now surface as explicit `pp_app_core` history outputs; the
  live document-open/session bridges no longer include `legacy_history_services.h`.
  Retained project open/save, prompt callbacks, metadata mutation, and concrete
  history execution remain in legacy adapters.
- 2026-06-12: DEBT-0050, DEBT-0053, and DEBT-0057 were narrowed. WebGL
  exported-image publishing, persistent-storage flushing, prepared-file
  handoff, and default canvas resolution now dispatch through injectable
  `pp::platform::WebPlatformServices` in `pp_platform_api`; the retained Web
  fallback implementation still lives in `src/platform_legacy/legacy_platform_services.cpp`
  until a dedicated Web platform shell injects the service directly.
- 2026-06-12: DEBT-0021 was narrowed. Layer rename execution now exposes
  app-core history intent explicitly, records undo before applying the new
  layer name, and has focused call-order coverage. Layer operation and merge
  plans also expose app-core history-intent helpers, while retained
  `NodePanelLayer`/`Canvas` execution still owns the concrete `ActionManager`
  entries for add/remove/property-change/clear paths.
- 2026-06-12: DEBT-0020 was narrowed. The shared document resize/canvas-clear
  live bridge no longer includes `legacy_history_services.h`; resize history
  clearing is now an explicit `DocumentResizeServices::clear_history()` app-core
  execution output implemented directly by the retained `ActionManager`, while
  canvas clear continues to record `ActionLayerClear` undo and mark the canvas
  unsaved inside the retained canvas operation. Legacy `Canvas::resize`,
  `Canvas::clear`, title updates, and direct `ActionManager` ownership remain
  open.
- 2026-06-04: DEBT-0009 was narrowed. `platform-build.ps1` and
  `platform-build.sh` now include the current headless component/test matrix,
  including brush-package coverage and the app-core startup/frame/shutdown/file/document/
  brush/canvas/history/grid/toolbar/tools/about/preferences/status automation
  tests, and `panopainter_platform_build_target_matrix_self_test` now verifies
  the wrapper defaults against the current CMake test executables. On
  2026-06-05 the default platform-build preset set was widened to Android
  standard arm64/x64, Quest arm64, and Focus/Wave arm64, with the shell wrapper
  gaining multi-preset output parity with PowerShell, so Android root CMake
  validation no longer silently skips newly extracted feature slices or named
  Android variants. Package targets remain open under DEBT-0009 and DEBT-0011.
- 2026-06-05: DEBT-0011 was narrowed. `package-smoke.ps1` and
  `package-smoke.sh` now have readiness-only modes that report the same
  Windows AppX, Android standard/Quest/Focus APK, Apple bundle, Linux app, and
  WebGL blocker matrix without requiring an app build first, and
  `panopainter_package_smoke_readiness_self_test` guards package-kind parity
  across both wrappers. Package target migration remains open.
- 2026-06-05: DEBT-0004 was narrowed for Apple compile coverage. The Mac mini
  build host `panopainter-mac` now pulls from Gitea over SSH, uses Homebrew
  CMake/Ninja/Git plus full Xcode through `DEVELOPER_DIR`, and
  `scripts/automation/apple-remote-build.ps1` drives the `macos`,
  `ios-simulator`, and `ios-device` root CMake headless component matrix.
  Apple app bundle/signing/package migration remains open under DEBT-0011 and
  DEBT-0059.
- 2026-06-05: DEBT-0022 was narrowed. `pp_app_core` now owns tested
  onion-skin frame range and alpha falloff planning, and live `NodeCanvas`
  panorama drawing consumes that helper instead of open-coding frame clamping
  and opacity falloff in the render loop. Later on 2026-06-05, animation
  timeline mouse scrubbing also moved to tested `pp_app_core` planning with
  `pano_cli plan-animation-timeline-scrub` coverage, so `NodeAnimationTimeline`
  no longer owns cursor-to-frame clamp policy. Animation panel layer/frame view
  projection now also uses a tested `pp_app_core` view model exposed by
  `pano_cli plan-animation-panel-view`, including stale-selection behavior.
  Legacy canvas/layer/UI execution remains open under DEBT-0022.
- 2026-06-05: DEBT-0047 was narrowed. PPBR export success-dialog metadata now
  lives in tested `pp_app_core`, is exposed through
  `pano_cli plan-brush-package-export`, and is consumed by
  `src/legacy_brush_package_export_services.*`. Retained `NodeDialogExportPPBR`
  reads, legacy `Image` header ownership, `PPBRInfo` conversion,
  `NodePanelBrushPreset::export_ppbr`, desktop worker threading, dialog
  lifetime, mobile/Web completion, and PPBR serialization remain open.
- 2026-06-05: DEBT-0021 was narrowed again. Layer panel selected-control and
  visibility view projection now goes through tested `pp_app_core` planning,
  `NodePanelLayer::update_attributes()` consumes that view model in the live
  app, and `pano_cli plan-layer-panel-view` exposes the same path for
  automation. Legacy layer mutation, UI node ownership, and undo wiring remain
  open under DEBT-0021.
- 2026-06-05: DEBT-0023 was narrowed again. Stroke-panel control projection now
  goes through tested `pp_app_core` planning, `NodePanelStroke::update_controls()`
  consumes that view model in the live app, and
  `pano_cli plan-brush-stroke-panel-view` exposes the same state path for
  automation. Legacy brush mutation, brush thumbnail ownership, popup behavior,
  and preset child-node mutation remain open under DEBT-0023.
- 2026-06-05: DEBT-0023 was narrowed again. App-level brush refresh projection
  now goes through tested `pp_app_core` planning, `App::brush_update()` consumes
  that view model in the live app, and `pano_cli plan-brush-refresh` exposes the
  same fan-out path for automation. Retained legacy quick/stroke/color widget
  writes plus `Brush`/`Canvas::I` ownership remain open under DEBT-0023.
- 2026-06-05: DEBT-0025 was narrowed. Quick size/flow slider preview planning
  now goes through tested `pp_app_core`, live `NodePanelQuick` slider callbacks
  consume that plan for cursor placement and pen/line tip flags, and
  `pano_cli plan-quick-slider-preview` exposes the path for automation. Legacy
  quick widgets, brush previews, popup state, and direct `CanvasMode*` field
  writes remain open under DEBT-0025.
- 2026-06-05: DEBT-0027 was narrowed. Canvas cursor visibility policy now goes
  through tested `pp_app_core`, live `NodeCanvas::update_cursor()` consumes the
  planner before retained platform cursor dispatch, and
  `pano_cli plan-canvas-cursor` exposes draw/erase versus non-paint mode,
  small-brush, not-painting, modifier, and malformed-brush states for
  automation. Legacy `Canvas`/`CanvasModePen` state reads and app cursor
  execution remain open under DEBT-0027.
- 2026-06-05: DEBT-0027 was narrowed again. The full draw-toolbar binding set
  now goes through tested `pp_app_core` planning, live
  `App::init_toolbar_draw()` consumes the same plan for button handler wiring
  and default draw-mode initialization, and `pano_cli plan-canvas-tool-toolbar`
  exposes the binding ids, actions, button-class expectation, and default
  selection for automation. Retained `NodeButton`/`NodeButtonCustom` lookup,
  legacy `Canvas` mode mutation, picking/touch-lock state, and transform action
  execution remain open under DEBT-0027.
- 2026-06-05: DEBT-0033 was narrowed again. Canvas reset-camera defaults,
  viewport density, and cursor mode now go through tested `pp_app_core` plans
  and shared `src/legacy_canvas_view_services.*` execution. Live Tools
  reset-camera, document open/new-document reset, cloud download reset, and
  options viewport/cursor callbacks consume that bridge, while
  `pano_cli plan-canvas-camera-reset`, `pano_cli plan-canvas-view-density`, and
  `pano_cli plan-canvas-view-cursor-mode` expose the paths for automation.
  This also narrows DEBT-0045 for viewport-density and cursor-mode preference
  execution, though preference persistence remains retained in the legacy
  canvas-view bridge.
- 2026-06-05: DEBT-0046 was narrowed. Main startup shader, asset, layout,
  title, and UI render-target sequencing now goes through tested `pp_app_core`
  resource plans and `src/legacy_app_startup_services.*`; `App::init` keeps
  the retained OpenGL startup task in place, then delegates startup resources
  and runtime side effects through the startup bridge. `pano_cli
  plan-app-startup-resources` exposes the resource path for automation.
- 2026-06-05: DEBT-0040/0041/0042 were narrowed. Close-unsaved,
  save-before-workflow, new-document overwrite, Save As overwrite, and
  save-error prompt metadata now comes from a tested pure document-session
  prompt catalog consumed by `src/legacy_document_session_services.*`, and
  `pano_cli plan-document-session-prompt` exposes the titles, messages,
  captions, and cancel visibility for automation. Retained `NodeMessageBox`
  creation, callback wiring, project-save execution, and canvas/document
  mutation remain open under the same debts.
- 2026-06-05: DEBT-0040/0041/0042 were narrowed again. Close-unsaved,
  save-before-workflow, new-document overwrite, and Save As overwrite prompt
  creation now uses `src/legacy_app_dialog_services.*`; retained
  document-session callbacks, save dialogs, canvas/project mutations, keyboard
  cleanup, and dialog lifetime still remain open under the same debts.
- 2026-06-05: DEBT-0003 was narrowed. Initial surface sizing, redraw/animation
  update gating, layout tick selection, resize render-target recreation,
  canvas-stroke draw eligibility, VR UI pass selection, main UI pass selection,
  UI observer clipping/on-screen transition/scissor projection, and redraw
  reset are now tested `pp_app_core` frame plans consumed by `App::create`,
  `App::tick`, `App::resize`, `App::update`, `App::draw`,
  `App::update_ui_observer`, and `pano_cli plan-app-frame`; retained layout
  traversal, toolbar widget writes, render-target recreation, `Node` parent
  walking, on-screen callback execution, and OpenGL/UI drawing remain in the
  legacy app.
- 2026-06-05: DEBT-0003 was narrowed. Pointer coordinate normalization,
  mouse designer-first routing, gesture midpoint/delta math, touch/key
  main-layout routing, VR spacebar camera-sync intent, UI visibility toggling,
  and stylus touch-lock attachment are now tested `pp_app_core` input plans
  consumed by `App::mouse_*`, `App::gesture_*`, `App::touch_tap`,
  `App::key_*`, `App::toggle_ui`, `App::set_stylus`, and
  `pano_cli plan-app-input`; retained
  `MouseEvent`/`GestureEvent`/`TouchEvent`/`KeyEvent` construction,
  UI child-node mutation, and legacy `Node` event dispatch remain in the app
  shell.
- 2026-06-05: DEBT-0003 was narrowed. Render/UI task dispatch, unique queued
  task replacement, async redraw notification, queue draining, render-context
  wrapping, UI tick redraw scheduling, UI-loop frame/FPS/live-reload timer
  cadence, and thread start/stop intents are now tested `pp_app_core` plans
  consumed by `App::render_task*`, `App::ui_task*`, `App::async_redraw`,
  `App::render_thread_*`, `App::ui_thread_*`, and
  `pano_cli plan-app-thread`; retained `std::thread`, condition-variable,
  OpenGL context, live asset reload, and task execution remain in the app
  shell.
- 2026-06-05: DEBT-0003 was narrowed again. Shutdown cleanup staging for
  UI-state save, stroke-preview renderer shutdown, recording stop,
  texture/shader invalidation, layout unload, UI render-target and face-plane
  destruction, panel-node release, and quick-mode cleanup now goes through
  tested `pp_app_core` plans consumed by `App::terminate` and
  `pano_cli plan-app-shutdown`; retained cleanup execution remains in the
  legacy app.
- 2026-06-05: DEBT-0003 was narrowed again. Command-line panorama conversion
  sequencing for renderer-state setup, temporary canvas allocation, project
  open, and equirectangular export now goes through tested `pp_app_core`
  planning consumed by `App::cmd_convert` and `pano_cli plan-command-convert`;
  retained OpenGL state dispatch and legacy `Canvas` project open/export
  execution remain in the legacy app.
- 2026-06-05: DEBT-0003 was narrowed again. Recording worker encode-wake
  eligibility now goes through tested `pp_app_core` planning consumed by
  `App::rec_loop` and `pano_cli plan-recording-session`; retained PBO
  equirect generation, dirty-stroke mutation, MP4 encoder calls, and frame
  label rendering remain in the legacy app/canvas/video path.
- 2026-06-05: DEBT-0037 was narrowed. MP4 recording export progress-dialog
  metadata now lives in tested `pp_app_core` planning, is exposed by
  `pano_cli plan-recording-session`, and retained progress bar creation now
  routes through `src/legacy_app_dialog_services.*`. Recording thread
  lifecycle, frame readback scheduling, progress lifetime, and MP4 writing
  remain open.
- 2026-06-05: DEBT-0038 was narrowed. Cloud transfer request/progress policy
  now lives in tested `pp_app_core` planning, live `App::download` and
  `App::upload` consume those plans before retained CURL setup, and
  `pano_cli plan-cloud-transfer` exposes missing endpoint, TLS policy,
  progress-callback, and zero/overrun progress cases for automation. CURL
  ownership, response/error handling, progress UI, cloud dialog/document
  execution, and injected network service work remain open under DEBT-0038.
- 2026-06-05: DEBT-0038 was narrowed again. Cloud upload warning/publish/
  success prompts, upload/bulk progress dialog titles, download-progress prompt
  metadata, and formatted download progress text now live in tested
  `pp_app_core` plans consumed by `src/legacy_cloud_services.*`; existing
  `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-upload-all`, and
  `pano_cli plan-cloud-transfer` expose that metadata for automation. Retained
  `NodeMessageBox`/progress-bar creation, save-before-upload threading, CURL
  ownership, response/error handling, and downloaded-project execution remain
  open under DEBT-0038.
- 2026-06-05: DEBT-0038 was narrowed again. Cloud download progress prompt
  creation now uses `src/legacy_app_dialog_services.*`, matching the upload
  warning/publish/success and bulk progress paths that already route through
  `App::message_box`/`App::show_progress`. Retained cloud prompt/progress
  lifetime, save-before-upload threading, CURL ownership, response/error
  handling, and downloaded-project execution remain open.
- 2026-06-05: DEBT-0035 was narrowed. Main-toolbar test-message dialog
  metadata now lives in tested `pp_app_core` planning, is exposed by
  `pano_cli plan-main-toolbar --command message-box`, and retained live
  message-box creation now routes through `src/legacy_app_dialog_services.*`.
  Open/save/settings routing, raw compatibility pointers, history/canvas
  adapter dispatch, and settings dialog execution remain open.
- 2026-06-05: DEBT-0058 was opened. App-level progress, message, and input
  dialog metadata now lives in tested `pp_app_core` planning consumed by
  `App::show_progress`, `App::message_box`, `App::input_box`, and
  `pano_cli plan-app-dialog`, but retained `Node*` dialog creation, layout
  insertion, callback wiring, and lifetime ownership remain in the legacy app.
  Later on 2026-06-05, retained `NodeProgressBar`, `NodeMessageBox`, and
  `NodeInputBox` creation moved into `src/legacy_app_dialog_services.*`, making
  the `App` methods thin adapters while retained layout/lifetime ownership
  remains open.
- 2026-06-05: DEBT-0043 and DEBT-0044 were narrowed. Export success,
  failure, and license-disabled dialog metadata plus export execution log
  labels now live in tested `pp_app_core` planning consumed by
  `src/legacy_document_export_services.*`, `App::dialog_export*`,
  `pano_cli plan-export-message`, and `pano_cli plan-export-report`.
  Retained canvas/video export calls, Web prepared-file handoff, picker
  execution, export-directory creation, and desktop timelapse worker threading
  remain open.
- 2026-06-05: DEBT-0050, DEBT-0051, DEBT-0052, DEBT-0053, and DEBT-0057 were narrowed.
  `pp_platform_api` now owns a tested `platform_policy` catalog for retained
  platform-family decisions covering iOS exported-image publishing, WebGL
  persistent-storage flushing, iOS document browse Inbox roots, Windows/macOS
  working-directory picker availability, iOS/Web prepared-file target
  selection, iOS collection-export working-directory policy, macOS PPBR data
  directory override, SonarPen availability, WebGL default canvas resolution,
  native UI/window state saving, live asset reload policy, recording cleanup
  policy, and canvas tip visibility.
  `WindowsPlatformServices` and the non-Windows legacy fallback consume those
  helpers while SDK-specific execution remains open in platform shells.
- 2026-06-05: DEBT-0054 was narrowed. Layout XML file reload policy now has a
  pure `plan_asset_file_load_with_probe` decision in `pp_platform_api`, with
  Windows/macOS mtime reload behavior and mobile/Web already-loaded no-op
  behavior selected by the tested `platform_policy` catalog. The live
  `plan_asset_file_load` wrapper still performs the retained `stat` probe for
  mtime platforms until asset/file watching is owned by injected storage
  services.
- 2026-06-12: DEBT-0054 was narrowed again. The retained mtime probe is now an
  explicit `pp_platform_api::probe_asset_file_timestamp` boundary with missing
  and existing file coverage, and `plan_asset_file_load_for_platform` consumes
  that probe instead of owning the `stat` call inline. Injected file-watch or
  storage services still need to own the live probe before the debt closes.
- 2026-06-12: DEBT-0054 was narrowed again. `LayoutManager::load` now calls the
  explicit timestamp probe only for platform families that use mtime reloads,
  then passes the result into the pure `plan_asset_file_load_with_probe`
  decision. The retained probe still uses local `stat` until injected storage
  or file-watch services replace it.
- 2026-06-12: DEBT-0045 was narrowed. Options-menu boolean preference
  persistence for VR controllers and auto-timelapse now routes through a named
  legacy preference-save helper inside `src/legacy_app_preference_services.*`
  instead of duplicating `Settings::set`/`Settings::save` in each callback.
  The helper still uses retained `Settings` storage until an injected
  preference store replaces it.
- 2026-06-12: DEBT-0045/0046 were narrowed. Retained preference persistence
  for options-menu VR-controller, auto-timelapse, viewport-density, cursor-mode,
  and startup run-counter writes now routes through `src/legacy_preference_storage.*`.
  The adapter still calls legacy `Settings` directly until a real preference
  store service is injected.
- 2026-06-12: DEBT-0045 was narrowed again. UI scale and retained UI-state/RTL
  saves now use `src/legacy_preference_storage.*`, leaving `App::set_ui_scale`
  and `App::ui_save` as UI/layout adapters instead of direct `Settings` writers.
- 2026-06-12: DEBT-0045/0052/0058 were narrowed. The whats-new dialog
  preference and Windows window-placement persistence now use
  `src/legacy_preference_storage.*` instead of writing `Settings` directly from
  dialog or platform entrypoint code.
- 2026-06-12: DEBT-0045/0046/0052/0058 were narrowed again. Matching retained
  preference reads for startup, UI restore/options, NodeCanvas defaults,
  whats-new state, and Windows window placement now use
  `src/legacy_preference_storage.*` instead of direct `Settings` reads.
- 2026-06-12: DEBT-0045/0052/0058 were narrowed again. App dialog, app layout,
  Windows entrypoint, and NodeCanvas paths that now use the retained preference
  storage adapter no longer include `settings.h` directly.
- 2026-06-12: DEBT-0046 was narrowed again. Startup preference loading now uses
  `src/legacy_preference_storage.*`, so `App::create` no longer includes
  `settings.h` directly for startup preferences.
- 2026-06-12: DEBT-0046 was narrowed again. `App::init` now reads startup
  preferences through a retained `LegacyStartupPreferenceSnapshot`, keeping the
  run-counter, auto-timelapse, and VR-controller preference keys inside
  `src/legacy_preference_storage.*`.
- 2026-06-12: DEBT-0045/0052 were narrowed again. Retained UI restore,
  NodeCanvas default, and Windows placement preference keys now live behind
  `LegacyUiPreferenceSnapshot`, `LegacyCanvasPreferenceSnapshot`, and
  `LegacyWindowPreferenceSnapshot` in `src/legacy_preference_storage.*`.
- 2026-06-12: DEBT-0045/0052 were narrowed again. UI scale, UI-state/RTL, and
  Windows placement write keys now also live behind named
  `src/legacy_preference_storage.*` helpers.
- 2026-06-12: DEBT-0045/0046/0058 were narrowed again. Whats-new,
  VR-controller, auto-timelapse, startup run-counter, viewport-density, and
  cursor-mode write keys now use named `src/legacy_preference_storage.*`
  helpers instead of generic key/value calls in legacy service bridges.
- 2026-06-12: DEBT-0045/0046/0052/0058 were narrowed again. The retained
  preference storage header now exposes only named snapshots/operations; generic
  key/value helpers are local to `src/legacy_preference_storage.cpp`.
- 2026-06-12: DEBT-0045/0046/0052/0058 were narrowed again. Retained preference
  keys are centralized as local constants inside `src/legacy_preference_storage.cpp`,
  with all typed legacy `Settings` calls routed through local helper functions.
- 2026-06-05: DEBT-0056 was narrowed. `src/asset.h` no longer exposes Android
  SDK types or forward declarations; retained Android asset-manager and asset
  handles are stored as opaque pointers and cast only inside `src/asset.cpp`,
  where the concrete `<android/asset_manager.h>` dependency remains. The
  legacy Android `asset.cpp` object compiled under the old Android CMake path;
  the full legacy `native-lib` target still fails later on unrelated
  pre-modernization C++ standard/header issues.
- 2026-06-05: DEBT-0009/0011 were narrowed for retained Android packages.
  Standard/Quest/Focus package CMake files now use CMake 3.10, target C++23,
  share the current modern component/service source set, and route `nanort`
  through a generated compatibility overlay without dirtying the submodule.
  The standard arm64 `native-lib` package target builds and links directly;
  Quest and Focus package CMake paths configure with the current Yoga source
  list. APK/package migration into root CMake remains open.
- 2026-06-05: DEBT-0009 was narrowed again. Android automation now queries
  `sdkmanager` for the newest available NDK/CMake packages, installs newer or
  missing versions when needed, selects the resulting pair through
  `scripts/automation/android-sdk-env.*`, and reports package update decisions
  in structured wrapper output. The current host reports NDK `30.0.14904198`
  and CMake `4.1.2` as `already-latest-available`; root Android `pp_assets`,
  retained standard `native-lib`, and retained Quest/Focus configure gates pass
  with that pair.
- 2026-06-05: DEBT-0004 was narrowed. Retained Linux and WebGL app CMake files
  now require CMake 3.10 and use target-level `cxx_std_23` with extensions off
  instead of global C++14 flags. `panopainter_retained_platform_cmake_self_test`
  guards those baselines while root CMake app/package target migration remains
  open.
- 2026-06-05: DEBT-0011 was narrowed again. Package readiness now includes a
  Linux app output blocker and points WebGL readiness at the retained WebGL app
  CMake entrypoint, with both PowerShell and shell wrappers reporting the
  retained Linux/WebGL CMake baseline prerequisite guarded by
  `panopainter_retained_platform_cmake_self_test`.
- 2026-06-05: DEBT-0011 was narrowed again. Root CMake now exposes
  `panopainter_linux_webgl_package_readiness`, a filtered package-smoke target
  for the retained Linux app and WebGL readiness blockers.
- 2026-06-05: DEBT-0011 was narrowed again. `package-smoke.ps1` now has
  `-AndroidNativeChecks`, which invokes the retained Android standard
  `native-lib` build and Quest/Focus configure helper for selected Android
  package kinds and reports those structured results beside the still-blocked
  APK readiness matrix. The shell readiness wrapper now advertises the same
  retained-native validation commands, and the package-smoke self-test guards
  that metadata.
- 2026-06-05: DEBT-0011 was narrowed again. Root CMake now exposes
  non-default package validation targets for package readiness and retained
  Android native package checks. `panopainter_android_native_package_smoke`
  calls the package-smoke Android native gate from CMake, validates the current
  latest SDK-managed NDK/CMake pair, and still reports APK outputs as blocked
  until root CMake owns real package targets.
- 2026-06-05: DEBT-0009 was narrowed again. Root CMake now exposes
  non-default platform-build automation targets for the default headless sweep,
  Android root CMake asset validation across standard/Quest/Focus presets, and
  the completed remote Apple compile validation. The Android target validated
  `pp_assets` across Android arm64/x64, Quest arm64, and Focus/Wave arm64 with
  the latest SDK-managed NDK/CMake pair. The platform-build wrapper defaults
  also include `pp_app_core_app_dialog_tests` again, so the target-matrix
  self-test is green.
- 2026-06-05: DEBT-0011 was narrowed again. Root CMake now exposes
  `panopainter_windows_app_package_smoke`, which calls the full Windows
  `package-smoke` command from CMake so the app executable/runtime payload check
  and Windows AppX blocker matrix are available from the CMake target graph.
- 2026-06-14: DEBT-0011 was narrowed again. `package-smoke.ps1` and
  `package-smoke.sh` now classify package readiness as `validated`,
  `compile-only`, or `blocked` instead of reporting every platform gate as
  blocked, and root CMake now exposes per-platform readiness targets for
  Windows AppX, Apple bundles, Android standard/Quest/Focus APKs, Linux app,
  and WebGL while real package outputs remain debt-tracked.
- 2026-06-05: DEBT-0007 was narrowed. `platform-build.ps1` now resolves
  `VCPKG_ROOT` for vcpkg presets from the environment or bundled Visual Studio
  installs, reports the selected vcpkg root in JSON, and root CMake exposes
  `panopainter_platform_build_vcpkg_ui_core` to validate the vcpkg-backed
  `pp_ui_core`/tinyxml2 boundary through `pp_ui_core_layout_xml_tests`.
- 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit,
  thumbnail, and object-draw history paths now query saved blend state through
  tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect
  export now binds cube textures through tested `pp_renderer_gl` dispatch; and
  `Font` text drawing now activates texture units through tested
  `pp_renderer_gl` dispatch. Desktop VR drawing also now routes active texture
  unit switches and fallback 2D texture unbinds through tested `pp_renderer_gl`
  dispatch. The debt remains open for live stroke
  rasterization, dual-brush compositing, pattern feedback math, thumbnail layer
  compositing, brush-preview compositing, and retained `ShaderManager::ext_*`
  compatibility fields.
- 2026-06-05: DEBT-0036 was narrowed again. Retained `NodeCanvas` and
  `NodeStrokePreview` draw-state paths now share `legacy_ui_gl_dispatch` for
  active-texture selection, fallback 2D texture unbinds, viewport/scissor
  execution, viewport and clear-color queries, clear-color restore,
  color-buffer clears, and capability query/apply adapter endpoints. Their live
  panorama and brush-preview compositing order remains legacy OpenGL and stays
  open under DEBT-0036.
- 2026-06-05: DEBT-0036 was narrowed again. Retained `CanvasMode` overlay,
  mask, transform, and canvas-tip pick paths now share `legacy_ui_gl_dispatch`
  for active-texture selection, capability query/apply, viewport execution,
  read-framebuffer queries, and RGBA8 pixel-readback adapter endpoints. Mode
  behavior, transform/cut execution, and live OpenGL paint UI drawing remain
  retained under DEBT-0036.
- 2026-06-05: DEBT-0036 was narrowed again. Retained `NodePanelGrid` heightmap
  draw and bake setup now share `legacy_ui_gl_dispatch` for active-texture
  selection, depth/blend capability query/apply, viewport query/execution,
  depth clears, and color-write-mask adapter endpoints. Grid lightmap baking,
  heightmap rendering, and retained OpenGL mesh/texture execution remain open.
- 2026-06-05: DEBT-0036 was narrowed again. Retained `CanvasLayer`
  cube/equirect generation and frame-clear paths now share
  `legacy_ui_gl_dispatch` for active-texture selection, cube texture binding,
  viewport execution, blend capability execution, clear-color query/restore,
  and color-buffer clear adapter endpoints. The cube-face framebuffer-to-texture
  copy remained open until it could use the shared retained framebuffer-copy
  utility bridge.
- 2026-06-05: DEBT-0036 was narrowed again. `CanvasLayer` cube-face
  framebuffer-to-texture copies now use the shared retained
  `copy_framebuffer_to_texture_target` utility bridge backed by tested
  `pp_renderer_gl` dispatch, so `src/canvas_layer.cpp` no longer owns a local
  raw `glCopyTexSubImage2D` adapter. The bridge remains retained until renderer
  services own cube and 2D copy execution.
- 2026-06-05: DEBT-0036 was narrowed again. Retained `Canvas` stroke,
  thumbnail, object-render, export, and `LayerFrame::clear` state endpoints now
  share `legacy_ui_gl_dispatch` for active-texture selection, fallback texture
  unbind, viewport/scissor execution, viewport and clear-color query,
  clear-color restore, and capability query/apply adapter endpoints. At that
  checkpoint, Canvas depth renderbuffer allocation/attachment/delete remained a
  retained renderer-resource bridge.
- 2026-06-05: DEBT-0036 was narrowed again. Retained `Canvas` and `RTT` depth
  renderbuffer allocation, framebuffer depth attachment, and renderbuffer
  deletion now share `legacy_gl_renderbuffer_dispatch`, so those files no
  longer carry duplicated raw renderbuffer callbacks. Renderer services still
  need to own renderbuffer lifetime before this bridge can be removed.
- 2026-06-05: DEBT-0036 was narrowed again. Retained `Texture2D`,
  `TextureCube`, and RTT texture allocation, deletion, binding, parameter
  setup, 2D update, and mipmap dispatch now share
  `legacy_gl_texture_dispatch`, so `src/texture.cpp` and `src/rtt.cpp` no
  longer carry duplicated raw texture callbacks. Renderer services still need
  to own texture resource lifetime before this bridge can be removed.
- 2026-06-05: DEBT-0036 was narrowed again. Retained `Texture2D` readback plus
  RTT framebuffer allocation, deletion, bind/restore, blit, readback, and PBO
  readback dispatch now share `legacy_gl_framebuffer_dispatch`, so
  `src/texture.cpp` and `src/rtt.cpp` no longer carry duplicated raw
  framebuffer/readback callbacks. Renderer services still need to own
  framebuffer and readback lifetime before this bridge can be removed.
- 2026-06-05: DEBT-0036 was narrowed again. Retained `Sampler` create,
  parameter, border-color, bind, and unbind dispatch now share
  `legacy_gl_sampler_dispatch`, and retained `PBO` allocation, framebuffer
  readback, map, unmap, and delete dispatch now share
  `legacy_gl_pixel_buffer_dispatch`. Renderer services still need to own sampler
  and pixel-buffer lifetime before these bridges can be removed.
- 2026-06-05: DEBT-0036 was narrowed again. Retained `Shape`, `TextMesh`, and
  `NodeColorWheel` mesh buffer/VAO creation, dynamic vertex/index uploads,
  fill/stroke/text draws, and buffer/VAO deletion now share
  `legacy_gl_mesh_dispatch`, so `src/shape.cpp`, `src/font.cpp`, and
  `src/node_colorwheel.cpp` no longer carry duplicated raw mesh callback
  clusters. Renderer services still need to own mesh resource lifetime before
  this bridge can be removed.
- 2026-06-05: DEBT-0036 was narrowed again. Retained shader source
  compilation/deletion, program attach/link/use/delete, attribute rebinding and
  location lookup, active-uniform enumeration, uniform-location discovery, and
  vec/mat/scalar uniform writes now share `legacy_gl_shader_dispatch`, so
  `src/shader.cpp` no longer carries raw shader/program/uniform callback
  ownership. Renderer services still need to own shader-program lifetime before
  this bridge can be removed.
- 2026-06-05: DEBT-0036 was narrowed again. Retained `gl_state` save/restore
  and `copy_framebuffer_to_texture_target` now reuse `legacy_ui_gl_dispatch`,
  `legacy_gl_framebuffer_dispatch`, `legacy_gl_shader_dispatch`, and
  `legacy_gl_sampler_dispatch`, so `src/util.cpp` no longer carries its local
  raw state/copy callback cluster. Renderer services still need to own state
  snapshot/restore and framebuffer-copy execution before these bridges can be
  removed.
- 2026-06-05: DEBT-0036 was narrowed again. Retained app startup, app clear,
  app UI viewport/scissor, command-convert renderer state, and desktop VR
  draw-state endpoints now share `legacy_ui_gl_dispatch`, so `src/app.cpp`,
  `src/app_commands.cpp`, and `src/app_vr.cpp` no longer carry duplicated raw
  callback clusters for capability, blend, clear, viewport, scissor, active
  texture, or 2D texture-unbind execution. Renderer services still need to own
  app and VR command execution before these bridges can be removed.
- 2026-06-05: DEBT-0036 was narrowed again. Retained RTT clear and
  masked-clear endpoints now share `legacy_ui_gl_dispatch`, so `src/rtt.cpp`
  no longer carries a local raw clear/color-mask callback cluster. Renderer
  services still need to own RTT render-target execution before this bridge can
  be removed.
- 2026-06-05: DEBT-0036 was narrowed again. Retained app startup logging,
  Windows early context logging/window-title detection, and shader capability
  detection now share `legacy_gl_runtime_dispatch`, so `src/app.cpp`,
  `src/main.cpp`, and `src/app_shaders.cpp` no longer carry duplicated raw
  runtime-query callback clusters. Renderer services still need to own runtime
  and capability probing before this bridge can be removed.
- 2026-06-06: DEBT-0036/DEBT-0017 were narrowed again. Retained HMD viewport
  setup, text atlas texture-unit activation, Windows/legacy default framebuffer
  binding, platform render-hint enable callbacks, the global OpenGL error drain,
  and Windows debug message callback installation now reuse
  `legacy_ui_gl_dispatch`/`legacy_gl_runtime_dispatch` instead of file-local raw
  OpenGL entrypoint adapters. Renderer/context services still need to own these
  paths before the retained dispatch bridges can be removed.
- 2026-06-05: DEBT-0061 was opened. `pp_platform_api` now owns a tested desktop
  XR runtime-selection policy that prefers OpenXR and labels OpenVR as a
  legacy fallback; `WindowsPlatformServices` consumes that policy before
  calling the retained OpenVR bridge. The actual OpenXR SDK/backend wiring
  remains open.
- 2026-06-05: DEBT-0008 was narrowed. `windows-msvc-default` and
  `windows-msvc-vcpkg-headless` now explicitly select the Visual Studio 18 2026
  generator and should be configured with the VS 2026-bundled CMake, avoiding a
  stale/generated-tree failure where a non-VS2026 CMake selected Ninja while
  also carrying an x64 platform value.
- 2026-06-05: DEBT-0062 was opened. VS 2026 builds generate a patched fmt
  header overlay in the build tree for `pp_legacy_vendor` so retained fmt skips
  the removed `stdext::checked_array_iterator` compatibility path without
  modifying the fmt submodule.
- 2026-06-05: DEBT-0061 was narrowed. While OpenVR remains the temporary desktop
  XR fallback, the Windows runtime payload now deploys `openvr_api.dll` beside
  `PanoPainter.exe` so Visual Studio launches exercise the retained bridge
  instead of failing during process load.
- 2026-06-06: DEBT-0063 was opened. The retained UI `Node` tree lifetime model
  now has an explicit roadmap track: raw parent/lookup pointers, public child
  ownership, raw callback targets, and manual destroy semantics must be
  replaced incrementally by `pp_ui_core` ownership handles, scoped callback
  connections, and mutation-safe dispatch tests before broad panel/dialog
  migration accelerates.
- 2026-06-06: DEBT-0063 was narrowed. `pp_ui_core::NodeLifetimeTree` now owns a
  pure checked-handle model for parent/child invariants, subtree destruction,
  scoped callback disconnection, and mutation-safe dispatch when callbacks
  destroy the dispatched node or add new connections. Retained `Node` adoption,
  layout reload coverage, and `pp_panopainter_ui` dialog/popup lifetime tests
  remain open.
- 2026-06-06: DEBT-0063 was narrowed again. The same `NodeLifetimeTree` model
  now covers pointer/keyboard capture ownership, automatic capture release when
  a captured node is destroyed, and whole-tree `clear()` invalidation for layout
  reloads. Retained `Node` adoption and app-specific popup/dialog lifetime
  tests remain open.
- 2026-06-06: DEBT-0063 was narrowed again. `pp_ui_core::UiOverlayLifetime`
  now models root and nested popup ownership, modal/modeless dialog capture
  policy, capture restoration after child/modal close, parent popup branch
  teardown, untracked close rejection, and layout-reload invalidation. Retained
  `NodePopupMenu`/`NodeDialog*` adoption remains open.
- 2026-06-06: DEBT-0063/DEBT-0058 were narrowed. `src/legacy_ui_overlay_services.*`
  now centralizes retained app-dialog overlay initialization and root attachment,
  and the app-level progress/message/input dialogs plus about/manual/changelog,
  document open/save/new/browse/resize/layer-rename, PPBR export, shortcuts, and
  what's-new overlays route through it. Raw `Node` ownership, callback captures,
  and retained popup/menu lifetime semantics remain open.
- 2026-06-06: DEBT-0063 was narrowed again. Retained File, Export, Edit, Tools,
  Panels, Options, About, and Layers app-menu popups now clone and attach through
  `src/legacy_ui_overlay_services.*`, so missing popup templates return explicit
  status/logging instead of direct `m_children[0]` dereferences in
  `App::init_menu_*`. Raw popup callback captures and retained close semantics
  remain open.
- 2026-06-06: DEBT-0063 was narrowed again. `src/legacy_ui_overlay_services.*`
  moved down to `pp_legacy_app` and now exposes retained root-popup attachment
  for `pp_panopainter_ui`; quick-panel brush/color popups, stroke-panel preset/
  tip/dual/pattern popups, and brush-panel preset menu insertion no longer call
  `root()->add_child(...)` directly. Base retained popup widgets such as
  `NodeComboBox`, raw callback captures, and close/capture semantics remain open.
- 2026-06-12: DEBT-0063 was narrowed again. Retained `NodeComboBox` popups and
  Open/Browse delete-confirmation dialogs now initialize and attach through
  `src/legacy_ui_overlay_services.*` instead of direct root insertion. Raw
  popup callback captures, retained close/capture semantics, and broader
  `Node` handle adoption remain open.
- 2026-06-12: DEBT-0063 was narrowed again. Top-toolbar stroke/color/layer/grid
  panel popups, quick-panel and stroke-panel popup tick decoration nodes, and
  brush preset menu template attachment now route through
  `src/legacy_ui_overlay_services.*` instead of direct root insertion or
  temporary panel child insertion. Raw popup callback captures, retained
  close/capture semantics, and broader `Node` handle adoption remain open.
- 2026-06-12: DEBT-0063 was narrowed again. Retained popup
  mouse-ignore/flood-events/capture-children setup and mouse capture activation
  for top-toolbar, quick-panel, stroke-panel, and combo-box popups now route
  through `src/legacy_ui_overlay_services.*`. Raw popup callback captures,
  retained close semantics, and broader `Node` handle adoption remain open.
- 2026-06-12: DEBT-0063 was narrowed again. Retained menu, submenu, combo-box,
  and brush-preset popup close/release execution now routes through
  `src/legacy_ui_overlay_services.*` instead of open-coded `mouse_release()` and
  `destroy()` pairs. Raw callback captures, retained popup close policy, and
  broader `Node` handle adoption remain open.
- 2026-06-12: DEBT-0063 was narrowed again. Top-toolbar, quick-panel, and
  stroke-panel popup tick-decoration close callbacks now bind through
  `src/legacy_ui_overlay_services.*` instead of per-panel lambdas. Other raw
  popup callbacks, retained close policy, and broader `Node` handle adoption
  remain open.
- 2026-06-12: DEBT-0063 was narrowed again. Brush, brush-preset, color, layer,
  grid, stroke, and color-picker popup-panel outside-click close handling now
  routes through `src/legacy_ui_overlay_services.*` for release, root removal,
  and callback dispatch instead of repeating that sequence in each panel class.
  Broader checked-handle and scoped-callback adoption remains open.
- 2026-06-12: DEBT-0063/DEBT-0058 were narrowed again. About, changelog,
  settings, and user-manual close buttons now bind retained destroy-on-click
  behavior through `src/legacy_ui_overlay_services.*` instead of per-dialog
  lambdas. Broader checked-handle and scoped-callback adoption remains open.
- 2026-06-12: DEBT-0063/DEBT-0058 were narrowed again. Message-box
  submit/cancel and input-box cancel destroy callbacks now use retained close
  helpers from `src/legacy_ui_overlay_services.*`. Dialog lifetime still uses
  raw `Node` callbacks until checked handles and scoped connections land.
- 2026-06-12: DEBT-0063/DEBT-0058 were narrowed again. Document open/save/new/
  browse/resize, layer-rename, cloud-browse, and PPBR export dialog cancel
  buttons now bind retained destroy-on-click behavior through
  `src/legacy_ui_overlay_services.*`.
- 2026-06-12: DEBT-0063/DEBT-0058 were narrowed again. App-level new-document,
  save, and layer-rename cancel callbacks now route retained dialog destruction
  plus virtual-keyboard hide through `src/legacy_ui_overlay_services.*`.
- 2026-06-12: DEBT-0063/DEBT-0058 were narrowed again. Browse accept, resize
  accept/failure, and what's-new read-later/close callbacks now route retained
  dialog destruction through `src/legacy_ui_overlay_services.*`.
- 2026-06-12: DEBT-0063/DEBT-0058 were narrowed again. Open/Browse
  delete-confirmation message boxes and shared destroy-on-click bindings now
  use the retained dialog close helper in `src/legacy_ui_overlay_services.*`.
- 2026-06-12: DEBT-0063/DEBT-0040/DEBT-0041/DEBT-0042 were narrowed again.
  Document-session overwrite prompts, unsaved-close prompts, save-before-workflow
  prompts, and accepted new/save document cleanup now route retained dialog
  closing through `src/legacy_ui_overlay_services.*`.
- 2026-06-12: DEBT-0063/DEBT-0058 were narrowed again. Cloud publish prompts,
  upload/download progress dialogs, and cloud browser download-close now route
  retained closing through `src/legacy_ui_overlay_services.*`.
- 2026-06-12: DEBT-0063/DEBT-0058 were narrowed again. App message-dialog
  cancel-button removal, layer-rename finish cleanup, and recording export
  progress cleanup now route retained closing through `src/legacy_ui_overlay_services.*`.
- 2026-06-12: DEBT-0063 was narrowed again. Floating-panel close and drag-outline
  cleanup now use the retained close helper in `src/legacy_ui_overlay_services.*`;
  drag placeholder reparenting and dock/drop mutation remain legacy-owned.
- 2026-06-12: DEBT-0063 was narrowed again. `NodePopupMenu` mouse-up release and
  destroy now route through the retained popup close helper in
  `src/legacy_ui_overlay_services.*`.
- 2026-06-12: DEBT-0063 was narrowed again. Layer-row, animation-timeline, and
  heightmap-overlay drag release paths now route mouse-capture release through
  `src/legacy_ui_overlay_services.*`, and brush/grid progress or recovery
  message dialogs now use the retained dialog close helper instead of direct
  `destroy()` calls. Checked handles and scoped callback ownership remain open.
- 2026-06-12: DEBT-0063 was narrowed again. Floating-panel placeholder detach/
  destroy, cloud-loading alignment cleanup, brush-preset item removal, and
  retained popup-panel parent detach now route through named retained node
  helpers in `src/legacy_ui_overlay_services.*`. Floating-panel dock/drop
  reparenting still uses legacy child-vector ownership until checked handles
  land.
- 2026-06-12: DEBT-0063/DEBT-0058 were narrowed again. Canvas export/project
  progress-bar cleanup and remote-page loading placeholder cleanup now route
  through retained close/destroy helpers in `src/legacy_ui_overlay_services.*`.
  The long-running canvas workflows still depend on legacy `App::I` UI tasking
  and raw progress-bar pointers.
- 2026-06-12: DEBT-0063 was narrowed again. Toolbar popup restoration from
  docked floating stroke/layer/grid panels and restored floating color-panel
  title cleanup now use retained detach/close/destroy helpers in
  `src/legacy_ui_overlay_services.*`. Dock/drop child ownership and raw panel
  globals remain legacy-owned.
- 2026-06-12: DEBT-0063 was narrowed again. Retained canvas mode mouse-capture
  release for camera, paint, line, grid, mask, and fill interactions now routes
  through `src/legacy_ui_overlay_services.*` instead of direct `NodeCanvas`
  release calls. Capture ownership is still raw until retained `Node` adopts
  checked handles and scoped event/capture lifetime.
- 2026-06-12: DEBT-0063 was narrowed again. Retained button, slider, scroll,
  color-wheel, color-quad, and canvas gesture-end capture release now route
  through `src/legacy_ui_overlay_services.*`. The controls still own raw event
  callback targets and public `Node` capture state.
- 2026-06-12: DEBT-0063/DEBT-0058 were narrowed again. Legacy document-open
  import/discard prompts and brush-package export completion now route retained
  dialog closing through `src/legacy_ui_overlay_services.*` instead of direct
  `destroy()` calls from service adapters.
- 2026-06-12: DEBT-0063 was narrowed again. Flood-fill tool teardown and
  checkbox icon removal now route retained node destruction through
  `src/legacy_ui_overlay_services.*`. The underlying `Node` tree still exposes
  raw child pointers and public destroy state.
- 2026-06-12: DEBT-0063 was narrowed again. Layout unload root destruction and
  popup tick-overlay close callbacks now route retained node destruction through
  `src/legacy_ui_overlay_services.*`. `Node::destroy()` itself remains the
  compatibility boundary until checked tree handles replace raw parent/child
  mutation.
- 2026-06-05: DEBT-0011 was narrowed. The Windows app package smoke target now
  passes the configure-time CMake executable into `package-smoke.ps1`, so VS
  2026 generator validation does not depend on an older `cmake` on PATH, and
  the smoke checks cover the Windows launch-folder DLL payload.
- 2026-06-05: DEBT-0010 was narrowed. `pp_paint_renderer` now consumes
  `pp_document` directly for pure frame/face compositing, expanding per-layer
  dirty face payload rectangles into a full renderer-sized RGBA buffer while
  preserving document layer visibility, opacity, blend mode, and uneven
  per-layer frame timelines.
- 2026-06-05: DEBT-0010 was narrowed again. The same compositor boundary now
  exposes a pure six-face document frame composite plus
  `pano_cli simulate-document-render` JSON automation, so headless tests can
  validate document payloads moving toward renderer/export services without a
  GL context.
- 2026-06-05: DEBT-0010 was narrowed again. `pp_paint_renderer` can now upload
  a pure document frame's six composited faces through the renderer-neutral
  `IRenderDevice` texture API, and the recording backend/CLI smoke validate
  six RGBA8 texture uploads plus explicit shader-read transitions.
- 2026-06-05: DEBT-0010/DEBT-0036 were narrowed. `pano_cli
  simulate-document-render` now also runs those recorded document upload
  commands through `pp_renderer_gl::plan_recorded_render_commands`, proving
  the six texture uploads and transitions are accepted by the current OpenGL
  command planner while live legacy GL execution remains retained.
- 2026-06-05: DEBT-0010 was narrowed again. `pp_app_core` now owns a tested
  metadata-only live-canvas-to-`pp_document::CanvasDocument` snapshot planner,
  `pano_cli plan-canvas-document-snapshot` exposes the projection as JSON
  automation, and `src/legacy_document_canvas_services.*` can build the same
  snapshot from retained `Canvas` dimensions, active layer/frame, layer
  visibility/opacity/alpha/blend metadata, and frame durations. Renderer-owned
  cube-face pixel payload readback remains open under DEBT-0010/DEBT-0036.
- 2026-06-05: DEBT-0010 was narrowed again. The canvas snapshot planner now
  accepts captured RGBA8 face payloads and attaches them to
  `pp_document::CanvasDocument`; `pano_cli plan-canvas-document-snapshot
  --captured-face-payloads-per-layer` covers payload-bearing automation, and
  `src/legacy_document_canvas_services.*` exposes an opt-in dirty-face payload
  snapshot path backed by retained `Layer::snapshot()` readback. Live save,
  export, and action-command adoption of that payload-bearing snapshot remains
  open under DEBT-0010/DEBT-0013/DEBT-0036.
- 2026-06-05: DEBT-0010/DEBT-0013/DEBT-0040/DEBT-0042 were narrowed. Live
  Save, Save As, Save Version, and save-before-workflow now prepare a
  payload-bearing canvas document snapshot plus `pp_app_core` save-readiness
  report before delegating to retained `Canvas::project_save`. The retained
  writer still owned PPI serialization, progress/threading, and compatibility
  quirks, so pure writer replacement remained open.
- 2026-06-06: DEBT-0010/DEBT-0013/DEBT-0040/DEBT-0042 were narrowed again.
  `pp_app_core` now owns a tested save-writer route policy for canvas snapshots,
  `pano_cli plan-canvas-document-snapshot` emits that route as JSON, and live
  Save, Save As, Save Version, and save-before-workflow run the pure PPI
  exporter for payload-complete snapshots and log byte counts before retained
  `Canvas::project_save` continues. The retained writer still owns actual save
  serialization, app metadata mutation, progress/threading, and compatibility
  quirks.
- 2026-06-06: DEBT-0040/DEBT-0042 were narrowed again. `pp_app_core` now owns
  the retained project-save target path planner for the requested PPI path,
  temporary `.tmp.ppi` path, and timelapse `.pptl` sidecar path; live
  `Canvas::project_save_thread` consumes it and
  `pano_cli plan-canvas-project-save-target` exposes it for automation. Actual
  PPI serialization, temporary-file swap execution, progress/threading,
  timelapse sidecar serialization, and app metadata mutation remain retained.
- 2026-06-06: DEBT-0040/DEBT-0042 were narrowed again. The same app-core
  project-save planner now owns the direct-write versus temporary-write mode
  decision for new and existing targets, including the retained compatibility
  fallback to direct target writes when the temporary file cannot be opened.
  `Canvas::project_save_thread` consumes that policy before retained
  serialization, and `pano_cli plan-canvas-project-save-target` reports it in
  JSON. Actual PPI bytes, temporary-file swap execution, progress/threading,
  timelapse sidecar serialization, and app metadata mutation remain retained.
- 2026-06-06: DEBT-0040/DEBT-0042 were narrowed again. `pp_app_core` now owns
  the retained project-save commit outcome policy for direct writes, successful
  temporary swaps, target-remove failures, and rename-after-remove failures,
  including an explicit `targetMayBeMissing` flag for failed swaps after the
  original target was removed. Live `Canvas::project_save_thread` consumes that
  result before retained success metadata mutation, and
  `pano_cli plan-canvas-project-save-target` reports the same commit plan.
  Actual PPI serialization, filesystem remove/rename execution,
  progress/threading, timelapse sidecar serialization, and app metadata
  mutation remain retained.
- 2026-06-06: DEBT-0040/DEBT-0042 were narrowed again. `pp_app_core` now owns
  the retained project-save post-commit side-effect policy: successful saves
  mark the document clean, commit new-document state, flush platform storage,
  optionally write a timelapse sidecar when an encoder exists, and always
  dismiss visible progress UI plus refresh the title. Live
  `Canvas::project_save_thread` consumes that plan, and
  `pano_cli plan-canvas-project-save-target` reports it in JSON. Actual state
  mutation, progress UI destruction, platform flush execution, title update,
  and timelapse sidecar serialization remain retained.
- 2026-06-05: DEBT-0010/DEBT-0013 were narrowed again. `pp_app_core` now
  exports payload-complete or metadata-only canvas document snapshots through
  the pure `pp_document` PPI writer and rejects snapshots that still require
  renderer readback; `pano_cli plan-canvas-document-snapshot` reports
  `ppiExport` readiness, byte counts, and decoded dirty-face counts.
- 2026-06-05: DEBT-0010/DEBT-0036 were narrowed again. Payload-complete
  `pano_cli plan-canvas-document-snapshot` automation now feeds the captured
  `pp_document` snapshot through `pp_paint_renderer::upload_document_frame_faces`
  and the renderer-neutral recording backend, reporting texture uploads,
  transitions, command counts, bytes, and active-frame payload counts.
- 2026-06-05: DEBT-0010/DEBT-0036/DEBT-0043 were narrowed. Live equirectangular,
  layer, animation-frame, and cube-face export adapters now prepare the
  payload-bearing canvas document snapshot and renderer-neutral upload report
  before retained `Canvas` export execution. Failures are logged and retained
  export still continues.
- 2026-06-05: DEBT-0036 was narrowed again. The renderer-neutral document-frame
  recorded-upload report now lives in `pp_paint_renderer` as
  `record_document_frame_upload`, with focused compositor coverage, and both
  `pano_cli plan-canvas-document-snapshot` plus live export readiness logging
  consume that shared helper instead of duplicating command counting.
- 2026-06-05: DEBT-0010/DEBT-0036/DEBT-0043 were narrowed again.
  `pp_paint_renderer::export_document_frame_face_pngs` now composites a
  document frame and encodes all six cube faces to PNG bytes through
  `pp_assets`, `pano_cli plan-canvas-document-snapshot` reports
  `facePngExport` readiness, and live image/collection/cube export readiness
  logging runs the same pure writer primitive before retained `Canvas` writers.
- 2026-06-05: DEBT-0036/DEBT-0043 were narrowed again. Live cube-face export
  now writes the pure document/renderer face PNG bytes to the retained
  `work_path/document-face.png` filename set and publishes each path through the
  platform service, falling back to retained `Canvas::export_cube_faces` if
  snapshot capture, face PNG generation, or file writing fails.
- 2026-06-05: DEBT-0043 was narrowed again. The retained cube-face filename
  set is now planned by tested `pp_app_core`
  `make_document_cube_face_export_target`, exposed through
  `pano_cli plan-export-target --kind cube-faces`, and consumed by
  `src/legacy_document_export_services.*` before writing pure face-PNG bytes.
  Storage service ownership and non-cube retained export writers remain open.
- 2026-06-05: DEBT-0043 was narrowed again. Cube-face export file writes and
  exported-image publishing now dispatch through tested `pp_app_core`
  `execute_document_cube_face_export_write`, so the live bridge only adapts the
  retained filesystem write and `App::publish_exported_image` calls. Dedicated
  storage/platform service ownership and non-cube retained export writers
  remain open.
- 2026-06-05: DEBT-0010/DEBT-0036/DEBT-0043 were narrowed again. The CLI
  payload-complete canvas snapshot path and live export adapter now share
  `pp_paint_renderer::prepare_document_frame_export_readiness`, centralizing
  the document-frame recorded upload report and six-face PNG export report used
  before retained writer fallback.
- 2026-06-05: DEBT-0010/DEBT-0036/DEBT-0043 were narrowed again.
  `pp_paint_renderer::export_document_frame_equirectangular_png` now converts
  the composited document-frame cube faces into an equirectangular PNG with the
  shader-equivalent cube sampling policy and `pp_assets` PNG encoding. Live PNG
  equirectangular export writes through that path before retained fallback;
  then-open JPEG/XMP metadata parity, GPU filtering/golden parity, Web
  prepared-file handoff, and the then-retained collection/depth/video writers
  stayed open.
- 2026-06-06: DEBT-0010/DEBT-0036/DEBT-0043 were narrowed again.
  `pp_app_core` now owns the document-snapshot export route decision used by
  live equirectangular, layer, animation-frame, and cube-face export bridges,
  including payload-complete writer use, PNG/JPEG equirectangular target
  support, collection/cube target support, target/platform unsupported
  fallback, and pending renderer-readback fallback reasons. `pano_cli
  plan-export-snapshot-route` exposes the same route policy, including
  unsupported target paths. Retained writers, Web handoff, video/depth export
  execution, and renderer-owned payload readback remain open.
- 2026-06-06: DEBT-0010/DEBT-0043 were narrowed again. `pp_app_core` now owns
  the current-platform support helper for document-snapshot export writers, and
  the live retained export bridge consumes that policy for equirectangular,
  layer, animation-frame, and cube-face snapshot-writer attempts instead of
  carrying local Web writer gates. `pano_cli plan-export-snapshot-route` now has
  smoke coverage for unsupported-platform fallback. Retained Web prepared-file
  handoff, incomplete-readback collection fallback, depth/video export
  execution, and broader renderer-owned export execution remain open.
- 2026-06-06: DEBT-0010/DEBT-0043 were narrowed again. Depth export now logs
  the same tested `pp_app_core` document-snapshot route fallback as an
  unsupported writer target before retained depth rendering, and
  `pano_cli plan-export-snapshot-route --kind depth` has smoke coverage for the
  unsupported-target JSON contract. The actual depth perspective render,
  depth-payload readback, write execution, progress/threading parity, and
  retained Web handoff remain open.
- 2026-06-06: DEBT-0010/DEBT-0043 were narrowed again. `pp_app_core` now owns a
  tested single-file export write/publish executor, and the live PNG/JPEG
  equirectangular document-snapshot writer uses it before retained fallback.
  The retained bridge still provides the filesystem/publish adapter, while
  Web prepared-file handoff, incomplete-readback collection fallback,
  depth/video export execution, progress/threading parity, and broader storage
  service ownership remain open.
- 2026-06-05: DEBT-0010/DEBT-0036/DEBT-0043 were narrowed again.
  `pp_paint_renderer` now exports independent layer equirectangular PNGs and
  merged animation-frame equirectangular PNGs from payload-complete
  `pp_document` snapshots, while `pp_app_core` owns the retained collection
  filename suffixes plus collection write/publish execution. Live layer and
  animation-frame PNG collection export uses those paths before retained
  fallback; Web handoff, incomplete renderer-readback snapshots, progress and
  threading parity, exact GPU filtering/goldens, depth, video, and then-open
  JPEG/XMP remained open.
- 2026-06-05: DEBT-0010/DEBT-0036/DEBT-0043 were narrowed again. Depth export
  now plans the retained image/depth output target paths in tested
  `pp_app_core`, has a tested `pp_app_core` two-payload write/publish executor
  ready for later pure payload adoption, and logs a tested
  `pp_paint_renderer::plan_document_depth_export_render` result from the live
  document snapshot before retained `Canvas::export_depth` runs. The actual
  1024x1024 perspective/depth rendering, readback, worker-thread behavior, and
  the legacy `.png` path names carrying JPEG-encoded bytes remain open.
- 2026-06-05: DEBT-0010/DEBT-0036 were narrowed again.
  `pano_cli plan-canvas-document-snapshot` now emits the same depth export
  render-plan readiness as the live adapter. Metadata-only snapshots report
  depth export unavailable until renderer payload readback is captured;
  payload-complete snapshots report the 1024x1024 merged-face and per-layer
  depth draw counts while still marking final depth image readback as
  renderer-owned.
- 2026-06-05: DEBT-0010/DEBT-0036/DEBT-0043 were narrowed again.
  `pp_assets` now owns RGBA8 JPEG encode/decode plus GPano XMP APP1 injection,
  `pp_paint_renderer` exports equirectangular JPEG+XMP bytes from the pure
  document-frame projection, and the live desktop `.jpg`/`.jpeg`
  equirectangular export bridge writes that payload before retained fallback.
  Web handoff, incomplete-readback snapshots, progress/threading parity, exact
  GPU filtering/goldens, depth render/readback, and video remain open.

## Open Debt

| ID | Status | Owner | Item | Reason | Validation | Removal Condition |
| --- | --- | --- | --- | --- | --- | --- |
| DEBT-0001 | Open | Modernization | Existing platform build files remain alongside new CMake | Required for incremental migration without losing platform coverage | Existing platform builds plus new CMake configure | Remove after all platform builds consume shared CMake targets |
| DEBT-0002 | Open | Modernization | Vendored SDK and patched libraries retained initially, including the generated Android package `nanort` compatibility overlay tracked by DEBT-0060 and the retained OpenVR SDK fallback tracked by DEBT-0061 | Some dependencies are SDK-only, patched, or have platform-specific binaries | Dependency inventory and platform build smoke tests | Replace with vcpkg packages or document permanent vendored status after triplet evaluation |
| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, `App::share_file`, `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, `App::rec_loop`, `App::update_ui_observer`, `App::render_task*`, `App::ui_task*`, `App::render_thread_*`, `App::ui_thread_*`, file-menu save actions, `NodeCanvas` canvas hotkeys, new/open/browse dirty-document workflow prompts, new-document target/resolution/overwrite decisions, save-as document file naming and overwrite decisions, save-version target decisions, export start/menu/target naming/path/message/report decisions, share-file saved-path decisions, file/image/save/directory picker selected-path decisions, display-file external-open decisions, virtual-keyboard visibility decisions, recording lifecycle/export progress/worker decisions, cloud-upload prompt/save-before-upload decisions, cloud-browse availability and selected-download decisions, bulk cloud-upload progress decisions, tools/options app preference decisions, app status/display and renderer diagnostic decisions, app dialog metadata decisions, app frame/UI-observer decisions, app thread/task orchestration decisions, document resize decisions, layer rename/menu decisions, Tools menu/panel decisions, About menu/diagnostic decisions, main toolbar/status decisions, `pano_cli classify-open`, `pano_cli plan-open-route`, `pano_cli plan-file-menu`, `pano_cli plan-new-document`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-export-start`, `pano_cli plan-export-menu`, `pano_cli plan-export-target`, `pano_cli plan-export-message`, `pano_cli plan-export-report`, `pano_cli plan-recording-session`, `pano_cli plan-app-preferences`, `pano_cli plan-app-status`, `pano_cli plan-app-dialog`, `pano_cli plan-app-thread`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, `pano_cli plan-about-menu`, `pano_cli plan-main-toolbar`, `pano_cli plan-document-resize`, `pano_cli plan-layer-rename`, `pano_cli plan-layer-menu`, `pano_cli plan-canvas-hotkey`, `pano_cli plan-share-file`, `pano_cli plan-picked-path`, `pano_cli plan-display-file`, `pano_cli plan-keyboard-visibility`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-browse`, `pano_cli plan-cloud-upload-all`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session/export/recording/preferences/status/dialog/thread/share/platform-I/O/display/keyboard/cloud/resize/layer/tools/about/toolbar/canvas-command contracts, but document creation/loading, brush import execution, saving, export execution, tools/options UI execution, Tools panel creation/execution, About dialog/diagnostic execution, toolbar/status dialog/history/canvas execution, app dialog node creation, status/display UI rendering, renderer diagnostic capability adaptation, app task/thread execution, UI observer parent walking/callback execution, document resize execution, layer rename/menu execution, settings persistence, platform share service execution, picker service execution, display-file service execution, keyboard service execution, recording/MP4/PBO execution, cloud upload execution, and cloud browse/download execution still reach legacy `Canvas::I`/UI/network/video/platform singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_file_menu_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_recording_tests`; `pp_app_core_app_frame_tests`; `pp_app_core_app_preferences_tests`; `pp_app_core_app_status_tests`; `pp_app_core_app_dialog_tests`; `pp_app_core_app_thread_tests`; `pp_app_core_tools_menu_tests`; `pp_app_core_about_menu_tests`; `pp_app_core_main_toolbar_tests`; `pp_app_core_document_resize_tests`; `pp_app_core_document_layer_tests`; `pp_app_core_document_sharing_tests`; `pp_app_core_document_platform_io_tests`; `pp_app_core_document_cloud_tests`; `pp_app_core_document_session_tests`; `pp_app_core_canvas_hotkey_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-open-route --path D:/Paint/demo.ppi --unsaved`; `pano_cli plan-file-menu --command save-as`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind animation-mp4 --demo`; `pano_cli plan-export-target --kind file --work-dir D:/Paint --doc-name demo --extension .png`; `pano_cli plan-export-message --kind timelapse --destination success`; `pano_cli plan-export-report --kind license-disabled`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-recording-session --running --no-encoder`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `pano_cli plan-app-status --doc-name demo --unsaved --resolution 2048 --resolution-index 3 --zoom 1.25 --history-bytes 1572864 --recording-running --encoder-available --encoded-frames 12 --framebuffer-fetch --float32 --float32-linear --float16`; `pano_cli plan-app-dialog --kind message --cancel`; `pano_cli plan-app-frame`; `pano_cli plan-app-thread --kind ui-loop --live-reload`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-menu --command merge --current-index 2 --lower-name Paint`; `pano_cli plan-canvas-hotkey --event key-up --key z --ctrl --undo-count 2`; `pano_cli plan-share-file --path D:/Paint/demo.ppi`; `pano_cli plan-picked-path --path D:/Paint/demo.ppi`; `pano_cli plan-display-file --path D:/Paint/export.png`; `pano_cli plan-keyboard-visibility --visible`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries |
| DEBT-0004 | Open | Modernization | Android, Linux, and WebGL retained CMake entrypoints now use the CMake 3.10/C++23 modernization baseline, but Android Gradle/APK, Linux app, WebGL app/package, Apple, and AppX build files remain platform-specific until root CMake alignment reaches them | Prevent platform regressions during incremental migration; raw Windows `.sln/.vcxproj` files were removed on 2026-05-31 by user decision | `cmake --preset windows-msvc-default`; `python scripts/dev/check_retained_platform_cmake.py`; platform-specific configure/build smoke checks as each platform is migrated | Root CMake owns every platform source list and package path |
| DEBT-0005 | Open | Modernization | Temporary local CTest harness is used before Catch2 is wired through vcpkg | `vcpkg` is not currently on PATH, but headless tests need to run now | `ctest --preset desktop-fast --build-config Debug` | Replace `tests/test_harness.h` tests with Catch2 tests once vcpkg toolchain/presets are validated |
| DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, and root CMake now exposes a focused `panopainter_platform_build_vcpkg_ui_core` target for the vcpkg-backed `pp_ui_core`/tinyxml2 boundary, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_platform_build_vcpkg_ui_core` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions |
| DEBT-0008 | Open | Modernization | `windows-msvc-default` and `windows-msvc-vcpkg-headless` explicitly select Visual Studio 18 2026 for local validation, but non-VS2026 CMake executables on PATH may not know that generator | The local machine has VS 2026, but using an older CMake can still default to Ninja or reject the VS 2026 generator | `cmake --preset windows-msvc-default`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter`; `ctest --preset desktop-fast --build-config Debug` | The repo automation invokes or locates a CMake executable that supports `Visual Studio 18 2026`, and VS 2026 generator validation is the normal Windows path without manual tool selection |
| DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, while retained standard/Quest/Focus package CMake paths now have a refreshed CMake 3.10/C++23 baseline outside root CMake; automation queries `sdkmanager`, installs newer or missing SDK Manager NDK/CMake packages, selects the resulting pair before configure, and reports update decisions; root CMake exposes non-default platform-build and retained native package validation targets | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64`; `cmake --build --preset android-x64`; `cmake --build --preset android-quest-arm64`; `cmake --build --preset android-focus-arm64`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_platform_build_android_assets`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages quest,focus -ConfigureOnly`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly -AndroidNativeChecks -PackageKinds android-standard-apk,android-quest-apk,android-focus-apk`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_native_package_smoke` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands |
| DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model with alpha-lock metadata, snapshot construction, per-layer frame metadata, renderer-free RGBA8 face payload storage, snapshot-embedded face-payload validation, renderer-free alpha8 selection-mask storage, PPI import/export helpers, stroke-script-to-face-payload CLI automation, `pp_paint_renderer` document face/frame compositors, renderer-neutral six-face texture upload, pure six-face PNG export, pure equirectangular PNG export, pure equirectangular JPEG+XMP export, pure layer/animation-frame PNG collection export, pure depth image/depth PNG export for payload-complete snapshots, shared document-frame export readiness reporting, depth export render-plan reporting, OpenGL command-planner validation through CLI render automation, live Canvas snapshot projection through `pp_app_core`/`legacy_document_canvas_services`, captured RGBA8 payload attachment to `pp_document`, live Save/Save As/Save Version/save-before-workflow snapshot-readiness reporting before retained save execution, pure app-core PPI export for payload-complete canvas snapshots, payload-complete canvas-snapshot renderer-upload plus face-PNG export automation, live cube-face face-PNG writer execution using app-core face target planning and write/publish service dispatch with retained fallback, live PNG/JPEG equirectangular writer execution using the paint-renderer equirectangular exports plus app-core file write/publish dispatch with retained fallback, live payload-complete layer/animation-frame collection writer execution using paint-renderer PNG sequences and app-core collection write/publish dispatch with retained fallback, live payload-complete depth export execution using pure paint-renderer PNG payloads plus app-core two-payload writing with retained fallback, tested app-core document-snapshot export route policy for writer versus retained fallback including current-platform support, and live equirectangular/layer/animation-frame/depth/cube-face export snapshot/render/export-readiness reporting through the shared readiness helper plus the depth render plan, but action-command adoption, live save-writer replacement, Web and incomplete-readback collection handoff, progress/threading parity, broader renderer-owned export execution, exact GPU/golden parity, live-camera depth parity, and renderer-owned cube-face readback ownership are not yet wired | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2`; `pano_cli load-project --path tests\data\projects\minimal-project.ppi`; `pano_cli simulate-document-render --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --width 64 --height 32`; `pano_cli plan-canvas-document-snapshot --captured-face-payloads-per-layer 1`; `pano_cli plan-export-snapshot-route --kind layers-collection --captured-face-payloads 3 --pending-face-payloads 6`; `pp_document_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pp_paint_renderer_compositor_tests`; `pp_app_core_document_canvas_tests`; `pp_app_core_document_export_tests`; `pano_cli_simulate_document_edits_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_simulate_document_render_smoke`; `pano_cli_plan_canvas_document_snapshot_smoke`; `pano_cli_plan_canvas_document_snapshot_payload_smoke`; `pano_cli_plan_export_snapshot_route_pending_smoke`; `pano_cli_save_document_project_roundtrip_smoke`; `pano_cli_apply_stroke_script_roundtrip_smoke`; `pano_cli_apply_stroke_script_rejects_tiny_canvas` | Legacy document behavior is represented by `pp_document`/`pp_paint_renderer` tests and the app consumes it through a boundary/facade |
| DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact and launch-folder DLL payload, reports a structured package readiness matrix for Windows AppX, Android standard/Quest/Focus APKs, Apple bundles, Linux app output, and WebGL output, and now classifies each package gate as `validated`, `compile-only`, or `blocked` based on local prerequisites plus root CMake package-target ownership; the Windows app smoke passes the configure-time CMake executable so VS 2026 generator validation does not depend on `cmake` from PATH, retained Android package native CMake paths, retained Linux/WebGL CMake baseline metadata, and per-platform readiness targets are reachable from package validation and root CMake package-readiness targets, but Windows AppX and Apple/WebGL outputs remain blocked where local toolchains or root package targets are missing and Android/Linux package readiness remains compile-only until root CMake owns real package targets | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_windows_app_package_smoke`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_windows_appx_package_readiness`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_apple_bundle_package_readiness`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -ReadinessOnly -AndroidNativeChecks -PackageKinds android-standard-apk,android-quest-apk,android-focus-apk`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_standard_apk_package_readiness`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_quest_apk_package_readiness`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_focus_apk_package_readiness`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_android_native_package_smoke`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_linux_app_package_readiness`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_webgl_package_readiness`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_linux_webgl_package_readiness`; `python scripts/dev/check_package_smoke_readiness.py`; `bash -n scripts/automation/package-smoke.sh` | Package-smoke builds and validates Windows AppX, Android APK variants, Linux app, Apple bundles, and WebGL output where local toolchains are present |
| DEBT-0012 | Open | Modernization | `pp_ui_core` uses vcpkg tinyxml2 on `windows-msvc-vcpkg-headless`, but retains `pp_vendor_tinyxml2` for default and unproven platform presets | Mobile/AppX/Apple triplets and app packaging still need validation before removing the vendored fallback | `ctest --preset desktop-fast-vcpkg --build-config Debug`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | All supported presets consume vcpkg tinyxml2 or document a permanent vendored exception |
| DEBT-0013 | Open | Modernization | `pp_assets`, `pp_document`, `pano_cli inspect-project`, `pano_cli load-project`, and `pano_cli save-project` validate the fixed PPI header, thumbnail/body byte layout, generated multi-layer/multi-frame PPI writing with explicit layer opacity/blend/alpha-lock/visibility metadata, per-layer frame durations, metadata-only and targeted dirty-face-payload save/load round-trips, layer/frame index, dirty-face descriptors, dirty-face PNG payload metadata, asset-level RGBA PNG payload decoding, pure document-to-PPI export, CLI document export automation, file-writing document export automation, stroke-script-generated document payload export, decoded pixel attachment to `pp_document`, live save-path snapshot-readiness reporting, and app-core canvas-snapshot-to-PPI export automation, but full legacy PPI round-trip parity and pure live save writer replacement are not yet extracted | Full PPI save parity requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_image_pixels_tests`; `pp_assets_ppi_header_tests`; `pp_document_ppi_import_tests`; `pp_document_ppi_export_tests`; `pano_cli_inspect_project_layout_smoke`; `pano_cli_load_project_metadata_smoke`; `pano_cli_save_project_roundtrip_smoke`; `pano_cli_save_project_payload_roundtrip_smoke`; `pano_cli_simulate_document_export_smoke`; `pano_cli_save_document_project_roundtrip_smoke`; `pano_cli_apply_stroke_script_roundtrip_smoke`; `pano_cli_apply_stroke_script_rejects_tiny_canvas` | Full PPI load/save fixtures cover thumbnails, decoded layer face payloads attached to documents, frames, corrupt payloads, dirty-face payload saving, arbitrary legacy canvas payload/layout combinations, and legacy app round-trip compatibility |
| DEBT-0014 | Open | Modernization | `windows-clangcl-asan` now configures as a headless Ninja/clang-cl preset and uses the release MSVC runtime required by ASan, but local builds still fail because installed clang-cl 18.1.8 is paired with VS 2026-preview STL headers that require Clang 20 or newer | Sanitizer validation should be local and repeatable, but this machine's compiler/header pairing is incompatible | `cmake --fresh --preset windows-clangcl-asan`; `cmake --build --preset windows-clangcl-asan --target pp_foundation` | Install/use Clang 20+ with the VS 2026 STL, or point the preset at a compatible VS 2022 toolchain, then make `platform-build.ps1 -Presets windows-clangcl-asan` pass for the headless matrix |
| DEBT-0015 | Open | Modernization | Cursor visibility requests now consume pure `pp_app_core` planning through `pano_cli plan-cursor-visibility`, `App::show_cursor`/`App::hide_cursor` dispatch through `PlatformServices` without platform guards, and Windows live execution uses injected `WindowsPlatformServices`, but macOS cursor execution still reaches the retained fallback adapter | Keep canvas cursor behavior stable while platform shells are extracted incrementally | `pp_app_core_document_platform_io_tests`; `pano_cli plan-cursor-visibility --visible`; `ctest --preset desktop-fast --build-config Debug` | Cursor visibility execution is owned by injected `pp_platform_*` services for every supported platform |
| DEBT-0016 | Open | Modernization | Clipboard get/set requests now consume pure `pp_app_core` planning through `pano_cli plan-clipboard-read` and `pano_cli plan-clipboard-write`; Windows live execution uses injected `WindowsPlatformServices`, Apple clipboard execution now uses `src/platform_apple/apple_platform_services.*`, Android clipboard execution now uses `src/platform_android/android_platform_services.*`, and only Linux/Web fallback behavior still routes through the retained non-Windows adapter | Keep picker/color text clipboard behavior stable while platform shells are extracted incrementally | `pp_app_core_document_platform_io_tests`; `pano_cli plan-clipboard-write --text #ff00aa`; `ctest --preset desktop-fast --build-config Debug` | Clipboard execution is owned by injected `pp_platform_*` services for every supported platform |
| DEBT-0017 | Open | Modernization | Startup storage path preparation, `App::clipboard_get_text`, `App::clipboard_set_text`, `App::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, `App::hideKeyboard`, `App::display_file`, `App::share_file`, native app/window close, UI-thread lifecycle hooks, render-context acquire/release/present hooks, render-target binding hooks, render platform hint hooks, render debug callback hooks, render-capture frame hooks, recording cleanup, live asset/layout reload policy, diagnostic stacktrace/crash hooks, per-frame platform hooks, `App::pick_image`, `App::pick_file`, the non-writer `App::pick_file_save`, `App::pick_dir`, working-directory picker/display-path policy, canvas input tip/pressure policy, prepared-file save/download handoff, work-directory document export collection policy, app network TLS verification policy, PPBR export data-directory policy, SonarPen availability/startup, and VR mode start/stop now call the SDK-free `pp::platform::PlatformServices` interface. Windows injects `WindowsPlatformServices` from `src/platform_windows/windows_platform_services.*`, Apple now injects `src/platform_apple/apple_platform_services.*`, Android now injects `src/platform_android/android_platform_services.*`, and the retained non-Windows fallback adapter in `src/platform_legacy/legacy_platform_services.*` is narrowed to Linux/Web behavior plus retained no-op branches, including retained iOS canvas tip behavior, retained non-Windows VR unsupported/no-op behavior, and retained macOS PPBR export directory override; `pp_platform_api` also owns the default network TLS policy helper consumed by retained curl sites that cannot yet depend on injected services | Preserve behavior while moving platform execution behind a testable service boundary before platform shell implementations are injected | `pp_platform_api_tests`; `pp_app_core_document_export_tests`; `pp_app_core_document_platform_io_tests`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Replace `src/platform_legacy/legacy_platform_services.*` with injected `pp_platform_*` service implementations owned by each non-Windows platform shell |
| DEBT-0019 | Open | Modernization | Unreferenced-parameter warnings are muted globally through `pp_project_warnings` with MSVC `/wd4100` and Clang/GCC `-Wno-unused-parameter` | Legacy callbacks, virtual hooks, serializer methods, and platform/API compatibility functions carry many intentionally unused parameters during the component split; muting this keeps stricter warning builds focused on higher-signal migration issues | `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset linux-clang --target pp_foundation` | Remove `/wd4100` and `-Wno-unused-parameter`, mark intentionally unused parameters with names/comments or `[[maybe_unused]]`, and make the Windows app plus headless Clang/GCC tests pass without unreferenced-parameter warnings |
| DEBT-0020 | Open | Modernization | Document resize dialog state, selected-resolution planning, and execution dispatch now consume pure `pp_app_core` through `NodeDialogResize`, `App::dialog_resize`, `pano_cli plan-document-resize`, and the `DocumentResizeServices` boundary, and live resize shares `src/legacy_document_canvas_services.*` with canvas clear commands; resize history clearing is an explicit app-core execution output implemented directly by the retained `ActionManager`, but the shared live bridge still calls legacy `Canvas::resize`, updates the legacy app title, and owns the retained `ActionManager` call site | Preserve existing layer/frame GPU resize behavior while the document model and canvas execution boundary are extracted incrementally | `pp_app_core_document_resize_tests`; `pano_cli plan-document-resize --current-resolution 2048 --selected-resolution-index 4`; `ctest --preset desktop-fast --build-config Debug` | Document resize execution is owned by injected document/app services with no legacy resize adapter, title shim, or retained `ActionManager` history clearing |
| DEBT-0021 | Open | Modernization | Layer rename planning/execution dispatch, layer panel operation planning/execution dispatch, layer panel selected-control/visibility view projection, and explicit layer history-intent helpers now consume pure `pp_app_core` through `App::dialog_layer_rename`, `App::init_sidebar` layer callbacks, `NodePanelLayer::update_attributes()`, `pano_cli plan-layer-rename`, `pano_cli plan-layer-operation`, `pano_cli plan-layer-panel-view`, `DocumentLayerRenameServices`, and `DocumentLayerOperationServices`, and the live execution adapters are centralized in `src/legacy_document_layer_services.*`; rename now records undo before applying the new name through separate app-core service calls, but the shared bridge and panel adapter still mutate legacy `Canvas` layer state, `NodeLayer`/`NodePanelLayer`, and retained `ActionManager` undo entries for add/remove/property-change/clear paths | Preserve existing UI/canvas behavior while document layer commands, panel projection, and undo history are extracted incrementally | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-rename --old-name Base --new-name Paint`; `pano_cli plan-layer-operation --kind add --layer-count 2 --index 1 --name Paint`; `pano_cli plan-layer-panel-view --layer-count 3 --current-index 1 --hidden-index 2 --locked-index 1 --current-opacity 0.25 --current-blend-mode 4`; `ctest --preset desktop-fast --build-config Debug` | Layer command execution and panel state projection are owned by the document/app command boundary with legacy `Canvas`/UI nodes acting only as adapters or removed entirely |
| DEBT-0022 | Open | Modernization | Animation panel frame command planning, panel action planning, panel view-model projection, timeline scrub planning, panel-control/timeline execution dispatch, selected-frame click dispatch, playback tick stepping, and play-mode toggles now consume pure `pp_app_core` through `NodePanelAnimation`, `NodeAnimationTimeline`, `pano_cli plan-animation-operation`, `pano_cli plan-animation-panel-action`, `pano_cli plan-animation-panel-view`, `pano_cli plan-animation-timeline-scrub`, and `DocumentAnimationServices`; live execution is centralized in `src/legacy_document_animation_services.*`, but that bridge still mutates or reads legacy `Canvas`/`Layer` frame state, canvas mode, animation-panel timeline/playback fields, and uses a temporary `NodePanelAnimation` friend adapter | Preserve existing animation panel behavior while timeline/frame commands move toward the document/app command boundary | `pp_app_core_document_animation_tests`; `pano_cli plan-animation-operation --kind add --frame-count 2 --current-frame 0`; `pano_cli plan-animation-operation --kind select --frame-count 3 --selected-frame 1 --layer-index 2 --layer-id 42`; `pano_cli plan-animation-operation --kind playback --total-duration 5 --current-frame 4 --offset 1`; `pano_cli plan-animation-operation --kind toggle-playback --playing`; `pano_cli plan-animation-panel-action --action next --total-duration 5 --current-frame 4`; `pano_cli plan-animation-panel-view --layer-count 2 --frame-count 3 --total-duration 6 --current-layer 1 --current-frame 4`; `pano_cli plan-animation-timeline-scrub --total-duration 5 --cursor-x 174.99`; `ctest --preset desktop-fast --build-config Debug` | Animation frame/timeline/playback execution is owned by injected document/app timeline services with no legacy `Canvas`/`Layer`/canvas-mode adapter and UI nodes acting only as adapters or removed entirely |
| DEBT-0023 | Open | Modernization | Brush/color/preset/stroke-settings UI planning, texture-list add/remove/reorder planning, brush preset-list add/select/move/remove/clear planning, stroke-panel slider/toggle/blend/reset planning, stroke-panel view projection, app-level brush refresh view projection, and execution dispatch now consume pure `pp_app_core` through `App::init_sidebar`, `App::brush_update()`, `NodePanelBrush`, `NodePanelBrushPreset`, `NodePanelStroke`, `NodePanelStroke::update_controls()`, restored/docked floating-panel callbacks, `pano_cli plan-brush-operation`, `pano_cli plan-brush-refresh`, `pano_cli plan-brush-texture-list`, `pano_cli plan-brush-preset-list`, `pano_cli plan-brush-stroke-control`, `pano_cli plan-brush-stroke-panel-view`, `BrushUiServices`, `BrushTextureListServices`, `BrushPresetListServices`, and `BrushStrokeControlServices`, and live execution is centralized in `src/legacy_brush_ui_services.*` or narrow legacy service bridges where possible, but preset-list execution still mutates legacy `NodePanelBrushPreset` child nodes directly while the bridge and panel adapter still mutate/read legacy `Brush`/`Canvas::I`, load/save legacy brush texture images, apply retained legacy quick/stroke/color widget writes, own brush thumbnail paths and popup behavior, and use temporary `NodePanelBrush`/`NodePanelBrushPreset` friend adapters to reach private list state | Preserve existing brush UI behavior while brush commands and view projection move toward a brush/app/asset command boundary and asset-managed texture/preset selection | `pp_app_core_brush_ui_tests`; `pano_cli plan-brush-operation --kind color --r 0.25 --g 0.5 --b 0.75 --a 1`; `pano_cli plan-brush-operation --kind pattern --path data/patterns/noise.png --thumb data/patterns/thumbs/noise.png`; `pano_cli plan-brush-refresh --floating-picker --tip-flow 0.8 --tip-size 48 --r 0.2 --g 0.3 --b 0.4 --a 1`; `pano_cli plan-brush-texture-list --kind add --dir brushes --data-path data --source C:/Temp/soft.png`; `pano_cli plan-brush-preset-list --kind remove --item-count 1 --current-index 0`; `pano_cli plan-brush-stroke-control --kind float --setting tip-size --value 42.5`; `pano_cli plan-brush-stroke-control --kind blend --setting pattern --blend-mode 3`; `pano_cli plan-brush-stroke-panel-view --tip-size 64 --jitter-scatter 0.4 --dual-disabled --tip-blend-mode 2 --pattern-blend-mode 5`; `ctest --preset desktop-fast --build-config Debug` | Brush color/texture/preset/stroke-settings, texture-list, preset-list, stroke-control execution, stroke-panel projection, and brush refresh projection are owned by injected brush/app/asset/UI services with no legacy brush/canvas adapter, direct `NodePanelBrushPreset` child mutation, brush thumbnail/popup ownership, legacy quick/stroke/color widget writes, or brush-panel friend access |
| DEBT-0024 | Open | Modernization | Grid/heightmap/lightmap UI planning and execution dispatch now consume pure `pp_app_core` through `NodePanelGrid`, `pano_cli plan-grid-operation`, and the `GridUiServices` boundary; live execution is centralized in `src/legacy_grid_ui_services.*`, and retained CPU lightmap row dispatch now uses shared `parallel_for` instead of platform-specific Win32/Apple worker APIs, but the bridge still performs legacy image loading, OpenGL texture updates, nanort lightmap baking/progress, and `Canvas::draw_objects` commit execution | Preserve grid/lightmap behavior while moving renderable grid commands toward app/renderer/document boundaries | `pp_app_core_grid_ui_tests`; `pano_cli plan-grid-operation --kind render --float32 --texture-resolution 1024 --samples 32`; `ctest --preset desktop-fast --build-config Debug` | Grid heightmap/lightmap execution is owned by app/renderer/document services with `NodePanelGrid` acting only as UI adapter |
| DEBT-0025 | Open | Modernization | Quick brush/color slot, mini-state, and size/flow slider preview planning now consume pure `pp_app_core` through `NodePanelQuick`, `pano_cli plan-quick-operation`, `pano_cli plan-quick-slider-preview`, and the `QuickUiServices` boundary; live slot/popup/restore/reset execution is centralized in `src/legacy_quick_ui_services.*`, and live slider callbacks now consume `pp_app_core` preview cursor/tip planning directly, but the bridge and panel adapter still mutate legacy quick UI widgets, `Brush` previews, color picker popup state, preset popup state, and direct legacy `CanvasModePen`/`CanvasModeLine` fields | Preserve quick-panel behavior while quick brush/color and slider preview commands move toward a brush/app command boundary with safer automation coverage | `pp_app_core_quick_ui_tests`; `pano_cli plan-quick-operation --kind brush --current-index 0 --slot-index 2`; `pano_cli plan-quick-operation --kind restore --brush-index 2 --color-index 1 --fire-event`; `pano_cli plan-quick-slider-preview --slider-x 10 --slider-y 20 --slider-height 40 --zoom 2 --pen-mode --no-line-mode`; `ctest --preset desktop-fast --build-config Debug` | Quick-panel selection, popup, restore, reset, brush preview, color execution, and slider preview mode updates are owned by injected app/brush/UI/canvas services with no legacy quick-panel adapter, popup adapter, direct brush-preview mutation, or direct `CanvasMode*` field writes |
| DEBT-0026 | Open | Modernization | Toolbar history command planning and canvas hotkey history dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `NodeCanvas`, `pano_cli plan-history-operation`, and the `HistoryUiServices` boundary, and both live callers share `src/legacy_history_services.*` for saturated legacy history metrics and execution, but the shared live bridge still mutates legacy `ActionManager` stacks directly | Preserve undo/redo/clear behavior while moving action history toward document/app command services | `pp_app_core_history_ui_tests`; `pano_cli plan-history-operation --kind undo --undo-count 2`; `pano_cli plan-history-operation --kind clear --undo-count 2 --redo-count 1 --memory-bytes 4096`; `ctest --preset desktop-fast --build-config Debug` | Undo/redo/clear execution is owned by injected document/app history services with no legacy `ActionManager` adapter |
| DEBT-0027 | Open | Modernization | Canvas draw-tool toolbar command, full draw-toolbar binding planning, canvas input mode switching, active-state planning/execution dispatch, canvas cursor visibility planning, and canvas keyboard/touch command planning now consume pure `pp_app_core` through `App::init_toolbar_draw`, `App::update`, `NodeCanvas`, `pano_cli plan-canvas-tool`, `pano_cli plan-canvas-tool-toolbar`, `pano_cli plan-canvas-tool-state`, `pano_cli plan-canvas-cursor`, `pano_cli plan-canvas-hotkey`, `CanvasToolServices`, and `CanvasHotkeyServices`, live toolbar/input/hotkey execution is centralized in `src/legacy_canvas_tool_services.*`, `NodeCanvas::update_cursor()` consumes the cursor planner before retained platform cursor dispatch, and canvas mode tip visibility plus pressure remapping now route through `PlatformServices`, but the bridge still mutates or reads legacy `Canvas` mode state, `CanvasModePen` drawing/resizing/picking state, touch-lock state, transform copy/cut action objects, `ActionManager`, legacy save UI, legacy stroke size controls, retained `NodeButton`/`NodeButtonCustom` lookup, and cursor/UI singletons | Preserve current toolbar, stylus eraser, cursor visibility, keyboard, and touch command behavior while canvas input/tools move toward an app/document command boundary | `pp_app_core_canvas_tool_ui_tests`; `pp_app_core_canvas_hotkey_tests`; `pp_platform_api_tests`; `pano_cli plan-canvas-tool --kind copy`; `pano_cli plan-canvas-tool-toolbar`; `pano_cli plan-canvas-tool-state --mode draw --picking --touch-lock`; `pano_cli plan-canvas-cursor --mode draw --visibility small-brush --brush-size 9.5`; `pano_cli plan-canvas-cursor --visibility small-brush --bad-size`; `pano_cli plan-canvas-hotkey --event key-up --key z --ctrl --undo-count 2`; `pano_cli plan-canvas-hotkey --event key-up --key s --ctrl --shift`; `ctest --preset desktop-fast --build-config Debug` | Canvas tool selection, toolbar binding, toolbar state refresh, picking, touch lock, stylus eraser/key mode switching, cursor visibility planning/execution, hotkey/touch command dispatch, save hotkeys, history hotkeys, brush-size hotkeys, and transform action execution are owned by injected app/document/canvas services with no legacy toolbar/canvas adapter |
| DEBT-0028 | Open | Modernization | Canvas clear command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, Layer menu clear, `pano_cli plan-canvas-clear`, and the `DocumentCanvasClearServices` boundary, and toolbar/Layer-menu clear share `src/legacy_document_canvas_services.*`, but the shared live bridge still calls legacy `Canvas::clear`, which records `ActionLayerClear`, clears the current layer/frame, and marks legacy `Canvas::I` unsaved | Preserve clear-current-layer behavior while canvas/document commands move toward document/app command services | `pp_app_core_document_canvas_tests`; `pano_cli plan-canvas-clear --r 0 --g 0.1 --b 0.2 --a 0.3`; `pano_cli plan-canvas-clear --no-canvas`; `pano_cli plan-layer-menu --command clear --current-index 1 --current-name Paint`; `ctest --preset desktop-fast --build-config Debug` | Canvas clear execution, undo recording, dirty-state updates, and clear color handling are owned by injected document/app services with no legacy canvas-clear adapter |
| DEBT-0029 | Open | Modernization | Image import route planning and execution dispatch now consume pure `pp_app_core` through the File menu, `pano_cli plan-image-import`, and the `DocumentImageImportServices` boundary, and live File-menu import execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still loads images with legacy `Image`, calls legacy `Canvas::import_equirectangular`, or configures legacy import transform mode directly | Preserve current File > Import behavior while image import moves toward document/app/asset command services | `pp_app_core_document_import_tests`; `pano_cli plan-image-import --width 4096 --height 2048`; `pano_cli plan-image-import --width 1024 --height 1024`; `ctest --preset desktop-fast --build-config Debug` | Image loading, equirectangular import, transform-placement import, and failure reporting are owned by injected document/app/asset services with File-menu callbacks acting only as adapters and no legacy image-import adapter |
| DEBT-0030 | Open | Modernization | File export menu action planning and execution dispatch now consume pure `pp_app_core` through the File menu, `pano_cli plan-export-menu`, and the `DocumentExportMenuServices` boundary, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still opens legacy export dialogs and then reaches legacy canvas/render/video export code | Preserve current export menu behavior while export command execution moves toward document/app/renderer/video services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind png`; `pano_cli plan-export-menu --kind animation-mp4 --demo`; `pano_cli plan-export-menu --kind layers --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Export menu routing, license gating, target creation, image/layer/cube/depth/animation/timelapse execution, and error reporting are owned by injected document/app/renderer/video services with File-menu callbacks acting only as UI adapters and no legacy export adapter |
| DEBT-0031 | Open | Modernization | Top-level File menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_file`, `pano_cli plan-file-menu`, and the `FileMenuServices` boundary, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still invokes legacy dialogs, platform pickers, cloud code, share code, and canvas import/export paths directly | Preserve File menu behavior while app workflows move toward app/document/platform command services | `pp_app_core_file_menu_tests`; `pano_cli plan-file-menu --command save-as`; `pano_cli plan-file-menu --command import`; `pano_cli plan-file-menu --command cloud-upload`; `ctest --preset desktop-fast --build-config Debug` | File menu routing, picker dispatch, save/share/cloud/resize/export execution, and image/project import execution are owned by injected app/document/platform services with `App::init_menu_file` acting only as a UI adapter and no legacy File menu adapter |
| DEBT-0032 | Open | Modernization | Layer menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_layer`, `pano_cli plan-layer-menu`, and the `DocumentLayerMenuServices` boundary; Layer menu clear reuses the `DocumentCanvasClearServices` executor; and Layer menu rename/clear/merge now share `src/legacy_document_layer_services.*`, but the bridge still calls the legacy rename dialog path, `NodePanelLayer::merge`, and reads `Canvas::I` animation/layer state directly | Preserve existing Layer menu behavior while layer commands move toward document/app services | `pp_app_core_document_layer_tests`; `pano_cli plan-layer-menu --command clear --current-index 1 --current-name Paint`; `pano_cli plan-layer-menu --command merge --current-index 2 --lower-name Paint`; `pano_cli plan-layer-merge --layer-count 3 --from-index 2 --to-index 1`; `pano_cli plan-layer-merge --layer-count 3 --from-index 2 --to-index 1 --animation-duration 3`; `pano_cli plan-layer-menu --command rename --no-current-layer`; `ctest --preset desktop-fast --build-config Debug` | Layer rename, merge-down execution, animation gating, and selected-layer state are owned by injected document/app services with Layer-menu callbacks acting only as UI adapters and no legacy Layer menu adapter |
| DEBT-0033 | Open | Modernization | Tools menu planning and direct command execution dispatch now consume pure `pp_app_core` through `App::init_menu_tools`, `pano_cli plan-tools-menu`, `pano_cli plan-tools-panel`, `pano_cli plan-canvas-camera-reset`, `pano_cli plan-canvas-view-density`, `pano_cli plan-canvas-view-cursor-mode`, and the `ToolsMenuServices` boundary, direct command execution is centralized in `src/legacy_app_shell_services.*`, SonarPen availability/startup now routes through `PlatformServices`, and reset-camera, viewport-density, and cursor-mode execution now share `src/legacy_canvas_view_services.*`, but live adapters still construct legacy `NodePanelFloating` panels, mutate legacy panel nodes, clear `CanvasModeGrid`, open legacy shortcuts UI, mutate retained `Canvas` camera/density/cursor state, write retained `Settings`, and rely on the legacy platform adapter for the retained iOS SonarPen bridge | Preserve current Tools menu and canvas-view behavior while UI shell actions move toward app/UI/platform/canvas services | `pp_app_core_tools_menu_tests`; `pp_app_core_canvas_view_tests`; `pp_platform_api_tests`; `pano_cli plan-tools-menu --command shortcuts`; `pano_cli plan-tools-panel --panel layers`; `pano_cli plan-tools-panel --panel animation --already-visible`; `pano_cli plan-canvas-camera-reset`; `pano_cli plan-canvas-view-density --density 1.5`; `pano_cli plan-canvas-view-cursor-mode --mode 3`; `ctest --preset desktop-fast --build-config Debug` | Tools panel creation, submenu routing, grid clear, camera reset, viewport density, cursor mode, shortcuts dialog, and SonarPen dispatch are owned by injected app/UI/platform/canvas services with `App::init_menu_tools` and options callbacks acting only as UI adapters and no legacy Tools/canvas-view adapter |
| DEBT-0034 | Open | Modernization | About menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_about`, `pano_cli plan-about-menu`, and the `AboutMenuServices` boundary, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still opens legacy About/manual/what's-new dialogs, invokes the injected crash hook, and runs the legacy Canvas stroke performance test directly | Preserve About menu behavior while dialogs and diagnostics move toward app/UI/platform services | `pp_app_core_about_menu_tests`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-about-menu --command performance --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | About/manual/what's-new dialog dispatch, crash-test dispatch, and performance-test execution are owned by injected app/UI/platform services with `App::init_menu_about` acting only as a UI adapter and no legacy About adapter |
| DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary; toolbar test-message dialog metadata now lives in `pp_app_core` through `plan_main_toolbar_message_dialog`, retained message-box creation and settings-dialog opening now route through `src/legacy_ui_overlay_services.*`, history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still opens legacy open/save flows, stores raw compatibility pointers, and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command message-box`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter |
| DEBT-0036 | Open | Modernization | `pp_renderer_api`, `pp_paint_renderer`, `pano_cli plan-paint-feedback`, and `pano_cli plan-stroke-composite` can choose backend-neutral complex paint feedback strategies for fixed-function blending, framebuffer-fetch-capable renderers, or ping-pong render targets. OpenGL extension detection now stores `pp::renderer::RenderDeviceFeatures` through `ShaderManager`, using `pp_renderer_gl::query_opengl_capability_detection`, `detect_opengl_feature_state`, and `render_device_features` as the backend conversion point; that feature snapshot now includes float32-linear filtering, so canvas stroke texture format selection, renderer diagnostics, grid lightmap render planning, and grid bake target selection no longer read `ShaderManager::ext_*` flags directly. `pp_paint_renderer::plan_canvas_blend_gate` owns the compatibility mapping from persisted layer/brush blend indices to the extracted stroke-composite planner, and live `Canvas::draw_merge` plus `NodeCanvas` panorama rendering both call it with the stored renderer-neutral feature set for their existing shader-blend gates and destination-copy versus framebuffer-fetch decisions. `pp_paint_renderer::plan_canvas_stroke_feedback` also owns the current destination-feedback decision, and live `Canvas::stroke_draw`, thumbnail layer blending, and `NodeStrokePreview` brush-preview rendering use it for framebuffer-fetch versus destination-copy decisions. Pure document-frame compositing, renderer-neutral six-face texture upload, recorded upload-command reporting, pure six-face PNG encoding, pure equirectangular PNG/JPEG+XMP encoding, and pure layer/animation-frame collection PNG encoding are now covered by `simulate-document-render`, payload-complete `plan-canvas-document-snapshot` automation, and the shared `pp_paint_renderer::prepare_document_frame_export_readiness` helper consumed by live export before retained writer fallback. The retained `copy_framebuffer_to_texture_2d` utility bridge now routes 2D framebuffer-to-texture copies through tested `pp_renderer_gl` dispatch, retained `RTT::create`/`RTT::destroy` render-target texture parameter setup, optional depth renderbuffer allocation, framebuffer allocation/attachment/status checks, binding restore, and resource deletion now route through tested `pp_renderer_gl` dispatch, retained RTT clear, masked clear with color-write-mask restore, texture bind/unbind, and RGBA8 dirty-region texture writes now route through tested `pp_renderer_gl` dispatch, retained Canvas, NodeCanvas, and NodeStrokePreview texture-unit switches now route through tested active-texture dispatch, retained Canvas, NodeCanvas, NodeStrokePreview, and desktop HMD viewport/scissor/capability execution now route through tested `pp_renderer_gl` dispatch adapters, retained NodeCanvas, CanvasMode, and NodePanelGrid capability-state snapshots now route through tested `pp_renderer_gl` query dispatch, CanvasLayer cube/equirect generation plus frame clears now share `legacy_ui_gl_dispatch` for active-texture selection, cube texture binding, viewport execution, blend capability execution, clear-color query/restore, and color-buffer clear adapter endpoints backed by tested `pp_renderer_gl` dispatch helpers, while the cube-face framebuffer-to-texture copy now uses the shared retained `copy_framebuffer_to_texture_target` utility bridge backed by tested `pp_renderer_gl` dispatch and remains tracked by DEBT-0036 until renderer services own copy execution, `NodePanelGrid` live heightmap draw and bake setup now route depth/blend state, depth clears, color-write-mask toggles, active texture selection, bake viewport execution, sun-overlay viewport query, and desktop texture-resize readback through tested `pp_renderer_gl` dispatch adapters; its active-texture, depth/blend capability query/apply, viewport query/execution, depth-clear, and color-write-mask adapter endpoints now share `legacy_ui_gl_dispatch`, retained CanvasMode overlay/mask/transform paths now route active texture, depth/blend state, transform/cut viewport execution, paint-mode blend/depth state snapshots, and canvas-tip pick framebuffer readback through tested `pp_renderer_gl` dispatch adapters; their active-texture, capability query/apply, viewport, read-framebuffer query, and RGBA8 pixel-readback adapter endpoints now share `legacy_ui_gl_dispatch`, retained simple UI draw paths now share `legacy_ui_gl_dispatch` for blend-state execution, fallback 2D texture unbinds, `NodeViewport` viewport query/restore, color-buffer clears, and clear-color restore, retained `NodeCanvas` plus `NodeStrokePreview` draw-state paths now share `legacy_ui_gl_dispatch` for active-texture selection, fallback texture unbind, viewport/scissor execution, viewport and clear-color queries, color-buffer clears, clear-color restore, and capability query/apply adapter endpoints backed by tested `pp_renderer_gl` dispatch helpers, retained `Canvas` stroke/object/thumbnail/export/frame-clear state endpoints now share `legacy_ui_gl_dispatch` for active-texture selection, fallback texture unbind, viewport/scissor execution, viewport and clear-color query, clear-color restore, and capability query/apply adapter endpoints, retained Canvas and RTT depth renderbuffer allocation/attachment/delete now share `legacy_gl_renderbuffer_dispatch` while renderer-resource ownership remains retained, retained `Texture2D`, `TextureCube`, and RTT texture allocation/delete/bind/parameter/update/mipmap dispatch now share `legacy_gl_texture_dispatch` while renderer-resource ownership remains retained, retained `Texture2D` readback plus RTT framebuffer allocation/delete/bind/restore/blit/readback and PBO readback dispatch now share `legacy_gl_framebuffer_dispatch` while renderer-resource ownership remains retained, retained Sampler create/parameter/border/bind dispatch shares `legacy_gl_sampler_dispatch` and retained PBO allocation/readback/map/unmap/delete dispatch shares `legacy_gl_pixel_buffer_dispatch` while renderer-resource ownership remains retained, retained `Shape`, `TextMesh`, and `NodeColorWheel` mesh buffer/VAO creation/upload/draw/delete dispatch now share `legacy_gl_mesh_dispatch` while renderer-resource ownership remains retained, retained shader source compilation/deletion, program attach/link/use/delete, attribute rebinding/location lookup, active-uniform enumeration, uniform-location discovery, and uniform writes now share `legacy_gl_shader_dispatch` while shader-program ownership remains retained, retained `gl_state` save/restore and `copy_framebuffer_to_texture_target` now reuse shared retained UI/framebuffer/shader/sampler bridge callbacks while renderer state and framebuffer-copy execution remain retained, retained app startup, app clear, app UI viewport/scissor, command-convert renderer state, and desktop VR draw-state endpoints now share `legacy_ui_gl_dispatch` while app/VR renderer execution remains retained, retained RTT clear and masked-clear endpoints now share `legacy_ui_gl_dispatch` while RTT render-target execution remains retained, retained app startup logging, Windows early context logging/window-title detection, and shader capability detection now share `legacy_gl_runtime_dispatch` while runtime/capability probing remains retained, and retained `CanvasLayer` frame-clear draw-state paths route saved clear-color query and restore through the same tested helpers, but actual live stroke rasterization, dual-brush compositing, pattern feedback math, thumbnail layer compositing, brush-preview compositing, grid lightmap/heightmap mesh rendering, Web/incomplete-readback collection handoff, depth export writer execution, progress/threading parity, exact GPU filtering parity, and the retained `ShaderManager::ext_*` compatibility fields still use legacy OpenGL canvas/UI execution | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_renderer_gl_capabilities_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `pano_cli plan-canvas-document-snapshot --captured-face-payloads-per-layer 1`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Live stroke/layer/export compositing chooses its feedback path through `pp_paint_renderer` and renderer services, with OpenGL golden parity and Vulkan/Metal lab tests covering framebuffer-fetch and ping-pong behavior |
| DEBT-0037 | Open | Modernization | Recording lifecycle/export planning, export progress-dialog metadata, and execution dispatch now consume pure `pp_app_core` through `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, `pano_cli plan-recording-session`, and the `RecordingServices` boundary; live execution is centralized in `src/legacy_recording_services.*`, retained MP4 export progress bar creation routes through `src/legacy_app_dialog_services.*`, retained `PBO` allocation/readback/map/unmap/delete operations now route through tested `pp_renderer_gl` dispatch, and the retained recording worker is now owned as `std::jthread` through `App::rec_thread` with explicit stop requests before notify/join, but the bridge still owns retained recording loop control, platform recorded-file cleanup, progress lifetime, `App::rec_loop` readback call sites, and `MP4Encoder::write_mp4` execution | Preserve current timelapse/MP4 behavior while recording moves toward app/document/renderer/video services | `pp_app_core_document_recording_tests`; `pp_renderer_gl_capabilities_tests`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-recording-session --platform-deletes-recorded-files`; `ctest --preset desktop-fast --build-config Debug` | Recording thread lifecycle, frame readback scheduling, platform cleanup, progress reporting, and MP4 writing are owned by injected app/renderer/video services with `App` methods acting only as adapters |
| DEBT-0038 | Open | Modernization | Cloud upload/browse/bulk planning, cloud prompt/progress metadata, and execution dispatch now consume pure `pp_app_core` through `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-upload-all`, `pano_cli plan-cloud-browse`, `pano_cli plan-cloud-transfer`, and the `CloudServices` boundary; live execution is centralized in `src/legacy_cloud_services.*`, cloud-browser dialog opening now routes through `src/legacy_ui_overlay_services.*`, retained upload/download CURL setup, progress callback dispatch, TLS-verification bypass policy consumption, upload form construction, and upload post-transfer response/error handling now also live behind dedicated helpers in `src/legacy_cloud_services.*` instead of `App::upload`/`App::download` or inline transfer setup, retained save-before-upload execution now routes through `src/legacy_document_session_services.*` instead of `src/legacy_cloud_services.cpp` calling `Canvas::I->project_save_thread(...)` directly, retained downloaded-project post-open reconciliation now routes through `src/legacy_document_open_services.*` instead of living inline in `src/legacy_cloud_services.cpp`, upload warning/publish/success prompts, upload/bulk progress dialog titles, download-progress prompt metadata, and formatted download progress text come from tested app-core plans, retained cloud prompt/progress creation routes through `src/legacy_app_dialog_services.*` via `App::message_box`/`App::show_progress` or direct bridge calls, and retained `Asset::open_url`, `LogRemote::net_init`, and `NodeDialogCloud::load_thumbs_thread` curl sites consume the `pp_platform_api` default TLS policy helper instead of spelling Android branches locally; the initial cloud-browser loading placeholder setup, file-list request/response handling, and slot creation/selection wiring in `NodeDialogCloud::load_thumbs_thread()` now also route through focused helpers in `src/node_dialog_cloud.*`, but the bridge still uses retained prompt/progress lifetime, OpenGL context guarding, `NodeDialogCloud` thumbnail execution, and transfer-thread execution while downloaded-project open, layer refresh, and action-history reset remain retained in the document-open bridge | Preserve current cloud behavior while cloud/network/document import flows move toward app/document/platform services | `pp_app_core_document_cloud_tests`; `pp_platform_api_tests`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `pano_cli plan-cloud-transfer --direction download --progress --disable-tls-verification`; `ctest --preset desktop-fast --build-config Debug` | Cloud upload/download, TLS policy, progress reporting, cloud browse dialog, downloaded project opening, layer refresh, OpenGL context ownership, and action-history reset are owned by injected app/document/network/platform/renderer services with `App` methods acting only as adapters |
| DEBT-0039 | Open | Modernization | Document-open planning and execution dispatch now consume pure `pp_app_core` through `App::open_document`, `pano_cli plan-open-route`, `DocumentOpenServices`, and `src/legacy_document_open_services.*`; document-open history clearing is now an explicit app-core output, but the bridge still opens ABR/PPBR import prompts before delegating import execution to `src/legacy_brush_package_import_services.*`, applies unsaved-project discard prompts, calls legacy project-open execution, refreshes layer UI, updates the app title, and executes retained history services locally | Preserve current file-open/import behavior while document loading and brush import move toward app/document/asset/UI services | `pp_app_core_document_route_tests`; `pp_app_core_document_session_tests`; `pano_cli plan-open-route --path D:/Paint/Scenes/demo.ppi --unsaved`; `pano_cli plan-open-route --path D:/Paint/Brushes/clouds.ABR --unsaved`; `ctest --preset desktop-fast --build-config Debug` | Brush import prompting, project-open execution, unsaved-project discard prompting, layer refresh, title updates, and history clearing are owned by injected app/document/asset/UI services with `App::open_document` acting only as an adapter |
| DEBT-0040 | Open | Modernization | Close request, document save, save-before-workflow planning/execution dispatch, close/save-before/save-error prompt metadata, and close/save/save-before history effects now consume pure `pp_app_core` through `App::request_close`, `App::save_document`, `App::continue_document_workflow_after_optional_save`, `pano_cli simulate-app-session`, `pano_cli plan-document-session-prompt`, `DocumentSaveServices`, `CloseRequestServices`, `DocumentWorkflowServices`, and `src/legacy_document_session_services.*`; close/save-before prompt creation now uses `src/legacy_app_dialog_services.*`, Save dialog working-directory picker visibility/path formatting now dispatches through `PlatformServices`, and existing-project save/save-before-workflow execution prepares a payload-bearing canvas snapshot report before retained saving, but the bridge still opens retained save dialogs, wires prompt callbacks directly, delegates actual writing to `Canvas::project_save`, mutates the unsaved flag on close confirmation, invokes native app close, and routes save-version through the retained legacy dialog | Preserve current close/save/dirty-workflow behavior while document session execution moves toward app/document/UI/platform services | `pp_app_core_document_session_tests`; `pp_platform_api_tests`; `pano_cli plan-document-session-prompt --kind close-unsaved`; `pano_cli plan-document-session-prompt --kind save-before-workflow`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `ctest --preset desktop-fast --build-config Debug` | Close prompt execution, native close requests, dirty-workflow save prompts, existing-project saves, save dialogs, save-version execution, and unsaved-flag mutation are owned by injected app/document/UI/platform services with `App` methods acting only as adapters |
| DEBT-0041 | Open | Modernization | Accepted new-document planning/execution dispatch and new-document overwrite prompt metadata now consume pure `pp_app_core` through `App::dialog_newdoc`, `pano_cli plan-new-document`, `pano_cli plan-document-session-prompt`, `NewDocumentServices`, and `src/legacy_document_session_services.*`; new-document overwrite prompt creation now uses `src/legacy_app_dialog_services.*`, and New Document dialog working-directory picker visibility/path formatting now dispatches through `PlatformServices`, but the bridge still mutates legacy app document fields, clears legacy layer UI, resizes legacy `Canvas`, clears legacy history, creates the default layer through legacy UI, mutates unsaved/new-document flags, updates the title, wires overwrite callbacks directly, and handles keyboard/dialog cleanup directly | Preserve current New Document dialog behavior while document creation moves toward app/document/UI services | `pp_app_core_document_session_tests`; `pp_platform_api_tests`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli plan-document-session-prompt --kind new-document-overwrite`; `pano_cli simulate-app-session --save-intent save`; `ctest --preset desktop-fast --build-config Debug` | New document creation, overwrite confirmation, canvas/document allocation, default layer creation, history clearing, title updates, dirty/new-document state, and keyboard/dialog cleanup are owned by injected app/document/UI services with `App::dialog_newdoc` acting only as a UI adapter |
| DEBT-0042 | Open | Modernization | Accepted Save As and Save Version planning/execution dispatch, Save As overwrite prompt metadata, and Save As/Save Version history effects now consume pure `pp_app_core` through `App::dialog_save`, `App::dialog_save_ver`, `pano_cli plan-document-file`, `pano_cli plan-document-version`, `pano_cli plan-document-session-prompt`, `DocumentFileSaveServices`, `DocumentVersionSaveServices`, and `src/legacy_document_session_services.*`; Save As overwrite prompt creation now uses `src/legacy_app_dialog_services.*`, and accepted Save As/Save Version execution prepares a payload-bearing canvas snapshot report before retained saving, but the bridge still wires overwrite callbacks directly, delegates actual writing to legacy `Canvas::project_save`, mutates app document name/path/directory fields, marks version saves dirty before saving, updates the title, and handles keyboard/dialog cleanup directly | Preserve current Save As and Save Version behavior while document persistence moves toward app/document/storage/UI services | `pp_app_core_document_session_tests`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-session-prompt --kind file-overwrite --name demo`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `pano_cli simulate-app-session --save-intent save-as`; `pano_cli simulate-app-session --save-intent save-version`; `ctest --preset desktop-fast --build-config Debug` | Save As overwrite prompting, project-save execution, app document metadata updates, title updates, version-save dirty-state handling, and keyboard/dialog cleanup are owned by injected app/document/storage/UI services with `App::dialog_save` and `App::dialog_save_ver` acting only as UI adapters |
| DEBT-0043 | Open | Modernization | Equirectangular, layer, animation-frame, depth, and cube-face export planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_export`, `App::dialog_export_layers`, `App::dialog_export_anim_frames`, `App::dialog_export_depth`, `App::dialog_export_cube_faces`, `pano_cli plan-export-*`, `DocumentExportServices`, and `src/legacy_document_export_services.*`; layer/frame dialogs also consume `plan_document_export_collection_target` plus `PlatformServices::uses_work_directory_document_export_collections()` instead of spelling local iOS branches, export success/failure/license dialog metadata plus execution log labels now come from `pp_app_core`, equirectangular/layer/animation-frame/depth/cube-face execution prepares a payload-bearing document snapshot plus the shared `pp_paint_renderer::prepare_document_frame_export_readiness` report, document-snapshot writer-versus-retained fallback routing now comes from tested `pp_app_core` policy including current-platform support consumed by the live bridge, depth export target naming and two-payload write order are covered by tested `pp_app_core` helpers, cube-face export writes the pure face PNG bytes to `pp_app_core` planned work-directory face paths through `execute_document_cube_face_export_write` before falling back to retained Canvas execution on failure, PNG/JPEG equirectangular export writes the pure `pp_paint_renderer` equirectangular payload through `execute_document_export_file_write` before retained fallback, and payload-complete layer/animation-frame collections write pure `pp_paint_renderer` PNG sequences through `execute_document_export_collection_write` before retained fallback, but the bridge still adapts retained filesystem writes/exported-image publishing locally, still calls legacy `Canvas` export methods for Web/incomplete-readback collection exports and depth rendering, creates export directories, handles picker-selected stems, performs Web prepared-file handoff directly, and leaves depth render/readback plus the legacy `.png`/JPEG payload mismatch on the retained path | Preserve current image/collection/depth/cube export behavior while export execution moves toward document/renderer/platform/storage services | `pp_app_core_document_export_tests`; `pp_platform_api_tests`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind layers`; `pano_cli plan-export-target --kind collection --work-dir D:/Paint --doc-name demo --suffix _layers`; `pano_cli plan-export-target --kind cube-faces --work-dir D:/Paint --doc-name demo`; `pano_cli plan-export-message --kind equirectangular --destination work --detail D:/Paint`; `pano_cli plan-export-report --kind license-disabled`; `pano_cli plan-export-snapshot-route --kind layers-collection --captured-face-payloads 3 --pending-face-payloads 6`; `pano_cli simulate-document-export`; `ctest --preset desktop-fast --build-config Debug` | File, collection, stem, depth, and remaining retained export execution, export-directory creation, Web file handoff, picker-selected stem handling, and legacy canvas export calls are owned by injected document/renderer/platform/storage services with export dialogs acting only as UI adapters |
| DEBT-0044 | Open | Modernization | Timelapse and animation MP4 export execution dispatch now consumes pure `pp_app_core` through `App::dialog_timelapse_export`, `App::dialog_export_mp4`, `pano_cli plan-export-menu`, `pano_cli plan-export-target --kind name`, `pano_cli plan-export-message`, `pano_cli plan-export-report`, `DocumentVideoExportServices`, and `src/legacy_document_export_services.*`, and success/failure/license dialog metadata plus execution log labels now come from `pp_app_core`; the asynchronous timelapse path now uses a service-owned `std::jthread` queue with UI-thread success-dialog handoff instead of a detached worker, but the bridge still calls `App::rec_export`, calls `Canvas::export_anim_mp4`, and owns mobile/Web save callbacks | Preserve current MP4/timelapse export behavior while video export moves toward app/document/renderer/video/platform/storage services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind animation-mp4`; `pano_cli plan-export-menu --kind timelapse`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -animation`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -timelapse`; `pano_cli plan-export-message --kind timelapse --destination success`; `pano_cli plan-export-report --kind animation-mp4 --message "video export path must not be empty"`; `ctest --preset desktop-fast --build-config Debug` | Timelapse and animation MP4 execution, desktop worker threading, frame readback/video encoding handoff, and mobile/Web save callbacks are owned by injected app/document/renderer/video/platform/storage services with export dialogs acting only as UI adapters |
| DEBT-0045 | Open | Modernization | Options-menu preference execution now consumes pure `pp_app_core` through UI scale, viewport scale, RTL direction, VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks plus `AppPreferenceServices` and `src/legacy_app_preference_services.*`; viewport-density and cursor-mode execution now delegate to `src/legacy_canvas_view_services.*`, and retained preference reads/writes for UI scale, UI-state/RTL, whats-new dialog state, viewport density, cursor mode, VR controllers, and auto-timelapse now route through `src/legacy_preference_storage.*` snapshots/helpers without direct `settings.h` includes or retained preference keys in the UI/dialog/canvas call sites, but the bridges still call legacy `App::set_ui_scale`, `App::set_ui_rtl`, `App::rec_start`, `App::rec_stop`, retained canvas view mutation, and retained `Settings` storage through that adapter; VR mode callbacks now call `App` VR wrappers that dispatch to `PlatformServices`, whose desktop runtime policy prefers OpenXR while the actual Windows OpenVR SDK bridge still lives in `WindowsPlatformServices` under DEBT-0061 | Preserve current options-menu behavior while preferences move toward app/UI/platform/storage services | `pp_app_core_app_preferences_tests`; `pp_app_core_canvas_view_tests`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `pano_cli plan-canvas-view-density --density 1.5`; `pano_cli plan-canvas-view-cursor-mode --mode 3`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Preference persistence, UI/layout direction, viewport density, cursor mode, VR mode start/stop/failure handling, VR-controller state, and auto-timelapse recording side effects are owned by injected app/UI/platform/storage services with options-menu callbacks acting only as UI adapters |
| DEBT-0046 | Open | Modernization | Startup preference/runtime execution and startup resource sequencing now consume pure `pp_app_core` through `App::init`, `pano_cli plan-app-startup`, `pano_cli plan-app-startup-resources`, `AppStartupServices`, `AppStartupResourceServices`, and `src/legacy_app_startup_services.*`, and startup preference load/read/write now routes through `src/legacy_preference_storage.*` with retained startup keys hidden behind `LegacyStartupPreferenceSnapshot`, but the bridge still calls legacy `Settings` storage through that adapter, `App::rec_start`, app VR-controller state mutation, message-box license warning execution, shader loading, asset initialization, layout creation, title updates, and UI render-target creation directly | Preserve current startup behavior while app startup moves toward app/preferences/storage/recording/UI/renderer services | `pp_app_core_app_startup_tests`; `pano_cli plan-app-startup --run-counter 7 --vr-controllers-disabled --license-invalid`; `pano_cli plan-app-startup --run-counter -1`; `pano_cli plan-app-startup-resources --width 1280 --height 720`; `pano_cli plan-app-startup-resources --bad-size`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Startup preference persistence, auto-timelapse startup, stored VR-controller state, license validation/warning, startup resource initialization, title updates, and UI render-target allocation are owned by injected app/preferences/storage/recording/UI/renderer services with `App::init` acting only as orchestration |
| DEBT-0047 | Open | Modernization | PPBR brush package export request validation, success-dialog metadata, and execution dispatch now consume pure `pp_app_core` through `App::dialog_ppbr_export`, `pano_cli plan-brush-package-export`, `BrushPackageExportServices`, and `src/legacy_brush_package_export_services.*`; PPBR header/path planning now consumes `pp_assets::brush_package`, the macOS data-directory override now routes through `PlatformServices`, and the desktop async path now uses a service-owned `std::jthread` worker with UI-thread dialog close/message handoff, but the bridge still reads `NodeDialogExportPPBR`, carries the legacy `Image` header object outside the pure request, converts to `NodePanelBrushPreset::PPBRInfo`, calls `NodePanelBrushPreset::export_ppbr`, and handles mobile/Web completion directly | Preserve current PPBR export behavior while brush assets, PPBR serialization, picker completion, and UI lifetime move toward asset/storage/UI/platform services | `pp_assets_brush_package_tests`; `pp_app_core_brush_package_export_tests`; `pp_platform_api_tests`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr --author Artist --dest-path D:/Paint/BrushPreviews --export-data --header-image`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr`; `pano_cli plan-brush-package-export`; `pano_cli plan-brush-package-export --path clouds`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr --dest-path D:/Paint/BrushPreviews --no-export-data`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | PPBR metadata collection, header-image ownership, serialization, picker-selected path execution, desktop threading, dialog lifetime, and mobile/Web completion are owned by injected brush asset/storage/UI/platform services with `App::dialog_ppbr_export` acting only as a UI adapter |
| DEBT-0048 | Open | Modernization | ABR/PPBR brush package import execution now consumes pure `pp_app_core` through document-open confirmation callbacks, `pano_cli plan-brush-package-import`, `BrushPackageImportServices`, and `src/legacy_brush_package_import_services.*`; imported brush tip/pattern target paths now consume `pp_assets::brush_package`, and the retained bridge now uses a service-owned `std::jthread` worker with UI-thread completion handoff instead of detached `NodePanelBrushPreset::import_abr`/`import_ppbr` launches, but it still depends on the legacy preset panel as the importer/storage owner | Preserve current brush import behavior while brush package parsing, preset storage, progress/error reporting, and UI refresh move toward asset/paint/UI services | `pp_assets_brush_package_tests`; `pp_app_core_brush_package_import_tests`; `pano_cli plan-brush-package-import --kind ppbr --path D:/Paint/Brushes/clouds.ppbr`; `pano_cli plan-brush-package-import --kind abr --path D:/Paint/Brushes/clouds.abr`; `pano_cli plan-brush-package-import --kind ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | ABR/PPBR parsing, preset creation/storage, import threading/progress, duplicate asset policy, and UI refresh are owned by injected brush asset/paint/UI services with document-open callbacks only confirming user intent |
| DEBT-0049 | Open | Modernization | `pp_assets::validate_ppbr_header` intentionally preserves the legacy PPBR version check from `NodePanelBrushPreset::import_ppbr`, which accepts files when either major is `0` or minor is `1` instead of requiring exactly version `0.1` | Avoid rejecting existing brush packages before compatibility fixtures prove the stricter rule is safe | `pp_assets_brush_package_tests`; `pano_cli plan-brush-package-export --path D:/Paint/clouds.ppbr`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Add PPBR compatibility fixtures for accepted/rejected historical package versions, then require canonical `0.1` or an explicit supported-version matrix and update live import accordingly |
| DEBT-0050 | Open | Modernization | iOS exported-image photo-library publishing and WebGL persistent-storage flushing now dispatch through platform service boundaries; the iOS/Web policy decision lives in tested `pp_platform_api::platform_policy`, iOS execution now lives in injected `src/platform_apple/apple_platform_services.*`, and WebGL flushing now goes through injectable `pp::platform::WebPlatformServices`, but retained Web execution still forwards to `webgl_sync` bridges | Preserve current iOS/Web export and save behavior while the Apple/Web platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; platform package smoke once Apple/Web root builds exist | Exported-image publishing and persistent-storage flushing are owned by injected Apple/Web `pp_platform_*` services with no legacy adapter branch |
| DEBT-0051 | Open | Modernization | Document browser search roots, Apple file/image/save/directory picker dispatch, Browse dialog working-directory picker visibility/path formatting, iOS Inbox roots, macOS empty-selection filtering, and macOS display-path formatting now dispatch through the tested `src/platform_apple/apple_platform_services.*` boundary consumed by `PlatformServices`; retained `src/platform_legacy/legacy_platform_services.*` still owns other non-Apple fallback behavior | Preserve current iOS document import/browse and desktop browse picker behavior while Apple platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Apple package smoke once root Apple builds exist | Document browse roots and browse-directory picker/display formatting are owned by injected Apple and desktop `pp_platform_*` services with no legacy adapter branch |
| DEBT-0052 | Open | Modernization | Native UI/window state saving now dispatches through `PlatformServices`; Windows/macOS save policy lives in tested `pp_platform_api::platform_policy`, macOS execution now lives in injected `src/platform_apple/apple_platform_services.*`, and Windows placement reads/writes now use `LegacyWindowPreferenceSnapshot` plus `src/legacy_preference_storage.*`, but Windows still stores placement through retained `Settings` behind the adapter | Preserve current Windows/macOS UI persistence while platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Windows app build; Apple package smoke once root Apple builds exist | UI/window state persistence is owned by injected platform services with no legacy adapter branch |
| DEBT-0053 | Open | Modernization | Prepared-file writable target selection and prepared-file export-dialog policy now dispatch through platform service boundaries; iOS temporary-file policy plus prepared-file save handoff now live in injected `src/platform_apple/apple_platform_services.*`, WebGL data-path planning lives in tested `pp_platform_api::platform_policy`, and WebGL prepared-file handoff now goes through injectable `pp::platform::WebPlatformServices`, but retained Web save/download handoff execution still lives in fallback adapters | Preserve mobile/Web export handoff behavior while platform shells are extracted incrementally | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug`; Windows app build; Apple/Web package smoke once root package builds exist | Prepared-file target selection, export-dialog policy, and save/download handoff are owned by injected platform services with no legacy adapter branch |
| DEBT-0054 | Open | Modernization | Layout XML file read/reload decisions now consume `pp_platform_api::plan_asset_file_load`; platform-family reload behavior lives in tested `pp_platform_api::platform_policy` and pure probed planning, but the live wrapper still performs direct `stat` probing for Windows/macOS mtime reload checks until platform storage/file-watch services exist | Preserve current layout hot-reload and mobile/Web single-load behavior while removing platform guards from the shared `LayoutManager` parser | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests`; Windows app build | Layout reload decisions are owned by injected platform storage/file-watch services or an asset manager boundary with platform-specific file watching removed from compile-time helpers |
| DEBT-0055 | Open | Modernization | `src/app.h` now forward-declares retained iOS/macOS/Android/Linux/Web platform handles instead of including platform SDK headers, and full SDK includes are isolated in `src/platform_legacy/legacy_platform_services.cpp`, but the `App` singleton still stores those platform handles directly | Reduce central header platform coupling incrementally without rewriting non-Windows platform entrypoints before Phase 6 | Windows app build; Apple/Android/Linux/Web package smoke once platform root builds are active | Platform handles are owned by injected `pp_platform_*` shell state or services, and `App` has no platform SDK handle fields or platform conditional members |
| DEBT-0056 | Open | Modernization | `src/asset.h` is now Android-SDK-free and uses opaque Android asset handles behind `Asset::set_android_asset_manager`, but retained `Asset` still owns a static Android asset-manager bridge and `src/asset.cpp` still performs Android `AAssetManager` reads directly; the current `android-arm64` root preset is headless and does not expose `pp_legacy_assets_io`, though the retained Android standard package `native-lib` now builds through its refreshed C++23 CMake path | Reduce legacy asset I/O header coupling without rewriting Android asset loading before the asset manager/storage boundary exists | Windows app build; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64 -Targets pp_assets`; `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard` | Android asset loading is owned by injected asset storage/platform services or `pp_assets` file providers, with no static Android asset manager on `Asset` |
| DEBT-0061 | Open | Modernization | Desktop XR runtime selection now lives in tested `pp_platform_api` policy and prefers OpenXR, but `WindowsPlatformServices` still reports OpenXR unavailable and reaches the retained OpenVR SDK bridge as a legacy fallback; Windows runtime deployment copies `openvr_api.dll` beside `PanoPainter.exe` until that fallback is removed | Preserve current desktop VR behavior while replacing OpenVR with OpenXR behind the platform/renderer boundary | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Add an OpenXR SDK/package target, implement desktop OpenXR startup/shutdown/pose/controller submission behind `pp_platform_vr` or `PlatformServices`, validate parity with mocked/runtime smoke coverage, and remove `libs/openvr` plus the OpenVR link/include paths from root CMake |
| DEBT-0063 | Open | Modernization | `pp_ui_core` now owns tested `NodeLifetimeTree` and `UiOverlayLifetime` models for checked handles, scoped callback connections, subtree destruction, capture release, and mutation-safe dispatch. `src/legacy_ui_overlay_services.*` now keeps a root-scoped checked-overlay registry for retained nodes; app-owned progress/message/input dialogs created through that service now also open through checked overlay handles when the main layout anchor is available; the main-toolbar settings dialog and cloud browser dialog now open through that same checked-overlay seam; and `src/node_panel_layer.h/.cpp`, `src/node_combobox.cpp`, `src/node_dialog_open.cpp`, `src/node_dialog_browse.cpp`, `src/node_panel_stroke.cpp`, `src/node_panel_brush.cpp`, `src/node_panel_color.cpp`, `src/node_panel_grid.cpp`, `src/node_dialog_picker.cpp`, `src/legacy_quick_ui_services.cpp`, and `src/node_popup_menu.h/.cpp` now route popup/dialog/menu lifetime through checked overlay handles or handle-based close instead of raw attach-and-destroy callbacks. Layer-panel selection/order state also stays on shared ownership rather than raw child pointers while mutation-safe close remains open in other legacy panel/dialog families | Preserve current UI behavior while completing safe panel/dialog lifetime migration incrementally | `pp_ui_core_layout_xml_tests`; `pp_ui_core_node_lifetime_tests`; `pp_ui_core_overlay_lifetime_tests`; `tests/ui_core/node_lifetime_tests.cpp:destroy_subtree_clears_child_connections`; `tests/ui_core/overlay_lifetime_tests.cpp:double_close_overlay_returns_invalid_argument`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target panopainter_app pp_ui_core_node_lifetime_tests pp_ui_core_overlay_lifetime_tests` | Remaining legacy popup/dialog families still use non-handle ownership and open lifetimes; migration stays open until their surfaces are converted, including lifecycle safety parity checks |
| DEBT-0057 | Open | Modernization | Default canvas allocation size now dispatches through `PlatformServices::default_canvas_resolution`, removing the `CANVAS_RES` platform macro from `src/canvas.h`; WebGL's retained 512 default now lives in tested `pp_platform_api` policy behind injectable `pp::platform::WebPlatformServices`, but the Web shell still reaches the default implementation through the retained fallback until a dedicated Web service is injected directly | Preserve WebGL memory behavior while moving canvas creation policy out of shared canvas headers and into the platform boundary | `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests`; Windows app build; WebGL package smoke once root Web build exists | Default canvas resolution is owned by injected `pp_platform_*` services for every supported platform, with no WebGL branch in the legacy fallback |
| DEBT-0058 | Open | Modernization | App-level progress/message/input dialog metadata, including message-dialog OK/cancel captions, now consumes pure `pp_app_core` through `App::show_progress`, `App::message_box`, `App::input_box`, `pano_cli plan-app-dialog`, and `pp_app_core_app_dialog_tests`; live execution is centralized in `src/legacy_app_dialog_services.*`, retained dialog opening now routes through `src/legacy_ui_overlay_services.*` with checked-handle registration when the main layout anchor exists, and whats-new dialog state persistence routes through `src/legacy_preference_storage.*`, but the bridge still creates retained `NodeProgressBar`, `NodeMessageBox`, and `NodeInputBox` instances and still relies on legacy callback/node implementations for final close behavior | Preserve current app-shell dialog behavior while moving shared dialog policy toward UI/app services | `pp_app_core_app_dialog_tests`; `pano_cli plan-app-dialog --kind progress --total -4`; `pano_cli plan-app-dialog --kind message --cancel`; `pano_cli plan-app-dialog --kind input --ok-caption Save`; `ctest --preset desktop-fast --build-config Debug`; Windows app build | Progress/message/input dialog creation, callback wiring, layout insertion, lifetime ownership, and headless automation are owned by injected app/UI services with `App` methods acting only as adapters |
| DEBT-0059 | Open | Modernization | iOS root CMake headless builds assign generated bundle identifiers and disable code signing for executable test/tool targets | The current Apple gate is compile validation for shared component targets; signed iOS app/package validation is not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\apple-remote-build.ps1 -Presets macos,ios-simulator,ios-device`; `sh scripts/automation/platform-build.sh "ios-device"` on `panopainter-mac` | Root CMake owns the signed Apple app/package targets, package-smoke validates Apple bundles where signing material is available, and headless iOS test/tool targets are either excluded from signed package builds or use explicit test-runner signing policy |
| DEBT-0064 | Open | Modernization | Stroke-preview result copy now routes through the renderer-facing `pp::paint_renderer::copy_stroke_preview_result_to_texture(...)` callback-only contract in `src/paint_renderer/compositor.h`; the node-level copy wrapper, final-composite wrapper, and pass-sequence wrapper are removed, and `pp_paint_renderer_compositor_tests` no longer needs either a test-local `Texture2D::bind()` definition. Live preview still owns concrete texture binding and OpenGL execution around the remaining preview flow | `tests` are configured before `pp_legacy_engine`, and linking the app legacy texture object into this planner test would still couple the renderer-neutral test target back to the app target graph | `cmake --build --preset windows-msvc-default --config Debug --target pp_paint_renderer_compositor_tests`; `ctest --preset desktop-fast --build-config Debug -R "pp_paint_renderer_compositor" --output-on-failure` | Keep preview-copy behavior on renderer-facing callback-only seams and move the remaining live preview copy execution behind renderer-owned services so the remaining preview flow can become fully app-texture-free |

## Closed Debt

| ID | Status | Owner | Item | Reason | Validation | Removal Condition |
| --- | --- | --- | --- | --- | --- | --- |
| DEBT-0060 | Closed | Modernization | Retained Android package CMake generated a patched `nanort.h` overlay in the build tree for `native-lib` | Current SDK Manager NDK/Clang rejects `TriangleSAHPred::operator=` assigning to a `const size_t` member | Closed on 2026-06-12: `powershell -ExecutionPolicy Bypass -File scripts\automation\android-legacy-package-build.ps1 -Packages standard` | Closed on 2026-06-12: Android package configure applies the tracked nanort source compatibility patch, standard/Quest/Focus package CMake files include `libs/nanort` directly, Android/GLES avoids desktop debug callback symbols, and the standard retained package links with the current app-service source list |
| DEBT-0062 | Closed | Modernization | VS 2026 builds generated a patched fmt `format.h` overlay in the build tree for `pp_legacy_vendor` | VS 2026's STL no longer exposes the legacy checked-array iterator used by the old fmt release | Closed on 2026-06-12: VS-bundled CMake build of `PanoPainter` and `pp_platform_api_tests`; `ctest --preset desktop-fast --build-config Debug -R pp_platform_api_tests --output-on-failure` | Closed on 2026-06-12: the generated overlay was removed, stale overlay directories are deleted during configure, and VS 2026 retained fmt sources use a generated forced-include compatibility header to keep fmt out of the removed checked-array-iterator branch |
| DEBT-0006 | Closed | Modernization | `pano_cli create-document` validates and emits JSON command contracts but does not yet invoke the legacy document/app model | The document model had not been extracted from `Canvas`/`App` yet | `ctest --preset desktop-fast --build-config Debug`; `pano_cli_create_document_smoke` | Closed on 2026-05-31: command now constructs a real `pp_document::CanvasDocument` |
| DEBT-0018 | Closed | Modernization | `pp_renderer_gl` owned a tested `OpenGlInitialState` plan for PanoPainter startup depth/blend policy, but `App::init` still executed the plan through direct OpenGL calls | Preserve behavior while moving renderer policy into the backend boundary before a live `IRenderDevice`/command context owns startup execution | `pp_renderer_gl_capabilities_tests`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Closed on 2026-06-03: `pp_renderer_gl::apply_panopainter_initial_state` now applies the startup state through a tested backend dispatch contract consumed by `App::init` |
