spec_driven_docs/services/
status.rs1use camino::{Utf8Path, Utf8PathBuf};
10use serde::Serialize;
11
12use crate::domain::manifest::{DOCS_SCRATCH_VAR, PLAN_ZONE_VAR, PlanZone};
13use crate::domain::paths::{
14 self, ActivePaths, Paths, UserEnv, docs_scratch_location, plan_zone_location, recorded_paths,
15 variable,
16};
17use crate::domain::profile::{DocsRoot, ProfileId};
18use crate::domain::version::CanonVersion;
19use crate::error::AppError;
20use crate::services::verifier;
21
22pub const SCHEMA: &str = "sdd.status/2";
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
31#[serde(rename_all = "kebab-case")]
32pub enum Alignment {
33 Aligned,
35 BinaryNewer,
37 InstanceNewer,
39}
40
41#[derive(Debug, Serialize)]
43pub struct StatusReport {
44 pub schema: &'static str,
46 pub instance: bool,
48 pub profile: Option<ProfileId>,
50 pub docs_root: Option<DocsRoot>,
52 pub plan_zone: Option<PlanZone>,
54 pub docs_scratch: Option<Utf8PathBuf>,
56 pub plan_zone_env: Option<String>,
59 pub docs_scratch_env: Option<String>,
61 pub canon_version: Option<CanonVersion>,
63 pub binary_version: CanonVersion,
65 pub alignment: Option<Alignment>,
67 pub managed_drift: usize,
69 pub adopted_drift: usize,
71 pub failures: usize,
73 pub ok: Option<bool>,
75 pub paths: Paths,
81}
82
83fn proposals(target: &Utf8Path, env: &UserEnv) -> paths::Proposals {
85 paths::proposals(env, |relative| target.join(relative).is_dir())
86}
87
88fn derived_paths(target: &Utf8Path, env: &UserEnv) -> Paths {
90 Paths {
91 user: env.user_paths(),
92 active: None,
93 candidates: paths::candidates(),
94 proposals: proposals(target, env),
95 }
96}
97
98fn absent(target: &Utf8Path) -> StatusReport {
99 let env = UserEnv::from_process();
100 StatusReport {
101 schema: SCHEMA,
102 paths: derived_paths(target, &env),
103 instance: false,
104 profile: None,
105 docs_root: None,
106 plan_zone: None,
107 docs_scratch: None,
108 plan_zone_env: variable(PLAN_ZONE_VAR),
109 docs_scratch_env: variable(DOCS_SCRATCH_VAR),
110 canon_version: None,
111 binary_version: CanonVersion::current(),
112 alignment: None,
113 managed_drift: 0,
114 adopted_drift: 0,
115 failures: 0,
116 ok: None,
117 }
118}
119
120pub fn status(target: &Utf8Path) -> Result<StatusReport, AppError> {
128 let manifest = match verifier::read_manifest(target) {
129 Ok(manifest) => manifest,
130 Err(AppError::ManifestMissing(_)) => return Ok(absent(target)),
131 Err(error) => return Err(error),
132 };
133 let report = verifier::verify(
134 target,
135 &crate::release::embedded::EmbeddedReleaseBundle::new(),
136 )?;
137 let binary = CanonVersion::current();
138 let alignment = match manifest.canon_version.cmp(&binary) {
139 std::cmp::Ordering::Equal => Alignment::Aligned,
140 std::cmp::Ordering::Less => Alignment::BinaryNewer,
141 std::cmp::Ordering::Greater => Alignment::InstanceNewer,
142 };
143 let env = UserEnv::from_process();
144 let mut resolved = derived_paths(target, &env);
145 resolved.active = Some(ActivePaths {
146 profile: manifest.profile,
147 destinations: recorded_paths(manifest.docs_root),
148 plan_zone: plan_zone_location(&manifest.plan_zone, &env),
149 docs_scratch: docs_scratch_location(manifest.docs_scratch.as_deref(), &env),
150 });
151 Ok(StatusReport {
152 schema: SCHEMA,
153 paths: resolved,
154 instance: true,
155 profile: Some(manifest.profile),
156 docs_root: Some(manifest.docs_root),
157 plan_zone: Some(manifest.plan_zone),
158 docs_scratch: manifest.docs_scratch,
159 plan_zone_env: variable(PLAN_ZONE_VAR),
160 docs_scratch_env: variable(DOCS_SCRATCH_VAR),
161 canon_version: Some(manifest.canon_version),
162 binary_version: binary,
163 alignment: Some(alignment),
164 managed_drift: report.managed_drift,
165 adopted_drift: report.adopted_drift,
166 failures: report.failures,
167 ok: Some(report.failures == 0),
168 })
169}