use std::path::{Path, PathBuf};
use crate::compare::{reference_zic, zdump};
use crate::error::Result;
use crate::json::escape;
use crate::manifest::{ArtifactCategory, OracleMode};
use crate::model::Database;
const SCHEMA: &str = "zic-rs-semantic-report-v1";
const FIXTURE_SET: &str = "semantic-witness-seed-v1";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BuildAxisEvidence {
Known(String),
UnknownUnmeasured,
UnavailableOnThisHost,
NotApplicable,
DocumentationOnly,
InferredForbidden,
}
impl BuildAxisEvidence {
fn disposition(&self) -> &'static str {
match self {
BuildAxisEvidence::Known(_) => "known",
BuildAxisEvidence::UnknownUnmeasured => "unknown_unmeasured",
BuildAxisEvidence::UnavailableOnThisHost => "unavailable_on_this_host",
BuildAxisEvidence::NotApplicable => "not_applicable",
BuildAxisEvidence::DocumentationOnly => "documentation_only",
BuildAxisEvidence::InferredForbidden => "inferred_forbidden",
}
}
fn to_json(&self) -> String {
let value = match self {
BuildAxisEvidence::Known(v) => escape(v),
_ => "null".to_string(),
};
format!(
"{{ \"disposition\": {}, \"value\": {} }}",
escape(self.disposition()),
value
)
}
}
#[derive(Debug, Clone)]
pub struct ReferenceBuildProfile {
pub source_release: BuildAxisEvidence,
pub binary_sha256: BuildAxisEvidence,
pub reference_platform: BuildAxisEvidence,
pub build_flags: BuildAxisEvidence,
pub time_t_model: BuildAxisEvidence,
pub runtime_leap_support: BuildAxisEvidence,
pub tzdir_resolution_policy: BuildAxisEvidence,
pub locale: BuildAxisEvidence,
pub warning_thresholds: BuildAxisEvidence,
}
impl ReferenceBuildProfile {
fn capture(
zic_version: &Option<String>,
zic_binary_sha256: &Option<String>,
env_lc_all: &Option<String>,
) -> Self {
let from_opt = |o: &Option<String>| match o {
Some(v) => BuildAxisEvidence::Known(v.clone()),
None => BuildAxisEvidence::UnknownUnmeasured,
};
ReferenceBuildProfile {
source_release: from_opt(zic_version),
binary_sha256: match zic_binary_sha256 {
Some(v) => BuildAxisEvidence::Known(v.clone()),
None => BuildAxisEvidence::UnavailableOnThisHost,
},
reference_platform: BuildAxisEvidence::Known(std::env::consts::OS.to_string()),
build_flags: BuildAxisEvidence::InferredForbidden,
time_t_model: BuildAxisEvidence::InferredForbidden,
runtime_leap_support: BuildAxisEvidence::UnknownUnmeasured,
tzdir_resolution_policy: BuildAxisEvidence::Known(
"explicit_tzif_path_argument".to_string(),
),
locale: from_opt(env_lc_all),
warning_thresholds: BuildAxisEvidence::UnknownUnmeasured,
}
}
fn to_json(&self) -> String {
format!(
"{{ \"source_release\": {}, \"binary_sha256\": {}, \"reference_platform\": {}, \
\"build_flags\": {}, \"time_t_model\": {}, \"runtime_leap_support\": {}, \
\"tzdir_resolution_policy\": {}, \"locale\": {}, \"warning_thresholds\": {} }}",
self.source_release.to_json(),
self.binary_sha256.to_json(),
self.reference_platform.to_json(),
self.build_flags.to_json(),
self.time_t_model.to_json(),
self.runtime_leap_support.to_json(),
self.tzdir_resolution_policy.to_json(),
self.locale.to_json(),
self.warning_thresholds.to_json(),
)
}
}
#[derive(Debug, Clone)]
pub struct OracleIdentity {
pub zic_program: String,
pub zdump_program: String,
pub zic_version: Option<String>,
pub zdump_version: Option<String>,
pub zic_binary_sha256: Option<String>,
pub zdump_binary_sha256: Option<String>,
pub zdump_command_line: String,
pub zoneinfo_resolution: &'static str,
pub env_tz: Option<String>,
pub env_lc_all: Option<String>,
pub reference_platform: &'static str,
pub build_profile: ReferenceBuildProfile,
}
fn tool_version(program: &str) -> Option<String> {
let out = std::process::Command::new(program)
.arg("--version")
.output()
.ok()?;
String::from_utf8_lossy(&out.stdout)
.lines()
.next()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
}
fn resolve_program(program: &str) -> Option<std::path::PathBuf> {
if program.contains('/') {
let p = std::path::PathBuf::from(program);
return p.exists().then_some(p);
}
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path)
.map(|dir| dir.join(program))
.find(|cand| cand.is_file())
}
fn binary_sha256(program: &str) -> Option<String> {
let path = resolve_program(program)?;
let bytes = std::fs::read(path).ok()?;
Some(crate::hash::sha256_hex(&bytes))
}
impl OracleIdentity {
fn capture(zic: &str, zdump: &str) -> Self {
OracleIdentity {
zic_program: zic.to_string(),
zdump_program: zdump.to_string(),
zic_version: tool_version(zic),
zdump_version: tool_version(zdump),
zic_binary_sha256: binary_sha256(zic),
zdump_binary_sha256: binary_sha256(zdump),
zdump_command_line: format!("{zdump} -v -c {HORIZON_LO},{HORIZON_HI} <tzif>"),
zoneinfo_resolution: "explicit_tzif_path_argument",
env_tz: std::env::var("TZ").ok(),
env_lc_all: std::env::var("LC_ALL").ok(),
reference_platform: std::env::consts::OS,
build_profile: ReferenceBuildProfile::capture(
&tool_version(zic),
&binary_sha256(zic),
&std::env::var("LC_ALL").ok(),
),
}
}
fn to_json(&self) -> String {
let opt = |o: &Option<String>| match o {
Some(v) => escape(v),
None => "null".to_string(),
};
format!(
"{{ \"zic\": {}, \"zdump\": {}, \"zic_version\": {}, \"zdump_version\": {}, \
\"zic_binary_sha256\": {}, \"zdump_binary_sha256\": {}, \"zdump_command_line\": {}, \
\"zoneinfo_resolution\": {}, \"env_tz\": {}, \"env_lc_all\": {}, \
\"reference_platform\": {}, \"reference_build_profile\": {}, \
\"reference_admission\": {{ \"live_oracle\": {}, \"sealed_reference\": {} }} }}",
escape(&self.zic_program),
escape(&self.zdump_program),
opt(&self.zic_version),
opt(&self.zdump_version),
opt(&self.zic_binary_sha256),
opt(&self.zdump_binary_sha256),
escape(&self.zdump_command_line),
escape(self.zoneinfo_resolution),
opt(&self.env_tz),
opt(&self.env_lc_all),
escape(self.reference_platform),
self.build_profile.to_json(),
crate::manifest::ReferenceAdmission {
locator: crate::manifest::ReferenceLocatorKind::LiveCurrentDirectory,
trust: crate::manifest::SignatureTrustModel::Unknown,
}
.to_json(),
crate::manifest::ADMITTED_2026B_REFERENCE.to_json(),
)
}
}
const HORIZON_LO: i32 = 1900;
const HORIZON_HI: i32 = 2200;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SemanticWitnessVerdict {
Match,
Mismatch,
SkippedOracleUnavailable,
NotApplicable,
OutOfHorizon,
KnownDivergence,
}
impl SemanticWitnessVerdict {
pub fn as_str(self) -> &'static str {
match self {
SemanticWitnessVerdict::Match => "match",
SemanticWitnessVerdict::Mismatch => "mismatch",
SemanticWitnessVerdict::SkippedOracleUnavailable => "skipped_oracle_unavailable",
SemanticWitnessVerdict::NotApplicable => "not_applicable",
SemanticWitnessVerdict::OutOfHorizon => "out_of_horizon",
SemanticWitnessVerdict::KnownDivergence => "known_divergence",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemanticObservation {
pub utc: String,
pub offset_seconds: i32,
pub is_dst: bool,
pub abbreviation: String,
}
#[derive(Debug, Clone)]
pub struct SemanticWitness {
pub zone: String,
pub timestamp: String,
pub reference: Option<SemanticObservation>,
pub zic_rs: Option<SemanticObservation>,
pub verdict: SemanticWitnessVerdict,
}
#[derive(Debug, Clone)]
pub struct SemanticWitnessReport {
pub oracle_mode: OracleMode,
pub oracle_identity: OracleIdentity,
pub horizon: (i32, i32),
pub witnesses: Vec<SemanticWitness>,
}
fn parse_obs(line: &str) -> Option<SemanticObservation> {
let (utc, rest) = line.split_once(" UT = ")?;
let toks: Vec<&str> = rest.split_whitespace().collect();
let isdst_pos = toks.iter().position(|t| t.starts_with("isdst="))?;
let gmtoff = toks.iter().find_map(|t| t.strip_prefix("gmtoff="))?;
let isdst = toks[isdst_pos].strip_prefix("isdst=")?;
let abbr = toks.get(isdst_pos.checked_sub(1)?)?;
Some(SemanticObservation {
utc: utc.trim().to_string(),
offset_seconds: gmtoff.parse().ok()?,
is_dst: isdst == "1",
abbreviation: (*abbr).to_string(),
})
}
fn representative_indices(n: usize) -> Vec<usize> {
if n == 0 {
return Vec::new();
}
let mut idx: Vec<usize> = Vec::new();
for i in [0usize, 1, 2] {
if i < n {
idx.push(i);
}
}
for i in [n.saturating_sub(2), n.saturating_sub(1)] {
if i >= 3 && !idx.contains(&i) {
idx.push(i);
}
}
idx
}
impl SemanticWitnessReport {
pub fn to_json(&self) -> String {
let mut s = String::new();
s.push_str("{\n");
s.push_str(&format!(" \"schema\": {},\n", escape(SCHEMA)));
s.push_str(&format!(
" \"artifact_category\": {},\n",
escape(ArtifactCategory::SemanticWitnessArtifact.as_str())
));
s.push_str(&format!(" \"fixture_set\": {},\n", escape(FIXTURE_SET)));
s.push_str(" \"witness_scope\": \"small_seed\",\n");
s.push_str(&format!(
" \"oracle_mode\": {},\n",
self.oracle_mode.to_json_field()
));
s.push_str(&format!(
" \"oracle_identity\": {},\n",
self.oracle_identity.to_json()
));
s.push_str(&format!(
" \"witness_horizon\": {{ \"start\": {}, \"end\": {}, \"reason\": {} }},\n",
self.horizon.0,
self.horizon.1,
escape(
"witnesses are scoped to this oracle horizon; a match is 'matched for the declared \
witness set', NOT a claim of universal semantic parity"
)
));
s.push_str(&format!(
" \"note\": {},\n",
escape(
"a semantic witness proves selected (offset, is_dst, abbreviation) behaviour under the \
zdump oracle; it is NOT a claim of RFC 9636 structural validity (that is the structural \
validator, T15.4)."
)
));
s.push_str(" \"witnesses\": [");
for (i, w) in self.witnesses.iter().enumerate() {
s.push_str(if i == 0 { "\n" } else { ",\n" });
let obs = |o: &Option<SemanticObservation>| match o {
Some(o) => format!(
"{{ \"offset_seconds\": {}, \"is_dst\": {}, \"abbreviation\": {} }}",
o.offset_seconds,
o.is_dst,
escape(&o.abbreviation)
),
None => "null".to_string(),
};
s.push_str(&format!(
" {{ \"zone\": {}, \"timestamp\": {}, \"reference\": {}, \"zic_rs\": {}, \
\"verdict\": {}, \"artifact_category\": {} }}",
escape(&w.zone),
escape(&w.timestamp),
obs(&w.reference),
obs(&w.zic_rs),
escape(w.verdict.as_str()),
escape(ArtifactCategory::SemanticWitnessArtifact.as_str()),
));
}
s.push_str(if self.witnesses.is_empty() {
"]\n"
} else {
"\n ]\n"
});
s.push_str("}\n");
s
}
}
pub fn build_semantic_witness_report(
db: &Database,
zones: &[String],
reference_zic: &str,
zdump_program: &str,
inputs: &[PathBuf],
work_dir: &Path,
) -> Result<SemanticWitnessReport> {
if !reference_zic::is_available(reference_zic) || !zdump::is_available(zdump_program) {
let reason = format!(
"reference oracle unavailable (need `{reference_zic}` and `{zdump_program}` on PATH)"
);
let witnesses = zones
.iter()
.map(|z| SemanticWitness {
zone: z.clone(),
timestamp: "-".into(),
reference: None,
zic_rs: None,
verdict: SemanticWitnessVerdict::SkippedOracleUnavailable,
})
.collect();
return Ok(SemanticWitnessReport {
oracle_mode: OracleMode::Unavailable(reason),
oracle_identity: OracleIdentity::capture(reference_zic, zdump_program),
horizon: (HORIZON_LO, HORIZON_HI),
witnesses,
});
}
let ref_root = work_dir.join("ref");
reference_zic::compile_with_reference(reference_zic, inputs, &ref_root)?;
let ours_root = work_dir.join("ours");
let mut witnesses = Vec::new();
for zone in zones {
let ours_bytes = match crate::compile_zone_to_bytes(db, zone) {
Ok(b) => b,
Err(_) => {
witnesses.push(SemanticWitness {
zone: zone.clone(),
timestamp: "-".into(),
reference: None,
zic_rs: None,
verdict: SemanticWitnessVerdict::NotApplicable,
});
continue;
}
};
let ours_path = ours_root.join(zone);
if let Some(parent) = ours_path.parent() {
std::fs::create_dir_all(parent).ok();
}
std::fs::write(&ours_path, &ours_bytes).ok();
let ref_path = reference_zic::compiled_path(&ref_root, zone);
let ours_dump = zdump::run(zdump_program, &ours_path, HORIZON_LO, HORIZON_HI)?;
let ref_dump = zdump::run(zdump_program, &ref_path, HORIZON_LO, HORIZON_HI)?;
let ours_obs: Vec<SemanticObservation> =
ours_dump.iter().filter_map(|l| parse_obs(l)).collect();
let ref_obs: Vec<SemanticObservation> =
ref_dump.iter().filter_map(|l| parse_obs(l)).collect();
let n = ours_obs.len().max(ref_obs.len());
for i in representative_indices(n) {
let zr = ours_obs.get(i).cloned();
let rf = ref_obs.get(i).cloned();
let verdict = match (&zr, &rf) {
(Some(a), Some(b))
if a.offset_seconds == b.offset_seconds
&& a.is_dst == b.is_dst
&& a.abbreviation == b.abbreviation =>
{
SemanticWitnessVerdict::Match
}
(Some(_), Some(_)) => SemanticWitnessVerdict::Mismatch,
_ => SemanticWitnessVerdict::NotApplicable,
};
let timestamp = zr
.as_ref()
.or(rf.as_ref())
.map(|o| o.utc.clone())
.unwrap_or_else(|| "-".into());
witnesses.push(SemanticWitness {
zone: zone.clone(),
timestamp,
reference: rf,
zic_rs: zr,
verdict,
});
}
}
Ok(SemanticWitnessReport {
oracle_mode: OracleMode::ReferenceZdump,
oracle_identity: OracleIdentity::capture(reference_zic, zdump_program),
horizon: (HORIZON_LO, HORIZON_HI),
witnesses,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_a_valid_zdump_line() {
let o = parse_obs(
"Sun Mar 14 06:59:59 2021 UT = Sun Mar 14 01:59:59 2021 EST isdst=0 gmtoff=-18000",
)
.unwrap();
assert_eq!(o.offset_seconds, -18000);
assert!(!o.is_dst);
assert_eq!(o.abbreviation, "EST");
}
#[test]
fn rejects_out_of_range_sentinel_line() {
assert!(parse_obs(
"Thu Jan 1 00:00:00 -2147481748 UT = -67768040609740800 (localtime failed)"
)
.is_none());
}
#[test]
fn verdict_vocab_is_finite_and_stable() {
for (v, s) in [
(SemanticWitnessVerdict::Match, "match"),
(SemanticWitnessVerdict::Mismatch, "mismatch"),
(
SemanticWitnessVerdict::SkippedOracleUnavailable,
"skipped_oracle_unavailable",
),
(SemanticWitnessVerdict::NotApplicable, "not_applicable"),
(SemanticWitnessVerdict::OutOfHorizon, "out_of_horizon"),
(SemanticWitnessVerdict::KnownDivergence, "known_divergence"),
] {
assert_eq!(v.as_str(), s);
}
}
}