skillnet 0.6.0

Manage canonical AI skill stores, derived views, and calibration data for multi-phase-plan.
Documentation
use std::collections::BTreeMap;

use serde_json::json;
use skillnet::calibration::{
    catalog::{
        MetaHeuristicInputs, PhaseInputs, PlanInputs, ThresholdSource, ThresholdStore, CATALOG,
        HEURISTICS, META_HEURISTICS,
    },
    sidecar::VerifyRecord,
    Db,
};
use tempfile::tempdir;

#[test]
fn catalog_is_readable_by_name_category_and_iteration() {
    let heuristic = CATALOG.get("long-serial-chain").unwrap();
    assert_eq!(heuristic.default_threshold(), 4.0);
    assert_eq!(CATALOG.by_category(heuristic.category()).count(), 4);
    assert_eq!(CATALOG.iter().count(), HEURISTICS.len());
}

#[test]
fn every_heuristic_fires_and_misses_on_synthetic_inputs() {
    for case in heuristic_cases() {
        let heuristic = CATALOG.get(case.name).unwrap();
        let threshold = heuristic.default_threshold();

        let fired = heuristic.evaluate(&case.firing, threshold);
        assert!(fired.fired, "{} should fire, got {fired:?}", case.name);
        assert!(fired.input_value >= threshold);

        let missed = heuristic.evaluate(&case.missing, threshold);
        assert!(!missed.fired, "{} should miss, got {missed:?}", case.name);
        assert!(missed.input_value < threshold);
    }
}

#[test]
fn meta_heuristics_fire_and_miss_on_synthetic_inputs() {
    let plan = plan_with(vec![phase(1, "01-setup", "medium", &["src/lib.rs"])]);
    let near = vec![outcome(9.0, 10.0, false)];
    let far = vec![outcome(1.0, 10.0, false)];
    assert_meta("threshold-proximity", inputs(&plan, &near), true);
    assert_meta("threshold-proximity", inputs(&plan, &far), false);

    let mut risky = plan.clone();
    risky.routing_dist.insert("max".to_string(), 1);
    assert_meta(
        "trigger-absence-with-risk-shape",
        inputs(&risky, &[outcome(0.0, 1.0, false)]),
        true,
    );
    assert_meta(
        "trigger-absence-with-risk-shape",
        inputs(&risky, &[outcome(1.0, 1.0, true)]),
        false,
    );

    let mut novel = inputs(&plan, &far);
    novel.novel_shape_signature = true;
    assert_meta("novel-shape-signature", novel, true);
    assert_meta("novel-shape-signature", inputs(&plan, &far), false);

    let mut outlier = inputs(&plan, &far);
    outlier.routing_tier_outlier_count = 1;
    assert_meta("routing-tier-outlier", outlier, true);
    assert_meta("routing-tier-outlier", inputs(&plan, &far), false);

    let verify = VerifyRecord {
        verified_at: 1,
        elapsed_seconds: None,
        outcome: "partial".to_string(),
        phase_outcomes: BTreeMap::new(),
        emergency_changes: Some(json!({"phase": "added"})),
        surprises: None,
    };
    let mut verify_inputs = inputs(&plan, &far);
    verify_inputs.verify = Some(&verify);
    assert_meta("verify-surprise", verify_inputs, true);
    assert_meta("verify-surprise", inputs(&plan, &far), false);

    let mut rerouting = inputs(&plan, &far);
    rerouting.rerouting_event_count = 1;
    assert_meta("rerouting-event", rerouting, true);
    assert_meta("rerouting-event", inputs(&plan, &far), false);

    let mut high_stakes = risky.clone();
    high_stakes.phases[0].working_tree = Some("/tmp/external".to_string());
    assert_meta("high-stakes-combo", inputs(&high_stakes, &far), true);
    assert_meta("high-stakes-combo", inputs(&risky, &far), false);

    let mut sampled = inputs(&plan, &far);
    sampled.random_sample = Some(0.01);
    assert_meta("uniform-random", sampled, true);
    let mut unsampled = inputs(&plan, &far);
    unsampled.random_sample = Some(0.5);
    assert_meta("uniform-random", unsampled, false);
}

#[test]
fn threshold_store_seeds_preserves_and_persists_overrides() {
    let temp = tempdir().unwrap();
    let db = Db::open(&temp.path().join("calibration.sqlite")).unwrap();

    let mut store = ThresholdStore::load(&db).unwrap();
    assert_eq!(store.get("long-serial-chain"), 4.0);
    assert_eq!(
        store.source("long-serial-chain"),
        Some(ThresholdSource::Default)
    );

    store.set("long-serial-chain", 6.0, "test").unwrap();
    assert_eq!(store.get("long-serial-chain"), 6.0);

    let store = ThresholdStore::load(&db).unwrap();
    assert_eq!(store.get("long-serial-chain"), 6.0);
    assert!(matches!(
        store.source("long-serial-chain"),
        Some(ThresholdSource::Override { updated_by, .. }) if updated_by.as_deref() == Some("test")
    ));
}

struct HeuristicCase {
    name: &'static str,
    firing: PlanInputs,
    missing: PlanInputs,
}

fn heuristic_cases() -> Vec<HeuristicCase> {
    vec![
        case(
            "shared-file-contention",
            plan_with(vec![
                phase(1, "01-a", "medium", &["src/lib.rs"]),
                phase(2, "02-b", "medium", &["src/lib.rs"]),
            ]),
            plan_with(vec![
                phase(1, "01-a", "medium", &["src/lib.rs"]),
                phase(2, "02-b", "medium", &["src/main.rs"]),
            ]),
        ),
        case(
            "external-repo-phases",
            with_external(plan_with(vec![phase(1, "01-a", "medium", &["src/lib.rs"])])),
            plan_with(vec![phase(1, "01-a", "medium", &["src/lib.rs"])]),
        ),
        case(
            "convergence-point",
            plan_with(vec![
                phase(1, "01-a", "medium", &[]),
                phase(2, "02-b", "medium", &[]),
                phase(3, "03-c", "medium", &[]),
                phase_with_deps(4, "04-d", "medium", &[], &[1, 2, 3]),
            ]),
            plan_with(vec![
                phase(1, "01-a", "medium", &[]),
                phase_with_deps(2, "02-b", "medium", &[], &[1]),
            ]),
        ),
        case(
            "ownership-boundary-spread",
            with_repo_spread(plan_with(vec![phase(1, "01-a", "medium", &[])]), 2),
            with_repo_spread(plan_with(vec![phase(1, "01-a", "medium", &[])]), 1),
        ),
        case(
            "risk-concentration",
            with_routing(plan_with(vec![
                phase(1, "01-a", "max", &[]),
                phase(2, "02-b", "max", &[]),
            ])),
            with_routing(plan_with(vec![phase(1, "01-a", "max", &[])])),
        ),
        case(
            "risk-late-in-plan",
            with_waves(
                with_routing(plan_with(vec![
                    phase(1, "01-a", "medium", &[]),
                    phase(2, "02-b", "medium", &[]),
                    phase(3, "03-c", "max", &[]),
                ])),
                vec![vec![1], vec![2], vec![3]],
            ),
            with_waves(
                with_routing(plan_with(vec![
                    phase(1, "01-a", "max", &[]),
                    phase(2, "02-b", "medium", &[]),
                    phase(3, "03-c", "medium", &[]),
                ])),
                vec![vec![1], vec![2], vec![3]],
            ),
        ),
        case(
            "infrastructure-spof",
            plan_with(vec![
                phase(1, "01-ci", "medium", &["flake.nix"]),
                phase_with_deps(2, "02-app", "medium", &["src/lib.rs"], &[1]),
            ]),
            plan_with(vec![
                phase(1, "01-ci", "medium", &["flake.nix"]),
                phase(2, "02-app", "medium", &["src/lib.rs"]),
            ]),
        ),
        case(
            "revendor-phase",
            plan_with(vec![phase(1, "01-bump-deps", "medium", &["Cargo.lock"])]),
            plan_with(vec![phase(1, "01-feature", "medium", &["src/lib.rs"])]),
        ),
        case(
            "long-serial-chain",
            with_depth(plan_with(vec![phase(1, "01-a", "medium", &[])]), 4),
            with_depth(plan_with(vec![phase(1, "01-a", "medium", &[])]), 3),
        ),
        case(
            "mid-plan-rerouting",
            with_phase_count(plan_with(vec![]), 10),
            with_phase_count(plan_with(vec![]), 9),
        ),
        case(
            "trivial-phase-swamp",
            with_dist(plan_with(vec![]), &[("low", 4), ("high", 1)]),
            with_dist(plan_with(vec![]), &[("low", 3), ("high", 1)]),
        ),
        case(
            "no-integrated-verification",
            plan_with(vec![phase(1, "01-feature", "medium", &["src/lib.rs"])]),
            plan_with(vec![phase(
                1,
                "01-integration-verify",
                "medium",
                &["tests/e2e.rs"],
            )]),
        ),
        case(
            "routing-tier-inversion",
            with_dist(
                plan_with(vec![phase(1, "01-leaf", "max", &[])]),
                &[("max", 1)],
            ),
            with_dist(
                plan_with(vec![phase(1, "01-leaf", "medium", &[])]),
                &[("max", 1)],
            ),
        ),
        case(
            "mechanical-streak",
            plan_with(vec![
                phase(1, "01-a", "low", &[]),
                phase(2, "02-b", "low", &[]),
                phase(3, "03-c", "low", &[]),
            ]),
            plan_with(vec![
                phase(1, "01-a", "low", &[]),
                phase(2, "02-b", "medium", &[]),
                phase(3, "03-c", "low", &[]),
            ]),
        ),
        case(
            "hidden-prerequisite",
            plan_with(vec![phase_with_deps(1, "01-a", "medium", &[], &[99])]),
            plan_with(vec![phase(1, "01-a", "medium", &[])]),
        ),
    ]
}

fn assert_meta(name: &str, inputs: MetaHeuristicInputs<'_>, expected: bool) {
    let heuristic = META_HEURISTICS
        .iter()
        .find(|heuristic| heuristic.name() == name)
        .unwrap();
    assert_eq!(heuristic.fires(&inputs), expected, "{name}");
}

fn inputs<'a>(
    plan: &'a PlanInputs,
    triggers: &'a [skillnet::calibration::catalog::TriggerOutcome],
) -> MetaHeuristicInputs<'a> {
    MetaHeuristicInputs::new(plan, triggers)
}

fn outcome(
    input_value: f64,
    threshold: f64,
    fired: bool,
) -> skillnet::calibration::catalog::TriggerOutcome {
    skillnet::calibration::catalog::TriggerOutcome {
        input_value,
        threshold,
        fired,
        section_added: None,
    }
}

fn case(name: &'static str, firing: PlanInputs, missing: PlanInputs) -> HeuristicCase {
    HeuristicCase {
        name,
        firing,
        missing,
    }
}

fn plan_with(phases: Vec<PhaseInputs>) -> PlanInputs {
    let mut routing_dist = BTreeMap::new();
    for phase in &phases {
        *routing_dist.entry(phase.routing_tier.clone()).or_default() += 1;
    }
    PlanInputs {
        path: "/tmp/plan".into(),
        name: "synthetic".to_string(),
        flavor: "codex".to_string(),
        worktype: Some("refactor".to_string()),
        phase_count: phases.len() as u32,
        wave_count: 1,
        max_chain_depth: 1,
        repo_spread: 1,
        routing_dist,
        waves: vec![phases.iter().map(|phase| phase.ordinal).collect()],
        phases,
    }
}

fn phase(ordinal: u32, slug: &str, routing_tier: &str, files: &[&str]) -> PhaseInputs {
    phase_with_deps(ordinal, slug, routing_tier, files, &[])
}

fn phase_with_deps(
    ordinal: u32,
    slug: &str,
    routing_tier: &str,
    files: &[&str],
    depends_on: &[u32],
) -> PhaseInputs {
    PhaseInputs {
        ordinal,
        slug: slug.to_string(),
        routing_tier: routing_tier.to_string(),
        files: files.iter().map(|file| (*file).to_string()).collect(),
        working_tree: None,
        depends_on: depends_on.to_vec(),
    }
}

fn with_external(mut plan: PlanInputs) -> PlanInputs {
    plan.phases[0].working_tree = Some("/tmp/external".to_string());
    plan
}

fn with_repo_spread(mut plan: PlanInputs, repo_spread: u32) -> PlanInputs {
    plan.repo_spread = repo_spread;
    plan
}

fn with_depth(mut plan: PlanInputs, depth: u32) -> PlanInputs {
    plan.max_chain_depth = depth;
    plan
}

fn with_phase_count(mut plan: PlanInputs, phase_count: u32) -> PlanInputs {
    plan.phase_count = phase_count;
    plan
}

fn with_waves(mut plan: PlanInputs, waves: Vec<Vec<u32>>) -> PlanInputs {
    plan.wave_count = waves.len() as u32;
    plan.waves = waves;
    plan
}

fn with_dist(mut plan: PlanInputs, entries: &[(&str, u32)]) -> PlanInputs {
    plan.routing_dist = entries
        .iter()
        .map(|(tier, count)| ((*tier).to_string(), *count))
        .collect();
    plan
}

fn with_routing(mut plan: PlanInputs) -> PlanInputs {
    plan.routing_dist.clear();
    for phase in &plan.phases {
        *plan
            .routing_dist
            .entry(phase.routing_tier.clone())
            .or_default() += 1;
    }
    plan
}