use crate::knowledge::{lang_by_id, libs_for, libraries, project_type_aliases, suggest_language};
use serde::Deserialize;
use crate::model::*;
use chrono::Utc;
#[derive(Clone)]
pub struct PlanInput {
pub name: String,
pub description: String,
pub project_type_raw: String,
pub audience: String,
pub scale: String,
pub language_pref: Option<String>,
pub database: String,
pub auth: String,
pub deployment: String,
pub features: Vec<String>,
pub constraints: Vec<String>,
pub template_slug: Option<String>,
}
pub fn detect_domain(idea: &str, ptype: &str) -> &'static str {
let low = idea.to_lowercase();
if low.contains("cube") || low.contains("puzzle") || low.contains("solver") || low.contains("algorithm") || low.contains("search") || low.contains("pathfinding") || low.contains("sort") {
return "algorithmic";
}
if low.contains("web") || low.contains("http") || low.contains("api") || low.contains("server") || low.contains("site") || ptype == "web-app" || ptype == "rest-api" {
return "web";
}
if low.contains("data") || low.contains("pipeline") || low.contains("etl") || low.contains("csv") || low.contains("parquet") || ptype == "data-pipeline" {
return "data";
}
if low.contains("ml") || low.contains("model") || low.contains("train") || low.contains("predict") || ptype == "ml-project" {
return "ml";
}
if low.contains("game") || low.contains("player") || low.contains("sprite") || ptype == "game" {
return "game";
}
if ptype == "cli" { return "cli"; }
"default"
}
pub fn domain_guidance(domain: &str) -> Option<String> {
match domain {
"algorithmic" => Some(include_str!("../data/domains/algorithmic.md").to_string()),
"web" => Some(include_str!("../data/domains/web.md").to_string()),
"data" => Some(include_str!("../data/domains/data.md").to_string()),
"ml" => Some(include_str!("../data/domains/ml.md").to_string()),
"game" => Some(include_str!("../data/domains/game.md").to_string()),
_ => None,
}
}
#[derive(Deserialize, Clone, Debug)]
pub struct CorpusEntry {
pub name: String,
pub keywords: Vec<String>,
pub problem_type: String,
pub file_tree: Vec<String>,
pub libraries: Vec<String>,
pub milestones: Vec<CorpusMilestone>,
pub risks: Vec<String>,
pub pitfalls: Vec<String>,
}
#[derive(Deserialize, Clone, Debug)]
pub struct CorpusMilestone {
pub name: String,
pub goal: String,
pub tasks: Vec<CorpusTask>,
}
#[derive(Deserialize, Clone, Debug)]
pub struct CorpusTask {
pub id: String,
pub title: String,
pub description: String,
pub depends_on: Vec<String>,
}
static CORPUS: std::sync::OnceLock<Vec<CorpusEntry>> = std::sync::OnceLock::new();
fn corpus() -> &'static [CorpusEntry] {
CORPUS.get_or_init(|| {
serde_json::from_str(include_str!("../data/corpus.json")).unwrap_or_default()
})
}
pub fn find_corpus(idea: &str) -> Option<CorpusEntry> {
let low = idea.to_lowercase();
let mut best: Option<(usize, CorpusEntry)> = None;
for c in corpus() {
let score: usize = c.keywords.iter().filter(|k| low.contains(&k.to_lowercase())).count();
if score >= 2 {
if best.as_ref().map(|(s, _)| score > *s).unwrap_or(true) {
best = Some((score, c.clone()));
}
}
}
best.map(|(_, c)| c)
}
pub fn template_slug_for(ptype: &str) -> Option<&'static str> {
match ptype {
"cli" => Some("cli-tool"),
"rest-api" => Some("rest-api"),
"web-app" => Some("web-app"),
"library" => Some("library"),
"desktop" => Some("desktop-app"),
"data-pipeline" => Some("data-pipeline"),
"ml-project" => Some("ml-project"),
_ => None,
}
}
pub fn infer_requirements(idea: &str) -> (String, String, String, String, Vec<String>) {
let low = idea.to_lowercase();
// database
let db = if low.contains("sqlite") { "sqlite" }
else if low.contains("postgres") || low.contains("postgresql") { "postgres" }
else if low.contains("mongo") { "mongo" }
else if low.contains("database") || low.contains("db") || low.contains("storage") || low.contains("persist") { "sqlite" }
else { "none" };
// auth
let auth = if low.contains("oauth") { "oauth" }
else if low.contains("jwt") || low.contains("token") { "api-key" }
else if low.contains("login") || low.contains("auth") || low.contains("admin") || low.contains("tripcode") { "basic" }
else { "none" };
// deployment
let deploy = if low.contains("docker") || low.contains("container") || low.contains("kubernetes") || low.contains("k8s") { "container" }
else if low.contains("cloud") || low.contains("serverless") || low.contains("lambda") || low.contains("aws") || low.contains("gcp") || low.contains("azure") { "cloud" }
else if low.contains("desktop") { "desktop" }
else { "local" };
// scale
let scale = if low.contains("large") || low.contains("scale") || low.contains("high traffic") || low.contains("millions") || low.contains("concurrent") || low.contains("production") { "large" }
else if low.contains("medium") { "medium" }
else { "small" };
// strip parentheticals from whole idea first
let mut cleaned = idea.to_string();
while let Some(s) = cleaned.find('(') {
if let Some(e) = cleaned[s..].find(')') {
cleaned = format!("{}{}", &cleaned[..s], &cleaned[s+e+1..]);
} else { break; }
}
// features: split on commas/semicolons/newlines; normalize and dedupe
let mut features: Vec<String> = Vec::new();
for part in cleaned.split(|c: char| c == ',' || c == ';' || c == '\n') {
let p = part.trim().trim_matches(|c: char| c == '.' || c == ' ' || c == '-').to_string();
if p.len() < 6 || p.len() > 110 { continue; }
let low = p.to_lowercase();
if low.contains("commands:") || low.contains("supports") || low.contains("designed") || low.contains("http://") || low.contains("--json") { continue; }
if p.split_whitespace().count() < 3 { continue; }
features.push(p);
}
// keyword-based feature extraction (supplement if few features)
if features.len() < 4 {
let low = idea.to_lowercase();
let cmd_keywords = ["init", "validate", "upload", "download", "search", "info", "review", "reviews", "update", "llmhelp", "run", "pack", "diff", "pull", "fork", "delete", "status", "doubles", "merge", "transfer", "bump"];
for kw in &cmd_keywords {
if low.contains(kw) {
let f = format!("{} command", kw);
if !features.iter().any(|x| x.to_lowercase() == f.to_lowercase()) {
features.push(f);
}
}
}
}
if features.is_empty() {
features.push(idea.trim().to_string());
}
// dedupe case-insensitively
let mut seen = std::collections::HashSet::new();
features.retain(|f| seen.insert(f.to_lowercase()));
features.truncate(10);
(db.to_string(), auth.to_string(), deploy.to_string(), scale.to_string(), features)
}
pub fn build_plan(input: PlanInput) -> ProjectPlan {
let ptype = project_type_aliases(&input.project_type_raw).to_string();
let lang_id = suggest_language(&ptype, input.language_pref.as_deref());
let lang = lang_by_id(&lang_id);
let domain = detect_domain(&input.description, &ptype);
let corpus_entry = find_corpus(&input.description);
let lang_name = lang.map(|l| l.name.clone()).unwrap_or_else(|| lang_id.clone());
let language_guidance = if lang_id == "rust" {
Some(include_str!("../data/rust_guidance.md").to_string())
} else {
None
};
let pitfalls = if let Some(c) = &corpus_entry {
if !c.pitfalls.is_empty() { c.pitfalls.clone() } else { build_pitfalls(&ptype, &lang_id, &input) }
} else {
build_pitfalls(&ptype, &lang_id, &input)
};
let mut libs: Vec<Library> = Vec::new();
let domains: Vec<&str> = match ptype.as_str() {
"cli" => vec!["cli", "serialization", "observability"],
"rest-api" => vec!["web-backend", "database", "serialization", "validation", "observability"],
"web-app" => vec!["web-frontend", "validation", "build"],
"library" => vec!["serialization"],
"desktop" => vec!["desktop", "ui"],
"mobile" => vec!["mobile", "ui"],
"data-pipeline" => vec!["data", "validation", "cli"],
"ml-project" => vec!["ml", "data", "web-backend"],
_ => vec!["cli"],
};
for d in &domains {
for l in libs_for(&lang_id, Some(d)) {
// avoid competing frameworks: keep first web-backend lib only
if d == &"web-backend" && libs.iter().any(|x| x.domain == "web-backend") {
continue;
}
libs.push((*l).clone());
}
}
if ptype == "rest-api" {
for l in libs_for(&lang_id, Some("async")) {
libs.push((*l).clone());
}
for l in libs_for(&lang_id, Some("http-client")) {
libs.push((*l).clone());
}
}
let mut seen = std::collections::HashSet::new();
libs.retain(|l| seen.insert(l.name.clone()));
// keyword-driven additions
let text = format!("{} {}", input.description, input.features.join(" ")).to_lowercase();
let keyword_map: Vec<(&str, &str)> = vec![
("tar", "archive"), ("tar.gz", "archive"), ("pack", "archive"),
("gzip", "compression"), ("compress", "compression"),
("walk", "filesystem"), ("directory", "filesystem"), ("file", "filesystem"),
("http", "http-client"), ("hub", "http-client"), ("api", "http-client"), ("request", "http-client"),
("sha", "crypto"), ("checksum", "crypto"), ("hash", "crypto"),
("temp", "filesystem"), ("tempfile", "filesystem"),
("self-update", "update"), ("self update", "update"), ("update", "update"),
("semver", "versioning"), ("version", "versioning"),
("json", "serialization"), ("toml", "serialization"), ("manifest", "serialization"),
("search", "serialization"),
];
for (kw, domain) in &keyword_map {
if text.contains(kw) {
if let Some(l) = libs_for(&lang_id, Some(domain)).first() {
if !libs.iter().any(|x| x.name == l.name) {
libs.push((**l).clone());
}
}
}
}
// remove irrelevant crates for cli type (image/chrono not needed unless keywords)
if ptype == "cli" {
if !text.contains("image") && !text.contains("thumbnail") && !text.contains("upload") {
libs.retain(|l| l.domain != "image");
}
if !text.contains("time") && !text.contains("timestamp") && !text.contains("date") {
libs.retain(|l| l.domain != "time");
}
}
// dedupe again after keyword additions
let mut seen2 = std::collections::HashSet::new();
libs.retain(|l| seen2.insert(l.name.clone()));
// corpus override for libraries
if let Some(c) = &corpus_entry {
if !c.libraries.is_empty() {
let mut new_libs = Vec::new();
for name in &c.libraries {
if let Some(l) = libraries().iter().find(|l| l.name == *name && l.lang == lang_id) {
new_libs.push((*l).clone());
} else {
new_libs.push(Library {
name: name.clone(),
lang: lang_id.clone(),
domain: "general".to_string(),
purpose: "Recommended by corpus".to_string(),
url: None,
note: None,
});
}
}
if !new_libs.is_empty() {
libs = new_libs;
}
}
}
if input.database == "none" {
libs.retain(|l| l.domain != "database");
}
// constraints influence on libraries
let constraints_text = input.constraints.join(" ").to_lowercase();
if constraints_text.contains("offline") {
libs.retain(|l| l.domain != "http-client" && l.domain != "update");
}
if constraints_text.contains("small") || constraints_text.contains("minimal") {
libs.retain(|l| l.name != "tracing");
}
let framework = libs.iter().find(|l| {
matches!(l.domain.as_str(), "web-backend" | "web-frontend" | "desktop" | "mobile")
}).map(|l| l.name.clone());
let stack = Stack {
language: lang_name.clone(),
framework: framework.clone(),
libraries: libs.iter().map(|l| l.name.clone()).collect(),
rationale: format!(
"{} chosen for {} project (scale: {}).",
lang_name, ptype, input.scale
),
};
let architecture = match ptype.as_str() {
"cli" => "Single binary CLI. Flow: main -> cli (clap) -> command dispatch -> domain logic -> io. Modules: main, cli, config, manifest, pack, hub, update, error, io. Use anyhow at boundaries, thiserror in library code. Keep main thin; all logic in lib/testable modules.".to_string(),
"rest-api" => "Layered API: router -> handlers -> service -> repository -> DB. Middleware: tracing, CORS, auth, rate-limit. Health + readiness endpoints. Migrations via sqlx. Config via env + dotenvy.".to_string(),
"web-app" => "SPA: pages -> components -> hooks -> API client -> backend. Route-based code splitting. Form validation with zod. State via react-query/swr. Error boundaries.".to_string(),
"library" => "Public API in lib.rs; internal modules private; feature flags for optional deps; property tests; docs with examples. Semver discipline.".to_string(),
"desktop" => "Tauri: Rust core (commands, state, fs, http) + web frontend. IPC commands typed; packaging via tauri-cli.".to_string(),
"mobile" => "Cross-platform: shared core (models, networking) + platform UI (SwiftUI/Compose). Offline-first with sync.".to_string(),
"data-pipeline" => "Stages: extract -> validate -> transform -> load. Idempotent, checkpointed, retryable. CLI entrypoint; config-driven.".to_string(),
"ml-project" => "Pipeline: data prep -> training -> evaluation -> artifact -> serving (FastAPI/gRPC). Experiment tracking, versioned datasets.".to_string(),
_ => "Modular monolith with clear module boundaries and dependency injection.".to_string(),
};
let file_tree = if let Some(c) = &corpus_entry {
c.file_tree.clone()
} else {
template_file_tree(&input.template_slug, &ptype)
};
let milestones = if let Some(c) = &corpus_entry {
if !c.milestones.is_empty() {
c.milestones.iter().map(|m| Milestone {
name: m.name.clone(),
goal: m.goal.clone(),
tasks: m.tasks.iter().map(|t| PlanTask {
id: t.id.clone(),
title: t.title.clone(),
description: t.description.clone(),
depends_on: t.depends_on.clone(),
}).collect(),
}).collect()
} else {
build_milestones(&ptype, &input)
}
} else {
build_milestones(&ptype, &input)
};
let mut standards = vec![
"Use strict typing; avoid `any`/`unwrap` in production paths.".to_string(),
"Handle errors explicitly; no silent failures.".to_string(),
"Add unit tests for core logic; integration tests for boundaries.".to_string(),
"Keep functions small; single responsibility.".to_string(),
"Document public API.".to_string(),
];
if lang_id == "rust" {
standards.push("Run cargo fmt, clippy; fix all warnings.".to_string());
standards.push("Prefer anyhow for app errors, thiserror for library errors.".to_string());
}
if lang_id == "typescript" {
standards.push("Enable strict TS; use eslint + prettier.".to_string());
}
if lang_id == "python" {
standards.push("Type hints everywhere; ruff + mypy; pyproject.toml.".to_string());
}
let constraints_text = input.constraints.join(" ").to_lowercase();
if constraints_text.contains("performance") || constraints_text.contains("fast") {
standards.push("Profile before optimizing; use rayon for parallelism; avoid allocations in hot paths.".to_string());
}
if constraints_text.contains("offline") {
standards.push("No network at runtime; bundle all assets.".to_string());
}
let mut risks = Vec::new();
if input.scale == "large" {
risks.push("Scope creep: break into phases; ship MVP first.".to_string());
}
if input.database != "none" {
risks.push("DB schema migrations: plan versioning early.".to_string());
}
if input.auth != "none" {
risks.push("Auth security: use established libs; never roll crypto.".to_string());
}
// type-specific risks
match ptype.as_str() {
"rest-api" => {
risks.push("Missing input validation at API boundary.".to_string());
risks.push("N+1 queries and missing DB indexes.".to_string());
risks.push("Breaking API changes without versioning.".to_string());
}
"web-app" => {
risks.push("State desync between client and server.".to_string());
risks.push("Bundle bloat and poor loading states.".to_string());
}
"data-pipeline" => {
risks.push("Partial failures and retry storms.".to_string());
risks.push("Schema drift breaking downstream consumers.".to_string());
risks.push("Memory blowup on large inputs.".to_string());
}
"ml-project" => {
risks.push("Data leakage between train/test.".to_string());
risks.push("Non-reproducible training (seeds/deps).".to_string());
risks.push("Serving without monitoring.".to_string());
}
"library" => {
risks.push("Breaking public API without major bump.".to_string());
risks.push("Leaking internal types.".to_string());
}
"desktop" => {
risks.push("Packaging and code-signing issues.".to_string());
risks.push("IPC security and large payload handling.".to_string());
}
_ => {}
}
let text = format!("{} {}", input.description, input.features.join(" ")).to_lowercase();
if ptype == "cli" {
risks.push("Tar path traversal and accidental inclusion of secrets in bundles.".to_string());
risks.push("Self-update supply chain: verify checksums; support rollback.".to_string());
risks.push("Hub API drift and versioning; handle 401/403/404 gracefully.".to_string());
risks.push("Unstable --json output breaking agent consumers.".to_string());
risks.push("Version conflicts and non-deterministic packaging.".to_string());
} else {
if text.contains("upload") || text.contains("image") {
risks.push("File upload abuse: enforce size limits, validate types, store outside web root.".to_string());
}
if text.contains("auth") || text.contains("login") || text.contains("admin") {
risks.push("CSRF/XSS and session fixation on auth flows.".to_string());
}
if text.contains("search") || text.contains("fts") {
risks.push("Search performance and injection on user queries.".to_string());
}
if text.contains("websocket") || text.contains("ws") || text.contains("live") || text.contains("real-time") {
risks.push("WebSocket connection limits and backpressure.".to_string());
}
if text.contains("public") || text.contains("post") || text.contains("comment") {
risks.push("Abuse/spam and rate limiting on public endpoints.".to_string());
}
}
if libs.is_empty() {
risks.push("No libraries matched; verify language/domain.".to_string());
}
let reqs = vec![
Requirement { key: "project_type".into(), value: ptype.clone() },
Requirement { key: "database".into(), value: input.database.clone() },
Requirement { key: "auth".into(), value: input.auth.clone() },
Requirement { key: "deployment".into(), value: input.deployment.clone() },
Requirement { key: "scale".into(), value: input.scale.clone() },
];
let mut requirements = reqs;
for f in &input.features {
requirements.push(Requirement { key: "feature".into(), value: f.clone() });
}
for c in &input.constraints {
requirements.push(Requirement { key: "constraint".into(), value: c.clone() });
}
// corpus override for risks
let risks = if let Some(c) = &corpus_entry {
if !c.risks.is_empty() { c.risks.clone() } else { risks }
} else {
risks
};
ProjectPlan {
name: input.name,
description: input.description,
project_type: ptype,
audience: input.audience,
scale: input.scale,
requirements,
stack,
libraries: libs,
architecture,
file_tree,
milestones,
coding_standards: standards,
risks,
template: input.template_slug,
created_at: Utc::now().to_rfc3339(),
ai_assistance: None,
language_guidance,
domain: domain.to_string(),
domain_guidance: domain_guidance(domain),
pitfalls,
}
}
fn template_file_tree(slug: &Option<String>, ptype: &str) -> Vec<String> {
let mut base = if let Some(s) = slug {
if let Some(t) = crate::templates::find_template(s) {
t.files.iter().map(|(p, _)| p.clone()).collect()
} else {
Vec::new()
}
} else {
Vec::new()
};
if base.is_empty() {
base = match ptype {
"cli" => vec!["Cargo.toml".into(), "src/main.rs".into(), "README.md".into()],
"rest-api" => vec!["Cargo.toml".into(), "src/main.rs".into(), "migrations/".into(), "README.md".into()],
"web-app" => vec!["package.json".into(), "index.html".into(), "src/main.tsx".into(), "src/App.tsx".into()],
"library" => vec!["Cargo.toml".into(), "src/lib.rs".into(), "README.md".into()],
"desktop" => vec!["src-tauri/Cargo.toml".into(), "src-tauri/src/main.rs".into()],
"data-pipeline" => vec!["pyproject.toml".into(), "pipeline.py".into()],
"ml-project" => vec!["pyproject.toml".into(), "train.py".into()],
_ => vec!["README.md".into()],
};
}
// expand cli file tree with common modules
if ptype == "cli" {
let extras = ["src/cli.rs", "src/config.rs", "src/error.rs", "tests/cli.rs"];
for e in extras {
if !base.iter().any(|b| b == e) {
base.push(e.into());
}
}
}
// expand rest-api file tree
if ptype == "rest-api" {
let extras = ["src/handlers.rs", "src/models.rs", "src/state.rs", "src/error.rs", "tests/api.rs"];
for e in extras {
if !base.iter().any(|b| b == e) {
base.push(e.into());
}
}
}
base
}
fn build_milestones(ptype: &str, input: &PlanInput) -> Vec<Milestone> {
let mut ms = Vec::new();
ms.push(Milestone {
name: "M0: Setup".into(),
goal: "Project scaffolding, tooling, CI".into(),
tasks: vec![
PlanTask { id: "M0-1".into(), title: "Scaffold project from template".into(), description: "Generate files, install deps.".into(), depends_on: vec![] },
PlanTask { id: "M0-2".into(), title: "Configure lint/format/test".into(), description: "Set up formatters and CI.".into(), depends_on: vec!["M0-1".into()] },
],
});
ms.push(Milestone {
name: "M1: Core".into(),
goal: "Implement core domain logic".into(),
tasks: vec![
PlanTask { id: "M1-1".into(), title: "Define data models".into(), description: "Structs/schemas for domain entities.".into(), depends_on: vec!["M0-1".into()] },
PlanTask { id: "M1-2".into(), title: "Implement core operations".into(), description: "Business logic with tests.".into(), depends_on: vec!["M1-1".into()] },
],
});
if !input.features.is_empty() {
let tasks: Vec<PlanTask> = input.features.iter().enumerate().map(|(i, f)| {
PlanTask {
id: format!("F{}-{}", i + 1, i + 1),
title: f.clone(),
description: format!("Implement and test: {}", f),
depends_on: vec!["M1-2".to_string()],
}
}).collect();
ms.push(Milestone {
name: "M2: Features".into(),
goal: "Deliver user-facing features".into(),
tasks,
});
}
// domain-specific milestone
let domain = detect_domain(&input.description, ptype);
match domain {
"algorithmic" => {
ms.push(Milestone {
name: "M-Algorithm".into(),
goal: "Model and solve".into(),
tasks: vec![
PlanTask { id: "MA-1".into(), title: "State model".into(), description: "Represent problem state; define invariants.".into(), depends_on: vec!["M1-1".into()] },
PlanTask { id: "MA-2".into(), title: "Move/transition generation".into(), description: "Generate and apply transitions; support undo.".into(), depends_on: vec!["MA-1".into()] },
PlanTask { id: "MA-3".into(), title: "Solver algorithm".into(), description: "IDA*/A*/DP with heuristic; validate correctness.".into(), depends_on: vec!["MA-2".into()] },
PlanTask { id: "MA-4".into(), title: "Benchmarks".into(), description: "Criterion benchmarks; profile hot paths.".into(), depends_on: vec!["MA-3".into()] },
],
});
}
"web" => {
ms.push(Milestone {
name: "M-Web".into(),
goal: "HTTP layer".into(),
tasks: vec![
PlanTask { id: "MW-1".into(), title: "Routes & handlers".into(), description: "CRUD endpoints, validation, errors.".into(), depends_on: vec!["M1-2".into()] },
PlanTask { id: "MW-2".into(), title: "Persistence".into(), description: "Migrations + repository.".into(), depends_on: vec!["MW-1".into()] },
PlanTask { id: "MW-3".into(), title: "Auth middleware".into(), description: format!("Auth: {}", input.auth), depends_on: vec!["MW-1".into()] },
],
});
}
"data" => {
ms.push(Milestone {
name: "M-Pipeline".into(),
goal: "ETL stages".into(),
tasks: vec![
PlanTask { id: "MP-1".into(), title: "Extract".into(), description: "Ingest sources.".into(), depends_on: vec!["M1-2".into()] },
PlanTask { id: "MP-2".into(), title: "Transform".into(), description: "Clean/transform.".into(), depends_on: vec!["MP-1".into()] },
PlanTask { id: "MP-3".into(), title: "Load".into(), description: "Write outputs.".into(), depends_on: vec!["MP-2".into()] },
],
});
}
"ml" => {
ms.push(Milestone {
name: "M-ML".into(),
goal: "Train & serve".into(),
tasks: vec![
PlanTask { id: "MM-1".into(), title: "Data prep".into(), description: "Datasets, splits.".into(), depends_on: vec!["M1-2".into()] },
PlanTask { id: "MM-2".into(), title: "Training".into(), description: "Model + optimizer.".into(), depends_on: vec!["MM-1".into()] },
PlanTask { id: "MM-3".into(), title: "Serving".into(), description: "Inference API.".into(), depends_on: vec!["MM-2".into()] },
],
});
}
_ => {}
}
match ptype {
"rest-api" => {
ms.push(Milestone {
name: "M2: API".into(),
goal: "HTTP endpoints".into(),
tasks: vec![
PlanTask { id: "M2-1".into(), title: "Routes & handlers".into(), description: "CRUD endpoints, validation, errors.".into(), depends_on: vec!["M1-2".into()] },
PlanTask { id: "M2-2".into(), title: "Persistence".into(), description: "DB migrations + repository.".into(), depends_on: vec!["M2-1".into()] },
PlanTask { id: "M2-3".into(), title: "Auth middleware".into(), description: format!("Auth: {}", input.auth), depends_on: vec!["M2-1".into()] },
],
});
}
"web-app" => {
ms.push(Milestone {
name: "M2: UI".into(),
goal: "Screens & interactions".into(),
tasks: vec![
PlanTask { id: "M2-1".into(), title: "Component library".into(), description: "Shared components.".into(), depends_on: vec!["M1-2".into()] },
PlanTask { id: "M2-2".into(), title: "Pages & routing".into(), description: "Routes, layouts.".into(), depends_on: vec!["M2-1".into()] },
PlanTask { id: "M2-3".into(), title: "API integration".into(), description: "Client + state management.".into(), depends_on: vec!["M2-2".into()] },
],
});
}
"data-pipeline" => {
ms.push(Milestone {
name: "M2: Pipeline".into(),
goal: "ETL stages".into(),
tasks: vec![
PlanTask { id: "M2-1".into(), title: "Extract".into(), description: "Ingest sources.".into(), depends_on: vec!["M1-2".into()] },
PlanTask { id: "M2-2".into(), title: "Transform".into(), description: "Clean/transform.".into(), depends_on: vec!["M2-1".into()] },
PlanTask { id: "M2-3".into(), title: "Load".into(), description: "Write outputs.".into(), depends_on: vec!["M2-2".into()] },
],
});
}
"ml-project" => {
ms.push(Milestone {
name: "M2: Training".into(),
goal: "Model training & eval".into(),
tasks: vec![
PlanTask { id: "M2-1".into(), title: "Data prep".into(), description: "Datasets, splits.".into(), depends_on: vec!["M1-2".into()] },
PlanTask { id: "M2-2".into(), title: "Training loop".into(), description: "Model + optimizer.".into(), depends_on: vec!["M2-1".into()] },
PlanTask { id: "M2-3".into(), title: "Evaluation".into(), description: "Metrics, checkpoints.".into(), depends_on: vec!["M2-2".into()] },
],
});
ms.push(Milestone {
name: "M3: Serving".into(),
goal: "Serve model".into(),
tasks: vec![
PlanTask { id: "M3-1".into(), title: "Inference API".into(), description: "FastAPI endpoint.".into(), depends_on: vec!["M2-3".into()] },
],
});
}
_ => {}
}
ms.push(Milestone {
name: "M-Final: Polish".into(),
goal: "Docs, tests, release".into(),
tasks: vec![
PlanTask { id: "MF-1".into(), title: "Documentation".into(), description: "README, usage, examples.".into(), depends_on: vec![] },
PlanTask { id: "MF-2".into(), title: "Release build".into(), description: format!("Deploy target: {}", input.deployment), depends_on: vec![] },
],
});
ms
}
fn build_pitfalls(ptype: &str, lang_id: &str, input: &PlanInput) -> Vec<String> {
let mut p = Vec::new();
// generic (kept minimal)
p.push("Skipping tests until the end; regressions accumulate.".to_string());
// type-specific
match ptype {
"cli" => {
p.push("Blocking the terminal with long-running tasks without progress output.".to_string());
p.push("Poor arg validation and unclear error messages.".to_string());
p.push("Not handling SIGINT/Ctrl-C gracefully.".to_string());
}
"rest-api" => {
p.push("Missing input validation at the boundary.".to_string());
p.push("N+1 queries and missing DB indexes.".to_string());
p.push("No rate limiting / auth on sensitive endpoints.".to_string());
p.push("Breaking API changes without versioning.".to_string());
}
"web-app" => {
p.push("State desync between client and server.".to_string());
p.push("Ignoring loading/error states in UI.".to_string());
p.push("Bundle bloat from unused dependencies.".to_string());
}
"library" => {
p.push("Leaking internal types in public API.".to_string());
p.push("Breaking semver without a major bump.".to_string());
p.push("Missing docs/examples.".to_string());
p.push("Overly broad public surface.".to_string());
}
"desktop" => {
p.push("Packaging and code-signing issues.".to_string());
p.push("Blocking the UI thread with sync work.".to_string());
p.push("Large bundle size.".to_string());
}
"mobile" => {
p.push("Platform-specific bugs and testing gaps.".to_string());
p.push("Offline sync conflicts.".to_string());
p.push("App store review rejections.".to_string());
}
"data-pipeline" => {
p.push("Not handling partial failures / retries.".to_string());
p.push("Schema drift breaking downstream consumers.".to_string());
p.push("Loading everything into memory.".to_string());
}
"ml-project" => {
p.push("Data leakage between train/test splits.".to_string());
p.push("Not pinning dependencies / random seeds.".to_string());
p.push("Serving a model without monitoring.".to_string());
}
_ => {}
}
if lang_id == "rust" {
p.push("Overusing clone() instead of references.".to_string());
p.push("unwrap() panics in production paths.".to_string());
p.push("Blocking async runtime with sync I/O.".to_string());
}
if input.features.iter().any(|f| f.to_lowercase().contains("auth")) {
p.push("Rolling your own crypto/auth; use established libs.".to_string());
}
let text = format!("{} {}", input.description, input.features.join(" ")).to_lowercase();
if text.contains("tar") || text.contains("pack") || text.contains("archive") {
p.push("Tar path traversal and accidental inclusion of secrets in bundles.".to_string());
p.push("Non-deterministic archive ordering breaking reproducible builds.".to_string());
}
if text.contains("self-update") || text.contains("self update") || text.contains("update") {
p.push("Self-update supply chain: verify checksums/signatures; support rollback.".to_string());
}
if text.contains("hub") || text.contains("api") {
p.push("Hub API drift and versioning; handle 401/403/404 gracefully.".to_string());
}
if text.contains("json") || text.contains("--json") {
p.push("Unstable --json output breaking agent consumers.".to_string());
}
p
}