use std::collections::BTreeMap;
use objects::object::TimelineToolCallStatus;
use serde_json::Value;
pub fn parse_relay_payload(payload: &str) -> (Value, Option<String>) {
if payload.trim().is_empty() {
return (Value::Null, None);
}
match serde_json::from_str::<Value>(payload) {
Ok(value) => (value, None),
Err(err) => (
Value::Null,
Some(format!(
"warning: failed to parse harness relay payload as JSON: {err}; continuing with null payload"
)),
),
}
}
pub fn value_string(value: &Value, path: &[&str]) -> Option<String> {
let mut current = value;
for segment in path {
current = current.get(*segment)?;
}
match current {
Value::String(s) => Some(s.clone()),
Value::Bool(v) => Some(v.to_string()),
Value::Number(v) => Some(v.to_string()),
_ => None,
}
}
pub fn first_value_string(value: &Value, paths: &[&[&str]]) -> Option<String> {
paths.iter().find_map(|path| value_string(value, path))
}
pub fn value_string_array(value: &Value, path: &[&str]) -> Option<Vec<String>> {
let mut current = value;
for segment in path {
current = current.get(*segment)?;
}
current.as_array().map(|items| {
items
.iter()
.filter_map(|item| item.as_str().map(ToString::to_string))
.collect()
})
}
pub fn value_array_join(value: &Value, path: &[&str]) -> Option<String> {
value_string_array(value, path).map(|items| items.join(","))
}
pub fn value_u64(value: &Value, path: &[&str]) -> Option<u64> {
let mut current = value;
for segment in path {
current = current.get(*segment)?;
}
current.as_u64()
}
pub fn value_u64_string(value: &Value, path: &[&str]) -> Option<String> {
value_u64(value, path).map(|v| v.to_string())
}
pub fn value_cost_micros(value: &Value, path: &[&str]) -> Option<String> {
value_cost_micros_u64(value, path).map(|v| v.to_string())
}
pub fn value_cost_micros_u64(value: &Value, path: &[&str]) -> Option<u64> {
let mut current = value;
for segment in path {
current = current.get(*segment)?;
}
current.as_f64().map(|v| (v * 1_000_000.0).round() as u64)
}
pub fn merge_string_vec(target: &mut Vec<String>, incoming: Vec<String>) {
for item in incoming {
if !item.trim().is_empty() && !target.contains(&item) {
target.push(item);
}
}
}
pub fn map_from_pairs<const N: usize>(
pairs: [(&str, Option<String>); N],
) -> BTreeMap<String, String> {
pairs
.into_iter()
.filter_map(|(key, value)| value.map(|value| (key.to_string(), value)))
.collect()
}
pub fn opencode_tool_name(payload: &Value) -> String {
first_value_string(
payload,
&[
&["tool", "name"],
&["toolName"],
&["tool_name"],
&["tool"],
&["name"],
],
)
.unwrap_or_else(|| "tool".to_string())
}
pub fn opencode_tool_status(payload: &Value) -> TimelineToolCallStatus {
let status = first_value_string(
payload,
&[
&["status"],
&["tool", "status"],
&["result", "status"],
&["output", "status"],
],
)
.unwrap_or_default()
.to_ascii_lowercase();
if status.contains("cancel") {
TimelineToolCallStatus::Cancelled
} else if status.contains("fail")
|| status.contains("error")
|| payload.get("error").is_some()
|| payload.get("exception").is_some()
{
TimelineToolCallStatus::Failed
} else {
TimelineToolCallStatus::Succeeded
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct VerificationClaimPolicyFacts {
pub allow_land_publish_followup: bool,
pub allow_matching_workflow_action: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VerificationClaimTrustFacts<'a> {
pub verified: bool,
pub recommended_action: &'a str,
pub remote_drift: &'a str,
pub workflow_status: &'a str,
}
pub fn repository_verification_allows_success_claim(
output_status: &str,
trust: VerificationClaimTrustFacts<'_>,
is_land_landed: bool,
recommended_matches_trust: bool,
policy: VerificationClaimPolicyFacts,
) -> bool {
if trust.verified || matches!(output_status, "blocked" | "failed") {
return true;
}
if policy.allow_land_publish_followup
&& is_land_landed
&& trust.recommended_action == "heddle push"
&& matches!(trust.remote_drift, "remote_untracked" | "remote_ahead")
{
return true;
}
if policy.allow_matching_workflow_action
&& trust.workflow_status == "ready"
&& recommended_matches_trust
{
return true;
}
false
}
pub fn raw_git_preservation_command() -> &'static str {
"heddle verify"
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn parse_relay_payload_and_paths() {
let (v, warn) = parse_relay_payload("");
assert!(v.is_null());
assert!(warn.is_none());
let (v, warn) = parse_relay_payload("{");
assert!(v.is_null());
assert!(warn.is_some());
let (v, _) = parse_relay_payload(r#"{"tool":{"name":"edit"},"status":"failed"}"#);
assert_eq!(opencode_tool_name(&v), "edit");
assert_eq!(opencode_tool_status(&v), TimelineToolCallStatus::Failed);
assert_eq!(value_string(&v, &["tool", "name"]).as_deref(), Some("edit"));
}
#[test]
fn verification_claim_and_land_rewrite() {
let verified = VerificationClaimTrustFacts {
verified: true,
recommended_action: "",
remote_drift: "",
workflow_status: "",
};
assert!(repository_verification_allows_success_claim(
"completed",
verified,
false,
false,
VerificationClaimPolicyFacts::default()
));
let unverified = VerificationClaimTrustFacts {
verified: false,
recommended_action: "",
remote_drift: "",
workflow_status: "",
};
assert!(!repository_verification_allows_success_claim(
"completed",
unverified,
false,
false,
VerificationClaimPolicyFacts::default()
));
let land_push = VerificationClaimTrustFacts {
verified: false,
recommended_action: "heddle push",
remote_drift: "remote_ahead",
workflow_status: "",
};
assert!(repository_verification_allows_success_claim(
"landed",
land_push,
true,
false,
VerificationClaimPolicyFacts {
allow_land_publish_followup: true,
allow_matching_workflow_action: false,
}
));
let _ = json!({});
assert_eq!(raw_git_preservation_command(), "heddle verify");
}
}