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