use std::path::{Path, PathBuf};
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct CitationIntegrityReport {
pub violations: Vec<String>,
pub files_checked: usize,
}
impl CitationIntegrityReport {
#[must_use]
pub const fn ok(&self) -> bool {
self.violations.is_empty()
}
}
fn internal_citations_enabled(config_path: Option<&Path>) -> bool {
config_path
.filter(|path| path.is_file())
.and_then(|path| std::fs::read_to_string(path).ok())
.and_then(|contents| serde_json::from_str::<Value>(&contents).ok())
.and_then(|config| {
config
.pointer("/features/internalCitations")
.and_then(Value::as_bool)
})
.unwrap_or(false)
}
fn finding_id(finding: &Value, index: usize) -> String {
finding
.get("@id")
.and_then(Value::as_str)
.map_or_else(|| format!("#{index}"), str::to_string)
}
fn jq_tostring(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
fn title_or_untitled(citation: &Value) -> String {
match citation.get("title") {
None | Some(Value::Null | Value::Bool(false)) => "<untitled>".to_string(),
Some(title) => jq_tostring(title),
}
}
fn is_traceable(citation: &Value, internal_ok: bool) -> bool {
let has_url = citation
.get("url")
.and_then(Value::as_str)
.is_some_and(|url| url.starts_with("http://") || url.starts_with("https://"));
let has_internal_note = internal_ok
&& citation
.get("citationType")
.and_then(Value::as_str)
.is_some_and(|t| t.starts_with("internal:"))
&& citation
.get("note")
.and_then(Value::as_str)
.is_some_and(|note| !note.is_empty());
has_url || has_internal_note
}
fn citation_role_missing(citation: &Value) -> bool {
citation
.get("citationRole")
.and_then(Value::as_str)
.is_none_or(str::is_empty)
}
fn check_finding(
file: &str,
finding: &Value,
index: usize,
internal_ok: bool,
violations: &mut Vec<String>,
) {
let loc = format!("{file}:{}: ", finding_id(finding, index));
let citations: Vec<&Value> = finding
.get("citations")
.and_then(Value::as_array)
.into_iter()
.flatten()
.collect();
let verdict = finding
.pointer("/extensions/harness/verification/verdict")
.and_then(Value::as_str)
.unwrap_or("inconclusive");
if citations.is_empty() {
violations.push(format!("{loc}no citations (MIF Level 3 requires >=1)"));
}
for citation in &citations {
if !is_traceable(citation, internal_ok) {
let title = title_or_untitled(citation);
let reason = if internal_ok {
"citation has neither a well-formed http(s) URL nor an internal: source with a note"
} else {
"citation missing well-formed http(s) URL"
};
violations.push(format!("{loc}{reason}: {title}"));
}
}
for citation in &citations {
if citation_role_missing(citation) {
violations.push(format!(
"{loc}citation missing citationRole: {}",
title_or_untitled(citation)
));
}
}
if verdict == "falsified" {
violations.push(format!(
"{loc}adversarial verdict is falsified; finding must not ship"
));
}
let dead_urls = finding
.pointer("/extensions/harness/citationStatus/deadUrls")
.and_then(Value::as_array)
.into_iter()
.flatten();
for dead in dead_urls {
violations.push(format!(
"{loc}citation URL listed dead: {}",
jq_tostring(dead)
));
}
}
#[must_use]
pub fn check_citation_integrity(
paths: &[PathBuf],
config_path: Option<&Path>,
) -> CitationIntegrityReport {
let internal_ok = internal_citations_enabled(config_path);
let mut violations = Vec::new();
for path in paths {
let file = path.display().to_string();
if !path.is_file() {
violations.push(format!("{file}: file not found"));
continue;
}
let parsed = std::fs::read_to_string(path)
.ok()
.and_then(|contents| serde_json::from_str::<Value>(&contents).ok());
let Some(parsed) = parsed else {
violations.push(format!("{file}: not valid JSON"));
continue;
};
let findings: Vec<&Value> = match &parsed {
Value::Array(items) => items.iter().collect(),
other => vec![other],
};
for (index, finding) in findings.iter().enumerate() {
check_finding(&file, finding, index, internal_ok, &mut violations);
}
}
CitationIntegrityReport {
violations,
files_checked: paths.len(),
}
}
#[cfg(test)]
mod tests {
use super::check_citation_integrity;
use std::fs;
use std::path::PathBuf;
fn write(dir: &std::path::Path, name: &str, contents: &str) -> PathBuf {
let path = dir.join(name);
fs::write(&path, contents).unwrap();
path
}
#[test]
fn passes_a_well_formed_finding_with_a_url_citation() {
let dir = tempfile::tempdir().unwrap();
let path = write(
dir.path(),
"good.json",
r#"{"@id":"urn:mif:f1","citations":[{"url":"https://example.com","citationRole":"supports","title":"Example"}]}"#,
);
let report = check_citation_integrity(&[path], None);
assert!(report.ok(), "{:?}", report.violations);
assert_eq!(report.files_checked, 1);
}
#[test]
fn flags_a_finding_with_no_citations() {
let dir = tempfile::tempdir().unwrap();
let path = write(
dir.path(),
"bad.json",
r#"{"@id":"urn:mif:f1","citations":[]}"#,
);
let report = check_citation_integrity(&[path], None);
assert_eq!(report.violations.len(), 1);
assert!(report.violations[0].contains("no citations"));
}
#[test]
fn flags_a_citation_missing_a_well_formed_url() {
let dir = tempfile::tempdir().unwrap();
let path = write(
dir.path(),
"bad.json",
r#"{"@id":"urn:mif:f1","citations":[{"url":"ftp://example.com","citationRole":"supports","title":"Bad"}]}"#,
);
let report = check_citation_integrity(&[path], None);
assert!(
report
.violations
.iter()
.any(|v| v.contains("citation missing well-formed http(s) URL: Bad"))
);
}
#[test]
fn accepts_an_internal_citation_only_when_the_feature_flag_is_enabled() {
let dir = tempfile::tempdir().unwrap();
let path = write(
dir.path(),
"finding.json",
r#"{"@id":"urn:mif:f1","citations":[{"citationType":"internal:doc","note":"quoted evidence","citationRole":"supports","title":"Internal"}]}"#,
);
let strict = check_citation_integrity(std::slice::from_ref(&path), None);
assert!(
strict
.violations
.iter()
.any(|v| v.contains("citation missing well-formed http(s) URL"))
);
let config = write(
dir.path(),
"harness.config.json",
r#"{"features":{"internalCitations":true}}"#,
);
let lenient = check_citation_integrity(&[path], Some(&config));
assert!(lenient.ok(), "{:?}", lenient.violations);
}
#[test]
fn flags_a_citation_missing_citation_role() {
let dir = tempfile::tempdir().unwrap();
let path = write(
dir.path(),
"bad.json",
r#"{"@id":"urn:mif:f1","citations":[{"url":"https://example.com","title":"NoRole"}]}"#,
);
let report = check_citation_integrity(&[path], None);
assert!(
report
.violations
.iter()
.any(|v| v.contains("citation missing citationRole: NoRole"))
);
}
#[test]
fn flags_a_falsified_finding() {
let dir = tempfile::tempdir().unwrap();
let path = write(
dir.path(),
"bad.json",
r#"{"@id":"urn:mif:f1","citations":[{"url":"https://example.com","citationRole":"supports"}],
"extensions":{"harness":{"verification":{"verdict":"falsified"}}}}"#,
);
let report = check_citation_integrity(&[path], None);
assert!(
report
.violations
.iter()
.any(|v| v.contains("adversarial verdict is falsified"))
);
}
#[test]
fn flags_a_citation_url_listed_dead() {
let dir = tempfile::tempdir().unwrap();
let path = write(
dir.path(),
"bad.json",
r#"{"@id":"urn:mif:f1","citations":[{"url":"https://example.com","citationRole":"supports"}],
"extensions":{"harness":{"citationStatus":{"deadUrls":["https://example.com"]}}}}"#,
);
let report = check_citation_integrity(&[path], None);
assert!(
report
.violations
.iter()
.any(|v| v.contains("citation URL listed dead: https://example.com"))
);
}
#[test]
fn handles_an_array_of_findings_and_derives_a_positional_id_when_id_is_absent() {
let dir = tempfile::tempdir().unwrap();
let path = write(
dir.path(),
"many.json",
r#"[{"citations":[]},{"@id":"urn:mif:f2","citations":[]}]"#,
);
let report = check_citation_integrity(&[path], None);
assert_eq!(report.violations.len(), 2);
assert!(report.violations[0].contains(":#0: no citations"));
assert!(report.violations[1].contains(":urn:mif:f2: no citations"));
}
#[test]
fn records_a_missing_file_as_a_violation_not_a_hard_error() {
let report = check_citation_integrity(&[PathBuf::from("/no/such/file.json")], None);
assert_eq!(report.violations.len(), 1);
assert!(report.violations[0].contains("file not found"));
}
#[test]
fn records_invalid_json_as_a_violation() {
let dir = tempfile::tempdir().unwrap();
let path = write(dir.path(), "broken.json", "not json");
let report = check_citation_integrity(&[path], None);
assert_eq!(report.violations.len(), 1);
assert!(report.violations[0].contains("not valid JSON"));
}
}