Skip to main content

chronicle/schema/
annotation.rs

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    /// Validate the annotation for structural correctness.
41    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    /// Validate a region annotation for structural correctness.
62    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}