1use crate::error::{ChronicleError, Result};
2use crate::git::GitOps;
3use crate::read::{self, MatchedAnnotation, ReadQuery};
4use crate::schema::common::{AstAnchor, LineRange};
5use crate::schema::v1::{
6 Constraint, ConstraintSource, ContextLevel, Provenance, ProvenanceOperation, RegionAnnotation,
7 SemanticDependency,
8};
9use crate::schema::v2;
10
11#[derive(Debug, Clone)]
13pub struct RegionRef {
14 pub region: RegionAnnotation,
15 pub commit: String,
16 pub timestamp: String,
17 pub summary: String,
18 pub context_level: ContextLevel,
19 pub provenance: Provenance,
20}
21
22#[derive(Debug)]
24pub struct LineAnnotationMap {
25 coverage: Vec<Vec<usize>>,
27}
28
29impl LineAnnotationMap {
30 pub fn build_from_regions(regions: &[RegionRef], total_lines: usize) -> Self {
32 Self::build(regions, total_lines)
33 }
34
35 fn build(regions: &[RegionRef], total_lines: usize) -> Self {
36 let mut coverage = vec![Vec::new(); total_lines];
37 for (idx, r) in regions.iter().enumerate() {
38 let start = r.region.lines.start.saturating_sub(1) as usize;
39 let end = (r.region.lines.end as usize).min(total_lines);
40 for slot in &mut coverage[start..end] {
41 slot.push(idx);
42 }
43 }
44 Self { coverage }
45 }
46
47 pub fn regions_at_line(&self, line: u32) -> &[usize] {
49 let idx = line.saturating_sub(1) as usize;
50 self.coverage.get(idx).map(|v| v.as_slice()).unwrap_or(&[])
51 }
52
53 pub fn next_annotated_line(&self, from: u32) -> Option<u32> {
56 let start = from.saturating_sub(1) as usize;
57 for (i, regions) in self.coverage[start..].iter().enumerate() {
58 if !regions.is_empty() {
59 return Some((start + i) as u32 + 1);
60 }
61 }
62 None
63 }
64
65 pub fn prev_annotated_line(&self, from: u32) -> Option<u32> {
67 let end = (from as usize).min(self.coverage.len());
68 for i in (0..end).rev() {
69 if !self.coverage[i].is_empty() {
70 return Some(i as u32 + 1);
71 }
72 }
73 None
74 }
75}
76
77#[derive(Debug)]
79pub struct ShowData {
80 pub file_path: String,
81 pub commit: String,
82 pub source_lines: Vec<String>,
83 pub regions: Vec<RegionRef>,
84 pub annotation_map: LineAnnotationMap,
85}
86
87pub fn build_show_data(
89 git_ops: &dyn GitOps,
90 file_path: &str,
91 commit: &str,
92 anchor: Option<&str>,
93) -> Result<ShowData> {
94 let source = git_ops
96 .file_at_commit(std::path::Path::new(file_path), commit)
97 .map_err(|e| ChronicleError::Git {
98 source: e,
99 location: snafu::Location::default(),
100 })?;
101
102 let source_lines: Vec<String> = source.lines().map(String::from).collect();
103 let total_lines = source_lines.len();
104
105 let query = ReadQuery {
107 file: file_path.to_string(),
108 anchor: anchor.map(String::from),
109 lines: None,
110 };
111 let read_result = read::execute(git_ops, &query)?;
112
113 let regions = convert_to_region_refs(read_result.annotations, file_path);
115
116 let annotation_map = LineAnnotationMap::build(®ions, total_lines);
117
118 Ok(ShowData {
119 file_path: file_path.to_string(),
120 commit: commit.to_string(),
121 source_lines,
122 regions,
123 annotation_map,
124 })
125}
126
127fn convert_to_region_refs(annotations: Vec<MatchedAnnotation>, file_path: &str) -> Vec<RegionRef> {
132 use std::collections::HashMap;
133
134 let mut best: HashMap<String, RegionRef> = HashMap::new();
135
136 for ann in annotations {
137 if ann.markers.is_empty() {
138 let key = format!("{}:{}", file_path, "__commit_level__");
141 let region_ref = RegionRef {
142 region: RegionAnnotation {
143 file: file_path.to_string(),
144 ast_anchor: AstAnchor {
145 unit_type: "commit".to_string(),
146 name: "(commit-level)".to_string(),
147 signature: None,
148 },
149 lines: LineRange { start: 1, end: 1 },
150 intent: ann.summary.clone(),
151 reasoning: ann.motivation.clone(),
152 constraints: vec![],
153 semantic_dependencies: vec![],
154 related_annotations: vec![],
155 tags: vec![],
156 risk_notes: None,
157 corrections: vec![],
158 },
159 commit: ann.commit.clone(),
160 timestamp: ann.timestamp.clone(),
161 summary: ann.summary.clone(),
162 context_level: ContextLevel::Inferred,
163 provenance: Provenance {
164 operation: ProvenanceOperation::Initial,
165 derived_from: vec![],
166 original_annotations_preserved: false,
167 synthesis_notes: None,
168 },
169 };
170 let existing = best.get(&key);
171 if existing.is_none() || region_ref.timestamp > existing.unwrap().timestamp {
172 best.insert(key, region_ref);
173 }
174 continue;
175 }
176
177 let mut markers_by_anchor: HashMap<String, Vec<&v2::CodeMarker>> = HashMap::new();
179 for marker in &ann.markers {
180 let anchor_name = marker
181 .anchor
182 .as_ref()
183 .map(|a| a.name.clone())
184 .unwrap_or_default();
185 markers_by_anchor
186 .entry(anchor_name)
187 .or_default()
188 .push(marker);
189 }
190
191 for (anchor_name, markers) in markers_by_anchor {
192 let key = format!("{}:{}", file_path, anchor_name);
193
194 let mut line_start = u32::MAX;
196 let mut line_end = 0u32;
197 for m in &markers {
198 if let Some(ref lines) = m.lines {
199 line_start = line_start.min(lines.start);
200 line_end = line_end.max(lines.end);
201 }
202 }
203 if line_start == u32::MAX {
204 line_start = 1;
205 line_end = 1;
206 }
207
208 let mut constraints = Vec::new();
210 let mut deps = Vec::new();
211 let mut risk_notes = Vec::new();
212
213 for m in &markers {
214 match &m.kind {
215 v2::MarkerKind::Contract {
216 description,
217 source,
218 } => {
219 let cs = match source {
220 v2::ContractSource::Author => ConstraintSource::Author,
221 v2::ContractSource::Inferred => ConstraintSource::Inferred,
222 };
223 constraints.push(Constraint {
224 text: description.clone(),
225 source: cs,
226 });
227 }
228 v2::MarkerKind::Hazard { description } => {
229 risk_notes.push(description.clone());
230 }
231 v2::MarkerKind::Dependency {
232 target_file,
233 target_anchor,
234 assumption,
235 } => {
236 deps.push(SemanticDependency {
237 file: target_file.clone(),
238 anchor: target_anchor.clone(),
239 nature: assumption.clone(),
240 });
241 }
242 v2::MarkerKind::Unstable { description, .. } => {
243 risk_notes.push(format!("[unstable] {}", description));
244 }
245 v2::MarkerKind::Security { description } => {
246 risk_notes.push(format!("[security] {}", description));
247 }
248 v2::MarkerKind::Performance { description } => {
249 risk_notes.push(format!("[performance] {}", description));
250 }
251 v2::MarkerKind::Deprecated { description, .. } => {
252 risk_notes.push(format!("[deprecated] {}", description));
253 }
254 v2::MarkerKind::TechDebt { description } => {
255 risk_notes.push(format!("[tech_debt] {}", description));
256 }
257 v2::MarkerKind::TestCoverage { description } => {
258 risk_notes.push(format!("[test_coverage] {}", description));
259 }
260 }
261 }
262
263 let ast_anchor = markers
264 .first()
265 .and_then(|m| m.anchor.clone())
266 .unwrap_or(AstAnchor {
267 unit_type: "unknown".to_string(),
268 name: anchor_name.clone(),
269 signature: None,
270 });
271
272 let region_ref = RegionRef {
273 region: RegionAnnotation {
274 file: file_path.to_string(),
275 ast_anchor,
276 lines: LineRange {
277 start: line_start,
278 end: line_end,
279 },
280 intent: ann.summary.clone(),
281 reasoning: ann.motivation.clone(),
282 constraints,
283 semantic_dependencies: deps,
284 related_annotations: vec![],
285 tags: vec![],
286 risk_notes: if risk_notes.is_empty() {
287 None
288 } else {
289 Some(risk_notes.join("; "))
290 },
291 corrections: vec![],
292 },
293 commit: ann.commit.clone(),
294 timestamp: ann.timestamp.clone(),
295 summary: ann.summary.clone(),
296 context_level: ContextLevel::Inferred,
297 provenance: Provenance {
298 operation: ProvenanceOperation::Initial,
299 derived_from: vec![],
300 original_annotations_preserved: false,
301 synthesis_notes: None,
302 },
303 };
304
305 let existing = best.get(&key);
306 if existing.is_none() || region_ref.timestamp > existing.unwrap().timestamp {
307 best.insert(key, region_ref);
308 }
309 }
310 }
311
312 let mut regions: Vec<RegionRef> = best.into_values().collect();
313 regions.sort_by_key(|r| r.region.lines.start);
314 regions
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_line_annotation_map_empty() {
323 let map = LineAnnotationMap::build(&[], 10);
324 assert!(map.regions_at_line(1).is_empty());
325 assert!(map.regions_at_line(5).is_empty());
326 assert!(map.next_annotated_line(1).is_none());
327 assert!(map.prev_annotated_line(10).is_none());
328 }
329
330 #[test]
331 fn test_line_annotation_map_coverage() {
332 use crate::schema::common::*;
333 use crate::schema::v1::*;
334
335 let regions = vec![RegionRef {
336 region: RegionAnnotation {
337 file: "test.rs".to_string(),
338 ast_anchor: AstAnchor {
339 unit_type: "function".to_string(),
340 name: "foo".to_string(),
341 signature: None,
342 },
343 lines: LineRange { start: 3, end: 5 },
344 intent: "test".to_string(),
345 reasoning: None,
346 constraints: vec![],
347 semantic_dependencies: vec![],
348 related_annotations: vec![],
349 tags: vec![],
350 risk_notes: None,
351 corrections: vec![],
352 },
353 commit: "abc".to_string(),
354 timestamp: "2025-01-01T00:00:00Z".to_string(),
355 summary: "test".to_string(),
356 context_level: ContextLevel::Inferred,
357 provenance: Provenance {
358 operation: ProvenanceOperation::Initial,
359 derived_from: vec![],
360 original_annotations_preserved: false,
361 synthesis_notes: None,
362 },
363 }];
364
365 let map = LineAnnotationMap::build(®ions, 10);
366 assert!(map.regions_at_line(1).is_empty());
367 assert!(map.regions_at_line(2).is_empty());
368 assert_eq!(map.regions_at_line(3), &[0]);
369 assert_eq!(map.regions_at_line(4), &[0]);
370 assert_eq!(map.regions_at_line(5), &[0]);
371 assert!(map.regions_at_line(6).is_empty());
372 }
373
374 #[test]
375 fn test_next_prev_annotated_line() {
376 use crate::schema::common::*;
377 use crate::schema::v1::*;
378
379 let regions = vec![RegionRef {
380 region: RegionAnnotation {
381 file: "test.rs".to_string(),
382 ast_anchor: AstAnchor {
383 unit_type: "function".to_string(),
384 name: "foo".to_string(),
385 signature: None,
386 },
387 lines: LineRange { start: 5, end: 8 },
388 intent: "test".to_string(),
389 reasoning: None,
390 constraints: vec![],
391 semantic_dependencies: vec![],
392 related_annotations: vec![],
393 tags: vec![],
394 risk_notes: None,
395 corrections: vec![],
396 },
397 commit: "abc".to_string(),
398 timestamp: "2025-01-01T00:00:00Z".to_string(),
399 summary: "test".to_string(),
400 context_level: ContextLevel::Inferred,
401 provenance: Provenance {
402 operation: ProvenanceOperation::Initial,
403 derived_from: vec![],
404 original_annotations_preserved: false,
405 synthesis_notes: None,
406 },
407 }];
408
409 let map = LineAnnotationMap::build(®ions, 15);
410 assert_eq!(map.next_annotated_line(1), Some(5));
411 assert_eq!(map.next_annotated_line(5), Some(5));
412 assert_eq!(map.next_annotated_line(9), None);
413 assert_eq!(map.prev_annotated_line(10), Some(8));
414 assert_eq!(map.prev_annotated_line(4), None);
415 }
416}