1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
//! Provider-neutral issue/change-request types (#1779).
//!
//! A minimal, **GitHub-only** slice of the provider abstraction proposed in
//! `#1573 §3.1`. [`GitProvider`] carries only the [`GitProvider::GitHub`]
//! variant today; `#1573` adds `GitLab { host }` alongside it. Everything
//! downstream of a fetch — question building, routing, and (later) decision
//! verification — consumes only [`IssueDoc`] and friends, so a future GitLab
//! fetcher slots in without touching that code.
use serde::{Deserialize, Serialize};
/// Which forge an [`IssueDoc`] (or [`ItemRef`]) came from.
///
/// Only [`GitHub`](Self::GitHub) exists today. `#1573` adds a `GitLab { host
/// }` variant for self-hosted instances; nothing here should assume `GitHub`
/// is the only case forever, but nothing needs to guard against a second
/// variant yet either.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GitProvider {
/// github.com. A GitHub Enterprise host is not supported yet: issue URLs
/// must be on github.com, and `gh` queries its default host.
GitHub,
}
/// Whether a referenced item is an issue or a change request (GitHub pull
/// request / GitLab merge request).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ItemKind {
/// A tracking issue.
Issue,
/// A pull request (GitHub) or merge request (GitLab), named neutrally
/// per the issue's "use neutral wording from the start" guidance —
/// rewording later means re-validating every prompt that mentions it.
ChangeRequest,
}
/// Whether an item is still open.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ItemState {
/// Open.
Open,
/// Closed (merged or not).
Closed,
}
/// A human-authored comment on an [`IssueDoc`].
///
/// Bot and system-event noise is filtered out by the fetch layer before an
/// `IssueDoc` is built — this type only ever holds a human comment.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Comment {
/// The commenter's display/login name.
pub author: String,
/// The comment body (raw markdown, as authored).
pub body: String,
/// The comment's GitHub database id, used by `verify-decision`'s
/// `--comment ID` to select one. `None` in hand-built test fixtures.
#[serde(default)]
pub id: Option<u64>,
}
/// A reference to another issue or change request, e.g. one cited by a
/// decision comment, or one that closed an issue.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ItemRef {
/// The provider this reference resolves against.
pub provider: GitProvider,
/// Opaque full project path (`"owner/repo"` on GitHub; GitLab paths
/// nest, per `#1573 §2.6`).
pub project: String,
/// Whether this reference is an issue or a change request.
pub kind: ItemKind,
/// The issue number / GitLab `iid`.
pub number: u64,
}
impl std::fmt::Display for ItemRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}#{}", self.project, self.number)
}
}
/// A fetched issue (or, in principle, change request) with everything a Jev
/// question needs to judge it: title, body, human comments, and what closed
/// it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IssueDoc {
/// Which forge this came from.
pub provider: GitProvider,
/// Opaque full project path (`"owner/repo"` on GitHub).
pub project: String,
/// The issue number.
pub number: u64,
/// Always [`ItemKind::Issue`] today — `#1779` only ever routes issues,
/// never change requests, but the field stays generic for reuse by a
/// later `verify-decision` cited-source fetch.
pub kind: ItemKind,
/// The issue title.
pub title: String,
/// Whether the issue is open or closed.
pub state: ItemState,
/// The issue body (raw markdown, as authored).
pub body: String,
/// Human comments, in creation order. Bot and system-event comments are
/// already filtered out.
pub comments: Vec<Comment>,
/// Change requests that closed this issue, if any.
pub closed_by: Vec<ItemRef>,
/// The issue's web URL.
pub url: String,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn item_ref_display_is_project_hash_number() {
let item_ref = ItemRef {
provider: GitProvider::GitHub,
project: "rust-works/omni-dev".to_string(),
kind: ItemKind::Issue,
number: 1779,
};
assert_eq!(item_ref.to_string(), "rust-works/omni-dev#1779");
}
#[test]
fn git_provider_serialises_lowercase() {
assert_eq!(
serde_json::to_value(GitProvider::GitHub).unwrap(),
serde_json::json!("github")
);
}
#[test]
fn item_kind_serialises_lowercase() {
assert_eq!(
serde_json::to_value(ItemKind::ChangeRequest).unwrap(),
serde_json::json!("changerequest")
);
}
#[test]
fn item_state_serialises_lowercase() {
assert_eq!(
serde_json::to_value(ItemState::Open).unwrap(),
serde_json::json!("open")
);
assert_eq!(
serde_json::to_value(ItemState::Closed).unwrap(),
serde_json::json!("closed")
);
}
}