1use std::collections::BTreeSet;
2use std::fmt;
3use std::str::FromStr;
4
5use chrono::{DateTime, Utc};
6use meerkat_core::SessionId;
7use meerkat_core::auth::PrincipalId;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use uuid::Uuid;
11
12use crate::WorkGraphError;
13pub use crate::machines::work_attention_lifecycle::WorkAttentionLifecycleMachineState as WorkAttentionMachineState;
14pub use crate::machines::work_execution_lifecycle::WorkExecutionEvidenceKind;
15pub use crate::machines::work_execution_lifecycle::WorkExecutionLifecycleMachineState as WorkExecutionMachineState;
16use crate::machines::workgraph_lifecycle as wg_dsl;
17pub use crate::machines::workgraph_lifecycle::WorkGraphLifecycleMachineState as WorkGraphMachineState;
18
19#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
20#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21#[serde(transparent)]
22pub struct WorkItemId(String);
23
24impl WorkItemId {
25 pub fn new(value: impl Into<String>) -> Result<Self, WorkGraphError> {
26 validate_token("work item id", value.into()).map(Self)
27 }
28
29 pub fn generated() -> Self {
30 Self(format!("work_{}", Uuid::now_v7()))
31 }
32
33 pub fn as_str(&self) -> &str {
34 &self.0
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
39#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
40#[serde(transparent)]
41pub struct WorkAttentionBindingId(String);
42
43impl WorkAttentionBindingId {
44 pub fn new(value: impl Into<String>) -> Result<Self, WorkGraphError> {
45 validate_token("work attention binding id", value.into()).map(Self)
46 }
47
48 pub fn generated() -> Self {
49 Self(format!("attention_{}", Uuid::now_v7()))
50 }
51
52 pub fn as_str(&self) -> &str {
53 &self.0
54 }
55}
56
57impl fmt::Display for WorkAttentionBindingId {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 f.write_str(self.as_str())
60 }
61}
62
63impl FromStr for WorkAttentionBindingId {
64 type Err = WorkGraphError;
65
66 fn from_str(value: &str) -> Result<Self, Self::Err> {
67 Self::new(value)
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
77#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
78#[serde(transparent)]
79pub struct WorkExecutionBindingId(String);
80
81impl WorkExecutionBindingId {
82 pub fn new(value: impl Into<String>) -> Result<Self, WorkGraphError> {
83 validate_token("work execution binding id", value.into()).map(Self)
84 }
85
86 pub fn generated() -> Self {
87 Self(format!("execution_{}", Uuid::now_v7()))
88 }
89
90 pub fn as_str(&self) -> &str {
91 &self.0
92 }
93}
94
95impl fmt::Display for WorkExecutionBindingId {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 f.write_str(self.as_str())
98 }
99}
100
101impl FromStr for WorkExecutionBindingId {
102 type Err = WorkGraphError;
103
104 fn from_str(value: &str) -> Result<Self, Self::Err> {
105 Self::new(value)
106 }
107}
108
109impl fmt::Display for WorkItemId {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 f.write_str(self.as_str())
112 }
113}
114
115impl FromStr for WorkItemId {
116 type Err = WorkGraphError;
117
118 fn from_str(value: &str) -> Result<Self, Self::Err> {
119 Self::new(value)
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
124#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
125#[serde(transparent)]
126pub struct WorkNamespace(String);
127
128impl WorkNamespace {
129 pub fn new(value: impl Into<String>) -> Result<Self, WorkGraphError> {
130 validate_token("work namespace", value.into()).map(Self)
131 }
132
133 pub fn default_namespace() -> Self {
134 Self("default".to_string())
135 }
136
137 pub fn as_str(&self) -> &str {
138 &self.0
139 }
140}
141
142impl Default for WorkNamespace {
143 fn default() -> Self {
144 Self::default_namespace()
145 }
146}
147
148impl fmt::Display for WorkNamespace {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 f.write_str(self.as_str())
151 }
152}
153
154impl FromStr for WorkNamespace {
155 type Err = WorkGraphError;
156
157 fn from_str(value: &str) -> Result<Self, Self::Err> {
158 Self::new(value)
159 }
160}
161
162fn validate_token(name: &str, value: String) -> Result<String, WorkGraphError> {
163 let trimmed = value.trim();
164 if trimmed.is_empty() {
165 return Err(WorkGraphError::InvalidInput(format!(
166 "{name} must not be empty"
167 )));
168 }
169 if trimmed.chars().any(char::is_control) {
170 return Err(WorkGraphError::InvalidInput(format!(
171 "{name} must not contain control characters"
172 )));
173 }
174 Ok(trimmed.to_string())
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
178#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
179#[serde(rename_all = "snake_case")]
180pub enum WorkStatus {
181 #[default]
182 Open,
183 InProgress,
184 Blocked,
185 Completed,
186 Cancelled,
187 Failed,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
191#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
192#[serde(rename_all = "snake_case")]
193pub enum WorkPriority {
194 Low,
195 #[default]
196 Medium,
197 High,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
201#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
202#[serde(rename_all = "snake_case")]
203pub enum WorkEdgeKind {
204 Blocks,
205 Parent,
206 Related,
207 Supersedes,
208 DerivedFrom,
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
212#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
213#[serde(rename_all = "snake_case")]
214pub enum WorkOwnerKind {
215 Principal,
216 Agent,
217 Session,
218 Mob,
219 Label,
220}
221
222impl WorkOwnerKind {
223 pub fn as_str(self) -> &'static str {
224 match self {
225 Self::Principal => "principal",
226 Self::Agent => "agent",
227 Self::Session => "session",
228 Self::Mob => "mob",
229 Self::Label => "label",
230 }
231 }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
235#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
236pub struct WorkOwnerKey {
237 pub kind: WorkOwnerKind,
238 pub id: String,
239}
240
241impl WorkOwnerKey {
242 pub fn new(kind: WorkOwnerKind, id: impl Into<String>) -> Result<Self, WorkGraphError> {
243 Ok(Self {
244 kind,
245 id: validate_token("work owner id", id.into())?,
246 })
247 }
248
249 pub fn principal(id: impl Into<String>) -> Result<Self, WorkGraphError> {
250 Self::new(WorkOwnerKind::Principal, id)
251 }
252
253 pub fn agent(id: impl Into<String>) -> Result<Self, WorkGraphError> {
254 Self::new(WorkOwnerKind::Agent, id)
255 }
256
257 pub fn session(id: impl Into<String>) -> Result<Self, WorkGraphError> {
258 Self::new(WorkOwnerKind::Session, id)
259 }
260
261 pub fn mob(id: impl Into<String>) -> Result<Self, WorkGraphError> {
262 Self::new(WorkOwnerKind::Mob, id)
263 }
264
265 pub fn label(id: impl Into<String>) -> Result<Self, WorkGraphError> {
266 Self::new(WorkOwnerKind::Label, id)
267 }
268
269 pub fn canonical(&self) -> String {
270 format!("{}:{}", self.kind.as_str(), self.id)
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
275#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
276pub struct WorkOwner {
277 pub key: WorkOwnerKey,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub display_name: Option<String>,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
283#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
284#[serde(tag = "kind", rename_all = "snake_case")]
285pub enum WorkCompletionPolicy {
286 #[default]
287 SelfAttest,
288 HostConfirmed,
289 PrincipalConfirmed,
290 Supervisor {
291 owner_key: WorkOwnerKey,
292 },
293 ReviewerQuorum {
294 #[cfg_attr(feature = "schema", schemars(range(min = 1, max = 64)))]
295 threshold: u16,
296 },
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
300#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
301#[serde(tag = "kind", rename_all = "snake_case")]
302pub enum PublicGoalCompletionPolicy {
303 #[default]
304 SelfAttest,
305}
306
307impl From<PublicGoalCompletionPolicy> for WorkCompletionPolicy {
308 fn from(policy: PublicGoalCompletionPolicy) -> Self {
309 match policy {
310 PublicGoalCompletionPolicy::SelfAttest => Self::SelfAttest,
311 }
312 }
313}
314
315impl WorkCompletionPolicy {
316 pub fn requires_trusted_principal(&self) -> bool {
317 matches!(
318 self,
319 Self::PrincipalConfirmed | Self::Supervisor { .. } | Self::ReviewerQuorum { .. }
320 )
321 }
322
323 pub(crate) fn to_machine(&self) -> wg_dsl::WorkCompletionPolicy {
324 match self {
325 Self::SelfAttest => wg_dsl::WorkCompletionPolicy::SelfAttest,
326 Self::HostConfirmed => wg_dsl::WorkCompletionPolicy::HostConfirmed,
327 Self::PrincipalConfirmed => wg_dsl::WorkCompletionPolicy::PrincipalConfirmed,
328 Self::Supervisor { .. } => wg_dsl::WorkCompletionPolicy::Supervisor,
329 Self::ReviewerQuorum { .. } => wg_dsl::WorkCompletionPolicy::ReviewerQuorum,
330 }
331 }
332
333 pub(crate) fn supervisor_owner_key(&self) -> Option<wg_dsl::WorkOwnerKey> {
334 match self {
335 Self::Supervisor { owner_key } => Some(work_owner_key_to_machine(owner_key)),
336 _ => None,
337 }
338 }
339
340 pub(crate) fn reviewer_quorum_threshold(&self) -> Option<u64> {
341 match self {
342 Self::ReviewerQuorum { threshold } => Some(u64::from(*threshold)),
343 _ => None,
344 }
345 }
346
347 pub(crate) fn from_machine(
348 policy: wg_dsl::WorkCompletionPolicy,
349 supervisor_owner_key: Option<wg_dsl::WorkOwnerKey>,
350 reviewer_quorum_threshold: Option<u64>,
351 ) -> Self {
352 match policy {
353 wg_dsl::WorkCompletionPolicy::SelfAttest => Self::SelfAttest,
354 wg_dsl::WorkCompletionPolicy::HostConfirmed => Self::HostConfirmed,
355 wg_dsl::WorkCompletionPolicy::PrincipalConfirmed => Self::PrincipalConfirmed,
356 wg_dsl::WorkCompletionPolicy::Supervisor => Self::Supervisor {
357 owner_key: supervisor_owner_key
358 .map(work_owner_key_from_machine)
359 .unwrap_or_else(|| WorkOwnerKey {
360 kind: WorkOwnerKind::Principal,
361 id: "supervisor".to_string(),
362 }),
363 },
364 wg_dsl::WorkCompletionPolicy::ReviewerQuorum => Self::ReviewerQuorum {
365 threshold: reviewer_quorum_threshold
366 .and_then(|threshold| u16::try_from(threshold).ok())
367 .unwrap_or(1),
368 },
369 }
370 }
371}
372
373impl WorkOwner {
374 pub fn new(key: WorkOwnerKey) -> Self {
375 Self {
376 key,
377 display_name: None,
378 }
379 }
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
383#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
384pub struct WorkClaim {
385 pub owner: WorkOwner,
386 pub claimed_at: DateTime<Utc>,
387 #[serde(default, skip_serializing_if = "Option::is_none")]
388 pub lease_expires_at: Option<DateTime<Utc>>,
389}
390
391#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
392#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
393pub struct ExternalWorkRef {
394 pub kind: String,
395 pub id: String,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
397 pub url: Option<String>,
398}
399
400#[derive(
408 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
409)]
410#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
411#[serde(rename_all = "snake_case")]
412pub enum WorkEvidenceKind {
413 #[default]
416 SelfAttest,
417 HostConfirmation,
418 PrincipalConfirmation,
419 SupervisorConfirmation,
420 ReviewerConfirmation,
421}
422
423impl WorkEvidenceKind {
424 pub(crate) fn to_machine(self) -> wg_dsl::WorkEvidenceKind {
425 match self {
426 Self::SelfAttest => wg_dsl::WorkEvidenceKind::SelfAttest,
427 Self::HostConfirmation => wg_dsl::WorkEvidenceKind::HostConfirmation,
428 Self::PrincipalConfirmation => wg_dsl::WorkEvidenceKind::PrincipalConfirmation,
429 Self::SupervisorConfirmation => wg_dsl::WorkEvidenceKind::SupervisorConfirmation,
430 Self::ReviewerConfirmation => wg_dsl::WorkEvidenceKind::ReviewerConfirmation,
431 }
432 }
433
434 pub(crate) fn from_kind_str(kind: &str) -> Option<Self> {
442 match kind {
443 "host_confirmation" => Some(Self::HostConfirmation),
444 "principal_confirmation" => Some(Self::PrincipalConfirmation),
445 "supervisor_confirmation" => Some(Self::SupervisorConfirmation),
446 "reviewer_confirmation" => Some(Self::ReviewerConfirmation),
447 _ => None,
448 }
449 }
450
451 pub(crate) fn is_reserved_confirmation(self) -> bool {
455 !matches!(self, Self::SelfAttest)
456 }
457
458 pub(crate) fn to_confirmation_observation(self) -> wg_dsl::WorkConfirmationEvidenceObservation {
463 match self {
464 Self::SelfAttest => wg_dsl::WorkConfirmationEvidenceObservation::Other,
465 Self::HostConfirmation => wg_dsl::WorkConfirmationEvidenceObservation::HostConfirmation,
466 Self::PrincipalConfirmation => {
467 wg_dsl::WorkConfirmationEvidenceObservation::PrincipalConfirmation
468 }
469 Self::SupervisorConfirmation => {
470 wg_dsl::WorkConfirmationEvidenceObservation::SupervisorConfirmation
471 }
472 Self::ReviewerConfirmation => {
473 wg_dsl::WorkConfirmationEvidenceObservation::ReviewerConfirmation
474 }
475 }
476 }
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
480#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
481pub struct WorkEvidenceRef {
482 pub kind: String,
488 pub id: String,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub label: Option<String>,
491 #[serde(default, skip_serializing_if = "Option::is_none")]
492 pub summary: Option<String>,
493 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub confirmation_kind: Option<WorkEvidenceKind>,
498 #[serde(default, skip_serializing_if = "Option::is_none")]
502 pub confirming_owner_key: Option<WorkOwnerKey>,
503 #[serde(default, skip_serializing_if = "Option::is_none")]
507 #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
508 pub execution_binding_id: Option<WorkExecutionBindingId>,
509}
510
511impl WorkEvidenceRef {
512 pub(crate) fn confirmation_classification(&self) -> Option<WorkEvidenceKind> {
523 self.confirmation_kind
524 .filter(|kind| kind.is_reserved_confirmation())
525 .or_else(|| WorkEvidenceKind::from_kind_str(&self.kind))
526 }
527}
528
529#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
530#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
531pub struct WorkItemRef {
532 pub realm_id: String,
533 pub namespace: WorkNamespace,
534 pub item_id: WorkItemId,
535}
536
537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
541#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
542#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
543pub enum WorkExecutionAuthority {
544 TargetOwner,
545 Principal { principal_id: PrincipalId },
546}
547
548impl WorkExecutionAuthority {
549 pub fn principal(principal_id: PrincipalId) -> Self {
550 Self::Principal { principal_id }
551 }
552
553 fn validate(&self) -> Result<(), WorkGraphError> {
554 match self {
555 Self::TargetOwner => Ok(()),
556 Self::Principal { .. } => Ok(()),
557 }
558 }
559}
560
561#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
567#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
568#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
569pub enum WorkExecutionTarget {
570 MobFlow {
571 mob_id: String,
572 flow_id: String,
573 flow_config_digest: String,
574 run_id: String,
575 execution_authority: WorkExecutionAuthority,
576 activation_params: Value,
577 },
578}
579
580impl WorkExecutionTarget {
581 const MAX_ACTIVATION_PARAMS_BYTES: usize = 64 * 1024;
582
583 pub fn mob_flow(
584 mob_id: impl Into<String>,
585 flow_id: impl Into<String>,
586 flow_config_digest: impl Into<String>,
587 run_id: impl Into<String>,
588 execution_authority: WorkExecutionAuthority,
589 activation_params: Value,
590 ) -> Result<Self, WorkGraphError> {
591 let mob_id = validate_token("mob id", mob_id.into())?;
592 let flow_id = validate_token("flow id", flow_id.into())?;
593 let flow_config_digest =
594 validate_sha256_digest("Flow run config digest", flow_config_digest.into())?;
595 let run_id = validate_token("flow run id", run_id.into())?;
596 execution_authority.validate()?;
597 Self::validate_activation_params(&activation_params)?;
598 Ok(Self::MobFlow {
599 mob_id,
600 flow_id,
601 flow_config_digest,
602 run_id,
603 execution_authority,
604 activation_params,
605 })
606 }
607
608 pub fn run_id(&self) -> &str {
609 match self {
610 Self::MobFlow { run_id, .. } => run_id,
611 }
612 }
613
614 fn validate_activation_params(value: &Value) -> Result<(), WorkGraphError> {
615 let bytes = serde_json::to_vec(value).map_err(|error| {
616 WorkGraphError::InvalidInput(format!(
617 "Flow activation parameters are not serializable: {error}"
618 ))
619 })?;
620 if bytes.len() > Self::MAX_ACTIVATION_PARAMS_BYTES {
621 return Err(WorkGraphError::InvalidInput(format!(
622 "Flow activation parameters exceed the {} byte durable binding limit",
623 Self::MAX_ACTIVATION_PARAMS_BYTES
624 )));
625 }
626 Ok(())
627 }
628}
629
630#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
638#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
639#[serde(deny_unknown_fields)]
640pub struct WorkExecutionBinding {
641 pub binding_id: WorkExecutionBindingId,
642 pub work_ref: WorkItemRef,
643 pub target: WorkExecutionTarget,
644 pub idempotency_key: String,
645 pub correlation_id: String,
646 #[serde(default, skip_serializing_if = "Option::is_none")]
647 pub supersedes: Option<WorkExecutionBindingId>,
648 #[cfg_attr(feature = "schema", schemars(with = "WorkExecutionMachineStateSchema"))]
649 pub machine_state: WorkExecutionMachineState,
650 pub created_at: DateTime<Utc>,
651}
652
653#[cfg(feature = "schema")]
654#[derive(schemars::JsonSchema)]
655#[allow(dead_code)]
656struct WorkExecutionMachineStateSchema {
657 lifecycle_phase: String,
658 binding_id: String,
659 run_id: String,
660 revision: u64,
661 last_failure_detail: Option<String>,
662 evidence_kind: Option<String>,
663}
664
665impl WorkExecutionBinding {
666 const MAX_IDEMPOTENCY_KEY_BYTES: usize = 256;
667
668 pub fn evidence_id(&self) -> String {
669 format!("work_execution:{}", self.binding_id)
670 }
671
672 pub(crate) fn validate(&self) -> Result<(), WorkGraphError> {
673 validate_token(
674 "work execution idempotency key",
675 self.idempotency_key.clone(),
676 )?;
677 if self.idempotency_key.len() > Self::MAX_IDEMPOTENCY_KEY_BYTES {
678 return Err(WorkGraphError::InvalidInput(format!(
679 "work execution idempotency key exceeds {} bytes",
680 Self::MAX_IDEMPOTENCY_KEY_BYTES
681 )));
682 }
683 let correlation = Uuid::parse_str(&self.correlation_id).map_err(|_| {
684 WorkGraphError::InvalidInput(
685 "work execution correlation id must be a canonical UUID".to_string(),
686 )
687 })?;
688 if correlation.is_nil() || correlation.to_string() != self.correlation_id {
689 return Err(WorkGraphError::InvalidInput(
690 "work execution correlation id must be a canonical non-nil UUID".to_string(),
691 ));
692 }
693 match &self.target {
694 WorkExecutionTarget::MobFlow {
695 mob_id,
696 flow_id,
697 flow_config_digest,
698 run_id,
699 execution_authority,
700 activation_params,
701 } => {
702 validate_token("mob id", mob_id.clone())?;
703 validate_token("flow id", flow_id.clone())?;
704 validate_sha256_digest("Flow run config digest", flow_config_digest.clone())?;
705 validate_token("flow run id", run_id.clone())?;
706 execution_authority.validate()?;
707 WorkExecutionTarget::validate_activation_params(activation_params)?;
708 }
709 }
710 Ok(())
711 }
712
713 pub(crate) fn has_same_immutable_spec(&self, other: &Self) -> bool {
718 self.binding_id == other.binding_id
719 && self.work_ref == other.work_ref
720 && self.target == other.target
721 && self.idempotency_key == other.idempotency_key
722 && self.correlation_id == other.correlation_id
723 && self.supersedes == other.supersedes
724 && self.created_at == other.created_at
725 }
726}
727
728#[derive(Debug, Clone, PartialEq, Eq)]
729pub struct WorkExecutionEvidenceProjection {
730 pub kind: WorkExecutionEvidenceKind,
731 pub label: Option<String>,
732 pub summary: Option<String>,
733}
734
735fn validate_sha256_digest(name: &str, value: String) -> Result<String, WorkGraphError> {
736 let valid = value.len() == "sha256:".len() + 64
737 && value.strip_prefix("sha256:").is_some_and(|hex| {
738 hex.bytes()
739 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
740 });
741 if !valid {
742 return Err(WorkGraphError::InvalidInput(format!(
743 "{name} must be a canonical lowercase SHA-256 digest"
744 )));
745 }
746 Ok(value)
747}
748
749#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
750#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
751#[serde(tag = "kind", rename_all = "snake_case")]
752pub enum WorkAttentionTarget {
753 Session { session_id: SessionId },
754 LoweredOwner { owner_key: WorkOwnerKey },
755}
756
757impl WorkAttentionTarget {
758 pub fn owner_key(&self) -> Result<WorkOwnerKey, WorkGraphError> {
759 match self {
760 Self::Session { session_id } => WorkOwnerKey::session(session_id.to_string()),
761 Self::LoweredOwner { owner_key } => Ok(owner_key.clone()),
762 }
763 }
764
765 pub fn target_key(&self) -> String {
770 match self {
771 Self::Session { session_id } => format!("session:{session_id}"),
772 Self::LoweredOwner { owner_key } => {
773 format!("owner:{}:{}", owner_key.kind.as_str(), owner_key.id)
774 }
775 }
776 }
777}
778
779#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
780#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
781#[serde(tag = "kind", rename_all = "snake_case")]
782pub enum GoalAttentionTarget {
783 Session { session_id: SessionId },
784 Owner { owner_key: WorkOwnerKey },
785}
786
787impl GoalAttentionTarget {
788 pub fn to_attention_target(&self) -> WorkAttentionTarget {
789 match self {
790 Self::Session { session_id } => WorkAttentionTarget::Session {
791 session_id: session_id.clone(),
792 },
793 Self::Owner { owner_key } => WorkAttentionTarget::LoweredOwner {
794 owner_key: owner_key.clone(),
795 },
796 }
797 }
798}
799
800#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
801#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
802#[serde(rename_all = "snake_case")]
803pub enum WorkAttentionMode {
804 #[default]
805 Pursue,
806 Coordinate,
807 Review,
808 Falsify,
809 Judge,
810 Observe,
811}
812
813#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
814#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
815#[serde(tag = "state", rename_all = "snake_case")]
816pub enum WorkAttentionStatus {
818 #[default]
819 Active,
820 Paused {
821 #[serde(default, skip_serializing_if = "Option::is_none")]
822 until: Option<DateTime<Utc>>,
823 },
824 Superseded,
825 Stopped,
826}
827
828impl WorkAttentionStatus {
829 pub fn status_key(&self) -> &'static str {
833 match self {
834 Self::Active => "active",
835 Self::Paused { .. } => "paused",
836 Self::Superseded => "superseded",
837 Self::Stopped => "stopped",
838 }
839 }
840
841 pub fn is_terminal(&self) -> bool {
844 matches!(self, Self::Superseded | Self::Stopped)
845 }
846}
847
848#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
849#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
850#[serde(rename_all = "snake_case")]
851pub enum AttentionDelegatedAuthority {
852 #[default]
853 AddEvidence,
854 CloseOwnReviewItem,
855 RequestClosure,
856 CloseIfPolicyAllows,
857}
858
859#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
860#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
861pub struct AttentionProjectionPolicy {
862 #[serde(default = "default_projection_max_text_chars")]
863 pub max_text_chars: u32,
864 #[serde(default = "default_include_parent_context")]
865 pub include_parent_context: bool,
866}
867
868fn default_include_parent_context() -> bool {
869 true
870}
871
872impl Default for AttentionProjectionPolicy {
873 fn default() -> Self {
874 Self {
875 max_text_chars: default_projection_max_text_chars(),
876 include_parent_context: true,
877 }
878 }
879}
880
881fn default_projection_max_text_chars() -> u32 {
882 4096
883}
884
885#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
886#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
887pub struct WorkAttentionBinding {
888 pub binding_id: WorkAttentionBindingId,
889 pub work_ref: WorkItemRef,
890 pub target: WorkAttentionTarget,
891 pub mode: WorkAttentionMode,
892 pub status: WorkAttentionStatus,
893 #[serde(default = "default_work_attention_machine_state")]
894 #[cfg_attr(feature = "schema", schemars(with = "WorkAttentionMachineStateSchema"))]
895 pub machine_state: WorkAttentionMachineState,
896 pub delegated_authority: AttentionDelegatedAuthority,
897 #[serde(default)]
898 pub projection_policy: AttentionProjectionPolicy,
899 pub created_at: DateTime<Utc>,
900 pub updated_at: DateTime<Utc>,
901}
902
903#[cfg(feature = "schema")]
904#[derive(schemars::JsonSchema)]
905#[allow(dead_code)]
906struct WorkAttentionMachineStateSchema {
907 lifecycle_phase: String,
908 revision: u64,
909 paused_until_utc_ms: Option<u64>,
910 superseded_by_binding_key: Option<String>,
911 terminal_at_utc_ms: Option<u64>,
912}
913
914fn default_work_attention_machine_state() -> WorkAttentionMachineState {
915 WorkAttentionMachineState::default()
916}
917
918#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
919pub struct WorkItem {
920 pub id: WorkItemId,
921 pub realm_id: String,
922 pub namespace: WorkNamespace,
923 pub title: String,
924 #[serde(default, skip_serializing_if = "Option::is_none")]
925 pub description: Option<String>,
926 pub status: WorkStatus,
927 #[serde(default)]
928 pub completion_policy: WorkCompletionPolicy,
929 pub priority: WorkPriority,
930 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
931 pub labels: BTreeSet<String>,
932 #[serde(default, skip_serializing_if = "Option::is_none")]
933 pub owner: Option<WorkOwner>,
934 #[serde(default, skip_serializing_if = "Option::is_none")]
935 pub claim: Option<WorkClaim>,
936 pub machine_state: WorkGraphMachineState,
937 pub revision: u64,
938 #[serde(default, skip_serializing_if = "Option::is_none")]
939 pub due_at: Option<DateTime<Utc>>,
940 #[serde(default, skip_serializing_if = "Option::is_none")]
941 pub not_before: Option<DateTime<Utc>>,
942 #[serde(default, skip_serializing_if = "Option::is_none")]
943 pub snoozed_until: Option<DateTime<Utc>>,
944 pub created_at: DateTime<Utc>,
945 pub updated_at: DateTime<Utc>,
946 #[serde(default, skip_serializing_if = "Option::is_none")]
947 pub terminal_at: Option<DateTime<Utc>>,
948 #[serde(default, skip_serializing_if = "Vec::is_empty")]
949 pub external_refs: Vec<ExternalWorkRef>,
950 #[serde(default, skip_serializing_if = "Vec::is_empty")]
951 pub evidence_refs: Vec<WorkEvidenceRef>,
952}
953
954#[derive(Deserialize)]
955struct WorkItemWire {
956 id: WorkItemId,
957 realm_id: String,
958 namespace: WorkNamespace,
959 title: String,
960 #[serde(default)]
961 description: Option<String>,
962 status: WorkStatus,
963 #[serde(default)]
964 completion_policy: WorkCompletionPolicy,
965 priority: WorkPriority,
966 #[serde(default)]
967 labels: BTreeSet<String>,
968 #[serde(default)]
969 owner: Option<WorkOwner>,
970 #[serde(default)]
971 claim: Option<WorkClaim>,
972 #[serde(default)]
973 machine_state: Option<WorkGraphMachineState>,
974 revision: u64,
975 #[serde(default)]
976 due_at: Option<DateTime<Utc>>,
977 #[serde(default)]
978 not_before: Option<DateTime<Utc>>,
979 #[serde(default)]
980 snoozed_until: Option<DateTime<Utc>>,
981 created_at: DateTime<Utc>,
982 updated_at: DateTime<Utc>,
983 #[serde(default)]
984 terminal_at: Option<DateTime<Utc>>,
985 #[serde(default)]
986 external_refs: Vec<ExternalWorkRef>,
987 #[serde(default)]
988 evidence_refs: Vec<WorkEvidenceRef>,
989}
990
991impl<'de> Deserialize<'de> for WorkItem {
992 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
993 where
994 D: serde::Deserializer<'de>,
995 {
996 let mut wire = WorkItemWire::deserialize(deserializer)?;
997 let machine_state = wire.machine_state.take().ok_or_else(|| {
998 serde::de::Error::custom(
999 "WorkItem is missing `machine_state`: lifecycle/revision authority is machine-owned \
1000 and cannot be reconstructed from projected fields",
1001 )
1002 })?;
1003 Ok(Self {
1004 id: wire.id,
1005 realm_id: wire.realm_id,
1006 namespace: wire.namespace,
1007 title: wire.title,
1008 description: wire.description,
1009 status: wire.status,
1010 completion_policy: wire.completion_policy,
1011 priority: wire.priority,
1012 labels: wire.labels,
1013 owner: wire.owner,
1014 claim: wire.claim,
1015 machine_state,
1016 revision: wire.revision,
1017 due_at: wire.due_at,
1018 not_before: wire.not_before,
1019 snoozed_until: wire.snoozed_until,
1020 created_at: wire.created_at,
1021 updated_at: wire.updated_at,
1022 terminal_at: wire.terminal_at,
1023 external_refs: wire.external_refs,
1024 evidence_refs: wire.evidence_refs,
1025 })
1026 }
1027}
1028
1029#[cfg(feature = "schema")]
1030impl schemars::JsonSchema for WorkItem {
1031 fn schema_name() -> std::borrow::Cow<'static, str> {
1041 "WorkItem".into()
1042 }
1043
1044 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1045 schemars::json_schema!({
1046 "type": "object",
1047 "required": [
1048 "id",
1049 "realm_id",
1050 "namespace",
1051 "title",
1052 "status",
1053 "completion_policy",
1054 "priority",
1055 "machine_state",
1056 "revision",
1057 "created_at",
1058 "updated_at"
1059 ],
1060 "properties": {
1061 "id": { "type": "string" },
1062 "realm_id": { "type": "string" },
1063 "namespace": { "type": "string" },
1064 "title": { "type": "string" },
1065 "description": { "type": ["string", "null"] },
1066 "status": {
1067 "type": "string",
1068 "enum": ["open", "in_progress", "blocked", "completed", "cancelled", "failed"]
1069 },
1070 "completion_policy": {
1071 "oneOf": [
1072 {
1073 "type": "object",
1074 "required": ["kind"],
1075 "properties": { "kind": { "type": "string", "const": "self_attest" } }
1076 },
1077 {
1078 "type": "object",
1079 "required": ["kind"],
1080 "properties": { "kind": { "type": "string", "const": "host_confirmed" } }
1081 },
1082 {
1083 "type": "object",
1084 "required": ["kind"],
1085 "properties": { "kind": { "type": "string", "const": "principal_confirmed" } }
1086 },
1087 {
1088 "type": "object",
1089 "required": ["kind", "owner_key"],
1090 "properties": {
1091 "kind": { "type": "string", "const": "supervisor" },
1092 "owner_key": {
1093 "type": "object",
1094 "required": ["kind", "id"],
1095 "properties": {
1096 "kind": {
1097 "type": "string",
1098 "enum": ["principal", "agent", "session", "mob", "label"]
1099 },
1100 "id": { "type": "string" }
1101 }
1102 }
1103 }
1104 },
1105 {
1106 "type": "object",
1107 "required": ["kind", "threshold"],
1108 "properties": {
1109 "kind": { "type": "string", "const": "reviewer_quorum" },
1110 "threshold": { "type": "integer", "format": "uint16", "minimum": 1, "maximum": 64 }
1111 }
1112 }
1113 ]
1114 },
1115 "priority": {
1116 "type": "string",
1117 "enum": ["low", "medium", "high"]
1118 },
1119 "labels": {
1120 "type": "array",
1121 "uniqueItems": true,
1122 "items": { "type": "string" }
1123 },
1124 "owner": {
1125 "anyOf": [
1126 {
1127 "type": "object",
1128 "required": ["key"],
1129 "properties": {
1130 "key": {
1131 "type": "object",
1132 "required": ["kind", "id"],
1133 "properties": {
1134 "kind": {
1135 "type": "string",
1136 "enum": ["principal", "agent", "session", "mob", "label"]
1137 },
1138 "id": { "type": "string" }
1139 }
1140 },
1141 "display_name": { "type": ["string", "null"] }
1142 }
1143 },
1144 { "type": "null" }
1145 ]
1146 },
1147 "claim": {
1148 "anyOf": [
1149 {
1150 "type": "object",
1151 "required": ["owner", "claimed_at"],
1152 "properties": {
1153 "owner": {
1154 "type": "object",
1155 "required": ["key"],
1156 "properties": {
1157 "key": {
1158 "type": "object",
1159 "required": ["kind", "id"],
1160 "properties": {
1161 "kind": {
1162 "type": "string",
1163 "enum": ["principal", "agent", "session", "mob", "label"]
1164 },
1165 "id": { "type": "string" }
1166 }
1167 },
1168 "display_name": { "type": ["string", "null"] }
1169 }
1170 },
1171 "claimed_at": { "type": "string", "format": "date-time" },
1172 "lease_expires_at": { "type": ["string", "null"], "format": "date-time" }
1173 }
1174 },
1175 { "type": "null" }
1176 ]
1177 },
1178 "machine_state": {
1179 "type": "object",
1180 "description": "Catalog-generated WorkGraphLifecycleMachine state projection."
1181 },
1182 "revision": { "type": "integer", "format": "uint64", "minimum": 0 },
1183 "due_at": { "type": ["string", "null"], "format": "date-time" },
1184 "not_before": { "type": ["string", "null"], "format": "date-time" },
1185 "snoozed_until": { "type": ["string", "null"], "format": "date-time" },
1186 "created_at": { "type": "string", "format": "date-time" },
1187 "updated_at": { "type": "string", "format": "date-time" },
1188 "terminal_at": { "type": ["string", "null"], "format": "date-time" },
1189 "external_refs": {
1190 "type": "array",
1191 "items": {
1192 "type": "object",
1193 "required": ["kind", "id"],
1194 "properties": {
1195 "kind": { "type": "string" },
1196 "id": { "type": "string" },
1197 "url": { "type": ["string", "null"] }
1198 }
1199 }
1200 },
1201 "evidence_refs": {
1202 "type": "array",
1203 "items": {
1204 "type": "object",
1205 "required": ["kind", "id"],
1206 "properties": {
1207 "kind": { "type": "string" },
1208 "id": { "type": "string" },
1209 "label": { "type": ["string", "null"] },
1210 "summary": { "type": ["string", "null"] },
1211 "confirmation_kind": {
1212 "anyOf": [
1213 {
1214 "oneOf": [
1215 {
1216 "type": "string",
1217 "enum": [
1218 "host_confirmation",
1219 "principal_confirmation",
1220 "supervisor_confirmation",
1221 "reviewer_confirmation"
1222 ]
1223 },
1224 { "type": "string", "const": "self_attest" }
1225 ]
1226 },
1227 { "type": "null" }
1228 ]
1229 },
1230 "confirming_owner_key": {
1231 "anyOf": [
1232 {
1233 "type": "object",
1234 "required": ["kind", "id"],
1235 "properties": {
1236 "kind": {
1237 "type": "string",
1238 "enum": ["principal", "agent", "session", "mob", "label"]
1239 },
1240 "id": { "type": "string" }
1241 }
1242 },
1243 { "type": "null" }
1244 ]
1245 },
1246 "execution_binding_id": {
1247 "type": ["string", "null"]
1248 }
1249 }
1250 }
1251 }
1252 }
1253 })
1254 }
1255}
1256
1257pub(crate) fn work_lifecycle_state_from_status(status: WorkStatus) -> wg_dsl::WorkLifecycleState {
1258 match status {
1259 WorkStatus::Open => wg_dsl::WorkLifecycleState::Open,
1260 WorkStatus::InProgress => wg_dsl::WorkLifecycleState::InProgress,
1261 WorkStatus::Blocked => wg_dsl::WorkLifecycleState::Blocked,
1262 WorkStatus::Completed => wg_dsl::WorkLifecycleState::Completed,
1263 WorkStatus::Cancelled => wg_dsl::WorkLifecycleState::Cancelled,
1264 WorkStatus::Failed => wg_dsl::WorkLifecycleState::Failed,
1265 }
1266}
1267
1268pub(crate) fn work_owner_kind_to_machine(kind: WorkOwnerKind) -> wg_dsl::WorkOwnerKind {
1269 match kind {
1270 WorkOwnerKind::Principal => wg_dsl::WorkOwnerKind::Principal,
1271 WorkOwnerKind::Agent => wg_dsl::WorkOwnerKind::Agent,
1272 WorkOwnerKind::Session => wg_dsl::WorkOwnerKind::Session,
1273 WorkOwnerKind::Mob => wg_dsl::WorkOwnerKind::Mob,
1274 WorkOwnerKind::Label => wg_dsl::WorkOwnerKind::Label,
1275 }
1276}
1277
1278pub(crate) fn work_owner_key_to_machine(owner: &WorkOwnerKey) -> wg_dsl::WorkOwnerKey {
1279 wg_dsl::WorkOwnerKey {
1280 kind: work_owner_kind_to_machine(owner.kind),
1281 id: owner.id.clone(),
1282 }
1283}
1284
1285fn work_owner_key_from_machine(owner: wg_dsl::WorkOwnerKey) -> WorkOwnerKey {
1286 let kind = match owner.kind {
1287 wg_dsl::WorkOwnerKind::Principal => WorkOwnerKind::Principal,
1288 wg_dsl::WorkOwnerKind::Agent => WorkOwnerKind::Agent,
1289 wg_dsl::WorkOwnerKind::Session => WorkOwnerKind::Session,
1290 wg_dsl::WorkOwnerKind::Mob => WorkOwnerKind::Mob,
1291 wg_dsl::WorkOwnerKind::Label => WorkOwnerKind::Label,
1292 };
1293 WorkOwnerKey { kind, id: owner.id }
1294}
1295
1296#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1297#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1298pub struct WorkEdge {
1299 pub realm_id: String,
1300 pub namespace: WorkNamespace,
1301 pub kind: WorkEdgeKind,
1302 pub from_id: WorkItemId,
1303 pub to_id: WorkItemId,
1304 pub created_at: DateTime<Utc>,
1305}
1306
1307#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1308#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1309#[serde(rename_all = "snake_case")]
1310pub enum WorkGraphEventKind {
1311 Created,
1312 Updated,
1313 Claimed,
1314 Released,
1315 Blocked,
1316 Closed,
1317 Linked,
1318 EvidenceAdded,
1319 AttentionCreated,
1320 AttentionUpdated,
1321 ExecutionBound,
1322 ExecutionTransitioned,
1323}
1324
1325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1326#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1327pub struct WorkGraphEvent {
1328 #[serde(default, skip_serializing_if = "Option::is_none")]
1329 pub seq: Option<i64>,
1330 pub realm_id: String,
1331 pub namespace: WorkNamespace,
1332 #[serde(default, skip_serializing_if = "Option::is_none")]
1333 pub item_id: Option<WorkItemId>,
1334 pub kind: WorkGraphEventKind,
1335 pub at: DateTime<Utc>,
1336 #[serde(default, skip_serializing_if = "Value::is_null")]
1337 pub payload: Value,
1338}
1339
1340impl WorkGraphEvent {
1341 pub fn item(
1342 realm_id: String,
1343 namespace: WorkNamespace,
1344 item_id: WorkItemId,
1345 kind: WorkGraphEventKind,
1346 at: DateTime<Utc>,
1347 payload: Value,
1348 ) -> Self {
1349 Self {
1350 seq: None,
1351 realm_id,
1352 namespace,
1353 item_id: Some(item_id),
1354 kind,
1355 at,
1356 payload,
1357 }
1358 }
1359
1360 pub fn graph(
1361 realm_id: String,
1362 namespace: WorkNamespace,
1363 kind: WorkGraphEventKind,
1364 at: DateTime<Utc>,
1365 payload: Value,
1366 ) -> Self {
1367 Self {
1368 seq: None,
1369 realm_id,
1370 namespace,
1371 item_id: None,
1372 kind,
1373 at,
1374 payload,
1375 }
1376 }
1377}
1378
1379#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1380#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1381pub struct CreateWorkItemRequest {
1382 #[serde(default, skip_serializing_if = "Option::is_none")]
1383 pub realm_id: Option<String>,
1384 #[serde(default, skip_serializing_if = "Option::is_none")]
1385 pub namespace: Option<WorkNamespace>,
1386 pub title: String,
1387 #[serde(default, skip_serializing_if = "Option::is_none")]
1388 pub description: Option<String>,
1389 #[serde(default)]
1390 pub priority: WorkPriority,
1391 #[serde(default)]
1392 pub completion_policy: WorkCompletionPolicy,
1393 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1394 pub labels: BTreeSet<String>,
1395 #[serde(default, skip_serializing_if = "Option::is_none")]
1396 pub due_at: Option<DateTime<Utc>>,
1397 #[serde(default, skip_serializing_if = "Option::is_none")]
1398 pub not_before: Option<DateTime<Utc>>,
1399 #[serde(default, skip_serializing_if = "Option::is_none")]
1400 pub snoozed_until: Option<DateTime<Utc>>,
1401 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1402 pub external_refs: Vec<ExternalWorkRef>,
1403 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1404 pub evidence_refs: Vec<WorkEvidenceRef>,
1405 #[serde(default, skip_serializing_if = "Option::is_none")]
1406 pub status: Option<WorkStatus>,
1407}
1408
1409#[derive(Debug, Clone, Serialize, Deserialize)]
1410#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1411pub struct UpdateWorkItemRequest {
1412 pub id: WorkItemId,
1413 #[serde(default, skip_serializing_if = "Option::is_none")]
1414 pub realm_id: Option<String>,
1415 #[serde(default, skip_serializing_if = "Option::is_none")]
1416 pub namespace: Option<WorkNamespace>,
1417 pub expected_revision: u64,
1418 #[serde(default, skip_serializing_if = "Option::is_none")]
1419 pub title: Option<String>,
1420 #[serde(default, skip_serializing_if = "Option::is_none")]
1421 pub description: Option<String>,
1422 #[serde(default, skip_serializing_if = "Option::is_none")]
1423 pub priority: Option<WorkPriority>,
1424 #[serde(default, skip_serializing_if = "Option::is_none")]
1425 pub completion_policy: Option<WorkCompletionPolicy>,
1426 #[serde(default, skip_serializing_if = "Option::is_none")]
1427 pub labels: Option<BTreeSet<String>>,
1428 #[serde(default, skip_serializing_if = "Option::is_none")]
1429 pub due_at: Option<DateTime<Utc>>,
1430 #[serde(default, skip_serializing_if = "Option::is_none")]
1431 pub not_before: Option<DateTime<Utc>>,
1432 #[serde(default, skip_serializing_if = "Option::is_none")]
1433 pub snoozed_until: Option<DateTime<Utc>>,
1434 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1435 pub external_refs: Vec<ExternalWorkRef>,
1436}
1437
1438#[derive(Debug, Clone, Serialize, Deserialize)]
1439#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1440pub struct PolicyEscalateRequest {
1441 pub id: WorkItemId,
1442 #[serde(default, skip_serializing_if = "Option::is_none")]
1443 pub realm_id: Option<String>,
1444 #[serde(default, skip_serializing_if = "Option::is_none")]
1445 pub namespace: Option<WorkNamespace>,
1446 pub expected_revision: u64,
1447 pub authority_projection: AttentionContextProjection,
1448 pub completion_policy: WorkCompletionPolicy,
1449}
1450
1451#[derive(Debug, Clone, Serialize, Deserialize)]
1452#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1453pub struct ClaimWorkItemRequest {
1454 pub id: WorkItemId,
1455 #[serde(default, skip_serializing_if = "Option::is_none")]
1456 pub realm_id: Option<String>,
1457 #[serde(default, skip_serializing_if = "Option::is_none")]
1458 pub namespace: Option<WorkNamespace>,
1459 pub expected_revision: u64,
1460 pub owner: WorkOwner,
1461 #[serde(default, skip_serializing_if = "Option::is_none")]
1462 pub lease_seconds: Option<u64>,
1463 #[serde(default, skip_serializing_if = "Option::is_none")]
1464 pub lease_expires_at: Option<DateTime<Utc>>,
1465}
1466
1467#[derive(Debug, Clone, Serialize, Deserialize)]
1468#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1469pub struct ReleaseWorkItemRequest {
1470 pub id: WorkItemId,
1471 #[serde(default, skip_serializing_if = "Option::is_none")]
1472 pub realm_id: Option<String>,
1473 #[serde(default, skip_serializing_if = "Option::is_none")]
1474 pub namespace: Option<WorkNamespace>,
1475 pub expected_revision: u64,
1476}
1477
1478#[derive(Debug, Clone, Serialize, Deserialize)]
1479#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1480pub struct CloseWorkItemRequest {
1481 pub id: WorkItemId,
1482 #[serde(default, skip_serializing_if = "Option::is_none")]
1483 pub realm_id: Option<String>,
1484 #[serde(default, skip_serializing_if = "Option::is_none")]
1485 pub namespace: Option<WorkNamespace>,
1486 pub expected_revision: u64,
1487 #[serde(default = "default_terminal_status")]
1488 pub status: WorkStatus,
1489}
1490
1491fn default_terminal_status() -> WorkStatus {
1492 WorkStatus::Completed
1493}
1494
1495#[derive(Debug, Clone, Serialize, Deserialize)]
1496#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1497pub struct LinkWorkItemsRequest {
1498 #[serde(default, skip_serializing_if = "Option::is_none")]
1499 pub realm_id: Option<String>,
1500 #[serde(default, skip_serializing_if = "Option::is_none")]
1501 pub namespace: Option<WorkNamespace>,
1502 pub kind: WorkEdgeKind,
1503 pub from_id: WorkItemId,
1504 pub to_id: WorkItemId,
1505}
1506
1507#[derive(Debug, Clone, Serialize, Deserialize)]
1508#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1509pub struct AddEvidenceRequest {
1510 pub id: WorkItemId,
1511 #[serde(default, skip_serializing_if = "Option::is_none")]
1512 pub realm_id: Option<String>,
1513 #[serde(default, skip_serializing_if = "Option::is_none")]
1514 pub namespace: Option<WorkNamespace>,
1515 pub expected_revision: u64,
1516 pub evidence: WorkEvidenceRef,
1517}
1518
1519#[derive(Debug, Clone, Serialize, Deserialize)]
1520#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1521pub struct GoalCreateRequest {
1522 #[serde(default, skip_serializing_if = "Option::is_none")]
1523 pub realm_id: Option<String>,
1524 #[serde(default, skip_serializing_if = "Option::is_none")]
1525 pub namespace: Option<WorkNamespace>,
1526 pub title: String,
1527 #[serde(default, skip_serializing_if = "Option::is_none")]
1528 pub description: Option<String>,
1529 pub target: GoalAttentionTarget,
1530 #[serde(default)]
1531 pub mode: WorkAttentionMode,
1532 #[serde(default)]
1533 pub completion_policy: WorkCompletionPolicy,
1534 #[serde(default)]
1535 pub delegated_authority: AttentionDelegatedAuthority,
1536 #[serde(default)]
1537 pub projection_policy: AttentionProjectionPolicy,
1538}
1539
1540#[derive(Debug, Clone, Serialize, Deserialize)]
1541#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1542pub struct PublicGoalCreateRequest {
1543 #[serde(default, skip_serializing_if = "Option::is_none")]
1544 pub realm_id: Option<String>,
1545 #[serde(default, skip_serializing_if = "Option::is_none")]
1546 pub namespace: Option<WorkNamespace>,
1547 pub title: String,
1548 #[serde(default, skip_serializing_if = "Option::is_none")]
1549 pub description: Option<String>,
1550 pub target: GoalAttentionTarget,
1551 #[serde(default)]
1552 pub mode: WorkAttentionMode,
1553 #[serde(default)]
1554 pub completion_policy: PublicGoalCompletionPolicy,
1555 #[serde(default)]
1556 pub delegated_authority: AttentionDelegatedAuthority,
1557 #[serde(default)]
1558 pub projection_policy: AttentionProjectionPolicy,
1559}
1560
1561impl From<PublicGoalCreateRequest> for GoalCreateRequest {
1562 fn from(request: PublicGoalCreateRequest) -> Self {
1563 Self {
1564 realm_id: request.realm_id,
1565 namespace: request.namespace,
1566 title: request.title,
1567 description: request.description,
1568 target: request.target,
1569 mode: request.mode,
1570 completion_policy: request.completion_policy.into(),
1571 delegated_authority: request.delegated_authority,
1572 projection_policy: request.projection_policy,
1573 }
1574 }
1575}
1576
1577#[derive(Debug, Clone, Serialize, Deserialize)]
1578#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1579pub struct GoalCreateResult {
1580 pub item: WorkItem,
1581 pub attention: WorkAttentionBinding,
1582}
1583
1584#[derive(Debug, Clone, Serialize, Deserialize)]
1585#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1586pub struct GoalStatusRequest {
1587 pub binding_id: WorkAttentionBindingId,
1588 #[serde(default, skip_serializing_if = "Option::is_none")]
1589 pub realm_id: Option<String>,
1590 #[serde(default, skip_serializing_if = "Option::is_none")]
1591 pub namespace: Option<WorkNamespace>,
1592}
1593
1594#[derive(Debug, Clone, Serialize, Deserialize)]
1595#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1596pub struct GoalStatusResult {
1597 pub item: WorkItem,
1598 pub attention: WorkAttentionBinding,
1599}
1600
1601#[derive(Debug, Clone, Serialize, Deserialize)]
1602#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1603pub struct GoalConfirmRequest {
1604 pub binding_id: WorkAttentionBindingId,
1605 #[serde(default, skip_serializing_if = "Option::is_none")]
1606 pub realm_id: Option<String>,
1607 #[serde(default, skip_serializing_if = "Option::is_none")]
1608 pub namespace: Option<WorkNamespace>,
1609 pub expected_revision: u64,
1610 pub evidence: WorkEvidenceRef,
1611 #[serde(skip)]
1612 #[cfg_attr(feature = "schema", schemars(skip))]
1613 pub principal: Option<WorkOwnerKey>,
1614 #[serde(skip)]
1615 #[cfg_attr(feature = "schema", schemars(skip))]
1616 pub trusted_principal: Option<WorkOwnerKey>,
1617}
1618
1619impl GoalConfirmRequest {
1620 pub fn with_trusted_principal(mut self, principal: Option<WorkOwnerKey>) -> Self {
1622 if self.trusted_principal.is_none() {
1623 self.trusted_principal = principal;
1624 }
1625 self
1626 }
1627}
1628
1629#[derive(Debug, Clone, Serialize, Deserialize)]
1630#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1631pub struct GoalConfirmResult {
1632 pub item: WorkItem,
1633 pub attention: WorkAttentionBinding,
1634}
1635
1636#[derive(Debug, Clone, Serialize, Deserialize)]
1637#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1638pub struct GoalRequestCloseRequest {
1639 pub binding_id: WorkAttentionBindingId,
1640 #[serde(default, skip_serializing_if = "Option::is_none")]
1641 pub realm_id: Option<String>,
1642 #[serde(default, skip_serializing_if = "Option::is_none")]
1643 pub namespace: Option<WorkNamespace>,
1644 pub expected_revision: u64,
1645 #[serde(default)]
1646 pub status: GoalTerminalStatus,
1647}
1648
1649#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1650#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1651#[serde(rename_all = "snake_case")]
1652pub enum GoalTerminalStatus {
1653 #[default]
1654 Completed,
1655 Cancelled,
1656 Failed,
1657}
1658
1659impl From<GoalTerminalStatus> for WorkStatus {
1660 fn from(status: GoalTerminalStatus) -> Self {
1661 match status {
1662 GoalTerminalStatus::Completed => Self::Completed,
1663 GoalTerminalStatus::Cancelled => Self::Cancelled,
1664 GoalTerminalStatus::Failed => Self::Failed,
1665 }
1666 }
1667}
1668
1669#[derive(Debug, Clone, Serialize, Deserialize)]
1670#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1671pub struct PublicGoalRequestCloseRequest {
1672 pub binding_id: WorkAttentionBindingId,
1673 #[serde(default, skip_serializing_if = "Option::is_none")]
1674 pub realm_id: Option<String>,
1675 #[serde(default, skip_serializing_if = "Option::is_none")]
1676 pub namespace: Option<WorkNamespace>,
1677 pub expected_revision: u64,
1678 #[serde(default)]
1679 pub status: GoalTerminalStatus,
1680}
1681
1682impl From<PublicGoalRequestCloseRequest> for GoalRequestCloseRequest {
1683 fn from(request: PublicGoalRequestCloseRequest) -> Self {
1684 Self {
1685 binding_id: request.binding_id,
1686 realm_id: request.realm_id,
1687 namespace: request.namespace,
1688 expected_revision: request.expected_revision,
1689 status: request.status,
1690 }
1691 }
1692}
1693
1694#[derive(Debug, Clone, Serialize, Deserialize)]
1695#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1696pub struct GoalRequestCloseResult {
1697 pub item: WorkItem,
1698 pub attention: WorkAttentionBinding,
1699}
1700
1701#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1702#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1703pub struct AttentionListRequest {
1704 #[serde(default, skip_serializing_if = "Option::is_none")]
1705 pub realm_id: Option<String>,
1706 #[serde(default, skip_serializing_if = "Option::is_none")]
1707 pub namespace: Option<WorkNamespace>,
1708 #[serde(default, skip_serializing_if = "Option::is_none")]
1709 pub target: Option<WorkAttentionTarget>,
1710 #[serde(default, skip_serializing_if = "Option::is_none")]
1711 pub status: Option<WorkAttentionStatus>,
1712}
1713
1714#[derive(Debug, Clone, Serialize, Deserialize)]
1715#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1716pub struct AttentionListResult {
1717 pub attention: Vec<WorkAttentionBinding>,
1718}
1719
1720#[derive(Debug, Clone, Serialize, Deserialize)]
1721#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1722pub struct AttentionBindingRequest {
1723 pub binding_id: WorkAttentionBindingId,
1724 #[serde(default, skip_serializing_if = "Option::is_none")]
1725 pub realm_id: Option<String>,
1726 #[serde(default, skip_serializing_if = "Option::is_none")]
1727 pub namespace: Option<WorkNamespace>,
1728}
1729
1730#[derive(Debug, Clone, Serialize, Deserialize)]
1731#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1732pub struct AttentionPauseRequest {
1733 pub binding_id: WorkAttentionBindingId,
1734 #[serde(default, skip_serializing_if = "Option::is_none")]
1735 pub realm_id: Option<String>,
1736 #[serde(default, skip_serializing_if = "Option::is_none")]
1737 pub namespace: Option<WorkNamespace>,
1738 pub expected_revision: u64,
1739 #[serde(default, skip_serializing_if = "Option::is_none")]
1740 pub until: Option<DateTime<Utc>>,
1741}
1742
1743#[derive(Debug, Clone, Serialize, Deserialize)]
1744#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1745pub struct AttentionResumeRequest {
1746 pub binding_id: WorkAttentionBindingId,
1747 #[serde(default, skip_serializing_if = "Option::is_none")]
1748 pub realm_id: Option<String>,
1749 #[serde(default, skip_serializing_if = "Option::is_none")]
1750 pub namespace: Option<WorkNamespace>,
1751 pub expected_revision: u64,
1752}
1753
1754#[derive(Debug, Clone, Serialize, Deserialize)]
1755#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1756pub struct AttentionReassignRequest {
1757 pub binding_id: WorkAttentionBindingId,
1758 #[serde(default, skip_serializing_if = "Option::is_none")]
1759 pub realm_id: Option<String>,
1760 #[serde(default, skip_serializing_if = "Option::is_none")]
1761 pub namespace: Option<WorkNamespace>,
1762 pub expected_revision: u64,
1763 pub authority_projection: AttentionContextProjection,
1764 pub target: GoalAttentionTarget,
1765}
1766
1767#[derive(Debug, Clone, Serialize, Deserialize)]
1768#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1769pub struct AttentionBindingResult {
1770 pub attention: WorkAttentionBinding,
1771}
1772
1773#[derive(Debug, Clone, Serialize, Deserialize)]
1774#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1775pub struct AttentionReassignResult {
1776 pub previous: WorkAttentionBinding,
1777 pub attention: WorkAttentionBinding,
1778}
1779
1780#[derive(Debug, Clone, Serialize, Deserialize)]
1788pub struct BreakGlassAttentionReassignRequest {
1789 pub binding_id: WorkAttentionBindingId,
1790 #[serde(default, skip_serializing_if = "Option::is_none")]
1791 pub realm_id: Option<String>,
1792 #[serde(default, skip_serializing_if = "Option::is_none")]
1793 pub namespace: Option<WorkNamespace>,
1794 pub expected_revision: u64,
1795 pub target: GoalAttentionTarget,
1796 pub principal: String,
1799 pub reason: String,
1801}
1802
1803#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1807pub struct AttentionPruneRequest {
1808 #[serde(default, skip_serializing_if = "Option::is_none")]
1809 pub realm_id: Option<String>,
1810 #[serde(default, skip_serializing_if = "Option::is_none")]
1811 pub namespace: Option<WorkNamespace>,
1812 #[serde(default, skip_serializing_if = "Option::is_none")]
1815 pub updated_before: Option<DateTime<Utc>>,
1816}
1817
1818#[derive(Debug, Clone, Serialize, Deserialize)]
1819pub struct AttentionPruneResult {
1820 pub pruned: u64,
1821}
1822
1823#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1824#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1825#[serde(rename_all = "snake_case")]
1826pub enum AttentionContinueOutcome {
1827 Accepted,
1828 Deduplicated,
1829 Rejected,
1830}
1831
1832#[derive(Debug, Clone, Serialize, Deserialize)]
1833#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1834pub struct AttentionContinueResult {
1835 pub outcome: AttentionContinueOutcome,
1836 #[serde(default, skip_serializing_if = "Option::is_none")]
1837 pub input_id: Option<String>,
1838 #[serde(default, skip_serializing_if = "Option::is_none")]
1839 pub existing_id: Option<String>,
1840 #[serde(default, skip_serializing_if = "Option::is_none")]
1841 pub reason: Option<String>,
1842}
1843
1844#[derive(Debug, Clone, Serialize, Deserialize)]
1845#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1846pub struct AttentionProjectionRequest {
1847 pub binding_id: WorkAttentionBindingId,
1848 #[serde(default, skip_serializing_if = "Option::is_none")]
1849 pub realm_id: Option<String>,
1850 #[serde(default, skip_serializing_if = "Option::is_none")]
1851 pub namespace: Option<WorkNamespace>,
1852}
1853
1854#[derive(Debug, Clone, Serialize, Deserialize)]
1855#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1856pub struct AttentionProjectionResult {
1857 pub projection: AttentionContextProjection,
1858}
1859
1860#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1861#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1862pub struct AttentionContextProjection {
1863 pub binding_id: WorkAttentionBindingId,
1864 pub work_ref: WorkItemRef,
1865 pub mode: WorkAttentionMode,
1866 pub binding_revision: u64,
1867 pub item_revision: u64,
1868 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1869 pub parent_refs: Vec<WorkItemRef>,
1870 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1871 pub parent_context: Vec<AttentionProjectionParentContext>,
1872 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1873 pub evidence_refs: Vec<WorkEvidenceRef>,
1874 pub authority: ProjectedAttentionAuthority,
1875 pub text: AttentionProjectionText,
1876}
1877
1878#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1879#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1880pub struct AttentionProjectionParentContext {
1881 pub work_ref: WorkItemRef,
1882 pub status: WorkStatus,
1883 pub revision: u64,
1884}
1885
1886#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1887#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1888pub struct ProjectedAttentionAuthority {
1889 pub can_get: bool,
1890 pub can_add_evidence: bool,
1891 pub can_release: bool,
1892 pub can_update: bool,
1893 pub can_block: bool,
1894 pub can_create: bool,
1895 pub can_link: bool,
1896 pub can_link_parent: bool,
1897 pub can_link_related: bool,
1898 pub can_link_derived_from: bool,
1899 #[serde(default)]
1900 pub can_close_own_review_item: bool,
1901 pub can_close_if_policy_allows: bool,
1902}
1903
1904#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1905#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1906pub struct AttentionProjectionText {
1907 pub title: String,
1908 pub rendered: String,
1909 pub truncated: bool,
1910}
1911
1912#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1913#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1914pub struct WorkItemFilter {
1915 #[serde(default, skip_serializing_if = "Option::is_none")]
1916 pub realm_id: Option<String>,
1917 #[serde(default, skip_serializing_if = "Option::is_none")]
1918 pub namespace: Option<WorkNamespace>,
1919 #[serde(default)]
1920 pub all_namespaces: bool,
1921 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1922 pub statuses: Vec<WorkStatus>,
1923 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1924 pub labels: Vec<String>,
1925 #[serde(default)]
1926 pub include_terminal: bool,
1927 #[serde(default, skip_serializing_if = "Option::is_none")]
1928 pub limit: Option<usize>,
1929}
1930
1931#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1932#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1933pub struct WorkExecutionBindingFilter {
1934 #[serde(default, skip_serializing_if = "Option::is_none")]
1935 pub realm_id: Option<String>,
1936 #[serde(default, skip_serializing_if = "Option::is_none")]
1937 pub namespace: Option<WorkNamespace>,
1938 #[serde(default, skip_serializing_if = "Option::is_none")]
1939 pub item_id: Option<WorkItemId>,
1940 #[serde(default)]
1941 pub current_only: bool,
1942 #[serde(default, skip_serializing_if = "Option::is_none")]
1943 pub limit: Option<usize>,
1944}
1945
1946#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1947#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1948pub struct ReadyWorkFilter {
1949 #[serde(default, skip_serializing_if = "Option::is_none")]
1950 pub realm_id: Option<String>,
1951 #[serde(default, skip_serializing_if = "Option::is_none")]
1952 pub namespace: Option<WorkNamespace>,
1953 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1954 pub labels: Vec<String>,
1955 #[serde(default, skip_serializing_if = "Option::is_none")]
1956 pub limit: Option<usize>,
1957}
1958
1959#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1960#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1961pub struct WorkGraphSnapshotFilter {
1962 #[serde(default, skip_serializing_if = "Option::is_none")]
1963 pub realm_id: Option<String>,
1964 #[serde(default, skip_serializing_if = "Option::is_none")]
1965 pub namespace: Option<WorkNamespace>,
1966 #[serde(default)]
1967 pub all_namespaces: bool,
1968 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1969 pub statuses: Vec<WorkStatus>,
1970 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1971 pub labels: Vec<String>,
1972 #[serde(default)]
1973 pub include_terminal: bool,
1974 #[serde(default, skip_serializing_if = "Option::is_none")]
1975 pub limit: Option<usize>,
1976}
1977
1978#[derive(Debug, Clone, Serialize, Deserialize)]
1981#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1982pub struct WorkGraphIdParams {
1983 pub id: WorkItemId,
1984 #[serde(default, skip_serializing_if = "Option::is_none")]
1985 pub realm_id: Option<String>,
1986 #[serde(default, skip_serializing_if = "Option::is_none")]
1987 pub namespace: Option<WorkNamespace>,
1988}
1989
1990#[derive(Debug, Clone, Serialize, Deserialize)]
1991#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1992pub struct WorkGraphSnapshot {
1993 pub realm_id: String,
1994 #[serde(default, skip_serializing_if = "Option::is_none")]
1995 pub namespace: Option<WorkNamespace>,
1996 pub all_namespaces: bool,
1997 pub captured_at: DateTime<Utc>,
1998 #[serde(default, skip_serializing_if = "Option::is_none")]
1999 pub event_high_water_mark: Option<i64>,
2000 pub items: Vec<WorkItem>,
2001 pub edges: Vec<WorkEdge>,
2002 #[serde(default)]
2003 pub attention: Vec<WorkAttentionBinding>,
2004 pub ready_item_ids: Vec<WorkItemId>,
2005}
2006
2007#[derive(Debug, Clone, Serialize, Deserialize)]
2008#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2009pub struct WorkGraphItemsResponse {
2010 pub items: Vec<WorkItem>,
2011}
2012
2013#[derive(Debug, Clone, Serialize, Deserialize)]
2014#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2015pub struct WorkGraphEventsResponse {
2016 pub events: Vec<WorkGraphEvent>,
2017}
2018
2019#[cfg(test)]
2020#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
2021mod tests {
2022 use super::*;
2023 use crate::machine::WorkGraphMachine;
2024
2025 fn machine_item() -> WorkItem {
2026 WorkGraphMachine::create_item(
2027 CreateWorkItemRequest {
2028 title: "deserialize-authority".to_string(),
2029 ..Default::default()
2030 },
2031 "realm".to_string(),
2032 WorkNamespace::default(),
2033 Utc::now(),
2034 )
2035 .expect("machine create_item")
2036 .0
2037 }
2038
2039 #[test]
2040 fn work_item_round_trip_preserves_machine_state() {
2041 let item = machine_item();
2042 let json = serde_json::to_string(&item).expect("serialize work item");
2043 let decoded: WorkItem = serde_json::from_str(&json).expect("deserialize work item");
2044 assert_eq!(
2045 decoded, item,
2046 "round-trip must preserve the whole work item"
2047 );
2048 assert_eq!(
2049 decoded.machine_state, item.machine_state,
2050 "round-trip must preserve machine-owned lifecycle authority verbatim"
2051 );
2052 }
2053
2054 #[test]
2055 fn work_item_without_machine_state_fails_closed() {
2056 let item = machine_item();
2057 let mut value = serde_json::to_value(&item).expect("serialize work item to value");
2058 value
2059 .as_object_mut()
2060 .expect("work item json object")
2061 .remove("machine_state");
2062
2063 let result: Result<WorkItem, _> = serde_json::from_value(value);
2064 let err = result.expect_err(
2065 "deserializing a WorkItem without machine_state must fail closed, \
2066 never fabricate lifecycle/revision authority from projected fields",
2067 );
2068 assert!(
2069 err.to_string().contains("machine_state"),
2070 "fail-closed error must cite the missing machine_state authority, got: {err}"
2071 );
2072 }
2073}