spec_driven_docs/services/
status.rs1use 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
21pub const SCHEMA: &str = "sdd.status/3";
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
30#[serde(rename_all = "kebab-case")]
31pub enum Alignment {
32 Aligned,
34 BinaryNewer,
36 InstanceNewer,
38}
39
40#[derive(Debug, Serialize)]
42pub struct StatusReport {
43 pub schema: &'static str,
45 pub instance: bool,
47 pub profile: Option<ProfileId>,
49 pub docs_root: Option<DocsRoot>,
51 pub docs_scratch: Option<Utf8PathBuf>,
53 pub docs_scratch_env: Option<String>,
56 pub canon_version: Option<CanonVersion>,
58 pub binary_version: CanonVersion,
60 pub alignment: Option<Alignment>,
62 pub managed_drift: usize,
64 pub adopted_drift: usize,
66 pub failures: usize,
68 pub ok: Option<bool>,
70 pub paths: Paths,
76}
77
78fn proposals(target: &Utf8Path, env: &UserEnv) -> paths::Proposals {
80 paths::proposals(env, |relative| target.join(relative).is_dir())
81}
82
83fn 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
113pub 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(target)?;
127 let binary = CanonVersion::current();
128 let alignment = match manifest.canon_version.cmp(&binary) {
129 std::cmp::Ordering::Equal => Alignment::Aligned,
130 std::cmp::Ordering::Less => Alignment::BinaryNewer,
131 std::cmp::Ordering::Greater => Alignment::InstanceNewer,
132 };
133 let env = UserEnv::from_process();
134 let mut resolved = derived_paths(target, &env);
135 resolved.active = Some(ActivePaths {
136 profile: manifest.profile,
137 destinations: recorded_paths(manifest.docs_root),
138 docs_scratch: docs_scratch_location(manifest.docs_scratch.as_deref(), &env),
139 });
140 Ok(StatusReport {
141 schema: SCHEMA,
142 paths: resolved,
143 instance: true,
144 profile: Some(manifest.profile),
145 docs_root: Some(manifest.docs_root),
146 docs_scratch: manifest.docs_scratch,
147 docs_scratch_env: variable(DOCS_SCRATCH_VAR),
148 canon_version: Some(manifest.canon_version),
149 binary_version: binary,
150 alignment: Some(alignment),
151 managed_drift: report.managed_drift,
152 adopted_drift: report.adopted_drift,
153 failures: report.failures,
154 ok: Some(report.failures == 0),
155 })
156}