1use std::collections::BTreeMap;
19
20use nibli_protocol::{ProofRule, ProofTrace};
21
22use crate::fact::humanize_fact;
23use crate::overlay::DomainGloss;
24use crate::proof::{CWA_FALSE_NOTE, NAF_NOTE, RenderedNode, build_node, humanize_rule_label};
25use crate::register::Register;
26use crate::summary::{
27 LeafKey, fact_to_english, parse_raw_fact, regroup_event_leaves, render_group, rule_to_english,
28};
29use crate::term::{is_event_skolem_arg, role_base, role_index};
30
31const MAX_DEPTH: usize = 64;
34
35pub fn collapse_proof(trace: &ProofTrace, register: Register) -> RenderedNode {
38 if trace.steps.is_empty() {
39 return build_node(trace, trace.root);
40 }
41 let mut steps = collapse_to_macrosteps(trace, register);
42 match steps.len() {
43 1 => step_to_rendered(&steps.pop().unwrap()),
44 0 => build_node(trace, trace.root),
45 _ => {
46 let nodes: Vec<RenderedNode> = steps.iter().map(step_to_rendered).collect();
47 let holds = nodes.iter().all(|n| n.holds);
48 RenderedNode {
49 icon: "∧",
50 label: "all of the following hold".to_string(),
51 css_class: "proof-conjunction",
52 holds,
53 is_leaf: false,
54 inline: false,
55 children: nodes,
56 }
57 }
58 }
59}
60
61#[derive(Clone)]
68pub(crate) struct MacroStep {
69 pub statement: String,
70 pub kind: MacroKind,
71 pub holds: bool,
72 pub premises: Vec<MacroStep>,
73 pub role_detail: Option<RenderedNode>,
75}
76
77#[derive(Clone)]
79pub(crate) enum MacroKind {
80 Given,
81 Derived(String),
83 Computed(String),
88 Checked,
89 NotDerivable,
90 Reference,
92 Raw(Box<RenderedNode>),
95}
96
97pub(crate) fn collapse_to_macrosteps(trace: &ProofTrace, register: Register) -> Vec<MacroStep> {
100 if trace.steps.is_empty() {
101 return Vec::new();
102 }
103 let mut seen: Vec<String> = Vec::new();
104 let leaves = collect_goal_leaves(trace, trace.root, 0);
105 collapse_goal_steps(trace, &leaves, register, &mut seen, 0)
106}
107
108fn computed_label(method: &str) -> &'static str {
116 match method {
117 "backend" => "computed (trusted backend)",
118 "arithmetic" | "numeric" => "computed (local)",
119 _ => "computed",
120 }
121}
122
123fn step_to_rendered(step: &MacroStep) -> RenderedNode {
126 match &step.kind {
127 MacroKind::Raw(node) => (**node).clone(),
128 MacroKind::Reference => reference_node(step.statement.clone()),
129 MacroKind::Given => macro_leaf(
130 &step.statement,
131 "given",
132 "▣",
133 "proof-asserted",
134 step.holds,
135 step.role_detail.clone(),
136 ),
137 MacroKind::Computed(method) => macro_leaf(
138 &step.statement,
139 computed_label(method),
140 "⊢",
141 "proof-check",
142 step.holds,
143 step.role_detail.clone(),
144 ),
145 MacroKind::Checked => macro_leaf(
146 &step.statement,
147 "checked",
148 "⊢",
149 "proof-check",
150 step.holds,
151 step.role_detail.clone(),
152 ),
153 MacroKind::NotDerivable => macro_leaf(
154 &step.statement,
155 "not derivable",
156 "✗",
157 "proof-failed",
158 false,
159 step.role_detail.clone(),
160 ),
161 MacroKind::Derived(label) => {
162 let just = rule_justification(label);
163 let mut children: Vec<RenderedNode> =
164 step.premises.iter().map(step_to_rendered).collect();
165 if let Some(rd) = &step.role_detail {
166 children.push(rd.clone());
167 }
168 RenderedNode {
169 icon: "⊢",
170 label: format!("{} [{just}]", step.statement),
171 css_class: "proof-derived",
172 holds: step.holds,
173 is_leaf: children.is_empty(),
174 inline: false,
175 children,
176 }
177 }
178 }
179}
180
181pub fn collapse_proof_with(
184 trace: &ProofTrace,
185 register: Register,
186 overlay: Option<&'static DomainGloss>,
187) -> RenderedNode {
188 crate::overlay::with_overlay(overlay, || collapse_proof(trace, register))
189}
190
191pub fn render_collapsed_text(
197 trace: &ProofTrace,
198 register: Register,
199 base_indent: usize,
200 include_detail: bool,
201) -> String {
202 let mut out = String::new();
203 if trace.naf_dependent {
204 out.push_str(NAF_NOTE);
205 out.push('\n');
206 }
207 if trace.cwa_false {
208 out.push_str(CWA_FALSE_NOTE);
209 out.push('\n');
210 }
211 let node = collapse_proof(trace, register);
212 out.push_str(&render_node_text(&node, base_indent, include_detail));
213 out
214}
215
216pub fn render_collapsed_text_with(
219 trace: &ProofTrace,
220 register: Register,
221 base_indent: usize,
222 include_detail: bool,
223 overlay: Option<&'static DomainGloss>,
224) -> String {
225 crate::overlay::with_overlay(overlay, || {
226 render_collapsed_text(trace, register, base_indent, include_detail)
227 })
228}
229
230pub fn render_node_text(node: &RenderedNode, base_indent: usize, include_detail: bool) -> String {
234 let mut out = String::new();
235 write_node_text(node, base_indent, include_detail, &mut out);
236 out
237}
238
239fn write_node_text(node: &RenderedNode, indent: usize, include_detail: bool, out: &mut String) {
240 if !include_detail && node.css_class == "proof-role-detail" {
241 return;
242 }
243 for _ in 0..indent {
244 out.push_str(" ");
245 }
246 if node.inline {
247 out.push_str(&format!("{} {}\n", node.icon, node.label));
248 } else {
249 let tag = if node.holds { "TRUE" } else { "FALSE" };
250 out.push_str(&format!("{} {} -> {}\n", node.icon, node.label, tag));
251 }
252 for child in &node.children {
253 write_node_text(child, indent + 1, include_detail, out);
254 }
255}
256
257fn collect_goal_leaves(trace: &ProofTrace, idx: u32, depth: usize) -> Vec<u32> {
262 if depth > MAX_DEPTH {
263 return vec![idx];
264 }
265 match trace.steps.get(idx as usize).map(|s| &s.rule) {
266 Some(
267 ProofRule::ExistsWitness { .. }
268 | ProofRule::Conjunction
269 | ProofRule::ModalPassthrough { .. },
270 ) => {
271 let mut out = Vec::new();
272 for &c in &trace.steps[idx as usize].children {
273 out.extend(collect_goal_leaves(trace, c, depth + 1));
274 }
275 out
276 }
277 _ => vec![idx],
278 }
279}
280
281fn collapse_goal_steps(
284 trace: &ProofTrace,
285 goals: &[u32],
286 register: Register,
287 seen: &mut Vec<String>,
288 depth: usize,
289) -> Vec<MacroStep> {
290 if depth > MAX_DEPTH {
291 return goals
292 .iter()
293 .map(|&g| raw_step(build_node(trace, g)))
294 .collect();
295 }
296 let mut order: Vec<LeafKey> = Vec::new();
297 let mut buckets: BTreeMap<LeafKey, Vec<u32>> = BTreeMap::new();
298 let mut singles: Vec<u32> = Vec::new();
299 for &g in goals {
300 if let Some(key) = goal_event_key(trace, g) {
301 if !buckets.contains_key(&key) {
302 order.push(key.clone());
303 }
304 buckets.entry(key).or_default().push(g);
305 } else {
306 singles.push(g);
307 }
308 }
309 let mut out = Vec::new();
310 for key in &order {
311 out.push(build_macro_step(
312 trace,
313 &buckets[key],
314 register,
315 seen,
316 depth,
317 ));
318 }
319 for &g in &singles {
320 out.push(build_single_step(trace, g, register, seen, depth));
321 }
322 out
323}
324
325fn leaf_step(
327 statement: String,
328 kind: MacroKind,
329 holds: bool,
330 role_detail: Option<RenderedNode>,
331) -> MacroStep {
332 MacroStep {
333 statement,
334 kind,
335 holds,
336 premises: Vec::new(),
337 role_detail,
338 }
339}
340
341fn raw_step(node: RenderedNode) -> MacroStep {
343 let statement = node.label.clone();
344 let holds = node.holds;
345 MacroStep {
346 statement,
347 kind: MacroKind::Raw(Box::new(node)),
348 holds,
349 premises: Vec::new(),
350 role_detail: None,
351 }
352}
353
354fn goal_event_key(trace: &ProofTrace, g: u32) -> Option<LeafKey> {
357 let rule = &trace.steps.get(g as usize)?.rule;
358 let fact = goal_fact(rule)?;
359 let (wrapper, relation, args) = parse_raw_fact(&fact)?;
360 if let (Some(base), Some(_)) = (role_base(&relation), role_index(&relation))
361 && args.len() >= 2
362 && is_event_skolem_arg(&args[0])
363 {
364 return Some((wrapper, base.to_string(), args[0].clone()));
365 }
366 if args.len() == 1 && is_event_skolem_arg(&args[0]) {
367 return Some((wrapper, relation, args[0].clone()));
368 }
369 None
370}
371
372fn goal_fact(rule: &ProofRule) -> Option<String> {
374 match rule {
375 ProofRule::Asserted { fact }
376 | ProofRule::Derived { fact, .. }
377 | ProofRule::ProofRef { fact } => Some(fact.clone()),
378 ProofRule::PredicateCheck { detail, .. } | ProofRule::ComputeCheck { detail, .. } => {
379 Some(detail.clone())
380 }
381 ProofRule::PredicateNotFound { predicate } => Some(predicate.clone()),
382 _ => None,
383 }
384}
385
386enum GroupKind {
388 Given,
389 Derived(String),
390 Computed(String),
392 Checked,
393 NotDerivable,
394 Reference,
395}
396
397fn classify_group(trace: &ProofTrace, steps: &[u32]) -> GroupKind {
398 let mut derived: Option<String> = None;
399 let mut computed_method: Option<String> = None;
400 let (mut given, mut checked, mut notfound, mut nonref) = (false, false, false, false);
401 for &g in steps {
402 match &trace.steps[g as usize].rule {
403 ProofRule::Derived { label, .. } => {
404 derived = Some(label.clone());
405 nonref = true;
406 }
407 ProofRule::Asserted { .. } => {
408 given = true;
409 nonref = true;
410 }
411 ProofRule::ComputeCheck { method, .. } => {
412 computed_method = Some(method.clone());
413 nonref = true;
414 }
415 ProofRule::PredicateCheck { .. } => {
416 checked = true;
417 nonref = true;
418 }
419 ProofRule::PredicateNotFound { .. }
420 | ProofRule::RuleAttemptFailed { .. }
421 | ProofRule::ExistsFailed => {
422 notfound = true;
423 nonref = true;
424 }
425 ProofRule::ProofRef { .. } => {}
426 _ => nonref = true,
427 }
428 }
429 if let Some(l) = derived {
430 GroupKind::Derived(l)
431 } else if given {
432 GroupKind::Given
433 } else if let Some(m) = computed_method {
434 GroupKind::Computed(m)
435 } else if checked {
436 GroupKind::Checked
437 } else if notfound {
438 GroupKind::NotDerivable
439 } else if !nonref {
440 GroupKind::Reference
441 } else {
442 GroupKind::Given
443 }
444}
445
446fn build_macro_step(
448 trace: &ProofTrace,
449 steps: &[u32],
450 register: Register,
451 seen: &mut Vec<String>,
452 depth: usize,
453) -> MacroStep {
454 let facts: Vec<String> = steps
455 .iter()
456 .filter_map(|&g| goal_fact(&trace.steps[g as usize].rule))
457 .collect();
458 let holds = steps.iter().all(|&g| trace.steps[g as usize].holds);
459 let Some(statement) = surface_statement(&facts, register) else {
460 return raw_step(verbose_group(trace, steps, holds)); };
462 if seen.contains(&statement) {
463 return leaf_step(statement, MacroKind::Reference, true, None);
464 }
465 seen.push(statement.clone());
466
467 match classify_group(trace, steps) {
468 GroupKind::Reference => leaf_step(statement, MacroKind::Reference, true, None),
469 GroupKind::NotDerivable => leaf_step(
470 statement,
471 MacroKind::NotDerivable,
472 false,
473 role_detail(trace, steps),
474 ),
475 GroupKind::Given => leaf_step(
476 statement,
477 MacroKind::Given,
478 holds,
479 role_detail(trace, steps),
480 ),
481 GroupKind::Computed(method) => leaf_step(
482 statement,
483 MacroKind::Computed(method),
484 holds,
485 role_detail(trace, steps),
486 ),
487 GroupKind::Checked => leaf_step(
488 statement,
489 MacroKind::Checked,
490 holds,
491 role_detail(trace, steps),
492 ),
493 GroupKind::Derived(label) => {
494 let mut cond_leaves: Vec<u32> = Vec::new();
495 for &g in steps {
496 if matches!(trace.steps[g as usize].rule, ProofRule::Derived { .. }) {
497 for &c in &trace.steps[g as usize].children {
498 cond_leaves.extend(collect_goal_leaves(trace, c, depth + 1));
499 }
500 }
501 }
502 let premises = collapse_goal_steps(trace, &cond_leaves, register, seen, depth + 1);
503 MacroStep {
504 statement,
505 kind: MacroKind::Derived(label),
506 holds,
507 premises,
508 role_detail: role_detail(trace, steps),
509 }
510 }
511 }
512}
513
514fn build_single_step(
518 trace: &ProofTrace,
519 g: u32,
520 register: Register,
521 seen: &mut Vec<String>,
522 depth: usize,
523) -> MacroStep {
524 let rule = &trace.steps[g as usize].rule;
525 let holds = trace.steps[g as usize].holds;
526 match rule {
527 ProofRule::Asserted { fact } => {
528 let stmt = flat_statement(fact, register);
529 if seen.contains(&stmt) {
530 return leaf_step(stmt, MacroKind::Reference, true, None);
531 }
532 seen.push(stmt.clone());
533 leaf_step(stmt, MacroKind::Given, holds, None)
534 }
535 ProofRule::ProofRef { fact } => leaf_step(
536 flat_statement(fact, register),
537 MacroKind::Reference,
538 true,
539 None,
540 ),
541 ProofRule::PredicateNotFound { predicate } => leaf_step(
542 flat_statement(predicate, register),
543 MacroKind::NotDerivable,
544 false,
545 None,
546 ),
547 ProofRule::ComputeCheck { detail, method } => leaf_step(
548 flat_statement(detail, register),
549 MacroKind::Computed(method.clone()),
550 holds,
551 None,
552 ),
553 ProofRule::PredicateCheck { detail, .. } => leaf_step(
554 flat_statement(detail, register),
555 MacroKind::Checked,
556 holds,
557 None,
558 ),
559 ProofRule::Derived { label, fact } => {
560 let stmt = flat_statement(fact, register);
561 if seen.contains(&stmt) {
562 return leaf_step(stmt, MacroKind::Reference, true, None);
563 }
564 seen.push(stmt.clone());
565 let mut cond_leaves: Vec<u32> = Vec::new();
566 for &c in &trace.steps[g as usize].children {
567 cond_leaves.extend(collect_goal_leaves(trace, c, depth + 1));
568 }
569 let premises = collapse_goal_steps(trace, &cond_leaves, register, seen, depth + 1);
570 MacroStep {
571 statement: stmt,
572 kind: MacroKind::Derived(label.clone()),
573 holds,
574 premises,
575 role_detail: None,
576 }
577 }
578 _ => raw_step(build_node(trace, g)),
581 }
582}
583
584fn surface_statement(facts: &[String], register: Register) -> Option<String> {
588 let (groups, flat) = regroup_event_leaves(facts, register);
589 if let Some((key, pm)) = groups.first() {
590 render_group(key.0.as_deref(), &key.1, pm)
591 } else {
592 flat.first().cloned()
593 }
594}
595
596fn flat_statement(fact: &str, register: Register) -> String {
599 fact_to_english(fact, register).unwrap_or_else(|| humanize_fact(fact))
600}
601
602fn rule_justification(label: &str) -> String {
605 let humanized = humanize_rule_label(label);
606 match rule_to_english(&humanized) {
607 Some(e) => format!("by the rule: {e}"),
608 None => format!("by rule: {humanized}"),
609 }
610}
611
612fn macro_leaf(
613 statement: &str,
614 just: &str,
615 icon: &'static str,
616 css_class: &'static str,
617 holds: bool,
618 detail: Option<RenderedNode>,
619) -> RenderedNode {
620 let mut children = Vec::new();
621 if let Some(d) = detail {
622 children.push(d);
623 }
624 RenderedNode {
625 icon,
626 label: format!("{statement} [{just}]"),
627 css_class,
628 holds,
629 is_leaf: children.is_empty(),
630 inline: false,
631 children,
632 }
633}
634
635fn reference_node(statement: String) -> RenderedNode {
636 RenderedNode {
637 icon: "↑",
638 label: format!("{statement} (shown above)"),
639 css_class: "proof-ref",
640 holds: true,
641 is_leaf: true,
642 inline: true,
643 children: Vec::new(),
644 }
645}
646
647fn role_detail(trace: &ProofTrace, steps: &[u32]) -> Option<RenderedNode> {
652 if steps.len() <= 1 {
653 return None;
654 }
655 let children: Vec<RenderedNode> = steps.iter().map(|&g| build_node(trace, g)).collect();
656 Some(RenderedNode {
657 icon: "▸",
658 label: "role-level detail".to_string(),
659 css_class: "proof-role-detail",
660 holds: true,
661 is_leaf: false,
662 inline: false,
663 children,
664 })
665}
666
667fn verbose_group(trace: &ProofTrace, steps: &[u32], holds: bool) -> RenderedNode {
670 if steps.len() == 1 {
671 return build_node(trace, steps[0]);
672 }
673 RenderedNode {
674 icon: "∧",
675 label: "(detail)".to_string(),
676 css_class: "proof-conjunction",
677 holds,
678 is_leaf: false,
679 inline: false,
680 children: steps.iter().map(|&g| build_node(trace, g)).collect(),
681 }
682}
683
684#[cfg(test)]
685mod tests {
686 use super::*;
687 use nibli_protocol::ProofStep;
688
689 fn step(rule: ProofRule, holds: bool, children: Vec<u32>) -> ProofStep {
690 ProofStep {
691 rule,
692 holds,
693 children,
694 }
695 }
696
697 fn asserted(fact: &str) -> ProofRule {
698 ProofRule::Asserted {
699 fact: fact.to_string(),
700 }
701 }
702
703 fn proofref(fact: &str) -> ProofRule {
704 ProofRule::ProofRef {
705 fact: fact.to_string(),
706 }
707 }
708
709 fn derived(fact: &str) -> ProofRule {
710 ProofRule::Derived {
711 label: "dog ∧ gerku_x1 ∧ gerku_x2 → animal ∧ danlu_x1 ∧ danlu_x2".to_string(),
712 fact: fact.to_string(),
713 }
714 }
715
716 fn syllogism_trace() -> ProofTrace {
724 ProofTrace {
725 steps: vec![
726 step(asserted("dog(sk_2)"), true, vec![]), step(asserted("dog_x1(sk_2, adam)"), true, vec![]), step(asserted("dog_x2(sk_2, zo'e)"), true, vec![]), step(derived("animal(sk_3(adam))"), true, vec![0, 1, 2]), step(proofref("dog(sk_2)"), true, vec![]), step(proofref("dog_x1(sk_2, adam)"), true, vec![]), step(proofref("dog_x2(sk_2, zo'e)"), true, vec![]), step(derived("animal_x1(sk_3(adam), adam)"), true, vec![4, 5, 6]), step(proofref("dog(sk_2)"), true, vec![]), step(proofref("dog_x1(sk_2, adam)"), true, vec![]), step(proofref("dog_x2(sk_2, zo'e)"), true, vec![]), step(derived("animal_x2(sk_3(adam), zo'e)"), true, vec![8, 9, 10]), step(ProofRule::Conjunction, true, vec![3, 7]), step(ProofRule::Conjunction, true, vec![12, 11]), step(
741 ProofRule::ExistsWitness {
742 var: "_ev0".to_string(),
743 term: nibli_protocol::LogicalTerm::Constant("sk_3(adam)".to_string()),
744 },
745 true,
746 vec![13],
747 ), ],
749 root: 14,
750 naf_dependent: false,
751 cwa_false: false,
752 }
753 }
754
755 #[test]
756 fn syllogism_collapses_to_conclusion_then_one_given_premise() {
757 let trace = syllogism_trace();
758 let node = collapse_proof(&trace, Register::Spec);
759 assert_eq!(node.css_class, "proof-derived");
761 assert!(node.holds);
762 assert!(node.label.contains("animal"), "label: {}", node.label);
763 assert!(
764 node.label.contains("by the rule") && node.label.contains("dog"),
765 "label: {}",
766 node.label
767 );
768 let premises: Vec<&RenderedNode> = node
772 .children
773 .iter()
774 .filter(|c| c.css_class != "proof-role-detail")
775 .collect();
776 assert_eq!(premises.len(), 1, "premises: {:?}", node.children);
777 assert!(premises[0].label.contains("dog"));
778 assert!(premises[0].label.contains("given"));
779 }
780
781 #[test]
782 fn syllogism_text_is_clean_two_lines() {
783 let trace = syllogism_trace();
784 let node = collapse_proof(&trace, Register::Spec);
785 let text = render_node_text(&node, 0, false);
786 assert!(!text.contains("role-level detail"), "text:\n{text}");
788 assert!(!text.contains("dog.dog"), "text:\n{text}");
789 assert!(!text.contains("Conjunction"), "text:\n{text}");
790 assert!(!text.contains("(see above)"), "text:\n{text}");
791 assert!(text.contains("animal"), "text:\n{text}");
793 assert!(text.contains("by the rule"), "text:\n{text}");
794 assert!(text.contains("dog"), "text:\n{text}");
795 assert!(text.contains("given"), "text:\n{text}");
796 assert_eq!(text.lines().count(), 2, "text:\n{text}");
797 }
798
799 #[test]
800 fn role_detail_is_present_but_only_shown_with_include_detail() {
801 let trace = syllogism_trace();
802 let node = collapse_proof(&trace, Register::Spec);
803 assert!(
805 node.children
806 .iter()
807 .any(|c| c.css_class == "proof-role-detail"),
808 "no role-detail cluster: {:?}",
809 node.children
810 );
811 let with = render_node_text(&node, 0, true);
813 assert!(with.contains("role-level detail"), "with:\n{with}");
814 assert!(
816 with.contains("animal") || with.contains("dog"),
817 "with:\n{with}"
818 );
819 }
820
821 #[test]
822 fn false_query_is_not_derivable() {
823 let trace = ProofTrace {
824 steps: vec![step(
825 ProofRule::PredicateNotFound {
826 predicate: "animal(adam)".to_string(),
827 },
828 false,
829 vec![],
830 )],
831 root: 0,
832 naf_dependent: false,
833 cwa_false: false,
834 };
835 let node = collapse_proof(&trace, Register::Spec);
836 assert!(!node.holds);
837 assert_eq!(node.css_class, "proof-failed");
838 assert!(
839 node.label.contains("not derivable"),
840 "label: {}",
841 node.label
842 );
843 assert!(node.label.contains("animal"), "label: {}", node.label);
844 }
845
846 #[test]
847 fn flat_given_fact_renders() {
848 let trace = ProofTrace {
849 steps: vec![step(asserted("animal(adam)"), true, vec![])],
850 root: 0,
851 naf_dependent: false,
852 cwa_false: false,
853 };
854 let node = collapse_proof(&trace, Register::Spec);
855 assert_eq!(node.css_class, "proof-asserted");
856 assert!(node.label.contains("given"), "label: {}", node.label);
857 assert!(node.label.contains("animal"), "label: {}", node.label);
858 }
859
860 #[test]
861 fn multi_hop_nests_two_rule_steps() {
862 let dl = |lhs_base: &str, rhs: &str, fact: &str| ProofRule::Derived {
865 label: format!("{lhs_base} ∧ {lhs_base}_x1 → {rhs} ∧ {rhs}_x1"),
866 fact: fact.to_string(),
867 };
868 let trace = ProofTrace {
869 steps: vec![
870 step(asserted("dog(sk_1)"), true, vec![]), step(asserted("dog_x1(sk_1, adam)"), true, vec![]), step(ProofRule::Conjunction, true, vec![0, 1]), step(
874 ProofRule::ExistsWitness {
875 var: "_e".into(),
876 term: nibli_protocol::LogicalTerm::Constant("sk_1".into()),
877 },
878 true,
879 vec![2],
880 ), step(dl("dog", "animal", "animal(sk_2)"), true, vec![3]), step(dl("dog", "animal", "animal_x1(sk_2, adam)"), true, vec![3]), step(ProofRule::Conjunction, true, vec![4, 5]), step(
885 ProofRule::ExistsWitness {
886 var: "_e".into(),
887 term: nibli_protocol::LogicalTerm::Constant("sk_2".into()),
888 },
889 true,
890 vec![6],
891 ), step(dl("animal", "alive", "alive(sk_3)"), true, vec![7]), step(dl("animal", "alive", "alive_x1(sk_3, adam)"), true, vec![7]), step(ProofRule::Conjunction, true, vec![8, 9]), step(
896 ProofRule::ExistsWitness {
897 var: "_e".into(),
898 term: nibli_protocol::LogicalTerm::Constant("sk_3".into()),
899 },
900 true,
901 vec![10],
902 ), ],
904 root: 11,
905 naf_dependent: false,
906 cwa_false: false,
907 };
908 let node = collapse_proof(&trace, Register::Spec);
909 let text = render_node_text(&node, 0, false);
910 assert_eq!(node.css_class, "proof-derived"); let mid: Vec<&RenderedNode> = node
913 .children
914 .iter()
915 .filter(|c| c.css_class != "proof-role-detail")
916 .collect();
917 assert_eq!(mid.len(), 1, "text:\n{text}");
918 assert_eq!(mid[0].css_class, "proof-derived"); let leaf: Vec<&RenderedNode> = mid[0]
920 .children
921 .iter()
922 .filter(|c| c.css_class != "proof-role-detail")
923 .collect();
924 assert_eq!(leaf.len(), 1, "text:\n{text}");
925 assert_eq!(leaf[0].css_class, "proof-asserted"); assert!(!text.contains("Conjunction"), "text:\n{text}");
928 assert_eq!(text.lines().count(), 3, "text:\n{text}");
929 }
930
931 #[test]
932 fn degrades_without_panic_on_unrecognized_shape() {
933 let trace = ProofTrace {
936 steps: vec![step(ProofRule::Negation, true, vec![])],
937 root: 0,
938 naf_dependent: false,
939 cwa_false: false,
940 };
941 let node = collapse_proof(&trace, Register::Spec);
942 let _ = render_node_text(&node, 0, false); assert!(!node.label.is_empty());
944 }
945
946 #[test]
947 fn empty_trace_does_not_panic() {
948 let trace = ProofTrace {
949 steps: vec![],
950 root: 0,
951 naf_dependent: false,
952 cwa_false: false,
953 };
954 let node = collapse_proof(&trace, Register::Spec);
955 let _ = render_node_text(&node, 0, false);
956 }
957}