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)]
101pub struct Receipt {
102 pub receipt_id: String,
103 pub hash: String,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub prev_receipt_hash: Option<String>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct AnalysisBundle {
118 pub snapshot_id: SnapshotId,
119 pub capability_vector: MaxCapabilityVector,
120 pub diagnostics: Vec<MaxDiagnostic>,
121 pub actions: Vec<MaxCodeAction>,
122 pub conformance_vector: ConformanceVector,
123 pub receipts: Vec<Receipt>,
124}
125
126impl Default for AnalysisBundle {
127 fn default() -> Self {
128 Self {
129 snapshot_id: SnapshotId(String::new()),
130 capability_vector: MaxCapabilityVector {
131 client: ClientCapabilities::default(),
132 server: ServerCapabilities::default(),
133 negotiated: serde_json::Value::Null,
134 experimental: serde_json::Value::Null,
135 gaps: Vec::new(),
136 },
137 diagnostics: Vec::new(),
138 actions: Vec::new(),
139 conformance_vector: ConformanceVector::default(),
140 receipts: Vec::new(),
141 }
142 }
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct LspStateModel {
151 pub instance_id: InstanceId,
152 pub phase: String, pub diagnostics: Vec<MaxDiagnostic>,
154 pub receipts: Vec<Receipt>,
155 pub policy_state: Option<PolicyState>,
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::ReceiptPlan;
162
163 #[test]
164 fn receipt_genesis_has_no_prev_hash() {
165 let r = Receipt {
166 receipt_id: "r-0".to_string(),
167 hash: "abc123".to_string(),
168 prev_receipt_hash: None,
169 };
170 assert!(
171 r.prev_receipt_hash.is_none(),
172 "genesis receipt must have no prev_receipt_hash"
173 );
174 }
175
176 #[test]
177 fn receipt_chain_links_prev_hash() {
178 let genesis = Receipt {
179 receipt_id: "r-0".to_string(),
180 hash: "hash0".to_string(),
181 prev_receipt_hash: None,
182 };
183 let next = Receipt {
184 receipt_id: "r-1".to_string(),
185 hash: "hash1".to_string(),
186 prev_receipt_hash: Some(genesis.hash.clone()),
187 };
188 assert_eq!(
189 next.prev_receipt_hash.as_deref(),
190 Some("hash0"),
191 "second receipt must link to genesis hash"
192 );
193 }
194
195 #[test]
196 fn receipt_plan_is_satisfied_by() {
197 let plan = ReceiptPlan {
198 expected_receipts: vec!["r-0".to_string(), "r-1".to_string()],
199 };
200 let receipts = [
201 Receipt {
202 receipt_id: "r-0".to_string(),
203 hash: "h0".to_string(),
204 prev_receipt_hash: None,
205 },
206 Receipt {
207 receipt_id: "r-1".to_string(),
208 hash: "h1".to_string(),
209 prev_receipt_hash: Some("h0".to_string()),
210 },
211 ];
212 let actual_ids: Vec<&str> = receipts.iter().map(|r| r.receipt_id.as_str()).collect();
213 for expected in &plan.expected_receipts {
214 assert!(
215 actual_ids.contains(&expected.as_str()),
216 "receipt {} missing",
217 expected
218 );
219 }
220 }
221
222 #[test]
223 fn analysis_bundle_is_empty() {
224 let bundle = AnalysisBundle::default();
225 assert!(bundle.diagnostics.is_empty());
226 assert!(bundle.actions.is_empty());
227 assert!(bundle.receipts.is_empty());
228 assert!(bundle.conformance_vector.admitted.is_empty());
229 }
230
231 #[test]
232 fn instance_id_from_str_and_display() {
233 let id = InstanceId::from("test-instance");
234 assert_eq!(id.to_string(), "test-instance");
235 assert_eq!(id.0, "test-instance");
236 }
237
238 #[test]
239 fn gate_id_from_str_and_display() {
240 let gate: GateId = GateId::from("gate-42");
241 assert_eq!(gate.to_string(), "gate-42");
242 }
243
244 #[test]
245 fn receipt_serde_roundtrip_omits_none_prev_hash() {
246 let r = Receipt {
247 receipt_id: "r-0".to_string(),
248 hash: "deadbeef".to_string(),
249 prev_receipt_hash: None,
250 };
251 let json = serde_json::to_string(&r).expect("serialize");
252 assert!(
253 !json.contains("prev_receipt_hash"),
254 "None must be omitted: {}",
255 json
256 );
257 let r2: Receipt = serde_json::from_str(&json).expect("deserialize");
258 assert!(r2.prev_receipt_hash.is_none());
259 }
260
261 #[test]
262 fn receipt_serde_roundtrip_includes_prev_hash_when_set() {
263 let r = Receipt {
264 receipt_id: "r-1".to_string(),
265 hash: "cafebabe".to_string(),
266 prev_receipt_hash: Some("deadbeef".to_string()),
267 };
268 let json = serde_json::to_string(&r).expect("serialize");
269 let r2: Receipt = serde_json::from_str(&json).expect("deserialize");
270 assert_eq!(r2.prev_receipt_hash.as_deref(), Some("deadbeef"));
271 }
272
273 #[test]
274 fn receipt_roundtrip_serde() {
275 let r = Receipt {
278 receipt_id: "r-42".to_string(),
279 hash: "sha256:abcdef1234567890".to_string(),
280 prev_receipt_hash: Some("sha256:0000000000000000".to_string()),
281 };
282 let json = serde_json::to_string(&r).expect("serialize");
283 let r2: Receipt = serde_json::from_str(&json).expect("deserialize");
284 assert_eq!(r2.receipt_id, r.receipt_id);
285 assert_eq!(r2.hash, r.hash);
286 assert_eq!(r2.prev_receipt_hash, r.prev_receipt_hash);
287 }
288
289 #[test]
290 fn receipt_with_no_prev_hash_is_genesis() {
291 let r = Receipt {
294 receipt_id: "r-genesis".to_string(),
295 hash: "sha256:genesis".to_string(),
296 prev_receipt_hash: None,
297 };
298 assert!(r.prev_receipt_hash.is_none());
299 let json = serde_json::to_string(&r).expect("serialize");
300 let r2: Receipt = serde_json::from_str(&json).expect("deserialize");
301 assert!(
302 r2.prev_receipt_hash.is_none(),
303 "genesis prev_receipt_hash must remain None after serde roundtrip"
304 );
305 }
306}