pleme-doc-gen 0.1.40

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-init — one-call bootstrap of an OSS-consumption inventory repo.
//!
//! Per the explosive-ecosystem-consumption skill + the operator's
//! "most automated and autogenerated way possible" direction:
//! collapse the 5-command inventory-repo setup into ONE typed
//! substrate call.
//!
//! After `pleme-doc-gen caixa-init --name my-inventory`, the operator
//! has a fully-staged repo ready to `git init && gh repo create &&
//! git push`. Pushing fires the convert-and-publish.yml workflow on
//! free GH compute, converting every committed .ossconv.lisp into a
//! published pleme-io wrapper repo.

use anyhow::Result;
use std::path::Path;

/// Files the inventory repo ships with on day-0. Each one is embedded
/// in the binary via include_str! — no template-file lookup at runtime.
const README_TEMPLATE: &str = r#"# {name}

Curated `.ossconv.lisp` inventory for pleme-io's typed OSS-consumption loop.

## Quick start

1. Add a `.ossconv.lisp` file under `inventory/<category>/`:

   ```lisp
   ;; inventory/rust-foundational/wrap-serde.ossconv.lisp
   (defossconv pleme-io-wrap-serde
     :upstream         "serde-rs/serde"
     :wrapper          "wrap-serde"
     :expect-ecosystem :rust-workspace
     :description      "pleme-io typed wrapper for serde-rs/serde"
     :license          "MIT"
     :publish-mode     :public)
   ```

2. Commit + push. The `convert-and-publish.yml` workflow auto-runs,
   creates the typed wrapper repo, and publishes to crates.io.

For multi-repo architecture suites, add `.fleet.lisp` files under
`fleets/`.

## Mechanics

- **Inputs**: `inventory/**/*.ossconv.lisp` + `fleets/**/*.fleet.lisp`
- **Engine**: `pleme-doc-gen convert` + `fleet-forge`
- **Outputs**: typed wrapper repos under `pleme-io/<wrapper>` + auto-released crates
- **Receipts**: per-conversion JSON in CI artifacts under `receipts/`

## Bootstrapped by

```
pleme-doc-gen caixa-init --name {name}
```

See `blackmatter-pleme/skills/explosive-ecosystem-consumption/SKILL.md`
for the complete operator playbook + the
[caixa-mass-generation](https://github.com/pleme-io/blackmatter-pleme/blob/main/skills/caixa-mass-generation/SKILL.md)
skill for the underlying primitives.
"#;

const WORKFLOW_TEMPLATE: &str = r#"# convert-and-publish — generated by pleme-doc-gen caixa-init.
# Runs on PR/push touching inventory/**/*.ossconv.lisp +
# fleets/**/*.fleet.lisp, plus nightly drift detection at 04:00 UTC.

name: convert-and-publish

on:
  pull_request:
    paths: ['inventory/**/*.ossconv.lisp', 'fleets/**/*.fleet.lisp']
  push:
    branches: [main]
    paths: ['inventory/**/*.ossconv.lisp', 'fleets/**/*.fleet.lisp']
  schedule:
    - cron: '0 4 * * *'

jobs:
  convert:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo install pleme-doc-gen --version "^0.1.17"

      - name: Auth gh CLI
        env:
          GH_TOKEN: ${{ secrets.PLEME_PUBLISH_PAT }}
        run: gh auth status

      - name: Convert every .ossconv.lisp
        env:
          GH_TOKEN: ${{ secrets.PLEME_PUBLISH_PAT }}
        run: |
          mkdir -p receipts conversions
          if compgen -G "inventory/**/*.ossconv.lisp" > /dev/null; then
            for f in inventory/**/*.ossconv.lisp; do
              echo "── $f ──"
              pleme-doc-gen convert --source "$f" --out ./conversions --yes \
                | tee -a receipts/conversions.jsonl
            done
          fi

      - name: Fleet-forge every .fleet.lisp
        env:
          GH_TOKEN: ${{ secrets.PLEME_PUBLISH_PAT }}
        run: |
          if compgen -G "fleets/**/*.fleet.lisp" > /dev/null; then
            for f in fleets/**/*.fleet.lisp; do
              echo "── $f ──"
              pleme-doc-gen fleet-forge --source "$f" --out ./conversions \
                | tee -a receipts/fleets.jsonl
            done
          fi

      - name: Aggregate receipts
        if: always()
        run: |
          if [ -f receipts/conversions.jsonl ]; then
            jq -s '
              { total:  length,
                ok:     [.[] | select(.gates.A == "pass" and .gates.B == "pass")] | length,
                failed: [.[] | select(.gates.A != "pass" or  .gates.B != "pass") | .conversion] }
            ' receipts/conversions.jsonl | tee receipts/summary.json
          fi

      - uses: actions/upload-artifact@v4
        with:
          name: conversion-receipts
          path: receipts/

      - name: Fail on broken conversions
        if: always()
        run: |
          if [ -f receipts/summary.json ]; then
            FAILED=$(jq '.failed | length' receipts/summary.json)
            if [ "$FAILED" -gt 0 ]; then
              echo "$FAILED conversion(s) failed"
              jq '.failed[]' receipts/summary.json
              exit 1
            fi
          fi
"#;

const EXAMPLE_OSSCONV: &str = r#";; Example .ossconv.lisp — one upstream OSS repo to wrap.
;;
;; Drop files like this under inventory/<category>/<wrapper-name>.ossconv.lisp
;; — the convert-and-publish.yml workflow loops `pleme-doc-gen convert`
;; over every file in the directory tree on PR + push + nightly.
;;
;; Replace the upstream + wrapper + description with your real target,
;; or delete this file once you've added real entries.

(defossconv pleme-io-wrap-EXAMPLE
  :upstream         "OWNER/REPO"
  :wrapper          "wrap-EXAMPLE"
  :rationale        "Replace with the typed reason this OSS is worth wrapping"
  :expect-ecosystem :rust-single-crate   ;; or any other supported ecosystem
  :description      "pleme-io typed wrapper for OWNER/REPO"
  :license          "MIT"
  :version          "0.1.0"
  :publish-org      "pleme-io"
  :publish-mode     :dry-run              ;; flip to :public when ready
  :emit-receipt     :stdin-for-bulk)
"#;

const EXAMPLE_FLEET: &str = r#";; Example .fleet.lisp — N-repo architecture suite.
;;
;; A fleet bundles N typed caixas as ONE form. `fleet-forge` explodes
;; them into N parallel repo scaffolds. Use for cohesive
;; architectures (microservice clusters, multi-platform packages,
;; observability stacks, etc.).
;;
;; Drop under fleets/<suite-name>.fleet.lisp + the workflow picks
;; them up alongside .ossconv.lisp inputs.

(defcaixa-fleet pleme-io-EXAMPLE-stack
  :description "Replace with your suite's architectural rationale"
  :caixas [
    {:name "EXAMPLE-component-1"
     :ecosystem :rust-single-crate
     :description "Component 1 — typed library"}
    {:name "EXAMPLE-component-2"
     :ecosystem :helm
     :description "Component 2 — typed Helm chart"}])
"#;

const GITIGNORE_TEMPLATE: &str = r#"# pleme-doc-gen caixa-init generated
conversions/
receipts/
target/
"#;

/// Bootstrap a complete OSS-consumption inventory repo at `out_root`.
/// Creates the dir structure + writes all templates with `{name}`
/// placeholders substituted.
pub fn init(out_root: &Path, name: &str) -> Result<Vec<std::path::PathBuf>> {
    let mut written: Vec<std::path::PathBuf> = Vec::new();
    std::fs::create_dir_all(out_root)?;
    std::fs::create_dir_all(out_root.join("inventory/rust-foundational"))?;
    std::fs::create_dir_all(out_root.join("inventory/rust-async"))?;
    std::fs::create_dir_all(out_root.join("inventory/cli-tools"))?;
    std::fs::create_dir_all(out_root.join("inventory/observability"))?;
    std::fs::create_dir_all(out_root.join("fleets"))?;
    std::fs::create_dir_all(out_root.join(".github/workflows"))?;

    let write = |rel: &str, body: String| -> Result<std::path::PathBuf> {
        let path = out_root.join(rel);
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        std::fs::write(&path, body)?;
        Ok(path)
    };

    written.push(write("README.md", README_TEMPLATE.replace("{name}", name))?);
    written.push(write(".github/workflows/convert-and-publish.yml",
        WORKFLOW_TEMPLATE.to_string())?);
    written.push(write("inventory/EXAMPLE.ossconv.lisp",
        EXAMPLE_OSSCONV.to_string())?);
    written.push(write("fleets/EXAMPLE.fleet.lisp",
        EXAMPLE_FLEET.to_string())?);
    written.push(write(".gitignore", GITIGNORE_TEMPLATE.to_string())?);

    Ok(written)
}

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

    #[test]
    fn init_creates_expected_layout() {
        let tmp = tempdir::TempDir::new("init").expect("tempdir");
        let files = init(tmp.path(), "test-inventory").expect("init");
        assert!(files.len() >= 5, "expected ≥5 files, got {}", files.len());
        assert!(tmp.path().join("README.md").is_file());
        assert!(tmp.path().join(".github/workflows/convert-and-publish.yml").is_file());
        assert!(tmp.path().join("inventory/EXAMPLE.ossconv.lisp").is_file());
        assert!(tmp.path().join("fleets/EXAMPLE.fleet.lisp").is_file());
        assert!(tmp.path().join("inventory/rust-foundational").is_dir());
        assert!(tmp.path().join("inventory/cli-tools").is_dir());
    }

    #[test]
    fn readme_substitutes_name_placeholder() {
        let tmp = tempdir::TempDir::new("init").expect("tempdir");
        init(tmp.path(), "my-cool-inventory").expect("init");
        let readme = std::fs::read_to_string(tmp.path().join("README.md"))
            .expect("readme");
        assert!(readme.contains("# my-cool-inventory"),
            "name should be substituted in header");
        assert!(readme.contains("pleme-doc-gen caixa-init --name my-cool-inventory"));
        assert!(!readme.contains("{name}"), "placeholder should be replaced");
    }

    #[test]
    fn workflow_delegates_to_substrate_published_pleme_doc_gen() {
        let tmp = tempdir::TempDir::new("init").expect("tempdir");
        init(tmp.path(), "x").expect("init");
        let wf = std::fs::read_to_string(
            tmp.path().join(".github/workflows/convert-and-publish.yml")
        ).expect("workflow");
        assert!(wf.contains("cargo install pleme-doc-gen"));
        assert!(wf.contains("pleme-doc-gen convert"));
        assert!(wf.contains("pleme-doc-gen fleet-forge"));
        assert!(wf.contains("PLEME_PUBLISH_PAT"));
        assert!(wf.contains("schedule:"));
        assert!(wf.contains("nightly") || wf.contains("0 4 * * *"));
    }

    #[test]
    fn examples_use_clear_placeholders_not_real_slugs() {
        let tmp = tempdir::TempDir::new("init").expect("tempdir");
        init(tmp.path(), "x").expect("init");
        let oss = std::fs::read_to_string(tmp.path().join("inventory/EXAMPLE.ossconv.lisp"))
            .expect("oss example");
        assert!(oss.contains("OWNER/REPO"),
            "example should use placeholder slugs, not a real repo");
        assert!(oss.contains(":dry-run"),
            "example default should be :dry-run for safety");
    }
}