Plan document file saves in app core

This commit is contained in:
2026-06-02 22:42:51 +02:00
parent c8d769c02c
commit 5841878df9
9 changed files with 199 additions and 25 deletions

View File

@@ -394,6 +394,9 @@ Known local toolchain state:
- `pano_cli classify-open` exposes the `pp_app_core` document-open route - `pano_cli classify-open` exposes the `pp_app_core` document-open route
contract as JSON and is covered for project files, ABR imports, PPBR contract as JSON and is covered for project files, ABR imports, PPBR
imports, and malformed path rejection. imports, and malformed path rejection.
- `pano_cli plan-document-file` exposes `pp_app_core` document-name
validation, legacy `.ppi` path construction, and overwrite-prompt decisions
as JSON and is covered for save-now and existing-target overwrite states.
- `pano_cli simulate-app-session` exposes `pp_app_core` project-open, - `pano_cli simulate-app-session` exposes `pp_app_core` project-open,
app-close, save, save-as, save-version, and save-before-workflow decisions app-close, save, save-as, save-version, and save-before-workflow decisions
as JSON and is covered for clean, dirty, already-prompting, missing-canvas, as JSON and is covered for clean, dirty, already-prompting, missing-canvas,
@@ -402,9 +405,9 @@ Known local toolchain state:
contract for PPI/project files, ABR imports, PPBR imports, inner-dot names, contract for PPI/project files, ABR imports, PPBR imports, inner-dot names,
and malformed paths before the live `App::open_document` performs UI or and malformed paths before the live `App::open_document` performs UI or
legacy canvas work. legacy canvas work.
- `pp_app_core_document_session_tests` covers clean and dirty app session plus - `pp_app_core_document_session_tests` covers clean and dirty app session,
save-request and save-before-workflow decisions without requiring a window, save-request, save-before-workflow, document file target, and overwrite
canvas, or message box. decisions without requiring a window, canvas, or message box.
- `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON` - `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON`
through the vcpkg preset; default and Android validation still use the through the vcpkg preset; default and Android validation still use the
retained vendored fallback tracked by DEBT-0012. retained vendored fallback tracked by DEBT-0012.

View File

@@ -13,7 +13,7 @@ and validation command.
| --- | --- | --- | --- | | --- | --- | --- | --- |
| PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture | | 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`, `pano_cli`, `pp_panopainter_ui`, `pp_document`, `pp_assets` | Project/ABR/PPBR route tests, malformed path tests, CLI route smoke, 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 |
| Document session decisions | `App::open_document`, `App::request_close`, save hotkeys, file menu, dialogs | `pp_app_core`, `pano_cli`, `pp_panopainter_ui` | Clean/dirty/prompt-open/save/save-as/save-version/save-before-workflow decision tests, CLI session smoke, app close/open/save/new/browse smoke | | Document session decisions | `App::open_document`, `App::request_close`, save hotkeys, file menu, dialogs | `pp_app_core`, `pano_cli`, `pp_panopainter_ui` | Clean/dirty/prompt-open/save/save-as/save-version/save-before-workflow/name/overwrite decision tests, CLI session and document-file smoke, app close/open/save/new/browse smoke |
| Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior | | 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 | | Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input |
| Save-as, overwrite prompts | App/dialogs | `pp_app_core`, `pp_panopainter_ui`, `pp_platform_*` | Decision tests, UI automation, and platform smoke | | Save-as, overwrite prompts | App/dialogs | `pp_app_core`, `pp_panopainter_ui`, `pp_platform_*` | Decision tests, UI automation, and platform smoke |

View File

@@ -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-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-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`, `App::request_close`, file-menu save actions, `NodeCanvas` save hotkeys, new/open/browse dirty-document workflow prompts, `pano_cli classify-open`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session contracts, but document loading and saving still reach legacy `Canvas::I` and UI singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `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-0003 | Open | Modernization | Existing singletons remain during initial split; `App::open_document`, `App::request_close`, file-menu save actions, `NodeCanvas` save hotkeys, new/open/browse dirty-document workflow prompts, new/save-as document file naming and overwrite decisions, `pano_cli classify-open`, `pano_cli plan-document-file`, and `pano_cli simulate-app-session` now consume pure `pp_app_core` route/session contracts, but document loading and saving still reach legacy `Canvas::I` and UI singletons | Avoid behavior changes while introducing component boundaries | App launch and component tests; `pp_app_core_document_route_tests`; `pp_app_core_document_session_tests`; `pano_cli classify-open --path D:/Paint/demo.ppi`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `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, 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-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-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 | | 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 |

View File

@@ -426,7 +426,9 @@ contract for project files, ABR imports, PPBR imports, and malformed path
rejection. `pano_cli simulate-app-session` exposes the pure `pp_app_core` rejection. `pano_cli simulate-app-session` exposes the pure `pp_app_core`
session decisions used by project-open, app-close, save, save-as, and session decisions used by project-open, app-close, save, save-as, and
save-version flows, plus the save-before-continue workflow gate used by save-version flows, plus the save-before-continue workflow gate used by
new-document/open/browse dialogs. new-document/open/browse dialogs. `pano_cli plan-document-file` exposes the
same app-core document-name validation, legacy `.ppi` path construction, and
overwrite prompt decision used by new-document and save-as dialogs.
`pano_cli parse-layout` exercises the XML layout path. Continue expanding `pano_cli parse-layout` exercises the XML layout path. Continue expanding
document behavior toward legacy Canvas parity and then port OpenGL classes document behavior toward legacy Canvas parity and then port OpenGL classes
behind the renderer boundary. behind the renderer boundary.

View File

@@ -1,5 +1,11 @@
#pragma once #pragma once
#include "foundation/result.h"
#include <string>
#include <string_view>
#include <utility>
namespace pp::app { namespace pp::app {
enum class ProjectOpenDecision { enum class ProjectOpenDecision {
@@ -33,6 +39,17 @@ enum class DocumentWorkflowDecision {
prompt_save_before_continue, prompt_save_before_continue,
}; };
enum class DocumentFileWriteDecision {
save_now,
prompt_overwrite,
};
struct DocumentFileTarget {
std::string name;
std::string directory;
std::string path;
};
[[nodiscard]] constexpr ProjectOpenDecision plan_project_open(bool has_unsaved_changes) noexcept [[nodiscard]] constexpr ProjectOpenDecision plan_project_open(bool has_unsaved_changes) noexcept
{ {
return has_unsaved_changes return has_unsaved_changes
@@ -97,4 +114,32 @@ enum class DocumentWorkflowDecision {
: DocumentWorkflowDecision::continue_now; : DocumentWorkflowDecision::continue_now;
} }
[[nodiscard]] inline pp::foundation::Result<DocumentFileTarget> make_document_file_target(
std::string_view work_directory,
std::string_view document_name)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentFileTarget>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
DocumentFileTarget target;
target.name = std::string(document_name);
target.directory = std::string(work_directory);
target.path.reserve(target.directory.size() + target.name.size() + 5);
target.path += target.directory;
target.path += "/";
target.path += target.name;
target.path += ".ppi";
return pp::foundation::Result<DocumentFileTarget>::success(std::move(target));
}
[[nodiscard]] constexpr DocumentFileWriteDecision plan_document_file_write(
bool target_exists) noexcept
{
return target_exists
? DocumentFileWriteDecision::prompt_overwrite
: DocumentFileWriteDecision::save_now;
}
} }

View File

@@ -157,21 +157,20 @@ void App::dialog_newdoc()
dialog->btn_ok->on_click = [this, dialog](Node*) dialog->btn_ok->on_click = [this, dialog](Node*)
{ {
std::string name = dialog->input->m_text; std::string name = dialog->input->m_text;
std::string path = work_path + "/" + name + ".ppi"; const auto target = pp::app::make_document_file_target(work_path, name);
if (!target)
if (name.empty())
{ {
message_box("Warning", "You need to specify a name to file."); message_box("Warning", "You need to specify a name to file.");
return; return;
} }
auto action = [this, dialog, name, path] { auto action = [this, dialog, target = target.value()] {
std::array<int, 6> resolutions{ 512, 1024, 1536, 2048, 4096, 8192 }; std::array<int, 6> resolutions{ 512, 1024, 1536, 2048, 4096, 8192 };
int res = resolutions[dialog->m_resolution->m_current_index]; int res = resolutions[dialog->m_resolution->m_current_index];
doc_name = name; doc_name = target.name;
doc_path = path; doc_path = target.path;
doc_filename = name + ".ppi"; doc_filename = target.name + ".ppi";
doc_dir = work_path; doc_dir = target.directory;
layers->clear(); layers->clear();
canvas->m_canvas->m_layers.clear(); canvas->m_canvas->m_layers.clear();
@@ -189,7 +188,8 @@ void App::dialog_newdoc()
App::I->hideKeyboard(); App::I->hideKeyboard();
}; };
if (Asset::exist(path)) const auto write_decision = pp::app::plan_document_file_write(Asset::exist(target.value().path));
if (write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite)
{ {
// ask confirm is file already exist // ask confirm is file already exist
auto msgbox = new NodeMessageBox(); auto msgbox = new NodeMessageBox();
@@ -363,32 +363,32 @@ void App::dialog_save()
dialog->btn_ok->on_click = [this, dialog](Node*) dialog->btn_ok->on_click = [this, dialog](Node*)
{ {
std::string name = dialog->input->m_text; std::string name = dialog->input->m_text;
std::string path = work_path + "/" + name + ".ppi"; const auto target = pp::app::make_document_file_target(work_path, name);
if (!target)
if (name.empty())
{ {
message_box("Warning", "You need to specify a name to file."); message_box("Warning", "You need to specify a name to file.");
return; return;
} }
auto action = [this, dialog, name, path] { auto action = [this, dialog, target = target.value()] {
canvas->m_canvas->project_save(path); canvas->m_canvas->project_save(target.path);
doc_name = name; doc_name = target.name;
doc_path = path; doc_path = target.path;
doc_dir = work_path; doc_dir = target.directory;
title_update(); title_update();
dialog->destroy(); dialog->destroy();
App::I->hideKeyboard(); App::I->hideKeyboard();
}; };
if (Asset::exist(path)) const auto write_decision = pp::app::plan_document_file_write(Asset::exist(target.value().path));
if (write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite)
{ {
// ask confirm is file already exist // ask confirm is file already exist
auto msgbox = new NodeMessageBox(); auto msgbox = new NodeMessageBox();
msgbox->set_manager(&layout); msgbox->set_manager(&layout);
msgbox->init(); msgbox->init();
msgbox->m_title->set_text("Warning"); msgbox->m_title->set_text("Warning");
msgbox->m_message->set_text(("Are you sure you want to overwrite " + name + "?").c_str()); msgbox->m_message->set_text(("Are you sure you want to overwrite " + target.value().name + "?").c_str());
msgbox->btn_ok->on_click = [this, msgbox, action](Node*) { msgbox->btn_ok->on_click = [this, msgbox, action](Node*) {
action(); action();
msgbox->destroy(); msgbox->destroy();

View File

@@ -371,6 +371,18 @@ if(TARGET pano_cli)
LABELS "app;integration;desktop-fast;fuzz" LABELS "app;integration;desktop-fast;fuzz"
) )
add_test(NAME pano_cli_plan_document_file_save_now_smoke
COMMAND pano_cli plan-document-file --work-dir D:/Paint --name demo)
set_tests_properties(pano_cli_plan_document_file_save_now_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-document-file\".*\"name\":\"demo\".*\"directory\":\"D:/Paint\".*\"path\":\"D:/Paint/demo.ppi\".*\"exists\":false.*\"decision\":\"save-now\"")
add_test(NAME pano_cli_plan_document_file_overwrite_smoke
COMMAND pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists)
set_tests_properties(pano_cli_plan_document_file_overwrite_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-document-file\".*\"path\":\"D:/Paint/demo.ppi\".*\"exists\":true.*\"decision\":\"prompt-overwrite\"")
add_test(NAME pano_cli_simulate_app_session_clean_smoke add_test(NAME pano_cli_simulate_app_session_clean_smoke
COMMAND pano_cli simulate-app-session) COMMAND pano_cli simulate-app-session)
set_tests_properties(pano_cli_simulate_app_session_clean_smoke PROPERTIES set_tests_properties(pano_cli_simulate_app_session_clean_smoke PROPERTIES

View File

@@ -115,6 +115,34 @@ void workflow_with_dirty_canvas_prompts_for_save(pp::tests::Harness& harness)
== pp::app::DocumentWorkflowDecision::prompt_save_before_continue); == pp::app::DocumentWorkflowDecision::prompt_save_before_continue);
} }
void document_file_target_rejects_empty_name(pp::tests::Harness& harness)
{
const auto target = pp::app::make_document_file_target("D:/Paint", "");
PP_EXPECT(harness, !target);
PP_EXPECT(harness, target.status().code == pp::foundation::StatusCode::invalid_argument);
}
void document_file_target_builds_legacy_ppi_path(pp::tests::Harness& harness)
{
const auto target = pp::app::make_document_file_target("D:/Paint", "demo");
PP_EXPECT(harness, target);
PP_EXPECT(harness, target.value().name == "demo");
PP_EXPECT(harness, target.value().directory == "D:/Paint");
PP_EXPECT(harness, target.value().path == "D:/Paint/demo.ppi");
}
void document_file_write_prompts_only_for_existing_targets(pp::tests::Harness& harness)
{
PP_EXPECT(
harness,
pp::app::plan_document_file_write(false)
== pp::app::DocumentFileWriteDecision::save_now);
PP_EXPECT(
harness,
pp::app::plan_document_file_write(true)
== pp::app::DocumentFileWriteDecision::prompt_overwrite);
}
} }
int main() int main()
@@ -131,5 +159,8 @@ int main()
harness.run("workflow without canvas is unavailable", workflow_without_canvas_is_unavailable); harness.run("workflow without canvas is unavailable", workflow_without_canvas_is_unavailable);
harness.run("workflow with clean canvas continues now", workflow_with_clean_canvas_continues_now); harness.run("workflow with clean canvas continues now", workflow_with_clean_canvas_continues_now);
harness.run("workflow with dirty canvas prompts for save", workflow_with_dirty_canvas_prompts_for_save); harness.run("workflow with dirty canvas prompts for save", workflow_with_dirty_canvas_prompts_for_save);
harness.run("document file target rejects empty name", document_file_target_rejects_empty_name);
harness.run("document file target builds legacy ppi path", document_file_target_builds_legacy_ppi_path);
harness.run("document file write prompts only for existing targets", document_file_write_prompts_only_for_existing_targets);
return harness.finish(); return harness.finish();
} }

View File

@@ -96,6 +96,12 @@ struct ClassifyOpenArgs {
std::string path; std::string path;
}; };
struct PlanDocumentFileArgs {
std::string work_directory;
std::string name;
bool target_exists = false;
};
struct SimulateAppSessionArgs { struct SimulateAppSessionArgs {
bool has_canvas = true; bool has_canvas = true;
bool new_document = false; bool new_document = false;
@@ -313,6 +319,18 @@ const char* document_workflow_decision_name(pp::app::DocumentWorkflowDecision de
return "unavailable"; return "unavailable";
} }
const char* document_file_write_decision_name(pp::app::DocumentFileWriteDecision decision) noexcept
{
switch (decision) {
case pp::app::DocumentFileWriteDecision::save_now:
return "save-now";
case pp::app::DocumentFileWriteDecision::prompt_overwrite:
return "prompt-overwrite";
}
return "save-now";
}
pp::foundation::Result<float> parse_float_arg(std::string_view text) pp::foundation::Result<float> parse_float_arg(std::string_view text)
{ {
float value = 0.0F; float value = 0.0F;
@@ -343,6 +361,7 @@ void print_help()
<< " import-image --path FILE [--document-width N] [--document-height N] [--face N] [--x N] [--y N]\n" << " import-image --path FILE [--document-width N] [--document-height N] [--face N] [--x N] [--y N]\n"
<< " inspect-project --path FILE\n" << " inspect-project --path FILE\n"
<< " classify-open --path FILE\n" << " classify-open --path FILE\n"
<< " plan-document-file --work-dir DIR --name NAME [--target-exists]\n"
<< " load-project --path FILE\n" << " load-project --path FILE\n"
<< " parse-layout --path FILE\n" << " parse-layout --path FILE\n"
<< " record-render [--width N] [--height N] [--exercise-clear]\n" << " record-render [--width N] [--height N] [--exercise-clear]\n"
@@ -1184,6 +1203,64 @@ int classify_open(int argc, char** argv)
return 0; return 0;
} }
pp::foundation::Status parse_plan_document_file_args(
int argc,
char** argv,
PlanDocumentFileArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--work-dir") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.work_directory = argv[++i];
} else if (key == "--name") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.name = argv[++i];
} else if (key == "--target-exists") {
args.target_exists = true;
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
if (args.work_directory.empty()) {
return pp::foundation::Status::invalid_argument("work directory must not be empty");
}
return pp::foundation::Status::success();
}
int plan_document_file(int argc, char** argv)
{
PlanDocumentFileArgs args;
const auto status = parse_plan_document_file_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-document-file", status.message);
return 2;
}
const auto target = pp::app::make_document_file_target(args.work_directory, args.name);
if (!target) {
print_error("plan-document-file", target.status().message);
return 2;
}
const auto write_decision = pp::app::plan_document_file_write(args.target_exists);
std::cout << "{\"ok\":true,\"command\":\"plan-document-file\""
<< ",\"target\":{\"name\":\"" << json_escape(target.value().name)
<< "\",\"directory\":\"" << json_escape(target.value().directory)
<< "\",\"path\":\"" << json_escape(target.value().path)
<< "\",\"exists\":" << json_bool(args.target_exists)
<< "},\"decision\":\""
<< document_file_write_decision_name(write_decision)
<< "\"}\n";
return 0;
}
pp::foundation::Status parse_simulate_app_session_args( pp::foundation::Status parse_simulate_app_session_args(
int argc, int argc,
char** argv, char** argv,
@@ -3162,6 +3239,10 @@ int main(int argc, char** argv)
return classify_open(argc, argv); return classify_open(argc, argv);
} }
if (command == "plan-document-file") {
return plan_document_file(argc, argv);
}
if (command == "load-project") { if (command == "load-project") {
return load_project(argc, argv); return load_project(argc, argv);
} }