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.
174 lines
5.7 KiB
Rust
174 lines
5.7 KiB
Rust
//! Target-path resolution for atom scaffolding.
|
|
//!
|
|
//! Given the repo root and a `ForgeRequest`, compute the five absolute
|
|
//! paths the generator will write, and the five relative template paths
|
|
//! it will read from. Decouples path arithmetic from I/O so tests can
|
|
//! assert directly on layout.
|
|
|
|
use super::GenerateError;
|
|
use crate::form::ForgeRequest;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
#[derive(Debug)]
|
|
pub struct TargetPaths {
|
|
pub md: PathBuf,
|
|
pub input_schema: PathBuf,
|
|
pub output_schema: PathBuf,
|
|
pub rust_src: PathBuf,
|
|
pub smoke_test: PathBuf,
|
|
}
|
|
|
|
impl TargetPaths {
|
|
/// Build the five destination paths for `req` under `repo_root`.
|
|
/// Returns `CrateNotFound` if `_primitives/_rust/<crate>/` is absent.
|
|
pub fn resolve(
|
|
repo_root: &Path,
|
|
req: &ForgeRequest,
|
|
) -> Result<Self, GenerateError> {
|
|
let crate_dir = repo_root
|
|
.join("_primitives/_rust")
|
|
.join(&req.crate_name);
|
|
if !crate_dir.is_dir() {
|
|
return Err(GenerateError::CrateNotFound(crate_dir));
|
|
}
|
|
let verb = &req.verb;
|
|
let verb_snake = req.verb.replace('-', "_");
|
|
Ok(Self {
|
|
md: crate_dir.join("atoms").join(format!("{verb}.md")),
|
|
input_schema: crate_dir
|
|
.join("atoms/schemas")
|
|
.join(format!("{verb}-input.json")),
|
|
output_schema: crate_dir
|
|
.join("atoms/schemas")
|
|
.join(format!("{verb}-output.json")),
|
|
rust_src: crate_dir
|
|
.join("src/atoms")
|
|
.join(format!("{verb_snake}.rs")),
|
|
smoke_test: crate_dir
|
|
.join("tests")
|
|
.join(format!("{verb_snake}_smoke.rs")),
|
|
})
|
|
}
|
|
|
|
/// Return `(template-rel-path, absolute-dest-path)` pairs in the same
|
|
/// order new-atom.sh emitted, so any downstream tooling that depends
|
|
/// on file-list ordering sees the same sequence.
|
|
pub fn pairs(&self) -> [(&'static str, &Path); 5] {
|
|
[
|
|
("atoms/__VERB__.md.template", &self.md),
|
|
(
|
|
"atoms/schemas/__VERB__-input.json.template",
|
|
&self.input_schema,
|
|
),
|
|
(
|
|
"atoms/schemas/__VERB__-output.json.template",
|
|
&self.output_schema,
|
|
),
|
|
("src/atoms/__VERB_SNAKE__.rs.template", &self.rust_src),
|
|
(
|
|
"tests/__VERB_SNAKE___smoke.rs.template",
|
|
&self.smoke_test,
|
|
),
|
|
]
|
|
}
|
|
|
|
/// Refuse to overwrite: error on the first extant target.
|
|
pub fn assert_none_exist(&self) -> Result<(), GenerateError> {
|
|
for (_, dest) in self.pairs().iter() {
|
|
if dest.exists() {
|
|
return Err(GenerateError::FileExists(dest.to_path_buf()));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Create `atoms/`, `atoms/schemas/`, `src/atoms/`, `tests/` under
|
|
/// the crate dir. Idempotent.
|
|
pub fn ensure_parent_dirs(&self) -> Result<(), GenerateError> {
|
|
let dirs = [
|
|
self.md.parent(),
|
|
self.input_schema.parent(),
|
|
self.output_schema.parent(),
|
|
self.rust_src.parent(),
|
|
self.smoke_test.parent(),
|
|
];
|
|
for dir in dirs.into_iter().flatten() {
|
|
fs::create_dir_all(dir)
|
|
.map_err(|e| GenerateError::Io(e, dir.to_path_buf()))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn req() -> ForgeRequest {
|
|
ForgeRequest {
|
|
crate_name: "kei-task".into(),
|
|
verb: "add-dep".into(),
|
|
kind: "command".into(),
|
|
description: "x".into(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn resolves_five_paths_under_crate() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let root = tmp.path();
|
|
fs::create_dir_all(root.join("_primitives/_rust/kei-task")).unwrap();
|
|
|
|
let t = TargetPaths::resolve(root, &req()).unwrap();
|
|
assert!(t.md.ends_with("kei-task/atoms/add-dep.md"));
|
|
assert!(t
|
|
.input_schema
|
|
.ends_with("kei-task/atoms/schemas/add-dep-input.json"));
|
|
assert!(t
|
|
.output_schema
|
|
.ends_with("kei-task/atoms/schemas/add-dep-output.json"));
|
|
assert!(t.rust_src.ends_with("kei-task/src/atoms/add_dep.rs"));
|
|
assert!(t
|
|
.smoke_test
|
|
.ends_with("kei-task/tests/add_dep_smoke.rs"));
|
|
}
|
|
|
|
#[test]
|
|
fn errors_when_crate_missing() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let err = TargetPaths::resolve(tmp.path(), &req()).unwrap_err();
|
|
assert!(matches!(err, GenerateError::CrateNotFound(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn assert_none_exist_trips_on_preexisting() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let root = tmp.path();
|
|
fs::create_dir_all(root.join("_primitives/_rust/kei-task/atoms")).unwrap();
|
|
fs::write(
|
|
root.join("_primitives/_rust/kei-task/atoms/add-dep.md"),
|
|
"x",
|
|
)
|
|
.unwrap();
|
|
|
|
let t = TargetPaths::resolve(root, &req()).unwrap();
|
|
let err = t.assert_none_exist().unwrap_err();
|
|
assert!(matches!(err, GenerateError::FileExists(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn ensure_parent_dirs_idempotent() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let root = tmp.path();
|
|
fs::create_dir_all(root.join("_primitives/_rust/kei-task")).unwrap();
|
|
|
|
let t = TargetPaths::resolve(root, &req()).unwrap();
|
|
t.ensure_parent_dirs().unwrap();
|
|
t.ensure_parent_dirs().unwrap(); // second call — no panic
|
|
assert!(t.md.parent().unwrap().is_dir());
|
|
assert!(t.input_schema.parent().unwrap().is_dir());
|
|
assert!(t.rust_src.parent().unwrap().is_dir());
|
|
assert!(t.smoke_test.parent().unwrap().is_dir());
|
|
}
|
|
}
|