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
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
//! Unified State Discovery models.
//!
//! Rust is the source of truth. Ported from
//! `src/qontinui_schemas/discovery/models.py`. TS and Python bindings
//! regenerate from the JSON Schemas emitted here.
//!
//! These schemas represent the output of state discovery from any source:
//! - Playwright (web extraction)
//! - UI Bridge (render log analysis)
//! - Recording (user session recording)
//! - Vision (screenshot analysis)
//! - Manual (user-defined)
//!
//! The unified format captures:
//! - **Images**: Bounding boxes on screenshots with pixel representation.
//! - **States**: Collections of images that appear together (co-occurrence).
//! - **Transitions**: Actions that change the active set of states.
//!
//! Shared across:
//! - `qontinui-web` backend (storage and API)
//! - `qontinui-web` frontend (display)
//! - `qontinui-runner` (producing results)
//! - `qontinui` library (state discovery algorithms)
//!
//! Wire-format notes:
//! - UUIDs / IDs serialize as plain strings (see crate-level docs).
//! - Dates/timestamps are ISO 8601 strings (no `chrono` dependency).
//! - Metadata uses `serde_json::Value` for free-form `dict[str, Any]` fields.
//! - Enum string values are lowercase `snake_case` to match the Python
//!   `str, Enum` base classes in `models.py`.

use std::collections::HashMap;

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

// ============================================================================
// Enums
// ============================================================================

/// Source of a state-discovery result.
///
/// Identifies which discovery pathway produced the state machine. Mirrors
/// Python `DiscoverySourceType(str, Enum)`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DiscoverySourceType {
    /// Playwright / web DOM extraction.
    Playwright,
    /// Runner UI Bridge (render-log analysis).
    UiBridge,
    /// User-session recording.
    Recording,
    /// Screenshot / vision analysis.
    Vision,
    /// Manually authored by the user.
    Manual,
}

/// Type of action that triggers a state transition.
///
/// Mirrors Python `TransitionTriggerType(str, Enum)`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TransitionTriggerType {
    /// Mouse click or tap.
    Click,
    /// Typed text input.
    Type,
    /// Scroll gesture.
    Scroll,
    /// Hover / pointer-over.
    Hover,
    /// Custom / tool-specific trigger.
    Custom,
}

impl Default for TransitionTriggerType {
    /// Matches Python `default=TransitionTriggerType.CLICK`.
    fn default() -> Self {
        Self::Click
    }
}

// ============================================================================
// Core Components
// ============================================================================

/// Bounding box for a discovered image element.
///
/// Pixel-space rectangle on a source screenshot. `width` / `height` are `> 0`
/// on the Python side (validator not duplicated here — this is a wire-format
/// layer).
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct DiscoveryBoundingBox {
    /// X coordinate of the top-left corner (pixels).
    pub x: f64,
    /// Y coordinate of the top-left corner (pixels).
    pub y: f64,
    /// Width of the bounding box (pixels, positive).
    pub width: f64,
    /// Height of the bounding box (pixels, positive).
    pub height: f64,
}

/// Trigger for a discovered state transition.
///
/// Describes the action (click, type, …) that caused a transition. All
/// identifying fields are optional — different discovery sources populate
/// different subsets.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct DiscoveryTransitionTrigger {
    /// Type of trigger action. Defaults to `click` when omitted on the wire.
    #[serde(default)]
    pub r#type: TransitionTriggerType,
    /// ID of the image that was clicked/interacted with.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub image_id: Option<String>,
    /// ID of the DOM element (for web extraction).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub element_id: Option<String>,
    /// CSS selector for the trigger element.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub selector: Option<String>,
    /// Value for type actions (text input).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub value: Option<String>,
}

// ============================================================================
// State Machine Components
// ============================================================================

/// Visual element within a discovered state.
///
/// Represents an image crop from a screenshot with its bounding box and
/// optional pixel-level identification.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct DiscoveredStateImage {
    /// Unique identifier for the image.
    pub id: String,
    /// ID of the source screenshot.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub screenshot_id: Option<String>,
    /// URL to the source screenshot.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub screenshot_url: Option<String>,
    /// Bounding box within the screenshot.
    pub bbox: DiscoveryBoundingBox,
    /// Hash of pixel data for deduplication.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub pixel_hash: Option<String>,
    /// ID of the state this image belongs to.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub state_id: Option<String>,
    /// Semantic type of the element (e.g. `button`, `input`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub element_type: Option<String>,
    /// Human-readable label for the image.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,
    /// Confidence score for this image (0.0–1.0). Defaults to `1.0`.
    #[serde(default = "default_confidence_one")]
    pub confidence: f64,
    /// Additional free-form metadata.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metadata: Option<HashMap<String, Value>>,
}

/// A discovered UI state (collection of co-occurring elements).
///
/// States represent distinct UI screens or views identified by the set of
/// images that consistently appear together.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct DiscoveredState {
    /// Unique identifier for the state.
    pub id: String,
    /// Human-readable name.
    pub name: String,
    /// IDs of images in this state.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub image_ids: Vec<String>,
    /// IDs of renders where this state appears.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub render_ids: Vec<String>,
    /// IDs of DOM elements (for web extraction).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub element_ids: Vec<String>,
    /// Confidence score for state detection (0.0–1.0). Defaults to `1.0`.
    #[serde(default = "default_confidence_one")]
    pub confidence: f64,
    /// Description of what this state represents.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// Additional free-form metadata.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metadata: Option<HashMap<String, Value>>,
}

/// A transition between discovered states.
///
/// Transitions represent actions that change the active set of states on the
/// screen.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct DiscoveredTransition {
    /// Unique identifier for the transition.
    pub id: String,
    /// ID of the source state.
    pub from_state_id: String,
    /// ID of the target state.
    pub to_state_id: String,
    /// What triggers this transition.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trigger: Option<DiscoveryTransitionTrigger>,
    /// Confidence score for transition detection (0.0–1.0). Defaults to `1.0`.
    #[serde(default = "default_confidence_one")]
    pub confidence: f64,
    /// Additional free-form metadata.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metadata: Option<HashMap<String, Value>>,
}

// ============================================================================
// Complete Result
// ============================================================================

/// Complete state-machine result from discovery.
///
/// Unified output format regardless of the source (Playwright, UI Bridge,
/// Recording, Vision, Manual).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct StateDiscoveryResult {
    /// Unique identifier for the result.
    pub id: String,
    /// ID of the project this belongs to.
    pub project_id: String,
    /// Human-readable name.
    pub name: String,
    /// Description of this state machine.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// How this state machine was discovered.
    pub source_type: DiscoverySourceType,
    /// ID of the source session (extraction, recording, …).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub source_session_id: Option<String>,
    /// Strategy used for discovery (`auto`, `fingerprint`, `legacy`, …).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub discovery_strategy: Option<String>,
    /// All discovered images.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub images: Vec<DiscoveredStateImage>,
    /// All discovered states.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub states: Vec<DiscoveredState>,
    /// All discovered transitions.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub transitions: Vec<DiscoveredTransition>,
    /// Mapping of element IDs to render IDs where they appear.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub element_to_renders: HashMap<String, Vec<String>>,
    /// Number of images (statistic).
    #[serde(default)]
    pub image_count: i64,
    /// Number of states (statistic).
    #[serde(default)]
    pub state_count: i64,
    /// Number of transitions (statistic).
    #[serde(default)]
    pub transition_count: i64,
    /// Number of renders analyzed (statistic).
    #[serde(default)]
    pub render_count: i64,
    /// Number of unique elements (statistic).
    #[serde(default)]
    pub unique_element_count: i64,
    /// Overall confidence score (0.0–1.0).
    #[serde(default)]
    pub confidence: f64,
    /// Additional discovery metadata.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub discovery_metadata: HashMap<String, Value>,
    /// ISO 8601 timestamp of creation.
    pub created_at: String,
    /// ISO 8601 timestamp of last update.
    pub updated_at: String,
}

/// Summary of a state-discovery result (for listings).
///
/// Lightweight projection of `StateDiscoveryResult` used by list endpoints.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct StateDiscoveryResultSummary {
    /// Unique identifier.
    pub id: String,
    /// ID of the project.
    pub project_id: String,
    /// Human-readable name.
    pub name: String,
    /// Description.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// Discovery source.
    pub source_type: DiscoverySourceType,
    /// Strategy used.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub discovery_strategy: Option<String>,
    /// Number of images.
    #[serde(default)]
    pub image_count: i64,
    /// Number of states.
    #[serde(default)]
    pub state_count: i64,
    /// Number of transitions.
    #[serde(default)]
    pub transition_count: i64,
    /// Confidence score (0.0–1.0).
    #[serde(default)]
    pub confidence: f64,
    /// ISO 8601 timestamp of creation.
    pub created_at: String,
}

/// API response for listing discovery results.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct StateDiscoveryResultListResponse {
    /// List of result summaries.
    pub items: Vec<StateDiscoveryResultSummary>,
    /// Total count of results.
    pub total: i64,
}

// ============================================================================
// API Schemas
// ============================================================================

/// Request payload to create a state-discovery result.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct StateDiscoveryResultCreate {
    /// Human-readable name.
    pub name: String,
    /// Description.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// Discovery source.
    pub source_type: DiscoverySourceType,
    /// ID of the source session.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub source_session_id: Option<String>,
    /// Strategy used.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub discovery_strategy: Option<String>,
    /// Discovered images.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub images: Vec<DiscoveredStateImage>,
    /// Discovered states.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub states: Vec<DiscoveredState>,
    /// Discovered transitions.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub transitions: Vec<DiscoveredTransition>,
    /// Element to renders mapping.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub element_to_renders: HashMap<String, Vec<String>>,
    /// Confidence score (0.0–1.0).
    #[serde(default)]
    pub confidence: f64,
    /// Additional metadata.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub discovery_metadata: HashMap<String, Value>,
}

/// Request payload to update a state-discovery result.
///
/// All fields optional; only supplied fields are applied.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct StateDiscoveryResultUpdate {
    /// Human-readable name.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Description.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// Updated images.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub images: Option<Vec<DiscoveredStateImage>>,
    /// Updated states.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub states: Option<Vec<DiscoveredState>>,
    /// Updated transitions.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub transitions: Option<Vec<DiscoveredTransition>>,
    /// Updated metadata.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub discovery_metadata: Option<HashMap<String, Value>>,
}

// ============================================================================
// Export / Import
// ============================================================================

/// Portable export format for state machines.
///
/// Used when exporting a discovery result to a shareable artifact.
/// `source_type` is kept as a free-form `String` to match Python's
/// `DiscoverySourceType | str` union (enabling imports that predate the enum).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct StateMachineExport {
    /// Export format version. Defaults to `"1.0.0"`.
    #[serde(default = "default_export_version")]
    pub version: String,
    /// State machine name.
    pub name: String,
    /// Description.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// Original discovery source (string for forward compatibility — Python
    /// accepts `DiscoverySourceType | str`).
    pub source_type: String,
    /// State images.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub images: Vec<DiscoveredStateImage>,
    /// States.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub states: Vec<DiscoveredState>,
    /// Transitions.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub transitions: Vec<DiscoveredTransition>,
    /// Element to renders mapping.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub element_to_renders: HashMap<String, Vec<String>>,
    /// Export metadata (original ID, export timestamp, …).
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub metadata: HashMap<String, Value>,
}

/// Request payload to import a state machine.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct StateMachineImport {
    /// The state machine to import.
    pub state_machine: StateMachineExport,
    /// Override name (uses export name when omitted).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
}

// ============================================================================
// Serde default helpers
// ============================================================================

fn default_confidence_one() -> f64 {
    1.0
}

fn default_export_version() -> String {
    "1.0.0".to_string()
}