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;
13use crate::domain::paths::{
14    self, ActivePaths, Paths, UserEnv, docs_scratch_location, recorded_paths, variable,
15};
16use crate::domain::profile::{DocsRoot, ProfileId};
17use crate::domain::version::CanonVersion;
18use crate::error::AppError;
19use crate::services::verifier;
20
21/// The machine schema this report declares.
22///
23/// Adding a field within schema 2 is additive. Removing one, renaming one,
24/// or changing one's type is the next schema, because a reader that
25/// branched on the old shape cannot tell the two apart otherwise.
26pub const SCHEMA: &str = "sdd.status/2";
27
28/// How an instance's canon version relates to this binary's.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
30#[serde(rename_all = "kebab-case")]
31pub enum Alignment {
32    /// The versions are equal.
33    Aligned,
34    /// The binary is newer; `sdd upgrade` moves the instance.
35    BinaryNewer,
36    /// The instance is newer; a newer `sdd` is required.
37    InstanceNewer,
38}
39
40/// One instance's state, as `sdd status` reports it.
41#[derive(Debug, Serialize)]
42pub struct StatusReport {
43    /// The machine schema this object declares.
44    pub schema: &'static str,
45    /// Whether the target carries an instance.
46    pub instance: bool,
47    /// The installed profile.
48    pub profile: Option<ProfileId>,
49    /// The instance's documentation root.
50    pub docs_root: Option<DocsRoot>,
51    /// The docs scratch the instance records.
52    pub docs_scratch: Option<Utf8PathBuf>,
53    /// What `SDD_DOCS_SCRATCH` carries here, when it is set. The variable
54    /// overrides the recorded value, so an audit needs both.
55    pub docs_scratch_env: Option<String>,
56    /// The canon version that produced the instance.
57    pub canon_version: Option<CanonVersion>,
58    /// The version this binary carries.
59    pub binary_version: CanonVersion,
60    /// How the two versions relate.
61    pub alignment: Option<Alignment>,
62    /// Managed files missing, symlinked, or byte-drifted.
63    pub managed_drift: usize,
64    /// Adopted files awaiting reconciliation.
65    pub adopted_drift: usize,
66    /// Verification failures in total.
67    pub failures: usize,
68    /// Whether verification passes.
69    pub ok: Option<bool>,
70    /// Every path this binary can name for this target and this user.
71    ///
72    /// A skill reads this section rather than spelling a path of its own.
73    /// Every entry states what decided it, so a reader can tell a recorded
74    /// answer from a derived one.
75    pub paths: Paths,
76}
77
78/// What this target offers for the docs scratch.
79fn proposals(target: &Utf8Path, env: &UserEnv) -> paths::Proposals {
80    paths::proposals(env, |relative| target.join(relative).is_dir())
81}
82
83/// Every path, with no instance recorded.
84fn derived_paths(target: &Utf8Path, env: &UserEnv) -> Paths {
85    Paths {
86        user: env.user_paths(),
87        active: None,
88        candidates: paths::candidates(),
89        proposals: proposals(target, env),
90    }
91}
92
93fn absent(target: &Utf8Path) -> StatusReport {
94    let env = UserEnv::from_process();
95    StatusReport {
96        schema: SCHEMA,
97        paths: derived_paths(target, &env),
98        instance: false,
99        profile: None,
100        docs_root: None,
101        docs_scratch: None,
102        docs_scratch_env: variable(DOCS_SCRATCH_VAR),
103        canon_version: None,
104        binary_version: CanonVersion::current(),
105        alignment: None,
106        managed_drift: 0,
107        adopted_drift: 0,
108        failures: 0,
109        ok: None,
110    }
111}
112
113/// Report the target's instance state.
114///
115/// # Errors
116///
117/// [`AppError::ManifestInvalid`] when a manifest exists but cannot be
118/// trusted, and I/O errors when the disk cannot be read. A missing
119/// manifest is a report, not an error.
120pub fn status(target: &Utf8Path) -> Result<StatusReport, AppError> {
121    let manifest = match verifier::read_manifest(target) {
122        Ok(manifest) => manifest,
123        Err(AppError::ManifestMissing(_)) => return Ok(absent(target)),
124        Err(error) => return Err(error),
125    };
126    let report = verifier::verify(
127        target,
128        &crate::release::embedded::EmbeddedReleaseBundle::new(),
129    )?;
130    let binary = CanonVersion::current();
131    let alignment = match manifest.canon_version.cmp(&binary) {
132        std::cmp::Ordering::Equal => Alignment::Aligned,
133        std::cmp::Ordering::Less => Alignment::BinaryNewer,
134        std::cmp::Ordering::Greater => Alignment::InstanceNewer,
135    };
136    let env = UserEnv::from_process();
137    let mut resolved = derived_paths(target, &env);
138    resolved.active = Some(ActivePaths {
139        profile: manifest.profile,
140        destinations: recorded_paths(manifest.docs_root),
141        docs_scratch: docs_scratch_location(manifest.docs_scratch.as_deref(), &env),
142    });
143    Ok(StatusReport {
144        schema: SCHEMA,
145        paths: resolved,
146        instance: true,
147        profile: Some(manifest.profile),
148        docs_root: Some(manifest.docs_root),
149        docs_scratch: manifest.docs_scratch,
150        docs_scratch_env: variable(DOCS_SCRATCH_VAR),
151        canon_version: Some(manifest.canon_version),
152        binary_version: binary,
153        alignment: Some(alignment),
154        managed_drift: report.managed_drift,
155        adopted_drift: report.adopted_drift,
156        failures: report.failures,
157        ok: Some(report.failures == 0),
158    })
159}