1use serde::{Deserialize, Serialize};
8
9use crate::core::{
10 Bead, Claim, DepEdge as CoreDepEdge, Tombstone as CoreTombstone, WallClock, Workflow,
11 WriteStamp,
12};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct DaemonInfo {
20 pub version: String,
21 pub protocol_version: u32,
22 pub pid: u32,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(tag = "kind", rename_all = "snake_case")]
31pub enum SyncWarning {
32 Fetch {
33 message: String,
34 at_wall_ms: u64,
35 },
36 Diverged {
37 local_oid: String,
38 remote_oid: String,
39 at_wall_ms: u64,
40 },
41 ForcePush {
42 previous_remote_oid: String,
43 remote_oid: String,
44 at_wall_ms: u64,
45 },
46 ClockSkew {
47 delta_ms: i64,
48 at_wall_ms: u64,
49 },
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct SyncStatus {
54 pub dirty: bool,
55 pub sync_in_progress: bool,
56 pub last_sync_wall_ms: Option<u64>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub next_retry_wall_ms: Option<u64>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub next_retry_in_ms: Option<u64>,
61 pub consecutive_failures: u32,
62 #[serde(default, skip_serializing_if = "Vec::is_empty")]
63 pub warnings: Vec<SyncWarning>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct StatusSummary {
68 pub total_issues: usize,
69 pub open_issues: usize,
70 pub in_progress_issues: usize,
71 pub blocked_issues: usize,
72 pub closed_issues: usize,
73 pub ready_issues: usize,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub tombstone_issues: Option<usize>,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub epics_eligible_for_closure: Option<usize>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct StatusOutput {
84 pub summary: StatusSummary,
85
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub sync: Option<SyncStatus>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct BlockedIssue {
96 #[serde(flatten)]
97 pub issue: IssueSummary,
98
99 pub blocked_by_count: usize,
100 pub blocked_by: Vec<String>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ReadyResult {
110 pub issues: Vec<IssueSummary>,
111 pub blocked_count: usize,
112 pub closed_count: usize,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct EpicStatus {
121 pub epic: IssueSummary,
122 pub total_children: usize,
123 pub closed_children: usize,
124 pub eligible_for_close: bool,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct CountGroup {
133 pub group: String,
134 pub count: usize,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138#[serde(untagged)]
139pub enum CountResult {
140 Simple {
141 count: usize,
142 },
143 Grouped {
144 total: usize,
145 groups: Vec<CountGroup>,
146 },
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct DeletedLookup {
155 pub found: bool,
156 pub id: String,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub record: Option<Tombstone>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct Note {
168 pub id: String,
169 pub content: String,
170 pub author: String,
171 pub at: WriteStamp,
172}
173
174impl From<&crate::core::Note> for Note {
175 fn from(n: &crate::core::Note) -> Self {
176 Self {
177 id: n.id.as_str().to_string(),
178 content: n.content.clone(),
179 author: n.author.as_str().to_string(),
180 at: n.at.clone(),
181 }
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct DepEdge {
191 pub from: String,
192 pub to: String,
193 pub kind: String,
194 pub created_at: WriteStamp,
195 pub created_by: String,
196 pub deleted_at: Option<WriteStamp>,
197 pub deleted_by: Option<String>,
198}
199
200impl From<&CoreDepEdge> for DepEdge {
201 fn from(edge: &CoreDepEdge) -> Self {
202 Self {
203 from: edge.key.from().as_str().to_string(),
204 to: edge.key.to().as_str().to_string(),
205 kind: edge.key.kind().as_str().to_string(),
206 created_at: edge.created.at.clone(),
207 created_by: edge.created.by.as_str().to_string(),
208 deleted_at: edge.deleted_stamp().map(|s| s.at.clone()),
209 deleted_by: edge.deleted_stamp().map(|s| s.by.as_str().to_string()),
210 }
211 }
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct Tombstone {
220 pub id: String,
221 pub deleted_at: WriteStamp,
222 pub deleted_by: String,
223 pub reason: Option<String>,
224}
225
226impl From<&CoreTombstone> for Tombstone {
227 fn from(t: &CoreTombstone) -> Self {
228 Self {
229 id: t.id.as_str().to_string(),
230 deleted_at: t.deleted.at.clone(),
231 deleted_by: t.deleted.by.as_str().to_string(),
232 reason: t.reason.clone(),
233 }
234 }
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct Issue {
244 pub id: String,
245 pub title: String,
246 pub description: String,
247 pub design: Option<String>,
248 pub acceptance_criteria: Option<String>,
249 pub status: String,
250 pub priority: u8,
251 #[serde(rename = "type")]
252 pub issue_type: String,
253 pub labels: Vec<String>,
254
255 pub assignee: Option<String>,
256 pub assignee_at: Option<WriteStamp>,
257 pub assignee_expires: Option<WallClock>,
258
259 pub created_at: WriteStamp,
260 pub created_by: String,
261 pub created_on_branch: Option<String>,
262
263 pub updated_at: WriteStamp,
264 pub updated_by: String,
265
266 pub closed_at: Option<WriteStamp>,
267 pub closed_by: Option<String>,
268 pub closed_reason: Option<String>,
269 pub closed_on_branch: Option<String>,
270
271 pub external_ref: Option<String>,
272 pub source_repo: Option<String>,
273
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub estimated_minutes: Option<u32>,
277
278 pub content_hash: String,
279
280 pub notes: Vec<Note>,
281
282 #[serde(default, skip_serializing_if = "Vec::is_empty")]
284 pub deps_incoming: Vec<DepEdge>,
285
286 #[serde(default, skip_serializing_if = "Vec::is_empty")]
288 pub deps_outgoing: Vec<DepEdge>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct IssueSummary {
294 pub id: String,
295 pub title: String,
296 pub description: String,
297 pub design: Option<String>,
298 pub acceptance_criteria: Option<String>,
299 pub status: String,
300 pub priority: u8,
301 #[serde(rename = "type")]
302 pub issue_type: String,
303 pub labels: Vec<String>,
304
305 pub assignee: Option<String>,
306 pub assignee_expires: Option<WallClock>,
307
308 pub created_at: WriteStamp,
309 pub created_by: String,
310
311 pub updated_at: WriteStamp,
312 pub updated_by: String,
313
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub estimated_minutes: Option<u32>,
317
318 pub content_hash: String,
319
320 pub note_count: usize,
321}
322
323impl Issue {
324 pub fn from_bead(bead: &Bead) -> Self {
325 let updated = bead.updated_stamp();
326
327 let (assignee, assignee_at, assignee_expires) = match &bead.fields.claim.value {
328 Claim::Claimed { assignee, expires } => (
329 Some(assignee.as_str().to_string()),
330 Some(bead.fields.claim.stamp.at.clone()),
331 *expires,
332 ),
333 Claim::Unclaimed => (None, None, None),
334 };
335
336 let (closed_at, closed_by, closed_reason, closed_on_branch) =
337 match &bead.fields.workflow.value {
338 Workflow::Closed(c) => (
339 Some(bead.fields.workflow.stamp.at.clone()),
340 Some(bead.fields.workflow.stamp.by.as_str().to_string()),
341 c.reason.clone(),
342 c.on_branch.clone(),
343 ),
344 _ => (None, None, None, None),
345 };
346
347 let notes = bead.notes.sorted().into_iter().map(Note::from).collect();
348
349 Self {
350 id: bead.core.id.as_str().to_string(),
351 title: bead.fields.title.value.clone(),
352 description: bead.fields.description.value.clone(),
353 design: bead.fields.design.value.clone(),
354 acceptance_criteria: bead.fields.acceptance_criteria.value.clone(),
355 status: bead.fields.workflow.value.status().to_string(),
356 priority: bead.fields.priority.value.value(),
357 issue_type: bead.fields.bead_type.value.as_str().to_string(),
358 labels: bead
359 .fields
360 .labels
361 .value
362 .iter()
363 .map(|l| l.as_str().to_string())
364 .collect(),
365 assignee,
366 assignee_at,
367 assignee_expires,
368 created_at: bead.core.created().at.clone(),
369 created_by: bead.core.created().by.as_str().to_string(),
370 created_on_branch: bead.core.created_on_branch().map(|s| s.to_string()),
371 updated_at: updated.at.clone(),
372 updated_by: updated.by.as_str().to_string(),
373 closed_at,
374 closed_by,
375 closed_reason,
376 closed_on_branch,
377 external_ref: bead.fields.external_ref.value.clone(),
378 source_repo: bead.fields.source_repo.value.clone(),
379 estimated_minutes: bead.fields.estimated_minutes.value,
380 content_hash: bead.content_hash().to_hex(),
381 notes,
382 deps_incoming: Vec::new(),
383 deps_outgoing: Vec::new(),
384 }
385 }
386}
387
388impl IssueSummary {
389 pub fn from_bead(bead: &Bead) -> Self {
390 let updated = bead.updated_stamp();
391 Self {
392 id: bead.core.id.as_str().to_string(),
393 title: bead.fields.title.value.clone(),
394 description: bead.fields.description.value.clone(),
395 design: bead.fields.design.value.clone(),
396 acceptance_criteria: bead.fields.acceptance_criteria.value.clone(),
397 status: bead.fields.workflow.value.status().to_string(),
398 priority: bead.fields.priority.value.value(),
399 issue_type: bead.fields.bead_type.value.as_str().to_string(),
400 labels: bead
401 .fields
402 .labels
403 .value
404 .iter()
405 .map(|l| l.as_str().to_string())
406 .collect(),
407 assignee: bead
408 .fields
409 .claim
410 .value
411 .assignee()
412 .map(|a| a.as_str().to_string()),
413 assignee_expires: bead.fields.claim.value.expires(),
414 created_at: bead.core.created().at.clone(),
415 created_by: bead.core.created().by.as_str().to_string(),
416 updated_at: updated.at.clone(),
417 updated_by: updated.by.as_str().to_string(),
418 estimated_minutes: bead.fields.estimated_minutes.value,
419 content_hash: bead.content_hash().to_hex(),
420 note_count: bead.notes.len(),
421 }
422 }
423
424 pub fn from_issue(issue: &Issue) -> Self {
425 Self {
426 id: issue.id.clone(),
427 title: issue.title.clone(),
428 description: issue.description.clone(),
429 design: issue.design.clone(),
430 acceptance_criteria: issue.acceptance_criteria.clone(),
431 status: issue.status.clone(),
432 priority: issue.priority,
433 issue_type: issue.issue_type.clone(),
434 labels: issue.labels.clone(),
435 assignee: issue.assignee.clone(),
436 assignee_expires: issue.assignee_expires,
437 created_at: issue.created_at.clone(),
438 created_by: issue.created_by.clone(),
439 updated_at: issue.updated_at.clone(),
440 updated_by: issue.updated_by.clone(),
441 estimated_minutes: issue.estimated_minutes,
442 content_hash: issue.content_hash.clone(),
443 note_count: issue.notes.len(),
444 }
445 }
446}