spec_driven_docs/commands/
hooks.rs1use camino::Utf8Path;
14
15use crate::cli::hooks::HooksArgs;
16use crate::context::AppContext;
17use crate::domain::instance_config::InstanceConfig;
18use crate::domain::marker;
19use crate::error::AppError;
20use crate::output;
21use crate::services::hooks_render::{RenderOptions, render_block};
22
23pub const CONFIG: &str = ".pre-commit-config.yaml";
25
26fn record_block_hash(target: &Utf8Path, spliced: &str) -> Result<(), AppError> {
28 let manifest_path = target.join(".spec-driven-docs/manifest.json");
29 let Ok(text) = std::fs::read_to_string(&manifest_path) else {
30 return Ok(());
33 };
34 let Some(hash) = marker::block_hash(spliced) else {
35 return Ok(());
36 };
37 let mut document: serde_json::Value = serde_json::from_str(&text)
38 .map_err(|error| AppError::ManifestInvalid(error.to_string()))?;
39 if let Some(blocks) = document
40 .get_mut("integration_blocks")
41 .and_then(serde_json::Value::as_array_mut)
42 {
43 for block in blocks.iter_mut() {
44 if block.get("path").and_then(serde_json::Value::as_str) == Some(CONFIG) {
45 block["marker_hash"] = serde_json::Value::String(hash.to_string());
46 }
47 }
48 }
49 let rendered = serde_json::to_string_pretty(&document)
50 .map_err(|error| AppError::ManifestInvalid(error.to_string()))?;
51 let scratch = manifest_path.with_extension("json.sdd-tmp");
52 std::fs::write(&scratch, format!("{rendered}\n"))?;
53 std::fs::rename(&scratch, &manifest_path)?;
54 Ok(())
55}
56
57pub fn run(_ctx: &AppContext, args: HooksArgs) -> Result<(), AppError> {
65 let target = Utf8Path::new(&args.target);
66 let declaration =
67 InstanceConfig::read(target).map_err(|error| AppError::Usage(error.to_string()))?;
68 let docs_root = args.docs_root.unwrap_or_else(|| {
71 crate::services::verifier::read_manifest(target)
72 .map_or_else(|_| "_docs".to_string(), |m| m.docs_root.to_string())
73 });
74
75 if !args.apply && !args.check {
76 output::line(
77 render_block(&RenderOptions {
78 docs_root,
79 entry: args.entry,
80 indent: args.indent,
81 declaration,
82 })
83 .trim_end_matches('\n'),
84 );
85 return Ok(());
86 }
87
88 let path = target.join(CONFIG);
89 let host = std::fs::read_to_string(&path)?;
90 let (base, _) = marker::split_block(&host)?;
97 let rendered = render_block(&RenderOptions {
98 docs_root,
99 entry: args.entry,
100 indent: marker::splice_indent(&base)?,
101 declaration,
102 });
103 let spliced = marker::splice(&base, &rendered)?;
104
105 if args.check {
106 if spliced == host {
107 return Ok(());
108 }
109 output::line(format!(
110 "FAIL {path} does not match the declaration; run 'sdd hooks --apply'"
111 ));
112 return Err(AppError::Violations { count: 1 });
113 }
114
115 if spliced == host {
116 output::line(format!("OK {path} already matches the declaration"));
117 return Ok(());
118 }
119 let scratch = path.with_extension("yaml.sdd-tmp");
122 std::fs::write(&scratch, &spliced)?;
123 std::fs::rename(&scratch, &path)?;
124 record_block_hash(target, &spliced)?;
128 output::line(format!("OK rewrote the managed region in {path}"));
129 Ok(())
130}