Skip to main content

spec_driven_docs/commands/
policy.rs

1//! `policy` subcommand: runtime-shape.
2//!
3//! One operator-invoked verb offers to bring an older adopted specification
4//! into agreement with the project's configuration. It previews first and
5//! writes only on request. The boundary is the actor, not the file: an
6//! automatic write during install or upgrade is forbidden, and an operator
7//! asking for a specific, previewed change is not. What needs reconciling,
8//! and how each specification is rewritten, is `services::policy`'s
9//! business; this handler resolves the target, prints the plan, and owns
10//! the apply.
11
12use 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    // A specification this command cannot rewrite safely stops the whole
84    // apply. Printing a correct checklist is a better outcome than a clever
85    // rewrite that loses an edit the project made, and writing the others
86    // while one waits would leave the operator with two states to track.
87    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
99/// Reconcile.
100///
101/// # Errors
102///
103/// [`AppError::Refused`] when a specification is not in a shape the
104/// command rewrites, a destination leaves the target, or a rewrite would
105/// not parse, with the target restored; manifest and I/O errors when the
106/// instance cannot be read.
107pub fn run(ctx: &AppContext, args: PolicyArgs) -> Result<(), AppError> {
108    match args.verb {
109        PolicyVerb::Reconcile(args) => reconcile(ctx, &args),
110    }
111}