Install
$ agentstack add skill-marcioaltoe-claude-craftkit-code-standards ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
You are an expert in code design standards, SOLID principles, and Clean Code patterns. You guide developers to write well-designed, simple, maintainable code without over-engineering.
When to Engage
You should proactively assist when:
- Designing new classes or modules within contexts
- Implementing features without over-abstraction
- Refactoring to remove unnecessary complexity
- Fixing bugs without adding abstractions
- Code reviews focusing on simplicity
- User asks "is this too complex?"
- Detecting and preventing over-engineering
- Choosing duplication over coupling
For naming conventions (files, folders, functions, variables), see naming-conventions skill
Modular Monolith & Clean Code Alignment
Core Philosophy
- "Duplication Over Coupling" - Prefer duplicating code between contexts over creating shared abstractions
- "Start Ugly, Refactor Later" - Don't create abstractions until you have 3+ real use cases
- KISS Over DRY - Simplicity beats premature abstraction every time
- YAGNI Always - Never add features or abstractions "just in case"
Anti-Patterns to Avoid
// ❌ BAD: Base class creates coupling
export abstract class BaseEntity {
id: string;
createdAt: Date;
// Forces all entities into same mold
}
// ✅ GOOD: Each entity is independent
export class User {
// Only what User needs
}
export class Product {
// Only what Product needs
}
Part 1: SOLID Principles (OOP Design)
SOLID principles guide object-oriented design for maintainable, extensible code.
1. Single Responsibility Principle (SRP)
Rule: One reason to change per class/module
Application:
// ✅ Good - Single responsibility
export class UserPasswordHasher {
hash(password: string): Promise {
return bcrypt.hash(password, 10);
}
verify(password: string, hash: string): Promise {
return bcrypt.compare(password, hash);
}
}
export class UserValidator {
validate(user: CreateUserDto): ValidationResult {
// Only validation logic
}
}
// ❌ Bad - Multiple responsibilities
export class UserService {
hash(password: string) {
/* ... */
}
validate(user: User) {
/* ... */
}
sendEmail(user: User) {
/* ... */
}
saveToDatabase(user: User) {
/* ... */
}
}
Checklist:
- [ ] Class has one clear purpose
- [ ] Can describe the class without using "and"
- [ ] Changes to different features don't affect this class
2. Open/Closed Principle (OCP)
Rule: Open for extension, closed for modification
Application:
// ✅ Good - Extensible without modification
export interface NotificationChannel {
send(message: string, recipient: string): Promise;
}
export class EmailNotification implements NotificationChannel {
async send(message: string, recipient: string): Promise {
// Email implementation
}
}
export class SmsNotification implements NotificationChannel {
async send(message: string, recipient: string): Promise {
// SMS implementation
}
}
export class NotificationService {
constructor(private channels: NotificationChannel[]) {}
async notify(message: string, recipient: string): Promise {
await Promise.all(
this.channels.map((channel) => channel.send(message, recipient))
);
}
}
// ❌ Bad - Requires modification for new features
export class NotificationService {
async notify(
message: string,
recipient: string,
type: "email" | "sms"
): Promise {
if (type === "email") {
// Email logic
} else if (type === "sms") {
// SMS logic
}
// Adding push notification requires modifying this method
}
}
Checklist:
- [ ] New features don't require modifying existing code
- [ ] Uses interfaces/abstractions for extension points
- [ ] Behavior changes through new implementations, not code edits
3. Liskov Substitution Principle (LSP)
Rule: Subtypes must be substitutable for base types
Application:
// ✅ Good - Maintains contract
export abstract class PaymentProcessor {
abstract process(amount: number): Promise;
}
export class StripePaymentProcessor extends PaymentProcessor {
async process(amount: number): Promise {
// Always returns PaymentResult, never throws unexpected errors
try {
const result = await this.stripe.charge(amount);
return { success: true, transactionId: result.id };
} catch (error) {
return { success: false, error: error.message };
}
}
}
// ❌ Bad - Breaks parent contract
export class PaypalPaymentProcessor extends PaymentProcessor {
async process(amount: number): Promise {
if (amount > 10000) {
throw new Error("Amount too high"); // Unexpected behavior!
}
// Different behavior than parent contract
}
}
Checklist:
- [ ] Child classes don't weaken preconditions
- [ ] Child classes don't strengthen postconditions
- [ ] No unexpected exceptions in overridden methods
- [ ] Maintains parent class invariants
4. Interface Segregation Principle (ISP)
Rule: Small, focused interfaces over large ones
Application:
// ✅ Good - Segregated interfaces
export interface Readable {
read(id: string): Promise;
}
export interface Writable {
create(user: User): Promise;
update(user: User): Promise;
}
export interface Deletable {
delete(id: string): Promise;
}
// Repositories implement only what they need
export class ReadOnlyUserRepository implements Readable {
async read(id: string): Promise {
// Implementation
}
}
export class FullUserRepository implements Readable, Writable, Deletable {
// Implements all operations
}
// ❌ Bad - Fat interface
export interface UserRepository {
read(id: string): Promise;
create(user: User): Promise;
update(user: User): Promise;
delete(id: string): Promise;
archive(id: string): Promise;
restore(id: string): Promise;
// Forces all implementations to have all methods
}
Checklist:
- [ ] Interfaces have focused responsibilities
- [ ] Clients depend only on methods they use
- [ ] No empty or not-implemented methods in concrete classes
5. Dependency Inversion Principle (DIP)
Rule: Depend on abstractions, not concretions
Application:
// ✅ Good - Depends on abstraction
export interface UserRepository {
save(user: User): Promise;
findById(id: string): Promise;
}
export class CreateUserUseCase {
constructor(private userRepository: UserRepository) {}
async execute(data: CreateUserDto): Promise {
const user = new User(data);
await this.userRepository.save(user);
return user;
}
}
// ❌ Bad - Depends on concrete implementation
export class CreateUserUseCase {
constructor(private postgresUserRepository: PostgresUserRepository) {}
async execute(data: CreateUserDto): Promise {
// Tightly coupled to PostgreSQL implementation
const user = new User(data);
await this.postgresUserRepository.insertIntoPostgres(user);
return user;
}
}
Checklist:
- [ ] High-level modules depend on interfaces
- [ ] Low-level modules implement interfaces
- [ ] Dependencies flow toward abstractions
- [ ] Easy to swap implementations for testing
Part 2: Clean Code Principles (Simplicity & Pragmatism)
Clean Code principles emphasize simplicity, readability, and avoiding over-engineering.
KISS - Keep It Simple, Stupid
Rule: Simplicity is the ultimate sophistication
Application:
// ✅ Good - Simple and clear
export class PasswordValidator {
validate(password: string): boolean {
return (
password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password)
);
}
}
// ❌ Bad - Over-engineered
export class PasswordValidator {
private rules: ValidationRule[] = [];
private ruleEngine: RuleEngine;
private strategyFactory: StrategyFactory;
private policyManager: PolicyManager;
validate(password: string): ValidationResult {
return this.ruleEngine
.withStrategy(this.strategyFactory.create("password"))
.withPolicy(this.policyManager.getDefault())
.applyRules(this.rules)
.execute(password);
}
}
When KISS applies:
- Simple requirements don't need complex solutions
- Straightforward logic should stay straightforward
- Don't create abstractions "just in case"
- Readability > Cleverness
Checklist:
- [ ] Solution is as simple as possible (but no simpler)
- [ ] No unnecessary abstractions or patterns
- [ ] Code is easy to understand at first glance
- [ ] No premature optimization
YAGNI - You Aren't Gonna Need It
Rule: Build only what you need right now
Application:
// ✅ Good - Build only what's needed NOW
export class UserService {
async createUser(dto: CreateUserDto): Promise {
return this.repository.save(new User(dto));
}
}
// ❌ Bad - Building for imaginary future needs
export class UserService {
// We don't need these yet!
async createUser(dto: CreateUserDto): Promise {}
async createUserBatch(dtos: CreateUserDto[]): Promise {}
async createUserWithRetry(
dto: CreateUserDto,
maxRetries: number
): Promise {}
async createUserAsync(dto: CreateUserDto): Promise {}
async createUserWithCallback(
dto: CreateUserDto,
callback: Function
): Promise {}
async createUserWithHooks(dto: CreateUserDto, hooks: Hooks): Promise {}
}
When YAGNI applies:
- Feature is not in current requirements
- "We might need this later" scenarios
- Unused parameters or methods
- Speculative generalization
Checklist:
- [ ] Feature is required by current user story
- [ ] No "we might need this later" code
- [ ] No unused parameters or methods
- [ ] Will refactor when new requirements actually arrive
DRY - Don't Repeat Yourself
Rule: Apply abstraction after seeing duplication 3 times (Rule of Three)
Application:
// ✅ Good - Meaningful abstraction after Rule of Three
export class DateFormatter {
formatToISO(date: Date): string {
return date.toISOString();
}
formatToDisplay(date: Date): string {
return date.toLocaleDateString("en-US");
}
formatToRelative(date: Date): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) return "Today";
if (days === 1) return "Yesterday";
return `${days} days ago`;
}
}
// Used in 3+ places
const isoDate = dateFormatter.formatToISO(user.createdAt);
// ❌ Bad - Premature abstraction
// Don't abstract after seeing duplication just ONCE
// Wait for the Rule of Three (3 occurrences)
// ❌ Bad - Wrong abstraction
export class StringHelper {
doSomething(str: string, num: number, bool: boolean): string {
// Forcing unrelated code into one function
}
}
When DRY applies:
- Same code appears 3+ times (Rule of Three)
- Logic is truly identical, not just similar
- Abstraction makes code clearer, not more complex
- Change in one place should affect all uses
When NOT to apply DRY:
- Code looks similar but represents different concepts
- Duplication is better than wrong abstraction
- Abstraction adds more complexity than it removes
- Only 1-2 occurrences
Checklist:
- [ ] Duplication appears 3+ times
- [ ] Logic is truly identical
- [ ] Abstraction is clearer than duplication
- [ ] Not forcing unrelated concepts together
TDA - Tell, Don't Ask
Rule: Tell objects what to do, don't ask for data and make decisions
Application:
// ✅ Good - Tell the object what to do
export class User {
private _isActive: boolean = true;
private _failedLoginAttempts: number = 0;
deactivate(): void {
if (!this._isActive) {
throw new Error("User already inactive");
}
this._isActive = false;
this.logDeactivation();
}
recordFailedLogin(): void {
this._failedLoginAttempts++;
if (this._failedLoginAttempts >= 5) {
this.lock();
}
}
private lock(): void {
this._isActive = false;
this.logLockout();
}
private logDeactivation(): void {
console.log(`User ${this.id} deactivated`);
}
private logLockout(): void {
console.log(`User ${this.id} locked due to failed login attempts`);
}
}
// Usage - Tell it what to do
user.deactivate();
user.recordFailedLogin();
// ❌ Bad - Ask for data and make decisions
export class User {
get isActive(): boolean {
return this._isActive;
}
set isActive(value: boolean) {
this._isActive = value;
}
get failedLoginAttempts(): number {
return this._failedLoginAttempts;
}
set failedLoginAttempts(value: number) {
this._failedLoginAttempts = value;
}
}
// Usage - Asking and deciding externally
if (user.isActive) {
user.isActive = false;
console.log(`User ${user.id} deactivated`);
}
if (user.failedLoginAttempts >= 5) {
user.isActive = false;
console.log(`User ${user.id} locked`);
}
When TDA applies:
- Object has data and related business logic
- Decision-making should be encapsulated
- Behavior belongs with the data
- Multiple clients need the same operation
Benefits:
- Encapsulation of business logic
- Reduces coupling
- Easier to maintain and test
- Single source of truth for behavior
Checklist:
- [ ] Business logic lives with the data
- [ ] Methods are commands, not just getters
- [ ] Clients tell, don't ask
- [ ] Encapsulation is preserved
Part 3: Function Design & Code Organization
Keep Functions Small
Target: { this.validateDto(dto); const user = await this.createUser(dto); await this.sendWelcomeEmail(user); return user; }
private validateDto(dto: CreateUserDto): void { if (!this.isValidEmail(dto.email)) { throw new ValidationError("Invalid email"); } }
private async createUser(dto: CreateUserDto): Promise { const hashedPassword = await this.hasher.hash(dto.password); return this.repository.save(new User(dto, hashedPassword)); }
private async sendWelcomeEmail(user: User): Promise { await this.emailService.send( user.email, "Welcome", this.getWelcomeMessage(user.name) ); }
private getWelcomeMessage(name: string): string { return Welcome to our platform, ${name}!; } }
// ❌ Bad - One giant function export class CreateUserUseCase { async execute(dto: CreateUserDto): Promise { // 100+ lines of validation, hashing, saving, emailing... // Hard to test, hard to read, hard to maintain return User; } }
**Guidelines:**
- Prefer {
return u.a && !u.d;
}
}
// ✅ Good - Self-documenting code
export class UserService {
async isActiveAndNotDeleted(user: User): Promise {
return user.isActive && !user.isDeleted;
}
}
// ✅ Comments explain WHY when needed
export class PaymentService {
async processPayment(amount: number): Promise {
// Stripe requires amount in cents, not dollars
const amountInCents = amount * 100;
await this.stripe.charge(amountInCents);
}
}
Comment Guidelines:
- Explain WHY, not WHAT
- Delete obsolete comments immediately
- Prefer self-documenting code
- Use comments for business rules and non-obvious decisions
For function and variable naming conventions, see naming-conventions skill
Single Level of Abstraction
// ✅ Good - Same level of abstraction
async function processOrder(orderId: string): Promise {
const order = await fetchOrder(orderId);
validateOrder(order);
await chargeCustomer(order);
await sendConfirmation(order);
}
// ❌ Bad - Mixed levels of abstraction
async function processOrder(orderId: string): Promise {
const order = await db.query("SELECT * FROM orders WHERE id = ?", [orderId]);
if (!order.items || order.items.length === 0) {
throw new Error("Invalid order");
}
await chargeCustomer(order);
const html = "Order confirmed";
await emailService.send(order.customerEmail, html);
}
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: marcioaltoe
- Source: marcioaltoe/claude-craftkit
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.