qontinui-types 0.6.0

Canonical DTO types for Qontinui. Rust is the source of truth; TypeScript and Python are generated from JSON Schema emitted by schemars.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
//! Findings models — AI-analysis result payloads.
//!
//! Rust is the source of truth. Ported from
//! `src/qontinui_schemas/findings/models.py` + `enums.py`. TS and Python
//! bindings regenerate from the JSON Schemas emitted here.
//!
//! A *finding* represents an issue, observation, or recommendation produced
//! by an AI analysis session. Findings flow Runner → Backend (create/update)
//! and Backend → Frontend (detail/list/summary).
//!
//! Wire-format notes:
//! - UUIDs are serialized as plain strings (see crate-level docs).
//! - Dates are ISO 8601 strings (no `chrono` dependency).
//! - Enum string values are lowercase `snake_case` to match the Python
//!   `str | Enum` base classes in `enums.py`.

use std::collections::HashMap;

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

// ============================================================================
// Enums (ported from findings/enums.py)
// ============================================================================

/// Category of a detected finding.
///
/// Determines the kind of issue or observation surfaced during analysis.
/// Mirrors Python `FindingCategory(str, Enum)`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FindingCategory {
    /// A bug in application code.
    CodeBug,
    /// A security concern (vulnerability, exposed secret, etc.).
    Security,
    /// A performance concern.
    Performance,
    /// A TODO or open task.
    Todo,
    /// A proposed enhancement.
    Enhancement,
    /// A configuration issue.
    ConfigIssue,
    /// A test-related issue.
    TestIssue,
    /// A documentation issue.
    Documentation,
    /// A runtime issue observed during execution.
    RuntimeIssue,
    /// An issue that was already fixed.
    AlreadyFixed,
    /// Behavior that looked suspicious but is expected.
    ExpectedBehavior,
}

/// Severity level of a finding.
///
/// Lifecycle (ordered, most-severe first): `CRITICAL` → `HIGH` → `MEDIUM` →
/// `LOW` → `INFO`. Mirrors Python `FindingSeverity(str, Enum)`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FindingSeverity {
    /// Blocks functionality — immediate attention required.
    Critical,
    /// Major issue — should be fixed soon.
    High,
    /// Moderate issue — should be fixed.
    Medium,
    /// Minor issue — fix when convenient.
    Low,
    /// Informational — no action required.
    Info,
}

/// Status of a finding.
///
/// Lifecycle: `DETECTED` → `IN_PROGRESS` → (`RESOLVED` | `WONT_FIX` |
/// `DEFERRED`). `NEEDS_INPUT` is a special state requiring user decision.
/// Mirrors Python `FindingStatus(str, Enum)`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FindingStatus {
    /// Newly detected.
    Detected,
    /// Being worked on.
    InProgress,
    /// Needs user input before it can proceed.
    NeedsInput,
    /// Resolved.
    Resolved,
    /// Acknowledged but won't be fixed.
    WontFix,
    /// Deferred to a later time.
    Deferred,
}

/// Type of action recommended for a finding.
///
/// Mirrors Python `FindingActionType(str, Enum)`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FindingActionType {
    /// Can be automatically fixed without user intervention.
    AutoFix,
    /// Requires user decision or input to resolve.
    NeedsUserInput,
    /// Requires manual intervention.
    Manual,
    /// No action needed — for awareness only.
    Informational,
}

// ============================================================================
// Supporting structs
// ============================================================================

/// Code context for a finding.
///
/// Provides location (file/line/column) and an optional snippet for findings
/// that relate to specific code.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct FindingCodeContext {
    /// File path where the finding was detected.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "file")]
    pub file: Option<String>,
    /// Line number where the finding was detected.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "line")]
    pub line: Option<i64>,
    /// Column number where the finding was detected.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "column")]
    pub column: Option<i64>,
    /// Code snippet related to the finding (max 1000 chars on the Python side).
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "snippet")]
    pub snippet: Option<String>,
}

/// User-input request attached to a finding.
///
/// Defines the question to pose and the expected input format when a finding
/// requires a user decision.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct FindingUserInput {
    /// Question to present to the user.
    #[serde(alias = "question")]
    pub question: String,
    /// Type of input expected — typically `"text"` or `"choice"`.
    #[serde(default = "default_input_type", alias = "input_type")]
    pub input_type: String,
    /// Options for choice-type input.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "options")]
    pub options: Option<Vec<String>>,
}

fn default_input_type() -> String {
    "text".to_string()
}

// ============================================================================
// Create / Update / Detail payloads
// ============================================================================

/// Schema for creating a finding (Runner → Backend).
///
/// Sent by the runner when an AI analysis session detects an issue or
/// observation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct FindingCreate {
    /// Parent task run ID.
    #[serde(alias = "task_run_id")]
    pub task_run_id: String,
    /// Session number where the finding was detected.
    #[serde(alias = "session_num")]
    pub session_num: i64,
    /// Category of the finding.
    #[serde(alias = "category")]
    pub category: FindingCategory,
    /// Severity level of the finding.
    #[serde(alias = "severity")]
    pub severity: FindingSeverity,
    /// Brief title describing the finding (max 500 chars on the Python side).
    #[serde(alias = "title")]
    pub title: String,
    /// Detailed description of the finding.
    #[serde(alias = "description")]
    pub description: String,
    /// Code context, if the finding relates to specific code.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "code_context"
    )]
    pub code_context: Option<FindingCodeContext>,
    /// Hash used to deduplicate findings across sessions.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "signature_hash"
    )]
    pub signature_hash: Option<String>,
    /// Type of action for this finding.
    #[serde(alias = "action_type")]
    pub action_type: FindingActionType,
    /// User-input request, if `action_type` requires a user decision.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "user_input")]
    pub user_input: Option<FindingUserInput>,
}

/// Request schema for batch finding creation.
///
/// Allows creating multiple findings in a single request. The Python side
/// enforces `1 <= len(findings) <= 50`; validators on the Rust side are
/// intentionally omitted to keep this a pure wire-format layer.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct FindingBatchCreate {
    /// Findings to create (1–50 items on the Python side).
    #[serde(alias = "findings")]
    pub findings: Vec<FindingCreate>,
}

/// Schema for updating a finding.
///
/// Used to update status, record a resolution, or capture a user response.
/// All fields are optional; only those supplied are applied.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct FindingUpdate {
    /// New status for the finding.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "status")]
    pub status: Option<FindingStatus>,
    /// Resolution description.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "resolution")]
    pub resolution: Option<String>,
    /// Session number where the finding was resolved.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "resolved_in_session"
    )]
    pub resolved_in_session: Option<i64>,
    /// User's response to a finding requiring input.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "user_response"
    )]
    pub user_response: Option<String>,
}

/// Detailed finding information (Backend → Frontend).
///
/// Used when retrieving individual finding details. The `id` is a UUID v4
/// string (see crate-level wire-format note).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct FindingDetail {
    /// Finding ID (UUID v4 string).
    #[serde(alias = "id")]
    pub id: String,
    /// Parent task run ID.
    #[serde(alias = "task_run_id")]
    pub task_run_id: String,
    /// Session number where the finding was detected.
    #[serde(alias = "session_num")]
    pub session_num: i64,
    /// Category of the finding.
    #[serde(alias = "category")]
    pub category: FindingCategory,
    /// Severity level of the finding.
    #[serde(alias = "severity")]
    pub severity: FindingSeverity,
    /// Current status of the finding.
    #[serde(alias = "status")]
    pub status: FindingStatus,
    /// Brief title describing the finding.
    #[serde(alias = "title")]
    pub title: String,
    /// Detailed description of the finding.
    #[serde(alias = "description")]
    pub description: String,
    /// Resolution description if resolved.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "resolution")]
    pub resolution: Option<String>,
    /// Code context, if the finding relates to specific code.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "code_context"
    )]
    pub code_context: Option<FindingCodeContext>,
    /// Hash used to deduplicate findings across sessions.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "signature_hash"
    )]
    pub signature_hash: Option<String>,
    /// Type of action for this finding.
    #[serde(alias = "action_type")]
    pub action_type: FindingActionType,
    /// User-input request, if `action_type` requires a user decision.
    #[serde(default, skip_serializing_if = "Option::is_none", alias = "user_input")]
    pub user_input: Option<FindingUserInput>,
    /// User's response, if input was requested.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "user_response"
    )]
    pub user_response: Option<String>,
    /// ISO 8601 timestamp (UTC) when the finding was detected.
    #[serde(alias = "detected_at")]
    pub detected_at: String,
    /// ISO 8601 timestamp (UTC) when the finding was resolved.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "resolved_at"
    )]
    pub resolved_at: Option<String>,
    /// Session number where the finding was resolved.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        alias = "resolved_in_session"
    )]
    pub resolved_in_session: Option<i64>,
}

/// Response schema for a paginated finding list.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct FindingListResponse {
    /// Findings on this page.
    #[serde(default, alias = "findings")]
    pub findings: Vec<FindingDetail>,
    /// Total count of findings matching the query.
    #[serde(alias = "total")]
    pub total: i64,
    /// Maximum items per page.
    #[serde(alias = "limit")]
    pub limit: i64,
    /// Number of items skipped.
    #[serde(alias = "offset")]
    pub offset: i64,
    /// Whether more items exist beyond this page.
    #[serde(alias = "has_more")]
    pub has_more: bool,
}

/// Summary statistics for findings in a task run.
///
/// Aggregated counts grouped along each axis (category, severity, status)
/// plus roll-up counts for UI dashboards.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct FindingSummary {
    /// Task run ID.
    #[serde(alias = "task_run_id")]
    pub task_run_id: String,
    /// Total number of findings.
    #[serde(default, alias = "total")]
    pub total: i64,
    /// Count of findings by category.
    #[serde(
        default,
        skip_serializing_if = "HashMap::is_empty",
        alias = "by_category"
    )]
    pub by_category: HashMap<String, i64>,
    /// Count of findings by severity.
    #[serde(
        default,
        skip_serializing_if = "HashMap::is_empty",
        alias = "by_severity"
    )]
    pub by_severity: HashMap<String, i64>,
    /// Count of findings by status.
    #[serde(
        default,
        skip_serializing_if = "HashMap::is_empty",
        alias = "by_status"
    )]
    pub by_status: HashMap<String, i64>,
    /// Number of findings awaiting user input.
    #[serde(default, alias = "needs_input_count")]
    pub needs_input_count: i64,
    /// Number of resolved findings.
    #[serde(default, alias = "resolved_count")]
    pub resolved_count: i64,
    /// Number of unresolved findings.
    #[serde(default, alias = "outstanding_count")]
    pub outstanding_count: i64,
}