From 6cf61a3ba851c715594e1a2b3302d26fc14f6a91 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:00:11 +0200 Subject: [PATCH 1/2] feat(core): wire worldscript_project_validate Tauri command (Wave 2 PR B) Strangler proof point per docs/native/CORE-MIGRATION-LEDGER.md's Wave 2 plan: a new src-tauri/src/commands/project_core.rs exposes worldscript_project_validate, delegating to the renderer-neutral worldscript-project crate (parse -> migrate to current schema -> validate) instead of any Tauri-local logic. worldscript-project is added as a path dependency in src-tauri's Cargo.toml, referencing a crate that is itself a member of the separate crates/ Cargo workspace - confirmed this cross-workspace path dependency compiles and links cleanly (cargo check/test/clippy all pass) without requiring the two workspaces to be unified, keeping the Wave 2 PR 1 decision to keep them independent intact. Backend-only: no frontend call site, no changes to services/desktopPlatform.ts or services/fs/projectFsStore.ts / services/storageService.ts's dispatch. This only proves the Tauri <-> Rust Core command boundary compiles and runs correctly; wiring an actual frontend caller is separate, later work. Co-Authored-By: Claude Sonnet 5 --- src-tauri/Cargo.lock | 9 ++ src-tauri/Cargo.toml | 1 + src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/project_core.rs | 131 +++++++++++++++++++++++++ src-tauri/src/lib.rs | 1 + 5 files changed, 143 insertions(+) create mode 100644 src-tauri/src/commands/project_core.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0987505b5..f1aed8d73 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6675,6 +6675,14 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "worldscript-project" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "worldscript-studio" version = "1.27.1" @@ -6699,6 +6707,7 @@ dependencies = [ "tauri-plugin-updater", "tauri-plugin-window-state", "tokio", + "worldscript-project", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5fed8d122..9b1820f32 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -40,6 +40,7 @@ tauri-plugin-process = "2" tokio = { version = "1", features = ["sync", "time", "process", "rt-multi-thread"] } candle-core = { version = "0.11", optional = true } candle-nn = { version = "0.11", optional = true } +worldscript-project = { path = "../crates/worldscript-project" } [profile.release] # QNBS-v3: Aggressive size + speed optimizations for desktop bundles diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 354e80417..02f2cdecb 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -3,4 +3,5 @@ //! native-compute surface isolated from the flat top-level command files //! (lora.rs / pandoc.rs) so the supervisor can grow its own task registry. +pub mod project_core; pub mod task_supervisor; diff --git a/src-tauri/src/commands/project_core.rs b/src-tauri/src/commands/project_core.rs new file mode 100644 index 000000000..781b15188 --- /dev/null +++ b/src-tauri/src/commands/project_core.rs @@ -0,0 +1,131 @@ +//! Wave 2 PR B — strangler proof point: wires one real Tauri command to the renderer-neutral +//! `worldscript-project` Rust Core crate (`docs/native/CORE-MIGRATION-LEDGER.md`). +//! +//! Backend-only. No frontend call site exists yet — `services/desktopPlatform.ts` and +//! `services/fs/projectFsStore.ts`/`services/storageService.ts`'s dispatch are untouched by this +//! PR. The point of this command is proving the Tauri <-> Rust Core boundary compiles, links, and +//! runs correctly before any UI wiring is attempted. + +use serde::Serialize; +use worldscript_project::{migrate_to_latest, parse_envelope, validate}; + +/// Structured verdict for a project envelope JSON string, mirroring the pipeline a future +/// desktop load path would run: parse -> migrate to current schema -> validate. +#[derive(Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ProjectValidationResult { + pub valid: bool, + /// The schema version after migration, when parsing succeeded. Absent on parse failure. + #[serde(skip_serializing_if = "Option::is_none")] + pub schema_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// Parses, migrates, and validates a `ProjectEnvelope` JSON string via the `worldscript-project` +/// crate. Never returns `Err` — parse/migrate/validate failures are reported as a structured +/// `ProjectValidationResult { valid: false, error: Some(..) }`, matching the honest-failure +/// convention `commands::task_supervisor` already established for this codebase. +#[tauri::command] +pub fn worldscript_project_validate(project_json: String) -> ProjectValidationResult { + let envelope = match parse_envelope(&project_json) { + Ok(envelope) => envelope, + Err(e) => { + return ProjectValidationResult { + valid: false, + schema_version: None, + error: Some(e.to_string()), + } + } + }; + + let migrated = match migrate_to_latest(envelope) { + Ok(migrated) => migrated, + Err(e) => { + return ProjectValidationResult { + valid: false, + schema_version: None, + error: Some(e.to_string()), + } + } + }; + + match validate(&migrated.project) { + Ok(()) => ProjectValidationResult { + valid: true, + schema_version: Some(migrated.schema_version), + error: None, + }, + Err(e) => ProjectValidationResult { + valid: false, + schema_version: Some(migrated.schema_version), + error: Some(e.to_string()), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn valid_current_schema_project_passes() { + let json = r#"{ + "schemaVersion": 2, + "project": { + "title": "T", "logline": "L", + "characters": [], "worlds": [], "manuscript": [] + } + }"#; + let result = worldscript_project_validate(json.to_string()); + assert_eq!( + result, + ProjectValidationResult { + valid: true, + schema_version: Some(2), + error: None, + } + ); + } + + #[test] + fn v1_project_is_migrated_before_validation() { + let json = r#"{ + "schemaVersion": 1, + "project": { + "title": "Old", "logline": "L", + "characters": [], "worlds": [], "manuscript": [] + } + }"#; + let result = worldscript_project_validate(json.to_string()); + assert!(result.valid); + assert_eq!(result.schema_version, Some(2)); + } + + #[test] + fn corrupt_json_reports_structured_failure_not_a_panic() { + let result = worldscript_project_validate("{ not json".to_string()); + assert!(!result.valid); + assert_eq!(result.schema_version, None); + assert!(result.error.is_some()); + } + + #[test] + fn duplicate_character_ids_fail_validation_with_schema_version_present() { + let json = r#"{ + "schemaVersion": 2, + "project": { + "title": "T", "logline": "L", + "characters": [ + {"id": "c1", "name": "A"}, + {"id": "c1", "name": "B"} + ], + "worlds": [], "manuscript": [] + } + }"#; + let result = worldscript_project_validate(json.to_string()); + assert!(!result.valid); + assert_eq!(result.schema_version, Some(2)); + assert!(result.error.unwrap().contains("c1")); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1263cbe34..1a116d380 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -166,6 +166,7 @@ pub fn run() { lora::set_lora_python_path, commands::task_supervisor::worldscript_task_supervisor_ping, commands::task_supervisor::worldscript_task_supervisor_submit, + commands::project_core::worldscript_project_validate, ]) .setup(|app| { if cfg!(debug_assertions) { From ebfbcd1c7acd614668028bf6fdc541b9df360cab Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:07:20 +0200 Subject: [PATCH 2/2] docs(core): note the EntityState normalization gap in project_core.rs CodeAnt correctly flagged that a real Redux-persisted project (using EntityState normalization for characters/worlds) would fail parse_envelope today. This is deliberate, existing scope from PR #409's schema.rs - not a regression introduced here - but that context lived only in schema.rs, not in this command's own docs. Add a pointer so a future reader of project_core.rs alone sees the limitation without having to already know schema.rs's history. Co-Authored-By: Claude Sonnet 5 --- src-tauri/src/commands/project_core.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src-tauri/src/commands/project_core.rs b/src-tauri/src/commands/project_core.rs index 781b15188..471e43526 100644 --- a/src-tauri/src/commands/project_core.rs +++ b/src-tauri/src/commands/project_core.rs @@ -5,6 +5,13 @@ //! `services/fs/projectFsStore.ts`/`services/storageService.ts`'s dispatch are untouched by this //! PR. The point of this command is proving the Tauri <-> Rust Core boundary compiles, links, and //! runs correctly before any UI wiring is attempted. +//! +//! **Cannot yet validate a live persisted project as-is.** `worldscript_project::schema` models +//! `characters`/`worlds` as plain arrays, not the `Character[] | EntityState` +//! union `types.ts` uses (Redux normalizes to `EntityState` at runtime) — see that module's own +//! doc comment. A real frontend caller will need a normalization adapter (array <-> EntityState) +//! before this command can validate what's actually persisted; that adapter is intentionally not +//! built here, matching this PR's no-frontend-call-site scope. use serde::Serialize; use worldscript_project::{migrate_to_latest, parse_envelope, validate};