use crate::domain::{ContextBundle, TargetTool};
pub fn render(bundle: &ContextBundle, max_chars: usize) -> String {
let mut output = String::new();
output.push_str("# spool context\n\n");
output.push_str(&format!("- task: {}\n", bundle.input.task));
output.push_str(&format!("- cwd: {}\n", bundle.input.cwd.display()));
if let Some(project) = &bundle.route.project {
output.push_str(&format!("- project: {} ({})\n", project.name, project.id));
}
output.push('\n');
if !bundle.route.lifecycle_candidates.is_empty() {
output.push_str("## 记忆(accepted / canonical)\n\n");
for candidate in &bundle.route.lifecycle_candidates {
let summary = truncate_chars(&candidate.summary, 120);
output.push_str(&format!(
"- [{}] [{}] **{}** — {}\n",
candidate.score, candidate.memory_type, candidate.title, summary
));
}
output.push('\n');
}
for candidate in &bundle.route.candidates {
output.push_str(&format!("## {}\n\n", candidate.relative_path));
output.push_str(&format!("- score: {}\n", candidate.score));
output.push_str(&format!("- reasons: {}\n\n", candidate.reasons.join("; ")));
output.push_str(&candidate.excerpt);
output.push_str("\n\n");
}
if output.chars().count() > max_chars {
output.chars().take(max_chars).collect()
} else {
output
}
}
fn truncate_chars(value: &str, max_chars: usize) -> String {
let chars: Vec<char> = value.chars().collect();
if chars.len() <= max_chars {
return value.to_string();
}
let mut out: String = chars.iter().take(max_chars).collect();
out.push('…');
out
}
pub fn render_explain(bundle: &ContextBundle) -> String {
let mut output = String::new();
output.push_str("# route explain\n\n");
output.push_str(&format!(
"- target: {}\n- cwd: {}\n- matched_project_id: {}\n- note_roots: {}\n- scan_roots: {}\n- limits: max_files={}, max_file_bytes={}, max_total_bytes={}, max_depth={}\n- note_count: {}\n\n",
match bundle.input.target {
TargetTool::Claude => "claude",
TargetTool::Codex => "codex",
TargetTool::Opencode => "opencode",
},
bundle.input.cwd.display(),
bundle.route.debug.matched_project_id.as_deref().unwrap_or("none"),
if bundle.route.debug.note_roots.is_empty() { "none".to_string() } else { bundle.route.debug.note_roots.join(", ") },
if bundle.route.debug.scan_roots.is_empty() { "none".to_string() } else { bundle.route.debug.scan_roots.join(", ") },
bundle.route.debug.limits.max_files,
bundle.route.debug.limits.max_file_bytes,
bundle.route.debug.limits.max_total_bytes,
bundle.route.debug.limits.max_depth,
bundle.route.debug.note_count,
));
match &bundle.route.project {
Some(project) => {
output.push_str(&format!(
"## project\n- id: {}\n- reason: {}\n\n",
project.id, project.reason
));
}
None => output.push_str("## project\n- none\n\n"),
}
output.push_str("## modules\n");
if bundle.route.modules.is_empty() {
output.push_str("- none\n");
} else {
for module in &bundle.route.modules {
output.push_str(&format!("- {}: {}\n", module.id, module.reasons.join("; ")));
}
}
output.push_str("\n## scenes\n");
if bundle.route.scenes.is_empty() {
output.push_str("- none\n");
} else {
for scene in &bundle.route.scenes {
let preferred_notes = if scene.preferred_notes.is_empty() {
"none".to_string()
} else {
scene.preferred_notes.join(", ")
};
output.push_str(&format!(
"- {}: {} | preferred_notes: {}\n",
scene.id,
scene.reasons.join("; "),
preferred_notes
));
}
}
output.push_str("\n## candidates\n");
if bundle.route.candidates.is_empty() {
output.push_str("- none\n");
} else {
for candidate in &bundle.route.candidates {
output.push_str(&format!(
"- {} [{}] (confidence={}): {}\n",
candidate.relative_path,
candidate.score,
match candidate.confidence {
crate::domain::ConfidenceTier::High => "high",
crate::domain::ConfidenceTier::Medium => "medium",
crate::domain::ConfidenceTier::Low => "low",
},
candidate.reasons.join("; ")
));
if !candidate.score_breakdown.is_empty() {
for contrib in &candidate.score_breakdown {
output.push_str(&format!(
" - {:?} {}/{} {:+}\n",
contrib.source, contrib.field, contrib.term, contrib.weight
));
}
}
}
}
output.push_str("\n## lifecycle_candidates\n");
if bundle.route.lifecycle_candidates.is_empty() {
output.push_str("- none\n");
} else {
for candidate in &bundle.route.lifecycle_candidates {
output.push_str(&format!(
"- {} [{}] ({}): {}\n",
candidate.title,
candidate.score,
candidate.memory_type,
candidate.reasons.join("; ")
));
}
}
output
}