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, effective_bumps, PackageVersion, VersionRepository};
17use semver::Version;
18use std::path::Path;
19
20/// Version status for one logical package.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct PackageStatus {
23    /// Logical package id.
24    pub id: String,
25    /// Tag-derived current version.
26    pub current: Version,
27    /// Intent-derived next version.
28    pub next: Version,
29    /// Effective bump after dependency propagation.
30    pub bump: Bump,
31}
32
33/// Complete read-only workspace status.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct WorkspaceStatus {
36    /// Pending intent ids.
37    pub intents: Vec<String>,
38    /// Package versions in id order.
39    pub packages: Vec<PackageStatus>,
40    /// Manifest versions that differ from tag-derived current versions.
41    pub drift: Vec<Drift>,
42}
43
44/// One manifest drift finding.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct Drift {
47    /// Logical package id.
48    pub package: String,
49    /// Workspace-relative manifest path.
50    pub file: std::path::PathBuf,
51    /// Tag-derived version.
52    pub expected: String,
53    /// Manifest-projected version.
54    pub actual: String,
55}
56
57impl WorkspaceStatus {
58    /// Load config, intents, Git tags, and compute effective versions.
59    pub fn load(root: &Path) -> Result<Self> {
60        let config = Config::load(root)?;
61        let intents = Intent::load_all(root, &config)?;
62        Self::compute(root, &config, &intents)
63    }
64
65    /// Compute status from already-loaded inputs.
66    pub fn compute(root: &Path, config: &Config, intents: &[Intent]) -> Result<Self> {
67        let repository = VersionRepository::discover(root)?;
68        let declared = aggregate_bumps(intents.iter().map(|intent| &intent.packages));
69        let bumps = effective_bumps(config, &declared);
70        let mut packages = Vec::with_capacity(config.packages.len());
71        let mut drift = Vec::new();
72        for (id, package) in &config.packages {
73            let current = repository.current_version(id, &package.tag)?;
74            let versions = PackageVersion::new(current, bumps[id]);
75            let expected = versions.current.to_string();
76            packages.push(PackageStatus {
77                id: id.clone(),
78                current: versions.current,
79                next: versions.next,
80                bump: versions.bump,
81            });
82            for projection in &package.projections {
83                if projection.mode == ProjectionMode::None || projection.adapter == Adapter::Go {
84                    continue;
85                }
86                let relative = package.path.join(&projection.file);
87                let text = std::fs::read_to_string(root.join(&relative))
88                    .map_err(|error| crate::Error::io(root.join(&relative), error))?;
89                let actual = read_projection_version(root, &relative, projection, &text)?;
90                if actual != expected {
91                    drift.push(Drift {
92                        package: id.clone(),
93                        file: relative,
94                        expected: expected.clone(),
95                        actual,
96                    });
97                }
98            }
99        }
100        Ok(Self {
101            intents: intents.iter().map(|intent| intent.id.clone()).collect(),
102            packages,
103            drift,
104        })
105    }
106}
107
108fn read_projection_version(
109    root: &Path,
110    relative: &Path,
111    projection: &crate::Projection,
112    text: &str,
113) -> Result<String> {
114    match projection.adapter {
115        Adapter::Npm => NpmAdapter.version(text),
116        Adapter::Cargo => match CargoAdapter.version(text)? {
117            Some(version) => Ok(version),
118            None => {
119                let workspace_path = crate::apply::workspace_manifest(root, relative)?;
120                let workspace = std::fs::read_to_string(root.join(&workspace_path))
121                    .map_err(|error| crate::Error::io(root.join(&workspace_path), error))?;
122                TomlFormat.read_text(&workspace, "/workspace/package/version")
123            }
124        },
125        Adapter::Pub => PubAdapter.version(text),
126        Adapter::Python => PythonAdapter.version(text),
127        Adapter::Msbuild => MsbuildAdapter.version(text),
128        Adapter::Json => JsonFormat.read_text(
129            text,
130            projection.pointer.as_deref().expect("validated pointer"),
131        ),
132        Adapter::Toml => TomlFormat.read_text(
133            text,
134            projection.pointer.as_deref().expect("validated pointer"),
135        ),
136        Adapter::Yaml => YamlFormat.read_text(
137            text,
138            projection.pointer.as_deref().expect("validated pointer"),
139        ),
140        Adapter::Go => unreachable!("Go has no manifest version"),
141    }
142}