skillnet 0.6.0

Manage canonical AI skill stores, derived views, and calibration data for multi-phase-plan.
Documentation
use std::{fs, path::PathBuf};

use assert_cmd::Command;
use predicates::prelude::*;
use rusqlite::Connection;
use serde_json::Value;
use tempfile::{tempdir, TempDir};

struct Fixture {
    repo: TempDir,
    plan: TempDir,
}

impl Fixture {
    fn new() -> Self {
        let repo = tempdir().unwrap();
        let plan = tempdir().unwrap();
        write_plan(plan.path());
        Self { repo, plan }
    }

    fn plan_dir(&self) -> PathBuf {
        self.plan.path().to_path_buf()
    }

    fn db_path(&self) -> PathBuf {
        self.repo
            .path()
            .join("data/multi-phase-plan/calibration.sqlite")
    }

    fn conn(&self) -> Connection {
        Connection::open(self.db_path()).unwrap()
    }

    fn command(&self) -> Command {
        let mut command = Command::cargo_bin("skillnet").unwrap();
        command.env("skillnet_DATA_DIR", self.repo.path().join("data"));
        command.env("SKILLNET_CONFIG", self.repo.path().join("skillnet.toml"));
        command.env_remove("SKILLNET_DATABASE_URL");
        command.env_remove("SKILLNET_DB_URL");
        command.env_remove("DATABASE_URL");
        command
    }
}

#[test]
fn init_produces_sidecar_and_record_ingests_it() {
    let fixture = Fixture::new();
    fixture
        .command()
        .args(["calibration", "init", fixture.plan_dir().to_str().unwrap()])
        .assert()
        .success()
        .stdout(predicate::str::contains(".calibration.json"));

    let sidecar_path = fixture.plan_dir().join(".calibration.json");
    let sidecar: Value = serde_json::from_str(&fs::read_to_string(&sidecar_path).unwrap()).unwrap();
    assert_eq!(sidecar["schema_version"], 1);
    assert_eq!(sidecar["plan"]["phase_count"], 3);
    assert_eq!(sidecar["triggers"].as_array().unwrap().len(), 15);

    fixture
        .command()
        .args([
            "calibration",
            "record",
            fixture.plan_dir().to_str().unwrap(),
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("recorded"));

    let conn = fixture.conn();
    assert_eq!(count(&conn, "plans"), 1);
    assert_eq!(count(&conn, "triggers"), 15);
}

#[test]
fn init_refuses_overwrite_and_force_preserves_state() {
    let fixture = Fixture::new();
    fixture
        .command()
        .args(["calibration", "init", fixture.plan_dir().to_str().unwrap()])
        .assert()
        .success();

    fixture
        .command()
        .args(["calibration", "init", fixture.plan_dir().to_str().unwrap()])
        .assert()
        .failure()
        .stderr(predicate::str::contains("refusing to overwrite"));

    let sidecar_path = fixture.plan_dir().join(".calibration.json");
    let mut sidecar: Value =
        serde_json::from_str(&fs::read_to_string(&sidecar_path).unwrap()).unwrap();
    sidecar["plan"]["id"] = Value::String("stable-id".to_string());
    sidecar["tags"]["owner"] = Value::String("calibration".to_string());
    sidecar["verify"] = serde_json::json!({
        "verified_at": 1,
        "elapsed_seconds": 3,
        "outcome": "partial",
        "phase_outcomes": {"01-foundation": "passed"},
        "emergency_changes": null,
        "surprises": "missed-signal: hidden-prerequisite: fixture"
    });
    fs::write(
        &sidecar_path,
        serde_json::to_string_pretty(&sidecar).unwrap(),
    )
    .unwrap();

    fixture
        .command()
        .args([
            "calibration",
            "init",
            "--force",
            fixture.plan_dir().to_str().unwrap(),
        ])
        .assert()
        .success();

    let forced: Value = serde_json::from_str(&fs::read_to_string(&sidecar_path).unwrap()).unwrap();
    assert_eq!(forced["plan"]["id"], "stable-id");
    assert_eq!(forced["tags"]["owner"], "calibration");
    assert_eq!(forced["verify"]["outcome"], "partial");
}

#[test]
fn eval_json_matches_recorded_triggers() {
    let fixture = Fixture::new();
    let eval_output = fixture
        .command()
        .args(["calibration", "eval", fixture.plan_dir().to_str().unwrap()])
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let eval_json: Value = serde_json::from_slice(&eval_output).unwrap();
    let eval_rows = eval_json.as_array().unwrap();

    fixture
        .command()
        .args(["calibration", "init", fixture.plan_dir().to_str().unwrap()])
        .assert()
        .success();
    fixture
        .command()
        .args([
            "calibration",
            "record",
            fixture.plan_dir().to_str().unwrap(),
        ])
        .assert()
        .success();

    let conn = fixture.conn();
    let mut rows = conn
        .prepare("SELECT name, input_value, threshold, fired FROM triggers ORDER BY name")
        .unwrap()
        .query_map([], |row| {
            Ok((
                row.get::<_, String>(0)?,
                row.get::<_, f64>(1)?,
                row.get::<_, f64>(2)?,
                row.get::<_, bool>(3)?,
            ))
        })
        .unwrap()
        .collect::<Result<Vec<_>, _>>()
        .unwrap();
    let mut eval_rows = eval_rows
        .iter()
        .map(|row| {
            (
                row["name"].as_str().unwrap().to_string(),
                row["input_value"].as_f64().unwrap(),
                row["threshold"].as_f64().unwrap(),
                row["fired"].as_bool().unwrap(),
            )
        })
        .collect::<Vec<_>>();
    rows.sort_by(|left, right| left.0.cmp(&right.0));
    eval_rows.sort_by(|left, right| left.0.cmp(&right.0));
    assert_eq!(eval_rows, rows);
}

#[test]
fn meta_heuristics_shape_hash_and_heuristics_commands_work() {
    let fixture = Fixture::new();

    let meta_output = fixture
        .command()
        .args([
            "calibration",
            "meta-heuristics",
            fixture.plan_dir().to_str().unwrap(),
        ])
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let meta_json: Value = serde_json::from_slice(&meta_output).unwrap();
    let fired = meta_json["fired"].as_array().unwrap();
    assert!(contains(fired, "threshold-proximity"));
    assert!(contains(fired, "high-stakes-combo"));

    let first_hash = shape_hash(&fixture);
    let second_hash = shape_hash(&fixture);
    assert_eq!(first_hash, second_hash);
    fs::write(
        fixture.plan_dir().join("03-integrate.md"),
        PHASE_3.replace("tests/e2e.rs", "tests/e2e.rs\n- tests/smoke.rs"),
    )
    .unwrap();
    assert_ne!(first_hash, shape_hash(&fixture));

    let list_output = fixture
        .command()
        .args(["calibration", "heuristics", "list", "--format", "json"])
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let list_json: Value = serde_json::from_slice(&list_output).unwrap();
    assert_eq!(list_json.as_array().unwrap().len(), 15);

    let category_output = fixture
        .command()
        .args([
            "calibration",
            "heuristics",
            "list",
            "--format",
            "json",
            "--category",
            "coordination",
        ])
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let category_json: Value = serde_json::from_slice(&category_output).unwrap();
    assert_eq!(category_json.as_array().unwrap().len(), 4);

    fixture
        .command()
        .args(["calibration", "heuristics", "show", "long-serial-chain"])
        .assert()
        .success()
        .stdout(predicate::str::contains("long-serial-chain"))
        .stdout(predicate::str::contains("Serial-chain recovery"));

    fixture
        .command()
        .args(["calibration", "heuristics", "show", "missing"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("unknown heuristic missing"));
}

#[test]
fn helper_commands_appear_in_help() {
    fixture_command()
        .args(["calibration", "--help"])
        .assert()
        .success()
        .stdout(predicate::str::contains("init"))
        .stdout(predicate::str::contains("eval"))
        .stdout(predicate::str::contains("meta-heuristics"))
        .stdout(predicate::str::contains("shape-hash"))
        .stdout(predicate::str::contains("heuristics"));

    fixture_command()
        .args(["calibration", "heuristics", "--help"])
        .assert()
        .success()
        .stdout(predicate::str::contains("list"))
        .stdout(predicate::str::contains("show"));
}

fn fixture_command() -> Command {
    let temp = tempdir().unwrap();
    let mut command = Command::cargo_bin("skillnet").unwrap();
    command.env("SKILLNET_CONFIG", temp.path().join("skillnet.toml"));
    command.env_remove("SKILLNET_DATABASE_URL");
    command.env_remove("SKILLNET_DB_URL");
    command.env_remove("DATABASE_URL");
    command
}

fn shape_hash(fixture: &Fixture) -> String {
    String::from_utf8(
        fixture
            .command()
            .args([
                "calibration",
                "shape-hash",
                fixture.plan_dir().to_str().unwrap(),
            ])
            .assert()
            .success()
            .get_output()
            .stdout
            .clone(),
    )
    .unwrap()
    .trim()
    .to_string()
}

fn contains(values: &[Value], needle: &str) -> bool {
    values.iter().any(|value| value.as_str() == Some(needle))
}

fn count(conn: &Connection, table: &str) -> i64 {
    conn.query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |row| {
        row.get(0)
    })
    .unwrap()
}

fn write_plan(path: &std::path::Path) {
    fs::write(path.join("README.md"), README).unwrap();
    fs::write(path.join("01-foundation.md"), PHASE_1).unwrap();
    fs::write(path.join("02-external.md"), PHASE_2).unwrap();
    fs::write(path.join("03-integrate.md"), PHASE_3).unwrap();
}

const README: &str = r#"# Synthetic calibration plan

> **Recommended Codex model: GPT 5.5 medium**

| Phase | File | Depends on | Touches | Can parallel with |
|---|---|---|---|---|
| 01 | [01-foundation.md](./01-foundation.md) | — | `src/lib.rs` | 02 |
| 02 | [02-external.md](./02-external.md) | — | `src/lib.rs` | 01 |
| 03 | [03-integrate.md](./03-integrate.md) | 01, 02 | `tests/e2e.rs` | — |
"#;

const PHASE_1: &str = r#"# Phase 01 — foundation

> **Recommended Codex model: GPT 5.5 medium**

## Working tree

same as primary

## Files likely touched

- `src/lib.rs`
"#;

const PHASE_2: &str = r#"# Phase 02 — external

> **Recommended Codex model: GPT 5.5 high**

## Working tree

/tmp/external-repo

## Files likely touched

- `src/lib.rs`
"#;

const PHASE_3: &str = r#"# Phase 03 — integrate

> **Recommended Codex model: GPT 5.5 max**

## Working tree

same as primary

## Files likely touched

- `tests/e2e.rs`
"#;