Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
138 changes: 138 additions & 0 deletions src-tauri/src/commands/project_core.rs
Original file line number Diff line number Diff line change
@@ -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<Character, string>`
//! 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<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}

/// 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,
Comment thread
qnbs marked this conversation as resolved.
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"));
}
}
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading