use std::path::{Path, PathBuf};
use crate::ontology::rdf::{iri, Graph, Term, RDF_TYPE};
use crate::ontology::shapes::RDFS_SUBCLASS_OF;
pub const SCHEMA: &str = "apr-parity-receipt/v2";
pub const EVIDENCE_DIR: &str = "evidence/parity";
pub const EXPECTED_FILE: &str = "evidence/parity/EXPECTED_RECEIPTS";
#[must_use]
pub fn parity(name: &str) -> String {
format!("{}parity/{name}", crate::ontology::rdf::ONT_BASE)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParityError {
pub file: String,
pub what: String,
}
impl std::fmt::Display for ParityError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.file, self.what)
}
}
impl std::error::Error for ParityError {}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ParityStats {
pub records: usize,
pub skipped: usize,
pub threshold_source_missing: usize,
pub errors: Vec<ParityError>,
pub expected: Option<usize>,
}
impl ParityStats {
#[must_use]
pub fn wrong_corpus(&self) -> Option<(usize, usize)> {
match self.expected {
Some(n) if n != self.records => Some((n, self.records)),
_ => None,
}
}
}
fn read_expected(root: &Path) -> Option<usize> {
let text = std::fs::read_to_string(root.join(EXPECTED_FILE)).ok()?;
text.lines()
.map(str::trim)
.find(|l| !l.is_empty() && !l.starts_with('#'))
.and_then(|l| l.parse().ok())
}
fn looks_legacy(v: &serde_json::Value) -> bool {
v.get("metrics").is_some_and(serde_json::Value::is_array) || v.get("parity").is_some()
}
pub fn extract(root: &Path, g: &mut Graph) -> ParityStats {
let mut stats = ParityStats {
expected: read_expected(root),
..ParityStats::default()
};
let mut files = Vec::new();
walk(&root.join(EVIDENCE_DIR), &mut files);
files.sort();
if !files.is_empty() {
declare_classes(g);
}
for f in files {
let rel = f
.strip_prefix(root)
.unwrap_or(&f)
.to_string_lossy()
.replace('\\', "/");
let Ok(text) = std::fs::read_to_string(&f) else {
stats.errors.push(ParityError {
file: rel,
what: "unreadable".into(),
});
continue;
};
let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
stats.errors.push(ParityError {
file: rel,
what: "not JSON".into(),
});
continue;
};
match v.get("schema").and_then(serde_json::Value::as_str) {
Some(SCHEMA) => {
emit(g, root, &rel, &v, &mut stats);
stats.records += 1;
}
other if looks_legacy(&v) => {
stats.errors.push(ParityError {
file: rel,
what: format!(
"an UNMIGRATED logit-parity record: schema {} is not {SCHEMA}. The legacy layout \
was migrated by #3577; a record the extractor cannot see is a record no shape \
can refuse.",
other.map_or_else(|| "absent".to_string(), |s| format!("{s:?}"))
),
});
}
_ => stats.skipped += 1,
}
}
stats
}
fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else {
return;
};
for e in rd.flatten() {
let p = e.path();
if p.is_dir() {
walk(&p, out);
} else if p.extension().and_then(|x| x.to_str()) == Some("json") {
out.push(p);
}
}
}
fn declare_classes(g: &mut Graph) {
for child in ["SelfComparedReceipt", "OracleComparedReceipt"] {
g.insert(
parity(child),
RDFS_SUBCLASS_OF.to_string(),
Term::iri(parity("ParityReceipt")),
);
}
}
fn s(v: &serde_json::Value, k: &str) -> Option<String> {
v.get(k).and_then(|x| match x {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Null => None,
other => Some(other.to_string()),
})
}
fn emit(g: &mut Graph, root: &Path, rel: &str, v: &serde_json::Value, stats: &mut ParityStats) {
let node = iri("parity-receipt", rel);
let comparator = v.get("comparator");
let kind = comparator.and_then(|c| s(c, "kind")).unwrap_or_default();
let class = if kind == "self" {
"SelfComparedReceipt"
} else {
"OracleComparedReceipt"
};
g.insert(node.clone(), RDF_TYPE, Term::iri(parity(class)));
g.insert(node.clone(), parity("file"), Term::string(rel));
for (key, prop) in [
("host", "host"),
("backend", "backend"),
("apr_version", "aprVersion"),
("generated_at", "generatedAt"),
("threshold_source", "thresholdSource"),
] {
if let Some(val) = s(v, key) {
g.insert(node.clone(), parity(prop), Term::string(val));
}
}
if let Some(b) = v
.get("partially_receipted")
.and_then(serde_json::Value::as_bool)
{
g.insert(node.clone(), parity("partiallyReceipted"), Term::boolean(b));
}
if let Some(sha) = v.get("cell").and_then(|c| s(c, "model_sha256")) {
g.insert(node.clone(), parity("modelSha256"), Term::string(sha));
}
for u in v
.get("unmeasured")
.and_then(serde_json::Value::as_array)
.into_iter()
.flatten()
.filter_map(serde_json::Value::as_str)
{
g.insert(node.clone(), parity("unmeasured"), Term::string(u));
}
if let Some(src) = s(v, "threshold_source") {
if !root.join(&src).exists() {
stats.threshold_source_missing += 1;
g.insert(
node.clone(),
parity("thresholdSourceMissing"),
Term::string(src),
);
}
}
if let Some(c) = comparator {
let cnode = iri("parity-comparator", rel);
g.insert(cnode.clone(), RDF_TYPE, Term::iri(parity("Comparator")));
g.insert(cnode.clone(), parity("kind"), Term::string(&kind));
if let Some(sha) = s(c, "comparator_sha") {
g.insert(cnode.clone(), parity("comparatorSha"), Term::string(sha));
}
if let Some(r) = s(c, "reason") {
g.insert(cnode.clone(), parity("reason"), Term::string(r));
}
g.insert(node, parity("comparator"), Term::iri(cnode));
}
}
const CONTROL_SAMPLE: &str = r#"{
"cell": {"model": "pc", "file": "./pc.gguf", "quant": "Q4_K_M"},
"host": "pc-host",
"backend": "cuda",
"apr_version": "0.0.0",
"generated_at": "1970-01-01",
"comparator": {"kind": "self", "reason": "positive control"},
"partially_receipted": true,
"threshold_source": "evidence/parity/thresholds.yaml",
"unmeasured": ["ORACLE ARM: not measured."],
"result": {"positions": 1, "parity": true},
"raw": {"model": "./pc.gguf", "metrics": []}
}"#;
#[must_use]
pub fn control_sample() -> serde_json::Value {
let mut v: serde_json::Value = serde_json::from_str(CONTROL_SAMPLE).unwrap_or_default();
if let Some(o) = v.as_object_mut() {
o.insert("schema".into(), SCHEMA.into());
}
v
}
#[must_use]
pub fn positive_control(sample: &serde_json::Value) -> bool {
let mut g = Graph::new();
let mut stats = ParityStats::default();
let root = Path::new(".");
emit(&mut g, root, "__pc_sample__", sample, &mut stats);
let with = !g
.objects(
&iri("parity-receipt", "__pc_sample__"),
&parity("comparator"),
)
.is_empty();
let mut stripped = sample.clone();
if let Some(o) = stripped.as_object_mut() {
o.remove("comparator");
}
let mut g2 = Graph::new();
emit(&mut g2, root, "__pc_planted__", &stripped, &mut stats);
let without = g2
.objects(
&iri("parity-receipt", "__pc_planted__"),
&parity("comparator"),
)
.is_empty();
with && without
}
#[cfg(test)]
#[path = "parity_receipt_tests.rs"]
mod tests;