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