diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index b0d20c3..eb731ed 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -391,6 +391,9 @@ Known local toolchain state: - `pano_cli apply-stroke-script` exposes file-driven stroke-script application to a pure document face payload and writes a PPI artifact for inspect/load round-trip automation. +- `pano_cli classify-open` exposes the `pp_app_core` document-open route + contract as JSON and is covered for project files, ABR imports, PPBR + imports, and malformed path rejection. - `pp_app_core_document_route_tests` covers the app document-open route contract for PPI/project files, ABR imports, PPBR imports, inner-dot names, and malformed paths before the live `App::open_document` performs UI or diff --git a/docs/modernization/capability-map.md b/docs/modernization/capability-map.md index 709043d..1c24d38 100644 --- a/docs/modernization/capability-map.md +++ b/docs/modernization/capability-map.md @@ -12,7 +12,7 @@ and validation command. | Capability | Current Area | Target Owner | Required Tests | | --- | --- | --- | --- | | PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture | -| Open-document routing | `App::open_document` | `pp_app_core`, `pp_panopainter_ui`, `pp_document`, `pp_assets` | Project/ABR/PPBR route tests, malformed path tests, app open smoke | +| Open-document routing | `App::open_document` | `pp_app_core`, `pano_cli`, `pp_panopainter_ui`, `pp_document`, `pp_assets` | Project/ABR/PPBR route tests, malformed path tests, CLI route smoke, app open smoke | | Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior | | Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input | | Save-as, overwrite prompts | App/dialogs | `pp_panopainter_ui`, `pp_platform_*` | UI automation and platform smoke | diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index e22ef8f..aa3a31a 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -22,7 +22,7 @@ agent or engineer to remove them without reconstructing context from chat. | --- | --- | --- | --- | --- | --- | --- | | 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 | 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` now consumes a pure `pp_app_core` route contract, but document loading still reaches legacy `Canvas::I` and UI singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries | +| DEBT-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document` and `pano_cli classify-open` now consume a pure `pp_app_core` route contract, but document loading still reaches legacy `Canvas::I` and UI singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `ctest --preset desktop-fast --build-config Debug` | Replace singleton reaches with context/service injection at component boundaries | | DEBT-0004 | Open | Modernization | Android, Linux, WebGL, 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`; 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, 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 | `$env:VCPKG_ROOT="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"; cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 28149f8..b456fad 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -167,10 +167,10 @@ app adapter until those paths are replaced by `pp_ui_core` and app-specific UI targets. `pp_app_core` now owns tested app-level document-open routing for project files, ABR imports, and PPBR imports without UI, filesystem, platform, or -renderer dependencies; `App::open_document` consumes this route contract while -legacy canvas/project loading remains in place. `panopainter_app` is now a real -static target that owns app orchestration sources, app version metadata, and -version-header generation. +renderer dependencies; `App::open_document` and `pano_cli classify-open` +consume this route contract while legacy canvas/project loading remains in +place. `panopainter_app` is now a real static target that owns app +orchestration sources, app version metadata, and version-header generation. `pp_panopainter_ui` now owns app-specific modal, dialog, panel, canvas, viewport, color-picker, stroke-preview, and tool UI workflow nodes outside `pp_legacy_app`; base `Node` controls and layout plumbing remain in the legacy @@ -283,10 +283,11 @@ Status: in progress. `tests/` exists, `desktop-fast`, `fuzz`, and `stress` CTest presets run headlessly, and PowerShell/bash wrappers exist for configure/build/test/analyze/platform-build/package-smoke. `pano_cli` exists -with JSON automation commands for creating a `pp_document` model, metadata-only -PPI project loading, and inspecting image signatures, PPI headers, and layout -XML; full document/app integration is debt-tracked as DEBT-0010 and full PPI -body parsing is debt-tracked as DEBT-0013. +with JSON automation commands for app document-open routing, creating a +`pp_document` model, metadata-only PPI project loading, and inspecting image +signatures, PPI headers, and layout XML; full document/app integration is +debt-tracked as DEBT-0010 and full PPI body parsing is debt-tracked as +DEBT-0013. Implementation tasks: @@ -416,9 +417,11 @@ stroke. `pano_cli apply-stroke-script` maps sampled script points into a bounded `pp_document` RGBA8 face payload, writes a PPI file, and verifies that the applied stroke payload survives inspect/load round-trip automation, with a rejection smoke test for unsafe tiny canvas dimensions. -`pano_cli parse-layout` exercises the XML layout path. Continue expanding -document behavior toward legacy Canvas parity and then port OpenGL classes -behind the renderer boundary. +`pano_cli classify-open` exposes the pure `pp_app_core` document-open route +contract for project files, ABR imports, PPBR imports, and malformed path +rejection. `pano_cli parse-layout` exercises the XML layout path. Continue +expanding document behavior toward legacy Canvas parity and then port OpenGL +classes behind the renderer boundary. Implementation tasks: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 996e696..0584231 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -333,6 +333,34 @@ if(TARGET pano_cli) LABELS "assets;document;integration;desktop-fast" PASS_REGULAR_EXPRESSION "\"command\":\"load-project\".*\"pixelDataLoaded\":false.*\"facePayloads\":0.*\"document\":\\{\"width\":64,\"height\":32,\"layers\":1,\"frames\":1,\"animationDurationMs\":100,\"layerNames\":\\[\"Ink\"\\],\"layerFrameCounts\":\\[1\\],\"layerDurationsMs\":\\[100\\]") + add_test(NAME pano_cli_classify_open_project_smoke + COMMAND pano_cli classify-open --path "D:/Paint/Scenes/demo.ppi") + set_tests_properties(pano_cli_classify_open_project_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"classify-open\".*\"kind\":\"open-project\".*\"directory\":\"D:/Paint/Scenes\".*\"name\":\"demo\".*\"extension\":\"ppi\"") + + add_test(NAME pano_cli_classify_open_brush_import_smoke + COMMAND pano_cli classify-open --path "D:/Paint/Brushes/clouds.ABR") + set_tests_properties(pano_cli_classify_open_brush_import_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"classify-open\".*\"kind\":\"import-abr\".*\"name\":\"clouds\".*\"extension\":\"abr\"") + + add_test(NAME pano_cli_classify_open_ppbr_import_smoke + COMMAND pano_cli classify-open --path "D:/Paint/Brushes/palette.PpBr") + set_tests_properties(pano_cli_classify_open_ppbr_import_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"classify-open\".*\"kind\":\"import-ppbr\".*\"name\":\"palette\".*\"extension\":\"ppbr\"") + + add_test(NAME pano_cli_classify_open_rejects_missing_directory + COMMAND "${CMAKE_COMMAND}" + -DPANO_CLI=$ + -DOPEN_PATH=demo.ppi + "-DEXPECTED_OUTPUT=document path must include a directory and file name" + -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/expect_pano_cli_classify_open_failure.cmake") + set_tests_properties(pano_cli_classify_open_rejects_missing_directory PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + ) + add_test(NAME pano_cli_save_project_roundtrip_smoke COMMAND "${CMAKE_COMMAND}" -DPANO_CLI=$ diff --git a/tests/cmake/expect_pano_cli_classify_open_failure.cmake b/tests/cmake/expect_pano_cli_classify_open_failure.cmake new file mode 100644 index 0000000..9361177 --- /dev/null +++ b/tests/cmake/expect_pano_cli_classify_open_failure.cmake @@ -0,0 +1,27 @@ +if(NOT DEFINED PANO_CLI) + message(FATAL_ERROR "PANO_CLI is required") +endif() + +if(NOT DEFINED OPEN_PATH) + message(FATAL_ERROR "OPEN_PATH is required") +endif() + +if(NOT DEFINED EXPECTED_OUTPUT) + message(FATAL_ERROR "EXPECTED_OUTPUT is required") +endif() + +execute_process( + COMMAND "${PANO_CLI}" classify-open --path "${OPEN_PATH}" + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE error) + +if(result EQUAL 0) + message(FATAL_ERROR "pano_cli classify-open unexpectedly succeeded: ${output}${error}") +endif() + +string(FIND "${output}${error}" "${EXPECTED_OUTPUT}" found_at) +if(found_at EQUAL -1) + message(FATAL_ERROR + "pano_cli classify-open failure output did not contain expected text.\nExpected: ${EXPECTED_OUTPUT}\nOutput: ${output}${error}") +endif() diff --git a/tools/pano_cli/CMakeLists.txt b/tools/pano_cli/CMakeLists.txt index 542a677..87f46ff 100644 --- a/tools/pano_cli/CMakeLists.txt +++ b/tools/pano_cli/CMakeLists.txt @@ -3,6 +3,7 @@ add_executable(pano_cli target_link_libraries(pano_cli PRIVATE pp_project_options pp_project_warnings + pp_app_core pp_foundation pp_assets pp_document diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 2c335a4..1e22f65 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -1,3 +1,4 @@ +#include "app_core/document_route.h" #include "assets/image_format.h" #include "assets/image_metadata.h" #include "assets/image_pixels.h" @@ -90,6 +91,10 @@ struct InspectProjectArgs { std::string path; }; +struct ClassifyOpenArgs { + std::string path; +}; + struct SimulateStrokeArgs { std::uint32_t x1 = 0; std::uint32_t y1 = 0; @@ -213,6 +218,20 @@ const char* json_bool(bool value) noexcept return value ? "true" : "false"; } +const char* document_open_kind_name(pp::app::DocumentOpenKind kind) noexcept +{ + switch (kind) { + case pp::app::DocumentOpenKind::import_abr: + return "import-abr"; + case pp::app::DocumentOpenKind::import_ppbr: + return "import-ppbr"; + case pp::app::DocumentOpenKind::open_project: + return "open-project"; + } + + return "open-project"; +} + pp::foundation::Result parse_float_arg(std::string_view text) { float value = 0.0F; @@ -242,6 +261,7 @@ void print_help() << " inspect-image --path FILE\n" << " import-image --path FILE [--document-width N] [--document-height N] [--face N] [--x N] [--y N]\n" << " inspect-project --path FILE\n" + << " classify-open --path FILE\n" << " load-project --path FILE\n" << " parse-layout --path FILE\n" << " record-render [--width N] [--height N] [--exercise-clear]\n" @@ -1035,6 +1055,53 @@ pp::foundation::Status parse_inspect_project_args(int argc, char** argv, Inspect return pp::foundation::Status::success(); } +pp::foundation::Status parse_classify_open_args(int argc, char** argv, ClassifyOpenArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--path") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + + args.path = argv[++i]; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + if (args.path.empty()) { + return pp::foundation::Status::invalid_argument("path must not be empty"); + } + + return pp::foundation::Status::success(); +} + +int classify_open(int argc, char** argv) +{ + ClassifyOpenArgs args; + const auto status = parse_classify_open_args(argc, argv, args); + if (!status.ok()) { + print_error("classify-open", status.message); + return 2; + } + + const auto route = pp::app::classify_document_open_path(args.path); + if (!route) { + print_error("classify-open", route.status().message); + return 2; + } + + std::cout << "{\"ok\":true,\"command\":\"classify-open\"" + << ",\"route\":{\"kind\":\"" << document_open_kind_name(route.value().kind) + << "\",\"path\":\"" << json_escape(route.value().path) + << "\",\"directory\":\"" << json_escape(route.value().directory) + << "\",\"name\":\"" << json_escape(route.value().name) + << "\",\"extension\":\"" << json_escape(route.value().extension) + << "\"}}\n"; + return 0; +} + int inspect_project(int argc, char** argv) { InspectProjectArgs args; @@ -2935,6 +3002,10 @@ int main(int argc, char** argv) return inspect_project(argc, argv); } + if (command == "classify-open") { + return classify_open(argc, argv); + } + if (command == "load-project") { return load_project(argc, argv); }