1use std::path::Path;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum CommentStyle {
6 Rust,
8 TypeScript,
10}
11
12impl CommentStyle {
13 pub fn detect(root: &Path) -> Option<Self> {
15 if root.join("Cargo.toml").exists() {
16 Some(Self::Rust)
17 } else if root.join("package.json").exists() {
18 Some(Self::TypeScript)
19 } else {
20 None
21 }
22 }
23
24 pub fn from_lang(lang: &str) -> Option<Self> {
25 match lang.to_lowercase().as_str() {
26 "rust" | "rs" => Some(Self::Rust),
27 "typescript" | "ts" | "javascript" | "js" => Some(Self::TypeScript),
28 _ => None,
29 }
30 }
31}
32
33pub fn generate_template(style: CommentStyle) -> String {
38 let sections = vec![
39 Section::heading("@c4 container"),
40 Section::heading("[Project Name]").h1(),
41 Section::blank(),
42 Section::line("[TODO: One-line description — what this system does and why it exists.]"),
43 Section::blank(),
44 Section::heading("Business Context").h2(),
46 Section::blank(),
47 Section::line("- TODO: What problem does this solve? (business need, not technical)"),
48 Section::line("- TODO: Who sponsors this project and what is their primary goal?"),
49 Section::line("- TODO: What existing process does this replace or augment?"),
50 Section::line("- TODO: What is the value proposition? (saves time, reduces risk, unlocks revenue)"),
51 Section::blank(),
52 Section::heading("Domain Model").h2(),
53 Section::blank(),
54 Section::line("- TODO: Key domain concepts and relationships (ubiquitous language)"),
55 Section::line("- TODO: Bounded contexts — where does this system's vocabulary differ from adjacent systems?"),
56 Section::line("- TODO: Domain events — what real-world events does this system respond to?"),
57 Section::blank(),
58 Section::heading("Users & Stakeholders").h2(),
59 Section::blank(),
60 Section::line("- TODO: Primary users — who uses this daily and what are their goals?"),
61 Section::line("- TODO: Secondary stakeholders — who cares about outcomes but doesn't interact directly?"),
62 Section::line("- TODO: What does a typical user workflow look like?"),
63 Section::blank(),
64 Section::heading("Success Criteria").h2(),
65 Section::blank(),
66 Section::line("- TODO: How do you know this project is succeeding? (measurable outcomes)"),
67 Section::line("- TODO: What must be true before this ships? (acceptance criteria)"),
68 Section::line("- TODO: What must this NOT do? (anti-goals, failure modes)"),
69 Section::blank(),
70 Section::heading("Constraints & Trade-offs").h2(),
71 Section::blank(),
72 Section::line("- TODO: Deliberate trade-offs (e.g., maintainability over performance)"),
73 Section::line("- TODO: Non-negotiable constraints (regulatory, timeline, budget, compatibility)"),
74 Section::line("- TODO: What was explicitly ruled out and why?"),
75 Section::blank(),
76 Section::heading("C4 Context").h2(),
78 Section::blank(),
79 Section::code_block(
80 "mermaid",
81 &[
82 "C4Context",
83 " title System Context Diagram",
84 "",
85 " Person(user, \"TODO: User\", \"TODO: Primary user/actor\")",
86 " System(system, \"TODO: System Name\", \"TODO: System purpose\")",
87 " System_Ext(ext1, \"TODO: External System\", \"TODO: External dependency\")",
88 "",
89 " Rel(user, system, \"Uses\")",
90 " Rel(system, ext1, \"TODO: relationship\", \"TODO: protocol\")",
91 "",
92 " UpdateLayoutConfig($c4ShapeInRow=\"3\", $c4BoundaryInRow=\"1\")",
93 ],
94 ),
95 Section::blank(),
96 Section::heading("Data Flow").h2(),
97 Section::blank(),
98 Section::line("1. TODO: Primary command/request flow (e.g., Frontend -> API -> Service -> DB)"),
99 Section::line("2. TODO: Primary data/response flow (e.g., DB -> Service -> Frontend)"),
100 Section::line("3. TODO: Secondary flows (settings, config, async jobs, etc.)"),
101 Section::blank(),
102 Section::heading("Concurrency & Data Patterns").h2(),
103 Section::blank(),
104 Section::line("- TODO: Key concurrency primitives (locks, channels, atomics, async, etc.)"),
105 Section::line("- TODO: Data access patterns (caching, buffering, connection pooling, etc.)"),
106 Section::blank(),
107 Section::heading("Deployment").h2(),
108 Section::blank(),
109 Section::line("- TODO: Where does this run? (local, cloud, hybrid, embedded)"),
110 Section::line("- TODO: Key infrastructure (Docker, K8s, serverless, etc.)"),
111 Section::blank(),
112 Section::heading("External Dependencies").h2(),
113 Section::blank(),
114 Section::line("- TODO: Third-party APIs and services"),
115 Section::line("- TODO: Databases and storage systems"),
116 ];
117
118 render(style, §ions)
119}
120
121enum Section {
124 Line(String),
125 Blank,
126 Heading { text: String, level: u8 },
127 CodeBlock { lang: String, lines: Vec<String> },
128}
129
130impl Section {
131 fn heading(text: &str) -> Self {
132 Self::Heading {
133 text: text.to_string(),
134 level: 0,
135 }
136 }
137
138 fn h1(self) -> Self {
139 match self {
140 Self::Heading { text, .. } => Self::Heading { text, level: 1 },
141 other => other,
142 }
143 }
144
145 fn h2(self) -> Self {
146 match self {
147 Self::Heading { text, .. } => Self::Heading { text, level: 2 },
148 other => other,
149 }
150 }
151
152 fn line(text: &str) -> Self {
153 Self::Line(text.to_string())
154 }
155
156 fn blank() -> Self {
157 Self::Blank
158 }
159
160 fn code_block(lang: &str, lines: &[&str]) -> Self {
161 Self::CodeBlock {
162 lang: lang.to_string(),
163 lines: lines.iter().map(|s| s.to_string()).collect(),
164 }
165 }
166}
167
168fn render(style: CommentStyle, sections: &[Section]) -> String {
169 let mut out = String::new();
170
171 for section in sections {
172 match section {
173 Section::Blank => {
174 out.push_str(&comment_line(style, ""));
175 out.push('\n');
176 }
177 Section::Line(text) => {
178 out.push_str(&comment_line(style, text));
179 out.push('\n');
180 }
181 Section::Heading { text, level } => {
182 let prefix = match level {
183 1 => "# ",
184 2 => "## ",
185 3 => "### ",
186 _ => "",
187 };
188 out.push_str(&comment_line(style, &format!("{}{}", prefix, text)));
189 out.push('\n');
190 }
191 Section::CodeBlock { lang, lines } => {
192 out.push_str(&comment_line(style, &format!("```{}", lang)));
193 out.push('\n');
194 for line in lines {
195 out.push_str(&comment_line(style, line));
196 out.push('\n');
197 }
198 out.push_str(&comment_line(style, "```"));
199 out.push('\n');
200 }
201 }
202 }
203
204 out
205}
206
207fn comment_line(style: CommentStyle, text: &str) -> String {
208 match style {
209 CommentStyle::Rust => {
210 if text.is_empty() {
211 "//!".to_string()
212 } else {
213 format!("//! {}", text)
214 }
215 }
216 CommentStyle::TypeScript => {
217 if text.is_empty() {
218 " *".to_string()
219 } else {
220 format!(" * {}", text)
221 }
222 }
223 }
224}
225
226pub fn wrap_jsdoc(content: &str) -> String {
228 format!("/**\n{} */\n", content)
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn rust_template_has_c4_marker() {
237 let out = generate_template(CommentStyle::Rust);
238 assert!(out.contains("//! @c4 container"));
239 }
240
241 #[test]
242 fn rust_template_has_all_sections() {
243 let out = generate_template(CommentStyle::Rust);
244 assert!(out.contains("# [Project Name]"));
245 assert!(out.contains("## Business Context"));
247 assert!(out.contains("## Domain Model"));
248 assert!(out.contains("## Users & Stakeholders"));
249 assert!(out.contains("## Success Criteria"));
250 assert!(out.contains("## Constraints & Trade-offs"));
251 assert!(out.contains("## C4 Context"));
253 assert!(out.contains("## Data Flow"));
254 assert!(out.contains("## Concurrency & Data Patterns"));
255 assert!(out.contains("## Deployment"));
256 assert!(out.contains("## External Dependencies"));
257 }
258
259 #[test]
260 fn rust_template_has_mermaid_block() {
261 let out = generate_template(CommentStyle::Rust);
262 assert!(out.contains("```mermaid"));
263 assert!(out.contains("C4Context"));
264 assert!(out.contains("```"));
265 }
266
267 #[test]
268 fn rust_template_has_todo_placeholders() {
269 let out = generate_template(CommentStyle::Rust);
270 assert!(out.contains("TODO:"));
271 assert!(out.contains("[TODO: One-line description"));
272 }
273
274 #[test]
275 fn typescript_template_uses_jsdoc_style() {
276 let out = generate_template(CommentStyle::TypeScript);
277 assert!(out.contains(" * @c4 container"));
278 assert!(out.contains(" * ## Data Flow"));
279 }
280
281 #[test]
282 fn detect_rust_from_cargo_toml() {
283 let tmp = tempfile::TempDir::new().unwrap();
284 std::fs::write(tmp.path().join("Cargo.toml"), "").unwrap();
285 assert_eq!(CommentStyle::detect(tmp.path()), Some(CommentStyle::Rust));
286 }
287
288 #[test]
289 fn detect_ts_from_package_json() {
290 let tmp = tempfile::TempDir::new().unwrap();
291 std::fs::write(tmp.path().join("package.json"), "").unwrap();
292 assert_eq!(
293 CommentStyle::detect(tmp.path()),
294 Some(CommentStyle::TypeScript)
295 );
296 }
297
298 #[test]
299 fn from_lang_parsing() {
300 assert_eq!(CommentStyle::from_lang("rust"), Some(CommentStyle::Rust));
301 assert_eq!(CommentStyle::from_lang("rs"), Some(CommentStyle::Rust));
302 assert_eq!(CommentStyle::from_lang("ts"), Some(CommentStyle::TypeScript));
303 assert_eq!(CommentStyle::from_lang("unknown"), None);
304 }
305
306 #[test]
307 fn blank_lines_are_comment_only() {
308 let out = generate_template(CommentStyle::Rust);
309 assert!(out.contains("\n//!\n"));
311 assert!(!out.contains("//! \n"));
312 }
313}