extern crate std;
use std::collections::BTreeMap;
use std::format;
use std::string::String;
use std::vec::Vec;
use crate::types::MotifClass;
#[derive(Debug, Clone)]
pub struct MotifRefinementEntry {
pub motif: MotifClass,
pub fixture_observed: &'static str,
pub current_affinity_tiers: u32,
pub observed_affinity_tiers: u32,
pub current_named_witnesses: Vec<&'static str>,
pub observed_top_witnesses: Vec<(&'static str, f64)>,
pub affinity_divergence: AffinityDivergence,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AffinityDivergence {
Subset,
Superset,
Disjoint,
ExactMatch,
Overlap,
}
impl AffinityDivergence {
pub fn classify(observed: u32, current: u32) -> Self {
if observed == current { return Self::ExactMatch; }
if observed & current == 0 { return Self::Disjoint; }
if observed & !current == 0 { return Self::Subset; }
if current & !observed == 0 { return Self::Superset; }
Self::Overlap
}
}
#[derive(Debug, Clone)]
pub struct MotifRefinementReport {
pub entries: Vec<MotifRefinementEntry>,
}
pub fn build_motif_refinement(
observations: Vec<MotifRefinementEntry>,
) -> MotifRefinementReport {
MotifRefinementReport { entries: observations }
}
#[derive(Debug, Clone)]
pub struct EpisodeMotifObservation {
pub motif: MotifClass,
pub fixture_name: &'static str,
pub observed_tier_mask: u32,
pub observed_top_witnesses: Vec<(&'static str, u64)>,
}
pub fn build_motif_refinement_from_observations<const M: usize>(
bank: &crate::heuristics_bank::HeuristicsBank<M>,
observations: &[EpisodeMotifObservation],
) -> MotifRefinementReport {
let mut entries: Vec<MotifRefinementEntry> = Vec::new();
for obs in observations {
let entry_opt = bank.entries_iter()
.find(|e| e.motif_class == obs.motif);
if let Some(bank_entry) = entry_opt {
let curated = bank_entry.affinity_tiers;
let witnesses: Vec<&'static str> = bank_entry.primary_witness_detectors
.iter().copied().collect();
let max_count = obs.observed_top_witnesses.iter()
.map(|(_, c)| *c).max().unwrap_or(1).max(1);
let observed_witnesses: Vec<(&'static str, f64)> = obs.observed_top_witnesses
.iter().map(|(n, c)| (*n, *c as f64 / max_count as f64)).collect();
entries.push(MotifRefinementEntry {
motif: obs.motif,
fixture_observed: obs.fixture_name,
current_affinity_tiers: curated,
observed_affinity_tiers: obs.observed_tier_mask,
current_named_witnesses: witnesses,
observed_top_witnesses: observed_witnesses,
affinity_divergence: AffinityDivergence::classify(
obs.observed_tier_mask, curated),
});
}
}
MotifRefinementReport { entries }
}
pub fn render_motif_refinement_md(report: &MotifRefinementReport) -> String {
let mut out = String::new();
out.push_str("# Per-motif refinement report (Phases ζ.4 + ζ.8)\n\n");
out.push_str("For each motif that typed on a confirmed positive fixture,\n");
out.push_str("the table reports: current hand-curated affinity-tier mask\n");
out.push_str("vs observed tier-firing on the matched episode; current\n");
out.push_str("named-witness list vs observed top-K detectors-by-firing.\n\n");
out.push_str("**Refinements are RECOMMENDATIONS, not bank mutations.**\n");
out.push_str("Phase ζ.9 separately gates any merge through leave-one-\n");
out.push_str("fixture-out cross-validation (`audit::loo_cv::refinement_passes_gate`).\n\n");
out.push_str("Source: Phase ζ.4 + ζ.8 audit harness.\n\n");
out.push_str("| Motif | Fixture | Curated mask | Observed mask | Divergence | Top witnesses (observed) |\n");
out.push_str("|-------|---------|-------------:|--------------:|-----------|-----|\n");
for e in &report.entries {
let witnesses_str: Vec<String> = e.observed_top_witnesses.iter()
.map(|(n, f)| format!("`{}` ({:.2})", n, f))
.collect();
out.push_str(&format!(
"| `{:?}` | `{}` | 0x{:08x} | 0x{:08x} | {:?} | {} |\n",
e.motif,
e.fixture_observed,
e.current_affinity_tiers,
e.observed_affinity_tiers,
e.affinity_divergence,
witnesses_str.join(", "),
));
}
out.push_str("\n## Summary by divergence\n\n");
let mut by_div: BTreeMap<&'static str, u32> = BTreeMap::new();
for e in &report.entries {
let key: &'static str = match e.affinity_divergence {
AffinityDivergence::ExactMatch => "ExactMatch",
AffinityDivergence::Subset => "Subset (curation includes dead bits)",
AffinityDivergence::Superset => "Superset (curation misses observed bits)",
AffinityDivergence::Disjoint => "Disjoint (anti-data)",
AffinityDivergence::Overlap => "Overlap (partial)",
};
*by_div.entry(key).or_insert(0) += 1;
}
for (k, v) in &by_div {
out.push_str(&format!("- **{}**: {} motif(s)\n", k, v));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn divergence_subset() {
assert_eq!(
AffinityDivergence::classify(0b011, 0b111),
AffinityDivergence::Subset);
}
#[test]
fn divergence_superset() {
assert_eq!(
AffinityDivergence::classify(0b111, 0b011),
AffinityDivergence::Superset);
}
#[test]
fn divergence_disjoint() {
assert_eq!(
AffinityDivergence::classify(0b100, 0b011),
AffinityDivergence::Disjoint);
}
#[test]
fn divergence_exact_match() {
assert_eq!(
AffinityDivergence::classify(0b101, 0b101),
AffinityDivergence::ExactMatch);
}
#[test]
fn divergence_overlap() {
assert_eq!(
AffinityDivergence::classify(0b110, 0b011),
AffinityDivergence::Overlap);
}
}