1use crate::codec::RecordCodec;
12use gonzalo_core::{RecordKey, RecordKind};
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeMap;
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub enum Provider {
19 GitHub,
20 Jira,
21 Linear,
22 GitLab,
23 Asana,
24 AzureDevOps,
25 Bugzilla,
26 Other(String),
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
34pub enum StateCategory {
35 Triage,
36 Backlog,
37 Open,
38 InProgress,
39 Pending,
41 Done,
42 Canceled,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub enum Resolution {
49 Done,
50 WontDo,
51 Duplicate,
52 Invalid,
53 CannotReproduce,
54 Moved,
55 Other(String),
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61pub struct State {
62 pub category: StateCategory,
63 pub resolution: Option<Resolution>,
64 pub raw_name: String,
65 pub raw_id: Option<String>,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71pub enum ActorRole {
72 Requester,
73 Assignee,
74 Submitter,
75 Follower,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct Actor {
81 pub role: ActorRole,
82 pub handle: String,
83 pub display: Option<String>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct Container {
91 pub kind: String,
92 pub id: String,
93 pub name: Option<String>,
94 pub primary: bool,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99pub enum LinkKind {
100 Blocks,
101 BlockedBy,
102 Relates,
103 Parent,
104 Child,
105 Duplicate,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub enum LinkTarget {
112 Record(RecordKey),
113 External(String),
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct Link {
119 pub kind: LinkKind,
120 pub target: LinkTarget,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
126pub enum BodyFormat {
127 Markdown,
128 Adf,
129 Html,
130 PlainText,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub struct TicketBody {
136 pub markdown: String,
137 pub format: BodyFormat,
138 pub raw: Option<String>,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
143pub enum PriorityLevel {
144 None,
145 Low,
146 Medium,
147 High,
148 Urgent,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
153pub struct Priority {
154 pub level: PriorityLevel,
155 pub raw: Option<String>,
156}
157
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163pub struct Ticket {
164 pub provider: Provider,
165 pub uid: String,
167 pub display: String,
169 pub item_type: String,
171 pub title: String,
172 pub state: State,
173 pub priority: Option<Priority>,
174 pub actors: Vec<Actor>,
175 pub labels: Vec<String>,
176 pub containers: Vec<Container>,
177 pub links: Vec<Link>,
178 pub body: TicketBody,
179 pub fields: BTreeMap<String, serde_json::Value>,
181}
182impl RecordCodec for Ticket {}
183impl Ticket {
184 pub const KIND: RecordKind = RecordKind::Ticket;
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190pub struct TicketEvent {
191 pub ticket_uid: String,
193 pub kind: String,
195 pub author: String,
196 pub at: i64,
198 pub body: String,
199}
200impl RecordCodec for TicketEvent {}
201impl TicketEvent {
202 pub const KIND: RecordKind = RecordKind::TicketEvent;
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use gonzalo_core::RecordKey;
209
210 fn sample_ticket() -> Ticket {
211 let mut fields = BTreeMap::new();
212 fields.insert("story_points".into(), serde_json::json!(5));
213 Ticket {
214 provider: Provider::GitHub,
215 uid: "gh:caliban-ai/gonzalo#15".into(),
216 display: "#15".into(),
217 item_type: "issue".into(),
218 title: "design: ticket-system capability layer".into(),
219 state: State {
220 category: StateCategory::InProgress,
221 resolution: None,
222 raw_name: "In progress".into(),
223 raw_id: Some("47fc9ee4".into()),
224 },
225 priority: Some(Priority {
226 level: PriorityLevel::Medium,
227 raw: Some("important-longterm".into()),
228 }),
229 actors: vec![Actor {
230 role: ActorRole::Assignee,
231 handle: "johnford2002".into(),
232 display: None,
233 }],
234 labels: vec!["area/integration".into(), "kind/design".into()],
235 containers: vec![Container {
236 kind: "repo".into(),
237 id: "caliban-ai/gonzalo".into(),
238 name: Some("gonzalo".into()),
239 primary: true,
240 }],
241 links: vec![Link {
242 kind: LinkKind::Relates,
243 target: LinkTarget::Record(RecordKey::new("caliban", "tickets", "gh:16")),
244 }],
245 body: TicketBody {
246 markdown: "Model tickets as a capability layer.".into(),
247 format: BodyFormat::Markdown,
248 raw: None,
249 },
250 fields,
251 }
252 }
253
254 #[test]
255 fn ticket_roundtrips_through_body() {
256 let t = sample_ticket();
257 assert_eq!(Ticket::from_body(&t.to_body().unwrap()).unwrap(), t);
258 assert_eq!(Ticket::KIND, RecordKind::Ticket);
259 }
260
261 #[test]
262 fn ticket_event_roundtrips_through_body() {
263 let e = TicketEvent {
264 ticket_uid: "gh:caliban-ai/gonzalo#15".into(),
265 kind: "comment".into(),
266 author: "johnford2002".into(),
267 at: 1_750_000_000,
268 body: "Begin the build.".into(),
269 };
270 assert_eq!(TicketEvent::from_body(&e.to_body().unwrap()).unwrap(), e);
271 assert_eq!(TicketEvent::KIND, RecordKind::TicketEvent);
272 }
273
274 #[test]
275 fn priority_levels_are_ordered() {
276 assert!(PriorityLevel::None < PriorityLevel::Urgent);
277 assert!(PriorityLevel::Low < PriorityLevel::High);
278 }
279}