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> {
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
}