use std::path::PathBuf;
use anyhow::Context;
use serde_json::json;
use tokio::fs;
use crate::{AppState, BugMonitorLogCandidate};
fn safe_segment(value: &str) -> String {
value
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '.' {
ch
} else {
'-'
}
})
.collect::<String>()
.trim_matches('-')
.to_string()
}
pub async fn write_log_evidence_artifact(
state: &AppState,
candidate: &BugMonitorLogCandidate,
) -> anyhow::Result<String> {
let project_id = safe_segment(&candidate.project_id);
let source_id = safe_segment(&candidate.source_id);
let digest = crate::sha256_hex(&[
&candidate.project_id,
&candidate.source_id,
&candidate.fingerprint,
&candidate.offset_start.to_string(),
]);
let file_name = format!("{}-{}.json", &digest[..16], candidate.offset_start);
let dir: PathBuf = state
.bug_monitor_log_evidence_dir
.join(&project_id)
.join(&source_id);
fs::create_dir_all(&dir).await?;
let path = dir.join(&file_name);
let payload = json!({
"schema_version": 1,
"project_id": candidate.project_id,
"source_id": candidate.source_id,
"repo": candidate.repo,
"workspace_root": candidate.workspace_root,
"log_path": candidate.path,
"offset_start": candidate.offset_start,
"offset_end": candidate.offset_end,
"inode": candidate.inode,
"detected_at_ms": candidate.timestamp_ms,
"parser_format": "auto",
"level": candidate.level,
"event": candidate.event,
"fingerprint": candidate.fingerprint,
"title": candidate.title,
"excerpt": candidate.excerpt,
"raw_excerpt_redacted": candidate.raw_excerpt_redacted,
"redactions": [],
});
let raw = serde_json::to_string_pretty(&payload)?;
fs::write(&path, raw)
.await
.with_context(|| format!("failed to write log evidence artifact {}", path.display()))?;
Ok(format!(
"tandem://bug-monitor/{}/evidence/{}/{}",
project_id, source_id, file_name
))
}