Skip to main content

dsfb_database/report/
mod.rs

1//! Report generation: CSV + JSON sidecars + plotter PNGs that the LaTeX
2//! paper includes verbatim. Every report header embeds the crate version
3//! and the non-claim block so a reviewer can verify provenance.
4
5// `plots` renders PNG figures via `plotters`; it is gated behind the
6// `report` feature so library-mode consumers opt out of the full
7// figure-rendering toolchain (font-kit, cm-super, etc.). Binaries that
8// use `plots` (main, pr_sweep) set `required-features = ["report"]`.
9#[cfg(feature = "report")]
10pub mod plots;
11#[cfg(feature = "report")]
12pub mod plots_live;
13
14use crate::grammar::Episode;
15use crate::metrics::PerMotifMetrics;
16use crate::non_claims;
17#[cfg(feature = "report")]
18use anyhow::Context;
19use anyhow::Result;
20use serde::Serialize;
21use std::fs::{self, File};
22use std::io::Write;
23use std::path::Path;
24
25#[derive(Debug, Serialize)]
26pub struct ReportHeader {
27    pub crate_version: &'static str,
28    pub generated_at: String,
29    pub non_claims: [&'static str; 6],
30    pub source: String,
31}
32
33pub fn write_episodes_csv(path: &Path, episodes: &[Episode]) -> Result<()> {
34    if let Some(parent) = path.parent() {
35        fs::create_dir_all(parent)?;
36    }
37    let mut wtr = csv::Writer::from_path(path)?;
38    wtr.write_record([
39        "motif",
40        "channel",
41        "t_start",
42        "t_end",
43        "peak",
44        "ema_at_boundary",
45        "trust_sum",
46    ])?;
47    for e in episodes {
48        wtr.write_record([
49            e.motif.name(),
50            e.channel.as_deref().unwrap_or(""),
51            &format!("{}", e.t_start),
52            &format!("{}", e.t_end),
53            &format!("{}", e.peak),
54            &format!("{}", e.ema_at_boundary),
55            &format!("{}", e.trust_sum),
56        ])?;
57    }
58    wtr.flush()?;
59    Ok(())
60}
61
62pub fn write_metrics_csv(path: &Path, metrics: &[PerMotifMetrics]) -> Result<()> {
63    if let Some(parent) = path.parent() {
64        fs::create_dir_all(parent)?;
65    }
66    let mut wtr = csv::Writer::from_path(path)?;
67    wtr.write_record([
68        "motif",
69        "tp",
70        "fp",
71        "fn",
72        "precision",
73        "recall",
74        "f1",
75        "ttd_median_s",
76        "ttd_p95_s",
77        "false_alarm_per_hour",
78        "compression_ratio",
79    ])?;
80    for m in metrics {
81        wtr.write_record([
82            &m.motif,
83            &m.tp.to_string(),
84            &m.fp.to_string(),
85            &m.fn_.to_string(),
86            &format!("{:.4}", m.precision),
87            &format!("{:.4}", m.recall),
88            &format!("{:.4}", m.f1),
89            &format!("{:.2}", m.time_to_detection_median_s),
90            &format!("{:.2}", m.time_to_detection_p95_s),
91            &format!("{:.4}", m.false_alarm_rate_per_hour),
92            &format!("{:.2}", m.episode_compression_ratio),
93        ])?;
94    }
95    wtr.flush()?;
96    Ok(())
97}
98
99/// JSON sidecar emitter (pretty-printed). Gated behind `report` so the
100/// library's default dependency tree does not carry `serde_json`. Main
101/// and any binary that writes JSON artefacts must declare
102/// `required-features = ["report"]`.
103#[cfg(feature = "report")]
104pub fn write_json<T: Serialize + ?Sized>(path: &Path, value: &T) -> Result<()> {
105    if let Some(parent) = path.parent() {
106        fs::create_dir_all(parent)?;
107    }
108    let s = serde_json::to_string_pretty(value)?;
109    File::create(path)
110        .with_context(|| format!("creating {}", path.display()))?
111        .write_all(s.as_bytes())?;
112    Ok(())
113}
114
115/// Write a free-form text header that every report directory carries.
116pub fn write_provenance(path: &Path, source: &str) -> Result<()> {
117    if let Some(parent) = path.parent() {
118        fs::create_dir_all(parent)?;
119    }
120    let mut f = File::create(path)?;
121    writeln!(f, "DSFB-Database report")?;
122    writeln!(f, "crate_version = {}", crate::CRATE_VERSION)?;
123    writeln!(f, "source        = {}", source)?;
124    writeln!(f)?;
125    writeln!(f, "Non-claims:")?;
126    writeln!(f, "{}", non_claims::as_block())?;
127    Ok(())
128}