Skip to main content

spec_driven_docs/services/
status.rs

1//! Report an instance's state as data.
2//!
3//! Status answers what the verifier's text answers, but as one typed
4//! record a machine can branch on, and it treats a missing instance as an
5//! answer rather than an error. A corrupt manifest still raises: absence
6//! and breakage are different findings, and reporting a broken instance as
7//! absent would invite a destructive re-init.
8
9use camino::{Utf8Path, Utf8PathBuf};
10use serde::Serialize;
11
12use crate::domain::manifest::{DOCS_SCRATCH_VAR, PLAN_ZONE_VAR, PlanZone};
13use crate::domain::profile::{DocsRoot, ProfileId};
14use crate::domain::version::CanonVersion;
15use crate::error::AppError;
16use crate::services::verifier;
17
18/// How an instance's canon version relates to this binary's.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
20#[serde(rename_all = "kebab-case")]
21pub enum Alignment {
22    /// The versions are equal.
23    Aligned,
24    /// The binary is newer; `sdd upgrade` moves the instance.
25    BinaryNewer,
26    /// The instance is newer; a newer `sdd` is required.
27    InstanceNewer,
28}
29
30/// One instance's state, as `sdd status` reports it.
31#[derive(Debug, Serialize)]
32pub struct StatusReport {
33    /// Whether the target carries an instance.
34    pub instance: bool,
35    /// The installed profile.
36    pub profile: Option<ProfileId>,
37    /// The instance's documentation root.
38    pub docs_root: Option<DocsRoot>,
39    /// The plan zone the instance records.
40    pub plan_zone: Option<PlanZone>,
41    /// The docs scratch the instance records.
42    pub docs_scratch: Option<Utf8PathBuf>,
43    /// What `SDD_PLAN_ZONE` carries here, when it is set. The variable
44    /// overrides the recorded value, so an audit needs both.
45    pub plan_zone_env: Option<String>,
46    /// What `SDD_DOCS_SCRATCH` carries here, when it is set.
47    pub docs_scratch_env: Option<String>,
48    /// The canon version that produced the instance.
49    pub canon_version: Option<CanonVersion>,
50    /// The version this binary carries.
51    pub binary_version: CanonVersion,
52    /// How the two versions relate.
53    pub alignment: Option<Alignment>,
54    /// Managed files missing, symlinked, or byte-drifted.
55    pub managed_drift: usize,
56    /// Adopted files awaiting reconciliation.
57    pub adopted_drift: usize,
58    /// Verification failures in total.
59    pub failures: usize,
60    /// Whether verification passes.
61    pub ok: Option<bool>,
62}
63
64/// What a variable carries, or `None` when it is unset or blank.
65fn variable(name: &str) -> Option<String> {
66    std::env::var(name)
67        .ok()
68        .map(|value| value.trim().to_string())
69        .filter(|value| !value.is_empty())
70}
71
72fn absent() -> StatusReport {
73    StatusReport {
74        instance: false,
75        profile: None,
76        docs_root: None,
77        plan_zone: None,
78        docs_scratch: None,
79        plan_zone_env: variable(PLAN_ZONE_VAR),
80        docs_scratch_env: variable(DOCS_SCRATCH_VAR),
81        canon_version: None,
82        binary_version: CanonVersion::current(),
83        alignment: None,
84        managed_drift: 0,
85        adopted_drift: 0,
86        failures: 0,
87        ok: None,
88    }
89}
90
91/// Report the target's instance state.
92///
93/// # Errors
94///
95/// [`AppError::ManifestInvalid`] when a manifest exists but cannot be
96/// trusted, and I/O errors when the disk cannot be read. A missing
97/// manifest is a report, not an error.
98pub fn status(target: &Utf8Path) -> Result<StatusReport, AppError> {
99    let manifest = match verifier::read_manifest(target) {
100        Ok(manifest) => manifest,
101        Err(AppError::ManifestMissing(_)) => return Ok(absent()),
102        Err(error) => return Err(error),
103    };
104    let report = verifier::verify(target)?;
105    let binary = CanonVersion::current();
106    let alignment = match manifest.canon_version.cmp(&binary) {
107        std::cmp::Ordering::Equal => Alignment::Aligned,
108        std::cmp::Ordering::Less => Alignment::BinaryNewer,
109        std::cmp::Ordering::Greater => Alignment::InstanceNewer,
110    };
111    Ok(StatusReport {
112        instance: true,
113        profile: Some(manifest.profile),
114        docs_root: Some(manifest.docs_root),
115        plan_zone: Some(manifest.plan_zone),
116        docs_scratch: manifest.docs_scratch,
117        plan_zone_env: variable(PLAN_ZONE_VAR),
118        docs_scratch_env: variable(DOCS_SCRATCH_VAR),
119        canon_version: Some(manifest.canon_version),
120        binary_version: binary,
121        alignment: Some(alignment),
122        managed_drift: report.managed_drift,
123        adopted_drift: report.adopted_drift,
124        failures: report.failures,
125        ok: Some(report.failures == 0),
126    })
127}