Skip to main content

karpal_verify/
report.rs

1use crate::{
2    ArtifactBatch, Certificate, DryRunner, ExecutionResult, ExecutionStatus, Obligation,
3    ObligationBundle, VerifierRunner,
4};
5
6#[cfg(feature = "std")]
7use std::fmt::Write as _;
8
9#[cfg(not(feature = "std"))]
10use alloc::{string::String, vec::Vec};
11#[cfg(feature = "std")]
12use std::{string::String, vec::Vec};
13
14/// Schema version for serialized verification report JSON.
15pub const VERIFICATION_REPORT_SCHEMA_VERSION: &str = "1";
16
17/// Per-obligation verification report.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ObligationReport {
20    pub obligation_name: String,
21    pub summary: String,
22    pub artifact_path: Option<String>,
23    pub lean_theorem_ref: Option<String>,
24    pub lean_diagnostics: Vec<String>,
25    pub result: Option<ExecutionResult>,
26    pub certificate: Option<Certificate>,
27    pub lean_certificate: Option<Certificate>,
28}
29
30impl ObligationReport {
31    pub fn status(&self) -> Option<ExecutionStatus> {
32        self.result.as_ref().map(|result| result.status)
33    }
34
35    pub fn succeeded(&self) -> bool {
36        self.result
37            .as_ref()
38            .map(|result| result.is_success())
39            .unwrap_or(false)
40    }
41}
42
43/// Report for the generated Lean module.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct ModuleReport {
46    pub module_name: String,
47    pub artifact_path: Option<String>,
48    pub theorem_refs: Vec<String>,
49    pub prelude_imports: Vec<String>,
50    pub prelude_aliases: Vec<String>,
51    pub diagnostics: Vec<String>,
52    pub theorem_failures: Vec<String>,
53    pub result: Option<ExecutionResult>,
54    pub certificate: Option<Certificate>,
55}
56
57impl ModuleReport {
58    pub fn status(&self) -> Option<ExecutionStatus> {
59        self.result.as_ref().map(|result| result.status)
60    }
61}
62
63/// Aggregate verification report for a bundle.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct VerificationReport {
66    pub bundle_name: String,
67    pub root: String,
68    pub obligations: Vec<ObligationReport>,
69    pub lean_module: Option<ModuleReport>,
70}
71
72impl VerificationReport {
73    pub fn obligation_count(&self) -> usize {
74        self.obligations.len()
75    }
76
77    pub fn success_count(&self) -> usize {
78        self.obligations
79            .iter()
80            .filter(|report| report.succeeded())
81            .count()
82    }
83
84    pub fn failure_count(&self) -> usize {
85        self.obligation_count().saturating_sub(self.success_count())
86    }
87
88    pub fn is_success(&self) -> bool {
89        self.failure_count() == 0
90    }
91
92    #[cfg(feature = "std")]
93    pub fn to_json(&self) -> String {
94        fn esc(s: &str) -> String {
95            s.replace('\\', "\\\\")
96                .replace('"', "\\\"")
97                .replace('\n', "\\n")
98        }
99
100        fn render_certificate_json(certificate: &Certificate) -> String {
101            format!(
102                "{{\"backend\":\"{}\",\"witness_ref\":\"{}\"}}",
103                esc(certificate.backend),
104                esc(&certificate.witness_ref)
105            )
106        }
107
108        let mut out = String::new();
109        let _ = write!(
110            out,
111            "{{\"schema_version\":\"{}\",\"bundle_name\":\"{}\",\"root\":\"{}\",\"success_count\":{},\"failure_count\":{},\"obligations\":[",
112            VERIFICATION_REPORT_SCHEMA_VERSION,
113            esc(&self.bundle_name),
114            esc(&self.root),
115            self.success_count(),
116            self.failure_count()
117        );
118
119        for (idx, obligation) in self.obligations.iter().enumerate() {
120            if idx > 0 {
121                out.push(',');
122            }
123            let _ = write!(
124                out,
125                "{{\"name\":\"{}\",\"summary\":\"{}\",\"status\":\"{}\",\"artifact_path\":{},\"lean_theorem_ref\":{},\"lean_diagnostic_count\":{},\"certificate\":{},\"lean_certificate\":{}}}",
126                esc(&obligation.obligation_name),
127                esc(&obligation.summary),
128                obligation
129                    .status()
130                    .map(|s| format!("{s:?}"))
131                    .unwrap_or_else(|| "None".into()),
132                obligation
133                    .artifact_path
134                    .as_ref()
135                    .map(|p| format!("\"{}\"", esc(p)))
136                    .unwrap_or_else(|| "null".into()),
137                obligation
138                    .lean_theorem_ref
139                    .as_ref()
140                    .map(|r| format!("\"{}\"", esc(r)))
141                    .unwrap_or_else(|| "null".into()),
142                obligation.lean_diagnostics.len(),
143                obligation
144                    .certificate
145                    .as_ref()
146                    .map(render_certificate_json)
147                    .unwrap_or_else(|| "null".into()),
148                obligation
149                    .lean_certificate
150                    .as_ref()
151                    .map(render_certificate_json)
152                    .unwrap_or_else(|| "null".into())
153            );
154        }
155        out.push(']');
156        if let Some(module) = &self.lean_module {
157            let _ = write!(
158                out,
159                ",\"lean_module\":{{\"module_name\":\"{}\",\"status\":\"{}\",\"theorem_count\":{},\"import_count\":{},\"alias_count\":{},\"diagnostic_count\":{},\"theorem_failure_count\":{},\"certificate\":{}}}",
160                esc(&module.module_name),
161                module
162                    .status()
163                    .map(|s| format!("{s:?}"))
164                    .unwrap_or_else(|| "None".into()),
165                module.theorem_refs.len(),
166                module.prelude_imports.len(),
167                module.prelude_aliases.len(),
168                module.diagnostics.len(),
169                module.theorem_failures.len(),
170                module
171                    .certificate
172                    .as_ref()
173                    .map(render_certificate_json)
174                    .unwrap_or_else(|| "null".into())
175            );
176        }
177        out.push('}');
178        out
179    }
180
181    #[cfg(feature = "std")]
182    pub fn to_markdown(&self) -> String {
183        let mut out = String::new();
184        let _ = writeln!(out, "# Verification Report");
185        let _ = writeln!(out);
186        let _ = writeln!(
187            out,
188            "- Schema version: `{}`",
189            VERIFICATION_REPORT_SCHEMA_VERSION
190        );
191        let _ = writeln!(out, "- Bundle: `{}`", self.bundle_name);
192        let _ = writeln!(out, "- Root: `{}`", self.root);
193        let _ = writeln!(out, "- Successes: {}", self.success_count());
194        let _ = writeln!(out, "- Failures: {}", self.failure_count());
195        let _ = writeln!(out);
196        let _ = writeln!(
197            out,
198            "| Obligation | Status | Artifact | Lean theorem | Lean diagnostics | SMT certificate | Lean certificate |"
199        );
200        let _ = writeln!(out, "|---|---|---|---|---|---|---|");
201        for obligation in &self.obligations {
202            let _ = writeln!(
203                out,
204                "| `{}` | `{}` | `{}` | `{}` | `{}` | `{}` | `{}` |",
205                obligation.obligation_name,
206                obligation
207                    .status()
208                    .map(|s| format!("{s:?}"))
209                    .unwrap_or_else(|| "None".into()),
210                obligation.artifact_path.as_deref().unwrap_or("-"),
211                obligation.lean_theorem_ref.as_deref().unwrap_or("-"),
212                if obligation.lean_diagnostics.is_empty() {
213                    "-".into()
214                } else {
215                    obligation.lean_diagnostics.join("; ")
216                },
217                obligation
218                    .certificate
219                    .as_ref()
220                    .map(|c| c.backend)
221                    .unwrap_or("-"),
222                obligation
223                    .lean_certificate
224                    .as_ref()
225                    .map(|c| c.backend)
226                    .unwrap_or("-")
227            );
228        }
229        if let Some(module) = &self.lean_module {
230            let _ = writeln!(out);
231            let _ = writeln!(out, "Lean module: `{}`", module.module_name);
232            let _ = writeln!(out, "Lean theorems: {}", module.theorem_refs.len());
233            let _ = writeln!(out, "Lean imports: {}", module.prelude_imports.len());
234            let _ = writeln!(out, "Lean aliases: {}", module.prelude_aliases.len());
235            let _ = writeln!(out, "Lean diagnostics: {}", module.diagnostics.len());
236            let _ = writeln!(
237                out,
238                "Lean theorem failures: {}",
239                module.theorem_failures.len()
240            );
241            let _ = writeln!(
242                out,
243                "Lean certificate: `{}`",
244                module
245                    .certificate
246                    .as_ref()
247                    .map(|c| c.witness_ref.as_str())
248                    .unwrap_or("-")
249            );
250        }
251        out
252    }
253}
254
255/// Build a dry-run report without spawning external tools.
256pub fn dry_run_report(bundle: &ObligationBundle, artifacts: &ArtifactBatch) -> VerificationReport {
257    execute_report(bundle, artifacts, &DryRunner)
258}
259
260/// Execute all plans in an artifact batch and attach results back to obligations.
261pub fn execute_report(
262    bundle: &ObligationBundle,
263    artifacts: &ArtifactBatch,
264    runner: &impl VerifierRunner,
265) -> VerificationReport {
266    let lean_record = artifacts
267        .records
268        .iter()
269        .find(|record| record.path.ends_with(".lean"));
270    let lean_result = lean_record.and_then(|record| {
271        artifacts
272            .plans
273            .iter()
274            .find(|plan| {
275                plan.kind == crate::CommandKind::Lean
276                    && plan.input_files.iter().any(|f| f == &record.path)
277            })
278            .map(|plan| runner.run(plan))
279    });
280
281    let mut obligations = Vec::new();
282
283    for obligation in bundle.obligations() {
284        let artifact_path = artifacts
285            .records
286            .iter()
287            .find(|record| record.name == obligation.name)
288            .map(|record| record.path.clone());
289        let result = artifacts
290            .plans
291            .iter()
292            .find(|plan| {
293                plan.kind == crate::CommandKind::Smt
294                    && plan
295                        .input_files
296                        .iter()
297                        .any(|f| artifact_path.as_ref() == Some(f))
298            })
299            .map(|plan| runner.run(plan));
300        let certificate = result.as_ref().and_then(|result| {
301            certificate_for_obligation(result, obligation, artifact_path.clone())
302        });
303        let lean_theorem = artifacts
304            .lean_export
305            .as_ref()
306            .and_then(|export| export.theorem_for_obligation(&obligation.name).cloned());
307        let lean_theorem_ref = artifacts.lean_export.as_ref().and_then(|export| {
308            lean_theorem
309                .as_ref()
310                .map(|theorem| theorem.witness_ref(&export.module_name))
311        });
312        let lean_diagnostics = lean_result
313            .as_ref()
314            .and_then(|result| result.lean_output.as_ref())
315            .and_then(|output| {
316                lean_theorem.as_ref().map(|theorem| {
317                    output
318                        .theorem_diagnostics(theorem)
319                        .into_iter()
320                        .map(|diagnostic| diagnostic.message.clone())
321                        .collect::<Vec<_>>()
322                })
323            })
324            .unwrap_or_default();
325        let lean_certificate = lean_result.as_ref().and_then(|result| {
326            lean_theorem_ref.as_ref().and_then(|witness_ref| {
327                certificate_for_witness::<crate::LeanCertificate>(
328                    result,
329                    obligation,
330                    witness_ref.clone(),
331                    lean_record.map(|record| record.path.clone()),
332                )
333            })
334        });
335
336        obligations.push(ObligationReport {
337            obligation_name: obligation.name.clone(),
338            summary: obligation.summary(),
339            artifact_path,
340            lean_theorem_ref,
341            lean_diagnostics,
342            result,
343            certificate,
344            lean_certificate,
345        });
346    }
347
348    let lean_module = lean_record.map(|record| {
349        let theorem_refs = artifacts
350            .lean_export
351            .as_ref()
352            .map(|export| {
353                export
354                    .theorems
355                    .iter()
356                    .map(|theorem| theorem.witness_ref(&export.module_name))
357                    .collect::<Vec<_>>()
358            })
359            .unwrap_or_default();
360        let prelude_imports = artifacts
361            .lean_export
362            .as_ref()
363            .map(|export| {
364                export
365                    .prelude
366                    .imports
367                    .iter()
368                    .map(|import| import.module.clone())
369                    .collect::<Vec<_>>()
370            })
371            .unwrap_or_default();
372        let prelude_aliases = artifacts
373            .lean_export
374            .as_ref()
375            .map(|export| {
376                export
377                    .prelude
378                    .aliases
379                    .iter()
380                    .map(|alias| format!("{} := {}", alias.alias, alias.target))
381                    .collect::<Vec<_>>()
382            })
383            .unwrap_or_default();
384        let diagnostics = lean_result
385            .as_ref()
386            .and_then(|result| result.lean_output.as_ref())
387            .map(|output| {
388                output
389                    .diagnostics
390                    .iter()
391                    .map(|diagnostic| diagnostic.message.clone())
392                    .collect::<Vec<_>>()
393            })
394            .unwrap_or_default();
395        let theorem_failures = match (
396            artifacts.lean_export.as_ref(),
397            lean_result
398                .as_ref()
399                .and_then(|result| result.lean_output.as_ref()),
400        ) {
401            (Some(export), Some(output)) => export
402                .theorems
403                .iter()
404                .filter(|theorem| output.has_theorem_failure(theorem))
405                .map(|theorem| theorem.witness_ref(&export.module_name))
406                .collect::<Vec<_>>(),
407            _ => Vec::new(),
408        };
409        let certificate = lean_result.as_ref().and_then(|result| {
410            certificate_for_module(
411                result,
412                &record.name,
413                Some(record.path.clone()),
414                theorem_refs.len(),
415            )
416        });
417
418        ModuleReport {
419            module_name: record.name.clone(),
420            artifact_path: Some(record.path.clone()),
421            theorem_refs,
422            prelude_imports,
423            prelude_aliases,
424            diagnostics,
425            theorem_failures,
426            result: lean_result.clone(),
427            certificate,
428        }
429    });
430
431    VerificationReport {
432        bundle_name: bundle.name.clone(),
433        root: artifacts.root.clone(),
434        obligations,
435        lean_module,
436    }
437}
438
439fn certificate_for_obligation(
440    result: &ExecutionResult,
441    obligation: &Obligation,
442    artifact_path: Option<String>,
443) -> Option<Certificate> {
444    let policy = result.verification_policy();
445    let witness_ref = format!("{}:{}", result.plan.executable, policy.witness_suffix);
446    match result.plan.kind {
447        crate::CommandKind::Smt => certificate_for_witness::<crate::SmtCertificate>(
448            result,
449            obligation,
450            witness_ref,
451            artifact_path,
452        ),
453        crate::CommandKind::Lean => certificate_for_witness::<crate::LeanCertificate>(
454            result,
455            obligation,
456            witness_ref,
457            artifact_path,
458        ),
459    }
460}
461
462fn certificate_for_witness<B: crate::VerificationBackend>(
463    result: &ExecutionResult,
464    obligation: &Obligation,
465    witness_ref: String,
466    artifact_path: Option<String>,
467) -> Option<Certificate> {
468    if !result.is_success() {
469        return None;
470    }
471
472    let mut cert = Certificate::from_obligation::<B>(obligation, witness_ref);
473
474    if let Some(version) = &result.backend_version {
475        cert = cert.with_backend_version(version.clone());
476    }
477
478    if let Some(path) = artifact_path {
479        cert = cert.with_artifact_path(path);
480    }
481
482    Some(cert)
483}
484
485fn certificate_for_module(
486    result: &ExecutionResult,
487    module_name: &str,
488    artifact_path: Option<String>,
489    theorem_count: usize,
490) -> Option<Certificate> {
491    if !result.is_success() {
492        return None;
493    }
494
495    let mut cert = Certificate::new(
496        <crate::LeanCertificate as crate::VerificationBackend>::NAME,
497        format!("Lean module {module_name}"),
498        crate::LeanCertificate::module_ref(module_name),
499    )
500    .with_notes(format!(
501        "verified module containing {theorem_count} theorem(s)"
502    ));
503
504    if let Some(version) = &result.backend_version {
505        cert = cert.with_backend_version(version.clone());
506    }
507
508    if let Some(path) = artifact_path {
509        cert = cert.with_artifact_path(path);
510    }
511
512    Some(cert)
513}
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518    use crate::{
519        AlgebraicSignature, ArtifactLayout, LeanConfig, Origin, SmtConfig, Sort,
520        dry_run_bundle_artifacts,
521    };
522
523    #[test]
524    fn dry_run_report_attaches_results_to_all_obligations() {
525        let sig = AlgebraicSignature::monoid(Sort::Int, "combine", "e");
526        let bundle = ObligationBundle::monoid(
527            "sum_monoid",
528            Origin::new("karpal-core", "Monoid for Sum<i32>"),
529            &sig,
530        );
531        let layout = ArtifactLayout::new("target/karpal-verify-report-test");
532        let artifacts = dry_run_bundle_artifacts(
533            &bundle,
534            &layout,
535            "KarpalVerify",
536            &SmtConfig::default(),
537            &LeanConfig::default(),
538        );
539
540        let report = dry_run_report(&bundle, &artifacts);
541        assert_eq!(report.obligation_count(), 3);
542        assert!(
543            report
544                .obligations
545                .iter()
546                .all(|entry| entry.status() == Some(ExecutionStatus::DryRun))
547        );
548        assert_eq!(
549            report.lean_module.as_ref().unwrap().status(),
550            Some(ExecutionStatus::DryRun)
551        );
552        assert_eq!(report.lean_module.as_ref().unwrap().theorem_refs.len(), 3);
553    }
554
555    #[test]
556    fn successful_execution_yields_certificates() {
557        let sig = AlgebraicSignature::semigroup(Sort::Int, "combine");
558        let bundle = ObligationBundle::semigroup(
559            "sum_semigroup",
560            Origin::new("karpal-core", "Semigroup for Sum<i32>"),
561            &sig,
562        );
563        let layout = ArtifactLayout::new("target/karpal-verify-report-test-2");
564        let artifacts = dry_run_bundle_artifacts(
565            &bundle,
566            &layout,
567            "KarpalVerify",
568            &SmtConfig::default(),
569            &LeanConfig::default(),
570        );
571
572        struct SuccessRunner;
573        impl VerifierRunner for SuccessRunner {
574            fn run(&self, plan: &crate::InvocationPlan) -> ExecutionResult {
575                ExecutionResult {
576                    plan: plan.clone(),
577                    status: match plan.kind {
578                        crate::CommandKind::Smt => ExecutionStatus::Unsat,
579                        crate::CommandKind::Lean => ExecutionStatus::Success,
580                    },
581                    stdout: String::new(),
582                    stderr: String::new(),
583                    exit_code: Some(0),
584                    backend_version: Some("tool 1.0".into()),
585                    smt_output: Some(crate::SmtOutput {
586                        status: Some(ExecutionStatus::Unsat),
587                        model: None,
588                        reason_unknown: None,
589                    }),
590                    lean_output: (plan.kind == crate::CommandKind::Lean).then(|| {
591                        crate::LeanOutput {
592                            diagnostics: vec![crate::LeanDiagnostic {
593                                file: Some("lean/KarpalVerify.lean".into()),
594                                line: Some(7),
595                                column: Some(2),
596                                severity: "error".into(),
597                                message: "unsolved goals in theorem associativity".into(),
598                                theorem_hits: vec!["associativity".into()],
599                            }],
600                            theorem_hits: vec!["associativity".into()],
601                        }
602                    }),
603                }
604            }
605        }
606
607        let report = execute_report(&bundle, &artifacts, &SuccessRunner);
608        assert_eq!(report.success_count(), 1);
609        assert!(report.obligations[0].certificate.is_some());
610        assert_eq!(
611            report.obligations[0]
612                .certificate
613                .as_ref()
614                .and_then(|c| c.backend_version.as_deref()),
615            Some("tool 1.0")
616        );
617        assert_eq!(
618            report
619                .lean_module
620                .as_ref()
621                .map(|module| module.theorem_refs.len()),
622            Some(1)
623        );
624        assert_eq!(
625            report.obligations[0].lean_theorem_ref.as_deref(),
626            Some("KarpalVerify.associativity")
627        );
628        assert_eq!(
629            report.obligations[0].lean_diagnostics,
630            vec!["unsolved goals in theorem associativity".to_string()]
631        );
632        assert_eq!(
633            report.obligations[0]
634                .lean_certificate
635                .as_ref()
636                .map(|certificate| certificate.backend),
637            Some("lean4")
638        );
639        assert_eq!(
640            report
641                .lean_module
642                .as_ref()
643                .and_then(|module| module.certificate.as_ref())
644                .map(|certificate| certificate.witness_ref.as_str()),
645            Some("KarpalVerify")
646        );
647        assert_eq!(
648            report
649                .lean_module
650                .as_ref()
651                .map(|module| module.theorem_failures.clone()),
652            Some(vec!["KarpalVerify.associativity".to_string()])
653        );
654    }
655
656    #[test]
657    fn report_serialization_includes_summary_data() {
658        let report = VerificationReport {
659            bundle_name: "demo".into(),
660            root: "target/demo".into(),
661            obligations: vec![ObligationReport {
662                obligation_name: "assoc".into(),
663                summary: "demo::assoc [associativity]".into(),
664                artifact_path: Some("target/demo/smt/assoc.smt2".into()),
665                lean_theorem_ref: None,
666                lean_diagnostics: Vec::new(),
667                result: None,
668                certificate: None,
669                lean_certificate: None,
670            }],
671            lean_module: None,
672        };
673
674        let json = report.to_json();
675        let markdown = report.to_markdown();
676        assert!(json.contains("\"schema_version\":\"1\""));
677        assert!(json.contains("\"bundle_name\":\"demo\""));
678        assert!(markdown.contains("# Verification Report"));
679        assert!(markdown.contains("Schema version: `1`"));
680        assert!(markdown.contains("`assoc`"));
681    }
682
683    #[test]
684    fn theorem_mapping_uses_exported_theorem_identity_not_obligation_name() {
685        let sig = AlgebraicSignature::semiring(Sort::Int, "add", "mul", "zero", "one");
686        let bundle = ObligationBundle::semiring(
687            "sum_semiring",
688            Origin::new("karpal-core", "Semiring for Sum<i32>"),
689            &sig,
690        );
691        let layout = ArtifactLayout::new("target/karpal-verify-report-test-3");
692        let artifacts = dry_run_bundle_artifacts(
693            &bundle,
694            &layout,
695            "KarpalVerify",
696            &SmtConfig::default(),
697            &LeanConfig::default(),
698        );
699
700        struct TheoremNameRunner;
701        impl VerifierRunner for TheoremNameRunner {
702            fn run(&self, plan: &crate::InvocationPlan) -> ExecutionResult {
703                ExecutionResult {
704                    plan: plan.clone(),
705                    status: match plan.kind {
706                        crate::CommandKind::Smt => ExecutionStatus::Unsat,
707                        crate::CommandKind::Lean => ExecutionStatus::Failure,
708                    },
709                    stdout: String::new(),
710                    stderr: String::new(),
711                    exit_code: Some(1),
712                    backend_version: Some("tool 1.0".into()),
713                    smt_output: Some(crate::SmtOutput {
714                        status: Some(ExecutionStatus::Unsat),
715                        model: None,
716                        reason_unknown: None,
717                    }),
718                    lean_output: (plan.kind == crate::CommandKind::Lean).then(|| {
719                        crate::LeanOutput {
720                            diagnostics: vec![crate::LeanDiagnostic {
721                                file: Some("lean/KarpalVerify.lean".into()),
722                                line: Some(9),
723                                column: Some(2),
724                                severity: "error".into(),
725                                message: "unsolved goals in theorem left_distributivity".into(),
726                                theorem_hits: vec!["left_distributivity".into()],
727                            }],
728                            theorem_hits: vec!["left_distributivity".into()],
729                        }
730                    }),
731                }
732            }
733        }
734
735        let report = execute_report(&bundle, &artifacts, &TheoremNameRunner);
736        let left_distributivity = report
737            .obligations
738            .iter()
739            .find(|entry| entry.obligation_name == "left_distributivity")
740            .expect("left distributivity obligation should be present");
741        assert_eq!(
742            left_distributivity.lean_diagnostics,
743            vec!["unsolved goals in theorem left_distributivity".to_string()]
744        );
745        assert_eq!(
746            report
747                .lean_module
748                .as_ref()
749                .map(|module| module.theorem_failures.clone()),
750            Some(vec!["KarpalVerify.left_distributivity".to_string()])
751        );
752    }
753
754    #[test]
755    fn theorem_mapping_can_fall_back_to_exported_line_spans() {
756        let sig = AlgebraicSignature::semiring(Sort::Int, "add", "mul", "zero", "one");
757        let bundle = ObligationBundle::semiring(
758            "sum_semiring",
759            Origin::new("karpal-core", "Semiring for Sum<i32>"),
760            &sig,
761        );
762        let layout = ArtifactLayout::new("target/karpal-verify-report-test-4");
763        let artifacts = dry_run_bundle_artifacts(
764            &bundle,
765            &layout,
766            "KarpalVerify",
767            &SmtConfig::default(),
768            &LeanConfig::default(),
769        );
770
771        let failure_line = artifacts
772            .lean_export
773            .as_ref()
774            .and_then(|export| export.theorem_for_obligation("left_distributivity"))
775            .map(|theorem| theorem.declaration_start_line)
776            .expect("left distributivity theorem span should be present");
777
778        struct LocationRunner {
779            failure_line: usize,
780        }
781        impl VerifierRunner for LocationRunner {
782            fn run(&self, plan: &crate::InvocationPlan) -> ExecutionResult {
783                ExecutionResult {
784                    plan: plan.clone(),
785                    status: match plan.kind {
786                        crate::CommandKind::Smt => ExecutionStatus::Unsat,
787                        crate::CommandKind::Lean => ExecutionStatus::Failure,
788                    },
789                    stdout: String::new(),
790                    stderr: String::new(),
791                    exit_code: Some(1),
792                    backend_version: Some("tool 1.0".into()),
793                    smt_output: Some(crate::SmtOutput {
794                        status: Some(ExecutionStatus::Unsat),
795                        model: None,
796                        reason_unknown: None,
797                    }),
798                    lean_output: (plan.kind == crate::CommandKind::Lean).then(|| {
799                        crate::LeanOutput {
800                            diagnostics: vec![crate::LeanDiagnostic {
801                                file: Some("lean/KarpalVerify.lean".into()),
802                                line: Some(self.failure_line),
803                                column: Some(2),
804                                severity: "error".into(),
805                                message: "type mismatch".into(),
806                                theorem_hits: Vec::new(),
807                            }],
808                            theorem_hits: Vec::new(),
809                        }
810                    }),
811                }
812            }
813        }
814
815        let report = execute_report(&bundle, &artifacts, &LocationRunner { failure_line });
816        let left_distributivity = report
817            .obligations
818            .iter()
819            .find(|entry| entry.obligation_name == "left_distributivity")
820            .expect("left distributivity obligation should be present");
821        assert_eq!(
822            left_distributivity.lean_diagnostics,
823            vec!["type mismatch".to_string()]
824        );
825        assert_eq!(
826            report
827                .lean_module
828                .as_ref()
829                .map(|module| module.theorem_failures.clone()),
830            Some(vec!["KarpalVerify.left_distributivity".to_string()])
831        );
832    }
833}