diff --git a/crates/cli/src/commands/db.rs b/crates/cli/src/commands/db.rs index f7e41b2c..f66a6d9f 100644 --- a/crates/cli/src/commands/db.rs +++ b/crates/cli/src/commands/db.rs @@ -23,7 +23,7 @@ pub async fn run(args: DbArgs, output_format: &str) -> anyhow::Result<()> { match args.command { DbCommands::Update => update_taxonomy_database(output_format).await?, DbCommands::Stats => { - let db = grat_core::taxonomy::loader::TaxonomyDatabase::load_embedded()?; + let db = grat_core::taxonomy::loader::TaxonomyDatabase::load_latest()?; if matches!( crate::output::OutputFormat::parse(output_format), crate::output::OutputFormat::Json @@ -63,7 +63,7 @@ async fn update_taxonomy_database(output_format: &str) -> Result<()> { crate::output::OutputFormat::Json ) { tokio::time::sleep(Duration::from_secs(1)).await; - let db = grat_core::taxonomy::loader::TaxonomyDatabase::load_embedded() + let db = grat_core::taxonomy::loader::TaxonomyDatabase::load_latest() .context("Failed to load updated taxonomy database")?; let payload = serde_json::json!({ "status": "ok", @@ -100,7 +100,7 @@ async fn update_taxonomy_database(output_format: &str) -> Result<()> { spinner.finish_with_message("✅ Taxonomy database updated successfully!"); - let db = grat_core::taxonomy::loader::TaxonomyDatabase::load_embedded() + let db = grat_core::taxonomy::loader::TaxonomyDatabase::load_latest() .context("Failed to load updated taxonomy database")?; println!("📊 Database now contains {} error definitions", db.len()); diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 9025a02e..0ebb6f99 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -43,6 +43,9 @@ struct Cli { #[arg(long, global = true)] no_color: bool, + + #[arg(long, global = true, help = "Disable network requests for updates")] + offline: bool, } #[derive(Subcommand)] @@ -88,6 +91,9 @@ async fn main() -> anyhow::Result<()> { let version: &'static str = Box::leak(build_version().into_boxed_str()); let matches = Cli::command().version(version).get_matches(); let cli = Cli::from_arg_matches(&matches)?; + + let _taxonomy_update_handle = + tokio::spawn(grat_core::taxonomy::updater::check_and_update(cli.offline)); let loaded_config = config::ConfigManager::new() .and_then(|manager| manager.load()) .ok(); diff --git a/crates/core/src/decode/chain_analyzer.rs b/crates/core/src/decode/chain_analyzer.rs index e74198ed..21df5a1c 100644 --- a/crates/core/src/decode/chain_analyzer.rs +++ b/crates/core/src/decode/chain_analyzer.rs @@ -238,7 +238,7 @@ impl ChainAnalyzer { // Detect failure // ---------------------------------------------------------------- if is_failure(event, &topics, &v0.data) { - return Some(self.build_chain(&stack, event)); + return Some(Self::build_chain(&stack, event)); } } @@ -251,7 +251,7 @@ impl ChainAnalyzer { /// Assemble a [`CallChain`] from the current stack snapshot and the event /// that triggered the failure detection. - fn build_chain(&self, stack: &[StackFrame], failing_event: &DiagnosticEvent) -> CallChain { + fn build_chain(stack: &[StackFrame], failing_event: &DiagnosticEvent) -> CallChain { if stack.is_empty() { // Failure occurred before any fn_call was seen - synthesize a // single-frame chain from the event's own contract_id. diff --git a/crates/core/src/decode/report.rs b/crates/core/src/decode/report.rs index d877e2cd..4c2ac5bd 100644 --- a/crates/core/src/decode/report.rs +++ b/crates/core/src/decode/report.rs @@ -4,7 +4,7 @@ use crate::taxonomy::loader::TaxonomyDatabase; use crate::types::report::{DiagnosticReport, RootCause, Severity, SuggestedFix}; pub fn build_report(error: &ClassifiedError) -> GratResult { - let db = TaxonomyDatabase::load_embedded()?; + let db = TaxonomyDatabase::load_latest()?; if let Some(entry) = db.lookup(&error.category, error.error_code) { let report = DiagnosticReport { diff --git a/crates/core/src/replay/sandbox.rs b/crates/core/src/replay/sandbox.rs index c02e6eb7..e2f2caa8 100644 --- a/crates/core/src/replay/sandbox.rs +++ b/crates/core/src/replay/sandbox.rs @@ -62,6 +62,7 @@ pub struct SandboxResult { /// tracing is never cut short by an artificial memory limit. const MEM_BYTES_CEILING: u64 = 500 * 1024 * 1024; +#[allow(clippy::too_many_lines)] pub async fn execute_with_tracing(state: &LedgerState, tx_hash: &str) -> GratResult { let inv = &state.invocation; diff --git a/crates/core/src/replay/state.rs b/crates/core/src/replay/state.rs index 3387bcb4..0ef21804 100644 --- a/crates/core/src/replay/state.rs +++ b/crates/core/src/replay/state.rs @@ -109,6 +109,7 @@ fn parse_protocol_version(value: Option<&serde_json::Value>) -> u32 { .unwrap_or(0) as u32 } +#[allow(clippy::too_many_lines)] async fn reconstruct_hot_path( ledger_sequence: u32, protocol_version: u32, diff --git a/crates/core/src/taxonomy/loader.rs b/crates/core/src/taxonomy/loader.rs index 5350bf02..3c140425 100644 --- a/crates/core/src/taxonomy/loader.rs +++ b/crates/core/src/taxonomy/loader.rs @@ -18,6 +18,37 @@ pub struct TaxonomyDatabase { } impl TaxonomyDatabase { + pub fn load_latest() -> GratResult { + if let Some(db_path) = crate::taxonomy::updater::db_file_path() { + if db_path.exists() { + if db_path.is_dir() { + if let Ok(db) = Self::load_from_dir(&db_path) { + return Ok(db); + } + } else if let Ok(content) = std::fs::read_to_string(&db_path) { + if let Ok(schema) = TaxonomyParser::parse(&content) { + let mut db = Self { + entries: HashMap::new(), + all_entries: Vec::new(), + }; + for entry in schema.errors { + db.entries + .insert((entry.category.clone(), entry.code), entry.clone()); + db.all_entries.push(entry); + } + tracing::info!( + "Loaded {} taxonomy entries from downloaded file", + db.entries.len() + ); + return Ok(db); + } + } + } + } + + Self::load_embedded() + } + pub fn load_embedded() -> GratResult { let mut db = Self { entries: HashMap::new(), diff --git a/crates/core/src/taxonomy/mod.rs b/crates/core/src/taxonomy/mod.rs index d29f6c6a..8cc6fd5c 100644 --- a/crates/core/src/taxonomy/mod.rs +++ b/crates/core/src/taxonomy/mod.rs @@ -1,3 +1,4 @@ pub mod linter; pub mod loader; pub mod schema; +pub mod updater; diff --git a/crates/core/src/taxonomy/updater.rs b/crates/core/src/taxonomy/updater.rs new file mode 100644 index 00000000..91788ebb --- /dev/null +++ b/crates/core/src/taxonomy/updater.rs @@ -0,0 +1,117 @@ +use crate::error::GratResult; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; +use std::time::{Duration, SystemTime}; + +const TAXONOMY_URL: &str = "https://raw.githubusercontent.com/grat-soroban/grat/main/taxonomy/enhanced_error_taxonomy.toml"; + +#[derive(Serialize, Deserialize)] +struct UpdateCache { + last_check: SystemTime, + version: Option, +} + +pub fn cache_file_path() -> Option { + directories::ProjectDirs::from("", "", "grat") + .map(|proj_dirs| proj_dirs.cache_dir().join("taxonomy_update.json")) +} + +pub fn db_file_path() -> Option { + directories::ProjectDirs::from("", "", "grat") + .map(|proj_dirs| proj_dirs.data_dir().join("taxonomy").join("database.toml")) +} + +pub async fn check_and_update(offline: bool) -> GratResult<()> { + if offline { + tracing::debug!("Offline mode enabled, skipping taxonomy update check"); + return Ok(()); + } + + let Some(cache_path) = cache_file_path() else { + return Ok(()); + }; + + if let Ok(content) = fs::read_to_string(&cache_path) { + if let Ok(cache) = serde_json::from_str::(&content) { + if let Ok(elapsed) = cache.last_check.elapsed() { + if elapsed.as_secs() < 24 * 60 * 60 { + tracing::debug!("Taxonomy update checked recently, skipping"); + return Ok(()); + } + } + } + } + + let client = match reqwest::Client::builder() + .user_agent("grat-taxonomy-updater") + .timeout(Duration::from_secs(10)) + .build() + { + Ok(c) => c, + Err(e) => { + tracing::warn!("Failed to build reqwest client for taxonomy update: {e}"); + return Ok(()); + } + }; + + tracing::debug!("Checking for latest taxonomy version"); + + let response = match client.get(TAXONOMY_URL).send().await { + Ok(r) => r, + Err(e) => { + tracing::warn!("Failed to download taxonomy database: {e}"); + return Ok(()); + } + }; + + if !response.status().is_success() { + tracing::warn!( + "Failed to download taxonomy database: HTTP {}", + response.status() + ); + return Ok(()); + } + + let content = match response.text().await { + Ok(t) => t, + Err(e) => { + tracing::warn!("Failed to read taxonomy database response: {e}"); + return Ok(()); + } + }; + + let Some(db_path) = db_file_path() else { + return Ok(()); + }; + + if let Some(parent) = db_path.parent() { + let _ = fs::create_dir_all(parent); + } + + // Atomically replace the database file + let temp_path = db_path.with_extension("tmp"); + if fs::write(&temp_path, &content).is_ok() { + if fs::rename(&temp_path, &db_path).is_err() { + tracing::warn!("Failed to replace taxonomy database atomically"); + let _ = fs::remove_file(temp_path); + } else { + tracing::info!("Taxonomy database updated successfully"); + } + } + + if let Some(parent) = cache_path.parent() { + let _ = fs::create_dir_all(parent); + } + + let new_cache = UpdateCache { + last_check: SystemTime::now(), + version: None, // We don't have versioning for raw file yet, just timestamp cache + }; + + if let Ok(serialized) = serde_json::to_string(&new_cache) { + let _ = fs::write(&cache_path, serialized); + } + + Ok(()) +}