use std::path::Path; /// Sentinel comment injected as the first line of scaffold templates. /// Only untouched templates contain this marker — real project content /// will never include it, so it avoids false positives when the project /// itself is an "Agentic AI Code Assistant". const TEMPLATE_SENTINEL: &str = ""; /// Marker found in the default `script/test` scaffold output. const TEMPLATE_MARKER_SCRIPT: &str = "No tests configured"; /// Summary of what parts of a project still need onboarding. #[derive(Debug, Clone, PartialEq, Eq)] pub struct OnboardingStatus { /// True when the project context spec needs to be populated. pub needs_context: bool, /// True when the tech stack spec needs to be populated. pub needs_stack: bool, /// True when `script/test` still contains the scaffold placeholder. pub needs_test_script: bool, /// True when `.storkit/project.toml` is missing or has no /// `[[component]]` entries. pub needs_project_toml: bool, } impl OnboardingStatus { /// Returns `true` when any onboarding step is still needed. pub fn needs_onboarding(&self) -> bool { self.needs_context || self.needs_stack } } /// Inspect the project at `project_root` and determine which onboarding /// steps are still required. pub fn check_onboarding_status(project_root: &Path) -> OnboardingStatus { let story_kit = project_root.join(".storkit"); OnboardingStatus { needs_context: is_template_or_missing( &story_kit.join("specs").join("00_CONTEXT.md"), TEMPLATE_SENTINEL, ), needs_stack: is_template_or_missing( &story_kit.join("specs").join("tech").join("STACK.md"), TEMPLATE_SENTINEL, ), needs_test_script: is_template_or_missing( &project_root.join("script").join("test"), TEMPLATE_MARKER_SCRIPT, ), needs_project_toml: needs_project_toml(&story_kit), } } /// Returns `true` when the file is missing, empty, or contains the /// given scaffold marker string. fn is_template_or_missing(path: &Path, marker: &str) -> bool { match std::fs::read_to_string(path) { Ok(content) => content.trim().is_empty() || content.contains(marker), Err(_) => true, } } /// Returns `true` when `project.toml` is missing or has no /// `[[component]]` entries. fn needs_project_toml(story_kit: &Path) -> bool { let toml_path = story_kit.join("project.toml"); match std::fs::read_to_string(toml_path) { Ok(content) => !content.contains("[[component]]"), Err(_) => true, } } #[cfg(test)] mod tests { use super::*; use std::fs; use tempfile::TempDir; fn setup_project(dir: &TempDir) -> std::path::PathBuf { let root = dir.path().to_path_buf(); let sk = root.join(".storkit"); fs::create_dir_all(sk.join("specs").join("tech")).unwrap(); fs::create_dir_all(root.join("script")).unwrap(); root } // ── needs_onboarding ────────────────────────────────────────── #[test] fn needs_onboarding_true_when_no_files_exist() { let dir = TempDir::new().unwrap(); let root = dir.path().to_path_buf(); let status = check_onboarding_status(&root); assert!(status.needs_onboarding()); assert!(status.needs_context); assert!(status.needs_stack); assert!(status.needs_test_script); assert!(status.needs_project_toml); } #[test] fn needs_onboarding_true_when_specs_contain_scaffold_sentinel() { let dir = TempDir::new().unwrap(); let root = setup_project(&dir); // Write content that includes the scaffold sentinel fs::write( root.join(".storkit/specs/00_CONTEXT.md"), "\n# Project Context\nPlaceholder...", ) .unwrap(); fs::write( root.join(".storkit/specs/tech/STACK.md"), "\n# Tech Stack\nPlaceholder...", ) .unwrap(); let status = check_onboarding_status(&root); assert!(status.needs_context); assert!(status.needs_stack); assert!(status.needs_onboarding()); } #[test] fn needs_onboarding_false_when_content_mentions_agentic_but_no_sentinel() { let dir = TempDir::new().unwrap(); let root = setup_project(&dir); // Real project content that happens to mention "Agentic AI Code Assistant" // but does NOT contain the scaffold sentinel — should NOT trigger onboarding. fs::write( root.join(".storkit/specs/00_CONTEXT.md"), "# Project Context\nTo build a standalone Agentic AI Code Assistant application.", ) .unwrap(); fs::write( root.join(".storkit/specs/tech/STACK.md"), "# Tech Stack\nThis is an Agentic Code Assistant binary.", ) .unwrap(); let status = check_onboarding_status(&root); assert!(!status.needs_context); assert!(!status.needs_stack); assert!(!status.needs_onboarding()); } #[test] fn needs_onboarding_false_when_specs_have_custom_content() { let dir = TempDir::new().unwrap(); let root = setup_project(&dir); fs::write( root.join(".storkit/specs/00_CONTEXT.md"), "# My Project\n\nThis is an e-commerce platform for selling widgets.", ) .unwrap(); fs::write( root.join(".storkit/specs/tech/STACK.md"), "# Tech Stack\n\n## Backend: Python + FastAPI\n## Frontend: React + TypeScript", ) .unwrap(); let status = check_onboarding_status(&root); assert!(!status.needs_context); assert!(!status.needs_stack); assert!(!status.needs_onboarding()); } #[test] fn needs_onboarding_true_when_specs_are_empty() { let dir = TempDir::new().unwrap(); let root = setup_project(&dir); fs::write(root.join(".storkit/specs/00_CONTEXT.md"), " \n").unwrap(); fs::write(root.join(".storkit/specs/tech/STACK.md"), "").unwrap(); let status = check_onboarding_status(&root); assert!(status.needs_context); assert!(status.needs_stack); } // ── needs_test_script ───────────────────────────────────────── #[test] fn needs_test_script_true_when_placeholder() { let dir = TempDir::new().unwrap(); let root = setup_project(&dir); fs::write( root.join("script/test"), "#!/usr/bin/env bash\nset -euo pipefail\necho \"No tests configured\"\n", ) .unwrap(); let status = check_onboarding_status(&root); assert!(status.needs_test_script); } #[test] fn needs_test_script_false_when_customised() { let dir = TempDir::new().unwrap(); let root = setup_project(&dir); fs::write( root.join("script/test"), "#!/usr/bin/env bash\nset -euo pipefail\ncargo test\n", ) .unwrap(); let status = check_onboarding_status(&root); assert!(!status.needs_test_script); } // ── needs_project_toml ──────────────────────────────────────── #[test] fn needs_project_toml_true_when_missing() { let dir = TempDir::new().unwrap(); let root = setup_project(&dir); let status = check_onboarding_status(&root); assert!(status.needs_project_toml); } #[test] fn needs_project_toml_true_when_no_components() { let dir = TempDir::new().unwrap(); let root = setup_project(&dir); fs::write(root.join(".storkit/project.toml"), "# empty config\n").unwrap(); let status = check_onboarding_status(&root); assert!(status.needs_project_toml); } #[test] fn needs_project_toml_false_when_has_components() { let dir = TempDir::new().unwrap(); let root = setup_project(&dir); fs::write( root.join(".storkit/project.toml"), "[[component]]\nname = \"app\"\npath = \".\"\nsetup = [\"cargo check\"]\n", ) .unwrap(); let status = check_onboarding_status(&root); assert!(!status.needs_project_toml); } // ── CLAUDE.md is not an onboarding step ────────────────────── #[test] fn onboarding_status_does_not_check_claude_md() { let dir = TempDir::new().unwrap(); let root = setup_project(&dir); // Write real content for the required onboarding files fs::write( root.join(".storkit/specs/00_CONTEXT.md"), "# My Project\n\nReal project context.", ) .unwrap(); fs::write( root.join(".storkit/specs/tech/STACK.md"), "# My Stack\n\nReal stack content.", ) .unwrap(); // CLAUDE.md is absent — should NOT affect onboarding result assert!(!root.join("CLAUDE.md").exists()); let status = check_onboarding_status(&root); assert!( !status.needs_context, "needs_context should be false with real content" ); assert!( !status.needs_stack, "needs_stack should be false with real content" ); assert!( !status.needs_onboarding(), "needs_onboarding() should be false regardless of CLAUDE.md presence" ); } // ── partial onboarding ──────────────────────────────────────── #[test] fn needs_onboarding_true_when_only_context_is_template() { let dir = TempDir::new().unwrap(); let root = setup_project(&dir); // Context still has sentinel fs::write( root.join(".storkit/specs/00_CONTEXT.md"), "\n# Project Context\nPlaceholder...", ) .unwrap(); // Stack is customised (no sentinel) fs::write( root.join(".storkit/specs/tech/STACK.md"), "# My Stack\nRuby on Rails + PostgreSQL", ) .unwrap(); let status = check_onboarding_status(&root); assert!(status.needs_context); assert!(!status.needs_stack); assert!(status.needs_onboarding()); } }