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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions crates/cli/src/output/human.rs
Original file line number Diff line number Diff line change
@@ -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));
Expand Down Expand Up @@ -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));
Expand Down
89 changes: 69 additions & 20 deletions crates/cli/src/output/renderers.rs
Original file line number Diff line number Diff line change
@@ -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] = ["░", "▒", "▓", "█"];
Expand All @@ -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()
}
Expand Down Expand Up @@ -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();
Expand All @@ -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));
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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"));
}
Expand All @@ -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 {
Expand Down
35 changes: 32 additions & 3 deletions crates/core/src/decode/report.rs
Original file line number Diff line number Diff line change
@@ -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<DiagnosticReport> {
Expand Down Expand Up @@ -59,3 +57,34 @@ pub fn build_report(error: &ClassifiedError) -> PrismResult<DiagnosticReport> {
))
}
}

#[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"
);
}
}
47 changes: 38 additions & 9 deletions crates/core/src/taxonomy/loader.rs
Original file line number Diff line number Diff line change
@@ -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<TaxonomySchema> {
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<TaxonomyEntry>,
}

impl TaxonomyDatabase {

pub fn load_embedded() -> PrismResult<Self> {
let mut db = Self {
entries: HashMap::new(),
Expand Down Expand Up @@ -105,6 +99,10 @@ impl TaxonomyDatabase {
.collect()
}

pub fn all_entries(&self) -> &[TaxonomyEntry] {
&self.all_entries
}

pub fn len(&self) -> usize {
self.entries.len()
}
Expand Down Expand Up @@ -153,4 +151,35 @@ 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
);
}
}
}
8 changes: 3 additions & 5 deletions crates/core/src/taxonomy/schema.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@


use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxonomyEntry {

pub id: String,

pub category: ErrorCategory,
Expand All @@ -23,10 +20,13 @@ pub struct TaxonomyEntry {

pub detailed_explanation: String,

#[serde(default)]
pub common_causes: Vec<TaxonomyCause>,

#[serde(default)]
pub suggested_fixes: Vec<TaxonomyFix>,

#[serde(default)]
pub related_errors: Vec<String>,

pub source_file: Option<String>,
Expand All @@ -38,15 +38,13 @@ pub struct TaxonomyEntry {

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxonomyCause {

pub description: String,

pub likelihood: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxonomyFix {

pub description: String,

pub difficulty: String,
Expand Down