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(
65        &target,
66        manifest.docs_root,
67        &crate::release::embedded::EmbeddedReleaseBundle::new(),
68    )?;
69    if plans.is_empty() {
70        output::line("OK every active declaration is authorized by a local specification");
71        return Ok(());
72    }
73    for plan in &plans {
74        print_plan(plan);
75    }
76    let checklist: Vec<String> = plans
77        .iter()
78        .filter_map(|plan| match &plan.action {
79            Action::Checklist { destination, .. } => Some(destination.to_string()),
80            Action::Seed { .. } | Action::Append { .. } => None,
81        })
82        .collect();
83    if !args.apply {
84        output::line("DRY RUN: no files written");
85        return Ok(());
86    }
87    // A specification this command cannot rewrite safely stops the whole
88    // apply. Printing a correct checklist is a better outcome than a clever
89    // rewrite that loses an edit the project made, and writing the others
90    // while one waits would leave the operator with two states to track.
91    if !checklist.is_empty() {
92        return Err(AppError::Refused(format!(
93            "nothing written; add the rule by hand to: {}",
94            checklist.join(", ")
95        )));
96    }
97    for written in crate::services::policy::apply_all(
98        &target,
99        &plans,
100        &crate::release::embedded::EmbeddedReleaseBundle::new(),
101    )? {
102        output::line(format!("OK wrote {written}"));
103    }
104    Ok(())
105}
106
107/// Reconcile.
108///
109/// # Errors
110///
111/// [`AppError::Refused`] when a specification is not in a shape the
112/// command rewrites, a destination leaves the target, or a rewrite would
113/// not parse, with the target restored; manifest and I/O errors when the
114/// instance cannot be read.
115pub fn run(ctx: &AppContext, args: PolicyArgs) -> Result<(), AppError> {
116    match args.verb {
117        PolicyVerb::Reconcile(args) => reconcile(ctx, &args),
118    }
119}