Skip to main content

apr_format/
falsifiers.rs

1//! GREEN contract falsifiers for the apr-format extraction (issue #2231).
2//!
3//! One `#[test]` per proof obligation in
4//! `contracts/apr-format-extraction-v1.yaml` + the companion
5//! `apr-format-leaf-sovereignty-v1.yaml`, named exactly as those contracts'
6//! `falsification_tests[].test` fields cite them so `pv lint` Gate-4 /
7//! strict-test-binding resolves the refs with no dangling-ref errors.
8//!
9//! Stage 1 shipped these as RED `unimplemented!` stubs (the obligation existed
10//! before the implementation). Stage 2 discharges them against the real bytes:
11//! the golden byte-identity oracle, the `cargo metadata` dependency closure, the
12//! CRC known-answer + golden trailer, the metadata round-trip, and the Jidoka
13//! quality gate. A falsifier going RED here means its obligation regressed.
14//!
15//! # f16 scoping note (issue #2231 / PMAT-905 class)
16//!
17//! Byte-identity is asserted for **F32** payloads only. The golden fixtures use
18//! F32 weights, so they are unaffected by the documented f16 write change (the
19//! leaf now uses IEEE round-to-nearest-even via the `half` crate instead of the
20//! legacy non-RNE `trueno::f32_to_f16`). See `crate::f16`.
21
22#![allow(
23    clippy::expect_used,
24    clippy::unwrap_used,
25    clippy::panic,
26    clippy::items_after_statements,
27    clippy::no_effect_underscore_binding,
28    clippy::float_cmp
29)]
30
31#[cfg(test)]
32mod tests {
33    use crate::types::{Compression, Metadata, ModelType, SaveOptions};
34    use std::collections::HashMap;
35    use std::path::PathBuf;
36
37    use serde::{Deserialize, Serialize};
38
39    /// The exact model that produced `tests/fixtures/golden_v1.apr` (captured
40    /// from the pre-extraction in-core save path — the byte-identity oracle).
41    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42    struct GoldenModel {
43        name: String,
44        weights: Vec<f32>,
45        bias: f32,
46    }
47
48    fn golden_model() -> GoldenModel {
49        GoldenModel {
50            name: "golden_v1".to_string(),
51            weights: vec![1.0, 2.0, 0.5, -0.5, 4.0, -2.0, 0.25, 8.0],
52            bias: 0.125,
53        }
54    }
55
56    /// The pinned `SaveOptions` that produced the golden fixture — every field
57    /// is fixed (no `chrono_lite_now()` / `CARGO_PKG_VERSION`) so the save is
58    /// deterministic and the bytes reproduce exactly.
59    fn golden_options() -> SaveOptions {
60        let metadata = Metadata {
61            created_at: "1700000000".to_string(),
62            aprender_version: "0.0.0-golden".to_string(),
63            model_name: Some("golden-v1".to_string()),
64            description: None,
65            training: None,
66            hyperparameters: HashMap::new(),
67            metrics: HashMap::new(),
68            custom: HashMap::new(),
69            distillation: None,
70            distillation_info: None,
71            license: None,
72            model_card: None,
73        };
74        SaveOptions {
75            compression: Compression::None,
76            metadata,
77            quality_score: Some(85),
78        }
79    }
80
81    fn fixtures() -> PathBuf {
82        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
83            .join("tests")
84            .join("fixtures")
85    }
86
87    fn golden_v1_bytes() -> Vec<u8> {
88        std::fs::read(fixtures().join("golden_v1.apr")).expect("read golden_v1.apr fixture")
89    }
90
91    /// FALSIFY-APRF-BYTE-IDENTITY: re-saving the captured golden model with the
92    /// pinned `SaveOptions` reproduces `golden_v1.apr` byte-for-byte (full file
93    /// incl. the CRC trailer). F32 payload — unaffected by the f16 write change.
94    #[test]
95    fn test_falsify_aprf_byte_identity_golden_roundtrip() {
96        let dir = std::env::temp_dir();
97        let path = dir.join("aprf_byte_identity_probe.apr");
98        crate::save(
99            &golden_model(),
100            ModelType::LinearRegression,
101            &path,
102            golden_options(),
103        )
104        .expect("save golden model with pinned options");
105
106        let produced = std::fs::read(&path).expect("read produced bytes");
107        let _ = std::fs::remove_file(&path);
108
109        let golden = golden_v1_bytes();
110        assert_eq!(
111            produced.len(),
112            golden.len(),
113            "byte length drifted (extraction changed the on-disk encoding)"
114        );
115        assert_eq!(
116            produced, golden,
117            "extracted save() output is NOT byte-identical to golden_v1.apr — \
118             the serializer order, padding, header layout, or CRC drifted"
119        );
120
121        // And the leaf reads its own/golden bytes back to the captured model.
122        let back: GoldenModel = crate::load_from_bytes(&golden, ModelType::LinearRegression)
123            .expect("load golden bytes");
124        assert_eq!(back, golden_model());
125    }
126
127    /// FALSIFY-APRF-SOVEREIGN-DEPS: the leaf's resolved dependency graph (ALL
128    /// features) contains zero ML/GPU/tokenizer/framework crates.
129    ///
130    /// Parses `cargo metadata` (the real resolver output) — this is the same
131    /// closure the CI guard `scripts/check_format_sovereignty.sh` checks.
132    #[test]
133    fn test_falsify_aprf_sovereign_deps_no_ml_gpu() {
134        use cargo_metadata::MetadataCommand;
135
136        const FORBIDDEN: &[&str] = &[
137            "trueno",
138            "aprender-compute",
139            "aprender-gpu",
140            "aprender-core",
141            "wgpu",
142            "naga",
143            "cudarc",
144            "cust",
145            "candle-core",
146            "candle-nn",
147            "tch",
148            "torch-sys",
149        ];
150
151        let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
152        let metadata = MetadataCommand::new()
153            .manifest_path(&manifest)
154            .features(cargo_metadata::CargoOpt::AllFeatures)
155            .exec()
156            .expect("cargo metadata for apr-format");
157
158        // Resolve the transitive closure of the apr-format package id.
159        let resolve = metadata.resolve.expect("resolve graph present");
160        let id2name: HashMap<_, _> = metadata
161            .packages
162            .iter()
163            .map(|p| (p.id.clone(), p.name.clone()))
164            .collect();
165        let root = metadata
166            .packages
167            .iter()
168            .find(|p| p.name == "apr-format")
169            .map(|p| p.id.clone())
170            .expect("apr-format package present");
171        let nodes: HashMap<_, _> = resolve.nodes.iter().map(|n| (n.id.clone(), n)).collect();
172
173        let mut seen = std::collections::HashSet::new();
174        let mut stack = vec![root];
175        while let Some(id) = stack.pop() {
176            if !seen.insert(id.clone()) {
177                continue;
178            }
179            if let Some(node) = nodes.get(&id) {
180                for dep in &node.deps {
181                    stack.push(dep.pkg.clone());
182                }
183            }
184        }
185
186        let names: std::collections::HashSet<&str> = seen
187            .iter()
188            .filter_map(|id| id2name.get(id).map(String::as_str))
189            .collect();
190
191        let leaked: Vec<&str> = FORBIDDEN
192            .iter()
193            .copied()
194            .filter(|f| names.contains(f))
195            .collect();
196
197        assert!(
198            leaked.is_empty(),
199            "apr-format leaf is NO LONGER sovereign — forbidden ML/GPU/framework \
200             crate(s) leaked into its dependency closure: {leaked:?}"
201        );
202    }
203
204    /// FALSIFY-APRF-CRC-INTEGRITY: the deduplicated `crc32` matches the canonical
205    /// check vector AND validates the golden file's stored trailer.
206    #[test]
207    fn test_falsify_aprf_crc_integrity_matches_legacy() {
208        // Canonical IEEE CRC32 check vector "123456789" -> 0xCBF43926.
209        assert_eq!(crate::crc32(b"123456789"), 0xCBF4_3926);
210        assert_eq!(crate::crc32(&[]), 0x0000_0000);
211        assert_eq!(crate::crc32(&[0x00]), 0xD202_EF8D);
212
213        // The leaf's crc32 must validate the core-written golden trailer.
214        let bytes = golden_v1_bytes();
215        let stored = u32::from_le_bytes([
216            bytes[bytes.len() - 4],
217            bytes[bytes.len() - 3],
218            bytes[bytes.len() - 2],
219            bytes[bytes.len() - 1],
220        ]);
221        let computed = crate::crc32(&bytes[..bytes.len() - 4]);
222        assert_eq!(
223            stored, computed,
224            "leaf crc32 diverged from the legacy table/fold — existing .apr files \
225             would fail integrity"
226        );
227
228        // Corrupting one body byte must change the checksum (integrity bite).
229        let mut tampered = bytes.clone();
230        tampered[crate::HEADER_SIZE + 1] ^= 0xFF;
231        let recomputed = crate::crc32(&tampered[..tampered.len() - 4]);
232        assert_ne!(recomputed, stored, "crc32 failed to detect a flipped byte");
233    }
234
235    /// FALSIFY-APRF-METADATA-FIDELITY: a save->load round-trip preserves every
236    /// populated metadata field exactly, and a license sets the LICENSED flag.
237    #[test]
238    fn test_falsify_aprf_metadata_fidelity_roundtrip() {
239        use crate::types::{Header, LicenseInfo, LicenseTier, TrainingInfo, HEADER_SIZE};
240
241        let mut hyper = HashMap::new();
242        hyper.insert("lr".to_string(), serde_json::json!(0.001));
243        let mut metrics = HashMap::new();
244        metrics.insert("acc".to_string(), serde_json::json!(0.97));
245        let mut custom = HashMap::new();
246        custom.insert("note".to_string(), serde_json::json!("hello"));
247
248        let metadata = Metadata {
249            created_at: "1234567890".to_string(),
250            aprender_version: "9.9.9-test".to_string(),
251            model_name: Some("fidelity".to_string()),
252            description: Some("round-trip every field".to_string()),
253            training: Some(TrainingInfo {
254                samples: Some(42),
255                duration_ms: Some(1000),
256                source: Some("unit-test".to_string()),
257            }),
258            hyperparameters: hyper,
259            metrics,
260            custom,
261            distillation: Some("teacher-hash".to_string()),
262            distillation_info: None,
263            license: Some(LicenseInfo {
264                uuid: "uuid-1".to_string(),
265                hash: "hash-1".to_string(),
266                expiry: None,
267                seats: Some(3),
268                licensee: Some("ACME".to_string()),
269                tier: LicenseTier::Enterprise,
270            }),
271            model_card: None,
272        };
273
274        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
275        struct M {
276            v: Vec<f32>,
277        }
278        let model = M { v: vec![1.0, 2.0] };
279
280        let dir = std::env::temp_dir();
281        let path = dir.join("aprf_metadata_fidelity.apr");
282        let options = SaveOptions {
283            compression: Compression::None,
284            metadata: metadata.clone(),
285            quality_score: None,
286        };
287        crate::save(&model, ModelType::LinearRegression, &path, options).expect("save");
288
289        // License presence must set the LICENSED header flag.
290        let raw = std::fs::read(&path).expect("read");
291        let header = Header::from_bytes(&raw[..HEADER_SIZE]).expect("hdr");
292        assert!(header.flags.is_licensed(), "LICENSED flag not set");
293
294        // Every populated field survives the round-trip via inspect().
295        let info = crate::inspect(&path).expect("inspect");
296        let _ = std::fs::remove_file(&path);
297        let m = info.metadata;
298        assert_eq!(m.created_at, metadata.created_at);
299        assert_eq!(m.aprender_version, metadata.aprender_version);
300        assert_eq!(m.model_name, metadata.model_name);
301        assert_eq!(m.description, metadata.description);
302        assert_eq!(m.distillation, metadata.distillation);
303        assert_eq!(m.hyperparameters, metadata.hyperparameters);
304        assert_eq!(m.metrics, metadata.metrics);
305        assert_eq!(m.custom, metadata.custom);
306        let (got, want) = (m.license.expect("lic"), metadata.license.expect("lic"));
307        assert_eq!(got.uuid, want.uuid);
308        assert_eq!(got.hash, want.hash);
309        assert_eq!(got.seats, want.seats);
310        assert_eq!(got.licensee, want.licensee);
311        assert_eq!(got.tier, want.tier);
312        let tr = m.training.expect("training");
313        assert_eq!(tr.samples, Some(42));
314    }
315
316    /// FALSIFY-APRF-API-COMPAT: the leaf's public re-export surface resolves
317    /// (the container types/functions the framework re-exports are all reachable
318    /// from `apr_format::*`). The cross-crate `?`-ergonomics half of this
319    /// obligation is proven in `aprender-core`
320    /// (`test_from_apr_format_question_mark_ergonomics`).
321    #[test]
322    fn test_falsify_aprf_api_compat_reexport_resolves() {
323        // Touch the public re-export surface so a dropped re-export fails to
324        // compile (the framework re-exports exactly these paths to its callers).
325        let crc: u32 = crate::crc32(b"abc");
326        assert_eq!(crc, crate::crc32(b"abc"));
327        let bits: u16 = crate::f32_to_f16(1.0);
328        assert_eq!(crate::f16_to_f32(bits), 1.0);
329
330        let hdr = crate::Header::new(ModelType::LinearRegression);
331        assert_eq!(hdr.magic, crate::MAGIC);
332        assert_eq!(crate::HEADER_SIZE, 32);
333
334        let _info_ty: Option<crate::ModelInfo> = None;
335        let _opts = crate::SaveOptions::default();
336        let v2 = crate::v2::AprV2Header::new();
337        assert_eq!(v2.magic, crate::v2::MAGIC_V2);
338        let card = crate::ModelCard::new("m", "1.0.0");
339        assert_eq!(card.version, "1.0.0");
340
341        // A full save->load round-trip through the re-exported entry points.
342        let dir = std::env::temp_dir();
343        let path = dir.join("aprf_api_compat.apr");
344        crate::save(
345            &vec![1.0_f32, 2.0],
346            ModelType::LinearRegression,
347            &path,
348            crate::SaveOptions::default(),
349        )
350        .expect("save via re-export");
351        let back: Vec<f32> =
352            crate::load(&path, ModelType::LinearRegression).expect("load via re-export");
353        let _ = std::fs::remove_file(&path);
354        assert_eq!(back, vec![1.0, 2.0]);
355    }
356
357    /// FALSIFY-APRF-QUALITY-GATE: the Jidoka quality gate is preserved —
358    /// `save(quality_score = Some(0))` is REFUSED and a known-good save is `Ok`.
359    #[test]
360    fn test_falsify_aprf_quality_gate_preserved() {
361        #[derive(Serialize)]
362        struct M {
363            v: Vec<f32>,
364        }
365        let model = M { v: vec![1.0] };
366        let dir = std::env::temp_dir();
367
368        // Some(0): explicit failure — must be refused.
369        let bad = SaveOptions {
370            quality_score: Some(0),
371            ..Default::default()
372        };
373        let refused = crate::save(
374            &model,
375            ModelType::LinearRegression,
376            dir.join("aprf_qgate_bad.apr"),
377            bad,
378        );
379        assert!(
380            matches!(refused, Err(crate::AprFormatError::ValidationError { .. })),
381            "Jidoka gate lost: save(Some(0)) was NOT refused"
382        );
383
384        // Some(85): passing — must be accepted.
385        let good = SaveOptions {
386            quality_score: Some(85),
387            ..Default::default()
388        };
389        let path = dir.join("aprf_qgate_good.apr");
390        let accepted = crate::save(&model, ModelType::LinearRegression, &path, good);
391        assert!(accepted.is_ok(), "known-good save(Some(85)) was refused");
392        let _ = std::fs::remove_file(&path);
393    }
394}