Skip to main content

vissue_control/
rpc.rs

1//! JSON-RPC 2.0 types. Handshake is camelCase; issue payloads are snake_case.
2
3use serde::{Deserialize, Serialize, de::DeserializeOwned};
4use serde_json::{Value, json};
5
6use crate::frame::FrameError;
7use vissue_core::error::Error as CoreError;
8use vissue_core::views::{
9    AgendaRow, ClaimRow, Excerpt, IssueDetail, IssueRow, Recall, RelatedHit, SearchHit, TreeNode,
10    WalkHit,
11};
12
13/// One entry in the on-disk change log.
14pub use vissue_core::events::Event;
15
16/// Protocol version accepted by `initialize`.
17pub const PROTOCOL_VERSION: u32 = 1;
18
19/// JSON-RPC parse error (`-32700`).
20pub const PARSE_ERROR: i32 = -32700;
21/// JSON-RPC invalid request (`-32600`).
22pub const INVALID_REQUEST: i32 = -32600;
23/// JSON-RPC method not found (`-32601`).
24pub const METHOD_NOT_FOUND: i32 = -32601;
25/// JSON-RPC invalid params (`-32602`).
26pub const INVALID_PARAMS: i32 = -32602;
27/// JSON-RPC internal error (`-32603`).
28pub const INTERNAL_ERROR: i32 = -32603;
29/// Issue not found (`-32004`).
30pub const NOT_FOUND: i32 = -32004;
31/// Claim conflict (`-32009`).
32pub const CONFLICT: i32 = -32009;
33/// Closed issue or invalid state (`-32010`).
34pub const INVALID_STATE: i32 = -32010;
35/// Blocker cycle (`-32022`).
36pub const CYCLE: i32 = -32022;
37
38/// Catalog rebuilt. Params: [`VaultChanged`].
39pub const NOTIFY_VAULT_CHANGED: &str = "vault/changed";
40/// Shared selection. Params: [`IssueSelected`].
41pub const NOTIFY_ISSUE_SELECTED: &str = "issue/selected";
42/// Owner is exiting. Params: `{}`.
43pub const NOTIFY_SHUTTING_DOWN: &str = "serve/shutting_down";
44
45/// Wire-level failure for a control client or dispatcher.
46#[derive(Debug)]
47pub enum Error {
48    /// Socket or file I/O.
49    Io(std::io::Error),
50    /// JSON encode or decode.
51    Json(serde_json::Error),
52    /// Frame read or write.
53    Frame(FrameError),
54    /// Server JSON-RPC error object.
55    Rpc(JsonRpcError),
56    /// Method or platform the client cannot handle.
57    Unsupported(&'static str),
58}
59
60impl std::fmt::Display for Error {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            Error::Io(err) => write!(f, "{err}"),
64            Error::Json(err) => write!(f, "{err}"),
65            Error::Frame(err) => write!(f, "{err}"),
66            Error::Rpc(err) => write!(f, "{}", err.message),
67            Error::Unsupported(msg) => write!(f, "{msg}"),
68        }
69    }
70}
71
72impl std::error::Error for Error {
73    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
74        match self {
75            Error::Io(err) => Some(err),
76            Error::Json(err) => Some(err),
77            Error::Frame(err) => Some(err),
78            _ => None,
79        }
80    }
81}
82
83impl From<std::io::Error> for Error {
84    fn from(err: std::io::Error) -> Self {
85        Error::Io(err)
86    }
87}
88
89impl From<serde_json::Error> for Error {
90    fn from(err: serde_json::Error) -> Self {
91        Error::Json(err)
92    }
93}
94
95impl From<FrameError> for Error {
96    fn from(err: FrameError) -> Self {
97        Error::Frame(err)
98    }
99}
100
101impl From<JsonRpcError> for Error {
102    fn from(err: JsonRpcError) -> Self {
103        Error::Rpc(err)
104    }
105}
106
107/// JSON-RPC request or notification id.
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(untagged)]
110pub enum JsonRpcId {
111    /// Numeric id.
112    Number(i64),
113    /// String id.
114    String(String),
115    /// JSON `null`. A response, never a notification.
116    Null,
117}
118
119/// JSON-RPC 2.0 request or notification envelope.
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
121pub struct JsonRpcRequest {
122    /// Always `"2.0"`.
123    pub jsonrpc: String,
124    /// Present on a call; absent on a notification.
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub id: Option<JsonRpcId>,
127    /// Method name.
128    pub method: String,
129    /// Params object, or omitted.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub params: Option<Value>,
132}
133
134impl JsonRpcRequest {
135    /// Request with `id`.
136    pub fn call(id: JsonRpcId, method: impl Into<String>, params: Value) -> Self {
137        Self {
138            jsonrpc: "2.0".into(),
139            id: Some(id),
140            method: method.into(),
141            params: Some(params),
142        }
143    }
144
145    /// Notification (no `id`).
146    pub fn notification(method: impl Into<String>, params: Value) -> Self {
147        Self {
148            jsonrpc: "2.0".into(),
149            id: None,
150            method: method.into(),
151            params: Some(params),
152        }
153    }
154
155    /// True when `id` is absent.
156    pub fn is_notification(&self) -> bool {
157        self.id.is_none()
158    }
159}
160
161/// JSON-RPC 2.0 response envelope.
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163pub struct JsonRpcResponse {
164    /// Always `"2.0"`.
165    pub jsonrpc: String,
166    /// Request id echoed back. `None` or [`JsonRpcId::Null`] on some errors.
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub id: Option<JsonRpcId>,
169    /// Success body.
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub result: Option<Value>,
172    /// Failure body.
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub error: Option<JsonRpcError>,
175}
176
177impl JsonRpcResponse {
178    /// Success response.
179    pub fn ok(id: Option<JsonRpcId>, result: Value) -> Self {
180        Self {
181            jsonrpc: "2.0".into(),
182            id,
183            result: Some(result),
184            error: None,
185        }
186    }
187
188    /// Error response.
189    pub fn err(id: Option<JsonRpcId>, error: JsonRpcError) -> Self {
190        Self {
191            jsonrpc: "2.0".into(),
192            id,
193            result: None,
194            error: Some(error),
195        }
196    }
197}
198
199/// JSON-RPC error object. Application codes carry `data.code`.
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201pub struct JsonRpcError {
202    /// JSON-RPC or application numeric code.
203    pub code: i32,
204    /// Human-readable message.
205    pub message: String,
206    /// Optional payload. Application codes put `code` here as a string.
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub data: Option<Value>,
209}
210
211/// `-32700` parse error.
212pub fn parse_error() -> JsonRpcError {
213    JsonRpcError {
214        code: PARSE_ERROR,
215        message: "parse error".into(),
216        data: None,
217    }
218}
219
220/// `-32600` invalid request.
221pub fn invalid_request() -> JsonRpcError {
222    JsonRpcError {
223        code: INVALID_REQUEST,
224        message: "invalid request".into(),
225        data: None,
226    }
227}
228
229/// `-32601` method not found. `data.method` is `method`.
230pub fn method_not_found(method: &str) -> JsonRpcError {
231    JsonRpcError {
232        code: METHOD_NOT_FOUND,
233        message: "method not found".into(),
234        data: Some(json!({ "method": method })),
235    }
236}
237
238/// `-32602` invalid params with `message`.
239pub fn invalid_params(message: impl Into<String>) -> JsonRpcError {
240    JsonRpcError {
241        code: INVALID_PARAMS,
242        message: message.into(),
243        data: None,
244    }
245}
246
247/// `-32603` internal error with `message`.
248pub fn internal_error(message: impl Into<String>) -> JsonRpcError {
249    JsonRpcError {
250        code: INTERNAL_ERROR,
251        message: message.into(),
252        data: None,
253    }
254}
255
256/// Map a typed core error onto the control-plane codes.
257pub fn error_from_core(err: &CoreError) -> JsonRpcError {
258    match err {
259        // The socket is started on a layout somebody named, so this cannot
260        // reach a client over it. It is mapped anyway: an unmapped variant is
261        // how the next one added becomes a compile error somewhere else.
262        CoreError::NotATracker { root, prefix } => JsonRpcError {
263            code: NOT_FOUND,
264            message: err.to_string(),
265            data: Some(json!({
266                "code": "not_a_tracker",
267                "root": root,
268                "prefix": prefix,
269            })),
270        },
271        CoreError::IssueNotFound { id } => JsonRpcError {
272            code: NOT_FOUND,
273            message: err.to_string(),
274            data: Some(json!({ "code": "not_found", "id": id })),
275        },
276        CoreError::DuplicateId { id, paths } => JsonRpcError {
277            code: CONFLICT,
278            message: err.to_string(),
279            data: Some(json!({
280                "code": "duplicate_id",
281                "id": id,
282                "paths": paths,
283            })),
284        },
285        CoreError::ClaimConflict { id, holder, .. } => JsonRpcError {
286            code: CONFLICT,
287            message: err.to_string(),
288            data: Some(json!({ "code": "conflict", "id": id, "holder": holder })),
289        },
290        CoreError::BlockerCycle { blocker, issue } => JsonRpcError {
291            code: CYCLE,
292            message: err.to_string(),
293            data: Some(json!({ "code": "cycle", "id": issue, "block": blocker })),
294        },
295        CoreError::InvalidState { id, state } => JsonRpcError {
296            code: INVALID_STATE,
297            message: err.to_string(),
298            data: Some(json!({ "code": "invalid_state", "id": id, "state": state })),
299        },
300        CoreError::StaleWrite {
301            id,
302            expected_state,
303            actual_state,
304            expected_gen,
305            actual_gen,
306        } => JsonRpcError {
307            code: INVALID_STATE,
308            message: err.to_string(),
309            data: Some(json!({
310                "code": "stale",
311                "id": id,
312                "expected_state": expected_state,
313                "actual_state": actual_state,
314                "expected_gen": expected_gen,
315                "actual_gen": actual_gen,
316            })),
317        },
318        CoreError::TerminalConflict {
319            id,
320            held,
321            attempted,
322        } => JsonRpcError {
323            code: CONFLICT,
324            message: err.to_string(),
325            data: Some(json!({
326                "code": "terminal_conflict",
327                "id": id,
328                "held": held,
329                "attempted": attempted,
330            })),
331        },
332        CoreError::Other(_) => internal_error(err.to_string()),
333    }
334}
335
336/// v1 methods the owner advertises on `initialize`.
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
338pub enum Method {
339    /// Handshake. Not listed in [`V1_CAPABILITIES`].
340    Initialize,
341    /// Process identity, root, prefix, and crate version.
342    IdentityGet,
343    /// Filtered issue rows.
344    IssueList,
345    /// One issue plus revision.
346    IssueGet,
347    /// Frontier: `issue/list` with `ready: true`.
348    IssueReady,
349    /// Substring search over id, title, properties, tags, and body.
350    IssueSearch,
351    /// Live claims.
352    IssueClaims,
353    /// Deadlines and scheduled starts.
354    IssueAgenda,
355    /// Alias of [`Self::IssueGet`].
356    IssueShow,
357    /// Secret-screened body range.
358    IssueExcerpt,
359    /// Children and blockers.
360    IssueTree,
361    /// Bounded neighborhood with evidence.
362    IssueRelated,
363    /// Direct children.
364    IssueChildren,
365    /// Walk up the blocker graph.
366    IssueAncestors,
367    /// Walk down the blocker graph.
368    IssueImpact,
369    /// Everything pointing at the id.
370    IssueBacklinks,
371    /// Shared selection; notifies `issue/selected`.
372    IssueOpen,
373    /// Create an issue.
374    IssueCreate,
375    /// State, priority, block, unblock.
376    IssueUpdate,
377    /// Take the issue.
378    IssueClaim,
379    /// Dated logbook entry.
380    IssueNote,
381    /// Move to another project.
382    IssueRefile,
383    /// Operation added after v1's first draft; see schema/vissue.capnp.
384    IssueAppend,
385    /// Operation added after v1's first draft; see schema/vissue.capnp.
386    IssueReject,
387    /// Operation added after v1's first draft; see schema/vissue.capnp.
388    IssueResolve,
389    /// Operation added after v1's first draft; see schema/vissue.capnp.
390    IssueVote,
391    /// Operation added after v1's first draft; see schema/vissue.capnp.
392    IssueDeed,
393    /// Operation added after v1's first draft; see schema/vissue.capnp.
394    IssueRecall,
395    /// Operation added after v1's first draft; see schema/vissue.capnp.
396    IssueConsensus,
397    /// Operation added after v1's first draft; see schema/vissue.capnp.
398    IssueFold,
399    /// Operation added after v1's first draft; see schema/vissue.capnp.
400    IssueNormalize,
401    /// Operation added after v1's first draft; see schema/vissue.capnp.
402    IssueCheck,
403    /// Operation added after v1's first draft; see schema/vissue.capnp.
404    IssueCount,
405    /// Operation added after v1's first draft; see schema/vissue.capnp.
406    IssueCycles,
407    /// Operation added after v1's first draft; see schema/vissue.capnp.
408    IssueDigest,
409    /// Operation added after v1's first draft; see schema/vissue.capnp.
410    IssueExport,
411    /// Operation added after v1's first draft; see schema/vissue.capnp.
412    IssueGraph,
413    /// Operation added after v1's first draft; see schema/vissue.capnp.
414    IssueRoadmap,
415    /// Operation added after v1's first draft; see schema/vissue.capnp.
416    IssueStale,
417    /// Operation added after v1's first draft; see schema/vissue.capnp.
418    IssueHygiene,
419    /// Operation added after v1's first draft; see schema/vissue.capnp.
420    IssueWaitingOn,
421    /// Operation added after v1's first draft; see schema/vissue.capnp.
422    IssueMirror,
423    /// Operation added after v1's first draft; see schema/vissue.capnp.
424    EventsPing,
425    /// Operation added after v1's first draft; see schema/vissue.capnp.
426    EventsWait,
427    /// Project names plus revision.
428    ProjectList,
429    /// Pull of the on-disk event log.
430    EventsSince,
431    /// Current generation and revision.
432    EventsGen,
433}
434
435impl Method {
436    /// Wire method name.
437    pub fn as_str(self) -> &'static str {
438        match self {
439            Self::Initialize => "initialize",
440            Self::IdentityGet => "identity/get",
441            Self::IssueList => "issue/list",
442            Self::IssueGet => "issue/get",
443            Self::IssueReady => "issue/ready",
444            Self::IssueSearch => "issue/search",
445            Self::IssueClaims => "issue/claims",
446            Self::IssueAgenda => "issue/agenda",
447            Self::IssueShow => "issue/show",
448            Self::IssueExcerpt => "issue/excerpt",
449            Self::IssueTree => "issue/tree",
450            Self::IssueRelated => "issue/related",
451            Self::IssueChildren => "issue/children",
452            Self::IssueAncestors => "issue/ancestors",
453            Self::IssueImpact => "issue/impact",
454            Self::IssueBacklinks => "issue/backlinks",
455            Self::IssueOpen => "issue/open",
456            Self::IssueCreate => "issue/create",
457            Self::IssueUpdate => "issue/update",
458            Self::IssueClaim => "issue/claim",
459            Self::IssueNote => "issue/note",
460            Self::IssueRefile => "issue/refile",
461            Self::ProjectList => "project/list",
462            Self::EventsSince => "events/since",
463            Self::EventsGen => "events/gen",
464            Self::IssueAppend => "issue/append",
465            Self::IssueReject => "issue/reject",
466            Self::IssueResolve => "issue/resolve",
467            Self::IssueVote => "issue/vote",
468            Self::IssueDeed => "issue/deed",
469            Self::IssueRecall => "issue/recall",
470            Self::IssueConsensus => "issue/consensus",
471            Self::IssueFold => "issue/fold",
472            Self::IssueNormalize => "issue/normalize",
473            Self::IssueCheck => "issue/check",
474            Self::IssueCount => "issue/count",
475            Self::IssueCycles => "issue/cycles",
476            Self::IssueDigest => "issue/digest",
477            Self::IssueExport => "issue/export",
478            Self::IssueGraph => "issue/graph",
479            Self::IssueRoadmap => "issue/roadmap",
480            Self::IssueStale => "issue/stale",
481            Self::IssueHygiene => "issue/hygiene",
482            Self::IssueWaitingOn => "issue/waiting_on",
483            Self::IssueMirror => "issue/mirror_check",
484            Self::EventsPing => "events/ping",
485            Self::EventsWait => "events/wait",
486        }
487    }
488
489    /// Parse a v1 wire name.
490    ///
491    /// # Errors
492    ///
493    /// Returns an error when `name` is not a v1 method.
494    pub fn parse(name: &str) -> Result<Self, JsonRpcError> {
495        match name {
496            "initialize" => Ok(Self::Initialize),
497            "identity/get" => Ok(Self::IdentityGet),
498            "issue/list" => Ok(Self::IssueList),
499            "issue/get" => Ok(Self::IssueGet),
500            "issue/ready" => Ok(Self::IssueReady),
501            "issue/search" => Ok(Self::IssueSearch),
502            "issue/claims" => Ok(Self::IssueClaims),
503            "issue/agenda" => Ok(Self::IssueAgenda),
504            "issue/show" => Ok(Self::IssueShow),
505            "issue/excerpt" => Ok(Self::IssueExcerpt),
506            "issue/tree" => Ok(Self::IssueTree),
507            "issue/related" => Ok(Self::IssueRelated),
508            "issue/children" => Ok(Self::IssueChildren),
509            "issue/ancestors" => Ok(Self::IssueAncestors),
510            "issue/impact" => Ok(Self::IssueImpact),
511            "issue/backlinks" => Ok(Self::IssueBacklinks),
512            "issue/open" => Ok(Self::IssueOpen),
513            "issue/create" => Ok(Self::IssueCreate),
514            "issue/update" => Ok(Self::IssueUpdate),
515            "issue/claim" => Ok(Self::IssueClaim),
516            "issue/note" => Ok(Self::IssueNote),
517            "issue/refile" => Ok(Self::IssueRefile),
518            "project/list" => Ok(Self::ProjectList),
519            "events/since" => Ok(Self::EventsSince),
520            "events/gen" => Ok(Self::EventsGen),
521            "issue/append" => Ok(Self::IssueAppend),
522            "issue/reject" => Ok(Self::IssueReject),
523            "issue/resolve" => Ok(Self::IssueResolve),
524            "issue/vote" => Ok(Self::IssueVote),
525            "issue/deed" => Ok(Self::IssueDeed),
526            "issue/recall" => Ok(Self::IssueRecall),
527            "issue/consensus" => Ok(Self::IssueConsensus),
528            "issue/fold" => Ok(Self::IssueFold),
529            "issue/normalize" => Ok(Self::IssueNormalize),
530            "issue/check" => Ok(Self::IssueCheck),
531            "issue/count" => Ok(Self::IssueCount),
532            "issue/cycles" => Ok(Self::IssueCycles),
533            "issue/digest" => Ok(Self::IssueDigest),
534            "issue/export" => Ok(Self::IssueExport),
535            "issue/graph" => Ok(Self::IssueGraph),
536            "issue/roadmap" => Ok(Self::IssueRoadmap),
537            "issue/stale" => Ok(Self::IssueStale),
538            "issue/hygiene" => Ok(Self::IssueHygiene),
539            "issue/waiting_on" => Ok(Self::IssueWaitingOn),
540            "issue/mirror_check" => Ok(Self::IssueMirror),
541            "events/ping" => Ok(Self::EventsPing),
542            "events/wait" => Ok(Self::EventsWait),
543            other => Err(method_not_found(other)),
544        }
545    }
546}
547
548/// Capability strings returned by `initialize` (v1). `initialize` itself is omitted.
549///
550/// This is the fourth place the method set is written down, after the dispatch table,
551/// the schema and the reference, and it is the one a client reads to decide what it
552/// may call. It fell nineteen methods behind while the other three agreed with each
553/// other, so a client inspecting capabilities would have concluded that append,
554/// vote, fold and every read added beside them did not exist.
555///
556/// `capabilities_match_the_schema` in vissue-serve holds this to the schema now.
557pub const V1_CAPABILITIES: &[&str] = &[
558    "issue/list",
559    "issue/get",
560    "issue/ready",
561    "issue/search",
562    "issue/claims",
563    "issue/agenda",
564    "issue/show",
565    "issue/excerpt",
566    "issue/tree",
567    "issue/related",
568    "issue/children",
569    "issue/ancestors",
570    "issue/impact",
571    "issue/backlinks",
572    "issue/open",
573    "issue/create",
574    "issue/update",
575    "issue/claim",
576    "issue/note",
577    "issue/refile",
578    "issue/append",
579    "issue/reject",
580    "issue/resolve",
581    "issue/vote",
582    "issue/deed",
583    "issue/recall",
584    "issue/consensus",
585    "issue/fold",
586    "issue/normalize",
587    "issue/check",
588    "issue/count",
589    "issue/cycles",
590    "issue/digest",
591    "issue/export",
592    "issue/graph",
593    "issue/roadmap",
594    "issue/stale",
595    "issue/hygiene",
596    "issue/waiting_on",
597    "issue/mirror_check",
598    "project/list",
599    "events/since",
600    "events/gen",
601    "events/ping",
602    "events/wait",
603    "identity/get",
604];
605
606/// `initialize` params. camelCase on the wire.
607#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
608#[serde(rename_all = "camelCase")]
609pub struct InitializeParams {
610    /// Must be [`PROTOCOL_VERSION`].
611    pub protocol_version: u32,
612    /// Client name, e.g. `vissue-tui`. Empty when omitted.
613    #[serde(default)]
614    pub client: String,
615    /// Connection identity. Required and non-empty.
616    pub agent: String,
617}
618
619/// `initialize` result. camelCase on the wire.
620#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
621#[serde(rename_all = "camelCase")]
622pub struct InitializeResult {
623    /// Echo of [`PROTOCOL_VERSION`].
624    pub protocol_version: u32,
625    /// Advertised methods. See [`V1_CAPABILITIES`].
626    pub capabilities: Vec<String>,
627    /// Tracker root the owner bound.
628    pub root: String,
629    /// Layout prefix the owner bound.
630    pub prefix: String,
631    /// On-disk generation counter.
632    pub generation: u64,
633    /// Serve-local catalog revision. Starts at 1.
634    pub revision: u64,
635    /// Owner identity.
636    pub identity: String,
637}
638
639/// Parse `initialize` params. Missing/empty `agent` and version != 1 are -32602.
640///
641/// # Errors
642///
643/// Returns an error when `value` is not an object, `protocolVersion` is
644/// missing, not a number, or not 1, or `agent` is missing or empty.
645pub fn parse_initialize_params(value: &Value) -> Result<InitializeParams, JsonRpcError> {
646    let obj = value
647        .as_object()
648        .ok_or_else(|| invalid_params("params must be an object"))?;
649    let version = match obj.get("protocolVersion") {
650        Some(Value::Number(n)) => n
651            .as_u64()
652            .ok_or_else(|| invalid_params("protocolVersion must be a number"))?,
653        Some(_) => return Err(invalid_params("protocolVersion must be a number")),
654        None => return Err(invalid_params("protocolVersion is required")),
655    };
656    if version != u64::from(PROTOCOL_VERSION) {
657        return Err(JsonRpcError {
658            code: INVALID_PARAMS,
659            message: "unsupported protocol version".into(),
660            data: Some(json!({ "supported": PROTOCOL_VERSION })),
661        });
662    }
663    let agent = match obj.get("agent") {
664        Some(Value::String(s)) if !s.trim().is_empty() => s.clone(),
665        _ => return Err(invalid_params("agent is required")),
666    };
667    let client = obj
668        .get("client")
669        .and_then(Value::as_str)
670        .unwrap_or("")
671        .to_string();
672    Ok(InitializeParams {
673        protocol_version: PROTOCOL_VERSION,
674        client,
675        agent,
676    })
677}
678
679/// Filters for `issue/list` and `issue/ready`. snake_case on the wire.
680#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
681pub struct IssueListParams {
682    /// Restrict to this project.
683    #[serde(default, skip_serializing_if = "Option::is_none")]
684    pub project: Option<String>,
685    /// Restrict to this TODO keyword.
686    #[serde(default, skip_serializing_if = "Option::is_none")]
687    pub state: Option<String>,
688    /// When `true`, only the frontier (no open blockers).
689    #[serde(default, skip_serializing_if = "Option::is_none")]
690    pub ready: Option<bool>,
691    /// Case-insensitive substring over id, title, tags, and properties.
692    #[serde(default, skip_serializing_if = "Option::is_none")]
693    pub query: Option<String>,
694    /// Max rows after offset.
695    #[serde(default, skip_serializing_if = "Option::is_none")]
696    pub limit: Option<usize>,
697    /// Skip this many matching rows.
698    #[serde(default, skip_serializing_if = "Option::is_none")]
699    pub offset: Option<usize>,
700    /// When this equals the current revision, the result is unchanged.
701    #[serde(default, skip_serializing_if = "Option::is_none")]
702    pub since_revision: Option<u64>,
703}
704
705/// Page of issue rows, or an unchanged marker.
706#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
707pub struct IssueListResult {
708    /// Matching rows. Empty when [`Self::unchanged`].
709    #[serde(default)]
710    pub issues: Vec<IssueRow>,
711    /// Issues in the selected project (or whole vault) before other filters.
712    #[serde(default)]
713    pub total: u64,
714    /// Rows matching state, ready, and query, before limit and offset.
715    #[serde(default)]
716    pub matched: u64,
717    /// Current serve revision.
718    pub revision: u64,
719    /// Current on-disk generation.
720    #[serde(default)]
721    pub generation: u64,
722    /// `since_revision` matched; `issues` is empty.
723    #[serde(default)]
724    pub unchanged: bool,
725}
726
727/// Single issue id.
728#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
729pub struct IdParams {
730    /// Issue id.
731    pub id: String,
732}
733
734/// One issue plus the serve revision.
735#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
736pub struct IssueGetResult {
737    /// Flattened detail fields on the wire.
738    #[serde(flatten)]
739    pub issue: IssueDetail,
740    /// Current serve revision.
741    pub revision: u64,
742}
743
744/// `issue/search` params. Default `limit` is 20.
745#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
746pub struct SearchParams {
747    /// Substring over id, title, properties, tags, and body.
748    pub query: String,
749    /// Max hits.
750    #[serde(default, skip_serializing_if = "Option::is_none")]
751    pub limit: Option<usize>,
752}
753
754/// `issue/claims` params.
755#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
756pub struct ClaimsParams {
757    /// Restrict to this holder.
758    #[serde(default, skip_serializing_if = "Option::is_none")]
759    pub holder: Option<String>,
760    /// Restrict to this project.
761    #[serde(default, skip_serializing_if = "Option::is_none")]
762    pub project: Option<String>,
763}
764
765/// `issue/agenda` params. Default `days` is 14.
766#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
767pub struct AgendaParams {
768    /// Horizon in days.
769    #[serde(default, skip_serializing_if = "Option::is_none")]
770    pub days: Option<i64>,
771    /// Restrict to this project.
772    #[serde(default, skip_serializing_if = "Option::is_none")]
773    pub project: Option<String>,
774}
775
776/// `issue/tree` params. `format` is `nodes`, `ascii`, or `dot`.
777#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
778pub struct TreeParams {
779    /// Root issue id.
780    pub id: String,
781    /// `nodes` (default), `ascii`, or `dot`.
782    #[serde(default, skip_serializing_if = "Option::is_none")]
783    pub format: Option<String>,
784}
785
786/// `issue/tree` result: a node graph or rendered text.
787#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
788#[serde(untagged)]
789pub enum TreeResult {
790    /// Structured tree (`format` omitted or `nodes`).
791    Nodes(TreeNode),
792    /// Rendered `ascii` or `dot`.
793    Text {
794        /// Graph text.
795        text: String,
796    },
797}
798
799/// `issue/related` params. Default `depth` is 2 and `limit` is 20.
800#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
801pub struct RelatedParams {
802    /// Center issue id.
803    pub id: String,
804    /// Graph walk depth.
805    #[serde(default, skip_serializing_if = "Option::is_none")]
806    pub depth: Option<usize>,
807    /// Max hits.
808    #[serde(default, skip_serializing_if = "Option::is_none")]
809    pub limit: Option<usize>,
810}
811
812/// Params for children, ancestors, impact, and backlinks.
813#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
814pub struct WalkParams {
815    /// Start issue id.
816    pub id: String,
817    /// Walk depth. Omitted means the method default.
818    #[serde(default, skip_serializing_if = "Option::is_none")]
819    pub depth: Option<usize>,
820}
821
822/// `project/list` result.
823#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
824pub struct ProjectListResult {
825    /// Project names under the prefix.
826    pub projects: Vec<String>,
827    /// Current serve revision.
828    pub revision: u64,
829}
830
831/// `events/since` params.
832#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
833pub struct EventsSinceParams {
834    /// Return events with sequence greater than this.
835    pub since: u64,
836    /// Max events.
837    #[serde(default, skip_serializing_if = "Option::is_none")]
838    pub limit: Option<usize>,
839}
840
841/// Pull of the on-disk event log.
842#[derive(Debug, Clone, Serialize, Deserialize)]
843pub struct EventsSinceResult {
844    /// Events after `since`.
845    pub events: Vec<Event>,
846    /// Current generation after the pull.
847    pub generation: u64,
848}
849
850/// `events/gen` result.
851#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
852pub struct EventsGenResult {
853    /// On-disk generation counter.
854    pub generation: u64,
855    /// Serve-local catalog revision.
856    pub revision: u64,
857}
858
859/// `identity/get` result.
860#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
861pub struct IdentityResult {
862    /// Connection or process identity.
863    pub identity: String,
864    /// Tracker root.
865    pub root: String,
866    /// Layout prefix.
867    pub prefix: String,
868    /// Crate version string.
869    pub version: String,
870}
871
872/// `issue/create` params. Fields match the CLI create verb.
873#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
874pub struct CreateParams {
875    /// Target project.
876    pub project: String,
877    /// Heading title.
878    pub title: String,
879    /// Override the connection agent.
880    #[serde(default, skip_serializing_if = "Option::is_none")]
881    pub agent: Option<String>,
882    /// Priority letter.
883    #[serde(default, skip_serializing_if = "Option::is_none")]
884    pub priority: Option<char>,
885    /// `:TYPE:` property.
886    #[serde(default, skip_serializing_if = "Option::is_none")]
887    pub issue_type: Option<String>,
888    /// Org deadline stamp.
889    #[serde(default, skip_serializing_if = "Option::is_none")]
890    pub deadline: Option<String>,
891    /// Org scheduled stamp.
892    #[serde(default, skip_serializing_if = "Option::is_none")]
893    pub scheduled: Option<String>,
894    /// Space-separated tags.
895    #[serde(default, skip_serializing_if = "Option::is_none")]
896    pub tags: Option<String>,
897    /// Parent issue id.
898    #[serde(default, skip_serializing_if = "Option::is_none")]
899    pub parent: Option<String>,
900    /// Body prose written under the properties drawer.
901    #[serde(default, skip_serializing_if = "Option::is_none")]
902    pub body: Option<String>,
903}
904
905/// `issue/update` params.
906#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
907pub struct UpdateParams {
908    /// Issue id.
909    pub id: String,
910    /// New TODO keyword.
911    #[serde(default, skip_serializing_if = "Option::is_none")]
912    pub state: Option<String>,
913    /// New priority letter.
914    #[serde(default, skip_serializing_if = "Option::is_none")]
915    pub priority: Option<String>,
916    /// Add this blocker.
917    #[serde(default, skip_serializing_if = "Option::is_none")]
918    pub block: Option<String>,
919    /// Remove this blocker.
920    #[serde(default, skip_serializing_if = "Option::is_none")]
921    pub unblock: Option<String>,
922    /// Refuse unless the heading is still this state.
923    #[serde(default, skip_serializing_if = "Option::is_none")]
924    pub if_state: Option<String>,
925    /// Refuse unless the corpus generation is still this value.
926    #[serde(default, skip_serializing_if = "Option::is_none")]
927    pub if_gen: Option<u64>,
928    /// Override the connection agent.
929    #[serde(default, skip_serializing_if = "Option::is_none")]
930    pub agent: Option<String>,
931}
932
933/// `issue/claim` params. `force` defaults to false.
934#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
935pub struct ClaimParams {
936    /// Issue id.
937    pub id: String,
938    /// Take over an existing claim.
939    #[serde(default)]
940    pub force: bool,
941    /// Override the connection agent.
942    #[serde(default, skip_serializing_if = "Option::is_none")]
943    pub agent: Option<String>,
944}
945
946/// `issue/vote` params. `choice` absent reads the tally without casting.
947#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
948pub struct VoteParams {
949    /// Issue id.
950    pub id: String,
951    /// What to vote for, one line. Absent reads the tally.
952    #[serde(default, skip_serializing_if = "Option::is_none")]
953    pub choice: Option<String>,
954    /// Override the connection agent.
955    #[serde(default, skip_serializing_if = "Option::is_none")]
956    pub agent: Option<String>,
957}
958
959/// `issue/deed` params. Both lists absent reads the citations without writing.
960#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
961pub struct DeedParams {
962    /// Issue id.
963    pub id: String,
964    /// Deed accessions this issue produced.
965    #[serde(default)]
966    pub add: Vec<String>,
967    /// Citations to drop.
968    #[serde(default)]
969    pub remove: Vec<String>,
970}
971
972/// `issue/recall` params. `depth` bounds the blocker walk and defaults to one.
973#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
974pub struct RecallParams {
975    /// Issue id.
976    pub id: String,
977    /// Hops of the blocker walk.
978    #[serde(default, skip_serializing_if = "Option::is_none")]
979    pub depth: Option<usize>,
980    /// Include a capped excerpt of each input's heading.
981    #[serde(default)]
982    pub excerpts: bool,
983}
984
985/// `issue/consensus` params.
986#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
987pub struct ConsensusParams {
988    /// Issue id.
989    pub id: String,
990    /// Roll up over the issue's children instead of its own ballots.
991    #[serde(default)]
992    pub children: bool,
993}
994
995/// Params for the reads that take an optional project filter: `issue/export`,
996/// `issue/graph`, `issue/roadmap`, `issue/cycles`, `issue/check`.
997#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
998pub struct ProjectFilterParams {
999    /// Only this project; every project when absent.
1000    #[serde(default, skip_serializing_if = "Option::is_none")]
1001    pub project: Option<String>,
1002}
1003
1004/// `issue/count` params.
1005#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1006pub struct CountParams {
1007    /// Only this project.
1008    #[serde(default, skip_serializing_if = "Option::is_none")]
1009    pub project: Option<String>,
1010    /// Only this state.
1011    #[serde(default, skip_serializing_if = "Option::is_none")]
1012    pub state: Option<String>,
1013    /// Only issues with no live blocker.
1014    #[serde(default)]
1015    pub ready_only: bool,
1016}
1017
1018/// `issue/stale` params.
1019#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1020pub struct StaleParams {
1021    /// How many days without a change counts as stale.
1022    pub days: i64,
1023    /// Only this project.
1024    #[serde(default, skip_serializing_if = "Option::is_none")]
1025    pub project: Option<String>,
1026}
1027
1028/// `issue/hygiene` params.
1029#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1030pub struct HygieneParams {
1031    /// Days before a claim counts as stalled; the default when absent.
1032    #[serde(default, skip_serializing_if = "Option::is_none")]
1033    pub stale_days: Option<i64>,
1034}
1035
1036/// `events/ping` params.
1037#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1038pub struct PingParams {
1039    /// Which detail to report; the summary when absent.
1040    #[serde(default, skip_serializing_if = "Option::is_none")]
1041    pub detail: Option<String>,
1042}
1043
1044/// `events/wait` params. Waits for the corpus generation to pass `last`, or for
1045/// `id` to reach a terminal state when one is given.
1046#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1047pub struct WaitParams {
1048    /// Generation to wait past.
1049    #[serde(default)]
1050    pub last: u64,
1051    /// Wait for this issue to reach a terminal state instead.
1052    #[serde(default, skip_serializing_if = "Option::is_none")]
1053    pub id: Option<String>,
1054    /// Poll interval in milliseconds.
1055    #[serde(default, skip_serializing_if = "Option::is_none")]
1056    pub poll_ms: Option<u64>,
1057    /// Give up after this many milliseconds.
1058    #[serde(default, skip_serializing_if = "Option::is_none")]
1059    pub timeout_ms: Option<u64>,
1060}
1061
1062/// `issue/mirror` params. Checks a mirror file's stamp against the tracker.
1063#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1064pub struct MirrorCheckParams {
1065    /// Mirror file whose SYNC stamp is compared against the corpus.
1066    pub path: String,
1067    /// Only these projects; the stamp's own list when empty, since the file records
1068    /// what it covered.
1069    #[serde(default)]
1070    pub projects: Vec<String>,
1071}
1072
1073/// `issue/mirror` reply.
1074#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1075pub struct MirrorCheckResult {
1076    /// Whether the stamp still matches the tracker.
1077    pub fresh: bool,
1078    /// The verdict, naming which projects moved when stale.
1079    pub report: String,
1080}
1081
1082/// `issue/digest` params.
1083#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1084pub struct DigestParams {
1085    /// Only these projects; every project when empty.
1086    #[serde(default)]
1087    pub projects: Vec<String>,
1088}
1089
1090/// A report-shaped reply: the same text the subcommand prints.
1091///
1092/// Shared by the reads that produce prose rather than structure. Giving each its own
1093/// type would be a contract per report to keep in step with the text, and the text is
1094/// the part anyone reads.
1095#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1096pub struct ReportResult {
1097    /// The report, as the subcommand would print it.
1098    pub report: String,
1099}
1100
1101/// `issue/check` reply. The counts travel beside the text because the subcommand
1102/// exits non-zero on an error count and a client needs the same signal.
1103#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1104pub struct CheckResult {
1105    /// Findings, ending in a summary line.
1106    pub report: String,
1107    /// Count of `[err]` findings.
1108    pub errors: usize,
1109    /// Count of `[warn]` findings.
1110    pub warnings: usize,
1111}
1112
1113/// One project's digest inside [`DigestResult`].
1114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1115pub struct ProjectDigestResult {
1116    /// Project directory name.
1117    pub project: String,
1118    /// Hash over that project's export.
1119    pub digest: String,
1120    /// Issue count in that project.
1121    pub issues: usize,
1122}
1123
1124/// `issue/digest` reply.
1125#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1126pub struct DigestResult {
1127    /// Hash over the per-project digests.
1128    pub combined: String,
1129    /// Sum of the per-project issue counts.
1130    pub issues: usize,
1131    /// Event-log generation the digest was taken at, so two digests can be placed in
1132    /// time relative to each other.
1133    pub generation: u64,
1134    /// Per project, sorted by name.
1135    pub projects: Vec<ProjectDigestResult>,
1136}
1137
1138/// `events/wait` reply. Waiting on a generation fills `generation` only; waiting on
1139/// an issue fills `state` and says whether it gave up.
1140#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1141pub struct WaitResult {
1142    /// Generation at the moment the wait returned.
1143    pub generation: u64,
1144    /// Heading state, when the wait was for an issue.
1145    #[serde(default, skip_serializing_if = "Option::is_none")]
1146    pub state: Option<String>,
1147    /// True when the timeout expired before a terminal state.
1148    #[serde(default)]
1149    pub timed_out: bool,
1150}
1151
1152/// `issue/append` params.
1153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1154pub struct AppendParams {
1155    /// Issue id.
1156    pub id: String,
1157    /// Report text, written under the heading with a dated stamp.
1158    pub text: String,
1159    /// Override the connection agent, which the stamp records.
1160    #[serde(default, skip_serializing_if = "Option::is_none")]
1161    pub agent: Option<String>,
1162}
1163
1164/// `issue/resolve` params.
1165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1166pub struct ResolveParams {
1167    /// Issue id whose sibling terminal is being picked.
1168    pub id: String,
1169    /// Terminal state to settle on.
1170    pub state: String,
1171}
1172
1173/// `issue/reject` params. Either `to` or `project` has to say where the
1174/// successor goes.
1175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1176pub struct RejectParams {
1177    /// Issue being cancelled.
1178    pub id: String,
1179    /// Existing issue to point at instead of creating a successor.
1180    #[serde(default, skip_serializing_if = "Option::is_none")]
1181    pub to: Option<String>,
1182    /// Project to create the successor in.
1183    #[serde(default, skip_serializing_if = "Option::is_none")]
1184    pub project: Option<String>,
1185    /// Successor title.
1186    #[serde(default, skip_serializing_if = "Option::is_none")]
1187    pub title: Option<String>,
1188    /// Why the original was rejected.
1189    #[serde(default, skip_serializing_if = "Option::is_none")]
1190    pub reason: Option<String>,
1191}
1192
1193/// `issue/fold` params.
1194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1195pub struct FoldParams {
1196    /// Inbox file whose unstamped `* TODO` headings become issues.
1197    pub file: String,
1198    /// Project the new issues land in.
1199    #[serde(default, skip_serializing_if = "Option::is_none")]
1200    pub project: Option<String>,
1201}
1202
1203/// `issue/normalize` params.
1204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1205pub struct NormalizeParams {
1206    /// Only this project; every project when absent.
1207    #[serde(default, skip_serializing_if = "Option::is_none")]
1208    pub project: Option<String>,
1209    /// Report what would change without writing it.
1210    #[serde(default)]
1211    pub dry_run: bool,
1212}
1213
1214/// `issue/note` params.
1215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1216pub struct NoteParams {
1217    /// Issue id.
1218    pub id: String,
1219    /// Logbook text.
1220    pub text: String,
1221}
1222
1223/// `issue/refile` params.
1224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1225pub struct RefileParams {
1226    /// Issue id.
1227    pub id: String,
1228    /// Destination project.
1229    pub to: String,
1230}
1231
1232/// Mutation result shared by create, update, claim, note, and refile.
1233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1234pub struct MutResult {
1235    /// True when the write succeeded.
1236    pub ok: bool,
1237    /// Same text the CLI would print.
1238    pub report: String,
1239    /// Post-write detail. Null on refile of a vanished source.
1240    #[serde(default)]
1241    pub issue: Option<IssueDetail>,
1242    /// Serve revision after the write.
1243    pub revision: u64,
1244    /// On-disk generation after the write.
1245    pub generation: u64,
1246}
1247
1248/// `vault/changed` params. Broadcast after a catalog rebuild.
1249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1250pub struct VaultChanged {
1251    /// On-disk generation.
1252    pub generation: u64,
1253    /// Serve-local revision.
1254    pub revision: u64,
1255    /// Dirty project names.
1256    #[serde(default)]
1257    pub projects: Vec<String>,
1258    /// Touched issue ids, when known.
1259    #[serde(default, skip_serializing_if = "Option::is_none")]
1260    pub ids: Option<Vec<String>>,
1261}
1262
1263/// `issue/selected` params. Broadcast after `issue/open`.
1264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1265pub struct IssueSelected {
1266    /// Selected issue id.
1267    pub id: String,
1268    /// Project of that issue.
1269    pub project: String,
1270}
1271
1272/// Push notifications. No `id` on the wire.
1273#[derive(Debug, Clone, PartialEq)]
1274pub enum Notification {
1275    /// [`NOTIFY_VAULT_CHANGED`].
1276    VaultChanged(VaultChanged),
1277    /// [`NOTIFY_ISSUE_SELECTED`].
1278    IssueSelected(IssueSelected),
1279    /// [`NOTIFY_SHUTTING_DOWN`].
1280    ServeShuttingDown,
1281    /// Method the client does not know, or params that failed to decode.
1282    Unknown {
1283        /// Wire method name.
1284        method: String,
1285        /// Raw params.
1286        params: Value,
1287    },
1288}
1289
1290impl Notification {
1291    /// Wire method name.
1292    pub fn method(&self) -> &str {
1293        match self {
1294            Self::VaultChanged(_) => NOTIFY_VAULT_CHANGED,
1295            Self::IssueSelected(_) => NOTIFY_ISSUE_SELECTED,
1296            Self::ServeShuttingDown => NOTIFY_SHUTTING_DOWN,
1297            Self::Unknown { method, .. } => method,
1298        }
1299    }
1300
1301    /// Parse a method/params pair. Unknown names stay [`Self::Unknown`].
1302    pub fn parse(method: &str, params: Value) -> Self {
1303        match method {
1304            NOTIFY_VAULT_CHANGED => match serde_json::from_value(params.clone()) {
1305                Ok(body) => Self::VaultChanged(body),
1306                Err(_) => Self::Unknown {
1307                    method: method.into(),
1308                    params,
1309                },
1310            },
1311            NOTIFY_ISSUE_SELECTED => match serde_json::from_value(params.clone()) {
1312                Ok(body) => Self::IssueSelected(body),
1313                Err(_) => Self::Unknown {
1314                    method: method.into(),
1315                    params,
1316                },
1317            },
1318            NOTIFY_SHUTTING_DOWN => Self::ServeShuttingDown,
1319            other => Self::Unknown {
1320                method: other.into(),
1321                params,
1322            },
1323        }
1324    }
1325
1326    /// Params object for the wire.
1327    pub fn to_params(&self) -> Value {
1328        match self {
1329            Self::VaultChanged(body) => serde_json::to_value(body).unwrap_or(Value::Null),
1330            Self::IssueSelected(body) => serde_json::to_value(body).unwrap_or(Value::Null),
1331            Self::ServeShuttingDown => json!({}),
1332            Self::Unknown { params, .. } => params.clone(),
1333        }
1334    }
1335}
1336
1337/// Typed v1 request.
1338#[derive(Debug, Clone, PartialEq)]
1339pub enum Request {
1340    /// Handshake.
1341    Initialize(InitializeParams),
1342    /// Process identity, root, prefix, and crate version.
1343    IdentityGet,
1344    /// Filtered issue rows.
1345    IssueList(IssueListParams),
1346    /// One issue plus revision.
1347    IssueGet(IdParams),
1348    /// Frontier: `issue/list` with `ready: true`.
1349    IssueReady(IssueListParams),
1350    /// Substring search over id, title, properties, tags, and body.
1351    IssueSearch(SearchParams),
1352    /// Live claims.
1353    IssueClaims(ClaimsParams),
1354    /// Deadlines and scheduled starts.
1355    IssueAgenda(AgendaParams),
1356    /// Alias of [`Self::IssueGet`].
1357    IssueShow(IdParams),
1358    /// Secret-screened body range.
1359    IssueExcerpt(IdParams),
1360    /// Children and blockers.
1361    IssueTree(TreeParams),
1362    /// Bounded neighborhood with evidence.
1363    IssueRelated(RelatedParams),
1364    /// Direct children.
1365    IssueChildren(WalkParams),
1366    /// Walk up the blocker graph.
1367    IssueAncestors(WalkParams),
1368    /// Walk down the blocker graph.
1369    IssueImpact(WalkParams),
1370    /// Everything pointing at the id.
1371    IssueBacklinks(WalkParams),
1372    /// Shared selection; notifies `issue/selected`.
1373    IssueOpen(IdParams),
1374    /// Create an issue.
1375    IssueCreate(CreateParams),
1376    /// State, priority, block, unblock.
1377    IssueUpdate(UpdateParams),
1378    /// Take the issue.
1379    IssueClaim(ClaimParams),
1380    /// Dated logbook entry.
1381    IssueNote(NoteParams),
1382    /// Move to another project.
1383    IssueRefile(RefileParams),
1384    /// Dated report under the heading.
1385    IssueAppend(AppendParams),
1386    /// Cancel and point at a successor.
1387    IssueReject(RejectParams),
1388    /// Settle on a sibling terminal state.
1389    IssueResolve(ResolveParams),
1390    /// Cast a ballot, or read the tally.
1391    IssueVote(VoteParams),
1392    /// Cite, drop, or read the deeds an issue produced.
1393    IssueDeed(DeedParams),
1394    /// The working set for an issue.
1395    IssueRecall(RecallParams),
1396    /// DeGroot consensus over an issue's ballots.
1397    IssueConsensus(ConsensusParams),
1398    /// Inbox headings become issues.
1399    IssueFold(FoldParams),
1400    /// Rewrite onto the property split.
1401    IssueNormalize(NormalizeParams),
1402    /// Validate the corpus.
1403    IssueCheck(ProjectFilterParams),
1404    /// Counts by project, state, readiness.
1405    IssueCount(CountParams),
1406    /// Blocker cycles, if any.
1407    IssueCycles(ProjectFilterParams),
1408    /// Corpus hash, combined and per project.
1409    IssueDigest(DigestParams),
1410    /// The corpus as text.
1411    IssueExport(ProjectFilterParams),
1412    /// One dot document.
1413    IssueGraph(ProjectFilterParams),
1414    /// One roadmap document.
1415    IssueRoadmap(ProjectFilterParams),
1416    /// Issues untouched for a number of days.
1417    IssueStale(StaleParams),
1418    /// Stalled claims plus validation.
1419    IssueHygiene(HygieneParams),
1420    /// What blocks one issue.
1421    IssueWaitingOn(IdParams),
1422    /// The mirror's stamp.
1423    IssueMirror(MirrorCheckParams),
1424    /// Liveness and detail.
1425    EventsPing(PingParams),
1426    /// Block until the generation moves, or an issue is terminal.
1427    EventsWait(WaitParams),
1428    /// Project names plus revision.
1429    ProjectList,
1430    /// Pull of the on-disk event log.
1431    EventsSince(EventsSinceParams),
1432    /// Current generation and revision.
1433    EventsGen,
1434}
1435
1436impl Request {
1437    /// Wire [`Method`] for this request.
1438    pub fn method(&self) -> Method {
1439        match self {
1440            Self::Initialize(_) => Method::Initialize,
1441            Self::IdentityGet => Method::IdentityGet,
1442            Self::IssueList(_) => Method::IssueList,
1443            Self::IssueGet(_) => Method::IssueGet,
1444            Self::IssueReady(_) => Method::IssueReady,
1445            Self::IssueSearch(_) => Method::IssueSearch,
1446            Self::IssueClaims(_) => Method::IssueClaims,
1447            Self::IssueAgenda(_) => Method::IssueAgenda,
1448            Self::IssueShow(_) => Method::IssueShow,
1449            Self::IssueExcerpt(_) => Method::IssueExcerpt,
1450            Self::IssueTree(_) => Method::IssueTree,
1451            Self::IssueRelated(_) => Method::IssueRelated,
1452            Self::IssueChildren(_) => Method::IssueChildren,
1453            Self::IssueAncestors(_) => Method::IssueAncestors,
1454            Self::IssueImpact(_) => Method::IssueImpact,
1455            Self::IssueBacklinks(_) => Method::IssueBacklinks,
1456            Self::IssueOpen(_) => Method::IssueOpen,
1457            Self::IssueCreate(_) => Method::IssueCreate,
1458            Self::IssueUpdate(_) => Method::IssueUpdate,
1459            Self::IssueClaim(_) => Method::IssueClaim,
1460            Self::IssueNote(_) => Method::IssueNote,
1461            Self::IssueRefile(_) => Method::IssueRefile,
1462            Self::IssueAppend(_) => Method::IssueAppend,
1463            Self::IssueReject(_) => Method::IssueReject,
1464            Self::IssueResolve(_) => Method::IssueResolve,
1465            Self::IssueVote(_) => Method::IssueVote,
1466            Self::IssueDeed(_) => Method::IssueDeed,
1467            Self::IssueRecall(_) => Method::IssueRecall,
1468            Self::IssueConsensus(_) => Method::IssueConsensus,
1469            Self::IssueFold(_) => Method::IssueFold,
1470            Self::IssueNormalize(_) => Method::IssueNormalize,
1471            Self::IssueCheck(_) => Method::IssueCheck,
1472            Self::IssueCount(_) => Method::IssueCount,
1473            Self::IssueCycles(_) => Method::IssueCycles,
1474            Self::IssueDigest(_) => Method::IssueDigest,
1475            Self::IssueExport(_) => Method::IssueExport,
1476            Self::IssueGraph(_) => Method::IssueGraph,
1477            Self::IssueRoadmap(_) => Method::IssueRoadmap,
1478            Self::IssueStale(_) => Method::IssueStale,
1479            Self::IssueHygiene(_) => Method::IssueHygiene,
1480            Self::IssueWaitingOn(_) => Method::IssueWaitingOn,
1481            Self::IssueMirror(_) => Method::IssueMirror,
1482            Self::EventsPing(_) => Method::EventsPing,
1483            Self::EventsWait(_) => Method::EventsWait,
1484            Self::ProjectList => Method::ProjectList,
1485            Self::EventsSince(_) => Method::EventsSince,
1486            Self::EventsGen => Method::EventsGen,
1487        }
1488    }
1489
1490    /// Parse a method/params pair.
1491    ///
1492    /// # Errors
1493    ///
1494    /// Returns an error when `method` is unknown or `params` fail to decode.
1495    pub fn parse(method: &str, params: Option<Value>) -> Result<Self, JsonRpcError> {
1496        let method = Method::parse(method)?;
1497        let params = match params {
1498            None | Some(Value::Null) => Value::Object(Default::default()),
1499            Some(v) => v,
1500        };
1501        match method {
1502            Method::Initialize => Ok(Self::Initialize(parse_initialize_params(&params)?)),
1503            Method::IdentityGet => Ok(Self::IdentityGet),
1504            Method::IssueList => Ok(Self::IssueList(decode_params(params)?)),
1505            Method::IssueGet => Ok(Self::IssueGet(decode_params(params)?)),
1506            Method::IssueReady => Ok(Self::IssueReady(decode_params(params)?)),
1507            Method::IssueSearch => Ok(Self::IssueSearch(decode_params(params)?)),
1508            Method::IssueClaims => Ok(Self::IssueClaims(decode_params(params)?)),
1509            Method::IssueAgenda => Ok(Self::IssueAgenda(decode_params(params)?)),
1510            Method::IssueShow => Ok(Self::IssueShow(decode_params(params)?)),
1511            Method::IssueExcerpt => Ok(Self::IssueExcerpt(decode_params(params)?)),
1512            Method::IssueTree => Ok(Self::IssueTree(decode_params(params)?)),
1513            Method::IssueRelated => Ok(Self::IssueRelated(decode_params(params)?)),
1514            Method::IssueChildren => Ok(Self::IssueChildren(decode_params(params)?)),
1515            Method::IssueAncestors => Ok(Self::IssueAncestors(decode_params(params)?)),
1516            Method::IssueImpact => Ok(Self::IssueImpact(decode_params(params)?)),
1517            Method::IssueBacklinks => Ok(Self::IssueBacklinks(decode_params(params)?)),
1518            Method::IssueOpen => Ok(Self::IssueOpen(decode_params(params)?)),
1519            Method::IssueCreate => Ok(Self::IssueCreate(decode_params(params)?)),
1520            Method::IssueUpdate => Ok(Self::IssueUpdate(decode_params(params)?)),
1521            Method::IssueClaim => Ok(Self::IssueClaim(decode_params(params)?)),
1522            Method::IssueNote => Ok(Self::IssueNote(decode_params(params)?)),
1523            Method::IssueRefile => Ok(Self::IssueRefile(decode_params(params)?)),
1524            Method::ProjectList => Ok(Self::ProjectList),
1525            Method::EventsSince => Ok(Self::EventsSince(decode_params(params)?)),
1526            Method::EventsGen => Ok(Self::EventsGen),
1527            Method::IssueAppend => Ok(Self::IssueAppend(decode_params(params)?)),
1528            Method::IssueReject => Ok(Self::IssueReject(decode_params(params)?)),
1529            Method::IssueResolve => Ok(Self::IssueResolve(decode_params(params)?)),
1530            Method::IssueVote => Ok(Self::IssueVote(decode_params(params)?)),
1531            Method::IssueDeed => Ok(Self::IssueDeed(decode_params(params)?)),
1532            Method::IssueRecall => Ok(Self::IssueRecall(decode_params(params)?)),
1533            Method::IssueConsensus => Ok(Self::IssueConsensus(decode_params(params)?)),
1534            Method::IssueFold => Ok(Self::IssueFold(decode_params(params)?)),
1535            Method::IssueNormalize => Ok(Self::IssueNormalize(decode_params(params)?)),
1536            Method::IssueCheck => Ok(Self::IssueCheck(decode_params(params)?)),
1537            Method::IssueCount => Ok(Self::IssueCount(decode_params(params)?)),
1538            Method::IssueCycles => Ok(Self::IssueCycles(decode_params(params)?)),
1539            Method::IssueDigest => Ok(Self::IssueDigest(decode_params(params)?)),
1540            Method::IssueExport => Ok(Self::IssueExport(decode_params(params)?)),
1541            Method::IssueGraph => Ok(Self::IssueGraph(decode_params(params)?)),
1542            Method::IssueRoadmap => Ok(Self::IssueRoadmap(decode_params(params)?)),
1543            Method::IssueStale => Ok(Self::IssueStale(decode_params(params)?)),
1544            Method::IssueHygiene => Ok(Self::IssueHygiene(decode_params(params)?)),
1545            Method::IssueWaitingOn => Ok(Self::IssueWaitingOn(decode_params(params)?)),
1546            Method::IssueMirror => Ok(Self::IssueMirror(decode_params(params)?)),
1547            Method::EventsPing => Ok(Self::EventsPing(decode_params(params)?)),
1548            Method::EventsWait => Ok(Self::EventsWait(decode_params(params)?)),
1549        }
1550    }
1551
1552    /// Params object for the wire.
1553    pub fn to_params(&self) -> Value {
1554        match self {
1555            Self::Initialize(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1556            Self::IssueAppend(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1557            Self::IssueReject(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1558            Self::IssueResolve(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1559            Self::IssueVote(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1560            Self::IssueDeed(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1561            Self::IssueRecall(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1562            Self::IssueConsensus(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1563            Self::IssueFold(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1564            Self::IssueNormalize(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1565            Self::IssueCheck(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1566            Self::IssueCount(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1567            Self::IssueCycles(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1568            Self::IssueDigest(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1569            Self::IssueExport(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1570            Self::IssueGraph(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1571            Self::IssueRoadmap(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1572            Self::IssueStale(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1573            Self::IssueHygiene(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1574            Self::IssueWaitingOn(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1575            Self::IssueMirror(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1576            Self::EventsPing(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1577            Self::EventsWait(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1578            Self::IdentityGet | Self::ProjectList | Self::EventsGen => json!({}),
1579            Self::IssueList(p) | Self::IssueReady(p) => {
1580                serde_json::to_value(p).unwrap_or(Value::Null)
1581            }
1582            Self::IssueGet(p) | Self::IssueShow(p) | Self::IssueExcerpt(p) | Self::IssueOpen(p) => {
1583                serde_json::to_value(p).unwrap_or(Value::Null)
1584            }
1585            Self::IssueSearch(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1586            Self::IssueClaims(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1587            Self::IssueAgenda(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1588            Self::IssueTree(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1589            Self::IssueRelated(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1590            Self::IssueChildren(p)
1591            | Self::IssueAncestors(p)
1592            | Self::IssueImpact(p)
1593            | Self::IssueBacklinks(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1594            Self::IssueCreate(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1595            Self::IssueUpdate(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1596            Self::IssueClaim(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1597            Self::IssueNote(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1598            Self::IssueRefile(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1599            Self::EventsSince(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1600        }
1601    }
1602}
1603
1604/// Typed v1 result body.
1605#[derive(Debug, Clone)]
1606pub enum Response {
1607    /// Handshake.
1608    Initialize(InitializeResult),
1609    /// Process identity, root, prefix, and crate version.
1610    IdentityGet(IdentityResult),
1611    /// Filtered issue rows.
1612    IssueList(IssueListResult),
1613    /// One issue plus revision.
1614    IssueGet(IssueGetResult),
1615    /// Frontier page.
1616    IssueReady(IssueListResult),
1617    /// Search hits.
1618    IssueSearch(Vec<SearchHit>),
1619    /// Live claims.
1620    IssueClaims(Vec<ClaimRow>),
1621    /// Deadlines and scheduled starts.
1622    IssueAgenda(Vec<AgendaRow>),
1623    /// Alias of [`Self::IssueGet`].
1624    IssueShow(IssueGetResult),
1625    /// Secret-screened body range.
1626    IssueExcerpt(Excerpt),
1627    /// Children and blockers.
1628    IssueTree(TreeResult),
1629    /// Bounded neighborhood with evidence.
1630    IssueRelated(Vec<RelatedHit>),
1631    /// Direct children.
1632    IssueChildren(Vec<WalkHit>),
1633    /// Walk up the blocker graph.
1634    IssueAncestors(Vec<WalkHit>),
1635    /// Walk down the blocker graph.
1636    IssueImpact(Vec<WalkHit>),
1637    /// Everything pointing at the id.
1638    IssueBacklinks(Vec<WalkHit>),
1639    /// Shared selection result.
1640    IssueOpen(IssueGetResult),
1641    /// Create result.
1642    IssueCreate(MutResult),
1643    /// Update result.
1644    IssueUpdate(MutResult),
1645    /// Claim result.
1646    IssueClaim(MutResult),
1647    /// Note result.
1648    IssueNote(MutResult),
1649    /// Refile result.
1650    IssueRefile(MutResult),
1651    /// Dated report under the heading.
1652    IssueAppend(MutResult),
1653    /// Cancel and point at a successor.
1654    IssueReject(MutResult),
1655    /// Settle on a sibling terminal state.
1656    IssueResolve(MutResult),
1657    /// Cast a ballot, or read the tally.
1658    IssueVote(MutResult),
1659    /// Cite, drop, or read the deeds an issue produced.
1660    IssueDeed(MutResult),
1661    /// The working set for an issue.
1662    IssueRecall(Recall),
1663    /// Consensus over an issue's ballots, or over its children.
1664    IssueConsensus(Value),
1665    /// Inbox headings become issues.
1666    IssueFold(MutResult),
1667    /// Rewrite onto the property split.
1668    IssueNormalize(MutResult),
1669    /// Validation findings plus counts.
1670    IssueCheck(CheckResult),
1671    /// Counts by project, state, readiness.
1672    IssueCount(ReportResult),
1673    /// Blocker cycles, if any.
1674    IssueCycles(ReportResult),
1675    /// Corpus hash, combined and per project.
1676    IssueDigest(DigestResult),
1677    /// The corpus as text.
1678    IssueExport(ReportResult),
1679    /// One dot document.
1680    IssueGraph(ReportResult),
1681    /// One roadmap document.
1682    IssueRoadmap(ReportResult),
1683    /// Issues untouched for a number of days.
1684    IssueStale(ReportResult),
1685    /// Stalled claims plus validation.
1686    IssueHygiene(ReportResult),
1687    /// What blocks one issue.
1688    IssueWaitingOn(ReportResult),
1689    /// The mirror's stamp.
1690    IssueMirror(MirrorCheckResult),
1691    /// Liveness and detail.
1692    EventsPing(ReportResult),
1693    /// Generation reached, or the state waited for.
1694    EventsWait(WaitResult),
1695    /// Project names plus revision.
1696    ProjectList(ProjectListResult),
1697    /// Pull of the on-disk event log.
1698    EventsSince(EventsSinceResult),
1699    /// Current generation and revision.
1700    EventsGen(EventsGenResult),
1701}
1702
1703impl Response {
1704    /// Serialize the result body (not the envelope).
1705    ///
1706    /// # Errors
1707    ///
1708    /// Returns an error when the body cannot be encoded as JSON.
1709    pub fn to_value(&self) -> Result<Value, serde_json::Error> {
1710        match self {
1711            Self::Initialize(v) => serde_json::to_value(v),
1712            Self::IdentityGet(v) => serde_json::to_value(v),
1713            Self::IssueAppend(v) => serde_json::to_value(v),
1714            Self::IssueReject(v) => serde_json::to_value(v),
1715            Self::IssueResolve(v) => serde_json::to_value(v),
1716            Self::IssueVote(v) => serde_json::to_value(v),
1717            Self::IssueDeed(v) => serde_json::to_value(v),
1718            Self::IssueRecall(v) => serde_json::to_value(v),
1719            Self::IssueConsensus(v) => serde_json::to_value(v),
1720            Self::IssueFold(v) => serde_json::to_value(v),
1721            Self::IssueNormalize(v) => serde_json::to_value(v),
1722            Self::IssueCheck(v) => serde_json::to_value(v),
1723            Self::IssueCount(v) => serde_json::to_value(v),
1724            Self::IssueCycles(v) => serde_json::to_value(v),
1725            Self::IssueDigest(v) => serde_json::to_value(v),
1726            Self::IssueExport(v) => serde_json::to_value(v),
1727            Self::IssueGraph(v) => serde_json::to_value(v),
1728            Self::IssueRoadmap(v) => serde_json::to_value(v),
1729            Self::IssueStale(v) => serde_json::to_value(v),
1730            Self::IssueHygiene(v) => serde_json::to_value(v),
1731            Self::IssueWaitingOn(v) => serde_json::to_value(v),
1732            Self::IssueMirror(v) => serde_json::to_value(v),
1733            Self::EventsPing(v) => serde_json::to_value(v),
1734            Self::EventsWait(v) => serde_json::to_value(v),
1735            Self::IssueList(v) | Self::IssueReady(v) => serde_json::to_value(v),
1736            Self::IssueGet(v) | Self::IssueShow(v) | Self::IssueOpen(v) => serde_json::to_value(v),
1737            Self::IssueSearch(v) => serde_json::to_value(v),
1738            Self::IssueClaims(v) => serde_json::to_value(v),
1739            Self::IssueAgenda(v) => serde_json::to_value(v),
1740            Self::IssueExcerpt(v) => serde_json::to_value(v),
1741            Self::IssueTree(v) => serde_json::to_value(v),
1742            Self::IssueRelated(v) => serde_json::to_value(v),
1743            Self::IssueChildren(v)
1744            | Self::IssueAncestors(v)
1745            | Self::IssueImpact(v)
1746            | Self::IssueBacklinks(v) => serde_json::to_value(v),
1747            Self::IssueCreate(v)
1748            | Self::IssueUpdate(v)
1749            | Self::IssueClaim(v)
1750            | Self::IssueNote(v)
1751            | Self::IssueRefile(v) => serde_json::to_value(v),
1752            Self::ProjectList(v) => serde_json::to_value(v),
1753            Self::EventsSince(v) => serde_json::to_value(v),
1754            Self::EventsGen(v) => serde_json::to_value(v),
1755        }
1756    }
1757}
1758
1759fn decode_params<T: DeserializeOwned>(value: Value) -> Result<T, JsonRpcError> {
1760    serde_json::from_value(value).map_err(|e| invalid_params(e.to_string()))
1761}
1762
1763#[cfg(test)]
1764mod tests {
1765    use super::*;
1766    use std::collections::BTreeMap;
1767
1768    #[test]
1769    fn initialize_missing_agent_is_invalid_params() {
1770        let err = parse_initialize_params(&json!({
1771            "protocolVersion": 1,
1772            "client": "vissue-tui"
1773        }))
1774        .unwrap_err();
1775        assert_eq!(err.code, INVALID_PARAMS);
1776        assert_eq!(err.message, "agent is required");
1777
1778        let err = parse_initialize_params(&json!({
1779            "protocolVersion": 1,
1780            "agent": ""
1781        }))
1782        .unwrap_err();
1783        assert_eq!(err.code, INVALID_PARAMS);
1784        assert_eq!(err.message, "agent is required");
1785
1786        let err = Request::parse(
1787            "initialize",
1788            Some(json!({"protocolVersion": 1, "agent": "   "})),
1789        )
1790        .unwrap_err();
1791        assert_eq!(err.code, INVALID_PARAMS);
1792    }
1793
1794    #[test]
1795    fn protocol_version_2_is_rejected() {
1796        let err = parse_initialize_params(&json!({
1797            "protocolVersion": 2,
1798            "agent": "rg@host"
1799        }))
1800        .unwrap_err();
1801        assert_eq!(err.code, INVALID_PARAMS);
1802        assert_eq!(err.message, "unsupported protocol version");
1803        assert_eq!(err.data, Some(json!({"supported": 1})));
1804    }
1805
1806    #[test]
1807    fn initialize_version_1_is_accepted() {
1808        let params = parse_initialize_params(&json!({
1809            "protocolVersion": 1,
1810            "client": "vissue-tui",
1811            "agent": "rg@host"
1812        }))
1813        .unwrap();
1814        assert_eq!(params.protocol_version, 1);
1815        assert_eq!(params.agent, "rg@host");
1816        assert_eq!(params.client, "vissue-tui");
1817    }
1818
1819    #[test]
1820    fn handshake_fields_are_camel_case() {
1821        let params = InitializeParams {
1822            protocol_version: 1,
1823            client: "vissue-tui".into(),
1824            agent: "rg@host".into(),
1825        };
1826        let value = serde_json::to_value(&params).unwrap();
1827        assert_eq!(value["protocolVersion"], 1);
1828        assert!(value.get("protocol_version").is_none());
1829
1830        let result = InitializeResult {
1831            protocol_version: 1,
1832            capabilities: vec!["issue/list".into()],
1833            root: "/tmp/tracker".into(),
1834            prefix: "Software".into(),
1835            generation: 3,
1836            revision: 1,
1837            identity: "rg@host".into(),
1838        };
1839        let value = serde_json::to_value(&result).unwrap();
1840        assert_eq!(value["protocolVersion"], 1);
1841        assert_eq!(value["generation"], 3);
1842    }
1843
1844    #[test]
1845    fn issue_payloads_are_snake_case() {
1846        let params = IssueListParams {
1847            since_revision: Some(41),
1848            ..IssueListParams::default()
1849        };
1850        let value = serde_json::to_value(&params).unwrap();
1851        assert_eq!(value["since_revision"], 41);
1852        assert!(value.get("sinceRevision").is_none());
1853    }
1854
1855    #[test]
1856    fn unknown_method_is_not_found() {
1857        // Deliberately a name no verb will ever take. This test used "issue/fold"
1858        // until fold became a method, and the same trap caught the owner's copy of
1859        // this test on the same day: an example chosen because it sounds plausible is
1860        // an example that will one day be real, and then the test asserts that a
1861        // working method is missing.
1862        const NEVER: &str = "issue/no-such-method";
1863        let err = Method::parse(NEVER).unwrap_err();
1864        assert_eq!(err.code, METHOD_NOT_FOUND);
1865        assert_eq!(err.data, Some(json!({"method": NEVER})));
1866    }
1867
1868    #[test]
1869    fn every_v1_capability_parses() {
1870        for name in V1_CAPABILITIES {
1871            assert!(Method::parse(name).is_ok(), "{name}");
1872        }
1873        assert_eq!(Method::Initialize.as_str(), "initialize");
1874    }
1875
1876    #[test]
1877    fn request_parse_roundtrips_issue_get() {
1878        let req = Request::parse("issue/get", Some(json!({"id": "atlas-1a2b"}))).unwrap();
1879        assert_eq!(req.method(), Method::IssueGet);
1880        assert_eq!(req.to_params()["id"], "atlas-1a2b");
1881    }
1882
1883    #[test]
1884    fn missing_id_on_issue_get_is_invalid_params() {
1885        let err = Request::parse("issue/get", Some(json!({}))).unwrap_err();
1886        assert_eq!(err.code, INVALID_PARAMS);
1887    }
1888
1889    #[test]
1890    fn core_errors_carry_data_code() {
1891        let err = error_from_core(&CoreError::IssueNotFound {
1892            id: "atlas-1a2b".into(),
1893        });
1894        assert_eq!(err.code, NOT_FOUND);
1895        assert_eq!(err.data.unwrap()["code"], "not_found");
1896
1897        let err = error_from_core(&CoreError::ClaimConflict {
1898            id: "atlas-1a2b".into(),
1899            holder: "other".into(),
1900            claimed_at: None,
1901        });
1902        assert_eq!(err.code, CONFLICT);
1903        let data = err.data.unwrap();
1904        assert_eq!(data["code"], "conflict");
1905        assert_eq!(data["holder"], "other");
1906
1907        let err = error_from_core(&CoreError::BlockerCycle {
1908            blocker: "a".into(),
1909            issue: "b".into(),
1910        });
1911        assert_eq!(err.code, CYCLE);
1912        let data = err.data.unwrap();
1913        assert_eq!(data["code"], "cycle");
1914        assert_eq!(data["id"], "b");
1915        assert_eq!(data["block"], "a");
1916
1917        let err = error_from_core(&CoreError::InvalidState {
1918            id: "atlas-4g5h".into(),
1919            state: "DONE".into(),
1920        });
1921        assert_eq!(err.code, INVALID_STATE);
1922        assert_eq!(err.data.unwrap()["code"], "invalid_state");
1923
1924        let err = error_from_core(&CoreError::DuplicateId {
1925            id: "atlas-1a2b".into(),
1926            paths: vec![
1927                std::path::PathBuf::from("/a/issues.org"),
1928                std::path::PathBuf::from("/b/issues.org"),
1929            ],
1930        });
1931        assert_eq!(err.code, CONFLICT);
1932        assert_eq!(err.data.unwrap()["code"], "duplicate_id");
1933    }
1934
1935    #[test]
1936    fn notification_parse_known_methods() {
1937        let n = Notification::parse(
1938            NOTIFY_VAULT_CHANGED,
1939            json!({"generation": 1, "revision": 2, "projects": ["atlas"]}),
1940        );
1941        assert!(matches!(n, Notification::VaultChanged(_)));
1942        assert_eq!(n.method(), NOTIFY_VAULT_CHANGED);
1943
1944        let n = Notification::parse(
1945            NOTIFY_ISSUE_SELECTED,
1946            json!({"id": "atlas-1a2b", "project": "atlas"}),
1947        );
1948        assert!(matches!(n, Notification::IssueSelected(_)));
1949
1950        let n = Notification::parse(NOTIFY_SHUTTING_DOWN, json!({}));
1951        assert!(matches!(n, Notification::ServeShuttingDown));
1952        assert_eq!(n.to_params(), json!({}));
1953    }
1954
1955    #[test]
1956    fn list_unchanged_deserializes_without_rows() {
1957        let page: IssueListResult =
1958            serde_json::from_value(json!({"unchanged": true, "revision": 41})).unwrap();
1959        assert!(page.unchanged);
1960        assert!(page.issues.is_empty());
1961        assert_eq!(page.revision, 41);
1962    }
1963
1964    #[test]
1965    fn response_to_value_serializes_initialize() {
1966        let resp = Response::Initialize(InitializeResult {
1967            protocol_version: 1,
1968            capabilities: V1_CAPABILITIES.iter().map(|s| (*s).to_string()).collect(),
1969            root: "/tmp".into(),
1970            prefix: "Software".into(),
1971            generation: 1,
1972            revision: 1,
1973            identity: "agent".into(),
1974        });
1975        let value = resp.to_value().unwrap();
1976        assert_eq!(value["protocolVersion"], 1);
1977        assert!(
1978            value["capabilities"]
1979                .as_array()
1980                .unwrap()
1981                .contains(&json!("issue/list"))
1982        );
1983    }
1984
1985    #[test]
1986    fn envelope_helpers_roundtrip() {
1987        let req = JsonRpcRequest::call(JsonRpcId::Number(1), "identity/get", json!({}));
1988        let bytes = serde_json::to_vec(&req).unwrap();
1989        let back: JsonRpcRequest = serde_json::from_slice(&bytes).unwrap();
1990        assert_eq!(back.method, "identity/get");
1991        assert!(!back.is_notification());
1992
1993        let note = JsonRpcRequest::notification(NOTIFY_SHUTTING_DOWN, json!({}));
1994        assert!(note.is_notification());
1995
1996        let ok = JsonRpcResponse::ok(Some(JsonRpcId::Number(1)), json!({"ok": true}));
1997        assert_eq!(ok.result.unwrap()["ok"], true);
1998        let err = JsonRpcResponse::err(Some(JsonRpcId::Null), parse_error());
1999        assert_eq!(err.error.unwrap().code, PARSE_ERROR);
2000    }
2001
2002    #[test]
2003    fn mut_and_walk_params_decode() {
2004        let claim = Request::parse(
2005            "issue/claim",
2006            Some(json!({"id": "atlas-1a2b", "force": true})),
2007        )
2008        .unwrap();
2009        match claim {
2010            Request::IssueClaim(p) => {
2011                assert!(p.force);
2012                assert_eq!(p.id, "atlas-1a2b");
2013            }
2014            other => panic!("{other:?}"),
2015        }
2016        let create = Request::parse(
2017            "issue/create",
2018            Some(json!({"project": "atlas", "title": "x"})),
2019        )
2020        .unwrap();
2021        assert_eq!(create.method(), Method::IssueCreate);
2022        assert!(Request::parse("issue/create", Some(json!({"title": "x"}))).is_err());
2023        assert_eq!(
2024            Request::parse("events/gen", None).unwrap().method(),
2025            Method::EventsGen
2026        );
2027        let _ = Request::IssueNote(NoteParams {
2028            id: "a".into(),
2029            text: "n".into(),
2030        })
2031        .to_params();
2032        let _ = Request::IssueRefile(RefileParams {
2033            id: "a".into(),
2034            to: "b".into(),
2035        })
2036        .to_params();
2037        let _ = Request::IssueUpdate(UpdateParams {
2038            id: "a".into(),
2039            state: Some("STARTED".into()),
2040            priority: None,
2041            block: None,
2042            unblock: None,
2043            if_state: None,
2044            if_gen: None,
2045            agent: None,
2046        })
2047        .to_params();
2048        let _ = Request::EventsSince(EventsSinceParams {
2049            since: 0,
2050            limit: Some(10),
2051        })
2052        .to_params();
2053        let _ = Request::IssueTree(TreeParams {
2054            id: "a".into(),
2055            format: Some("ascii".into()),
2056        })
2057        .to_params();
2058        let _ = Request::IssueRelated(RelatedParams {
2059            id: "a".into(),
2060            depth: Some(2),
2061            limit: Some(20),
2062        })
2063        .to_params();
2064        let _ = Request::IssueChildren(WalkParams {
2065            id: "a".into(),
2066            depth: None,
2067        })
2068        .to_params();
2069        let _ = Request::IssueSearch(SearchParams {
2070            query: "q".into(),
2071            limit: None,
2072        })
2073        .to_params();
2074        let _ = Request::IssueClaims(ClaimsParams::default()).to_params();
2075        let _ = Request::IssueAgenda(AgendaParams::default()).to_params();
2076        let _ = Request::IdentityGet.to_params();
2077    }
2078
2079    #[test]
2080    fn response_variants_serialize() {
2081        let detail = IssueDetail {
2082            id: "atlas-1a2b".into(),
2083            project: "atlas".into(),
2084            title: "t".into(),
2085            state: "TODO".into(),
2086            priority: "B".into(),
2087            properties: BTreeMap::new(),
2088            org_tags: vec![],
2089            tags: vec![],
2090            blocked_by: vec![],
2091            deeds: vec![],
2092            parent: None,
2093            claimed_by: None,
2094            claimed_at: None,
2095            file: "issues.org:1-2".into(),
2096            line_start: 1,
2097            line_end: 2,
2098            body: "what the issue asks for".into(),
2099            logbook: vec![],
2100        };
2101        let get = IssueGetResult {
2102            issue: detail.clone(),
2103            revision: 1,
2104        };
2105        assert!(Response::IssueGet(get.clone()).to_value().unwrap()["id"] == "atlas-1a2b");
2106        assert!(Response::IssueShow(get.clone()).to_value().is_ok());
2107        assert!(Response::IssueOpen(get).to_value().is_ok());
2108        assert!(
2109            Response::IssueExcerpt(Excerpt {
2110                id: "atlas-1a2b".into(),
2111                file: "issues.org".into(),
2112                line_start: 1,
2113                line_end: 2,
2114                text: "body".into(),
2115                suppressed: false,
2116            })
2117            .to_value()
2118            .is_ok()
2119        );
2120        assert!(Response::IssueSearch(vec![]).to_value().unwrap().is_array());
2121        assert!(Response::IssueClaims(vec![]).to_value().unwrap().is_array());
2122        assert!(Response::IssueAgenda(vec![]).to_value().unwrap().is_array());
2123        assert!(
2124            Response::IssueRelated(vec![])
2125                .to_value()
2126                .unwrap()
2127                .is_array()
2128        );
2129        assert!(
2130            Response::IssueChildren(vec![])
2131                .to_value()
2132                .unwrap()
2133                .is_array()
2134        );
2135        assert!(
2136            Response::IssueAncestors(vec![])
2137                .to_value()
2138                .unwrap()
2139                .is_array()
2140        );
2141        assert!(Response::IssueImpact(vec![]).to_value().unwrap().is_array());
2142        assert!(
2143            Response::IssueBacklinks(vec![])
2144                .to_value()
2145                .unwrap()
2146                .is_array()
2147        );
2148        assert!(
2149            Response::ProjectList(ProjectListResult {
2150                projects: vec!["atlas".into()],
2151                revision: 1,
2152            })
2153            .to_value()
2154            .is_ok()
2155        );
2156        assert!(
2157            Response::EventsGen(EventsGenResult {
2158                generation: 1,
2159                revision: 1,
2160            })
2161            .to_value()
2162            .is_ok()
2163        );
2164        assert!(
2165            Response::EventsSince(EventsSinceResult {
2166                events: vec![],
2167                generation: 1,
2168            })
2169            .to_value()
2170            .is_ok()
2171        );
2172        assert!(
2173            Response::IdentityGet(IdentityResult {
2174                identity: "a".into(),
2175                root: "/".into(),
2176                prefix: "Software".into(),
2177                version: "0.2.0".into(),
2178            })
2179            .to_value()
2180            .is_ok()
2181        );
2182        let mut_ok = MutResult {
2183            ok: true,
2184            report: "ok".into(),
2185            issue: Some(detail),
2186            revision: 2,
2187            generation: 3,
2188        };
2189        assert!(Response::IssueClaim(mut_ok.clone()).to_value().is_ok());
2190        assert!(Response::IssueCreate(mut_ok.clone()).to_value().is_ok());
2191        assert!(Response::IssueUpdate(mut_ok.clone()).to_value().is_ok());
2192        assert!(Response::IssueNote(mut_ok.clone()).to_value().is_ok());
2193        assert!(Response::IssueRefile(mut_ok).to_value().is_ok());
2194        assert!(
2195            Response::IssueTree(TreeResult::Text { text: "* a".into() })
2196                .to_value()
2197                .is_ok()
2198        );
2199        assert!(
2200            Response::IssueList(IssueListResult {
2201                revision: 1,
2202                ..IssueListResult::default()
2203            })
2204            .to_value()
2205            .is_ok()
2206        );
2207        assert!(
2208            Response::IssueReady(IssueListResult {
2209                revision: 1,
2210                ..IssueListResult::default()
2211            })
2212            .to_value()
2213            .is_ok()
2214        );
2215    }
2216
2217    #[test]
2218    fn parse_every_method_with_minimal_params() {
2219        let id = json!({"id": "atlas-1a2b"});
2220        for (method, params) in [
2221            ("identity/get", json!({})),
2222            ("issue/list", json!({})),
2223            ("issue/get", id.clone()),
2224            ("issue/ready", json!({})),
2225            ("issue/search", json!({"query": "q"})),
2226            ("issue/claims", json!({})),
2227            ("issue/agenda", json!({})),
2228            ("issue/show", id.clone()),
2229            ("issue/excerpt", id.clone()),
2230            ("issue/tree", id.clone()),
2231            ("issue/related", id.clone()),
2232            ("issue/children", id.clone()),
2233            ("issue/ancestors", id.clone()),
2234            ("issue/impact", id.clone()),
2235            ("issue/backlinks", id.clone()),
2236            ("issue/open", id.clone()),
2237            ("issue/create", json!({"project": "atlas", "title": "t"})),
2238            ("issue/update", id.clone()),
2239            ("issue/claim", id.clone()),
2240            ("issue/note", json!({"id": "atlas-1a2b", "text": "n"})),
2241            ("issue/refile", json!({"id": "atlas-1a2b", "to": "beacon"})),
2242            ("project/list", json!({})),
2243            ("events/since", json!({"since": 0})),
2244            ("events/gen", json!({})),
2245        ] {
2246            let req = Request::parse(method, Some(params)).expect(method);
2247            assert_eq!(req.method().as_str(), method);
2248            let _ = req.to_params();
2249        }
2250    }
2251
2252    #[test]
2253    fn helper_errors_have_stable_codes() {
2254        assert_eq!(invalid_request().code, INVALID_REQUEST);
2255        assert_eq!(internal_error("x").code, INTERNAL_ERROR);
2256        assert_eq!(parse_error().code, PARSE_ERROR);
2257        let err = Error::Rpc(invalid_params("agent is required"));
2258        assert_eq!(err.to_string(), "agent is required");
2259        let _ = Error::Unsupported("unix only");
2260        let _ = Notification::parse("vault/changed", json!(null));
2261        let _ = Notification::parse("issue/selected", json!(null));
2262        let _ = Notification::parse("other/x", json!({"a": 1}));
2263        let n = Notification::Unknown {
2264            method: "x".into(),
2265            params: json!({"a": 1}),
2266        };
2267        assert_eq!(n.to_params()["a"], 1);
2268        assert_eq!(n.method(), "x");
2269    }
2270    /// Every method has a typed request form, and it round-trips.
2271    ///
2272    /// Nineteen methods reached the wire with no typed form for a while, and the
2273    /// typed helpers answered "send it untyped" per method. That was honest and it
2274    /// was a hole: a client wanting typed access to `issue/check` could not have it,
2275    /// and the two enums drifted from the method list by exactly the amount nobody
2276    /// was checking.
2277    ///
2278    /// Driven from `V1_CAPABILITIES`, so a method added to the wire without a typed
2279    /// form fails here rather than being discovered by whoever wanted it.
2280    #[test]
2281    fn every_advertised_method_has_a_typed_request() {
2282        for name in V1_CAPABILITIES {
2283            let method = Method::parse(name).unwrap_or_else(|_| panic!("{name} does not parse"));
2284            assert_eq!(
2285                method.as_str(),
2286                *name,
2287                "{name} does not round-trip as a method"
2288            );
2289
2290            // Empty params: what matters here is that a typed form exists and that
2291            // its required fields are the reason a decode fails, not the absence of
2292            // any form at all.
2293            let parsed = Request::parse(name, Some(json!({})));
2294            if let Ok(req) = parsed {
2295                assert_eq!(
2296                    req.method().as_str(),
2297                    *name,
2298                    "{name} parsed into a request that reports a different method"
2299                );
2300                // And the params it holds serialize back to an object.
2301                assert!(
2302                    req.to_params().is_object(),
2303                    "{name} does not serialize its params to an object"
2304                );
2305            }
2306        }
2307    }
2308
2309    /// And every typed response encodes.
2310    #[test]
2311    fn the_new_typed_responses_encode() {
2312        let cases = vec![
2313            Response::IssueCheck(CheckResult {
2314                report: "ok".into(),
2315                errors: 0,
2316                warnings: 2,
2317            }),
2318            Response::IssueCount(ReportResult {
2319                report: "3 issues".into(),
2320            }),
2321            Response::IssueDigest(DigestResult {
2322                combined: "abcd".into(),
2323                issues: 3,
2324                generation: 4,
2325                projects: vec![ProjectDigestResult {
2326                    project: "atlas".into(),
2327                    digest: "beef".into(),
2328                    issues: 3,
2329                }],
2330            }),
2331            Response::EventsWait(WaitResult {
2332                generation: 7,
2333                state: Some("DONE".into()),
2334                timed_out: false,
2335            }),
2336        ];
2337        for case in cases {
2338            let value = case.to_value().expect("encode");
2339            assert!(value.is_object(), "{value} is not an object");
2340        }
2341
2342        // The check counts survive the trip, since a client acts on them.
2343        let encoded = Response::IssueCheck(CheckResult {
2344            report: "two warnings".into(),
2345            errors: 0,
2346            warnings: 2,
2347        })
2348        .to_value()
2349        .unwrap();
2350        assert_eq!(encoded["warnings"], 2);
2351        assert_eq!(encoded["errors"], 0);
2352    }
2353}