1use logicpearl_core::{ArtifactRenderer, Result};
10use logicpearl_ir::{LogicPearlGateIr, RuleVerificationStatus};
11use owo_colors::OwoColorize;
12
13pub struct TextInspector;
14
15impl ArtifactRenderer<LogicPearlGateIr> for TextInspector {
16 fn render(&self, gate: &LogicPearlGateIr) -> Result<String> {
17 let mut lines = Vec::new();
18
19 lines.push(format!(
21 "{} {}",
22 "━━ Gate:".bold(),
23 gate.gate_id.bold().bright_green()
24 ));
25 lines.push(format!(
26 " {} {}",
27 "IR version".bright_black(),
28 gate.ir_version
29 ));
30 lines.push(format!(
31 " {} {}",
32 "Features".bright_black(),
33 gate.input_schema.features.len()
34 ));
35 lines.push(format!(" {} {}", "Rules".bright_black(), gate.rules.len()));
36
37 if let Some(verification) = &gate.verification {
38 if let Some(scope) = &verification.correctness_scope {
39 lines.push(format!(
40 " {} {}",
41 "Correctness scope".bright_black(),
42 scope
43 ));
44 }
45 }
46
47 let semantic_features = gate
48 .input_schema
49 .features
50 .iter()
51 .filter(|feature| feature.semantics.is_some())
52 .count();
53 if semantic_features > 0 {
54 lines.push(format!(
55 " {} {}",
56 "Feature dictionary".bright_black(),
57 semantic_features
58 ));
59 }
60
61 lines.push(String::new());
63 lines.push(format!("{}", "━━ Rules ━━".bold()));
64
65 let rule_count = gate.rules.len();
66 for (i, rule) in gate.rules.iter().enumerate() {
67 let is_last = i == rule_count - 1;
68 let branch = if is_last { "└─" } else { "├─" };
69 let continuation = if is_last { " " } else { "│ " };
70
71 let (symbol, status_text) = match &rule.verification_status {
72 Some(RuleVerificationStatus::SolverVerified) => (
73 format!("{}", "✓".green()),
74 format!("{}", "solver_verified".green()),
75 ),
76 Some(RuleVerificationStatus::PipelineUnverified) => (
77 format!("{}", "⚠".yellow()),
78 format!("{}", "pipeline_unverified".yellow()),
79 ),
80 Some(RuleVerificationStatus::HeuristicUnverified) => (
81 format!("{}", "⚠".yellow()),
82 format!("{}", "heuristic_unverified".yellow()),
83 ),
84 Some(RuleVerificationStatus::RefinedUnverified) => (
85 format!("{}", "⚠".yellow()),
86 format!("{}", "refined_unverified".yellow()),
87 ),
88 None => (format!("{}", "✗".red()), format!("{}", "unknown".red())),
89 };
90
91 lines.push(format!(
92 " {} {} {} {} {} {}",
93 branch.bright_black(),
94 format!("bit {}", rule.bit).bright_cyan(),
95 rule.id.bold(),
96 "→".bright_black(),
97 symbol,
98 status_text,
99 ));
100
101 if let Some(label) = &rule.label {
102 lines.push(format!(
103 " {} {} {}",
104 continuation.bright_black(),
105 "label:".bright_black(),
106 label
107 ));
108 }
109 if let Some(message) = &rule.message {
110 lines.push(format!(
111 " {} {} {}",
112 continuation.bright_black(),
113 "message:".bright_black(),
114 message
115 ));
116 }
117 if let Some(hint) = &rule.counterfactual_hint {
118 lines.push(format!(
119 " {} {} {}",
120 continuation.bright_black(),
121 "counterfactual:".bright_black(),
122 hint
123 ));
124 }
125 }
126
127 Ok(lines.join("\n"))
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::TextInspector;
134 use logicpearl_core::ArtifactRenderer;
135 use logicpearl_ir::{
136 CombineStrategy, ComparisonExpression, ComparisonOperator, ComparisonValue,
137 EvaluationConfig, Expression, FeatureDefinition, FeatureType, GateType, InputSchema,
138 LogicPearlGateIr, RuleDefinition, RuleKind, RuleVerificationStatus,
139 };
140
141 #[test]
142 fn renders_backend_neutral_solver_verified_status() {
143 owo_colors::set_override(false);
145
146 let gate = LogicPearlGateIr {
147 ir_version: "1.0".to_string(),
148 gate_id: "demo_gate".to_string(),
149 gate_type: GateType::BitmaskGate,
150 input_schema: InputSchema {
151 features: vec![FeatureDefinition {
152 id: "f_age".to_string(),
153 feature_type: FeatureType::Int,
154 description: Some("age".to_string()),
155 values: None,
156 min: None,
157 max: None,
158 editable: None,
159 semantics: None,
160 governance: None,
161 derived: None,
162 }],
163 },
164 rules: vec![RuleDefinition {
165 id: "rule_000".to_string(),
166 kind: RuleKind::Predicate,
167 bit: 0,
168 deny_when: Expression::Comparison(ComparisonExpression {
169 feature: "f_age".to_string(),
170 op: ComparisonOperator::Lt,
171 value: ComparisonValue::FeatureRef {
172 feature_ref: "f_age".to_string(),
173 },
174 }),
175 label: None,
176 message: None,
177 severity: None,
178 counterfactual_hint: None,
179 verification_status: Some(RuleVerificationStatus::SolverVerified),
180 }],
181 evaluation: EvaluationConfig {
182 combine: CombineStrategy::BitwiseOr,
183 allow_when_bitmask: 0,
184 },
185 verification: None,
186 provenance: None,
187 };
188
189 let rendered = TextInspector
190 .render(&gate)
191 .expect("text inspector should render a simple gate");
192 assert!(
193 rendered.contains("solver_verified"),
194 "should contain solver_verified: {rendered}"
195 );
196 assert!(
197 rendered.contains("✓"),
198 "should contain check mark: {rendered}"
199 );
200 assert!(
201 rendered.contains("demo_gate"),
202 "should contain gate id: {rendered}"
203 );
204 assert!(!rendered.contains("z3_verified"));
205 }
206}