1use hj_core::{Handoff, HandoffItem, HandoffState};
2
3pub fn render_markdown(handoff: &Handoff, state: Option<&HandoffState>) -> String {
4 let project = handoff.project.as_deref().unwrap_or("unknown");
5 let updated = handoff.updated.as_deref().unwrap_or("unknown");
6 let branch = state
7 .and_then(|value| value.branch.as_deref())
8 .unwrap_or("unknown");
9 let build = state
10 .and_then(|value| value.build.as_deref())
11 .unwrap_or("unknown");
12 let tests = state
13 .and_then(|value| value.tests.as_deref())
14 .unwrap_or("unknown");
15
16 let mut out = String::new();
17 out.push_str(&format!("# Handoff — {project} ({updated})\n\n"));
18 out.push_str(&format!(
19 "**Branch:** {branch} | **Build:** {build} | **Tests:** {tests}\n"
20 ));
21 if let Some(notes) = state
22 .and_then(|value| value.notes.as_deref())
23 .filter(|notes| !notes.is_empty())
24 {
25 out.push_str(&format!("{notes}\n"));
26 }
27
28 out.push_str("\n## Items\n\n");
29 out.push_str("| ID | P | Status | Title |\n");
30 out.push_str("|---|---|---|---|\n");
31
32 let mut items: Vec<&HandoffItem> = handoff.active_items().collect();
33 items.sort_by_key(|item| {
34 (
35 priority_rank(item.priority.as_deref()),
36 status_rank(item.status.as_deref()),
37 item.id.as_str(),
38 )
39 });
40 for item in items {
41 out.push_str(&format!(
42 "| {} | {} | {} | {} |\n",
43 item.id,
44 item.priority.as_deref().unwrap_or("-"),
45 item.status.as_deref().unwrap_or("-"),
46 item.title
47 ));
48 }
49
50 out.push_str("\n## Log\n\n");
51 for entry in handoff.log.iter().take(5) {
52 let date = entry.date.as_deref().unwrap_or("unknown");
53 if entry.commits.is_empty() {
54 out.push_str(&format!("- {date}: {}\n", entry.summary));
55 } else {
56 out.push_str(&format!(
57 "- {date}: {} [{}]\n",
58 entry.summary,
59 entry.commits.join(", ")
60 ));
61 }
62 }
63
64 out
65}
66
67fn priority_rank(priority: Option<&str>) -> u8 {
68 match priority {
69 Some("P0") => 0,
70 Some("P1") => 1,
71 Some("P2") => 2,
72 _ => 9,
73 }
74}
75
76fn status_rank(status: Option<&str>) -> u8 {
77 match status {
78 Some("open") => 0,
79 Some("blocked") => 1,
80 _ => 9,
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use hj_core::{Handoff, HandoffItem, HandoffState, LogEntry};
87
88 use super::render_markdown;
89
90 #[test]
91 fn renders_summary_markdown() {
92 let handoff = Handoff {
93 project: Some("hj".into()),
94 updated: Some("2026-04-15".into()),
95 items: vec![HandoffItem {
96 id: "hj-1".into(),
97 priority: Some("P1".into()),
98 status: Some("open".into()),
99 title: "Ship reconcile".into(),
100 ..HandoffItem::default()
101 }],
102 log: vec![LogEntry {
103 date: Some("2026-04-15".into()),
104 summary: "Scaffolded workspace".into(),
105 commits: vec!["abc1234".into()],
106 ..LogEntry::default()
107 }],
108 ..Handoff::default()
109 };
110 let state = HandoffState {
111 branch: Some("main".into()),
112 build: Some("clean".into()),
113 tests: Some("passing".into()),
114 ..HandoffState::default()
115 };
116
117 let rendered = render_markdown(&handoff, Some(&state));
118 assert!(rendered.contains("# Handoff — hj (2026-04-15)"));
119 assert!(rendered.contains("| hj-1 | P1 | open | Ship reconcile |"));
120 assert!(rendered.contains("- 2026-04-15: Scaffolded workspace [abc1234]"));
121 }
122}