Skip to main content

meerkat_workgraph/
types.rs

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