//! `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::<Vec<_>>()
.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<String> = 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<String> = 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]"));
}
}