diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 1266a3c6..353d3bf9 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -9,6 +9,26 @@ https://semver.org/spec/v2.0.0.html[Semantic Versioning]. === https://github.com/hyperpolymath/panll/compare/v0.1.0...HEAD[Unreleased] +==== Changed (2026-10-14 — orphaned Rust backend modules wired into the lib) + +* *43 orphaned modules under `+src-gossamer/src/+` now compile.* They were +declared by no crate root, so they were never compiled, linted or tested. +All are GTK/WebKit-free and are now `+pub mod+` in `+lib.rs+`; +`+main.rs+` still adds only `+system_tray+`. `+cargo test --lib+` runs +285 tests (was 23). +* Clippy (`+-D warnings+`) and rustfmt fixes in the newly compiled code. +Most were blank lines left where `+#[tauri::command]+` attributes had +been deleted. `+minter_mint_panel+` now takes a `+MintRequest+` instead +of 12 positional arguments. +* Tauri leftovers removed from `+compat+`, `+error+` +(`+to_tauri_result+` → `+to_command_result+`), `+cloudguard::commands+` +and `+ums_cartridge::commands+`. +* The handlers in these modules are *not yet registered* in +`+register_commands+` (`+main.rs+`); that is follow-up work. The +`+Cargo.toml+` header ("All 103 command handlers are registered") and +`+EXPLAINME-new.adoc+` ("270 migrated commands") now state the real +figure: 36 registered. + ==== Fixed (2026-10-14 — panel-clades Types/Layout/Foreign typecheck) * `+Types.idr+`, `+Layout.idr+` and `+Foreign.idr+` had never typechecked. diff --git a/Cargo.toml b/Cargo.toml index d5f8f65a..434ed13a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,13 +3,15 @@ # PanLL eNSAID — Gossamer backend # # This replaces the former src-tauri/ Tauri 2.0 backend with a Gossamer-native -# binary. All 103 command handlers are registered via gossamer_rs::App::command() -# instead of #[tauri::command] macros. Business logic is unchanged — only the -# IPC dispatch layer changed. +# binary. IPC commands are registered via gossamer_rs::App::command() in +# `register_commands` (src-gossamer/src/main.rs) instead of #[tauri::command] +# macros. Only 36 command handlers are registered today; the panel-backend +# modules in the lib carry many more that compile and are tested but are not +# yet registered (docs/TECHNICAL_DEBT.adoc, F1). # -# The gossamer-rs crate links to libgossamer.so (Zig FFI) at runtime, providing -# the same webview + IPC channel that Tauri provided, but without the Tauri -# runtime dependency. +# The gossamer-rs crate statically links libgossamer (Zig FFI, built from +# source or taken from GOSSAMER_LIB_DIR), providing the same webview + IPC +# channel that Tauri provided, but without the Tauri runtime dependency. [package] name = "panll" diff --git a/EXPLAINME-new.adoc b/EXPLAINME-new.adoc index 696a9b6a..0ce82f8c 100644 --- a/EXPLAINME-new.adoc +++ b/EXPLAINME-new.adoc @@ -41,7 +41,7 @@ Soundness is relative to ReScript's type system. It guarantees the UI state mach ____5 MB binary vs 100+ MB Electron. No garbage collector pauses during real-time panel updates.____ How this is implemented:: -The backend is Rust (`link:src-tauri/[]` or equivalent Rust backend) using Gossamer (WebKitGTK) for the webview shell. Rust's ownership model eliminates GC. Gossamer compiles to a ~5 MB binary. Filesystem watching uses the `notify` crate. +The backend is Rust (`link:src-gossamer/[]`) using Gossamer (WebKitGTK) for the webview shell. Rust's ownership model eliminates GC. Gossamer compiles to a ~5 MB binary. Filesystem watching uses the `notify` crate. Caveat:: The 5 MB figure is the Rust binary; the full installation includes WebKitGTK dependencies (provided by the host OS or container). Linux is the primary target; macOS and Windows require WebKitGTK availability. Gossamer is actively maintained but less battle-tested than Electron. @@ -133,8 +133,8 @@ The import validates JSON structure, not the semantic truth of the event chain. | `src/` (ReScript) | 4-pane UI, TEA architecture, Anti-Crash, Vexometer, Contractiles -| `src-tauri/` or Rust backend -| Gossamer webview, 270 migrated commands, filesystem watching +| `src-gossamer/` (Rust backend) +| Gossamer webview shell. 36 IPC commands are registered in `main.rs`; the other panel backends (including filesystem watching) compile and are tested in the lib, but their handlers are not yet registered | `beam/panll_beam/` | BEAM middleware scaffold (HTTP/GraphQL/gRPC) diff --git a/docs/TECHNICAL_DEBT.adoc b/docs/TECHNICAL_DEBT.adoc index bae5eeac..1f06a010 100644 --- a/docs/TECHNICAL_DEBT.adoc +++ b/docs/TECHNICAL_DEBT.adoc @@ -156,8 +156,12 @@ bridge), done first. |Medium [arabic] . *Pilot (F2):* finish `+coprocessor+` IPC registration → builds the reusable async→sync `+block_on+` bridge for `+app.command+`. -. *Census:* declare all 38 modules; `+cargo check+` each; record which -compile clean vs. bit-rotted (never compiled — expect API drift). +. *Census:* ✅ done 2026-10-14. All 43 orphaned modules (38 directories +plus `+boj_live+`, `+compat+`, `+echidna_live+`, `+error+`, +`+verisim_live+`) are declared in `+lib.rs+`. None needed +`+gossamer_rs+`; all compiled with no API drift. Clippy/rustfmt fixes +applied and Tauri leftovers removed. `+cargo test --lib+`: 285 passing +(was 23). Handlers are still *not registered* in `+register_commands+`. . *Bulk wire* in dependency/risk order using the proven pattern; de-Tauri as we go; cross-check command names against the ReScript frontend `+invoke()+` contract so we wire what the UI actually calls. @@ -171,8 +175,10 @@ Original debt items: 5 (D1–D5) Resolved: 5 (all — 2026-05-17) Follow-up debt opened: 3 (F1–F3, see above) Build status: cargo check / clippy green; bin link needs GTK env -Clippy -D warnings: clean (0, all targets) -Lib tests: 23 passing (cargo test --lib, GTK-free) +Clippy -D warnings: clean (0, all targets, default features) +Lib tests: 285 passing (cargo test --lib, GTK-free; 2026-10-14) +F1 census: done — 43 modules compile in the lib; 36 IPC + commands registered, rest pending .... === Success Criteria diff --git a/src-gossamer/src/a2ml/commands.rs b/src-gossamer/src/a2ml/commands.rs index e5275c4d..ac1c2b3d 100644 --- a/src-gossamer/src/a2ml/commands.rs +++ b/src-gossamer/src/a2ml/commands.rs @@ -39,14 +39,10 @@ fn collect_a2ml_files(dir: &Path, results: &mut Vec, root: &Path) { // non-source directories to keep discovery fast. let name = entry.file_name(); let name_str = name.to_string_lossy(); - if name_str == ".machine_readable" - || name_str == "panel-clades" - || name_str == "layouts" - || name_str == "src" - || name_str == "docs" - { - collect_a2ml_files(&path, results, root); - } else if !name_str.starts_with('.') && name_str != "node_modules" && name_str != "target" && name_str != "lib" { + let skip = name_str != ".machine_readable" + && (name_str.starts_with('.') + || matches!(name_str.as_ref(), "node_modules" | "target" | "lib")); + if !skip { collect_a2ml_files(&path, results, root); } } else if let Some(ext) = path.extension() { @@ -66,7 +62,6 @@ fn collect_a2ml_files(dir: &Path, results: &mut Vec, root: &Path) { /// /// Returns the file content as a JSON object with `path` and `content` fields. /// The ReScript A2mlEngine handles the actual parsing. - pub fn a2ml_load_manifest(path: String) -> Result { let file_path = if Path::new(&path).is_absolute() { PathBuf::from(&path) @@ -100,7 +95,6 @@ pub fn a2ml_load_manifest(path: String) -> Result { /// - File contains recognisable A2ML markers (sections, S-expressions, or headers) /// /// Returns a JSON object with `valid`, `errors`, and `warnings` fields. - pub fn a2ml_validate(path: String) -> Result { let file_path = if Path::new(&path).is_absolute() { PathBuf::from(&path) @@ -152,7 +146,9 @@ pub fn a2ml_validate(path: String) -> Result { let has_spdx = trimmed.contains("SPDX-License-Identifier"); if !has_sexpr && !has_sections && !has_dividers { - warnings.push("No recognisable A2ML structure (S-expressions, sections, or headers)".to_string()); + warnings.push( + "No recognisable A2ML structure (S-expressions, sections, or headers)".to_string(), + ); } if !has_spdx { @@ -185,7 +181,6 @@ pub fn a2ml_validate(path: String) -> Result { /// Recursively searches the repository tree (excluding `node_modules`, `target`, /// `lib`, and hidden directories other than `.machine_readable`). Returns a /// JSON array of relative file paths. - pub fn a2ml_list() -> Result { let root = find_repo_root()?; let mut files: Vec = Vec::new(); @@ -213,10 +208,10 @@ mod tests { fn test_find_repo_root_from_subdir() { // This test will work when run from within the PanLL repo let result = find_repo_root(); - // Should succeed or fail gracefully — we just check it doesn't panic - match result { - Ok(path) => assert!(path.exists()), - Err(_) => {} // OK if not in a repo context + // Should succeed or fail gracefully — we just check it doesn't panic. + // An Err is fine when not in a repo context. + if let Ok(path) = result { + assert!(path.exists()); } } diff --git a/src-gossamer/src/aerie/commands.rs b/src-gossamer/src/aerie/commands.rs index 42e9df71..e15bfafa 100644 --- a/src-gossamer/src/aerie/commands.rs +++ b/src-gossamer/src/aerie/commands.rs @@ -9,15 +9,14 @@ //! These run in-process (no external service needed) using std::net for //! latency and reqwest for download throughput measurement. +use serde_json::json; use std::net::TcpStream; use std::time::Instant; -use serde_json::json; /// Default target for latency probes. Override with AERIE_LATENCY_TARGET env var. /// Format: "host:port". fn latency_target() -> String { - std::env::var("AERIE_LATENCY_TARGET") - .unwrap_or_else(|_| "1.1.1.1:443".to_string()) + std::env::var("AERIE_LATENCY_TARGET").unwrap_or_else(|_| "1.1.1.1:443".to_string()) } /// Default URL for speed test downloads. Override with AERIE_SPEED_URL env var. @@ -37,7 +36,6 @@ fn speed_test_url() -> String { /// runtime for too long. For a truly non-blocking version, this would need /// tokio::net::TcpStream, but the timeout keeps it safe enough for a panel /// probe that runs on user action. - pub async fn aerie_get_latency() -> Result { let target = latency_target(); let addr: std::net::SocketAddr = target @@ -55,15 +53,13 @@ pub async fn aerie_get_latency() -> Result { }) .to_string()) } - Err(e) => { - Ok(json!({ - "latency_ms": null, - "target": target, - "status": "unreachable", - "error": format!("{e}"), - }) - .to_string()) - } + Err(e) => Ok(json!({ + "latency_ms": null, + "target": target, + "status": "unreachable", + "error": format!("{e}"), + }) + .to_string()), } } @@ -72,7 +68,6 @@ pub async fn aerie_get_latency() -> Result { /// Downloads a 1 MB payload from the configured URL and measures throughput. /// Returns JSON with `speed_mbps`, `bytes_downloaded`, `duration_ms`, and /// `status` fields. On failure, returns `status: "failed"` with the error. - pub async fn aerie_speed_test() -> Result { let url = speed_test_url(); let client = reqwest::Client::builder() @@ -83,7 +78,10 @@ pub async fn aerie_speed_test() -> Result { let start = Instant::now(); match client.get(&url).send().await { Ok(resp) => { - let bytes = resp.bytes().await.map_err(|e| format!("Download error: {e}"))?; + let bytes = resp + .bytes() + .await + .map_err(|e| format!("Download error: {e}"))?; let elapsed = start.elapsed(); let bytes_count = bytes.len() as f64; let duration_secs = elapsed.as_secs_f64(); @@ -99,17 +97,15 @@ pub async fn aerie_speed_test() -> Result { }) .to_string()) } - Err(e) => { - Ok(json!({ - "speed_mbps": null, - "bytes_downloaded": 0, - "duration_ms": 0, - "url": url, - "status": "failed", - "error": format!("{e}"), - }) - .to_string()) - } + Err(e) => Ok(json!({ + "speed_mbps": null, + "bytes_downloaded": 0, + "duration_ms": 0, + "url": url, + "status": "failed", + "error": format!("{e}"), + }) + .to_string()), } } diff --git a/src-gossamer/src/ai/commands.rs b/src-gossamer/src/ai/commands.rs index 15693c2e..e7551431 100644 --- a/src-gossamer/src/ai/commands.rs +++ b/src-gossamer/src/ai/commands.rs @@ -34,8 +34,7 @@ fn load_config() -> Result { save_config(&defaults)?; return Ok(defaults); } - let content = - fs::read_to_string(&path).map_err(|e| format!("Cannot read config: {e}"))?; + let content = fs::read_to_string(&path).map_err(|e| format!("Cannot read config: {e}"))?; serde_json::from_str(&content).map_err(|e| format!("Cannot parse config: {e}")) } @@ -44,8 +43,7 @@ fn save_config(config: &AiProvidersFile) -> Result<(), String> { let path = config_path()?; // Ensure parent directory exists. if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .map_err(|e| format!("Cannot create config directory: {e}"))?; + fs::create_dir_all(parent).map_err(|e| format!("Cannot create config directory: {e}"))?; } let json = serde_json::to_string_pretty(config) .map_err(|e| format!("Cannot serialise config: {e}"))?; @@ -57,7 +55,6 @@ fn save_config(config: &AiProvidersFile) -> Result<(), String> { /// Returns the provider's response as a JSON-serialised `SendMessageResponse`. /// If the provider returns 429, the response has `quota_exhausted: true` and /// the frontend should retry with the next provider. - pub async fn ai_send_message(request: SendMessageRequest) -> Result { let config_file = load_config()?; @@ -72,16 +69,13 @@ pub async fn ai_send_message(request: SendMessageRequest) -> Result = config_file - .providers - .iter() - .filter(|p| p.enabled) - .collect(); + let mut candidates: Vec<&ProviderConfig> = + config_file.providers.iter().filter(|p| p.enabled).collect(); candidates.sort_by_key(|p| p.priority); (*candidates .first() .ok_or("No enabled providers configured")?) - .clone() + .clone() }; let response = providers::send_message( @@ -99,12 +93,10 @@ pub async fn ai_send_message(request: SendMessageRequest) -> Result Result { let config_file = load_config()?; - let pid: ProviderId = - serde_json::from_str(&format!("\"{}\"", provider_id.to_lowercase())) - .map_err(|_| format!("Unknown provider: {provider_id}"))?; + let pid: ProviderId = serde_json::from_str(&format!("\"{}\"", provider_id.to_lowercase())) + .map_err(|_| format!("Unknown provider: {provider_id}"))?; let provider = config_file .providers @@ -168,12 +160,10 @@ pub async fn ai_check_provider(provider_id: String) -> Result { } /// Change the selected model for a provider. - pub async fn ai_set_model(provider_id: String, model: String) -> Result { let mut config_file = load_config()?; - let pid: ProviderId = - serde_json::from_str(&format!("\"{}\"", provider_id.to_lowercase())) - .map_err(|_| format!("Unknown provider: {provider_id}"))?; + let pid: ProviderId = serde_json::from_str(&format!("\"{}\"", provider_id.to_lowercase())) + .map_err(|_| format!("Unknown provider: {provider_id}"))?; let found = config_file .providers @@ -188,12 +178,10 @@ pub async fn ai_set_model(provider_id: String, model: String) -> Result Result { let mut config_file = load_config()?; - let pid: ProviderId = - serde_json::from_str(&format!("\"{}\"", provider_id.to_lowercase())) - .map_err(|_| format!("Unknown provider: {provider_id}"))?; + let pid: ProviderId = serde_json::from_str(&format!("\"{}\"", provider_id.to_lowercase())) + .map_err(|_| format!("Unknown provider: {provider_id}"))?; let found = config_file .providers @@ -208,12 +196,10 @@ pub async fn ai_set_priority(provider_id: String, priority: u32) -> Result Result { let mut config_file = load_config()?; - let pid: ProviderId = - serde_json::from_str(&format!("\"{}\"", provider_id.to_lowercase())) - .map_err(|_| format!("Unknown provider: {provider_id}"))?; + let pid: ProviderId = serde_json::from_str(&format!("\"{}\"", provider_id.to_lowercase())) + .map_err(|_| format!("Unknown provider: {provider_id}"))?; let found = config_file .providers @@ -231,7 +217,6 @@ pub async fn ai_toggle_provider(provider_id: String) -> Result { /// Clear the conversation history. This is a frontend-only operation; /// this command exists as a hook for any backend cleanup needed later /// (e.g., clearing cached token counts). - pub async fn ai_clear_history() -> Result { Ok(serde_json::json!({ "cleared": true }).to_string()) } @@ -240,7 +225,6 @@ pub async fn ai_clear_history() -> Result { /// /// Reads SCM files, AI manifest, detected languages, and assembles a /// structured context string for the AI system prompt. - pub async fn ai_build_context(repo_path: String) -> Result { let context_str = context::build_repo_context(&repo_path)?; Ok(serde_json::json!({ "context": context_str }).to_string()) @@ -250,7 +234,6 @@ pub async fn ai_build_context(repo_path: String) -> Result { /// /// Returns the full `AiProvidersFile` as JSON. The frontend uses this /// to initialise the provider key ring on panel open. - pub async fn ai_get_state() -> Result { let config_file = load_config()?; serde_json::to_string(&config_file).map_err(|e| format!("Serialisation error: {e}")) @@ -265,7 +248,6 @@ pub async fn ai_get_state() -> Result { /// /// Returns `"streaming_started"` immediately. Errors during streaming are /// emitted as `StreamChunk::Error` events rather than returned here. - pub async fn ai_send_message_streaming( app_handle: crate::compat::AppHandle, request: StreamingRequest, @@ -284,16 +266,13 @@ pub async fn ai_send_message_streaming( .clone() } else { // Auto-select: highest priority, enabled, Anthropic preferred. - let mut candidates: Vec<&ProviderConfig> = config_file - .providers - .iter() - .filter(|p| p.enabled) - .collect(); + let mut candidates: Vec<&ProviderConfig> = + config_file.providers.iter().filter(|p| p.enabled).collect(); candidates.sort_by_key(|p| p.priority); (*candidates .first() .ok_or("No enabled providers configured")?) - .clone() + .clone() }; // Verify this is Anthropic — streaming is only implemented for Anthropic. @@ -328,10 +307,7 @@ pub async fn ai_send_message_streaming( .await { use crate::compat::Emitter; - let _ = app_handle.emit( - "ai:stream-chunk", - &StreamChunk::Error(e), - ); + let _ = app_handle.emit("ai:stream-chunk", &StreamChunk::Error(e)); } }); diff --git a/src-gossamer/src/ai/mod.rs b/src-gossamer/src/ai/mod.rs index f9cff886..00b4cb80 100644 --- a/src-gossamer/src/ai/mod.rs +++ b/src-gossamer/src/ai/mod.rs @@ -12,7 +12,7 @@ //! - `context.rs`: System prompt assembly from repo SCM files, panels, VoiceTags //! - `commands.rs`: Tauri command handlers exposed to the ReScript frontend -pub mod types; -pub mod providers; -pub mod context; pub mod commands; +pub mod context; +pub mod providers; +pub mod types; diff --git a/src-gossamer/src/ai/providers.rs b/src-gossamer/src/ai/providers.rs index e13678a8..fb41b0c3 100644 --- a/src-gossamer/src/ai/providers.rs +++ b/src-gossamer/src/ai/providers.rs @@ -117,7 +117,10 @@ pub async fn send_anthropic( }); } - let resp_text = resp.text().await.map_err(|e| format!("Body read error: {e}"))?; + let resp_text = resp + .text() + .await + .map_err(|e| format!("Body read error: {e}"))?; if !status.is_success() { return Err(format!("Anthropic HTTP {}: {}", status, resp_text)); } @@ -213,7 +216,10 @@ pub async fn send_google( }); } - let resp_text = resp.text().await.map_err(|e| format!("Body read error: {e}"))?; + let resp_text = resp + .text() + .await + .map_err(|e| format!("Body read error: {e}"))?; if !status.is_success() { return Err(format!("Google HTTP {}: {}", status, resp_text)); } @@ -353,7 +359,10 @@ async fn send_openai_compatible( }); } - let resp_text = resp.text().await.map_err(|e| format!("Body read error: {e}"))?; + let resp_text = resp + .text() + .await + .map_err(|e| format!("Body read error: {e}"))?; if !status.is_success() { return Err(format!("{:?} HTTP {}: {}", provider_id, status, resp_text)); } @@ -369,9 +378,7 @@ async fn send_openai_compatible( .to_string(); let input_tokens = parsed["usage"]["prompt_tokens"].as_u64().unwrap_or(0) as u32; - let output_tokens = parsed["usage"]["completion_tokens"] - .as_u64() - .unwrap_or(0) as u32; + let output_tokens = parsed["usage"]["completion_tokens"].as_u64().unwrap_or(0) as u32; Ok(SendMessageResponse { content, @@ -432,7 +439,10 @@ pub async fn send_local( .map_err(|e| format!("Local (Ollama) request failed: {e}"))?; let status = resp.status(); - let resp_text = resp.text().await.map_err(|e| format!("Body read error: {e}"))?; + let resp_text = resp + .text() + .await + .map_err(|e| format!("Body read error: {e}"))?; if !status.is_success() { return Err(format!("Local HTTP {}: {}", status, resp_text)); } @@ -480,8 +490,8 @@ pub async fn send_anthropic_streaming( tool_results: Option<&[ToolResult]>, app_handle: crate::compat::AppHandle, ) -> Result<(), String> { - use futures::StreamExt; use crate::compat::Emitter; + use futures::StreamExt; let api_key = resolve_api_key(config)?; @@ -628,10 +638,8 @@ pub async fn send_anthropic_streaming( .as_str() .unwrap_or("") .to_string(); - let _ = app_handle.emit( - "ai:stream-chunk", - &StreamChunk::ToolUseStart { id, name }, - ); + let _ = app_handle + .emit("ai:stream-chunk", &StreamChunk::ToolUseStart { id, name }); } // text blocks: wait for deltas } @@ -639,37 +647,29 @@ pub async fn send_anthropic_streaming( let delta_type = parsed["delta"]["type"].as_str().unwrap_or(""); match delta_type { "text_delta" => { - let text = parsed["delta"]["text"] - .as_str() - .unwrap_or("") - .to_string(); - let _ = app_handle - .emit("ai:stream-chunk", &StreamChunk::TextDelta(text)); + let text = parsed["delta"]["text"].as_str().unwrap_or("").to_string(); + let _ = + app_handle.emit("ai:stream-chunk", &StreamChunk::TextDelta(text)); } "input_json_delta" => { let partial_json = parsed["delta"]["partial_json"] .as_str() .unwrap_or("") .to_string(); - let _ = app_handle.emit( - "ai:stream-chunk", - &StreamChunk::ToolUseDelta(partial_json), - ); + let _ = app_handle + .emit("ai:stream-chunk", &StreamChunk::ToolUseDelta(partial_json)); } _ => {} } } "content_block_stop" => { if in_tool_use { - let _ = - app_handle.emit("ai:stream-chunk", &StreamChunk::ToolUseEnd); + let _ = app_handle.emit("ai:stream-chunk", &StreamChunk::ToolUseEnd); in_tool_use = false; } } "message_delta" => { - output_tokens = parsed["usage"]["output_tokens"] - .as_u64() - .unwrap_or(0) as u32; + output_tokens = parsed["usage"]["output_tokens"].as_u64().unwrap_or(0) as u32; } "message_stop" => { let _ = app_handle.emit( @@ -685,8 +685,7 @@ pub async fn send_anthropic_streaming( .as_str() .unwrap_or("Unknown streaming error") .to_string(); - let _ = app_handle - .emit("ai:stream-chunk", &StreamChunk::Error(error_msg)); + let _ = app_handle.emit("ai:stream-chunk", &StreamChunk::Error(error_msg)); } _ => {} } @@ -704,7 +703,9 @@ pub async fn send_message( user_content: &str, ) -> Result { match config.id { - ProviderId::Anthropic => send_anthropic(config, system_prompt, messages, user_content).await, + ProviderId::Anthropic => { + send_anthropic(config, system_prompt, messages, user_content).await + } ProviderId::Google => send_google(config, system_prompt, messages, user_content).await, ProviderId::Mistral => send_mistral(config, system_prompt, messages, user_content).await, ProviderId::OpenAI => send_openai(config, system_prompt, messages, user_content).await, diff --git a/src-gossamer/src/ai/types.rs b/src-gossamer/src/ai/types.rs index 78f1974a..22efd873 100644 --- a/src-gossamer/src/ai/types.rs +++ b/src-gossamer/src/ai/types.rs @@ -131,7 +131,10 @@ pub enum StreamChunk { /// The tool_use content block is complete; input JSON is fully accumulated. ToolUseEnd, /// The message is complete. Carries final token usage. - Complete { input_tokens: u32, output_tokens: u32 }, + Complete { + input_tokens: u32, + output_tokens: u32, + }, /// An error occurred during streaming. Error(String), } @@ -256,20 +259,28 @@ mod tests { ]; for id in &ids { let json = serde_json::to_string(id).expect("ProviderId must serialise"); - let _back: ProviderId = serde_json::from_str(&json).expect("ProviderId must deserialise"); + let _back: ProviderId = + serde_json::from_str(&json).expect("ProviderId must deserialise"); } } #[test] fn smoke_ai_providers_file_defaults_has_five_providers() { let file = AiProvidersFile::defaults(); - assert_eq!(file.providers.len(), 5, "defaults() must return 5 providers"); + assert_eq!( + file.providers.len(), + 5, + "defaults() must return 5 providers" + ); } #[test] fn smoke_ai_providers_file_defaults_anthropic_is_enabled_and_priority_one() { let file = AiProvidersFile::defaults(); - let anthropic = file.providers.iter().find(|p| p.id == ProviderId::Anthropic) + let anthropic = file + .providers + .iter() + .find(|p| p.id == ProviderId::Anthropic) .expect("Anthropic provider must exist"); assert!(anthropic.enabled, "Anthropic must be enabled by default"); assert_eq!(anthropic.priority, 1, "Anthropic must have priority 1"); diff --git a/src-gossamer/src/boj/commands.rs b/src-gossamer/src/boj/commands.rs index 914d1003..e3e40fe9 100644 --- a/src-gossamer/src/boj/commands.rs +++ b/src-gossamer/src/boj/commands.rs @@ -54,43 +54,36 @@ fn post(path: &str, body: serde_json::Value, timeout_secs: u64) -> Result Result { get("/health", 5) } /// GET /cartridges — list all loaded cartridges. - pub fn boj_list_cartridges() -> Result { get("/cartridges", 10) } /// GET /cartridges/{name} — get details of a specific cartridge. - pub fn boj_get_cartridge(name: String) -> Result { get(&format!("/cartridges/{}", name), 10) } /// POST /cartridges/{name}/load — load a cartridge. - pub fn boj_load_cartridge(name: String) -> Result { post(&format!("/cartridges/{}/load", name), json!({}), 30) } /// POST /cartridges/{name}/unload — unload a cartridge. - pub fn boj_unload_cartridge(name: String) -> Result { post(&format!("/cartridges/{}/unload", name), json!({}), 10) } /// GET /topology — get the BoJ service topology matrix. - pub fn boj_topology() -> Result { get("/topology", 10) } /// POST /cartridges/{name}/invoke — invoke a cartridge tool/function. - pub fn boj_invoke(name: String, tool: String, args: Option) -> Result { let parsed_args: serde_json::Value = args .and_then(|a| serde_json::from_str(&a).ok()) @@ -103,7 +96,6 @@ pub fn boj_invoke(name: String, tool: String, args: Option) -> Result Result { get("/umoja/status", 10) } diff --git a/src-gossamer/src/boj_live.rs b/src-gossamer/src/boj_live.rs index ac4b53a2..46b43c1e 100644 --- a/src-gossamer/src/boj_live.rs +++ b/src-gossamer/src/boj_live.rs @@ -45,7 +45,6 @@ pub struct BojHealth { /// Check BoJ-server health (async). /// /// `GET /health` — returns the full health envelope as a JSON string. - pub async fn boj_live_health() -> Result { let endpoint = boj_endpoint(); let health: BojHealth = http_client::get_json(&endpoint, "/health") @@ -57,7 +56,6 @@ pub async fn boj_live_health() -> Result { /// List available cartridges from BoJ-server (async). /// /// `GET /cartridges` — returns the cartridge array as a JSON string. - pub async fn boj_live_cartridges() -> Result { let endpoint = boj_endpoint(); let cartridges: serde_json::Value = http_client::get_json(&endpoint, "/cartridges") @@ -70,15 +68,13 @@ pub async fn boj_live_cartridges() -> Result { /// /// `POST /cartridges/{cartridge}/invoke` — sends `{"tool": ..., "args": ...}` /// and returns the invocation result as a JSON string. - pub async fn boj_live_invoke( cartridge: String, tool: String, params: String, ) -> Result { let endpoint = boj_endpoint(); - let parsed_params: serde_json::Value = - serde_json::from_str(¶ms).unwrap_or(json!({})); + let parsed_params: serde_json::Value = serde_json::from_str(¶ms).unwrap_or(json!({})); let body = json!({ "tool": tool, "args": parsed_params, @@ -96,7 +92,6 @@ pub async fn boj_live_invoke( /// Get BoJ topology / cartridge dependency graph (async). /// /// `GET /topology` — returns the topology matrix as a JSON string. - pub async fn boj_live_topology() -> Result { let endpoint = boj_endpoint(); let topology: serde_json::Value = http_client::get_json(&endpoint, "/topology") @@ -109,7 +104,6 @@ pub async fn boj_live_topology() -> Result { /// /// Returns `{"reachable": bool, "endpoint": "..."}` as a JSON string. /// Panels use this for connection-dot indicators in the panel bar. - pub async fn boj_live_check() -> Result { let endpoint = boj_endpoint(); let reachable = http_client::check_health(&endpoint, "/health").await?; diff --git a/src-gossamer/src/capture/commands.rs b/src-gossamer/src/capture/commands.rs index e8fb699a..c1a75793 100644 --- a/src-gossamer/src/capture/commands.rs +++ b/src-gossamer/src/capture/commands.rs @@ -8,26 +8,22 @@ //! Demo packages are ZIP files containing captures + state JSON. use std::fs; -use std::path::PathBuf; use std::io::Write; +use std::path::PathBuf; /// Get the PanLL captures directory, creating it if needed. fn captures_dir() -> Result { - let base = dirs::data_dir() - .ok_or_else(|| "Cannot determine data directory".to_string())?; + let base = dirs::data_dir().ok_or_else(|| "Cannot determine data directory".to_string())?; let dir = base.join("panll").join("captures"); - fs::create_dir_all(&dir) - .map_err(|e| format!("Cannot create captures dir: {e}"))?; + fs::create_dir_all(&dir).map_err(|e| format!("Cannot create captures dir: {e}"))?; Ok(dir) } /// Get the PanLL demos directory, creating it if needed. fn demos_dir() -> Result { - let base = dirs::data_dir() - .ok_or_else(|| "Cannot determine data directory".to_string())?; + let base = dirs::data_dir().ok_or_else(|| "Cannot determine data directory".to_string())?; let dir = base.join("panll").join("demos"); - fs::create_dir_all(&dir) - .map_err(|e| format!("Cannot create demos dir: {e}"))?; + fs::create_dir_all(&dir).map_err(|e| format!("Cannot create demos dir: {e}"))?; Ok(dir) } @@ -36,7 +32,6 @@ fn demos_dir() -> Result { /// The frontend captures a panel's DOM via html2canvas, converts to base64, /// and sends it here for persistence. File naming uses the capture ID for /// deterministic paths. - pub async fn save_screenshot( capture_id: String, panel_id: String, @@ -65,8 +60,7 @@ pub async fn save_screenshot( // For now, write raw base64 and let the frontend handle display. let decoded = base64_decode(raw_b64)?; - let mut file = fs::File::create(&path) - .map_err(|e| format!("Cannot create file: {e}"))?; + let mut file = fs::File::create(&path).map_err(|e| format!("Cannot create file: {e}"))?; file.write_all(&decoded) .map_err(|e| format!("Write error: {e}"))?; @@ -84,7 +78,10 @@ pub async fn save_screenshot( fn base64_decode(input: &str) -> Result, String> { let table: [u8; 256] = { let mut t = [255u8; 256]; - for (i, &c) in b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".iter().enumerate() { + for (i, &c) in b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + .iter() + .enumerate() + { t[c as usize] = i as u8; } t[b'=' as usize] = 0; @@ -117,7 +114,6 @@ fn base64_decode(input: &str) -> Result, String> { /// Invoke the system print dialog for the active panel. /// Uses Tauri's webview print capability. - pub async fn print_panel(panel_id: String) -> Result { // The actual printing is triggered in the frontend via window.print(). // This command exists for logging and to return the panel ID for confirmation. @@ -125,27 +121,24 @@ pub async fn print_panel(panel_id: String) -> Result { } /// Save a demo package as a JSON file. Full ZIP packaging is a future enhancement. - pub async fn save_demo(demo_json: String) -> Result { let dir = demos_dir()?; - let demo: super::types::DemoPackage = serde_json::from_str(&demo_json) - .map_err(|e| format!("Invalid demo JSON: {e}"))?; + let demo: super::types::DemoPackage = + serde_json::from_str(&demo_json).map_err(|e| format!("Invalid demo JSON: {e}"))?; let filename = format!("{}.panll-demo.json", demo.id); let path = dir.join(&filename); - let pretty = serde_json::to_string_pretty(&demo) - .map_err(|e| format!("Serialisation error: {e}"))?; + let pretty = + serde_json::to_string_pretty(&demo).map_err(|e| format!("Serialisation error: {e}"))?; - fs::write(&path, &pretty) - .map_err(|e| format!("Write error: {e}"))?; + fs::write(&path, &pretty).map_err(|e| format!("Write error: {e}"))?; Ok(format!("Demo '{}' saved to {}", demo.title, path.display())) } /// Load all demo packages from the demos directory. - pub async fn load_demos() -> Result { let dir = demos_dir()?; if !dir.exists() { @@ -153,8 +146,7 @@ pub async fn load_demos() -> Result { } let mut demos: Vec = Vec::new(); - let entries = fs::read_dir(&dir) - .map_err(|e| format!("Cannot read demos dir: {e}"))?; + let entries = fs::read_dir(&dir).map_err(|e| format!("Cannot read demos dir: {e}"))?; for entry in entries { let entry = entry.map_err(|e| format!("Dir entry error: {e}"))?; @@ -169,20 +161,17 @@ pub async fn load_demos() -> Result { } } - serde_json::to_string(&demos) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&demos).map_err(|e| format!("Serialisation error: {e}")) } /// Delete a demo package from disk. - pub async fn delete_demo(demo_id: String) -> Result { let dir = demos_dir()?; let filename = format!("{demo_id}.panll-demo.json"); let path = dir.join(&filename); if path.exists() { - fs::remove_file(&path) - .map_err(|e| format!("Delete error: {e}"))?; + fs::remove_file(&path).map_err(|e| format!("Delete error: {e}"))?; Ok(format!("Demo '{demo_id}' deleted")) } else { Err(format!("Demo '{demo_id}' not found")) diff --git a/src-gossamer/src/capture/mod.rs b/src-gossamer/src/capture/mod.rs index 8497a891..db71e350 100644 --- a/src-gossamer/src/capture/mod.rs +++ b/src-gossamer/src/capture/mod.rs @@ -11,5 +11,5 @@ //! and sent to Rust for file I/O. Recordings use ffmpeg as an external //! subprocess for screen capture. -pub mod types; pub mod commands; +pub mod types; diff --git a/src-gossamer/src/capture/types.rs b/src-gossamer/src/capture/types.rs index 4a5e66d3..0ec4b1d6 100644 --- a/src-gossamer/src/capture/types.rs +++ b/src-gossamer/src/capture/types.rs @@ -68,7 +68,8 @@ mod tests { let formats = [CaptureFormat::Png, CaptureFormat::Pdf, CaptureFormat::Svg]; for fmt in formats { let json = serde_json::to_string(&fmt).expect("serialise CaptureFormat must succeed"); - let _back: CaptureFormat = serde_json::from_str(&json).expect("deserialise must succeed"); + let _back: CaptureFormat = + serde_json::from_str(&json).expect("deserialise must succeed"); } } diff --git a/src-gossamer/src/clade_scanner/commands.rs b/src-gossamer/src/clade_scanner/commands.rs index c97a0b99..1796d1f3 100644 --- a/src-gossamer/src/clade_scanner/commands.rs +++ b/src-gossamer/src/clade_scanner/commands.rs @@ -34,7 +34,6 @@ fn clades_dir() -> PathBuf { /// /// Each entry is `{"id": "", "content": ""}`. /// Directories without an `.a2ml` file are silently skipped. - pub async fn scan_clade_files() -> Result { let dir = clades_dir(); if !dir.is_dir() { diff --git a/src-gossamer/src/cloudguard/api.rs b/src-gossamer/src/cloudguard/api.rs index 5d438433..ebf5d65a 100644 --- a/src-gossamer/src/cloudguard/api.rs +++ b/src-gossamer/src/cloudguard/api.rs @@ -29,9 +29,8 @@ const CF_API_BASE: &str = "https://api.cloudflare.com/client/v4"; const MIN_REQUEST_INTERVAL: Duration = Duration::from_millis(333); /// Thread-safe timestamp of the last API request, used for rate limiting. -static LAST_REQUEST: Lazy> = Lazy::new(|| { - Mutex::new(Instant::now() - MIN_REQUEST_INTERVAL) -}); +static LAST_REQUEST: Lazy> = + Lazy::new(|| Mutex::new(Instant::now() - MIN_REQUEST_INTERVAL)); /// Returns the CF API base URL, respecting the `CLOUDFLARE_API_URL` override. fn cf_api_base() -> String { @@ -42,8 +41,9 @@ fn cf_api_base() -> String { /// In the Tauri app, this is set from the OS keyring at startup. /// Returns an error if no token is configured. fn cf_api_token() -> Result { - env::var("CLOUDFLARE_API_TOKEN") - .map_err(|_| "CLOUDFLARE_API_TOKEN not set. Configure your Cloudflare API token.".to_string()) + env::var("CLOUDFLARE_API_TOKEN").map_err(|_| { + "CLOUDFLARE_API_TOKEN not set. Configure your Cloudflare API token.".to_string() + }) } /// Waits if necessary to respect the rate limit, then updates the timestamp. @@ -179,7 +179,8 @@ pub fn get_zone_settings(zone_id: &str) -> Result, String> { return Err(extract_cf_errors(&body)); } - body.result.ok_or_else(|| "No settings returned".to_string()) + body.result + .ok_or_else(|| "No settings returned".to_string()) } /// Update a single zone setting by ID. @@ -192,7 +193,12 @@ pub fn update_zone_setting( let client = http_client(10)?; rate_limit_wait(); - let url = format!("{}/zones/{}/settings/{}", cf_api_base(), zone_id, setting_id); + let url = format!( + "{}/zones/{}/settings/{}", + cf_api_base(), + zone_id, + setting_id + ); let patch = CfSettingPatch { value }; let resp = client @@ -210,7 +216,8 @@ pub fn update_zone_setting( return Err(extract_cf_errors(&body)); } - body.result.ok_or_else(|| "Setting update returned no result".to_string()) + body.result + .ok_or_else(|| "Setting update returned no result".to_string()) } /// Batch-update multiple settings for a zone in a single API call. @@ -244,7 +251,8 @@ pub fn update_zone_settings_batch( return Err(extract_cf_errors(&body)); } - body.result.ok_or_else(|| "Batch settings update returned no result".to_string()) + body.result + .ok_or_else(|| "Batch settings update returned no result".to_string()) } // ============================================================================ @@ -299,10 +307,7 @@ pub fn list_dns_records(zone_id: &str) -> Result, String> { } /// Create a new DNS record in a zone. -pub fn create_dns_record( - zone_id: &str, - record: &CfDnsRecordCreate, -) -> Result { +pub fn create_dns_record(zone_id: &str, record: &CfDnsRecordCreate) -> Result { let token = cf_api_token()?; let client = http_client(10)?; rate_limit_wait(); @@ -323,7 +328,8 @@ pub fn create_dns_record( return Err(extract_cf_errors(&body)); } - body.result.ok_or_else(|| "DNS record creation returned no result".to_string()) + body.result + .ok_or_else(|| "DNS record creation returned no result".to_string()) } /// Update an existing DNS record. @@ -357,7 +363,8 @@ pub fn update_dns_record( return Err(extract_cf_errors(&body)); } - body.result.ok_or_else(|| "DNS record update returned no result".to_string()) + body.result + .ok_or_else(|| "DNS record update returned no result".to_string()) } /// Delete a DNS record by ID. @@ -415,7 +422,8 @@ pub fn get_dnssec_status(zone_id: &str) -> Result { return Err(extract_cf_errors(&body)); } - body.result.ok_or_else(|| "No DNSSEC status returned".to_string()) + body.result + .ok_or_else(|| "No DNSSEC status returned".to_string()) } /// Enable DNSSEC for a zone via `PATCH /zones/{zone_id}/dnssec`. @@ -440,7 +448,8 @@ pub fn enable_dnssec(zone_id: &str) -> Result { return Err(extract_cf_errors(&body)); } - body.result.ok_or_else(|| "DNSSEC enable returned no result".to_string()) + body.result + .ok_or_else(|| "DNSSEC enable returned no result".to_string()) } // ============================================================================ diff --git a/src-gossamer/src/cloudguard/commands.rs b/src-gossamer/src/cloudguard/commands.rs index 2859ec01..e02bee75 100644 --- a/src-gossamer/src/cloudguard/commands.rs +++ b/src-gossamer/src/cloudguard/commands.rs @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MPL-2.0 -//! CloudGuard Tauri command handlers. +//! CloudGuard IPC command handlers. //! -//! Each `#[tauri::command]` function is a thin wrapper around the API client +//! Each command function is a thin wrapper around the API client //! (`super::api`) that serialises results to JSON strings for the frontend. -//! This matches the PanLL pattern where Tauri commands return `Result` -//! and the ReScript frontend parses the JSON. +//! This matches the PanLL pattern where IPC commands return +//! `Result` and the frontend parses the JSON. //! //! Commands: //! - `cloudguard_verify_token` — verify CF API token (health check) @@ -34,7 +34,6 @@ use super::types::*; /// Verify the Cloudflare API token. Returns a JSON string with token status. /// Frontend calls this on panel open and periodically to maintain connection state. - pub fn cloudguard_verify_token() -> Result { let status = api::verify_token()?; Ok(json!({ "status": "connected", "message": status }).to_string()) @@ -46,7 +45,6 @@ pub fn cloudguard_verify_token() -> Result { /// List all zones (domains) in the Cloudflare account. /// Returns a JSON array of zone objects. - pub fn cloudguard_list_zones() -> Result { let zones = api::list_zones()?; serde_json::to_string(&zones).map_err(|e| format!("JSON serialisation error: {}", e)) @@ -54,7 +52,6 @@ pub fn cloudguard_list_zones() -> Result { /// Get details for a single zone by ID. /// Returns a JSON zone object. - pub fn cloudguard_get_zone(zone_id: String) -> Result { let zone = api::get_zone(&zone_id)?; serde_json::to_string(&zone).map_err(|e| format!("JSON serialisation error: {}", e)) @@ -66,7 +63,6 @@ pub fn cloudguard_get_zone(zone_id: String) -> Result { /// Get all settings for a zone. /// Returns a JSON array of setting objects with id, value, editable, modified_on. - pub fn cloudguard_get_settings(zone_id: String) -> Result { let settings = api::get_zone_settings(&zone_id)?; serde_json::to_string(&settings).map_err(|e| format!("JSON serialisation error: {}", e)) @@ -75,7 +71,6 @@ pub fn cloudguard_get_settings(zone_id: String) -> Result { /// Update a single zone setting by ID. /// `value` is a JSON-encoded string that gets parsed to `serde_json::Value`. /// Returns the updated setting as JSON. - pub fn cloudguard_update_setting( zone_id: String, setting_id: String, @@ -90,7 +85,6 @@ pub fn cloudguard_update_setting( /// Batch-update multiple settings for a zone. /// `settings_json` is a JSON-encoded array of `[{"id": "...", "value": ...}]`. /// Returns the updated settings as JSON. - pub fn cloudguard_update_settings_batch( zone_id: String, settings_json: String, @@ -121,13 +115,12 @@ pub fn cloudguard_update_settings_batch( /// List all DNS records for a zone. /// Returns a JSON array of DNS record objects. - pub fn cloudguard_list_dns_records(zone_id: String) -> Result { let records = api::list_dns_records(&zone_id)?; serde_json::to_string(&records).map_err(|e| format!("JSON serialisation error: {}", e)) } -/// Deserialisable payload for creating a DNS record via Tauri command. +/// Deserialisable payload for creating a DNS record via IPC command. #[derive(Deserialize)] pub struct CreateDnsRecordPayload { pub zone_id: String, @@ -142,7 +135,6 @@ pub struct CreateDnsRecordPayload { /// Create a new DNS record in a zone. /// Returns the created record as JSON. - pub fn cloudguard_create_dns_record(payload: CreateDnsRecordPayload) -> Result { let record = CfDnsRecordCreate { record_type: payload.record_type, @@ -157,7 +149,7 @@ pub fn cloudguard_create_dns_record(payload: CreateDnsRecordPayload) -> Result Result { let patch = CfDnsRecordPatch { record_type: payload.record_type, @@ -190,7 +181,6 @@ pub fn cloudguard_update_dns_record(payload: UpdateDnsRecordPayload) -> Result Result { api::delete_dns_record(&zone_id, &record_id)?; Ok(json!({ "status": "deleted", "record_id": record_id }).to_string()) @@ -202,7 +192,6 @@ pub fn cloudguard_delete_dns_record(zone_id: String, record_id: String) -> Resul /// Get DNSSEC status for a zone. /// Returns DNSSEC status JSON including DS record info. - pub fn cloudguard_get_dnssec(zone_id: String) -> Result { let status = api::get_dnssec_status(&zone_id)?; serde_json::to_string(&status).map_err(|e| format!("JSON serialisation error: {}", e)) @@ -210,7 +199,6 @@ pub fn cloudguard_get_dnssec(zone_id: String) -> Result { /// Enable DNSSEC for a zone. /// Returns the updated DNSSEC status as JSON. - pub fn cloudguard_enable_dnssec(zone_id: String) -> Result { let status = api::enable_dnssec(&zone_id)?; serde_json::to_string(&status).map_err(|e| format!("JSON serialisation error: {}", e)) @@ -242,7 +230,6 @@ pub fn cloudguard_enable_dnssec(zone_id: String) -> Result { /// - 0rtt: "on" /// - brotli: "on" /// - early_hints: "on" - pub fn cloudguard_harden_zone(zone_id: String) -> Result { let hsts_value = json!({ "strict_transport_security": { @@ -286,7 +273,6 @@ pub fn cloudguard_harden_zone(zone_id: String) -> Result { /// Download the offline configuration for a zone. Fetches settings and DNS /// records, saves them to `~/.config/cloudguard/configs/{domain}.json`, /// and returns the file path and metadata. - pub fn cloudguard_download_config(zone_id: String) -> Result { let path = super::config::download_zone_config(&zone_id)?; let result = json!({ diff --git a/src-gossamer/src/cloudguard/config.rs b/src-gossamer/src/cloudguard/config.rs index 6fe188ee..46b2bc4c 100644 --- a/src-gossamer/src/cloudguard/config.rs +++ b/src-gossamer/src/cloudguard/config.rs @@ -85,8 +85,7 @@ pub fn download_zone_config(zone_id: &str) -> Result { let json = serde_json::to_string_pretty(&config) .map_err(|e| format!("JSON serialisation error: {}", e))?; - fs::write(&path, json) - .map_err(|e| format!("Failed to write config to {:?}: {}", path, e))?; + fs::write(&path, json).map_err(|e| format!("Failed to write config to {:?}: {}", path, e))?; Ok(path.to_string_lossy().to_string()) } @@ -120,8 +119,8 @@ pub fn list_saved_configs() -> Result { let dir = config_dir()?; let mut configs = Vec::new(); - let entries = fs::read_dir(&dir) - .map_err(|e| format!("Failed to read config directory: {}", e))?; + let entries = + fs::read_dir(&dir).map_err(|e| format!("Failed to read config directory: {}", e))?; for entry in entries { let entry = entry.map_err(|e| format!("Directory entry error: {}", e))?; @@ -141,8 +140,7 @@ pub fn list_saved_configs() -> Result { } } - serde_json::to_string(&configs) - .map_err(|e| format!("JSON serialisation error: {}", e)) + serde_json::to_string(&configs).map_err(|e| format!("JSON serialisation error: {}", e)) } /// Sanitise a domain name for use as a filename (replace dots with underscores, @@ -150,7 +148,13 @@ pub fn list_saved_configs() -> Result { fn sanitise_filename(domain: &str) -> String { domain .chars() - .map(|c| if c.is_alphanumeric() || c == '-' { c } else { '_' }) + .map(|c| { + if c.is_alphanumeric() || c == '-' { + c + } else { + '_' + } + }) .collect() } diff --git a/src-gossamer/src/cloudguard/diff.rs b/src-gossamer/src/cloudguard/diff.rs index b06b6fc0..18e77e2c 100644 --- a/src-gossamer/src/cloudguard/diff.rs +++ b/src-gossamer/src/cloudguard/diff.rs @@ -60,7 +60,13 @@ pub struct DiffResult { fn normalise_value(v: &Value) -> String { match v { Value::String(s) => s.clone(), - Value::Bool(b) => if *b { "on".to_string() } else { "off".to_string() }, + Value::Bool(b) => { + if *b { + "on".to_string() + } else { + "off".to_string() + } + } Value::Number(n) => n.to_string(), Value::Null => "null".to_string(), _ => serde_json::to_string(v).unwrap_or_else(|_| "?".to_string()), @@ -174,10 +180,15 @@ pub fn compute_diff( let timestamp = super::config::list_saved_configs() .ok() - .and_then(|_| Some(format!("{}Z", std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0)))) + .map(|_| { + format!( + "{}Z", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) + ) + }) .unwrap_or_default(); Ok(DiffResult { diff --git a/src-gossamer/src/cloudguard/mod.rs b/src-gossamer/src/cloudguard/mod.rs index 97def0db..a58e2186 100644 --- a/src-gossamer/src/cloudguard/mod.rs +++ b/src-gossamer/src/cloudguard/mod.rs @@ -12,8 +12,8 @@ //! `CLOUDFLARE_API_TOKEN` env var (CLI/CI). pub mod api; -pub mod types; pub mod commands; pub mod config; pub mod diff; pub mod trustfile; +pub mod types; diff --git a/src-gossamer/src/cloudguard/trustfile.rs b/src-gossamer/src/cloudguard/trustfile.rs index a38d8616..04be9614 100644 --- a/src-gossamer/src/cloudguard/trustfile.rs +++ b/src-gossamer/src/cloudguard/trustfile.rs @@ -234,7 +234,13 @@ pub fn default_constraints() -> Vec { fn normalise_setting_value(v: &Value) -> String { match v { Value::String(s) => format!("\"{}\"", s), - Value::Bool(b) => if *b { "\"on\"".to_string() } else { "\"off\"".to_string() }, + Value::Bool(b) => { + if *b { + "\"on\"".to_string() + } else { + "\"off\"".to_string() + } + } Value::Number(n) => n.to_string(), Value::Null => "null".to_string(), _ => serde_json::to_string(v).unwrap_or_else(|_| "?".to_string()), @@ -248,10 +254,8 @@ pub fn evaluate_constraints( settings: &[CfZoneSetting], constraints: &[PolicyConstraint], ) -> AuditResult { - let settings_map: std::collections::HashMap<&str, &Value> = settings - .iter() - .map(|s| (s.id.as_str(), &s.value)) - .collect(); + let settings_map: std::collections::HashMap<&str, &Value> = + settings.iter().map(|s| (s.id.as_str(), &s.value)).collect(); let mut findings = Vec::new(); let mut passed = 0u32; @@ -297,7 +301,11 @@ pub fn evaluate_constraints( } let total = (passed + findings.len() as u32) as f64; - let score = if total > 0.0 { passed as f64 / total } else { 0.0 }; + let score = if total > 0.0 { + passed as f64 / total + } else { + 0.0 + }; let timestamp = format!( "{}Z", @@ -321,17 +329,31 @@ pub fn evaluate_constraints( /// Check if the HSTS setting matches the expected configuration: /// enabled=true, max_age>=31536000, includeSubDomains=true, preload=true, nosniff=true. fn check_hsts_constraint(value: &Value) -> bool { - let sts = value - .get("strict_transport_security") - .or_else(|| value.get("value").and_then(|v| v.get("strict_transport_security"))); + let sts = value.get("strict_transport_security").or_else(|| { + value + .get("value") + .and_then(|v| v.get("strict_transport_security")) + }); match sts { Some(obj) => { - let enabled = obj.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false); + let enabled = obj + .get("enabled") + .and_then(|v| v.as_bool()) + .unwrap_or(false); let max_age = obj.get("max_age").and_then(|v| v.as_u64()).unwrap_or(0); - let subdomains = obj.get("include_subdomains").and_then(|v| v.as_bool()).unwrap_or(false); - let preload = obj.get("preload").and_then(|v| v.as_bool()).unwrap_or(false); - let nosniff = obj.get("nosniff").and_then(|v| v.as_bool()).unwrap_or(false); + let subdomains = obj + .get("include_subdomains") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let preload = obj + .get("preload") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let nosniff = obj + .get("nosniff") + .and_then(|v| v.as_bool()) + .unwrap_or(false); enabled && max_age >= 31536000 && subdomains && preload && nosniff } diff --git a/src-gossamer/src/cloudguard/types.rs b/src-gossamer/src/cloudguard/types.rs index 637a4b77..f5a89979 100644 --- a/src-gossamer/src/cloudguard/types.rs +++ b/src-gossamer/src/cloudguard/types.rs @@ -299,8 +299,8 @@ mod tests { #[test] fn smoke_cf_api_response_success_parse() { let json = r#"{"success":true,"errors":[],"messages":[],"result":"ok"}"#; - let resp: CfApiResponse = serde_json::from_str(json) - .expect("CfApiResponse must parse"); + let resp: CfApiResponse = + serde_json::from_str(json).expect("CfApiResponse must parse"); assert!(resp.success); assert!(resp.errors.is_empty()); assert_eq!(resp.result, Some("ok".to_string())); @@ -309,8 +309,8 @@ mod tests { #[test] fn smoke_cf_api_response_failure_parse() { let json = r#"{"success":false,"errors":[{"code":1003,"message":"Invalid zone"}],"messages":[],"result":null}"#; - let resp: CfApiResponse = serde_json::from_str(json) - .expect("Failed CfApiResponse must parse"); + let resp: CfApiResponse = + serde_json::from_str(json).expect("Failed CfApiResponse must parse"); assert!(!resp.success); assert_eq!(resp.errors.len(), 1); assert_eq!(resp.errors[0].code, 1003); @@ -318,7 +318,10 @@ mod tests { #[test] fn smoke_cf_api_error_has_code_and_message() { - let err = CfApiError { code: 7003, message: "Could not route to /zones".to_string() }; + let err = CfApiError { + code: 7003, + message: "Could not route to /zones".to_string(), + }; assert_eq!(err.code, 7003); assert!(!err.message.is_empty()); } diff --git a/src-gossamer/src/compat.rs b/src-gossamer/src/compat.rs index 590a9dc1..640fbd81 100644 --- a/src-gossamer/src/compat.rs +++ b/src-gossamer/src/compat.rs @@ -1,28 +1,26 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) // -//! Tauri compatibility shim for Gossamer migration. +//! Backend → frontend event bus and async-spawn helpers. //! -//! Provides `AppHandle` and `Emitter` replacements so that modules which -//! previously relied on `tauri::AppHandle` for event emission can continue -//! working with minimal changes. Events are delivered to the frontend via -//! Gossamer's `gossamer_eval()` (injecting JS into the webview), which is -//! functionally equivalent to Tauri's event system. +//! Provides an `AppHandle` and an `Emitter` trait so backend modules can emit +//! events without depending on the webview shell. Events are queued here and +//! delivered to the frontend via Gossamer's `gossamer_eval()` (injecting JS +//! into the webview). //! -//! This shim is intentionally minimal — it covers only the patterns used +//! This layer is intentionally minimal — it covers only the patterns used //! in the PanLL codebase (emit string events, spawn async tasks). Modules //! that need richer capabilities should be refactored to use gossamer-rs //! directly. -use std::sync::{Arc, Mutex}; use once_cell::sync::Lazy; use serde::Serialize; +use std::sync::{Arc, Mutex}; /// Global event bus — collects events that the main loop will flush to the webview. /// -/// In the Tauri architecture, `app.emit("channel", payload)` pushes an event -/// through Tauri's internal IPC. Here we queue events and the Gossamer main -/// loop (or a dedicated flush thread) will deliver them via `gossamer_eval()`. +/// `app.emit("channel", payload)` queues an event here; the Gossamer main +/// loop (or a dedicated flush thread) delivers it via `gossamer_eval()`. static EVENT_BUS: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(Vec::new()))); @@ -35,11 +33,11 @@ pub struct PendingEvent { pub payload: String, } -/// Gossamer-compatible application handle. +/// Application handle used to emit events to the frontend. /// -/// Drop-in replacement for `tauri::AppHandle`. Cloneable and Send + Sync -/// so it can be passed to background threads and async tasks. -#[derive(Clone)] +/// Cloneable and Send + Sync so it can be passed to background threads and +/// async tasks. +#[derive(Clone, Default)] pub struct AppHandle { // Reserved for future Gossamer handle reference. _phantom: (), @@ -52,10 +50,10 @@ impl AppHandle { } } -/// Emitter trait — mirrors `tauri::Emitter` for event emission. +/// Emitter trait — event emission to the frontend. /// -/// Modules that `use tauri::Emitter;` can instead `use crate::compat::Emitter;` -/// and the `app_handle.emit(channel, payload)` calls compile unchanged. +/// Modules `use crate::compat::Emitter;` and call +/// `app_handle.emit(channel, payload)`. pub trait Emitter { /// Emit an event to the frontend. fn emit(&self, channel: &str, payload: &S) -> Result<(), String>; @@ -91,8 +89,8 @@ pub fn drain_events() -> Vec { /// Spawn an async task on the Tokio runtime. /// -/// Replaces `tauri::async_runtime::spawn()`. Uses the global Tokio runtime -/// initialised in main.rs. +/// Must be called from within a Tokio runtime context (`tokio::spawn` panics +/// otherwise). pub fn spawn_async(future: F) where F: std::future::Future + Send + 'static, diff --git a/src-gossamer/src/dlc_workshop/commands.rs b/src-gossamer/src/dlc_workshop/commands.rs index 7ce9796f..32426a4b 100644 --- a/src-gossamer/src/dlc_workshop/commands.rs +++ b/src-gossamer/src/dlc_workshop/commands.rs @@ -25,8 +25,7 @@ const DLC_DIR: &str = "/tmp/panll/dlc-workshop"; /// Ensure the DLC directory exists, creating it lazily if needed. fn ensure_dlc_dir() -> Result { let path = PathBuf::from(DLC_DIR); - fs::create_dir_all(&path) - .map_err(|e| format!("Cannot create DLC directory {DLC_DIR}: {e}"))?; + fs::create_dir_all(&path).map_err(|e| format!("Cannot create DLC directory {DLC_DIR}: {e}"))?; Ok(path) } @@ -35,7 +34,6 @@ fn ensure_dlc_dir() -> Result { /// Reads `.puzzle.json` files from the DLC directory and returns their /// contents as a JSON array. Currently returns stub data when no puzzles /// exist on disk. - pub async fn dlc_load_puzzles() -> Result { let dlc_dir = ensure_dlc_dir()?; @@ -43,7 +41,10 @@ pub async fn dlc_load_puzzles() -> Result { .map_err(|e| format!("Cannot read DLC directory: {e}"))? .filter_map(|entry| entry.ok()) .filter(|entry| { - entry.file_name().to_string_lossy().ends_with(".puzzle.json") + entry + .file_name() + .to_string_lossy() + .ends_with(".puzzle.json") }) .filter_map(|entry| { let content = fs::read_to_string(entry.path()).ok()?; @@ -58,38 +59,39 @@ pub async fn dlc_load_puzzles() -> Result { name_a.cmp(name_b) }); - serde_json::to_string(&puzzles) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&puzzles).map_err(|e| format!("Serialisation error: {e}")) } /// Save a puzzle to disk. /// /// Expects a JSON string containing puzzle data with at least an `id` field. /// Writes the puzzle to `/.puzzle.json`. - pub async fn dlc_save_puzzle(data: String) -> Result { let dlc_dir = ensure_dlc_dir()?; - let puzzle: serde_json::Value = serde_json::from_str(&data) - .map_err(|e| format!("Invalid puzzle JSON: {e}"))?; + let puzzle: serde_json::Value = + serde_json::from_str(&data).map_err(|e| format!("Invalid puzzle JSON: {e}"))?; - let id = puzzle.get("id") + let id = puzzle + .get("id") .and_then(|v| v.as_str()) .ok_or_else(|| "Puzzle data must contain an 'id' field".to_string())?; let filename = format!("{id}.puzzle.json"); let path = dlc_dir.join(&filename); - fs::write(&path, serde_json::to_string_pretty(&puzzle).unwrap_or_default()) - .map_err(|e| format!("Cannot write puzzle file: {e}"))?; + fs::write( + &path, + serde_json::to_string_pretty(&puzzle).unwrap_or_default(), + ) + .map_err(|e| format!("Cannot write puzzle file: {e}"))?; let result = json!({ "saved": true, "id": id, "path": path.to_string_lossy(), }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Run the solution test suite for a single puzzle. @@ -97,7 +99,6 @@ pub async fn dlc_save_puzzle(data: String) -> Result { /// Looks up the puzzle by ID and simulates running its solution test. /// When the real VM test harness is implemented, this will execute the /// puzzle's instructions and verify the solution against expected output. - pub async fn dlc_run_test(puzzle_id: String) -> Result { let dlc_dir = ensure_dlc_dir()?; let path = dlc_dir.join(format!("{puzzle_id}.puzzle.json")); @@ -114,8 +115,7 @@ pub async fn dlc_run_test(puzzle_id: String) -> Result { "output": "All assertions passed", "stub": !puzzle_exists, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Run all tests in the DLC pack. @@ -123,7 +123,6 @@ pub async fn dlc_run_test(puzzle_id: String) -> Result { /// Iterates over all `.puzzle.json` files and simulates running each test. /// Returns aggregate results. When the real VM test harness is implemented, /// this will execute each puzzle sequentially and report pass/fail. - pub async fn dlc_run_all_tests() -> Result { let dlc_dir = ensure_dlc_dir()?; @@ -131,7 +130,10 @@ pub async fn dlc_run_all_tests() -> Result { .map_err(|e| format!("Cannot read DLC directory: {e}"))? .filter_map(|entry| entry.ok()) .filter(|entry| { - entry.file_name().to_string_lossy().ends_with(".puzzle.json") + entry + .file_name() + .to_string_lossy() + .ends_with(".puzzle.json") }) .count(); @@ -143,8 +145,7 @@ pub async fn dlc_run_all_tests() -> Result { "totalTimeMs": puzzle_count as u64 * 42, "results": [], }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Browse DLC assets (sprites, sounds, etc.). @@ -152,7 +153,6 @@ pub async fn dlc_run_all_tests() -> Result { /// Scans the DLC directory for asset files and returns metadata. /// Currently returns stub asset data. When asset management is fully /// implemented, this will scan actual sprite/sound directories. - pub async fn dlc_browse_assets() -> Result { let result = json!({ "assets": [ @@ -174,8 +174,7 @@ pub async fn dlc_browse_assets() -> Result { "totalAssets": 2, "totalSizeBytes": 32768, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Package the DLC pack for distribution. @@ -184,14 +183,14 @@ pub async fn dlc_browse_assets() -> Result { /// Creates a manifest file in the DLC directory. When full packaging is /// implemented, this will bundle puzzles and assets into a distributable /// archive. - pub async fn dlc_package(data: String) -> Result { let dlc_dir = ensure_dlc_dir()?; - let meta: serde_json::Value = serde_json::from_str(&data) - .map_err(|e| format!("Invalid packaging JSON: {e}"))?; + let meta: serde_json::Value = + serde_json::from_str(&data).map_err(|e| format!("Invalid packaging JSON: {e}"))?; - let pack_name = meta.get("name") + let pack_name = meta + .get("name") .and_then(|v| v.as_str()) .unwrap_or("unnamed-pack"); @@ -209,8 +208,11 @@ pub async fn dlc_package(data: String) -> Result { "format": "idaptik-dlc-v1", }); - fs::write(&manifest_path, serde_json::to_string_pretty(&manifest).unwrap_or_default()) - .map_err(|e| format!("Cannot write pack manifest: {e}"))?; + fs::write( + &manifest_path, + serde_json::to_string_pretty(&manifest).unwrap_or_default(), + ) + .map_err(|e| format!("Cannot write pack manifest: {e}"))?; let result = json!({ "packaged": true, @@ -218,15 +220,13 @@ pub async fn dlc_package(data: String) -> Result { "manifestPath": manifest_path.to_string_lossy(), "stub": true, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Import a puzzle from a file path. /// /// Reads a JSON puzzle file from the given path and copies it into the /// DLC directory. The puzzle must have a valid `id` field. - pub async fn dlc_import_puzzle(path: String) -> Result { let dlc_dir = ensure_dlc_dir()?; let source = PathBuf::from(&path); @@ -235,19 +235,23 @@ pub async fn dlc_import_puzzle(path: String) -> Result { return Err(format!("Source file not found: {path}")); } - let content = fs::read_to_string(&source) - .map_err(|e| format!("Cannot read source file: {e}"))?; + let content = + fs::read_to_string(&source).map_err(|e| format!("Cannot read source file: {e}"))?; - let puzzle: serde_json::Value = serde_json::from_str(&content) - .map_err(|e| format!("Invalid puzzle JSON: {e}"))?; + let puzzle: serde_json::Value = + serde_json::from_str(&content).map_err(|e| format!("Invalid puzzle JSON: {e}"))?; - let id = puzzle.get("id") + let id = puzzle + .get("id") .and_then(|v| v.as_str()) .ok_or_else(|| "Imported puzzle must contain an 'id' field".to_string())?; let dest = dlc_dir.join(format!("{id}.puzzle.json")); - fs::write(&dest, serde_json::to_string_pretty(&puzzle).unwrap_or_default()) - .map_err(|e| format!("Cannot write imported puzzle: {e}"))?; + fs::write( + &dest, + serde_json::to_string_pretty(&puzzle).unwrap_or_default(), + ) + .map_err(|e| format!("Cannot write imported puzzle: {e}"))?; let result = json!({ "imported": true, @@ -255,8 +259,7 @@ pub async fn dlc_import_puzzle(path: String) -> Result { "sourcePath": path, "destPath": dest.to_string_lossy(), }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Export a puzzle to a file. @@ -264,7 +267,6 @@ pub async fn dlc_import_puzzle(path: String) -> Result { /// Reads the puzzle by ID from the DLC directory and returns its contents /// along with the source path. The frontend can then use Tauri's save /// dialog to write the file to the user's chosen location. - pub async fn dlc_export_puzzle(puzzle_id: String) -> Result { let dlc_dir = ensure_dlc_dir()?; let path = dlc_dir.join(format!("{puzzle_id}.puzzle.json")); @@ -273,11 +275,10 @@ pub async fn dlc_export_puzzle(puzzle_id: String) -> Result { return Err(format!("Puzzle not found: {puzzle_id}")); } - let content = fs::read_to_string(&path) - .map_err(|e| format!("Cannot read puzzle file: {e}"))?; + let content = fs::read_to_string(&path).map_err(|e| format!("Cannot read puzzle file: {e}"))?; - let puzzle: serde_json::Value = serde_json::from_str(&content) - .map_err(|e| format!("Cannot parse puzzle file: {e}"))?; + let puzzle: serde_json::Value = + serde_json::from_str(&content).map_err(|e| format!("Cannot parse puzzle file: {e}"))?; let result = json!({ "exported": true, @@ -285,8 +286,7 @@ pub async fn dlc_export_puzzle(puzzle_id: String) -> Result { "sourcePath": path.to_string_lossy(), "puzzle": puzzle, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } #[cfg(test)] @@ -328,8 +328,7 @@ mod tests { // Save the puzzle. let save_result = dlc_save_puzzle(puzzle_data.to_string()).await; assert!(save_result.is_ok()); - let save_json: serde_json::Value = - serde_json::from_str(&save_result.unwrap()).unwrap(); + let save_json: serde_json::Value = serde_json::from_str(&save_result.unwrap()).unwrap(); assert_eq!(save_json["saved"], true); assert_eq!(save_json["id"], "test-puzzle-save"); diff --git a/src-gossamer/src/echidna_live.rs b/src-gossamer/src/echidna_live.rs index e91a572b..4c3bbce6 100644 --- a/src-gossamer/src/echidna_live.rs +++ b/src-gossamer/src/echidna_live.rs @@ -68,7 +68,6 @@ pub struct ProofResult { /// Check ECHIDNA health (async). /// /// `GET /health` — returns the prover health envelope. - pub async fn echidna_live_health() -> Result { let endpoint = echidna_endpoint(); let health: EchidnaHealth = http_client::get_json(&endpoint, "/health").await?; @@ -79,7 +78,6 @@ pub async fn echidna_live_health() -> Result { /// /// `POST /api/tactics/recommend` — given an obligation shape, returns /// ranked tactic recommendations with confidence scores. - pub async fn echidna_live_recommend_tactics(obligation: String) -> Result { let endpoint = echidna_endpoint(); let body = json!({ "obligation": obligation }); @@ -90,7 +88,6 @@ pub async fn echidna_live_recommend_tactics(obligation: String) -> Result Result { let endpoint = echidna_endpoint(); let body = json!({ "obligation": obligation }); @@ -100,7 +97,6 @@ pub async fn echidna_live_submit_obligation(obligation: String) -> Result Result { let endpoint = echidna_endpoint(); let path = format!("/api/obligations/{}", obligation_id); @@ -110,7 +106,6 @@ pub async fn echidna_live_get_result(obligation_id: String) -> Result Result { let endpoint = echidna_endpoint(); http_client::get_raw(&endpoint, "/api/stats").await diff --git a/src-gossamer/src/error.rs b/src-gossamer/src/error.rs index 0be6ebf1..550da11f 100644 --- a/src-gossamer/src/error.rs +++ b/src-gossamer/src/error.rs @@ -1,15 +1,16 @@ // SPDX-License-Identifier: MPL-2.0 -//! Unified error module for the PanLL Tauri backend. +//! Unified error module for the PanLL Gossamer backend. //! //! Provides a single `PanllError` enum that covers all common failure modes //! across panels: I/O, JSON serialisation, mutex poisoning, missing resources, //! invalid input, network faults, and internal bugs. Every variant carries //! enough context for a human-readable error message. //! -//! Tauri commands return `Result`, so the module also provides -//! [`to_tauri_result`] to convert `Result` at the command -//! boundary, and an `Into` impl for direct use with `.map_err()`. +//! IPC command handlers return `Result`, so the module also +//! provides [`to_command_result`] to convert `Result` at the +//! command boundary, and an `Into` impl for direct use with +//! `.map_err()`. use std::fmt; use std::sync::PoisonError; @@ -98,7 +99,7 @@ impl From> for PanllError { } // --------------------------------------------------------------------------- -// Into — needed for Tauri command returns (Result) +// Into — needed for IPC command returns (Result) // --------------------------------------------------------------------------- impl From for String { @@ -111,18 +112,17 @@ impl From for String { // Helper // --------------------------------------------------------------------------- -/// Convert a `PanllError` result to a `String` result for Tauri command boundaries. +/// Convert a `PanllError` result to a `String` result for IPC command boundaries. /// -/// Tauri commands must return `Result`. This helper lets internal +/// IPC command handlers return `Result`. This helper lets internal /// functions use the richer `PanllError` type and convert at the boundary: /// /// ```rust,ignore -/// #[tauri::command] /// pub async fn my_command() -> Result { -/// to_tauri_result(do_work()) +/// to_command_result(do_work()) /// } /// ``` -pub fn to_tauri_result(result: Result) -> Result { +pub fn to_command_result(result: Result) -> Result { result.map_err(|e| e.to_string()) } @@ -209,17 +209,16 @@ mod tests { } #[test] - fn test_to_tauri_result_ok() { + fn test_to_command_result_ok() { let result: Result = Ok(42); - let tauri_result = to_tauri_result(result); - assert_eq!(tauri_result, Ok(42)); + let command_result = to_command_result(result); + assert_eq!(command_result, Ok(42)); } #[test] - fn test_to_tauri_result_err() { - let result: Result = - Err(PanllError::Internal("boom".to_string())); - let tauri_result = to_tauri_result(result); - assert_eq!(tauri_result, Err("Internal error: boom".to_string())); + fn test_to_command_result_err() { + let result: Result = Err(PanllError::Internal("boom".to_string())); + let command_result = to_command_result(result); + assert_eq!(command_result, Err("Internal error: boom".to_string())); } } diff --git a/src-gossamer/src/farm/commands.rs b/src-gossamer/src/farm/commands.rs index b8bf306d..a4bc4ca8 100644 --- a/src-gossamer/src/farm/commands.rs +++ b/src-gossamer/src/farm/commands.rs @@ -24,10 +24,9 @@ fn manifest_path() -> Result { /// Read and parse the farm manifest from disk. fn load_manifest() -> Result { let path = manifest_path()?; - let content = fs::read_to_string(&path) - .map_err(|e| format!("Cannot read {}: {}", path.display(), e))?; - serde_json::from_str(&content) - .map_err(|e| format!("Cannot parse manifest: {e}")) + let content = + fs::read_to_string(&path).map_err(|e| format!("Cannot read {}: {}", path.display(), e))?; + serde_json::from_str(&content).map_err(|e| format!("Cannot parse manifest: {e}")) } /// Build a reverse lookup from repo name → group name. @@ -50,7 +49,6 @@ fn build_group_lookup(manifest: &FarmManifest) -> HashMap { /// Returns a JSON-serialised `FarmInventory` with all repos, groups, /// distinct languages, and forge names. The frontend parses this once /// and caches it in the model. - pub async fn farm_list_repos() -> Result { let manifest = load_manifest()?; let group_lookup = build_group_lookup(&manifest); @@ -62,7 +60,10 @@ pub async fn farm_list_repos() -> Result { if let Some(repos) = &manifest.repos { for (name, repo) in repos { let lang = repo.language.clone().unwrap_or_default(); - let priority = repo.priority.clone().unwrap_or_else(|| "medium".to_string()); + let priority = repo + .priority + .clone() + .unwrap_or_else(|| "medium".to_string()); let forges = repo.forges.clone().unwrap_or_default(); let auto_prop = repo.auto_propagate.unwrap_or(false); let description = repo.description.clone().unwrap_or_default(); @@ -99,10 +100,7 @@ pub async fn farm_list_repos() -> Result { let mut groups = HashMap::new(); if let Some(manifest_groups) = &manifest.repo_groups { for (name, group) in manifest_groups { - groups.insert( - name.clone(), - group.repos.clone().unwrap_or_default(), - ); + groups.insert(name.clone(), group.repos.clone().unwrap_or_default()); } } @@ -114,14 +112,12 @@ pub async fn farm_list_repos() -> Result { forge_names, }; - serde_json::to_string(&inventory) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&inventory).map_err(|e| format!("Serialisation error: {e}")) } /// Get details for a single repo by name. /// /// Returns the repo entry as JSON, or an error if not found. - pub async fn farm_get_repo(name: String) -> Result { let manifest = load_manifest()?; let group_lookup = build_group_lookup(&manifest); @@ -133,20 +129,21 @@ pub async fn farm_get_repo(name: String) -> Result { name: name.clone(), description: repo.description.clone().unwrap_or_default(), language: repo.language.clone().unwrap_or_default(), - priority: repo.priority.clone().unwrap_or_else(|| "medium".to_string()), + priority: repo + .priority + .clone() + .unwrap_or_else(|| "medium".to_string()), forges: repo.forges.clone().unwrap_or_default(), auto_propagate: repo.auto_propagate.unwrap_or(false), group: group_lookup.get(&name).cloned(), }; - serde_json::to_string(&entry) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&entry).map_err(|e| format!("Serialisation error: {e}")) } /// Get aggregate statistics from the farm manifest. /// /// Returns counts grouped by language, forge, priority, and group. - pub async fn farm_get_stats() -> Result { let manifest = load_manifest()?; @@ -157,12 +154,18 @@ pub async fn farm_get_stats() -> Result { let mut total = 0u32; if let Some(repos) = &manifest.repos { - for (_name, repo) in repos { + for repo in repos.values() { total += 1; - let lang = repo.language.clone().unwrap_or_else(|| "unknown".to_string()); + let lang = repo + .language + .clone() + .unwrap_or_else(|| "unknown".to_string()); *by_language.entry(lang).or_insert(0) += 1; - let pri = repo.priority.clone().unwrap_or_else(|| "medium".to_string()); + let pri = repo + .priority + .clone() + .unwrap_or_else(|| "medium".to_string()); *by_priority.entry(pri).or_insert(0) += 1; if let Some(forges) = &repo.forges { @@ -175,7 +178,7 @@ pub async fn farm_get_stats() -> Result { // Count repos per group. let group_lookup = build_group_lookup(&manifest); - for (_repo, group) in &group_lookup { + for group in group_lookup.values() { *by_group.entry(group.clone()).or_insert(0) += 1; } @@ -187,6 +190,5 @@ pub async fn farm_get_stats() -> Result { "by_group": by_group, }); - serde_json::to_string(&stats) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&stats).map_err(|e| format!("Serialisation error: {e}")) } diff --git a/src-gossamer/src/farm/mod.rs b/src-gossamer/src/farm/mod.rs index bd612d93..564962c6 100644 --- a/src-gossamer/src/farm/mod.rs +++ b/src-gossamer/src/farm/mod.rs @@ -6,5 +6,5 @@ //! exposes it to the ReScript frontend via Tauri commands. No HTTP service //! required — this is purely filesystem-based. -pub mod types; pub mod commands; +pub mod types; diff --git a/src-gossamer/src/feedback/commands.rs b/src-gossamer/src/feedback/commands.rs index 019d7223..bb6bb201 100644 --- a/src-gossamer/src/feedback/commands.rs +++ b/src-gossamer/src/feedback/commands.rs @@ -28,11 +28,9 @@ fn feedback_dir() -> Result { /// /// Returns a JSON object with `path` (where the file was saved) and `id` /// (the timestamp-based identifier). - pub async fn feedback_save_report(report_json: String) -> Result { let dir = feedback_dir()?; - fs::create_dir_all(&dir) - .map_err(|e| format!("Failed to create feedback directory: {e}"))?; + fs::create_dir_all(&dir).map_err(|e| format!("Failed to create feedback directory: {e}"))?; let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -43,8 +41,8 @@ pub async fn feedback_save_report(report_json: String) -> Result let filepath = dir.join(&filename); // Parse the incoming JSON to validate it, then wrap with metadata. - let parsed: serde_json::Value = serde_json::from_str(&report_json) - .map_err(|e| format!("Invalid JSON in report: {e}"))?; + let parsed: serde_json::Value = + serde_json::from_str(&report_json).map_err(|e| format!("Invalid JSON in report: {e}"))?; let wrapped = json!({ "id": format!("feedback-{timestamp}"), @@ -52,11 +50,10 @@ pub async fn feedback_save_report(report_json: String) -> Result "report": parsed, }); - let content = serde_json::to_string_pretty(&wrapped) - .map_err(|e| format!("Serialisation error: {e}"))?; + let content = + serde_json::to_string_pretty(&wrapped).map_err(|e| format!("Serialisation error: {e}"))?; - fs::write(&filepath, content) - .map_err(|e| format!("Failed to write report: {e}"))?; + fs::write(&filepath, content).map_err(|e| format!("Failed to write report: {e}"))?; Ok(json!({ "path": filepath.to_string_lossy(), diff --git a/src-gossamer/src/fleet/commands.rs b/src-gossamer/src/fleet/commands.rs index 6d4326f5..8a3c4af5 100644 --- a/src-gossamer/src/fleet/commands.rs +++ b/src-gossamer/src/fleet/commands.rs @@ -14,8 +14,7 @@ use serde_json::json; /// Base URL for the fleet API. Override with FLEET_URL env var. fn fleet_url() -> String { - std::env::var("FLEET_URL") - .unwrap_or_else(|_| "http://localhost:8080/api/v1".to_string()) + std::env::var("FLEET_URL").unwrap_or_else(|_| "http://localhost:8080/api/v1".to_string()) } /// HTTP client with a 5-second timeout — fleet is local, should be fast. @@ -31,14 +30,16 @@ fn client() -> Result { /// On connection failure, returns a JSON array with all 6 canonical bots in /// "offline" status so the panel always has something to render. The bot list /// is: rhodibot, echidnabot, sustainabot, glambot, seambot, finishbot. - pub async fn fleet_get_bots() -> Result { let url = format!("{}/bots", fleet_url()); let client = client()?; match client.get(&url).send().await { Ok(resp) if resp.status().is_success() => { - let body = resp.text().await.map_err(|e| format!("Body read error: {e}"))?; + let body = resp + .text() + .await + .map_err(|e| format!("Body read error: {e}"))?; // Validate the response is actually JSON (not a stray HTML page from // another service on the same port). if serde_json::from_str::(&body).is_ok() { @@ -75,14 +76,16 @@ pub async fn fleet_get_bots() -> Result { /// /// On failure, returns an empty JSON array rather than an error so the panel /// renders an empty table instead of an error banner. - pub async fn fleet_get_findings() -> Result { let url = format!("{}/findings", fleet_url()); let client = client()?; match client.get(&url).send().await { Ok(resp) if resp.status().is_success() => { - let body = resp.text().await.map_err(|e| format!("Body read error: {e}"))?; + let body = resp + .text() + .await + .map_err(|e| format!("Body read error: {e}"))?; // Validate the response is JSON, not HTML from a stray service. if serde_json::from_str::(&body).is_ok() { Ok(body) @@ -98,7 +101,6 @@ pub async fn fleet_get_findings() -> Result { /// /// Unlike the read commands, dispatch failures are surfaced as errors because /// the user explicitly triggered an action and needs to know it failed. - pub async fn fleet_dispatch(finding_id: String, bot_id: String) -> Result { let url = format!("{}/dispatch", fleet_url()); let client = client()?; @@ -116,7 +118,9 @@ pub async fn fleet_dispatch(finding_id: String, bot_id: String) -> Result String { /// Makes a GET request with a short timeout and returns connectivity status. /// The frontend uses this to show the connection indicator in the Game Preview /// panel header. - pub async fn game_preview_check_server(url: String) -> Result { let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(5)) @@ -93,16 +92,14 @@ pub async fn game_preview_check_server(url: String) -> Result { "connected": resp.status().is_success(), "status": status, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } Err(_) => { let result = json!({ "connected": false, "status": 0, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } } } @@ -118,7 +115,6 @@ pub async fn game_preview_check_server(url: String) -> Result { /// - `npx` (from Node/Deno) available on `$PATH`. /// /// Returns JSON with `pid`, `url`, and `projectDir` on success. - pub async fn game_preview_start_server( project_dir: String, port: Option, @@ -182,8 +178,7 @@ pub async fn game_preview_start_server( "projectDir": project_dir, "status": "started", }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Stop the running Vite dev-server. @@ -191,7 +186,6 @@ pub async fn game_preview_start_server( /// Sends SIGTERM (Unix) or taskkill (Windows) to the tracked dev-server /// process. Clears the tracked state afterwards regardless of kill outcome /// so a new server can be started. - pub async fn game_preview_stop_server() -> Result { let (pid, url) = { let state = DEV_SERVER.lock().map_err(|e| format!("Lock error: {e}"))?; @@ -233,8 +227,7 @@ pub async fn game_preview_stop_server() -> Result { "pid": pid, "url": url, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Send a control command to the game engine. @@ -246,7 +239,6 @@ pub async fn game_preview_stop_server() -> Result { /// /// The engine control endpoint is expected to be provided by an IDApTIK /// Vite plugin that injects `/__engine/*` routes into the dev server. - pub async fn game_preview_control(command: String) -> Result { let valid_commands = ["pause", "resume", "step"]; if !valid_commands.contains(&command.as_str()) { @@ -289,15 +281,13 @@ pub async fn game_preview_control(command: String) -> Result { // Fallback: echo the command state locally. let result = json!({ "state": command }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Start a named game recording session. /// /// Creates a marker file in `/tmp/panll/game-clips/` to track the recording. /// Returns the generated recording ID so the frontend can reference it later. - pub async fn game_preview_record_start(name: String) -> Result { let clips_dir = ensure_clips_dir()?; let id = generate_recording_id(); @@ -313,23 +303,24 @@ pub async fn game_preview_record_start(name: String) -> Result { "status": "recording", }); - fs::write(&marker_path, serde_json::to_string_pretty(&metadata).unwrap_or_default()) - .map_err(|e| format!("Cannot write recording marker: {e}"))?; + fs::write( + &marker_path, + serde_json::to_string_pretty(&metadata).unwrap_or_default(), + ) + .map_err(|e| format!("Cannot write recording marker: {e}"))?; let result = json!({ "id": id, "name": name, "status": "recording", }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Stop the current game recording session. /// /// Finds the active `.recording` marker, renames it to `.clip`, and returns /// clip metadata. If no active recording is found, returns an error. - pub async fn game_preview_record_stop() -> Result { let clips_dir = ensure_clips_dir()?; @@ -338,20 +329,19 @@ pub async fn game_preview_record_stop() -> Result { .map_err(|e| format!("Cannot read clips directory: {e}"))? .filter_map(|entry| entry.ok()) .filter(|entry| { - entry.path().extension() + entry + .path() + .extension() .map(|ext| ext == "recording") .unwrap_or(false) }) .collect(); - if recordings.is_empty() { - return Err("No active recording found".to_string()); - } - // Sort by name (timestamp-based) so we get the latest. recordings.sort_by_key(|e| e.file_name()); - // SAFETY: `recordings` is guaranteed non-empty — checked at line 146. - let latest = recordings.last().expect("recordings: non-empty after is_empty guard"); + let Some(latest) = recordings.last() else { + return Err("No active recording found".to_string()); + }; let recording_path = latest.path(); // Read the recording metadata. @@ -362,22 +352,21 @@ pub async fn game_preview_record_stop() -> Result { // Update status and add end timestamp. metadata["status"] = json!("complete"); - metadata["ended_at"] = json!( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs() - ); + metadata["ended_at"] = json!(SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs()); // Rename .recording → .clip let clip_path = recording_path.with_extension("clip"); - fs::write(&clip_path, serde_json::to_string_pretty(&metadata).unwrap_or_default()) - .map_err(|e| format!("Cannot write clip file: {e}"))?; - fs::remove_file(&recording_path) - .map_err(|e| format!("Cannot remove recording marker: {e}"))?; - - serde_json::to_string(&metadata) - .map_err(|e| format!("Serialisation error: {e}")) + fs::write( + &clip_path, + serde_json::to_string_pretty(&metadata).unwrap_or_default(), + ) + .map_err(|e| format!("Cannot write clip file: {e}"))?; + fs::remove_file(&recording_path).map_err(|e| format!("Cannot remove recording marker: {e}"))?; + + serde_json::to_string(&metadata).map_err(|e| format!("Serialisation error: {e}")) } /// Capture the current game frame as a screenshot. @@ -392,7 +381,6 @@ pub async fn game_preview_record_stop() -> Result { /// Prerequisites: /// - Dev server running (via `game_preview_start_server`). /// - IDApTIK Vite plugin providing the `/__engine/screenshot` endpoint. - pub async fn game_preview_screenshot() -> Result { let clips_dir = ensure_clips_dir()?; let ts = SystemTime::now() @@ -442,8 +430,7 @@ pub async fn game_preview_screenshot() -> Result { "filename": filename, "stub": true, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Return render statistics from the game engine. @@ -460,7 +447,6 @@ pub async fn game_preview_screenshot() -> Result { /// Prerequisites: /// - Dev server running (via `game_preview_start_server`). /// - IDApTIK Vite plugin providing the `/__engine/stats` endpoint. - pub async fn game_preview_stats() -> Result { // Try to fetch live stats from the running engine. let server_url = { @@ -494,15 +480,13 @@ pub async fn game_preview_stats() -> Result { "spriteCount": 342, "stub": true, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// List all saved clips from `/tmp/panll/game-clips/`. /// /// Reads `.clip` files from the clips directory and returns their metadata /// as a JSON array. Clips are sorted by ID (timestamp-based, newest first). - pub async fn game_preview_clips_list() -> Result { let clips_dir = ensure_clips_dir()?; @@ -510,7 +494,9 @@ pub async fn game_preview_clips_list() -> Result { .map_err(|e| format!("Cannot read clips directory: {e}"))? .filter_map(|entry| entry.ok()) .filter(|entry| { - entry.path().extension() + entry + .path() + .extension() .map(|ext| ext == "clip") .unwrap_or(false) }) @@ -527,15 +513,13 @@ pub async fn game_preview_clips_list() -> Result { id_b.cmp(id_a) }); - serde_json::to_string(&clips) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&clips).map_err(|e| format!("Serialisation error: {e}")) } /// Delete a clip by its ID. /// /// Removes the corresponding `.clip` file from the clips directory. /// Returns confirmation JSON on success. - pub async fn game_preview_clip_delete(id: String) -> Result { let clips_dir = ensure_clips_dir()?; let clip_path = clips_dir.join(format!("{id}.clip")); @@ -544,15 +528,13 @@ pub async fn game_preview_clip_delete(id: String) -> Result { return Err(format!("Clip not found: {id}")); } - fs::remove_file(&clip_path) - .map_err(|e| format!("Cannot delete clip {id}: {e}"))?; + fs::remove_file(&clip_path).map_err(|e| format!("Cannot delete clip {id}: {e}"))?; let result = json!({ "deleted": id, "success": true, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } #[cfg(test)] @@ -572,7 +554,10 @@ mod tests { #[test] fn test_generate_recording_id() { let id = generate_recording_id(); - assert!(id.starts_with("clip-"), "ID should start with 'clip-': {id}"); + assert!( + id.starts_with("clip-"), + "ID should start with 'clip-': {id}" + ); // ID should contain a numeric timestamp after the prefix. let timestamp_part = &id[5..]; assert!( @@ -630,7 +615,10 @@ mod tests { assert!(result.is_ok()); let json: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); assert!(json["stub"].as_bool().unwrap()); - assert!(json["filename"].as_str().unwrap().starts_with("screenshot-")); + assert!(json["filename"] + .as_str() + .unwrap() + .starts_with("screenshot-")); assert!(json["filename"].as_str().unwrap().ends_with(".png")); }); } @@ -660,8 +648,7 @@ mod tests { // Stop the recording. let stop_result = game_preview_record_stop().await; assert!(stop_result.is_ok()); - let stop_json: serde_json::Value = - serde_json::from_str(&stop_result.unwrap()).unwrap(); + let stop_json: serde_json::Value = serde_json::from_str(&stop_result.unwrap()).unwrap(); assert_eq!(stop_json["status"], "complete"); // Clip should appear in the list. diff --git a/src-gossamer/src/governance/commands.rs b/src-gossamer/src/governance/commands.rs index d9a78b81..aa2c38aa 100644 --- a/src-gossamer/src/governance/commands.rs +++ b/src-gossamer/src/governance/commands.rs @@ -58,7 +58,6 @@ async fn nesy_invoke(tool: &str, args: serde_json::Value) -> Result Result { match nesy_invoke("confidence-query", json!({"query": query})).await { Ok(response) => Ok(response), @@ -85,7 +84,6 @@ pub async fn governance_nesy_query(query: String) -> Result { /// subsystem can approve or reject with reasoning. /// /// Falls back to a mock approval when the BoJ server is unreachable. - pub async fn governance_nesy_validate(adjustment: String) -> Result { match nesy_invoke("validate-adjustment", json!({"adjustment": adjustment})).await { Ok(response) => Ok(response), @@ -111,7 +109,6 @@ pub async fn governance_nesy_validate(adjustment: String) -> Result Result { match nesy_invoke("stability-probe", json!({})).await { Ok(response) => Ok(response), @@ -155,8 +152,14 @@ mod tests { let result = governance_nesy_query("vexation=0.5,stability=0.55".to_string()).await; assert!(result.is_ok(), "Expected Ok with mock response"); let body = result.unwrap(); - assert!(body.contains("confidence"), "Mock should contain confidence field"); - assert!(body.contains("mock"), "Mock should identify itself as mock source"); + assert!( + body.contains("confidence"), + "Mock should contain confidence field" + ); + assert!( + body.contains("mock"), + "Mock should identify itself as mock source" + ); } #[tokio::test] @@ -164,8 +167,14 @@ mod tests { let result = governance_nesy_validate("HaltInference: test reason".to_string()).await; assert!(result.is_ok(), "Expected Ok with mock response"); let body = result.unwrap(); - assert!(body.contains("approved"), "Mock should contain approved field"); - assert!(body.contains("mock"), "Mock should identify itself as mock source"); + assert!( + body.contains("approved"), + "Mock should contain approved field" + ); + assert!( + body.contains("mock"), + "Mock should identify itself as mock source" + ); } #[tokio::test] @@ -181,6 +190,9 @@ mod tests { body.contains("recommendation"), "Mock should contain recommendation field" ); - assert!(body.contains("mock"), "Mock should identify itself as mock source"); + assert!( + body.contains("mock"), + "Mock should identify itself as mock source" + ); } } diff --git a/src-gossamer/src/hypatia/commands.rs b/src-gossamer/src/hypatia/commands.rs index 4cd8ee9a..bfcb72dd 100644 --- a/src-gossamer/src/hypatia/commands.rs +++ b/src-gossamer/src/hypatia/commands.rs @@ -14,8 +14,7 @@ use serde_json::json; /// Base URL for the Hypatia API. Override with HYPATIA_URL env var. fn hypatia_url() -> String { - std::env::var("HYPATIA_URL") - .unwrap_or_else(|_| "http://localhost:4040/api/v1".to_string()) + std::env::var("HYPATIA_URL").unwrap_or_else(|_| "http://localhost:4040/api/v1".to_string()) } /// HTTP client with a 10-second timeout — scans can take a moment. @@ -30,15 +29,15 @@ fn client() -> Result { /// /// Returns a JSON array of network objects with name, status, and accuracy /// fields. On connection failure, returns an empty array. - pub async fn hypatia_get_networks() -> Result { let url = format!("{}/networks", hypatia_url()); let client = client()?; match client.get(&url).send().await { - Ok(resp) if resp.status().is_success() => { - resp.text().await.map_err(|e| format!("Body read error: {e}")) - } + Ok(resp) if resp.status().is_success() => resp + .text() + .await + .map_err(|e| format!("Body read error: {e}")), _ => Ok("[]".to_string()), } } @@ -47,15 +46,15 @@ pub async fn hypatia_get_networks() -> Result { /// /// Returns a JSON array of scan result objects. On connection failure, returns /// an empty array so the panel renders an empty table. - pub async fn hypatia_get_scans() -> Result { let url = format!("{}/scans", hypatia_url()); let client = client()?; match client.get(&url).send().await { - Ok(resp) if resp.status().is_success() => { - resp.text().await.map_err(|e| format!("Body read error: {e}")) - } + Ok(resp) if resp.status().is_success() => resp + .text() + .await + .map_err(|e| format!("Body read error: {e}")), _ => Ok("[]".to_string()), } } @@ -66,7 +65,6 @@ pub async fn hypatia_get_scans() -> Result { /// the user explicitly triggered an action. The repo_name parameter matches /// the ReScript frontend's `repoName` field (camelCase in JSON, snake_case /// in Rust via Tauri's automatic rename). - pub async fn hypatia_scan_repo(repo_name: String) -> Result { let url = format!("{}/scans", hypatia_url()); let client = client()?; @@ -83,7 +81,9 @@ pub async fn hypatia_scan_repo(repo_name: String) -> Result { .map_err(|e| format!("Hypatia scan failed (server unreachable): {e}"))?; if resp.status().is_success() { - resp.text().await.map_err(|e| format!("Body read error: {e}")) + resp.text() + .await + .map_err(|e| format!("Body read error: {e}")) } else { Err(format!("Hypatia scan returned HTTP {}", resp.status())) } diff --git a/src-gossamer/src/identity.rs b/src-gossamer/src/identity.rs index 6ccb64f1..5946d4c3 100644 --- a/src-gossamer/src/identity.rs +++ b/src-gossamer/src/identity.rs @@ -113,7 +113,7 @@ pub fn identity_save( /// Load an identity snapshot by ID. /// -/// Primary source: VeriSimDB (GET /state/). +/// Primary source: VeriSimDB (`GET /state/`). /// Fallback: `~/.panll/identities/.json`. pub fn identity_load(id: &str) -> Result { let verisim_url = diff --git a/src-gossamer/src/k9/commands.rs b/src-gossamer/src/k9/commands.rs index fb510d31..00e54112 100644 --- a/src-gossamer/src/k9/commands.rs +++ b/src-gossamer/src/k9/commands.rs @@ -69,7 +69,6 @@ fn detect_security_level(content: &str) -> &'static str { /// /// Returns the file content as a JSON object with `path`, `content`, /// `securityLevel`, and `hasK9Magic` fields. - pub fn k9_load_contractile(path: String) -> Result { let file_path = if Path::new(&path).is_absolute() { PathBuf::from(&path) @@ -111,7 +110,6 @@ pub fn k9_load_contractile(path: String) -> Result { /// - Hunt-level files must have `signature_required = true` /// /// Returns a JSON object with `valid`, `errors`, `warnings`, and `securityLevel`. - pub fn k9_validate(path: String) -> Result { let file_path = if Path::new(&path).is_absolute() { PathBuf::from(&path) @@ -222,7 +220,6 @@ pub fn k9_validate(path: String) -> Result { /// is matched case-insensitively with hyphens replacing spaces. /// /// Example: `name = "Protocol Design"` matches `layouts/protocol-design.k9.ncl`. - pub fn k9_apply_layout(name: String) -> Result { let root = find_repo_root()?; let layouts_dir = root.join("layouts"); @@ -242,9 +239,7 @@ pub fn k9_apply_layout(name: String) -> Result { let path = entry.path(); if let Some(filename) = path.file_name() { let filename_str = filename.to_string_lossy().to_lowercase(); - if filename_str.ends_with(".k9.ncl") - && filename_str.contains(&normalised) - { + if filename_str.ends_with(".k9.ncl") && filename_str.contains(&normalised) { let content = std::fs::read_to_string(&path) .map_err(|e| format!("Failed to read layout {}: {}", filename_str, e))?; diff --git a/src-gossamer/src/level_architect/commands.rs b/src-gossamer/src/level_architect/commands.rs index 4013efc3..81c4ff14 100644 --- a/src-gossamer/src/level_architect/commands.rs +++ b/src-gossamer/src/level_architect/commands.rs @@ -19,19 +19,18 @@ use serde_json::json; /// /// Reads the file, validates it as JSON, and returns the content. /// Returns an error if the file does not exist or contains invalid JSON. - pub async fn load_level(path: String) -> Result { let file_path = Path::new(&path); if !file_path.exists() { return Err(format!("Level file not found: {path}")); } - let content = fs::read_to_string(file_path) - .map_err(|e| format!("Cannot read level file: {e}"))?; + let content = + fs::read_to_string(file_path).map_err(|e| format!("Cannot read level file: {e}"))?; // Validate that the content is valid JSON. - let _parsed: serde_json::Value = serde_json::from_str(&content) - .map_err(|e| format!("Invalid JSON in level file: {e}"))?; + let _parsed: serde_json::Value = + serde_json::from_str(&content).map_err(|e| format!("Invalid JSON in level file: {e}"))?; Ok(content) } @@ -40,33 +39,29 @@ pub async fn load_level(path: String) -> Result { /// /// Validates the data as JSON, pretty-prints it, and writes to disk. /// Creates parent directories if they do not exist. - pub async fn save_level(path: String, data: String) -> Result { let file_path = Path::new(&path); // Validate JSON before writing. - let parsed: serde_json::Value = serde_json::from_str(&data) - .map_err(|e| format!("Invalid JSON data: {e}"))?; + let parsed: serde_json::Value = + serde_json::from_str(&data).map_err(|e| format!("Invalid JSON data: {e}"))?; // Ensure parent directory exists. if let Some(parent) = file_path.parent() { - fs::create_dir_all(parent) - .map_err(|e| format!("Cannot create directory: {e}"))?; + fs::create_dir_all(parent).map_err(|e| format!("Cannot create directory: {e}"))?; } - let pretty = serde_json::to_string_pretty(&parsed) - .map_err(|e| format!("Serialisation error: {e}"))?; + let pretty = + serde_json::to_string_pretty(&parsed).map_err(|e| format!("Serialisation error: {e}"))?; - fs::write(file_path, &pretty) - .map_err(|e| format!("Cannot write level file: {e}"))?; + fs::write(file_path, &pretty).map_err(|e| format!("Cannot write level file: {e}"))?; let result = json!({ "saved": true, "path": path, "bytes": pretty.len(), }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Export level data as a LevelConfig.res source file string. @@ -75,29 +70,33 @@ pub async fn save_level(path: String, data: String) -> Result { /// that can be pasted into the IDApTIK game source. Currently produces /// a stub template; future versions will fully translate entities, /// patrols, and defence flags into typed ReScript records. - pub async fn export_level_config(data: String) -> Result { - let parsed: serde_json::Value = serde_json::from_str(&data) - .map_err(|e| format!("Invalid JSON data: {e}"))?; + let parsed: serde_json::Value = + serde_json::from_str(&data).map_err(|e| format!("Invalid JSON data: {e}"))?; - let level_name = parsed.get("levelName") + let level_name = parsed + .get("levelName") .and_then(|v| v.as_str()) .unwrap_or("Untitled"); - let grid_width = parsed.get("gridWidth") + let grid_width = parsed + .get("gridWidth") .and_then(|v| v.as_i64()) .unwrap_or(20); - let grid_height = parsed.get("gridHeight") + let grid_height = parsed + .get("gridHeight") .and_then(|v| v.as_i64()) .unwrap_or(15); - let entity_count = parsed.get("entities") + let entity_count = parsed + .get("entities") .and_then(|v| v.as_array()) .map(|a| a.len()) .unwrap_or(0); - let patrol_count = parsed.get("patrols") + let patrol_count = parsed + .get("patrols") .and_then(|v| v.as_array()) .map(|a| a.len()) .unwrap_or(0); @@ -123,8 +122,7 @@ let config = {{ "entityCount": entity_count, "patrolCount": patrol_count, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Browse available game assets for level placement. @@ -132,7 +130,6 @@ let config = {{ /// Returns a JSON array of assets grouped by category (devices, guards, /// collectables, decorations, triggers). Currently returns stub data /// representing sample IDApTIK game assets. - pub async fn browse_level_assets() -> Result { let result = json!({ "assets": [ @@ -201,8 +198,7 @@ pub async fn browse_level_assets() -> Result { } ] }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Validate the current level design and return any issues. @@ -211,16 +207,14 @@ pub async fn browse_level_assets() -> Result { /// overlapping entities, missing patrol waypoints, and empty defence flags. /// Takes the level JSON data as input and returns a JSON object with a /// `valid` boolean and an `issues` array. - pub async fn validate_level(data: String) -> Result { - let parsed: serde_json::Value = serde_json::from_str(&data) - .map_err(|e| format!("Invalid JSON data: {e}"))?; + let parsed: serde_json::Value = + serde_json::from_str(&data).map_err(|e| format!("Invalid JSON data: {e}"))?; let mut issues: Vec = Vec::new(); // Check for entities array. - let entities = parsed.get("entities") - .and_then(|v| v.as_array()); + let entities = parsed.get("entities").and_then(|v| v.as_array()); match entities { None => { @@ -244,9 +238,9 @@ pub async fn validate_level(data: String) -> Result { } // Check for at least one spawn point. - let has_spawn = ents.iter().any(|e| { - e.get("kind").and_then(|k| k.as_str()) == Some("EntitySpawnPoint") - }); + let has_spawn = ents + .iter() + .any(|e| e.get("kind").and_then(|k| k.as_str()) == Some("EntitySpawnPoint")); if !has_spawn { issues.push(json!({ "severity": "error", @@ -263,7 +257,11 @@ pub async fn validate_level(data: String) -> Result { for entity in ents { let x = entity.get("gridX").and_then(|v| v.as_i64()).unwrap_or(-1); let y = entity.get("gridY").and_then(|v| v.as_i64()).unwrap_or(-1); - let id = entity.get("id").and_then(|v| v.as_str()).unwrap_or("?").to_string(); + let id = entity + .get("id") + .and_then(|v| v.as_str()) + .unwrap_or("?") + .to_string(); positions.entry((x, y)).or_default().push(id); } for ((x, y), ids) in &positions { @@ -304,7 +302,10 @@ pub async fn validate_level(data: String) -> Result { // Check for empty waypoints. if let Some(waypoints) = patrol.get("waypoints").and_then(|v| v.as_array()) { if waypoints.len() < 2 { - let gid = patrol.get("guardId").and_then(|v| v.as_str()).unwrap_or("?"); + let gid = patrol + .get("guardId") + .and_then(|v| v.as_str()) + .unwrap_or("?"); issues.push(json!({ "severity": "warning", "message": format!("Patrol for {} has fewer than 2 waypoints", gid), @@ -317,17 +318,16 @@ pub async fn validate_level(data: String) -> Result { } } - let valid = !issues.iter().any(|i| { - i.get("severity").and_then(|s| s.as_str()) == Some("error") - }); + let valid = !issues + .iter() + .any(|i| i.get("severity").and_then(|s| s.as_str()) == Some("error")); let result = json!({ "valid": valid, "issueCount": issues.len(), "issues": issues, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } #[cfg(test)] @@ -367,15 +367,13 @@ mod tests { // Save. let save_result = save_level(path.clone(), data_str).await; assert!(save_result.is_ok()); - let save_json: serde_json::Value = - serde_json::from_str(&save_result.unwrap()).unwrap(); + let save_json: serde_json::Value = serde_json::from_str(&save_result.unwrap()).unwrap(); assert_eq!(save_json["saved"], true); // Load. let load_result = load_level(path.clone()).await; assert!(load_result.is_ok()); - let loaded: serde_json::Value = - serde_json::from_str(&load_result.unwrap()).unwrap(); + let loaded: serde_json::Value = serde_json::from_str(&load_result.unwrap()).unwrap(); assert_eq!(loaded["levelName"], "Test Level"); assert_eq!(loaded["gridWidth"], 20); @@ -390,7 +388,8 @@ mod tests { let result = save_level( "/tmp/panll/bad.json".to_string(), "not valid json {{{".to_string(), - ).await; + ) + .await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Invalid JSON")); }); @@ -480,9 +479,9 @@ mod tests { let json: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); assert_eq!(json["valid"], false); let issues = json["issues"].as_array().unwrap(); - assert!(issues.iter().any(|i| { - i["message"].as_str().unwrap().contains("spawn point") - })); + assert!(issues + .iter() + .any(|i| { i["message"].as_str().unwrap().contains("spawn point") })); }); } @@ -501,9 +500,9 @@ mod tests { assert!(result.is_ok()); let json: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); let issues = json["issues"].as_array().unwrap(); - assert!(issues.iter().any(|i| { - i["message"].as_str().unwrap().contains("Overlapping") - })); + assert!(issues + .iter() + .any(|i| { i["message"].as_str().unwrap().contains("Overlapping") })); }); } @@ -537,7 +536,10 @@ mod tests { assert_eq!(json["valid"], false); let issues = json["issues"].as_array().unwrap(); assert!(issues.iter().any(|i| { - i["message"].as_str().unwrap().contains("non-existent guard") + i["message"] + .as_str() + .unwrap() + .contains("non-existent guard") })); }); } diff --git a/src-gossamer/src/lib.rs b/src-gossamer/src/lib.rs index 46236b2e..70964f45 100644 --- a/src-gossamer/src/lib.rs +++ b/src-gossamer/src/lib.rs @@ -35,9 +35,65 @@ pub mod llm_coding; /// Previously orphaned (declared by no crate root, so never compiled). Wired /// into the library here so it builds, lints, and its tests run. Note: the /// async command handlers are not yet registered in the binary's IPC table — -/// that integration is tracked as follow-up debt (see TECHNICAL_DEBT.md). +/// that integration is tracked as follow-up debt (see +/// `docs/TECHNICAL_DEBT.adoc`, F1/F2). pub mod coprocessor; +// --------------------------------------------------------------------------- +// Formerly orphaned modules — panel backends, live service connections +// (`*_live`), and shared helpers (`compat`, `error`). +// +// These modules were declared by no crate root, so they were never compiled, +// linted, or tested. They are GTK/WebKit-free and now build as part of the +// library; each module's own `//!` docs describe it. Their command handlers +// are NOT yet registered in the binary's IPC table (`register_commands` in +// `main.rs`) — that is follow-up work (`docs/TECHNICAL_DEBT.adoc`, F1). +// --------------------------------------------------------------------------- + +pub mod a2ml; +pub mod aerie; +pub mod ai; +pub mod boj; +pub mod boj_live; +pub mod capture; +pub mod clade_scanner; +pub mod cloudguard; +pub mod compat; +pub mod dlc_workshop; +pub mod echidna_live; +pub mod error; +pub mod farm; +pub mod feedback; +pub mod fleet; +pub mod game_preview; +pub mod governance; +pub mod hypatia; +pub mod k9; +pub mod level_architect; +pub mod minter; +pub mod multiplayer_monitor; +pub mod network_topology; +pub mod observability; +pub mod overlay; +pub mod plaza; +pub mod provenance; +pub mod release_manager; +pub mod repoloader; +pub mod script_gist; +pub mod security; +pub mod system_update; +pub mod typell; +pub mod umoja; +pub mod ums; +pub mod ums_cartridge; +pub mod valence_shell; +pub mod verisim_live; +pub mod vm_inspector; +pub mod voicetag; +pub mod watcher; +pub mod wiring_inspector; +pub mod workspace; + /// Cross-module integration tests. These exercise the public command-backed /// functions end-to-end. They live in the lib (not `tests/`) so they run via /// `cargo test --lib` without cargo also force-building the GTK-linked diff --git a/src-gossamer/src/minter/commands.rs b/src-gossamer/src/minter/commands.rs index 61b16669..153f097d 100644 --- a/src-gossamer/src/minter/commands.rs +++ b/src-gossamer/src/minter/commands.rs @@ -47,14 +47,18 @@ fn to_snake_case(name: &str) -> String { /// Validate a panel name against basic rules. /// Returns "valid" or an error description. - pub async fn minter_validate_name(name: String) -> Result { if name.is_empty() { return Ok("Panel name cannot be empty".to_string()); } // Must start with uppercase letter - if !name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) { + if !name + .chars() + .next() + .map(|c| c.is_uppercase()) + .unwrap_or(false) + { return Ok("Panel name must be PascalCase (start with uppercase)".to_string()); } @@ -65,8 +69,8 @@ pub async fn minter_validate_name(name: String) -> Result { // Check for reserved names let reserved = [ - "Model", "View", "Update", "Msg", "App", "Main", "Tea", "Panel", - "Pane", "PaneL", "PaneN", "PaneW", + "Model", "View", "Update", "Msg", "App", "Main", "Tea", "Panel", "Pane", "PaneL", "PaneN", + "PaneW", ]; if reserved.contains(&name.as_str()) { return Ok(format!("'{}' is a reserved name", name)); @@ -75,7 +79,11 @@ pub async fn minter_validate_name(name: String) -> Result { // Check if files already exist (collision detection) let model_path = PathBuf::from("src/model").join(format!("{}Model.res", name)); if model_path.exists() { - return Ok(format!("Panel '{}' already exists (found {})", name, model_path.display())); + return Ok(format!( + "Panel '{}' already exists (found {})", + name, + model_path.display() + )); } Ok("valid".to_string()) @@ -86,39 +94,28 @@ pub async fn minter_validate_name(name: String) -> Result { /// Generates all ReScript source files and Rust backend stubs, then patches /// the global wiring files to register the new panel. Returns a JSON-serialised /// MintResult with the list of created and patched files. - -pub async fn minter_mint_panel( - panel_name: String, - short_name: String, - description: String, - icon: String, - backend_kind: String, - accessibility: String, - capabilities: String, - endpoint: String, - plugin_type: Option, - protocol_dependencies: Option, - sandbox_policy: Option, - groove_endpoint: Option, -) -> Result { - let _request = MintRequest { - panel_name: panel_name.clone(), - short_name: short_name.clone(), - description: description.clone(), - icon: icon.clone(), - backend_kind: backend_kind.clone(), - accessibility: accessibility.clone(), - capabilities: capabilities.clone(), - endpoint: endpoint.clone(), - plugin_type: plugin_type.clone(), - protocol_dependencies: protocol_dependencies.clone(), - sandbox_policy: sandbox_policy.clone(), - groove_endpoint: groove_endpoint.clone(), - }; +pub async fn minter_mint_panel(request: MintRequest) -> Result { + // `plugin_type`, `protocol_dependencies`, `sandbox_policy` and + // `groove_endpoint` are accepted but not yet used by the generators. + let MintRequest { + panel_name, + short_name, + description, + icon, + backend_kind, + accessibility, + capabilities, + endpoint, + .. + } = request; // Validate panel_name to prevent path traversal — must be alphanumeric PascalCase. if panel_name.is_empty() - || !panel_name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) + || !panel_name + .chars() + .next() + .map(|c| c.is_uppercase()) + .unwrap_or(false) || !panel_name.chars().all(|c| c.is_alphanumeric()) { return Err("Panel name must be non-empty alphanumeric PascalCase".to_string()); @@ -141,25 +138,45 @@ pub async fn minter_mint_panel( let model_path = PathBuf::from("src/model").join(format!("{}Model.res", panel_name)); let model_path = model_path.to_string_lossy().to_string(); let model_content = generate_model(&panel_name, &camel, &description); - write_file_safe(&model_path, &model_content, &mut files_created, &mut warnings)?; + write_file_safe( + &model_path, + &model_content, + &mut files_created, + &mut warnings, + )?; // 2. Module registration let module_path = PathBuf::from("src/modules").join(format!("{}Module.res", panel_name)); let module_path = module_path.to_string_lossy().to_string(); let module_content = generate_module(&panel_name, &description, &capabilities); - write_file_safe(&module_path, &module_content, &mut files_created, &mut warnings)?; + write_file_safe( + &module_path, + &module_content, + &mut files_created, + &mut warnings, + )?; // 3. Engine (pure computation) let engine_path = PathBuf::from("src/core").join(format!("{}Engine.res", panel_name)); let engine_path = engine_path.to_string_lossy().to_string(); let engine_content = generate_engine(&panel_name); - write_file_safe(&engine_path, &engine_content, &mut files_created, &mut warnings)?; + write_file_safe( + &engine_path, + &engine_content, + &mut files_created, + &mut warnings, + )?; // 4. Component (view) let component_path = PathBuf::from("src/components").join(format!("{}.res", panel_name)); let component_path = component_path.to_string_lossy().to_string(); let component_content = generate_component(&panel_name, &camel, &description); - write_file_safe(&component_path, &component_content, &mut files_created, &mut warnings)?; + write_file_safe( + &component_path, + &component_content, + &mut files_created, + &mut warnings, + )?; // 5. Commands (if backend needed) if has_backend { @@ -180,15 +197,30 @@ pub async fn minter_mint_panel( let mod_path = rust_dir.join("mod.rs"); let mod_content = generate_rust_mod(&panel_name, &description); - write_file_safe(&mod_path.to_string_lossy(), &mod_content, &mut files_created, &mut warnings)?; + write_file_safe( + &mod_path.to_string_lossy(), + &mod_content, + &mut files_created, + &mut warnings, + )?; let types_path = rust_dir.join("types.rs"); let types_content = generate_rust_types(&panel_name); - write_file_safe(&types_path.to_string_lossy(), &types_content, &mut files_created, &mut warnings)?; + write_file_safe( + &types_path.to_string_lossy(), + &types_content, + &mut files_created, + &mut warnings, + )?; let commands_path = rust_dir.join("commands.rs"); let commands_content = generate_rust_commands(&panel_name, &snake); - write_file_safe(&commands_path.to_string_lossy(), &commands_content, &mut files_created, &mut warnings)?; + write_file_safe( + &commands_path.to_string_lossy(), + &commands_content, + &mut files_created, + &mut warnings, + )?; } // ========================================================================= @@ -203,7 +235,8 @@ pub async fn minter_mint_panel( } // Patch PanelRegistry.res — add panel metadata entry - if let Err(e) = patch_panel_registry(&panel_name, &short_name, &description, &icon, has_backend) { + if let Err(e) = patch_panel_registry(&panel_name, &short_name, &description, &icon, has_backend) + { warnings.push(format!("Could not patch PanelRegistry.res: {}", e)); } else { files_patched.push("src/modules/PanelRegistry.res".to_string()); @@ -243,8 +276,13 @@ pub async fn minter_mint_panel( let manifest_dir = format!("src/panels/{}/panels", snake); let manifest_path = format!("{}/manifest.json", manifest_dir); - let manifest_content = generate_panel_manifest(&panel_name, &snake, has_backend); - write_file_safe(&manifest_path, &manifest_content, &mut files_created, &mut warnings)?; + let manifest_content = generate_panel_manifest(&snake, has_backend); + write_file_safe( + &manifest_path, + &manifest_content, + &mut files_created, + &mut warnings, + )?; // ========================================================================= // Generate clade manifest (A2ML) @@ -253,7 +291,12 @@ pub async fn minter_mint_panel( let clade_dir = format!("panel-clades/clades/{}", camel); let clade_path = format!("{}/{}.a2ml", clade_dir, panel_name); let clade_content = generate_clade_manifest(&panel_name, &camel, &description, has_backend); - write_file_safe(&clade_path, &clade_content, &mut files_created, &mut warnings)?; + write_file_safe( + &clade_path, + &clade_content, + &mut files_created, + &mut warnings, + )?; // ========================================================================= // Generate bot directives @@ -262,7 +305,12 @@ pub async fn minter_mint_panel( let directives_dir = "panel-clades/.machine_readable/bot_directives"; let directive_path = format!("{}/{}.scm", directives_dir, snake); let directive_content = generate_bot_directives(&panel_name, &snake, has_backend); - write_file_safe(&directive_path, &directive_content, &mut files_created, &mut warnings)?; + write_file_safe( + &directive_path, + &directive_content, + &mut files_created, + &mut warnings, + )?; // ========================================================================= // Post-mint validation: verify wiring + run bot checks @@ -294,8 +342,7 @@ pub async fn minter_mint_panel( bot_findings, }; - serde_json::to_string(&result) - .map_err(|e| format!("Failed to serialise result: {}", e)) + serde_json::to_string(&result).map_err(|e| format!("Failed to serialise result: {}", e)) } // ============================================================================= @@ -320,8 +367,7 @@ fn write_file_safe( .map_err(|e| format!("Failed to create directory for {}: {}", path, e))?; } - fs::write(path, content) - .map_err(|e| format!("Failed to write {}: {}", path, e))?; + fs::write(path, content).map_err(|e| format!("Failed to write {}: {}", path, e))?; created.push(path.to_string()); Ok(()) @@ -359,10 +405,10 @@ type {camel}State = {{ /// Generate the XModule.res file — capability registration. fn generate_module(name: &str, description: &str, capabilities_json: &str) -> String { // Parse capabilities from JSON - let caps: Vec<(String, String)> = serde_json::from_str(capabilities_json) - .unwrap_or_default(); + let caps: Vec<(String, String)> = serde_json::from_str(capabilities_json).unwrap_or_default(); - let cap_entries: String = caps.iter() + let cap_entries: String = caps + .iter() .map(|(id, label)| format!(r#" {{id: "{}", label: "{}"}}"#, id, label)) .collect::>() .join(",\n"); @@ -608,8 +654,7 @@ pub async fn {snake}_health() -> Result {{ /// Patch PanelSwitcherModel.res to add a new panel ID variant. fn patch_panel_switcher_model(name: &str) -> Result<(), String> { let path = "src/model/PanelSwitcherModel.res"; - let content = fs::read_to_string(path) - .map_err(|e| format!("Read failed: {}", e))?; + let content = fs::read_to_string(path).map_err(|e| format!("Read failed: {}", e))?; let variant = format!("Panel{}", name); if content.contains(&variant) { @@ -622,13 +667,9 @@ fn patch_panel_switcher_model(name: &str) -> Result<(), String> { return Err("Could not find PanelMinter variant as anchor".to_string()); } - let patched = content.replace( - anchor, - &format!("{}\n | {}", anchor, variant), - ); + let patched = content.replace(anchor, &format!("{}\n | {}", anchor, variant)); - fs::write(path, patched) - .map_err(|e| format!("Write failed: {}", e)) + fs::write(path, patched).map_err(|e| format!("Write failed: {}", e)) } /// Patch PanelRegistry.res to add panel metadata. @@ -640,8 +681,7 @@ fn patch_panel_registry( has_backend: bool, ) -> Result<(), String> { let path = "src/modules/PanelRegistry.res"; - let content = fs::read_to_string(path) - .map_err(|e| format!("Read failed: {}", e))?; + let content = fs::read_to_string(path).map_err(|e| format!("Read failed: {}", e))?; let panel_id = format!("Panel{}", name); if content.contains(&panel_id) { @@ -676,15 +716,13 @@ fn patch_panel_registry( ); let patched = content.replace(anchor, &entry); - fs::write(path, patched) - .map_err(|e| format!("Write failed: {}", e)) + fs::write(path, patched).map_err(|e| format!("Write failed: {}", e)) } /// Patch Model.res to add include and state field. fn patch_model(name: &str, camel: &str) -> Result<(), String> { let path = "src/Model.res"; - let content = fs::read_to_string(path) - .map_err(|e| format!("Read failed: {}", e))?; + let content = fs::read_to_string(path).map_err(|e| format!("Read failed: {}", e))?; if content.contains(&format!("include {}Model", name)) { return Ok(()); // Already included @@ -694,10 +732,7 @@ fn patch_model(name: &str, camel: &str) -> Result<(), String> { let include_anchor = "include PanelSwitcherModel"; let patched = content.replace( include_anchor, - &format!( - "include {}Model\n\n{}", - name, include_anchor, - ), + &format!("include {}Model\n\n{}", name, include_anchor,), ); // Add state field before panelSwitcher field @@ -720,15 +755,13 @@ fn patch_model(name: &str, camel: &str) -> Result<(), String> { ), ); - fs::write(path, patched) - .map_err(|e| format!("Write failed: {}", e)) + fs::write(path, patched).map_err(|e| format!("Write failed: {}", e)) } /// Patch Msg.res to add message type and routing variant. fn patch_msg(name: &str, camel: &str) -> Result<(), String> { let path = "src/Msg.res"; - let content = fs::read_to_string(path) - .map_err(|e| format!("Read failed: {}", e))?; + let content = fs::read_to_string(path).map_err(|e| format!("Read failed: {}", e))?; if content.contains(&format!("| {}(", name)) { return Ok(()); // Already present @@ -754,21 +787,16 @@ type {camel}Msg = let variant_anchor = " | PanelSwitcher(panelSwitcherMsg)"; let patched = patched.replace( variant_anchor, - &format!( - " | {}({}Msg)\n{}", - name, camel, variant_anchor, - ), + &format!(" | {}({}Msg)\n{}", name, camel, variant_anchor,), ); - fs::write(path, patched) - .map_err(|e| format!("Write failed: {}", e)) + fs::write(path, patched).map_err(|e| format!("Write failed: {}", e)) } /// Patch View.res to add renderActivePanel case. fn patch_view(name: &str, camel: &str) -> Result<(), String> { let path = "src/View.res"; - let content = fs::read_to_string(path) - .map_err(|e| format!("Read failed: {}", e))?; + let content = fs::read_to_string(path).map_err(|e| format!("Read failed: {}", e))?; let case = format!("Panel{}", name); if content.contains(&case) { @@ -785,8 +813,7 @@ fn patch_view(name: &str, camel: &str) -> Result<(), String> { ), ); - fs::write(path, patched) - .map_err(|e| format!("Write failed: {}", e)) + fs::write(path, patched).map_err(|e| format!("Write failed: {}", e)) } // ============================================================================= @@ -798,7 +825,7 @@ fn patch_view(name: &str, camel: &str) -> Result<(), String> { /// This manifest declares the panel's endpoints for both Gossamer and Tauri /// runtimes, health check configuration, and data sources. PanelHarness /// discovers this file and wires the panel into the runtime. -fn generate_panel_manifest(name: &str, snake: &str, has_backend: bool) -> String { +fn generate_panel_manifest(snake: &str, has_backend: bool) -> String { let health_block = if has_backend { format!( r#", @@ -822,11 +849,7 @@ fn generate_panel_manifest(name: &str, snake: &str, has_backend: bool) -> String String::new() }; - let capabilities = if has_backend { - r#"["shell"]"# - } else { - "[]" - }; + let capabilities = if has_backend { r#"["shell"]"# } else { "[]" }; format!( r#"{{ @@ -858,7 +881,12 @@ fn generate_panel_manifest(name: &str, snake: &str, has_backend: bool) -> String /// /// This registers the panel in the clade taxonomy so the CladeBrowser, /// Provisioner, and bot fleet can discover and classify it. -fn generate_clade_manifest(name: &str, camel: &str, description: &str, has_backend: bool) -> String { +fn generate_clade_manifest( + name: &str, + camel: &str, + description: &str, + has_backend: bool, +) -> String { let backend_trait = if has_backend { "true" } else { "false" }; format!( r#"; SPDX-License-Identifier: MPL-2.0 @@ -968,12 +996,7 @@ fn generate_bot_directives(name: &str, snake: &str, has_backend: bool) -> String /// /// This is what powers the Wiring Inspector — it checks whether each /// global file was successfully patched with the panel's entry. -fn verify_wiring( - name: &str, - _camel: &str, - snake: &str, - has_backend: bool, -) -> Vec { +fn verify_wiring(name: &str, _camel: &str, snake: &str, has_backend: bool) -> Vec { let variant = format!("Panel{}", name); let include_marker = format!("include {}Model", name); let msg_marker = format!("| {}(", name); @@ -981,12 +1004,24 @@ fn verify_wiring( let update_marker = format!("update{}", name); let checks = vec![ - ("src/model/PanelSwitcherModel.res", "Panel ID variant", &variant), - ("src/modules/PanelRegistry.res", "Panel metadata entry", &variant), + ( + "src/model/PanelSwitcherModel.res", + "Panel ID variant", + &variant, + ), + ( + "src/modules/PanelRegistry.res", + "Panel metadata entry", + &variant, + ), ("src/Model.res", "Include + state field", &include_marker), ("src/Msg.res", "Message type + routing variant", &msg_marker), ("src/View.res", "renderActivePanel case", &view_marker), - ("src/Update.res", "Sub-updater + routing case", &update_marker), + ( + "src/Update.res", + "Sub-updater + routing case", + &update_marker, + ), ]; let mut details: Vec = checks @@ -999,7 +1034,11 @@ fn verify_wiring( file: file.to_string(), description: desc.to_string(), connected, - note: if connected { None } else { Some(format!("Missing: {}", marker)) }, + note: if connected { + None + } else { + Some(format!("Missing: {}", marker)) + }, } }) .collect(); @@ -1117,7 +1156,8 @@ fn run_post_mint_checks( bot: "glambot".to_string(), rule_id: "PANEL-010".to_string(), severity: "warning".to_string(), - message: "Component has no ARIA labels — screen readers cannot navigate".to_string(), + message: "Component has no ARIA labels — screen readers cannot navigate" + .to_string(), file: Some(component_path.clone()), }); } @@ -1126,7 +1166,9 @@ fn run_post_mint_checks( bot: "glambot".to_string(), rule_id: "PANEL-010".to_string(), severity: "info".to_string(), - message: "Component has no role attributes — consider adding for semantic structure".to_string(), + message: + "Component has no role attributes — consider adding for semantic structure" + .to_string(), file: Some(component_path.clone()), }); } @@ -1138,7 +1180,8 @@ fn run_post_mint_checks( bot: "glambot".to_string(), rule_id: "PANEL-012".to_string(), severity: "warning".to_string(), - message: "A2ML clade manifest not generated — panel won't appear in CladeBrowser".to_string(), + message: "A2ML clade manifest not generated — panel won't appear in CladeBrowser" + .to_string(), file: Some(clade_path.to_string()), }); } @@ -1151,7 +1194,8 @@ fn run_post_mint_checks( bot: "glambot".to_string(), rule_id: "PANEL-013".to_string(), severity: "info".to_string(), - message: "Engine has no doc comments — add /// annotations for documentation".to_string(), + message: "Engine has no doc comments — add /// annotations for documentation" + .to_string(), file: Some(engine_path), }); } @@ -1187,7 +1231,10 @@ fn run_post_mint_checks( bot: "finishbot".to_string(), rule_id: "PANEL-040".to_string(), severity: "warning".to_string(), - message: format!("No test file found at {} — create tests before release", test_path), + message: format!( + "No test file found at {} — create tests before release", + test_path + ), file: Some(test_path), }); } @@ -1198,7 +1245,8 @@ fn run_post_mint_checks( bot: "finishbot".to_string(), rule_id: "PANEL-044".to_string(), severity: "warning".to_string(), - message: "Bot directives file not generated — fleet cannot validate this panel".to_string(), + message: "Bot directives file not generated — fleet cannot validate this panel" + .to_string(), file: Some(directive_path.to_string()), }); } @@ -1209,8 +1257,7 @@ fn run_post_mint_checks( /// Patch Update.res to add a minimal sub-updater and routing case. fn patch_update(name: &str, camel: &str) -> Result<(), String> { let path = "src/Update.res"; - let content = fs::read_to_string(path) - .map_err(|e| format!("Read failed: {}", e))?; + let content = fs::read_to_string(path).map_err(|e| format!("Read failed: {}", e))?; if content.contains(&format!("update{}", name)) { return Ok(()); // Already present @@ -1249,6 +1296,5 @@ let update{name} = (model: model, msg: {camel}Msg): (model, Tea_Cmd.t) => { ), ); - fs::write(path, patched) - .map_err(|e| format!("Write failed: {}", e)) + fs::write(path, patched).map_err(|e| format!("Write failed: {}", e)) } diff --git a/src-gossamer/src/minter/mod.rs b/src-gossamer/src/minter/mod.rs index fa916dce..9fdbc7d2 100644 --- a/src-gossamer/src/minter/mod.rs +++ b/src-gossamer/src/minter/mod.rs @@ -13,5 +13,5 @@ //! navigation by default — it is structurally harder to create an //! inaccessible panel than an accessible one. -pub mod types; pub mod commands; +pub mod types; diff --git a/src-gossamer/src/multiplayer_monitor/commands.rs b/src-gossamer/src/multiplayer_monitor/commands.rs index 6be98112..93c30ab0 100644 --- a/src-gossamer/src/multiplayer_monitor/commands.rs +++ b/src-gossamer/src/multiplayer_monitor/commands.rs @@ -35,9 +35,8 @@ impl ConnectionState { } } -static CONNECTION_STATE: Lazy> = Lazy::new(|| { - Mutex::new(ConnectionState::new()) -}); +static CONNECTION_STATE: Lazy> = + Lazy::new(|| Mutex::new(ConnectionState::new())); /// Serialises tests that share `CONNECTION_STATE` so they cannot race. #[cfg(test)] @@ -56,13 +55,14 @@ fn now_secs() -> u64 { /// Validates the URL and records the connection in memory. Currently a stub /// that simulates a successful WebSocket handshake. When the real Phoenix /// bridge is implemented, this will open a persistent WebSocket connection. - pub async fn multiplayer_connect(url: String) -> Result { if url.is_empty() { return Err("Server URL cannot be empty".to_string()); } - let mut state = CONNECTION_STATE.lock().map_err(|e| format!("Lock error: {e}"))?; + let mut state = CONNECTION_STATE + .lock() + .map_err(|e| format!("Lock error: {e}"))?; state.connected = true; state.server_url = Some(url.clone()); state.connected_at = Some(now_secs()); @@ -73,17 +73,17 @@ pub async fn multiplayer_connect(url: String) -> Result { "wsState": "connected", "connectedAt": state.connected_at, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Disconnect from the sync server. /// /// Clears the in-memory connection state. When the real bridge is /// implemented, this will close the WebSocket and clean up channels. - pub async fn multiplayer_disconnect() -> Result { - let mut state = CONNECTION_STATE.lock().map_err(|e| format!("Lock error: {e}"))?; + let mut state = CONNECTION_STATE + .lock() + .map_err(|e| format!("Lock error: {e}"))?; let was_connected = state.connected; state.connected = false; state.server_url = None; @@ -94,8 +94,7 @@ pub async fn multiplayer_disconnect() -> Result { "wasConnected": was_connected, "wsState": "disconnected", }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Read the current multiplayer state (players, channels, locks). @@ -103,9 +102,10 @@ pub async fn multiplayer_disconnect() -> Result { /// Returns stub data representing a typical multiplayer session with two /// players, two channels, and one device lock. When the real bridge is /// implemented, this will query the live Phoenix server state. - pub async fn multiplayer_read_state() -> Result { - let state = CONNECTION_STATE.lock().map_err(|e| format!("Lock error: {e}"))?; + let state = CONNECTION_STATE + .lock() + .map_err(|e| format!("Lock error: {e}"))?; let ts = now_secs() as f64; let result = json!({ @@ -156,8 +156,7 @@ pub async fn multiplayer_read_state() -> Result { }, ], }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Read state diffs between local and remote. @@ -165,7 +164,6 @@ pub async fn multiplayer_read_state() -> Result { /// Returns stub diff entries showing typical sync divergence scenarios. /// When the real bridge is implemented, this will compare local ETS state /// against the server's authoritative state. - pub async fn multiplayer_read_diffs() -> Result { let ts = now_secs() as f64; @@ -191,8 +189,7 @@ pub async fn multiplayer_read_diffs() -> Result { "totalUnresolved": 1, "totalResolved": 1, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Read ETS cache entries for inspection. @@ -200,7 +197,6 @@ pub async fn multiplayer_read_diffs() -> Result { /// Returns stub ETS table entries representing typical Erlang Term Storage /// data from the Phoenix server. When the real bridge is implemented, this /// will query the server's ETS tables via the debug API. - pub async fn multiplayer_read_ets() -> Result { let result = json!({ "entries": [ @@ -226,8 +222,7 @@ pub async fn multiplayer_read_ets() -> Result { "tableCount": 2, "totalEntries": 3, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Trigger a reconnection test cycle. @@ -236,9 +231,10 @@ pub async fn multiplayer_read_ets() -> Result { /// When the real bridge is implemented, this will actually drop and /// re-establish the WebSocket connection, measuring reconnection latency /// and state recovery. - pub async fn multiplayer_reconnection_test() -> Result { - let state = CONNECTION_STATE.lock().map_err(|e| format!("Lock error: {e}"))?; + let state = CONNECTION_STATE + .lock() + .map_err(|e| format!("Lock error: {e}"))?; if !state.connected { return Err("Cannot run reconnection test: not connected".to_string()); @@ -252,8 +248,7 @@ pub async fn multiplayer_reconnection_test() -> Result { "channelsRejoined": 2, "messagesLost": 0, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } #[cfg(test)] diff --git a/src-gossamer/src/network_topology/commands.rs b/src-gossamer/src/network_topology/commands.rs index 0744a081..0c496342 100644 --- a/src-gossamer/src/network_topology/commands.rs +++ b/src-gossamer/src/network_topology/commands.rs @@ -19,7 +19,6 @@ use serde_json::json; /// /// Currently returns stub data representing a small sample network. When the /// IDApTIK engine bridge is implemented this will query the live game state. - pub async fn read_network_topology() -> Result { let result = json!({ "devices": [ @@ -102,8 +101,7 @@ pub async fn read_network_topology() -> Result { } ] }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Read DNS resolution entries from the running game. @@ -111,7 +109,6 @@ pub async fn read_network_topology() -> Result { /// Returns a JSON array of DNS entries, each with hostname, resolved IP, /// record type, and TTL. Currently returns stub data representing a sample /// DNS table from the IDApTIK game world. - pub async fn read_dns_table() -> Result { let result = json!({ "entries": [ @@ -147,8 +144,7 @@ pub async fn read_dns_table() -> Result { } ] }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Read packet flow events from the running game for animation. @@ -156,7 +152,6 @@ pub async fn read_dns_table() -> Result { /// Returns a JSON array of recent packet flow events, each tied to a /// connection ID with timestamp, size, and blocked status. Currently /// returns stub data representing sample network traffic. - pub async fn read_packet_flow() -> Result { let ts = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -191,8 +186,7 @@ pub async fn read_packet_flow() -> Result { } ] }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Export the current topology as an SVG string. @@ -201,7 +195,6 @@ pub async fn read_packet_flow() -> Result { /// devices as circles and connections as lines. Currently returns a stub /// SVG with placeholder geometry. When the engine bridge is implemented, /// this will render the live topology graph. - pub async fn export_topology_svg() -> Result { let svg = r#" @@ -232,8 +225,7 @@ pub async fn export_topology_svg() -> Result { "connectionCount": 3, "stub": true, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } #[cfg(test)] diff --git a/src-gossamer/src/observability/commands.rs b/src-gossamer/src/observability/commands.rs index b6cf713c..f5fe61e9 100644 --- a/src-gossamer/src/observability/commands.rs +++ b/src-gossamer/src/observability/commands.rs @@ -22,7 +22,6 @@ use serde_json::json; /// /// Mock implementation: returns a success acknowledgement with the report ID. /// Real implementation will POST to BoJ observe-mcp cartridge endpoint. - pub async fn observe_export_sarif(report_id: String) -> Result { if report_id.is_empty() { return Err("report_id must not be empty".to_string()); @@ -48,7 +47,6 @@ pub async fn observe_export_sarif(report_id: String) -> Result { /// Mock implementation: parses the batch to count spans and returns /// an acceptance summary. Real implementation will POST to the /// OTLP HTTP endpoint (typically :4318/v1/traces). - pub async fn observe_export_traces(batch: String) -> Result { if batch.is_empty() { return Err("batch must not be empty".to_string()); @@ -72,7 +70,6 @@ pub async fn observe_export_traces(batch: String) -> Result { /// /// Mock implementation: returns static placeholder values. /// Real implementation will GET /cartridges/observe-mcp/tools/summary. - pub async fn observe_summary() -> Result { let response = json!({ "status": "healthy", diff --git a/src-gossamer/src/overlay/commands.rs b/src-gossamer/src/overlay/commands.rs index cc06bc78..1adfe9c5 100644 --- a/src-gossamer/src/overlay/commands.rs +++ b/src-gossamer/src/overlay/commands.rs @@ -84,13 +84,11 @@ fn delete(path: &str, timeout_secs: u64) -> Result { /// GET /status — combined status of all overlay networks. /// Returns JSON: `{tor: "connected"|"disconnected"|..., ipfs: ..., ethereum: ...}` - pub fn overlay_status() -> Result { get("/status", 5) } /// GET /health — overlay subsystem health check. - pub fn overlay_health() -> Result { get("/health", 5) } @@ -101,7 +99,6 @@ pub fn overlay_health() -> Result { /// POST /tor/connect — connect to Tor control port. /// Body: `{control_host?, control_port?, socks_port?, auth_method?, auth_data?}` - pub fn overlay_tor_connect( control_host: Option, control_port: Option, @@ -120,48 +117,42 @@ pub fn overlay_tor_connect( } /// POST /tor/disconnect — disconnect from Tor. - pub fn overlay_tor_disconnect() -> Result { post("/tor/disconnect", json!({}), 5) } /// GET /tor/status — Tor connection status. - pub fn overlay_tor_status() -> Result { get("/tor/status", 5) } /// POST /tor/hidden-service — create a new Tor hidden service. /// Body: `{port, target_port}` - -pub fn overlay_tor_create_hidden_service( - port: u16, - target_port: u16, -) -> Result { - post("/tor/hidden-service", json!({"port": port, "target_port": target_port}), 30) +pub fn overlay_tor_create_hidden_service(port: u16, target_port: u16) -> Result { + post( + "/tor/hidden-service", + json!({"port": port, "target_port": target_port}), + 30, + ) } /// DELETE /tor/hidden-service/{onion} — destroy a hidden service. - pub fn overlay_tor_destroy_hidden_service(onion_address: String) -> Result { delete(&format!("/tor/hidden-service/{}", onion_address), 10) } /// GET /tor/circuits — list active Tor circuits. - pub fn overlay_tor_list_circuits() -> Result { get("/tor/circuits", 10) } /// GET /tor/circuits/{id} — get a specific circuit's details (hops, status). - pub fn overlay_tor_get_circuit(circuit_id: String) -> Result { get(&format!("/tor/circuits/{}", circuit_id), 10) } /// POST /tor/resolve — resolve a hostname through Tor. /// Body: `{hostname}` - pub fn overlay_tor_resolve(hostname: String) -> Result { post("/tor/resolve", json!({"hostname": hostname}), 30) } @@ -172,7 +163,6 @@ pub fn overlay_tor_resolve(hostname: String) -> Result { /// POST /ipfs/connect — connect to IPFS daemon. /// Body: `{api_host?, api_port?, gateway_port?, repo_path?}` - pub fn overlay_ipfs_connect( api_host: Option, api_port: Option, @@ -189,20 +179,17 @@ pub fn overlay_ipfs_connect( } /// POST /ipfs/disconnect — disconnect from IPFS. - pub fn overlay_ipfs_disconnect() -> Result { post("/ipfs/disconnect", json!({}), 5) } /// GET /ipfs/status — IPFS daemon status. - pub fn overlay_ipfs_status() -> Result { get("/ipfs/status", 5) } /// POST /ipfs/add — add content to IPFS, returns CID. /// Body: `{data, content_type?}` - pub fn overlay_ipfs_add(data: String, content_type: Option) -> Result { post( "/ipfs/add", @@ -212,25 +199,21 @@ pub fn overlay_ipfs_add(data: String, content_type: Option) -> Result Result { get(&format!("/ipfs/cat/{}", cid), 30) } /// POST /ipfs/pin — pin content by CID. - pub fn overlay_ipfs_pin(cid: String) -> Result { post("/ipfs/pin", json!({"cid": cid}), 30) } /// POST /ipfs/unpin — unpin content by CID. - pub fn overlay_ipfs_unpin(cid: String) -> Result { post("/ipfs/unpin", json!({"cid": cid}), 30) } /// GET /ipfs/dag/{cid} — get DAG node for proof graph traversal. - pub fn overlay_ipfs_dag_get(cid: String) -> Result { get(&format!("/ipfs/dag/{}", cid), 30) } @@ -241,7 +224,6 @@ pub fn overlay_ipfs_dag_get(cid: String) -> Result { /// POST /eth/connect — connect to Ethereum JSON-RPC endpoint. /// Body: `{rpc_url?, network?, chain_id?}` - pub fn overlay_eth_connect( rpc_url: Option, network: Option, @@ -256,27 +238,27 @@ pub fn overlay_eth_connect( } /// POST /eth/disconnect — disconnect from Ethereum. - pub fn overlay_eth_disconnect() -> Result { post("/eth/disconnect", json!({}), 5) } /// GET /eth/status — Ethereum connection status. - pub fn overlay_eth_status() -> Result { get("/eth/status", 5) } /// POST /eth/timestamp-proof — anchor a proof certificate hash on-chain. /// Body: `{proof_hash}`. Returns tx hash, block number, timestamp. - pub fn overlay_eth_timestamp_proof(proof_hash: String) -> Result { - post("/eth/timestamp-proof", json!({"proof_hash": proof_hash}), 60) + post( + "/eth/timestamp-proof", + json!({"proof_hash": proof_hash}), + 60, + ) } /// POST /eth/verify-timestamp — verify a previously anchored timestamp. /// Body: `{tx_hash}`. Returns proof hash, block, timestamp, contract address. - pub fn overlay_eth_verify_timestamp(tx_hash: String) -> Result { post("/eth/verify-timestamp", json!({"tx_hash": tx_hash}), 30) } diff --git a/src-gossamer/src/plaza/commands.rs b/src-gossamer/src/plaza/commands.rs index a624fee7..830e4604 100644 --- a/src-gossamer/src/plaza/commands.rs +++ b/src-gossamer/src/plaza/commands.rs @@ -47,7 +47,9 @@ fn detect_license(repo_path: &Path) -> Option { return Some("PMPL-1.0-or-later".to_string()); } else if lower.contains("mozilla public license") { return Some("MPL-2.0".to_string()); - } else if lower.contains("mit license") || lower.contains("permission is hereby granted") { + } else if lower.contains("mit license") + || lower.contains("permission is hereby granted") + { return Some("MIT".to_string()); } else if lower.contains("apache license") { return Some("Apache-2.0".to_string()); @@ -66,11 +68,10 @@ fn detect_license(repo_path: &Path) -> Option { /// Source file extensions to scan for SPDX headers. const SOURCE_EXTENSIONS: &[&str] = &[ - "rs", "res", "resi", "gleam", "ex", "exs", "idr", "zig", - "ml", "mli", "hs", "adb", "ads", "jl", "sh", "bash", - "js", "mjs", "jsx", "ts", "tsx", "json", "toml", "yaml", "yml", - "nix", "scm", "ncl", "v", "go", "py", "rb", "java", "kt", - "c", "h", "cpp", "hpp", "swift", "lua", "dart", "php", + "rs", "res", "resi", "gleam", "ex", "exs", "idr", "zig", "ml", "mli", "hs", "adb", "ads", "jl", + "sh", "bash", "js", "mjs", "jsx", "ts", "tsx", "json", "toml", "yaml", "yml", "nix", "scm", + "ncl", "v", "go", "py", "rb", "java", "kt", "c", "h", "cpp", "hpp", "swift", "lua", "dart", + "php", ]; /// Check if a path is a source file worth scanning. @@ -94,16 +95,32 @@ fn scan_repo(repo_path: &Path) -> RepoScanResult { // Check for exhibits. let has_exhibit_a = repo_path.join("EXHIBIT-A-ETHICAL-USE.txt").exists() - || repo_path.join("legal").join("exhibits").join("EXHIBIT-A-ETHICAL-USE.txt").exists() - || repo_path.join("v1.0").join("exhibits").join("EXHIBIT-A-ETHICAL-USE.txt").exists(); + || repo_path + .join("legal") + .join("exhibits") + .join("EXHIBIT-A-ETHICAL-USE.txt") + .exists() + || repo_path + .join("v1.0") + .join("exhibits") + .join("EXHIBIT-A-ETHICAL-USE.txt") + .exists(); let has_exhibit_b = repo_path.join("EXHIBIT-B-QUANTUM-SAFE.txt").exists() - || repo_path.join("legal").join("exhibits").join("EXHIBIT-B-QUANTUM-SAFE.txt").exists() - || repo_path.join("v1.0").join("exhibits").join("EXHIBIT-B-QUANTUM-SAFE.txt").exists(); + || repo_path + .join("legal") + .join("exhibits") + .join("EXHIBIT-B-QUANTUM-SAFE.txt") + .exists() + || repo_path + .join("v1.0") + .join("exhibits") + .join("EXHIBIT-B-QUANTUM-SAFE.txt") + .exists(); // Check for provenance signatures. - let has_provenance_sig = repo_path.join(".provenance").exists() - || repo_path.join("PROVENANCE.sig").exists(); + let has_provenance_sig = + repo_path.join(".provenance").exists() || repo_path.join("PROVENANCE.sig").exists(); // Count source files and SPDX headers (limited depth for speed). let mut total_source = 0u32; @@ -122,8 +139,12 @@ fn scan_repo(repo_path: &Path) -> RepoScanResult { if path.is_dir() { let dirname = path.file_name().unwrap_or_default().to_string_lossy(); // Skip hidden dirs, node_modules, target, etc. - if dirname.starts_with('.') || dirname == "node_modules" || dirname == "target" - || dirname == "_build" || dirname == "dist" || dirname == "build" + if dirname.starts_with('.') + || dirname == "node_modules" + || dirname == "target" + || dirname == "_build" + || dirname == "dist" + || dirname == "build" { continue; } @@ -158,7 +179,6 @@ fn scan_repo(repo_path: &Path) -> RepoScanResult { /// /// Takes a repo name and scans its directory under the canonical repos path. /// Returns a JSON-serialised `RepoScanResult`. - pub async fn plaza_scan_repo(repo_name: String) -> Result { let repo_path = repos_dir().join(&repo_name); if !repo_path.exists() { @@ -166,15 +186,13 @@ pub async fn plaza_scan_repo(repo_name: String) -> Result { } let result = scan_repo(&repo_path); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Compute adoption statistics across the entire ecosystem. /// /// Scans all directories under the canonical repos path and aggregates /// license detection results. Returns JSON-serialised `AdoptionStats`. - pub async fn plaza_adoption_stats() -> Result { let base = repos_dir(); if !base.exists() { @@ -188,8 +206,7 @@ pub async fn plaza_adoption_stats() -> Result { let mut quantum_signed = 0u32; let mut by_license: HashMap = HashMap::new(); - let entries = fs::read_dir(&base) - .map_err(|e| format!("Cannot read repos dir: {e}"))?; + let entries = fs::read_dir(&base).map_err(|e| format!("Cannot read repos dir: {e}"))?; for entry in entries.flatten() { let path = entry.path(); @@ -234,38 +251,59 @@ pub async fn plaza_adoption_stats() -> Result { by_license, }; - serde_json::to_string(&stats) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&stats).map_err(|e| format!("Serialisation error: {e}")) } /// Known compatibility matrix for PMPL with common licenses. /// PMPL-1.0 is file-level copyleft (like MPL-2.0), so it's compatible /// with most permissive and weak-copyleft licenses. - pub async fn plaza_check_compatibility(license: String) -> Result { let (compatible, notes) = match license.to_uppercase().as_str() { - "MIT" => (true, "Fully compatible. MIT files can coexist with PMPL files in the same project."), - "BSD-2-CLAUSE" | "BSD-3-CLAUSE" | "BSD" => (true, "Fully compatible. BSD files can coexist with PMPL files."), - "APACHE-2.0" => (true, "Compatible. Apache-2.0 patent grant complements PMPL."), - "MPL-2.0" => (true, "Fully compatible. PMPL is built on MPL-2.0 as its base layer."), - "LGPL-2.1" | "LGPL-2.1-OR-LATER" | "LGPL-3.0" | "LGPL-3.0-OR-LATER" => { - (true, "Compatible for library use. LGPL files may impose dynamic linking requirements.") - } - "GPL-2.0" | "GPL-2.0-OR-LATER" => { - (true, "Compatible per MPL-2.0 Section 3.3. GPL may apply to combined work.") - } - "GPL-3.0" | "GPL-3.0-OR-LATER" => { - (true, "Compatible per MPL-2.0 Section 3.3. GPL-3.0 applies to combined work.") - } - "AGPL-3.0" | "AGPL-3.0-OR-LATER" => { - (false, "Not directly compatible. AGPL network copyleft conflicts with PMPL file-level scope.") - } - "CC0-1.0" | "UNLICENSE" => { - (true, "Compatible. Public domain dedications can coexist with any license.") - } - "ISC" => (true, "Fully compatible. ISC is functionally equivalent to MIT."), + "MIT" => ( + true, + "Fully compatible. MIT files can coexist with PMPL files in the same project.", + ), + "BSD-2-CLAUSE" | "BSD-3-CLAUSE" | "BSD" => ( + true, + "Fully compatible. BSD files can coexist with PMPL files.", + ), + "APACHE-2.0" => ( + true, + "Compatible. Apache-2.0 patent grant complements PMPL.", + ), + "MPL-2.0" => ( + true, + "Fully compatible. PMPL is built on MPL-2.0 as its base layer.", + ), + "LGPL-2.1" | "LGPL-2.1-OR-LATER" | "LGPL-3.0" | "LGPL-3.0-OR-LATER" => ( + true, + "Compatible for library use. LGPL files may impose dynamic linking requirements.", + ), + "GPL-2.0" | "GPL-2.0-OR-LATER" => ( + true, + "Compatible per MPL-2.0 Section 3.3. GPL may apply to combined work.", + ), + "GPL-3.0" | "GPL-3.0-OR-LATER" => ( + true, + "Compatible per MPL-2.0 Section 3.3. GPL-3.0 applies to combined work.", + ), + "AGPL-3.0" | "AGPL-3.0-OR-LATER" => ( + false, + "Not directly compatible. AGPL network copyleft conflicts with PMPL file-level scope.", + ), + "CC0-1.0" | "UNLICENSE" => ( + true, + "Compatible. Public domain dedications can coexist with any license.", + ), + "ISC" => ( + true, + "Fully compatible. ISC is functionally equivalent to MIT.", + ), "WTFPL" => (true, "Compatible. Permissive license with no restrictions."), - _ => (false, "Unknown license. Manual compatibility review recommended."), + _ => ( + false, + "Unknown license. Manual compatibility review recommended.", + ), }; let result = serde_json::json!({ @@ -274,6 +312,5 @@ pub async fn plaza_check_compatibility(license: String) -> Result = authors .into_iter() - .map(|(name, (email, lines))| { - json!({ "name": name, "email": email, "lines": lines }) - }) + .map(|(name, (email, lines))| json!({ "name": name, "email": email, "lines": lines })) .collect(); author_list.sort_by(|a, b| { - b["lines"].as_u64().unwrap_or(0).cmp(&a["lines"].as_u64().unwrap_or(0)) + b["lines"] + .as_u64() + .unwrap_or(0) + .cmp(&a["lines"].as_u64().unwrap_or(0)) }); let result = json!({ @@ -160,10 +160,9 @@ const UNSOUND_PATTERNS: &[(&str, &str, &str)] = &[ /// Admitted, assert_total, unsafeCoerce, unsafePerformIO, Obj.magic, etc.). /// Returns a JSON object with `matches` (array of {pattern, line, line_number, /// language, severity}) and `total_matches`. - pub async fn provenance_scan_unsound(file_path: String) -> Result { - let content = fs::read_to_string(&file_path) - .map_err(|e| format!("Cannot read {file_path}: {e}"))?; + let content = + fs::read_to_string(&file_path).map_err(|e| format!("Cannot read {file_path}: {e}"))?; let mut matches = Vec::new(); diff --git a/src-gossamer/src/release_manager/commands.rs b/src-gossamer/src/release_manager/commands.rs index 7b31e714..4a97b6f8 100644 --- a/src-gossamer/src/release_manager/commands.rs +++ b/src-gossamer/src/release_manager/commands.rs @@ -54,7 +54,6 @@ fn parse_semver(version: &str) -> Option<(u32, u32, u32)> { /// Accepts a `fromVersion` string and returns stub changelog entries /// representing commits since that version. When full git integration is /// implemented, this will run `git log` against the IDApTIK repository. - pub async fn release_generate_changelog(from_version: String) -> Result { let ts = now_secs(); @@ -89,8 +88,7 @@ pub async fn release_generate_changelog(from_version: String) -> Result Result Result { +pub async fn release_build_artifacts(version: String, platforms: String) -> Result { if version.is_empty() { return Err("Version cannot be empty".to_string()); } @@ -113,8 +107,12 @@ pub async fn release_build_artifacts( } let valid_platforms = [ - "web", "desktop-linux", "desktop-mac", "desktop-windows", - "mobile-android", "mobile-ios", + "web", + "desktop-linux", + "desktop-mac", + "desktop-windows", + "mobile-android", + "mobile-ios", ]; for p in &platform_list { @@ -148,8 +146,7 @@ pub async fn release_build_artifacts( "status": "ready", "stub": true, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Publish a release to a channel. @@ -158,11 +155,7 @@ pub async fn release_build_artifacts( /// stable). Creates a release manifest in the releases directory. When full /// distribution is implemented, this will upload artifacts and create a /// GitHub release. - -pub async fn release_publish( - version: String, - channel: String, -) -> Result { +pub async fn release_publish(version: String, channel: String) -> Result { if version.is_empty() { return Err("Version cannot be empty".to_string()); } @@ -199,15 +192,13 @@ pub async fn release_publish( "manifestPath": release_path.to_string_lossy(), "publishedAt": ts, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Read release history. /// /// Scans the releases directory for release manifest files and returns /// them as a JSON array sorted by publication time (newest first). - pub async fn release_read_history() -> Result { let releases_dir = ensure_releases_dir()?; @@ -231,8 +222,7 @@ pub async fn release_read_history() -> Result { ts_b.cmp(&ts_a) }); - serde_json::to_string(&releases) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&releases).map_err(|e| format!("Serialisation error: {e}")) } /// Bump the version number. @@ -240,7 +230,6 @@ pub async fn release_read_history() -> Result { /// Accepts a bump type ("major", "minor", or "patch") and returns the /// bumped version. Reads the current version from a `version.json` file /// in the releases directory, defaulting to "0.1.0" if none exists. - pub async fn release_bump_version(bump_type: String) -> Result { let valid_bump_types = ["major", "minor", "patch"]; if !valid_bump_types.contains(&bump_type.as_str()) { @@ -293,8 +282,7 @@ pub async fn release_bump_version(bump_type: String) -> Result { ) .map_err(|e| format!("Cannot write version file: {e}"))?; - serde_json::to_string(&version_data) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&version_data).map_err(|e| format!("Serialisation error: {e}")) } #[cfg(test)] @@ -321,8 +309,7 @@ mod tests { /// Clean up release manifests created during tests. fn cleanup_release(version: &str, channel: &str) { - let path = - PathBuf::from(RELEASES_DIR).join(format!("release-{version}-{channel}.json")); + let path = PathBuf::from(RELEASES_DIR).join(format!("release-{version}-{channel}.json")); let _ = fs::remove_file(path); } @@ -345,7 +332,10 @@ mod tests { let result = ensure_releases_dir(); assert!(result.is_ok(), "Should create releases directory"); let path = result.unwrap(); - assert!(path.exists(), "Releases directory should exist after creation"); + assert!( + path.exists(), + "Releases directory should exist after creation" + ); } #[test] @@ -413,8 +403,7 @@ mod tests { #[test] fn test_release_publish_invalid_channel() { rt().block_on(async { - let result = - release_publish("1.0.0".to_string(), "nightly".to_string()).await; + let result = release_publish("1.0.0".to_string(), "nightly".to_string()).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Unknown channel")); }); @@ -426,10 +415,9 @@ mod tests { rt().block_on(async { let result = release_read_history().await; assert!(result.is_ok()); - let releases: Vec = - serde_json::from_str(&result.unwrap()).unwrap(); - // Should be a valid array (may be empty or contain previous test releases). - assert!(releases.len() >= 0); + // Must parse as a JSON array (may be empty or contain previous + // test releases). + let _releases: Vec = serde_json::from_str(&result.unwrap()).unwrap(); }); } diff --git a/src-gossamer/src/repoloader/commands.rs b/src-gossamer/src/repoloader/commands.rs index 8e0cefe3..04659d56 100644 --- a/src-gossamer/src/repoloader/commands.rs +++ b/src-gossamer/src/repoloader/commands.rs @@ -40,8 +40,7 @@ fn load_recent() -> RecentReposFile { fn save_recent(recent: &RecentReposFile) -> Result<(), String> { let path = recent_repos_path()?; if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .map_err(|e| format!("Cannot create config directory: {e}"))?; + fs::create_dir_all(parent).map_err(|e| format!("Cannot create config directory: {e}"))?; } let json = serde_json::to_string_pretty(recent) .map_err(|e| format!("Cannot serialise recent repos: {e}"))?; @@ -80,7 +79,6 @@ fn update_recent(repo_path: &str, repo_name: &str) -> Result<(), String> { /// /// If the repo has a `.machine_readable/PANELS.a2ml`, parses it and returns /// the saved config. Otherwise, generates fresh suggestions from scan data. - pub async fn repoloader_scan(repo_path: String) -> Result { let path = PathBuf::from(&repo_path); let repo = scanner::scan_repo(&path)?; @@ -119,7 +117,6 @@ pub async fn repoloader_scan(repo_path: String) -> Result { /// Save panel configuration to `.machine_readable/PANELS.a2ml`. /// /// Takes the repo path and a JSON array of panel entries. - pub async fn repoloader_save_panels( repo_path: String, panels_json: String, @@ -128,12 +125,11 @@ pub async fn repoloader_save_panels( let mr_dir = path.join(".machine_readable"); // Ensure .machine_readable/ exists. - fs::create_dir_all(&mr_dir) - .map_err(|e| format!("Cannot create .machine_readable/: {e}"))?; + fs::create_dir_all(&mr_dir).map_err(|e| format!("Cannot create .machine_readable/: {e}"))?; // Parse the panels from JSON. - let panels: Vec = serde_json::from_str(&panels_json) - .map_err(|e| format!("Cannot parse panels JSON: {e}"))?; + let panels: Vec = + serde_json::from_str(&panels_json).map_err(|e| format!("Cannot parse panels JSON: {e}"))?; let repo_name = path .file_name() @@ -158,15 +154,11 @@ pub async fn repoloader_save_panels( } output.push_str(" )\n"); output.push_str(" (portfolio\n"); - output.push_str(&format!( - " (name \"{} Development\")\n", - repo_name - )); + output.push_str(&format!(" (name \"{} Development\")\n", repo_name)); output.push_str(" (isolation \"native\")))\n"); let panels_path = mr_dir.join("PANELS.a2ml"); - fs::write(&panels_path, &output) - .map_err(|e| format!("Cannot write PANELS.a2ml: {e}"))?; + fs::write(&panels_path, &output).map_err(|e| format!("Cannot write PANELS.a2ml: {e}"))?; Ok(serde_json::json!({ "saved": true, @@ -177,7 +169,6 @@ pub async fn repoloader_save_panels( } /// List recently loaded repositories. - pub async fn repoloader_list_recent() -> Result { let recent = load_recent(); serde_json::to_string(&recent).map_err(|e| format!("Serialisation error: {e}")) @@ -186,7 +177,6 @@ pub async fn repoloader_list_recent() -> Result { /// Search the git-private-farm manifest for repos matching a query. /// /// Searches repo names and descriptions (case-insensitive substring match). - pub async fn repoloader_search_farm(query: String) -> Result { let home = dirs::home_dir().ok_or("Cannot determine home directory")?; let manifest_path = home.join(".git-private-farm").join("farm-manifest.json"); @@ -198,8 +188,8 @@ pub async fn repoloader_search_farm(query: String) -> Result { let content = fs::read_to_string(&manifest_path) .map_err(|e| format!("Cannot read farm manifest: {e}"))?; - let manifest: serde_json::Value = serde_json::from_str(&content) - .map_err(|e| format!("Cannot parse farm manifest: {e}"))?; + let manifest: serde_json::Value = + serde_json::from_str(&content).map_err(|e| format!("Cannot parse farm manifest: {e}"))?; let query_lower = query.to_lowercase(); let mut results: Vec = Vec::new(); diff --git a/src-gossamer/src/repoloader/mod.rs b/src-gossamer/src/repoloader/mod.rs index 2ca32327..ffa8dc0e 100644 --- a/src-gossamer/src/repoloader/mod.rs +++ b/src-gossamer/src/repoloader/mod.rs @@ -6,6 +6,6 @@ //! detects languages, and suggests which PanLL panels to activate. //! Panel configs are saved as `.machine_readable/PANELS.a2ml` portfolios. -pub mod types; -pub mod scanner; pub mod commands; +pub mod scanner; +pub mod types; diff --git a/src-gossamer/src/repoloader/scanner.rs b/src-gossamer/src/repoloader/scanner.rs index 858383a0..aebc8341 100644 --- a/src-gossamer/src/repoloader/scanner.rs +++ b/src-gossamer/src/repoloader/scanner.rs @@ -60,8 +60,8 @@ pub fn scan_repo(repo_path: &Path) -> Result { let has_machine_readable = mr_dir.exists(); let has_panels_manifest = mr_dir.join("PANELS.a2ml").exists(); let has_state = mr_dir.join("STATE.scm").exists(); - let has_ai_manifest = repo_path.join("0-AI-MANIFEST.a2ml").exists() - || repo_path.join("AI.a2ml").exists(); + let has_ai_manifest = + repo_path.join("0-AI-MANIFEST.a2ml").exists() || repo_path.join("AI.a2ml").exists(); // Extract description from AI manifest if available. let description = extract_description(repo_path).unwrap_or_default(); @@ -231,7 +231,10 @@ pub fn suggest_panels(repo: &RepoInfo) -> Vec { if repo.languages.len() >= 3 { suggestions.push(PanelSuggestion { panel_name: "Playgrounds".to_string(), - reason: format!("Multi-language repo ({} languages detected)", repo.languages.len()), + reason: format!( + "Multi-language repo ({} languages detected)", + repo.languages.len() + ), priority: "low".to_string(), enabled: false, }); diff --git a/src-gossamer/src/script_gist/commands.rs b/src-gossamer/src/script_gist/commands.rs index 99fb1c31..323c6db5 100644 --- a/src-gossamer/src/script_gist/commands.rs +++ b/src-gossamer/src/script_gist/commands.rs @@ -16,8 +16,7 @@ use serde_json::{json, Value}; /// Resolve the gist storage directory (`~/.panll/gists/`). fn gists_dir() -> Result { - let home = dirs::home_dir() - .ok_or_else(|| "Cannot determine home directory".to_string())?; + let home = dirs::home_dir().ok_or_else(|| "Cannot determine home directory".to_string())?; Ok(home.join(".panll").join("gists")) } @@ -28,14 +27,12 @@ fn gists_dir() -> Result { /// writes to `~/.panll/gists/.json`. /// /// Returns a JSON object with `path`, `id`, and `status`. - pub async fn script_gist_save(gist_json: String) -> Result { let dir = gists_dir()?; - fs::create_dir_all(&dir) - .map_err(|e| format!("Failed to create gists directory: {e}"))?; + fs::create_dir_all(&dir).map_err(|e| format!("Failed to create gists directory: {e}"))?; - let parsed: Value = serde_json::from_str(&gist_json) - .map_err(|e| format!("Invalid JSON in gist: {e}"))?; + let parsed: Value = + serde_json::from_str(&gist_json).map_err(|e| format!("Invalid JSON in gist: {e}"))?; let id = parsed["id"] .as_str() @@ -44,11 +41,10 @@ pub async fn script_gist_save(gist_json: String) -> Result { let filename = format!("{id}.json"); let filepath = dir.join(&filename); - let content = serde_json::to_string_pretty(&parsed) - .map_err(|e| format!("Serialisation error: {e}"))?; + let content = + serde_json::to_string_pretty(&parsed).map_err(|e| format!("Serialisation error: {e}"))?; - fs::write(&filepath, content) - .map_err(|e| format!("Failed to write gist: {e}"))?; + fs::write(&filepath, content).map_err(|e| format!("Failed to write gist: {e}"))?; Ok(json!({ "path": filepath.to_string_lossy(), @@ -67,23 +63,18 @@ pub async fn script_gist_save(gist_json: String) -> Result { /// - `TargetBoj`: delegates to the BoJ cartridge invoke pathway /// /// The frontend receives the result and wraps it into a `gistResult`. - pub async fn script_gist_execute(gist_json: String) -> Result { - let parsed: Value = serde_json::from_str(&gist_json) - .map_err(|e| format!("Invalid JSON in gist: {e}"))?; + let parsed: Value = + serde_json::from_str(&gist_json).map_err(|e| format!("Invalid JSON in gist: {e}"))?; - let id = parsed["id"] - .as_str() - .unwrap_or("unknown"); + let id = parsed["id"].as_str().unwrap_or("unknown"); let target = parsed["target"] .as_str() - .or_else(|| parsed["target"].as_object().and_then(|_| Some("TargetBoj"))) + .or_else(|| parsed["target"].as_object().map(|_| "TargetBoj")) .unwrap_or("TargetDeno"); - let code = parsed["code"] - .as_str() - .unwrap_or(""); + let code = parsed["code"].as_str().unwrap_or(""); // Dispatch based on target type. let result = match target { @@ -127,8 +118,9 @@ pub async fn script_gist_execute(gist_json: String) -> Result { "invoker": "user", }) } - "TargetBoj" | _ => { - // BoJ cartridge dispatch — extract cartridge name from target. + _ => { + // "TargetBoj" and any unrecognised target fall through to BoJ + // cartridge dispatch — extract cartridge name from target. let cartridge = parsed["target"] .as_object() .and_then(|obj| obj.get("TargetBoj")) @@ -156,19 +148,19 @@ pub async fn script_gist_execute(gist_json: String) -> Result { /// /// Receives the full serialised `scriptGistState` JSON and returns it /// after validation. The frontend replaces its state with the returned data. - pub async fn script_gist_restore_snapshot(snapshot_json: String) -> Result { // Validate the snapshot is well-formed JSON. - let parsed: Value = serde_json::from_str(&snapshot_json) - .map_err(|e| format!("Invalid snapshot JSON: {e}"))?; + let parsed: Value = + serde_json::from_str(&snapshot_json).map_err(|e| format!("Invalid snapshot JSON: {e}"))?; // Verify it has the expected top-level fields for a scriptGistState. if !parsed.is_object() { return Err("Snapshot must be a JSON object".to_string()); } - let obj = parsed.as_object() - .ok_or_else(|| "Snapshot is not a JSON object (unreachable after is_object check)".to_string())?; + let obj = parsed.as_object().ok_or_else(|| { + "Snapshot is not a JSON object (unreachable after is_object check)".to_string() + })?; if !obj.contains_key("gists") { return Err("Snapshot missing 'gists' field — not a valid scriptGistState".to_string()); } @@ -180,7 +172,6 @@ pub async fn script_gist_restore_snapshot(snapshot_json: String) -> Result Result { let dir = gists_dir()?; @@ -205,8 +196,7 @@ pub async fn script_gist_list() -> Result { }) .collect(); - serde_json::to_string(&entries) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&entries).map_err(|e| format!("Serialisation error: {e}")) } #[cfg(test)] @@ -245,7 +235,9 @@ mod tests { #[tokio::test] async fn test_script_gist_restore_valid() { let snapshot = r#"{"gists":[],"templates":[]}"#; - let result = script_gist_restore_snapshot(snapshot.to_string()).await.unwrap(); + let result = script_gist_restore_snapshot(snapshot.to_string()) + .await + .unwrap(); let parsed: Value = serde_json::from_str(&result).unwrap(); assert!(parsed["gists"].is_array()); } diff --git a/src-gossamer/src/security/commands.rs b/src-gossamer/src/security/commands.rs index 9a91fb20..e5f0c25d 100644 --- a/src-gossamer/src/security/commands.rs +++ b/src-gossamer/src/security/commands.rs @@ -18,14 +18,13 @@ use super::types::{DetectedSecret, RedactionPattern, RedactionResult, TrustfileP /// /// This is the workhorse of the security layer — called when sharing, /// saving, or displaying content that might contain secrets. - pub async fn redact_text( text: String, panel_id: String, patterns_json: String, ) -> Result { - let patterns: Vec = serde_json::from_str(&patterns_json) - .map_err(|e| format!("Invalid patterns JSON: {e}"))?; + let patterns: Vec = + serde_json::from_str(&patterns_json).map_err(|e| format!("Invalid patterns JSON: {e}"))?; let mut result_text = text.clone(); let mut secrets: Vec = Vec::new(); @@ -60,7 +59,9 @@ pub async fn redact_text( } // Apply redaction to the result text. - result_text = re.replace_all(&result_text, placeholder.as_str()).to_string(); + result_text = re + .replace_all(&result_text, placeholder.as_str()) + .to_string(); } let result = RedactionResult { @@ -69,12 +70,10 @@ pub async fn redact_text( total_redacted, }; - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Store a secret in the vault via reasonably-good-tool CLI. - pub async fn vault_store(key: String, value: String) -> Result { let output = Command::new("reasonably-good-tool") .args(["vault", "set", &key, &value]) @@ -90,7 +89,6 @@ pub async fn vault_store(key: String, value: String) -> Result { } /// Retrieve a secret from the vault via reasonably-good-tool CLI. - pub async fn vault_retrieve(key: String) -> Result { let output = Command::new("reasonably-good-tool") .args(["vault", "get", &key]) @@ -106,7 +104,6 @@ pub async fn vault_retrieve(key: String) -> Result { } /// List all keys in the vault (names only, not values). - pub async fn vault_list() -> Result { let output = Command::new("reasonably-good-tool") .args(["vault", "list"]) @@ -115,7 +112,8 @@ pub async fn vault_list() -> Result { if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); - let keys: Vec = stdout.lines() + let keys: Vec = stdout + .lines() .filter(|l| !l.is_empty()) .map(|l| VaultKey { key: l.trim().to_string(), @@ -124,8 +122,7 @@ pub async fn vault_list() -> Result { }) .collect(); - serde_json::to_string(&keys) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&keys).map_err(|e| format!("Serialisation error: {e}")) } else { let stderr = String::from_utf8_lossy(&output.stderr); Err(format!("Vault list failed: {stderr}")) @@ -136,7 +133,6 @@ pub async fn vault_list() -> Result { /// /// The Trustfile defines the security policy for a workspace session. /// This command reads the file and returns a parsed TrustfilePolicy. - pub async fn load_trustfile(repo_path: String) -> Result { let trustfile_path = Path::new(&repo_path).join("Trustfile.a2ml"); @@ -144,27 +140,25 @@ pub async fn load_trustfile(repo_path: String) -> Result { return Err("No Trustfile.a2ml found in this repository".to_string()); } - let content = fs::read_to_string(&trustfile_path) - .map_err(|e| format!("Cannot read Trustfile: {e}"))?; + let content = + fs::read_to_string(&trustfile_path).map_err(|e| format!("Cannot read Trustfile: {e}"))?; // Parse the Trustfile content. The a2ml format uses S-expression-like // syntax. For now, we do a simplified parse extracting key fields. let policy = parse_trustfile(&content, &trustfile_path.to_string_lossy())?; - serde_json::to_string(&policy) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&policy).map_err(|e| format!("Serialisation error: {e}")) } /// Simplified Trustfile parser. Extracts key security policy fields from /// the a2ml S-expression format. fn parse_trustfile(content: &str, file_path: &str) -> Result { // Extract security-level. - let security_level = extract_field(content, "security-level") - .unwrap_or_else(|| "medium".to_string()); + let security_level = + extract_field(content, "security-level").unwrap_or_else(|| "medium".to_string()); // Extract redaction mode. - let redaction_mode = extract_field(content, "mode") - .unwrap_or_else(|| "on-share".to_string()); + let redaction_mode = extract_field(content, "mode").unwrap_or_else(|| "on-share".to_string()); // Extract require-approval. let require_approval = extract_field(content, "require-approval") @@ -172,14 +166,13 @@ fn parse_trustfile(content: &str, file_path: &str) -> Result Option { // Skip whitespace. let rest = rest.trim_start(); - if rest.starts_with('"') { + if let Some(quoted) = rest.strip_prefix('"') { // Quoted string value. - let end = rest[1..].find('"')?; - Some(rest[1..1 + end].to_string()) + let end = quoted.find('"')?; + Some(quoted[..end].to_string()) } else { // Bare value (until whitespace or closing paren). let end = rest.find(|c: char| c.is_whitespace() || c == ')')?; diff --git a/src-gossamer/src/security/mod.rs b/src-gossamer/src/security/mod.rs index 1a1724fb..45798c92 100644 --- a/src-gossamer/src/security/mod.rs +++ b/src-gossamer/src/security/mod.rs @@ -9,5 +9,5 @@ //! - TOTP-based 2FA (RFC 6238) using HMAC-SHA1 //! - Trustfile.a2ml parsing and security policy loading -pub mod types; pub mod commands; +pub mod types; diff --git a/src-gossamer/src/system_update/commands.rs b/src-gossamer/src/system_update/commands.rs index 21e3738e..30c85e37 100644 --- a/src-gossamer/src/system_update/commands.rs +++ b/src-gossamer/src/system_update/commands.rs @@ -131,8 +131,14 @@ fn discover_rpm_ostree() -> Component { /// Discover Flatpak apps with updates. fn discover_flatpak() -> Vec { - let output = run_cmd_quiet("flatpak", &["list", "--app", "--columns=application,version"]); - let update_output = run_cmd_quiet("flatpak", &["remote-ls", "--updates", "--columns=application,version"]); + let output = run_cmd_quiet( + "flatpak", + &["list", "--app", "--columns=application,version"], + ); + let update_output = run_cmd_quiet( + "flatpak", + &["remote-ls", "--updates", "--columns=application,version"], + ); let updates: Vec<&str> = update_output.lines().collect(); @@ -387,8 +393,7 @@ pub fn system_update_apply_component(component_id: String) -> Result (true, o), Err(e) => (false, e), @@ -515,9 +520,7 @@ pub fn system_update_last_summary() -> Result { .collect::>() .join("\n"); - let flatpak_count = run_cmd_quiet("flatpak", &["list", "--app"]) - .lines() - .count(); + let flatpak_count = run_cmd_quiet("flatpak", &["list", "--app"]).lines().count(); let asdf_count = run_cmd_quiet("opsm", &["plugin", "list"]) .lines() diff --git a/src-gossamer/src/typell/commands.rs b/src-gossamer/src/typell/commands.rs index 04190208..f4043d53 100644 --- a/src-gossamer/src/typell/commands.rs +++ b/src-gossamer/src/typell/commands.rs @@ -54,28 +54,28 @@ fn post(path: &str, body: serde_json::Value, timeout_secs: u64) -> Result Result { get("/health", 5) } /// POST /check — type-check a term or expression. - pub fn typell_check(expression: String, context: Option) -> Result { let ctx: serde_json::Value = context .and_then(|c| serde_json::from_str(&c).ok()) .unwrap_or(json!({})); - post("/check", json!({"expression": expression, "context": ctx}), 30) + post( + "/check", + json!({"expression": expression, "context": ctx}), + 30, + ) } /// POST /infer — infer the type of an expression. - pub fn typell_infer(expression: String) -> Result { post("/infer", json!({"expression": expression}), 30) } /// POST /refine — apply refinement types to a specification. - pub fn typell_refine(spec: String, constraints: Option) -> Result { let cons: serde_json::Value = constraints .and_then(|c| serde_json::from_str(&c).ok()) @@ -84,19 +84,16 @@ pub fn typell_refine(spec: String, constraints: Option) -> Result Result { post("/compute", json!({"term": term}), 30) } /// GET /signatures — list available type signatures. - pub fn typell_list_signatures() -> Result { get("/signatures", 10) } /// GET /universes — get type universe hierarchy. - pub fn typell_universes() -> Result { get("/universes", 10) } diff --git a/src-gossamer/src/umoja/commands.rs b/src-gossamer/src/umoja/commands.rs index 4aa1a67c..f2cfc266 100644 --- a/src-gossamer/src/umoja/commands.rs +++ b/src-gossamer/src/umoja/commands.rs @@ -16,7 +16,6 @@ use serde_json::json; /// Add a new peer to the Umoja federation by address. /// /// Mock response: returns the new peer's assigned node ID and initial state. - pub async fn umoja_add_peer(address: String) -> Result { if address.is_empty() { return Err("Peer address must not be empty".to_string()); @@ -24,7 +23,9 @@ pub async fn umoja_add_peer(address: String) -> Result { // Mock: generate a deterministic node ID from the address. let node_id = format!( "node-{:08x}", - address.bytes().fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32)) + address + .bytes() + .fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32)) ); let response = json!({ "ok": true, @@ -39,7 +40,6 @@ pub async fn umoja_add_peer(address: String) -> Result { /// Disconnect a peer from the Umoja federation by node ID. /// /// Mock response: confirms disconnection and reports the peer as Stale. - pub async fn umoja_disconnect_peer(node_id: String) -> Result { if node_id.is_empty() { return Err("Node ID must not be empty".to_string()); @@ -56,7 +56,6 @@ pub async fn umoja_disconnect_peer(node_id: String) -> Result { /// Trigger a manual gossip round across the Umoja federation. /// /// Mock response: returns the new round number and peer count. - pub async fn umoja_trigger_gossip() -> Result { let response = json!({ "ok": true, @@ -70,7 +69,6 @@ pub async fn umoja_trigger_gossip() -> Result { /// Request a catalogue sync with a specific peer by node ID. /// /// Mock response: returns sync status and digest comparison. - pub async fn umoja_sync_catalogue(node_id: String) -> Result { if node_id.is_empty() { return Err("Node ID must not be empty".to_string()); @@ -90,7 +88,6 @@ pub async fn umoja_sync_catalogue(node_id: String) -> Result { /// Retrieve metrics for a specific peer by node ID. /// /// Mock response: returns latency, uptime, and gossip round data. - pub async fn umoja_peer_metrics(node_id: String) -> Result { if node_id.is_empty() { return Err("Node ID must not be empty".to_string()); diff --git a/src-gossamer/src/ums/commands.rs b/src-gossamer/src/ums/commands.rs index e564a2b7..b9727e51 100644 --- a/src-gossamer/src/ums/commands.rs +++ b/src-gossamer/src/ums/commands.rs @@ -28,8 +28,7 @@ const UMS_DIR: &str = "/tmp/panll/ums-projects"; /// Ensure the UMS projects directory exists, creating it lazily if needed. fn ensure_ums_dir() -> Result { let path = PathBuf::from(UMS_DIR); - fs::create_dir_all(&path) - .map_err(|e| format!("Cannot create UMS directory {UMS_DIR}: {e}"))?; + fs::create_dir_all(&path).map_err(|e| format!("Cannot create UMS directory {UMS_DIR}: {e}"))?; Ok(path) } @@ -45,7 +44,6 @@ fn now_ts() -> u64 { /// /// Scans for `.project.json` manifest files and returns their contents /// as a JSON array, sorted by project name. - pub async fn ums_load_projects() -> Result { let ums_dir = ensure_ums_dir()?; @@ -67,15 +65,13 @@ pub async fn ums_load_projects() -> Result { name_a.cmp(name_b) }); - serde_json::to_string(&projects) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&projects).map_err(|e| format!("Serialisation error: {e}")) } /// Create a new mod project with the given name and description. /// /// Creates a project directory under the UMS root and writes a /// `.project.json` manifest file with initial metadata. - pub async fn ums_create_project(name: String, description: String) -> Result { let ums_dir = ensure_ums_dir()?; @@ -130,15 +126,13 @@ pub async fn ums_create_project(name: String, description: String) -> Result Result { let ums_dir = ensure_ums_dir()?; let manifest_path = ums_dir.join(&project_id).join(".project.json"); @@ -147,14 +141,12 @@ pub async fn ums_open_project(project_id: String) -> Result { return Err(format!("Project not found: {project_id}")); } - fs::read_to_string(&manifest_path) - .map_err(|e| format!("Cannot read project manifest: {e}")) + fs::read_to_string(&manifest_path).map_err(|e| format!("Cannot read project manifest: {e}")) } /// Delete a mod project by its ID. /// /// Removes the entire project directory and all its contents. - pub async fn ums_delete_project(project_id: String) -> Result { let ums_dir = ensure_ums_dir()?; let project_dir = ums_dir.join(&project_id); @@ -170,8 +162,7 @@ pub async fn ums_delete_project(project_id: String) -> Result { "deleted": true, "id": project_id, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Run ABI validation on a level. @@ -180,7 +171,6 @@ pub async fn ums_delete_project(project_id: String) -> Result { /// guards-in-zones, defence-targets-valid, zones-ordered, pbx-consistent, /// and devices-exist. When the real Idris2 ABI harness is integrated, /// these checks will delegate to the formally verified proofs. - pub async fn ums_validate_level(level_id: String) -> Result { // Stub: all proofs pass. When the real ABI harness is integrated, // each proof obligation will be checked against the level data. @@ -197,8 +187,7 @@ pub async fn ums_validate_level(level_id: String) -> Result { "errors": [], "stub": true, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Load available mod templates. @@ -206,7 +195,6 @@ pub async fn ums_validate_level(level_id: String) -> Result { /// Returns a set of built-in templates for bootstrapping mod projects. /// When the template registry is implemented, this will scan the /// template directory for user-contributed templates as well. - pub async fn ums_load_templates() -> Result { let templates = json!([ { @@ -250,8 +238,7 @@ pub async fn ums_load_templates() -> Result { "previewImagePath": "templates/tower-defence-preview.png", }, ]); - serde_json::to_string(&templates) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&templates).map_err(|e| format!("Serialisation error: {e}")) } /// Instantiate a mod template to create a new project. @@ -259,7 +246,6 @@ pub async fn ums_load_templates() -> Result { /// Copies template files into a new project directory. Currently uses /// stub data; when the template registry is implemented, this will copy /// actual template files. - pub async fn ums_instantiate_template( template_id: String, project_name: String, @@ -282,8 +268,7 @@ pub async fn ums_instantiate_template( "projectId": project_id, "path": created.get("path"), }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Load assets for a project. @@ -291,7 +276,6 @@ pub async fn ums_instantiate_template( /// Scans the project's `assets/` directory for known asset file types /// and returns metadata. When the full asset pipeline is implemented, /// this will also read asset manifests and usage tracking. - pub async fn ums_load_assets(project_id: String) -> Result { let ums_dir = ensure_ums_dir()?; let assets_dir = ums_dir.join(&project_id).join("assets"); @@ -331,22 +315,16 @@ pub async fn ums_load_assets(project_id: String) -> Result { }) .collect(); - serde_json::to_string(&assets) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&assets).map_err(|e| format!("Serialisation error: {e}")) } /// Import an asset file into a project. /// /// Copies the file at `file_path` into the project's `assets/` directory. - -pub async fn ums_import_asset( - project_id: String, - file_path: String, -) -> Result { +pub async fn ums_import_asset(project_id: String, file_path: String) -> Result { let ums_dir = ensure_ums_dir()?; let assets_dir = ums_dir.join(&project_id).join("assets"); - fs::create_dir_all(&assets_dir) - .map_err(|e| format!("Cannot create assets directory: {e}"))?; + fs::create_dir_all(&assets_dir).map_err(|e| format!("Cannot create assets directory: {e}"))?; let source = PathBuf::from(&file_path); if !source.exists() { @@ -358,8 +336,7 @@ pub async fn ums_import_asset( .ok_or_else(|| "Cannot determine file name".to_string())?; let dest = assets_dir.join(file_name); - fs::copy(&source, &dest) - .map_err(|e| format!("Cannot copy asset file: {e}"))?; + fs::copy(&source, &dest).map_err(|e| format!("Cannot copy asset file: {e}"))?; let result = json!({ "imported": true, @@ -367,8 +344,7 @@ pub async fn ums_import_asset( "fileName": file_name.to_string_lossy(), "destPath": dest.to_string_lossy(), }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Publish a mod to a distribution target. @@ -376,11 +352,7 @@ pub async fn ums_import_asset( /// Creates a distributable package in the project's `exports/` directory. /// When full distribution is implemented, this will upload to the /// specified platform (GitHub, Workshop, etc.). - -pub async fn ums_publish_mod( - project_id: String, - platform: String, -) -> Result { +pub async fn ums_publish_mod(project_id: String, platform: String) -> Result { let ums_dir = ensure_ums_dir()?; let project_dir = ums_dir.join(&project_id); let manifest_path = project_dir.join(".project.json"); @@ -426,8 +398,7 @@ pub async fn ums_publish_mod( "version": version, "exportPath": export_file.to_string_lossy(), }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Load the modding API reference documentation. @@ -435,7 +406,6 @@ pub async fn ums_publish_mod( /// Returns built-in API entries for the IDApTIK modding API. /// When the full documentation system is implemented, this will /// load entries from doc-comment extraction and API manifests. - pub async fn ums_load_api_reference() -> Result { let entries = json!([ { @@ -503,8 +473,7 @@ pub async fn ums_load_api_reference() -> Result { "since": "0.2.0", }, ]); - serde_json::to_string(&entries) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&entries).map_err(|e| format!("Serialisation error: {e}")) } #[cfg(test)] @@ -536,9 +505,11 @@ mod tests { #[test] fn test_ums_create_and_load_project() { rt().block_on(async { - let create_result = - ums_create_project("Test Mod Alpha".to_string(), "A test mod project".to_string()) - .await; + let create_result = ums_create_project( + "Test Mod Alpha".to_string(), + "A test mod project".to_string(), + ) + .await; assert!(create_result.is_ok()); let create_json: serde_json::Value = serde_json::from_str(&create_result.unwrap()).unwrap(); @@ -559,8 +530,7 @@ mod tests { #[test] fn test_ums_create_project_empty_name() { rt().block_on(async { - let result = - ums_create_project("!!!".to_string(), "Bad name".to_string()).await; + let result = ums_create_project("!!!".to_string(), "Bad name".to_string()).await; assert!(result.is_err()); assert!(result .unwrap_err() @@ -590,11 +560,8 @@ mod tests { fn test_ums_create_and_delete_project() { rt().block_on(async { // Create a project. - let _ = ums_create_project( - "Delete Me".to_string(), - "Will be deleted".to_string(), - ) - .await; + let _ = + ums_create_project("Delete Me".to_string(), "Will be deleted".to_string()).await; // Verify it exists. let open_result = ums_open_project("delete-me".to_string()).await; @@ -618,8 +585,7 @@ mod tests { rt().block_on(async { let result = ums_validate_level("test-level-001".to_string()).await; assert!(result.is_ok()); - let json: serde_json::Value = - serde_json::from_str(&result.unwrap()).unwrap(); + let json: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); assert_eq!(json["levelId"], "test-level-001"); assert_eq!(json["allPassed"], true); assert_eq!(json["guardsInZones"], true); @@ -635,8 +601,7 @@ mod tests { rt().block_on(async { let result = ums_load_templates().await; assert!(result.is_ok()); - let templates: Vec = - serde_json::from_str(&result.unwrap()).unwrap(); + let templates: Vec = serde_json::from_str(&result.unwrap()).unwrap(); assert!(templates.len() >= 5); assert!(templates.iter().any(|t| t["id"] == "basic-level")); assert!(templates.iter().any(|t| t["id"] == "full-campaign")); @@ -652,8 +617,7 @@ mod tests { ) .await; assert!(result.is_ok()); - let json: serde_json::Value = - serde_json::from_str(&result.unwrap()).unwrap(); + let json: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); assert_eq!(json["instantiated"], true); assert_eq!(json["templateId"], "basic-level"); @@ -665,16 +629,12 @@ mod tests { fn test_ums_load_assets_empty() { rt().block_on(async { // Create a project to have an assets directory. - let _ = ums_create_project( - "Asset Test".to_string(), - "For asset testing".to_string(), - ) - .await; + let _ = + ums_create_project("Asset Test".to_string(), "For asset testing".to_string()).await; let result = ums_load_assets("asset-test".to_string()).await; assert!(result.is_ok()); - let assets: Vec = - serde_json::from_str(&result.unwrap()).unwrap(); + let assets: Vec = serde_json::from_str(&result.unwrap()).unwrap(); assert_eq!(assets.len(), 0); cleanup_test_project("asset-test"); @@ -709,11 +669,8 @@ mod tests { fs::write(tmp_file, b"fake png data").unwrap(); // Import the asset. - let import_result = ums_import_asset( - "import-asset-test".to_string(), - tmp_file.to_string(), - ) - .await; + let import_result = + ums_import_asset("import-asset-test".to_string(), tmp_file.to_string()).await; assert!(import_result.is_ok()); let import_json: serde_json::Value = serde_json::from_str(&import_result.unwrap()).unwrap(); @@ -735,11 +692,8 @@ mod tests { #[test] fn test_ums_publish_mod_not_found() { rt().block_on(async { - let result = ums_publish_mod( - "nonexistent-project".to_string(), - "github".to_string(), - ) - .await; + let result = + ums_publish_mod("nonexistent-project".to_string(), "github".to_string()).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Project not found")); }); @@ -755,14 +709,9 @@ mod tests { ) .await; - let result = ums_publish_mod( - "publish-test".to_string(), - "github".to_string(), - ) - .await; + let result = ums_publish_mod("publish-test".to_string(), "github".to_string()).await; assert!(result.is_ok()); - let json: serde_json::Value = - serde_json::from_str(&result.unwrap()).unwrap(); + let json: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); assert_eq!(json["published"], true); assert_eq!(json["platform"], "github"); @@ -775,8 +724,7 @@ mod tests { rt().block_on(async { let result = ums_load_api_reference().await; assert!(result.is_ok()); - let entries: Vec = - serde_json::from_str(&result.unwrap()).unwrap(); + let entries: Vec = serde_json::from_str(&result.unwrap()).unwrap(); assert!(entries.len() >= 8); assert!(entries.iter().any(|e| e["name"] == "createLevel")); assert!(entries.iter().any(|e| e["name"] == "validateABI")); diff --git a/src-gossamer/src/ums_cartridge/commands.rs b/src-gossamer/src/ums_cartridge/commands.rs index 2a8026c6..eda279de 100644 --- a/src-gossamer/src/ums_cartridge/commands.rs +++ b/src-gossamer/src/ums_cartridge/commands.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MPL-2.0 // -// commands.rs — Tauri IPC commands for the UMS BoJ cartridge proxy. +// commands.rs — IPC commands for the UMS BoJ cartridge proxy. // // Author: Jonathan D.A. Jewell // @@ -12,9 +12,9 @@ // ## Architecture // // The validation logic is implemented as standalone functions (not tied to -// Tauri) so that unit tests can call them directly without an async runtime -// where possible. The Tauri `#[tauri::command]` wrappers are thin shells -// around these core functions. +// the IPC layer) so that unit tests can call them directly without an async +// runtime where possible. The IPC command handlers are thin shells around +// these core functions. // // ## Shared bridge directory // @@ -346,7 +346,7 @@ fn check_devices_exist(level: &LevelData) -> (bool, Vec) { /// Run all five validation checks and produce a composite result. /// -/// This is the core validation function, usable without Tauri. It mirrors +/// This is the core validation function, usable without the IPC layer. It mirrors /// `validate.validateLevel` in the Zig FFI and the `ValidatedLevel` record /// in `src/abi/Validation.idr`. /// @@ -383,7 +383,7 @@ pub fn validate_level_data(level: &LevelData) -> ValidationResult { } // --------------------------------------------------------------------------- -// Tauri commands — CartridgeAbi tool handlers for BoJ ums-mcp routing +// IPC commands — CartridgeAbi tool handlers for BoJ ums-mcp routing // --------------------------------------------------------------------------- /// Run the five ABI proof checks on level data JSON. @@ -406,7 +406,6 @@ pub fn validate_level_data(level: &LevelData) -> ValidationResult { /// # Returns /// /// JSON string containing the `ValidationResult`, or an error if parsing fails. - pub async fn ums_cartridge_validate(level: String) -> Result { let level_data: LevelData = serde_json::from_str(&level) .map_err(|e| format!("Failed to parse level data for validation: {e}"))?; @@ -426,8 +425,7 @@ pub async fn ums_cartridge_validate(level: String) -> Result { "source": "ums-cartridge", }); - serde_json::to_string(&output) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&output).map_err(|e| format!("Serialisation error: {e}")) } /// Load level JSON from the shared bridge directory. @@ -443,7 +441,6 @@ pub async fn ums_cartridge_validate(level: String) -> Result { /// # Returns /// /// The level JSON as a string, or an error if the file cannot be read. - pub async fn ums_cartridge_load_level(name: String) -> Result { let file_path = if std::path::Path::new(&name).is_absolute() { PathBuf::from(&name) @@ -477,12 +474,11 @@ pub async fn ums_cartridge_load_level(name: String) -> Result { /// # Returns /// /// JSON object with `saved: true` and the file path, or an error. - pub async fn ums_cartridge_save_level(level: String) -> Result { let dir = ensure_bridge_dir()?; - let parsed: Value = serde_json::from_str(&level) - .map_err(|e| format!("Failed to parse level JSON: {e}"))?; + let parsed: Value = + serde_json::from_str(&level).map_err(|e| format!("Failed to parse level JSON: {e}"))?; let name = parsed .get("name") @@ -494,8 +490,7 @@ pub async fn ums_cartridge_save_level(level: String) -> Result { let pretty = serde_json::to_string_pretty(&parsed) .map_err(|e| format!("Failed to serialise level: {e}"))?; - fs::write(&file_path, &pretty) - .map_err(|e| format!("Failed to write level file: {e}"))?; + fs::write(&file_path, &pretty).map_err(|e| format!("Failed to write level file: {e}"))?; let result = json!({ "saved": true, @@ -504,8 +499,7 @@ pub async fn ums_cartridge_save_level(level: String) -> Result { "savedAt": now_ts().to_string(), }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// List available levels in the shared bridge directory. @@ -516,7 +510,6 @@ pub async fn ums_cartridge_save_level(level: String) -> Result { /// # Returns /// /// JSON array of `{ name, path }` objects, or an error. - pub async fn ums_cartridge_list_levels() -> Result { let dir = ensure_bridge_dir()?; @@ -537,8 +530,7 @@ pub async fn ums_cartridge_list_levels() -> Result { }) .collect(); - serde_json::to_string(&entries) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&entries).map_err(|e| format!("Serialisation error: {e}")) } /// Generate a ReScript LevelConfig from level data. @@ -554,7 +546,6 @@ pub async fn ums_cartridge_list_levels() -> Result { /// # Returns /// /// ReScript source code as a string, or an error if parsing fails. - pub async fn ums_cartridge_export_config(level: String) -> Result { let level_data: LevelData = serde_json::from_str(&level) .map_err(|e| format!("Failed to parse level for export: {e}"))?; @@ -571,18 +562,9 @@ pub async fn ums_cartridge_export_config(level: String) -> Result= 3, "Should have at least 3 errors"); } - // -- Tauri command integration tests (async) --------------------------- + // -- IPC command integration tests (async) ----------------------------- #[test] fn test_ums_cartridge_validate_valid_level() { @@ -939,8 +942,7 @@ mod tests { assert_eq!(save_parsed["name"], "cartridge_test_level"); // Load it back. - let load_result = - ums_cartridge_load_level("cartridge_test_level".to_string()).await; + let load_result = ums_cartridge_load_level("cartridge_test_level".to_string()).await; assert!(load_result.is_ok(), "Load should succeed after save"); let loaded: LevelData = serde_json::from_str(&load_result.unwrap()).unwrap(); @@ -955,8 +957,7 @@ mod tests { #[test] fn test_load_level_not_found() { rt().block_on(async { - let result = - ums_cartridge_load_level("nonexistent_level_12345".to_string()).await; + let result = ums_cartridge_load_level("nonexistent_level_12345".to_string()).await; assert!(result.is_err(), "Should fail for missing level"); assert!(result.unwrap_err().contains("Level not found")); }); @@ -987,12 +988,15 @@ mod tests { entries.len() ); - let names: Vec<&str> = entries - .iter() - .filter_map(|e| e["name"].as_str()) - .collect(); - assert!(names.contains(&"list_test_alpha"), "Should find list_test_alpha"); - assert!(names.contains(&"list_test_bravo"), "Should find list_test_bravo"); + let names: Vec<&str> = entries.iter().filter_map(|e| e["name"].as_str()).collect(); + assert!( + names.contains(&"list_test_alpha"), + "Should find list_test_alpha" + ); + assert!( + names.contains(&"list_test_bravo"), + "Should find list_test_bravo" + ); cleanup_bridge_level("list_test_alpha"); cleanup_bridge_level("list_test_bravo"); diff --git a/src-gossamer/src/valence_shell/commands.rs b/src-gossamer/src/valence_shell/commands.rs index 5955c416..cb6a624b 100644 --- a/src-gossamer/src/valence_shell/commands.rs +++ b/src-gossamer/src/valence_shell/commands.rs @@ -60,16 +60,14 @@ const BASE_DIR: &str = "/tmp/panll"; /// Return the recordings directory, creating it lazily. fn recordings_dir() -> Result { let dir = PathBuf::from(BASE_DIR).join("recordings"); - fs::create_dir_all(&dir) - .map_err(|e| format!("Cannot create recordings dir: {e}"))?; + fs::create_dir_all(&dir).map_err(|e| format!("Cannot create recordings dir: {e}"))?; Ok(dir) } /// Return the checkpoints directory, creating it lazily. fn checkpoints_dir() -> Result { let dir = PathBuf::from(BASE_DIR).join("checkpoints"); - fs::create_dir_all(&dir) - .map_err(|e| format!("Cannot create checkpoints dir: {e}"))?; + fs::create_dir_all(&dir).map_err(|e| format!("Cannot create checkpoints dir: {e}"))?; Ok(dir) } @@ -95,7 +93,6 @@ fn generate_id() -> String { /// Check if the `valence-shell` binary exists on PATH. /// /// Returns JSON: `{"available": bool, "version": "..."}`. - pub async fn valence_shell_check() -> Result { let available = which::which("valence-shell").is_ok(); let version = if available { @@ -116,8 +113,7 @@ pub async fn valence_shell_check() -> Result { "version": version, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Spawn a shell process with piped stdin/stdout. @@ -128,14 +124,12 @@ pub async fn valence_shell_check() -> Result { /// Stderr is merged into stdout via `2>&1` shell invocation. /// /// Returns JSON with the session_id, shell, cwd, and status. - pub async fn valence_shell_spawn(shell: String, cwd: String) -> Result { let session_id = generate_id(); // Resolve the shell binary. Fall back to /bin/sh if the requested // shell is not found on PATH. - let shell_bin = which::which(&shell) - .unwrap_or_else(|_| PathBuf::from("/bin/sh")); + let shell_bin = which::which(&shell).unwrap_or_else(|_| PathBuf::from("/bin/sh")); // Validate that the working directory exists. let cwd_path = PathBuf::from(&cwd); @@ -152,15 +146,17 @@ pub async fn valence_shell_spawn(shell: String, cwd: String) -> Result Result Result, input: String, @@ -271,7 +265,7 @@ pub async fn valence_shell_input( let mut line = String::new(); match session.stdout_reader.read_line(&mut line) { - Ok(0) => break, // EOF + Ok(0) => break, // EOF Ok(_) => { collected.push_str(&line); // Append to the ring buffer, evicting oldest if full. @@ -295,8 +289,7 @@ pub async fn valence_shell_input( "exit_code": exit_code, }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Run a command as a one-shot execution (no persistent session). @@ -324,14 +317,12 @@ async fn run_oneshot(input: &str) -> Result { "exit_code": output.status.code(), }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Start recording terminal output in asciicast v2 format. /// /// Creates a `.cast` file with the asciicast header in `/tmp/panll/recordings/`. - pub async fn valence_shell_record_start(name: String) -> Result { let dir = recordings_dir()?; let id = generate_id(); @@ -351,8 +342,8 @@ pub async fn valence_shell_record_start(name: String) -> Result } }); - let header_line = serde_json::to_string(&header) - .map_err(|e| format!("Serialisation error: {e}"))?; + let header_line = + serde_json::to_string(&header).map_err(|e| format!("Serialisation error: {e}"))?; fs::write(&path, format!("{header_line}\n")) .map_err(|e| format!("Cannot write recording file: {e}"))?; @@ -364,15 +355,13 @@ pub async fn valence_shell_record_start(name: String) -> Result "status": "recording", }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Stop the active recording and close the `.cast` file. /// /// Stub: marks the most recent `.cast` file as complete by appending /// a final timestamp event. - pub async fn valence_shell_record_stop() -> Result { let dir = recordings_dir()?; @@ -380,19 +369,17 @@ pub async fn valence_shell_record_stop() -> Result { let latest = fs::read_dir(&dir) .map_err(|e| format!("Cannot read recordings dir: {e}"))? .filter_map(|e| e.ok()) - .filter(|e| { - e.path() - .extension() - .and_then(|ext| ext.to_str()) - == Some("cast") - }) + .filter(|e| e.path().extension().and_then(|ext| ext.to_str()) == Some("cast")) .max_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok())); match latest { Some(entry) => { let path = entry.path(); // Append a final event line: [elapsed, "o", "Recording stopped.\r\n"] - let stop_line = format!("[{}.0, \"o\", \"\\r\\nRecording stopped.\\r\\n\"]\n", unix_now()); + let stop_line = format!( + "[{}.0, \"o\", \"\\r\\nRecording stopped.\\r\\n\"]\n", + unix_now() + ); fs::OpenOptions::new() .append(true) .open(&path) @@ -413,8 +400,7 @@ pub async fn valence_shell_record_stop() -> Result { "status": "stopped", }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } None => Err("No active recording found".to_string()), } @@ -423,14 +409,12 @@ pub async fn valence_shell_record_stop() -> Result { /// List all recordings from `/tmp/panll/recordings/`. /// /// Returns a JSON array of recording metadata objects. - pub async fn valence_shell_recordings_list() -> Result { let dir = recordings_dir()?; let mut recordings = Vec::new(); if dir.exists() { - let entries = fs::read_dir(&dir) - .map_err(|e| format!("Cannot read recordings dir: {e}"))?; + let entries = fs::read_dir(&dir).map_err(|e| format!("Cannot read recordings dir: {e}"))?; for entry in entries { let entry = entry.map_err(|e| format!("Dir entry error: {e}"))?; @@ -449,7 +433,9 @@ pub async fn valence_shell_recordings_list() -> Result { content.lines().next().and_then(|line| { serde_json::from_str::(line) .ok() - .and_then(|v| v.get("title").and_then(|t| t.as_str()).map(String::from)) + .and_then(|v| { + v.get("title").and_then(|t| t.as_str()).map(String::from) + }) }) }) .unwrap_or_else(|| "Untitled".to_string()); @@ -466,17 +452,14 @@ pub async fn valence_shell_recordings_list() -> Result { } } - serde_json::to_string(&recordings) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&recordings).map_err(|e| format!("Serialisation error: {e}")) } /// Delete a recording file by ID. - pub async fn valence_shell_recording_delete(id: String) -> Result { let path = recordings_dir()?.join(format!("{id}.cast")); if path.exists() { - fs::remove_file(&path) - .map_err(|e| format!("Delete error: {e}"))?; + fs::remove_file(&path).map_err(|e| format!("Delete error: {e}"))?; Ok(format!("Recording '{id}' deleted")) } else { Err(format!("Recording '{id}' not found")) @@ -487,7 +470,6 @@ pub async fn valence_shell_recording_delete(id: String) -> Result Result { let dir = checkpoints_dir()?; let id = generate_id(); @@ -503,11 +485,9 @@ pub async fn valence_shell_checkpoint_create(label: String) -> Result Result, @@ -526,10 +505,9 @@ pub async fn valence_shell_checkpoint_restore( return Err(format!("Checkpoint '{id}' not found")); } - let content = fs::read_to_string(&path) - .map_err(|e| format!("Read error: {e}"))?; - let mut checkpoint: serde_json::Value = serde_json::from_str(&content) - .map_err(|e| format!("Parse error: {e}"))?; + let content = fs::read_to_string(&path).map_err(|e| format!("Read error: {e}"))?; + let mut checkpoint: serde_json::Value = + serde_json::from_str(&content).map_err(|e| format!("Parse error: {e}"))?; // If a session is specified and the checkpoint has a cwd, restore it // by writing a `cd` command to the session's stdin. @@ -554,21 +532,19 @@ pub async fn valence_shell_checkpoint_restore( obj.insert("status".to_string(), serde_json::json!("restored")); } - serde_json::to_string(&checkpoint) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&checkpoint).map_err(|e| format!("Serialisation error: {e}")) } /// List all checkpoints from `/tmp/panll/checkpoints/`. /// /// Returns a JSON array of checkpoint objects. - pub async fn valence_shell_checkpoints_list() -> Result { let dir = checkpoints_dir()?; let mut checkpoints = Vec::new(); if dir.exists() { - let entries = fs::read_dir(&dir) - .map_err(|e| format!("Cannot read checkpoints dir: {e}"))?; + let entries = + fs::read_dir(&dir).map_err(|e| format!("Cannot read checkpoints dir: {e}"))?; for entry in entries { let entry = entry.map_err(|e| format!("Dir entry error: {e}"))?; @@ -587,8 +563,7 @@ pub async fn valence_shell_checkpoints_list() -> Result { } } - serde_json::to_string(&checkpoints) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&checkpoints).map_err(|e| format!("Serialisation error: {e}")) } /// Capture the current terminal state from a session's output buffer. @@ -600,7 +575,6 @@ pub async fn valence_shell_checkpoints_list() -> Result { /// /// Returns JSON with `content` (the terminal text), `width`, `height` /// (number of lines returned), `timestamp`, and `session_id`. - pub async fn valence_shell_screenshot( session_id: Option, lines: Option, @@ -629,8 +603,7 @@ pub async fn valence_shell_screenshot( "height": buf[start..].len(), "timestamp": unix_now(), }); - return serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")); + return serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")); } else { return Err(format!("Session '{sid}' not found")); } @@ -663,8 +636,7 @@ pub async fn valence_shell_screenshot( "height": 1, "timestamp": unix_now(), }); - return serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")); + return serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")); } let result = serde_json::json!({ @@ -673,15 +645,13 @@ pub async fn valence_shell_screenshot( "timestamp": unix_now(), }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } /// Export a recording in a specified format. /// /// Stub implementation: confirms the export request. Real implementation /// will convert the `.cast` file to the requested format (e.g., gif, svg). - pub async fn valence_shell_recording_export(id: String, format: String) -> Result { let cast_path = recordings_dir()?.join(format!("{id}.cast")); if !cast_path.exists() { @@ -695,8 +665,7 @@ pub async fn valence_shell_recording_export(id: String, format: String) -> Resul "message": format!("Recording '{id}' queued for export as {format}"), }); - serde_json::to_string(&result) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&result).map_err(|e| format!("Serialisation error: {e}")) } // --------------------------------------------------------------------------- @@ -726,14 +695,12 @@ mod tests { // Also clear any leaked shell sessions from previous tests. // Recover from poisoned mutex to prevent cascading test failures. - if let Ok(mut sessions) = SESSIONS + let mut sessions = SESSIONS .lock() - .map_or_else(|poisoned| Ok::<_, std::convert::Infallible>(poisoned.into_inner()), Ok) - { - // Kill any lingering child processes before dropping sessions. - for (_, mut session) in sessions.drain() { - let _ = session.child.kill(); - } + .unwrap_or_else(|poisoned| poisoned.into_inner()); + // Kill any lingering child processes before dropping sessions. + for (_, mut session) in sessions.drain() { + let _ = session.child.kill(); } } @@ -847,7 +814,9 @@ mod tests { async fn test_recording_delete() { let _guard = TEST_LOCK.lock().await; cleanup_test_dirs(); - let start = valence_shell_record_start("delete-test".to_string()).await.unwrap(); + let start = valence_shell_record_start("delete-test".to_string()) + .await + .unwrap(); let json: serde_json::Value = serde_json::from_str(&start).unwrap(); let id = json["id"].as_str().unwrap().to_string(); @@ -915,7 +884,10 @@ mod tests { let result = valence_shell_screenshot(None, None).await; assert!(result.is_ok()); let json: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); - assert!(json["content"].as_str().unwrap().contains("No active sessions")); + assert!(json["content"] + .as_str() + .unwrap() + .contains("No active sessions")); assert_eq!(json["width"], 120); } diff --git a/src-gossamer/src/verisim_live.rs b/src-gossamer/src/verisim_live.rs index 3a1f453a..9705f87f 100644 --- a/src-gossamer/src/verisim_live.rs +++ b/src-gossamer/src/verisim_live.rs @@ -21,8 +21,7 @@ const DEFAULT_VERISIMDB_URL: &str = "http://localhost:8080"; /// Build the VeriSimDB endpoint, respecting the `VERISIMDB_URL` /// environment variable if set. fn verisim_endpoint() -> ServiceEndpoint { - let url = - std::env::var("VERISIMDB_URL").unwrap_or_else(|_| DEFAULT_VERISIMDB_URL.to_string()); + let url = std::env::var("VERISIMDB_URL").unwrap_or_else(|_| DEFAULT_VERISIMDB_URL.to_string()); ServiceEndpoint::new(&url) } @@ -55,7 +54,6 @@ pub struct OctadSummary { /// Check VeriSimDB health (async). /// /// `GET /health` — returns the full health envelope as a JSON string. - pub async fn verisim_live_health() -> Result { let endpoint = verisim_endpoint(); let health: VeriSimHealth = http_client::get_json(&endpoint, "/health").await?; @@ -65,7 +63,6 @@ pub async fn verisim_live_health() -> Result { /// List octads from VeriSimDB (async). /// /// `GET /api/octads` — returns a JSON array of octad summaries. - pub async fn verisim_live_list_octads() -> Result { let endpoint = verisim_endpoint(); let octads: Vec = http_client::get_json(&endpoint, "/api/octads").await?; @@ -76,7 +73,6 @@ pub async fn verisim_live_list_octads() -> Result { /// /// `POST /api/vcl` with a JSON body containing the query string. /// Returns the query result as a JSON string. - pub async fn verisim_live_query(query: String) -> Result { let endpoint = verisim_endpoint(); let body = json!({ "query": query }); @@ -86,7 +82,6 @@ pub async fn verisim_live_query(query: String) -> Result { /// Get a single octad by ID (async). /// /// `GET /api/octads/:id` — returns the octad with all modalities. - pub async fn verisim_live_get_octad(id: String) -> Result { let endpoint = verisim_endpoint(); let path = format!("/api/octads/{}", id); diff --git a/src-gossamer/src/vm_inspector/commands.rs b/src-gossamer/src/vm_inspector/commands.rs index bab6ca06..85db6dd6 100644 --- a/src-gossamer/src/vm_inspector/commands.rs +++ b/src-gossamer/src/vm_inspector/commands.rs @@ -123,8 +123,7 @@ fn state_to_json(vm: &VmState) -> Result { "history_len": vm.history.len(), "channels": vm.channels, }); - serde_json::to_string(&payload) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&payload).map_err(|e| format!("Serialisation error: {e}")) } /// Capture a pre-execution snapshot and push it onto the history ring buffer. @@ -471,7 +470,6 @@ fn execute_one(vm: &mut VmState) -> Result<(), String> { /// Read the current VM state. Returns JSON with pc, stack, memory, running /// status, instruction list, step counts, and history length. - pub async fn vm_inspector_read_state() -> Result { let vm = VM.lock().map_err(|e| format!("Lock poisoned: {e}"))?; state_to_json(&vm) @@ -479,7 +477,6 @@ pub async fn vm_inspector_read_state() -> Result { /// Execute one instruction forward. Captures a snapshot before execution /// so that `step_backward` can restore it. - pub async fn vm_inspector_step_forward() -> Result { let mut vm = VM.lock().map_err(|e| format!("Lock poisoned: {e}"))?; capture_snapshot(&mut vm); @@ -489,10 +486,11 @@ pub async fn vm_inspector_step_forward() -> Result { /// Reverse one instruction by restoring the most recent snapshot from /// the history ring buffer. - pub async fn vm_inspector_step_backward() -> Result { let mut vm = VM.lock().map_err(|e| format!("Lock poisoned: {e}"))?; - let snap = vm.history.pop_back() + let snap = vm + .history + .pop_back() .ok_or_else(|| "No history to reverse".to_string())?; vm.pc = snap.pc; vm.stack = snap.stack; @@ -506,7 +504,6 @@ pub async fn vm_inspector_step_backward() -> Result { /// Run the VM until end of program or error (simulates hitting a /// breakpoint). Executes up to 100,000 instructions to prevent /// infinite loops. - pub async fn vm_inspector_run() -> Result { let mut vm = VM.lock().map_err(|e| format!("Lock poisoned: {e}"))?; vm.running = true; @@ -539,7 +536,6 @@ pub async fn vm_inspector_run() -> Result { /// JLE, LOOP, CALL, SEND, RECV. /// /// Returns a JSON array of `Instruction` objects with tier assignments. - pub async fn vm_inspector_load_program(assembly: String) -> Result { let mut instructions = Vec::new(); @@ -561,10 +557,31 @@ pub async fn vm_inspector_load_program(assembly: String) -> Result Result Result { let vm = VM.lock().map_err(|e| format!("Lock poisoned: {e}"))?; - serde_json::to_string(&*vm) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&*vm).map_err(|e| format!("Serialisation error: {e}")) } /// Read VM state from a JSON file on disk. Parses the file as a /// `VmState` and loads it into the global VM, replacing the current /// state entirely. - pub async fn vm_inspector_read_file(path: String) -> Result { - let content = fs::read_to_string(&path) - .map_err(|e| format!("Cannot read '{}': {e}", path))?; - let loaded: VmState = serde_json::from_str(&content) - .map_err(|e| format!("Cannot parse VM state: {e}"))?; + let content = fs::read_to_string(&path).map_err(|e| format!("Cannot read '{}': {e}", path))?; + let loaded: VmState = + serde_json::from_str(&content).map_err(|e| format!("Cannot parse VM state: {e}"))?; let mut vm = VM.lock().map_err(|e| format!("Lock poisoned: {e}"))?; *vm = loaded; @@ -832,11 +844,11 @@ mod tests { let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); reset_vm(); load_instructions(vec![ - instr_op("PUSH", 99), // value to store - instr_op("PUSH", 10), // address - instr("STORE"), // mem[10] = 99 - instr_op("PUSH", 10), // address to load - instr("LOAD"), // push mem[10] → 99 + instr_op("PUSH", 99), // value to store + instr_op("PUSH", 10), // address + instr("STORE"), // mem[10] = 99 + instr_op("PUSH", 10), // address to load + instr("LOAD"), // push mem[10] → 99 ]); { let mut vm = VM.lock().unwrap_or_else(|e| e.into_inner()); @@ -917,10 +929,10 @@ mod tests { let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); reset_vm(); load_instructions(vec![ - instr_op("PUSH", 5), // tier 0 - instr_op("PUSH", 3), // tier 0 - instr("ADD"), // tier 1 - instr("NOOP"), // tier 3 + instr_op("PUSH", 5), // tier 0 + instr_op("PUSH", 3), // tier 0 + instr("ADD"), // tier 1 + instr("NOOP"), // tier 3 ]); { let mut vm = VM.lock().unwrap_or_else(|e| e.into_inner()); @@ -1003,11 +1015,7 @@ mod tests { fn test_mul() { let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); reset_vm(); - load_instructions(vec![ - instr_op("PUSH", 6), - instr_op("PUSH", 7), - instr("MUL"), - ]); + load_instructions(vec![instr_op("PUSH", 6), instr_op("PUSH", 7), instr("MUL")]); { let mut vm = VM.lock().unwrap_or_else(|e| e.into_inner()); for _ in 0..3 { @@ -1039,10 +1047,10 @@ mod tests { reset_vm(); // PUSH 1, JNZ 4, PUSH 99, NOOP, PUSH 42 load_instructions(vec![ - instr_op("PUSH", 1), // 0: push 1 - instr_op("JNZ", 3), // 1: TOS=1 != 0, jump to 3 - instr_op("PUSH", 99), // 2: should be skipped - instr_op("PUSH", 42), // 3: lands here + instr_op("PUSH", 1), // 0: push 1 + instr_op("JNZ", 3), // 1: TOS=1 != 0, jump to 3 + instr_op("PUSH", 99), // 2: should be skipped + instr_op("PUSH", 42), // 3: lands here ]); { let mut vm = VM.lock().unwrap_or_else(|e| e.into_inner()); @@ -1060,10 +1068,10 @@ mod tests { let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); reset_vm(); load_instructions(vec![ - instr_op("PUSH", 0), // 0: push 0 - instr_op("JNZ", 3), // 1: TOS=0, no jump - instr_op("PUSH", 99), // 2: executed - instr_op("PUSH", 42), // 3: also executed + instr_op("PUSH", 0), // 0: push 0 + instr_op("JNZ", 3), // 1: TOS=0, no jump + instr_op("PUSH", 99), // 2: executed + instr_op("PUSH", 42), // 3: also executed ]); { let mut vm = VM.lock().unwrap_or_else(|e| e.into_inner()); @@ -1085,8 +1093,8 @@ mod tests { instr_op("PUSH", 5), instr_op("PUSH", 3), instr_op("JLE", 4), - instr_op("PUSH", 99), // 3: skipped - instr_op("PUSH", 42), // 4: lands here + instr_op("PUSH", 99), // 3: skipped + instr_op("PUSH", 42), // 4: lands here ]); { let mut vm = VM.lock().unwrap_or_else(|e| e.into_inner()); @@ -1104,16 +1112,18 @@ mod tests { reset_vm(); // PUSH 3, LOOP back to 1 → decrements 3→2→1→0, pops on 0 load_instructions(vec![ - instr_op("PUSH", 3), // 0: counter = 3 - instr_op("LOOP", 1), // 1: decrement, loop if > 0 - instr("NOOP"), // 2: after loop + instr_op("PUSH", 3), // 0: counter = 3 + instr_op("LOOP", 1), // 1: decrement, loop if > 0 + instr("NOOP"), // 2: after loop ]); { let mut vm = VM.lock().unwrap_or_else(|e| e.into_inner()); // Execute: PUSH(3), LOOP(3→2, jump), LOOP(2→1, jump), LOOP(1→0, pop), NOOP for _ in 0..5 { capture_snapshot(&mut vm); - if execute_one(&mut vm).is_err() { break; } + if execute_one(&mut vm).is_err() { + break; + } } // Counter was popped when it reached 0. assert!(vm.stack.is_empty()); @@ -1146,11 +1156,7 @@ mod tests { let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); reset_vm(); // PUSH 77, SEND, RECV — should round-trip through channel. - load_instructions(vec![ - instr_op("PUSH", 77), - instr("SEND"), - instr("RECV"), - ]); + load_instructions(vec![instr_op("PUSH", 77), instr("SEND"), instr("RECV")]); { let mut vm = VM.lock().unwrap_or_else(|e| e.into_inner()); for _ in 0..3 { diff --git a/src-gossamer/src/voicetag/commands.rs b/src-gossamer/src/voicetag/commands.rs index 0c69d1ba..78148b8e 100644 --- a/src-gossamer/src/voicetag/commands.rs +++ b/src-gossamer/src/voicetag/commands.rs @@ -16,12 +16,10 @@ use walkdir::WalkDir; /// Returns the raw JSON content so the ReScript frontend can parse it with /// its own type-safe deserialiser. If the file does not exist, returns an /// empty MRI file structure so the frontend can initialise cleanly. - pub async fn voicetag_load(path: String) -> Result { let p = Path::new(&path); if p.exists() { - fs::read_to_string(p) - .map_err(|e| format!("Cannot read {}: {}", path, e)) + fs::read_to_string(p).map_err(|e| format!("Cannot read {}: {}", path, e)) } else { // File doesn't exist yet — return empty structure so the frontend // can initialise a new sidecar without special error handling. @@ -33,7 +31,6 @@ pub async fn voicetag_load(path: String) -> Result { /// /// The content is a pre-serialised JSON string from the ReScript frontend. /// Creates parent directories if they don't exist (handles nested source trees). - pub async fn voicetag_save(path: String, content: String) -> Result { let p = Path::new(&path); @@ -45,8 +42,7 @@ pub async fn voicetag_save(path: String, content: String) -> Result Result Result { let p = Path::new(&path); if p.exists() { - fs::remove_file(p) - .map_err(|e| format!("Cannot delete {}: {}", path, e))?; + fs::remove_file(p).map_err(|e| format!("Cannot delete {}: {}", path, e))?; Ok(format!("Deleted {}", path)) } else { Ok(format!("Already gone: {}", path)) @@ -80,7 +74,6 @@ pub async fn voicetag_delete(path: String) -> Result { /// {"sidecar": "src/Update.res.mri.json", "source": "src/Update.res"} /// ] /// ``` - pub async fn voicetag_scan(path: String) -> Result { let root = Path::new(&path); if !root.is_dir() { @@ -99,7 +92,8 @@ pub async fn voicetag_scan(path: String) -> Result { if name.ends_with(".mri.json") { let sidecar = file_path.to_string_lossy().to_string(); // Derive the source file path by stripping `.mri.json` suffix. - let source = sidecar.strip_suffix(".mri.json") + let source = sidecar + .strip_suffix(".mri.json") .unwrap_or(&sidecar) .to_string(); results.push(serde_json::json!({ @@ -110,8 +104,7 @@ pub async fn voicetag_scan(path: String) -> Result { } } - serde_json::to_string(&results) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&results).map_err(|e| format!("Serialisation error: {e}")) } // --------------------------------------------------------------------------- @@ -124,8 +117,8 @@ mod tests { #[test] fn smoke_empty_mri_json_is_valid() { let empty = r#"{"version":"1.0","sourceFile":"","tags":[],"lastModified":0}"#; - let parsed: serde_json::Value = serde_json::from_str(empty) - .expect("empty MRI JSON must be valid JSON"); + let parsed: serde_json::Value = + serde_json::from_str(empty).expect("empty MRI JSON must be valid JSON"); assert_eq!(parsed["version"], "1.0"); assert!(parsed["tags"].as_array().unwrap().is_empty()); } @@ -148,8 +141,10 @@ mod tests { assert!(name.ends_with(".mri.json"), "{name} should match .mri.json"); } for name in non_matching { - assert!(!name.ends_with(".mri.json") || name == ".mri.json.bak", - "{name} should NOT match .mri.json"); + assert!( + !name.ends_with(".mri.json") || name == ".mri.json.bak", + "{name} should NOT match .mri.json" + ); } } } diff --git a/src-gossamer/src/watcher/commands.rs b/src-gossamer/src/watcher/commands.rs index 626a71ac..91a2b3d2 100644 --- a/src-gossamer/src/watcher/commands.rs +++ b/src-gossamer/src/watcher/commands.rs @@ -7,7 +7,7 @@ //! to. Commands are designed to be safe to call multiple times (idempotent //! start/stop) and to survive the watched directory not existing yet. //! -//! We use `notify` (https://docs.rs/notify) which wraps inotify/FSEvents/ +//! We use `notify` () which wraps inotify/FSEvents/ //! ReadDirectoryChangesW per-platform behind a unified API. The alternative //! in Go (`fsnotify`) has a long history of goroutine leaks on recursive //! watches and platform-specific edge cases that silently drop events. Rust's @@ -22,11 +22,11 @@ use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::time::Duration; +use crate::compat::Emitter; use notify::RecursiveMode; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use once_cell::sync::Lazy; use serde_json::json; -use crate::compat::Emitter; use super::types::{WatchEvent, WatchEventKind, WatcherStatus}; @@ -97,7 +97,6 @@ fn build_watch_event(path: &std::path::Path, kind: WatchEventKind) -> WatchEvent /// Events are emitted on the `watcher://event` channel as JSON-serialised /// `WatchEvent` objects that the ReScript `WatcherCmd.listen` function /// deserialises into TEA messages. - pub async fn watcher_start( app: crate::compat::AppHandle, paths: Vec, @@ -172,7 +171,6 @@ pub async fn watcher_start( /// Stop the filesystem watcher. /// /// Idempotent — safe to call even if the watcher is not running. - pub async fn watcher_stop() -> Result { let mut state = WATCHER.lock().map_err(|e| format!("Lock error: {e}"))?; state.debouncer = None; @@ -185,7 +183,6 @@ pub async fn watcher_stop() -> Result { /// /// If the watcher is not running, returns an error suggesting `watcher_start` /// should be called first. The path is watched recursively. - pub async fn watcher_add_path(path: String) -> Result { let mut state = WATCHER.lock().map_err(|e| format!("Lock error: {e}"))?; @@ -218,7 +215,6 @@ pub async fn watcher_add_path(path: String) -> Result { /// /// If the watcher is not running or the path is not being watched, this /// returns gracefully without error (idempotent). - pub async fn watcher_remove_path(path: String) -> Result { let mut state = WATCHER.lock().map_err(|e| format!("Lock error: {e}"))?; @@ -241,7 +237,6 @@ pub async fn watcher_remove_path(path: String) -> Result { /// /// Returns whether the watcher is running, which paths are watched, and /// the total event count since last start. - pub async fn watcher_status() -> Result { let state = WATCHER.lock().map_err(|e| format!("Lock error: {e}"))?; diff --git a/src-gossamer/src/watcher/mod.rs b/src-gossamer/src/watcher/mod.rs index b3fb0c0c..7e638dd8 100644 --- a/src-gossamer/src/watcher/mod.rs +++ b/src-gossamer/src/watcher/mod.rs @@ -17,5 +17,5 @@ //! The watcher is debounced (500ms) to avoid flooding the TEA loop with //! rapid successive events from editors that write-then-rename. -pub mod types; pub mod commands; +pub mod types; diff --git a/src-gossamer/src/watcher/types.rs b/src-gossamer/src/watcher/types.rs index ee330b44..084e2c3c 100644 --- a/src-gossamer/src/watcher/types.rs +++ b/src-gossamer/src/watcher/types.rs @@ -78,7 +78,8 @@ mod tests { ]; for kind in kinds { let json = serde_json::to_string(&kind).expect("serialise WatchEventKind must succeed"); - let _back: WatchEventKind = serde_json::from_str(&json).expect("deserialise must succeed"); + let _back: WatchEventKind = + serde_json::from_str(&json).expect("deserialise must succeed"); } } diff --git a/src-gossamer/src/wiring_inspector/commands.rs b/src-gossamer/src/wiring_inspector/commands.rs index 25e6f0a8..e7306d1a 100644 --- a/src-gossamer/src/wiring_inspector/commands.rs +++ b/src-gossamer/src/wiring_inspector/commands.rs @@ -66,7 +66,6 @@ fn pcc_binary() -> Result { /// - The PCC binary is not found /// - The subprocess fails to start /// - The subprocess exits with a non-zero code - pub async fn wiring_inspector_verify() -> Result { let binary = pcc_binary()?; let root = repo_root()?; @@ -103,7 +102,6 @@ pub async fn wiring_inspector_verify() -> Result { /// # Arguments /// /// * `panel_id` - The panel identifier to verify (e.g. "WiringInspector"). - pub async fn wiring_inspector_verify_panel(panel_id: String) -> Result { let binary = pcc_binary()?; let root = repo_root()?; @@ -129,6 +127,8 @@ pub async fn wiring_inspector_verify_panel(panel_id: String) -> Result Result { - let base = dirs::config_dir() - .ok_or_else(|| "Cannot determine config directory".to_string())?; + let base = dirs::config_dir().ok_or_else(|| "Cannot determine config directory".to_string())?; let panll_dir = base.join("panll"); - fs::create_dir_all(&panll_dir) - .map_err(|e| format!("Cannot create config dir: {e}"))?; + fs::create_dir_all(&panll_dir).map_err(|e| format!("Cannot create config dir: {e}"))?; Ok(panll_dir) } /// Save an arrangement to disk as JSON. - pub async fn save_arrangement(arrangement: Arrangement) -> Result { let dir = config_dir()?.join("arrangements"); - fs::create_dir_all(&dir) - .map_err(|e| format!("Cannot create arrangements dir: {e}"))?; + fs::create_dir_all(&dir).map_err(|e| format!("Cannot create arrangements dir: {e}"))?; let path = dir.join(format!("{}.json", arrangement.id)); let json = serde_json::to_string_pretty(&arrangement) .map_err(|e| format!("Serialisation error: {e}"))?; - fs::write(&path, &json) - .map_err(|e| format!("Write error: {e}"))?; + fs::write(&path, &json).map_err(|e| format!("Write error: {e}"))?; - Ok(format!("Arrangement '{}' saved to {}", arrangement.name, path.display())) + Ok(format!( + "Arrangement '{}' saved to {}", + arrangement.name, + path.display() + )) } /// Load all saved arrangements from disk. - pub async fn load_arrangements() -> Result { let dir = config_dir()?.join("arrangements"); if !dir.exists() { @@ -47,8 +45,7 @@ pub async fn load_arrangements() -> Result { } let mut arrangements: Vec = Vec::new(); - let entries = fs::read_dir(&dir) - .map_err(|e| format!("Cannot read arrangements dir: {e}"))?; + let entries = fs::read_dir(&dir).map_err(|e| format!("Cannot read arrangements dir: {e}"))?; for entry in entries { let entry = entry.map_err(|e| format!("Dir entry error: {e}"))?; @@ -58,22 +55,24 @@ pub async fn load_arrangements() -> Result { .map_err(|e| format!("Read error for {}: {e}", path.display()))?; match serde_json::from_str::(&content) { Ok(arr) => arrangements.push(arr), - Err(e) => eprintln!("Warning: skipping invalid arrangement {}: {e}", path.display()), + Err(e) => eprintln!( + "Warning: skipping invalid arrangement {}: {e}", + path.display() + ), } } } - serde_json::to_string(&arrangements) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&arrangements).map_err(|e| format!("Serialisation error: {e}")) } /// Delete an arrangement file from disk. - pub async fn delete_arrangement(arrangement_id: String) -> Result { - let path = config_dir()?.join("arrangements").join(format!("{arrangement_id}.json")); + let path = config_dir()? + .join("arrangements") + .join(format!("{arrangement_id}.json")); if path.exists() { - fs::remove_file(&path) - .map_err(|e| format!("Delete error: {e}"))?; + fs::remove_file(&path).map_err(|e| format!("Delete error: {e}"))?; Ok(format!("Arrangement '{arrangement_id}' deleted")) } else { Err(format!("Arrangement '{arrangement_id}' not found")) @@ -81,24 +80,24 @@ pub async fn delete_arrangement(arrangement_id: String) -> Result Result { let dir = config_dir()?.join("sessions"); - fs::create_dir_all(&dir) - .map_err(|e| format!("Cannot create sessions dir: {e}"))?; + fs::create_dir_all(&dir).map_err(|e| format!("Cannot create sessions dir: {e}"))?; let path = dir.join(format!("{}.json", session.id)); - let json = serde_json::to_string_pretty(&session) - .map_err(|e| format!("Serialisation error: {e}"))?; + let json = + serde_json::to_string_pretty(&session).map_err(|e| format!("Serialisation error: {e}"))?; - fs::write(&path, &json) - .map_err(|e| format!("Write error: {e}"))?; + fs::write(&path, &json).map_err(|e| format!("Write error: {e}"))?; - Ok(format!("Session '{}' saved to {}", session.name, path.display())) + Ok(format!( + "Session '{}' saved to {}", + session.name, + path.display() + )) } /// Load all saved sessions from disk. - pub async fn load_sessions() -> Result { let dir = config_dir()?.join("sessions"); if !dir.exists() { @@ -106,8 +105,7 @@ pub async fn load_sessions() -> Result { } let mut sessions: Vec = Vec::new(); - let entries = fs::read_dir(&dir) - .map_err(|e| format!("Cannot read sessions dir: {e}"))?; + let entries = fs::read_dir(&dir).map_err(|e| format!("Cannot read sessions dir: {e}"))?; for entry in entries { let entry = entry.map_err(|e| format!("Dir entry error: {e}"))?; @@ -122,17 +120,16 @@ pub async fn load_sessions() -> Result { } } - serde_json::to_string(&sessions) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&sessions).map_err(|e| format!("Serialisation error: {e}")) } /// Delete a session file from disk. - pub async fn delete_session(session_id: String) -> Result { - let path = config_dir()?.join("sessions").join(format!("{session_id}.json")); + let path = config_dir()? + .join("sessions") + .join(format!("{session_id}.json")); if path.exists() { - fs::remove_file(&path) - .map_err(|e| format!("Delete error: {e}"))?; + fs::remove_file(&path).map_err(|e| format!("Delete error: {e}"))?; Ok(format!("Session '{session_id}' deleted")) } else { Err(format!("Session '{session_id}' not found")) diff --git a/src-gossamer/src/workspace/mod.rs b/src-gossamer/src/workspace/mod.rs index f0f4d348..6f299b28 100644 --- a/src-gossamer/src/workspace/mod.rs +++ b/src-gossamer/src/workspace/mod.rs @@ -10,6 +10,6 @@ //! //! All state serialisation uses JSON files in the PanLL config directory. -pub mod types; pub mod commands; pub mod sysinfo; +pub mod types; diff --git a/src-gossamer/src/workspace/sysinfo.rs b/src-gossamer/src/workspace/sysinfo.rs index 38e3b133..372e7272 100644 --- a/src-gossamer/src/workspace/sysinfo.rs +++ b/src-gossamer/src/workspace/sysinfo.rs @@ -38,7 +38,11 @@ fn read_cpu_usage() -> f64 { let nice: f64 = parts[2].parse().unwrap_or(0.0); let system: f64 = parts[3].parse().unwrap_or(0.0); let idle: f64 = parts[4].parse().unwrap_or(0.0); - let iowait: f64 = if parts.len() > 5 { parts[5].parse().unwrap_or(0.0) } else { 0.0 }; + let iowait: f64 = if parts.len() > 5 { + parts[5].parse().unwrap_or(0.0) + } else { + 0.0 + }; let total = user + nice + system + idle + iowait; if total > 0.0 { @@ -60,12 +64,14 @@ fn read_memory_info() -> (u64, u64) { for line in content.lines() { if line.starts_with("MemTotal:") { - total_kb = line.split_whitespace() + total_kb = line + .split_whitespace() .nth(1) .and_then(|v| v.parse().ok()) .unwrap_or(0); } else if line.starts_with("MemAvailable:") { - available_kb = line.split_whitespace() + available_kb = line + .split_whitespace() .nth(1) .and_then(|v| v.parse().ok()) .unwrap_or(0); @@ -92,9 +98,9 @@ fn read_disk_usage() -> (u64, u64) { // Prefer the mount point named in PANLL_DATA_MOUNT (default: "eclipse"), // then fall back to root. - let mount_hint = std::env::var("PANLL_DATA_MOUNT") - .unwrap_or_else(|_| "eclipse".to_string()); - let mount_point = content.lines() + let mount_hint = std::env::var("PANLL_DATA_MOUNT").unwrap_or_else(|_| "eclipse".to_string()); + let mount_point = content + .lines() .find(|l| l.contains(&mount_hint)) .or_else(|| content.lines().find(|l| l.starts_with("/ "))) .and_then(|l| l.split_whitespace().nth(1)) @@ -125,7 +131,8 @@ fn read_uptime() -> u64 { Err(_) => return 0, }; - content.split_whitespace() + content + .split_whitespace() .next() .and_then(|v| v.parse::().ok()) .map(|v| v as u64) @@ -134,7 +141,6 @@ fn read_uptime() -> u64 { /// Tauri command: get current system information for status bar widgets. /// Returns a JSON-serialised SystemInfo struct. - pub async fn get_system_info() -> Result { let cpu_usage = read_cpu_usage(); let (memory_total, memory_used) = read_memory_info(); @@ -150,6 +156,5 @@ pub async fn get_system_info() -> Result { uptime_seconds, }; - serde_json::to_string(&info) - .map_err(|e| format!("Serialisation error: {e}")) + serde_json::to_string(&info).map_err(|e| format!("Serialisation error: {e}")) } diff --git a/src-gossamer/src/workspace/types.rs b/src-gossamer/src/workspace/types.rs index dc8026dc..1b590027 100644 --- a/src-gossamer/src/workspace/types.rs +++ b/src-gossamer/src/workspace/types.rs @@ -141,7 +141,8 @@ mod tests { visible: true, }; let json = serde_json::to_string(&pos).expect("PanelPosition must serialise"); - let back: PanelPosition = serde_json::from_str(&json).expect("PanelPosition must deserialise"); + let back: PanelPosition = + serde_json::from_str(&json).expect("PanelPosition must deserialise"); assert_eq!(back.panel_id, "panel-l"); assert!(back.visible); assert!(back.width > 0.0); @@ -192,9 +193,18 @@ mod tests { disk_used: 100_000_000_000, uptime_seconds: 86400, }; - assert!(info.memory_used <= info.memory_total, "used memory must not exceed total"); - assert!(info.disk_used <= info.disk_total, "used disk must not exceed total"); - assert!(info.cpu_usage >= 0.0 && info.cpu_usage <= 100.0, "CPU must be in [0,100]"); + assert!( + info.memory_used <= info.memory_total, + "used memory must not exceed total" + ); + assert!( + info.disk_used <= info.disk_total, + "used disk must not exceed total" + ); + assert!( + info.cpu_usage >= 0.0 && info.cpu_usage <= 100.0, + "CPU must be in [0,100]" + ); } #[test] @@ -202,13 +212,21 @@ mod tests { let group = PanelGroup { id: "grp-001".to_string(), name: "Dev Tools".to_string(), - panel_ids: vec!["panel-l".to_string(), "panel-n".to_string(), "panel-w".to_string()], + panel_ids: vec![ + "panel-l".to_string(), + "panel-n".to_string(), + "panel-w".to_string(), + ], locked: false, visible: true, z_index: 0, shared_with: vec![], }; let ids: std::collections::HashSet<_> = group.panel_ids.iter().collect(); - assert_eq!(ids.len(), group.panel_ids.len(), "panel IDs in group must be unique"); + assert_eq!( + ids.len(), + group.panel_ids.len(), + "panel IDs in group must be unique" + ); } }