use crate::domain::{
OutputFormat, TargetTool, WakeupMemoryItem, WakeupPacket, WakeupProfile, WakeupRecommendedNote,
};
pub fn render(packet: &WakeupPacket, format: OutputFormat) -> String {
match format {
OutputFormat::Json => render_json(packet),
OutputFormat::Markdown => render_markdown(packet),
OutputFormat::Prompt => render_prompt(packet),
}
}
pub fn render_json(packet: &WakeupPacket) -> String {
serde_json::to_string_pretty(packet).unwrap_or_else(|_| "{}".to_string())
}
pub fn render_markdown(packet: &WakeupPacket) -> String {
let mut output = String::new();
output.push_str("# wakeup packet\n\n");
output.push_str(&format!("- profile: {}\n", profile_label(packet.profile)));
output.push_str(&format!("- target: {}\n", target_label(packet.target)));
output.push_str(&format!("- task: {}\n", packet.query.task));
output.push_str(&format!("- cwd: {}\n", packet.query.cwd));
if let Some(project_name) = &packet.identity.project_name {
output.push_str(&format!("- project: {}\n", project_name));
}
if !packet.identity.developer_roots.is_empty() {
output.push_str(&format!(
"- developer_roots: {}\n",
packet.identity.developer_roots.join(", ")
));
}
output.push('\n');
if let Some(index) = &packet.knowledge_index {
output.push_str("## Knowledge index\n\n");
output.push_str(index);
if !index.ends_with('\n') {
output.push('\n');
}
output.push('\n');
}
render_section(&mut output, "Working style", &packet.working_style.items);
render_section(&mut output, "Active context", &packet.active_context.items);
render_section(&mut output, "Constraints", &packet.constraints);
render_section(&mut output, "Decisions", &packet.decisions);
render_section(&mut output, "Incidents", &packet.incidents);
render_notes(&mut output, &packet.recommended_notes);
if !packet.maintenance_hints.is_empty() {
output.push_str("## Maintenance hints\n\n");
for hint in &packet.maintenance_hints {
output.push_str(&format!("- {}\n", hint));
}
output.push('\n');
}
output.push_str("## Policy\n\n");
output.push_str(&format!(
"- mode: {}\n- max_sensitivity_included: {}\n- redactions_applied: {}\n- suppressed_note_count: {}\n",
packet.policy.policy_mode,
packet
.policy
.max_sensitivity_included
.as_deref()
.unwrap_or("none"),
packet.policy.redactions_applied,
packet.policy.suppressed_note_count,
));
output
}
pub fn render_prompt(packet: &WakeupPacket) -> String {
let mut output = String::new();
output.push_str(match packet.target {
TargetTool::Claude => "以下是给 Claude 使用的 wake-up packet。\n\n",
TargetTool::Codex => "以下是给 Codex 使用的 wake-up packet。\n\n",
TargetTool::Opencode => "以下是给 OpenCode 使用的 wake-up packet。\n\n",
});
output.push_str(&format!("Profile: {}\n", profile_label(packet.profile)));
if let Some(project_name) = &packet.identity.project_name {
output.push_str(&format!("Project: {}\n", project_name));
}
output.push_str(&format!("Task: {}\n", packet.query.task));
let total_items = packet.working_style.items.len()
+ packet.active_context.items.len()
+ packet.constraints.len()
+ packet.decisions.len()
+ packet.incidents.len();
if total_items > 0 {
output.push_str(&format!("Memories loaded: {total_items}\n"));
}
output.push('\n');
if let Some(index) = &packet.knowledge_index {
output.push_str("Knowledge index (auto-synthesized):\n");
output.push_str(index);
if !index.ends_with('\n') {
output.push('\n');
}
output.push('\n');
}
if !packet.working_style.items.is_empty() {
output.push_str("Working style:\n");
for item in &packet.working_style.items {
output.push_str(&format!("- {}\n", item.summary));
}
output.push('\n');
}
if !packet.active_context.items.is_empty() {
output.push_str("Active context:\n");
for item in &packet.active_context.items {
output.push_str(&format!("- {}\n", item.summary));
}
output.push('\n');
}
if !packet.recommended_notes.is_empty() {
output.push_str("Recommended notes:\n");
for note in &packet.recommended_notes {
output.push_str(&format!("- {} ({})\n", note.title, note.path));
}
output.push('\n');
}
if !packet.maintenance_hints.is_empty() {
output.push_str("Maintenance:\n");
for hint in &packet.maintenance_hints {
output.push_str(&format!("- {}\n", hint));
}
}
output
}
fn render_section(output: &mut String, heading: &str, items: &[WakeupMemoryItem]) {
output.push_str(&format!("## {}\n\n", heading));
if items.is_empty() {
output.push_str("- none\n\n");
return;
}
for item in items {
output.push_str(&format!("- {} — {}\n", item.title, item.summary));
}
output.push('\n');
}
fn render_notes(output: &mut String, notes: &[WakeupRecommendedNote]) {
output.push_str("## Recommended notes\n\n");
if notes.is_empty() {
output.push_str("- none\n\n");
return;
}
for note in notes {
output.push_str(&format!(
"- {} [{}] — {}\n",
note.title, note.path, note.why_relevant
));
}
output.push('\n');
}
fn profile_label(profile: WakeupProfile) -> &'static str {
match profile {
WakeupProfile::Developer => "developer",
WakeupProfile::Project => "project",
}
}
fn target_label(target: TargetTool) -> &'static str {
match target {
TargetTool::Claude => "claude",
TargetTool::Codex => "codex",
TargetTool::Opencode => "opencode",
}
}
#[cfg(test)]
mod tests {
use super::{render_markdown, render_prompt};
use crate::domain::{
ConfidenceTier, TargetTool, WakeupIdentity, WakeupPacket, WakeupPolicy, WakeupProfile,
WakeupProvenance, WakeupQuery, WakeupRecommendedNote, WakeupSection,
};
fn make_packet(target: TargetTool) -> WakeupPacket {
WakeupPacket {
version: "wakeup.v1".to_string(),
generated_at: "unix:1".to_string(),
target,
profile: WakeupProfile::Project,
query: WakeupQuery {
task: "demo task".to_string(),
cwd: "/tmp/repo".to_string(),
files: vec!["src/app.rs".to_string()],
},
identity: WakeupIdentity {
project_id: Some("spool".to_string()),
project_name: Some("spool".to_string()),
repo_paths: vec!["/tmp/repo".to_string()],
modules: Vec::new(),
scenes: Vec::new(),
active_profile: "project".to_string(),
developer_roots: Vec::new(),
},
knowledge_index: None,
working_style: WakeupSection::default(),
active_context: WakeupSection::default(),
priorities: Vec::new(),
constraints: Vec::new(),
decisions: Vec::new(),
incidents: Vec::new(),
recommended_notes: vec![WakeupRecommendedNote {
path: "10-Projects/demo.md".to_string(),
title: "Demo".to_string(),
memory_type: Some("project".to_string()),
why_relevant: "matched project token".to_string(),
score: 10,
confidence: ConfidenceTier::Medium,
}],
maintenance_hints: Vec::new(),
provenance: WakeupProvenance::default(),
policy: WakeupPolicy {
max_sensitivity_included: Some("internal".to_string()),
redactions_applied: false,
suppressed_note_count: 0,
policy_mode: "conservative_default".to_string(),
},
}
}
#[test]
fn prompt_renderer_should_include_target_specific_intro() {
let rendered = render_prompt(&make_packet(TargetTool::Codex));
assert!(rendered.contains("给 Codex 使用的 wake-up packet"));
}
#[test]
fn markdown_renderer_should_include_policy_block() {
let rendered = render_markdown(&make_packet(TargetTool::Claude));
assert!(rendered.contains("## Policy"));
assert!(rendered.contains("max_sensitivity_included: internal"));
}
}