nd / nd-cli/src/resume.rs

raw file 6568 bytes
//! `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]"));
    }
}