feat: initial commit - opencode spec-driven development boilerplate
Converted from Claude Code boilerplate to opencode: - CLAUDE.md -> AGENTS.md (opencode instructions) - .claude/settings.json -> opencode.json (permissions schema) - .claude/agents/ -> .opencode/agent/ (subagents with mode: subagent) - .claude/commands/ -> .opencode/command/ (slash commands with ) - .claude/skills/ -> .opencode/skills/ (7 skills, removed allowed-tools) - DevContainer updated to install opencode - All .claude/ paths and Claude Code references updated
This commit is contained in:
627
.opencode/skills/development-guidelines/guides/implementation.md
Normal file
627
.opencode/skills/development-guidelines/guides/implementation.md
Normal file
@ -0,0 +1,627 @@
|
||||
# Implementation Guide
|
||||
|
||||
## TypeScript/JavaScript Conventions
|
||||
|
||||
### Type Definitions
|
||||
|
||||
**Using built-in types**:
|
||||
```typescript
|
||||
// ✅ Good: Use built-in types
|
||||
function processItems(items: string[]): Record<string, number> {
|
||||
return items.reduce((acc, item) => {
|
||||
acc[item] = (acc[item] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
}
|
||||
|
||||
// ❌ Bad: Importing from the typing module
|
||||
import { List, Dict } from 'typing';
|
||||
function processItems(items: List[str]): Dict[str, int] { }
|
||||
```
|
||||
|
||||
**Type annotation principles**:
|
||||
```typescript
|
||||
// ✅ Good: Explicit type annotations
|
||||
function calculateTotal(prices: number[]): number {
|
||||
return prices.reduce((sum, price) => sum + price, 0);
|
||||
}
|
||||
|
||||
// ❌ Bad: Over-relying on type inference
|
||||
function calculateTotal(prices) { // becomes the any type
|
||||
return prices.reduce((sum, price) => sum + price, 0);
|
||||
}
|
||||
```
|
||||
|
||||
**Interface vs. type alias**:
|
||||
```typescript
|
||||
// Interface: extensible object types
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
// Extension
|
||||
interface ExtendedTask extends Task {
|
||||
priority: string;
|
||||
}
|
||||
|
||||
// Type alias: union types, primitive types, etc.
|
||||
type TaskStatus = 'todo' | 'in_progress' | 'completed';
|
||||
type TaskId = string;
|
||||
type Nullable<T> = T | null;
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
**Variables and functions**:
|
||||
```typescript
|
||||
// Variables: camelCase, nouns
|
||||
const userName = 'John';
|
||||
const taskList = [];
|
||||
const isCompleted = true;
|
||||
|
||||
// Functions: camelCase, start with a verb
|
||||
function fetchUserData() { }
|
||||
function validateEmail(email: string) { }
|
||||
function calculateTotalPrice(items: Item[]) { }
|
||||
|
||||
// Boolean: start with is, has, should, or can
|
||||
const isValid = true;
|
||||
const hasPermission = false;
|
||||
const shouldRetry = true;
|
||||
const canDelete = false;
|
||||
```
|
||||
|
||||
**Classes and interfaces**:
|
||||
```typescript
|
||||
// Classes: PascalCase, nouns
|
||||
class TaskManager { }
|
||||
class UserAuthenticationService { }
|
||||
|
||||
// Interfaces: PascalCase
|
||||
interface TaskRepository { }
|
||||
interface UserProfile { }
|
||||
|
||||
// Type aliases: PascalCase
|
||||
type TaskStatus = 'todo' | 'in_progress' | 'completed';
|
||||
```
|
||||
|
||||
**Constants**:
|
||||
```typescript
|
||||
// UPPER_SNAKE_CASE
|
||||
const MAX_RETRY_COUNT = 3;
|
||||
const API_BASE_URL = 'https://api.example.com';
|
||||
const DEFAULT_TIMEOUT = 5000;
|
||||
|
||||
// For configuration objects
|
||||
const CONFIG = {
|
||||
maxRetryCount: 3,
|
||||
apiBaseUrl: 'https://api.example.com',
|
||||
defaultTimeout: 5000,
|
||||
} as const;
|
||||
```
|
||||
|
||||
**File names**:
|
||||
```typescript
|
||||
// Class files: PascalCase
|
||||
// TaskService.ts
|
||||
// UserRepository.ts
|
||||
|
||||
// Functions and utilities: camelCase
|
||||
// formatDate.ts
|
||||
// validateEmail.ts
|
||||
|
||||
// Components (React, etc.): PascalCase
|
||||
// TaskList.tsx
|
||||
// UserProfile.tsx
|
||||
|
||||
// Constants: kebab-case or UPPER_SNAKE_CASE
|
||||
// api-endpoints.ts
|
||||
// ERROR_MESSAGES.ts
|
||||
```
|
||||
|
||||
### Function Design
|
||||
|
||||
**Single responsibility principle**:
|
||||
```typescript
|
||||
// ✅ Good: A single responsibility
|
||||
function calculateTotalPrice(items: CartItem[]): number {
|
||||
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
||||
}
|
||||
|
||||
function formatPrice(amount: number): string {
|
||||
return `¥${amount.toLocaleString()}`;
|
||||
}
|
||||
|
||||
// ❌ Bad: Multiple responsibilities
|
||||
function calculateAndFormatPrice(items: CartItem[]): string {
|
||||
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
||||
return `¥${total.toLocaleString()}`;
|
||||
}
|
||||
```
|
||||
|
||||
**Function length**:
|
||||
- Target: within 20 lines
|
||||
- Recommended: within 50 lines
|
||||
- 100 lines or more: consider refactoring
|
||||
|
||||
**Number of parameters**:
|
||||
```typescript
|
||||
// ✅ Good: Group into an object
|
||||
interface CreateTaskOptions {
|
||||
title: string;
|
||||
description?: string;
|
||||
priority?: 'high' | 'medium' | 'low';
|
||||
dueDate?: Date;
|
||||
}
|
||||
|
||||
function createTask(options: CreateTaskOptions): Task {
|
||||
// implementation
|
||||
}
|
||||
|
||||
// ❌ Bad: Too many parameters
|
||||
function createTask(
|
||||
title: string,
|
||||
description: string,
|
||||
priority: string,
|
||||
dueDate: Date,
|
||||
tags: string[],
|
||||
assignee: string
|
||||
): Task {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
**Custom error classes**:
|
||||
```typescript
|
||||
// Defining error classes
|
||||
class ValidationError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public field: string,
|
||||
public value: unknown
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends Error {
|
||||
constructor(
|
||||
public resource: string,
|
||||
public id: string
|
||||
) {
|
||||
super(`${resource} not found: ${id}`);
|
||||
this.name = 'NotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseError extends Error {
|
||||
constructor(message: string, public cause?: Error) {
|
||||
super(message);
|
||||
this.name = 'DatabaseError';
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error handling patterns**:
|
||||
```typescript
|
||||
// ✅ Good: Proper error handling
|
||||
async function getTask(id: string): Promise<Task> {
|
||||
try {
|
||||
const task = await repository.findById(id);
|
||||
|
||||
if (!task) {
|
||||
throw new NotFoundError('Task', id);
|
||||
}
|
||||
|
||||
return task;
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
// Expected error: handle appropriately
|
||||
logger.warn(`Task not found: ${id}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Unexpected error: wrap and propagate upward
|
||||
throw new DatabaseError('Failed to retrieve the task', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Bad: Ignoring the error
|
||||
async function getTask(id: string): Promise<Task | null> {
|
||||
try {
|
||||
return await repository.findById(id);
|
||||
} catch (error) {
|
||||
return null; // error information is lost
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error messages**:
|
||||
```typescript
|
||||
// ✅ Good: Specific and suggests a solution
|
||||
throw new ValidationError(
|
||||
'Title must be 1-200 characters. Current length: 250',
|
||||
'title',
|
||||
title
|
||||
);
|
||||
|
||||
// ❌ Bad: Vague and unhelpful
|
||||
throw new Error('Invalid input');
|
||||
```
|
||||
|
||||
### Asynchronous Processing
|
||||
|
||||
**Using async/await**:
|
||||
```typescript
|
||||
// ✅ Good: async/await
|
||||
async function fetchUserTasks(userId: string): Promise<Task[]> {
|
||||
try {
|
||||
const user = await userRepository.findById(userId);
|
||||
const tasks = await taskRepository.findByUserId(user.id);
|
||||
return tasks;
|
||||
} catch (error) {
|
||||
logger.error('Failed to retrieve tasks', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Bad: Promise chains
|
||||
function fetchUserTasks(userId: string): Promise<Task[]> {
|
||||
return userRepository.findById(userId)
|
||||
.then(user => taskRepository.findByUserId(user.id))
|
||||
.then(tasks => tasks)
|
||||
.catch(error => {
|
||||
logger.error('Failed to retrieve tasks', error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Parallel processing**:
|
||||
```typescript
|
||||
// ✅ Good: Run in parallel with Promise.all
|
||||
async function fetchMultipleUsers(ids: string[]): Promise<User[]> {
|
||||
const promises = ids.map(id => userRepository.findById(id));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
// ❌ Bad: Sequential execution
|
||||
async function fetchMultipleUsers(ids: string[]): Promise<User[]> {
|
||||
const users: User[] = [];
|
||||
for (const id of ids) {
|
||||
const user = await userRepository.findById(id); // slow
|
||||
users.push(user);
|
||||
}
|
||||
return users;
|
||||
}
|
||||
```
|
||||
|
||||
## Comment Conventions
|
||||
|
||||
### Documentation Comments
|
||||
|
||||
**TSDoc format**:
|
||||
```typescript
|
||||
/**
|
||||
* Creates a task
|
||||
*
|
||||
* @param data - Data for the task to create
|
||||
* @returns The created task
|
||||
* @throws {ValidationError} When the data is invalid
|
||||
* @throws {DatabaseError} When a database error occurs
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const task = await createTask({
|
||||
* title: 'New task',
|
||||
* priority: 'high'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
async function createTask(data: CreateTaskData): Promise<Task> {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
### Inline Comments
|
||||
|
||||
**Good comments**:
|
||||
```typescript
|
||||
// ✅ Explain the reason
|
||||
// Invalidate the cache to fetch the latest data
|
||||
cache.clear();
|
||||
|
||||
// ✅ Explain complex logic
|
||||
// Compute the maximum subarray sum using Kadane's algorithm
|
||||
// Time complexity: O(n)
|
||||
let maxSoFar = arr[0];
|
||||
let maxEndingHere = arr[0];
|
||||
|
||||
// ✅ Make use of TODO and FIXME
|
||||
// TODO: Implement caching (Issue #123)
|
||||
// FIXME: Performance degrades with large datasets (Issue #456)
|
||||
// HACK: Temporary workaround, needs refactoring later
|
||||
```
|
||||
|
||||
**Bad comments**:
|
||||
```typescript
|
||||
// ❌ Merely restating the code
|
||||
// Increment i by 1
|
||||
i++;
|
||||
|
||||
// ❌ Stale information
|
||||
// This code was added in 2020 (unnecessary information)
|
||||
|
||||
// ❌ Commented-out code
|
||||
// const oldImplementation = () => { ... }; // should be deleted
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### Input Validation
|
||||
|
||||
```typescript
|
||||
// ✅ Good: Strict validation
|
||||
function validateEmail(email: string): void {
|
||||
if (!email || typeof email !== 'string') {
|
||||
throw new ValidationError('Email address is required', 'email', email);
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
throw new ValidationError('Email address format is invalid', 'email', email);
|
||||
}
|
||||
|
||||
if (email.length > 254) {
|
||||
throw new ValidationError('Email address is too long', 'email', email);
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Bad: No validation
|
||||
function validateEmail(email: string): void {
|
||||
// no validation
|
||||
}
|
||||
```
|
||||
|
||||
### Managing Sensitive Information
|
||||
|
||||
```typescript
|
||||
// ✅ Good: Load from environment variables
|
||||
import { config } from './config';
|
||||
|
||||
const apiKey = process.env.API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('The API_KEY environment variable is not set');
|
||||
}
|
||||
|
||||
// ❌ Bad: Hardcoding
|
||||
const apiKey = 'sk-1234567890abcdef'; // never do this!
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
### Choosing Data Structures
|
||||
|
||||
```typescript
|
||||
// ✅ Good: O(1) access with a Map
|
||||
const userMap = new Map(users.map(u => [u.id, u]));
|
||||
const user = userMap.get(userId); // O(1)
|
||||
|
||||
// ❌ Bad: O(n) search with an array
|
||||
const user = users.find(u => u.id === userId); // O(n)
|
||||
```
|
||||
|
||||
### Loop Optimization
|
||||
|
||||
```typescript
|
||||
// ✅ Good: Move unnecessary computation outside the loop
|
||||
const length = items.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
process(items[i]);
|
||||
}
|
||||
|
||||
// ❌ Bad: Recomputing length every iteration
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
process(items[i]);
|
||||
}
|
||||
|
||||
// ✅ Better: Use for...of
|
||||
for (const item of items) {
|
||||
process(item);
|
||||
}
|
||||
```
|
||||
|
||||
### Memoization
|
||||
|
||||
```typescript
|
||||
// Caching computation results
|
||||
const cache = new Map<string, Result>();
|
||||
|
||||
function expensiveCalculation(input: string): Result {
|
||||
if (cache.has(input)) {
|
||||
return cache.get(input)!;
|
||||
}
|
||||
|
||||
const result = /* heavy computation */;
|
||||
cache.set(input, result);
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
## Test Code
|
||||
|
||||
### Test Structure (Given-When-Then)
|
||||
|
||||
```typescript
|
||||
describe('TaskService', () => {
|
||||
describe('create', () => {
|
||||
it('can create a task with valid data', async () => {
|
||||
// Given: setup
|
||||
const service = new TaskService(mockRepository);
|
||||
const taskData = {
|
||||
title: 'Test task',
|
||||
description: 'Description for testing',
|
||||
};
|
||||
|
||||
// When: execute
|
||||
const result = await service.create(taskData);
|
||||
|
||||
// Then: verify
|
||||
expect(result).toBeDefined();
|
||||
expect(result.id).toBeDefined();
|
||||
expect(result.title).toBe('Test task');
|
||||
expect(result.description).toBe('Description for testing');
|
||||
expect(result.createdAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('throws a ValidationError when the title is empty', async () => {
|
||||
// Given: setup
|
||||
const service = new TaskService(mockRepository);
|
||||
const invalidData = { title: '' };
|
||||
|
||||
// When/Then: execute and verify
|
||||
await expect(
|
||||
service.create(invalidData)
|
||||
).rejects.toThrow(ValidationError);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Creating Mocks
|
||||
|
||||
```typescript
|
||||
// ✅ Good: Mocks based on an interface
|
||||
const mockRepository: TaskRepository = {
|
||||
save: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
// Configure behavior per test
|
||||
beforeEach(() => {
|
||||
mockRepository.findById = jest.fn((id) => {
|
||||
if (id === 'existing-id') {
|
||||
return Promise.resolve(mockTask);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Refactoring
|
||||
|
||||
### Eliminating Magic Numbers
|
||||
|
||||
```typescript
|
||||
// ✅ Good: Define constants
|
||||
const MAX_RETRY_COUNT = 3;
|
||||
const RETRY_DELAY_MS = 1000;
|
||||
|
||||
for (let i = 0; i < MAX_RETRY_COUNT; i++) {
|
||||
try {
|
||||
return await fetchData();
|
||||
} catch (error) {
|
||||
if (i < MAX_RETRY_COUNT - 1) {
|
||||
await sleep(RETRY_DELAY_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Bad: Magic numbers
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
return await fetchData();
|
||||
} catch (error) {
|
||||
if (i < 2) {
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Extracting Functions
|
||||
|
||||
```typescript
|
||||
// ✅ Good: Extract functions
|
||||
function processOrder(order: Order): void {
|
||||
validateOrder(order);
|
||||
calculateTotal(order);
|
||||
applyDiscounts(order);
|
||||
saveOrder(order);
|
||||
}
|
||||
|
||||
function validateOrder(order: Order): void {
|
||||
if (!order.items || order.items.length === 0) {
|
||||
throw new ValidationError('No items selected', 'items', order.items);
|
||||
}
|
||||
}
|
||||
|
||||
function calculateTotal(order: Order): void {
|
||||
order.total = order.items.reduce(
|
||||
(sum, item) => sum + item.price * item.quantity,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
// ❌ Bad: A long function
|
||||
function processOrder(order: Order): void {
|
||||
if (!order.items || order.items.length === 0) {
|
||||
throw new ValidationError('No items selected', 'items', order.items);
|
||||
}
|
||||
|
||||
order.total = order.items.reduce(
|
||||
(sum, item) => sum + item.price * item.quantity,
|
||||
0
|
||||
);
|
||||
|
||||
if (order.coupon) {
|
||||
order.total -= order.total * order.coupon.discountRate;
|
||||
}
|
||||
|
||||
repository.save(order);
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
Confirm before completing implementation:
|
||||
|
||||
### Code Quality
|
||||
- [ ] Naming is clear and consistent
|
||||
- [ ] Functions have a single responsibility
|
||||
- [ ] No magic numbers
|
||||
- [ ] Type annotations are written appropriately
|
||||
- [ ] Error handling is implemented
|
||||
|
||||
### Security
|
||||
- [ ] Input validation is implemented
|
||||
- [ ] No sensitive information is hardcoded
|
||||
- [ ] SQL injection countermeasures are in place
|
||||
|
||||
### Performance
|
||||
- [ ] Appropriate data structures are used
|
||||
- [ ] Unnecessary computation is avoided
|
||||
- [ ] Loops are optimized
|
||||
|
||||
### Testing
|
||||
- [ ] Unit tests are written
|
||||
- [ ] Tests pass
|
||||
- [ ] Edge cases are covered
|
||||
|
||||
### Documentation
|
||||
- [ ] Functions and classes have TSDoc comments
|
||||
- [ ] Complex logic has comments
|
||||
- [ ] TODOs and FIXMEs are noted (where applicable)
|
||||
|
||||
### Tooling
|
||||
- [ ] No lint errors
|
||||
- [ ] Type checking passes
|
||||
- [ ] Formatting is consistent
|
||||
421
.opencode/skills/development-guidelines/guides/process.md
Normal file
421
.opencode/skills/development-guidelines/guides/process.md
Normal file
@ -0,0 +1,421 @@
|
||||
# Process Guide
|
||||
|
||||
## Basic Principles
|
||||
|
||||
### 1. Include Plenty of Concrete Examples
|
||||
|
||||
Present concrete code examples, not just abstract rules.
|
||||
|
||||
**Bad example**:
|
||||
```
|
||||
Make variable names easy to understand
|
||||
```
|
||||
|
||||
**Good example**:
|
||||
```typescript
|
||||
// ✅ Good: The role is clear
|
||||
const userAuthentication = new UserAuthenticationService();
|
||||
const taskRepository = new TaskRepository();
|
||||
|
||||
// ❌ Bad: Ambiguous
|
||||
const auth = new Service();
|
||||
const repo = new Repository();
|
||||
```
|
||||
|
||||
### 2. Explain the Reasoning
|
||||
|
||||
Make clear "why we do it this way."
|
||||
|
||||
**Example**:
|
||||
```
|
||||
## Don't Ignore Errors
|
||||
|
||||
Reason: Ignoring errors makes it difficult to identify the cause of a problem.
|
||||
Handle expected errors appropriately, and propagate unexpected errors upward
|
||||
so they can be recorded in the logs.
|
||||
```
|
||||
|
||||
### 3. Set Measurable Criteria
|
||||
|
||||
Avoid vague expressions and provide concrete numbers.
|
||||
|
||||
**Bad example**:
|
||||
```
|
||||
Keep code coverage high
|
||||
```
|
||||
|
||||
**Good example**:
|
||||
```
|
||||
Code coverage targets:
|
||||
- Unit tests: 80% or higher
|
||||
- Integration tests: 60% or higher
|
||||
- E2E tests: 100% of the main flows
|
||||
```
|
||||
|
||||
## Git Operation Rules
|
||||
|
||||
### Branching Strategy (Adopting Git Flow)
|
||||
|
||||
**What is Git Flow**:
|
||||
A branching model proposed by Vincent Driessen that systematically manages feature development, releases, and hotfixes. With clearly divided roles, it enables parallel work and stable releases in team development.
|
||||
|
||||
**Branch structure**:
|
||||
```
|
||||
main (production environment)
|
||||
└── develop (development/integration environment)
|
||||
├── feature/* (new feature development)
|
||||
├── fix/* (bug fixes)
|
||||
└── release/* (release preparation) *as needed
|
||||
```
|
||||
|
||||
**Operation rules**:
|
||||
- **main**: Holds only stable code already released to production. Versioned with tags
|
||||
- **develop**: Integrates the latest development code for the next release. Automated tests run in CI
|
||||
- **feature/\*, fix/\***: Branch from develop, and merge into develop via a PR after the work is complete
|
||||
- **No direct commits**: Require PR review on all branches to ensure code quality
|
||||
- **Merge policy**: Recommend squash merge for feature→develop, and a merge commit for develop→main
|
||||
|
||||
**Benefits of Git Flow**:
|
||||
- Branch roles are clear, making parallel development by multiple people easier
|
||||
- The production environment (main) is always kept clean
|
||||
- During emergencies, hotfix branches enable rapid response (introduce as needed)
|
||||
|
||||
### Commit Message Conventions
|
||||
|
||||
**Conventional Commits is recommended**:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
**List of types**:
|
||||
```
|
||||
feat: New feature (minor version up)
|
||||
fix: Bug fix (patch version up)
|
||||
docs: Documentation
|
||||
style: Formatting (no effect on code behavior)
|
||||
refactor: Refactoring
|
||||
perf: Performance improvement
|
||||
test: Adding or fixing tests
|
||||
build: Build system
|
||||
ci: CI/CD configuration
|
||||
chore: Other (dependency updates, etc.)
|
||||
|
||||
BREAKING CHANGE: Breaking change (major version up)
|
||||
```
|
||||
|
||||
**Example of a good commit message**:
|
||||
|
||||
```
|
||||
feat(task): Add priority-setting feature
|
||||
|
||||
Users can now set a priority (high/medium/low) on tasks.
|
||||
|
||||
Implementation details:
|
||||
- Added a priority field to the Task model
|
||||
- Added a --priority option to the CLI
|
||||
- Implemented sorting by priority
|
||||
|
||||
Breaking changes:
|
||||
- The structure of the Task type has changed
|
||||
- Existing task data requires migration
|
||||
|
||||
Closes #123
|
||||
BREAKING CHANGE: Added a required priority field to the Task type
|
||||
```
|
||||
|
||||
### Pull Request Template
|
||||
|
||||
**An effective PR template**:
|
||||
|
||||
```markdown
|
||||
## Type of Change
|
||||
- [ ] New feature (feat)
|
||||
- [ ] Bug fix (fix)
|
||||
- [ ] Refactoring (refactor)
|
||||
- [ ] Documentation (docs)
|
||||
- [ ] Other (chore)
|
||||
|
||||
## Changes
|
||||
### What was changed
|
||||
[Brief description]
|
||||
|
||||
### Why it was changed
|
||||
[Background and reasoning]
|
||||
|
||||
### How it was changed
|
||||
- [Change 1]
|
||||
- [Change 2]
|
||||
|
||||
## Testing
|
||||
### Tests performed
|
||||
- [ ] Added unit tests
|
||||
- [ ] Added integration tests
|
||||
- [ ] Performed manual testing
|
||||
|
||||
### Test results
|
||||
[Description of the test results]
|
||||
|
||||
## Related Issues
|
||||
Closes #[number]
|
||||
Refs #[number]
|
||||
|
||||
## Review Points
|
||||
[Points you especially want reviewers to look at]
|
||||
```
|
||||
|
||||
## Test Strategy
|
||||
|
||||
### Test Pyramid
|
||||
|
||||
```
|
||||
/\
|
||||
/E2E\ Few (slow, high cost)
|
||||
/------\
|
||||
/ Integ. \ Medium
|
||||
/----------\
|
||||
/ Unit \ Many (fast, low cost)
|
||||
/--------------\
|
||||
```
|
||||
|
||||
**Target ratio**:
|
||||
- Unit tests: 70%
|
||||
- Integration tests: 20%
|
||||
- E2E tests: 10%
|
||||
|
||||
### How to Write Tests
|
||||
|
||||
**Given-When-Then pattern**:
|
||||
|
||||
```typescript
|
||||
describe('TaskService', () => {
|
||||
describe('task creation', () => {
|
||||
it('can create a task with valid data', async () => {
|
||||
// Given: setup
|
||||
const service = new TaskService(mockRepository);
|
||||
const validData = { title: 'Test' };
|
||||
|
||||
// When: execute
|
||||
const result = await service.create(validData);
|
||||
|
||||
// Then: verify
|
||||
expect(result.id).toBeDefined();
|
||||
expect(result.title).toBe('Test');
|
||||
});
|
||||
|
||||
it('throws a ValidationError when the title is empty', async () => {
|
||||
// Given: setup
|
||||
const service = new TaskService(mockRepository);
|
||||
const invalidData = { title: '' };
|
||||
|
||||
// When/Then: execute and verify
|
||||
await expect(
|
||||
service.create(invalidData)
|
||||
).rejects.toThrow(ValidationError);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Coverage Targets
|
||||
|
||||
**Measurable targets**:
|
||||
|
||||
```json
|
||||
// jest.config.js
|
||||
{
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"branches": 80,
|
||||
"functions": 80,
|
||||
"lines": 80,
|
||||
"statements": 80
|
||||
},
|
||||
"./src/services/": {
|
||||
"branches": 90,
|
||||
"functions": 90,
|
||||
"lines": 90,
|
||||
"statements": 90
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Reasoning**:
|
||||
- Critical business logic (services/) requires high coverage
|
||||
- The UI layer is acceptable at a lower level
|
||||
- Don't aim for 100% (balance cost and benefit)
|
||||
|
||||
## Code Review Process
|
||||
|
||||
### Purpose of Review
|
||||
|
||||
1. **Quality assurance**: Early detection of bugs
|
||||
2. **Knowledge sharing**: Understanding the codebase across the whole team
|
||||
3. **Learning opportunity**: Sharing best practices
|
||||
|
||||
### Points for Effective Reviews
|
||||
|
||||
**For reviewers**:
|
||||
|
||||
1. **Constructive feedback**
|
||||
```markdown
|
||||
## ❌ Bad example
|
||||
This code is no good.
|
||||
|
||||
## ✅ Good example
|
||||
This implementation results in O(n²) time complexity.
|
||||
Using a Map can improve it to O(n):
|
||||
|
||||
```typescript
|
||||
const taskMap = new Map(tasks.map(t => [t.id, t]));
|
||||
const result = ids.map(id => taskMap.get(id));
|
||||
```
|
||||
```
|
||||
|
||||
2. **State the priority explicitly**
|
||||
```markdown
|
||||
[Required] Security: A password is being output to the logs
|
||||
[Recommended] Performance: Avoid DB calls inside a loop
|
||||
[Suggestion] Readability: Could this function name be made clearer?
|
||||
[Question] Could you explain the intent of this processing?
|
||||
```
|
||||
|
||||
3. **Positive feedback too**
|
||||
```markdown
|
||||
✨ This implementation is easy to understand!
|
||||
👍 Edge cases are thoroughly considered
|
||||
💡 This pattern looks reusable elsewhere
|
||||
```
|
||||
|
||||
**For reviewees**:
|
||||
|
||||
1. **Perform a self-review**
|
||||
- Review your own code before creating the PR
|
||||
- Add comments where explanation is needed
|
||||
|
||||
2. **Aim for small PRs**
|
||||
- 1 PR = 1 feature
|
||||
- Number of changed files: 10 or fewer recommended
|
||||
- Number of changed lines: 300 or fewer recommended
|
||||
|
||||
3. **Explain carefully**
|
||||
- Why you chose this implementation
|
||||
- Alternatives you considered
|
||||
- Points you especially want reviewed
|
||||
|
||||
### Review Time Guidelines
|
||||
|
||||
- Small PR (100 lines or fewer): 15 minutes
|
||||
- Medium PR (100-300 lines): 30 minutes
|
||||
- Large PR (300 lines or more): 1 hour or more
|
||||
|
||||
**Principle**: Avoid large PRs; split them up
|
||||
|
||||
## Promoting Automation (Where Applicable)
|
||||
|
||||
### Automating Quality Checks
|
||||
|
||||
**Automation items and the tools adopted**:
|
||||
|
||||
1. **Lint checks**
|
||||
- **ESLint 9.x** + **@typescript-eslint**
|
||||
- Unifies coding conventions with a TypeScript-specific rule set
|
||||
- Automatically detects potential bugs and deprecated patterns
|
||||
- Configuration file: `eslint.config.js` (Flat Config format)
|
||||
|
||||
2. **Code formatting**
|
||||
- **Prettier 3.x**
|
||||
- Automatically formats code style, reducing debate during review
|
||||
- Used together with ESLint, avoiding conflicts via `eslint-config-prettier`
|
||||
- Configuration file: `.prettierrc`
|
||||
|
||||
3. **Type checking**
|
||||
- **TypeScript Compiler (tsc) 5.x**
|
||||
- Checks only type errors with `tsc --noEmit`
|
||||
- Verifies type safety independently of the build
|
||||
- Configuration file: `tsconfig.json`
|
||||
|
||||
4. **Running tests**
|
||||
- **Vitest 2.x**
|
||||
- Fast startup and execution based on Vite
|
||||
- Natively supports TypeScript/ESM and works with zero configuration
|
||||
- Coverage measurement (@vitest/coverage-v8) is built in as standard
|
||||
- Modern development experience with HMR support
|
||||
|
||||
5. **Build verification**
|
||||
- **TypeScript Compiler (tsc)**
|
||||
- Guarantees a type-checked build with the standard compiler
|
||||
- Simple configuration with no additional tools required
|
||||
- Centralizes output settings in `tsconfig.json`
|
||||
|
||||
**Implementation methods**:
|
||||
|
||||
**1. CI/CD (GitHub Actions)**
|
||||
```yaml
|
||||
# .github/workflows/ci.yml
|
||||
name: CI
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run typecheck
|
||||
- run: npm run test
|
||||
- run: npm run build
|
||||
```
|
||||
|
||||
**2. Pre-commit hooks (Husky 9.x + lint-staged)**
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"build": "tsc"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
```bash
|
||||
# .husky/pre-commit
|
||||
npm run lint-staged
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
**Effects of adoption**:
|
||||
- Automated checks run before committing, preventing defective code from being introduced
|
||||
- CI runs automatically when a PR is created, ensuring quality before merging
|
||||
- Early detection reduces fix costs by up to 80% (compared to when bugs are found after production)
|
||||
|
||||
**Why this configuration was chosen**:
|
||||
- A standard and modern configuration in the TypeScript ecosystem as of 2025
|
||||
- High compatibility between tools, with few configuration conflicts
|
||||
- An excellent balance between development experience and execution speed
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] A branching strategy is decided
|
||||
- [ ] Commit message conventions are clear
|
||||
- [ ] A PR template is prepared
|
||||
- [ ] Test types and coverage targets are set
|
||||
- [ ] A code review process is defined
|
||||
- [ ] A CI/CD pipeline is built
|
||||
Reference in New Issue
Block a user