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;
43
44#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
45#[serde(deny_unknown_fields)]
46pub struct MachineSystemInfoDocument {
47    pub version: u32,
48    pub cli_version: String,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
52#[serde(deny_unknown_fields)]
53pub struct MachineQueuePaths {
54    pub repo_root: String,
55    pub queue_path: String,
56    pub done_path: String,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub project_config_path: Option<String>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub global_config_path: Option<String>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
64#[serde(deny_unknown_fields)]
65pub struct MachineQueueReadDocument {
66    pub version: u32,
67    pub paths: MachineQueuePaths,
68    pub active: QueueFile,
69    pub done: QueueFile,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub next_runnable_task_id: Option<String>,
72    #[schemars(schema_with = "json_value_schema")]
73    pub runnability: JsonValue,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
77#[serde(deny_unknown_fields)]
78pub struct MachineContinuationAction {
79    pub title: String,
80    pub command: String,
81    pub detail: String,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
85#[serde(deny_unknown_fields)]
86pub struct MachineContinuationSummary {
87    pub headline: String,
88    pub detail: String,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub blocking: Option<BlockingState>,
91    #[serde(default)]
92    pub next_steps: Vec<MachineContinuationAction>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
96#[serde(deny_unknown_fields)]
97pub struct MachineValidationWarning {
98    pub task_id: String,
99    pub message: String,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
103#[serde(deny_unknown_fields)]
104pub struct MachineQueueValidateDocument {
105    pub version: u32,
106    pub valid: bool,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub blocking: Option<BlockingState>,
109    #[serde(default)]
110    pub warnings: Vec<MachineValidationWarning>,
111    pub continuation: MachineContinuationSummary,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
115#[serde(deny_unknown_fields)]
116pub struct MachineQueueRepairDocument {
117    pub version: u32,
118    pub dry_run: bool,
119    pub changed: bool,
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub blocking: Option<BlockingState>,
122    #[schemars(schema_with = "json_value_schema")]
123    pub report: JsonValue,
124    pub continuation: MachineContinuationSummary,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
128#[serde(deny_unknown_fields)]
129pub struct MachineQueueUndoDocument {
130    pub version: u32,
131    pub dry_run: bool,
132    pub restored: bool,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub blocking: Option<BlockingState>,
135    #[schemars(schema_with = "option_json_value_schema")]
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub result: Option<JsonValue>,
138    pub continuation: MachineContinuationSummary,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
142#[serde(deny_unknown_fields)]
143pub struct MachineResumeDecision {
144    pub status: String,
145    pub scope: String,
146    pub reason: String,
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub task_id: Option<String>,
149    pub message: String,
150    pub detail: String,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
154#[serde(deny_unknown_fields)]
155pub struct MachineConfigResolveDocument {
156    pub version: u32,
157    pub paths: MachineQueuePaths,
158    pub safety: MachineConfigSafetySummary,
159    pub config: Config,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub resume_preview: Option<MachineResumeDecision>,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
165#[serde(deny_unknown_fields)]
166pub struct MachineConfigSafetySummary {
167    pub repo_trusted: bool,
168    pub dirty_repo: bool,
169    pub git_publish_mode: GitPublishMode,
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub approval_mode: Option<RunnerApprovalMode>,
172    pub ci_gate_enabled: bool,
173    pub git_revert_mode: GitRevertMode,
174    pub parallel_configured: bool,
175    pub execution_interactivity: String,
176    pub interactive_approval_supported: bool,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
180#[serde(deny_unknown_fields)]
181pub struct MachineCliSpecDocument {
182    pub version: u32,
183    pub spec: CliSpec,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
187#[serde(deny_unknown_fields)]
188pub struct MachineTaskCreateRequest {
189    pub version: u32,
190    pub title: String,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub description: Option<String>,
193    pub priority: String,
194    #[serde(default)]
195    pub tags: Vec<String>,
196    #[serde(default, skip_serializing_if = "Vec::is_empty")]
197    pub scope: Vec<String>,
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub template: Option<String>,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub target: Option<String>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
205#[serde(deny_unknown_fields)]
206pub struct MachineTaskCreateDocument {
207    pub version: u32,
208    pub task: Task,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
212#[serde(deny_unknown_fields)]
213pub struct MachineTaskMutationDocument {
214    pub version: u32,
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub blocking: Option<BlockingState>,
217    #[schemars(schema_with = "json_value_schema")]
218    pub report: JsonValue,
219    pub continuation: MachineContinuationSummary,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
223#[serde(deny_unknown_fields)]
224pub struct MachineGraphReadDocument {
225    pub version: u32,
226    #[schemars(schema_with = "json_value_schema")]
227    pub graph: JsonValue,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
231#[serde(deny_unknown_fields)]
232pub struct MachineDashboardReadDocument {
233    pub version: u32,
234    #[schemars(schema_with = "json_value_schema")]
235    pub dashboard: JsonValue,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
239#[serde(deny_unknown_fields)]
240pub struct MachineDecomposeDocument {
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 result: JsonValue,
246    pub continuation: MachineContinuationSummary,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
250#[serde(deny_unknown_fields)]
251pub struct MachineDoctorReportDocument {
252    pub version: u32,
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub blocking: Option<BlockingState>,
255    #[schemars(schema_with = "json_value_schema")]
256    pub report: JsonValue,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
260#[serde(deny_unknown_fields)]
261pub struct MachineParallelStatusDocument {
262    pub version: u32,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub blocking: Option<BlockingState>,
265    pub continuation: MachineContinuationSummary,
266    #[schemars(schema_with = "json_value_schema")]
267    pub status: JsonValue,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
271#[serde(rename_all = "snake_case")]
272pub enum MachineRunEventKind {
273    RunStarted,
274    QueueSnapshot,
275    ConfigResolved,
276    ResumeDecision,
277    TaskSelected,
278    PhaseEntered,
279    PhaseCompleted,
280    RunnerOutput,
281    BlockedStateChanged,
282    BlockedStateCleared,
283    Warning,
284    RunFinished,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
288#[serde(deny_unknown_fields)]
289pub struct MachineRunEventEnvelope {
290    pub version: u32,
291    pub kind: MachineRunEventKind,
292    pub timestamp: String,
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub run_mode: Option<String>,
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub task_id: Option<String>,
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub phase: Option<String>,
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub exit_code: Option<i32>,
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub message: Option<String>,
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub stream: Option<String>,
305    #[schemars(schema_with = "option_json_value_schema")]
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub payload: Option<JsonValue>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
311#[serde(deny_unknown_fields)]
312pub struct MachineRunSummaryDocument {
313    pub version: u32,
314    pub task_id: Option<String>,
315    pub exit_code: i32,
316    pub outcome: String,
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub blocking: Option<BlockingState>,
319}
320
321fn json_value_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
322    <JsonValue as JsonSchema>::json_schema(generator)
323}
324
325fn option_json_value_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
326    <Option<JsonValue> as JsonSchema>::json_schema(generator)
327}