1use std::fmt;
14use std::str::FromStr;
15
16use chrono::{DateTime, Utc};
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19use systemprompt_identifiers::{
20 ContentHash, ContextId, DocumentUploadId, IssueReportId, McpExecutionId, SessionId, TaskId,
21 TraceId, UserId,
22};
23
24#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
25#[error("unknown {kind} `{value}`")]
26pub struct UnknownIssueValue {
27 pub kind: &'static str,
28 pub value: String,
29}
30
31macro_rules! issue_enum {
32 ($name:ident, $kind:literal, { $($variant:ident => $wire:literal),+ $(,)? }) => {
33 impl $name {
34 pub const ALL: &'static [Self] = &[$(Self::$variant),+];
35
36 #[must_use]
37 pub const fn as_str(self) -> &'static str {
38 match self {
39 $(Self::$variant => $wire),+
40 }
41 }
42 }
43
44 impl fmt::Display for $name {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 f.write_str(self.as_str())
47 }
48 }
49
50 impl FromStr for $name {
51 type Err = UnknownIssueValue;
52
53 fn from_str(s: &str) -> Result<Self, Self::Err> {
54 match s {
55 $($wire => Ok(Self::$variant),)+
56 other => Err(UnknownIssueValue {
57 kind: $kind,
58 value: other.to_owned(),
59 }),
60 }
61 }
62 }
63 };
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
67#[serde(rename_all = "snake_case")]
68pub enum IssueKind {
69 Bug,
70 Feedback,
71}
72
73issue_enum!(IssueKind, "issue kind", {
74 Bug => "bug",
75 Feedback => "feedback",
76});
77
78#[derive(
79 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
80)]
81#[serde(rename_all = "snake_case")]
82pub enum IssueSeverity {
83 Low,
84 Medium,
85 High,
86 Critical,
87}
88
89issue_enum!(IssueSeverity, "issue severity", {
90 Low => "low",
91 Medium => "medium",
92 High => "high",
93 Critical => "critical",
94});
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
97#[serde(rename_all = "snake_case")]
98pub enum IssueStatus {
99 Open,
100 Triaged,
101 NeedsInfo,
102 Resolved,
103 Closed,
104}
105
106issue_enum!(IssueStatus, "issue status", {
107 Open => "open",
108 Triaged => "triaged",
109 NeedsInfo => "needs_info",
110 Resolved => "resolved",
111 Closed => "closed",
112});
113
114impl IssueStatus {
115 #[must_use]
116 pub const fn is_active(self) -> bool {
117 matches!(self, Self::Open | Self::Triaged | Self::NeedsInfo)
118 }
119}
120
121#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
124pub struct IssueContext {
125 pub trace: Option<TraceId>,
126 pub context: Option<ContextId>,
127 pub session: Option<SessionId>,
128 pub task: Option<TaskId>,
129 pub mcp_execution: Option<McpExecutionId>,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
133pub struct IssueAttachment {
134 pub upload_id: DocumentUploadId,
135 pub content_sha256: ContentHash,
136 pub file_name: String,
137 pub mime_type: String,
138 pub size_bytes: i64,
139 pub created_at: DateTime<Utc>,
140}
141
142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
144pub struct IssueReport {
145 pub id: IssueReportId,
146 pub reporter_user_id: UserId,
147 pub via_agent: Option<String>,
148 pub kind: IssueKind,
149 pub severity: IssueSeverity,
150 pub status: IssueStatus,
151 pub title: String,
152 pub body: String,
153 pub occurrences: i32,
154 pub context: IssueContext,
155 pub client: serde_json::Value,
157 pub triage_note: Option<String>,
158 pub triaged_by: Option<UserId>,
159 pub created_at: DateTime<Utc>,
160 pub last_seen_at: DateTime<Utc>,
161 pub updated_at: DateTime<Utc>,
162 pub closed_at: Option<DateTime<Utc>>,
163 #[serde(default)]
164 pub attachments: Vec<IssueAttachment>,
165}