purplelint
v0.6.0
Published
Purpose-driven architecture linting protocol. Agent-agnostic. Open spec.
Maintainers
Readme
Install
npm install -D purplelintOr run directly:
npx purplelint initESLint catches semicolons. ArchUnit catches import direction.
purplelint catches "this code will lose us money."
The Problem
Your payment webhook processes the event before persisting it. A retry fires. The customer gets charged twice.
Your junior dev calls jwt.decode() instead of jwt.verify(). Every request is now authenticated with unsigned tokens.
Your architecture rules exist in a wiki nobody reads, enforced by PR reviews nobody has time for. AI agents ship code fast — but they don't know why your layers exist or what order your payment flow requires.
Static linters can't catch these. They don't understand intent.
What purplelint Does
purplelint guards architectural intent, not code style. You define purposes — each one explains why a constraint exists, what the correct sequence of operations is, and what a violation looks like. Then any LLM evaluates your code against them.
# purplelint/billing-tracking.yml
id: billing-tracking
purpose: >
Payment flows must follow a strict sequence.
Webhook: verify signature -> persist raw event -> deduplicate -> process -> acknowledge.
Any step out of order causes double charges or lost events.
violations:
- "SEQUENCE BREAK: Webhook processes event before persisting it"
- "MISSING STEP: Webhook handler without signature verification as the FIRST operation"
- "MISSING STEP: Payment mutation without idempotency key"
good_examples:
- title: "Correct sequence: webhook flow"
code: |
// 1. VERIFY — always first
const event = stripe.webhooks.constructEvent(body, sig, secret);
// 2. PERSIST — store raw event before any processing
await db.webhookEvent.create({ eventId: event.id, raw: body });
// 3. DEDUPLICATE — skip if already processed
if (await isProcessed(event.id)) return res.json({ received: true });
// 4. PROCESS — business logic
await paymentService.fulfill(event);
// 5. ACKNOWLEDGE
await markProcessed(event.id);
bad_examples:
- title: "Broken sequence: process before store"
code: |
const event = JSON.parse(req.body); // no signature verification!
await updateSubscription(event.data); // processes before storing
await db.webhookEvent.create({ data: event }); // crash = event lostThis isn't a regex. It's a sequence diagram encoded as a purpose, evaluated by an LLM that understands control flow.
Quick Start
npx purplelint initThat's it. purplelint scans your project, detects your stack, and walks you through the full setup:
┌ purplelint — Architecture Checkpoint
│
◇ Found 9 architecture pattern(s)
│
◆ Generate purpose files for: (space: toggle, a: all, enter: confirm)
│ ◼ billing-tracking — Payment flows must follow a strict sequence
│ ◼ auth-boundary — Auth must follow a strict sequence per request
│ ◼ oauth-flow — OAuth must follow state+PKCE → callback → verify → exchange
│ ◼ cache-safety — Cache must enforce TTL, invalidation, and stampede protection
│ ◼ data-integrity — Database must enforce uniqueness at DB level
│ ...
│
◆ Detected: Claude, Codex
│
◆ Continue with:
│ ● Claude
│ ○ Codex
│ ○ Skip
│
◆ When should purplelint run?
│ ○ Every commit (pre-commit hook)
│ ○ Before push (pre-push hook)
│ ● PR only (GitHub Actions CI)
│ ○ Manual only (npm run purplelint)
│
◇ Created .github/workflows/purplelint.yml
◇ Saved setup guide: purplelint/SETUP.md
└ ReadyThen run checks:
# Interactive — pick which purposes to check
npx purplelint run -i
# Single purpose
npx purplelint run --purpose billing-tracking
# All purposes
npx purplelint run --all
# Skip a purpose for N days
npx purplelint skip billing-tracking 7
# Validate purpose file schema
npx purplelint validate
# List configured purposes
npx purplelint listAuto-Detection: 14 Built-in Detectors
purplelint init doesn't dump a generic config. It reads your package.json, requirements.txt, pyproject.toml, and source files, then generates only the purposes that apply to your stack.
| Detector | What It Guards | Triggered By | |---|---|---| | billing-tracking | Payment sequence: verify -> persist -> deduplicate -> process -> acknowledge | Stripe, Paddle, LemonSqueezy, or payment patterns in code | | auth-boundary | Auth sequence: extract -> verify -> validate -> attach -> check | jsonwebtoken, jose, passport, next-auth, or JWT patterns | | oauth-flow | OAuth sequence: state+PKCE -> callback -> verify state -> exchange code | OAuth/OIDC patterns, Google/GitHub/Auth0 providers | | transaction-safety | Transaction wrapping, unbounded queries, soft delete | Prisma, TypeORM, Drizzle, SQLAlchemy, Django ORM | | layer-boundary | Controller/service/repo separation of concerns | Express/Fastify routes, Django views, Spring controllers | | test-integrity | Weak assertions, snapshot abuse, mock anti-patterns | Jest, Vitest, pytest, or test files detected | | design-system | Raw Tailwind colors, inline styles, bypassed component library | Tailwind, styled-components, Chakra UI, Material UI | | error-handling | Empty catch blocks, swallowed promises, console.log errors | Any project with try/catch or .catch() patterns | | secret-safety | Hardcoded API keys, .env in git, secrets in logs | Any project (always relevant) | | api-contract | Missing input validation, inconsistent responses, stack trace leaks | Express, Fastify, Django REST, Spring Boot | | performance | N+1 queries, await-in-loop, unbounded queries | ORM or database usage detected | | race-condition | Unsafe concurrent state mutations | Concurrent processing patterns detected | | cache-safety | Missing TTL, stale cache, stampede, shared key collisions | Redis, Memcached, node-cache, or caching patterns | | data-integrity | Check-then-insert races, missing DB-level uniqueness constraints | ORM usage with unique fields or upsert patterns |
Language-agnostic. Works with TypeScript, Python, Go, Java, Rust, Ruby, and mixed-language monorepos.
Sequence Diagrams as Lint Rules
Most linters check syntax. purplelint checks sequences — the order operations must happen.
Payment Webhook (correct sequence):
Client Server Database Payment Provider
| | | |
| POST /webhook | | |
|--------------->| | |
| | 1. VERIFY sig | |
| |--------------->| |
| | 2. PERSIST raw | |
| |--------------->| |
| | 3. DEDUPLICATE | |
| |<---------------| |
| | 4. PROCESS | |
| |--------------->| |
| | 5. ACKNOWLEDGE | |
| 200 OK | | |
|<---------------| | |When your code does step 4 before step 2, purplelint flags it as SEQUENCE BREAK. Not a style issue. A "you will lose money" issue.
The same applies to auth flows (decode before verify = accept unsigned tokens) and database operations (read before lock = race condition).
Agent-Agnostic
purplelint produces structured prompts. Pipe them to any LLM. purplelint init auto-detects installed agents and offers to run your first check immediately.
Claude Code:
npx purplelint run --purpose billing-tracking --output prompt | claudeCodex (OpenAI):
npx purplelint run --all --output prompt | codexCursor / Windsurf:
Add to .cursorrules or .windsurfrules:
Before building, read the /purplelint directory and evaluate changes against each purpose.GitHub Actions CI:
- run: npx purplelint run --all --output json > purplelint-results.json
- run: cat purplelint-results.json | your-model-runnerAny agent, any model. purplelint is the protocol. Your LLM is the evaluator.
Directory Structure
# Single repo
my-app/
└── purplelint/
├── purplelint.yml # config + purpose index
├── billing-tracking.yml # payment sequence rules
└── auth-boundary.yml # auth flow rules
# Monorepo — cascading lookup (like .gitignore)
monorepo/
├── purplelint/ # shared across all packages
│ ├── purplelint.yml
│ └── billing-tracking.yml
└── packages/
├── api/
│ └── purplelint/ # api-specific overrides
│ └── auth-boundary.yml
└── worker/
└── purplelint/ # worker-specific
└── race-condition.ymlClosest purplelint/ folder wins. Inherits from parent unless inherit: false.
Purpose File Anatomy
id: auth-boundary
purpose: >
Auth must follow a strict sequence per request: extract token ->
verify signature -> validate expiry -> attach user context ->
check permissions. This runs ONCE in middleware, never in handlers.
violations:
- "SEQUENCE BREAK: Token used before signature verification"
- "MISSING STEP: Token expiry not validated"
- "BOUNDARY: Token parsing outside auth middleware"
- "LEAK: Error messages revealing whether email exists"
good_examples:
- title: "Correct: middleware auth pipeline"
code: |
// Step 1: EXTRACT
const token = req.headers.authorization?.split(" ")[1];
// Step 2: VERIFY signature (not decode!)
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Step 3: VALIDATE expiry
if (decoded.exp < Date.now() / 1000) throw new TokenExpiredError();
// Step 4: ATTACH
req.user = { id: decoded.sub, role: decoded.role };
bad_examples:
- title: "Broken: decode without verify, auth in handler"
code: |
app.get("/api/data", (req, res) => {
const token = req.headers.authorization?.split(" ")[1];
const user = jwt.decode(token); // accepts UNSIGNED tokens!
if (user.role !== "admin") return res.status(403);
});
context_hint: >
Trace the auth sequence: extract -> verify -> validate expiry ->
attach context -> check permissions. Flag jwt.decode (not verify),
missing exp checks, and auth logic outside middleware.
exceptions:
- "Test files (*.test.ts, *.spec.ts)"
- "Auth middleware implementation itself"Each purpose is both a lint rule AND documentation. New engineers read these and understand why the architecture exists, not just what the rules are.
Design Philosophy
Protocol, not product. The spec is open. Build your own runner. Share your purposes across teams.
"When in doubt, pass." The prompt explicitly instructs models to only flag clear violations. False positives kill trust faster than missed bugs.
You choose when it runs. Every commit, before push, on PRs only, or manual. purplelint init sets up the integration you pick — hooks, CI, scripts. This is an architecture checkpoint, not a style cop.
Sequence-first violations. Instead of "don't do X", purplelint says "step 4 must come after step 2." Order-of-operations bugs cause the most expensive incidents.
Why Not Just...
"...use ESLint / Biome / Ruff?"
Those catch syntax and style. no-unused-vars is useful. But no ESLint rule can express "webhook must persist before processing" or "auth must verify before decode." purplelint operates at the architecture level, where the expensive bugs live.
"...use ArchUnit / ArchGuard?"
ArchUnit is Java-only and checks import/dependency graphs. It can't evaluate control flow sequences or understand why a constraint exists. purplelint is language-agnostic and uses LLMs to understand intent, not just structure.
"...write custom ESLint rules?"
You could. For one language. It takes days per rule, requires AST knowledge, and produces rules nobody maintains. A purplelint purpose file takes 5 minutes to write, works across languages, and reads like documentation.
"...just put it in the PR review checklist?"
You already tried that. It didn't scale past 3 engineers. purplelint automates the architectural checks that senior engineers do manually, so reviews focus on design decisions instead of catching known anti-patterns.
"...add it to .cursorrules?"
Good start — and purplelint works with Cursor. But .cursorrules is unstructured prose. purplelint gives you a schema (violations, examples, sequences, exceptions), works in CI, and produces structured output you can aggregate across a team.
CLI Reference
purplelint <command> [options]
Commands:
init Scan project and generate purpose files
validate Validate purpose files against schema
run Run architecture checks
list List configured purposes
skip Skip a purpose for N days
Options:
--help, -h Show help
--version Show version
init options:
--dir <path> Output directory (default: ./purplelint)
-y, --yes Accept all defaults
run options:
-i, --interactive Select purposes interactively
--purpose <id> Run a single purpose
--all Run all configured purposes
--output <format> Output format: prompt, json, markdown
--diff <ref> Git diff reference (default: staged changes)
--context <mode> Context strategy: diff, diff+imports
--dir <path> Config directory
skip options:
purplelint skip <id> <days> Skip a purpose for N days
purplelint skip --clear Clear all skips
validate options:
--dir <path> Config directoryRequirements
- Node.js >= 18
- npm, npx, or any Node package manager
No runtime dependencies on any specific LLM provider. purplelint generates prompts — you choose who evaluates them.
Purpose Hub — Build Your Own
Purpose files are just YAML. You can build your own collection and share it across projects.
Share via git:
# Team-wide purpose library
git clone https://github.com/your-org/purplelint-purposes.git purplelint/Share via npm:
# Publish as a package
npm install -D @your-org/purplelint-purposes
# Symlink or copy into your project
cp node_modules/@your-org/purplelint-purposes/*.yml purplelint/Share via gist:
# Download a single purpose
curl -o purplelint/oauth-flow.yml https://gist.githubusercontent.com/.../oauth-flow.ymlBuild a domain-specific hub:
your-org/purplelint-purposes/
├── fintech/
│ ├── pci-compliance.yml
│ ├── transaction-audit.yml
│ └── anti-fraud-sequence.yml
├── healthcare/
│ ├── hipaa-data-flow.yml
│ └── consent-sequence.yml
└── saas/
├── tenant-isolation.yml
└── subscription-lifecycle.ymlEach purpose file is self-contained — id, purpose, violations, examples. No build step. No config inheritance. Copy the YAML, run the check.
Contributing
See CONTRIBUTING.md.
git clone https://github.com/room821/purplelint.git
cd purplelint
npm install
npm test
npm run buildAdding a new detector: Create a detector in src/core/scanner.ts, add a preset in presets/, and add tests. See existing detectors for the pattern.
Contributing purposes: Purpose files are just YAML. Create a .yml following the purpose file schema, test it with npx purplelint validate, and open a PR.
License
MIT -- Room821
