spec_driven_docs/commands/
policy.rs1use camino::{Utf8Path, Utf8PathBuf};
13
14use crate::cli::policy::{PolicyArgs, PolicyVerb, ReconcileArgs};
15use crate::context::AppContext;
16use crate::error::AppError;
17use crate::output;
18use crate::services::policy::{Action, Plan};
19
20fn resolve_target(ctx: &AppContext, target: &Utf8Path) -> Result<Utf8PathBuf, AppError> {
21 if target.is_absolute() {
22 Ok(target.to_path_buf())
23 } else if target == "." {
24 Ok(ctx.cwd.clone())
25 } else {
26 Err(AppError::Usage("target must be absolute or .".to_string()))
27 }
28}
29
30fn print_plan(plan: &Plan) {
31 match &plan.action {
32 Action::Seed { destination, .. } => {
33 output::line(format!(
34 "{destination}: absent; write the seed, which defines `{}`",
35 plan.reconciliation.sentinel.rule
36 ));
37 }
38 Action::Append {
39 destination, block, ..
40 } => {
41 output::line(format!(
42 "{destination}: append `{}` to its Requirements section:",
43 plan.reconciliation.sentinel.rule
44 ));
45 for line in block.lines() {
46 output::line(format!(" {line}"));
47 }
48 }
49 Action::Checklist { destination, block } => {
50 output::line(format!(
51 "{destination}: not in a shape this command rewrites; add `{}` by hand:",
52 plan.reconciliation.sentinel.rule
53 ));
54 for line in block.lines() {
55 output::line(format!(" {line}"));
56 }
57 }
58 }
59}
60
61fn reconcile(ctx: &AppContext, args: &ReconcileArgs) -> Result<(), AppError> {
62 let target = resolve_target(ctx, &args.target)?;
63 let manifest = crate::services::verifier::read_manifest(&target)?;
64 let plans = crate::services::policy::plan(&target, manifest.docs_root)?;
65 if plans.is_empty() {
66 output::line("OK every active declaration is authorized by a local specification");
67 return Ok(());
68 }
69 for plan in &plans {
70 print_plan(plan);
71 }
72 let checklist: Vec<String> = plans
73 .iter()
74 .filter_map(|plan| match &plan.action {
75 Action::Checklist { destination, .. } => Some(destination.to_string()),
76 Action::Seed { .. } | Action::Append { .. } => None,
77 })
78 .collect();
79 if !args.apply {
80 output::line("DRY RUN: no files written");
81 return Ok(());
82 }
83 if !checklist.is_empty() {
88 return Err(AppError::Refused(format!(
89 "nothing written; add the rule by hand to: {}",
90 checklist.join(", ")
91 )));
92 }
93 for written in crate::services::policy::apply_all(&target, &plans)? {
94 output::line(format!("OK wrote {written}"));
95 }
96 Ok(())
97}
98
99pub fn run(ctx: &AppContext, args: PolicyArgs) -> Result<(), AppError> {
108 match args.verb {
109 PolicyVerb::Reconcile(args) => reconcile(ctx, &args),
110 }
111}