| name | pact-coding-standards |
| description | Clean code principles, error handling patterns, and coding standards for PACT Code phase.
Use when: implementing features, refactoring code, reviewing code quality,
establishing coding conventions, or handling errors and exceptions.
Triggers on: code quality, clean code, refactoring, error handling,
logging patterns, naming conventions, code review, code phase.
|
PACT Coding Standards
Clean code principles for the Code phase of PACT. This skill provides
essential coding guidelines and links to detailed patterns for implementation.
Core Principles
1. Single Responsibility Principle
Each function, class, or module should have exactly one reason to change.
class UserManager {
createUser(data) { }
validateEmail(email) { }
sendWelcomeEmail(user) { }
generateReport() { }
updateUserPreferences(userId, prefs) { }
}
class UserService {
createUser(data) { }
updateUser(userId, data) { }
}
class EmailValidator {
validate(email) { }
}
class NotificationService {
sendWelcomeEmail(user) { }
}
class UserReportGenerator {
generate(criteria) { }
}
2. DRY (Don't Repeat Yourself)
Extract duplicated logic into reusable functions.
function createUser(data) {
if (!data.email || !data.email.includes('@')) {
throw new Error('Invalid email');
}
}
function updateUser(id, data) {
if (data.email && !data.email.includes('@')) {
throw new Error('Invalid email');
}
}
function validateEmail(email) {
if (!email || !email.includes('@')) {
throw new ValidationError('Invalid email format');
}
}
function createUser(data) {
validateEmail(data.email);
}
function updateUser(id, data) {
if (data.email) {
validateEmail(data.email);
}
}
3. KISS (Keep It Simple, Stupid)
Choose the simplest solution that works.
class ConfigurationFactoryBuilderManager {
static getInstance() {
return new ConfigurationFactoryBuilder()
.withDefaults()
.withEnvironment()
.build()
.getConfiguration();
}
}
const config = {
port: process.env.PORT || 3000,
dbUrl: process.env.DATABASE_URL,
debug: process.env.NODE_ENV !== 'production'
};
4. Defensive Programming
Validate inputs, handle edge cases, and fail gracefully.
function processOrder(order) {
if (!order) {
throw new ValidationError('Order is required');
}
if (!order.items || order.items.length === 0) {
throw new ValidationError('Order must have at least one item');
}
if (order.total < 0) {
throw new ValidationError('Order total cannot be negative');
}
const customerEmail = order.customer?.email ?? 'no-email@placeholder.com';
return {
id: generateOrderId(),
items: order.items.map(item => ({
...item,
price: Math.max(0, item.price)
})),
total: order.total,
customerEmail
};
}
Naming Conventions
Functions
function getUser(id) { }
function createOrder(data) { }
function updateProfile(id, data) { }
function deleteComment(id) { }
function validateEmail(email) { }
function calculateTotal(items) { }
function formatDate(date) { }
function isActive(user) { }
function hasPermission(user, action) { }
function canEdit(user, resource) { }
Variables
const userCount = 42;
const activeUsers = users.filter();
const maxRetryAttempts = 3;
const isActive = true;
const hasPermission = false;
const canEdit = user.role === 'admin';
const shouldRetry = attempts < maxRetryAttempts;
const users = [];
const orderItems = [];
const userById = {};
const priceByProductId = {};
Constants
const MAX_RETRY_ATTEMPTS = 3;
const DEFAULT_PAGE_SIZE = 20;
const API_BASE_URL = 'https://api.example.com';
const CONFIG = Object.freeze({
database: {
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT, 10)
}
});
Classes
class UserRepository { }
class OrderService { }
class EmailValidator { }
class PaymentGateway { }
interface Cacheable { }
interface UserRepository { }
Error Handling
Fail Fast, Recover Gracefully
async function createUser(data) {
if (!data.email) {
throw new ValidationError('Email is required');
}
if (!isValidEmail(data.email)) {
throw new ValidationError('Invalid email format');
}
const existing = await userRepo.findByEmail(data.email);
if (existing) {
throw new ConflictError('Email already registered');
}
try {
const user = await userRepo.save(data);
await emailService.sendWelcome(user.email);
return user;
} catch (error) {
if (error instanceof DatabaseError) {
logger.error('Database error creating user', { error, data });
throw new ServiceError('Unable to create user, please try again');
}
throw error;
}
}
Custom Error Classes
class AppError extends Error {
constructor(message, code, statusCode = 500) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.statusCode = statusCode;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message, details = []) {
super(message, 'VALIDATION_ERROR', 400);
this.details = details;
}
}
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} with id ${id} not found`, 'NOT_FOUND', 404);
this.resource = resource;
this.resourceId = id;
}
}
class ConflictError extends AppError {
constructor(message) {
super(message, 'CONFLICT', 409);
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Authentication required') {
super(message, 'UNAUTHORIZED', 401);
}
}
For comprehensive error patterns: See references/error-handling-patterns.md
Logging
Log Levels
| Level | When to Use | Example |
|---|
| ERROR | Failures requiring attention | Database connection failed |
| WARN | Potentially harmful situations | Rate limit approaching |
| INFO | Normal operational events | User created, order placed |
| DEBUG | Detailed debugging info | Function parameters, intermediate values |
Structured Logging
const logger = require('./logger');
console.log('User created: ' + user.id);
console.log('Error: ' + error.message);
logger.info('User created', {
userId: user.id,
email: user.email,
source: 'signup'
});
logger.error('Failed to process payment', {
orderId: order.id,
amount: order.total,
error: error.message,
errorCode: error.code,
stack: error.stack
});
app.use((req, res, next) => {
const requestId = req.headers['x-request-id'] || uuidv4();
req.requestId = requestId;
logger.info('Request received', {
requestId,
method: req.method,
path: req.path,
userAgent: req.headers['user-agent'],
ip: req.ip
});
res.on('finish', () => {
logger.info('Request completed', {
requestId,
statusCode: res.statusCode,
duration: Date.now() - req.startTime
});
});
next();
});
Code Organization
File Size Guidelines
- Maximum file size: 500 lines
- Maximum function size: 50 lines
- Maximum line length: 100 characters
Module Structure
const express = require('express');
const { validate } = require('class-validator');
const { UserService } = require('../services/UserService');
const { logger } = require('../utils/logger');
const MAX_PAGE_SIZE = 100;
const DEFAULT_PAGE_SIZE = 20;
class UserController {
constructor(userService) {
this.userService = userService;
}
async getUsers(req, res, next) {
}
}
function validatePagination(page, limit) {
}
module.exports = { UserController };
Interpreting design-doc optionals
When a design doc spec'd an OPTIONAL cross-link sentence, closing paragraph, or sister-section reference, the CODE author MUST evaluate the optional content's logical relationship to the preceding paragraph before deciding how to render it.
- Strengthening clause — the optional content reinforces, qualifies, or extends the directive in the preceding paragraph (e.g., a cross-link that tells the reader where the next degree of detail lives). FOLD it inline into the preceding paragraph for tighter prose flow.
- Discursive observation — the optional content introduces a new framing, raises a separate point, or pivots to an adjacent concern. KEEP it as a standalone paragraph.
The judgment lives at write time: read the preceding paragraph and ask "does this optional content add to that paragraph's directive, or does it start a new one?" Strengthening = fold; discursive = keep.
Detection signature
Design-doc phrasings like "CODE may include or drop", "optional closing cross-link", or "optional discursive paragraph" mark the entry condition. Apply the strengthening-vs-discursive judgment before composing the section; do not default to either form mechanically.
Worked example — fold-in cross-link
In pact-plugin/skills/pact-testing-strategies/SKILL.md, the section "Sibling-file convention for parametrized noise-budget regression" closes its canonical-mitigation paragraph with an inline cross-reference to the sister "Author-blindness in HANDOFF arithmetic" section rather than a standalone closing paragraph. The cross-link functions as a strengthening clause on the elevated-priority directive that precedes it — telling the reader where the pair-with-cross-stream-verifier rule lives — so it folds into the same paragraph instead of opening a new one. The fold preserves the cross-reference content while keeping the canonical mitigation a single tight directive.
Code Quality Checklist
Before completing CODE phase:
Structure
Naming
Error Handling
Documentation
Quality
Scripts
Lint Check Script
A helper script is available at scripts/lint-check.sh to run project linters.
chmod +x scripts/lint-check.sh
./scripts/lint-check.sh
Import Hygiene Check (--files mode)
Behavioral tests cannot see an unused import — a dead import os passes
every test green. The import-hygiene check closes that blind spot
mechanically. Run it over EXACTLY the .py files you modified before
running the test suite:
bash <skill-directory>/scripts/lint-check.sh --files 'path/to/modified_a.py' 'path/to/modified_b.py'
Single-quote each filename — a filename is untrusted shell input.
(When your dispatch names a plugin root, the script lives at
<plugin-root>/skills/pact-coding-standards/scripts/lint-check.sh.)
Verdict-line contract — the LAST stdout line is always exactly one of:
| Verdict | Meaning | Exit code |
|---|
IMPORT-HYGIENE: PASS | No findings in the checked files | 0 |
IMPORT-HYGIENE: FINDINGS (n) | n findings printed above the verdict | 1 |
IMPORT-HYGIENE: SKIPPED (<reason>) | Check could not run (no Python files given, or no usable checker) | 0 |
Record the verdict line verbatim in your HANDOFF produced field — every
run ends in a verdict, and a SKIPPED must be visible, never silent.
What to do with findings:
- Fix every finding in files you modified: delete the unused import, or fix
the undefined name.
- Never lint-fix files you did not touch — the check's scope is your diff.
- A deliberate side-effect import or re-export keeps a reasoned suppression
at the import site:
# noqa: F401 # re-export: <why>. Never a bare
# noqa without a reason.
- If the script is missing or errors, state that in your HANDOFF instead of
skipping silently.
How it checks (internal — the coder-facing contract is just the command
and the verdict): an execution-probed ladder tries ruff, then pyflakes, then
flake8, each restricted to the unused-import/undefined-name classes only
(--select F401,F821) so you are never asked to fix style noise; when no
linter is installed it falls back to the stdlib AST checker
scripts/check_unused_imports.py (unused imports only, try/except-scoped
imports treated as advisory). A checker crash degrades to SKIPPED — the
check fails open and never blocks you on its own breakage.
Detailed References
For comprehensive coding guidance: