Skip to main content

ferraus_core/
envelope.rs

1//! The `--json` result envelope and the provenance sidecar.
2
3use crate::ctx::{InputRecord, ModelRecord, OutputRecord, WarningRecord};
4use crate::error::Error;
5use serde::{Deserialize, Serialize};
6use serde_json::{Map, Value};
7use sha2::{Digest, Sha256};
8use std::io::Read;
9use std::path::Path;
10
11pub const SCHEMA_VERSION: u32 = 1;
12pub const FERRAUS_VERSION: &str = env!("CARGO_PKG_VERSION");
13
14/// Every command's structured result.
15#[derive(Clone, Debug, Serialize)]
16pub struct Envelope {
17    pub ok: bool,
18    pub command: String,
19    pub ferraus_version: String,
20    pub schema_version: u32,
21    pub inputs: Vec<InputRecord>,
22    pub outputs: Vec<OutputRecord>,
23    pub params: Map<String, Value>,
24    pub models: Vec<ModelRecord>,
25    pub metrics: Map<String, Value>,
26    pub warnings: Vec<WarningRecord>,
27    #[serde(skip_serializing_if = "Value::is_null")]
28    pub result: Value,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub error: Option<Error>,
31    pub timing_ms: u64,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub dry_run: Option<bool>,
34}
35
36impl Envelope {
37    pub fn error(command: &str, e: Error, timing_ms: u64) -> Envelope {
38        Envelope {
39            ok: false,
40            command: command.to_string(),
41            ferraus_version: FERRAUS_VERSION.to_string(),
42            schema_version: SCHEMA_VERSION,
43            inputs: vec![],
44            outputs: vec![],
45            params: Map::new(),
46            models: vec![],
47            metrics: Map::new(),
48            warnings: vec![],
49            result: Value::Null,
50            error: Some(e),
51            timing_ms,
52            dry_run: None,
53        }
54    }
55
56    pub fn exit_code(&self) -> i32 {
57        self.error.as_ref().map(|e| e.exit_code).unwrap_or(0)
58    }
59}
60
61#[derive(Clone, Debug, Serialize, Deserialize)]
62pub struct HostInfo {
63    pub os: String,
64    pub arch: String,
65    pub device: String,
66    pub threads: usize,
67}
68
69impl HostInfo {
70    pub fn current(device: &str, threads: usize) -> HostInfo {
71        HostInfo {
72            os: std::env::consts::OS.into(),
73            arch: std::env::consts::ARCH.into(),
74            device: device.into(),
75            threads,
76        }
77    }
78}
79
80#[derive(Clone, Debug, Serialize, Deserialize)]
81pub struct HashedFile {
82    pub path: String,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub sha256: Option<String>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub duration_s: Option<f64>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub role: Option<String>,
89}
90
91/// `<output>.ferraus.json`.
92#[derive(Clone, Debug, Serialize, Deserialize)]
93pub struct Sidecar {
94    pub ferraus_version: String,
95    pub schema_version: u32,
96    pub command: String,
97    pub argv: Vec<String>,
98    pub params: Map<String, Value>,
99    pub inputs: Vec<HashedFile>,
100    pub outputs: Vec<HashedFile>,
101    pub models: Vec<Value>,
102    pub plugins: Vec<Value>,
103    pub host: HostInfo,
104    /// `true`, `false`, or `"unknown"` (plugins).
105    pub deterministic: Value,
106    #[serde(default)]
107    pub warnings: Vec<Value>,
108    pub timing_ms: u64,
109}
110
111impl Sidecar {
112    pub fn path_for(output: &Path) -> std::path::PathBuf {
113        std::path::PathBuf::from(format!("{}.ferraus.json", output.display()))
114    }
115}
116
117/// SHA-256 of a file, streamed.
118pub fn sha256_file(path: &Path) -> std::io::Result<String> {
119    let mut f = std::fs::File::open(path)?;
120    let mut h = Sha256::new();
121    let mut buf = vec![0u8; 1 << 16];
122    loop {
123        let n = f.read(&mut buf)?;
124        if n == 0 {
125            break;
126        }
127        h.update(&buf[..n]);
128    }
129    Ok(hex::encode(h.finalize()))
130}
131
132pub fn sha256_bytes(b: &[u8]) -> String {
133    hex::encode(Sha256::digest(b))
134}