2026-02-24 15:34:31 +00:00
|
|
|
use std::path::Path;
|
|
|
|
|
|
2026-02-24 16:13:39 +00:00
|
|
|
/// 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 = "<!-- story-kit:scaffold-template -->";
|
2026-02-24 15:34:31 +00:00
|
|
|
|
|
|
|
|
/// 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 `.story_kit/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(".story_kit");
|
|
|
|
|
|
|
|
|
|
OnboardingStatus {
|
|
|
|
|
needs_context: is_template_or_missing(
|
|
|
|
|
&story_kit.join("specs").join("00_CONTEXT.md"),
|
2026-02-24 16:13:39 +00:00
|
|
|
TEMPLATE_SENTINEL,
|
2026-02-24 15:34:31 +00:00
|
|
|
),
|
|
|
|
|
needs_stack: is_template_or_missing(
|
|
|
|
|
&story_kit.join("specs").join("tech").join("STACK.md"),
|
2026-02-24 16:13:39 +00:00
|
|
|
TEMPLATE_SENTINEL,
|
2026-02-24 15:34:31 +00:00
|
|
|
),
|
|
|
|
|
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(".story_kit");
|
|
|
|
|
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]
|
2026-02-24 16:13:39 +00:00
|
|
|
fn needs_onboarding_true_when_specs_contain_scaffold_sentinel() {
|
2026-02-24 15:34:31 +00:00
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
let root = setup_project(&dir);
|
|
|
|
|
|
2026-02-24 16:13:39 +00:00
|
|
|
// Write content that includes the scaffold sentinel
|
2026-02-24 15:34:31 +00:00
|
|
|
fs::write(
|
|
|
|
|
root.join(".story_kit/specs/00_CONTEXT.md"),
|
2026-02-24 16:13:39 +00:00
|
|
|
"<!-- story-kit:scaffold-template -->\n# Project Context\nPlaceholder...",
|
2026-02-24 15:34:31 +00:00
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
fs::write(
|
|
|
|
|
root.join(".story_kit/specs/tech/STACK.md"),
|
2026-02-24 16:13:39 +00:00
|
|
|
"<!-- story-kit:scaffold-template -->\n# Tech Stack\nPlaceholder...",
|
2026-02-24 15:34:31 +00:00
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let status = check_onboarding_status(&root);
|
|
|
|
|
assert!(status.needs_context);
|
|
|
|
|
assert!(status.needs_stack);
|
|
|
|
|
assert!(status.needs_onboarding());
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 16:13:39 +00:00
|
|
|
#[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(".story_kit/specs/00_CONTEXT.md"),
|
|
|
|
|
"# Project Context\nTo build a standalone Agentic AI Code Assistant application.",
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
fs::write(
|
|
|
|
|
root.join(".story_kit/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());
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 15:34:31 +00:00
|
|
|
#[test]
|
|
|
|
|
fn needs_onboarding_false_when_specs_have_custom_content() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
let root = setup_project(&dir);
|
|
|
|
|
|
|
|
|
|
fs::write(
|
|
|
|
|
root.join(".story_kit/specs/00_CONTEXT.md"),
|
|
|
|
|
"# My Project\n\nThis is an e-commerce platform for selling widgets.",
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
fs::write(
|
|
|
|
|
root.join(".story_kit/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(".story_kit/specs/00_CONTEXT.md"), " \n").unwrap();
|
|
|
|
|
fs::write(root.join(".story_kit/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(".story_kit/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(".story_kit/project.toml"),
|
|
|
|
|
"[[component]]\nname = \"app\"\npath = \".\"\nsetup = [\"cargo check\"]\n",
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let status = check_onboarding_status(&root);
|
|
|
|
|
assert!(!status.needs_project_toml);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 18:29:32 +00:00
|
|
|
// ── 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(".story_kit/specs/00_CONTEXT.md"),
|
|
|
|
|
"# My Project\n\nReal project context.",
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
fs::write(
|
|
|
|
|
root.join(".story_kit/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"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 15:34:31 +00:00
|
|
|
// ── partial onboarding ────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn needs_onboarding_true_when_only_context_is_template() {
|
|
|
|
|
let dir = TempDir::new().unwrap();
|
|
|
|
|
let root = setup_project(&dir);
|
|
|
|
|
|
2026-02-24 16:13:39 +00:00
|
|
|
// Context still has sentinel
|
2026-02-24 15:34:31 +00:00
|
|
|
fs::write(
|
|
|
|
|
root.join(".story_kit/specs/00_CONTEXT.md"),
|
2026-02-24 16:13:39 +00:00
|
|
|
"<!-- story-kit:scaffold-template -->\n# Project Context\nPlaceholder...",
|
2026-02-24 15:34:31 +00:00
|
|
|
)
|
|
|
|
|
.unwrap();
|
2026-02-24 16:13:39 +00:00
|
|
|
// Stack is customised (no sentinel)
|
2026-02-24 15:34:31 +00:00
|
|
|
fs::write(
|
|
|
|
|
root.join(".story_kit/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());
|
|
|
|
|
}
|
|
|
|
|
}
|