planner / src/planner.rs

raw file 35516 bytes
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
}