Skip to main content

commit_wizard/engine/models/runtime/
resolution.rs

1use crate::engine::{
2    config::{BaseConfig, RulesConfig},
3    constants::app_config_dir,
4    models::policy::Policy,
5};
6use std::{collections::BTreeMap, path::PathBuf};
7
8#[derive(Debug, Clone)]
9pub struct RuntimeResolution {
10    // the config options we have available, determined by looking for config files in the current directory and parent directories, as well as global config locations
11    pub sources: AvailableConfigOptions,
12    // the resolved config we will use for this run, determined by merging the available config options based on precedence rules
13    pub config: Option<ResolvedConfig>,
14    // resolved policy, which is the final set of rules and base config that we will use to validate commits, determined by applying the config options according to the policy defined in the config and the precedence rules
15    pub policy: Policy,
16}
17
18impl Default for RuntimeResolution {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl RuntimeResolution {
25    pub fn new() -> Self {
26        Self {
27            sources: AvailableConfigOptions::new(),
28            config: None,
29            policy: Policy::default(),
30        }
31    }
32}
33
34#[derive(Debug, Clone)]
35pub struct ResolvedConfig {
36    /// the path of the config file that contributed to the resolved config, if any
37    pub path: Option<PathBuf>,
38    /// the rules used to resolve the config (kept for reference/diagnostics)
39    /// Note: rules are merged into `base` during resolution
40    pub rules: RulesConfig,
41    /// the base config with rules applied - the single source of truth for policy
42    /// This is the complete, merged configuration (base + rules) passed to policy engine.
43    /// Policy reads from this field only; no side effects or dynamic rule lookups.
44    pub base: BaseConfig,
45}
46
47#[derive(Debug, Clone)]
48pub struct RuntimePaths {
49    /// the current working directory, determined at runtime
50    pub cwd: PathBuf,
51    /// the global paths for config, cache, and state, determined at runtime
52    pub global: RuntimeGlobalPaths,
53    /// whether we are in a git repository, determined by looking for a .git directory in the current or parent directories
54    pub in_git_repo: bool,
55    /// default is cwd, but updated if we detect a git repository root
56    pub repo_root: Option<PathBuf>,
57    /// an optional explicit config path provided by the user, which overrides all other config sources if present
58    pub explicit_config_path: Option<PathBuf>,
59    /// an optional explicit registry provided by the user, which overrides discovered registries if present
60    pub explicit_registry: Option<String>,
61    /// an optional explicit registry ref (e.g. git tag or branch) provided by the user, which overrides the registry config if present
62    pub explicit_registry_ref: Option<String>,
63    /// an optional explicit registry section provided by the user, which overrides discovered registry sections if present
64    pub explicit_registry_section: Option<String>,
65}
66
67impl Default for RuntimePaths {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl RuntimePaths {
74    pub fn new() -> Self {
75        let cwd = std::env::current_dir().unwrap_or_else(|e| {
76            eprintln!("[warn] Failed to determine current directory: {e} — using '.'");
77            PathBuf::from(".")
78        });
79        let global = RuntimeGlobalPaths::new().unwrap_or_else(|e| {
80            eprintln!(
81                "[warn] Failed to resolve global paths: {e} — using '.' for config/cache/state"
82            );
83            RuntimeGlobalPaths {
84                config: PathBuf::from("."),
85                cache: PathBuf::from("."),
86                state: PathBuf::from("."),
87            }
88        });
89        Self {
90            cwd,
91            global,
92            in_git_repo: false,
93            repo_root: None,
94            explicit_config_path: None,
95            explicit_registry: None,
96            explicit_registry_ref: None,
97            explicit_registry_section: None,
98        }
99    }
100}
101
102#[derive(Debug, Clone)]
103pub struct RuntimeGlobalPaths {
104    /// the users global configuration directory, determined at runtime
105    pub config: PathBuf,
106    /// the users global cache directory, determined at runtime
107    pub cache: PathBuf,
108    /// the users global state directory, determined at runtime
109    pub state: PathBuf,
110}
111
112impl RuntimeGlobalPaths {
113    pub fn new() -> crate::engine::error::Result<Self> {
114        use crate::engine::constants::{app_cache_dir, app_state_dir};
115        let config = app_config_dir()?;
116        let cache = app_cache_dir()?;
117        let state = app_state_dir()?;
118        Ok(Self {
119            config,
120            cache,
121            state,
122        })
123    }
124}
125
126impl Default for RuntimeGlobalPaths {
127    fn default() -> Self {
128        Self::new().unwrap_or_else(|e| {
129            eprintln!(
130                "[warn] Failed to resolve global paths: {e} — using '.' for config/cache/state"
131            );
132            Self {
133                config: std::path::PathBuf::from("."),
134                cache: std::path::PathBuf::from("."),
135                state: std::path::PathBuf::from("."),
136            }
137        })
138    }
139}
140
141#[derive(Debug, Clone)]
142pub struct AvailableConfigOptions {
143    pub cli_config: Option<AvailableConfig>,
144    /// Config built from `CW_*` environment variables (SRS §4).
145    pub env_config: Option<AvailableConfig>,
146    pub repo_config: Option<AvailableConfig>,
147    pub global_config: Option<AvailableConfig>,
148    /// Loaded registries (SRS §5). Each entry holds its resolved config.
149    pub registries: Vec<RegistryOptions>,
150}
151
152impl Default for AvailableConfigOptions {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158impl AvailableConfigOptions {
159    pub fn new() -> Self {
160        Self {
161            cli_config: None,
162            env_config: None,
163            repo_config: None,
164            global_config: None,
165            registries: vec![],
166        }
167    }
168
169    pub fn is_empty(&self) -> bool {
170        self.cli_config.is_none()
171            && self.env_config.is_none()
172            && self.repo_config.is_none()
173            && self.global_config.is_none()
174            && self.registries.is_empty()
175    }
176
177    pub fn has_cli_config(&self) -> bool {
178        self.cli_config.is_some()
179    }
180
181    pub fn set_cli_config(&mut self, config: AvailableConfig) -> &mut Self {
182        self.cli_config = Some(config);
183        self
184    }
185
186    pub fn has_repo_config(&self) -> bool {
187        self.repo_config.is_some()
188    }
189
190    pub fn set_repo_config(&mut self, config: AvailableConfig) -> &mut Self {
191        self.repo_config = Some(config);
192        self
193    }
194
195    pub fn has_global_config(&self) -> bool {
196        self.global_config.is_some()
197    }
198
199    pub fn set_global_config(&mut self, config: AvailableConfig) -> &mut Self {
200        self.global_config = Some(config);
201        self
202    }
203
204    pub fn has_registries(&self) -> bool {
205        !self.registries.is_empty()
206    }
207
208    pub fn registry_count(&self) -> usize {
209        self.registries.len()
210    }
211}
212
213#[derive(Debug, Clone)]
214pub struct RegistryOptions {
215    // unique identifier for the registry: url#ref or url#ref/section for sectioned registries
216    pub id: String,
217    // an identifier for the registry, e.g. "default" or "main"
218    // used by git-based registries to determine which branch or tag to use, and by users to specify which registry to use when multiple are available
219    pub tag: String,
220    // external reference to the registry, e.g. a git URL or a local path
221    pub url: String,
222    // the git ref (branch, tag, or commit SHA) used to load this registry
223    pub r#ref: String,
224    // the section within the registry (for sectioned registries), if any
225    pub section: Option<String>,
226    // if flat registry, the resolved config options from the registry URL, determined by fetching and parsing the registry config file
227    pub config: Option<AvailableConfig>,
228    // if hierarchical registry, the resolved config options from the registry URL and section, determined by fetching and parsing the registry config file and extracting the specified section
229    pub sections: Option<BTreeMap<String, AvailableConfig>>,
230    // whether this is the active registry selected via precedence (CLI > ENV > repo > global)
231    pub is_active: bool,
232}
233
234#[derive(Debug, Clone)]
235pub struct AvailableConfig {
236    pub rules: Option<RulesConfig>,
237    pub base: Option<BaseConfig>,
238}
239
240pub fn resolve_available_config(
241    base: Option<BaseConfig>,
242    rules: Option<RulesConfig>,
243) -> AvailableConfig {
244    AvailableConfig { base, rules }
245}
246
247pub fn resolve_policy(config: Option<&ResolvedConfig>) -> Policy {
248    match config {
249        Some(cfg) => Policy::from_config(cfg),
250        None => Policy::default(),
251    }
252}