1use crate::{ConformanceVector, MaxCodeAction, MaxDiagnostic, PolicyState, SnapshotId};
2use lsp_types_max::{ClientCapabilities, ServerCapabilities};
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct InstanceId(pub String);
11
12impl From<String> for InstanceId {
13 fn from(s: String) -> Self {
14 InstanceId(s)
15 }
16}
17
18impl From<&str> for InstanceId {
19 fn from(s: &str) -> Self {
20 InstanceId(s.to_string())
21 }
22}
23
24impl PartialEq<str> for InstanceId {
25 fn eq(&self, other: &str) -> bool {
26 self.0 == other
27 }
28}
29impl PartialEq<InstanceId> for str {
30 fn eq(&self, other: &InstanceId) -> bool {
31 self == other.0
32 }
33}
34
35impl std::fmt::Display for InstanceId {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 write!(f, "{}", self.0)
38 }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
46pub struct GateId(pub String);
47
48impl std::fmt::Display for GateId {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 write!(f, "{}", self.0)
51 }
52}
53
54impl From<String> for GateId {
55 fn from(s: String) -> Self {
56 Self(s)
57 }
58}
59
60impl From<&str> for GateId {
61 fn from(s: &str) -> Self {
62 Self(s.to_owned())
63 }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ReceiptObligation {
68 pub required_receipts: Vec<String>,
69}
70
71#[derive(Debug, Clone, Default, Serialize, Deserialize)]
76pub struct MaxCapabilityVector {
77 pub client: ClientCapabilities,
78 pub server: ServerCapabilities,
79 pub negotiated: serde_json::Value,
80 pub experimental: serde_json::Value,
81 pub gaps: Vec<CapabilityGap>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct CapabilityGap {
86 pub capability_path: String,
87 pub reason: String,
88}
89
90#[derive(Debug, Clone, Default, Serialize, Deserialize)]
95pub struct Receipt {
96 pub receipt_id: String,
97 pub hash: String,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub prev_receipt_hash: Option<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct AnalysisBundle {
112 pub snapshot_id: SnapshotId,
113 pub capability_vector: MaxCapabilityVector,
114 pub diagnostics: Vec<MaxDiagnostic>,
115 pub actions: Vec<MaxCodeAction>,
116 pub conformance_vector: ConformanceVector,
117 pub receipts: Vec<Receipt>,
118}
119
120impl Default for AnalysisBundle {
121 fn default() -> Self {
122 Self {
123 snapshot_id: SnapshotId(String::new()),
124 capability_vector: MaxCapabilityVector {
125 client: lsp_types_max::ClientCapabilities::default(),
126 server: lsp_types_max::ServerCapabilities::default(),
127 negotiated: serde_json::Value::Null,
128 experimental: serde_json::Value::Null,
129 gaps: Vec::new(),
130 },
131 diagnostics: Vec::new(),
132 actions: Vec::new(),
133 conformance_vector: ConformanceVector::default(),
134 receipts: Vec::new(),
135 }
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct LspStateModel {
145 pub instance_id: InstanceId,
146 pub phase: String, pub diagnostics: Vec<MaxDiagnostic>,
148 pub receipts: Vec<Receipt>,
149 pub policy_state: Option<PolicyState>,
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::ReceiptPlan;
156
157 #[test]
158 fn receipt_genesis_has_no_prev_hash() {
159 let r = Receipt {
160 receipt_id: "r-0".to_string(),
161 hash: "abc123".to_string(),
162 prev_receipt_hash: None,
163 };
164 assert!(
165 r.prev_receipt_hash.is_none(),
166 "genesis receipt must have no prev_receipt_hash"
167 );
168 }
169
170 #[test]
171 fn receipt_chain_links_prev_hash() {
172 let genesis = Receipt {
173 receipt_id: "r-0".to_string(),
174 hash: "hash0".to_string(),
175 prev_receipt_hash: None,
176 };
177 let next = Receipt {
178 receipt_id: "r-1".to_string(),
179 hash: "hash1".to_string(),
180 prev_receipt_hash: Some(genesis.hash.clone()),
181 };
182 assert_eq!(
183 next.prev_receipt_hash.as_deref(),
184 Some("hash0"),
185 "second receipt must link to genesis hash"
186 );
187 }
188
189 #[test]
190 fn receipt_plan_is_satisfied_by() {
191 let plan = ReceiptPlan {
192 expected_receipts: vec!["r-0".to_string(), "r-1".to_string()],
193 };
194 let receipts = [
195 Receipt {
196 receipt_id: "r-0".to_string(),
197 hash: "h0".to_string(),
198 prev_receipt_hash: None,
199 },
200 Receipt {
201 receipt_id: "r-1".to_string(),
202 hash: "h1".to_string(),
203 prev_receipt_hash: Some("h0".to_string()),
204 },
205 ];
206 let actual_ids: Vec<&str> = receipts.iter().map(|r| r.receipt_id.as_str()).collect();
207 for expected in &plan.expected_receipts {
208 assert!(
209 actual_ids.contains(&expected.as_str()),
210 "receipt {} missing",
211 expected
212 );
213 }
214 }
215
216 #[test]
217 fn analysis_bundle_is_empty() {
218 let bundle = AnalysisBundle::default();
219 assert!(bundle.diagnostics.is_empty());
220 assert!(bundle.actions.is_empty());
221 assert!(bundle.receipts.is_empty());
222 assert!(bundle.conformance_vector.admitted.is_empty());
223 }
224
225 #[test]
226 fn instance_id_from_str_and_display() {
227 let id = InstanceId::from("test-instance");
228 assert_eq!(id.to_string(), "test-instance");
229 assert_eq!(id.0, "test-instance");
230 }
231
232 #[test]
233 fn gate_id_from_str_and_display() {
234 let gate: GateId = GateId::from("gate-42");
235 assert_eq!(gate.to_string(), "gate-42");
236 }
237
238 #[test]
239 fn receipt_serde_roundtrip_omits_none_prev_hash() {
240 let r = Receipt {
241 receipt_id: "r-0".to_string(),
242 hash: "deadbeef".to_string(),
243 prev_receipt_hash: None,
244 };
245 let json = serde_json::to_string(&r).expect("serialize");
246 assert!(
247 !json.contains("prev_receipt_hash"),
248 "None must be omitted: {}",
249 json
250 );
251 let r2: Receipt = serde_json::from_str(&json).expect("deserialize");
252 assert!(r2.prev_receipt_hash.is_none());
253 }
254
255 #[test]
256 fn receipt_serde_roundtrip_includes_prev_hash_when_set() {
257 let r = Receipt {
258 receipt_id: "r-1".to_string(),
259 hash: "cafebabe".to_string(),
260 prev_receipt_hash: Some("deadbeef".to_string()),
261 };
262 let json = serde_json::to_string(&r).expect("serialize");
263 let r2: Receipt = serde_json::from_str(&json).expect("deserialize");
264 assert_eq!(r2.prev_receipt_hash.as_deref(), Some("deadbeef"));
265 }
266}