Skip to main content

lsp_max_protocol/
core.rs

1use crate::{ConformanceVector, MaxCodeAction, MaxDiagnostic, PolicyState, SnapshotId};
2use lsp_types_max::{ClientCapabilities, ServerCapabilities};
3use serde::{Deserialize, Serialize};
4
5// ---------------------------------------------------------------------------
6// InstanceId — newtype for LSP instance identifiers
7// ---------------------------------------------------------------------------
8
9#[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// ---------------------------------------------------------------------------
42// GateId / ReceiptObligation
43// ---------------------------------------------------------------------------
44
45#[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// ---------------------------------------------------------------------------
72// MaxCapabilityVector / CapabilityGap
73// ---------------------------------------------------------------------------
74
75#[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// ---------------------------------------------------------------------------
91// Receipt
92// ---------------------------------------------------------------------------
93
94/// A content-addressed ledger entry: `hash` is the digest of an artifact's exact
95/// bytes, and `prev_receipt_hash` closes the Merkle chain (`None` only for
96/// genesis). See the runnable witness in `examples/receipt_chain_explained.rs`,
97/// which demonstrates content-addressing, tamper detection, the circular-hash
98/// trap, and chain linkage — and panics if any of them regress. For receipt
99/// verification driving the conformance gate, see `examples/admission_pipeline.rs`.
100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
101pub struct Receipt {
102    pub receipt_id: String,
103    pub hash: String,
104    /// Hash of the immediately preceding receipt in the instance ledger.
105    /// `None` for genesis (first) receipts only.  All subsequent receipts
106    /// must set this to close the Merkle chain and make `verify_instance_ledger`
107    /// meaningful for non-LSP_1 instances.
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub prev_receipt_hash: Option<String>,
110}
111
112// ---------------------------------------------------------------------------
113// AnalysisBundle
114// ---------------------------------------------------------------------------
115
116#[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// ---------------------------------------------------------------------------
146// LspStateModel
147// ---------------------------------------------------------------------------
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct LspStateModel {
151    pub instance_id: InstanceId,
152    pub phase: String, // e.g. "Uninitialized", "Initializing", "Initialized", etc.
153    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        // Construct a Receipt with all fields set, serialize to JSON, deserialize
276        // back, and assert field equality.
277        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        // A Receipt constructed with prev_receipt_hash = None is a genesis entry;
292        // the field must remain None after round-trip through serde.
293        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}