Skip to main content

logicpearl_render/
lib.rs

1// SPDX-License-Identifier: MIT
2//! Human-readable rendering for LogicPearl artifacts.
3//!
4//! This crate converts validated artifact IR into terminal-oriented text for
5//! inspection. It intentionally renders existing metadata and rules instead
6//! of inferring domain meaning that should have been supplied by feature
7//! dictionaries or integrations.
8
9use 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        // Gate header
20        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        // Rules section
62        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        // Disable colors so assertions can match plain text.
144        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}