diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0987505b..f1aed8d7 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 5fed8d12..9b1820f3 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 354e8041..02f2cdec 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 00000000..471e4352 --- /dev/null +++ b/src-tauri/src/commands/project_core.rs @@ -0,0 +1,138 @@ +//! 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. +//! +//! **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}; + +/// 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 1263cbe3..1a116d38 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) {