//! `nd resume` — summarise a maintainer's identity across all of their //! projects: what they've built, which skills it demonstrates. use nd_core::models::Project; use nd_core::resume::Skills; /// "rust ×3, clap ×2, axum" — top n items fn x2(items: &[(String, usize)], n: usize) -> String { items .iter() .take(n) .map(|(k, c)| { if *c > 1 { format!("{k} ×{c}") } else { k.clone() } }) .collect::>() .join(", ") } /// languages + tech keywords, deduped (case-insensitive), compact. fn built_with(p: &Project) -> String { let lower = |v: &str| v.trim().to_lowercase(); let langs: Vec = p.languages.iter().map(|s| s.trim().to_string()).collect(); let extra: Vec<(String, usize)> = p .code_keywords .iter() .filter(|k| !langs.iter().any(|l| lower(l) == lower(k))) .map(|k| (k.clone(), 1usize)) .collect(); let mut parts: Vec = langs; let extra_s = x2(&extra, 4); if !extra_s.is_empty() { parts.push(extra_s); } parts.join(", ") } fn first_sentence(s: &str) -> String { let s = s.replace('\n', " "); let s = match s.find(". ") { Some(i) => &s[..i + 1], None => s.as_str(), }; s.chars().take(240).collect() } fn first_seen(projects: &[Project]) -> String { projects .iter() .filter_map(|p| p.created_at.split('T').next().map(|d| d.to_string())) .min() .unwrap_or_default() } pub fn render_markdown(projects: &[Project], skills: &Skills, owner: &str, hub: &str) -> String { let dl: u64 = projects.iter().map(|p| p.downloads.max(0) as u64).sum(); let mut out = format!("## Projects — maintainer `{owner}` on {hub}\n\n"); out.push_str(&format!( "{} project{} · {} download{} · first published {}", projects.len(), if projects.len() == 1 { "" } else { "s" }, dl, if dl == 1 { "" } else { "s" }, first_seen(projects) )); if !skills.models.is_empty() { out.push_str(&format!(" · vibed with {}", skills.models.join(", "))); } out.push_str("\n\n"); for p in projects { let url = format!("{}/p/{}", hub, p.url_path()); if p.description.trim().is_empty() && p.llm_description.trim().is_empty() { out.push_str(&format!("- **[{}]({})**\n", p.name, url)); } else { let d = if p.description.trim().is_empty() { first_sentence(p.llm_description.trim()) } else { p.description.trim().to_string() }; out.push_str(&format!("- **[{}]({})** — {}\n", p.name, url, d)); } if !p.llm_description.trim().is_empty() { out.push_str(&format!(" {}\n", first_sentence(p.llm_description.trim()))); } out.push_str(&format!(" *built with:* {}\n", built_with(p))); } out.push_str("\n### Skills\n"); if !skills.languages.is_empty() { out.push_str(&format!("- **languages:** {}\n", x2(&skills.languages, 10))); } if !skills.tools.is_empty() { out.push_str(&format!("- **frameworks & tools:** {}\n", x2(&skills.tools, 10))); } if !skills.topics.is_empty() { out.push_str(&format!("- **domains:** {}\n", x2(&skills.topics, 10))); } out.push_str(&format!( "\n> generated by `nd resume` — every link above is a live, downloadable\n> project with reviews, provenance and machine specs on {hub}.\n" )); out } pub fn render_plain(projects: &[Project], skills: &Skills, owner: &str, hub: &str) -> String { let mut out = format!("resume — maintainer {} on {hub}\n", owner); out.push_str(&format!( "{} project(s) · engines: {}", projects.len(), x2(&skills.languages, 6) )); if !skills.models.is_empty() { out.push_str(&format!(" · vibe stack: {}", skills.models.join(", "))); } out.push('\n'); for p in projects { out.push_str(&format!("\n {} {} [{}/p/{}]\n", p.name, p.version, hub, p.url_path())); if !p.description.is_empty() { out.push_str(&format!(" {}\n", p.description.trim())); } let bw = built_with(p); if !bw.is_empty() { out.push_str(&format!(" built with {}\n", bw)); } } out.push_str("\nskills:\n"); if !skills.languages.is_empty() { out.push_str(&format!(" languages: {}\n", x2(&skills.languages, 10))); } if !skills.tools.is_empty() { out.push_str(&format!(" tools: {}\n", x2(&skills.tools, 10))); } if !skills.topics.is_empty() { out.push_str(&format!(" domains: {}\n", x2(&skills.topics, 10))); } out } #[cfg(test)] mod tests { use super::*; fn p(name: &str, langs: &[&str], tools: &[&str], topics: &[&str]) -> Project { Project { name: name.into(), slug: name.into(), version: "1.0.0".into(), description: format!("{name} does a thing."), languages: langs.iter().map(|s| s.to_string()).collect(), code_keywords: tools.iter().map(|s| s.to_string()).collect(), keywords: topics.iter().map(|s| s.to_string()).collect(), owner: Some("abcd1234xyz".into()), created_at: "2026-10-16T10:00:00+00:00".into(), ..Default::default() } } #[test] fn skills_ranking_and_markdown() { let projects = vec![ p("a", &["rust"], &["clap", "tokio"], &["cli"]), p("b", &["rust", "js"], &["clap"], &["cli", "llm"]), ]; let skills = nd_core::resume::collect(&projects); assert_eq!(skills.languages[0], ("rust".into(), 2)); assert_eq!(skills.tools[0], ("clap".into(), 2)); assert_eq!(skills.topics[0], ("cli".into(), 2)); let md = render_markdown(&projects, &skills, "abcd1234", "https://notdouble.com"); assert!(md.contains("## Projects — maintainer `abcd1234`")); assert!(md.contains("**[a](https://notdouble.com/p/abcd1234/a)**")); assert!(md.contains("- **languages:** rust ×2, js")); assert!(md.contains("- **frameworks & tools:** clap ×2, tokio")); let plain = render_plain(&projects, &skills, "abcd1234", "https://notdouble.com"); assert!(plain.contains("resume — maintainer abcd1234")); assert!(plain.contains("a 1.0.0 [https://notdouble.com/p/abcd1234/a]")); } }