Skip to main content

xchecker_templates/
lib.rs

1//! Spec templates for xchecker
2//!
3//! This module provides built-in templates for bootstrapping specs quickly.
4//! Templates include predefined problem statements, configuration, and example
5//! partial spec flows for common use cases.
6//!
7//! Requirements:
8//! - 4.7.1: `xchecker template list` lists built-in templates
9//! - 4.7.2: `xchecker template init <template> <spec-id>` seeds spec from template
10//! - 4.7.3: Each template has a README describing intended use
11
12use anyhow::{Context, Result};
13use camino::Utf8Path;
14use xchecker_utils::atomic_write;
15use xchecker_utils::paths;
16
17/// Built-in template identifiers
18pub const TEMPLATE_FULLSTACK_NEXTJS: &str = "fullstack-nextjs";
19pub const TEMPLATE_RUST_MICROSERVICE: &str = "rust-microservice";
20pub const TEMPLATE_PYTHON_FASTAPI: &str = "python-fastapi";
21pub const TEMPLATE_DOCS_REFACTOR: &str = "docs-refactor";
22
23/// All available built-in templates
24pub const BUILT_IN_TEMPLATES: &[&str] = &[
25    TEMPLATE_FULLSTACK_NEXTJS,
26    TEMPLATE_RUST_MICROSERVICE,
27    TEMPLATE_PYTHON_FASTAPI,
28    TEMPLATE_DOCS_REFACTOR,
29];
30
31/// Template metadata
32#[derive(Debug, Clone)]
33pub struct TemplateInfo {
34    /// Template identifier
35    pub id: &'static str,
36    /// Human-readable name
37    pub name: &'static str,
38    /// Short description
39    pub description: &'static str,
40    /// Intended use case
41    pub use_case: &'static str,
42    /// Prerequisites
43    pub prerequisites: &'static [&'static str],
44}
45
46/// Get metadata for all built-in templates
47#[must_use]
48pub fn list_templates() -> Vec<TemplateInfo> {
49    vec![
50        TemplateInfo {
51            id: TEMPLATE_FULLSTACK_NEXTJS,
52            name: "Full-Stack Next.js",
53            description: "Template for full-stack web applications using Next.js",
54            use_case: "Building modern web applications with React, Next.js, and a backend API",
55            prerequisites: &["Node.js 18+", "npm or yarn", "Next.js knowledge"],
56        },
57        TemplateInfo {
58            id: TEMPLATE_RUST_MICROSERVICE,
59            name: "Rust Microservice",
60            description: "Template for Rust-based microservices and CLI tools",
61            use_case: "Building performant backend services, CLI tools, or system utilities in Rust",
62            prerequisites: &["Rust 1.70+", "Cargo", "Basic Rust knowledge"],
63        },
64        TemplateInfo {
65            id: TEMPLATE_PYTHON_FASTAPI,
66            name: "Python FastAPI",
67            description: "Template for Python REST APIs using FastAPI",
68            use_case: "Building REST APIs, data processing services, or ML model endpoints",
69            prerequisites: &["Python 3.10+", "pip or poetry", "FastAPI knowledge"],
70        },
71        TemplateInfo {
72            id: TEMPLATE_DOCS_REFACTOR,
73            name: "Documentation Refactor",
74            description: "Template for documentation improvements and refactoring",
75            use_case: "Restructuring, improving, or migrating documentation",
76            prerequisites: &["Markdown knowledge", "Understanding of target docs"],
77        },
78    ]
79}
80
81/// Get template info by ID
82#[must_use]
83pub fn get_template(id: &str) -> Option<TemplateInfo> {
84    list_templates().into_iter().find(|t| t.id == id)
85}
86
87/// Check if a template ID is valid
88#[must_use]
89pub fn is_valid_template(id: &str) -> bool {
90    BUILT_IN_TEMPLATES.contains(&id)
91}
92
93/// Initialize a spec from a template
94///
95/// Creates the spec directory structure and seeds it with template content:
96/// - Problem statement in context/
97/// - Minimal .xchecker/config.toml (if not exists)
98/// - Example partial spec flow
99/// - README for the template
100///
101/// # Arguments
102/// * `template_id` - The template identifier
103/// * `spec_id` - The spec ID to create
104///
105/// # Returns
106/// * `Ok(())` on success
107/// * `Err(_)` if template is invalid or spec creation fails
108pub fn init_from_template(template_id: &str, spec_id: &str) -> Result<()> {
109    // Validate template
110    let template = get_template(template_id).ok_or_else(|| {
111        anyhow::anyhow!(
112            "Unknown template '{}'. Run 'xchecker template list' to see available templates.",
113            template_id
114        )
115    })?;
116
117    // Create spec directory structure
118    let spec_dir = paths::spec_root(spec_id);
119    let artifacts_dir = spec_dir.join("artifacts");
120    let context_dir = spec_dir.join("context");
121    let receipts_dir = spec_dir.join("receipts");
122
123    // Check if spec already exists
124    if spec_dir.exists() {
125        anyhow::bail!("Spec '{}' already exists at: {}", spec_id, spec_dir);
126    }
127
128    // Create directories
129    paths::ensure_dir_all(&artifacts_dir)
130        .with_context(|| format!("Failed to create artifacts directory: {}", artifacts_dir))?;
131    paths::ensure_dir_all(&context_dir)
132        .with_context(|| format!("Failed to create context directory: {}", context_dir))?;
133    paths::ensure_dir_all(&receipts_dir)
134        .with_context(|| format!("Failed to create receipts directory: {}", receipts_dir))?;
135
136    // Generate template content
137    let problem_statement = generate_problem_statement(template_id, spec_id);
138    let readme_content = generate_readme(&template);
139
140    // Write problem statement to context
141    let problem_path = context_dir.join("problem-statement.md");
142    atomic_write::write_file_atomic(&problem_path, &problem_statement)
143        .with_context(|| format!("Failed to write problem statement: {}", problem_path))?;
144
145    // Write README to spec directory
146    let readme_path = spec_dir.join("README.md");
147    atomic_write::write_file_atomic(&readme_path, &readme_content)
148        .with_context(|| format!("Failed to write README: {}", readme_path))?;
149
150    // Create minimal config if .xchecker/config.toml doesn't exist
151    ensure_minimal_config()?;
152
153    Ok(())
154}
155
156/// Generate problem statement content for a template
157fn generate_problem_statement(template_id: &str, spec_id: &str) -> String {
158    match template_id {
159        TEMPLATE_FULLSTACK_NEXTJS => format!(
160            r#"# Problem Statement: {spec_id}
161
162## Overview
163
164Build a full-stack web application using Next.js with the following capabilities:
165
166- Modern React-based frontend with server-side rendering
167- API routes for backend functionality
168- Database integration (PostgreSQL/Prisma recommended)
169- Authentication and authorization
170- Responsive design with Tailwind CSS
171
172## Goals
173
1741. Create a production-ready Next.js application
1752. Implement core features with proper error handling
1763. Set up testing infrastructure (Jest, React Testing Library)
1774. Configure CI/CD pipeline
1785. Document API endpoints and usage
179
180## Constraints
181
182- Use TypeScript for type safety
183- Follow Next.js App Router conventions
184- Implement proper security practices
185- Ensure accessibility compliance (WCAG 2.1 AA)
186
187## Success Criteria
188
189- All core features implemented and tested
190- Performance targets met (Core Web Vitals)
191- Documentation complete
192- CI/CD pipeline operational
193"#,
194            spec_id = spec_id
195        ),
196        TEMPLATE_RUST_MICROSERVICE => format!(
197            r#"# Problem Statement: {spec_id}
198
199## Overview
200
201Build a Rust microservice/CLI tool with the following capabilities:
202
203- High-performance request handling
204- Structured logging and observability
205- Configuration management
206- Graceful shutdown handling
207- Comprehensive error handling
208
209## Goals
210
2111. Create a production-ready Rust service
2122. Implement core business logic
2133. Set up testing infrastructure (unit, integration, property-based)
2144. Configure CI/CD pipeline
2155. Document API/CLI usage
216
217## Constraints
218
219- Use stable Rust (1.70+)
220- Follow Rust idioms and best practices
221- Minimize dependencies where practical
222- Ensure cross-platform compatibility (Linux, macOS, Windows)
223
224## Success Criteria
225
226- All core features implemented and tested
227- Performance targets met
228- Documentation complete
229- CI/CD pipeline operational
230"#,
231            spec_id = spec_id
232        ),
233        TEMPLATE_PYTHON_FASTAPI => format!(
234            r#"# Problem Statement: {spec_id}
235
236## Overview
237
238Build a Python REST API using FastAPI with the following capabilities:
239
240- RESTful API endpoints with automatic OpenAPI documentation
241- Database integration (SQLAlchemy/PostgreSQL)
242- Authentication (JWT/OAuth2)
243- Input validation with Pydantic
244- Async request handling
245
246## Goals
247
2481. Create a production-ready FastAPI application
2492. Implement core API endpoints
2503. Set up testing infrastructure (pytest, httpx)
2514. Configure CI/CD pipeline
2525. Document API endpoints
253
254## Constraints
255
256- Use Python 3.10+
257- Follow PEP 8 style guidelines
258- Use type hints throughout
259- Implement proper error handling
260
261## Success Criteria
262
263- All API endpoints implemented and tested
264- OpenAPI documentation complete
265- Performance targets met
266- CI/CD pipeline operational
267"#,
268            spec_id = spec_id
269        ),
270        TEMPLATE_DOCS_REFACTOR => format!(
271            r#"# Problem Statement: {spec_id}
272
273## Overview
274
275Refactor and improve documentation with the following goals:
276
277- Restructure documentation for better navigation
278- Improve clarity and consistency
279- Add missing documentation
280- Update outdated content
281- Improve code examples
282
283## Goals
284
2851. Audit existing documentation
2862. Create documentation structure plan
2873. Rewrite/improve key sections
2884. Add missing content
2895. Validate all code examples
290
291## Constraints
292
293- Maintain backward compatibility with existing links where possible
294- Follow documentation style guide
295- Ensure all code examples are tested
296- Keep documentation in sync with code
297
298## Success Criteria
299
300- Documentation structure improved
301- All sections reviewed and updated
302- Code examples validated
303- Navigation improved
304- Search functionality working
305"#,
306            spec_id = spec_id
307        ),
308        _ => format!(
309            r#"# Problem Statement: {spec_id}
310
311## Overview
312
313[Describe the problem you're trying to solve]
314
315## Goals
316
3171. [Goal 1]
3182. [Goal 2]
3193. [Goal 3]
320
321## Constraints
322
323- [Constraint 1]
324- [Constraint 2]
325
326## Success Criteria
327
328- [Criterion 1]
329- [Criterion 2]
330"#,
331            spec_id = spec_id
332        ),
333    }
334}
335
336/// Generate README content for a template
337fn generate_readme(template: &TemplateInfo) -> String {
338    let prerequisites = template
339        .prerequisites
340        .iter()
341        .map(|p| format!("- {}", p))
342        .collect::<Vec<_>>()
343        .join("\n");
344
345    format!(
346        r#"# {name}
347
348{description}
349
350## Intended Use
351
352{use_case}
353
354## Prerequisites
355
356{prerequisites}
357
358## Getting Started
359
3601. Review the problem statement in `context/problem-statement.md`
3612. Run the requirements phase:
362   ```bash
363   xchecker resume <spec-id> --phase requirements
364   ```
3653. Review generated requirements in `artifacts/`
3664. Continue with design phase:
367   ```bash
368   xchecker resume <spec-id> --phase design
369   ```
3705. Continue through remaining phases as needed
371
372## Basic Flow
373
374```
375Requirements → Design → Tasks → Review → Fixup → Final
376```
377
378Each phase builds on the previous one:
379- **Requirements**: Generate detailed requirements from the problem statement
380- **Design**: Create architecture and design documents
381- **Tasks**: Break down into implementation tasks
382- **Review**: Review and validate the spec
383- **Fixup**: Apply any suggested changes
384- **Final**: Finalize the spec
385
386## Commands
387
388```bash
389# Check spec status
390xchecker status <spec-id>
391
392# Resume from a specific phase
393xchecker resume <spec-id> --phase <phase>
394
395# Run in dry-run mode (no LLM calls)
396xchecker resume <spec-id> --phase <phase> --dry-run
397```
398
399## More Information
400
401- [xchecker Documentation](https://github.com/your-org/xchecker)
402- [Configuration Guide](../../docs/CONFIGURATION.md)
403"#,
404        name = template.name,
405        description = template.description,
406        use_case = template.use_case,
407        prerequisites = prerequisites,
408    )
409}
410
411/// Ensure minimal .xchecker/config.toml exists
412fn ensure_minimal_config() -> Result<()> {
413    let config_dir = Utf8Path::new(".xchecker");
414    let config_path = config_dir.join("config.toml");
415
416    // Only create if it doesn't exist
417    if config_path.exists() {
418        return Ok(());
419    }
420
421    // Create .xchecker directory if needed
422    if !config_dir.exists() {
423        paths::ensure_dir_all(config_dir)
424            .with_context(|| format!("Failed to create config directory: {}", config_dir))?;
425    }
426
427    let config_content = r#"# xchecker configuration
428# See docs/CONFIGURATION.md for all options
429
430[defaults]
431# model = "haiku"
432# max_turns = 5
433
434[packet]
435# packet_max_bytes = 65536
436# packet_max_lines = 1200
437
438[runner]
439# runner_mode = "auto"
440
441[llm]
442# provider = "claude-cli"
443# execution_strategy = "controlled"
444"#;
445
446    atomic_write::write_file_atomic(&config_path, config_content)
447        .with_context(|| format!("Failed to write config file: {}", config_path))?;
448
449    Ok(())
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn test_list_templates() {
458        let templates = list_templates();
459        assert_eq!(templates.len(), 4);
460
461        let ids: Vec<&str> = templates.iter().map(|t| t.id).collect();
462        assert!(ids.contains(&TEMPLATE_FULLSTACK_NEXTJS));
463        assert!(ids.contains(&TEMPLATE_RUST_MICROSERVICE));
464        assert!(ids.contains(&TEMPLATE_PYTHON_FASTAPI));
465        assert!(ids.contains(&TEMPLATE_DOCS_REFACTOR));
466    }
467
468    #[test]
469    fn test_get_template_valid() {
470        let template = get_template(TEMPLATE_FULLSTACK_NEXTJS);
471        assert!(template.is_some());
472        let t = template.unwrap();
473        assert_eq!(t.id, TEMPLATE_FULLSTACK_NEXTJS);
474        assert!(!t.name.is_empty());
475        assert!(!t.description.is_empty());
476    }
477
478    #[test]
479    fn test_get_template_invalid() {
480        let template = get_template("nonexistent-template");
481        assert!(template.is_none());
482    }
483
484    #[test]
485    fn test_is_valid_template() {
486        assert!(is_valid_template(TEMPLATE_FULLSTACK_NEXTJS));
487        assert!(is_valid_template(TEMPLATE_RUST_MICROSERVICE));
488        assert!(is_valid_template(TEMPLATE_PYTHON_FASTAPI));
489        assert!(is_valid_template(TEMPLATE_DOCS_REFACTOR));
490        assert!(!is_valid_template("invalid-template"));
491    }
492
493    #[test]
494    fn test_generate_problem_statement() {
495        let content = generate_problem_statement(TEMPLATE_FULLSTACK_NEXTJS, "my-app");
496        assert!(content.contains("my-app"));
497        assert!(content.contains("Next.js"));
498        assert!(content.contains("Problem Statement"));
499    }
500
501    #[test]
502    fn test_generate_readme() {
503        let template = get_template(TEMPLATE_RUST_MICROSERVICE).unwrap();
504        let readme = generate_readme(&template);
505        assert!(readme.contains("Rust Microservice"));
506        assert!(readme.contains("Prerequisites"));
507        assert!(readme.contains("Getting Started"));
508    }
509
510    #[test]
511    fn test_init_from_template() {
512        // Use isolated home to avoid conflicts
513        let _temp_dir = paths::with_isolated_home();
514
515        let result = init_from_template(TEMPLATE_RUST_MICROSERVICE, "test-rust-service");
516        assert!(result.is_ok());
517
518        // Verify directories were created
519        let spec_dir = paths::spec_root("test-rust-service");
520        assert!(spec_dir.exists());
521        assert!(spec_dir.join("artifacts").exists());
522        assert!(spec_dir.join("context").exists());
523        assert!(spec_dir.join("receipts").exists());
524
525        // Verify files were created
526        assert!(spec_dir.join("context/problem-statement.md").exists());
527        assert!(spec_dir.join("README.md").exists());
528    }
529
530    #[test]
531    fn test_init_from_template_invalid() {
532        let _temp_dir = paths::with_isolated_home();
533
534        let result = init_from_template("invalid-template", "test-spec");
535        assert!(result.is_err());
536        assert!(result.unwrap_err().to_string().contains("Unknown template"));
537    }
538
539    #[test]
540    fn test_init_from_template_already_exists() {
541        let _temp_dir = paths::with_isolated_home();
542
543        // Create first spec
544        init_from_template(TEMPLATE_PYTHON_FASTAPI, "existing-spec").unwrap();
545
546        // Try to create again
547        let result = init_from_template(TEMPLATE_PYTHON_FASTAPI, "existing-spec");
548        assert!(result.is_err());
549        assert!(result.unwrap_err().to_string().contains("already exists"));
550    }
551}