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