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