1use std::collections::BTreeSet;
2use std::fmt;
3use std::str::FromStr;
4
5use chrono::{DateTime, Utc};
6use meerkat_core::SessionId;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use uuid::Uuid;
10
11use crate::WorkGraphError;
12pub use crate::machines::work_attention_lifecycle::WorkAttentionLifecycleMachineState as WorkAttentionMachineState;
13use crate::machines::workgraph_lifecycle as wg_dsl;
14pub use crate::machines::workgraph_lifecycle::WorkGraphLifecycleMachineState as WorkGraphMachineState;
15
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18#[serde(transparent)]
19pub struct WorkItemId(String);
20
21impl WorkItemId {
22 pub fn new(value: impl Into<String>) -> Result<Self, WorkGraphError> {
23 validate_token("work item id", value.into()).map(Self)
24 }
25
26 pub fn generated() -> Self {
27 Self(format!("work_{}", Uuid::now_v7()))
28 }
29
30 pub fn as_str(&self) -> &str {
31 &self.0
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
36#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
37#[serde(transparent)]
38pub struct WorkAttentionBindingId(String);
39
40impl WorkAttentionBindingId {
41 pub fn new(value: impl Into<String>) -> Result<Self, WorkGraphError> {
42 validate_token("work attention binding id", value.into()).map(Self)
43 }
44
45 pub fn generated() -> Self {
46 Self(format!("attention_{}", Uuid::now_v7()))
47 }
48
49 pub fn as_str(&self) -> &str {
50 &self.0
51 }
52}
53
54impl fmt::Display for WorkAttentionBindingId {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 f.write_str(self.as_str())
57 }
58}
59
60impl FromStr for WorkAttentionBindingId {
61 type Err = WorkGraphError;
62
63 fn from_str(value: &str) -> Result<Self, Self::Err> {
64 Self::new(value)
65 }
66}
67
68impl fmt::Display for WorkItemId {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.write_str(self.as_str())
71 }
72}
73
74impl FromStr for WorkItemId {
75 type Err = WorkGraphError;
76
77 fn from_str(value: &str) -> Result<Self, Self::Err> {
78 Self::new(value)
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
83#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
84#[serde(transparent)]
85pub struct WorkNamespace(String);
86
87impl WorkNamespace {
88 pub fn new(value: impl Into<String>) -> Result<Self, WorkGraphError> {
89 validate_token("work namespace", value.into()).map(Self)
90 }
91
92 pub fn default_namespace() -> Self {
93 Self("default".to_string())
94 }
95
96 pub fn as_str(&self) -> &str {
97 &self.0
98 }
99}
100
101impl Default for WorkNamespace {
102 fn default() -> Self {
103 Self::default_namespace()
104 }
105}
106
107impl fmt::Display for WorkNamespace {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 f.write_str(self.as_str())
110 }
111}
112
113impl FromStr for WorkNamespace {
114 type Err = WorkGraphError;
115
116 fn from_str(value: &str) -> Result<Self, Self::Err> {
117 Self::new(value)
118 }
119}
120
121fn validate_token(name: &str, value: String) -> Result<String, WorkGraphError> {
122 let trimmed = value.trim();
123 if trimmed.is_empty() {
124 return Err(WorkGraphError::InvalidInput(format!(
125 "{name} must not be empty"
126 )));
127 }
128 if trimmed.chars().any(char::is_control) {
129 return Err(WorkGraphError::InvalidInput(format!(
130 "{name} must not contain control characters"
131 )));
132 }
133 Ok(trimmed.to_string())
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
137#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
138#[serde(rename_all = "snake_case")]
139pub enum WorkStatus {
140 #[default]
141 Open,
142 InProgress,
143 Blocked,
144 Completed,
145 Cancelled,
146 Failed,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
150#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
151#[serde(rename_all = "snake_case")]
152pub enum WorkPriority {
153 Low,
154 #[default]
155 Medium,
156 High,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
160#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
161#[serde(rename_all = "snake_case")]
162pub enum WorkEdgeKind {
163 Blocks,
164 Parent,
165 Related,
166 Supersedes,
167 DerivedFrom,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
172#[serde(rename_all = "snake_case")]
173pub enum WorkOwnerKind {
174 Principal,
175 Agent,
176 Session,
177 Mob,
178 Label,
179}
180
181impl WorkOwnerKind {
182 pub fn as_str(self) -> &'static str {
183 match self {
184 Self::Principal => "principal",
185 Self::Agent => "agent",
186 Self::Session => "session",
187 Self::Mob => "mob",
188 Self::Label => "label",
189 }
190 }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
194#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
195pub struct WorkOwnerKey {
196 pub kind: WorkOwnerKind,
197 pub id: String,
198}
199
200impl WorkOwnerKey {
201 pub fn new(kind: WorkOwnerKind, id: impl Into<String>) -> Result<Self, WorkGraphError> {
202 Ok(Self {
203 kind,
204 id: validate_token("work owner id", id.into())?,
205 })
206 }
207
208 pub fn principal(id: impl Into<String>) -> Result<Self, WorkGraphError> {
209 Self::new(WorkOwnerKind::Principal, id)
210 }
211
212 pub fn agent(id: impl Into<String>) -> Result<Self, WorkGraphError> {
213 Self::new(WorkOwnerKind::Agent, id)
214 }
215
216 pub fn session(id: impl Into<String>) -> Result<Self, WorkGraphError> {
217 Self::new(WorkOwnerKind::Session, id)
218 }
219
220 pub fn mob(id: impl Into<String>) -> Result<Self, WorkGraphError> {
221 Self::new(WorkOwnerKind::Mob, id)
222 }
223
224 pub fn label(id: impl Into<String>) -> Result<Self, WorkGraphError> {
225 Self::new(WorkOwnerKind::Label, id)
226 }
227
228 pub fn canonical(&self) -> String {
229 format!("{}:{}", self.kind.as_str(), self.id)
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
235pub struct WorkOwner {
236 pub key: WorkOwnerKey,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub display_name: Option<String>,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
242#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
243#[serde(tag = "kind", rename_all = "snake_case")]
244pub enum WorkCompletionPolicy {
245 #[default]
246 SelfAttest,
247 HostConfirmed,
248 PrincipalConfirmed,
249 Supervisor {
250 owner_key: WorkOwnerKey,
251 },
252 ReviewerQuorum {
253 threshold: u16,
254 },
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
258#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
259#[serde(tag = "kind", rename_all = "snake_case")]
260pub enum PublicGoalCompletionPolicy {
261 #[default]
262 SelfAttest,
263}
264
265impl From<PublicGoalCompletionPolicy> for WorkCompletionPolicy {
266 fn from(policy: PublicGoalCompletionPolicy) -> Self {
267 match policy {
268 PublicGoalCompletionPolicy::SelfAttest => Self::SelfAttest,
269 }
270 }
271}
272
273impl WorkCompletionPolicy {
274 pub fn requires_trusted_principal(&self) -> bool {
275 matches!(
276 self,
277 Self::PrincipalConfirmed | Self::Supervisor { .. } | Self::ReviewerQuorum { .. }
278 )
279 }
280
281 pub(crate) fn to_machine(&self) -> wg_dsl::WorkCompletionPolicy {
282 match self {
283 Self::SelfAttest => wg_dsl::WorkCompletionPolicy::SelfAttest,
284 Self::HostConfirmed => wg_dsl::WorkCompletionPolicy::HostConfirmed,
285 Self::PrincipalConfirmed => wg_dsl::WorkCompletionPolicy::PrincipalConfirmed,
286 Self::Supervisor { .. } => wg_dsl::WorkCompletionPolicy::Supervisor,
287 Self::ReviewerQuorum { .. } => wg_dsl::WorkCompletionPolicy::ReviewerQuorum,
288 }
289 }
290
291 pub(crate) fn supervisor_owner_key(&self) -> Option<wg_dsl::WorkOwnerKey> {
292 match self {
293 Self::Supervisor { owner_key } => Some(work_owner_key_to_machine(owner_key)),
294 _ => None,
295 }
296 }
297
298 pub(crate) fn reviewer_quorum_threshold(&self) -> Option<u64> {
299 match self {
300 Self::ReviewerQuorum { threshold } => Some(u64::from(*threshold)),
301 _ => None,
302 }
303 }
304
305 pub(crate) fn from_machine(
306 policy: wg_dsl::WorkCompletionPolicy,
307 supervisor_owner_key: Option<wg_dsl::WorkOwnerKey>,
308 reviewer_quorum_threshold: Option<u64>,
309 ) -> Self {
310 match policy {
311 wg_dsl::WorkCompletionPolicy::SelfAttest => Self::SelfAttest,
312 wg_dsl::WorkCompletionPolicy::HostConfirmed => Self::HostConfirmed,
313 wg_dsl::WorkCompletionPolicy::PrincipalConfirmed => Self::PrincipalConfirmed,
314 wg_dsl::WorkCompletionPolicy::Supervisor => Self::Supervisor {
315 owner_key: supervisor_owner_key
316 .map(work_owner_key_from_machine)
317 .unwrap_or_else(|| WorkOwnerKey {
318 kind: WorkOwnerKind::Principal,
319 id: "supervisor".to_string(),
320 }),
321 },
322 wg_dsl::WorkCompletionPolicy::ReviewerQuorum => Self::ReviewerQuorum {
323 threshold: reviewer_quorum_threshold
324 .and_then(|threshold| u16::try_from(threshold).ok())
325 .unwrap_or(1),
326 },
327 }
328 }
329}
330
331impl WorkOwner {
332 pub fn new(key: WorkOwnerKey) -> Self {
333 Self {
334 key,
335 display_name: None,
336 }
337 }
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
342pub struct WorkClaim {
343 pub owner: WorkOwner,
344 pub claimed_at: DateTime<Utc>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub lease_expires_at: Option<DateTime<Utc>>,
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
350#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
351pub struct ExternalWorkRef {
352 pub kind: String,
353 pub id: String,
354 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub url: Option<String>,
356}
357
358#[derive(
366 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
367)]
368#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
369#[serde(rename_all = "snake_case")]
370pub enum WorkEvidenceKind {
371 #[default]
374 SelfAttest,
375 HostConfirmation,
376 PrincipalConfirmation,
377 SupervisorConfirmation,
378 ReviewerConfirmation,
379}
380
381impl WorkEvidenceKind {
382 pub(crate) fn to_machine(self) -> wg_dsl::WorkEvidenceKind {
383 match self {
384 Self::SelfAttest => wg_dsl::WorkEvidenceKind::SelfAttest,
385 Self::HostConfirmation => wg_dsl::WorkEvidenceKind::HostConfirmation,
386 Self::PrincipalConfirmation => wg_dsl::WorkEvidenceKind::PrincipalConfirmation,
387 Self::SupervisorConfirmation => wg_dsl::WorkEvidenceKind::SupervisorConfirmation,
388 Self::ReviewerConfirmation => wg_dsl::WorkEvidenceKind::ReviewerConfirmation,
389 }
390 }
391
392 pub(crate) fn from_kind_str(kind: &str) -> Option<Self> {
400 match kind {
401 "host_confirmation" => Some(Self::HostConfirmation),
402 "principal_confirmation" => Some(Self::PrincipalConfirmation),
403 "supervisor_confirmation" => Some(Self::SupervisorConfirmation),
404 "reviewer_confirmation" => Some(Self::ReviewerConfirmation),
405 _ => None,
406 }
407 }
408
409 pub(crate) fn is_reserved_confirmation(self) -> bool {
413 !matches!(self, Self::SelfAttest)
414 }
415
416 pub(crate) fn to_confirmation_observation(self) -> wg_dsl::WorkConfirmationEvidenceObservation {
421 match self {
422 Self::SelfAttest => wg_dsl::WorkConfirmationEvidenceObservation::Other,
423 Self::HostConfirmation => wg_dsl::WorkConfirmationEvidenceObservation::HostConfirmation,
424 Self::PrincipalConfirmation => {
425 wg_dsl::WorkConfirmationEvidenceObservation::PrincipalConfirmation
426 }
427 Self::SupervisorConfirmation => {
428 wg_dsl::WorkConfirmationEvidenceObservation::SupervisorConfirmation
429 }
430 Self::ReviewerConfirmation => {
431 wg_dsl::WorkConfirmationEvidenceObservation::ReviewerConfirmation
432 }
433 }
434 }
435}
436
437#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
438#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
439pub struct WorkEvidenceRef {
440 pub kind: String,
446 pub id: String,
447 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub label: Option<String>,
449 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub summary: Option<String>,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub confirmation_kind: Option<WorkEvidenceKind>,
456 #[serde(default, skip_serializing_if = "Option::is_none")]
460 pub confirming_owner_key: Option<WorkOwnerKey>,
461}
462
463impl WorkEvidenceRef {
464 pub(crate) fn confirmation_classification(&self) -> Option<WorkEvidenceKind> {
475 self.confirmation_kind
476 .filter(|kind| kind.is_reserved_confirmation())
477 .or_else(|| WorkEvidenceKind::from_kind_str(&self.kind))
478 }
479}
480
481#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
482#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
483pub struct WorkItemRef {
484 pub realm_id: String,
485 pub namespace: WorkNamespace,
486 pub item_id: WorkItemId,
487}
488
489#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
490#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
491#[serde(tag = "kind", rename_all = "snake_case")]
492pub enum WorkAttentionTarget {
493 Session { session_id: SessionId },
494 LoweredOwner { owner_key: WorkOwnerKey },
495}
496
497impl WorkAttentionTarget {
498 pub fn owner_key(&self) -> Result<WorkOwnerKey, WorkGraphError> {
499 match self {
500 Self::Session { session_id } => WorkOwnerKey::session(session_id.to_string()),
501 Self::LoweredOwner { owner_key } => Ok(owner_key.clone()),
502 }
503 }
504}
505
506#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
507#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
508#[serde(tag = "kind", rename_all = "snake_case")]
509pub enum GoalAttentionTarget {
510 Session { session_id: SessionId },
511}
512
513impl GoalAttentionTarget {
514 pub fn to_attention_target(&self) -> WorkAttentionTarget {
515 match self {
516 Self::Session { session_id } => WorkAttentionTarget::Session {
517 session_id: session_id.clone(),
518 },
519 }
520 }
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
524#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
525#[serde(rename_all = "snake_case")]
526pub enum WorkAttentionMode {
527 #[default]
528 Pursue,
529 Coordinate,
530 Review,
531 Falsify,
532 Judge,
533 Observe,
534}
535
536#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
537#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
538#[serde(tag = "state", rename_all = "snake_case")]
539pub enum WorkAttentionStatus {
540 #[default]
541 Active,
542 Paused {
543 #[serde(default, skip_serializing_if = "Option::is_none")]
544 until: Option<DateTime<Utc>>,
545 },
546 Superseded,
547 Stopped,
548}
549
550#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
551#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
552#[serde(rename_all = "snake_case")]
553pub enum AttentionDelegatedAuthority {
554 #[default]
555 AddEvidence,
556 CloseOwnReviewItem,
557 RequestClosure,
558 CloseIfPolicyAllows,
559}
560
561#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
562#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
563pub struct AttentionProjectionPolicy {
564 #[serde(default = "default_projection_max_text_chars")]
565 pub max_text_chars: u32,
566 #[serde(default = "default_include_parent_context")]
567 pub include_parent_context: bool,
568}
569
570fn default_include_parent_context() -> bool {
571 true
572}
573
574impl Default for AttentionProjectionPolicy {
575 fn default() -> Self {
576 Self {
577 max_text_chars: default_projection_max_text_chars(),
578 include_parent_context: true,
579 }
580 }
581}
582
583fn default_projection_max_text_chars() -> u32 {
584 4096
585}
586
587#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
588#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
589pub struct WorkAttentionBinding {
590 pub binding_id: WorkAttentionBindingId,
591 pub work_ref: WorkItemRef,
592 pub target: WorkAttentionTarget,
593 pub mode: WorkAttentionMode,
594 pub status: WorkAttentionStatus,
595 #[serde(default = "default_work_attention_machine_state")]
596 #[cfg_attr(feature = "schema", schemars(with = "WorkAttentionMachineStateSchema"))]
597 pub machine_state: WorkAttentionMachineState,
598 pub delegated_authority: AttentionDelegatedAuthority,
599 #[serde(default)]
600 pub projection_policy: AttentionProjectionPolicy,
601 pub created_at: DateTime<Utc>,
602 pub updated_at: DateTime<Utc>,
603}
604
605#[cfg(feature = "schema")]
606#[derive(schemars::JsonSchema)]
607#[allow(dead_code)]
608struct WorkAttentionMachineStateSchema {
609 lifecycle_phase: String,
610 revision: u64,
611 paused_until_utc_ms: Option<u64>,
612 superseded_by_binding_key: Option<String>,
613 terminal_at_utc_ms: Option<u64>,
614}
615
616fn default_work_attention_machine_state() -> WorkAttentionMachineState {
617 WorkAttentionMachineState::default()
618}
619
620#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
621pub struct WorkItem {
622 pub id: WorkItemId,
623 pub realm_id: String,
624 pub namespace: WorkNamespace,
625 pub title: String,
626 #[serde(default, skip_serializing_if = "Option::is_none")]
627 pub description: Option<String>,
628 pub status: WorkStatus,
629 #[serde(default)]
630 pub completion_policy: WorkCompletionPolicy,
631 pub priority: WorkPriority,
632 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
633 pub labels: BTreeSet<String>,
634 #[serde(default, skip_serializing_if = "Option::is_none")]
635 pub owner: Option<WorkOwner>,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
637 pub claim: Option<WorkClaim>,
638 pub machine_state: WorkGraphMachineState,
639 pub revision: u64,
640 #[serde(default, skip_serializing_if = "Option::is_none")]
641 pub due_at: Option<DateTime<Utc>>,
642 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub not_before: Option<DateTime<Utc>>,
644 #[serde(default, skip_serializing_if = "Option::is_none")]
645 pub snoozed_until: Option<DateTime<Utc>>,
646 pub created_at: DateTime<Utc>,
647 pub updated_at: DateTime<Utc>,
648 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub terminal_at: Option<DateTime<Utc>>,
650 #[serde(default, skip_serializing_if = "Vec::is_empty")]
651 pub external_refs: Vec<ExternalWorkRef>,
652 #[serde(default, skip_serializing_if = "Vec::is_empty")]
653 pub evidence_refs: Vec<WorkEvidenceRef>,
654}
655
656#[derive(Deserialize)]
657struct WorkItemWire {
658 id: WorkItemId,
659 realm_id: String,
660 namespace: WorkNamespace,
661 title: String,
662 #[serde(default)]
663 description: Option<String>,
664 status: WorkStatus,
665 #[serde(default)]
666 completion_policy: WorkCompletionPolicy,
667 priority: WorkPriority,
668 #[serde(default)]
669 labels: BTreeSet<String>,
670 #[serde(default)]
671 owner: Option<WorkOwner>,
672 #[serde(default)]
673 claim: Option<WorkClaim>,
674 #[serde(default)]
675 machine_state: Option<WorkGraphMachineState>,
676 revision: u64,
677 #[serde(default)]
678 due_at: Option<DateTime<Utc>>,
679 #[serde(default)]
680 not_before: Option<DateTime<Utc>>,
681 #[serde(default)]
682 snoozed_until: Option<DateTime<Utc>>,
683 created_at: DateTime<Utc>,
684 updated_at: DateTime<Utc>,
685 #[serde(default)]
686 terminal_at: Option<DateTime<Utc>>,
687 #[serde(default)]
688 external_refs: Vec<ExternalWorkRef>,
689 #[serde(default)]
690 evidence_refs: Vec<WorkEvidenceRef>,
691}
692
693impl<'de> Deserialize<'de> for WorkItem {
694 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
695 where
696 D: serde::Deserializer<'de>,
697 {
698 let mut wire = WorkItemWire::deserialize(deserializer)?;
699 let machine_state = wire.machine_state.take().ok_or_else(|| {
700 serde::de::Error::custom(
701 "WorkItem is missing `machine_state`: lifecycle/revision authority is machine-owned \
702 and cannot be reconstructed from projected fields",
703 )
704 })?;
705 Ok(Self {
706 id: wire.id,
707 realm_id: wire.realm_id,
708 namespace: wire.namespace,
709 title: wire.title,
710 description: wire.description,
711 status: wire.status,
712 completion_policy: wire.completion_policy,
713 priority: wire.priority,
714 labels: wire.labels,
715 owner: wire.owner,
716 claim: wire.claim,
717 machine_state,
718 revision: wire.revision,
719 due_at: wire.due_at,
720 not_before: wire.not_before,
721 snoozed_until: wire.snoozed_until,
722 created_at: wire.created_at,
723 updated_at: wire.updated_at,
724 terminal_at: wire.terminal_at,
725 external_refs: wire.external_refs,
726 evidence_refs: wire.evidence_refs,
727 })
728 }
729}
730
731#[cfg(feature = "schema")]
732impl schemars::JsonSchema for WorkItem {
733 fn schema_name() -> std::borrow::Cow<'static, str> {
743 "WorkItem".into()
744 }
745
746 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
747 schemars::json_schema!({
748 "type": "object",
749 "required": [
750 "id",
751 "realm_id",
752 "namespace",
753 "title",
754 "status",
755 "completion_policy",
756 "priority",
757 "machine_state",
758 "revision",
759 "created_at",
760 "updated_at"
761 ],
762 "properties": {
763 "id": { "type": "string" },
764 "realm_id": { "type": "string" },
765 "namespace": { "type": "string" },
766 "title": { "type": "string" },
767 "description": { "type": ["string", "null"] },
768 "status": {
769 "type": "string",
770 "enum": ["open", "in_progress", "blocked", "completed", "cancelled", "failed"]
771 },
772 "completion_policy": {
773 "oneOf": [
774 {
775 "type": "object",
776 "required": ["kind"],
777 "properties": { "kind": { "type": "string", "const": "self_attest" } }
778 },
779 {
780 "type": "object",
781 "required": ["kind"],
782 "properties": { "kind": { "type": "string", "const": "host_confirmed" } }
783 },
784 {
785 "type": "object",
786 "required": ["kind"],
787 "properties": { "kind": { "type": "string", "const": "principal_confirmed" } }
788 },
789 {
790 "type": "object",
791 "required": ["kind", "owner_key"],
792 "properties": {
793 "kind": { "type": "string", "const": "supervisor" },
794 "owner_key": {
795 "type": "object",
796 "required": ["kind", "id"],
797 "properties": {
798 "kind": {
799 "type": "string",
800 "enum": ["principal", "agent", "session", "mob", "label"]
801 },
802 "id": { "type": "string" }
803 }
804 }
805 }
806 },
807 {
808 "type": "object",
809 "required": ["kind", "threshold"],
810 "properties": {
811 "kind": { "type": "string", "const": "reviewer_quorum" },
812 "threshold": { "type": "integer", "format": "uint16", "minimum": 0, "maximum": 65535 }
813 }
814 }
815 ]
816 },
817 "priority": {
818 "type": "string",
819 "enum": ["low", "medium", "high"]
820 },
821 "labels": {
822 "type": "array",
823 "uniqueItems": true,
824 "items": { "type": "string" }
825 },
826 "owner": {
827 "anyOf": [
828 {
829 "type": "object",
830 "required": ["key"],
831 "properties": {
832 "key": {
833 "type": "object",
834 "required": ["kind", "id"],
835 "properties": {
836 "kind": {
837 "type": "string",
838 "enum": ["principal", "agent", "session", "mob", "label"]
839 },
840 "id": { "type": "string" }
841 }
842 },
843 "display_name": { "type": ["string", "null"] }
844 }
845 },
846 { "type": "null" }
847 ]
848 },
849 "claim": {
850 "anyOf": [
851 {
852 "type": "object",
853 "required": ["owner", "claimed_at"],
854 "properties": {
855 "owner": {
856 "type": "object",
857 "required": ["key"],
858 "properties": {
859 "key": {
860 "type": "object",
861 "required": ["kind", "id"],
862 "properties": {
863 "kind": {
864 "type": "string",
865 "enum": ["principal", "agent", "session", "mob", "label"]
866 },
867 "id": { "type": "string" }
868 }
869 },
870 "display_name": { "type": ["string", "null"] }
871 }
872 },
873 "claimed_at": { "type": "string", "format": "date-time" },
874 "lease_expires_at": { "type": ["string", "null"], "format": "date-time" }
875 }
876 },
877 { "type": "null" }
878 ]
879 },
880 "machine_state": {
881 "type": "object",
882 "description": "Catalog-generated WorkGraphLifecycleMachine state projection."
883 },
884 "revision": { "type": "integer", "format": "uint64", "minimum": 0 },
885 "due_at": { "type": ["string", "null"], "format": "date-time" },
886 "not_before": { "type": ["string", "null"], "format": "date-time" },
887 "snoozed_until": { "type": ["string", "null"], "format": "date-time" },
888 "created_at": { "type": "string", "format": "date-time" },
889 "updated_at": { "type": "string", "format": "date-time" },
890 "terminal_at": { "type": ["string", "null"], "format": "date-time" },
891 "external_refs": {
892 "type": "array",
893 "items": {
894 "type": "object",
895 "required": ["kind", "id"],
896 "properties": {
897 "kind": { "type": "string" },
898 "id": { "type": "string" },
899 "url": { "type": ["string", "null"] }
900 }
901 }
902 },
903 "evidence_refs": {
904 "type": "array",
905 "items": {
906 "type": "object",
907 "required": ["kind", "id"],
908 "properties": {
909 "kind": { "type": "string" },
910 "id": { "type": "string" },
911 "label": { "type": ["string", "null"] },
912 "summary": { "type": ["string", "null"] },
913 "confirmation_kind": {
914 "anyOf": [
915 {
916 "oneOf": [
917 {
918 "type": "string",
919 "enum": [
920 "host_confirmation",
921 "principal_confirmation",
922 "supervisor_confirmation",
923 "reviewer_confirmation"
924 ]
925 },
926 { "type": "string", "const": "self_attest" }
927 ]
928 },
929 { "type": "null" }
930 ]
931 },
932 "confirming_owner_key": {
933 "anyOf": [
934 {
935 "type": "object",
936 "required": ["kind", "id"],
937 "properties": {
938 "kind": {
939 "type": "string",
940 "enum": ["principal", "agent", "session", "mob", "label"]
941 },
942 "id": { "type": "string" }
943 }
944 },
945 { "type": "null" }
946 ]
947 }
948 }
949 }
950 }
951 }
952 })
953 }
954}
955
956pub(crate) fn work_lifecycle_state_from_status(status: WorkStatus) -> wg_dsl::WorkLifecycleState {
957 match status {
958 WorkStatus::Open => wg_dsl::WorkLifecycleState::Open,
959 WorkStatus::InProgress => wg_dsl::WorkLifecycleState::InProgress,
960 WorkStatus::Blocked => wg_dsl::WorkLifecycleState::Blocked,
961 WorkStatus::Completed => wg_dsl::WorkLifecycleState::Completed,
962 WorkStatus::Cancelled => wg_dsl::WorkLifecycleState::Cancelled,
963 WorkStatus::Failed => wg_dsl::WorkLifecycleState::Failed,
964 }
965}
966
967pub(crate) fn work_owner_kind_to_machine(kind: WorkOwnerKind) -> wg_dsl::WorkOwnerKind {
968 match kind {
969 WorkOwnerKind::Principal => wg_dsl::WorkOwnerKind::Principal,
970 WorkOwnerKind::Agent => wg_dsl::WorkOwnerKind::Agent,
971 WorkOwnerKind::Session => wg_dsl::WorkOwnerKind::Session,
972 WorkOwnerKind::Mob => wg_dsl::WorkOwnerKind::Mob,
973 WorkOwnerKind::Label => wg_dsl::WorkOwnerKind::Label,
974 }
975}
976
977pub(crate) fn work_owner_key_to_machine(owner: &WorkOwnerKey) -> wg_dsl::WorkOwnerKey {
978 wg_dsl::WorkOwnerKey {
979 kind: work_owner_kind_to_machine(owner.kind),
980 id: owner.id.clone(),
981 }
982}
983
984fn work_owner_key_from_machine(owner: wg_dsl::WorkOwnerKey) -> WorkOwnerKey {
985 let kind = match owner.kind {
986 wg_dsl::WorkOwnerKind::Principal => WorkOwnerKind::Principal,
987 wg_dsl::WorkOwnerKind::Agent => WorkOwnerKind::Agent,
988 wg_dsl::WorkOwnerKind::Session => WorkOwnerKind::Session,
989 wg_dsl::WorkOwnerKind::Mob => WorkOwnerKind::Mob,
990 wg_dsl::WorkOwnerKind::Label => WorkOwnerKind::Label,
991 };
992 WorkOwnerKey { kind, id: owner.id }
993}
994
995#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
996#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
997pub struct WorkEdge {
998 pub realm_id: String,
999 pub namespace: WorkNamespace,
1000 pub kind: WorkEdgeKind,
1001 pub from_id: WorkItemId,
1002 pub to_id: WorkItemId,
1003 pub created_at: DateTime<Utc>,
1004}
1005
1006#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1007#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1008#[serde(rename_all = "snake_case")]
1009pub enum WorkGraphEventKind {
1010 Created,
1011 Updated,
1012 Claimed,
1013 Released,
1014 Blocked,
1015 Closed,
1016 Linked,
1017 EvidenceAdded,
1018 AttentionCreated,
1019 AttentionUpdated,
1020}
1021
1022#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1023#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1024pub struct WorkGraphEvent {
1025 #[serde(default, skip_serializing_if = "Option::is_none")]
1026 pub seq: Option<i64>,
1027 pub realm_id: String,
1028 pub namespace: WorkNamespace,
1029 #[serde(default, skip_serializing_if = "Option::is_none")]
1030 pub item_id: Option<WorkItemId>,
1031 pub kind: WorkGraphEventKind,
1032 pub at: DateTime<Utc>,
1033 #[serde(default, skip_serializing_if = "Value::is_null")]
1034 pub payload: Value,
1035}
1036
1037impl WorkGraphEvent {
1038 pub fn item(
1039 realm_id: String,
1040 namespace: WorkNamespace,
1041 item_id: WorkItemId,
1042 kind: WorkGraphEventKind,
1043 at: DateTime<Utc>,
1044 payload: Value,
1045 ) -> Self {
1046 Self {
1047 seq: None,
1048 realm_id,
1049 namespace,
1050 item_id: Some(item_id),
1051 kind,
1052 at,
1053 payload,
1054 }
1055 }
1056
1057 pub fn graph(
1058 realm_id: String,
1059 namespace: WorkNamespace,
1060 kind: WorkGraphEventKind,
1061 at: DateTime<Utc>,
1062 payload: Value,
1063 ) -> Self {
1064 Self {
1065 seq: None,
1066 realm_id,
1067 namespace,
1068 item_id: None,
1069 kind,
1070 at,
1071 payload,
1072 }
1073 }
1074}
1075
1076#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1077#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1078pub struct CreateWorkItemRequest {
1079 #[serde(default, skip_serializing_if = "Option::is_none")]
1080 pub realm_id: Option<String>,
1081 #[serde(default, skip_serializing_if = "Option::is_none")]
1082 pub namespace: Option<WorkNamespace>,
1083 pub title: String,
1084 #[serde(default, skip_serializing_if = "Option::is_none")]
1085 pub description: Option<String>,
1086 #[serde(default)]
1087 pub priority: WorkPriority,
1088 #[serde(default)]
1089 pub completion_policy: WorkCompletionPolicy,
1090 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1091 pub labels: BTreeSet<String>,
1092 #[serde(default, skip_serializing_if = "Option::is_none")]
1093 pub due_at: Option<DateTime<Utc>>,
1094 #[serde(default, skip_serializing_if = "Option::is_none")]
1095 pub not_before: Option<DateTime<Utc>>,
1096 #[serde(default, skip_serializing_if = "Option::is_none")]
1097 pub snoozed_until: Option<DateTime<Utc>>,
1098 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1099 pub external_refs: Vec<ExternalWorkRef>,
1100 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1101 pub evidence_refs: Vec<WorkEvidenceRef>,
1102 #[serde(default, skip_serializing_if = "Option::is_none")]
1103 pub status: Option<WorkStatus>,
1104}
1105
1106#[derive(Debug, Clone, Serialize, Deserialize)]
1107#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1108pub struct UpdateWorkItemRequest {
1109 pub id: WorkItemId,
1110 #[serde(default, skip_serializing_if = "Option::is_none")]
1111 pub realm_id: Option<String>,
1112 #[serde(default, skip_serializing_if = "Option::is_none")]
1113 pub namespace: Option<WorkNamespace>,
1114 pub expected_revision: u64,
1115 #[serde(default, skip_serializing_if = "Option::is_none")]
1116 pub title: Option<String>,
1117 #[serde(default, skip_serializing_if = "Option::is_none")]
1118 pub description: Option<String>,
1119 #[serde(default, skip_serializing_if = "Option::is_none")]
1120 pub priority: Option<WorkPriority>,
1121 #[serde(default, skip_serializing_if = "Option::is_none")]
1122 pub completion_policy: Option<WorkCompletionPolicy>,
1123 #[serde(default, skip_serializing_if = "Option::is_none")]
1124 pub labels: Option<BTreeSet<String>>,
1125 #[serde(default, skip_serializing_if = "Option::is_none")]
1126 pub due_at: Option<DateTime<Utc>>,
1127 #[serde(default, skip_serializing_if = "Option::is_none")]
1128 pub not_before: Option<DateTime<Utc>>,
1129 #[serde(default, skip_serializing_if = "Option::is_none")]
1130 pub snoozed_until: Option<DateTime<Utc>>,
1131 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1132 pub external_refs: Vec<ExternalWorkRef>,
1133}
1134
1135#[derive(Debug, Clone, Serialize, Deserialize)]
1136#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1137pub struct ClaimWorkItemRequest {
1138 pub id: WorkItemId,
1139 #[serde(default, skip_serializing_if = "Option::is_none")]
1140 pub realm_id: Option<String>,
1141 #[serde(default, skip_serializing_if = "Option::is_none")]
1142 pub namespace: Option<WorkNamespace>,
1143 pub expected_revision: u64,
1144 pub owner: WorkOwner,
1145 #[serde(default, skip_serializing_if = "Option::is_none")]
1146 pub lease_seconds: Option<u64>,
1147 #[serde(default, skip_serializing_if = "Option::is_none")]
1148 pub lease_expires_at: Option<DateTime<Utc>>,
1149}
1150
1151#[derive(Debug, Clone, Serialize, Deserialize)]
1152#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1153pub struct ReleaseWorkItemRequest {
1154 pub id: WorkItemId,
1155 #[serde(default, skip_serializing_if = "Option::is_none")]
1156 pub realm_id: Option<String>,
1157 #[serde(default, skip_serializing_if = "Option::is_none")]
1158 pub namespace: Option<WorkNamespace>,
1159 pub expected_revision: u64,
1160}
1161
1162#[derive(Debug, Clone, Serialize, Deserialize)]
1163#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1164pub struct CloseWorkItemRequest {
1165 pub id: WorkItemId,
1166 #[serde(default, skip_serializing_if = "Option::is_none")]
1167 pub realm_id: Option<String>,
1168 #[serde(default, skip_serializing_if = "Option::is_none")]
1169 pub namespace: Option<WorkNamespace>,
1170 pub expected_revision: u64,
1171 #[serde(default = "default_terminal_status")]
1172 pub status: WorkStatus,
1173}
1174
1175fn default_terminal_status() -> WorkStatus {
1176 WorkStatus::Completed
1177}
1178
1179#[derive(Debug, Clone, Serialize, Deserialize)]
1180#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1181pub struct LinkWorkItemsRequest {
1182 #[serde(default, skip_serializing_if = "Option::is_none")]
1183 pub realm_id: Option<String>,
1184 #[serde(default, skip_serializing_if = "Option::is_none")]
1185 pub namespace: Option<WorkNamespace>,
1186 pub kind: WorkEdgeKind,
1187 pub from_id: WorkItemId,
1188 pub to_id: WorkItemId,
1189}
1190
1191#[derive(Debug, Clone, Serialize, Deserialize)]
1192#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1193pub struct AddEvidenceRequest {
1194 pub id: WorkItemId,
1195 #[serde(default, skip_serializing_if = "Option::is_none")]
1196 pub realm_id: Option<String>,
1197 #[serde(default, skip_serializing_if = "Option::is_none")]
1198 pub namespace: Option<WorkNamespace>,
1199 pub expected_revision: u64,
1200 pub evidence: WorkEvidenceRef,
1201}
1202
1203#[derive(Debug, Clone, Serialize, Deserialize)]
1204#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1205pub struct GoalCreateRequest {
1206 #[serde(default, skip_serializing_if = "Option::is_none")]
1207 pub realm_id: Option<String>,
1208 #[serde(default, skip_serializing_if = "Option::is_none")]
1209 pub namespace: Option<WorkNamespace>,
1210 pub title: String,
1211 #[serde(default, skip_serializing_if = "Option::is_none")]
1212 pub description: Option<String>,
1213 pub target: GoalAttentionTarget,
1214 #[serde(default)]
1215 pub mode: WorkAttentionMode,
1216 #[serde(default)]
1217 pub completion_policy: WorkCompletionPolicy,
1218 #[serde(default)]
1219 pub delegated_authority: AttentionDelegatedAuthority,
1220 #[serde(default)]
1221 pub projection_policy: AttentionProjectionPolicy,
1222}
1223
1224#[derive(Debug, Clone, Serialize, Deserialize)]
1225#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1226pub struct PublicGoalCreateRequest {
1227 #[serde(default, skip_serializing_if = "Option::is_none")]
1228 pub realm_id: Option<String>,
1229 #[serde(default, skip_serializing_if = "Option::is_none")]
1230 pub namespace: Option<WorkNamespace>,
1231 pub title: String,
1232 #[serde(default, skip_serializing_if = "Option::is_none")]
1233 pub description: Option<String>,
1234 pub target: GoalAttentionTarget,
1235 #[serde(default)]
1236 pub mode: WorkAttentionMode,
1237 #[serde(default)]
1238 pub completion_policy: PublicGoalCompletionPolicy,
1239 #[serde(default)]
1240 pub delegated_authority: AttentionDelegatedAuthority,
1241 #[serde(default)]
1242 pub projection_policy: AttentionProjectionPolicy,
1243}
1244
1245impl From<PublicGoalCreateRequest> for GoalCreateRequest {
1246 fn from(request: PublicGoalCreateRequest) -> Self {
1247 Self {
1248 realm_id: request.realm_id,
1249 namespace: request.namespace,
1250 title: request.title,
1251 description: request.description,
1252 target: request.target,
1253 mode: request.mode,
1254 completion_policy: request.completion_policy.into(),
1255 delegated_authority: request.delegated_authority,
1256 projection_policy: request.projection_policy,
1257 }
1258 }
1259}
1260
1261#[derive(Debug, Clone, Serialize, Deserialize)]
1262#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1263pub struct GoalCreateResult {
1264 pub item: WorkItem,
1265 pub attention: WorkAttentionBinding,
1266}
1267
1268#[derive(Debug, Clone, Serialize, Deserialize)]
1269#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1270pub struct GoalStatusRequest {
1271 pub binding_id: WorkAttentionBindingId,
1272 #[serde(default, skip_serializing_if = "Option::is_none")]
1273 pub realm_id: Option<String>,
1274 #[serde(default, skip_serializing_if = "Option::is_none")]
1275 pub namespace: Option<WorkNamespace>,
1276}
1277
1278#[derive(Debug, Clone, Serialize, Deserialize)]
1279#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1280pub struct GoalStatusResult {
1281 pub item: WorkItem,
1282 pub attention: WorkAttentionBinding,
1283}
1284
1285#[derive(Debug, Clone, Serialize, Deserialize)]
1286#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1287pub struct GoalConfirmRequest {
1288 pub binding_id: WorkAttentionBindingId,
1289 #[serde(default, skip_serializing_if = "Option::is_none")]
1290 pub realm_id: Option<String>,
1291 #[serde(default, skip_serializing_if = "Option::is_none")]
1292 pub namespace: Option<WorkNamespace>,
1293 pub expected_revision: u64,
1294 pub evidence: WorkEvidenceRef,
1295 #[serde(skip)]
1296 #[cfg_attr(feature = "schema", schemars(skip))]
1297 pub principal: Option<WorkOwnerKey>,
1298 #[serde(skip)]
1299 #[cfg_attr(feature = "schema", schemars(skip))]
1300 pub trusted_principal: Option<WorkOwnerKey>,
1301}
1302
1303impl GoalConfirmRequest {
1304 pub fn with_trusted_principal(mut self, principal: Option<WorkOwnerKey>) -> Self {
1306 if self.trusted_principal.is_none() {
1307 self.trusted_principal = principal;
1308 }
1309 self
1310 }
1311}
1312
1313#[derive(Debug, Clone, Serialize, Deserialize)]
1314#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1315pub struct GoalConfirmResult {
1316 pub item: WorkItem,
1317 pub attention: WorkAttentionBinding,
1318}
1319
1320#[derive(Debug, Clone, Serialize, Deserialize)]
1321#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1322pub struct GoalRequestCloseRequest {
1323 pub binding_id: WorkAttentionBindingId,
1324 #[serde(default, skip_serializing_if = "Option::is_none")]
1325 pub realm_id: Option<String>,
1326 #[serde(default, skip_serializing_if = "Option::is_none")]
1327 pub namespace: Option<WorkNamespace>,
1328 pub expected_revision: u64,
1329 #[serde(default)]
1330 pub status: GoalTerminalStatus,
1331}
1332
1333#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1334#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1335#[serde(rename_all = "snake_case")]
1336pub enum GoalTerminalStatus {
1337 #[default]
1338 Completed,
1339 Cancelled,
1340 Failed,
1341}
1342
1343impl From<GoalTerminalStatus> for WorkStatus {
1344 fn from(status: GoalTerminalStatus) -> Self {
1345 match status {
1346 GoalTerminalStatus::Completed => Self::Completed,
1347 GoalTerminalStatus::Cancelled => Self::Cancelled,
1348 GoalTerminalStatus::Failed => Self::Failed,
1349 }
1350 }
1351}
1352
1353#[derive(Debug, Clone, Serialize, Deserialize)]
1354#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1355pub struct PublicGoalRequestCloseRequest {
1356 pub binding_id: WorkAttentionBindingId,
1357 #[serde(default, skip_serializing_if = "Option::is_none")]
1358 pub realm_id: Option<String>,
1359 #[serde(default, skip_serializing_if = "Option::is_none")]
1360 pub namespace: Option<WorkNamespace>,
1361 pub expected_revision: u64,
1362 #[serde(default)]
1363 pub status: GoalTerminalStatus,
1364}
1365
1366impl From<PublicGoalRequestCloseRequest> for GoalRequestCloseRequest {
1367 fn from(request: PublicGoalRequestCloseRequest) -> Self {
1368 Self {
1369 binding_id: request.binding_id,
1370 realm_id: request.realm_id,
1371 namespace: request.namespace,
1372 expected_revision: request.expected_revision,
1373 status: request.status,
1374 }
1375 }
1376}
1377
1378#[derive(Debug, Clone, Serialize, Deserialize)]
1379#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1380pub struct GoalRequestCloseResult {
1381 pub item: WorkItem,
1382 pub attention: WorkAttentionBinding,
1383}
1384
1385#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1386#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1387pub struct AttentionListRequest {
1388 #[serde(default, skip_serializing_if = "Option::is_none")]
1389 pub realm_id: Option<String>,
1390 #[serde(default, skip_serializing_if = "Option::is_none")]
1391 pub namespace: Option<WorkNamespace>,
1392 #[serde(default, skip_serializing_if = "Option::is_none")]
1393 pub target: Option<WorkAttentionTarget>,
1394 #[serde(default, skip_serializing_if = "Option::is_none")]
1395 pub status: Option<WorkAttentionStatus>,
1396}
1397
1398#[derive(Debug, Clone, Serialize, Deserialize)]
1399#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1400pub struct AttentionListResult {
1401 pub attention: Vec<WorkAttentionBinding>,
1402}
1403
1404#[derive(Debug, Clone, Serialize, Deserialize)]
1405#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1406pub struct AttentionBindingRequest {
1407 pub binding_id: WorkAttentionBindingId,
1408 #[serde(default, skip_serializing_if = "Option::is_none")]
1409 pub realm_id: Option<String>,
1410 #[serde(default, skip_serializing_if = "Option::is_none")]
1411 pub namespace: Option<WorkNamespace>,
1412}
1413
1414#[derive(Debug, Clone, Serialize, Deserialize)]
1415#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1416pub struct AttentionPauseRequest {
1417 pub binding_id: WorkAttentionBindingId,
1418 #[serde(default, skip_serializing_if = "Option::is_none")]
1419 pub realm_id: Option<String>,
1420 #[serde(default, skip_serializing_if = "Option::is_none")]
1421 pub namespace: Option<WorkNamespace>,
1422 pub expected_revision: u64,
1423 #[serde(default, skip_serializing_if = "Option::is_none")]
1424 pub until: Option<DateTime<Utc>>,
1425}
1426
1427#[derive(Debug, Clone, Serialize, Deserialize)]
1428#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1429pub struct AttentionResumeRequest {
1430 pub binding_id: WorkAttentionBindingId,
1431 #[serde(default, skip_serializing_if = "Option::is_none")]
1432 pub realm_id: Option<String>,
1433 #[serde(default, skip_serializing_if = "Option::is_none")]
1434 pub namespace: Option<WorkNamespace>,
1435 pub expected_revision: u64,
1436}
1437
1438#[derive(Debug, Clone, Serialize, Deserialize)]
1439#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1440pub struct AttentionReassignRequest {
1441 pub binding_id: WorkAttentionBindingId,
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 target: GoalAttentionTarget,
1447}
1448
1449#[derive(Debug, Clone, Serialize, Deserialize)]
1450#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1451pub struct AttentionBindingResult {
1452 pub attention: WorkAttentionBinding,
1453}
1454
1455#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1456#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1457#[serde(rename_all = "snake_case")]
1458pub enum AttentionContinueOutcome {
1459 Accepted,
1460 Deduplicated,
1461 Rejected,
1462}
1463
1464#[derive(Debug, Clone, Serialize, Deserialize)]
1465#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1466pub struct AttentionContinueResult {
1467 pub outcome: AttentionContinueOutcome,
1468 #[serde(default, skip_serializing_if = "Option::is_none")]
1469 pub input_id: Option<String>,
1470 #[serde(default, skip_serializing_if = "Option::is_none")]
1471 pub existing_id: Option<String>,
1472 #[serde(default, skip_serializing_if = "Option::is_none")]
1473 pub reason: Option<String>,
1474}
1475
1476#[derive(Debug, Clone, Serialize, Deserialize)]
1477#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1478pub struct AttentionProjectionRequest {
1479 pub binding_id: WorkAttentionBindingId,
1480 #[serde(default, skip_serializing_if = "Option::is_none")]
1481 pub realm_id: Option<String>,
1482 #[serde(default, skip_serializing_if = "Option::is_none")]
1483 pub namespace: Option<WorkNamespace>,
1484}
1485
1486#[derive(Debug, Clone, Serialize, Deserialize)]
1487#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1488pub struct AttentionProjectionResult {
1489 pub projection: AttentionContextProjection,
1490}
1491
1492#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1493#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1494pub struct AttentionContextProjection {
1495 pub binding_id: WorkAttentionBindingId,
1496 pub work_ref: WorkItemRef,
1497 pub mode: WorkAttentionMode,
1498 pub binding_revision: u64,
1499 pub item_revision: u64,
1500 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1501 pub parent_refs: Vec<WorkItemRef>,
1502 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1503 pub parent_context: Vec<AttentionProjectionParentContext>,
1504 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1505 pub evidence_refs: Vec<WorkEvidenceRef>,
1506 pub authority: ProjectedAttentionAuthority,
1507 pub text: AttentionProjectionText,
1508}
1509
1510#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1511#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1512pub struct AttentionProjectionParentContext {
1513 pub work_ref: WorkItemRef,
1514 pub status: WorkStatus,
1515 pub revision: u64,
1516}
1517
1518#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1519#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1520pub struct ProjectedAttentionAuthority {
1521 pub can_get: bool,
1522 pub can_add_evidence: bool,
1523 pub can_release: bool,
1524 pub can_update: bool,
1525 pub can_block: bool,
1526 pub can_create: bool,
1527 pub can_link: bool,
1528 pub can_link_parent: bool,
1529 pub can_link_related: bool,
1530 pub can_link_derived_from: bool,
1531 #[serde(default)]
1532 pub can_close_own_review_item: bool,
1533 pub can_close_if_policy_allows: bool,
1534}
1535
1536#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1537#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1538pub struct AttentionProjectionText {
1539 pub title: String,
1540 pub rendered: String,
1541 pub truncated: bool,
1542}
1543
1544#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1545#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1546pub struct WorkItemFilter {
1547 #[serde(default, skip_serializing_if = "Option::is_none")]
1548 pub realm_id: Option<String>,
1549 #[serde(default, skip_serializing_if = "Option::is_none")]
1550 pub namespace: Option<WorkNamespace>,
1551 #[serde(default)]
1552 pub all_namespaces: bool,
1553 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1554 pub statuses: Vec<WorkStatus>,
1555 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1556 pub labels: Vec<String>,
1557 #[serde(default)]
1558 pub include_terminal: bool,
1559 #[serde(default, skip_serializing_if = "Option::is_none")]
1560 pub limit: Option<usize>,
1561}
1562
1563#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1564#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1565pub struct ReadyWorkFilter {
1566 #[serde(default, skip_serializing_if = "Option::is_none")]
1567 pub realm_id: Option<String>,
1568 #[serde(default, skip_serializing_if = "Option::is_none")]
1569 pub namespace: Option<WorkNamespace>,
1570 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1571 pub labels: Vec<String>,
1572 #[serde(default, skip_serializing_if = "Option::is_none")]
1573 pub limit: Option<usize>,
1574}
1575
1576#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1577#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1578pub struct WorkGraphSnapshotFilter {
1579 #[serde(default, skip_serializing_if = "Option::is_none")]
1580 pub realm_id: Option<String>,
1581 #[serde(default, skip_serializing_if = "Option::is_none")]
1582 pub namespace: Option<WorkNamespace>,
1583 #[serde(default)]
1584 pub all_namespaces: bool,
1585 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1586 pub statuses: Vec<WorkStatus>,
1587 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1588 pub labels: Vec<String>,
1589 #[serde(default)]
1590 pub include_terminal: bool,
1591 #[serde(default, skip_serializing_if = "Option::is_none")]
1592 pub limit: Option<usize>,
1593}
1594
1595#[derive(Debug, Clone, Serialize, Deserialize)]
1598#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1599pub struct WorkGraphIdParams {
1600 pub id: WorkItemId,
1601 #[serde(default, skip_serializing_if = "Option::is_none")]
1602 pub realm_id: Option<String>,
1603 #[serde(default, skip_serializing_if = "Option::is_none")]
1604 pub namespace: Option<WorkNamespace>,
1605}
1606
1607#[derive(Debug, Clone, Serialize, Deserialize)]
1608#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1609pub struct WorkGraphSnapshot {
1610 pub realm_id: String,
1611 #[serde(default, skip_serializing_if = "Option::is_none")]
1612 pub namespace: Option<WorkNamespace>,
1613 pub all_namespaces: bool,
1614 pub captured_at: DateTime<Utc>,
1615 #[serde(default, skip_serializing_if = "Option::is_none")]
1616 pub event_high_water_mark: Option<i64>,
1617 pub items: Vec<WorkItem>,
1618 pub edges: Vec<WorkEdge>,
1619 #[serde(default)]
1620 pub attention: Vec<WorkAttentionBinding>,
1621 pub ready_item_ids: Vec<WorkItemId>,
1622}
1623
1624#[derive(Debug, Clone, Serialize, Deserialize)]
1625#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1626pub struct WorkGraphItemsResponse {
1627 pub items: Vec<WorkItem>,
1628}
1629
1630#[derive(Debug, Clone, Serialize, Deserialize)]
1631#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1632pub struct WorkGraphEventsResponse {
1633 pub events: Vec<WorkGraphEvent>,
1634}
1635
1636#[cfg(test)]
1637#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
1638mod tests {
1639 use super::*;
1640 use crate::machine::WorkGraphMachine;
1641
1642 fn machine_item() -> WorkItem {
1643 WorkGraphMachine::create_item(
1644 CreateWorkItemRequest {
1645 title: "deserialize-authority".to_string(),
1646 ..Default::default()
1647 },
1648 "realm".to_string(),
1649 WorkNamespace::default(),
1650 Utc::now(),
1651 )
1652 .expect("machine create_item")
1653 .0
1654 }
1655
1656 #[test]
1657 fn work_item_round_trip_preserves_machine_state() {
1658 let item = machine_item();
1659 let json = serde_json::to_string(&item).expect("serialize work item");
1660 let decoded: WorkItem = serde_json::from_str(&json).expect("deserialize work item");
1661 assert_eq!(
1662 decoded, item,
1663 "round-trip must preserve the whole work item"
1664 );
1665 assert_eq!(
1666 decoded.machine_state, item.machine_state,
1667 "round-trip must preserve machine-owned lifecycle authority verbatim"
1668 );
1669 }
1670
1671 #[test]
1672 fn work_item_without_machine_state_fails_closed() {
1673 let item = machine_item();
1674 let mut value = serde_json::to_value(&item).expect("serialize work item to value");
1675 value
1676 .as_object_mut()
1677 .expect("work item json object")
1678 .remove("machine_state");
1679
1680 let result: Result<WorkItem, _> = serde_json::from_value(value);
1681 let err = result.expect_err(
1682 "deserializing a WorkItem without machine_state must fail closed, \
1683 never fabricate lifecycle/revision authority from projected fields",
1684 );
1685 assert!(
1686 err.to_string().contains("machine_state"),
1687 "fail-closed error must cite the missing machine_state authority, got: {err}"
1688 );
1689 }
1690}