whyno-core 0.5.0

Permission check pipeline, fix engine, and state types
Documentation
//! Cascade simulation for fix pruning.
//!
//! Applies fixes to cloned state, re-runs check pipeline, prunes
//! fixes from later layers that now pass due to earlier layer's fix.
//! Fixes within same layer are alternatives (user picks one) and
//! are never pruned against each other.

mod apply;

use std::collections::HashSet;
use std::hash::{DefaultHasher, Hash, Hasher};

use crate::checks::{run_checks, CoreLayer};
use crate::operation::MetadataParams;
use crate::state::SystemState;

use super::{Fix, FixPlan};

/// Maximum number of layer groups to simulate.
const MAX_ITERATIONS: usize = 5;

/// Simulates fix application, prunes cross-layer redundancies.
///
/// For each layer group, applies lowest-impact fix to cloned state,
/// Checks if any later layer that previously failed now passes.
/// If so, that later layer's fixes are pruned.
#[must_use]
pub fn simulate_cascade(
    fixes: Vec<Fix>,
    mut warnings: Vec<String>,
    state: &SystemState,
) -> FixPlan {
    if fixes.is_empty() {
        return FixPlan { fixes, warnings };
    }

    let original_failures = run_checks(state, &MetadataParams::default()).failed_layers();
    let groups = unique_layers(&fixes);
    let mut mutated = state.clone();
    let mut resolved: Vec<CoreLayer> = Vec::new();
    let mut seen_hashes: HashSet<u64> = HashSet::new();

    for (i, layer) in groups.iter().enumerate() {
        if i >= MAX_ITERATIONS {
            break;
        }

        let before = run_checks(&mutated, &MetadataParams::default()).failed_layers();
        let state_hash = hash_failure_set(&before);
        if !seen_hashes.insert(state_hash) {
            warnings.push("fix cascade cycle detected \u{2014} manual review required".into());
            break;
        }

        apply_first_fix_for_layer(&fixes, *layer, &mut mutated);
        let after_failures = run_checks(&mutated, &MetadataParams::default()).failed_layers();

        detect_new_failures(&after_failures, &original_failures, &mut warnings);
        find_resolved(&before, &after_failures, &groups, *layer, &mut resolved);
    }

    let kept = prune_resolved(fixes, &resolved);
    FixPlan {
        fixes: kept,
        warnings,
    }
}

fn unique_layers(fixes: &[Fix]) -> Vec<CoreLayer> {
    let mut layers = Vec::new();
    for fix in fixes {
        if !layers.contains(&fix.layer) {
            layers.push(fix.layer);
        }
    }
    layers
}

fn apply_first_fix_for_layer(fixes: &[Fix], layer: CoreLayer, mutated: &mut SystemState) {
    if let Some(fix) = fixes.iter().find(|f| f.layer == layer) {
        apply::apply_fix(fix, mutated);
    }
}

fn detect_new_failures(current: &[CoreLayer], original: &[CoreLayer], warnings: &mut Vec<String>) {
    for layer in current {
        if !original.contains(layer) {
            let msg = format!("applying fix revealed new {layer:?} failure (cascade break)");
            if !warnings.contains(&msg) {
                warnings.push(msg);
            }
        }
    }
}

fn find_resolved(
    before: &[CoreLayer],
    after: &[CoreLayer],
    all_layers: &[CoreLayer],
    current: CoreLayer,
    resolved: &mut Vec<CoreLayer>,
) {
    for layer in all_layers {
        if *layer == current || resolved.contains(layer) {
            continue;
        }
        let failed_before = before.contains(layer);
        let passes_after = !after.contains(layer);
        if failed_before && passes_after {
            resolved.push(*layer);
        }
    }
}

fn hash_failure_set(layers: &[CoreLayer]) -> u64 {
    let mut hasher = DefaultHasher::new();
    layers.hash(&mut hasher);
    hasher.finish()
}

fn prune_resolved(fixes: Vec<Fix>, resolved: &[CoreLayer]) -> Vec<Fix> {
    fixes
        .into_iter()
        .filter(|f| !resolved.contains(&f.layer))
        .collect()
}

#[cfg(test)]
#[path = "cascade_tests.rs"]
mod tests;

#[cfg(test)]
#[path = "cascade/apply_tests.rs"]
mod apply_tests;

#[cfg(test)]
#[path = "cascade/apply_mount_tests.rs"]
mod apply_mount_tests;