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(¶ms).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(¶ms).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(¶ms).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}