# Modernization Debt Log

Status: live
Last updated: 2026-06-15

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.

## Recent Reductions

- 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-15: `DEBT-0049` was narrowed again. `pp_assets_brush_package_tests`
  now names the currently accepted legacy PPBR version matrix explicitly:
  canonical `0.1`, tolerated `0.2`, tolerated `1.1`, and rejected `1.2`. The
  live parser still preserves the legacy tolerance until broader compatibility
  fixtures prove strict canonical enforcement is 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-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-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-15: `DEBT-0036` was narrowed again. `Canvas::draw_merge_branch_orchestration()`
  now drops the unused `draw_checkerboard` parameter from the internal branch
  wrapper, shrinking the remaining call-shape mismatch while the blocked
  draw-merge extraction still stays in `STR-016`.
- 2026-06-15: `DEBT-0036` was narrowed again. `Canvas::draw_merge_temporary_paint_branch()`
  now drops the unused `copy_blend_destination` parameter from the temporary-paint
  wrapper, shrinking the remaining draw-merge helper API mismatch while the
  blocked extraction still stays in `STR-016`.
- 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, shrinking the
  remaining draw-merge helper API mismatch while `STR-016` stays blocked.
- 2026-06-15: `DEBT-0036` was narrowed again. The remaining
  `Canvas::draw_merge()` inline-default comment in `src/canvas.cpp` now
  matches the header's `SIXPLETTE(true)` default, so the draw-merge mismatch
  is comment-only and aligned while `STR-016` stays 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 so the gate actually runs.
- 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`;
  the harness still stays minimal and opt-in.
- 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
  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`, and Windows live execution uses injected `WindowsPlatformServices`, but Apple/Android clipboard execution still reaches retained fallback adapter branches from `App::clipboard_get_text` and `App::clipboard_set_text` | 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, and Windows injects `WindowsPlatformServices` from `src/platform_windows/windows_platform_services.*`; Windows render-platform hint and debug-output state token/enable sequencing now delegates to tested `pp_renderer_gl` helpers, leaving Windows with context, callback, console, and Win32 ownership; the retained macOS fallback render-platform hint enable sequence also delegates to the same tested `pp_renderer_gl` helper; non-Windows live implementations still use `src/platform_legacy/legacy_platform_services.*`, a named fallback adapter that forwards to retained Apple/Android/Linux/Web bridge functions and retained no-op branches, including retained iOS canvas tip behavior, retained macOS directory picker/display-path behavior, retained iOS SonarPen bridge, 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.*`, and retained `PBO` allocation/readback/map/unmap/delete operations now route through tested `pp_renderer_gl` dispatch, but the bridge still owns legacy recording thread startup/shutdown, platform recorded-file cleanup, progress lifetime, retained `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`, but the bridge still launches legacy desktop timelapse worker threads, 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`, and the macOS data-directory override now routes through `PlatformServices`, but the bridge still reads `NodeDialogExportPPBR`, carries the legacy `Image` header object outside the pure request, converts to `NodePanelBrushPreset::PPBRInfo`, calls `NodePanelBrushPreset::export_ppbr`, owns desktop worker-thread dispatch, dialog destruction, and 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`, but the bridge still launches detached legacy `NodePanelBrushPreset::import_abr`/`import_ppbr` worker threads and 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`, and WebGL flushing now goes through injectable `pp::platform::WebPlatformServices`, but non-Windows execution still lives in retained fallback adapters and forwards to retained `save_image_library`/`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 creates the Apple bridge and 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`, and Windows placement reads/writes now use `LegacyWindowPreferenceSnapshot` plus `src/legacy_preference_storage.*`, but macOS execution still lives in `src/platform_legacy/legacy_platform_services.*` and forwards to the retained Objective-C app bridge while 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 and WebGL data-path target planning live in tested `pp_platform_api::platform_policy`, and WebGL prepared-file handoff now goes through injectable `pp::platform::WebPlatformServices`, but retained iOS/Web save/download handoff execution still lives in retained 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` |
