KeiSeiKit-1.0/_primitives/_rust/kei-forge/src/generate.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

159 lines
5.4 KiB
Rust

//! Atom-scaffolding generator — pure Rust templating.
//!
//! Reads the five templates in `<repo>/_templates/atom/`, substitutes the
//! six placeholder tokens (`__CRATE__`, `__CRATE_SNAKE__`, `__VERB__`,
//! `__VERB_SNAKE__`, `__KIND__`, `__DESCRIPTION__`), and writes the
//! resulting files into `<repo>/_primitives/_rust/<crate>/`.
//!
//! No shell-out. No sed. The Rust string replace cannot be coerced into
//! executing a secondary expression, so the description-injection attack
//! class defended by `form::validate_description` is structurally gone —
//! the whitelist stays as defence-in-depth, not the primary barrier.
//!
//! Atomicity: every file written is accumulated in a rollback list; on
//! any write failure the accumulator is flushed (files deleted best-
//! effort) before the error surfaces. Matches new-atom.sh's `trap ERR`.
use crate::form::ForgeRequest;
use serde::Serialize;
use std::fs;
use std::path::{Path, PathBuf};
mod placeholders;
mod paths;
mod rollback;
#[cfg(test)]
mod atom_tests;
use placeholders::Placeholders;
use paths::TargetPaths;
use rollback::Rollback;
/// Structured failure modes returned by the pure-Rust generator.
#[derive(Debug)]
pub enum GenerateError {
/// `<repo>/_primitives/_rust/<crate>/` does not exist.
CrateNotFound(PathBuf),
/// One of the five target files already exists — refuse to overwrite.
FileExists(PathBuf),
/// `<repo>/_templates/atom/` missing or a template file unreadable.
TemplateMissing(PathBuf),
/// Filesystem I/O failed mid-write.
Io(std::io::Error, PathBuf),
}
impl std::fmt::Display for GenerateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CrateNotFound(p) => write!(f, "crate directory not found: {}", p.display()),
Self::FileExists(p) => write!(f, "file already exists: {}", p.display()),
Self::TemplateMissing(p) => write!(f, "template missing: {}", p.display()),
Self::Io(e, p) => write!(f, "i/o error on {}: {e}", p.display()),
}
}
}
/// Result of a scaffolding attempt — wire-compatible with the previous
/// shell-out implementation.
#[derive(Debug, Serialize)]
pub struct ForgeResult {
pub success: bool,
pub files: Vec<String>,
pub errors: Vec<String>,
}
impl ForgeResult {
pub fn ok(files: Vec<String>) -> Self {
Self { success: true, files, errors: Vec::new() }
}
pub fn fail(err: impl Into<String>) -> Self {
Self { success: false, files: Vec::new(), errors: vec![err.into()] }
}
}
/// Locate the repo root by walking up from CARGO_MANIFEST_DIR until we
/// see `_templates/atom/`. Falls back to CWD if the env var is unset
/// (detached binary) or nothing matches (ship-of-Theseus invariant).
pub fn repo_root() -> PathBuf {
let start = std::env::var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| std::env::current_dir().unwrap_or_default());
let mut cur: &Path = &start;
loop {
if cur.join("_templates/atom").is_dir() {
return cur.to_path_buf();
}
match cur.parent() {
Some(p) => cur = p,
None => return start,
}
}
}
/// Thin wrapper the HTTP layer calls — discovers repo root, invokes the
/// pure-Rust core, projects errors onto the public `ForgeResult` shape.
pub fn forge(req: &ForgeRequest) -> ForgeResult {
let root = repo_root();
match generate_atom(req, &root) {
Ok(files) => ForgeResult::ok(
files
.into_iter()
.map(|p| rel_to_root(&p, &root))
.collect(),
),
Err(e) => ForgeResult::fail(e.to_string()),
}
}
/// Core entry point — pure fn over (req, root), exposed for unit tests.
///
/// On success returns the five absolute paths in declaration order. On
/// failure, no partial writes survive (rollback on drop).
pub fn generate_atom(
req: &ForgeRequest,
repo_root: &Path,
) -> Result<Vec<PathBuf>, GenerateError> {
let placeholders = Placeholders::from_request(req);
let targets = TargetPaths::resolve(repo_root, req)?;
let template_dir = repo_root.join("_templates/atom");
if !template_dir.is_dir() {
return Err(GenerateError::TemplateMissing(template_dir));
}
targets.assert_none_exist()?;
targets.ensure_parent_dirs()?;
let mut rollback = Rollback::new();
for (template_rel, dest) in targets.pairs().iter() {
let src = template_dir.join(template_rel);
let content = fs::read_to_string(&src)
.map_err(|_| GenerateError::TemplateMissing(src.clone()))?;
let rendered = placeholders.substitute(&content);
write_or_rollback(dest, &rendered, &mut rollback)?;
}
Ok(rollback.finish())
}
/// Write one file, register in the rollback list, rollback on error.
fn write_or_rollback(
dest: &Path,
content: &str,
rollback: &mut Rollback,
) -> Result<(), GenerateError> {
match fs::write(dest, content) {
Ok(()) => {
rollback.record(dest.to_path_buf());
Ok(())
}
Err(e) => Err(GenerateError::Io(e, dest.to_path_buf())),
}
}
/// Render path relative to repo-root for the JSON response.
pub(crate) fn rel_to_root(path: &Path, root: &Path) -> String {
path.strip_prefix(root)
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| path.to_string_lossy().into_owned())
}