use crate::{
api::{ApiMetadata, ApiStreamEvent, ProjectStatusResponse, StreamEventKind},
project::KNOWLEDGE_MAP_RELATIVE_PATH,
};
use super::{CliError, OutputFormat};
pub fn render_project_status(
response: &ProjectStatusResponse,
format: OutputFormat,
) -> Result<String, CliError> {
match format {
OutputFormat::Text => render_text("project.status", response),
OutputFormat::Json => serialize_line(response),
OutputFormat::Markdown => render_text("project.status", response),
OutputFormat::StreamingJson => render_streaming_project_status(response),
}
}
pub(super) fn render_response<T>(
operation: &str,
metadata: ApiMetadata,
response: &T,
format: OutputFormat,
) -> Result<String, CliError>
where
T: serde::Serialize,
{
match format {
OutputFormat::Text => render_text(operation, response),
OutputFormat::Json => serialize_line(response),
OutputFormat::Markdown => render_text(operation, response),
OutputFormat::StreamingJson => render_streaming_response(operation, metadata, response),
}
}
pub(super) fn render_text<T>(operation: &str, response: &T) -> Result<String, CliError>
where
T: serde::Serialize,
{
let value = serde_json::to_value(response)
.map_err(|error| CliError::RenderFailed(error.to_string()))?;
let line = match operation {
"project.status" => value["project_name"]
.as_str()
.unwrap_or("relay-knowledge")
.to_owned(),
"knowledge.ingest" => format!(
"ingested graph_version={} evidence_count={}",
value["metadata"]["graph_version"].as_u64().unwrap_or(0),
value["receipt"]["evidence_count"].as_u64().unwrap_or(0)
),
"knowledge.retrieve_context" => {
format!(
"results={}",
value["results"].as_array().map_or(0, Vec::len)
)
}
"files.index" => format!(
"file_roots={} indexed_files={} missing_files={} scan_errors={} truncated_roots={}",
value["summary"]["root_count"].as_u64().unwrap_or(0),
value["summary"]["indexed_file_count"].as_u64().unwrap_or(0),
value["summary"]["missing_file_count"].as_u64().unwrap_or(0),
value["summary"]["scan_error_count"].as_u64().unwrap_or(0),
value["summary"]["truncated_root_count"]
.as_u64()
.unwrap_or(0)
),
"files.query" => format!(
"results={} truncated={} duration_ms={}",
value["results"].as_array().map_or(0, Vec::len),
value["truncated"].as_bool().unwrap_or(false),
value["duration_ms"].as_u64().unwrap_or(0)
),
"files.content" => format!(
"results={} truncated={} duration_ms={}",
value["results"].as_array().map_or(0, Vec::len),
value["truncated"].as_bool().unwrap_or(false),
value["duration_ms"].as_u64().unwrap_or(0)
),
"graph.inspect" => format!(
"graph_version={} entities={} evidence={} code_files={} code_symbols={} repo_code_files={} repo_code_symbols={}",
value["graph"]["graph_version"].as_u64().unwrap_or(0),
value["graph"]["entity_count"].as_u64().unwrap_or(0),
value["graph"]["evidence_count"].as_u64().unwrap_or(0),
value["graph"]["code_file_count"].as_u64().unwrap_or(0),
value["graph"]["code_symbol_count"].as_u64().unwrap_or(0),
value["repository_code_totals"]["indexed_file_count"]
.as_u64()
.unwrap_or(0),
value["repository_code_totals"]["symbol_count"]
.as_u64()
.unwrap_or(0)
),
"index.refresh" => format!(
"refreshed_indexes={}",
value["indexes"].as_array().map_or(0, Vec::len)
),
"knowledge.map.init"
| "knowledge.map.source.add"
| "knowledge.map.source.update"
| "knowledge.map.source.remove" => format!(
"knowledge_map={} version={}",
value["path"]
.as_str()
.unwrap_or(KNOWLEDGE_MAP_RELATIVE_PATH),
value["map_version"].as_u64().unwrap_or(0)
),
"knowledge.map.show" => render_knowledge_map_show(&value),
"knowledge.map.history" => render_knowledge_map_history(&value),
"knowledge.map.route" => format!(
"topic={} sources={}",
value["topic"].as_str().unwrap_or("unknown"),
value["sources"].as_array().map_or(0, Vec::len)
),
"knowledge.map.validate" => format!(
"knowledge_map_valid={} diagnostics={}",
value["valid"].as_bool().unwrap_or(false),
value["diagnostics"].as_array().map_or(0, Vec::len)
),
"knowledge.map.agent_snippet" => value["snippet"].as_str().unwrap_or("").to_owned(),
"repository.map.init"
| "repository.map.show"
| "repository.map.history"
| "repository.map.validate" => format!(
"repository_maps={}",
value["results"].as_array().map_or(0, Vec::len)
),
"repository.map.directory.add"
| "repository.map.directory.update"
| "repository.map.directory.remove"
| "repository.map.migrate" => format!(
"repository_map={} version={}",
value["path"].as_str().unwrap_or("unknown"),
value["map_version"].as_u64().unwrap_or(0)
),
"worker.status" => format!(
"workers={}",
value["workers"].as_array().map_or(0, Vec::len)
),
"worker.run_once" => format!(
"task={} proposals={}",
value["task"]["task_id"].as_str().unwrap_or("none"),
value["proposals"].as_array().map_or(0, Vec::len)
),
"proposal.list" => format!(
"proposals={}",
value["proposals"].as_array().map_or(0, Vec::len)
),
"proposal.show" => format!(
"proposal={} conflicts={}",
value["proposal"]["proposal_id"]
.as_str()
.unwrap_or("unknown"),
value["conflicts"].as_array().map_or(0, Vec::len)
),
"proposal.accept" | "proposal.reject" | "proposal.supersede" => format!(
"proposal={} state={}",
value["proposal"]["proposal_id"]
.as_str()
.unwrap_or("unknown"),
value["proposal"]["state"].as_str().unwrap_or("unknown")
),
"audit.query" => format!(
"audit_events={}",
value["events"].as_array().map_or(0, Vec::len)
),
"provider.embedding.probe" => format!(
"provider={} ok={} model={} dimension={}",
value["provider"].as_str().unwrap_or("none"),
value["ok"].as_bool().unwrap_or(false),
value["model"].as_str().unwrap_or("unknown"),
value["dimension"].as_u64().unwrap_or(0)
),
"service.health" => format!(
"healthy={} repo_code_files={} repo_code_symbols={}",
value["healthy"].as_bool().unwrap_or(false),
value["repository_code_totals"]["indexed_file_count"]
.as_u64()
.unwrap_or(0),
value["repository_code_totals"]["symbol_count"]
.as_u64()
.unwrap_or(0)
),
"service.status" => format!(
"service={} mode={} storage={} missing_shards={}",
value["service_name"].as_str().unwrap_or("relay-knowledge"),
value["mode"].as_str().unwrap_or("disabled"),
value["storage"]["topology"].as_str().unwrap_or("unknown"),
value["storage"]["missing_shard_count"]
.as_u64()
.unwrap_or(0)
),
"code.repo.index" => {
if let Some(task) = value["task"].as_object() {
format!(
"index task={} state={} scope={}",
task.get("task_id")
.and_then(serde_json::Value::as_str)
.unwrap_or("unknown"),
task.get("state")
.and_then(serde_json::Value::as_str)
.unwrap_or("queued"),
task.get("source_scope")
.and_then(serde_json::Value::as_str)
.unwrap_or("unknown")
)
} else {
format!(
"indexed files={} symbols={} references={} chunks={} degraded={}",
value["summary"]["indexed_file_count"].as_u64().unwrap_or(0),
value["summary"]["symbol_count"].as_u64().unwrap_or(0),
value["summary"]["reference_count"].as_u64().unwrap_or(0),
value["summary"]["chunk_count"].as_u64().unwrap_or(0),
value["summary"]["degraded_file_count"]
.as_u64()
.unwrap_or(0)
)
}
}
"code.repo.scope_preview" => format!(
"preview files={} bytes={} unsupported={} expected_degraded={}",
value["preview"]["selected_file_count"]
.as_u64()
.unwrap_or(0),
value["preview"]["selected_byte_count"]
.as_u64()
.unwrap_or(0),
value["preview"]["unsupported_file_count"]
.as_u64()
.unwrap_or(0),
value["preview"]["expected_degraded_file_count"]
.as_u64()
.unwrap_or(0)
),
"code.repo.query" => format!(
"results={}",
value["results"].as_array().map_or(0, Vec::len)
),
"code.repo.context" => format!(
"entry_points={} related_symbols={} graph_paths={} truncated={}",
value["entry_points"].as_array().map_or(0, Vec::len),
value["related_symbols"].as_array().map_or(0, Vec::len),
value["graph_paths"].as_array().map_or(0, Vec::len),
value["truncated"].as_bool().unwrap_or(false)
),
"code.repo.feature_flags" => format!(
"feature_flags={} degraded={}",
value["flags"].as_array().map_or(0, Vec::len),
value["degraded_reason"].as_str().unwrap_or("none")
),
"code.repo.framework_graph" => format!(
"framework_nodes={} framework_edges={} truncated={} degraded={}",
value["graph"]["nodes"].as_array().map_or(0, Vec::len),
value["graph"]["edges"].as_array().map_or(0, Vec::len),
value["graph"]["truncated"].as_bool().unwrap_or(false),
value["degraded_reason"].as_str().unwrap_or("none")
),
"code.repo.impact" => format!(
"changed_in_scope={} results={}",
value["path_groups"]["in_scope_changed_paths"]
.as_array()
.map_or(0, Vec::len),
value["results"].as_array().map_or(0, Vec::len)
),
"code.repo.view" => format!(
"view={} nodes={} edges={} sections={} evidence={} stale={} degraded={}",
value["request"]["view_kind"].as_str().unwrap_or("unknown"),
value["nodes"].as_array().map_or(0, Vec::len),
value["edges"].as_array().map_or(0, Vec::len),
value["sections"].as_array().map_or(0, Vec::len),
value["evidence"].as_array().map_or(0, Vec::len),
value["metadata"]["stale"]
.as_bool()
.or_else(|| value["freshness"]["scope_stale"].as_bool())
.unwrap_or(false),
value["degraded_reason"].as_str().unwrap_or("none")
),
"code.repo.list" => render_code_repository_list(&value),
"code.repo.status" => render_code_repository_status(&value),
"code.repo.report" => format!(
"repo={} files={} freshness={}",
value["report"]["alias"].as_str().unwrap_or(""),
value["report"]["indexed_file_count"].as_u64().unwrap_or(0),
value["report"]["freshness_state"]
.as_str()
.unwrap_or("unknown")
),
"code.repo.software" => format!(
"software scope={} components={} dependency_usages={} sdk_usages={} files={} topics={} relationships={} build_targets={} iac_resources={} design_elements={} stale={}",
value["status"]["source_scope"]
.as_str()
.unwrap_or("unknown"),
value["components"].as_array().map_or(0, Vec::len),
value["dependency_usages"].as_array().map_or(0, Vec::len),
value["sdk_usages"].as_array().map_or(0, Vec::len),
value["files"].as_array().map_or(0, Vec::len),
value["topics"].as_array().map_or(0, Vec::len),
value["relationships"].as_array().map_or(0, Vec::len),
value["build_targets"].as_array().map_or(0, Vec::len),
value["iac_resources"].as_array().map_or(0, Vec::len),
value["design_elements"].as_array().map_or(0, Vec::len),
value["status"]["stale"].as_bool().unwrap_or(true)
),
"service.plan" => format!(
"service_plan={} dry_run={} executed={} path={}",
value["plan"]["action"].as_str().unwrap_or("install"),
value["plan"]["dry_run"].as_bool().unwrap_or(true),
value["execution"]["executed"].as_bool().unwrap_or(false),
value["plan"]["definition_path"].as_str().unwrap_or("")
),
"service.definition.write" => format!(
"service_definition_written={}",
value["written"].as_bool().unwrap_or(false)
),
"service.operator.status" | "service.operator.pause" | "service.operator.resume" => {
format!(
"operator={}",
value["operator"]["state"].as_str().unwrap_or("disabled")
)
}
"service.worker.run" => format!(
"worker={} claimed={} task_state={}",
value["worker_kind"].as_str().unwrap_or("code_index"),
value["claimed"].as_bool().unwrap_or(false),
value["task"]["state"].as_str().unwrap_or("none")
),
"setup.doctor" => format!(
"setup_configuration_ready={} live_health_checked={} checks={} actions={}",
value["configuration_ready"].as_bool().unwrap_or(false),
value["live_health_checked"].as_bool().unwrap_or(false),
value["checks"].as_array().map_or(0, Vec::len),
value["recommended_actions"].as_array().map_or(0, Vec::len)
),
"setup.profile" => format!(
"setup_profile={} env_vars={} commands={}",
value["profile"].as_str().unwrap_or("unknown"),
value["environment"].as_array().map_or(0, Vec::len),
value["commands"].as_array().map_or(0, Vec::len)
),
_ => operation.to_owned(),
};
Ok(format!("{line}\n"))
}
fn render_code_repository_status(value: &serde_json::Value) -> String {
let mut rendered = format!(
"repo={} files={} symbols={} stale={} task={} checkpoint={}",
value["status"]["alias"].as_str().unwrap_or(""),
value["status"]["indexed_file_count"].as_u64().unwrap_or(0),
value["status"]["symbol_count"].as_u64().unwrap_or(0),
value["status"]["stale"].as_bool().unwrap_or(true),
value["active_task"]["state"].as_str().unwrap_or("none"),
value["checkpoint"]["state"].as_str().unwrap_or("none")
);
if let Some(error_kind) = value["active_task"]["last_error_kind"].as_str() {
rendered.push_str(" error_kind=");
rendered.push_str(error_kind);
}
if let Some(error_message) = value["active_task"]["last_error_message"].as_str() {
rendered.push_str(" error=");
rendered.push_str(&serde_json::Value::String(error_message.to_owned()).to_string());
}
rendered
}
fn render_knowledge_map_history(value: &serde_json::Value) -> String {
let mut lines = vec![format!(
"knowledge_map={} map_version={} earliest={} omitted_through={} from={} through={} next={}",
value["path"]
.as_str()
.unwrap_or(KNOWLEDGE_MAP_RELATIVE_PATH),
value["map_version"].as_u64().unwrap_or(0),
value["earliest_available_version"].as_u64().unwrap_or(1),
value["omitted_through"].as_u64().unwrap_or(0),
value["from_version"].as_u64().unwrap_or(0),
value["through_version"].as_u64().unwrap_or(0),
value["next_from_version"]
.as_u64()
.map_or_else(|| "none".to_owned(), |version| version.to_string())
)];
if let Some(entries) = value["entries"].as_array() {
lines.extend(entries.iter().map(|entry| {
format!(
"version={} action={} actor={} summary={}",
entry["version"].as_u64().unwrap_or(0),
single_line(entry["action"].as_str().unwrap_or("unknown")),
single_line(entry["actor"].as_str().unwrap_or("unknown")),
single_line(entry["summary"].as_str().unwrap_or(""))
)
}));
}
lines.join("\n")
}
fn render_knowledge_map_show(value: &serde_json::Value) -> String {
let history = &value["map"]["history"];
let complete = history["complete"].as_bool().unwrap_or(true);
let omitted_through = history["omitted_through"].as_u64().unwrap_or(0);
let mut output = format!(
"knowledge_map={} topics={} sources={} routes={} history_complete={} history_omitted_through={} history_recent={}",
value["path"]
.as_str()
.unwrap_or(KNOWLEDGE_MAP_RELATIVE_PATH),
value["map"]["topics"].as_array().map_or(0, Vec::len),
value["map"]["sources"].as_array().map_or(0, Vec::len),
value["map"]["routes"].as_array().map_or(0, Vec::len),
complete,
omitted_through,
history["recent"].as_array().map_or(0, Vec::len)
);
if !complete {
output.push_str(&format!(
"\nhistory_notice=entries through version {omitted_through} are outside this view; run relay-knowledge map history without --from to read the earliest available page"
));
}
output
}
fn single_line(value: &str) -> String {
value.replace(['\r', '\n'], " ")
}
fn render_code_repository_list(value: &serde_json::Value) -> String {
let Some(repositories) = value["repositories"].as_array() else {
return "repositories=0".to_owned();
};
let mut lines = Vec::with_capacity(repositories.len() + 1);
lines.push(format!("repositories={}", repositories.len()));
lines.extend(repositories.iter().map(|repository| {
format!(
"repo={} state={} files={} symbols={} stale={} commit={} root={}",
repository["alias"].as_str().unwrap_or("unknown"),
repository["state"].as_str().unwrap_or("unknown"),
repository["indexed_file_count"].as_u64().unwrap_or(0),
repository["symbol_count"].as_u64().unwrap_or(0),
repository["stale"].as_bool().unwrap_or(true),
repository["last_indexed_commit"]
.as_str()
.unwrap_or("unknown"),
repository["root_path"].as_str().unwrap_or("unknown"),
)
}));
lines.join("\n")
}
fn render_streaming_response<T>(
operation: &str,
metadata: ApiMetadata,
response: &T,
) -> Result<String, CliError>
where
T: serde::Serialize,
{
let payload = serde_json::to_value(response)
.map_err(|error| CliError::RenderFailed(error.to_string()))?;
let events = [
ApiStreamEvent::operation(
StreamEventKind::Started,
operation,
metadata.clone(),
Some("operation started"),
None,
),
ApiStreamEvent::operation(
StreamEventKind::Item,
operation,
metadata.clone(),
None,
Some(payload),
),
ApiStreamEvent::operation(
StreamEventKind::Completed,
operation,
metadata,
Some("operation completed"),
None,
),
];
let mut output = String::new();
for event in events {
output.push_str(&serialize_line(&event)?);
}
Ok(output)
}
#[cfg(test)]
#[path = "mod_tests.rs"]
mod mod_tests;
fn render_streaming_project_status(response: &ProjectStatusResponse) -> Result<String, CliError> {
let events = [
ApiStreamEvent::project_status(StreamEventKind::Started, response, Some("status started")),
ApiStreamEvent::project_status(
StreamEventKind::Progress,
response,
Some("runtime configuration loaded"),
),
ApiStreamEvent::project_status(StreamEventKind::Item, response, None),
ApiStreamEvent::project_status(
StreamEventKind::Completed,
response,
Some("status completed"),
),
];
let mut output = String::new();
for event in events {
output.push_str(&serialize_line(&event)?);
}
Ok(output)
}
pub(super) fn serialize_line<T>(value: &T) -> Result<String, CliError>
where
T: serde::Serialize,
{
let line =
serde_json::to_string(value).map_err(|error| CliError::RenderFailed(error.to_string()))?;
Ok(format!("{line}\n"))
}