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.
//! Per-action README + root catalog index emitter.

use crate::{category, Action};
use std::collections::BTreeMap;
use std::fmt::Write;
use std::fs;
use std::path::Path;

pub fn write_per_action(actions_dir: &Path, actions: &[Action]) -> anyhow::Result<usize> {
    // Build category → sibling-name set for cross-discovery
    let mut by_cat: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
    for a in actions {
        by_cat.entry(a.category.as_str()).or_default().push(&a.name);
    }
    let mut n = 0;
    for a in actions {
        let siblings = by_cat.get(a.category.as_str()).cloned().unwrap_or_default();
        let readme = emit_per_action(a, &siblings);
        let path = actions_dir.join(&a.name).join("README.md");
        fs::write(&path, readme)?;
        n += 1;
    }
    Ok(n)
}

fn emit_per_action(a: &Action, siblings: &[&str]) -> String {
    let mut md = String::new();
    let intro = category::intro(&a.category);
    let _ = writeln!(md, "# pleme-io · {}", a.name);
    let _ = writeln!(md);
    let _ = writeln!(md, "> {}", a.description);
    let _ = writeln!(md);
    let _ = writeln!(md, "**Category**: `{}` — {intro}", a.category);
    let _ = writeln!(md, "**Backend**: {}", a.backend);
    let _ = writeln!(md, "**Auto-published**: pinnable via `@v0.13.x` tags or floating `@v1` / `@main`");
    let _ = writeln!(md);
    let _ = writeln!(md, "## 30-second quickstart\n");
    let _ = writeln!(md, "```yaml");
    let _ = writeln!(md, "steps:");
    let _ = writeln!(md, "  - uses: actions/checkout@v4");
    let _ = writeln!(md, "  - uses: pleme-io/actions/{}@v1", a.name);
    if !a.inputs.is_empty() {
        let _ = writeln!(md, "    with:");
        for (i, (name, spec)) in a.inputs.iter().take(3).enumerate() {
            let _ = i;
            if spec.required {
                let _ = writeln!(md, "      {name}: <required>");
            } else if let Some(d) = &spec.default {
                let _ = writeln!(md, "      {name}: \"{d}\"");
            }
        }
    }
    let _ = writeln!(md, "```\n");

    if !a.inputs.is_empty() {
        let _ = writeln!(md, "## Inputs\n");
        let _ = writeln!(md, "| Name | Required | Default | Description |");
        let _ = writeln!(md, "|---|---|---|---|");
        for (name, spec) in &a.inputs {
            let req = if spec.required { "yes" } else { "no" };
            let dflt = spec.default.as_deref().map_or("".to_string(), |d| format!("`{d}`"));
            let desc = spec.description.as_deref().unwrap_or("");
            let _ = writeln!(md, "| `{name}` | {req} | {dflt} | {desc} |");
        }
        let _ = writeln!(md);
    }

    if !a.outputs.is_empty() {
        let _ = writeln!(md, "## Outputs\n");
        let _ = writeln!(md, "| Name | Description |");
        let _ = writeln!(md, "|---|---|");
        for (name, desc) in &a.outputs {
            let _ = writeln!(md, "| `{name}` | {desc} |");
        }
        let _ = writeln!(md);
    }

    let _ = writeln!(md, "## Configuration via `.pleme-io-release.toml`\n");
    let _ = writeln!(md, "Per-repo defaults follow 3-tier precedence:");
    let _ = writeln!(md, "**env var (workflow input) > `.pleme-io-release.toml` > hardcoded default**.\n");
    let _ = writeln!(md, "See the [full config schema](https://github.com/pleme-io/substrate/blob/main/lib/release/example-config.toml).\n");

    let _ = writeln!(md, "## Architecture\n");
    let _ = writeln!(md, "Composite GitHub Action. Logic lives in [`run.tlisp`](./run.tlisp);");
    let _ = writeln!(md, "[`action.yml`](./action.yml) orchestrates install steps + one");
    let _ = writeln!(md, "`tatara-script` invocation.\n");
    let _ = writeln!(md, "Per the ★★ NO-SHELL prime directive");
    let _ = writeln!(md, "([pleme-io-pattern-core skill](https://github.com/pleme-io/blackmatter-pleme/blob/main/skills/pleme-io-pattern-core/SKILL.md)):");
    let _ = writeln!(md, "this action's primary logic is typed Lisp, not bash. The substrate's");
    let _ = writeln!(md, "[`action-shell-lint`](../action-shell-lint/) enforces this fleet-wide on every PR.\n");

    let _ = writeln!(md, "## Related primitives — `{}` category\n", a.category);
    let related: Vec<&&str> = siblings.iter().filter(|&&n| n != a.name).take(8).collect();
    if related.is_empty() {
        let _ = writeln!(md, "(this is the only primitive in this category)\n");
    } else {
        let links: Vec<String> = related.iter().map(|n| format!("[`{n}`](../{n}/)")).collect();
        let _ = writeln!(md, "{}", links.join(" · "));
        let _ = writeln!(md);
    }

    let _ = writeln!(md, "## Auto-published on free public CI\n");
    let _ = writeln!(md, "Every push to `main` on `pleme-io/actions`:");
    let _ = writeln!(md, "1. `auto-bump.yml` fires (~10s) → tags `v0.13.{{next}}`");
    let _ = writeln!(md, "2. `release.yml` cuts the Docker image (if applicable) + fast-forwards `v1`");
    let _ = writeln!(md, "3. Consumers using `@v1` see the new revision automatically");
    let _ = writeln!(md);
    let _ = writeln!(md, "**$0/month cost** — GitHub-hosted runners + public-repo free tier.\n");

    let _ = writeln!(md, "## License\n\nMIT.\n");
    let _ = writeln!(md, "---");
    let _ = writeln!(md, "*Auto-generated from `action.yml` by [`pleme-doc-gen`](https://github.com/pleme-io/pleme-doc-gen). Do not hand-edit.*");
    md
}

pub fn emit_index(actions: &[Action]) -> String {
    let total = actions.len();
    let mut by_cat: BTreeMap<&str, Vec<&Action>> = BTreeMap::new();
    for a in actions {
        by_cat.entry(a.category.as_str()).or_default().push(a);
    }

    let mut md = String::new();
    let _ = writeln!(md, "# pleme-io action catalog — {total} typed primitives");
    let _ = writeln!(md);
    let _ = writeln!(md, "> The typed CI/CD vocabulary that powers the pleme-io fleet.");
    let _ = writeln!(md, "> **All actions auto-publish to free public GitHub-hosted compute.**\n");

    let _ = writeln!(md, "## Quickstart — adopt the directive in 6 lines\n");
    let _ = writeln!(md, "```yaml");
    let _ = writeln!(md, "# .github/workflows/auto-release.yml");
    let _ = writeln!(md, "on:");
    let _ = writeln!(md, "  push: {{ branches: [main] }}");
    let _ = writeln!(md, "jobs:");
    let _ = writeln!(md, "  release:");
    let _ = writeln!(md, "    uses: pleme-io/substrate/.github/workflows/auto-release.yml@main");
    let _ = writeln!(md, "    secrets: inherit");
    let _ = writeln!(md, "```\n");

    let _ = writeln!(md, "## Operator-facing CLI\n");
    let _ = writeln!(md, "```bash");
    let _ = writeln!(md, "cargo install pleme-io-releaser");
    let _ = writeln!(md, "pleme-release detect / plan / onboard");
    let _ = writeln!(md, "```\n");

    let _ = writeln!(md, "## Discovery\n");
    let _ = writeln!(md, "```bash");
    let _ = writeln!(md, "nix eval --raw github:pleme-io/substrate#lib.aarch64-darwin.release.patterns");
    let _ = writeln!(md, "```\n");

    let _ = writeln!(md, "## The {total}-primitive vocabulary\n");
    for (cat, group) in &by_cat {
        let _ = writeln!(md, "\n### `{cat}` — {} primitive(s)\n", group.len());
        let intro = category::intro(cat);
        let _ = writeln!(md, "> {intro}\n");
        for a in group {
            let _ = writeln!(md, "- [`{}`](./{}/) — {}", a.name, a.name, a.description);
        }
    }

    let _ = writeln!(md);
    let _ = writeln!(md, "---\n");
    let _ = writeln!(md, "## How this catalog grows\n");
    let _ = writeln!(md, "Per the **★★ generation-over-composition** prime directive:");
    let _ = writeln!(md, "this `README.md` is mechanically auto-generated from `action.yml` files");
    let _ = writeln!(md, "via [`pleme-doc-gen`](https://github.com/pleme-io/pleme-doc-gen) (Rust binary,");
    let _ = writeln!(md, "published to crates.io via the directive's own dogfood).");
    let _ = writeln!(md);
    let _ = writeln!(md, "## License\n\nMIT.");
    md
}