pleme-doc-gen 0.1.41

Rust replacement for the M0 Python _gen-patterns.py + _gen-docs.py scripts in pleme-io/actions. Walks every action.yml + emits substrate's patterns-full.nix + per-action README.md + root catalog. Per the NO-SHELL prime directive.
//! caixa_naming — typed default-target convention for eaten caixas.
//!
//! Per operator convention: every caixa absorbed via `eat-and-ship`
//! defaults to the slug `pleme-io/caixa-<basename>`, where
//! `<basename>` is the source path's directory name (sanitized to a
//! valid GitHub repo name).
//!
//! Examples:
//!   /tmp/clones/sharkdp__fd/fd      → pleme-io/caixa-fd
//!   ~/code/github/BurntSushi/ripgrep → pleme-io/caixa-ripgrep
//!   /tmp/eaten-typed-src             → pleme-io/caixa-eaten-typed-src
//!
//! The `caixa-` prefix makes eaten repos visually distinct from
//! pleme-io's native repos in the org listing — anyone browsing
//! pleme-io sees `caixa-*` and knows it's an absorbed upstream.
//!
//! Operators override per-invocation via `--target <owner>/<name>`.

use std::path::Path;

/// The canonical org for eaten caixas.
pub const DEFAULT_OWNER: &str = "pleme-io";
/// The canonical prefix that signals "absorbed upstream".
pub const CAIXA_PREFIX: &str = "caixa-";

/// Compute the default target slug for a given source path. Per
/// operator convention: `pleme-io/caixa-<sanitized-basename>`.
pub fn default_target_slug(source: &Path) -> String {
    let basename = source.file_name()
        .and_then(|s| s.to_str())
        .unwrap_or("unknown");
    format!("{DEFAULT_OWNER}/{CAIXA_PREFIX}{}", sanitize_repo_name(basename))
}

/// Sanitize an arbitrary string into a valid GitHub repo name.
/// GitHub repo names allow `[A-Za-z0-9._-]`; everything else collapses
/// to `-`. Empty input → "unknown".
pub fn sanitize_repo_name(s: &str) -> String {
    if s.is_empty() { return "unknown".to_string(); }
    let cleaned: String = s.chars().map(|c| match c {
        'A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '_' | '-' => c,
        _ => '-',
    }).collect();
    // Strip leading dots / dashes (GitHub rejects repo names starting
    // with `.`).
    let cleaned = cleaned.trim_start_matches(|c: char| c == '.' || c == '-').to_string();
    if cleaned.is_empty() { "unknown".to_string() } else { cleaned }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn fd_basename_yields_caixa_fd() {
        assert_eq!(
            default_target_slug(Path::new("/tmp/clones/sharkdp__fd/fd")),
            "pleme-io/caixa-fd"
        );
    }

    #[test]
    fn ripgrep_basename_yields_caixa_ripgrep() {
        assert_eq!(
            default_target_slug(Path::new("/some/path/ripgrep")),
            "pleme-io/caixa-ripgrep"
        );
    }

    #[test]
    fn trailing_slash_handled() {
        // PathBuf strips trailing slash on file_name lookup, so basename
        // is the actual directory name.
        assert_eq!(
            default_target_slug(Path::new("/x/y/zoxide/")),
            "pleme-io/caixa-zoxide"
        );
    }

    #[test]
    fn sanitize_collapses_unsafe_chars() {
        assert_eq!(sanitize_repo_name("foo/bar"), "foo-bar");
        assert_eq!(sanitize_repo_name("hello world"), "hello-world");
        assert_eq!(sanitize_repo_name("foo@1.0"), "foo-1.0");
        assert_eq!(sanitize_repo_name("multi__under"), "multi__under");
    }

    #[test]
    fn sanitize_strips_leading_dots_and_dashes() {
        assert_eq!(sanitize_repo_name(".github"), "github");
        assert_eq!(sanitize_repo_name("-private"), "private");
        assert_eq!(sanitize_repo_name("...dots"), "dots");
    }

    #[test]
    fn sanitize_empty_returns_unknown() {
        assert_eq!(sanitize_repo_name(""), "unknown");
        assert_eq!(sanitize_repo_name(".."), "unknown");
    }
}