Skip to main content

meerkat_workgraph/
types.rs

1use std::collections::BTreeSet;
2use std::fmt;
3use std::str::FromStr;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use uuid::Uuid;
9
10use crate::WorkGraphError;
11use crate::machines::workgraph_lifecycle as wg_dsl;
12pub use crate::machines::workgraph_lifecycle::WorkGraphLifecycleMachineState as WorkGraphMachineState;
13
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
15#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
16#[serde(transparent)]
17pub struct WorkItemId(String);
18
19impl WorkItemId {
20    pub fn new(value: impl Into<String>) -> Result<Self, WorkGraphError> {
21        validate_token("work item id", value.into()).map(Self)
22    }
23
24    pub fn generated() -> Self {
25        Self(format!("work_{}", Uuid::now_v7()))
26    }
27
28    pub fn as_str(&self) -> &str {
29        &self.0
30    }
31}
32
33impl fmt::Display for WorkItemId {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.write_str(self.as_str())
36    }
37}
38
39impl FromStr for WorkItemId {
40    type Err = WorkGraphError;
41
42    fn from_str(value: &str) -> Result<Self, Self::Err> {
43        Self::new(value)
44    }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
48#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
49#[serde(transparent)]
50pub struct WorkNamespace(String);
51
52impl WorkNamespace {
53    pub fn new(value: impl Into<String>) -> Result<Self, WorkGraphError> {
54        validate_token("work namespace", value.into()).map(Self)
55    }
56
57    pub fn default_namespace() -> Self {
58        Self("default".to_string())
59    }
60
61    pub fn as_str(&self) -> &str {
62        &self.0
63    }
64}
65
66impl Default for WorkNamespace {
67    fn default() -> Self {
68        Self::default_namespace()
69    }
70}
71
72impl fmt::Display for WorkNamespace {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        f.write_str(self.as_str())
75    }
76}
77
78impl FromStr for WorkNamespace {
79    type Err = WorkGraphError;
80
81    fn from_str(value: &str) -> Result<Self, Self::Err> {
82        Self::new(value)
83    }
84}
85
86fn validate_token(name: &str, value: String) -> Result<String, WorkGraphError> {
87    let trimmed = value.trim();
88    if trimmed.is_empty() {
89        return Err(WorkGraphError::InvalidInput(format!(
90            "{name} must not be empty"
91        )));
92    }
93    if trimmed.chars().any(char::is_control) {
94        return Err(WorkGraphError::InvalidInput(format!(
95            "{name} must not contain control characters"
96        )));
97    }
98    Ok(trimmed.to_string())
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
102#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
103#[serde(rename_all = "snake_case")]
104pub enum WorkStatus {
105    #[default]
106    Open,
107    InProgress,
108    Blocked,
109    Completed,
110    Cancelled,
111    Failed,
112}
113
114impl WorkStatus {
115    pub fn is_terminal(self) -> bool {
116        matches!(self, Self::Completed | Self::Cancelled | Self::Failed)
117    }
118
119    pub fn is_terminal_success(self) -> bool {
120        matches!(self, Self::Completed)
121    }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
125#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
126#[serde(rename_all = "snake_case")]
127pub enum WorkPriority {
128    Low,
129    #[default]
130    Medium,
131    High,
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
135#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
136#[serde(rename_all = "snake_case")]
137pub enum WorkEdgeKind {
138    Blocks,
139    Parent,
140    Related,
141    Supersedes,
142    DerivedFrom,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
146#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
147#[serde(rename_all = "snake_case")]
148pub enum WorkOwnerKind {
149    Principal,
150    Agent,
151    Session,
152    Mob,
153    Label,
154}
155
156impl WorkOwnerKind {
157    pub fn as_str(self) -> &'static str {
158        match self {
159            Self::Principal => "principal",
160            Self::Agent => "agent",
161            Self::Session => "session",
162            Self::Mob => "mob",
163            Self::Label => "label",
164        }
165    }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
169#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
170pub struct WorkOwnerKey {
171    pub kind: WorkOwnerKind,
172    pub id: String,
173}
174
175impl WorkOwnerKey {
176    pub fn new(kind: WorkOwnerKind, id: impl Into<String>) -> Result<Self, WorkGraphError> {
177        Ok(Self {
178            kind,
179            id: validate_token("work owner id", id.into())?,
180        })
181    }
182
183    pub fn principal(id: impl Into<String>) -> Result<Self, WorkGraphError> {
184        Self::new(WorkOwnerKind::Principal, id)
185    }
186
187    pub fn agent(id: impl Into<String>) -> Result<Self, WorkGraphError> {
188        Self::new(WorkOwnerKind::Agent, id)
189    }
190
191    pub fn session(id: impl Into<String>) -> Result<Self, WorkGraphError> {
192        Self::new(WorkOwnerKind::Session, id)
193    }
194
195    pub fn mob(id: impl Into<String>) -> Result<Self, WorkGraphError> {
196        Self::new(WorkOwnerKind::Mob, id)
197    }
198
199    pub fn label(id: impl Into<String>) -> Result<Self, WorkGraphError> {
200        Self::new(WorkOwnerKind::Label, id)
201    }
202
203    pub fn canonical(&self) -> String {
204        format!("{}:{}", self.kind.as_str(), self.id)
205    }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
209#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
210pub struct WorkOwner {
211    pub key: WorkOwnerKey,
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    pub display_name: Option<String>,
214}
215
216impl WorkOwner {
217    pub fn new(key: WorkOwnerKey) -> Self {
218        Self {
219            key,
220            display_name: None,
221        }
222    }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
227pub struct WorkClaim {
228    pub owner: WorkOwner,
229    pub claimed_at: DateTime<Utc>,
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub lease_expires_at: Option<DateTime<Utc>>,
232}
233
234impl WorkClaim {
235    pub fn is_active_at(&self, now: DateTime<Utc>) -> bool {
236        self.lease_expires_at
237            .is_none_or(|lease_expires_at| lease_expires_at > now)
238    }
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
243pub struct ExternalWorkRef {
244    pub kind: String,
245    pub id: String,
246    #[serde(default, skip_serializing_if = "Option::is_none")]
247    pub url: Option<String>,
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
252pub struct WorkEvidenceRef {
253    pub kind: String,
254    pub id: String,
255    #[serde(default, skip_serializing_if = "Option::is_none")]
256    pub label: Option<String>,
257    #[serde(default, skip_serializing_if = "Option::is_none")]
258    pub summary: Option<String>,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
262pub struct WorkItem {
263    pub id: WorkItemId,
264    pub realm_id: String,
265    pub namespace: WorkNamespace,
266    pub title: String,
267    #[serde(default, skip_serializing_if = "Option::is_none")]
268    pub description: Option<String>,
269    pub status: WorkStatus,
270    pub priority: WorkPriority,
271    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
272    pub labels: BTreeSet<String>,
273    #[serde(default, skip_serializing_if = "Option::is_none")]
274    pub owner: Option<WorkOwner>,
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub claim: Option<WorkClaim>,
277    #[serde(default = "default_workgraph_machine_state")]
278    pub machine_state: WorkGraphMachineState,
279    pub revision: u64,
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub due_at: Option<DateTime<Utc>>,
282    #[serde(default, skip_serializing_if = "Option::is_none")]
283    pub not_before: Option<DateTime<Utc>>,
284    #[serde(default, skip_serializing_if = "Option::is_none")]
285    pub snoozed_until: Option<DateTime<Utc>>,
286    pub created_at: DateTime<Utc>,
287    pub updated_at: DateTime<Utc>,
288    #[serde(default, skip_serializing_if = "Option::is_none")]
289    pub terminal_at: Option<DateTime<Utc>>,
290    #[serde(default, skip_serializing_if = "Vec::is_empty")]
291    pub external_refs: Vec<ExternalWorkRef>,
292    #[serde(default, skip_serializing_if = "Vec::is_empty")]
293    pub evidence_refs: Vec<WorkEvidenceRef>,
294}
295
296fn default_workgraph_machine_state() -> WorkGraphMachineState {
297    WorkGraphMachineState::default()
298}
299
300#[derive(Deserialize)]
301struct WorkItemWire {
302    id: WorkItemId,
303    realm_id: String,
304    namespace: WorkNamespace,
305    title: String,
306    #[serde(default)]
307    description: Option<String>,
308    status: WorkStatus,
309    priority: WorkPriority,
310    #[serde(default)]
311    labels: BTreeSet<String>,
312    #[serde(default)]
313    owner: Option<WorkOwner>,
314    #[serde(default)]
315    claim: Option<WorkClaim>,
316    #[serde(default)]
317    machine_state: Option<WorkGraphMachineState>,
318    revision: u64,
319    #[serde(default)]
320    due_at: Option<DateTime<Utc>>,
321    #[serde(default)]
322    not_before: Option<DateTime<Utc>>,
323    #[serde(default)]
324    snoozed_until: Option<DateTime<Utc>>,
325    created_at: DateTime<Utc>,
326    updated_at: DateTime<Utc>,
327    #[serde(default)]
328    terminal_at: Option<DateTime<Utc>>,
329    #[serde(default)]
330    external_refs: Vec<ExternalWorkRef>,
331    #[serde(default)]
332    evidence_refs: Vec<WorkEvidenceRef>,
333}
334
335impl<'de> Deserialize<'de> for WorkItem {
336    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
337    where
338        D: serde::Deserializer<'de>,
339    {
340        let mut wire = WorkItemWire::deserialize(deserializer)?;
341        let machine_state = wire
342            .machine_state
343            .take()
344            .unwrap_or_else(|| legacy_workgraph_machine_state(&wire));
345        Ok(Self {
346            id: wire.id,
347            realm_id: wire.realm_id,
348            namespace: wire.namespace,
349            title: wire.title,
350            description: wire.description,
351            status: wire.status,
352            priority: wire.priority,
353            labels: wire.labels,
354            owner: wire.owner,
355            claim: wire.claim,
356            machine_state,
357            revision: wire.revision,
358            due_at: wire.due_at,
359            not_before: wire.not_before,
360            snoozed_until: wire.snoozed_until,
361            created_at: wire.created_at,
362            updated_at: wire.updated_at,
363            terminal_at: wire.terminal_at,
364            external_refs: wire.external_refs,
365            evidence_refs: wire.evidence_refs,
366        })
367    }
368}
369
370#[cfg(feature = "schema")]
371impl schemars::JsonSchema for WorkItem {
372    fn schema_name() -> std::borrow::Cow<'static, str> {
373        "WorkItem".into()
374    }
375
376    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
377        schemars::json_schema!({
378            "type": "object",
379            "required": [
380                "id",
381                "realm_id",
382                "namespace",
383                "title",
384                "status",
385                "priority",
386                "machine_state",
387                "revision",
388                "created_at",
389                "updated_at"
390            ],
391            "properties": {
392                "id": { "type": "string" },
393                "realm_id": { "type": "string" },
394                "namespace": { "type": "string" },
395                "title": { "type": "string" },
396                "description": { "type": ["string", "null"] },
397                "status": {
398                    "type": "string",
399                    "enum": ["open", "in_progress", "blocked", "completed", "cancelled", "failed"]
400                },
401                "priority": {
402                    "type": "string",
403                    "enum": ["low", "medium", "high"]
404                },
405                "labels": {
406                    "type": "array",
407                    "uniqueItems": true,
408                    "items": { "type": "string" }
409                },
410                "owner": {
411                    "anyOf": [
412                        {
413                            "type": "object",
414                            "required": ["key"],
415                            "properties": {
416                                "key": {
417                                    "type": "object",
418                                    "required": ["kind", "id"],
419                                    "properties": {
420                                        "kind": {
421                                            "type": "string",
422                                            "enum": ["principal", "agent", "session", "mob", "label"]
423                                        },
424                                        "id": { "type": "string" }
425                                    }
426                                },
427                                "display_name": { "type": ["string", "null"] }
428                            }
429                        },
430                        { "type": "null" }
431                    ]
432                },
433                "claim": {
434                    "anyOf": [
435                        {
436                            "type": "object",
437                            "required": ["owner", "claimed_at"],
438                            "properties": {
439                                "owner": { "type": "object" },
440                                "claimed_at": { "type": "string", "format": "date-time" },
441                                "lease_expires_at": { "type": ["string", "null"], "format": "date-time" }
442                            }
443                        },
444                        { "type": "null" }
445                    ]
446                },
447                "machine_state": {
448                    "type": "object",
449                    "description": "Catalog-generated WorkGraphLifecycleMachine state projection."
450                },
451                "revision": { "type": "integer", "format": "uint64", "minimum": 0 },
452                "due_at": { "type": ["string", "null"], "format": "date-time" },
453                "not_before": { "type": ["string", "null"], "format": "date-time" },
454                "snoozed_until": { "type": ["string", "null"], "format": "date-time" },
455                "created_at": { "type": "string", "format": "date-time" },
456                "updated_at": { "type": "string", "format": "date-time" },
457                "terminal_at": { "type": ["string", "null"], "format": "date-time" },
458                "external_refs": {
459                    "type": "array",
460                    "items": {
461                        "type": "object",
462                        "required": ["kind", "id"],
463                        "properties": {
464                            "kind": { "type": "string" },
465                            "id": { "type": "string" },
466                            "url": { "type": ["string", "null"] }
467                        }
468                    }
469                },
470                "evidence_refs": {
471                    "type": "array",
472                    "items": {
473                        "type": "object",
474                        "required": ["kind", "id"],
475                        "properties": {
476                            "kind": { "type": "string" },
477                            "id": { "type": "string" },
478                            "label": { "type": ["string", "null"] },
479                            "summary": { "type": ["string", "null"] }
480                        }
481                    }
482                }
483            }
484        })
485    }
486}
487
488fn legacy_workgraph_machine_state(wire: &WorkItemWire) -> WorkGraphMachineState {
489    let mut machine_state = WorkGraphMachineState {
490        lifecycle_phase: work_lifecycle_state_from_status(wire.status),
491        revision: wire.revision,
492        due_at_utc_ms: wire.due_at.map(datetime_to_millis),
493        not_before_utc_ms: wire.not_before.map(datetime_to_millis),
494        snoozed_until_utc_ms: wire.snoozed_until.map(datetime_to_millis),
495        terminal_at_utc_ms: wire.terminal_at.map(datetime_to_millis),
496        evidence_count: wire.evidence_refs.len().try_into().unwrap_or(u64::MAX),
497        ..default_workgraph_machine_state()
498    };
499    if let Some(claim) = &wire.claim {
500        machine_state.claim_owner_key = Some(work_owner_key_to_machine(&claim.owner.key));
501        machine_state.claimed_at_utc_ms = Some(datetime_to_millis(claim.claimed_at));
502        machine_state.lease_expires_at_utc_ms = claim.lease_expires_at.map(datetime_to_millis);
503    }
504    machine_state
505}
506
507fn work_lifecycle_state_from_status(status: WorkStatus) -> wg_dsl::WorkLifecycleState {
508    match status {
509        WorkStatus::Open => wg_dsl::WorkLifecycleState::Open,
510        WorkStatus::InProgress => wg_dsl::WorkLifecycleState::InProgress,
511        WorkStatus::Blocked => wg_dsl::WorkLifecycleState::Blocked,
512        WorkStatus::Completed => wg_dsl::WorkLifecycleState::Completed,
513        WorkStatus::Cancelled => wg_dsl::WorkLifecycleState::Cancelled,
514        WorkStatus::Failed => wg_dsl::WorkLifecycleState::Failed,
515    }
516}
517
518fn work_owner_key_to_machine(owner: &WorkOwnerKey) -> wg_dsl::WorkOwnerKey {
519    let kind = match owner.kind {
520        WorkOwnerKind::Principal => wg_dsl::WorkOwnerKind::Principal,
521        WorkOwnerKind::Agent => wg_dsl::WorkOwnerKind::Agent,
522        WorkOwnerKind::Session => wg_dsl::WorkOwnerKind::Session,
523        WorkOwnerKind::Mob => wg_dsl::WorkOwnerKind::Mob,
524        WorkOwnerKind::Label => wg_dsl::WorkOwnerKind::Label,
525    };
526    wg_dsl::WorkOwnerKey {
527        kind,
528        id: owner.id.clone(),
529    }
530}
531
532fn datetime_to_millis(dt: DateTime<Utc>) -> u64 {
533    u64::try_from(dt.timestamp_millis()).unwrap_or(0)
534}
535
536#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
537#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
538pub struct WorkEdge {
539    pub realm_id: String,
540    pub namespace: WorkNamespace,
541    pub kind: WorkEdgeKind,
542    pub from_id: WorkItemId,
543    pub to_id: WorkItemId,
544    pub created_at: DateTime<Utc>,
545}
546
547#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
548#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
549#[serde(rename_all = "snake_case")]
550pub enum WorkGraphEventKind {
551    Created,
552    Updated,
553    Claimed,
554    Released,
555    Blocked,
556    Closed,
557    Linked,
558    EvidenceAdded,
559}
560
561#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
562#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
563pub struct WorkGraphEvent {
564    #[serde(default, skip_serializing_if = "Option::is_none")]
565    pub seq: Option<i64>,
566    pub realm_id: String,
567    pub namespace: WorkNamespace,
568    #[serde(default, skip_serializing_if = "Option::is_none")]
569    pub item_id: Option<WorkItemId>,
570    pub kind: WorkGraphEventKind,
571    pub at: DateTime<Utc>,
572    #[serde(default, skip_serializing_if = "Value::is_null")]
573    pub payload: Value,
574}
575
576impl WorkGraphEvent {
577    pub fn item(
578        realm_id: String,
579        namespace: WorkNamespace,
580        item_id: WorkItemId,
581        kind: WorkGraphEventKind,
582        at: DateTime<Utc>,
583        payload: Value,
584    ) -> Self {
585        Self {
586            seq: None,
587            realm_id,
588            namespace,
589            item_id: Some(item_id),
590            kind,
591            at,
592            payload,
593        }
594    }
595
596    pub fn graph(
597        realm_id: String,
598        namespace: WorkNamespace,
599        kind: WorkGraphEventKind,
600        at: DateTime<Utc>,
601        payload: Value,
602    ) -> Self {
603        Self {
604            seq: None,
605            realm_id,
606            namespace,
607            item_id: None,
608            kind,
609            at,
610            payload,
611        }
612    }
613}
614
615#[derive(Debug, Clone, Serialize, Deserialize)]
616#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
617pub struct CreateWorkItemRequest {
618    #[serde(default, skip_serializing_if = "Option::is_none")]
619    pub realm_id: Option<String>,
620    #[serde(default, skip_serializing_if = "Option::is_none")]
621    pub namespace: Option<WorkNamespace>,
622    pub title: String,
623    #[serde(default, skip_serializing_if = "Option::is_none")]
624    pub description: Option<String>,
625    #[serde(default)]
626    pub priority: WorkPriority,
627    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
628    pub labels: BTreeSet<String>,
629    #[serde(default, skip_serializing_if = "Option::is_none")]
630    pub due_at: Option<DateTime<Utc>>,
631    #[serde(default, skip_serializing_if = "Option::is_none")]
632    pub not_before: Option<DateTime<Utc>>,
633    #[serde(default, skip_serializing_if = "Option::is_none")]
634    pub snoozed_until: Option<DateTime<Utc>>,
635    #[serde(default, skip_serializing_if = "Vec::is_empty")]
636    pub external_refs: Vec<ExternalWorkRef>,
637    #[serde(default, skip_serializing_if = "Vec::is_empty")]
638    pub evidence_refs: Vec<WorkEvidenceRef>,
639    #[serde(default, skip_serializing_if = "Option::is_none")]
640    pub status: Option<WorkStatus>,
641}
642
643#[derive(Debug, Clone, Serialize, Deserialize)]
644#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
645pub struct UpdateWorkItemRequest {
646    pub id: WorkItemId,
647    #[serde(default, skip_serializing_if = "Option::is_none")]
648    pub realm_id: Option<String>,
649    #[serde(default, skip_serializing_if = "Option::is_none")]
650    pub namespace: Option<WorkNamespace>,
651    pub expected_revision: u64,
652    #[serde(default, skip_serializing_if = "Option::is_none")]
653    pub title: Option<String>,
654    #[serde(default, skip_serializing_if = "Option::is_none")]
655    pub description: Option<String>,
656    #[serde(default, skip_serializing_if = "Option::is_none")]
657    pub priority: Option<WorkPriority>,
658    #[serde(default, skip_serializing_if = "Option::is_none")]
659    pub labels: Option<BTreeSet<String>>,
660    #[serde(default, skip_serializing_if = "Option::is_none")]
661    pub due_at: Option<DateTime<Utc>>,
662    #[serde(default, skip_serializing_if = "Option::is_none")]
663    pub not_before: Option<DateTime<Utc>>,
664    #[serde(default, skip_serializing_if = "Option::is_none")]
665    pub snoozed_until: Option<DateTime<Utc>>,
666    #[serde(default, skip_serializing_if = "Vec::is_empty")]
667    pub external_refs: Vec<ExternalWorkRef>,
668}
669
670#[derive(Debug, Clone, Serialize, Deserialize)]
671#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
672pub struct ClaimWorkItemRequest {
673    pub id: WorkItemId,
674    #[serde(default, skip_serializing_if = "Option::is_none")]
675    pub realm_id: Option<String>,
676    #[serde(default, skip_serializing_if = "Option::is_none")]
677    pub namespace: Option<WorkNamespace>,
678    pub expected_revision: u64,
679    pub owner: WorkOwner,
680    #[serde(default, skip_serializing_if = "Option::is_none")]
681    pub lease_seconds: Option<u64>,
682    #[serde(default, skip_serializing_if = "Option::is_none")]
683    pub lease_expires_at: Option<DateTime<Utc>>,
684}
685
686#[derive(Debug, Clone, Serialize, Deserialize)]
687#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
688pub struct ReleaseWorkItemRequest {
689    pub id: WorkItemId,
690    #[serde(default, skip_serializing_if = "Option::is_none")]
691    pub realm_id: Option<String>,
692    #[serde(default, skip_serializing_if = "Option::is_none")]
693    pub namespace: Option<WorkNamespace>,
694    pub expected_revision: u64,
695}
696
697#[derive(Debug, Clone, Serialize, Deserialize)]
698#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
699pub struct CloseWorkItemRequest {
700    pub id: WorkItemId,
701    #[serde(default, skip_serializing_if = "Option::is_none")]
702    pub realm_id: Option<String>,
703    #[serde(default, skip_serializing_if = "Option::is_none")]
704    pub namespace: Option<WorkNamespace>,
705    pub expected_revision: u64,
706    #[serde(default = "default_terminal_status")]
707    pub status: WorkStatus,
708}
709
710fn default_terminal_status() -> WorkStatus {
711    WorkStatus::Completed
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize)]
715#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
716pub struct LinkWorkItemsRequest {
717    #[serde(default, skip_serializing_if = "Option::is_none")]
718    pub realm_id: Option<String>,
719    #[serde(default, skip_serializing_if = "Option::is_none")]
720    pub namespace: Option<WorkNamespace>,
721    pub kind: WorkEdgeKind,
722    pub from_id: WorkItemId,
723    pub to_id: WorkItemId,
724}
725
726#[derive(Debug, Clone, Serialize, Deserialize)]
727#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
728pub struct AddEvidenceRequest {
729    pub id: WorkItemId,
730    #[serde(default, skip_serializing_if = "Option::is_none")]
731    pub realm_id: Option<String>,
732    #[serde(default, skip_serializing_if = "Option::is_none")]
733    pub namespace: Option<WorkNamespace>,
734    pub expected_revision: u64,
735    pub evidence: WorkEvidenceRef,
736}
737
738#[derive(Debug, Clone, Serialize, Deserialize, Default)]
739#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
740pub struct WorkItemFilter {
741    #[serde(default, skip_serializing_if = "Option::is_none")]
742    pub realm_id: Option<String>,
743    #[serde(default, skip_serializing_if = "Option::is_none")]
744    pub namespace: Option<WorkNamespace>,
745    #[serde(default)]
746    pub all_namespaces: bool,
747    #[serde(default, skip_serializing_if = "Vec::is_empty")]
748    pub statuses: Vec<WorkStatus>,
749    #[serde(default, skip_serializing_if = "Vec::is_empty")]
750    pub labels: Vec<String>,
751    #[serde(default)]
752    pub include_terminal: bool,
753    #[serde(default, skip_serializing_if = "Option::is_none")]
754    pub limit: Option<usize>,
755}
756
757#[derive(Debug, Clone, Serialize, Deserialize, Default)]
758#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
759pub struct ReadyWorkFilter {
760    #[serde(default, skip_serializing_if = "Option::is_none")]
761    pub realm_id: Option<String>,
762    #[serde(default, skip_serializing_if = "Option::is_none")]
763    pub namespace: Option<WorkNamespace>,
764    #[serde(default, skip_serializing_if = "Vec::is_empty")]
765    pub labels: Vec<String>,
766    #[serde(default, skip_serializing_if = "Option::is_none")]
767    pub limit: Option<usize>,
768}
769
770#[derive(Debug, Clone, Serialize, Deserialize, Default)]
771#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
772pub struct WorkGraphSnapshotFilter {
773    #[serde(default, skip_serializing_if = "Option::is_none")]
774    pub realm_id: Option<String>,
775    #[serde(default, skip_serializing_if = "Option::is_none")]
776    pub namespace: Option<WorkNamespace>,
777    #[serde(default)]
778    pub all_namespaces: bool,
779    #[serde(default, skip_serializing_if = "Vec::is_empty")]
780    pub statuses: Vec<WorkStatus>,
781    #[serde(default, skip_serializing_if = "Vec::is_empty")]
782    pub labels: Vec<String>,
783    #[serde(default)]
784    pub include_terminal: bool,
785    #[serde(default, skip_serializing_if = "Option::is_none")]
786    pub limit: Option<usize>,
787}
788
789#[derive(Debug, Clone, Serialize, Deserialize)]
790#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
791pub struct WorkGraphSnapshot {
792    pub realm_id: String,
793    #[serde(default, skip_serializing_if = "Option::is_none")]
794    pub namespace: Option<WorkNamespace>,
795    pub all_namespaces: bool,
796    pub captured_at: DateTime<Utc>,
797    #[serde(default, skip_serializing_if = "Option::is_none")]
798    pub event_high_water_mark: Option<i64>,
799    pub items: Vec<WorkItem>,
800    pub edges: Vec<WorkEdge>,
801    pub ready_item_ids: Vec<WorkItemId>,
802}
803
804#[derive(Debug, Clone, Serialize, Deserialize)]
805#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
806pub struct WorkGraphItemsResponse {
807    pub items: Vec<WorkItem>,
808}
809
810#[derive(Debug, Clone, Serialize, Deserialize)]
811#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
812pub struct WorkGraphEventsResponse {
813    pub events: Vec<WorkGraphEvent>,
814}