Skip to main content

intentional_core/
status.rs

1// ---
2// relationships:
3//   implements: intent-driven-polyglot-release
4// ---
5
6//! Workspace status computation.
7
8use crate::adapters::{
9    CargoAdapter, FormatAdapter, JsonFormat, MsbuildAdapter, NpmAdapter, PubAdapter, PythonAdapter,
10    TomlFormat, YamlFormat,
11};
12use crate::config::Config;
13use crate::error::Result;
14use crate::intent::Intent;
15use crate::model::{Adapter, Bump, ProjectionMode};
16use crate::version::{aggregate_bumps, resolve_versions, VersionRepository};
17use semver::Version;
18use std::path::Path;
19
20/// Stable diagnostic code emitted while primary baseline authority is absent.
21pub const MISSING_BASELINE_CODE: &str = "missing-baseline";
22
23/// Stable recovery action emitted with missing-baseline diagnostics.
24pub const MISSING_BASELINE_NEXT_ACTION: &str = "intentional tag --baseline";
25
26/// Version status for one release unit.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ReleaseUnitStatus {
29    /// Release-unit id.
30    pub id: String,
31    /// Tag-derived current version.
32    pub current: Version,
33    /// Intent-derived next version.
34    pub next: Version,
35    /// Effective bump after dependency propagation.
36    pub bump: Bump,
37}
38
39/// Complete read-only workspace status.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct WorkspaceStatus {
42    /// Pending intent ids.
43    pub intents: Vec<String>,
44    /// Release-unit versions in id order.
45    pub release_units: Vec<ReleaseUnitStatus>,
46    /// Manifest versions that differ from tag-derived current versions.
47    pub drift: Vec<Drift>,
48    /// Release units whose primary baseline tag has not been established.
49    pub missing_baselines: Vec<String>,
50    /// Recoverable missing or inconsistent annotated release records.
51    pub tag_record_issues: Vec<String>,
52}
53
54/// One manifest drift finding.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct Drift {
57    /// Release-unit id.
58    pub release_unit: String,
59    /// Workspace-relative manifest path.
60    pub file: std::path::PathBuf,
61    /// Tag-derived version.
62    pub expected: String,
63    /// Manifest-projected version.
64    pub actual: String,
65}
66
67impl WorkspaceStatus {
68    /// Load config, intents, Git tags, and compute effective versions.
69    pub fn load(root: &Path) -> Result<Self> {
70        let config = Config::load(root)?;
71        let intents = Intent::load_all(root, &config)?;
72        Self::compute(root, &config, &intents)
73    }
74
75    /// Compute status from already-loaded inputs.
76    pub fn compute(root: &Path, config: &Config, intents: &[Intent]) -> Result<Self> {
77        let repository = VersionRepository::discover(root)?;
78        let declared = aggregate_bumps(intents.iter().map(|intent| &intent.release_units));
79        let mut current_versions = std::collections::BTreeMap::new();
80        let mut missing_baselines = Vec::new();
81        for id in config.release_units.keys() {
82            let (_, primary) = config.primary_tag(id)?;
83            let current = repository.current_version(id, &primary.template)?;
84            if !repository.has_matching_tag(id, &primary.template)? {
85                missing_baselines.push(id.clone());
86            }
87            current_versions.insert(id.clone(), current);
88        }
89        let resolved = resolve_versions(config, &declared, &current_versions)?;
90        let mut release_units = Vec::with_capacity(config.release_units.len());
91        let mut drift = Vec::new();
92        for (id, release_unit) in &config.release_units {
93            let versions = &resolved[id];
94            let expected = versions.current.to_string();
95            release_units.push(ReleaseUnitStatus {
96                id: id.clone(),
97                current: versions.current.clone(),
98                next: versions.next.clone(),
99                bump: versions.bump,
100            });
101            for projection in &release_unit.projections {
102                if projection.mode == ProjectionMode::None || projection.adapter == Adapter::Go {
103                    continue;
104                }
105                let relative = release_unit.path.join(&projection.file);
106                let text = std::fs::read_to_string(root.join(&relative))
107                    .map_err(|error| crate::Error::io(root.join(&relative), error))?;
108                let actual = read_projection_version(root, &relative, projection, &text)?;
109                if actual != expected {
110                    drift.push(Drift {
111                        release_unit: id.clone(),
112                        file: relative,
113                        expected: expected.clone(),
114                        actual,
115                    });
116                }
117            }
118        }
119        Ok(Self {
120            intents: intents.iter().map(|intent| intent.id.clone()).collect(),
121            release_units,
122            drift,
123            missing_baselines,
124            tag_record_issues: crate::tag::tag_record_issues(root, config)?,
125        })
126    }
127}
128
129pub(crate) fn read_projection_version(
130    root: &Path,
131    relative: &Path,
132    projection: &crate::Projection,
133    text: &str,
134) -> Result<String> {
135    match projection.adapter {
136        Adapter::Npm => NpmAdapter.version(text),
137        Adapter::Cargo => match CargoAdapter.version(text)? {
138            Some(version) => Ok(version),
139            None => {
140                let workspace_path = crate::apply::workspace_manifest(root, relative)?;
141                let workspace = std::fs::read_to_string(root.join(&workspace_path))
142                    .map_err(|error| crate::Error::io(root.join(&workspace_path), error))?;
143                TomlFormat.read_text(&workspace, "/workspace/package/version")
144            }
145        },
146        Adapter::Pub => PubAdapter.version(text),
147        Adapter::Python => PythonAdapter.version(text),
148        Adapter::Msbuild => MsbuildAdapter.version(text),
149        Adapter::Json => JsonFormat.read_text(
150            text,
151            projection.pointer.as_deref().expect("validated pointer"),
152        ),
153        Adapter::Toml => TomlFormat.read_text(
154            text,
155            projection.pointer.as_deref().expect("validated pointer"),
156        ),
157        Adapter::Yaml => YamlFormat.read_text(
158            text,
159            projection.pointer.as_deref().expect("validated pointer"),
160        ),
161        Adapter::Go => unreachable!("Go has no manifest version"),
162    }
163}