DEV Community

Cover image for A CI tool that stops AI from undoing your team's architectural decisions
Jarod Stewart
Jarod Stewart

Posted on

A CI tool that stops AI from undoing your team's architectural decisions

AI coding tools are fast. They're also confidently wrong about your team's conventions.

You migrate off moment.js. Cursor adds it back. You build a repository layer. Copilot writes db.select() directly in page.tsx. You ship a design system with a <Button> component. Claude writes a custom <button> with inline Tailwind. You ban axios. An AI assistant adds it to package.json.

They generate from training data, not from your architecture. And the legacy patterns in your codebase are the ones they see most -- so they reinforce them.

I kept seeing this across teams and decided to build something to fix it.

Baseline

Baseline is a Rust-based CLI that enforces team decisions in CI. You define rules in a single TOML file, and it catches violations before they land.

npx code-baseline scan
Enter fullscreen mode Exit fullscreen mode

It covers a class of rules that ESLint can't express.

Banning patterns where they don't belong

AI loves bypassing your abstractions. Scope bans to specific paths:

[[rule]]
id = "no-db-in-pages"
type = "banned-pattern"
pattern = "db."
glob = "app/**/page.tsx"
severity = "error"
message = "Use the repository layer, not direct DB access in pages"
suggest = "Import from @/lib/repositories instead"
Enter fullscreen mode Exit fullscreen mode

Banning imports and dependencies

Deprecated packages are one autocomplete away from returning:

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

[[rule]]
id = "no-request"
type = "banned-dependency"
severity = "error"
packages = ["request", "request-promise", "axios"]
message = "Use native fetch or undici"
Enter fullscreen mode Exit fullscreen mode

The banned-dependency rule parses package.json directly -- it catches packages added as dependencies even if no source file imports them yet.

Ratcheting legacy code to zero

This is the feature I'm most proud of. Say you have 200 calls to legacyFetch() and want to migrate to apiFetch():

[[rule]]
id = "ratchet-legacy-fetch"
type = "ratchet"
severity = "error"
pattern = "legacyFetch("
max_count = 200
glob = "src/**/*.ts"
message = "Migrate to apiFetch"
Enter fullscreen mode Exit fullscreen mode

Set the ceiling at 200. Next sprint, migrate some, lower it to 180. The number only goes down. Any PR that adds new legacy calls fails CI.

ESLint is pass/fail. It can't count occurrences across your codebase and enforce a decreasing ceiling over time.

Tailwind + shadcn enforcement

If you use Tailwind with shadcn/ui, AI will write bg-white and text-gray-900 everywhere. Your design system says bg-background and text-foreground. Dark mode breaks silently.

Baseline ships two rules for this:

Dark mode enforcement flags color classes missing a dark: variant:

7:21  error  Missing dark: variant for color class: 'bg-white'
  │ <div className="bg-white border border-gray-200 rounded-lg">
  → Use 'bg-background' instead — it adapts to light/dark automatically
Enter fullscreen mode Exit fullscreen mode

Semantic token enforcement bans raw color classes entirely and suggests your tokens. Ships with 130+ default mappings.

Both rules understand className, class, cn(), clsx(), cva(), and twMerge().

Proximity rules

Enforce that related patterns appear near each other:

[[rule]]
id = "org-scoped-deletes"
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"
Enter fullscreen mode Exit fullscreen mode

Presets

Get started in one line instead of writing rules from scratch:

[baseline]
extends = ["ai-codegen", "security", "nextjs"]
Enter fullscreen mode Exit fullscreen mode
Preset What it catches
ai-codegen as any, empty catch, console.log, TODO, placeholder text, var, require in TS, and more (12 rules)
security Hardcoded secrets, eval, dangerouslySetInnerHTML, .env files, http:// URLs (10 rules)
nextjs Use next/image, next/link, next/font; no next/head or next/router in App Router (8 rules)
shadcn-strict Dark mode enforcement, theme tokens, no inline styles, no CSS-in-JS (5 rules)

What the output looks like

src/utils/helpers.ts
  1:0  error  moment.js is deprecated — use date-fns  no-moment
    │ import moment from 'moment';
    → import { format } from 'date-fns'

src/components/BadCard.tsx
  7:21  error  Missing dark: variant for 'bg-white'  enforce-dark-mode
    │ <div className="bg-white border border-gray-200 rounded-lg">
    → Use 'bg-background' instead

✗ 9 violations (7 error, 2 warning)
Enter fullscreen mode Exit fullscreen mode

CI integration

Ships a GitHub Action that annotates violations inline on PR diffs:

- uses: stewartjarod/baseline@main
  with:
    paths: 'src'
Enter fullscreen mode Exit fullscreen mode

On PRs it automatically scans only changed files. Outputs GitHub annotations, SARIF for Code Scanning, and markdown summaries.

The technical bits

  • Written in Rust. Single binary, no Node runtime.
  • Tree-sitter for AST-aware rules (component size, nested components, useState/useEffect analysis)
  • Parallel scanning via rayon
  • Respects .gitignore automatically
  • Also runs as an MCP server so AI tools can query your rules directly
  • Available on crates.io and npm

Install

# Run instantly
npx code-baseline scan

# Or install
npm install -g code-baseline
cargo install code-baseline
Enter fullscreen mode Exit fullscreen mode

Linters catch syntax. Formatters fix whitespace. Baseline enforces the decisions your team has already made -- especially the ones AI keeps ignoring.

MIT licensed. Feedback and contributions welcome.

GitHub: stewartjarod/baseline

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.