Skip to main content

ta_submit/
vcs_plugin_protocol.rs

1//! JSON-over-stdio protocol types for external VCS adapter plugins.
2//!
3//! VCS adapter plugins communicate with TA using a request/response protocol
4//! over stdin/stdout. TA spawns the plugin process, writes one JSON request
5//! line to stdin, reads one JSON response line from stdout.
6//!
7//! ## Protocol overview
8//!
9//! Every exchange is a single JSON line in each direction:
10//!
11//! ```text
12//! TA → plugin: {"method":"<name>","params":{...}}
13//! plugin → TA: {"ok":true,"result":{...}}   or   {"ok":false,"error":"..."}
14//! ```
15//!
16//! ## Message methods
17//!
18//! | Method              | Direction | Description                                      |
19//! |---------------------|-----------|--------------------------------------------------|
20//! | `handshake`         | TA→plugin | Version negotiation; first call on every spawn   |
21//! | `detect`            | TA→plugin | Auto-detect from project root                    |
22//! | `exclude_patterns`  | TA→plugin | Patterns to exclude from staging copy             |
23//! | `save_state`        | TA→plugin | Save VCS working state before apply              |
24//! | `restore_state`     | TA→plugin | Restore saved state after apply                  |
25//! | `commit`            | TA→plugin | Commit staged changes                            |
26//! | `push`              | TA→plugin | Push committed changes                           |
27//! | `open_review`       | TA→plugin | Open a review request (PR, Swarm review, etc.)   |
28//! | `revision_id`       | TA→plugin | Get current revision identifier                  |
29//! | `protected_targets` | TA→plugin | Get §15 protected submit targets                 |
30//! | `verify_target`     | TA→plugin | §15 invariant check post-prepare()               |
31//! | `sync_upstream`     | TA→plugin | Sync local workspace with upstream               |
32//! | `prepare`           | TA→plugin | Create feature branch / changelist               |
33//! | `check_review`      | TA→plugin | Check status of an open review                   |
34//! | `merge_review`      | TA→plugin | Merge / submit a review                          |
35
36use serde::{Deserialize, Serialize};
37use std::collections::HashMap;
38
39/// Protocol version implemented by this TA build.
40pub const PROTOCOL_VERSION: u32 = 1;
41
42// ---------------------------------------------------------------------------
43// Request envelope
44// ---------------------------------------------------------------------------
45
46/// Request sent from TA to a VCS plugin over stdin.
47///
48/// One JSON line per request. The plugin processes it and writes one
49/// `VcsPluginResponse` line to stdout, then the process exits.
50#[derive(Debug, Serialize, Deserialize)]
51pub struct VcsPluginRequest {
52    /// Method name (e.g., "handshake", "commit", "detect").
53    pub method: String,
54
55    /// Method parameters (structure depends on method).
56    pub params: serde_json::Value,
57}
58
59// ---------------------------------------------------------------------------
60// Response envelope
61// ---------------------------------------------------------------------------
62
63/// Response sent from a VCS plugin to TA over stdout.
64///
65/// One JSON line per response.
66#[derive(Debug, Serialize, Deserialize)]
67pub struct VcsPluginResponse {
68    /// Whether the operation succeeded.
69    pub ok: bool,
70
71    /// Result payload (structure depends on method; null on error).
72    #[serde(default)]
73    pub result: serde_json::Value,
74
75    /// Human-readable error message (only set when ok=false).
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub error: Option<String>,
78}
79
80impl VcsPluginResponse {
81    /// Construct a success response.
82    pub fn success(result: serde_json::Value) -> Self {
83        Self {
84            ok: true,
85            result,
86            error: None,
87        }
88    }
89
90    /// Construct an error response.
91    pub fn error(msg: impl Into<String>) -> Self {
92        Self {
93            ok: false,
94            result: serde_json::Value::Null,
95            error: Some(msg.into()),
96        }
97    }
98}
99
100// ---------------------------------------------------------------------------
101// Handshake
102// ---------------------------------------------------------------------------
103
104/// Parameters for the `handshake` method.
105#[derive(Debug, Serialize, Deserialize)]
106pub struct HandshakeParams {
107    /// TA binary version string (semver).
108    pub ta_version: String,
109    /// Protocol version TA is using (currently 1).
110    pub protocol_version: u32,
111}
112
113/// Result from the `handshake` method.
114#[derive(Debug, Serialize, Deserialize)]
115pub struct HandshakeResult {
116    /// Plugin's self-reported version string.
117    pub plugin_version: String,
118    /// Protocol version the plugin supports.
119    pub protocol_version: u32,
120    /// Adapter name (e.g., "perforce", "svn").
121    pub adapter_name: String,
122    /// List of capabilities this plugin supports (maps to `plugin.toml`).
123    #[serde(default)]
124    pub capabilities: Vec<String>,
125}
126
127// ---------------------------------------------------------------------------
128// detect
129// ---------------------------------------------------------------------------
130
131/// Parameters for the `detect` method.
132#[derive(Debug, Serialize, Deserialize)]
133pub struct DetectParams {
134    /// Absolute path to the project root directory.
135    pub project_root: String,
136}
137
138/// Result from the `detect` method.
139#[derive(Debug, Serialize, Deserialize)]
140pub struct DetectResult {
141    /// Whether this adapter applies to the given project root.
142    pub detected: bool,
143}
144
145// ---------------------------------------------------------------------------
146// exclude_patterns
147// ---------------------------------------------------------------------------
148
149// No params needed — plugin returns patterns for its VCS metadata dirs.
150
151/// Result from the `exclude_patterns` method.
152#[derive(Debug, Serialize, Deserialize)]
153pub struct ExcludePatternsResult {
154    /// Patterns in .taignore format (e.g., ".p4config", ".svn/").
155    pub patterns: Vec<String>,
156}
157
158// ---------------------------------------------------------------------------
159// save_state / restore_state
160// ---------------------------------------------------------------------------
161
162/// Result from the `save_state` method.
163#[derive(Debug, Serialize, Deserialize)]
164pub struct SaveStateResult {
165    /// Opaque adapter state. TA passes this back to `restore_state`.
166    /// `null` means no state was saved (adapter returned None).
167    pub state: serde_json::Value,
168}
169
170/// Parameters for the `restore_state` method.
171#[derive(Debug, Serialize, Deserialize)]
172pub struct RestoreStateParams {
173    /// Opaque state returned by a previous `save_state` call.
174    pub state: serde_json::Value,
175}
176
177// ---------------------------------------------------------------------------
178// prepare
179// ---------------------------------------------------------------------------
180
181/// Parameters for the `prepare` method.
182#[derive(Debug, Serialize, Deserialize)]
183pub struct PrepareParams {
184    /// Goal ID.
185    pub goal_id: String,
186    /// Goal title.
187    pub goal_title: String,
188    /// Absolute path to the workspace.
189    pub workspace_path: String,
190    /// Branch prefix from config (e.g., "feature/").
191    pub branch_prefix: String,
192    /// Co-author string for commit messages, if any.
193    #[serde(default)]
194    pub co_author: Option<String>,
195}
196
197// ---------------------------------------------------------------------------
198// commit
199// ---------------------------------------------------------------------------
200
201/// Parameters for the `commit` method.
202#[derive(Debug, Serialize, Deserialize)]
203pub struct CommitParams {
204    /// Goal ID.
205    pub goal_id: String,
206    /// Goal title.
207    pub goal_title: String,
208    /// Commit message text.
209    pub message: String,
210    /// Files changed (paths relative to workspace root).
211    pub changed_files: Vec<String>,
212}
213
214/// Result from the `commit` method.
215#[derive(Debug, Serialize, Deserialize)]
216pub struct CommitResult {
217    /// Commit identifier (hash, changelist number, etc.).
218    pub commit_id: String,
219    /// Human-readable message.
220    pub message: String,
221    /// Adapter-specific metadata.
222    #[serde(default)]
223    pub metadata: HashMap<String, String>,
224}
225
226// ---------------------------------------------------------------------------
227// push
228// ---------------------------------------------------------------------------
229
230/// Parameters for the `push` method.
231#[derive(Debug, Serialize, Deserialize)]
232pub struct PushParams {
233    /// Goal ID.
234    pub goal_id: String,
235}
236
237/// Result from the `push` method.
238#[derive(Debug, Serialize, Deserialize)]
239pub struct PushResult {
240    /// Remote reference (branch name, changelist URL, etc.).
241    pub remote_ref: String,
242    /// Human-readable message.
243    pub message: String,
244    /// Adapter-specific metadata.
245    #[serde(default)]
246    pub metadata: HashMap<String, String>,
247}
248
249// ---------------------------------------------------------------------------
250// open_review
251// ---------------------------------------------------------------------------
252
253/// Parameters for the `open_review` method.
254#[derive(Debug, Serialize, Deserialize)]
255pub struct OpenReviewParams {
256    /// Goal ID.
257    pub goal_id: String,
258    /// Goal title.
259    pub goal_title: String,
260    /// Draft package summary (human-readable description of changes).
261    pub draft_summary: String,
262    /// List of changed files.
263    pub changed_files: Vec<String>,
264}
265
266/// Result from the `open_review` method.
267#[derive(Debug, Serialize, Deserialize)]
268pub struct OpenReviewResult {
269    /// Review URL (GitHub PR, Perforce Swarm review, etc.).
270    pub review_url: String,
271    /// Review identifier (PR number, CL number, etc.).
272    pub review_id: String,
273    /// Human-readable message.
274    pub message: String,
275    /// Adapter-specific metadata.
276    #[serde(default)]
277    pub metadata: HashMap<String, String>,
278}
279
280// ---------------------------------------------------------------------------
281// revision_id
282// ---------------------------------------------------------------------------
283
284/// Result from the `revision_id` method.
285#[derive(Debug, Serialize, Deserialize)]
286pub struct RevisionIdResult {
287    /// Current revision identifier (e.g., "abc1234", "r1234", "@5678").
288    pub revision_id: String,
289}
290
291// ---------------------------------------------------------------------------
292// protected_targets / verify_target  (§15 compliance)
293// ---------------------------------------------------------------------------
294
295/// Result from the `protected_targets` method.
296#[derive(Debug, Serialize, Deserialize)]
297pub struct ProtectedTargetsResult {
298    /// List of protected refs/branches/paths.
299    pub targets: Vec<String>,
300}
301
302// verify_target: no additional result struct needed — the envelope ok/error is sufficient.
303// On success the plugin returns `ok: true` with an empty result.
304// On violation it returns `ok: false` with a descriptive error.
305
306// ---------------------------------------------------------------------------
307// sync_upstream
308// ---------------------------------------------------------------------------
309
310/// Result from the `sync_upstream` method.
311#[derive(Debug, Serialize, Deserialize)]
312pub struct SyncUpstreamResult {
313    /// Whether upstream had new changes.
314    pub updated: bool,
315    /// Files with merge conflicts.
316    pub conflicts: Vec<String>,
317    /// Number of new upstream commits.
318    pub new_commits: u32,
319    /// Human-readable summary.
320    pub message: String,
321    /// Adapter-specific metadata.
322    #[serde(default)]
323    pub metadata: HashMap<String, String>,
324}
325
326// ---------------------------------------------------------------------------
327// check_review
328// ---------------------------------------------------------------------------
329
330/// Parameters for the `check_review` method.
331#[derive(Debug, Serialize, Deserialize)]
332pub struct CheckReviewParams {
333    /// Review identifier (PR number, CL number, etc.).
334    pub review_id: String,
335}
336
337/// Result from the `check_review` method.
338#[derive(Debug, Serialize, Deserialize)]
339pub struct CheckReviewResult {
340    /// Whether review info was found (false = review not found by adapter).
341    pub found: bool,
342    /// Current state (e.g., "open", "merged", "closed").
343    #[serde(default)]
344    pub state: String,
345    /// Whether CI checks are passing.
346    #[serde(default)]
347    pub checks_passing: Option<bool>,
348}
349
350// ---------------------------------------------------------------------------
351// merge_review
352// ---------------------------------------------------------------------------
353
354/// Parameters for the `merge_review` method.
355#[derive(Debug, Serialize, Deserialize)]
356pub struct MergeReviewParams {
357    /// Review identifier to merge.
358    pub review_id: String,
359}
360
361/// Result from the `merge_review` method.
362#[derive(Debug, Serialize, Deserialize)]
363pub struct MergeReviewResult {
364    /// Whether the merge was completed immediately.
365    pub merged: bool,
366    /// Merge commit SHA or changelist number.
367    #[serde(default)]
368    pub merge_commit: Option<String>,
369    /// Human-readable message.
370    pub message: String,
371    /// Adapter-specific metadata.
372    #[serde(default)]
373    pub metadata: HashMap<String, String>,
374}
375
376// ---------------------------------------------------------------------------
377// Tests
378// ---------------------------------------------------------------------------
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[test]
385    fn request_roundtrip() {
386        let req = VcsPluginRequest {
387            method: "handshake".to_string(),
388            params: serde_json::json!({
389                "ta_version": "0.13.5-alpha",
390                "protocol_version": 1
391            }),
392        };
393        let json = serde_json::to_string(&req).unwrap();
394        let parsed: VcsPluginRequest = serde_json::from_str(&json).unwrap();
395        assert_eq!(parsed.method, "handshake");
396    }
397
398    #[test]
399    fn response_success_roundtrip() {
400        let resp = VcsPluginResponse::success(serde_json::json!({"adapter_name": "perforce"}));
401        let json = serde_json::to_string(&resp).unwrap();
402        let parsed: VcsPluginResponse = serde_json::from_str(&json).unwrap();
403        assert!(parsed.ok);
404        assert!(parsed.error.is_none());
405    }
406
407    #[test]
408    fn response_error_roundtrip() {
409        let resp = VcsPluginResponse::error("p4 not found");
410        let json = serde_json::to_string(&resp).unwrap();
411        let parsed: VcsPluginResponse = serde_json::from_str(&json).unwrap();
412        assert!(!parsed.ok);
413        assert_eq!(parsed.error.as_deref(), Some("p4 not found"));
414    }
415
416    #[test]
417    fn handshake_params_roundtrip() {
418        let params = HandshakeParams {
419            ta_version: "0.13.5-alpha".to_string(),
420            protocol_version: PROTOCOL_VERSION,
421        };
422        let json = serde_json::to_string(&params).unwrap();
423        let parsed: HandshakeParams = serde_json::from_str(&json).unwrap();
424        assert_eq!(parsed.protocol_version, 1);
425    }
426
427    #[test]
428    fn handshake_result_roundtrip() {
429        let result = HandshakeResult {
430            plugin_version: "0.1.0".to_string(),
431            protocol_version: 1,
432            adapter_name: "perforce".to_string(),
433            capabilities: vec![
434                "commit".to_string(),
435                "push".to_string(),
436                "protected_targets".to_string(),
437            ],
438        };
439        let json = serde_json::to_string(&result).unwrap();
440        let parsed: HandshakeResult = serde_json::from_str(&json).unwrap();
441        assert_eq!(parsed.adapter_name, "perforce");
442        assert!(parsed
443            .capabilities
444            .contains(&"protected_targets".to_string()));
445    }
446
447    #[test]
448    fn commit_params_roundtrip() {
449        let params = CommitParams {
450            goal_id: "abc123".to_string(),
451            goal_title: "Fix bug".to_string(),
452            message: "Fix critical bug\n\nCo-authored-by: test".to_string(),
453            changed_files: vec!["src/main.rs".to_string()],
454        };
455        let json = serde_json::to_string(&params).unwrap();
456        let parsed: CommitParams = serde_json::from_str(&json).unwrap();
457        assert_eq!(parsed.goal_id, "abc123");
458        assert_eq!(parsed.changed_files.len(), 1);
459    }
460
461    #[test]
462    fn detect_params_roundtrip() {
463        let params = DetectParams {
464            project_root: "/home/user/project".to_string(),
465        };
466        let json = serde_json::to_string(&params).unwrap();
467        let parsed: DetectParams = serde_json::from_str(&json).unwrap();
468        assert_eq!(parsed.project_root, "/home/user/project");
469    }
470
471    #[test]
472    fn protocol_version_is_one() {
473        assert_eq!(PROTOCOL_VERSION, 1);
474    }
475}