use camino::Utf8Path;
use crate::cli::hooks::HooksArgs;
use crate::context::AppContext;
use crate::domain::instance_config::InstanceConfig;
use crate::domain::marker;
use crate::error::AppError;
use crate::output;
use crate::services::hooks_render::{RenderOptions, render_block};
pub const CONFIG: &str = ".pre-commit-config.yaml";
fn record_block_hash(target: &Utf8Path, spliced: &str) -> Result<(), AppError> {
let manifest_path = target.join(".spec-driven-docs/manifest.json");
let Ok(text) = std::fs::read_to_string(&manifest_path) else {
return Ok(());
};
let Some(hash) = marker::block_hash(spliced) else {
return Ok(());
};
let mut document: serde_json::Value = serde_json::from_str(&text)
.map_err(|error| AppError::ManifestInvalid(error.to_string()))?;
if let Some(blocks) = document
.get_mut("integration_blocks")
.and_then(serde_json::Value::as_array_mut)
{
for block in blocks.iter_mut() {
if block.get("path").and_then(serde_json::Value::as_str) == Some(CONFIG) {
block["marker_hash"] = serde_json::Value::String(hash.to_string());
}
}
}
let rendered = serde_json::to_string_pretty(&document)
.map_err(|error| AppError::ManifestInvalid(error.to_string()))?;
let scratch = manifest_path.with_extension("json.sdd-tmp");
std::fs::write(&scratch, format!("{rendered}\n"))?;
std::fs::rename(&scratch, &manifest_path)?;
Ok(())
}
pub fn run(_ctx: &AppContext, args: HooksArgs) -> Result<(), AppError> {
let target = Utf8Path::new(&args.target);
let declaration =
InstanceConfig::read(target).map_err(|error| AppError::Usage(error.to_string()))?;
let docs_root = args.docs_root.unwrap_or_else(|| {
crate::services::verifier::read_manifest(target)
.map_or_else(|_| "_docs".to_string(), |m| m.docs_root.to_string())
});
if !args.apply && !args.check {
output::line(
render_block(&RenderOptions {
docs_root,
entry: args.entry,
indent: args.indent,
declaration,
})
.trim_end_matches('\n'),
);
return Ok(());
}
let path = target.join(CONFIG);
let host = std::fs::read_to_string(&path)?;
let (base, _) = marker::split_block(&host)?;
let rendered = render_block(&RenderOptions {
docs_root,
entry: args.entry,
indent: marker::splice_indent(&base)?,
declaration,
});
let spliced = marker::splice(&base, &rendered)?;
if args.check {
if spliced == host {
return Ok(());
}
output::line(format!(
"FAIL {path} does not match the declaration; run 'sdd hooks --apply'"
));
return Err(AppError::Violations { count: 1 });
}
if spliced == host {
output::line(format!("OK {path} already matches the declaration"));
return Ok(());
}
let scratch = path.with_extension("yaml.sdd-tmp");
std::fs::write(&scratch, &spliced)?;
std::fs::rename(&scratch, &path)?;
record_block_hash(target, &spliced)?;
output::line(format!("OK rewrote the managed region in {path}"));
Ok(())
}