Skip to main content

atm_core/boundary/
mod.rs

1//! Phase R boundary skeleton contracts.
2
3use crate::error::AtmError;
4use crate::protocol::{FramePayload, RequestEnvelope, RequestId, ResponseEnvelope};
5pub use crate::protocol::{NotificationEvent, RuntimeStatusSnapshot};
6use crate::schema::AtmMessageId;
7use crate::types::{AgentName, PaneId, TaskId, TeamName};
8pub use atm_storage::contract::{AckTransition, Message, MessageKey, TaskState};
9pub use atm_storage::{
10    BuiltInNudgeTemplateKind, NudgeTemplateOverrideStore, TeamNudgeTemplateOverrideMode,
11    TeamNudgeTemplateOverrideRow,
12};
13
14/// Workspace-convention seal only; not compiler-enforced outside this crate.
15///
16/// Only ATM workspace crates may implement boundary traits. Enforced by
17/// boundary lint, forbidden-edge rules, and review gates; this is a documented
18/// enforcement limitation until the trait surfaces move behind stricter crate
19/// extraction or compiler-enforced sealing.
20#[doc(hidden)]
21pub mod sealed {
22    pub trait Sealed {}
23}
24
25mod mail;
26mod runtime;
27mod store;
28
29// Intentional re-export façade: the boundary module is the stable public import
30// surface for Phase R/AA contracts, so callers should not need to know whether
31// an item lives in `mail` or `store`.
32pub use mail::*;
33pub use runtime::*;
34pub use store::*;
35
36/// BOUNDARY-AtmProtocol — see docs/atm-core/boundaries.md.
37pub trait AtmProtocol: sealed::Sealed {
38    /// # Errors
39    ///
40    /// Returns `AtmError` when a protocol request envelope cannot be converted
41    /// into a frame payload.
42    fn request_to_frame(
43        &self,
44        request_id: RequestId,
45        request: RequestEnvelope,
46    ) -> Result<FramePayload, AtmError>;
47
48    /// # Errors
49    ///
50    /// Returns `AtmError` when a frame payload cannot be decoded into a
51    /// protocol request envelope.
52    fn request_from_frame(
53        &self,
54        frame: FramePayload,
55    ) -> Result<(RequestId, RequestEnvelope), AtmError>;
56
57    /// # Errors
58    ///
59    /// Returns `AtmError` when a protocol response envelope cannot be
60    /// converted into a frame payload.
61    fn response_to_frame(
62        &self,
63        request_id: RequestId,
64        response: ResponseEnvelope,
65    ) -> Result<FramePayload, AtmError>;
66
67    /// # Errors
68    ///
69    /// Returns `AtmError` when a frame payload cannot be decoded into a
70    /// protocol response envelope.
71    fn response_from_frame(
72        &self,
73        frame: FramePayload,
74    ) -> Result<(RequestId, ResponseEnvelope), AtmError>;
75}
76
77/// BOUNDARY-ClientTransport — see docs/atm-core/boundaries.md.
78pub trait ClientTransport: sealed::Sealed + Send + Sync {
79    /// # Errors
80    ///
81    /// Returns `AtmError` when the framed request cannot be delivered or when
82    /// the peer returns an unrecoverable protocol response.
83    fn send(&self, request: RequestEnvelope) -> Result<ResponseEnvelope, AtmError>;
84}
85
86/// BOUNDARY-ServerTransport — see docs/atm-core/boundaries.md.
87pub trait ServerTransport: sealed::Sealed {
88    /// # Errors
89    ///
90    /// Returns `AtmError` when framing, transport serving, or dispatch handoff
91    /// cannot proceed reliably.
92    fn serve(
93        &self,
94        dispatcher: std::sync::Arc<dyn RequestDispatcher + Send + Sync>,
95    ) -> Result<(), AtmError>;
96}
97
98/// BOUNDARY-RequestDispatcher — see docs/atm-core/boundaries.md.
99pub trait RequestDispatcher: sealed::Sealed + Send + Sync {
100    /// # Errors
101    ///
102    /// Returns `AtmError` when protocol request routing or handler dispatch
103    /// cannot produce a valid response.
104    fn dispatch(&self, request: RequestEnvelope) -> Result<ResponseEnvelope, AtmError>;
105}
106
107/// BOUNDARY-StatusSource — see docs/atm-core/boundaries.md.
108pub trait StatusSource: sealed::Sealed {
109    /// # Errors
110    ///
111    /// Returns `AtmError` when a runtime status snapshot cannot be collected.
112    fn snapshot(&self) -> Result<RuntimeStatusSnapshot, AtmError>;
113}
114
115#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
116pub struct PostSendHookEvent {
117    pub sender: AgentName,
118    pub sender_team: TeamName,
119    pub recipient: AgentName,
120    pub recipient_team: TeamName,
121    pub message_id: AtmMessageId,
122    pub description: String,
123    pub requires_ack: bool,
124    pub is_ack: bool,
125    pub task_id: Option<TaskId>,
126    pub recipient_pane_id: Option<PaneId>,
127}
128
129pub fn built_in_nudge_template_kind_from_post_send_event(
130    event: &PostSendHookEvent,
131) -> BuiltInNudgeTemplateKind {
132    match (event.is_ack, event.task_id.is_some(), event.requires_ack) {
133        (true, true, _) => BuiltInNudgeTemplateKind::AcknowledgeTask,
134        (true, false, _) => BuiltInNudgeTemplateKind::Acknowledge,
135        (false, true, true) => BuiltInNudgeTemplateKind::DeliveryTaskAck,
136        (false, true, false) => BuiltInNudgeTemplateKind::DeliveryTask,
137        (false, false, true) => BuiltInNudgeTemplateKind::DeliveryAck,
138        (false, false, false) => BuiltInNudgeTemplateKind::Delivery,
139    }
140}
141
142#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
143pub struct ResolvedBuiltInNudgeTemplate {
144    pub kind: BuiltInNudgeTemplateKind,
145    pub body: Option<String>,
146}
147
148#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
149#[serde(rename_all = "snake_case")]
150pub enum BuiltInNudgeSinkTarget {
151    Tmux,
152    Graft,
153}
154
155#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
156pub struct InternalNudgeEnvelope {
157    pub event: PostSendHookEvent,
158    pub sink_target: BuiltInNudgeSinkTarget,
159    pub template: ResolvedBuiltInNudgeTemplate,
160}
161
162#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
163pub struct LocalTmuxNudgeTarget {
164    pub pane_id: PaneId,
165    pub rendered_nudge: String,
166}
167
168#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
169pub struct GraftNudgeTarget {
170    pub recipient: AgentName,
171    pub recipient_team: TeamName,
172}
173
174#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
175pub enum PostSendBuiltInTarget {
176    LocalTmux(LocalTmuxNudgeTarget),
177    Graft(GraftNudgeTarget),
178}
179
180#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
181pub struct BuiltInPostSendDispatch {
182    pub event: PostSendHookEvent,
183    pub target: PostSendBuiltInTarget,
184}
185
186#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
187#[serde(rename_all = "snake_case")]
188pub enum PostSendEmissionPath {
189    ExternalHook,
190    LocalTmux,
191    GraftPort,
192}
193
194#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
195pub struct HookExecutionSummary {
196    matched_rules: usize,
197    succeeded_rules: usize,
198    failed_rules: usize,
199}
200
201impl HookExecutionSummary {
202    pub fn new(
203        matched_rules: usize,
204        succeeded_rules: usize,
205        failed_rules: usize,
206    ) -> Result<Self, AtmError> {
207        if succeeded_rules + failed_rules > matched_rules {
208            return Err(AtmError::validation(format!(
209                "invalid post-send hook execution summary: succeeded ({succeeded_rules}) + failed ({failed_rules}) exceeds matched ({matched_rules})"
210            ))
211            .with_recovery(
212                "Count each matching post-send hook rule exactly once before constructing hook execution summary state.",
213            ));
214        }
215        Ok(Self {
216            matched_rules,
217            succeeded_rules,
218            failed_rules,
219        })
220    }
221
222    pub const fn matched_rules(&self) -> usize {
223        self.matched_rules
224    }
225
226    pub const fn succeeded_rules(&self) -> usize {
227        self.succeeded_rules
228    }
229
230    pub const fn failed_rules(&self) -> usize {
231        self.failed_rules
232    }
233}
234
235#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
236pub enum PostSendEmissionOutcome {
237    NoCapability {
238        hook_summary: HookExecutionSummary,
239    },
240    Delivered {
241        path: PostSendEmissionPath,
242        hook_summary: HookExecutionSummary,
243    },
244    Failed {
245        hook_summary: HookExecutionSummary,
246        warning: crate::send::WarningEntry,
247    },
248}
249/// BOUNDARY-PostSendHookEmitter — see docs/atm-core/boundaries.md.
250pub trait PostSendHookEmitter: sealed::Sealed + Send + Sync {
251    /// # Errors
252    ///
253    /// Returns `AtmError` when one direct post-send emission attempt fails
254    /// after durable message persistence has already succeeded.
255    fn emit_post_send(
256        &self,
257        dispatch: &BuiltInPostSendDispatch,
258    ) -> Result<PostSendEmissionPath, AtmError>;
259}
260
261/// BOUNDARY-GraftPostSendPort — see docs/atm-core/boundaries.md.
262pub trait GraftPostSendPort: sealed::Sealed + Send + Sync {
263    /// # Errors
264    ///
265    /// Returns `AtmError` when one graft-backed post-send emission attempt
266    /// fails after durable message persistence has already succeeded.
267    fn deliver_post_send(
268        &self,
269        event: &PostSendHookEvent,
270        target: &GraftNudgeTarget,
271    ) -> Result<(), AtmError>;
272}