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 #[cfg_attr(feature = "schema", schemars(range(min = 1, max = 64)))]
254 threshold: u16,
255 },
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
259#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
260#[serde(tag = "kind", rename_all = "snake_case")]
261pub enum PublicGoalCompletionPolicy {
262 #[default]
263 SelfAttest,
264}
265
266impl From<PublicGoalCompletionPolicy> for WorkCompletionPolicy {
267 fn from(policy: PublicGoalCompletionPolicy) -> Self {
268 match policy {
269 PublicGoalCompletionPolicy::SelfAttest => Self::SelfAttest,
270 }
271 }
272}
273
274impl WorkCompletionPolicy {
275 pub fn requires_trusted_principal(&self) -> bool {
276 matches!(
277 self,
278 Self::PrincipalConfirmed | Self::Supervisor { .. } | Self::ReviewerQuorum { .. }
279 )
280 }
281
282 pub(crate) fn to_machine(&self) -> wg_dsl::WorkCompletionPolicy {
283 match self {
284 Self::SelfAttest => wg_dsl::WorkCompletionPolicy::SelfAttest,
285 Self::HostConfirmed => wg_dsl::WorkCompletionPolicy::HostConfirmed,
286 Self::PrincipalConfirmed => wg_dsl::WorkCompletionPolicy::PrincipalConfirmed,
287 Self::Supervisor { .. } => wg_dsl::WorkCompletionPolicy::Supervisor,
288 Self::ReviewerQuorum { .. } => wg_dsl::WorkCompletionPolicy::ReviewerQuorum,
289 }
290 }
291
292 pub(crate) fn supervisor_owner_key(&self) -> Option<wg_dsl::WorkOwnerKey> {
293 match self {
294 Self::Supervisor { owner_key } => Some(work_owner_key_to_machine(owner_key)),
295 _ => None,
296 }
297 }
298
299 pub(crate) fn reviewer_quorum_threshold(&self) -> Option<u64> {
300 match self {
301 Self::ReviewerQuorum { threshold } => Some(u64::from(*threshold)),
302 _ => None,
303 }
304 }
305
306 pub(crate) fn from_machine(
307 policy: wg_dsl::WorkCompletionPolicy,
308 supervisor_owner_key: Option<wg_dsl::WorkOwnerKey>,
309 reviewer_quorum_threshold: Option<u64>,
310 ) -> Self {
311 match policy {
312 wg_dsl::WorkCompletionPolicy::SelfAttest => Self::SelfAttest,
313 wg_dsl::WorkCompletionPolicy::HostConfirmed => Self::HostConfirmed,
314 wg_dsl::WorkCompletionPolicy::PrincipalConfirmed => Self::PrincipalConfirmed,
315 wg_dsl::WorkCompletionPolicy::Supervisor => Self::Supervisor {
316 owner_key: supervisor_owner_key
317 .map(work_owner_key_from_machine)
318 .unwrap_or_else(|| WorkOwnerKey {
319 kind: WorkOwnerKind::Principal,
320 id: "supervisor".to_string(),
321 }),
322 },
323 wg_dsl::WorkCompletionPolicy::ReviewerQuorum => Self::ReviewerQuorum {
324 threshold: reviewer_quorum_threshold
325 .and_then(|threshold| u16::try_from(threshold).ok())
326 .unwrap_or(1),
327 },
328 }
329 }
330}
331
332impl WorkOwner {
333 pub fn new(key: WorkOwnerKey) -> Self {
334 Self {
335 key,
336 display_name: None,
337 }
338 }
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
343pub struct WorkClaim {
344 pub owner: WorkOwner,
345 pub claimed_at: DateTime<Utc>,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub lease_expires_at: Option<DateTime<Utc>>,
348}
349
350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
351#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
352pub struct ExternalWorkRef {
353 pub kind: String,
354 pub id: String,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub url: Option<String>,
357}
358
359#[derive(
367 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
368)]
369#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
370#[serde(rename_all = "snake_case")]
371pub enum WorkEvidenceKind {
372 #[default]
375 SelfAttest,
376 HostConfirmation,
377 PrincipalConfirmation,
378 SupervisorConfirmation,
379 ReviewerConfirmation,
380}
381
382impl WorkEvidenceKind {
383 pub(crate) fn to_machine(self) -> wg_dsl::WorkEvidenceKind {
384 match self {
385 Self::SelfAttest => wg_dsl::WorkEvidenceKind::SelfAttest,
386 Self::HostConfirmation => wg_dsl::WorkEvidenceKind::HostConfirmation,
387 Self::PrincipalConfirmation => wg_dsl::WorkEvidenceKind::PrincipalConfirmation,
388 Self::SupervisorConfirmation => wg_dsl::WorkEvidenceKind::SupervisorConfirmation,
389 Self::ReviewerConfirmation => wg_dsl::WorkEvidenceKind::ReviewerConfirmation,
390 }
391 }
392
393 pub(crate) fn from_kind_str(kind: &str) -> Option<Self> {
401 match kind {
402 "host_confirmation" => Some(Self::HostConfirmation),
403 "principal_confirmation" => Some(Self::PrincipalConfirmation),
404 "supervisor_confirmation" => Some(Self::SupervisorConfirmation),
405 "reviewer_confirmation" => Some(Self::ReviewerConfirmation),
406 _ => None,
407 }
408 }
409
410 pub(crate) fn is_reserved_confirmation(self) -> bool {
414 !matches!(self, Self::SelfAttest)
415 }
416
417 pub(crate) fn to_confirmation_observation(self) -> wg_dsl::WorkConfirmationEvidenceObservation {
422 match self {
423 Self::SelfAttest => wg_dsl::WorkConfirmationEvidenceObservation::Other,
424 Self::HostConfirmation => wg_dsl::WorkConfirmationEvidenceObservation::HostConfirmation,
425 Self::PrincipalConfirmation => {
426 wg_dsl::WorkConfirmationEvidenceObservation::PrincipalConfirmation
427 }
428 Self::SupervisorConfirmation => {
429 wg_dsl::WorkConfirmationEvidenceObservation::SupervisorConfirmation
430 }
431 Self::ReviewerConfirmation => {
432 wg_dsl::WorkConfirmationEvidenceObservation::ReviewerConfirmation
433 }
434 }
435 }
436}
437
438#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
439#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
440pub struct WorkEvidenceRef {
441 pub kind: String,
447 pub id: String,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
449 pub label: Option<String>,
450 #[serde(default, skip_serializing_if = "Option::is_none")]
451 pub summary: Option<String>,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
456 pub confirmation_kind: Option<WorkEvidenceKind>,
457 #[serde(default, skip_serializing_if = "Option::is_none")]
461 pub confirming_owner_key: Option<WorkOwnerKey>,
462}
463
464impl WorkEvidenceRef {
465 pub(crate) fn confirmation_classification(&self) -> Option<WorkEvidenceKind> {
476 self.confirmation_kind
477 .filter(|kind| kind.is_reserved_confirmation())
478 .or_else(|| WorkEvidenceKind::from_kind_str(&self.kind))
479 }
480}
481
482#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
483#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
484pub struct WorkItemRef {
485 pub realm_id: String,
486 pub namespace: WorkNamespace,
487 pub item_id: WorkItemId,
488}
489
490#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
491#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
492#[serde(tag = "kind", rename_all = "snake_case")]
493pub enum WorkAttentionTarget {
494 Session { session_id: SessionId },
495 LoweredOwner { owner_key: WorkOwnerKey },
496}
497
498impl WorkAttentionTarget {
499 pub fn owner_key(&self) -> Result<WorkOwnerKey, WorkGraphError> {
500 match self {
501 Self::Session { session_id } => WorkOwnerKey::session(session_id.to_string()),
502 Self::LoweredOwner { owner_key } => Ok(owner_key.clone()),
503 }
504 }
505
506 pub fn target_key(&self) -> String {
511 match self {
512 Self::Session { session_id } => format!("session:{session_id}"),
513 Self::LoweredOwner { owner_key } => {
514 format!("owner:{}:{}", owner_key.kind.as_str(), owner_key.id)
515 }
516 }
517 }
518}
519
520#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
521#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
522#[serde(tag = "kind", rename_all = "snake_case")]
523pub enum GoalAttentionTarget {
524 Session { session_id: SessionId },
525 Owner { owner_key: WorkOwnerKey },
526}
527
528impl GoalAttentionTarget {
529 pub fn to_attention_target(&self) -> WorkAttentionTarget {
530 match self {
531 Self::Session { session_id } => WorkAttentionTarget::Session {
532 session_id: session_id.clone(),
533 },
534 Self::Owner { owner_key } => WorkAttentionTarget::LoweredOwner {
535 owner_key: owner_key.clone(),
536 },
537 }
538 }
539}
540
541#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
542#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
543#[serde(rename_all = "snake_case")]
544pub enum WorkAttentionMode {
545 #[default]
546 Pursue,
547 Coordinate,
548 Review,
549 Falsify,
550 Judge,
551 Observe,
552}
553
554#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
555#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
556#[serde(tag = "state", rename_all = "snake_case")]
557pub enum WorkAttentionStatus {
559 #[default]
560 Active,
561 Paused {
562 #[serde(default, skip_serializing_if = "Option::is_none")]
563 until: Option<DateTime<Utc>>,
564 },
565 Superseded,
566 Stopped,
567}
568
569impl WorkAttentionStatus {
570 pub fn status_key(&self) -> &'static str {
574 match self {
575 Self::Active => "active",
576 Self::Paused { .. } => "paused",
577 Self::Superseded => "superseded",
578 Self::Stopped => "stopped",
579 }
580 }
581
582 pub fn is_terminal(&self) -> bool {
585 matches!(self, Self::Superseded | Self::Stopped)
586 }
587}
588
589#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
590#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
591#[serde(rename_all = "snake_case")]
592pub enum AttentionDelegatedAuthority {
593 #[default]
594 AddEvidence,
595 CloseOwnReviewItem,
596 RequestClosure,
597 CloseIfPolicyAllows,
598}
599
600#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
601#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
602pub struct AttentionProjectionPolicy {
603 #[serde(default = "default_projection_max_text_chars")]
604 pub max_text_chars: u32,
605 #[serde(default = "default_include_parent_context")]
606 pub include_parent_context: bool,
607}
608
609fn default_include_parent_context() -> bool {
610 true
611}
612
613impl Default for AttentionProjectionPolicy {
614 fn default() -> Self {
615 Self {
616 max_text_chars: default_projection_max_text_chars(),
617 include_parent_context: true,
618 }
619 }
620}
621
622fn default_projection_max_text_chars() -> u32 {
623 4096
624}
625
626#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
627#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
628pub struct WorkAttentionBinding {
629 pub binding_id: WorkAttentionBindingId,
630 pub work_ref: WorkItemRef,
631 pub target: WorkAttentionTarget,
632 pub mode: WorkAttentionMode,
633 pub status: WorkAttentionStatus,
634 #[serde(default = "default_work_attention_machine_state")]
635 #[cfg_attr(feature = "schema", schemars(with = "WorkAttentionMachineStateSchema"))]
636 pub machine_state: WorkAttentionMachineState,
637 pub delegated_authority: AttentionDelegatedAuthority,
638 #[serde(default)]
639 pub projection_policy: AttentionProjectionPolicy,
640 pub created_at: DateTime<Utc>,
641 pub updated_at: DateTime<Utc>,
642}
643
644#[cfg(feature = "schema")]
645#[derive(schemars::JsonSchema)]
646#[allow(dead_code)]
647struct WorkAttentionMachineStateSchema {
648 lifecycle_phase: String,
649 revision: u64,
650 paused_until_utc_ms: Option<u64>,
651 superseded_by_binding_key: Option<String>,
652 terminal_at_utc_ms: Option<u64>,
653}
654
655fn default_work_attention_machine_state() -> WorkAttentionMachineState {
656 WorkAttentionMachineState::default()
657}
658
659#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
660pub struct WorkItem {
661 pub id: WorkItemId,
662 pub realm_id: String,
663 pub namespace: WorkNamespace,
664 pub title: String,
665 #[serde(default, skip_serializing_if = "Option::is_none")]
666 pub description: Option<String>,
667 pub status: WorkStatus,
668 #[serde(default)]
669 pub completion_policy: WorkCompletionPolicy,
670 pub priority: WorkPriority,
671 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
672 pub labels: BTreeSet<String>,
673 #[serde(default, skip_serializing_if = "Option::is_none")]
674 pub owner: Option<WorkOwner>,
675 #[serde(default, skip_serializing_if = "Option::is_none")]
676 pub claim: Option<WorkClaim>,
677 pub machine_state: WorkGraphMachineState,
678 pub revision: u64,
679 #[serde(default, skip_serializing_if = "Option::is_none")]
680 pub due_at: Option<DateTime<Utc>>,
681 #[serde(default, skip_serializing_if = "Option::is_none")]
682 pub not_before: Option<DateTime<Utc>>,
683 #[serde(default, skip_serializing_if = "Option::is_none")]
684 pub snoozed_until: Option<DateTime<Utc>>,
685 pub created_at: DateTime<Utc>,
686 pub updated_at: DateTime<Utc>,
687 #[serde(default, skip_serializing_if = "Option::is_none")]
688 pub terminal_at: Option<DateTime<Utc>>,
689 #[serde(default, skip_serializing_if = "Vec::is_empty")]
690 pub external_refs: Vec<ExternalWorkRef>,
691 #[serde(default, skip_serializing_if = "Vec::is_empty")]
692 pub evidence_refs: Vec<WorkEvidenceRef>,
693}
694
695#[derive(Deserialize)]
696struct WorkItemWire {
697 id: WorkItemId,
698 realm_id: String,
699 namespace: WorkNamespace,
700 title: String,
701 #[serde(default)]
702 description: Option<String>,
703 status: WorkStatus,
704 #[serde(default)]
705 completion_policy: WorkCompletionPolicy,
706 priority: WorkPriority,
707 #[serde(default)]
708 labels: BTreeSet<String>,
709 #[serde(default)]
710 owner: Option<WorkOwner>,
711 #[serde(default)]
712 claim: Option<WorkClaim>,
713 #[serde(default)]
714 machine_state: Option<WorkGraphMachineState>,
715 revision: u64,
716 #[serde(default)]
717 due_at: Option<DateTime<Utc>>,
718 #[serde(default)]
719 not_before: Option<DateTime<Utc>>,
720 #[serde(default)]
721 snoozed_until: Option<DateTime<Utc>>,
722 created_at: DateTime<Utc>,
723 updated_at: DateTime<Utc>,
724 #[serde(default)]
725 terminal_at: Option<DateTime<Utc>>,
726 #[serde(default)]
727 external_refs: Vec<ExternalWorkRef>,
728 #[serde(default)]
729 evidence_refs: Vec<WorkEvidenceRef>,
730}
731
732impl<'de> Deserialize<'de> for WorkItem {
733 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
734 where
735 D: serde::Deserializer<'de>,
736 {
737 let mut wire = WorkItemWire::deserialize(deserializer)?;
738 let machine_state = wire.machine_state.take().ok_or_else(|| {
739 serde::de::Error::custom(
740 "WorkItem is missing `machine_state`: lifecycle/revision authority is machine-owned \
741 and cannot be reconstructed from projected fields",
742 )
743 })?;
744 Ok(Self {
745 id: wire.id,
746 realm_id: wire.realm_id,
747 namespace: wire.namespace,
748 title: wire.title,
749 description: wire.description,
750 status: wire.status,
751 completion_policy: wire.completion_policy,
752 priority: wire.priority,
753 labels: wire.labels,
754 owner: wire.owner,
755 claim: wire.claim,
756 machine_state,
757 revision: wire.revision,
758 due_at: wire.due_at,
759 not_before: wire.not_before,
760 snoozed_until: wire.snoozed_until,
761 created_at: wire.created_at,
762 updated_at: wire.updated_at,
763 terminal_at: wire.terminal_at,
764 external_refs: wire.external_refs,
765 evidence_refs: wire.evidence_refs,
766 })
767 }
768}
769
770#[cfg(feature = "schema")]
771impl schemars::JsonSchema for WorkItem {
772 fn schema_name() -> std::borrow::Cow<'static, str> {
782 "WorkItem".into()
783 }
784
785 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
786 schemars::json_schema!({
787 "type": "object",
788 "required": [
789 "id",
790 "realm_id",
791 "namespace",
792 "title",
793 "status",
794 "completion_policy",
795 "priority",
796 "machine_state",
797 "revision",
798 "created_at",
799 "updated_at"
800 ],
801 "properties": {
802 "id": { "type": "string" },
803 "realm_id": { "type": "string" },
804 "namespace": { "type": "string" },
805 "title": { "type": "string" },
806 "description": { "type": ["string", "null"] },
807 "status": {
808 "type": "string",
809 "enum": ["open", "in_progress", "blocked", "completed", "cancelled", "failed"]
810 },
811 "completion_policy": {
812 "oneOf": [
813 {
814 "type": "object",
815 "required": ["kind"],
816 "properties": { "kind": { "type": "string", "const": "self_attest" } }
817 },
818 {
819 "type": "object",
820 "required": ["kind"],
821 "properties": { "kind": { "type": "string", "const": "host_confirmed" } }
822 },
823 {
824 "type": "object",
825 "required": ["kind"],
826 "properties": { "kind": { "type": "string", "const": "principal_confirmed" } }
827 },
828 {
829 "type": "object",
830 "required": ["kind", "owner_key"],
831 "properties": {
832 "kind": { "type": "string", "const": "supervisor" },
833 "owner_key": {
834 "type": "object",
835 "required": ["kind", "id"],
836 "properties": {
837 "kind": {
838 "type": "string",
839 "enum": ["principal", "agent", "session", "mob", "label"]
840 },
841 "id": { "type": "string" }
842 }
843 }
844 }
845 },
846 {
847 "type": "object",
848 "required": ["kind", "threshold"],
849 "properties": {
850 "kind": { "type": "string", "const": "reviewer_quorum" },
851 "threshold": { "type": "integer", "format": "uint16", "minimum": 1, "maximum": 64 }
852 }
853 }
854 ]
855 },
856 "priority": {
857 "type": "string",
858 "enum": ["low", "medium", "high"]
859 },
860 "labels": {
861 "type": "array",
862 "uniqueItems": true,
863 "items": { "type": "string" }
864 },
865 "owner": {
866 "anyOf": [
867 {
868 "type": "object",
869 "required": ["key"],
870 "properties": {
871 "key": {
872 "type": "object",
873 "required": ["kind", "id"],
874 "properties": {
875 "kind": {
876 "type": "string",
877 "enum": ["principal", "agent", "session", "mob", "label"]
878 },
879 "id": { "type": "string" }
880 }
881 },
882 "display_name": { "type": ["string", "null"] }
883 }
884 },
885 { "type": "null" }
886 ]
887 },
888 "claim": {
889 "anyOf": [
890 {
891 "type": "object",
892 "required": ["owner", "claimed_at"],
893 "properties": {
894 "owner": {
895 "type": "object",
896 "required": ["key"],
897 "properties": {
898 "key": {
899 "type": "object",
900 "required": ["kind", "id"],
901 "properties": {
902 "kind": {
903 "type": "string",
904 "enum": ["principal", "agent", "session", "mob", "label"]
905 },
906 "id": { "type": "string" }
907 }
908 },
909 "display_name": { "type": ["string", "null"] }
910 }
911 },
912 "claimed_at": { "type": "string", "format": "date-time" },
913 "lease_expires_at": { "type": ["string", "null"], "format": "date-time" }
914 }
915 },
916 { "type": "null" }
917 ]
918 },
919 "machine_state": {
920 "type": "object",
921 "description": "Catalog-generated WorkGraphLifecycleMachine state projection."
922 },
923 "revision": { "type": "integer", "format": "uint64", "minimum": 0 },
924 "due_at": { "type": ["string", "null"], "format": "date-time" },
925 "not_before": { "type": ["string", "null"], "format": "date-time" },
926 "snoozed_until": { "type": ["string", "null"], "format": "date-time" },
927 "created_at": { "type": "string", "format": "date-time" },
928 "updated_at": { "type": "string", "format": "date-time" },
929 "terminal_at": { "type": ["string", "null"], "format": "date-time" },
930 "external_refs": {
931 "type": "array",
932 "items": {
933 "type": "object",
934 "required": ["kind", "id"],
935 "properties": {
936 "kind": { "type": "string" },
937 "id": { "type": "string" },
938 "url": { "type": ["string", "null"] }
939 }
940 }
941 },
942 "evidence_refs": {
943 "type": "array",
944 "items": {
945 "type": "object",
946 "required": ["kind", "id"],
947 "properties": {
948 "kind": { "type": "string" },
949 "id": { "type": "string" },
950 "label": { "type": ["string", "null"] },
951 "summary": { "type": ["string", "null"] },
952 "confirmation_kind": {
953 "anyOf": [
954 {
955 "oneOf": [
956 {
957 "type": "string",
958 "enum": [
959 "host_confirmation",
960 "principal_confirmation",
961 "supervisor_confirmation",
962 "reviewer_confirmation"
963 ]
964 },
965 { "type": "string", "const": "self_attest" }
966 ]
967 },
968 { "type": "null" }
969 ]
970 },
971 "confirming_owner_key": {
972 "anyOf": [
973 {
974 "type": "object",
975 "required": ["kind", "id"],
976 "properties": {
977 "kind": {
978 "type": "string",
979 "enum": ["principal", "agent", "session", "mob", "label"]
980 },
981 "id": { "type": "string" }
982 }
983 },
984 { "type": "null" }
985 ]
986 }
987 }
988 }
989 }
990 }
991 })
992 }
993}
994
995pub(crate) fn work_lifecycle_state_from_status(status: WorkStatus) -> wg_dsl::WorkLifecycleState {
996 match status {
997 WorkStatus::Open => wg_dsl::WorkLifecycleState::Open,
998 WorkStatus::InProgress => wg_dsl::WorkLifecycleState::InProgress,
999 WorkStatus::Blocked => wg_dsl::WorkLifecycleState::Blocked,
1000 WorkStatus::Completed => wg_dsl::WorkLifecycleState::Completed,
1001 WorkStatus::Cancelled => wg_dsl::WorkLifecycleState::Cancelled,
1002 WorkStatus::Failed => wg_dsl::WorkLifecycleState::Failed,
1003 }
1004}
1005
1006pub(crate) fn work_owner_kind_to_machine(kind: WorkOwnerKind) -> wg_dsl::WorkOwnerKind {
1007 match kind {
1008 WorkOwnerKind::Principal => wg_dsl::WorkOwnerKind::Principal,
1009 WorkOwnerKind::Agent => wg_dsl::WorkOwnerKind::Agent,
1010 WorkOwnerKind::Session => wg_dsl::WorkOwnerKind::Session,
1011 WorkOwnerKind::Mob => wg_dsl::WorkOwnerKind::Mob,
1012 WorkOwnerKind::Label => wg_dsl::WorkOwnerKind::Label,
1013 }
1014}
1015
1016pub(crate) fn work_owner_key_to_machine(owner: &WorkOwnerKey) -> wg_dsl::WorkOwnerKey {
1017 wg_dsl::WorkOwnerKey {
1018 kind: work_owner_kind_to_machine(owner.kind),
1019 id: owner.id.clone(),
1020 }
1021}
1022
1023fn work_owner_key_from_machine(owner: wg_dsl::WorkOwnerKey) -> WorkOwnerKey {
1024 let kind = match owner.kind {
1025 wg_dsl::WorkOwnerKind::Principal => WorkOwnerKind::Principal,
1026 wg_dsl::WorkOwnerKind::Agent => WorkOwnerKind::Agent,
1027 wg_dsl::WorkOwnerKind::Session => WorkOwnerKind::Session,
1028 wg_dsl::WorkOwnerKind::Mob => WorkOwnerKind::Mob,
1029 wg_dsl::WorkOwnerKind::Label => WorkOwnerKind::Label,
1030 };
1031 WorkOwnerKey { kind, id: owner.id }
1032}
1033
1034#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1035#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1036pub struct WorkEdge {
1037 pub realm_id: String,
1038 pub namespace: WorkNamespace,
1039 pub kind: WorkEdgeKind,
1040 pub from_id: WorkItemId,
1041 pub to_id: WorkItemId,
1042 pub created_at: DateTime<Utc>,
1043}
1044
1045#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1046#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1047#[serde(rename_all = "snake_case")]
1048pub enum WorkGraphEventKind {
1049 Created,
1050 Updated,
1051 Claimed,
1052 Released,
1053 Blocked,
1054 Closed,
1055 Linked,
1056 EvidenceAdded,
1057 AttentionCreated,
1058 AttentionUpdated,
1059}
1060
1061#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1062#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1063pub struct WorkGraphEvent {
1064 #[serde(default, skip_serializing_if = "Option::is_none")]
1065 pub seq: Option<i64>,
1066 pub realm_id: String,
1067 pub namespace: WorkNamespace,
1068 #[serde(default, skip_serializing_if = "Option::is_none")]
1069 pub item_id: Option<WorkItemId>,
1070 pub kind: WorkGraphEventKind,
1071 pub at: DateTime<Utc>,
1072 #[serde(default, skip_serializing_if = "Value::is_null")]
1073 pub payload: Value,
1074}
1075
1076impl WorkGraphEvent {
1077 pub fn item(
1078 realm_id: String,
1079 namespace: WorkNamespace,
1080 item_id: WorkItemId,
1081 kind: WorkGraphEventKind,
1082 at: DateTime<Utc>,
1083 payload: Value,
1084 ) -> Self {
1085 Self {
1086 seq: None,
1087 realm_id,
1088 namespace,
1089 item_id: Some(item_id),
1090 kind,
1091 at,
1092 payload,
1093 }
1094 }
1095
1096 pub fn graph(
1097 realm_id: String,
1098 namespace: WorkNamespace,
1099 kind: WorkGraphEventKind,
1100 at: DateTime<Utc>,
1101 payload: Value,
1102 ) -> Self {
1103 Self {
1104 seq: None,
1105 realm_id,
1106 namespace,
1107 item_id: None,
1108 kind,
1109 at,
1110 payload,
1111 }
1112 }
1113}
1114
1115#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1116#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1117pub struct CreateWorkItemRequest {
1118 #[serde(default, skip_serializing_if = "Option::is_none")]
1119 pub realm_id: Option<String>,
1120 #[serde(default, skip_serializing_if = "Option::is_none")]
1121 pub namespace: Option<WorkNamespace>,
1122 pub title: String,
1123 #[serde(default, skip_serializing_if = "Option::is_none")]
1124 pub description: Option<String>,
1125 #[serde(default)]
1126 pub priority: WorkPriority,
1127 #[serde(default)]
1128 pub completion_policy: WorkCompletionPolicy,
1129 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1130 pub labels: BTreeSet<String>,
1131 #[serde(default, skip_serializing_if = "Option::is_none")]
1132 pub due_at: Option<DateTime<Utc>>,
1133 #[serde(default, skip_serializing_if = "Option::is_none")]
1134 pub not_before: Option<DateTime<Utc>>,
1135 #[serde(default, skip_serializing_if = "Option::is_none")]
1136 pub snoozed_until: Option<DateTime<Utc>>,
1137 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1138 pub external_refs: Vec<ExternalWorkRef>,
1139 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1140 pub evidence_refs: Vec<WorkEvidenceRef>,
1141 #[serde(default, skip_serializing_if = "Option::is_none")]
1142 pub status: Option<WorkStatus>,
1143}
1144
1145#[derive(Debug, Clone, Serialize, Deserialize)]
1146#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1147pub struct UpdateWorkItemRequest {
1148 pub id: WorkItemId,
1149 #[serde(default, skip_serializing_if = "Option::is_none")]
1150 pub realm_id: Option<String>,
1151 #[serde(default, skip_serializing_if = "Option::is_none")]
1152 pub namespace: Option<WorkNamespace>,
1153 pub expected_revision: u64,
1154 #[serde(default, skip_serializing_if = "Option::is_none")]
1155 pub title: Option<String>,
1156 #[serde(default, skip_serializing_if = "Option::is_none")]
1157 pub description: Option<String>,
1158 #[serde(default, skip_serializing_if = "Option::is_none")]
1159 pub priority: Option<WorkPriority>,
1160 #[serde(default, skip_serializing_if = "Option::is_none")]
1161 pub completion_policy: Option<WorkCompletionPolicy>,
1162 #[serde(default, skip_serializing_if = "Option::is_none")]
1163 pub labels: Option<BTreeSet<String>>,
1164 #[serde(default, skip_serializing_if = "Option::is_none")]
1165 pub due_at: Option<DateTime<Utc>>,
1166 #[serde(default, skip_serializing_if = "Option::is_none")]
1167 pub not_before: Option<DateTime<Utc>>,
1168 #[serde(default, skip_serializing_if = "Option::is_none")]
1169 pub snoozed_until: Option<DateTime<Utc>>,
1170 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1171 pub external_refs: Vec<ExternalWorkRef>,
1172}
1173
1174#[derive(Debug, Clone, Serialize, Deserialize)]
1175#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1176pub struct PolicyEscalateRequest {
1177 pub id: WorkItemId,
1178 #[serde(default, skip_serializing_if = "Option::is_none")]
1179 pub realm_id: Option<String>,
1180 #[serde(default, skip_serializing_if = "Option::is_none")]
1181 pub namespace: Option<WorkNamespace>,
1182 pub expected_revision: u64,
1183 pub authority_projection: AttentionContextProjection,
1184 pub completion_policy: WorkCompletionPolicy,
1185}
1186
1187#[derive(Debug, Clone, Serialize, Deserialize)]
1188#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1189pub struct ClaimWorkItemRequest {
1190 pub id: WorkItemId,
1191 #[serde(default, skip_serializing_if = "Option::is_none")]
1192 pub realm_id: Option<String>,
1193 #[serde(default, skip_serializing_if = "Option::is_none")]
1194 pub namespace: Option<WorkNamespace>,
1195 pub expected_revision: u64,
1196 pub owner: WorkOwner,
1197 #[serde(default, skip_serializing_if = "Option::is_none")]
1198 pub lease_seconds: Option<u64>,
1199 #[serde(default, skip_serializing_if = "Option::is_none")]
1200 pub lease_expires_at: Option<DateTime<Utc>>,
1201}
1202
1203#[derive(Debug, Clone, Serialize, Deserialize)]
1204#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1205pub struct ReleaseWorkItemRequest {
1206 pub id: WorkItemId,
1207 #[serde(default, skip_serializing_if = "Option::is_none")]
1208 pub realm_id: Option<String>,
1209 #[serde(default, skip_serializing_if = "Option::is_none")]
1210 pub namespace: Option<WorkNamespace>,
1211 pub expected_revision: u64,
1212}
1213
1214#[derive(Debug, Clone, Serialize, Deserialize)]
1215#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1216pub struct CloseWorkItemRequest {
1217 pub id: WorkItemId,
1218 #[serde(default, skip_serializing_if = "Option::is_none")]
1219 pub realm_id: Option<String>,
1220 #[serde(default, skip_serializing_if = "Option::is_none")]
1221 pub namespace: Option<WorkNamespace>,
1222 pub expected_revision: u64,
1223 #[serde(default = "default_terminal_status")]
1224 pub status: WorkStatus,
1225}
1226
1227fn default_terminal_status() -> WorkStatus {
1228 WorkStatus::Completed
1229}
1230
1231#[derive(Debug, Clone, Serialize, Deserialize)]
1232#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1233pub struct LinkWorkItemsRequest {
1234 #[serde(default, skip_serializing_if = "Option::is_none")]
1235 pub realm_id: Option<String>,
1236 #[serde(default, skip_serializing_if = "Option::is_none")]
1237 pub namespace: Option<WorkNamespace>,
1238 pub kind: WorkEdgeKind,
1239 pub from_id: WorkItemId,
1240 pub to_id: WorkItemId,
1241}
1242
1243#[derive(Debug, Clone, Serialize, Deserialize)]
1244#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1245pub struct AddEvidenceRequest {
1246 pub id: WorkItemId,
1247 #[serde(default, skip_serializing_if = "Option::is_none")]
1248 pub realm_id: Option<String>,
1249 #[serde(default, skip_serializing_if = "Option::is_none")]
1250 pub namespace: Option<WorkNamespace>,
1251 pub expected_revision: u64,
1252 pub evidence: WorkEvidenceRef,
1253}
1254
1255#[derive(Debug, Clone, Serialize, Deserialize)]
1256#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1257pub struct GoalCreateRequest {
1258 #[serde(default, skip_serializing_if = "Option::is_none")]
1259 pub realm_id: Option<String>,
1260 #[serde(default, skip_serializing_if = "Option::is_none")]
1261 pub namespace: Option<WorkNamespace>,
1262 pub title: String,
1263 #[serde(default, skip_serializing_if = "Option::is_none")]
1264 pub description: Option<String>,
1265 pub target: GoalAttentionTarget,
1266 #[serde(default)]
1267 pub mode: WorkAttentionMode,
1268 #[serde(default)]
1269 pub completion_policy: WorkCompletionPolicy,
1270 #[serde(default)]
1271 pub delegated_authority: AttentionDelegatedAuthority,
1272 #[serde(default)]
1273 pub projection_policy: AttentionProjectionPolicy,
1274}
1275
1276#[derive(Debug, Clone, Serialize, Deserialize)]
1277#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1278pub struct PublicGoalCreateRequest {
1279 #[serde(default, skip_serializing_if = "Option::is_none")]
1280 pub realm_id: Option<String>,
1281 #[serde(default, skip_serializing_if = "Option::is_none")]
1282 pub namespace: Option<WorkNamespace>,
1283 pub title: String,
1284 #[serde(default, skip_serializing_if = "Option::is_none")]
1285 pub description: Option<String>,
1286 pub target: GoalAttentionTarget,
1287 #[serde(default)]
1288 pub mode: WorkAttentionMode,
1289 #[serde(default)]
1290 pub completion_policy: PublicGoalCompletionPolicy,
1291 #[serde(default)]
1292 pub delegated_authority: AttentionDelegatedAuthority,
1293 #[serde(default)]
1294 pub projection_policy: AttentionProjectionPolicy,
1295}
1296
1297impl From<PublicGoalCreateRequest> for GoalCreateRequest {
1298 fn from(request: PublicGoalCreateRequest) -> Self {
1299 Self {
1300 realm_id: request.realm_id,
1301 namespace: request.namespace,
1302 title: request.title,
1303 description: request.description,
1304 target: request.target,
1305 mode: request.mode,
1306 completion_policy: request.completion_policy.into(),
1307 delegated_authority: request.delegated_authority,
1308 projection_policy: request.projection_policy,
1309 }
1310 }
1311}
1312
1313#[derive(Debug, Clone, Serialize, Deserialize)]
1314#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1315pub struct GoalCreateResult {
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 GoalStatusRequest {
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}
1329
1330#[derive(Debug, Clone, Serialize, Deserialize)]
1331#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1332pub struct GoalStatusResult {
1333 pub item: WorkItem,
1334 pub attention: WorkAttentionBinding,
1335}
1336
1337#[derive(Debug, Clone, Serialize, Deserialize)]
1338#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1339pub struct GoalConfirmRequest {
1340 pub binding_id: WorkAttentionBindingId,
1341 #[serde(default, skip_serializing_if = "Option::is_none")]
1342 pub realm_id: Option<String>,
1343 #[serde(default, skip_serializing_if = "Option::is_none")]
1344 pub namespace: Option<WorkNamespace>,
1345 pub expected_revision: u64,
1346 pub evidence: WorkEvidenceRef,
1347 #[serde(skip)]
1348 #[cfg_attr(feature = "schema", schemars(skip))]
1349 pub principal: Option<WorkOwnerKey>,
1350 #[serde(skip)]
1351 #[cfg_attr(feature = "schema", schemars(skip))]
1352 pub trusted_principal: Option<WorkOwnerKey>,
1353}
1354
1355impl GoalConfirmRequest {
1356 pub fn with_trusted_principal(mut self, principal: Option<WorkOwnerKey>) -> Self {
1358 if self.trusted_principal.is_none() {
1359 self.trusted_principal = principal;
1360 }
1361 self
1362 }
1363}
1364
1365#[derive(Debug, Clone, Serialize, Deserialize)]
1366#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1367pub struct GoalConfirmResult {
1368 pub item: WorkItem,
1369 pub attention: WorkAttentionBinding,
1370}
1371
1372#[derive(Debug, Clone, Serialize, Deserialize)]
1373#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1374pub struct GoalRequestCloseRequest {
1375 pub binding_id: WorkAttentionBindingId,
1376 #[serde(default, skip_serializing_if = "Option::is_none")]
1377 pub realm_id: Option<String>,
1378 #[serde(default, skip_serializing_if = "Option::is_none")]
1379 pub namespace: Option<WorkNamespace>,
1380 pub expected_revision: u64,
1381 #[serde(default)]
1382 pub status: GoalTerminalStatus,
1383}
1384
1385#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1386#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1387#[serde(rename_all = "snake_case")]
1388pub enum GoalTerminalStatus {
1389 #[default]
1390 Completed,
1391 Cancelled,
1392 Failed,
1393}
1394
1395impl From<GoalTerminalStatus> for WorkStatus {
1396 fn from(status: GoalTerminalStatus) -> Self {
1397 match status {
1398 GoalTerminalStatus::Completed => Self::Completed,
1399 GoalTerminalStatus::Cancelled => Self::Cancelled,
1400 GoalTerminalStatus::Failed => Self::Failed,
1401 }
1402 }
1403}
1404
1405#[derive(Debug, Clone, Serialize, Deserialize)]
1406#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1407pub struct PublicGoalRequestCloseRequest {
1408 pub binding_id: WorkAttentionBindingId,
1409 #[serde(default, skip_serializing_if = "Option::is_none")]
1410 pub realm_id: Option<String>,
1411 #[serde(default, skip_serializing_if = "Option::is_none")]
1412 pub namespace: Option<WorkNamespace>,
1413 pub expected_revision: u64,
1414 #[serde(default)]
1415 pub status: GoalTerminalStatus,
1416}
1417
1418impl From<PublicGoalRequestCloseRequest> for GoalRequestCloseRequest {
1419 fn from(request: PublicGoalRequestCloseRequest) -> Self {
1420 Self {
1421 binding_id: request.binding_id,
1422 realm_id: request.realm_id,
1423 namespace: request.namespace,
1424 expected_revision: request.expected_revision,
1425 status: request.status,
1426 }
1427 }
1428}
1429
1430#[derive(Debug, Clone, Serialize, Deserialize)]
1431#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1432pub struct GoalRequestCloseResult {
1433 pub item: WorkItem,
1434 pub attention: WorkAttentionBinding,
1435}
1436
1437#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1438#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1439pub struct AttentionListRequest {
1440 #[serde(default, skip_serializing_if = "Option::is_none")]
1441 pub realm_id: Option<String>,
1442 #[serde(default, skip_serializing_if = "Option::is_none")]
1443 pub namespace: Option<WorkNamespace>,
1444 #[serde(default, skip_serializing_if = "Option::is_none")]
1445 pub target: Option<WorkAttentionTarget>,
1446 #[serde(default, skip_serializing_if = "Option::is_none")]
1447 pub status: Option<WorkAttentionStatus>,
1448}
1449
1450#[derive(Debug, Clone, Serialize, Deserialize)]
1451#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1452pub struct AttentionListResult {
1453 pub attention: Vec<WorkAttentionBinding>,
1454}
1455
1456#[derive(Debug, Clone, Serialize, Deserialize)]
1457#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1458pub struct AttentionBindingRequest {
1459 pub binding_id: WorkAttentionBindingId,
1460 #[serde(default, skip_serializing_if = "Option::is_none")]
1461 pub realm_id: Option<String>,
1462 #[serde(default, skip_serializing_if = "Option::is_none")]
1463 pub namespace: Option<WorkNamespace>,
1464}
1465
1466#[derive(Debug, Clone, Serialize, Deserialize)]
1467#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1468pub struct AttentionPauseRequest {
1469 pub binding_id: WorkAttentionBindingId,
1470 #[serde(default, skip_serializing_if = "Option::is_none")]
1471 pub realm_id: Option<String>,
1472 #[serde(default, skip_serializing_if = "Option::is_none")]
1473 pub namespace: Option<WorkNamespace>,
1474 pub expected_revision: u64,
1475 #[serde(default, skip_serializing_if = "Option::is_none")]
1476 pub until: Option<DateTime<Utc>>,
1477}
1478
1479#[derive(Debug, Clone, Serialize, Deserialize)]
1480#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1481pub struct AttentionResumeRequest {
1482 pub binding_id: WorkAttentionBindingId,
1483 #[serde(default, skip_serializing_if = "Option::is_none")]
1484 pub realm_id: Option<String>,
1485 #[serde(default, skip_serializing_if = "Option::is_none")]
1486 pub namespace: Option<WorkNamespace>,
1487 pub expected_revision: u64,
1488}
1489
1490#[derive(Debug, Clone, Serialize, Deserialize)]
1491#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1492pub struct AttentionReassignRequest {
1493 pub binding_id: WorkAttentionBindingId,
1494 #[serde(default, skip_serializing_if = "Option::is_none")]
1495 pub realm_id: Option<String>,
1496 #[serde(default, skip_serializing_if = "Option::is_none")]
1497 pub namespace: Option<WorkNamespace>,
1498 pub expected_revision: u64,
1499 pub authority_projection: AttentionContextProjection,
1500 pub target: GoalAttentionTarget,
1501}
1502
1503#[derive(Debug, Clone, Serialize, Deserialize)]
1504#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1505pub struct AttentionBindingResult {
1506 pub attention: WorkAttentionBinding,
1507}
1508
1509#[derive(Debug, Clone, Serialize, Deserialize)]
1510#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1511pub struct AttentionReassignResult {
1512 pub previous: WorkAttentionBinding,
1513 pub attention: WorkAttentionBinding,
1514}
1515
1516#[derive(Debug, Clone, Serialize, Deserialize)]
1524pub struct BreakGlassAttentionReassignRequest {
1525 pub binding_id: WorkAttentionBindingId,
1526 #[serde(default, skip_serializing_if = "Option::is_none")]
1527 pub realm_id: Option<String>,
1528 #[serde(default, skip_serializing_if = "Option::is_none")]
1529 pub namespace: Option<WorkNamespace>,
1530 pub expected_revision: u64,
1531 pub target: GoalAttentionTarget,
1532 pub principal: String,
1535 pub reason: String,
1537}
1538
1539#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1543pub struct AttentionPruneRequest {
1544 #[serde(default, skip_serializing_if = "Option::is_none")]
1545 pub realm_id: Option<String>,
1546 #[serde(default, skip_serializing_if = "Option::is_none")]
1547 pub namespace: Option<WorkNamespace>,
1548 #[serde(default, skip_serializing_if = "Option::is_none")]
1551 pub updated_before: Option<DateTime<Utc>>,
1552}
1553
1554#[derive(Debug, Clone, Serialize, Deserialize)]
1555pub struct AttentionPruneResult {
1556 pub pruned: u64,
1557}
1558
1559#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1560#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1561#[serde(rename_all = "snake_case")]
1562pub enum AttentionContinueOutcome {
1563 Accepted,
1564 Deduplicated,
1565 Rejected,
1566}
1567
1568#[derive(Debug, Clone, Serialize, Deserialize)]
1569#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1570pub struct AttentionContinueResult {
1571 pub outcome: AttentionContinueOutcome,
1572 #[serde(default, skip_serializing_if = "Option::is_none")]
1573 pub input_id: Option<String>,
1574 #[serde(default, skip_serializing_if = "Option::is_none")]
1575 pub existing_id: Option<String>,
1576 #[serde(default, skip_serializing_if = "Option::is_none")]
1577 pub reason: Option<String>,
1578}
1579
1580#[derive(Debug, Clone, Serialize, Deserialize)]
1581#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1582pub struct AttentionProjectionRequest {
1583 pub binding_id: WorkAttentionBindingId,
1584 #[serde(default, skip_serializing_if = "Option::is_none")]
1585 pub realm_id: Option<String>,
1586 #[serde(default, skip_serializing_if = "Option::is_none")]
1587 pub namespace: Option<WorkNamespace>,
1588}
1589
1590#[derive(Debug, Clone, Serialize, Deserialize)]
1591#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1592pub struct AttentionProjectionResult {
1593 pub projection: AttentionContextProjection,
1594}
1595
1596#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1597#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1598pub struct AttentionContextProjection {
1599 pub binding_id: WorkAttentionBindingId,
1600 pub work_ref: WorkItemRef,
1601 pub mode: WorkAttentionMode,
1602 pub binding_revision: u64,
1603 pub item_revision: u64,
1604 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1605 pub parent_refs: Vec<WorkItemRef>,
1606 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1607 pub parent_context: Vec<AttentionProjectionParentContext>,
1608 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1609 pub evidence_refs: Vec<WorkEvidenceRef>,
1610 pub authority: ProjectedAttentionAuthority,
1611 pub text: AttentionProjectionText,
1612}
1613
1614#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1615#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1616pub struct AttentionProjectionParentContext {
1617 pub work_ref: WorkItemRef,
1618 pub status: WorkStatus,
1619 pub revision: u64,
1620}
1621
1622#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1623#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1624pub struct ProjectedAttentionAuthority {
1625 pub can_get: bool,
1626 pub can_add_evidence: bool,
1627 pub can_release: bool,
1628 pub can_update: bool,
1629 pub can_block: bool,
1630 pub can_create: bool,
1631 pub can_link: bool,
1632 pub can_link_parent: bool,
1633 pub can_link_related: bool,
1634 pub can_link_derived_from: bool,
1635 #[serde(default)]
1636 pub can_close_own_review_item: bool,
1637 pub can_close_if_policy_allows: bool,
1638}
1639
1640#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1641#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1642pub struct AttentionProjectionText {
1643 pub title: String,
1644 pub rendered: String,
1645 pub truncated: bool,
1646}
1647
1648#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1649#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1650pub struct WorkItemFilter {
1651 #[serde(default, skip_serializing_if = "Option::is_none")]
1652 pub realm_id: Option<String>,
1653 #[serde(default, skip_serializing_if = "Option::is_none")]
1654 pub namespace: Option<WorkNamespace>,
1655 #[serde(default)]
1656 pub all_namespaces: bool,
1657 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1658 pub statuses: Vec<WorkStatus>,
1659 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1660 pub labels: Vec<String>,
1661 #[serde(default)]
1662 pub include_terminal: bool,
1663 #[serde(default, skip_serializing_if = "Option::is_none")]
1664 pub limit: Option<usize>,
1665}
1666
1667#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1668#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1669pub struct ReadyWorkFilter {
1670 #[serde(default, skip_serializing_if = "Option::is_none")]
1671 pub realm_id: Option<String>,
1672 #[serde(default, skip_serializing_if = "Option::is_none")]
1673 pub namespace: Option<WorkNamespace>,
1674 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1675 pub labels: Vec<String>,
1676 #[serde(default, skip_serializing_if = "Option::is_none")]
1677 pub limit: Option<usize>,
1678}
1679
1680#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1681#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1682pub struct WorkGraphSnapshotFilter {
1683 #[serde(default, skip_serializing_if = "Option::is_none")]
1684 pub realm_id: Option<String>,
1685 #[serde(default, skip_serializing_if = "Option::is_none")]
1686 pub namespace: Option<WorkNamespace>,
1687 #[serde(default)]
1688 pub all_namespaces: bool,
1689 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1690 pub statuses: Vec<WorkStatus>,
1691 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1692 pub labels: Vec<String>,
1693 #[serde(default)]
1694 pub include_terminal: bool,
1695 #[serde(default, skip_serializing_if = "Option::is_none")]
1696 pub limit: Option<usize>,
1697}
1698
1699#[derive(Debug, Clone, Serialize, Deserialize)]
1702#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1703pub struct WorkGraphIdParams {
1704 pub id: WorkItemId,
1705 #[serde(default, skip_serializing_if = "Option::is_none")]
1706 pub realm_id: Option<String>,
1707 #[serde(default, skip_serializing_if = "Option::is_none")]
1708 pub namespace: Option<WorkNamespace>,
1709}
1710
1711#[derive(Debug, Clone, Serialize, Deserialize)]
1712#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1713pub struct WorkGraphSnapshot {
1714 pub realm_id: String,
1715 #[serde(default, skip_serializing_if = "Option::is_none")]
1716 pub namespace: Option<WorkNamespace>,
1717 pub all_namespaces: bool,
1718 pub captured_at: DateTime<Utc>,
1719 #[serde(default, skip_serializing_if = "Option::is_none")]
1720 pub event_high_water_mark: Option<i64>,
1721 pub items: Vec<WorkItem>,
1722 pub edges: Vec<WorkEdge>,
1723 #[serde(default)]
1724 pub attention: Vec<WorkAttentionBinding>,
1725 pub ready_item_ids: Vec<WorkItemId>,
1726}
1727
1728#[derive(Debug, Clone, Serialize, Deserialize)]
1729#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1730pub struct WorkGraphItemsResponse {
1731 pub items: Vec<WorkItem>,
1732}
1733
1734#[derive(Debug, Clone, Serialize, Deserialize)]
1735#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1736pub struct WorkGraphEventsResponse {
1737 pub events: Vec<WorkGraphEvent>,
1738}
1739
1740#[cfg(test)]
1741#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
1742mod tests {
1743 use super::*;
1744 use crate::machine::WorkGraphMachine;
1745
1746 fn machine_item() -> WorkItem {
1747 WorkGraphMachine::create_item(
1748 CreateWorkItemRequest {
1749 title: "deserialize-authority".to_string(),
1750 ..Default::default()
1751 },
1752 "realm".to_string(),
1753 WorkNamespace::default(),
1754 Utc::now(),
1755 )
1756 .expect("machine create_item")
1757 .0
1758 }
1759
1760 #[test]
1761 fn work_item_round_trip_preserves_machine_state() {
1762 let item = machine_item();
1763 let json = serde_json::to_string(&item).expect("serialize work item");
1764 let decoded: WorkItem = serde_json::from_str(&json).expect("deserialize work item");
1765 assert_eq!(
1766 decoded, item,
1767 "round-trip must preserve the whole work item"
1768 );
1769 assert_eq!(
1770 decoded.machine_state, item.machine_state,
1771 "round-trip must preserve machine-owned lifecycle authority verbatim"
1772 );
1773 }
1774
1775 #[test]
1776 fn work_item_without_machine_state_fails_closed() {
1777 let item = machine_item();
1778 let mut value = serde_json::to_value(&item).expect("serialize work item to value");
1779 value
1780 .as_object_mut()
1781 .expect("work item json object")
1782 .remove("machine_state");
1783
1784 let result: Result<WorkItem, _> = serde_json::from_value(value);
1785 let err = result.expect_err(
1786 "deserializing a WorkItem without machine_state must fail closed, \
1787 never fabricate lifecycle/revision authority from projected fields",
1788 );
1789 assert!(
1790 err.to_string().contains("machine_state"),
1791 "fail-closed error must cite the missing machine_state authority, got: {err}"
1792 );
1793 }
1794}