KeiSeiKit-1.0/_assembler/src/assembler.rs
Parfii-bot a4e667de10 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

164 lines
5.5 KiB
Rust

//! Agent assembler — composes markdown from manifest + blocks.
//! Output is deterministic: same manifest + blocks → byte-identical .md.
use crate::manifest::Manifest;
use crate::registry_client;
use crate::substrate;
use std::fs;
use std::path::Path;
pub fn assemble(m: &Manifest, blocks_dir: &Path) -> Result<String, String> {
// Substrate role expansion uses the kit root (parent of _blocks/).
let root = blocks_dir
.parent()
.ok_or_else(|| "blocks_dir has no parent (can't locate _roles/ and _capabilities/)".to_string())?;
let mut out = String::new();
write_frontmatter(m, &mut out);
write_role(m, &mut out);
write_substrate(m, root, &mut out)?;
write_rule_blocks(m, &mut out)?;
write_blocks(m, blocks_dir, &mut out)?;
write_domain_scope(m, &mut out);
write_handoffs(m, &mut out);
write_output_format(m, &mut out);
write_forbidden(m, &mut out);
write_references(m, &mut out);
Ok(out)
}
fn write_substrate(m: &Manifest, root: &Path, out: &mut String) -> Result<(), String> {
let Some(role) = &m.substrate_role else {
return Ok(());
};
let section = substrate::build_substrate_section(root, role)?;
out.push_str(&section);
Ok(())
}
fn write_frontmatter(m: &Manifest, out: &mut String) {
let desc = m.description.replace('\n', " ");
out.push_str("---\n");
out.push_str(&format!("name: {}\n", m.name));
out.push_str(&format!("description: {}\n", desc.trim()));
out.push_str(&format!("tools: {}\n", m.tools.join(", ")));
out.push_str(&format!("model: {}\n", m.model));
out.push_str("---\n\n");
out.push_str(&format!(
"<!-- GENERATED by _assembler (Rust) from _manifests/{}.toml — DO NOT EDIT. Edit the manifest. -->\n\n",
m.name
));
}
fn write_role(m: &Manifest, out: &mut String) {
out.push_str("# ROLE\n\n");
out.push_str(m.role.trim());
out.push_str("\n\n");
}
fn write_blocks(m: &Manifest, blocks_dir: &Path, out: &mut String) -> Result<(), String> {
for block in &m.blocks {
let path = blocks_dir.join(format!("{block}.md"));
let text = fs::read_to_string(&path)
.map_err(|e| format!("read {}: {e}", path.display()))?;
out.push_str(text.trim());
out.push_str("\n\n");
}
Ok(())
}
fn write_rule_blocks(m: &Manifest, out: &mut String) -> Result<(), String> {
if m.rule_blocks.is_empty() {
return Ok(());
}
let db_path = registry_client::default_db_path();
if !db_path.exists() {
eprintln!("warn [assembler]: registry not found at {} — skipping rule_blocks", db_path.display());
return Ok(());
}
let conn = registry_client::open_read_only(&db_path)?;
for name in &m.rule_blocks {
match registry_client::find_rule(&conn, name) {
Ok(Some(body)) => {
out.push_str(&format!("<!-- RULE: {name} -->\n"));
out.push_str(body.trim());
out.push_str("\n<!-- /RULE -->\n\n");
}
Ok(None) => {
return Err(format!(
"rule_block '{name}' not found in registry — \
run `kei-decompose decompose-rules` first or remove from manifest"
));
}
Err(e) => return Err(format!("registry lookup for '{name}': {e}")),
}
}
Ok(())
}
fn write_domain_scope(m: &Manifest, out: &mut String) {
out.push_str("# DOMAIN SCOPE\n\n**In:**\n");
for item in &m.domain_in {
out.push_str(&format!("- {item}\n"));
}
out.push_str("\n**Out (hand off):**\n");
for h in &m.handoff {
out.push_str(&format!("- `{}` — {}\n", h.target, h.trigger));
}
out.push('\n');
}
fn write_handoffs(m: &Manifest, out: &mut String) {
out.push_str("# HANDOFFS\n\n");
for h in &m.handoff {
out.push_str(&format!("- **{}** — {}\n", h.target, h.trigger));
}
out.push('\n');
}
fn write_output_format(m: &Manifest, out: &mut String) {
out.push_str("# OUTPUT FORMAT\n\n```\n");
out.push_str(&format!("=== {} REPORT ===\n", m.name.to_uppercase()));
out.push_str("Goal: <one-line>\n");
out.push_str("Scope: <in / out>\n");
out.push_str("Plan: <N steps>\n");
out.push_str("Executed: <files touched, LOC delta>\n");
out.push_str("Verify: <each criterion pass/fail>\n");
out.push_str("Evidence grades: <E1-E6 for each major claim>\n");
out.push_str("Handoffs made: <list>\n");
for extra in &m.output_extra_fields {
out.push_str(extra);
out.push('\n');
}
out.push_str("Blockers / next: <list>\n");
out.push_str("```\n\n");
}
fn write_forbidden(m: &Manifest, out: &mut String) {
out.push_str("# FORBIDDEN\n\n");
for item in &m.forbidden_domain {
out.push_str(&format!("- {item}\n"));
}
out.push('\n');
}
fn write_references(m: &Manifest, out: &mut String) {
out.push_str("# REFERENCES\n\n");
out.push_str("- `~/.claude/CLAUDE.md` — baseline umbrella\n");
out.push_str("- `~/.claude/memory/MEMORY.md` — memory index (adjust if your Claude Code user-slug path differs)\n");
if let Some(mp) = &m.memory_project {
out.push_str(&format!(
"- `~/.claude/memory/{mp}` — project memory (adjust path if needed)\n"
));
}
if let Some(pc) = &m.project_claudemd {
out.push_str(&format!("- `{pc}` — project CLAUDE.md\n"));
}
if let Some(refs) = &m.references {
for r in &refs.extra {
out.push_str(&format!("- `{r}`\n"));
}
}
}