1use crate::engine::{Context, TemporalBound};
2use crate::parsing::ast::{DateTimeValue, FactValue, LemmaSpec};
3use crate::parsing::source::Source;
4use crate::Error;
5use std::collections::BTreeSet;
6use std::sync::Arc;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct TemporalSlice {
12 pub from: Option<DateTimeValue>,
14 pub to: Option<DateTimeValue>,
16}
17
18fn implicit_spec_refs(spec: &LemmaSpec) -> Vec<(String, Source)> {
20 spec.facts
21 .iter()
22 .filter_map(|fact| {
23 if let FactValue::SpecReference(spec_ref) = &fact.value {
24 if spec_ref.hash_pin.is_none() {
25 return Some((spec_ref.name.clone(), fact.source_location.clone()));
26 }
27 }
28 None
29 })
30 .collect()
31}
32
33fn implicit_spec_ref_names(spec: &LemmaSpec) -> Vec<String> {
35 implicit_spec_refs(spec)
36 .into_iter()
37 .map(|(n, _)| n)
38 .collect()
39}
40
41pub fn compute_temporal_slices(spec_arc: &Arc<LemmaSpec>, context: &Context) -> Vec<TemporalSlice> {
53 let (eff_from, eff_to) = context.effective_range(spec_arc);
54 let range_start = TemporalBound::from_start(eff_from.as_ref());
55 let range_end = TemporalBound::from_end(eff_to.as_ref());
56
57 let direct_implicit_names = implicit_spec_ref_names(spec_arc);
58 if direct_implicit_names.is_empty() {
59 return vec![TemporalSlice {
60 from: eff_from,
61 to: eff_to,
62 }];
63 }
64
65 let mut visited_names: BTreeSet<String> = BTreeSet::new();
68 let mut pending_names: Vec<String> = direct_implicit_names;
69 let mut all_boundaries: BTreeSet<DateTimeValue> = BTreeSet::new();
70
71 while let Some(dep_name) = pending_names.pop() {
72 if !visited_names.insert(dep_name.clone()) {
73 continue;
74 }
75
76 let dep_versions: Vec<Arc<LemmaSpec>> =
77 context.iter().filter(|d| d.name == dep_name).collect();
78 if dep_versions.is_empty() {
79 continue;
82 }
83
84 let boundaries = context.version_boundaries(&dep_name);
85 for boundary in boundaries {
86 let bound = TemporalBound::At(boundary.clone());
87 if bound > range_start && bound < range_end {
88 all_boundaries.insert(boundary);
89 }
90 }
91 for dep_spec in &dep_versions {
92 for transitive_name in implicit_spec_ref_names(dep_spec) {
93 if !visited_names.contains(&transitive_name) {
94 pending_names.push(transitive_name);
95 }
96 }
97 }
98 }
99
100 if all_boundaries.is_empty() {
101 return vec![TemporalSlice {
102 from: eff_from,
103 to: eff_to,
104 }];
105 }
106
107 let mut slices = Vec::new();
109 let mut cursor = eff_from.clone();
110
111 for boundary in &all_boundaries {
112 slices.push(TemporalSlice {
113 from: cursor,
114 to: Some(boundary.clone()),
115 });
116 cursor = Some(boundary.clone());
117 }
118
119 slices.push(TemporalSlice {
120 from: cursor,
121 to: eff_to,
122 });
123
124 slices
125}
126
127pub fn validate_temporal_coverage(context: &Context) -> Vec<Error> {
136 let mut errors = Vec::new();
137
138 for spec_arc in context.iter() {
139 let (eff_from, eff_to) = context.effective_range(&spec_arc);
140 let dep_refs = implicit_spec_refs(&spec_arc);
141
142 for (dep_name, ref_source) in &dep_refs {
143 let gaps = context.dep_coverage_gaps(dep_name, eff_from.as_ref(), eff_to.as_ref());
144
145 for (gap_start, gap_end) in &gaps {
146 let (message, suggestion) =
147 format_coverage_gap(&spec_arc.name, dep_name, gap_start, gap_end, &eff_from);
148 errors.push(Error::validation_with_context(
149 message,
150 Some(ref_source.clone()),
151 Some(suggestion),
152 Some(Arc::clone(&spec_arc)),
153 None,
154 ));
155 }
156 }
157 }
158
159 errors
160}
161
162fn format_coverage_gap(
163 spec_name: &str,
164 dep_name: &str,
165 gap_start: &Option<DateTimeValue>,
166 gap_end: &Option<DateTimeValue>,
167 spec_from: &Option<DateTimeValue>,
168) -> (String, String) {
169 let message = match (gap_start, gap_end) {
170 (None, Some(end)) => format!(
171 "'{}' depends on '{}', but no version of '{}' is active before {}",
172 spec_name, dep_name, dep_name, end
173 ),
174 (Some(start), None) => format!(
175 "'{}' depends on '{}', but no version of '{}' is active after {}",
176 spec_name, dep_name, dep_name, start
177 ),
178 (Some(start), Some(end)) => format!(
179 "'{}' depends on '{}', but no version of '{}' is active between {} and {}",
180 spec_name, dep_name, dep_name, start, end
181 ),
182 (None, None) => format!(
183 "'{}' depends on '{}', but no version of '{}' exists",
184 spec_name, dep_name, dep_name
185 ),
186 };
187
188 let suggestion = if gap_start.is_none() && gap_end.is_none() && dep_name.starts_with('@') {
189 format!(
190 "Run `lemma get` or `lemma get {}` to fetch this dependency.",
191 dep_name
192 )
193 } else if gap_start.is_none() && spec_from.is_none() {
194 format!(
195 "Add an effective_from date to '{}' so it starts when '{}' is available, \
196 or add an earlier version of '{}'.",
197 spec_name, dep_name, dep_name
198 )
199 } else if gap_end.is_none() {
200 format!(
201 "Add a newer version of '{}' that covers the remaining range.",
202 dep_name
203 )
204 } else {
205 format!(
206 "Add a version of '{}' that covers the gap, \
207 or adjust the effective_from date on '{}'.",
208 dep_name, spec_name
209 )
210 };
211
212 (message, suggestion)
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::parsing::ast::{FactValue, LemmaFact, LemmaSpec, Reference, SpecRef};
219 use crate::parsing::source::Source;
220 use crate::Span;
221
222 fn date(year: i32, month: u32, day: u32) -> DateTimeValue {
223 DateTimeValue {
224 year,
225 month,
226 day,
227 hour: 0,
228 minute: 0,
229 second: 0,
230 microsecond: 0,
231 timezone: None,
232 }
233 }
234
235 fn dummy_source() -> Source {
236 Source::new(
237 "test",
238 Span {
239 start: 0,
240 end: 0,
241 line: 0,
242 col: 0,
243 },
244 )
245 }
246
247 fn make_spec(name: &str) -> LemmaSpec {
248 LemmaSpec::new(name.to_string())
249 }
250
251 fn make_spec_with_range(name: &str, effective_from: Option<DateTimeValue>) -> LemmaSpec {
252 let mut spec = make_spec(name);
253 spec.effective_from = effective_from;
254 spec
255 }
256
257 fn add_spec_ref_fact(spec: &mut LemmaSpec, fact_name: &str, dep_name: &str) {
258 spec.facts.push(LemmaFact {
259 reference: Reference::local(fact_name.to_string()),
260 value: FactValue::SpecReference(SpecRef {
261 name: dep_name.to_string(),
262 from_registry: false,
263 hash_pin: None,
264 effective: None,
265 }),
266 source_location: dummy_source(),
267 });
268 }
269
270 #[test]
271 fn no_deps_produces_single_slice() {
272 let mut ctx = Context::new();
273 let spec = Arc::new(make_spec_with_range("a", Some(date(2025, 1, 1))));
274 ctx.insert_spec(Arc::clone(&spec), false).unwrap();
275
276 let slices = compute_temporal_slices(&spec, &ctx);
277 assert_eq!(slices.len(), 1);
278 assert_eq!(slices[0].from, Some(date(2025, 1, 1)));
279 assert_eq!(slices[0].to, None);
280 }
281
282 #[test]
283 fn single_dep_no_boundary_in_range() {
284 let mut ctx = Context::new();
285 let mut main_spec = make_spec_with_range("main", Some(date(2025, 1, 1)));
286 add_spec_ref_fact(&mut main_spec, "dep", "config");
287 let main_arc = Arc::new(main_spec);
288 ctx.insert_spec(Arc::clone(&main_arc), false).unwrap();
289
290 let config = Arc::new(make_spec("config"));
291 ctx.insert_spec(config, false).unwrap();
292
293 let slices = compute_temporal_slices(&main_arc, &ctx);
294 assert_eq!(slices.len(), 1);
295 }
296
297 #[test]
298 fn single_dep_one_boundary_produces_two_slices() {
299 let mut ctx = Context::new();
300
301 let config_v1 = Arc::new(make_spec("config"));
302 ctx.insert_spec(config_v1, false).unwrap();
303 let config_v2 = Arc::new(make_spec_with_range("config", Some(date(2025, 2, 1))));
304 ctx.insert_spec(config_v2, false).unwrap();
305
306 let mut main_spec = make_spec_with_range("main", Some(date(2025, 1, 1)));
308 add_spec_ref_fact(&mut main_spec, "cfg", "config");
309 let main_arc = Arc::new(main_spec);
310 ctx.insert_spec(Arc::clone(&main_arc), false).unwrap();
311
312 let slices = compute_temporal_slices(&main_arc, &ctx);
313 assert_eq!(slices.len(), 2);
314 assert_eq!(slices[0].from, Some(date(2025, 1, 1)));
315 assert_eq!(slices[0].to, Some(date(2025, 2, 1)));
316 assert_eq!(slices[1].from, Some(date(2025, 2, 1)));
317 assert_eq!(slices[1].to, None);
318 }
319
320 #[test]
321 fn boundary_outside_range_ignored() {
322 let mut ctx = Context::new();
323
324 let config_v1 = Arc::new(make_spec("config"));
325 ctx.insert_spec(config_v1, false).unwrap();
326 let config_v2 = Arc::new(make_spec_with_range("config", Some(date(2025, 6, 1))));
327 ctx.insert_spec(config_v2, false).unwrap();
328
329 let main_v1 = make_spec_with_range("main", Some(date(2025, 1, 1)));
331 let main_v2 = make_spec_with_range("main", Some(date(2025, 3, 1)));
332 let mut main_v1 = main_v1;
333 add_spec_ref_fact(&mut main_v1, "cfg", "config");
334 let main_arc = Arc::new(main_v1);
335 ctx.insert_spec(Arc::clone(&main_arc), false).unwrap();
336 ctx.insert_spec(Arc::new(main_v2), false).unwrap();
337
338 let slices = compute_temporal_slices(&main_arc, &ctx);
339 assert_eq!(slices.len(), 1);
340 }
341
342 #[test]
343 fn transitive_dep_boundary_included() {
344 let mut ctx = Context::new();
345
346 let mut config = make_spec("config");
347 add_spec_ref_fact(&mut config, "rates_ref", "rates");
348 ctx.insert_spec(Arc::new(config), false).unwrap();
349
350 let rates_v1 = Arc::new(make_spec("rates"));
351 ctx.insert_spec(rates_v1, false).unwrap();
352 let rates_v2 = Arc::new(make_spec_with_range("rates", Some(date(2025, 2, 1))));
353 ctx.insert_spec(rates_v2, false).unwrap();
354
355 let mut main_spec = make_spec_with_range("main", Some(date(2025, 1, 1)));
357 add_spec_ref_fact(&mut main_spec, "cfg", "config");
358 let main_arc = Arc::new(main_spec);
359 ctx.insert_spec(Arc::clone(&main_arc), false).unwrap();
360
361 let slices = compute_temporal_slices(&main_arc, &ctx);
362 assert_eq!(slices.len(), 2);
363 assert_eq!(slices[0].to, Some(date(2025, 2, 1)));
364 assert_eq!(slices[1].from, Some(date(2025, 2, 1)));
365 }
366
367 #[test]
368 fn unbounded_spec_with_versioned_dep() {
369 let mut ctx = Context::new();
370
371 let dep_v1 = Arc::new(make_spec("dep"));
372 ctx.insert_spec(dep_v1, false).unwrap();
373 let dep_v2 = Arc::new(make_spec_with_range("dep", Some(date(2025, 6, 1))));
374 ctx.insert_spec(dep_v2, false).unwrap();
375
376 let mut main_spec = make_spec("main");
377 add_spec_ref_fact(&mut main_spec, "d", "dep");
378 let main_arc = Arc::new(main_spec);
379 ctx.insert_spec(Arc::clone(&main_arc), false).unwrap();
380
381 let slices = compute_temporal_slices(&main_arc, &ctx);
382 assert_eq!(slices.len(), 2);
383 assert_eq!(slices[0].from, None);
384 assert_eq!(slices[0].to, Some(date(2025, 6, 1)));
385 assert_eq!(slices[1].from, Some(date(2025, 6, 1)));
386 assert_eq!(slices[1].to, None);
387 }
388
389 #[test]
390 fn pinned_ref_does_not_create_boundary() {
391 let mut ctx = Context::new();
392
393 let dep_v1 = Arc::new(make_spec("dep"));
394 ctx.insert_spec(dep_v1, false).unwrap();
395 let dep_v2 = Arc::new(make_spec_with_range("dep", Some(date(2025, 6, 1))));
396 ctx.insert_spec(dep_v2, false).unwrap();
397
398 let mut main_spec = make_spec("main");
399 main_spec.facts.push(LemmaFact {
400 reference: Reference::local("d".to_string()),
401 value: FactValue::SpecReference(SpecRef {
402 name: "dep".to_string(),
403 from_registry: false,
404 hash_pin: Some("abcd1234".to_string()),
405 effective: None,
406 }),
407 source_location: dummy_source(),
408 });
409 let main_arc = Arc::new(main_spec);
410 ctx.insert_spec(Arc::clone(&main_arc), false).unwrap();
411
412 let slices = compute_temporal_slices(&main_arc, &ctx);
413 assert_eq!(slices.len(), 1);
414 }
415}