Skip to main content

lexicon_spec/
manifest.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use crate::common::{ConformanceStyle, NamingConvention, RepoType};
5use crate::version::SchemaVersion;
6
7/// Root manifest for a lexicon-managed repository.
8///
9/// Stored at `.lexicon/manifest.toml`.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Manifest {
12    pub schema_version: SchemaVersion,
13    pub project: ProjectMeta,
14    #[serde(default)]
15    pub preferences: Preferences,
16    #[serde(default)]
17    pub policy: PolicyConfig,
18}
19
20/// Metadata about the managed project.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ProjectMeta {
23    pub name: String,
24    pub description: String,
25    pub repo_type: RepoType,
26    /// Domain of the project, e.g. "key-value store", "parser", "web framework".
27    pub domain: String,
28    pub created_at: DateTime<Utc>,
29    pub updated_at: DateTime<Utc>,
30}
31
32/// User preferences that inform artifact generation.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Preferences {
35    pub naming_convention: NamingConvention,
36    pub conformance_style: ConformanceStyle,
37}
38
39impl Default for Preferences {
40    fn default() -> Self {
41        Self {
42            naming_convention: NamingConvention::KebabCase,
43            conformance_style: ConformanceStyle::TraitBased,
44        }
45    }
46}
47
48/// Policy configuration for AI safety and change control.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct PolicyConfig {
51    /// Glob patterns for files AI may edit freely.
52    #[serde(default)]
53    pub ai_may_edit: Vec<String>,
54    /// Glob patterns for files AI changes require manual review.
55    #[serde(default)]
56    pub ai_requires_review: Vec<String>,
57    /// Glob patterns for files AI must never edit.
58    #[serde(default)]
59    pub ai_protected: Vec<String>,
60    /// Whether weakening a gate definition requires explicit approval.
61    #[serde(default = "default_true")]
62    pub gate_weakening_requires_approval: bool,
63    /// Whether deleting tests requires explicit approval.
64    #[serde(default = "default_true")]
65    pub test_deletion_requires_approval: bool,
66}
67
68fn default_true() -> bool {
69    true
70}
71
72impl Default for PolicyConfig {
73    fn default() -> Self {
74        Self {
75            ai_may_edit: vec!["src/**/*.rs".to_string(), "tests/**/*.rs".to_string()],
76            ai_requires_review: vec![
77                "specs/**/*.toml".to_string(),
78                "CLAUDE.md".to_string(),
79            ],
80            ai_protected: vec![
81                ".lexicon/manifest.toml".to_string(),
82                "specs/gates.toml".to_string(),
83            ],
84            gate_weakening_requires_approval: true,
85            test_deletion_requires_approval: true,
86        }
87    }
88}
89
90impl Manifest {
91    /// Create a new manifest with sensible defaults.
92    pub fn new(name: String, description: String, repo_type: RepoType, domain: String) -> Self {
93        let now = Utc::now();
94        Self {
95            schema_version: SchemaVersion::CURRENT,
96            project: ProjectMeta {
97                name,
98                description,
99                repo_type,
100                domain,
101                created_at: now,
102                updated_at: now,
103            },
104            preferences: Preferences::default(),
105            policy: PolicyConfig::default(),
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_manifest_toml_roundtrip() {
116        let manifest = Manifest::new(
117            "my-lib".to_string(),
118            "A test library".to_string(),
119            RepoType::Library,
120            "key-value store".to_string(),
121        );
122        let toml_str = toml::to_string_pretty(&manifest).unwrap();
123        let parsed: Manifest = toml::from_str(&toml_str).unwrap();
124        assert_eq!(parsed.project.name, "my-lib");
125        assert_eq!(parsed.project.domain, "key-value store");
126        assert!(parsed.policy.gate_weakening_requires_approval);
127    }
128
129    #[test]
130    fn test_default_policy() {
131        let policy = PolicyConfig::default();
132        assert!(policy.gate_weakening_requires_approval);
133        assert!(policy.test_deletion_requires_approval);
134        assert!(!policy.ai_may_edit.is_empty());
135        assert!(!policy.ai_protected.is_empty());
136    }
137}