Skip to main content

chronicle/schema/
v1.rs

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