1use serde::{Deserialize, Serialize};
2
3use super::correction::Correction;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Annotation {
7 pub schema: String,
8 pub commit: String,
9 pub timestamp: String,
10 #[serde(skip_serializing_if = "Option::is_none")]
11 pub task: Option<String>,
12 pub summary: String,
13 pub context_level: ContextLevel,
14 pub regions: Vec<RegionAnnotation>,
15 #[serde(default, skip_serializing_if = "Vec::is_empty")]
16 pub cross_cutting: Vec<CrossCuttingConcern>,
17 pub provenance: Provenance,
18}
19
20impl Annotation {
21 pub fn new_initial(commit: String, summary: String, context_level: ContextLevel) -> Self {
22 Self {
23 schema: "chronicle/v1".to_string(),
24 commit,
25 timestamp: chrono::Utc::now().to_rfc3339(),
26 task: None,
27 summary,
28 context_level,
29 regions: Vec::new(),
30 cross_cutting: Vec::new(),
31 provenance: Provenance {
32 operation: ProvenanceOperation::Initial,
33 derived_from: Vec::new(),
34 original_annotations_preserved: false,
35 synthesis_notes: None,
36 },
37 }
38 }
39
40 pub fn validate(&self) -> Result<(), String> {
42 if self.schema != "chronicle/v1" {
43 return Err(format!("unsupported schema version: {}", self.schema));
44 }
45 if self.commit.is_empty() {
46 return Err("commit SHA is empty".to_string());
47 }
48 if self.summary.is_empty() {
49 return Err("summary is empty".to_string());
50 }
51 for (i, region) in self.regions.iter().enumerate() {
52 if let Err(e) = region.validate() {
53 return Err(format!("region[{}]: {}", i, e));
54 }
55 }
56 Ok(())
57 }
58}
59
60impl RegionAnnotation {
61 pub fn validate(&self) -> Result<(), String> {
63 if self.file.is_empty() {
64 return Err("file is empty".to_string());
65 }
66 if self.intent.is_empty() {
67 return Err("intent is empty".to_string());
68 }
69 if self.lines.start > self.lines.end {
70 return Err(format!(
71 "invalid line range: start ({}) > end ({})",
72 self.lines.start, self.lines.end
73 ));
74 }
75 Ok(())
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
80#[serde(rename_all = "snake_case")]
81pub enum ContextLevel {
82 Enhanced,
83 Inferred,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct RegionAnnotation {
88 pub file: String,
89 pub ast_anchor: AstAnchor,
90 pub lines: LineRange,
91 pub intent: String,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub reasoning: Option<String>,
94 #[serde(default, skip_serializing_if = "Vec::is_empty")]
95 pub constraints: Vec<Constraint>,
96 #[serde(default, skip_serializing_if = "Vec::is_empty")]
97 pub semantic_dependencies: Vec<SemanticDependency>,
98 #[serde(default, skip_serializing_if = "Vec::is_empty")]
99 pub related_annotations: Vec<RelatedAnnotation>,
100 #[serde(default, skip_serializing_if = "Vec::is_empty")]
101 pub tags: Vec<String>,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub risk_notes: Option<String>,
104 #[serde(default, skip_serializing_if = "Vec::is_empty")]
105 pub corrections: Vec<Correction>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct AstAnchor {
110 pub unit_type: String,
111 pub name: String,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub signature: Option<String>,
114}
115
116#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
117pub struct LineRange {
118 pub start: u32,
119 pub end: u32,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct Constraint {
124 pub text: String,
125 pub source: ConstraintSource,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
129#[serde(rename_all = "snake_case")]
130pub enum ConstraintSource {
131 Author,
132 Inferred,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct SemanticDependency {
137 pub file: String,
138 pub anchor: String,
139 pub nature: String,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct RelatedAnnotation {
144 pub commit: String,
145 pub anchor: String,
146 pub relationship: String,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct CrossCuttingConcern {
151 pub description: String,
152 pub regions: Vec<CrossCuttingRegionRef>,
153 #[serde(default, skip_serializing_if = "Vec::is_empty")]
154 pub tags: Vec<String>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct CrossCuttingRegionRef {
159 pub file: String,
160 pub anchor: String,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct Provenance {
165 pub operation: ProvenanceOperation,
166 #[serde(default, skip_serializing_if = "Vec::is_empty")]
167 pub derived_from: Vec<String>,
168 pub original_annotations_preserved: bool,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub synthesis_notes: Option<String>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
174#[serde(rename_all = "snake_case")]
175pub enum ProvenanceOperation {
176 Initial,
177 Squash,
178 Amend,
179}