From pedantic-coder
This skill should be used when the user's codebase has multiple approaches to the same problem, mixes paradigms (callbacks and promises, classes and functions for the same purpose), or when someone introduces a 'local shortcut' that differs from the established convention. Covers pattern consistency and the rule that a codebase should solve each category of problem exactly one way.
npx claudepluginhub oborchers/fractional-cto --plugin pedantic-coderThis skill uses the workspace's default tool permissions.
For each category of problem in a codebase, there is one pattern. One. Not "usually" one. Not "preferably" one. Not "one, except when it is more convenient to do something else." ONE.
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
For each category of problem in a codebase, there is one pattern. One. Not "usually" one. Not "preferably" one. Not "one, except when it is more convenient to do something else." ONE.
Configuration loading? One way. Error handling? One way. Data validation? One way. HTTP requests? One way. Logging? One way. String constants? One way. The moment a second approach appears, you have two problems: the original problem and the problem of not knowing which solution to use. By the third approach, the codebase is a museum of abandoned conventions.
This is the most violated principle in software engineering. Not because developers do not understand it, but because every individual shortcut feels harmless. "It is just one place." "This module is different." "I will refactor it later." None of these excuses survive contact with a team.
Every codebase faces the same categories of cross-cutting problems. For each one, pick a pattern and enforce it everywhere. The specific pattern matters less than the fact that there is exactly one.
One settings pattern. Everywhere.
Python: Pydantic BaseSettings with .env file loading.
TypeScript: Zod schema validating process.env.
Go: Struct with envconfig tags.
Pick one. If the project uses Pydantic BaseSettings, then every module that needs a config value gets it from that settings object. No os.getenv() calls scattered in business logic. No dotenv.load() in random files. No config.yaml alongside the .env. One source of truth, one loading mechanism, one validation step.
GOOD:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
redis_url: str
stripe_api_key: str
max_retry_attempts: int = 3
model_config = {"env_file": ".env"}
settings = Settings()
BAD -- three config patterns in one project:
# settings.py -- Pydantic (pattern 1)
class Settings(BaseSettings):
database_url: str
# worker.py -- raw os.getenv (pattern 2)
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost")
# billing.py -- dotenv + os.environ (pattern 3)
load_dotenv()
STRIPE_KEY = os.environ["STRIPE_API_KEY"]
Three modules, three config patterns. Which one is "correct"? All of them. None of them. The developer reading billing.py has no way to know that the project's convention is Pydantic settings. They see os.environ and assume that is the pattern.
One error type hierarchy. One wrapping strategy. One escalation pattern.
Python: Custom exception classes inheriting from a base ServiceError, using raise ... from exc for chaining.
TypeScript: Typed error classes extending a base AppError, caught at the boundary layer.
Go: Error wrapping with fmt.Errorf("%w", err), sentinel errors for known cases.
GOOD -- Python, one exception hierarchy:
class ServiceError(Exception):
"""Base error for all service-layer exceptions."""
class NotFoundError(ServiceError):
"""Raised when a requested resource does not exist."""
class ValidationError(ServiceError):
"""Raised when input fails validation."""
class ConflictError(ServiceError):
"""Raised when an operation conflicts with current state."""
# Every service uses the same pattern:
def get_user(user_id: str) -> User:
user = repo.find_by_id(user_id)
if user is None:
raise NotFoundError(f"User {user_id} not found")
return user
BAD -- three error patterns in one project:
# users.py -- custom exceptions (pattern 1)
raise NotFoundError(f"User {user_id} not found")
# products.py -- returns None (pattern 2)
def get_product(product_id: str) -> Product | None:
return repo.find_by_id(product_id) # caller must check
# orders.py -- returns tuple (pattern 3)
def get_order(order_id: str) -> tuple[Order | None, str | None]:
order = repo.find_by_id(order_id)
if order is None:
return None, "Order not found"
return order, None
One validation library. One place where validation happens.
Python: Pydantic models at the API boundary.
TypeScript: Zod schemas at the API boundary.
Go: go-playground/validator struct tags.
GOOD -- TypeScript, Zod everywhere:
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(1).max(255),
email: z.string().email(),
role: z.enum(["admin", "member", "viewer"]),
});
const UpdateUserSchema = CreateUserSchema.partial();
const CreateProductSchema = z.object({
name: z.string().min(1).max(255),
price: z.number().positive(),
category: z.enum(["electronics", "clothing", "food"]),
});
// Every route handler: parse input with Zod, then proceed
app.post("/users", (req, res) => {
const input = CreateUserSchema.parse(req.body);
// ...
});
BAD -- three validation patterns in one project:
// users.ts -- Zod (pattern 1)
const input = CreateUserSchema.parse(req.body);
// products.ts -- manual checks (pattern 2)
if (!req.body.name || typeof req.body.name !== "string") {
return res.status(400).json({ error: "name is required" });
}
if (!req.body.price || req.body.price <= 0) {
return res.status(400).json({ error: "price must be positive" });
}
// orders.ts -- Joi (pattern 3)
const schema = Joi.object({
customer_id: Joi.string().required(),
items: Joi.array().min(1).required(),
});
const { error, value } = schema.validate(req.body);
One HTTP wrapper. One retry strategy. One timeout configuration.
// GOOD -- one HTTP client pattern
import { httpClient } from "@/lib/http";
// Every external call goes through the same client:
const user = await httpClient.get("/users/123");
const payment = await httpClient.post("/payments", { amount: 1000 });
// BAD -- three HTTP patterns in one project
import axios from "axios"; // pattern 1
const user = await axios.get("https://api.example.com/users/123");
const resp = await fetch("https://api.example.com/payments", { // pattern 2
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: 1000 }),
});
import got from "got"; // pattern 3
const order = await got("https://api.example.com/orders/456").json();
One structured logger. One format. One way to add context.
GOOD -- Go, one logger everywhere:
// All modules use the same slog pattern:
func (s *UserService) CreateUser(ctx context.Context, input CreateUserInput) (*User, error) {
slog.InfoContext(ctx, "creating user", "email", input.Email)
// ...
slog.ErrorContext(ctx, "failed to create user", "error", err)
}
func (s *ProductService) CreateProduct(ctx context.Context, input CreateProductInput) (*Product, error) {
slog.InfoContext(ctx, "creating product", "name", input.Name)
// ...
slog.ErrorContext(ctx, "failed to create product", "error", err)
}
BAD -- three logging patterns in one project:
// user_service.go -- slog (pattern 1)
slog.Info("creating user", "email", input.Email)
// product_service.go -- log (pattern 2)
log.Printf("Creating product: %s", input.Name)
// order_service.go -- fmt (pattern 3)
fmt.Fprintf(os.Stderr, "creating order for customer %s\n", input.CustomerID)
One approach to defining fixed sets of values.
Python: StrEnum for finite sets, module-level UPPER_SNAKE constants for standalone values.
TypeScript: as const objects or string literal unions. Never bare inline strings.
Go: const block with typed string constants.
GOOD -- Python, StrEnum for every finite set:
from enum import StrEnum
class OrderStatus(StrEnum):
PENDING = "pending"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class UserRole(StrEnum):
ADMIN = "admin"
MEMBER = "member"
VIEWER = "viewer"
BAD -- three constant patterns in one project:
# orders.py -- inline strings (pattern 1)
if order.status == "pending":
order.status = "processing"
# users.py -- module-level strings (pattern 2)
ROLE_ADMIN = "admin"
ROLE_MEMBER = "member"
# billing.py -- Enum (pattern 3)
class PaymentStatus(Enum):
PENDING = "pending"
COMPLETED = "completed"
One pattern for supplying defaults.
Python: Field(default=...) in Pydantic models, parameter defaults in function signatures.
TypeScript: ?? (nullish coalescing) for runtime defaults, default parameters for function signatures.
Go: Zero values by design, explicit WithOption pattern for configuration.
This is how every pattern violation begins. A developer needs a config value. The project uses Pydantic BaseSettings. But adding a field to the settings class requires updating the .env.example, the test fixtures, and the deployment config. So they write:
# "It's just one place."
db_url = os.getenv("DB_URL")
Now there are two config patterns. The next developer sees os.getenv and thinks it is acceptable. They add another. Within three months:
# settings.py
class Settings(BaseSettings):
database_url: str
redis_url: str
# worker.py
QUEUE_URL = os.getenv("QUEUE_URL")
# billing.py
load_dotenv()
STRIPE_KEY = os.environ["STRIPE_API_KEY"]
# notifications.py
import yaml
with open("config.yaml") as f:
config = yaml.safe_load(f)
SMTP_HOST = config["smtp"]["host"]
Four config patterns. One settings class that nobody trusts because it is visibly incomplete. The fix is not "add a comment saying to use Settings." The fix is to revert the shortcut, add the field to Settings, and update all three downstream files. The shortcut was cheaper for the author and more expensive for every subsequent reader.
If the codebase already has os.getenv in 15 places and Pydantic BaseSettings in 3 places, the existing dominant pattern wins -- even if the minority pattern is "better." Consistency across 18 files matters more than using the ideal tool in 3 of them.
The only exception: you refactor all 18 files in a single, focused PR. Then the new pattern wins because the old one no longer exists.
Working implementations in examples/:
examples/pattern-consistency.md -- Multi-language examples showing pattern drift vs pattern discipline for configuration, error handling, and validation in Python, TypeScript, and GoWhen reviewing code for pattern consistency:
os.getenv, process.env, or os.Environ calls outside the config moduleslog, log, and fmt.Printlnas const, or all const blocks. Never inline strings for values that belong to a finite set?? everywhere or || everywhere, not a mix