code-baseline 1.6.0

Enforce architectural decisions AI coding tools keep ignoring
Documentation
# baseline.toml — Baseline configuration
# Docs: https://github.com/stewartjarod/baseline

[baseline]
name = "my-project"

# Presets: load a curated set of rules in one line.
# Available presets: "shadcn-strict", "shadcn-migrate", "ai-safety",
#                    "security", "nextjs", "ai-codegen", "react",
#                    "nextjs-best-practices", "accessibility", "react-native"
# Any [[rule]] with the same id as a preset rule overrides it.
# extends = ["shadcn-strict"]

# Files to include in scanning (default: everything)
include = ["src/**/*", "app/**/*", "components/**/*"]

# Files to exclude (defaults shown below are always applied)
exclude = [
    "**/node_modules/**",
    "**/target/**",
    "**/.git/**",
    "**/dist/**",
    "**/build/**",
    "**/.next/**",
    "**/vendor/**",
]

# Plugin files: load additional rules from external TOML files
# plugins = ["./plugins/react-rules.toml", "./plugins/security-rules.toml"]

# Scoped presets: apply a preset only to files under a specific path.
# Useful for monorepos where different apps need different presets.
# Each [[baseline.scoped]] entry takes a preset name and a directory path.
# Rule globs are automatically prefixed with the path.
#
# [[baseline.scoped]]
# preset = "nextjs"
# path = "apps/web"
#
# Multiple presets can be combined in a single entry:
# [[baseline.scoped]]
# preset = ["nextjs", "shadcn-strict"]
# path = "apps/web"
#
# [[baseline.scoped]]
# preset = "react-native"
# path = "apps/mobile"
#
# Exclude specific rules from a scoped preset:
# [[baseline.scoped]]
# preset = "nextjs"
# path = "apps/web"
# exclude_rules = ["no-private-env-client", "no-sync-scripts"]


# ══════════════════════════════════════════════
# TAILWIND + SHADCN RULES
# ══════════════════════════════════════════════

# ──────────────────────────────────────────────
# Tailwind Dark Mode Enforcement
# Ensures every hardcoded Tailwind color class
# has a corresponding dark: variant.
# ──────────────────────────────────────────────

# [[rule]]
# id = "enforce-dark-mode"
# type = "tailwind-dark-mode"
# severity = "error"
# glob = "**/*.{tsx,jsx}"
# message = "Missing dark: variant for color class"
# suggest = "Use a shadcn semantic token or add a dark: counterpart"
# allowed_classes = ["bg-brand-gradient"]
# exclude_glob = ["**/components/ui/**"]

# ──────────────────────────────────────────────
# shadcn Theme Token Enforcement
# ──────────────────────────────────────────────

# [[rule]]
# id = "use-theme-tokens"
# type = "tailwind-theme-tokens"
# severity = "warning"
# glob = "**/*.{tsx,jsx}"
# message = "Use shadcn semantic token instead of raw color"
# token_map = ["bg-indigo-600=bg-brand", "text-indigo-50=text-brand-foreground"]
# allowed_classes = ["bg-green-500"]
# exclude_glob = ["**/components/ui/**"]


# ══════════════════════════════════════════════
# MIGRATION RULES
# ══════════════════════════════════════════════

# ──────────────────────────────────────────────
# Banned Patterns
# ──────────────────────────────────────────────

[[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"

# ──────────────────────────────────────────────
# Banned Imports
# ──────────────────────────────────────────────

[[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"

# ──────────────────────────────────────────────
# Banned Dependencies
# ──────────────────────────────────────────────

[[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"


# ══════════════════════════════════════════════
# RATCHET RULES
# Enforce decreasing counts of legacy patterns.
# Use `baseline snapshot` to find current count.
# ══════════════════════════════════════════════

# [[rule]]
# id = "ratchet-legacy-fetch"
# type = "ratchet"
# severity = "error"
# pattern = "legacyFetch("
# max_count = 47
# glob = "src/**/*.ts"
# message = "Migrate remaining legacyFetch calls to apiFetch"
# regex = false        # set to true for regex pattern matching

# [[rule]]
# id = "ratchet-hex-colors"
# type = "ratchet"
# severity = "warning"
# pattern = "#[0-9a-fA-F]{3,8}"
# regex = true
# max_count = 20
# glob = "**/*.{tsx,jsx,css}"
# message = "Migrate hardcoded hex colors to CSS variables"


# ══════════════════════════════════════════════
# REQUIRED PATTERNS
# Ensure certain patterns exist in matching files.
# Supports optional condition_pattern for "if X then Y" logic.
# ══════════════════════════════════════════════

# [[rule]]
# id = "error-boundary-in-pages"
# type = "required-pattern"
# severity = "error"
# glob = "src/pages/**/*.tsx"
# pattern = "ErrorBoundary"
# message = "All page components must wrap content in an ErrorBoundary"

# [[rule]]
# id = "validate-post-handlers"
# type = "required-pattern"
# severity = "error"
# glob = "src/api/**/*.ts"
# pattern = "validateInput"
# condition_pattern = "app.post("
# message = "POST handlers must validate input"


# ══════════════════════════════════════════════
# FILE PRESENCE
# Ensure required project files exist.
# ══════════════════════════════════════════════

# [[rule]]
# id = "has-readme"
# type = "file-presence"
# severity = "warning"
# required_files = ["README.md", "LICENSE", ".env.example"]
# message = "Projects must have required documentation files"

# [[rule]]
# id = "no-env-committed"
# type = "file-presence"
# severity = "error"
# forbidden_files = [".env", ".env.local", ".env.production"]
# message = "Environment files must not be committed"
# suggest = "Add to .gitignore and remove from version control"


# ══════════════════════════════════════════════
# WINDOW / PROXIMITY RULES
# Enforce that two patterns appear within N lines.
# pattern = trigger, condition_pattern = required nearby,
# max_count = window size (lines to search).
# ══════════════════════════════════════════════

# [[rule]]
# id = "org-scoped-queries"
# type = "window-pattern"
# severity = "error"
# pattern = "DELETE FROM"
# condition_pattern = "organizationId"
# max_count = 80
# glob = "src/**/*.ts"
# message = "DELETE queries must include organizationId within 80 lines"
# suggest = "Add WHERE organizationId = $orgId to scope the query"

# [[rule]]
# id = "async-try-catch"
# type = "window-pattern"
# severity = "warning"
# pattern = "async ("
# condition_pattern = "try {"
# max_count = 10
# regex = true
# glob = "src/api/**/*.ts"
# message = "Async handlers should have try/catch within 10 lines"


# ══════════════════════════════════════════════
# FILE-CONTEXT CONDITIONING
# Rules can be conditioned on file content.
# file_contains / file_not_contains activate rules selectively.
# ══════════════════════════════════════════════

# [[rule]]
# id = "no-private-env-in-client"
# type = "banned-pattern"
# severity = "error"
# pattern = "process.env.SECRET"
# regex = true
# glob = "src/**/*.{ts,tsx}"
# file_contains = "'use client'"
# skip_strings = true   # ignore matches inside string literals (requires `ast` feature)
# message = "Do not access private env vars in client components"

# [[rule]]
# id = "server-only-imports"
# type = "banned-import"
# severity = "error"
# packages = ["@tanstack/react-query"]
# glob = "src/**/*.ts"
# file_not_contains = "'use client'"
# message = "React Query is client-only — this appears to be a server component"


# ══════════════════════════════════════════════
# ESCAPE-HATCH COMMENTS
# Suppress violations with inline comments:
#   // baseline:allow-{rule-id}          (same line)
#   // baseline:allow-all                (same line, any rule)
#   // baseline:allow-next-line {rule-id} (next line)
# ══════════════════════════════════════════════


# ══════════════════════════════════════════════
# PER-RULE EXCLUSIONS
# Any rule can use exclude_glob to skip specific paths,
# even if they match the inclusion glob.
# ══════════════════════════════════════════════

# [[rule]]
# id = "no-radix-direct"
# type = "banned-import"
# severity = "error"
# packages = ["@radix-ui/react-dialog", "@radix-ui/react-popover"]
# exclude_glob = ["**/components/ui/**"]
# message = "Import shadcn wrappers, not Radix directly"


# ══════════════════════════════════════════════
# AST RULES (require `ast` feature flag)
# Install with: cargo install code-baseline --features ast
# These rules use tree-sitter for structural analysis
# of React/TypeScript/JSX/JavaScript files.
# ══════════════════════════════════════════════

# ──────────────────────────────────────────────
# Max Component Size
# Flag components that exceed a line count threshold.
# ──────────────────────────────────────────────

# [[rule]]
# id = "max-component-size"
# type = "max-component-size"
# severity = "warning"
# glob = "**/*.{tsx,jsx}"
# max_count = 150
# message = "Component exceeds 150 lines — split into smaller components"
# suggest = "Extract logic into custom hooks or break into sub-components"

# ──────────────────────────────────────────────
# No Nested Components
# Detects component definitions inside other components.
# Nested components are re-created every render, causing
# remounting and state loss.
# ──────────────────────────────────────────────

# [[rule]]
# id = "no-nested-components"
# type = "no-nested-components"
# severity = "error"
# glob = "**/*.{tsx,jsx}"
# message = "Component defined inside another component — causes remounting on every render"
# suggest = "Move the component to module level or pass as a prop"

# ──────────────────────────────────────────────
# Prefer useReducer
# Flag components with too many useState calls.
# Related state is better modelled with useReducer.
# ──────────────────────────────────────────────

# [[rule]]
# id = "prefer-use-reducer"
# type = "prefer-use-reducer"
# severity = "warning"
# glob = "**/*.{tsx,jsx}"
# max_count = 4
# message = "Component has 4+ useState calls — consider useReducer for related state"
# suggest = "Group related state into a single useReducer"

# ──────────────────────────────────────────────
# No Cascading setState in useEffect
# Flag useEffect callbacks with too many setState calls.
# Multiple setState in an effect often causes cascading renders.
# ──────────────────────────────────────────────

# [[rule]]
# id = "no-cascading-set-state"
# type = "no-cascading-set-state"
# severity = "warning"
# glob = "**/*.{tsx,jsx}"
# max_count = 3
# message = "useEffect has 3+ setState calls — consider useReducer or derived state"
# suggest = "Combine state updates with useReducer or compute derived values"