Skip to main content

lexicon_spec/
contract.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use crate::common::{ContractStatus, Severity, Stability};
5use crate::version::SchemaVersion;
6
7/// A contract defines the stable behavioral specification for a component.
8///
9/// Contracts are the primary artifact in lexicon. They declare what a
10/// component must do, what it must not do, and what is explicitly out of scope.
11///
12/// Stored at `specs/contracts/<id>.toml`.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Contract {
15    pub schema_version: SchemaVersion,
16    /// Unique slug identifier, e.g. "key-value-store".
17    pub id: String,
18    pub title: String,
19    /// A longer description of the contract's purpose and context.
20    #[serde(default)]
21    pub description: String,
22    pub status: ContractStatus,
23    pub stability: Stability,
24    /// High-level scope description.
25    pub scope: String,
26    /// What the component provides.
27    #[serde(default)]
28    pub capabilities: Vec<String>,
29    /// Invariants that must always hold.
30    #[serde(default)]
31    pub invariants: Vec<Invariant>,
32    /// Behavior that is required.
33    #[serde(default)]
34    pub required_semantics: Vec<Semantic>,
35    /// Behavior that is explicitly forbidden.
36    #[serde(default)]
37    pub forbidden_semantics: Vec<Semantic>,
38    /// Edge cases with expected behavior.
39    #[serde(default)]
40    pub edge_cases: Vec<EdgeCase>,
41    /// Usage examples.
42    #[serde(default)]
43    pub examples: Vec<Example>,
44    /// What this component explicitly does not do.
45    #[serde(default)]
46    pub non_goals: Vec<String>,
47    /// Implementation-level notes (not part of the contract).
48    #[serde(default)]
49    pub implementation_notes: Vec<String>,
50    /// What tests are expected for this contract.
51    #[serde(default)]
52    pub test_expectations: Vec<String>,
53    /// Expected public API items (traits, methods, types) this contract covers.
54    #[serde(default)]
55    pub expected_api: Vec<String>,
56    /// Version history of contract changes.
57    #[serde(default)]
58    pub history: Vec<HistoryEntry>,
59    pub created_at: DateTime<Utc>,
60    pub updated_at: DateTime<Utc>,
61}
62
63/// An invariant that must always hold.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct Invariant {
66    /// Unique identifier within the contract.
67    pub id: String,
68    pub description: String,
69    #[serde(default)]
70    pub severity: Severity,
71    /// Tags for linking to conformance tests.
72    #[serde(default)]
73    pub test_tags: Vec<String>,
74}
75
76/// A semantic requirement or prohibition.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct Semantic {
79    /// Unique identifier within the contract.
80    pub id: String,
81    pub description: String,
82    /// Tags for linking to conformance tests.
83    #[serde(default)]
84    pub test_tags: Vec<String>,
85}
86
87/// An edge case with expected behavior.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct EdgeCase {
90    pub id: String,
91    pub scenario: String,
92    pub expected_behavior: String,
93}
94
95/// A usage example.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct Example {
98    pub title: String,
99    pub description: String,
100    #[serde(default)]
101    pub code: Option<String>,
102}
103
104/// A record of a contract change.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct HistoryEntry {
107    pub version: String,
108    pub date: DateTime<Utc>,
109    pub description: String,
110    pub author: String,
111}
112
113impl Contract {
114    /// Create a new draft contract with the given id and title.
115    pub fn new_draft(id: String, title: String, scope: String) -> Self {
116        let now = Utc::now();
117        Self {
118            schema_version: SchemaVersion::CURRENT,
119            id,
120            title,
121            description: String::new(),
122            status: ContractStatus::Draft,
123            stability: Stability::Experimental,
124            scope,
125            capabilities: Vec::new(),
126            invariants: Vec::new(),
127            required_semantics: Vec::new(),
128            forbidden_semantics: Vec::new(),
129            edge_cases: Vec::new(),
130            examples: Vec::new(),
131            non_goals: Vec::new(),
132            implementation_notes: Vec::new(),
133            test_expectations: Vec::new(),
134            expected_api: Vec::new(),
135            history: Vec::new(),
136            created_at: now,
137            updated_at: now,
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_contract_toml_roundtrip() {
148        let mut contract = Contract::new_draft(
149            "key-value-store".to_string(),
150            "Key-Value Store Contract".to_string(),
151            "Defines the behavior of a basic key-value store".to_string(),
152        );
153        contract.capabilities.push("get/set/delete operations".to_string());
154        contract.invariants.push(Invariant {
155            id: "inv-001".to_string(),
156            description: "A key set with a value must return that value on get".to_string(),
157            severity: Severity::Required,
158            test_tags: vec!["conformance".to_string()],
159        });
160        contract.required_semantics.push(Semantic {
161            id: "req-001".to_string(),
162            description: "get(key) returns None for missing keys".to_string(),
163            test_tags: vec!["conformance".to_string(), "basic".to_string()],
164        });
165        contract.forbidden_semantics.push(Semantic {
166            id: "forbid-001".to_string(),
167            description: "Must not panic on missing key lookup".to_string(),
168            test_tags: vec!["safety".to_string()],
169        });
170
171        let toml_str = toml::to_string_pretty(&contract).unwrap();
172        let parsed: Contract = toml::from_str(&toml_str).unwrap();
173        assert_eq!(parsed.id, "key-value-store");
174        assert_eq!(parsed.invariants.len(), 1);
175        assert_eq!(parsed.required_semantics.len(), 1);
176        assert_eq!(parsed.forbidden_semantics.len(), 1);
177    }
178
179    #[test]
180    fn test_contract_defaults() {
181        let c = Contract::new_draft(
182            "test".to_string(),
183            "Test".to_string(),
184            "scope".to_string(),
185        );
186        assert_eq!(c.status, ContractStatus::Draft);
187        assert_eq!(c.stability, Stability::Experimental);
188        assert!(c.invariants.is_empty());
189    }
190}