1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use crate::common::{ConformanceStyle, NamingConvention, RepoType};
5use crate::version::SchemaVersion;
6
7#[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#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ProjectMeta {
23 pub name: String,
24 pub description: String,
25 pub repo_type: RepoType,
26 pub domain: String,
28 pub created_at: DateTime<Utc>,
29 pub updated_at: DateTime<Utc>,
30}
31
32#[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#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct PolicyConfig {
51 #[serde(default)]
53 pub ai_may_edit: Vec<String>,
54 #[serde(default)]
56 pub ai_requires_review: Vec<String>,
57 #[serde(default)]
59 pub ai_protected: Vec<String>,
60 #[serde(default = "default_true")]
62 pub gate_weakening_requires_approval: bool,
63 #[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 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}