use std::{
fs,
path::{Component, Path},
process::Command,
};
use serde_json::Value;
use sha2::{Digest, Sha256};
use crate::interface::CliError;
const BASE: &str = "b496bf794e1f1a23c0f4c68e54d3325222dbf0dc";
const EVIDENCE_PATH: &str =
".shepherd/runs/v656/lanes/harness-semantics/evidence/active-adapters.json";
const HARNESSES: [&str; 3] = ["claude", "codex", "pi"];
const MANIFESTS: [(&str, &str); 4] = [
(
"component-runtime",
"packages/component-runtime/package.json",
),
("claude-shepherd", "packages/harness-claude/package.json"),
("codex-shepherd", "packages/harness-codex/package.json"),
("pi-shepherd", "packages/harness-pi/package.json"),
];
pub(crate) fn validate(evidence_path: &Path, root: &Path) -> Result<(), CliError> {
let evidence = read_json(evidence_path)?;
equal(&evidence, "/schema", "shepherd.active-adapters-evidence/3")?;
equal(&evidence, "/accepted_base", BASE)?;
let candidate_tree = candidate_tree(root)?;
equal(&evidence, "/candidate_tree", &candidate_tree)?;
let input_lock =
read_json(&root.join(".shepherd/runs/v656/lanes/harness-semantics/packed-inputs.json"))?;
equal(&input_lock, "/schema", "shepherd.packed-inputs/1")?;
if evidence.pointer("/inputs") != input_lock.pointer("/inputs") {
return Err(error(
"packed input hash evidence does not match the measured input lock",
));
}
validate_pi_observer(&evidence, root, "agent_settled")?;
validate_pi_observer(&evidence, root, "session_shutdown")?;
let command = evidence
.pointer("/probe_command")
.and_then(Value::as_array)
.ok_or_else(|| error("probe_command must be an array"))?;
let expected = ["scripts/test-packed-plugin.sh", "--evidence", EVIDENCE_PATH];
if command.iter().filter_map(Value::as_str).collect::<Vec<_>>() != expected {
return Err(error(
"probe_command is not the exact packed gate invocation",
));
}
for (name, relative) in MANIFESTS {
let recorded = text(
&evidence,
&format!("/inputs/package_manifest_sha256/{name}"),
)?;
if recorded != sha256_file(&contained(root, relative)?)? {
return Err(error(format!("packed {name} manifest hash is stale")));
}
}
let tarballs = evidence
.pointer("/inputs/tarball_sha256")
.and_then(Value::as_object)
.ok_or_else(|| error("tarball_sha256 must be an object"))?;
let expected_tarballs = [
"pzzld-component-runtime-6.5.6.tgz",
"pzzld-claude-shepherd-6.5.6.tgz",
"pzzld-codex-shepherd-6.5.6.tgz",
"pzzld-pi-shepherd-6.5.6.tgz",
];
if tarballs.len() != expected_tarballs.len()
|| expected_tarballs.iter().any(|name| {
tarballs
.get(*name)
.and_then(Value::as_str)
.is_none_or(|hash| !is_hash(hash))
})
{
return Err(error(
"active adapter evidence lacks the exact four packed tarball hashes",
));
}
for harness in HARNESSES {
validate_chain(&evidence, root, harness)?;
}
validate_matrix_evidence(&evidence, root)?;
Ok(())
}
fn candidate_tree(root: &Path) -> Result<String, CliError> {
let index = std::env::temp_dir().join(format!(
"shepherd-v656-candidate-{}.index",
std::process::id()
));
let _ = fs::remove_file(&index);
let run = |arguments: &[&str]| -> Result<std::process::Output, CliError> {
Command::new("git")
.args(arguments)
.current_dir(root)
.env("GIT_INDEX_FILE", &index)
.output()
.map_err(|source| error(format!("cannot execute git candidate-tree probe: {source}")))
};
let result = (|| {
for arguments in [
&["read-tree", "HEAD"][..],
&["add", "-A"][..],
&[
"add",
"-f",
"--",
".shepherd/runs/v656/lanes/harness-semantics/packed-inputs.json",
][..],
&[
"rm",
"-r",
"-q",
"--cached",
"--ignore-unmatch",
"--",
".shepherd/runs/v656/lanes/harness-semantics/evidence",
][..],
] {
let output = run(arguments)?;
if !output.status.success() {
return Err(error(format!(
"git candidate-tree step failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
)));
}
}
let output = run(&["write-tree"])?;
if !output.status.success() {
return Err(error(format!(
"git candidate-tree write failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
)));
}
let tree = String::from_utf8_lossy(&output.stdout).trim().to_owned();
if tree.len() != 40 || !tree.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(error("git candidate-tree output is not an object id"));
}
Ok(tree)
})();
let _ = fs::remove_file(index);
result
}
fn validate_pi_observer(evidence: &Value, root: &Path, observer: &str) -> Result<(), CliError> {
let prefix = format!("/pi_terminal_observers/{observer}");
equal(evidence, &format!("{prefix}/observer"), observer)?;
for phase in ["start", "stop"] {
equal(evidence, &format!("{prefix}/{phase}/request/harness"), "pi")?;
equal(
evidence,
&format!("{prefix}/{phase}/response/harness"),
"pi",
)?;
equal(evidence, &format!("{prefix}/{phase}/response/run"), "v656")?;
}
equal(evidence, &format!("{prefix}/start/source"), "managed-child")?;
equal(evidence, &format!("{prefix}/start/operation"), "start")?;
equal(
evidence,
&format!("{prefix}/start/response/state"),
"active",
)?;
equal(evidence, &format!("{prefix}/stop/source"), "managed-child")?;
equal(evidence, &format!("{prefix}/stop/operation"), "stop")?;
equal(
evidence,
&format!("{prefix}/stop/response/state"),
"stopped",
)?;
if evidence
.pointer(&format!("{prefix}/start/response/revision"))
.and_then(Value::as_u64)
!= Some(1)
|| evidence
.pointer(&format!("{prefix}/stop/request/expected_revision"))
.and_then(Value::as_u64)
!= Some(1)
|| evidence
.pointer(&format!("{prefix}/stop/response/revision"))
.and_then(Value::as_u64)
!= Some(2)
{
return Err(error(format!(
"Pi {observer} did not prove revision-1 to revision-2 stop"
)));
}
for field in [
"run",
"harness",
"agent_id",
"agent_type",
"role",
"session_id",
] {
same(
evidence,
&format!("{prefix}/start/response/{field}"),
&format!("{prefix}/stop/response/{field}"),
observer,
)?;
}
for field in ["harness", "agent_id", "agent_type", "session_id"] {
same(
evidence,
&format!("{prefix}/start/request/{field}"),
&format!("{prefix}/stop/request/{field}"),
observer,
)?;
for phase in ["start", "stop"] {
same(
evidence,
&format!("{prefix}/{phase}/request/{field}"),
&format!("{prefix}/{phase}/response/{field}"),
observer,
)?;
}
}
let carrier = format!(
"shepherd:{}",
text(evidence, &format!("{prefix}/start/response/role"))?
);
equal(
evidence,
&format!("{prefix}/start/request/role_carrier"),
&carrier,
)?;
equal(
evidence,
&format!("{prefix}/stop/request/role_carrier"),
&carrier,
)?;
let artifact = text(evidence, &format!("{prefix}/result_artifact/path"))?;
let expected =
format!(".shepherd/runs/v656/lanes/harness-semantics/evidence/reports/pi-{observer}.md");
if artifact != expected {
return Err(error(format!(
"Pi {observer} result artifact path is not owned"
)));
}
let relative_artifact = expected
.strip_prefix(".shepherd/runs/v656/")
.unwrap_or(&expected);
equal(
evidence,
&format!("{prefix}/stop/request/result_artifact"),
relative_artifact,
)?;
equal(
evidence,
&format!("{prefix}/stop/response/result_artifact"),
relative_artifact,
)?;
let content = text(evidence, &format!("{prefix}/result_artifact/content"))?;
let hash = text(evidence, &format!("{prefix}/result_artifact/sha256"))?;
let bytes = fs::read(contained(root, artifact)?).map_err(|source| {
error(format!(
"cannot read Pi {observer} result artifact: {source}"
))
})?;
if bytes != content.as_bytes() || sha256(&bytes) != hash {
return Err(error(format!("Pi {observer} result artifact is stale")));
}
Ok(())
}
fn validate_matrix_evidence(evidence: &Value, root: &Path) -> Result<(), CliError> {
for harness in HARNESSES {
let chain = format!("/real_chains/{harness}");
let matrix =
|event: &str, field: &str| format!("/matrix_evidence/{event}/{harness}/{field}");
for event in ["identity", "dispatch"] {
same(
evidence,
&matrix(event, "native_input"),
&format!("{chain}/resolve/request"),
harness,
)?;
same(
evidence,
&matrix(event, "native_result"),
&format!("{chain}/resolve/response"),
harness,
)?;
}
let lifecycle_input = evidence
.pointer(&matrix("lifecycle", "native_input"))
.ok_or_else(|| error("missing lifecycle matrix input"))?;
if lifecycle_input.get("start") != evidence.pointer(&format!("{chain}/start/request"))
|| lifecycle_input.get("stop") != evidence.pointer(&format!("{chain}/stop/request"))
{
return Err(error(format!(
"{harness} lifecycle matrix input is not the real chain"
)));
}
let lifecycle_result = evidence
.pointer(&matrix("lifecycle", "native_result"))
.ok_or_else(|| error("missing lifecycle matrix result"))?;
if lifecycle_result.get("start") != evidence.pointer(&format!("{chain}/start/response"))
|| lifecycle_result.get("stop") != evidence.pointer(&format!("{chain}/stop/response"))
{
return Err(error(format!(
"{harness} lifecycle matrix result is not the real chain"
)));
}
same(
evidence,
&matrix("guard", "native_input"),
&format!("{chain}/guard/request"),
harness,
)?;
same(
evidence,
&matrix("guard", "native_result"),
&format!("{chain}/guard/response"),
harness,
)?;
same(
evidence,
&matrix("report", "native_input"),
&format!("{chain}/stop/request"),
harness,
)?;
same(
evidence,
&matrix("report", "native_result"),
&format!("{chain}/result_artifact"),
harness,
)?;
same(
evidence,
&matrix("failure", "native_input"),
&format!("/negative_controls/{harness}/request"),
harness,
)?;
same(
evidence,
&matrix("failure", "native_result"),
&format!("/negative_controls/{harness}/result"),
harness,
)?;
for event in [
"identity",
"lifecycle",
"dispatch",
"guard",
"report",
"compaction",
"failure",
"host-limitations",
] {
same(
evidence,
&matrix(event, "negative_control"),
&format!("/negative_controls/{harness}"),
harness,
)?;
}
let rejected = evidence
.pointer(&format!("/negative_controls/{harness}"))
.ok_or_else(|| error(format!("missing {harness} negative control")))?;
for field in [
"run",
"harness",
"agent_type",
"role_carrier",
"lane",
"session_id",
"tool_call_id",
"tool_name",
"tool_input",
] {
same(
evidence,
&format!("/negative_controls/{harness}/request/{field}"),
&format!("{chain}/resolve/request/{field}"),
harness,
)?;
}
let rejected_agent = rejected
.pointer("/request/agent_id")
.and_then(Value::as_str)
.ok_or_else(|| error(format!("missing {harness} rejected agent id")))?;
let rejected_run = rejected
.pointer("/request/run")
.and_then(Value::as_str)
.ok_or_else(|| error(format!("missing {harness} rejected run")))?;
let detail = rejected
.pointer("/result/detail")
.and_then(Value::as_str)
.unwrap_or_default();
if rejected.pointer("/result/ok") != Some(&Value::Bool(false))
|| detail.is_empty()
|| !detail.contains(rejected_agent)
|| !detail.contains(rejected_run)
|| rejected.pointer("/request/agent_id")
== evidence.pointer(&format!("{chain}/start/request/agent_id"))
{
return Err(error(format!(
"{harness} negative control did not prove cross-identity rejection"
)));
}
let compaction_input = evidence
.pointer(&matrix("compaction", "native_input"))
.ok_or_else(|| error("missing compaction input"))?;
if compaction_input.get("event").and_then(Value::as_str) != Some("PreCompact") {
return Err(error(format!(
"{harness} compaction input is not native PreCompact"
)));
}
let compaction = evidence
.pointer(&matrix("compaction", "native_result"))
.ok_or_else(|| error("missing compaction result"))?;
if harness == "claude" {
if compaction.get("supported") != Some(&Value::Bool(true))
|| compaction.get("command").and_then(Value::as_str)
!= Some("hooks/tests/test_compaction_run_scope.sh")
|| !root
.join("hooks/tests/test_compaction_run_scope.sh")
.is_file()
|| compaction.get("exit").and_then(Value::as_i64) != Some(0)
|| compaction.get("output").and_then(Value::as_str)
!= Some("test_compaction_run_scope: PASS")
{
return Err(error(
"Claude compaction probe did not execute successfully",
));
}
} else if compaction.get("supported") != Some(&Value::Bool(false))
|| compaction.get("advertised") != Some(&Value::Bool(false))
{
return Err(error(format!(
"{harness} falsely advertises compaction support"
)));
}
}
validate_host_inventory(evidence, root)
}
fn validate_host_inventory(evidence: &Value, root: &Path) -> Result<(), CliError> {
for (harness, relative) in [
("claude", "hooks/hooks.json"),
("codex", "plugins/shepherd/codex/hooks/hooks.json"),
] {
let manifest = read_json(&root.join(relative))?;
let mut events = manifest
.pointer("/hooks")
.and_then(Value::as_object)
.ok_or_else(|| error(format!("{harness} hook inventory is malformed")))?
.keys()
.cloned()
.collect::<Vec<_>>();
events.sort();
let compaction = events.iter().any(|event| event == "PreCompact");
let expected = Value::Array(events.into_iter().map(Value::String).collect());
if evidence.pointer(&format!(
"/matrix_evidence/host-limitations/{harness}/native_result/events"
)) != Some(&expected)
|| evidence.pointer(&format!(
"/matrix_evidence/compaction/{harness}/native_input/advertised_events"
)) != Some(&expected)
|| evidence.pointer(&format!(
"/matrix_evidence/host-limitations/{harness}/native_result/compaction"
)) != Some(&Value::Bool(compaction))
|| evidence.pointer(&format!(
"/matrix_evidence/compaction/{harness}/native_result/supported"
)) != Some(&Value::Bool(compaction))
{
return Err(error(format!("{harness} host inventory is stale")));
}
}
let pi_manifest = read_json(&root.join("packages/harness-pi/shepherd.pi.json"))?;
let pi_hooks = pi_manifest
.pointer("/hooks")
.and_then(Value::as_object)
.ok_or_else(|| error("Pi hook inventory is malformed"))?;
let mut measured_pi_events = pi_hooks.keys().cloned().collect::<Vec<_>>();
measured_pi_events.sort();
let expected_pi_events =
Value::Array(measured_pi_events.into_iter().map(Value::String).collect());
let pi_events = evidence
.pointer("/pi_terminal_observers/registered_events")
.and_then(Value::as_array)
.ok_or_else(|| error("Pi registered event inventory is missing"))?;
if evidence.pointer("/pi_terminal_observers/registered_events") != Some(&expected_pi_events)
|| !pi_events.iter().any(|value| value == "agent_settled")
|| !pi_events.iter().any(|value| value == "session_shutdown")
|| pi_events.iter().any(|value| value == "precompact")
|| evidence.pointer("/matrix_evidence/host-limitations/pi/native_result/events")
!= Some(&Value::Array(pi_events.clone()))
|| evidence.pointer("/matrix_evidence/compaction/pi/native_input/advertised_events")
!= Some(&Value::Array(pi_events.clone()))
|| evidence.pointer("/matrix_evidence/host-limitations/pi/native_result/compaction")
!= Some(&Value::Bool(false))
|| evidence.pointer("/matrix_evidence/compaction/pi/native_result/supported")
!= Some(&Value::Bool(false))
{
return Err(error(
"Pi host inventory does not match registered observers",
));
}
Ok(())
}
fn validate_chain(evidence: &Value, root: &Path, harness: &str) -> Result<(), CliError> {
let prefix = format!("/real_chains/{harness}");
for phase in ["start", "resolve", "stop"] {
equal(
evidence,
&format!("{prefix}/{phase}/request/harness"),
harness,
)?;
equal(
evidence,
&format!("{prefix}/{phase}/response/harness"),
harness,
)?;
equal(evidence, &format!("{prefix}/{phase}/request/run"), "v656")?;
equal(evidence, &format!("{prefix}/{phase}/response/run"), "v656")?;
}
equal(
evidence,
&format!("{prefix}/guard/request/dispatch/harness"),
harness,
)?;
equal(
evidence,
&format!("{prefix}/guard/request/dispatch/run"),
"v656",
)?;
validate_identity_continuity(evidence, &prefix, harness)?;
equal(
evidence,
&format!("{prefix}/start/response/state"),
"active",
)?;
if evidence.pointer(&format!("{prefix}/resolve/response/path_in_write_scope"))
!= Some(&Value::Bool(true))
{
return Err(error(format!(
"{harness} resolve did not prove bounded scope"
)));
}
equal(
evidence,
&format!("{prefix}/guard/response/decision"),
"allow",
)?;
equal(
evidence,
&format!("{prefix}/stop/response/state"),
"stopped",
)?;
if evidence
.pointer(&format!("{prefix}/start/response/revision"))
.and_then(Value::as_u64)
!= Some(1)
|| evidence
.pointer(&format!("{prefix}/resolve/response/revision"))
.is_some()
|| evidence
.pointer(&format!("{prefix}/stop/request/expected_revision"))
.and_then(Value::as_u64)
!= Some(1)
|| evidence
.pointer(&format!("{prefix}/stop/response/revision"))
.and_then(Value::as_u64)
!= Some(2)
{
return Err(error(format!(
"{harness} lifecycle revisions are not start=1 resolve=absent expected=1 stop=2"
)));
}
let artifact = format!(
".shepherd/runs/v656/lanes/harness-semantics/evidence/reports/{harness}-engineer.md"
);
equal(
evidence,
&format!("{prefix}/result_artifact/path"),
&artifact,
)?;
let relative_artifact = artifact.strip_prefix(".shepherd/runs/v656/").unwrap();
equal(
evidence,
&format!("{prefix}/stop/request/result_artifact"),
relative_artifact,
)?;
equal(
evidence,
&format!("{prefix}/stop/response/result_artifact"),
relative_artifact,
)?;
let content = text(evidence, &format!("{prefix}/result_artifact/content"))?;
let recorded = text(evidence, &format!("{prefix}/result_artifact/sha256"))?;
if !is_hash(recorded) || sha256(content.as_bytes()) != recorded {
return Err(error(format!("{harness} result content hash is invalid")));
}
let path = contained(root, &artifact)?;
let bytes =
fs::read(&path).map_err(|e| error(format!("cannot read {}: {e}", path.display())))?;
if bytes != content.as_bytes() || sha256(&bytes) != recorded {
return Err(error(format!(
"{harness} stopped result artifact is missing or stale"
)));
}
Ok(())
}
fn validate_identity_continuity(
evidence: &Value,
prefix: &str,
harness: &str,
) -> Result<(), CliError> {
let request_fields = [
"run",
"harness",
"agent_id",
"agent_type",
"lane",
"session_id",
];
for field in request_fields {
same(
evidence,
&format!("{prefix}/start/request/{field}"),
&format!("{prefix}/resolve/request/{field}"),
harness,
)?;
same(
evidence,
&format!("{prefix}/start/request/{field}"),
&format!("{prefix}/stop/request/{field}"),
harness,
)?;
for phase in ["start", "resolve", "stop"] {
same(
evidence,
&format!("{prefix}/{phase}/request/{field}"),
&format!("{prefix}/{phase}/response/{field}"),
harness,
)?;
}
}
let response_fields = [
"run",
"harness",
"agent_id",
"agent_type",
"role",
"lane",
"session_id",
];
for field in response_fields {
same(
evidence,
&format!("{prefix}/start/response/{field}"),
&format!("{prefix}/resolve/response/{field}"),
harness,
)?;
same(
evidence,
&format!("{prefix}/start/response/{field}"),
&format!("{prefix}/stop/response/{field}"),
harness,
)?;
}
same(
evidence,
&format!("{prefix}/resolve/response"),
&format!("{prefix}/guard/request/dispatch"),
harness,
)?;
same(
evidence,
&format!("{prefix}/resolve/request/tool_name"),
&format!("{prefix}/guard/request/tool_name"),
harness,
)?;
same(
evidence,
&format!("{prefix}/resolve/request/tool_input"),
&format!("{prefix}/guard/request/tool_input"),
harness,
)?;
same(
evidence,
&format!("{prefix}/resolve/request/tool_call_id"),
&format!("{prefix}/resolve/response/tool_use_id"),
harness,
)?;
same(
evidence,
&format!("{prefix}/resolve/response/role"),
&format!("{prefix}/guard/request/role"),
harness,
)?;
same(
evidence,
&format!("{prefix}/start/response/revision"),
&format!("{prefix}/stop/request/expected_revision"),
harness,
)?;
for (path, schema) in [
("start/response/schema", "shepherd.dispatch/3"),
("resolve/response/schema", "shepherd.identity-resolution/1"),
("stop/response/schema", "shepherd.dispatch/3"),
] {
equal(evidence, &format!("{prefix}/{path}"), schema)?;
}
let role = text(evidence, &format!("{prefix}/start/response/role"))?;
let carrier = format!("shepherd:{role}");
for phase in ["start", "resolve", "stop"] {
equal(
evidence,
&format!("{prefix}/{phase}/request/role_carrier"),
&carrier,
)?;
}
Ok(())
}
fn same(evidence: &Value, left: &str, right: &str, harness: &str) -> Result<(), CliError> {
let left_value = evidence
.pointer(left)
.ok_or_else(|| error(format!("missing identity field at {left}")))?;
let right_value = evidence
.pointer(right)
.ok_or_else(|| error(format!("missing identity field at {right}")))?;
if left_value != right_value {
return Err(error(format!(
"{harness} identity continuity mismatch between {left} and {right}"
)));
}
Ok(())
}
fn read_json(path: &Path) -> Result<Value, 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("evidence must be a regular file"));
}
let bytes =
fs::read(path).map_err(|e| error(format!("cannot read {}: {e}", path.display())))?;
serde_json::from_slice(&bytes).map_err(|e| error(format!("invalid evidence JSON: {e}")))
}
fn contained(root: &Path, relative: &str) -> Result<std::path::PathBuf, CliError> {
let path = Path::new(relative);
if path.is_absolute() || !path.components().all(|c| matches!(c, Component::Normal(_))) {
return Err(error(format!("path escapes repository root: {relative}")));
}
Ok(root.join(path))
}
fn equal(value: &Value, pointer: &str, expected: &str) -> Result<(), CliError> {
if text(value, pointer)? != expected {
return Err(error(format!("unexpected value at {pointer}")));
}
Ok(())
}
fn text<'a>(value: &'a Value, pointer: &str) -> Result<&'a str, CliError> {
value
.pointer(pointer)
.and_then(Value::as_str)
.ok_or_else(|| error(format!("missing string at {pointer}")))
}
fn sha256_file(path: &Path) -> Result<String, CliError> {
fs::read(path)
.map(|v| sha256(&v))
.map_err(|e| error(format!("cannot hash {}: {e}", path.display())))
}
fn sha256(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let digest = Sha256::digest(bytes);
let mut output = String::with_capacity(digest.len() * 2);
for byte in digest {
output.push(HEX[usize::from(byte >> 4)] as char);
output.push(HEX[usize::from(byte & 0x0f)] as char);
}
output
}
fn is_hash(value: &str) -> bool {
value.len() == 64
&& value
.bytes()
.all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
}
fn error(message: impl Into<String>) -> CliError {
CliError::message(format!("harness evidence: {}", message.into()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hashes_are_lowercase_sha256() {
assert!(is_hash(&"a".repeat(64)));
assert!(!is_hash(&"A".repeat(64)));
assert!(!is_hash("abc"));
}
#[test]
fn paths_are_repository_relative() {
assert!(contained(Path::new("/tmp/root"), "packages/a.json").is_ok());
assert!(contained(Path::new("/tmp/root"), "../a.json").is_err());
}
}