use std::{
collections::{BTreeMap, BTreeSet},
fs,
path::{Component, Path, PathBuf},
};
use serde::Deserialize;
use serde_json::Value;
use crate::interface::CliError;
const HARNESSES: [&str; 3] = ["claude", "codex", "pi"];
const EVENTS: [&str; 8] = [
"identity",
"lifecycle",
"dispatch",
"guard",
"report",
"compaction",
"failure",
"host-limitations",
];
const ARTIFACTS: [&str; 6] = [
".shepherd/runs/v646/harness-parity.md",
".shepherd/runs/v656/lanes/harness-semantics/evidence/active-adapters.json",
".shepherd/runs/v656/lanes/harness-semantics/evidence/reports/claude-engineer.md",
".shepherd/runs/v656/lanes/harness-semantics/evidence/reports/codex-engineer.md",
".shepherd/runs/v656/lanes/harness-semantics/evidence/reports/pi-engineer.md",
"hooks/tests/test_compaction_run_scope.sh",
];
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Matrix {
schema: String,
contract: String,
harnesses: Vec<String>,
events: Vec<Event>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Event {
event: String,
cells: BTreeMap<String, Cell>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Cell {
status: String,
native_input: String,
native_result: String,
adapter_translation: String,
artifact_path: String,
negative_control: String,
#[serde(default)]
limitations: Vec<Limitation>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Limitation {
reason: String,
citation: String,
}
pub(crate) fn validate(spec_path: &Path, root: &Path) -> Result<(), CliError> {
let matrix: Matrix = serde_json::from_slice(&read_regular(spec_path)?)
.map_err(|e| error(format!("invalid matrix JSON: {e}")))?;
if matrix.schema != "shepherd.harness-semantics-matrix/1" {
return Err(error("unsupported matrix schema"));
}
if matrix.contract != "fl03:shepherd@6.5.6" {
return Err(error(format!(
"matrix contract must be fl03:shepherd@6.5.6, got {}",
matrix.contract
)));
}
if matrix.harnesses != HARNESSES {
return Err(error("harness axis must be exactly claude, codex, pi"));
}
if matrix.events.is_empty() {
return Err(error("zero events are not a semantic matrix"));
}
let mut seen = BTreeSet::new();
for event in &matrix.events {
required(&event.event, "event")?;
if !seen.insert(event.event.as_str()) {
return Err(error(format!("duplicate event: {}", event.event)));
}
let observed = event
.cells
.keys()
.map(String::as_str)
.collect::<BTreeSet<_>>();
let expected = HARNESSES.into_iter().collect::<BTreeSet<_>>();
if let Some(missing) = expected.difference(&observed).next() {
return Err(error(format!(
"{}: missing harness cell: {missing}",
event.event
)));
}
if let Some(extra) = observed.difference(&expected).next() {
return Err(error(format!(
"{}: unexpected harness cell: {extra}",
event.event
)));
}
for harness in HARNESSES {
validate_cell(root, &event.event, harness, &event.cells[harness])?;
}
}
let expected = EVENTS.into_iter().collect::<BTreeSet<_>>();
if seen != expected {
return Err(error("event axis mismatch"));
}
validate_status_contract(root, &matrix)?;
Ok(())
}
fn validate_cell(root: &Path, event: &str, harness: &str, cell: &Cell) -> Result<(), CliError> {
if !matches!(
cell.status.as_str(),
"supported" | "limited" | "unsupported"
) {
return Err(error(format!(
"{event}/{harness}: invalid status {}",
cell.status
)));
}
for (name, value) in [
("native_input", &cell.native_input),
("native_result", &cell.native_result),
("adapter_translation", &cell.adapter_translation),
("artifact_path", &cell.artifact_path),
("negative_control", &cell.negative_control),
] {
required(value, &format!("{event}/{harness}.{name}"))?;
}
for (field, value) in [
("native_input", &cell.native_input),
("native_result", &cell.native_result),
("negative_control", &cell.negative_control),
] {
let expected = format!(
".shepherd/runs/v656/lanes/harness-semantics/evidence/active-adapters.json#matrix_evidence.{event}.{harness}.{field}"
);
if value != &expected {
return Err(error(format!(
"{event}/{harness}.{field} must reference owned raw evidence: {expected}"
)));
}
}
if cell.status != "supported" && cell.limitations.is_empty() {
return Err(error(format!(
"{event}/{harness}: {} cell requires limitations",
cell.status
)));
}
for (index, limitation) in cell.limitations.iter().enumerate() {
required(
&limitation.reason,
&format!("{event}/{harness}.limitations[{index}].reason"),
)?;
required(
&limitation.citation,
&format!("{event}/{harness}.limitations[{index}].citation"),
)?;
require_source(
root,
&limitation.citation,
&format!("{event}/{harness} limitation citation"),
)?;
}
require_source(
root,
&cell.adapter_translation,
&format!("{event}/{harness}.adapter_translation"),
)?;
require_source(
root,
&cell.artifact_path,
&format!("{event}/{harness}.artifact_path"),
)?;
if !ARTIFACTS.contains(&cell.artifact_path.as_str()) {
return Err(error(format!(
"{event}/{harness}.artifact_path is not an owned evidence artifact: {}",
cell.artifact_path
)));
}
validate_fragment(
root,
&cell.native_input,
&format!("{event}/{harness}.native_input"),
)?;
validate_fragment(
root,
&cell.native_result,
&format!("{event}/{harness}.native_result"),
)?;
validate_fragment(
root,
&cell.negative_control,
&format!("{event}/{harness}.negative_control"),
)?;
Ok(())
}
fn validate_status_contract(root: &Path, matrix: &Matrix) -> Result<(), CliError> {
let manifest: Value = serde_json::from_slice(&read_regular(
&root.join("plugins/shepherd/codex/hooks/hooks.json"),
)?)
.map_err(|e| error(format!("invalid Codex hooks manifest: {e}")))?;
let hooks = manifest
.get("hooks")
.and_then(Value::as_object)
.ok_or_else(|| error("Codex hooks manifest has no hook inventory"))?;
let trusted_child_correlation =
hooks.contains_key("SubagentStart") && hooks.contains_key("SubagentStop");
for event in &matrix.events {
for harness in HARNESSES {
let expected = if event.event == "host-limitations" {
"limited"
} else if event.event == "compaction" {
if harness == "claude" {
"supported"
} else {
"unsupported"
}
} else if harness == "codex"
&& matches!(event.event.as_str(), "lifecycle" | "dispatch" | "report")
&& !trusted_child_correlation
{
"limited"
} else {
"supported"
};
let observed = event.cells[harness].status.as_str();
if observed != expected {
return Err(error(format!(
"{}:{harness} status must be {expected}, got {observed}",
event.event
)));
}
}
}
Ok(())
}
fn validate_fragment(root: &Path, reference: &str, label: &str) -> Result<(), CliError> {
let Some((source, fragment)) = reference.split_once('#') else {
return Err(error(format!(
"{label} must reference owned raw evidence with a JSON fragment: {reference}"
)));
};
if !source.ends_with(".json") {
return Err(error(format!(
"{label} fragment must reference JSON: {reference}"
)));
}
let mut current: Value = serde_json::from_slice(&read_regular(&source_path(root, source)?)?)
.map_err(|e| error(format!("{label} contains invalid JSON: {e}")))?;
for part in fragment.split('.') {
if part.is_empty() {
return Err(error(format!(
"{label} fragment does not resolve: {reference}"
)));
}
current = current
.get(part)
.cloned()
.ok_or_else(|| error(format!("{label} fragment does not resolve: {reference}")))?;
}
if current.is_null() || current.as_str() == Some("") {
return Err(error(format!(
"{label} fragment resolves to empty evidence: {reference}"
)));
}
Ok(())
}
fn require_source(root: &Path, reference: &str, label: &str) -> Result<(), CliError> {
let path = source_path(root, reference.split_once('#').map_or(reference, |v| v.0))?;
if !fs::symlink_metadata(&path).is_ok_and(|metadata| metadata.file_type().is_file()) {
return Err(error(format!("{label} does not exist: {reference}")));
}
Ok(())
}
fn source_path(root: &Path, value: &str) -> Result<PathBuf, CliError> {
let path = Path::new(value);
if path.is_absolute() || !path.components().all(|c| matches!(c, Component::Normal(_))) {
return Err(error(format!(
"source path escapes repository root: {value}"
)));
}
Ok(root.join(path))
}
fn read_regular(path: &Path) -> Result<Vec<u8>, CliError> {
let metadata = fs::symlink_metadata(path)
.map_err(|e| error(format!("cannot inspect {}: {e}", path.display())))?;
if !metadata.file_type().is_file() {
return Err(error(format!("not a regular file: {}", path.display())));
}
fs::read(path).map_err(|e| error(format!("cannot read {}: {e}", path.display())))
}
fn required(value: &str, label: &str) -> Result<(), CliError> {
if value.trim().is_empty() {
Err(error(format!("{label} must be a non-empty string")))
} else {
Ok(())
}
}
fn error(message: impl Into<String>) -> CliError {
CliError::message(format!("harness matrix: {}", message.into()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_paths_are_contained() {
assert!(source_path(Path::new("/tmp/root"), "packages/a.json").is_ok());
assert!(source_path(Path::new("/tmp/root"), "../a.json").is_err());
}
#[test]
fn required_fields_reject_whitespace() {
assert!(required(" ", "field").is_err());
assert!(required("value", "field").is_ok());
}
}