Skip to main content

ralph/contracts/
machine.rs

1//! Versioned machine-contract documents for app/CLI integration.
2//!
3//! Responsibilities:
4//! - Define the stable JSON documents consumed by the macOS app via `ralph machine ...`.
5//! - Centralize machine-only request/response and event envelope types.
6//! - Provide schema-friendly wrappers around queue/config/task/run data.
7//!
8//! Not handled here:
9//! - Command execution or clap wiring.
10//! - Human CLI rendering.
11//! - Queue/task/run business logic.
12//!
13//! Invariants/assumptions:
14//! - Every machine document includes an explicit `version`.
15//! - Breaking wire changes require version bumps.
16//! - Run events are emitted as NDJSON envelopes ordered by occurrence.
17
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20use serde_json::Value as JsonValue;
21
22use super::{
23    BlockingState, CliSpec, Config, GitPublishMode, GitRevertMode, QueueFile, RunnerApprovalMode,
24    Task,
25};
26
27pub const MACHINE_SYSTEM_INFO_VERSION: u32 = 1;
28pub const MACHINE_QUEUE_READ_VERSION: u32 = 1;
29pub const MACHINE_QUEUE_VALIDATE_VERSION: u32 = 1;
30pub const MACHINE_QUEUE_REPAIR_VERSION: u32 = 1;
31pub const MACHINE_QUEUE_UNDO_VERSION: u32 = 1;
32pub const MACHINE_CONFIG_RESOLVE_VERSION: u32 = 3;
33pub const MACHINE_TASK_CREATE_VERSION: u32 = 1;
34pub const MACHINE_TASK_MUTATION_VERSION: u32 = 2;
35pub const MACHINE_GRAPH_READ_VERSION: u32 = 1;
36pub const MACHINE_DASHBOARD_READ_VERSION: u32 = 1;
37pub const MACHINE_DECOMPOSE_VERSION: u32 = 2;
38pub const MACHINE_RUN_EVENT_VERSION: u32 = 3;
39pub const MACHINE_RUN_SUMMARY_VERSION: u32 = 2;
40pub const MACHINE_DOCTOR_REPORT_VERSION: u32 = 2;
41pub const MACHINE_PARALLEL_STATUS_VERSION: u32 = 2;
42pub const MACHINE_CLI_SPEC_VERSION: u32 = 2;
43pub const MACHINE_ERROR_VERSION: u32 = 1;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
46#[serde(rename_all = "snake_case")]
47pub enum MachineErrorCode {
48    CliUnavailable,
49    PermissionDenied,
50    ConfigIncompatible,
51    ParseError,
52    NetworkError,
53    QueueCorrupted,
54    ResourceBusy,
55    VersionMismatch,
56    TaskMutationConflict,
57    Unknown,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
61#[serde(deny_unknown_fields)]
62pub struct MachineErrorDocument {
63    pub version: u32,
64    pub code: MachineErrorCode,
65    pub message: String,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub detail: Option<String>,
68    pub retryable: bool,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
72#[serde(deny_unknown_fields)]
73pub struct MachineSystemInfoDocument {
74    pub version: u32,
75    pub cli_version: String,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
79#[serde(deny_unknown_fields)]
80pub struct MachineQueuePaths {
81    pub repo_root: String,
82    pub queue_path: String,
83    pub done_path: String,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub project_config_path: Option<String>,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub global_config_path: Option<String>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
91#[serde(deny_unknown_fields)]
92pub struct MachineQueueReadDocument {
93    pub version: u32,
94    pub paths: MachineQueuePaths,
95    pub active: QueueFile,
96    pub done: QueueFile,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub next_runnable_task_id: Option<String>,
99    #[schemars(schema_with = "json_value_schema")]
100    pub runnability: JsonValue,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
104#[serde(deny_unknown_fields)]
105pub struct MachineContinuationAction {
106    pub title: String,
107    pub command: String,
108    pub detail: String,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
112#[serde(deny_unknown_fields)]
113pub struct MachineContinuationSummary {
114    pub headline: String,
115    pub detail: String,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub blocking: Option<BlockingState>,
118    #[serde(default)]
119    pub next_steps: Vec<MachineContinuationAction>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
123#[serde(deny_unknown_fields)]
124pub struct MachineValidationWarning {
125    pub task_id: String,
126    pub message: String,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
130#[serde(deny_unknown_fields)]
131pub struct MachineQueueValidateDocument {
132    pub version: u32,
133    pub valid: bool,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub blocking: Option<BlockingState>,
136    #[serde(default)]
137    pub warnings: Vec<MachineValidationWarning>,
138    pub continuation: MachineContinuationSummary,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
142#[serde(deny_unknown_fields)]
143pub struct MachineQueueRepairDocument {
144    pub version: u32,
145    pub dry_run: bool,
146    pub changed: bool,
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub blocking: Option<BlockingState>,
149    #[schemars(schema_with = "json_value_schema")]
150    pub report: JsonValue,
151    pub continuation: MachineContinuationSummary,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
155#[serde(deny_unknown_fields)]
156pub struct MachineQueueUndoDocument {
157    pub version: u32,
158    pub dry_run: bool,
159    pub restored: bool,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub blocking: Option<BlockingState>,
162    #[schemars(schema_with = "option_json_value_schema")]
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub result: Option<JsonValue>,
165    pub continuation: MachineContinuationSummary,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
169#[serde(deny_unknown_fields)]
170pub struct MachineResumeDecision {
171    pub status: String,
172    pub scope: String,
173    pub reason: String,
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub task_id: Option<String>,
176    pub message: String,
177    pub detail: String,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
181#[serde(deny_unknown_fields)]
182pub struct MachineConfigResolveDocument {
183    pub version: u32,
184    pub paths: MachineQueuePaths,
185    pub safety: MachineConfigSafetySummary,
186    pub config: Config,
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub resume_preview: Option<MachineResumeDecision>,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
192#[serde(deny_unknown_fields)]
193pub struct MachineConfigSafetySummary {
194    pub repo_trusted: bool,
195    pub dirty_repo: bool,
196    pub git_publish_mode: GitPublishMode,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub approval_mode: Option<RunnerApprovalMode>,
199    pub ci_gate_enabled: bool,
200    pub git_revert_mode: GitRevertMode,
201    pub parallel_configured: bool,
202    pub execution_interactivity: String,
203    pub interactive_approval_supported: bool,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
207#[serde(deny_unknown_fields)]
208pub struct MachineCliSpecDocument {
209    pub version: u32,
210    pub spec: CliSpec,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
214#[serde(deny_unknown_fields)]
215pub struct MachineTaskCreateRequest {
216    pub version: u32,
217    pub title: String,
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub description: Option<String>,
220    pub priority: String,
221    #[serde(default)]
222    pub tags: Vec<String>,
223    #[serde(default, skip_serializing_if = "Vec::is_empty")]
224    pub scope: Vec<String>,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub template: Option<String>,
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub target: Option<String>,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
232#[serde(deny_unknown_fields)]
233pub struct MachineTaskCreateDocument {
234    pub version: u32,
235    pub task: Task,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
239#[serde(deny_unknown_fields)]
240pub struct MachineTaskMutationDocument {
241    pub version: u32,
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub blocking: Option<BlockingState>,
244    #[schemars(schema_with = "json_value_schema")]
245    pub report: JsonValue,
246    pub continuation: MachineContinuationSummary,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
250#[serde(deny_unknown_fields)]
251pub struct MachineGraphReadDocument {
252    pub version: u32,
253    #[schemars(schema_with = "json_value_schema")]
254    pub graph: JsonValue,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
258#[serde(deny_unknown_fields)]
259pub struct MachineDashboardReadDocument {
260    pub version: u32,
261    #[schemars(schema_with = "json_value_schema")]
262    pub dashboard: JsonValue,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
266#[serde(deny_unknown_fields)]
267pub struct MachineDecomposeDocument {
268    pub version: u32,
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub blocking: Option<BlockingState>,
271    #[schemars(schema_with = "json_value_schema")]
272    pub result: JsonValue,
273    pub continuation: MachineContinuationSummary,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
277#[serde(deny_unknown_fields)]
278pub struct MachineDoctorReportDocument {
279    pub version: u32,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub blocking: Option<BlockingState>,
282    #[schemars(schema_with = "json_value_schema")]
283    pub report: JsonValue,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
287#[serde(deny_unknown_fields)]
288pub struct MachineParallelStatusDocument {
289    pub version: u32,
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub blocking: Option<BlockingState>,
292    pub continuation: MachineContinuationSummary,
293    #[schemars(schema_with = "json_value_schema")]
294    pub status: JsonValue,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
298#[serde(rename_all = "snake_case")]
299pub enum MachineRunEventKind {
300    RunStarted,
301    QueueSnapshot,
302    ConfigResolved,
303    ResumeDecision,
304    TaskSelected,
305    PhaseEntered,
306    PhaseCompleted,
307    RunnerOutput,
308    BlockedStateChanged,
309    BlockedStateCleared,
310    Warning,
311    RunFinished,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
315#[serde(deny_unknown_fields)]
316pub struct MachineRunEventEnvelope {
317    pub version: u32,
318    pub kind: MachineRunEventKind,
319    pub timestamp: String,
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub run_mode: Option<String>,
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub task_id: Option<String>,
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub phase: Option<String>,
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub exit_code: Option<i32>,
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub message: Option<String>,
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub stream: Option<String>,
332    #[schemars(schema_with = "option_json_value_schema")]
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub payload: Option<JsonValue>,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
338#[serde(deny_unknown_fields)]
339pub struct MachineRunSummaryDocument {
340    pub version: u32,
341    pub task_id: Option<String>,
342    pub exit_code: i32,
343    pub outcome: String,
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub blocking: Option<BlockingState>,
346}
347
348fn json_value_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
349    <JsonValue as JsonSchema>::json_schema(generator)
350}
351
352fn option_json_value_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
353    <Option<JsonValue> as JsonSchema>::json_schema(generator)
354}