use anyhow::Result;
use std::path::Path;
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/
"#;
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");
}
}