code-baseline 1.6.0

Enforce architectural decisions AI coding tools keep ignoring
Documentation
use std::path::Path;

/// Detected project type based on config files present.
#[derive(Debug, PartialEq)]
pub enum ProjectType {
    /// shadcn/ui + Tailwind CSS project (components.json + tailwind config)
    ShadcnTailwind,
    /// Tailwind CSS project (tailwind config but no shadcn)
    TailwindOnly,
    /// Generic JS/TS project (package.json but no Tailwind)
    Generic,
    /// Unknown project type
    Unknown,
}

/// Detect the project type by checking for config files in the given directory.
pub fn detect_project(dir: &Path) -> ProjectType {
    let has_shadcn = dir.join("components.json").exists();
    let has_tailwind = dir.join("tailwind.config.js").exists()
        || dir.join("tailwind.config.ts").exists()
        || dir.join("tailwind.config.mjs").exists()
        || dir.join("tailwind.config.cjs").exists();
    let has_package_json = dir.join("package.json").exists();

    match (has_shadcn, has_tailwind, has_package_json) {
        (true, _, _) => ProjectType::ShadcnTailwind,
        (false, true, _) => ProjectType::TailwindOnly,
        (false, false, true) => ProjectType::Generic,
        _ => ProjectType::Unknown,
    }
}

/// Generate a starter `baseline.toml` config for the detected project type.
pub fn generate_config(project_type: &ProjectType) -> String {
    match project_type {
        ProjectType::ShadcnTailwind => generate_shadcn_config(),
        ProjectType::TailwindOnly => generate_tailwind_config(),
        ProjectType::Generic => generate_generic_config(),
        ProjectType::Unknown => generate_generic_config(),
    }
}

fn generate_shadcn_config() -> String {
    r#"# baseline.toml — Baseline for shadcn/Tailwind
# Generated by `baseline init` (shadcn + Tailwind detected)

[baseline]
name = "my-project"
extends = ["shadcn-strict"]
include = ["src/**/*", "app/**/*", "components/**/*"]
exclude = ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/build/**"]

# The "shadcn-strict" preset includes these rules:
#   enforce-dark-mode    (error)   — dark: variant required for color classes
#   use-theme-tokens     (error)   — raw Tailwind colors → shadcn tokens
#   no-inline-styles     (warning) — ban style={{ }}
#   no-css-in-js         (error)   — ban styled-components, emotion
#   no-competing-frameworks (error) — ban bootstrap, MUI, antd

# Override a preset rule by redeclaring it with the same id:
# [[rule]]
# id = "use-theme-tokens"
# type = "tailwind-theme-tokens"
# severity = "warning"
# glob = "**/*.{tsx,jsx}"
# message = "Use shadcn semantic token instead of raw color"
"#
    .to_string()
}

fn generate_tailwind_config() -> String {
    r#"# baseline.toml — Baseline for Tailwind CSS
# Generated by `baseline init` (Tailwind detected)

[baseline]
name = "my-project"
include = ["src/**/*", "app/**/*", "components/**/*"]
exclude = ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/build/**"]

# ──────────────────────────────────────────────
# Ban Inline Styles
# Catch style={{ }} in JSX — use Tailwind classes instead.
# ──────────────────────────────────────────────

[[rule]]
id = "no-inline-styles"
type = "banned-pattern"
severity = "warning"
pattern = "style={{"
glob = "**/*.{tsx,jsx}"
message = "Avoid inline styles — use Tailwind utility classes instead"
suggest = "Replace style={{ ... }} with Tailwind classes"

# ──────────────────────────────────────────────
# Ban CSS-in-JS Libraries
# If you've committed to Tailwind, these shouldn't be imported.
# ──────────────────────────────────────────────

[[rule]]
id = "no-css-in-js"
type = "banned-import"
severity = "error"
packages = ["styled-components", "@emotion/styled", "@emotion/css", "@emotion/react"]
message = "CSS-in-JS libraries conflict with Tailwind — use utility classes instead"

# ──────────────────────────────────────────────
# Ban Competing CSS Frameworks
# Prevent mixing multiple CSS frameworks in the same project.
# ──────────────────────────────────────────────

[[rule]]
id = "no-competing-frameworks"
type = "banned-dependency"
severity = "error"
packages = ["bootstrap", "bulma", "@mui/material", "antd"]
message = "Competing CSS framework detected — this project uses Tailwind"

# ──────────────────────────────────────────────
# Uncomment these rules if you add shadcn/ui:
# ──────────────────────────────────────────────

# [[rule]]
# id = "enforce-dark-mode"
# type = "tailwind-dark-mode"
# severity = "error"
# glob = "**/*.{tsx,jsx}"
# message = "Missing dark: variant for color class"

# [[rule]]
# id = "use-theme-tokens"
# type = "tailwind-theme-tokens"
# severity = "warning"
# glob = "**/*.{tsx,jsx}"
# message = "Use shadcn semantic token instead of raw color"
"#
    .to_string()
}

fn generate_generic_config() -> String {
    r#"# baseline.toml — Baseline configuration
# Generated by `baseline init`
#
# Tip: If you use Tailwind CSS + shadcn/ui, add a components.json
# and re-run `baseline init` for pre-configured theming rules.

[baseline]
name = "my-project"
include = ["src/**/*", "app/**/*"]
exclude = ["**/node_modules/**", "**/dist/**", "**/build/**"]

# ──────────────────────────────────────────────
# Example: Ban a pattern
# ──────────────────────────────────────────────

# [[rule]]
# id = "no-console-log"
# type = "banned-pattern"
# severity = "warning"
# pattern = "console.log("
# glob = "src/**/*.{ts,tsx}"
# message = "Remove console.log before committing"

# ──────────────────────────────────────────────
# Example: Ban an import
# ──────────────────────────────────────────────

# [[rule]]
# id = "no-moment"
# type = "banned-import"
# severity = "error"
# packages = ["moment"]
# message = "moment.js is deprecated — use date-fns or Temporal API"

# ──────────────────────────────────────────────
# Example: Ban a dependency
# ──────────────────────────────────────────────

# [[rule]]
# id = "no-request"
# type = "banned-dependency"
# severity = "error"
# packages = ["request"]
# message = "The 'request' package is deprecated — use 'node-fetch'"
"#
    .to_string()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;

    #[test]
    fn detect_shadcn_project() {
        let dir = tempfile::tempdir().unwrap();
        fs::write(dir.path().join("components.json"), "{}").unwrap();
        fs::write(dir.path().join("tailwind.config.ts"), "").unwrap();
        fs::write(dir.path().join("package.json"), "{}").unwrap();
        assert_eq!(detect_project(dir.path()), ProjectType::ShadcnTailwind);
    }

    #[test]
    fn detect_shadcn_without_tailwind_config() {
        let dir = tempfile::tempdir().unwrap();
        fs::write(dir.path().join("components.json"), "{}").unwrap();
        fs::write(dir.path().join("package.json"), "{}").unwrap();
        // shadcn implies Tailwind even without explicit tailwind config
        assert_eq!(detect_project(dir.path()), ProjectType::ShadcnTailwind);
    }

    #[test]
    fn detect_tailwind_only() {
        let dir = tempfile::tempdir().unwrap();
        fs::write(dir.path().join("tailwind.config.js"), "").unwrap();
        fs::write(dir.path().join("package.json"), "{}").unwrap();
        assert_eq!(detect_project(dir.path()), ProjectType::TailwindOnly);
    }

    #[test]
    fn detect_tailwind_ts_config() {
        let dir = tempfile::tempdir().unwrap();
        fs::write(dir.path().join("tailwind.config.ts"), "").unwrap();
        assert_eq!(detect_project(dir.path()), ProjectType::TailwindOnly);
    }

    #[test]
    fn detect_generic_project() {
        let dir = tempfile::tempdir().unwrap();
        fs::write(dir.path().join("package.json"), "{}").unwrap();
        assert_eq!(detect_project(dir.path()), ProjectType::Generic);
    }

    #[test]
    fn detect_unknown() {
        let dir = tempfile::tempdir().unwrap();
        assert_eq!(detect_project(dir.path()), ProjectType::Unknown);
    }

    #[test]
    fn shadcn_config_uses_extends() {
        let config = generate_config(&ProjectType::ShadcnTailwind);
        assert!(config.contains(r#"extends = ["shadcn-strict"]"#));
        // Should not contain inline rule definitions (only commented overrides)
        assert!(!config.contains("\n[[rule]]"));
    }

    #[test]
    fn tailwind_config_has_migration_rules() {
        let config = generate_config(&ProjectType::TailwindOnly);
        assert!(config.contains("banned-pattern"));
        assert!(config.contains("banned-import"));
        assert!(config.contains("banned-dependency"));
        // Tailwind rules should be commented out
        assert!(config.contains("# type = \"tailwind-dark-mode\""));
    }

    #[test]
    fn generic_config_has_examples() {
        let config = generate_config(&ProjectType::Generic);
        assert!(config.contains("banned-pattern"));
        assert!(config.contains("banned-import"));
        assert!(config.contains("banned-dependency"));
    }
}