diff --git a/crates/cli/src/output/human.rs b/crates/cli/src/output/human.rs index fb0e3fbc..1cf02742 100644 --- a/crates/cli/src/output/human.rs +++ b/crates/cli/src/output/human.rs @@ -1,8 +1,8 @@ - - use prism_core::types::report::DiagnosticReport; -use crate::output::renderers::{render_section_header, render_error_card, render_fix_list, BudgetBar}; +use crate::output::renderers::{ + render_cause_list, render_error_card, render_fix_list, render_section_header, BudgetBar, +}; pub fn print_report(report: &DiagnosticReport) -> anyhow::Result<()> { println!("{}", render_error_card(report)); @@ -38,6 +38,11 @@ pub fn print_report(report: &DiagnosticReport) -> anyhow::Result<()> { ); } + if !report.root_causes.is_empty() { + println!(); + println!("{}", render_cause_list(&report.root_causes)); + } + if !report.suggested_fixes.is_empty() { println!(); println!("{}", render_fix_list(&report.suggested_fixes)); diff --git a/crates/cli/src/output/renderers.rs b/crates/cli/src/output/renderers.rs index 15f5830e..4c6bb764 100644 --- a/crates/cli/src/output/renderers.rs +++ b/crates/cli/src/output/renderers.rs @@ -1,12 +1,10 @@ - - #![allow(dead_code)] +use crate::output::theme::ColorPalette; use colored::Colorize; -use prism_core::types::report::{DiagnosticReport, TransactionContext}; +use prism_core::types::report::{DiagnosticReport, RootCause, TransactionContext}; use prism_core::types::trace::ResourceProfile; use tabled::{Table, Tabled}; -use crate::output::theme::ColorPalette; const BAR_WIDTH: usize = 10; const HEAT_BLOCKS: [&str; 4] = ["░", "▒", "▓", "█"]; @@ -23,6 +21,10 @@ pub fn render_fix_list(fixes: &[prism_core::types::report::SuggestedFix]) -> Str FixList::new(fixes).render() } +pub fn render_cause_list(causes: &[RootCause]) -> String { + CauseList::new(causes).render() +} + pub fn render_state_diff_table(diff: &prism_core::types::trace::StateDiff) -> String { StateDiffTable::new(diff).render() } @@ -64,12 +66,13 @@ impl<'a> ErrorCard<'a> { let mut output = String::new(); let category_badge = format!("[{}]", self.report.error_category.to_uppercase()); - let error_line = format!( - " {} ({})", - self.report.error_name, self.report.error_code - ); + let error_line = format!(" {} ({})", self.report.error_name, self.report.error_code); - let max_width = error_line.len().max(self.report.summary.len()).max(category_badge.len()) + 4; + let max_width = error_line + .len() + .max(self.report.summary.len()) + .max(category_badge.len()) + + 4; let border = "█".repeat(max_width); let border_colored = border.red().bold().to_string(); @@ -83,7 +86,11 @@ impl<'a> ErrorCard<'a> { if let Some(contract_error) = &self.report.contract_error { let component_line = format!("Component: {}", contract_error.contract_id); - output.push_str(&format!("{} {}\n", "█".red().bold(), component_line.white())); + output.push_str(&format!( + "{} {}\n", + "█".red().bold(), + component_line.white() + )); } output.push_str(&format!("{} {}\n", "█".red().bold(), summary_colored)); @@ -128,6 +135,34 @@ impl<'a> FixList<'a> { } } +pub struct CauseList<'a> { + causes: &'a [RootCause], +} + +impl<'a> CauseList<'a> { + pub fn new(causes: &'a [RootCause]) -> Self { + Self { causes } + } + + pub fn render(&self) -> String { + if self.causes.is_empty() { + return String::new(); + } + + let mut output = String::new(); + let palette = ColorPalette::default(); + + output.push_str(&palette.accent_text("COMMON CAUSES\n")); + + for cause in self.causes { + let likelihood = format!("[{}]", cause.likelihood).cyan(); + output.push_str(&format!(" - {} {}\n", likelihood, cause.description)); + } + + output + } +} + /// Renders a colored budget utilization bar for Soroban resource usage. pub struct BudgetBar { label: &'static str, @@ -414,7 +449,8 @@ mod tests { error_category: "Contract".to_string(), error_code: 1, error_name: "InsufficientBalance".to_string(), - summary: "The account does not have enough balance to complete this transaction.".to_string(), + summary: "The account does not have enough balance to complete this transaction." + .to_string(), detailed_explanation: String::new(), severity: Severity::Error, root_causes: Vec::new(), @@ -448,15 +484,13 @@ mod tests { #[test] fn heatmap_renders_function_names() { - let profile = make_profile(vec![ - ResourceHotspot { - location: "transfer::invoke".to_string(), - cpu_instructions: 800_000, - cpu_percentage: 80.0, - memory_bytes: 300_000, - memory_percentage: 30.0, - }, - ]); + let profile = make_profile(vec![ResourceHotspot { + location: "transfer::invoke".to_string(), + cpu_instructions: 800_000, + cpu_percentage: 80.0, + memory_bytes: 300_000, + memory_percentage: 30.0, + }]); let output = render_heatmap(&profile); assert!(output.contains("transfer::invoke")); } @@ -470,6 +504,21 @@ mod tests { assert!(rendered.contains("does not have enough balance")); } + #[test] + fn cause_list_renders_common_causes() { + let causes = vec![RootCause { + description: "The transaction was submitted with an undersized resource budget." + .to_string(), + likelihood: "high".to_string(), + }]; + + let rendered = render_cause_list(&causes); + + assert!(rendered.contains("COMMON CAUSES")); + assert!(rendered.contains("undersized resource budget")); + assert!(rendered.contains("high")); + } + #[test] fn render_context_table_with_arguments() { let context = TransactionContext { diff --git a/crates/core/src/decode/report.rs b/crates/core/src/decode/report.rs index b221c8ed..e4141c43 100644 --- a/crates/core/src/decode/report.rs +++ b/crates/core/src/decode/report.rs @@ -1,8 +1,6 @@ - - use crate::decode::host_error::ClassifiedError; -use crate::taxonomy::loader::TaxonomyDatabase; use crate::error::PrismResult; +use crate::taxonomy::loader::TaxonomyDatabase; use crate::types::report::{DiagnosticReport, RootCause, Severity, SuggestedFix}; pub fn build_report(error: &ClassifiedError) -> PrismResult { @@ -59,3 +57,34 @@ pub fn build_report(error: &ClassifiedError) -> PrismResult { )) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::taxonomy::schema::ErrorCategory; + + #[test] + fn tier1_common_causes_surface_in_decoded_report() { + let classified = ClassifiedError { + category: ErrorCategory::Budget, + error_code: 0, + is_contract_error: false, + contract_id: None, + raw_data: serde_json::Value::Null, + }; + + let report = build_report(&classified).expect("Report should build"); + + assert!( + !report.root_causes.is_empty(), + "Decoded report should include at least one common cause" + ); + assert!( + report + .root_causes + .iter() + .any(|cause| cause.description.contains("loops")), + "Decoded report should include taxonomy common cause descriptions" + ); + } +} diff --git a/crates/core/src/taxonomy/data/budget.toml b/crates/core/src/taxonomy/data/budget.toml index bafeb07a..03f5672f 100644 --- a/crates/core/src/taxonomy/data/budget.toml +++ b/crates/core/src/taxonomy/data/budget.toml @@ -10,7 +10,7 @@ source_module = "soroban-env-host/src/budget.rs" id = "host.budget.limit_exceeded.cpu" category = "budget" code = 0 -name = "LimitExceeded" +name = "CPUExceeded" severity = "Error" since_protocol = 20 summary = "The transaction exceeded its CPU instruction budget." @@ -54,3 +54,110 @@ requires_upgrade = true related_errors = ["host.budget.limit_exceeded.memory"] source_file = "soroban-env-host/src/budget.rs" documentation_url = "https://soroban.stellar.org/docs/fundamentals/fees-and-metering" + +[[errors]] +id = "host.budget.insufficient_instructions" +category = "budget" +code = 1 +name = "InsufficientInstructions" +severity = "Error" +since_protocol = 20 +summary = "The transaction did not have enough CPU instructions allocated." +detailed_explanation = """ +The transaction's resource declaration did not reserve enough CPU instructions for the contract +invocation to complete. This usually happens when submitted limits are stale, manually edited, or +lower than the values returned by simulation. +""" +related_errors = ["host.budget.limit_exceeded.cpu"] +source_file = "soroban-env-host/src/budget.rs" +documentation_url = "https://soroban.stellar.org/docs/fundamentals/fees-and-metering" + +[[errors.common_causes]] +description = "The transaction was submitted with CPU limits lower than the simulated requirement" +likelihood = "high" + +[[errors.common_causes]] +description = "Contract logic became more expensive after the transaction was prepared" +likelihood = "medium" + +[[errors.suggested_fixes]] +description = "Re-run simulation and submit the transaction with the updated CPU instruction limit" +difficulty = "easy" +requires_upgrade = false + +[[errors.suggested_fixes]] +description = "Reduce the amount of work performed by the contract invocation" +difficulty = "medium" +requires_upgrade = true + +[[errors]] +id = "host.budget.insufficient_memory" +category = "budget" +code = 2 +name = "InsufficientMemory" +severity = "Error" +since_protocol = 20 +summary = "The transaction did not have enough memory allocated." +detailed_explanation = """ +The transaction's resource declaration did not reserve enough memory for the host objects, contract +data, or intermediate values created during execution. +""" +related_errors = ["host.budget.limit_exceeded.cpu"] +source_file = "soroban-env-host/src/budget.rs" +documentation_url = "https://soroban.stellar.org/docs/fundamentals/fees-and-metering" + +[[errors.common_causes]] +description = "The invocation builds large vectors, maps, bytes, or strings in memory" +likelihood = "high" + +[[errors.common_causes]] +description = "A cross-contract call returned more data than the caller budgeted for" +likelihood = "medium" + +[[errors.suggested_fixes]] +description = "Re-run simulation and submit the transaction with the updated memory limit" +difficulty = "easy" +requires_upgrade = false + +[[errors.suggested_fixes]] +description = "Split large inputs or outputs into smaller contract calls" +difficulty = "medium" +requires_upgrade = true + +[[errors]] +id = "host.budget.exceeded_limit" +category = "budget" +code = 8 +name = "ExceededLimit" +severity = "Error" +since_protocol = 20 +summary = "The transaction exceeded an allocated resource limit." +detailed_explanation = """ +The host stopped execution because one of the metered resource limits was exceeded. The exact +resource may depend on the diagnostic events and the resource profile for the transaction. +""" +related_errors = [ + "host.budget.limit_exceeded.cpu", + "host.budget.insufficient_instructions", + "host.budget.insufficient_memory", +] +source_file = "soroban-env-host/src/budget.rs" +documentation_url = "https://soroban.stellar.org/docs/fundamentals/fees-and-metering" + +[[errors.common_causes]] +description = "The transaction used more CPU, memory, or ledger I/O than its declared resource limits allowed" +likelihood = "high" + +[[errors.common_causes]] +description = "The resource footprint was estimated before inputs or contract state changed" +likelihood = "medium" + +[[errors.suggested_fixes]] +description = "Simulate the transaction again and submit it with the returned resource limits" +difficulty = "easy" +requires_upgrade = false + +[[errors.suggested_fixes]] +description = "Inspect the resource profile to identify the expensive contract call" +difficulty = "medium" +requires_upgrade = false diff --git a/crates/core/src/taxonomy/data/context.toml b/crates/core/src/taxonomy/data/context.toml index 5955e4a2..26c4ab90 100644 --- a/crates/core/src/taxonomy/data/context.toml +++ b/crates/core/src/taxonomy/data/context.toml @@ -6,6 +6,39 @@ name = "Context" description = "Errors triggered by invalid execution context, such as calling host functions outside of a valid invocation or re-entrancy violations." source_module = "soroban-env-host/src/host.rs" +[[errors]] +id = "host.context.unknown_error" +category = "context" +code = 0 +name = "UnknownError" +severity = "Error" +since_protocol = 20 +summary = "An unexpected Soroban runtime error occurred." +detailed_explanation = """ +The host returned a context error that does not map to a more specific execution-context failure. +This can indicate malformed transaction data, an unsupported host state, or a platform issue. +""" +related_errors = ["host.context.internal_error"] +source_file = "soroban-env-host/src/host.rs" + +[[errors.common_causes]] +description = "The transaction reached a host state that Prism cannot classify more specifically" +likelihood = "medium" + +[[errors.common_causes]] +description = "Malformed or unsupported transaction data triggered a generic context failure" +likelihood = "medium" + +[[errors.suggested_fixes]] +description = "Check diagnostic events and transaction metadata for the host operation that failed" +difficulty = "medium" +requires_upgrade = false + +[[errors.suggested_fixes]] +description = "Reproduce the transaction on the same network and report it if the error persists" +difficulty = "medium" +requires_upgrade = false + [[errors]] id = "host.context.invalid_action" category = "context" diff --git a/crates/core/src/taxonomy/data/storage.toml b/crates/core/src/taxonomy/data/storage.toml index e3aea1b3..c72986ea 100644 --- a/crates/core/src/taxonomy/data/storage.toml +++ b/crates/core/src/taxonomy/data/storage.toml @@ -40,3 +40,102 @@ requires_upgrade = false related_errors = ["host.storage.missing_key"] source_file = "soroban-env-host/src/storage.rs" + +[[errors]] +id = "host.storage.entry_not_found" +category = "storage" +code = 1 +name = "EntryNotFound" +severity = "Error" +since_protocol = 20 +summary = "The contract attempted to read a ledger key that does not exist or has been archived." +detailed_explanation = """ +The host could not load the requested ledger entry. The key may never have been written, may have +expired from live state, or may need restoration before the contract can access it. +""" +related_errors = ["host.storage.access_denied"] +source_file = "soroban-env-host/src/storage.rs" + +[[errors.common_causes]] +description = "The contract read a key before any transaction created it" +likelihood = "high" + +[[errors.common_causes]] +description = "The ledger entry expired or was archived before this transaction ran" +likelihood = "medium" + +[[errors.suggested_fixes]] +description = "Create or initialize the missing ledger entry before reading it" +difficulty = "easy" +requires_upgrade = false + +[[errors.suggested_fixes]] +description = "Restore archived contract data before submitting the transaction" +difficulty = "medium" +requires_upgrade = false + +[[errors]] +id = "host.storage.exceeded_limit" +category = "storage" +code = 2 +name = "ExceededLimit" +severity = "Error" +since_protocol = 20 +summary = "The operation attempted to exceed resource or size limits for ledger data." +detailed_explanation = """ +The storage operation would exceed a protocol limit for ledger data, such as entry size, footprint +size, or the amount of data read or written by the transaction. +""" +related_errors = ["host.budget.exceeded_limit"] +source_file = "soroban-env-host/src/storage.rs" + +[[errors.common_causes]] +description = "The contract attempted to write a ledger entry larger than the protocol allows" +likelihood = "high" + +[[errors.common_causes]] +description = "The transaction touched more ledger entries than its footprint or limits allowed" +likelihood = "medium" + +[[errors.suggested_fixes]] +description = "Reduce the size of stored values or split the data across smaller entries" +difficulty = "medium" +requires_upgrade = true + +[[errors.suggested_fixes]] +description = "Break the storage update into multiple smaller transactions" +difficulty = "medium" +requires_upgrade = true + +[[errors]] +id = "host.storage.internal_error" +category = "storage" +code = 3 +name = "InternalError" +severity = "Error" +since_protocol = 20 +summary = "An internal storage failure occurred while accessing contract data." +detailed_explanation = """ +The storage layer reported an unexpected internal failure. This code alone is not enough to identify +the root issue; diagnostic events and transaction context usually provide the next clue. +""" +related_errors = [] +source_file = "soroban-env-host/src/storage.rs" + +[[errors.common_causes]] +description = "The host encountered an unexpected storage state while loading or writing ledger data" +likelihood = "medium" + +[[errors.common_causes]] +description = "The transaction hit a storage edge case that needs diagnostic events to identify" +likelihood = "medium" + +[[errors.suggested_fixes]] +description = "Inspect diagnostic events for the ledger key or storage operation that failed" +difficulty = "medium" +requires_upgrade = false + +[[errors.suggested_fixes]] +description = "Retry on a fresh ledger state or report the transaction if the failure is reproducible" +difficulty = "medium" +requires_upgrade = false diff --git a/crates/core/src/taxonomy/data/value.toml b/crates/core/src/taxonomy/data/value.toml index e1968ca6..7b0e0e89 100644 --- a/crates/core/src/taxonomy/data/value.toml +++ b/crates/core/src/taxonomy/data/value.toml @@ -1,5 +1,5 @@ # Value Error Taxonomy -# Category: Value — errors related to SCVal type conversions and validation. +# Category: Value - errors related to SCVal type conversions and validation. [category] name = "Value" @@ -7,25 +7,26 @@ description = "Errors triggered when converting, validating, or operating on Sor source_module = "soroban-env-host/src/host/conversion.rs" [[errors]] -id = "host.value.invalid_input" +id = "host.value.unknown_error" category = "value" code = 0 -name = "InvalidInput" +name = "UnknownError" severity = "Error" since_protocol = 20 -summary = "An invalid value was passed to a host function." +summary = "Invalid value: a host function received an argument of the wrong type or format." detailed_explanation = """ -Host functions expect values of specific types. When a value of the wrong type or an invalid \ -representation is passed, this error is raised. Common examples include passing a negative \ -number where an unsigned value is expected, or passing a malformed byte array. +The host could not classify the value failure more specifically. This usually means a value passed +to a host function could not be converted, decoded, or validated against the expected shape. """ +related_errors = ["host.value.unexpected_type", "host.value.invalid_input"] +source_file = "soroban-env-host/src/host/conversion.rs" [[errors.common_causes]] -description = "Type mismatch between contract code arguments and host function expectations" +description = "Type mismatch between contract call arguments and host function expectations" likelihood = "high" [[errors.common_causes]] -description = "Malformed or corrupted data passed from contract storage" +description = "Malformed or corrupted data was read from contract storage" likelihood = "medium" [[errors.suggested_fixes]] @@ -33,5 +34,233 @@ description = "Check the function signature in the contract spec and ensure argu difficulty = "easy" requires_upgrade = false -related_errors = ["host.value.unexpected_type"] +[[errors]] +id = "host.value.unexpected_type" +category = "value" +code = 1 +name = "UnexpectedType" +severity = "Error" +since_protocol = 20 +summary = "The provided value is not of the expected type for this operation." +detailed_explanation = """ +A host function or contract ABI expected one Soroban value type but received another, such as a +symbol where an address was expected or a map where a vector was expected. +""" +related_errors = ["host.value.unknown_error", "host.value.invalid_input"] +source_file = "soroban-env-host/src/host/conversion.rs" + +[[errors.common_causes]] +description = "A caller passed an argument type that does not match the contract function signature" +likelihood = "high" + +[[errors.common_causes]] +description = "Stored contract data was decoded using a newer or different expected type" +likelihood = "medium" + +[[errors.suggested_fixes]] +description = "Compare the submitted ScVal arguments with the contract spec" +difficulty = "easy" +requires_upgrade = false + +[[errors.suggested_fixes]] +description = "Migrate or rewrite stored data so it matches the current contract type" +difficulty = "medium" +requires_upgrade = true + +[[errors]] +id = "host.value.unexpected_size" +category = "value" +code = 2 +name = "UnexpectedSize" +severity = "Error" +since_protocol = 20 +summary = "The provided value has an unexpected size or length." +detailed_explanation = """ +The host received a value with a length, byte count, or collection size that does not match the +operation's requirements. +""" +related_errors = ["host.value.invalid_input"] +source_file = "soroban-env-host/src/host/conversion.rs" + +[[errors.common_causes]] +description = "A byte array, signature, key, or hash has the wrong length" +likelihood = "high" + +[[errors.common_causes]] +description = "A collection argument contains too many or too few elements" +likelihood = "medium" + +[[errors.suggested_fixes]] +description = "Validate byte lengths and collection sizes before building the transaction" +difficulty = "easy" +requires_upgrade = false + +[[errors.suggested_fixes]] +description = "Update the caller to encode fixed-size values using the exact expected length" +difficulty = "medium" +requires_upgrade = false + +[[errors]] +id = "host.value.missing_value" +category = "value" +code = 3 +name = "MissingValue" +severity = "Error" +since_protocol = 20 +summary = "A required value was missing from the input." +detailed_explanation = """ +The host expected a value to be present but found no value, such as a missing map key, absent +optional argument, or missing decoded field. +""" +related_errors = ["host.value.invalid_input"] +source_file = "soroban-env-host/src/host/conversion.rs" + +[[errors.common_causes]] +description = "A required contract argument or map entry was omitted" +likelihood = "high" + +[[errors.common_causes]] +description = "The caller encoded an optional value as missing when the contract required it" +likelihood = "medium" + +[[errors.suggested_fixes]] +description = "Include the missing argument or field when constructing the contract call" +difficulty = "easy" +requires_upgrade = false + +[[errors.suggested_fixes]] +description = "Add input validation before submitting the transaction" +difficulty = "easy" +requires_upgrade = false + +[[errors]] +id = "host.value.internal_error" +category = "value" +code = 4 +name = "InternalError" +severity = "Fatal" +since_protocol = 20 +summary = "An internal host error occurred while processing a value." +detailed_explanation = """ +The host encountered an unexpected internal state while converting, validating, or handling a +Soroban value. +""" +related_errors = ["host.context.internal_error"] +source_file = "soroban-env-host/src/host/conversion.rs" + +[[errors.common_causes]] +description = "The host hit an unexpected value conversion or validation state" +likelihood = "medium" + +[[errors.common_causes]] +description = "Malformed input exposed a host-side edge case" +likelihood = "medium" + +[[errors.suggested_fixes]] +description = "Inspect diagnostic events for the value conversion that failed" +difficulty = "medium" +requires_upgrade = false + +[[errors.suggested_fixes]] +description = "Report a reproducible transaction with the full input values" +difficulty = "medium" +requires_upgrade = false + +[[errors]] +id = "host.value.added_value" +category = "value" +code = 5 +name = "AddedValue" +severity = "Error" +since_protocol = 20 +summary = "An unexpected or disallowed value was provided." +detailed_explanation = """ +The host received an extra value in a place where no additional value was allowed, such as an +unexpected field, argument, or authorization payload component. +""" +related_errors = ["host.value.invalid_input"] +source_file = "soroban-env-host/src/host/conversion.rs" + +[[errors.common_causes]] +description = "The caller supplied extra arguments or fields that the contract does not accept" +likelihood = "high" + +[[errors.common_causes]] +description = "A generated transaction builder included stale data from an older contract interface" +likelihood = "medium" + +[[errors.suggested_fixes]] +description = "Remove unexpected arguments or fields from the contract call" +difficulty = "easy" +requires_upgrade = false + +[[errors.suggested_fixes]] +description = "Regenerate client bindings from the current contract spec" +difficulty = "easy" +requires_upgrade = false + +[[errors]] +id = "host.value.invalid_input" +category = "value" +code = 6 +name = "InvalidInput" +severity = "Error" +since_protocol = 20 +summary = "Malformed ScVal conversion, out-of-range integer, or otherwise invalid input passed to a host function." +detailed_explanation = """ +The input value had the right general shape but failed validation, conversion, or range checks +required by the host function. +""" +related_errors = ["host.value.unexpected_type", "host.value.unexpected_size"] source_file = "soroban-env-host/src/host/conversion.rs" + +[[errors.common_causes]] +description = "A ScVal was malformed or encoded with a value outside the allowed range" +likelihood = "high" + +[[errors.common_causes]] +description = "The caller passed contract input produced by an incompatible SDK or serializer" +likelihood = "medium" + +[[errors.suggested_fixes]] +description = "Rebuild the transaction arguments with the current SDK and contract spec" +difficulty = "easy" +requires_upgrade = false + +[[errors.suggested_fixes]] +description = "Validate numeric ranges, addresses, and encoded values before submission" +difficulty = "easy" +requires_upgrade = false + +[[errors]] +id = "host.value.authentication_error" +category = "value" +code = 7 +name = "AuthenticationError" +severity = "Error" +since_protocol = 20 +summary = "An authentication-related value was invalid or malformed." +detailed_explanation = """ +The host rejected a value used during authorization, such as an auth payload, signer value, nonce, +or credential component. +""" +related_errors = ["host.auth.invalid_signature", "host.auth.missing_auth"] +source_file = "soroban-env-host/src/host/conversion.rs" + +[[errors.common_causes]] +description = "Authorization payload values do not match the invocation being authorized" +likelihood = "high" + +[[errors.common_causes]] +description = "Signer, nonce, or credential data was malformed before signature verification" +likelihood = "medium" + +[[errors.suggested_fixes]] +description = "Regenerate the auth entries from the exact invocation being submitted" +difficulty = "easy" +requires_upgrade = false + +[[errors.suggested_fixes]] +description = "Check signer, nonce, and credential encoding before collecting signatures" +difficulty = "medium" +requires_upgrade = false diff --git a/crates/core/src/taxonomy/loader.rs b/crates/core/src/taxonomy/loader.rs index 7fa83fd6..99695dfd 100644 --- a/crates/core/src/taxonomy/loader.rs +++ b/crates/core/src/taxonomy/loader.rs @@ -1,29 +1,23 @@ - - -use crate::taxonomy::schema::{ErrorCategory, TaxonomyEntry, TaxonomySchema}; use crate::error::{PrismError, PrismResult}; +use crate::taxonomy::schema::{ErrorCategory, TaxonomyEntry, TaxonomySchema}; use std::collections::HashMap; pub struct TaxonomyParser; impl TaxonomyParser { - pub fn parse(input: &str) -> PrismResult { - toml::from_str(input).map_err(|e| { - PrismError::TaxonomyError(format!("TOML parse error: {e}")) - }) + toml::from_str(input) + .map_err(|e| PrismError::TaxonomyError(format!("TOML parse error: {e}"))) } } pub struct TaxonomyDatabase { - entries: HashMap<(ErrorCategory, u32), TaxonomyEntry>, all_entries: Vec, } impl TaxonomyDatabase { - pub fn load_embedded() -> PrismResult { let mut db = Self { entries: HashMap::new(), @@ -105,6 +99,10 @@ impl TaxonomyDatabase { .collect() } + pub fn all_entries(&self) -> &[TaxonomyEntry] { + &self.all_entries + } + pub fn len(&self) -> usize { self.entries.len() } @@ -153,4 +151,99 @@ mod tests { let result = TaxonomyParser::parse(toml); assert!(result.is_err()); } + + #[test] + fn tier1_entries_with_fixes_have_common_causes() { + let db = TaxonomyDatabase::load_embedded().expect("Taxonomy should load"); + assert!(!db.is_empty(), "Taxonomy should contain entries"); + + for entry in db.all_entries() { + if entry.suggested_fixes.is_empty() { + continue; + } + + assert!( + !entry.common_causes.is_empty(), + "{} has suggested fixes but no common causes", + entry.id + ); + assert!( + entry.common_causes.len() <= 3, + "{} has more than three common causes", + entry.id + ); + assert!( + entry + .common_causes + .iter() + .all(|cause| !cause.description.trim().is_empty()), + "{} has an empty common cause description", + entry.id + ); + } + } + + #[test] + fn taxonomy_covers_tier1_static_mapping_codes() { + let db = TaxonomyDatabase::load_embedded().expect("Taxonomy should load"); + + let expected_codes = [ + ( + ErrorCategory::Budget, + crate::decode::mappings::budget::BUDGET_ERROR_DETAILS + .iter() + .map(|detail| (detail.code, detail.name)) + .collect::>(), + ), + ( + ErrorCategory::Storage, + crate::decode::mappings::storage::STORAGE_ERROR_DETAILS + .iter() + .map(|detail| (detail.code, detail.name)) + .collect::>(), + ), + ( + ErrorCategory::Auth, + crate::decode::mappings::auth::AUTH_ERROR_DETAILS + .iter() + .map(|detail| (detail.code, detail.name)) + .collect::>(), + ), + ( + ErrorCategory::Context, + crate::decode::mappings::context::CONTEXT_ERROR_DETAILS + .iter() + .map(|detail| (detail.code, detail.name)) + .collect::>(), + ), + ( + ErrorCategory::Value, + crate::decode::mappings::value::VALUE_ERROR_DETAILS + .iter() + .map(|detail| (detail.code, detail.name)) + .collect::>(), + ), + ]; + + for (category, details) in expected_codes { + for (code, name) in details { + let entry = db.lookup(&category, code).unwrap_or_else(|| { + panic!("Missing taxonomy entry for {category} code {code} ({name})") + }); + + assert_eq!( + entry.name, name, + "Taxonomy entry name for {category} code {code} should match static mapping" + ); + assert!( + !entry.common_causes.is_empty(), + "Taxonomy entry for {category} code {code} ({name}) has no common causes" + ); + assert!( + entry.common_causes.len() <= 3, + "Taxonomy entry for {category} code {code} ({name}) has more than three common causes" + ); + } + } + } } diff --git a/crates/core/src/taxonomy/schema.rs b/crates/core/src/taxonomy/schema.rs index d59cce2f..fb41fa04 100644 --- a/crates/core/src/taxonomy/schema.rs +++ b/crates/core/src/taxonomy/schema.rs @@ -1,10 +1,7 @@ - - use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TaxonomyEntry { - pub id: String, pub category: ErrorCategory, @@ -23,10 +20,13 @@ pub struct TaxonomyEntry { pub detailed_explanation: String, + #[serde(default)] pub common_causes: Vec, + #[serde(default)] pub suggested_fixes: Vec, + #[serde(default)] pub related_errors: Vec, pub source_file: Option, @@ -38,7 +38,6 @@ pub struct TaxonomyEntry { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TaxonomyCause { - pub description: String, pub likelihood: String, @@ -46,7 +45,6 @@ pub struct TaxonomyCause { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TaxonomyFix { - pub description: String, pub difficulty: String,