use super::*;
pub(crate) fn pairing_json(p: Option<Pairing>) -> serde_json::Value {
match p {
Some(Pairing::Paired) => serde_json::json!("paired"),
Some(Pairing::PendingNoResult) => serde_json::json!("pending"),
Some(Pairing::OrphanResult) => serde_json::json!("orphan"),
None => serde_json::Value::Null,
}
}
pub(crate) fn refetch_json(session_id: &str, h: &Hit) -> serde_json::Value {
if !h.from_sidecar && h.line > 0 {
serde_json::json!(format!("csift show @{session_id} --line {}", h.line))
} else if let Some(u) = &h.uuid {
serde_json::json!(format!("csift show @{session_id} --uuid {u}"))
} else {
serde_json::Value::Null
}
}
pub(crate) fn refetch_uuid_json(session_id: &str, h: &Hit) -> serde_json::Value {
match &h.uuid {
Some(u) => serde_json::json!(format!("csift show @{session_id} --uuid {u}")),
None => serde_json::Value::Null,
}
}
pub(crate) fn compaction_mode_json(h: &Hit) -> serde_json::Value {
match h
.compaction
.as_ref()
.and_then(|c| c.mode)
.map(crate::model::SummarizeMode::slug)
{
Some(slug) => serde_json::json!(slug),
None => serde_json::Value::Null,
}
}
pub(crate) fn compact_metadata_json(h: &Hit) -> serde_json::Value {
h.compaction
.as_ref()
.and_then(|c| c.metadata.clone())
.unwrap_or(serde_json::Value::Null)
}
pub(crate) fn hit_json(ex: &Exchange, h: &Hit) -> serde_json::Value {
let session_id: &str = &ex.session_id;
let (from, to) = match &h.direction {
Some((f, t)) => (serde_json::json!(f), serde_json::json!(t)),
None => (serde_json::Value::Null, serde_json::Value::Null),
};
let pairing = pairing_json(h.pair);
serde_json::json!({
"session_id": ex.session_id,
"is_subagent": ex.is_subagent,
"parent_session_id": ex.parent_session_id,
"label": h.class.map(Class::path),
"labels": h.labels,
"delivered": h.delivered(),
"excerpt": h.excerpt,
"ts_utc": h.timestamp_utc,
"ts_local": h.timestamp_utc.as_deref().and_then(local_iso),
"tool_name": h.tool_name,
"from": from,
"to": to,
"pairing": pairing,
"is_error": h.is_error,
"denial_kind": h.denial_kind,
"tool_use_id": h.tool_use_id,
"queue_operation": h.queue_operation,
"queue_reason": h.queue_reason,
"task_ids": h.task_ids,
"orphan_kind": h.orphan_kind,
"resume_paired": h.resume_paired,
"survival": h.survival.as_str(),
"abandoned_root_line": ex.abandoned_root_line,
"replay_copy_of": h.replay_copy_of,
"mode": compaction_mode_json(h),
"compact_metadata": compact_metadata_json(h),
"scheduled_at": h.scheduled_at,
"line": if h.from_sidecar { serde_json::Value::Null } else { serde_json::json!(h.line) },
"uuid": h.uuid,
"source": if h.from_sidecar { serde_json::json!("elicitation-sidecar") } else { serde_json::Value::Null },
"image_ids": h.image_ids,
"refetch": refetch_json(session_id, h),
"refetch_uuid": refetch_uuid_json(session_id, h),
})
}
pub(crate) fn render_json(
outcome: &SearchOutcome,
diagnosis: Option<&EmptyDiagnosis>,
) -> Result<()> {
use serde_json::json;
println!(
"{}",
serde_json::to_string(&crate::text::envelope_scope_header(
"search",
outcome.scope_top,
outcome.scope_sub,
json!({})
))?
);
for ex in &outcome.exchanges {
let hits: Vec<_> = ex.hits.iter().map(|h| hit_json(ex, h)).collect();
let mut obj = json!({
"kind": "exchange",
"session_id": ex.session_id,
"is_subagent": ex.is_subagent,
"parent_session_id": ex.parent_session_id,
"turn_index": ex.turn_index,
"superseded_draft": ex.superseded_draft,
"ts_utc": ex.started_utc,
"ts_local": ex.started_utc.as_deref().and_then(local_iso),
"hits": hits,
"record_uuids": ex.record_uuids,
});
if let Some(d) = &ex.draft_diff {
d.attach_json(&mut obj);
}
if !ex.siblings.is_empty() || ex.siblings_hidden > 0 {
let sibs: Vec<_> = ex.siblings.iter().map(|h| hit_json(ex, h)).collect();
obj["siblings"] = json!(sibs);
obj["siblings_hidden"] = json!(ex.siblings_hidden);
obj["turn_lines"] = json!([ex.turn_lines.0, ex.turn_lines.1]);
}
println!("{}", serde_json::to_string(&obj)?);
}
let mut session_ids: Vec<&str> = outcome
.exchanges
.iter()
.map(|ex| ex.session_id.as_str())
.collect();
session_ids.sort_unstable();
session_ids.dedup();
let ids_total = session_ids.len();
let ids_truncated = ids_total > 100;
session_ids.truncate(100);
let mut summary_fields = json!({
"matched": outcome.exchanges.len(),
"sessions": distinct_session_count(&outcome.exchanges),
"transcript_ids": session_ids,
"transcript_ids_truncated": ids_truncated,
"dropped_by_cap": outcome.dropped_by_cap,
"skipped_lines": outcome.skipped_lines,
"superseded_drafts": outcome.chain.drafts,
"abandoned_records": outcome.chain.abandoned_records,
"rewound_turns": outcome.chain.rewound_turns,
"replay_copies": outcome.chain.replay_copies,
"boundary_cut_line": outcome.chain.boundary_cut_line,
"leaf_source": outcome.chain.leaf_source,
"with_elicitation_sidecar": merged_any_sidecar(&outcome.exchanges),
"excerpts_truncated": any_truncated_excerpt(&outcome.exchanges),
});
if let Some(d) = diagnosis {
summary_fields["definitive_absence"] = json!(true);
summary_fields["active_filters"] = json!(d.active_filters);
summary_fields["gated_leaves_unreached"] = json!(d.gated_unreached);
summary_fields["excluded_by_label"] = match &d.excluded_by_label {
Some((rows, recs)) => {
let by: serde_json::Map<String, serde_json::Value> =
rows.iter().map(|(l, n)| (l.clone(), json!(n))).collect();
json!({ "records": recs, "by_label": serde_json::Value::Object(by) })
}
None => serde_json::Value::Null,
};
}
let summary = crate::text::envelope_summary(summary_fields);
println!("{}", serde_json::to_string(&summary)?);
Ok(())
}