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}