shepherd-cli 6.5.6

The canonical shepherd command-line interface over the per-project registry, run artifacts, and sprint pipeline.
use std::{
    collections::BTreeSet,
    fs,
    path::{Component, Path},
};

use crate::interface::CliError;

const EXPECTED: [(&str, &str, &str, &str); 6] = [
    (
        "pi-bootstrap",
        "pi-bootstrap",
        "pi-bootstrap_good.txt",
        "pi-bootstrap_bad.txt",
    ),
    (
        "least-authority",
        "least-authority",
        "v656/least-authority_good.txt",
        "v656/least-authority_bad.txt",
    ),
    (
        "first-run",
        "first-run",
        "v656/first-run_good.txt",
        "v656/first-run_bad.txt",
    ),
    (
        "gate-provenance",
        "gate-provenance",
        "v656/gate-provenance_good.txt",
        "v656/gate-provenance_bad.txt",
    ),
    (
        "release-trust",
        "release-trust",
        "v656/release-trust_good.txt",
        "v656/release-trust_bad.txt",
    ),
    (
        "harness-semantics",
        "harness-semantics",
        "v656/harness-semantics_good.txt",
        "v656/harness-semantics_bad.txt",
    ),
];
const LEGACY: [&str; 14] = [
    "reflection_good.txt",
    "reflection_bad.txt",
    "discovery_good.txt",
    "discovery_bad.txt",
    "dispatch_good.txt",
    "dispatch_bad.txt",
    "pi_tool_call_id_good.txt",
    "pi_tool_call_id_bad.txt",
    "content_good.txt",
    "content_bad.txt",
    "plugin_distribution_good.txt",
    "plugin_distribution_bad.txt",
    "cargo_native_distribution_good.txt",
    "cargo_native_distribution_bad.txt",
];

#[derive(Clone, Debug, Eq, PartialEq)]
struct Row {
    owner: String,
    kind: String,
    good: String,
    bad: String,
}

pub(crate) fn validate_v656(
    manifest: &Path,
    cases: &Path,
    rubrics: &Path,
) -> Result<String, CliError> {
    let raw =
        fs::read(manifest).map_err(|e| err(format!("cannot read {}: {e}", manifest.display())))?;
    let rows = parse(&raw)?;
    contract(&rows)?;
    for row in &rows {
        regular(&cases.join(&row.good), "good sibling", &row.good)?;
        regular(&cases.join(&row.bad), "bad sibling", &row.bad)?;
        regular(
            &rubrics.join(format!("{}.rubric.json", row.kind)),
            "rubric",
            &row.kind,
        )?;
    }
    Ok(rows
        .iter()
        .map(|r| format!("{}\t{}\t{}\t{}", r.owner, r.kind, r.good, r.bad))
        .collect::<Vec<_>>()
        .join("\n")
        + "\n")
}

fn parse(raw: &[u8]) -> Result<Vec<Row>, CliError> {
    if !raw.is_empty() && !raw.ends_with(b"\n") {
        return Err(err("manifest must end with a newline"));
    }
    let text = std::str::from_utf8(raw).map_err(|e| err(format!("manifest must be UTF-8: {e}")))?;
    let mut rows = Vec::new();
    for (index, line) in text.lines().enumerate() {
        let number = index + 1;
        if line.is_empty() {
            continue;
        }
        if line.starts_with('#') {
            if number == 1 && line == "# owner\tkind\tgood\tbad" {
                continue;
            }
            return Err(err(if number == 1 {
                "manifest header comment is malformed".into()
            } else {
                format!("unexpected manifest comment on line {number}")
            }));
        }
        let fields = line.split('\t').collect::<Vec<_>>();
        if fields.len() != 4 || fields.iter().any(|field| field.is_empty()) {
            return Err(err(format!(
                "manifest line {number} must contain exactly four non-empty TSV fields"
            )));
        }
        safe(fields[2], "good input")?;
        safe(fields[3], "bad input")?;
        if fields[2] == fields[3] {
            return Err(err(format!(
                "same good/bad input for {}: {}",
                fields[0], fields[2]
            )));
        }
        rows.push(Row {
            owner: fields[0].into(),
            kind: fields[1].into(),
            good: fields[2].into(),
            bad: fields[3].into(),
        });
    }
    Ok(rows)
}

fn safe(value: &str, label: &str) -> Result<(), CliError> {
    let path = Path::new(value);
    if value.is_empty()
        || value.contains('\\')
        || path.is_absolute()
        || !path.components().all(|c| matches!(c, Component::Normal(_)))
    {
        return Err(err(format!(
            "{label} must be a contained relative POSIX path: {value}"
        )));
    }
    Ok(())
}

fn contract(rows: &[Row]) -> Result<(), CliError> {
    if rows.len() != 6 {
        return Err(err(format!(
            "manifest must contain exactly six owned partitions, found {}",
            rows.len()
        )));
    }
    let inputs = rows
        .iter()
        .flat_map(|r| [&r.good, &r.bad])
        .map(String::as_str)
        .collect::<Vec<_>>();
    let unique = inputs.iter().copied().collect::<BTreeSet<_>>();
    if inputs.len() != unique.len() {
        return Err(err("duplicate eval input in v656 manifest"));
    }
    let legacy = LEGACY.iter().copied().collect::<BTreeSet<_>>();
    let collisions = unique.intersection(&legacy).copied().collect::<Vec<_>>();
    if !collisions.is_empty() {
        return Err(err(format!(
            "v656 input collides with legacy pair: {}",
            collisions.join(", ")
        )));
    }
    let expected = EXPECTED
        .iter()
        .map(|(o, k, g, b)| Row {
            owner: (*o).into(),
            kind: (*k).into(),
            good: (*g).into(),
            bad: (*b).into(),
        })
        .collect::<Vec<_>>();
    if rows != expected {
        return Err(err(
            "manifest owner/kind/input partitions do not match the closed v656 contract",
        ));
    }
    Ok(())
}

fn regular(path: &Path, label: &str, value: &str) -> Result<(), CliError> {
    let metadata =
        fs::symlink_metadata(path).map_err(|_| err(format!("missing {label}: {value}")))?;
    if !metadata.file_type().is_file() {
        return Err(err(format!("{label} is not a regular file: {value}")));
    }
    Ok(())
}
fn err(message: impl Into<String>) -> CliError {
    CliError::message(format!("v656 manifest: {}", message.into()))
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn syntax_rejects_traversal_and_missing_newline() {
        assert!(
            parse(b"o\tk\t../g\tb\n")
                .unwrap_err()
                .message_text()
                .is_some_and(|message| message.contains("contained relative POSIX path"))
        );
        assert!(
            parse(b"o\tk\tg\tb")
                .unwrap_err()
                .message_text()
                .is_some_and(|message| message.contains("must end with a newline"))
        );
    }
    #[test]
    fn contract_is_exactly_six_canonical_rows() {
        let rows = EXPECTED
            .iter()
            .map(|(o, k, g, b)| Row {
                owner: (*o).into(),
                kind: (*k).into(),
                good: (*g).into(),
                bad: (*b).into(),
            })
            .collect::<Vec<_>>();
        contract(&rows).unwrap();
        assert!(contract(&rows[..5]).is_err());
    }
}