1use crate::{
2 ArtifactLayout, DryRunner, LeanConfig, LeanManifestReportFiles, LocalProcessRunner,
3 ObligationBundle, SmtConfig, VerificationReport, VerifierRunner, dry_run_bundle_artifacts,
4 dry_run_report, execute_report, write_bundle_artifacts,
5};
6#[cfg(feature = "amari")]
7use crate::{StatisticalVerification, ThreeTierVerificationReport, three_tier_report};
8use std::{fs, io, path::Path, string::String};
9
10pub const VERIFICATION_SIDECAR_SCHEMA_VERSION: &str = "1";
12
13pub const DEFAULT_REPORT_STEM: &str = "verification-report";
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ReportFiles {
19 pub json_path: String,
20 pub markdown_path: String,
21 pub lean_diagnostics_json_path: Option<String>,
22 pub lean_manifest_path: Option<String>,
23 pub three_tier_json_path: Option<String>,
24 pub three_tier_markdown_path: Option<String>,
25}
26
27#[derive(Debug, Clone)]
29pub struct VerificationSession {
30 bundle: ObligationBundle,
31 layout: ArtifactLayout,
32 lean_module_name: String,
33 smt: SmtConfig,
34 lean: LeanConfig,
35 report_stem: String,
36 #[cfg(feature = "amari")]
37 statistical_verifications: Vec<StatisticalVerification>,
38}
39
40impl VerificationSession {
41 pub fn new(
42 bundle: ObligationBundle,
43 layout: ArtifactLayout,
44 lean_module_name: impl Into<String>,
45 ) -> Self {
46 Self {
47 bundle,
48 layout,
49 lean_module_name: lean_module_name.into(),
50 smt: SmtConfig::default(),
51 lean: LeanConfig::default(),
52 report_stem: DEFAULT_REPORT_STEM.into(),
53 #[cfg(feature = "amari")]
54 statistical_verifications: Vec::new(),
55 }
56 }
57
58 pub fn with_smt_config(mut self, smt: SmtConfig) -> Self {
59 self.smt = smt;
60 self
61 }
62
63 pub fn with_lean_config(mut self, lean: LeanConfig) -> Self {
64 self.lean = lean;
65 self
66 }
67
68 pub fn with_report_stem(mut self, report_stem: impl Into<String>) -> Self {
69 self.report_stem = report_stem.into();
70 self
71 }
72
73 #[cfg(feature = "amari")]
74 pub fn with_statistical_verification(mut self, verification: StatisticalVerification) -> Self {
75 self.statistical_verifications.push(verification);
76 self
77 }
78
79 #[cfg(feature = "amari")]
80 pub fn with_statistical_verifications(
81 mut self,
82 verifications: impl IntoIterator<Item = StatisticalVerification>,
83 ) -> Self {
84 self.statistical_verifications.extend(verifications);
85 self
86 }
87
88 pub fn bundle(&self) -> &ObligationBundle {
89 &self.bundle
90 }
91
92 pub fn layout(&self) -> &ArtifactLayout {
93 &self.layout
94 }
95
96 pub fn lean_module_name(&self) -> &str {
97 &self.lean_module_name
98 }
99
100 pub fn report_stem(&self) -> &str {
101 &self.report_stem
102 }
103
104 pub fn dry_run_report(&self) -> VerificationReport {
106 let artifacts = dry_run_bundle_artifacts(
107 &self.bundle,
108 &self.layout,
109 &self.lean_module_name,
110 &self.smt,
111 &self.lean,
112 );
113 dry_run_report(&self.bundle, &artifacts)
114 }
115
116 pub fn verify_report(&self, runner: &impl VerifierRunner) -> io::Result<VerificationReport> {
118 let artifacts = write_bundle_artifacts(
119 &self.bundle,
120 &self.layout,
121 &self.lean_module_name,
122 &self.smt,
123 &self.lean,
124 )?;
125 Ok(execute_report(&self.bundle, &artifacts, runner))
126 }
127
128 pub fn verify_local_report(&self) -> io::Result<VerificationReport> {
130 self.verify_report(&LocalProcessRunner)
131 }
132
133 pub fn verify_with_ci_outputs(
135 &self,
136 runner: &impl VerifierRunner,
137 ) -> io::Result<VerificationOutput> {
138 let artifacts = write_bundle_artifacts(
139 &self.bundle,
140 &self.layout,
141 &self.lean_module_name,
142 &self.smt,
143 &self.lean,
144 )?;
145 let report = execute_report(&self.bundle, &artifacts, runner);
146 let report_files = write_report_files(
147 &self.layout.root,
148 &self.report_stem,
149 &self.bundle,
150 &report,
151 artifacts.lean_manifest.as_ref(),
152 #[cfg(feature = "amari")]
153 &self.statistical_verifications,
154 )?;
155 Ok(VerificationOutput {
156 report,
157 report_files,
158 })
159 }
160
161 pub fn verify_local_with_ci_outputs(&self) -> io::Result<VerificationOutput> {
163 self.verify_with_ci_outputs(&LocalProcessRunner)
164 }
165
166 pub fn verify_with_dry_runner(&self) -> io::Result<VerificationReport> {
168 self.verify_report(&DryRunner)
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct VerificationOutput {
175 pub report: VerificationReport,
176 pub report_files: ReportFiles,
177}
178
179pub fn verify_bundle(
181 bundle: &ObligationBundle,
182 layout: &ArtifactLayout,
183 lean_module_name: &str,
184 smt: &SmtConfig,
185 lean: &LeanConfig,
186 runner: &impl VerifierRunner,
187) -> io::Result<VerificationReport> {
188 VerificationSession::new(bundle.clone(), layout.clone(), lean_module_name)
189 .with_smt_config(smt.clone())
190 .with_lean_config(lean.clone())
191 .verify_report(runner)
192}
193
194pub fn verify_bundle_with_ci_outputs(
196 bundle: &ObligationBundle,
197 layout: &ArtifactLayout,
198 lean_module_name: &str,
199 smt: &SmtConfig,
200 lean: &LeanConfig,
201 runner: &impl VerifierRunner,
202) -> io::Result<VerificationOutput> {
203 VerificationSession::new(bundle.clone(), layout.clone(), lean_module_name)
204 .with_smt_config(smt.clone())
205 .with_lean_config(lean.clone())
206 .verify_with_ci_outputs(runner)
207}
208
209fn write_report_files(
210 root: impl AsRef<Path>,
211 report_stem: &str,
212 bundle: &ObligationBundle,
213 report: &VerificationReport,
214 lean_manifest: Option<&crate::artifact::LeanManifest>,
215 #[cfg(feature = "amari")] statistical_verifications: &[StatisticalVerification],
216) -> io::Result<ReportFiles> {
217 #[cfg(not(feature = "amari"))]
218 let _ = bundle;
219 let root = root.as_ref();
220 fs::create_dir_all(root)?;
221
222 let lean_manifest_path = report.lean_module.as_ref().map(|module| {
223 path_to_string(
224 &root
225 .join("lean")
226 .join(format!("{}.manifest.json", module.module_name)),
227 )
228 });
229
230 let lean_diagnostics_json_path = report
231 .lean_module
232 .as_ref()
233 .map(|_| -> io::Result<String> {
234 let path = root.join(format!("{report_stem}.lean-diagnostics.json"));
235 fs::write(&path, render_lean_diagnostics_json(report))?;
236 Ok(path_to_string(&path))
237 })
238 .transpose()?;
239
240 #[cfg(feature = "amari")]
241 let three_tier = (!statistical_verifications.is_empty())
242 .then(|| three_tier_report(bundle, statistical_verifications, Some(report)));
243 #[cfg(feature = "amari")]
244 let three_tier_json_path = if let Some(three_tier) = &three_tier {
245 let path = root.join(format!("{report_stem}.three-tier.json"));
246 fs::write(
247 &path,
248 render_three_tier_json_with_links(three_tier, report_stem, root),
249 )?;
250 Some(path_to_string(&path))
251 } else {
252 None
253 };
254 #[cfg(feature = "amari")]
255 let three_tier_markdown_path = if let Some(three_tier) = &three_tier {
256 let path = root.join(format!("{report_stem}.three-tier.md"));
257 fs::write(
258 &path,
259 render_three_tier_markdown_with_links(three_tier, report_stem, root),
260 )?;
261 Some(path_to_string(&path))
262 } else {
263 None
264 };
265 #[cfg(not(feature = "amari"))]
266 let three_tier_json_path = None;
267 #[cfg(not(feature = "amari"))]
268 let three_tier_markdown_path = None;
269
270 let json_path = root.join(format!("{report_stem}.json"));
271 let markdown_path = root.join(format!("{report_stem}.md"));
272 let report_files = ReportFiles {
273 json_path: path_to_string(&json_path),
274 markdown_path: path_to_string(&markdown_path),
275 lean_diagnostics_json_path,
276 lean_manifest_path,
277 three_tier_json_path,
278 three_tier_markdown_path,
279 };
280 fs::write(
281 &json_path,
282 render_report_json_with_links(report, &report_files),
283 )?;
284 fs::write(
285 &markdown_path,
286 render_report_markdown_with_links(report, &report_files),
287 )?;
288 if let (Some(path), Some(manifest)) = (&report_files.lean_manifest_path, lean_manifest) {
289 write_lean_manifest_with_report_links(path, manifest, &report_files)?;
290 }
291
292 Ok(report_files)
293}
294
295fn render_report_json_with_links(report: &VerificationReport, files: &ReportFiles) -> String {
296 let mut json = report.to_json();
297 if json.ends_with('}') {
298 json.pop();
299 json.push_str(",\"report_files\":{");
300 json.push_str(&format!(
301 "\"schema_version\":\"{}\",\"json_path\":\"{}\",\"markdown_path\":\"{}\"",
302 VERIFICATION_SIDECAR_SCHEMA_VERSION,
303 escape_json(&files.json_path),
304 escape_json(&files.markdown_path)
305 ));
306 if let Some(path) = &files.lean_diagnostics_json_path {
307 json.push_str(&format!(
308 ",\"lean_diagnostics_json_path\":\"{}\"",
309 escape_json(path)
310 ));
311 }
312 if let Some(path) = &files.lean_manifest_path {
313 json.push_str(&format!(
314 ",\"lean_manifest_path\":\"{}\"",
315 escape_json(path)
316 ));
317 }
318 if let Some(path) = &files.three_tier_json_path {
319 json.push_str(&format!(
320 ",\"three_tier_json_path\":\"{}\"",
321 escape_json(path)
322 ));
323 }
324 if let Some(path) = &files.three_tier_markdown_path {
325 json.push_str(&format!(
326 ",\"three_tier_markdown_path\":\"{}\"",
327 escape_json(path)
328 ));
329 }
330 json.push_str("}}");
331 }
332 json
333}
334
335fn render_report_markdown_with_links(report: &VerificationReport, files: &ReportFiles) -> String {
336 let mut markdown = report.to_markdown();
337 markdown.push_str("\nReport files:\n");
338 markdown.push_str(&format!(
339 "- Schema version: `{}`\n",
340 VERIFICATION_SIDECAR_SCHEMA_VERSION
341 ));
342 markdown.push_str(&format!("- JSON: `{}`\n", files.json_path));
343 markdown.push_str(&format!("- Markdown: `{}`\n", files.markdown_path));
344 if let Some(path) = &files.lean_diagnostics_json_path {
345 markdown.push_str(&format!("- Lean diagnostics JSON: `{}`\n", path));
346 }
347 if let Some(path) = &files.lean_manifest_path {
348 markdown.push_str(&format!("- Lean manifest: `{}`\n", path));
349 }
350 if let Some(path) = &files.three_tier_json_path {
351 markdown.push_str(&format!("- Three-tier JSON: `{}`\n", path));
352 }
353 if let Some(path) = &files.three_tier_markdown_path {
354 markdown.push_str(&format!("- Three-tier Markdown: `{}`\n", path));
355 }
356 markdown
357}
358
359#[cfg(feature = "amari")]
360fn render_three_tier_json_with_links(
361 report: &ThreeTierVerificationReport,
362 report_stem: &str,
363 root: &Path,
364) -> String {
365 let mut json = report.to_json();
366 if json.ends_with('}') {
367 json.pop();
368 json.push_str(",\"report_files\":{");
369 json.push_str(&format!(
370 "\"schema_version\":\"{}\",\"json_path\":\"{}\",\"markdown_path\":\"{}\"",
371 VERIFICATION_SIDECAR_SCHEMA_VERSION,
372 escape_json(&path_to_string(
373 &root.join(format!("{report_stem}.three-tier.json"))
374 )),
375 escape_json(&path_to_string(
376 &root.join(format!("{report_stem}.three-tier.md"))
377 ))
378 ));
379 json.push_str("}}");
380 }
381 json
382}
383
384#[cfg(feature = "amari")]
385fn render_three_tier_markdown_with_links(
386 report: &ThreeTierVerificationReport,
387 report_stem: &str,
388 root: &Path,
389) -> String {
390 let mut markdown = report.to_markdown();
391 markdown.push_str("\nReport files:\n");
392 markdown.push_str(&format!(
393 "- Schema version: `{}`\n",
394 VERIFICATION_SIDECAR_SCHEMA_VERSION
395 ));
396 markdown.push_str(&format!(
397 "- JSON: `{}`\n",
398 path_to_string(&root.join(format!("{report_stem}.three-tier.json")))
399 ));
400 markdown.push_str(&format!(
401 "- Markdown: `{}`\n",
402 path_to_string(&root.join(format!("{report_stem}.three-tier.md")))
403 ));
404 markdown
405}
406
407fn escape_json(s: &str) -> String {
408 s.replace('\\', "\\\\")
409 .replace('"', "\\\"")
410 .replace('\n', "\\n")
411}
412
413fn write_lean_manifest_with_report_links(
414 manifest_path: &str,
415 manifest: &crate::artifact::LeanManifest,
416 files: &ReportFiles,
417) -> io::Result<()> {
418 let manifest = manifest.clone().with_report_files({
419 let report_files =
420 LeanManifestReportFiles::new(files.json_path.clone(), files.markdown_path.clone());
421 match &files.lean_diagnostics_json_path {
422 Some(path) => report_files.with_lean_diagnostics_json_path(path.clone()),
423 None => report_files,
424 }
425 });
426 fs::write(manifest_path, manifest.to_json())
427}
428
429fn render_lean_diagnostics_json(report: &VerificationReport) -> String {
430 fn esc(s: &str) -> String {
431 s.replace('\\', "\\\\")
432 .replace('"', "\\\"")
433 .replace('\n', "\\n")
434 }
435
436 let module = match &report.lean_module {
437 Some(module) => module,
438 None => return "null".into(),
439 };
440
441 let obligation_entries = report
442 .obligations
443 .iter()
444 .filter(|obligation| {
445 obligation.lean_theorem_ref.is_some() || !obligation.lean_diagnostics.is_empty()
446 })
447 .map(|obligation| {
448 let diagnostics = obligation
449 .lean_diagnostics
450 .iter()
451 .map(|diagnostic| format!("\"{}\"", esc(diagnostic)))
452 .collect::<Vec<_>>()
453 .join(",");
454 format!(
455 "{{\"obligation_name\":\"{}\",\"theorem_ref\":{},\"diagnostics\":[{}]}}",
456 esc(&obligation.obligation_name),
457 obligation
458 .lean_theorem_ref
459 .as_ref()
460 .map(|theorem_ref| format!("\"{}\"", esc(theorem_ref)))
461 .unwrap_or_else(|| "null".into()),
462 diagnostics
463 )
464 })
465 .collect::<Vec<_>>()
466 .join(",");
467
468 let module_diagnostics = module
469 .diagnostics
470 .iter()
471 .map(|diagnostic| format!("\"{}\"", esc(diagnostic)))
472 .collect::<Vec<_>>()
473 .join(",");
474 let theorem_failures = module
475 .theorem_failures
476 .iter()
477 .map(|failure| format!("\"{}\"", esc(failure)))
478 .collect::<Vec<_>>()
479 .join(",");
480
481 format!(
482 "{{\"schema_version\":\"{}\",\"module_name\":\"{}\",\"module_diagnostics\":[{}],\"theorem_failures\":[{}],\"obligations\":[{}]}}",
483 VERIFICATION_SIDECAR_SCHEMA_VERSION,
484 esc(&module.module_name),
485 module_diagnostics,
486 theorem_failures,
487 obligation_entries
488 )
489}
490
491fn path_to_string(path: &Path) -> String {
492 path.to_string_lossy().into_owned()
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use crate::{
499 AlgebraicSignature, ExecutionResult, ExecutionStatus, LeanDiagnostic, LeanOutput, Origin,
500 SmtOutput, Sort,
501 };
502 #[cfg(feature = "amari")]
503 use crate::{StatisticalBound, verify_rare_event};
504
505 fn sample_session(root: &Path) -> VerificationSession {
506 let sig = AlgebraicSignature::monoid(Sort::Int, "combine", "e");
507 let bundle = ObligationBundle::monoid(
508 "sum_monoid",
509 Origin::new("karpal-core", "Monoid for Sum<i32>"),
510 &sig,
511 );
512
513 VerificationSession::new(bundle, ArtifactLayout::new(root), "KarpalVerify")
514 }
515
516 struct SuccessRunner;
517
518 impl VerifierRunner for SuccessRunner {
519 fn run(&self, plan: &crate::InvocationPlan) -> ExecutionResult {
520 ExecutionResult {
521 plan: plan.clone(),
522 status: match plan.kind {
523 crate::CommandKind::Smt => ExecutionStatus::Unsat,
524 crate::CommandKind::Lean | crate::CommandKind::Kani => ExecutionStatus::Success,
525 },
526 stdout: String::new(),
527 stderr: String::new(),
528 exit_code: Some(0),
529 backend_version: Some("tool 1.0".into()),
530 smt_output: Some(SmtOutput {
531 status: Some(ExecutionStatus::Unsat),
532 model: None,
533 reason_unknown: None,
534 }),
535 lean_output: (plan.kind == crate::CommandKind::Lean).then(|| LeanOutput {
536 diagnostics: vec![LeanDiagnostic {
537 file: Some("lean/KarpalVerify.lean".into()),
538 line: Some(4),
539 column: Some(2),
540 severity: "warning".into(),
541 message: "declaration uses sorry: associativity".into(),
542 theorem_hits: vec!["associativity".into()],
543 }],
544 theorem_hits: vec!["associativity".into()],
545 }),
546 }
547 }
548 }
549
550 #[test]
551 fn dry_run_report_orchestrates_artifacts_and_report() {
552 let session = sample_session(Path::new("target/karpal-verify-session-dry-run"));
553 let report = session.dry_run_report();
554
555 assert_eq!(report.obligation_count(), 3);
556 assert_eq!(report.root, "target/karpal-verify-session-dry-run");
557 assert!(
558 report
559 .obligations
560 .iter()
561 .all(|entry| entry.status() == Some(ExecutionStatus::DryRun))
562 );
563 }
564
565 #[test]
566 fn verify_report_builds_artifacts_runs_plans_and_returns_report() {
567 let temp = std::env::temp_dir().join("karpal_verify_session_report_test");
568 if temp.exists() {
569 let _ = fs::remove_dir_all(&temp);
570 }
571
572 let session = sample_session(&temp);
573 let report = session
574 .verify_report(&SuccessRunner)
575 .expect("session verification should succeed");
576 assert!(report.is_success());
577 assert!(temp.join("smt").exists());
578 assert!(temp.join("lean").exists());
579 assert_eq!(
580 report
581 .lean_module
582 .as_ref()
583 .map(|module| module.theorem_failures.clone()),
584 Some(vec!["KarpalVerify.associativity".into()])
585 );
586
587 let _ = fs::remove_dir_all(&temp);
588 }
589
590 #[test]
591 fn verify_with_ci_outputs_writes_reports_beside_artifacts() {
592 let temp = std::env::temp_dir().join("karpal_verify_session_ci_output_test");
593 if temp.exists() {
594 let _ = fs::remove_dir_all(&temp);
595 }
596
597 let output = sample_session(&temp)
598 .with_report_stem("summary")
599 .verify_with_ci_outputs(&SuccessRunner)
600 .expect("ci outputs should be written");
601
602 assert!(output.report.is_success());
603 assert!(Path::new(&output.report_files.json_path).exists());
604 assert!(Path::new(&output.report_files.markdown_path).exists());
605 assert!(
606 Path::new(
607 output
608 .report_files
609 .lean_diagnostics_json_path
610 .as_deref()
611 .expect("lean diagnostics sidecar should be written")
612 )
613 .exists()
614 );
615 assert!(
616 Path::new(
617 output
618 .report_files
619 .lean_manifest_path
620 .as_deref()
621 .expect("lean manifest path should be recorded")
622 )
623 .exists()
624 );
625 assert!(output.report_files.json_path.ends_with("summary.json"));
626 assert!(output.report_files.markdown_path.ends_with("summary.md"));
627 #[cfg(not(feature = "amari"))]
628 {
629 assert!(output.report_files.three_tier_json_path.is_none());
630 assert!(output.report_files.three_tier_markdown_path.is_none());
631 }
632 assert!(
633 output
634 .report_files
635 .lean_diagnostics_json_path
636 .as_deref()
637 .unwrap()
638 .ends_with("summary.lean-diagnostics.json")
639 );
640 assert!(
641 output
642 .report_files
643 .lean_manifest_path
644 .as_deref()
645 .unwrap()
646 .ends_with("lean/KarpalVerify.manifest.json")
647 );
648
649 let json = fs::read_to_string(&output.report_files.json_path)
650 .expect("summary json should be readable");
651 let markdown = fs::read_to_string(&output.report_files.markdown_path)
652 .expect("summary markdown should be readable");
653 let sidecar = fs::read_to_string(
654 output
655 .report_files
656 .lean_diagnostics_json_path
657 .as_deref()
658 .expect("lean diagnostics sidecar path should be present"),
659 )
660 .expect("lean diagnostics sidecar should be readable");
661 let manifest = fs::read_to_string(
662 output
663 .report_files
664 .lean_manifest_path
665 .as_deref()
666 .expect("lean manifest path should be present"),
667 )
668 .expect("lean manifest should be readable");
669 assert!(json.contains("\"schema_version\":\"1\""));
670 assert!(json.contains("\"report_files\""));
671 assert!(json.contains("\"lean_manifest_path\""));
672 assert!(json.contains("\"lean_diagnostics_json_path\""));
673 assert!(json.contains("\"schema_version\":\"1\",\"json_path\""));
674 assert!(markdown.contains("Report files:"));
675 assert!(markdown.contains("Schema version: `1`"));
676 assert!(markdown.contains("Lean diagnostics JSON"));
677 assert!(markdown.contains("Lean manifest"));
678 assert!(sidecar.contains("\"schema_version\":\"1\""));
679 assert!(manifest.contains("\"schema_version\":\"1\""));
680 assert!(manifest.contains("\"report_files\""));
681 assert!(manifest.contains("\"json_path\""));
682 assert!(manifest.contains("\"markdown_path\""));
683 assert!(manifest.contains("\"lean_diagnostics_json_path\""));
684
685 let _ = fs::remove_dir_all(&temp);
686 }
687
688 #[cfg(feature = "amari")]
689 #[test]
690 fn verify_with_ci_outputs_writes_three_tier_sidecars_when_statistical_evidence_exists() {
691 let temp = std::env::temp_dir().join("karpal_verify_session_three_tier_test");
692 if temp.exists() {
693 let _ = fs::remove_dir_all(&temp);
694 }
695
696 let bundle = sample_session(&temp).bundle().clone();
697 let statistical = verify_rare_event(
698 &bundle.obligations()[0],
699 &StatisticalBound::new(0.05).with_samples(128),
700 || false,
701 );
702
703 let output = sample_session(&temp)
704 .with_report_stem("summary")
705 .with_statistical_verification(statistical)
706 .verify_with_ci_outputs(&DryRunner)
707 .expect("ci outputs should be written");
708
709 let three_tier_json_path = output
710 .report_files
711 .three_tier_json_path
712 .as_deref()
713 .expect("three-tier json should be written");
714 let three_tier_markdown_path = output
715 .report_files
716 .three_tier_markdown_path
717 .as_deref()
718 .expect("three-tier markdown should be written");
719 assert!(Path::new(three_tier_json_path).exists());
720 assert!(Path::new(three_tier_markdown_path).exists());
721 assert!(three_tier_json_path.ends_with("summary.three-tier.json"));
722 assert!(three_tier_markdown_path.ends_with("summary.three-tier.md"));
723
724 let json = fs::read_to_string(three_tier_json_path).expect("three-tier json readable");
725 let markdown =
726 fs::read_to_string(three_tier_markdown_path).expect("three-tier markdown readable");
727 let main_json =
728 fs::read_to_string(&output.report_files.json_path).expect("main report json readable");
729 let main_markdown = fs::read_to_string(&output.report_files.markdown_path)
730 .expect("main report markdown readable");
731
732 assert!(json.contains("\"impossible_count\":1"));
733 assert!(json.contains("\"external_count\":2"));
734 assert!(json.contains("\"report_files\""));
735 assert!(markdown.contains("Three-Tier Verification Report"));
736 assert!(markdown.contains("Schema version: `1`"));
737 assert!(main_json.contains("\"three_tier_json_path\""));
738 assert!(main_json.contains("\"three_tier_markdown_path\""));
739 assert!(main_markdown.contains("Three-tier JSON"));
740 assert!(main_markdown.contains("Three-tier Markdown"));
741
742 let _ = fs::remove_dir_all(&temp);
743 }
744}