Skip to main content

runx_contracts/
handoff.rs

1//! Handoff boundary contracts: per-signal records (`runx.handoff_signal.v1`)
2//! and the rolled-up handoff state (`runx.handoff_state.v1`).
3use serde::{Deserialize, Serialize};
4
5use crate::schema::{IsoDateTime, NonEmptyString, RunxSchema};
6
7#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
8pub enum HandoffSignalSchema {
9    #[serde(rename = "runx.handoff_signal.v1")]
10    V1,
11}
12
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
14#[serde(rename_all = "snake_case")]
15pub enum HandoffSignalSource {
16    PullRequestComment,
17    PullRequestReview,
18    PullRequestState,
19    IssueComment,
20    DiscussionReply,
21    EmailReply,
22    DirectMessageReply,
23    ManualNote,
24    SystemEvent,
25}
26
27#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
28#[serde(rename_all = "snake_case")]
29pub enum HandoffDisposition {
30    Acknowledged,
31    Interested,
32    RequestedChanges,
33    Accepted,
34    ApprovedToSend,
35    Merged,
36    Declined,
37    RequestedNoContact,
38    Rerouted,
39}
40
41/// The actor attribution carried on a handoff signal.
42#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
43#[serde(deny_unknown_fields)]
44pub struct HandoffSignalActor {
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub actor_id: Option<NonEmptyString>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub display_name: Option<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub role: Option<NonEmptyString>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub provider_identity: Option<NonEmptyString>,
53}
54
55/// The handoff signal's own source reference (a distinct, smaller shape than the
56/// general `Reference`: only `type`/`uri`/`label`/`recorded_at`).
57#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
58#[serde(deny_unknown_fields)]
59pub struct HandoffSignalSourceRef {
60    #[serde(rename = "type")]
61    pub ref_type: NonEmptyString,
62    pub uri: NonEmptyString,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub label: Option<String>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub recorded_at: Option<IsoDateTime>,
67}
68
69#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
70#[serde(deny_unknown_fields)]
71#[runx_schema(id = "runx.handoff_signal.v1")]
72pub struct HandoffSignal {
73    pub schema: HandoffSignalSchema,
74    pub signal_id: NonEmptyString,
75    pub handoff_id: NonEmptyString,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub boundary_kind: Option<NonEmptyString>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub target_repo: Option<NonEmptyString>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub target_locator: Option<NonEmptyString>,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub contact_locator: Option<NonEmptyString>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub thread_locator: Option<NonEmptyString>,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub outbox_entry_id: Option<NonEmptyString>,
88    pub source: HandoffSignalSource,
89    pub disposition: HandoffDisposition,
90    pub recorded_at: IsoDateTime,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub actor: Option<HandoffSignalActor>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub notes: Option<String>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub labels: Option<Vec<NonEmptyString>>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub source_ref: Option<HandoffSignalSourceRef>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub metadata: Option<crate::JsonObject>,
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
104pub enum HandoffStateSchema {
105    #[serde(rename = "runx.handoff_state.v1")]
106    V1,
107}
108
109#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
110#[serde(rename_all = "snake_case")]
111pub enum HandoffStatus {
112    AwaitingResponse,
113    Engaged,
114    NeedsRevision,
115    Accepted,
116    ApprovedToSend,
117    Completed,
118    Declined,
119    Rerouted,
120    Suppressed,
121}
122
123#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
124#[serde(rename_all = "snake_case")]
125pub enum SuppressionReason {
126    RequestedNoContact,
127    RemoveRequest,
128    OperatorBlock,
129    LegalRequest,
130}
131
132#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
133#[serde(deny_unknown_fields)]
134#[runx_schema(id = "runx.handoff_state.v1")]
135pub struct HandoffState {
136    pub schema: HandoffStateSchema,
137    pub handoff_id: NonEmptyString,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub boundary_kind: Option<NonEmptyString>,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub target_repo: Option<NonEmptyString>,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub target_locator: Option<NonEmptyString>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub contact_locator: Option<NonEmptyString>,
146    pub status: HandoffStatus,
147    pub signal_count: u64,
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub last_signal_id: Option<NonEmptyString>,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub last_signal_at: Option<IsoDateTime>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub last_signal_disposition: Option<HandoffDisposition>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub suppression_record_id: Option<NonEmptyString>,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub suppression_reason: Option<SuppressionReason>,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub summary: Option<String>,
160}