#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
#[path = "build_support/agent_templates.rs"]
mod agent_templates;
#[path = "build_support/edit_ops_spec.rs"]
mod edit_ops_spec;
use agent_templates::generate_codex_agent_templates;
use edit_ops_spec::{
EditOpsSpec, NestedNodeRule, RuntimeGetRule, RuntimeSetMode, RuntimeSetRule, load_edit_ops_spec,
};
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
const DISTRIBUTED_SKILL_DIRS: &[&str] = &[
"discuss",
"gov",
"quick",
"spec",
"rfc-writer",
"adr-writer",
"wi-writer",
"guard-writer",
"commit",
"migrate",
"decision-analysis",
"detach",
];
fn main() {
println!("cargo:rerun-if-changed=.claude/skills");
println!("cargo:rerun-if-changed=.claude/agents");
println!("cargo:rerun-if-changed=gov/schema/edit-ops.schema.json");
println!("cargo:rerun-if-changed=gov/schema/edit-ops.json");
generate_skill_assets().expect("failed to generate skill asset manifest");
generate_edit_rules().expect("failed to generate edit rules from SSOT");
generate_codex_agent_templates().expect("failed to generate codex agent templates");
}
fn generate_skill_assets() -> Result<(), Box<dyn Error>> {
let skill_root = Path::new(".claude/skills");
let mut files = Vec::new();
for skill_dir in DISTRIBUTED_SKILL_DIRS {
collect_files(&skill_root.join(skill_dir), &mut files)?;
}
files.sort();
let mut out = String::new();
out.push_str("// @generated by build.rs from distributed .claude/skills/* bundles\n");
out.push_str("// Do not edit manually.\n\n");
out.push_str("pub const SKILL_ASSETS: &[(&str, &str)] = &[\n");
for file in files {
let skill_rel = file
.strip_prefix(skill_root)
.map_err(|e| format!("failed to relativize skill asset {}: {e}", file.display()))?;
let skill_rel = path_to_slash_string(skill_rel)?;
let output_rel = format!("skills/{skill_rel}");
let source_rel = format!("/.claude/skills/{skill_rel}");
out.push_str(&format!(
" ({:?}, include_str!(concat!(env!(\"CARGO_MANIFEST_DIR\"), {:?}))),\n",
output_rel, source_rel
));
}
out.push_str("];\n");
let out_dir = std::env::var("OUT_DIR")?;
let out_path = Path::new(&out_dir).join("skill_assets.rs");
fs::write(out_path, out)?;
Ok(())
}
fn collect_files(root: &Path, files: &mut Vec<PathBuf>) -> Result<(), Box<dyn Error>> {
for entry in
fs::read_dir(root).map_err(|e| format!("failed to read {}: {e}", root.display()))?
{
let entry = entry?;
let path = entry.path();
let file_type = entry.file_type()?;
if file_type.is_dir() {
collect_files(&path, files)?;
} else if file_type.is_file() {
files.push(path);
}
}
Ok(())
}
fn path_to_slash_string(path: &Path) -> Result<String, Box<dyn Error>> {
let mut parts = Vec::new();
for component in path.components() {
let component = component.as_os_str().to_str().ok_or_else(|| {
format!(
"skill asset path is not valid UTF-8: {}",
path.to_string_lossy()
)
})?;
parts.push(component.to_string());
}
Ok(parts.join("/"))
}
fn generate_edit_rules() -> Result<(), Box<dyn Error>> {
let spec_path = Path::new("gov/schema/edit-ops.json");
let schema_path = Path::new("gov/schema/edit-ops.schema.json");
let spec = load_edit_ops_spec(spec_path, schema_path)?;
let rendered = render_edit_rules(&spec)?;
let rendered_runtime = render_edit_runtime(&spec)?;
let out_dir = std::env::var("OUT_DIR")?;
let out_path = Path::new(&out_dir).join("edit_rules_generated.rs");
let out_runtime_path = Path::new(&out_dir).join("edit_runtime_generated.rs");
fs::write(out_path, rendered)?;
fs::write(out_runtime_path, rendered_runtime)?;
Ok(())
}
fn render_edit_rules(spec: &EditOpsSpec) -> Result<String, Box<dyn Error>> {
let mut out = String::new();
out.push_str("// @generated by build.rs from gov/schema/edit-ops.json\n");
out.push_str("// Do not edit manually.\n\n");
out.push_str(&format!(
"#[cfg(test)]\npub const EDIT_RULES_VERSION: u32 = {};\n\n",
spec.version
));
out.push_str("define_alias_resolver! {\n");
for alias in &spec.aliases {
out.push_str(&format!(
" ({:?}, {:?}),\n",
alias.alias, alias.canonical
));
}
out.push_str("}\n\n");
out.push_str("define_legacy_prefix_resolver! {\n");
for rule in &spec.legacy_prefixes {
out.push_str(&format!(" ({:?}, [", rule.prefix));
for (idx, field) in rule.allowed_fields.iter().enumerate() {
if idx > 0 {
out.push_str(", ");
}
out.push_str(&format!("{field:?}"));
}
out.push_str("]),\n");
}
out.push_str("}\n\n");
out.push_str("pub const SIMPLE_RULES: &[SimpleFieldRule] = &[\n");
for field in &spec.simple_rules {
let kind = match field.kind.as_str() {
"scalar" => "Scalar",
"list" => "List",
other => return Err(format!("unknown simple field kind in SSOT: {other}").into()),
};
out.push_str(" SimpleFieldRule {\n");
out.push_str(&format!(" artifact: {:?},\n", field.artifact));
out.push_str(&format!(" name: {:?},\n", field.name));
out.push_str(&format!(" kind: FieldKind::{kind},\n"));
out.push_str(" verbs: &[\n");
for verb in &field.verbs {
out.push_str(&format!(" {verb:?},\n"));
}
out.push_str(" ],\n");
out.push_str(" },\n");
}
out.push_str("];\n\n");
let mut nested_defs = String::new();
let mut nested_roots = String::new();
for root in &spec.nested_rules {
let const_name = format!(
"NESTED_NODE_{}_{}_ROOT",
root.artifact.to_uppercase(),
sanitize_const_fragment(&root.root)
);
render_nested_node_defs(&mut nested_defs, &const_name, &root.node)?;
nested_roots.push_str(" NestedRootRule {\n");
nested_roots.push_str(&format!(" artifact: {:?},\n", root.artifact));
nested_roots.push_str(&format!(" root: {:?},\n", root.root));
nested_roots.push_str(&format!(
" content_path: {},\n",
runtime_path_expr(&root.content_path)
));
nested_roots.push_str(&format!(" node: &{},\n", const_name));
nested_roots.push_str(" },\n");
}
out.push_str(&nested_defs);
out.push_str("pub const NESTED_RULES: &[NestedRootRule] = &[\n");
out.push_str(&nested_roots);
out.push_str("];\n");
out.push('\n');
out.push_str("pub const VALIDATION_RULES: &[FieldValidationRule] = &[\n");
for rule in &spec.validation_rules {
let kind = match rule.rule.as_str() {
"semver" => "Semver",
"clause_superseded_by" => "ClauseSupersededBy",
"artifact_ref" => "ArtifactRef",
"enum_value" => "EnumValue",
other => return Err(format!("unknown validation rule in SSOT: {other}").into()),
};
out.push_str(" FieldValidationRule {\n");
out.push_str(&format!(" artifact: {:?},\n", rule.artifact));
out.push_str(&format!(" field: {:?},\n", rule.field));
out.push_str(&format!(" kind: ValidationKind::{kind},\n"));
out.push_str(" },\n");
}
out.push_str("];\n");
Ok(out)
}
fn sanitize_const_fragment(name: &str) -> String {
name.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() {
ch.to_ascii_uppercase()
} else {
'_'
}
})
.collect()
}
fn render_nested_node_defs(
out: &mut String,
const_name: &str,
node: &NestedNodeRule,
) -> Result<(), Box<dyn Error>> {
match node {
NestedNodeRule::Scalar { verbs, set_mode } => {
out.push_str(&format!(
"const {const_name}: NestedNodeRule = NestedNodeRule {{\n"
));
out.push_str(" kind: NestedNodeKind::Scalar,\n");
out.push_str(&format!(" verbs: {},\n", render_verbs_expr(verbs)));
out.push_str(" text_key: None,\n");
out.push_str(&format!(
" set_mode: {},\n",
render_nested_scalar_mode_expr(set_mode.as_ref())?
));
out.push_str(" item: None,\n");
out.push_str(" fields: &[],\n");
out.push_str("};\n\n");
}
NestedNodeRule::Object { verbs, fields } => {
for field in fields {
let child_const = format!("{const_name}_{}", sanitize_const_fragment(&field.name));
render_nested_node_defs(out, &child_const, &field.node)?;
}
let fields_const = format!("{const_name}_FIELDS");
out.push_str(&format!("const {fields_const}: &[NestedChildRule] = &[\n"));
for field in fields {
let child_const = format!("{const_name}_{}", sanitize_const_fragment(&field.name));
out.push_str(" NestedChildRule {\n");
out.push_str(&format!(" name: {:?},\n", field.name));
out.push_str(&format!(" node: &{},\n", child_const));
out.push_str(" },\n");
}
out.push_str("];\n");
out.push_str(&format!(
"const {const_name}: NestedNodeRule = NestedNodeRule {{\n"
));
out.push_str(" kind: NestedNodeKind::Object,\n");
out.push_str(&format!(" verbs: {},\n", render_verbs_expr(verbs)));
out.push_str(" text_key: None,\n");
out.push_str(" set_mode: None,\n");
out.push_str(" item: None,\n");
out.push_str(&format!(" fields: {},\n", fields_const));
out.push_str("};\n\n");
}
NestedNodeRule::List {
verbs,
text_key,
item,
} => {
let item_const = format!("{const_name}_ITEM");
render_nested_node_defs(out, &item_const, item)?;
out.push_str(&format!(
"const {const_name}: NestedNodeRule = NestedNodeRule {{\n"
));
out.push_str(" kind: NestedNodeKind::List,\n");
out.push_str(&format!(" verbs: {},\n", render_verbs_expr(verbs)));
out.push_str(&format!(
" text_key: {},\n",
match text_key {
Some(key) => format!("Some({key:?})"),
None => "None".to_string(),
}
));
out.push_str(" set_mode: None,\n");
out.push_str(&format!(" item: Some(&{}),\n", item_const));
out.push_str(" fields: &[],\n");
out.push_str("};\n\n");
}
}
Ok(())
}
fn render_verbs_expr(verbs: &[String]) -> String {
let mut out = String::from("&[");
for (idx, verb) in verbs.iter().enumerate() {
if idx > 0 {
out.push_str(", ");
}
out.push_str(&format!("{verb:?}"));
}
out.push(']');
out
}
fn render_nested_scalar_mode_expr(mode: Option<&RuntimeSetMode>) -> Result<String, Box<dyn Error>> {
match mode {
None => Ok("None".to_string()),
Some(RuntimeSetMode::String) => Ok("Some(NestedScalarMode::String)".to_string()),
Some(RuntimeSetMode::Integer) => Ok("Some(NestedScalarMode::Integer)".to_string()),
Some(RuntimeSetMode::Enum {
allowed,
invalid_msg,
code,
}) => {
let allowed_expr = {
let mut expr = String::from("&[");
for (idx, value) in allowed.iter().enumerate() {
if idx > 0 {
expr.push_str(", ");
}
expr.push_str(&format!("{value:?}"));
}
expr.push(']');
expr
};
let code_expr = match code {
Some(code) => format!("Some(DiagnosticCode::{})", diagnostic_code_variant(code)?),
None => "None".to_string(),
};
Ok(format!(
"Some(NestedScalarMode::Enum {{ allowed: {}, invalid_msg: {:?}, code: {} }})",
allowed_expr, invalid_msg, code_expr
))
}
}
}
fn diagnostic_code_variant(code: &str) -> Result<&str, Box<dyn Error>> {
code.strip_prefix('E')
.map(|_| code)
.ok_or_else(|| format!("invalid diagnostic code in SSOT: {code}").into())
}
fn render_edit_runtime(spec: &EditOpsSpec) -> Result<String, Box<dyn Error>> {
let mut out = String::new();
out.push_str("// @generated by build.rs from gov/schema/edit-ops.json\n");
out.push_str("// Do not edit manually.\n\n");
out.push_str("const RUNTIME_FIELDS: &[RuntimeFieldEntry] = &[\n");
for field in &spec.runtime_fields {
out.push_str(" RuntimeFieldEntry {\n");
out.push_str(&format!(
" artifact: {},\n",
runtime_artifact_expr(&field.artifact)?
));
out.push_str(&format!(" field: {:?},\n", field.name));
out.push_str(&format!(
" get: {},\n",
runtime_get_expr(field.get.as_ref())?
));
out.push_str(&format!(
" set: {},\n",
runtime_set_expr(field.set.as_ref())?
));
out.push_str(&format!(
" list_path: {},\n",
runtime_list_path_expr(field.list_path.as_ref())
));
out.push_str(" },\n");
}
out.push_str("];\n");
Ok(out)
}
fn runtime_artifact_expr(artifact: &str) -> Result<&'static str, Box<dyn Error>> {
match artifact {
"rfc" => Ok("ArtifactType::Rfc"),
"clause" => Ok("ArtifactType::Clause"),
"adr" => Ok("ArtifactType::Adr"),
"work" => Ok("ArtifactType::WorkItem"),
"guard" => Ok("ArtifactType::Guard"),
other => Err(format!("unknown runtime artifact in SSOT: {other}").into()),
}
}
fn runtime_path_expr(path: &[String]) -> String {
let mut out = String::from("&[");
for (idx, seg) in path.iter().enumerate() {
if idx > 0 {
out.push_str(", ");
}
out.push_str(&format!("{seg:?}"));
}
out.push(']');
out
}
fn runtime_list_path_expr(path: Option<&Vec<String>>) -> String {
match path {
Some(path) => format!("Some({})", runtime_path_expr(path)),
None => "None".to_string(),
}
}
fn runtime_get_expr(get: Option<&RuntimeGetRule>) -> Result<String, Box<dyn Error>> {
let Some(get) = get else {
return Ok("None".to_string());
};
let render = match get.render.as_str() {
"scalar" => "RenderMode::Scalar".to_string(),
"csv_strings" => "RenderMode::CsvStrings".to_string(),
"line_strings" => "RenderMode::LineStrings".to_string(),
"text_lines" => {
let text_key = get.text_key.as_ref().ok_or_else(|| {
"runtime get render=text_lines requires text_key in SSOT".to_string()
})?;
format!("RenderMode::TextLines {{ text_key: {:?} }}", text_key)
}
"status_lines" => {
let status_key = get.status_key.as_ref().ok_or_else(|| {
"runtime get render=status_lines requires status_key in SSOT".to_string()
})?;
let text_key = get.text_key.as_ref().ok_or_else(|| {
"runtime get render=status_lines requires text_key in SSOT".to_string()
})?;
format!(
"RenderMode::StatusLines {{ status_key: {:?}, text_key: {:?} }}",
status_key, text_key
)
}
other => return Err(format!("unknown runtime get render in SSOT: {other}").into()),
};
Ok(format!(
"Some(SimpleFieldSpec {{ path: {}, render: {} }})",
runtime_path_expr(&get.path),
render
))
}
fn runtime_set_expr(set: Option<&RuntimeSetRule>) -> Result<String, Box<dyn Error>> {
let Some(set) = set else {
return Ok("None".to_string());
};
let mode = match &set.mode {
RuntimeSetMode::String => "SetMode::String".to_string(),
RuntimeSetMode::Integer => "SetMode::Integer".to_string(),
RuntimeSetMode::Enum {
allowed,
invalid_msg,
code,
} => {
let allowed_expr = runtime_path_expr(allowed);
let code_expr = match code {
Some(code) => format!("Some(DiagnosticCode::{code})"),
None => "None".to_string(),
};
format!(
"SetMode::Enum {{ allowed: {}, invalid_msg: {:?}, code: {} }}",
allowed_expr, invalid_msg, code_expr
)
}
};
Ok(format!(
"Some(SimpleSetSpec {{ path: {}, mode: {} }})",
runtime_path_expr(&set.path),
mode
))
}