feat(ecc): prune plugin 43→12 items, promote 7 rules to .claude/rules/ (#245)

ECC community plugin pruning: removed 530+ non-essential files
(.cursor/, .opencode/, docs/ja-JP, docs/zh-CN, docs/zh-TW,
language-specific skills/agents/rules). Retained 4 agents,
3 commands, 5 skills. Promoted 13 rule files (8 common + 5
typescript) to .claude/rules/ for CC native loading. Extracted
reusable patterns to EXTRACTED-PATTERNS.md.
This commit is contained in:
park-kyungchan
2026-02-20 15:34:51 +09:00
committed by GitHub
parent 24047351c2
commit 1bd68ff534
536 changed files with 253 additions and 111479 deletions

View File

@@ -1,105 +0,0 @@
# スキル
スキルは Claude Code が文脈に基づいて読み込む知識モジュールです。ワークフロー定義とドメイン知識を含みます。
## スキルカテゴリ
### 言語別パターン
- `python-patterns/` - Python 設計パターン
- `golang-patterns/` - Go 設計パターン
- `frontend-patterns/` - React/Next.js パターン
- `backend-patterns/` - API とデータベースパターン
### 言語別テスト
- `python-testing/` - Python テスト戦略
- `golang-testing/` - Go テスト戦略
- `cpp-testing/` - C++ テスト
### フレームワーク
- `django-patterns/` - Django ベストプラクティス
- `django-tdd/` - Django テスト駆動開発
- `django-security/` - Django セキュリティ
- `springboot-patterns/` - Spring Boot パターン
- `springboot-tdd/` - Spring Boot テスト
- `springboot-security/` - Spring Boot セキュリティ
### データベース
- `postgres-patterns/` - PostgreSQL パターン
- `jpa-patterns/` - JPA/Hibernate パターン
### セキュリティ
- `security-review/` - セキュリティチェックリスト
- `security-scan/` - セキュリティスキャン
### ワークフロー
- `tdd-workflow/` - テスト駆動開発ワークフロー
- `continuous-learning/` - 継続的学習
### ドメイン特定
- `eval-harness/` - 評価ハーネス
- `iterative-retrieval/` - 反復的検索
## スキル構造
各スキルは自分のディレクトリに SKILL.md ファイルを含みます:
```
skills/
├── python-patterns/
│ └── SKILL.md # 実装パターン、例、ベストプラクティス
├── golang-testing/
│ └── SKILL.md
├── django-patterns/
│ └── SKILL.md
...
```
## スキルを使用します
Claude Code はコンテキストに基づいてスキルを自動的に読み込みます。例:
- Python ファイルを編集している場合 → `python-patterns``python-testing` が読み込まれる
- Django プロジェクトの場合 → `django-*` スキルが読み込まれる
- テスト駆動開発をしている場合 → `tdd-workflow` が読み込まれる
## スキルの作成
新しいスキルを作成するには:
1. `skills/your-skill-name/` ディレクトリを作成
2. `SKILL.md` ファイルを追加
3. テンプレート:
```markdown
---
name: your-skill-name
description: Brief description shown in skill list
---
# Your Skill Title
Brief overview.
## Core Concepts
Key patterns and guidelines.
## Code Examples
\`\`\`language
// Practical, tested examples
\`\`\`
## Best Practices
- Actionable guideline 1
- Actionable guideline 2
## When to Use
Describe scenarios where this skill applies.
```
---
**覚えておいてください**:スキルは参照資料です。実装ガイダンスを提供し、ベストプラクティスを示します。スキルとルールを一緒に使用して、高品質なコードを確認してください。

View File

@@ -1,587 +0,0 @@
---
name: backend-patterns
description: Backend architecture patterns, API design, database optimization, and server-side best practices for Node.js, Express, and Next.js API routes.
---
# バックエンド開発パターン
スケーラブルなサーバーサイドアプリケーションのためのバックエンドアーキテクチャパターンとベストプラクティス。
## API設計パターン
### RESTful API構造
```typescript
// ✅ リソースベースのURL
GET /api/markets #
GET /api/markets/:id #
POST /api/markets #
PUT /api/markets/:id #
PATCH /api/markets/:id #
DELETE /api/markets/:id #
// ✅ フィルタリング、ソート、ページネーション用のクエリパラメータ
GET /api/markets?status=active&sort=volume&limit=20&offset=0
```
### リポジトリパターン
```typescript
// データアクセスロジックの抽象化
interface MarketRepository {
findAll(filters?: MarketFilters): Promise<Market[]>
findById(id: string): Promise<Market | null>
create(data: CreateMarketDto): Promise<Market>
update(id: string, data: UpdateMarketDto): Promise<Market>
delete(id: string): Promise<void>
}
class SupabaseMarketRepository implements MarketRepository {
async findAll(filters?: MarketFilters): Promise<Market[]> {
let query = supabase.from('markets').select('*')
if (filters?.status) {
query = query.eq('status', filters.status)
}
if (filters?.limit) {
query = query.limit(filters.limit)
}
const { data, error } = await query
if (error) throw new Error(error.message)
return data
}
// その他のメソッド...
}
```
### サービスレイヤーパターン
```typescript
// ビジネスロジックをデータアクセスから分離
class MarketService {
constructor(private marketRepo: MarketRepository) {}
async searchMarkets(query: string, limit: number = 10): Promise<Market[]> {
// ビジネスロジック
const embedding = await generateEmbedding(query)
const results = await this.vectorSearch(embedding, limit)
// 完全なデータを取得
const markets = await this.marketRepo.findByIds(results.map(r => r.id))
// 類似度でソート
return markets.sort((a, b) => {
const scoreA = results.find(r => r.id === a.id)?.score || 0
const scoreB = results.find(r => r.id === b.id)?.score || 0
return scoreA - scoreB
})
}
private async vectorSearch(embedding: number[], limit: number) {
// ベクトル検索の実装
}
}
```
### ミドルウェアパターン
```typescript
// リクエスト/レスポンス処理パイプライン
export function withAuth(handler: NextApiHandler): NextApiHandler {
return async (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ error: 'Unauthorized' })
}
try {
const user = await verifyToken(token)
req.user = user
return handler(req, res)
} catch (error) {
return res.status(401).json({ error: 'Invalid token' })
}
}
}
// 使用方法
export default withAuth(async (req, res) => {
// ハンドラーはreq.userにアクセス可能
})
```
## データベースパターン
### クエリ最適化
```typescript
// ✅ 良い: 必要な列のみを選択
const { data } = await supabase
.from('markets')
.select('id, name, status, volume')
.eq('status', 'active')
.order('volume', { ascending: false })
.limit(10)
// ❌ 悪い: すべてを選択
const { data } = await supabase
.from('markets')
.select('*')
```
### N+1クエリ防止
```typescript
// ❌ 悪い: N+1クエリ問題
const markets = await getMarkets()
for (const market of markets) {
market.creator = await getUser(market.creator_id) // Nクエリ
}
// ✅ 良い: バッチフェッチ
const markets = await getMarkets()
const creatorIds = markets.map(m => m.creator_id)
const creators = await getUsers(creatorIds) // 1クエリ
const creatorMap = new Map(creators.map(c => [c.id, c]))
markets.forEach(market => {
market.creator = creatorMap.get(market.creator_id)
})
```
### トランザクションパターン
```typescript
async function createMarketWithPosition(
marketData: CreateMarketDto,
positionData: CreatePositionDto
) {
// Supabaseトランザクションを使用
const { data, error } = await supabase.rpc('create_market_with_position', {
market_data: marketData,
position_data: positionData
})
if (error) throw new Error('Transaction failed')
return data
}
// SupabaseのSQL関数
CREATE OR REPLACE FUNCTION create_market_with_position(
market_data jsonb,
position_data jsonb
)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
BEGIN
--
INSERT INTO markets VALUES (market_data);
INSERT INTO positions VALUES (position_data);
RETURN jsonb_build_object('success', true);
EXCEPTION
WHEN OTHERS THEN
--
RETURN jsonb_build_object('success', false, 'error', SQLERRM);
END;
$$;
```
## キャッシング戦略
### Redisキャッシングレイヤー
```typescript
class CachedMarketRepository implements MarketRepository {
constructor(
private baseRepo: MarketRepository,
private redis: RedisClient
) {}
async findById(id: string): Promise<Market | null> {
// 最初にキャッシュをチェック
const cached = await this.redis.get(`market:${id}`)
if (cached) {
return JSON.parse(cached)
}
// キャッシュミス - データベースから取得
const market = await this.baseRepo.findById(id)
if (market) {
// 5分間キャッシュ
await this.redis.setex(`market:${id}`, 300, JSON.stringify(market))
}
return market
}
async invalidateCache(id: string): Promise<void> {
await this.redis.del(`market:${id}`)
}
}
```
### Cache-Asideパターン
```typescript
async function getMarketWithCache(id: string): Promise<Market> {
const cacheKey = `market:${id}`
// キャッシュを試す
const cached = await redis.get(cacheKey)
if (cached) return JSON.parse(cached)
// キャッシュミス - DBから取得
const market = await db.markets.findUnique({ where: { id } })
if (!market) throw new Error('Market not found')
// キャッシュを更新
await redis.setex(cacheKey, 300, JSON.stringify(market))
return market
}
```
## エラーハンドリングパターン
### 集中エラーハンドラー
```typescript
class ApiError extends Error {
constructor(
public statusCode: number,
public message: string,
public isOperational = true
) {
super(message)
Object.setPrototypeOf(this, ApiError.prototype)
}
}
export function errorHandler(error: unknown, req: Request): Response {
if (error instanceof ApiError) {
return NextResponse.json({
success: false,
error: error.message
}, { status: error.statusCode })
}
if (error instanceof z.ZodError) {
return NextResponse.json({
success: false,
error: 'Validation failed',
details: error.errors
}, { status: 400 })
}
// 予期しないエラーをログに記録
console.error('Unexpected error:', error)
return NextResponse.json({
success: false,
error: 'Internal server error'
}, { status: 500 })
}
// 使用方法
export async function GET(request: Request) {
try {
const data = await fetchData()
return NextResponse.json({ success: true, data })
} catch (error) {
return errorHandler(error, request)
}
}
```
### 指数バックオフによるリトライ
```typescript
async function fetchWithRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
let lastError: Error
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (error) {
lastError = error as Error
if (i < maxRetries - 1) {
// 指数バックオフ: 1秒、2秒、4秒
const delay = Math.pow(2, i) * 1000
await new Promise(resolve => setTimeout(resolve, delay))
}
}
}
throw lastError!
}
// 使用方法
const data = await fetchWithRetry(() => fetchFromAPI())
```
## 認証と認可
### JWTトークン検証
```typescript
import jwt from 'jsonwebtoken'
interface JWTPayload {
userId: string
email: string
role: 'admin' | 'user'
}
export function verifyToken(token: string): JWTPayload {
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload
return payload
} catch (error) {
throw new ApiError(401, 'Invalid token')
}
}
export async function requireAuth(request: Request) {
const token = request.headers.get('authorization')?.replace('Bearer ', '')
if (!token) {
throw new ApiError(401, 'Missing authorization token')
}
return verifyToken(token)
}
// APIルートでの使用方法
export async function GET(request: Request) {
const user = await requireAuth(request)
const data = await getDataForUser(user.userId)
return NextResponse.json({ success: true, data })
}
```
### ロールベースアクセス制御
```typescript
type Permission = 'read' | 'write' | 'delete' | 'admin'
interface User {
id: string
role: 'admin' | 'moderator' | 'user'
}
const rolePermissions: Record<User['role'], Permission[]> = {
admin: ['read', 'write', 'delete', 'admin'],
moderator: ['read', 'write', 'delete'],
user: ['read', 'write']
}
export function hasPermission(user: User, permission: Permission): boolean {
return rolePermissions[user.role].includes(permission)
}
export function requirePermission(permission: Permission) {
return (handler: (request: Request, user: User) => Promise<Response>) => {
return async (request: Request) => {
const user = await requireAuth(request)
if (!hasPermission(user, permission)) {
throw new ApiError(403, 'Insufficient permissions')
}
return handler(request, user)
}
}
}
// 使用方法 - HOFがハンドラーをラップ
export const DELETE = requirePermission('delete')(
async (request: Request, user: User) => {
// ハンドラーは検証済みの権限を持つ認証済みユーザーを受け取る
return new Response('Deleted', { status: 200 })
}
)
```
## レート制限
### シンプルなインメモリレートリミッター
```typescript
class RateLimiter {
private requests = new Map<string, number[]>()
async checkLimit(
identifier: string,
maxRequests: number,
windowMs: number
): Promise<boolean> {
const now = Date.now()
const requests = this.requests.get(identifier) || []
// ウィンドウ外の古いリクエストを削除
const recentRequests = requests.filter(time => now - time < windowMs)
if (recentRequests.length >= maxRequests) {
return false // レート制限超過
}
// 現在のリクエストを追加
recentRequests.push(now)
this.requests.set(identifier, recentRequests)
return true
}
}
const limiter = new RateLimiter()
export async function GET(request: Request) {
const ip = request.headers.get('x-forwarded-for') || 'unknown'
const allowed = await limiter.checkLimit(ip, 100, 60000) // 100 req/分
if (!allowed) {
return NextResponse.json({
error: 'Rate limit exceeded'
}, { status: 429 })
}
// リクエストを続行
}
```
## バックグラウンドジョブとキュー
### シンプルなキューパターン
```typescript
class JobQueue<T> {
private queue: T[] = []
private processing = false
async add(job: T): Promise<void> {
this.queue.push(job)
if (!this.processing) {
this.process()
}
}
private async process(): Promise<void> {
this.processing = true
while (this.queue.length > 0) {
const job = this.queue.shift()!
try {
await this.execute(job)
} catch (error) {
console.error('Job failed:', error)
}
}
this.processing = false
}
private async execute(job: T): Promise<void> {
// ジョブ実行ロジック
}
}
// マーケットインデックス作成用の使用方法
interface IndexJob {
marketId: string
}
const indexQueue = new JobQueue<IndexJob>()
export async function POST(request: Request) {
const { marketId } = await request.json()
// ブロッキングの代わりにキューに追加
await indexQueue.add({ marketId })
return NextResponse.json({ success: true, message: 'Job queued' })
}
```
## ロギングとモニタリング
### 構造化ロギング
```typescript
interface LogContext {
userId?: string
requestId?: string
method?: string
path?: string
[key: string]: unknown
}
class Logger {
log(level: 'info' | 'warn' | 'error', message: string, context?: LogContext) {
const entry = {
timestamp: new Date().toISOString(),
level,
message,
...context
}
console.log(JSON.stringify(entry))
}
info(message: string, context?: LogContext) {
this.log('info', message, context)
}
warn(message: string, context?: LogContext) {
this.log('warn', message, context)
}
error(message: string, error: Error, context?: LogContext) {
this.log('error', message, {
...context,
error: error.message,
stack: error.stack
})
}
}
const logger = new Logger()
// 使用方法
export async function GET(request: Request) {
const requestId = crypto.randomUUID()
logger.info('Fetching markets', {
requestId,
method: 'GET',
path: '/api/markets'
})
try {
const markets = await fetchMarkets()
return NextResponse.json({ success: true, data: markets })
} catch (error) {
logger.error('Failed to fetch markets', error as Error, { requestId })
return NextResponse.json({ error: 'Internal error' }, { status: 500 })
}
}
```
**注意**: バックエンドパターンは、スケーラブルで保守可能なサーバーサイドアプリケーションを実現します。複雑さのレベルに適したパターンを選択してください。

View File

@@ -1,429 +0,0 @@
---
name: clickhouse-io
description: ClickHouse database patterns, query optimization, analytics, and data engineering best practices for high-performance analytical workloads.
---
# ClickHouse 分析パターン
高性能分析とデータエンジニアリングのためのClickHouse固有のパターン。
## 概要
ClickHouseは、オンライン分析処理OLAP用のカラム指向データベース管理システムDBMSです。大規模データセットに対する高速分析クエリに最適化されています。
**主な機能:**
- カラム指向ストレージ
- データ圧縮
- 並列クエリ実行
- 分散クエリ
- リアルタイム分析
## テーブル設計パターン
### MergeTreeエンジン最も一般的
```sql
CREATE TABLE markets_analytics (
date Date,
market_id String,
market_name String,
volume UInt64,
trades UInt32,
unique_traders UInt32,
avg_trade_size Float64,
created_at DateTime
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(date)
ORDER BY (date, market_id)
SETTINGS index_granularity = 8192;
```
### ReplacingMergeTree重複排除
```sql
-- 重複がある可能性のあるデータ(複数のソースからなど)用
CREATE TABLE user_events (
event_id String,
user_id String,
event_type String,
timestamp DateTime,
properties String
) ENGINE = ReplacingMergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (user_id, event_id, timestamp)
PRIMARY KEY (user_id, event_id);
```
### AggregatingMergeTree事前集計
```sql
-- 集計メトリクスの維持用
CREATE TABLE market_stats_hourly (
hour DateTime,
market_id String,
total_volume AggregateFunction(sum, UInt64),
total_trades AggregateFunction(count, UInt32),
unique_users AggregateFunction(uniq, String)
) ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (hour, market_id);
-- 集計データのクエリ
SELECT
hour,
market_id,
sumMerge(total_volume) AS volume,
countMerge(total_trades) AS trades,
uniqMerge(unique_users) AS users
FROM market_stats_hourly
WHERE hour >= toStartOfHour(now() - INTERVAL 24 HOUR)
GROUP BY hour, market_id
ORDER BY hour DESC;
```
## クエリ最適化パターン
### 効率的なフィルタリング
```sql
-- ✅ 良い: インデックス列を最初に使用
SELECT *
FROM markets_analytics
WHERE date >= '2025-01-01'
AND market_id = 'market-123'
AND volume > 1000
ORDER BY date DESC
LIMIT 100;
-- ❌ 悪い: インデックスのない列を最初にフィルタリング
SELECT *
FROM markets_analytics
WHERE volume > 1000
AND market_name LIKE '%election%'
AND date >= '2025-01-01';
```
### 集計
```sql
-- ✅ 良い: ClickHouse固有の集計関数を使用
SELECT
toStartOfDay(created_at) AS day,
market_id,
sum(volume) AS total_volume,
count() AS total_trades,
uniq(trader_id) AS unique_traders,
avg(trade_size) AS avg_size
FROM trades
WHERE created_at >= today() - INTERVAL 7 DAY
GROUP BY day, market_id
ORDER BY day DESC, total_volume DESC;
-- ✅ パーセンタイルにはquantileを使用percentileより効率的
SELECT
quantile(0.50)(trade_size) AS median,
quantile(0.95)(trade_size) AS p95,
quantile(0.99)(trade_size) AS p99
FROM trades
WHERE created_at >= now() - INTERVAL 1 HOUR;
```
### ウィンドウ関数
```sql
-- 累計計算
SELECT
date,
market_id,
volume,
sum(volume) OVER (
PARTITION BY market_id
ORDER BY date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_volume
FROM markets_analytics
WHERE date >= today() - INTERVAL 30 DAY
ORDER BY market_id, date;
```
## データ挿入パターン
### 一括挿入(推奨)
```typescript
import { ClickHouse } from 'clickhouse'
const clickhouse = new ClickHouse({
url: process.env.CLICKHOUSE_URL,
port: 8123,
basicAuth: {
username: process.env.CLICKHOUSE_USER,
password: process.env.CLICKHOUSE_PASSWORD
}
})
// ✅ バッチ挿入(効率的)
async function bulkInsertTrades(trades: Trade[]) {
const values = trades.map(trade => `(
'${trade.id}',
'${trade.market_id}',
'${trade.user_id}',
${trade.amount},
'${trade.timestamp.toISOString()}'
)`).join(',')
await clickhouse.query(`
INSERT INTO trades (id, market_id, user_id, amount, timestamp)
VALUES ${values}
`).toPromise()
}
// ❌ 個別挿入(低速)
async function insertTrade(trade: Trade) {
// ループ内でこれをしないでください!
await clickhouse.query(`
INSERT INTO trades VALUES ('${trade.id}', ...)
`).toPromise()
}
```
### ストリーミング挿入
```typescript
// 継続的なデータ取り込み用
import { createWriteStream } from 'fs'
import { pipeline } from 'stream/promises'
async function streamInserts() {
const stream = clickhouse.insert('trades').stream()
for await (const batch of dataSource) {
stream.write(batch)
}
await stream.end()
}
```
## マテリアライズドビュー
### リアルタイム集計
```sql
-- 時間別統計のマテリアライズドビューを作成
CREATE MATERIALIZED VIEW market_stats_hourly_mv
TO market_stats_hourly
AS SELECT
toStartOfHour(timestamp) AS hour,
market_id,
sumState(amount) AS total_volume,
countState() AS total_trades,
uniqState(user_id) AS unique_users
FROM trades
GROUP BY hour, market_id;
-- マテリアライズドビューのクエリ
SELECT
hour,
market_id,
sumMerge(total_volume) AS volume,
countMerge(total_trades) AS trades,
uniqMerge(unique_users) AS users
FROM market_stats_hourly
WHERE hour >= now() - INTERVAL 24 HOUR
GROUP BY hour, market_id;
```
## パフォーマンスモニタリング
### クエリパフォーマンス
```sql
-- 低速クエリをチェック
SELECT
query_id,
user,
query,
query_duration_ms,
read_rows,
read_bytes,
memory_usage
FROM system.query_log
WHERE type = 'QueryFinish'
AND query_duration_ms > 1000
AND event_time >= now() - INTERVAL 1 HOUR
ORDER BY query_duration_ms DESC
LIMIT 10;
```
### テーブル統計
```sql
-- テーブルサイズをチェック
SELECT
database,
table,
formatReadableSize(sum(bytes)) AS size,
sum(rows) AS rows,
max(modification_time) AS latest_modification
FROM system.parts
WHERE active
GROUP BY database, table
ORDER BY sum(bytes) DESC;
```
## 一般的な分析クエリ
### 時系列分析
```sql
-- 日次アクティブユーザー
SELECT
toDate(timestamp) AS date,
uniq(user_id) AS daily_active_users
FROM events
WHERE timestamp >= today() - INTERVAL 30 DAY
GROUP BY date
ORDER BY date;
-- リテンション分析
SELECT
signup_date,
countIf(days_since_signup = 0) AS day_0,
countIf(days_since_signup = 1) AS day_1,
countIf(days_since_signup = 7) AS day_7,
countIf(days_since_signup = 30) AS day_30
FROM (
SELECT
user_id,
min(toDate(timestamp)) AS signup_date,
toDate(timestamp) AS activity_date,
dateDiff('day', signup_date, activity_date) AS days_since_signup
FROM events
GROUP BY user_id, activity_date
)
GROUP BY signup_date
ORDER BY signup_date DESC;
```
### ファネル分析
```sql
-- コンバージョンファネル
SELECT
countIf(step = 'viewed_market') AS viewed,
countIf(step = 'clicked_trade') AS clicked,
countIf(step = 'completed_trade') AS completed,
round(clicked / viewed * 100, 2) AS view_to_click_rate,
round(completed / clicked * 100, 2) AS click_to_completion_rate
FROM (
SELECT
user_id,
session_id,
event_type AS step
FROM events
WHERE event_date = today()
)
GROUP BY session_id;
```
### コホート分析
```sql
-- サインアップ月別のユーザーコホート
SELECT
toStartOfMonth(signup_date) AS cohort,
toStartOfMonth(activity_date) AS month,
dateDiff('month', cohort, month) AS months_since_signup,
count(DISTINCT user_id) AS active_users
FROM (
SELECT
user_id,
min(toDate(timestamp)) OVER (PARTITION BY user_id) AS signup_date,
toDate(timestamp) AS activity_date
FROM events
)
GROUP BY cohort, month, months_since_signup
ORDER BY cohort, months_since_signup;
```
## データパイプラインパターン
### ETLパターン
```typescript
// 抽出、変換、ロード
async function etlPipeline() {
// 1. ソースから抽出
const rawData = await extractFromPostgres()
// 2. 変換
const transformed = rawData.map(row => ({
date: new Date(row.created_at).toISOString().split('T')[0],
market_id: row.market_slug,
volume: parseFloat(row.total_volume),
trades: parseInt(row.trade_count)
}))
// 3. ClickHouseにロード
await bulkInsertToClickHouse(transformed)
}
// 定期的に実行
setInterval(etlPipeline, 60 * 60 * 1000) // 1時間ごと
```
### 変更データキャプチャCDC
```typescript
// PostgreSQLの変更をリッスンしてClickHouseに同期
import { Client } from 'pg'
const pgClient = new Client({ connectionString: process.env.DATABASE_URL })
pgClient.query('LISTEN market_updates')
pgClient.on('notification', async (msg) => {
const update = JSON.parse(msg.payload)
await clickhouse.insert('market_updates', [
{
market_id: update.id,
event_type: update.operation, // INSERT, UPDATE, DELETE
timestamp: new Date(),
data: JSON.stringify(update.new_data)
}
])
})
```
## ベストプラクティス
### 1. パーティショニング戦略
- 時間でパーティション化(通常は月または日)
- パーティションが多すぎないようにする(パフォーマンスへの影響)
- パーティションキーにはDATEタイプを使用
### 2. ソートキー
- 最も頻繁にフィルタリングされる列を最初に配置
- カーディナリティを考慮(高カーディナリティを最初に)
- 順序は圧縮に影響
### 3. データタイプ
- 最小の適切なタイプを使用UInt32 vs UInt64
- 繰り返される文字列にはLowCardinalityを使用
- カテゴリカルデータにはEnumを使用
### 4. 避けるべき
- SELECT *(列を指定)
- FINAL代わりにクエリ前にデータをマージ
- JOINが多すぎる分析用に非正規化
- 小さな頻繁な挿入(代わりにバッチ処理)
### 5. モニタリング
- クエリパフォーマンスを追跡
- ディスク使用量を監視
- マージ操作をチェック
- 低速クエリログをレビュー
**注意**: ClickHouseは分析ワークロードに優れています。クエリパターンに合わせてテーブルを設計し、挿入をバッチ化し、リアルタイム集計にはマテリアライズドビューを活用します。

View File

@@ -1,527 +0,0 @@
---
name: coding-standards
description: TypeScript、JavaScript、React、Node.js開発のための汎用コーディング標準、ベストプラクティス、パターン。
---
# コーディング標準とベストプラクティス
すべてのプロジェクトに適用される汎用的なコーディング標準。
## コード品質の原則
### 1. 可読性優先
* コードは書くよりも読まれることが多い
* 明確な変数名と関数名
* コメントよりも自己文書化コードを優先
* 一貫したフォーマット
### 2. KISS (Keep It Simple, Stupid)
* 機能する最もシンプルなソリューションを採用
* 過剰設計を避ける
* 早すぎる最適化を避ける
* 理解しやすさ > 巧妙なコード
### 3. DRY (Don't Repeat Yourself)
* 共通ロジックを関数に抽出
* 再利用可能なコンポーネントを作成
* ユーティリティ関数をモジュール間で共有
* コピー&ペーストプログラミングを避ける
### 4. YAGNI (You Aren't Gonna Need It)
* 必要ない機能を事前に構築しない
* 推測的な一般化を避ける
* 必要なときのみ複雑さを追加
* シンプルに始めて、必要に応じてリファクタリング
## TypeScript/JavaScript標準
### 変数の命名
```typescript
// ✅ GOOD: Descriptive names
const marketSearchQuery = 'election'
const isUserAuthenticated = true
const totalRevenue = 1000
// ❌ BAD: Unclear names
const q = 'election'
const flag = true
const x = 1000
```
### 関数の命名
```typescript
// ✅ GOOD: Verb-noun pattern
async function fetchMarketData(marketId: string) { }
function calculateSimilarity(a: number[], b: number[]) { }
function isValidEmail(email: string): boolean { }
// ❌ BAD: Unclear or noun-only
async function market(id: string) { }
function similarity(a, b) { }
function email(e) { }
```
### 不変性パターン(重要)
```typescript
// ✅ ALWAYS use spread operator
const updatedUser = {
...user,
name: 'New Name'
}
const updatedArray = [...items, newItem]
// ❌ NEVER mutate directly
user.name = 'New Name' // BAD
items.push(newItem) // BAD
```
### エラーハンドリング
```typescript
// ✅ GOOD: Comprehensive error handling
async function fetchData(url: string) {
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return await response.json()
} catch (error) {
console.error('Fetch failed:', error)
throw new Error('Failed to fetch data')
}
}
// ❌ BAD: No error handling
async function fetchData(url) {
const response = await fetch(url)
return response.json()
}
```
### Async/Awaitベストプラクティス
```typescript
// ✅ GOOD: Parallel execution when possible
const [users, markets, stats] = await Promise.all([
fetchUsers(),
fetchMarkets(),
fetchStats()
])
// ❌ BAD: Sequential when unnecessary
const users = await fetchUsers()
const markets = await fetchMarkets()
const stats = await fetchStats()
```
### 型安全性
```typescript
// ✅ GOOD: Proper types
interface Market {
id: string
name: string
status: 'active' | 'resolved' | 'closed'
created_at: Date
}
function getMarket(id: string): Promise<Market> {
// Implementation
}
// ❌ BAD: Using 'any'
function getMarket(id: any): Promise<any> {
// Implementation
}
```
## Reactベストプラクティス
### コンポーネント構造
```typescript
// ✅ GOOD: Functional component with types
interface ButtonProps {
children: React.ReactNode
onClick: () => void
disabled?: boolean
variant?: 'primary' | 'secondary'
}
export function Button({
children,
onClick,
disabled = false,
variant = 'primary'
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{children}
</button>
)
}
// ❌ BAD: No types, unclear structure
export function Button(props) {
return <button onClick={props.onClick}>{props.children}</button>
}
```
### カスタムフック
```typescript
// ✅ GOOD: Reusable custom hook
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(handler)
}, [value, delay])
return debouncedValue
}
// Usage
const debouncedQuery = useDebounce(searchQuery, 500)
```
### 状態管理
```typescript
// ✅ GOOD: Proper state updates
const [count, setCount] = useState(0)
// Functional update for state based on previous state
setCount(prev => prev + 1)
// ❌ BAD: Direct state reference
setCount(count + 1) // Can be stale in async scenarios
```
### 条件付きレンダリング
```typescript
// ✅ GOOD: Clear conditional rendering
{isLoading && <Spinner />}
{error && <ErrorMessage error={error} />}
{data && <DataDisplay data={data} />}
// ❌ BAD: Ternary hell
{isLoading ? <Spinner /> : error ? <ErrorMessage error={error} /> : data ? <DataDisplay data={data} /> : null}
```
## API設計標準
### REST API規約
```
GET /api/markets # List all markets
GET /api/markets/:id # Get specific market
POST /api/markets # Create new market
PUT /api/markets/:id # Update market (full)
PATCH /api/markets/:id # Update market (partial)
DELETE /api/markets/:id # Delete market
# Query parameters for filtering
GET /api/markets?status=active&limit=10&offset=0
```
### レスポンス形式
```typescript
// ✅ GOOD: Consistent response structure
interface ApiResponse<T> {
success: boolean
data?: T
error?: string
meta?: {
total: number
page: number
limit: number
}
}
// Success response
return NextResponse.json({
success: true,
data: markets,
meta: { total: 100, page: 1, limit: 10 }
})
// Error response
return NextResponse.json({
success: false,
error: 'Invalid request'
}, { status: 400 })
```
### 入力検証
```typescript
import { z } from 'zod'
// ✅ GOOD: Schema validation
const CreateMarketSchema = z.object({
name: z.string().min(1).max(200),
description: z.string().min(1).max(2000),
endDate: z.string().datetime(),
categories: z.array(z.string()).min(1)
})
export async function POST(request: Request) {
const body = await request.json()
try {
const validated = CreateMarketSchema.parse(body)
// Proceed with validated data
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({
success: false,
error: 'Validation failed',
details: error.errors
}, { status: 400 })
}
}
}
```
## ファイル構成
### プロジェクト構造
```
src/
├── app/ # Next.js App Router
│ ├── api/ # API routes
│ ├── markets/ # Market pages
│ └── (auth)/ # Auth pages (route groups)
├── components/ # React components
│ ├── ui/ # Generic UI components
│ ├── forms/ # Form components
│ └── layouts/ # Layout components
├── hooks/ # Custom React hooks
├── lib/ # Utilities and configs
│ ├── api/ # API clients
│ ├── utils/ # Helper functions
│ └── constants/ # Constants
├── types/ # TypeScript types
└── styles/ # Global styles
```
### ファイル命名
```
components/Button.tsx # PascalCase for components
hooks/useAuth.ts # camelCase with 'use' prefix
lib/formatDate.ts # camelCase for utilities
types/market.types.ts # camelCase with .types suffix
```
## コメントとドキュメント
### コメントを追加するタイミング
```typescript
// ✅ GOOD: Explain WHY, not WHAT
// Use exponential backoff to avoid overwhelming the API during outages
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000)
// Deliberately using mutation here for performance with large arrays
items.push(newItem)
// ❌ BAD: Stating the obvious
// Increment counter by 1
count++
// Set name to user's name
name = user.name
```
### パブリックAPIのJSDoc
````typescript
/**
* Searches markets using semantic similarity.
*
* @param query - Natural language search query
* @param limit - Maximum number of results (default: 10)
* @returns Array of markets sorted by similarity score
* @throws {Error} If OpenAI API fails or Redis unavailable
*
* @example
* ```typescript
* const results = await searchMarkets('election', 5)
* console.log(results[0].name) // "Trump vs Biden"
* ```
*/
export async function searchMarkets(
query: string,
limit: number = 10
): Promise<Market[]> {
// Implementation
}
````
## パフォーマンスベストプラクティス
### メモ化
```typescript
import { useMemo, useCallback } from 'react'
// ✅ GOOD: Memoize expensive computations
const sortedMarkets = useMemo(() => {
return markets.sort((a, b) => b.volume - a.volume)
}, [markets])
// ✅ GOOD: Memoize callbacks
const handleSearch = useCallback((query: string) => {
setSearchQuery(query)
}, [])
```
### 遅延読み込み
```typescript
import { lazy, Suspense } from 'react'
// ✅ GOOD: Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart'))
export function Dashboard() {
return (
<Suspense fallback={<Spinner />}>
<HeavyChart />
</Suspense>
)
}
```
### データベースクエリ
```typescript
// ✅ GOOD: Select only needed columns
const { data } = await supabase
.from('markets')
.select('id, name, status')
.limit(10)
// ❌ BAD: Select everything
const { data } = await supabase
.from('markets')
.select('*')
```
## テスト標準
### テスト構造AAAパターン
```typescript
test('calculates similarity correctly', () => {
// Arrange
const vector1 = [1, 0, 0]
const vector2 = [0, 1, 0]
// Act
const similarity = calculateCosineSimilarity(vector1, vector2)
// Assert
expect(similarity).toBe(0)
})
```
### テストの命名
```typescript
// ✅ GOOD: Descriptive test names
test('returns empty array when no markets match query', () => { })
test('throws error when OpenAI API key is missing', () => { })
test('falls back to substring search when Redis unavailable', () => { })
// ❌ BAD: Vague test names
test('works', () => { })
test('test search', () => { })
```
## コードスメルの検出
以下のアンチパターンに注意してください。
### 1. 長い関数
```typescript
// ❌ BAD: Function > 50 lines
function processMarketData() {
// 100 lines of code
}
// ✅ GOOD: Split into smaller functions
function processMarketData() {
const validated = validateData()
const transformed = transformData(validated)
return saveData(transformed)
}
```
### 2. 深いネスト
```typescript
// ❌ BAD: 5+ levels of nesting
if (user) {
if (user.isAdmin) {
if (market) {
if (market.isActive) {
if (hasPermission) {
// Do something
}
}
}
}
}
// ✅ GOOD: Early returns
if (!user) return
if (!user.isAdmin) return
if (!market) return
if (!market.isActive) return
if (!hasPermission) return
// Do something
```
### 3. マジックナンバー
```typescript
// ❌ BAD: Unexplained numbers
if (retryCount > 3) { }
setTimeout(callback, 500)
// ✅ GOOD: Named constants
const MAX_RETRIES = 3
const DEBOUNCE_DELAY_MS = 500
if (retryCount > MAX_RETRIES) { }
setTimeout(callback, DEBOUNCE_DELAY_MS)
```
**覚えておいてください**: コード品質は妥協できません。明確で保守可能なコードにより、迅速な開発と自信を持ったリファクタリングが可能になります。

View File

@@ -1,298 +0,0 @@
---
name: configure-ecc
description: Everything Claude Code のインタラクティブなインストーラー — スキルとルールの選択とインストールをユーザーレベルまたはプロジェクトレベルのディレクトリへガイドし、パスを検証し、必要に応じてインストールされたファイルを最適化します。
---
# Configure Everything Claude Code (ECC)
Everything Claude Code プロジェクトのインタラクティブなステップバイステップのインストールウィザードです。`AskUserQuestion` を使用してスキルとルールの選択的インストールをユーザーにガイドし、正確性を検証し、最適化を提供します。
## 起動タイミング
- ユーザーが "configure ecc"、"install ecc"、"setup everything claude code" などと言った場合
- ユーザーがこのプロジェクトからスキルまたはルールを選択的にインストールしたい場合
- ユーザーが既存の ECC インストールを検証または修正したい場合
- ユーザーがインストールされたスキルまたはルールをプロジェクト用に最適化したい場合
## 前提条件
このスキルは起動前に Claude Code からアクセス可能である必要があります。ブートストラップには2つの方法があります
1. **プラグイン経由**: `/plugin install everything-claude-code` — プラグインがこのスキルを自動的にロードします
2. **手動**: このスキルのみを `~/.claude/skills/configure-ecc/SKILL.md` にコピーし、"configure ecc" と言って起動します
---
## ステップ 0: ECC リポジトリのクローン
インストールの前に、最新の ECC ソースを `/tmp` にクローンします:
```bash
rm -rf /tmp/everything-claude-code
git clone https://github.com/affaan-m/everything-claude-code.git /tmp/everything-claude-code
```
以降のすべてのコピー操作のソースとして `ECC_ROOT=/tmp/everything-claude-code` を設定します。
クローンが失敗した場合(ネットワークの問題など)、`AskUserQuestion` を使用してユーザーに既存の ECC クローンへのローカルパスを提供するよう依頼します。
---
## ステップ 1: インストールレベルの選択
`AskUserQuestion` を使用してユーザーにインストール先を尋ねます:
```
Question: "ECC コンポーネントをどこにインストールしますか?"
Options:
- "User-level (~/.claude/)" — "すべての Claude Code プロジェクトに適用されます"
- "Project-level (.claude/)" — "現在のプロジェクトのみに適用されます"
- "Both" — "共通/共有アイテムはユーザーレベル、プロジェクト固有アイテムはプロジェクトレベル"
```
選択を `INSTALL_LEVEL` として保存します。ターゲットディレクトリを設定します:
- User-level: `TARGET=~/.claude`
- Project-level: `TARGET=.claude`(現在のプロジェクトルートからの相対パス)
- Both: `TARGET_USER=~/.claude``TARGET_PROJECT=.claude`
ターゲットディレクトリが存在しない場合は作成します:
```bash
mkdir -p $TARGET/skills $TARGET/rules
```
---
## ステップ 2: スキルの選択とインストール
### 2a: スキルカテゴリの選択
27個のスキルが4つのカテゴリに分類されています。`multiSelect: true``AskUserQuestion` を使用します:
```
Question: "どのスキルカテゴリをインストールしますか?"
Options:
- "Framework & Language" — "Django, Spring Boot, Go, Python, Java, Frontend, Backend パターン"
- "Database" — "PostgreSQL, ClickHouse, JPA/Hibernate パターン"
- "Workflow & Quality" — "TDD, 検証, 学習, セキュリティレビュー, コンパクション"
- "All skills" — "利用可能なすべてのスキルをインストール"
```
### 2b: 個別スキルの確認
選択された各カテゴリについて、以下の完全なスキルリストを表示し、ユーザーに確認または特定のものの選択解除を依頼します。リストが4項目を超える場合、リストをテキストとして表示し、`AskUserQuestion` で「リストされたすべてをインストール」オプションと、ユーザーが特定の名前を貼り付けるための「その他」オプションを使用します。
**カテゴリ: Framework & Language16スキル**
| スキル | 説明 |
|-------|-------------|
| `backend-patterns` | バックエンドアーキテクチャ、API設計、Node.js/Express/Next.js のサーバーサイドベストプラクティス |
| `coding-standards` | TypeScript、JavaScript、React、Node.js の汎用コーディング標準 |
| `django-patterns` | Django アーキテクチャ、DRF による REST API、ORM、キャッシング、シグナル、ミドルウェア |
| `django-security` | Django セキュリティ: 認証、CSRF、SQL インジェクション、XSS 防止 |
| `django-tdd` | pytest-django、factory_boy、モック、カバレッジによる Django テスト |
| `django-verification` | Django 検証ループ: マイグレーション、リンティング、テスト、セキュリティスキャン |
| `frontend-patterns` | React、Next.js、状態管理、パフォーマンス、UI パターン |
| `golang-patterns` | 慣用的な Go パターン、堅牢な Go アプリケーションのための規約 |
| `golang-testing` | Go テスト: テーブル駆動テスト、サブテスト、ベンチマーク、ファジング |
| `java-coding-standards` | Spring Boot 用 Java コーディング標準: 命名、不変性、Optional、ストリーム |
| `python-patterns` | Pythonic なイディオム、PEP 8、型ヒント、ベストプラクティス |
| `python-testing` | pytest、TDD、フィクスチャ、モック、パラメータ化による Python テスト |
| `springboot-patterns` | Spring Boot アーキテクチャ、REST API、レイヤードサービス、キャッシング、非同期 |
| `springboot-security` | Spring Security: 認証/認可、検証、CSRF、シークレット、レート制限 |
| `springboot-tdd` | JUnit 5、Mockito、MockMvc、Testcontainers による Spring Boot TDD |
| `springboot-verification` | Spring Boot 検証: ビルド、静的解析、テスト、セキュリティスキャン |
**カテゴリ: Database3スキル**
| スキル | 説明 |
|-------|-------------|
| `clickhouse-io` | ClickHouse パターン、クエリ最適化、分析、データエンジニアリング |
| `jpa-patterns` | JPA/Hibernate エンティティ設計、リレーションシップ、クエリ最適化、トランザクション |
| `postgres-patterns` | PostgreSQL クエリ最適化、スキーマ設計、インデックス作成、セキュリティ |
**カテゴリ: Workflow & Quality8スキル**
| スキル | 説明 |
|-------|-------------|
| `continuous-learning` | セッションから再利用可能なパターンを学習済みスキルとして自動抽出 |
| `continuous-learning-v2` | 信頼度スコアリングを持つ本能ベースの学習、スキル/コマンド/エージェントに進化 |
| `eval-harness` | 評価駆動開発EDDのための正式な評価フレームワーク |
| `iterative-retrieval` | サブエージェントコンテキスト問題のための段階的コンテキスト改善 |
| `security-review` | セキュリティチェックリスト: 認証、入力、シークレット、API、決済機能 |
| `strategic-compact` | 論理的な間隔で手動コンテキスト圧縮を提案 |
| `tdd-workflow` | 80%以上のカバレッジで TDD を強制: ユニット、統合、E2E |
| `verification-loop` | 検証と品質ループのパターン |
**スタンドアロン**
| スキル | 説明 |
|-------|-------------|
| `project-guidelines-example` | プロジェクト固有のスキルを作成するためのテンプレート |
### 2c: インストールの実行
選択された各スキルについて、スキルディレクトリ全体をコピーします:
```bash
cp -r $ECC_ROOT/skills/<skill-name> $TARGET/skills/
```
注: `continuous-learning``continuous-learning-v2` には追加ファイルconfig.json、フック、スクリプトがあります — SKILL.md だけでなく、ディレクトリ全体がコピーされることを確認してください。
---
## ステップ 3: ルールの選択とインストール
`multiSelect: true``AskUserQuestion` を使用します:
```
Question: "どのルールセットをインストールしますか?"
Options:
- "Common rules (Recommended)" — "言語に依存しない原則: コーディングスタイル、git ワークフロー、テスト、セキュリティなど8ファイル"
- "TypeScript/JavaScript" — "TS/JS パターン、フック、Playwright によるテスト5ファイル"
- "Python" — "Python パターン、pytest、black/ruff フォーマット5ファイル"
- "Go" — "Go パターン、テーブル駆動テスト、gofmt/staticcheck5ファイル"
```
インストールを実行:
```bash
# 共通ルールrules/ にフラットコピー)
cp -r $ECC_ROOT/rules/common/* $TARGET/rules/
# 言語固有のルールrules/ にフラットコピー)
cp -r $ECC_ROOT/rules/typescript/* $TARGET/rules/ # 選択された場合
cp -r $ECC_ROOT/rules/python/* $TARGET/rules/ # 選択された場合
cp -r $ECC_ROOT/rules/golang/* $TARGET/rules/ # 選択された場合
```
**重要**: ユーザーが言語固有のルールを選択したが、共通ルールを選択しなかった場合、警告します:
> "言語固有のルールは共通ルールを拡張します。共通ルールなしでインストールすると、不完全なカバレッジになる可能性があります。共通ルールもインストールしますか?"
---
## ステップ 4: インストール後の検証
インストール後、以下の自動チェックを実行します:
### 4a: ファイルの存在確認
インストールされたすべてのファイルをリストし、ターゲットロケーションに存在することを確認します:
```bash
ls -la $TARGET/skills/
ls -la $TARGET/rules/
```
### 4b: パス参照のチェック
インストールされたすべての `.md` ファイルでパス参照をスキャンします:
```bash
grep -rn "~/.claude/" $TARGET/skills/ $TARGET/rules/
grep -rn "../common/" $TARGET/rules/
grep -rn "skills/" $TARGET/skills/
```
**プロジェクトレベルのインストールの場合**`~/.claude/` パスへの参照をフラグします:
- スキルが `~/.claude/settings.json` を参照している場合 — これは通常問題ありません(設定は常にユーザーレベルです)
- スキルが `~/.claude/skills/` または `~/.claude/rules/` を参照している場合 — プロジェクトレベルのみにインストールされている場合、これは壊れている可能性があります
- スキルが別のスキルを名前で参照している場合 — 参照されているスキルもインストールされているか確認します
### 4c: スキル間の相互参照のチェック
一部のスキルは他のスキルを参照します。これらの依存関係を検証します:
- `django-tdd``django-patterns` を参照する可能性があります
- `springboot-tdd``springboot-patterns` を参照する可能性があります
- `continuous-learning-v2``~/.claude/homunculus/` ディレクトリを参照します
- `python-testing``python-patterns` を参照する可能性があります
- `golang-testing``golang-patterns` を参照する可能性があります
- 言語固有のルールは `common/` の対応物を参照します
### 4d: 問題の報告
見つかった各問題について、報告します:
1. **ファイル**: 問題のある参照を含むファイル
2. **行**: 行番号
3. **問題**: 何が間違っているか(例: "~/.claude/skills/python-patterns を参照していますが、python-patterns がインストールされていません"
4. **推奨される修正**: 何をすべきか(例: "python-patterns スキルをインストール" または "パスを .claude/skills/ に更新"
---
## ステップ 5: インストールされたファイルの最適化(オプション)
`AskUserQuestion` を使用します:
```
Question: "インストールされたファイルをプロジェクト用に最適化しますか?"
Options:
- "Optimize skills" — "無関係なセクションを削除、パスを調整、技術スタックに合わせて調整"
- "Optimize rules" — "カバレッジ目標を調整、プロジェクト固有のパターンを追加、ツール設定をカスタマイズ"
- "Optimize both" — "インストールされたすべてのファイルの完全な最適化"
- "Skip" — "すべてをそのまま維持"
```
### スキルを最適化する場合:
1. インストールされた各 SKILL.md を読み取ります
2. ユーザーにプロジェクトの技術スタックを尋ねます(まだ不明な場合)
3. 各スキルについて、無関係なセクションの削除を提案します
4. インストール先(ソースリポジトリではなく)で SKILL.md ファイルをその場で編集します
5. ステップ4で見つかったパスの問題を修正します
### ルールを最適化する場合:
1. インストールされた各ルール .md ファイルを読み取ります
2. ユーザーに設定について尋ねます:
- テストカバレッジ目標デフォルト80%
- 優先フォーマットツール
- Git ワークフロー規約
- セキュリティ要件
3. インストール先でルールファイルをその場で編集します
**重要**: インストール先(`$TARGET/`)のファイルのみを変更し、ソース ECC リポジトリ(`$ECC_ROOT/`)のファイルは決して変更しないでください。
---
## ステップ 6: インストールサマリー
`/tmp` からクローンされたリポジトリをクリーンアップします:
```bash
rm -rf /tmp/everything-claude-code
```
次にサマリーレポートを出力します:
```
## ECC インストール完了
### インストール先
- レベル: [user-level / project-level / both]
- パス: [ターゲットパス]
### インストールされたスキル([数]
- skill-1, skill-2, skill-3, ...
### インストールされたルール([数]
- common8ファイル
- typescript5ファイル
- ...
### 検証結果
- [数]個の問題が見つかり、[数]個が修正されました
- [残っている問題をリスト]
### 適用された最適化
- [加えられた変更をリスト、または "なし"]
```
---
## トラブルシューティング
### "スキルが Claude Code に認識されません"
- スキルディレクトリに `SKILL.md` ファイルが含まれていることを確認します(単なる緩い .md ファイルではありません)
- ユーザーレベルの場合: `~/.claude/skills/<skill-name>/SKILL.md` が存在するか確認します
- プロジェクトレベルの場合: `.claude/skills/<skill-name>/SKILL.md` が存在するか確認します
### "ルールが機能しません"
- ルールはフラットファイルで、サブディレクトリにはありません: `$TARGET/rules/coding-style.md`(正しい) vs `$TARGET/rules/common/coding-style.md`(フラットインストールでは不正)
- ルールをインストール後、Claude Code を再起動します
### "プロジェクトレベルのインストール後のパス参照エラー"
- 一部のスキルは `~/.claude/` パスを前提としています。ステップ4の検証を実行してこれらを見つけて修正します。
- `continuous-learning-v2` の場合、`~/.claude/homunculus/` ディレクトリは常にユーザーレベルです — これは想定されており、エラーではありません。

View File

@@ -1,284 +0,0 @@
---
name: continuous-learning-v2
description: フックを介してセッションを観察し、信頼度スコアリング付きのアトミックなインスティンクトを作成し、スキル/コマンド/エージェントに進化させるインスティンクトベースの学習システム。
version: 2.0.0
---
# Continuous Learning v2 - インスティンクトベースアーキテクチャ
Claude Codeセッションを信頼度スコアリング付きの小さな学習済み行動である「インスティンクト」を通じて再利用可能な知識に変える高度な学習システム。
## v2の新機能
| 機能 | v1 | v2 |
|---------|----|----|
| 観察 | Stopフックセッション終了 | PreToolUse/PostToolUse100%信頼性) |
| 分析 | メインコンテキスト | バックグラウンドエージェントHaiku |
| 粒度 | 完全なスキル | アトミック「インスティンクト」 |
| 信頼度 | なし | 0.3-0.9重み付け |
| 進化 | 直接スキルへ | インスティンクト → クラスター → スキル/コマンド/エージェント |
| 共有 | なし | インスティンクトのエクスポート/インポート |
## インスティンクトモデル
インスティンクトは小さな学習済み行動です:
```yaml
---
id: prefer-functional-style
trigger: "when writing new functions"
confidence: 0.7
domain: "code-style"
source: "session-observation"
---
# 関数型スタイルを優先
## Action
適切な場合はクラスよりも関数型パターンを使用します。
## Evidence
- 関数型パターンの優先が5回観察されました
- ユーザーが2025-01-15にクラスベースのアプローチを関数型に修正しました
```
**プロパティ:**
- **アトミック** — 1つのトリガー、1つのアクション
- **信頼度重み付け** — 0.3 = 暫定的、0.9 = ほぼ確実
- **ドメインタグ付き** — code-style、testing、git、debugging、workflowなど
- **証拠に基づく** — それを作成した観察を追跡
## 仕組み
```
Session Activity
│ フックがプロンプト + ツール使用をキャプチャ100%信頼性)
┌─────────────────────────────────────────┐
│ observations.jsonl │
│ (prompts, tool calls, outcomes) │
└─────────────────────────────────────────┘
│ Observerエージェントが読み取りバックグラウンド、Haiku
┌─────────────────────────────────────────┐
│ パターン検出 │
│ • ユーザー修正 → インスティンクト │
│ • エラー解決 → インスティンクト │
│ • 繰り返しワークフロー → インスティンクト │
└─────────────────────────────────────────┘
│ 作成/更新
┌─────────────────────────────────────────┐
│ instincts/personal/ │
│ • prefer-functional.md (0.7) │
│ • always-test-first.md (0.9) │
│ • use-zod-validation.md (0.6) │
└─────────────────────────────────────────┘
│ /evolveクラスター
┌─────────────────────────────────────────┐
│ evolved/ │
│ • commands/new-feature.md │
│ • skills/testing-workflow.md │
│ • agents/refactor-specialist.md │
└─────────────────────────────────────────┘
```
## クイックスタート
### 1. 観察フックを有効化
`~/.claude/settings.json`に追加します。
**プラグインとしてインストールした場合**(推奨):
```json
{
"hooks": {
"PreToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh pre"
}]
}],
"PostToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh post"
}]
}]
}
}
```
**`~/.claude/skills`に手動でインストールした場合**
```json
{
"hooks": {
"PreToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh pre"
}]
}],
"PostToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh post"
}]
}]
}
}
```
### 2. ディレクトリ構造を初期化
Python CLIが自動的に作成しますが、手動で作成することもできます
```bash
mkdir -p ~/.claude/homunculus/{instincts/{personal,inherited},evolved/{agents,skills,commands}}
touch ~/.claude/homunculus/observations.jsonl
```
### 3. インスティンクトコマンドを使用
```bash
/instinct-status # 信頼度スコア付きの学習済みインスティンクトを表示
/evolve # 関連するインスティンクトをスキル/コマンドにクラスター化
/instinct-export # 共有のためにインスティンクトをエクスポート
/instinct-import # 他の人からインスティンクトをインポート
```
## コマンド
| コマンド | 説明 |
|---------|-------------|
| `/instinct-status` | すべての学習済みインスティンクトを信頼度と共に表示 |
| `/evolve` | 関連するインスティンクトをスキル/コマンドにクラスター化 |
| `/instinct-export` | 共有のためにインスティンクトをエクスポート |
| `/instinct-import <file>` | 他の人からインスティンクトをインポート |
## 設定
`config.json`を編集:
```json
{
"version": "2.0",
"observation": {
"enabled": true,
"store_path": "~/.claude/homunculus/observations.jsonl",
"max_file_size_mb": 10,
"archive_after_days": 7
},
"instincts": {
"personal_path": "~/.claude/homunculus/instincts/personal/",
"inherited_path": "~/.claude/homunculus/instincts/inherited/",
"min_confidence": 0.3,
"auto_approve_threshold": 0.7,
"confidence_decay_rate": 0.05
},
"observer": {
"enabled": true,
"model": "haiku",
"run_interval_minutes": 5,
"patterns_to_detect": [
"user_corrections",
"error_resolutions",
"repeated_workflows",
"tool_preferences"
]
},
"evolution": {
"cluster_threshold": 3,
"evolved_path": "~/.claude/homunculus/evolved/"
}
}
```
## ファイル構造
```
~/.claude/homunculus/
├── identity.json # プロフィール、技術レベル
├── observations.jsonl # 現在のセッション観察
├── observations.archive/ # 処理済み観察
├── instincts/
│ ├── personal/ # 自動学習されたインスティンクト
│ └── inherited/ # 他の人からインポート
└── evolved/
├── agents/ # 生成された専門エージェント
├── skills/ # 生成されたスキル
└── commands/ # 生成されたコマンド
```
## Skill Creatorとの統合
[Skill Creator GitHub App](https://skill-creator.app)を使用すると、**両方**が生成されます:
- 従来のSKILL.mdファイル後方互換性のため
- インスティンクトコレクションv2学習システム用
リポジトリ分析からのインスティンクトには`source: "repo-analysis"`があり、ソースリポジトリURLが含まれます。
## 信頼度スコアリング
信頼度は時間とともに進化します:
| スコア | 意味 | 動作 |
|-------|---------|----------|
| 0.3 | 暫定的 | 提案されるが強制されない |
| 0.5 | 中程度 | 関連する場合に適用 |
| 0.7 | 強い | 適用が自動承認される |
| 0.9 | ほぼ確実 | コア動作 |
**信頼度が上がる**場合:
- パターンが繰り返し観察される
- ユーザーが提案された動作を修正しない
- 他のソースからの類似インスティンクトが一致する
**信頼度が下がる**場合:
- ユーザーが明示的に動作を修正する
- パターンが長期間観察されない
- 矛盾する証拠が現れる
## 観察にスキルではなくフックを使用する理由は?
> 「v1はスキルに依存して観察していました。スキルは確率的で、Claudeの判断に基づいて約50-80%の確率で発火します。」
フックは**100%の確率で**決定論的に発火します。これは次のことを意味します:
- すべてのツール呼び出しが観察される
- パターンが見逃されない
- 学習が包括的
## 後方互換性
v2はv1と完全に互換性があります
- 既存の`~/.claude/skills/learned/`スキルは引き続き機能
- Stopフックは引き続き実行されるただしv2にもフィードされる
- 段階的な移行パス:両方を並行して実行
## プライバシー
- 観察はマシン上で**ローカル**に保持されます
- **インスティンクト**(パターン)のみをエクスポート可能
- 実際のコードや会話内容は共有されません
- エクスポートする内容を制御できます
## 関連
- [Skill Creator](https://skill-creator.app) - リポジトリ履歴からインスティンクトを生成
- [Homunculus](https://github.com/humanplane/homunculus) - v2アーキテクチャのインスピレーション
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - 継続的学習セクション
---
*インスティンクトベースの学習一度に1つの観察で、Claudeにあなたのパターンを教える。*

View File

@@ -1,137 +0,0 @@
---
name: observer
description: セッションの観察を分析してパターンを検出し、本能を作成するバックグラウンドエージェント。コスト効率のためにHaikuを使用します。
model: haiku
run_mode: background
---
# Observerエージェント
Claude Codeセッションからの観察を分析してパターンを検出し、本能を作成するバックグラウンドエージェント。
## 実行タイミング
- セッションで重要なアクティビティがあった後(20以上のツール呼び出し)
- ユーザーが`/analyze-patterns`を実行したとき
- スケジュールされた間隔(設定可能、デフォルト5分)
- 観察フックによってトリガーされたとき(SIGUSR1)
## 入力
`~/.claude/homunculus/observations.jsonl`から観察を読み取ります:
```jsonl
{"timestamp":"2025-01-22T10:30:00Z","event":"tool_start","session":"abc123","tool":"Edit","input":"..."}
{"timestamp":"2025-01-22T10:30:01Z","event":"tool_complete","session":"abc123","tool":"Edit","output":"..."}
{"timestamp":"2025-01-22T10:30:05Z","event":"tool_start","session":"abc123","tool":"Bash","input":"npm test"}
{"timestamp":"2025-01-22T10:30:10Z","event":"tool_complete","session":"abc123","tool":"Bash","output":"All tests pass"}
```
## パターン検出
観察から以下のパターンを探します:
### 1. ユーザー修正
ユーザーのフォローアップメッセージがClaudeの前のアクションを修正する場合:
- "いいえ、YではなくXを使ってください"
- "実は、意図したのは..."
- 即座の元に戻す/やり直しパターン
→ 本能を作成: "Xを行う際は、Yを優先する"
### 2. エラー解決
エラーの後に修正が続く場合:
- ツール出力にエラーが含まれる
- 次のいくつかのツール呼び出しで修正
- 同じエラータイプが複数回同様に解決される
→ 本能を作成: "エラーXに遭遇した場合、Yを試す"
### 3. 反復ワークフロー
同じツールシーケンスが複数回使用される場合:
- 類似した入力を持つ同じツールシーケンス
- 一緒に変更されるファイルパターン
- 時間的にクラスタ化された操作
→ ワークフロー本能を作成: "Xを行う際は、手順Y、Z、Wに従う"
### 4. ツールの好み
特定のツールが一貫して好まれる場合:
- 常にEditの前にGrepを使用
- Bash catよりもReadを好む
- 特定のタスクに特定のBashコマンドを使用
→ 本能を作成: "Xが必要な場合、ツールYを使用する"
## 出力
`~/.claude/homunculus/instincts/personal/`に本能を作成/更新:
```yaml
---
id: prefer-grep-before-edit
trigger: "コードを変更するために検索する場合"
confidence: 0.65
domain: "workflow"
source: "session-observation"
---
# Editの前にGrepを優先
## アクション
Editを使用する前に、常にGrepを使用して正確な場所を見つけます。
## 証拠
- セッションabc123で8回観察
- パターン: Grep → Read → Editシーケンス
- 最終観察: 2025-01-22
```
## 信頼度計算
観察頻度に基づく初期信頼度:
- 1-2回の観察: 0.3(暫定的)
- 3-5回の観察: 0.5(中程度)
- 6-10回の観察: 0.7(強い)
- 11回以上の観察: 0.85(非常に強い)
信頼度は時間とともに調整:
- 確認する観察ごとに+0.05
- 矛盾する観察ごとに-0.1
- 観察なしで週ごとに-0.02(減衰)
## 重要なガイドライン
1. **保守的に**: 明確なパターンのみ本能を作成(3回以上の観察)
2. **具体的に**: 広範なトリガーよりも狭いトリガーが良い
3. **証拠を追跡**: 本能につながった観察を常に含める
4. **プライバシーを尊重**: 実際のコードスニペットは含めず、パターンのみ
5. **類似を統合**: 新しい本能が既存のものと類似している場合、重複ではなく更新
## 分析セッション例
観察が与えられた場合:
```jsonl
{"event":"tool_start","tool":"Grep","input":"pattern: useState"}
{"event":"tool_complete","tool":"Grep","output":"Found in 3 files"}
{"event":"tool_start","tool":"Read","input":"src/hooks/useAuth.ts"}
{"event":"tool_complete","tool":"Read","output":"[file content]"}
{"event":"tool_start","tool":"Edit","input":"src/hooks/useAuth.ts..."}
```
分析:
- 検出されたワークフロー: Grep → Read → Edit
- 頻度: このセッションで5回確認
- 本能を作成:
- trigger: "コードを変更する場合"
- action: "Grepで検索し、Readで確認し、次にEdit"
- confidence: 0.6
- domain: "workflow"
## Skill Creatorとの統合
Skill Creator(リポジトリ分析)から本能がインポートされる場合、以下を持ちます:
- `source: "repo-analysis"`
- `source_repo: "https://github.com/..."`
これらは、より高い初期信頼度(0.7以上)を持つチーム/プロジェクトの規約として扱うべきです。

View File

@@ -1,110 +0,0 @@
---
name: continuous-learning
description: Claude Codeセッションから再利用可能なパターンを自動的に抽出し、将来の使用のために学習済みスキルとして保存します。
---
# 継続学習スキル
Claude Codeセッションを終了時に自動的に評価し、学習済みスキルとして保存できる再利用可能なパターンを抽出します。
## 動作原理
このスキルは各セッション終了時に**Stopフック**として実行されます:
1. **セッション評価**: セッションに十分なメッセージがあるか確認(デフォルト: 10以上)
2. **パターン検出**: セッションから抽出可能なパターンを識別
3. **スキル抽出**: 有用なパターンを`~/.claude/skills/learned/`に保存
## 設定
`config.json`を編集してカスタマイズ:
```json
{
"min_session_length": 10,
"extraction_threshold": "medium",
"auto_approve": false,
"learned_skills_path": "~/.claude/skills/learned/",
"patterns_to_detect": [
"error_resolution",
"user_corrections",
"workarounds",
"debugging_techniques",
"project_specific"
],
"ignore_patterns": [
"simple_typos",
"one_time_fixes",
"external_api_issues"
]
}
```
## パターンの種類
| パターン | 説明 |
|---------|-------------|
| `error_resolution` | 特定のエラーの解決方法 |
| `user_corrections` | ユーザー修正からのパターン |
| `workarounds` | フレームワーク/ライブラリの癖への解決策 |
| `debugging_techniques` | 効果的なデバッグアプローチ |
| `project_specific` | プロジェクト固有の規約 |
## フック設定
`~/.claude/settings.json`に追加:
```json
{
"hooks": {
"Stop": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "~/.claude/skills/continuous-learning/evaluate-session.sh"
}]
}]
}
}
```
## Stopフックを使用する理由
- **軽量**: セッション終了時に1回だけ実行
- **ノンブロッキング**: すべてのメッセージにレイテンシを追加しない
- **完全なコンテキスト**: セッション全体のトランスクリプトにアクセス可能
## 関連項目
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - 継続学習に関するセクション
- `/learn`コマンド - セッション中の手動パターン抽出
---
## 比較ノート (調査: 2025年1月)
### vs Homunculus (github.com/humanplane/homunculus)
Homunculus v2はより洗練されたアプローチを採用:
| 機能 | このアプローチ | Homunculus v2 |
|---------|--------------|---------------|
| 観察 | Stopフック(セッション終了時) | PreToolUse/PostToolUseフック(100%信頼性) |
| 分析 | メインコンテキスト | バックグラウンドエージェント(Haiku) |
| 粒度 | 完全なスキル | 原子的な「本能」 |
| 信頼度 | なし | 0.3-0.9の重み付け |
| 進化 | 直接スキルへ | 本能 → クラスタ → スキル/コマンド/エージェント |
| 共有 | なし | 本能のエクスポート/インポート |
**homunculusからの重要な洞察:**
> "v1はスキルに観察を依存していました。スキルは確率的で、発火率は約50-80%です。v2は観察にフック(100%信頼性)を使用し、学習された振る舞いの原子単位として本能を使用します。"
### v2の潜在的な改善
1. **本能ベースの学習** - 信頼度スコアリングを持つ、より小さく原子的な振る舞い
2. **バックグラウンド観察者** - 並行して分析するHaikuエージェント
3. **信頼度の減衰** - 矛盾した場合に本能の信頼度が低下
4. **ドメインタグ付け** - コードスタイル、テスト、git、デバッグなど
5. **進化パス** - 関連する本能をスキル/コマンドにクラスタ化
詳細: `/Users/affoon/Documents/tasks/12-continuous-learning-v2.md`を参照。

View File

@@ -1,322 +0,0 @@
---
name: cpp-testing
description: C++ テストの作成/更新/修正、GoogleTest/CTest の設定、失敗またはフレーキーなテストの診断、カバレッジ/サニタイザーの追加時にのみ使用します。
---
# C++ Testingエージェントスキル
CMake/CTest を使用した GoogleTest/GoogleMock による最新の C++C++17/20向けのエージェント重視のテストワークフローです。
## 使用タイミング
- 新しい C++ テストの作成または既存のテストの修正
- C++ コンポーネントのユニット/統合テストカバレッジの設計
- テストカバレッジ、CI ゲーティング、リグレッション保護の追加
- 一貫した実行のための CMake/CTest ワークフローの設定
- テスト失敗またはフレーキーな動作の調査
- メモリ/レース診断のためのサニタイザーの有効化
### 使用すべきでない場合
- テスト変更を伴わない新しい製品機能の実装
- テストカバレッジや失敗に関連しない大規模なリファクタリング
- 検証するテストリグレッションのないパフォーマンスチューニング
- C++ 以外のプロジェクトまたはテスト以外のタスク
## コア概念
- **TDD ループ**: red → green → refactorテスト優先、最小限の修正、その後クリーンアップ
- **分離**: グローバル状態よりも依存性注入とフェイクを優先
- **テストレイアウト**: `tests/unit``tests/integration``tests/testdata`
- **モック vs フェイク**: 相互作用にはモック、ステートフルな動作にはフェイク
- **CTest ディスカバリー**: 安定したテストディスカバリーのために `gtest_discover_tests()` を使用
- **CI シグナル**: 最初にサブセットを実行し、次に `--output-on-failure` でフルスイートを実行
## TDD ワークフロー
RED → GREEN → REFACTOR ループに従います:
1. **RED**: 新しい動作をキャプチャする失敗するテストを書く
2. **GREEN**: 合格する最小限の変更を実装する
3. **REFACTOR**: テストがグリーンのままクリーンアップする
```cpp
// tests/add_test.cpp
#include <gtest/gtest.h>
int Add(int a, int b); // プロダクションコードによって提供されます。
TEST(AddTest, AddsTwoNumbers) { // RED
EXPECT_EQ(Add(2, 3), 5);
}
// src/add.cpp
int Add(int a, int b) { // GREEN
return a + b;
}
// REFACTOR: テストが合格したら簡素化/名前変更
```
## コード例
### 基本的なユニットテストgtest
```cpp
// tests/calculator_test.cpp
#include <gtest/gtest.h>
int Add(int a, int b); // プロダクションコードによって提供されます。
TEST(CalculatorTest, AddsTwoNumbers) {
EXPECT_EQ(Add(2, 3), 5);
}
```
### フィクスチャgtest
```cpp
// tests/user_store_test.cpp
// 擬似コードスタブ: UserStore/User をプロジェクトの型に置き換えてください。
#include <gtest/gtest.h>
#include <memory>
#include <optional>
#include <string>
struct User { std::string name; };
class UserStore {
public:
explicit UserStore(std::string /*path*/) {}
void Seed(std::initializer_list<User> /*users*/) {}
std::optional<User> Find(const std::string &/*name*/) { return User{"alice"}; }
};
class UserStoreTest : public ::testing::Test {
protected:
void SetUp() override {
store = std::make_unique<UserStore>(":memory:");
store->Seed({{"alice"}, {"bob"}});
}
std::unique_ptr<UserStore> store;
};
TEST_F(UserStoreTest, FindsExistingUser) {
auto user = store->Find("alice");
ASSERT_TRUE(user.has_value());
EXPECT_EQ(user->name, "alice");
}
```
### モックgmock
```cpp
// tests/notifier_test.cpp
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <string>
class Notifier {
public:
virtual ~Notifier() = default;
virtual void Send(const std::string &message) = 0;
};
class MockNotifier : public Notifier {
public:
MOCK_METHOD(void, Send, (const std::string &message), (override));
};
class Service {
public:
explicit Service(Notifier &notifier) : notifier_(notifier) {}
void Publish(const std::string &message) { notifier_.Send(message); }
private:
Notifier &notifier_;
};
TEST(ServiceTest, SendsNotifications) {
MockNotifier notifier;
Service service(notifier);
EXPECT_CALL(notifier, Send("hello")).Times(1);
service.Publish("hello");
}
```
### CMake/CTest クイックスタート
```cmake
# CMakeLists.txt抜粋
cmake_minimum_required(VERSION 3.20)
project(example LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
# プロジェクトロックされたバージョンを優先します。タグを使用する場合は、プロジェクトポリシーに従って固定されたバージョンを使用します。
set(GTEST_VERSION v1.17.0) # プロジェクトポリシーに合わせて調整します。
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/refs/tags/${GTEST_VERSION}.zip
)
FetchContent_MakeAvailable(googletest)
add_executable(example_tests
tests/calculator_test.cpp
src/calculator.cpp
)
target_link_libraries(example_tests GTest::gtest GTest::gmock GTest::gtest_main)
enable_testing()
include(GoogleTest)
gtest_discover_tests(example_tests)
```
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
ctest --test-dir build --output-on-failure
```
## テストの実行
```bash
ctest --test-dir build --output-on-failure
ctest --test-dir build -R ClampTest
ctest --test-dir build -R "UserStoreTest.*" --output-on-failure
```
```bash
./build/example_tests --gtest_filter=ClampTest.*
./build/example_tests --gtest_filter=UserStoreTest.FindsExistingUser
```
## 失敗のデバッグ
1. gtest フィルタで単一の失敗したテストを再実行します。
2. 失敗したアサーションの周りにスコープ付きログを追加します。
3. サニタイザーを有効にして再実行します。
4. 根本原因が修正されたら、フルスイートに拡張します。
## カバレッジ
グローバルフラグではなく、ターゲットレベルの設定を優先します。
```cmake
option(ENABLE_COVERAGE "Enable coverage flags" OFF)
if(ENABLE_COVERAGE)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
target_compile_options(example_tests PRIVATE --coverage)
target_link_options(example_tests PRIVATE --coverage)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
target_compile_options(example_tests PRIVATE -fprofile-instr-generate -fcoverage-mapping)
target_link_options(example_tests PRIVATE -fprofile-instr-generate)
endif()
endif()
```
GCC + gcov + lcov:
```bash
cmake -S . -B build-cov -DENABLE_COVERAGE=ON
cmake --build build-cov -j
ctest --test-dir build-cov
lcov --capture --directory build-cov --output-file coverage.info
lcov --remove coverage.info '/usr/*' --output-file coverage.info
genhtml coverage.info --output-directory coverage
```
Clang + llvm-cov:
```bash
cmake -S . -B build-llvm -DENABLE_COVERAGE=ON -DCMAKE_CXX_COMPILER=clang++
cmake --build build-llvm -j
LLVM_PROFILE_FILE="build-llvm/default.profraw" ctest --test-dir build-llvm
llvm-profdata merge -sparse build-llvm/default.profraw -o build-llvm/default.profdata
llvm-cov report build-llvm/example_tests -instr-profile=build-llvm/default.profdata
```
## サニタイザー
```cmake
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
if(ENABLE_ASAN)
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
add_link_options(-fsanitize=address)
endif()
if(ENABLE_UBSAN)
add_compile_options(-fsanitize=undefined -fno-omit-frame-pointer)
add_link_options(-fsanitize=undefined)
endif()
if(ENABLE_TSAN)
add_compile_options(-fsanitize=thread)
add_link_options(-fsanitize=thread)
endif()
```
## フレーキーテストのガードレール
- 同期に `sleep` を使用しないでください。条件変数またはラッチを使用してください。
- 一時ディレクトリをテストごとに一意にし、常にクリーンアップしてください。
- ユニットテストで実際の時間、ネットワーク、ファイルシステムの依存関係を避けてください。
- ランダム化された入力には決定論的シードを使用してください。
## ベストプラクティス
### すべきこと
- テストを決定論的かつ分離されたものに保つ
- グローバル変数よりも依存性注入を優先する
- 前提条件には `ASSERT_*` を使用し、複数のチェックには `EXPECT_*` を使用する
- CTest ラベルまたはディレクトリでユニットテストと統合テストを分離する
- メモリとレース検出のために CI でサニタイザーを実行する
### すべきでないこと
- ユニットテストで実際の時間やネットワークに依存しない
- 条件変数を使用できる場合、同期としてスリープを使用しない
- 単純な値オブジェクトをオーバーモックしない
- 重要でないログに脆弱な文字列マッチングを使用しない
### よくある落とし穴
- **固定一時パスの使用** → テストごとに一意の一時ディレクトリを生成し、クリーンアップします。
- **ウォールクロック時間への依存** → クロックを注入するか、偽の時間ソースを使用します。
- **フレーキーな並行性テスト** → 条件変数/ラッチと境界付き待機を使用します。
- **隠れたグローバル状態** → フィクスチャでグローバル状態をリセットするか、グローバル変数を削除します。
- **オーバーモック** → ステートフルな動作にはフェイクを優先し、相互作用のみをモックします。
- **サニタイザー実行の欠落** → CI に ASan/UBSan/TSan ビルドを追加します。
- **デバッグのみのビルドでのカバレッジ** → カバレッジターゲットが一貫したフラグを使用することを確認します。
## オプションの付録: ファジングとプロパティテスト
プロジェクトがすでに LLVM/libFuzzer またはプロパティテストライブラリをサポートしている場合にのみ使用してください。
- **libFuzzer**: 最小限の I/O で純粋関数に最適です。
- **RapidCheck**: 不変条件を検証するプロパティベースのテストです。
最小限の libFuzzer ハーネス(擬似コード: ParseConfig を置き換えてください):
```cpp
#include <cstddef>
#include <cstdint>
#include <string>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
std::string input(reinterpret_cast<const char *>(data), size);
// ParseConfig(input); // プロジェクト関数
return 0;
}
```
## GoogleTest の代替
- **Catch2**: ヘッダーオンリー、表現力豊かなマッチャー
- **doctest**: 軽量、最小限のコンパイルオーバーヘッド

View File

@@ -1,733 +0,0 @@
---
name: django-patterns
description: Django architecture patterns, REST API design with DRF, ORM best practices, caching, signals, middleware, and production-grade Django apps.
---
# Django 開発パターン
スケーラブルで保守可能なアプリケーションのための本番グレードのDjangoアーキテクチャパターン。
## いつ有効化するか
- Djangoウェブアプリケーションを構築するとき
- Django REST Framework APIを設計するとき
- Django ORMとモデルを扱うとき
- Djangoプロジェクト構造を設定するとき
- キャッシング、シグナル、ミドルウェアを実装するとき
## プロジェクト構造
### 推奨レイアウト
```
myproject/
├── config/
│ ├── __init__.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py # 基本設定
│ │ ├── development.py # 開発設定
│ │ ├── production.py # 本番設定
│ │ └── test.py # テスト設定
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── manage.py
└── apps/
├── __init__.py
├── users/
│ ├── __init__.py
│ ├── models.py
│ ├── views.py
│ ├── serializers.py
│ ├── urls.py
│ ├── permissions.py
│ ├── filters.py
│ ├── services.py
│ └── tests/
└── products/
└── ...
```
### 分割設定パターン
```python
# config/settings/base.py
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
SECRET_KEY = env('DJANGO_SECRET_KEY')
DEBUG = False
ALLOWED_HOSTS = []
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'corsheaders',
# Local apps
'apps.users',
'apps.products',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
WSGI_APPLICATION = 'config.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': env('DB_NAME'),
'USER': env('DB_USER'),
'PASSWORD': env('DB_PASSWORD'),
'HOST': env('DB_HOST'),
'PORT': env('DB_PORT', default='5432'),
}
}
# config/settings/development.py
from .base import *
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
DATABASES['default']['NAME'] = 'myproject_dev'
INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# config/settings/production.py
from .base import *
DEBUG = False
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# ロギング
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'WARNING',
'class': 'logging.FileHandler',
'filename': '/var/log/django/django.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'WARNING',
'propagate': True,
},
},
}
```
## モデル設計パターン
### モデルのベストプラクティス
```python
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.core.validators import MinValueValidator, MaxValueValidator
class User(AbstractUser):
"""AbstractUserを拡張したカスタムユーザーモデル。"""
email = models.EmailField(unique=True)
phone = models.CharField(max_length=20, blank=True)
birth_date = models.DateField(null=True, blank=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
class Meta:
db_table = 'users'
verbose_name = 'user'
verbose_name_plural = 'users'
ordering = ['-date_joined']
def __str__(self):
return self.email
def get_full_name(self):
return f"{self.first_name} {self.last_name}".strip()
class Product(models.Model):
"""適切なフィールド設定を持つProductモデル。"""
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True, max_length=250)
description = models.TextField(blank=True)
price = models.DecimalField(
max_digits=10,
decimal_places=2,
validators=[MinValueValidator(0)]
)
stock = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
category = models.ForeignKey(
'Category',
on_delete=models.CASCADE,
related_name='products'
)
tags = models.ManyToManyField('Tag', blank=True, related_name='products')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'products'
ordering = ['-created_at']
indexes = [
models.Index(fields=['slug']),
models.Index(fields=['-created_at']),
models.Index(fields=['category', 'is_active']),
]
constraints = [
models.CheckConstraint(
check=models.Q(price__gte=0),
name='price_non_negative'
)
]
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
```
### QuerySetのベストプラクティス
```python
from django.db import models
class ProductQuerySet(models.QuerySet):
"""Productモデルのカスタム QuerySet。"""
def active(self):
"""アクティブな製品のみを返す。"""
return self.filter(is_active=True)
def with_category(self):
"""N+1クエリを避けるために関連カテゴリを選択。"""
return self.select_related('category')
def with_tags(self):
"""多対多リレーションシップのためにタグをプリフェッチ。"""
return self.prefetch_related('tags')
def in_stock(self):
"""在庫が0より大きい製品を返す。"""
return self.filter(stock__gt=0)
def search(self, query):
"""名前または説明で製品を検索。"""
return self.filter(
models.Q(name__icontains=query) |
models.Q(description__icontains=query)
)
class Product(models.Model):
# ... フィールド ...
objects = ProductQuerySet.as_manager() # カスタムQuerySetを使用
# 使用例
Product.objects.active().with_category().in_stock()
```
### マネージャーメソッド
```python
class ProductManager(models.Manager):
"""複雑なクエリ用のカスタムマネージャー。"""
def get_or_none(self, **kwargs):
"""DoesNotExistの代わりにオブジェクトまたはNoneを返す。"""
try:
return self.get(**kwargs)
except self.model.DoesNotExist:
return None
def create_with_tags(self, name, price, tag_names):
"""関連タグを持つ製品を作成。"""
product = self.create(name=name, price=price)
tags = [Tag.objects.get_or_create(name=name)[0] for name in tag_names]
product.tags.set(tags)
return product
def bulk_update_stock(self, product_ids, quantity):
"""複数の製品の在庫を一括更新。"""
return self.filter(id__in=product_ids).update(stock=quantity)
# モデル内
class Product(models.Model):
# ... フィールド ...
custom = ProductManager()
```
## Django REST Frameworkパターン
### シリアライザーパターン
```python
from rest_framework import serializers
from django.contrib.auth.password_validation import validate_password
from .models import Product, User
class ProductSerializer(serializers.ModelSerializer):
"""Productモデルのシリアライザー。"""
category_name = serializers.CharField(source='category.name', read_only=True)
average_rating = serializers.FloatField(read_only=True)
discount_price = serializers.SerializerMethodField()
class Meta:
model = Product
fields = [
'id', 'name', 'slug', 'description', 'price',
'discount_price', 'stock', 'category_name',
'average_rating', 'created_at'
]
read_only_fields = ['id', 'slug', 'created_at']
def get_discount_price(self, obj):
"""該当する場合は割引価格を計算。"""
if hasattr(obj, 'discount') and obj.discount:
return obj.price * (1 - obj.discount.percent / 100)
return obj.price
def validate_price(self, value):
"""価格が非負であることを確認。"""
if value < 0:
raise serializers.ValidationError("Price cannot be negative.")
return value
class ProductCreateSerializer(serializers.ModelSerializer):
"""製品作成用のシリアライザー。"""
class Meta:
model = Product
fields = ['name', 'description', 'price', 'stock', 'category']
def validate(self, data):
"""複数フィールドのカスタム検証。"""
if data['price'] > 10000 and data['stock'] > 100:
raise serializers.ValidationError(
"Cannot have high-value products with large stock."
)
return data
class UserRegistrationSerializer(serializers.ModelSerializer):
"""ユーザー登録用のシリアライザー。"""
password = serializers.CharField(
write_only=True,
required=True,
validators=[validate_password],
style={'input_type': 'password'}
)
password_confirm = serializers.CharField(write_only=True, style={'input_type': 'password'})
class Meta:
model = User
fields = ['email', 'username', 'password', 'password_confirm']
def validate(self, data):
"""パスワードが一致することを検証。"""
if data['password'] != data['password_confirm']:
raise serializers.ValidationError({
"password_confirm": "Password fields didn't match."
})
return data
def create(self, validated_data):
"""ハッシュ化されたパスワードでユーザーを作成。"""
validated_data.pop('password_confirm')
password = validated_data.pop('password')
user = User.objects.create(**validated_data)
user.set_password(password)
user.save()
return user
```
### ViewSetパターン
```python
from rest_framework import viewsets, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from django_filters.rest_framework import DjangoFilterBackend
from .models import Product
from .serializers import ProductSerializer, ProductCreateSerializer
from .permissions import IsOwnerOrReadOnly
from .filters import ProductFilter
from .services import ProductService
class ProductViewSet(viewsets.ModelViewSet):
"""Productモデル用のViewSet。"""
queryset = Product.objects.select_related('category').prefetch_related('tags')
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_class = ProductFilter
search_fields = ['name', 'description']
ordering_fields = ['price', 'created_at', 'name']
ordering = ['-created_at']
def get_serializer_class(self):
"""アクションに基づいて適切なシリアライザーを返す。"""
if self.action == 'create':
return ProductCreateSerializer
return ProductSerializer
def perform_create(self, serializer):
"""ユーザーコンテキストで保存。"""
serializer.save(created_by=self.request.user)
@action(detail=False, methods=['get'])
def featured(self, request):
"""注目の製品を返す。"""
featured = self.queryset.filter(is_featured=True)[:10]
serializer = self.get_serializer(featured, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def purchase(self, request, pk=None):
"""製品を購入。"""
product = self.get_object()
service = ProductService()
result = service.purchase(product, request.user)
return Response(result, status=status.HTTP_201_CREATED)
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
def my_products(self, request):
"""現在のユーザーが作成した製品を返す。"""
products = self.queryset.filter(created_by=request.user)
page = self.paginate_queryset(products)
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
```
### カスタムアクション
```python
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def add_to_cart(request):
"""製品をユーザーのカートに追加。"""
product_id = request.data.get('product_id')
quantity = request.data.get('quantity', 1)
try:
product = Product.objects.get(id=product_id)
except Product.DoesNotExist:
return Response(
{'error': 'Product not found'},
status=status.HTTP_404_NOT_FOUND
)
cart, _ = Cart.objects.get_or_create(user=request.user)
CartItem.objects.create(
cart=cart,
product=product,
quantity=quantity
)
return Response({'message': 'Added to cart'}, status=status.HTTP_201_CREATED)
```
## サービスレイヤーパターン
```python
# apps/orders/services.py
from typing import Optional
from django.db import transaction
from .models import Order, OrderItem
class OrderService:
"""注文関連のビジネスロジック用のサービスレイヤー。"""
@staticmethod
@transaction.atomic
def create_order(user, cart: Cart) -> Order:
"""カートから注文を作成。"""
order = Order.objects.create(
user=user,
total_price=cart.total_price
)
for item in cart.items.all():
OrderItem.objects.create(
order=order,
product=item.product,
quantity=item.quantity,
price=item.product.price
)
# カートをクリア
cart.items.all().delete()
return order
@staticmethod
def process_payment(order: Order, payment_data: dict) -> bool:
"""注文の支払いを処理。"""
# 決済ゲートウェイとの統合
payment = PaymentGateway.charge(
amount=order.total_price,
token=payment_data['token']
)
if payment.success:
order.status = Order.Status.PAID
order.save()
# 確認メールを送信
OrderService.send_confirmation_email(order)
return True
return False
@staticmethod
def send_confirmation_email(order: Order):
"""注文確認メールを送信。"""
# メール送信ロジック
pass
```
## キャッシング戦略
### ビューレベルのキャッシング
```python
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
@method_decorator(cache_page(60 * 15), name='dispatch') # 15分
class ProductListView(generic.ListView):
model = Product
template_name = 'products/list.html'
context_object_name = 'products'
```
### テンプレートフラグメントのキャッシング
```django
{% load cache %}
{% cache 500 sidebar %}
... 高コストなサイドバーコンテンツ ...
{% endcache %}
```
### 低レベルキャッシング
```python
from django.core.cache import cache
def get_featured_products():
"""キャッシング付きで注目の製品を取得。"""
cache_key = 'featured_products'
products = cache.get(cache_key)
if products is None:
products = list(Product.objects.filter(is_featured=True))
cache.set(cache_key, products, timeout=60 * 15) # 15分
return products
```
### QuerySetのキャッシング
```python
from django.core.cache import cache
def get_popular_categories():
cache_key = 'popular_categories'
categories = cache.get(cache_key)
if categories is None:
categories = list(Category.objects.annotate(
product_count=Count('products')
).filter(product_count__gt=10).order_by('-product_count')[:20])
cache.set(cache_key, categories, timeout=60 * 60) # 1時間
return categories
```
## シグナル
### シグナルパターン
```python
# apps/users/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model
from .models import Profile
User = get_user_model()
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
"""ユーザーが作成されたときにプロファイルを作成。"""
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
"""ユーザーが保存されたときにプロファイルを保存。"""
instance.profile.save()
# apps/users/apps.py
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.users'
def ready(self):
"""アプリが準備できたらシグナルをインポート。"""
import apps.users.signals
```
## ミドルウェア
### カスタムミドルウェア
```python
# middleware/active_user_middleware.py
import time
from django.utils.deprecation import MiddlewareMixin
class ActiveUserMiddleware(MiddlewareMixin):
"""アクティブユーザーを追跡するミドルウェア。"""
def process_request(self, request):
"""受信リクエストを処理。"""
if request.user.is_authenticated:
# 最終アクティブ時刻を更新
request.user.last_active = timezone.now()
request.user.save(update_fields=['last_active'])
class RequestLoggingMiddleware(MiddlewareMixin):
"""リクエストロギング用のミドルウェア。"""
def process_request(self, request):
"""リクエスト開始時刻をログ。"""
request.start_time = time.time()
def process_response(self, request, response):
"""リクエスト期間をログ。"""
if hasattr(request, 'start_time'):
duration = time.time() - request.start_time
logger.info(f'{request.method} {request.path} - {response.status_code} - {duration:.3f}s')
return response
```
## パフォーマンス最適化
### N+1クエリの防止
```python
# Bad - N+1クエリ
products = Product.objects.all()
for product in products:
print(product.category.name) # 各製品に対して個別のクエリ
# Good - select_relatedで単一クエリ
products = Product.objects.select_related('category').all()
for product in products:
print(product.category.name)
# Good - 多対多のためのprefetch
products = Product.objects.prefetch_related('tags').all()
for product in products:
for tag in product.tags.all():
print(tag.name)
```
### データベースインデックス
```python
class Product(models.Model):
name = models.CharField(max_length=200, db_index=True)
slug = models.SlugField(unique=True)
category = models.ForeignKey('Category', on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['name']),
models.Index(fields=['-created_at']),
models.Index(fields=['category', 'created_at']),
]
```
### 一括操作
```python
# 一括作成
Product.objects.bulk_create([
Product(name=f'Product {i}', price=10.00)
for i in range(1000)
])
# 一括更新
products = Product.objects.all()[:100]
for product in products:
product.is_active = True
Product.objects.bulk_update(products, ['is_active'])
# 一括削除
Product.objects.filter(stock=0).delete()
```
## クイックリファレンス
| パターン | 説明 |
|---------|-------------|
| 分割設定 | dev/prod/test設定の分離 |
| カスタムQuerySet | 再利用可能なクエリメソッド |
| サービスレイヤー | ビジネスロジックの分離 |
| ViewSet | REST APIエンドポイント |
| シリアライザー検証 | リクエスト/レスポンス変換 |
| select_related | 外部キー最適化 |
| prefetch_related | 多対多最適化 |
| キャッシュファースト | 高コスト操作のキャッシング |
| シグナル | イベント駆動アクション |
| ミドルウェア | リクエスト/レスポンス処理 |
**覚えておいてください**: Djangoは多くのショートカットを提供しますが、本番アプリケーションでは、構造と組織が簡潔なコードよりも重要です。保守性を重視して構築してください。

View File

@@ -1,592 +0,0 @@
---
name: django-security
description: Django security best practices, authentication, authorization, CSRF protection, SQL injection prevention, XSS prevention, and secure deployment configurations.
---
# Django セキュリティベストプラクティス
一般的な脆弱性から保護するためのDjangoアプリケーションの包括的なセキュリティガイドライン。
## いつ有効化するか
- Django認証と認可を設定するとき
- ユーザー権限とロールを実装するとき
- 本番セキュリティ設定を構成するとき
- Djangoアプリケーションのセキュリティ問題をレビューするとき
- Djangoアプリケーションを本番環境にデプロイするとき
## 核となるセキュリティ設定
### 本番設定の構成
```python
# settings/production.py
import os
DEBUG = False # 重要: 本番環境では絶対にTrueにしない
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
# セキュリティヘッダー
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000 # 1年
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
# HTTPSとクッキー
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SAMESITE = 'Lax'
# シークレットキー(環境変数経由で設定する必要があります)
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
if not SECRET_KEY:
raise ImproperlyConfigured('DJANGO_SECRET_KEY environment variable is required')
# パスワード検証
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 12,
}
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
```
## 認証
### カスタムユーザーモデル
```python
# apps/users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
"""より良いセキュリティのためのカスタムユーザーモデル。"""
email = models.EmailField(unique=True)
phone = models.CharField(max_length=20, blank=True)
USERNAME_FIELD = 'email' # メールをユーザー名として使用
REQUIRED_FIELDS = ['username']
class Meta:
db_table = 'users'
verbose_name = 'User'
verbose_name_plural = 'Users'
def __str__(self):
return self.email
# settings/base.py
AUTH_USER_MODEL = 'users.User'
```
### パスワードハッシング
```python
# デフォルトではDjangoはPBKDF2を使用。より強力なセキュリティのために:
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
]
```
### セッション管理
```python
# セッション設定
SESSION_ENGINE = 'django.contrib.sessions.backends.cache' # または 'db'
SESSION_CACHE_ALIAS = 'default'
SESSION_COOKIE_AGE = 3600 * 24 * 7 # 1週間
SESSION_SAVE_EVERY_REQUEST = False
SESSION_EXPIRE_AT_BROWSER_CLOSE = False # より良いUXですが、セキュリティは低い
```
## 認可
### パーミッション
```python
# models.py
from django.db import models
from django.contrib.auth.models import Permission
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
permissions = [
('can_publish', 'Can publish posts'),
('can_edit_others', 'Can edit posts of others'),
]
def user_can_edit(self, user):
"""ユーザーがこの投稿を編集できるかチェック。"""
return self.author == user or user.has_perm('app.can_edit_others')
# views.py
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views.generic import UpdateView
class PostUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
model = Post
permission_required = 'app.can_edit_others'
raise_exception = True # リダイレクトの代わりに403を返す
def get_queryset(self):
"""ユーザーが自分の投稿のみを編集できるようにする。"""
return Post.objects.filter(author=self.request.user)
```
### カスタムパーミッション
```python
# permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""所有者のみがオブジェクトを編集できるようにする。"""
def has_object_permission(self, request, view, obj):
# 読み取り権限は任意のリクエストに許可
if request.method in permissions.SAFE_METHODS:
return True
# 書き込み権限は所有者のみ
return obj.author == request.user
class IsAdminOrReadOnly(permissions.BasePermission):
"""管理者は何でもでき、他は読み取りのみ。"""
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return request.user and request.user.is_staff
class IsVerifiedUser(permissions.BasePermission):
"""検証済みユーザーのみを許可。"""
def has_permission(self, request, view):
return request.user and request.user.is_authenticated and request.user.is_verified
```
### ロールベースアクセス制御(RBAC)
```python
# models.py
from django.contrib.auth.models import AbstractUser, Group
class User(AbstractUser):
ROLE_CHOICES = [
('admin', 'Administrator'),
('moderator', 'Moderator'),
('user', 'Regular User'),
]
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='user')
def is_admin(self):
return self.role == 'admin' or self.is_superuser
def is_moderator(self):
return self.role in ['admin', 'moderator']
# Mixin
class AdminRequiredMixin:
"""管理者ロールを要求するMixin。"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.is_admin():
from django.core.exceptions import PermissionDenied
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
```
## SQLインジェクション防止
### Django ORM保護
```python
# GOOD: Django ORMは自動的にパラメータをエスケープ
def get_user(username):
return User.objects.get(username=username) # 安全
# GOOD: raw()でパラメータを使用
def search_users(query):
return User.objects.raw('SELECT * FROM users WHERE username = %s', [query])
# BAD: ユーザー入力を直接補間しない
def get_user_bad(username):
return User.objects.raw(f'SELECT * FROM users WHERE username = {username}') # 脆弱!
# GOOD: 適切なエスケープでfilterを使用
def get_users_by_email(email):
return User.objects.filter(email__iexact=email) # 安全
# GOOD: 複雑なクエリにQオブジェクトを使用
from django.db.models import Q
def search_users_complex(query):
return User.objects.filter(
Q(username__icontains=query) |
Q(email__icontains=query)
) # 安全
```
### raw()での追加セキュリティ
```python
# 生のSQLを使用する必要がある場合は、常にパラメータを使用
User.objects.raw(
'SELECT * FROM users WHERE email = %s AND status = %s',
[user_input_email, status]
)
```
## XSS防止
### テンプレートエスケープ
```django
{# Djangoはデフォルトで変数を自動エスケープ - 安全 #}
{{ user_input }} {# エスケープされたHTML #}
{# 信頼できるコンテンツのみを明示的に安全とマーク #}
{{ trusted_html|safe }} {# エスケープされない #}
{# 安全なHTMLのためにテンプレートフィルタを使用 #}
{{ user_input|escape }} {# デフォルトと同じ #}
{{ user_input|striptags }} {# すべてのHTMLタグを削除 #}
{# JavaScriptエスケープ #}
<script>
var username = {{ username|escapejs }};
</script>
```
### 安全な文字列処理
```python
from django.utils.safestring import mark_safe
from django.utils.html import escape
# BAD: エスケープせずにユーザー入力を安全とマークしない
def render_bad(user_input):
return mark_safe(user_input) # 脆弱!
# GOOD: 最初にエスケープ、次に安全とマーク
def render_good(user_input):
return mark_safe(escape(user_input))
# GOOD: 変数を持つHTMLにformat_htmlを使用
from django.utils.html import format_html
def greet_user(username):
return format_html('<span class="user">{}</span>', escape(username))
```
### HTTPヘッダー
```python
# settings.py
SECURE_CONTENT_TYPE_NOSNIFF = True # MIMEスニッフィングを防止
SECURE_BROWSER_XSS_FILTER = True # XSSフィルタを有効化
X_FRAME_OPTIONS = 'DENY' # クリックジャッキングを防止
# カスタムミドルウェア
from django.conf import settings
class SecurityHeaderMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response['X-Content-Type-Options'] = 'nosniff'
response['X-Frame-Options'] = 'DENY'
response['X-XSS-Protection'] = '1; mode=block'
response['Content-Security-Policy'] = "default-src 'self'"
return response
```
## CSRF保護
### デフォルトCSRF保護
```python
# settings.py - CSRFはデフォルトで有効
CSRF_COOKIE_SECURE = True # HTTPSでのみ送信
CSRF_COOKIE_HTTPONLY = True # JavaScriptアクセスを防止
CSRF_COOKIE_SAMESITE = 'Lax' # 一部のケースでCSRFを防止
CSRF_TRUSTED_ORIGINS = ['https://example.com'] # 信頼されたドメイン
# テンプレート使用
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Submit</button>
</form>
# AJAXリクエスト
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
fetch('/api/endpoint/', {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
```
### ビューの除外(慎重に使用)
```python
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt # 絶対に必要な場合のみ使用!
def webhook_view(request):
# 外部サービスからのWebhook
pass
```
## ファイルアップロードセキュリティ
### ファイル検証
```python
import os
from django.core.exceptions import ValidationError
def validate_file_extension(value):
"""ファイル拡張子を検証。"""
ext = os.path.splitext(value.name)[1]
valid_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.pdf']
if not ext.lower() in valid_extensions:
raise ValidationError('Unsupported file extension.')
def validate_file_size(value):
"""ファイルサイズを検証最大5MB"""
filesize = value.size
if filesize > 5 * 1024 * 1024:
raise ValidationError('File too large. Max size is 5MB.')
# models.py
class Document(models.Model):
file = models.FileField(
upload_to='documents/',
validators=[validate_file_extension, validate_file_size]
)
```
### 安全なファイルストレージ
```python
# settings.py
MEDIA_ROOT = '/var/www/media/'
MEDIA_URL = '/media/'
# 本番環境でメディアに別のドメインを使用
MEDIA_DOMAIN = 'https://media.example.com'
# ユーザーアップロードを直接提供しない
# 静的ファイルにはwhitenoiseまたはCDNを使用
# メディアファイルには別のサーバーまたはS3を使用
```
## APIセキュリティ
### レート制限
```python
# settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day',
'upload': '10/hour',
}
}
# カスタムスロットル
from rest_framework.throttling import UserRateThrottle
class BurstRateThrottle(UserRateThrottle):
scope = 'burst'
rate = '60/min'
class SustainedRateThrottle(UserRateThrottle):
scope = 'sustained'
rate = '1000/day'
```
### API用認証
```python
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
# views.py
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
@api_view(['GET', 'POST'])
@permission_classes([IsAuthenticated])
def protected_view(request):
return Response({'message': 'You are authenticated'})
```
## セキュリティヘッダー
### Content Security Policy
```python
# settings.py
CSP_DEFAULT_SRC = "'self'"
CSP_SCRIPT_SRC = "'self' https://cdn.example.com"
CSP_STYLE_SRC = "'self' 'unsafe-inline'"
CSP_IMG_SRC = "'self' data: https:"
CSP_CONNECT_SRC = "'self' https://api.example.com"
# Middleware
class CSPMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response['Content-Security-Policy'] = (
f"default-src {CSP_DEFAULT_SRC}; "
f"script-src {CSP_SCRIPT_SRC}; "
f"style-src {CSP_STYLE_SRC}; "
f"img-src {CSP_IMG_SRC}; "
f"connect-src {CSP_CONNECT_SRC}"
)
return response
```
## 環境変数
### シークレットの管理
```python
# python-decoupleまたはdjango-environを使用
import environ
env = environ.Env(
# キャスティング、デフォルト値を設定
DEBUG=(bool, False)
)
# .envファイルを読み込む
environ.Env.read_env()
SECRET_KEY = env('DJANGO_SECRET_KEY')
DATABASE_URL = env('DATABASE_URL')
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
# .envファイルこれをコミットしない
DEBUG=False
SECRET_KEY=your-secret-key-here
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
ALLOWED_HOSTS=example.com,www.example.com
```
## セキュリティイベントのログ記録
```python
# settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'WARNING',
'class': 'logging.FileHandler',
'filename': '/var/log/django/security.log',
},
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.security': {
'handlers': ['file', 'console'],
'level': 'WARNING',
'propagate': True,
},
'django.request': {
'handlers': ['file'],
'level': 'ERROR',
'propagate': False,
},
},
}
```
## クイックセキュリティチェックリスト
| チェック | 説明 |
|-------|-------------|
| `DEBUG = False` | 本番環境でDEBUGを決して実行しない |
| HTTPSのみ | SSLを強制、セキュアクッキー |
| 強力なシークレット | SECRET_KEYに環境変数を使用 |
| パスワード検証 | すべてのパスワードバリデータを有効化 |
| CSRF保護 | デフォルトで有効、無効にしない |
| XSS防止 | Djangoは自動エスケープ、ユーザー入力で`|safe`を使用しない |
| SQLインジェクション | ORMを使用、クエリで文字列を連結しない |
| ファイルアップロード | ファイルタイプとサイズを検証 |
| レート制限 | APIエンドポイントをスロットル |
| セキュリティヘッダー | CSP、X-Frame-Options、HSTS |
| ログ記録 | セキュリティイベントをログ |
| 更新 | DjangoとDependenciesを最新に保つ |
**覚えておいてください**: セキュリティは製品ではなく、プロセスです。定期的にセキュリティプラクティスをレビューし、更新してください。

View File

@@ -1,728 +0,0 @@
---
name: django-tdd
description: Django testing strategies with pytest-django, TDD methodology, factory_boy, mocking, coverage, and testing Django REST Framework APIs.
---
# Django テスト駆動開発(TDD)
pytest、factory_boy、Django REST Frameworkを使用したDjangoアプリケーションのテスト駆動開発。
## いつ有効化するか
- 新しいDjangoアプリケーションを書くとき
- Django REST Framework APIを実装するとき
- Djangoモデル、ビュー、シリアライザーをテストするとき
- Djangoプロジェクトのテストインフラを設定するとき
## DjangoのためのTDDワークフロー
### Red-Green-Refactorサイクル
```python
# ステップ1: RED - 失敗するテストを書く
def test_user_creation():
user = User.objects.create_user(email='test@example.com', password='testpass123')
assert user.email == 'test@example.com'
assert user.check_password('testpass123')
assert not user.is_staff
# ステップ2: GREEN - テストを通す
# Userモデルまたはファクトリーを作成
# ステップ3: REFACTOR - テストをグリーンに保ちながら改善
```
## セットアップ
### pytest設定
```ini
# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = config.settings.test
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--reuse-db
--nomigrations
--cov=apps
--cov-report=html
--cov-report=term-missing
--strict-markers
markers =
slow: marks tests as slow
integration: marks tests as integration tests
```
### テスト設定
```python
# config/settings/test.py
from .base import *
DEBUG = True
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
# マイグレーションを無効化して高速化
class DisableMigrations:
def __contains__(self, item):
return True
def __getitem__(self, item):
return None
MIGRATION_MODULES = DisableMigrations()
# より高速なパスワードハッシング
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
# メールバックエンド
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Celeryは常にeager
CELERY_TASK_ALWAYS_EAGER = True
CELERY_TASK_EAGER_PROPAGATES = True
```
### conftest.py
```python
# tests/conftest.py
import pytest
from django.utils import timezone
from django.contrib.auth import get_user_model
User = get_user_model()
@pytest.fixture(autouse=True)
def timezone_settings(settings):
"""一貫したタイムゾーンを確保。"""
settings.TIME_ZONE = 'UTC'
@pytest.fixture
def user(db):
"""テストユーザーを作成。"""
return User.objects.create_user(
email='test@example.com',
password='testpass123',
username='testuser'
)
@pytest.fixture
def admin_user(db):
"""管理者ユーザーを作成。"""
return User.objects.create_superuser(
email='admin@example.com',
password='adminpass123',
username='admin'
)
@pytest.fixture
def authenticated_client(client, user):
"""認証済みクライアントを返す。"""
client.force_login(user)
return client
@pytest.fixture
def api_client():
"""DRF APIクライアントを返す。"""
from rest_framework.test import APIClient
return APIClient()
@pytest.fixture
def authenticated_api_client(api_client, user):
"""認証済みAPIクライアントを返す。"""
api_client.force_authenticate(user=user)
return api_client
```
## Factory Boy
### ファクトリーセットアップ
```python
# tests/factories.py
import factory
from factory import fuzzy
from datetime import datetime, timedelta
from django.contrib.auth import get_user_model
from apps.products.models import Product, Category
User = get_user_model()
class UserFactory(factory.django.DjangoModelFactory):
"""Userモデルのファクトリー。"""
class Meta:
model = User
email = factory.Sequence(lambda n: f"user{n}@example.com")
username = factory.Sequence(lambda n: f"user{n}")
password = factory.PostGenerationMethodCall('set_password', 'testpass123')
first_name = factory.Faker('first_name')
last_name = factory.Faker('last_name')
is_active = True
class CategoryFactory(factory.django.DjangoModelFactory):
"""Categoryモデルのファクトリー。"""
class Meta:
model = Category
name = factory.Faker('word')
slug = factory.LazyAttribute(lambda obj: obj.name.lower())
description = factory.Faker('text')
class ProductFactory(factory.django.DjangoModelFactory):
"""Productモデルのファクトリー。"""
class Meta:
model = Product
name = factory.Faker('sentence', nb_words=3)
slug = factory.LazyAttribute(lambda obj: obj.name.lower().replace(' ', '-'))
description = factory.Faker('text')
price = fuzzy.FuzzyDecimal(10.00, 1000.00, 2)
stock = fuzzy.FuzzyInteger(0, 100)
is_active = True
category = factory.SubFactory(CategoryFactory)
created_by = factory.SubFactory(UserFactory)
@factory.post_generation
def tags(self, create, extracted, **kwargs):
"""製品にタグを追加。"""
if not create:
return
if extracted:
for tag in extracted:
self.tags.add(tag)
```
### ファクトリーの使用
```python
# tests/test_models.py
import pytest
from tests.factories import ProductFactory, UserFactory
def test_product_creation():
"""ファクトリーを使用した製品作成をテスト。"""
product = ProductFactory(price=100.00, stock=50)
assert product.price == 100.00
assert product.stock == 50
assert product.is_active is True
def test_product_with_tags():
"""タグ付き製品をテスト。"""
tags = [TagFactory(name='electronics'), TagFactory(name='new')]
product = ProductFactory(tags=tags)
assert product.tags.count() == 2
def test_multiple_products():
"""複数の製品作成をテスト。"""
products = ProductFactory.create_batch(10)
assert len(products) == 10
```
## モデルテスト
### モデルテスト
```python
# tests/test_models.py
import pytest
from django.core.exceptions import ValidationError
from tests.factories import UserFactory, ProductFactory
class TestUserModel:
"""Userモデルをテスト。"""
def test_create_user(self, db):
"""通常のユーザー作成をテスト。"""
user = UserFactory(email='test@example.com')
assert user.email == 'test@example.com'
assert user.check_password('testpass123')
assert not user.is_staff
assert not user.is_superuser
def test_create_superuser(self, db):
"""スーパーユーザー作成をテスト。"""
user = UserFactory(
email='admin@example.com',
is_staff=True,
is_superuser=True
)
assert user.is_staff
assert user.is_superuser
def test_user_str(self, db):
"""ユーザーの文字列表現をテスト。"""
user = UserFactory(email='test@example.com')
assert str(user) == 'test@example.com'
class TestProductModel:
"""Productモデルをテスト。"""
def test_product_creation(self, db):
"""製品作成をテスト。"""
product = ProductFactory()
assert product.id is not None
assert product.is_active is True
assert product.created_at is not None
def test_product_slug_generation(self, db):
"""自動スラッグ生成をテスト。"""
product = ProductFactory(name='Test Product')
assert product.slug == 'test-product'
def test_product_price_validation(self, db):
"""価格が負の値にならないことをテスト。"""
product = ProductFactory(price=-10)
with pytest.raises(ValidationError):
product.full_clean()
def test_product_manager_active(self, db):
"""アクティブマネージャーメソッドをテスト。"""
ProductFactory.create_batch(5, is_active=True)
ProductFactory.create_batch(3, is_active=False)
active_count = Product.objects.active().count()
assert active_count == 5
def test_product_stock_management(self, db):
"""在庫管理をテスト。"""
product = ProductFactory(stock=10)
product.reduce_stock(5)
product.refresh_from_db()
assert product.stock == 5
with pytest.raises(ValueError):
product.reduce_stock(10) # 在庫不足
```
## ビューテスト
### Djangoビューテスト
```python
# tests/test_views.py
import pytest
from django.urls import reverse
from tests.factories import ProductFactory, UserFactory
class TestProductViews:
"""製品ビューをテスト。"""
def test_product_list(self, client, db):
"""製品リストビューをテスト。"""
ProductFactory.create_batch(10)
response = client.get(reverse('products:list'))
assert response.status_code == 200
assert len(response.context['products']) == 10
def test_product_detail(self, client, db):
"""製品詳細ビューをテスト。"""
product = ProductFactory()
response = client.get(reverse('products:detail', kwargs={'slug': product.slug}))
assert response.status_code == 200
assert response.context['product'] == product
def test_product_create_requires_login(self, client, db):
"""製品作成に認証が必要であることをテスト。"""
response = client.get(reverse('products:create'))
assert response.status_code == 302
assert response.url.startswith('/accounts/login/')
def test_product_create_authenticated(self, authenticated_client, db):
"""認証済みユーザーとしての製品作成をテスト。"""
response = authenticated_client.get(reverse('products:create'))
assert response.status_code == 200
def test_product_create_post(self, authenticated_client, db, category):
"""POSTによる製品作成をテスト。"""
data = {
'name': 'Test Product',
'description': 'A test product',
'price': '99.99',
'stock': 10,
'category': category.id,
}
response = authenticated_client.post(reverse('products:create'), data)
assert response.status_code == 302
assert Product.objects.filter(name='Test Product').exists()
```
## DRF APIテスト
### シリアライザーテスト
```python
# tests/test_serializers.py
import pytest
from rest_framework.exceptions import ValidationError
from apps.products.serializers import ProductSerializer
from tests.factories import ProductFactory
class TestProductSerializer:
"""ProductSerializerをテスト。"""
def test_serialize_product(self, db):
"""製品のシリアライズをテスト。"""
product = ProductFactory()
serializer = ProductSerializer(product)
data = serializer.data
assert data['id'] == product.id
assert data['name'] == product.name
assert data['price'] == str(product.price)
def test_deserialize_product(self, db):
"""製品データのデシリアライズをテスト。"""
data = {
'name': 'Test Product',
'description': 'Test description',
'price': '99.99',
'stock': 10,
'category': 1,
}
serializer = ProductSerializer(data=data)
assert serializer.is_valid()
product = serializer.save()
assert product.name == 'Test Product'
assert float(product.price) == 99.99
def test_price_validation(self, db):
"""価格検証をテスト。"""
data = {
'name': 'Test Product',
'price': '-10.00',
'stock': 10,
}
serializer = ProductSerializer(data=data)
assert not serializer.is_valid()
assert 'price' in serializer.errors
def test_stock_validation(self, db):
"""在庫が負にならないことをテスト。"""
data = {
'name': 'Test Product',
'price': '99.99',
'stock': -5,
}
serializer = ProductSerializer(data=data)
assert not serializer.is_valid()
assert 'stock' in serializer.errors
```
### API ViewSetテスト
```python
# tests/test_api.py
import pytest
from rest_framework.test import APIClient
from rest_framework import status
from django.urls import reverse
from tests.factories import ProductFactory, UserFactory
class TestProductAPI:
"""Product APIエンドポイントをテスト。"""
@pytest.fixture
def api_client(self):
"""APIクライアントを返す。"""
return APIClient()
def test_list_products(self, api_client, db):
"""製品リストをテスト。"""
ProductFactory.create_batch(10)
url = reverse('api:product-list')
response = api_client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.data['count'] == 10
def test_retrieve_product(self, api_client, db):
"""製品取得をテスト。"""
product = ProductFactory()
url = reverse('api:product-detail', kwargs={'pk': product.id})
response = api_client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.data['id'] == product.id
def test_create_product_unauthorized(self, api_client, db):
"""認証なしの製品作成をテスト。"""
url = reverse('api:product-list')
data = {'name': 'Test Product', 'price': '99.99'}
response = api_client.post(url, data)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_create_product_authorized(self, authenticated_api_client, db):
"""認証済みユーザーとしての製品作成をテスト。"""
url = reverse('api:product-list')
data = {
'name': 'Test Product',
'description': 'Test',
'price': '99.99',
'stock': 10,
}
response = authenticated_api_client.post(url, data)
assert response.status_code == status.HTTP_201_CREATED
assert response.data['name'] == 'Test Product'
def test_update_product(self, authenticated_api_client, db):
"""製品更新をテスト。"""
product = ProductFactory(created_by=authenticated_api_client.user)
url = reverse('api:product-detail', kwargs={'pk': product.id})
data = {'name': 'Updated Product'}
response = authenticated_api_client.patch(url, data)
assert response.status_code == status.HTTP_200_OK
assert response.data['name'] == 'Updated Product'
def test_delete_product(self, authenticated_api_client, db):
"""製品削除をテスト。"""
product = ProductFactory(created_by=authenticated_api_client.user)
url = reverse('api:product-detail', kwargs={'pk': product.id})
response = authenticated_api_client.delete(url)
assert response.status_code == status.HTTP_204_NO_CONTENT
def test_filter_products_by_price(self, api_client, db):
"""価格による製品フィルタリングをテスト。"""
ProductFactory(price=50)
ProductFactory(price=150)
url = reverse('api:product-list')
response = api_client.get(url, {'price_min': 100})
assert response.status_code == status.HTTP_200_OK
assert response.data['count'] == 1
def test_search_products(self, api_client, db):
"""製品検索をテスト。"""
ProductFactory(name='Apple iPhone')
ProductFactory(name='Samsung Galaxy')
url = reverse('api:product-list')
response = api_client.get(url, {'search': 'Apple'})
assert response.status_code == status.HTTP_200_OK
assert response.data['count'] == 1
```
## モッキングとパッチング
### 外部サービスのモック
```python
# tests/test_views.py
from unittest.mock import patch, Mock
import pytest
class TestPaymentView:
"""モックされた決済ゲートウェイで決済ビューをテスト。"""
@patch('apps.payments.services.stripe')
def test_successful_payment(self, mock_stripe, client, user, product):
"""モックされたStripeで成功した決済をテスト。"""
# モックを設定
mock_stripe.Charge.create.return_value = {
'id': 'ch_123',
'status': 'succeeded',
'amount': 9999,
}
client.force_login(user)
response = client.post(reverse('payments:process'), {
'product_id': product.id,
'token': 'tok_visa',
})
assert response.status_code == 302
mock_stripe.Charge.create.assert_called_once()
@patch('apps.payments.services.stripe')
def test_failed_payment(self, mock_stripe, client, user, product):
"""失敗した決済をテスト。"""
mock_stripe.Charge.create.side_effect = Exception('Card declined')
client.force_login(user)
response = client.post(reverse('payments:process'), {
'product_id': product.id,
'token': 'tok_visa',
})
assert response.status_code == 302
assert 'error' in response.url
```
### メール送信のモック
```python
# tests/test_email.py
from django.core import mail
from django.test import override_settings
@override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend')
def test_order_confirmation_email(db, order):
"""注文確認メールをテスト。"""
order.send_confirmation_email()
assert len(mail.outbox) == 1
assert order.user.email in mail.outbox[0].to
assert 'Order Confirmation' in mail.outbox[0].subject
```
## 統合テスト
### 完全フローテスト
```python
# tests/test_integration.py
import pytest
from django.urls import reverse
from tests.factories import UserFactory, ProductFactory
class TestCheckoutFlow:
"""完全なチェックアウトフローをテスト。"""
def test_guest_to_purchase_flow(self, client, db):
"""ゲストから購入までの完全なフローをテスト。"""
# ステップ1: 登録
response = client.post(reverse('users:register'), {
'email': 'test@example.com',
'password': 'testpass123',
'password_confirm': 'testpass123',
})
assert response.status_code == 302
# ステップ2: ログイン
response = client.post(reverse('users:login'), {
'email': 'test@example.com',
'password': 'testpass123',
})
assert response.status_code == 302
# ステップ3: 製品を閲覧
product = ProductFactory(price=100)
response = client.get(reverse('products:detail', kwargs={'slug': product.slug}))
assert response.status_code == 200
# ステップ4: カートに追加
response = client.post(reverse('cart:add'), {
'product_id': product.id,
'quantity': 1,
})
assert response.status_code == 302
# ステップ5: チェックアウト
response = client.get(reverse('checkout:review'))
assert response.status_code == 200
assert product.name in response.content.decode()
# ステップ6: 購入を完了
with patch('apps.checkout.services.process_payment') as mock_payment:
mock_payment.return_value = True
response = client.post(reverse('checkout:complete'))
assert response.status_code == 302
assert Order.objects.filter(user__email='test@example.com').exists()
```
## テストのベストプラクティス
### すべきこと
- **ファクトリーを使用**: 手動オブジェクト作成の代わりに
- **テストごとに1つのアサーション**: テストを焦点を絞る
- **説明的なテスト名**: `test_user_cannot_delete_others_post`
- **エッジケースをテスト**: 空の入力、None値、境界条件
- **外部サービスをモック**: 外部APIに依存しない
- **フィクスチャを使用**: 重複を排除
- **パーミッションをテスト**: 認可が機能することを確認
- **テストを高速に保つ**: `--reuse-db``--nomigrations`を使用
### すべきでないこと
- **Django内部をテストしない**: Djangoが機能することを信頼
- **サードパーティコードをテストしない**: ライブラリが機能することを信頼
- **失敗するテストを無視しない**: すべてのテストが通る必要がある
- **テストを依存させない**: テストは任意の順序で実行できるべき
- **過度にモックしない**: 外部依存関係のみをモック
- **プライベートメソッドをテストしない**: パブリックインターフェースをテスト
- **本番データベースを使用しない**: 常にテストデータベースを使用
## カバレッジ
### カバレッジ設定
```bash
# カバレッジでテストを実行
pytest --cov=apps --cov-report=html --cov-report=term-missing
# HTMLレポートを生成
open htmlcov/index.html
```
### カバレッジ目標
| コンポーネント | 目標カバレッジ |
|-----------|-----------------|
| モデル | 90%+ |
| シリアライザー | 85%+ |
| ビュー | 80%+ |
| サービス | 90%+ |
| ユーティリティ | 80%+ |
| 全体 | 80%+ |
## クイックリファレンス
| パターン | 使用法 |
|---------|-------|
| `@pytest.mark.django_db` | データベースアクセスを有効化 |
| `client` | Djangoテストクライアント |
| `api_client` | DRF APIクライアント |
| `factory.create_batch(n)` | 複数のオブジェクトを作成 |
| `patch('module.function')` | 外部依存関係をモック |
| `override_settings` | 設定を一時的に変更 |
| `force_authenticate()` | テストで認証をバイパス |
| `assertRedirects` | リダイレクトをチェック |
| `assertTemplateUsed` | テンプレート使用を検証 |
| `mail.outbox` | 送信されたメールをチェック |
**覚えておいてください**: テストはドキュメントです。良いテストはコードがどのように動作すべきかを説明します。シンプルで、読みやすく、保守可能に保ってください。

View File

@@ -1,460 +0,0 @@
---
name: django-verification
description: Verification loop for Django projects: migrations, linting, tests with coverage, security scans, and deployment readiness checks before release or PR.
---
# Django 検証ループ
PR前、大きな変更後、デプロイ前に実行して、Djangoアプリケーションの品質とセキュリティを確保します。
## フェーズ1: 環境チェック
```bash
# Pythonバージョンを確認
python --version # プロジェクト要件と一致すること
# 仮想環境をチェック
which python
pip list --outdated
# 環境変数を確認
python -c "import os; import environ; print('DJANGO_SECRET_KEY set' if os.environ.get('DJANGO_SECRET_KEY') else 'MISSING: DJANGO_SECRET_KEY')"
```
環境が誤って構成されている場合は、停止して修正します。
## フェーズ2: コード品質とフォーマット
```bash
# 型チェック
mypy . --config-file pyproject.toml
# ruffでリンティング
ruff check . --fix
# blackでフォーマット
black . --check
black . # 自動修正
# インポートソート
isort . --check-only
isort . # 自動修正
# Django固有のチェック
python manage.py check --deploy
```
一般的な問題:
- パブリック関数の型ヒントの欠落
- PEP 8フォーマット違反
- ソートされていないインポート
- 本番構成に残されたデバッグ設定
## フェーズ3: マイグレーション
```bash
# 未適用のマイグレーションをチェック
python manage.py showmigrations
# 欠落しているマイグレーションを作成
python manage.py makemigrations --check
# マイグレーション適用のドライラン
python manage.py migrate --plan
# マイグレーションを適用(テスト環境)
python manage.py migrate
# マイグレーションの競合をチェック
python manage.py makemigrations --merge # 競合がある場合のみ
```
レポート:
- 保留中のマイグレーション数
- マイグレーションの競合
- マイグレーションのないモデルの変更
## フェーズ4: テスト + カバレッジ
```bash
# pytestですべてのテストを実行
pytest --cov=apps --cov-report=html --cov-report=term-missing --reuse-db
# 特定のアプリテストを実行
pytest apps/users/tests/
# マーカーで実行
pytest -m "not slow" # 遅いテストをスキップ
pytest -m integration # 統合テストのみ
# カバレッジレポート
open htmlcov/index.html
```
レポート:
- 合計テスト: X成功、Y失敗、Zスキップ
- 全体カバレッジ: XX%
- アプリごとのカバレッジ内訳
カバレッジ目標:
| コンポーネント | 目標 |
|-----------|--------|
| モデル | 90%+ |
| シリアライザー | 85%+ |
| ビュー | 80%+ |
| サービス | 90%+ |
| 全体 | 80%+ |
## フェーズ5: セキュリティスキャン
```bash
# 依存関係の脆弱性
pip-audit
safety check --full-report
# Djangoセキュリティチェック
python manage.py check --deploy
# Banditセキュリティリンター
bandit -r . -f json -o bandit-report.json
# シークレットスキャンgitleaksがインストールされている場合
gitleaks detect --source . --verbose
# 環境変数チェック
python -c "from django.core.exceptions import ImproperlyConfigured; from django.conf import settings; settings.DEBUG"
```
レポート:
- 見つかった脆弱な依存関係
- セキュリティ構成の問題
- ハードコードされたシークレットが検出
- DEBUGモードのステータス本番環境ではFalseであるべき
## フェーズ6: Django管理コマンド
```bash
# モデルの問題をチェック
python manage.py check
# 静的ファイルを収集
python manage.py collectstatic --noinput --clear
# スーパーユーザーを作成(テストに必要な場合)
echo "from apps.users.models import User; User.objects.create_superuser('admin@example.com', 'admin')" | python manage.py shell
# データベースの整合性
python manage.py check --database default
# キャッシュの検証Redisを使用している場合
python -c "from django.core.cache import cache; cache.set('test', 'value', 10); print(cache.get('test'))"
```
## フェーズ7: パフォーマンスチェック
```bash
# Django Debug Toolbar出力N+1クエリをチェック
# DEBUG=Trueで開発モードで実行してページにアクセス
# SQLパネルで重複クエリを探す
# クエリ数分析
django-admin debugsqlshell # django-debug-sqlshellがインストールされている場合
# 欠落しているインデックスをチェック
python manage.py shell << EOF
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SELECT table_name, index_name FROM information_schema.statistics WHERE table_schema = 'public'")
print(cursor.fetchall())
EOF
```
レポート:
- ページあたりのクエリ数典型的なページで50未満であるべき
- 欠落しているデータベースインデックス
- 重複クエリが検出
## フェーズ8: 静的アセット
```bash
# npm依存関係をチェックnpmを使用している場合
npm audit
npm audit fix
# 静的ファイルをビルドwebpack/viteを使用している場合
npm run build
# 静的ファイルを検証
ls -la staticfiles/
python manage.py findstatic css/style.css
```
## フェーズ9: 構成レビュー
```python
# Pythonシェルで実行して設定を検証
python manage.py shell << EOF
from django.conf import settings
import os
# 重要なチェック
checks = {
'DEBUG is False': not settings.DEBUG,
'SECRET_KEY set': bool(settings.SECRET_KEY and len(settings.SECRET_KEY) > 30),
'ALLOWED_HOSTS set': len(settings.ALLOWED_HOSTS) > 0,
'HTTPS enabled': getattr(settings, 'SECURE_SSL_REDIRECT', False),
'HSTS enabled': getattr(settings, 'SECURE_HSTS_SECONDS', 0) > 0,
'Database configured': settings.DATABASES['default']['ENGINE'] != 'django.db.backends.sqlite3',
}
for check, result in checks.items():
status = '' if result else ''
print(f"{status} {check}")
EOF
```
## フェーズ10: ログ設定
```bash
# ログ出力をテスト
python manage.py shell << EOF
import logging
logger = logging.getLogger('django')
logger.warning('Test warning message')
logger.error('Test error message')
EOF
# ログファイルをチェック(設定されている場合)
tail -f /var/log/django/django.log
```
## フェーズ11: APIドキュメントDRFの場合
```bash
# スキーマを生成
python manage.py generateschema --format openapi-json > schema.json
# スキーマを検証
# schema.jsonが有効なJSONかチェック
python -c "import json; json.load(open('schema.json'))"
# Swagger UIにアクセスdrf-yasgを使用している場合
# ブラウザで http://localhost:8000/swagger/ を訪問
```
## フェーズ12: 差分レビュー
```bash
# 差分統計を表示
git diff --stat
# 実際の変更を表示
git diff
# 変更されたファイルを表示
git diff --name-only
# 一般的な問題をチェック
git diff | grep -i "todo\|fixme\|hack\|xxx"
git diff | grep "print(" # デバッグステートメント
git diff | grep "DEBUG = True" # デバッグモード
git diff | grep "import pdb" # デバッガー
```
チェックリスト:
- デバッグステートメントprint、pdb、breakpoint())なし
- 重要なコードにTODO/FIXMEコメントなし
- ハードコードされたシークレットや資格情報なし
- モデル変更のためのデータベースマイグレーションが含まれている
- 構成の変更が文書化されている
- 外部呼び出しのエラーハンドリングが存在
- 必要な場所でトランザクション管理
## 出力テンプレート
```
DJANGO 検証レポート
==========================
フェーズ1: 環境チェック
✓ Python 3.11.5
✓ 仮想環境がアクティブ
✓ すべての環境変数が設定済み
フェーズ2: コード品質
✓ mypy: 型エラーなし
✗ ruff: 3つの問題が見つかりました自動修正済み
✓ black: フォーマット問題なし
✓ isort: インポートが適切にソート済み
✓ manage.py check: 問題なし
フェーズ3: マイグレーション
✓ 未適用のマイグレーションなし
✓ マイグレーションの競合なし
✓ すべてのモデルにマイグレーションあり
フェーズ4: テスト + カバレッジ
テスト: 247成功、0失敗、5スキップ
カバレッジ:
全体: 87%
users: 92%
products: 89%
orders: 85%
payments: 91%
フェーズ5: セキュリティスキャン
✗ pip-audit: 2つの脆弱性が見つかりました修正が必要
✓ safety check: 問題なし
✓ bandit: セキュリティ問題なし
✓ シークレットが検出されず
✓ DEBUG = False
フェーズ6: Djangoコマンド
✓ collectstatic 完了
✓ データベース整合性OK
✓ キャッシュバックエンド到達可能
フェーズ7: パフォーマンス
✓ N+1クエリが検出されず
✓ データベースインデックスが構成済み
✓ クエリ数が許容範囲
フェーズ8: 静的アセット
✓ npm audit: 脆弱性なし
✓ アセットが正常にビルド
✓ 静的ファイルが収集済み
フェーズ9: 構成
✓ DEBUG = False
✓ SECRET_KEY 構成済み
✓ ALLOWED_HOSTS 設定済み
✓ HTTPS 有効
✓ HSTS 有効
✓ データベース構成済み
フェーズ10: ログ
✓ ログが構成済み
✓ ログファイルが書き込み可能
フェーズ11: APIドキュメント
✓ スキーマ生成済み
✓ Swagger UIアクセス可能
フェーズ12: 差分レビュー
変更されたファイル: 12
+450、-120行
✓ デバッグステートメントなし
✓ ハードコードされたシークレットなし
✓ マイグレーションが含まれる
推奨: ⚠️ デプロイ前にpip-auditの脆弱性を修正してください
次のステップ:
1. 脆弱な依存関係を更新
2. セキュリティスキャンを再実行
3. 最終テストのためにステージングにデプロイ
```
## デプロイ前チェックリスト
- [ ] すべてのテストが成功
- [ ] カバレッジ ≥ 80%
- [ ] セキュリティ脆弱性なし
- [ ] 未適用のマイグレーションなし
- [ ] 本番設定でDEBUG = False
- [ ] SECRET_KEYが適切に構成
- [ ] ALLOWED_HOSTSが正しく設定
- [ ] データベースバックアップが有効
- [ ] 静的ファイルが収集され提供
- [ ] ログが構成され動作中
- [ ] エラー監視Sentryなどが構成済み
- [ ] CDNが構成済み該当する場合
- [ ] Redis/キャッシュバックエンドが構成済み
- [ ] Celeryワーカーが実行中該当する場合
- [ ] HTTPS/SSLが構成済み
- [ ] 環境変数が文書化済み
## 継続的インテグレーション
### GitHub Actionsの例
```yaml
# .github/workflows/django-verification.yml
name: Django Verification
on: [push, pull_request]
jobs:
verify:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Cache pip
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install ruff black mypy pytest pytest-django pytest-cov bandit safety pip-audit
- name: Code quality checks
run: |
ruff check .
black . --check
isort . --check-only
mypy .
- name: Security scan
run: |
bandit -r . -f json -o bandit-report.json
safety check --full-report
pip-audit
- name: Run tests
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
DJANGO_SECRET_KEY: test-secret-key
run: |
pytest --cov=apps --cov-report=xml --cov-report=term-missing
- name: Upload coverage
uses: codecov/codecov-action@v3
```
## クイックリファレンス
| チェック | コマンド |
|-------|---------|
| 環境 | `python --version` |
| 型チェック | `mypy .` |
| リンティング | `ruff check .` |
| フォーマット | `black . --check` |
| マイグレーション | `python manage.py makemigrations --check` |
| テスト | `pytest --cov=apps` |
| セキュリティ | `pip-audit && bandit -r .` |
| Djangoチェック | `python manage.py check --deploy` |
| 静的ファイル収集 | `python manage.py collectstatic --noinput` |
| 差分統計 | `git diff --stat` |
**覚えておいてください**: 自動化された検証は一般的な問題を捕捉しますが、手動でのコードレビューとステージング環境でのテストに代わるものではありません。

View File

@@ -1,227 +0,0 @@
---
name: eval-harness
description: Claude Codeセッションの正式な評価フレームワークで、評価駆動開発EDDの原則を実装します
tools: Read, Write, Edit, Bash, Grep, Glob
---
# Eval Harnessスキル
Claude Codeセッションの正式な評価フレームワークで、評価駆動開発EDDの原則を実装します。
## 哲学
評価駆動開発は評価を「AI開発のユニットテスト」として扱います
- 実装前に期待される動作を定義
- 開発中に継続的に評価を実行
- 変更ごとにリグレッションを追跡
- 信頼性測定にpass@kメトリクスを使用
## 評価タイプ
### 能力評価
Claudeが以前できなかったことができるようになったかをテスト
```markdown
[CAPABILITY EVAL: feature-name]
Task: Claudeが達成すべきことの説明
Success Criteria:
- [ ] 基準1
- [ ] 基準2
- [ ] 基準3
Expected Output: 期待される結果の説明
```
### リグレッション評価
変更が既存の機能を破壊しないことを確認:
```markdown
[REGRESSION EVAL: feature-name]
Baseline: SHAまたはチェックポイント名
Tests:
- existing-test-1: PASS/FAIL
- existing-test-2: PASS/FAIL
- existing-test-3: PASS/FAIL
Result: X/Y passed (previously Y/Y)
```
## 評価者タイプ
### 1. コードベース評価者
コードを使用した決定論的チェック:
```bash
# ファイルに期待されるパターンが含まれているかチェック
grep -q "export function handleAuth" src/auth.ts && echo "PASS" || echo "FAIL"
# テストが成功するかチェック
npm test -- --testPathPattern="auth" && echo "PASS" || echo "FAIL"
# ビルドが成功するかチェック
npm run build && echo "PASS" || echo "FAIL"
```
### 2. モデルベース評価者
Claudeを使用して自由形式の出力を評価
```markdown
[MODEL GRADER PROMPT]
次のコード変更を評価してください:
1. 記述された問題を解決していますか?
2. 構造化されていますか?
3. エッジケースは処理されていますか?
4. エラー処理は適切ですか?
Score: 1-5 (1=poor, 5=excellent)
Reasoning: [説明]
```
### 3. 人間評価者
手動レビューのためにフラグを立てる:
```markdown
[HUMAN REVIEW REQUIRED]
Change: 何が変更されたかの説明
Reason: 人間のレビューが必要な理由
Risk Level: LOW/MEDIUM/HIGH
```
## メトリクス
### pass@k
「k回の試行で少なくとも1回成功」
- pass@1: 最初の試行での成功率
- pass@3: 3回以内の成功
- 一般的な目標: pass@3 > 90%
### pass^k
「k回の試行すべてが成功」
- より高い信頼性の基準
- pass^3: 3回連続成功
- クリティカルパスに使用
## 評価ワークフロー
### 1. 定義(コーディング前)
```markdown
## EVAL DEFINITION: feature-xyz
### Capability Evals
1. 新しいユーザーアカウントを作成できる
2. メール形式を検証できる
3. パスワードを安全にハッシュ化できる
### Regression Evals
1. 既存のログインが引き続き機能する
2. セッション管理が変更されていない
3. ログアウトフローが維持されている
### Success Metrics
- pass@3 > 90% for capability evals
- pass^3 = 100% for regression evals
```
### 2. 実装
定義された評価に合格するコードを書く。
### 3. 評価
```bash
# 能力評価を実行
[各能力評価を実行し、PASS/FAILを記録]
# リグレッション評価を実行
npm test -- --testPathPattern="existing"
# レポートを生成
```
### 4. レポート
```markdown
EVAL REPORT: feature-xyz
========================
Capability Evals:
create-user: PASS (pass@1)
validate-email: PASS (pass@2)
hash-password: PASS (pass@1)
Overall: 3/3 passed
Regression Evals:
login-flow: PASS
session-mgmt: PASS
logout-flow: PASS
Overall: 3/3 passed
Metrics:
pass@1: 67% (2/3)
pass@3: 100% (3/3)
Status: READY FOR REVIEW
```
## 統合パターン
### 実装前
```
/eval define feature-name
```
`.claude/evals/feature-name.md`に評価定義ファイルを作成
### 実装中
```
/eval check feature-name
```
現在の評価を実行してステータスを報告
### 実装後
```
/eval report feature-name
```
完全な評価レポートを生成
## 評価の保存
プロジェクト内に評価を保存:
```
.claude/
evals/
feature-xyz.md # 評価定義
feature-xyz.log # 評価実行履歴
baseline.json # リグレッションベースライン
```
## ベストプラクティス
1. **コーディング前に評価を定義** - 成功基準について明確に考えることを強制
2. **頻繁に評価を実行** - リグレッションを早期に検出
3. **時間経過とともにpass@kを追跡** - 信頼性のトレンドを監視
4. **可能な限りコード評価者を使用** - 決定論的 > 確率的
5. **セキュリティは人間レビュー** - セキュリティチェックを完全に自動化しない
6. **評価を高速に保つ** - 遅い評価は実行されない
7. **コードと一緒に評価をバージョン管理** - 評価はファーストクラスの成果物
## 例:認証の追加
```markdown
## EVAL: add-authentication
### Phase 1: Define (10 min)
Capability Evals:
- [ ] ユーザーはメール/パスワードで登録できる
- [ ] ユーザーは有効な資格情報でログインできる
- [ ] 無効な資格情報は適切なエラーで拒否される
- [ ] セッションはページリロード後も持続する
- [ ] ログアウトはセッションをクリアする
Regression Evals:
- [ ] 公開ルートは引き続きアクセス可能
- [ ] APIレスポンスは変更されていない
- [ ] データベーススキーマは互換性がある
### Phase 2: Implement (varies)
[コードを書く]
### Phase 3: Evaluate
Run: /eval check add-authentication
### Phase 4: Report
EVAL REPORT: add-authentication
==============================
Capability: 5/5 passed (pass@3: 100%)
Regression: 3/3 passed (pass^3: 100%)
Status: SHIP IT
```

View File

@@ -1,631 +0,0 @@
---
name: frontend-patterns
description: React、Next.js、状態管理、パフォーマンス最適化、UIベストプラクティスのためのフロントエンド開発パターン。
---
# フロントエンド開発パターン
React、Next.js、高性能ユーザーインターフェースのためのモダンなフロントエンドパターン。
## コンポーネントパターン
### 継承よりコンポジション
```typescript
// ✅ GOOD: Component composition
interface CardProps {
children: React.ReactNode
variant?: 'default' | 'outlined'
}
export function Card({ children, variant = 'default' }: CardProps) {
return <div className={`card card-${variant}`}>{children}</div>
}
export function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="card-header">{children}</div>
}
export function CardBody({ children }: { children: React.ReactNode }) {
return <div className="card-body">{children}</div>
}
// Usage
<Card>
<CardHeader>Title</CardHeader>
<CardBody>Content</CardBody>
</Card>
```
### 複合コンポーネント
```typescript
interface TabsContextValue {
activeTab: string
setActiveTab: (tab: string) => void
}
const TabsContext = createContext<TabsContextValue | undefined>(undefined)
export function Tabs({ children, defaultTab }: {
children: React.ReactNode
defaultTab: string
}) {
const [activeTab, setActiveTab] = useState(defaultTab)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
{children}
</TabsContext.Provider>
)
}
export function TabList({ children }: { children: React.ReactNode }) {
return <div className="tab-list">{children}</div>
}
export function Tab({ id, children }: { id: string, children: React.ReactNode }) {
const context = useContext(TabsContext)
if (!context) throw new Error('Tab must be used within Tabs')
return (
<button
className={context.activeTab === id ? 'active' : ''}
onClick={() => context.setActiveTab(id)}
>
{children}
</button>
)
}
// Usage
<Tabs defaultTab="overview">
<TabList>
<Tab id="overview">Overview</Tab>
<Tab id="details">Details</Tab>
</TabList>
</Tabs>
```
### レンダープロップパターン
```typescript
interface DataLoaderProps<T> {
url: string
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode
}
export function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [url])
return <>{children(data, loading, error)}</>
}
// Usage
<DataLoader<Market[]> url="/api/markets">
{(markets, loading, error) => {
if (loading) return <Spinner />
if (error) return <Error error={error} />
return <MarketList markets={markets!} />
}}
</DataLoader>
```
## カスタムフックパターン
### 状態管理フック
```typescript
export function useToggle(initialValue = false): [boolean, () => void] {
const [value, setValue] = useState(initialValue)
const toggle = useCallback(() => {
setValue(v => !v)
}, [])
return [value, toggle]
}
// Usage
const [isOpen, toggleOpen] = useToggle()
```
### 非同期データ取得フック
```typescript
interface UseQueryOptions<T> {
onSuccess?: (data: T) => void
onError?: (error: Error) => void
enabled?: boolean
}
export function useQuery<T>(
key: string,
fetcher: () => Promise<T>,
options?: UseQueryOptions<T>
) {
const [data, setData] = useState<T | null>(null)
const [error, setError] = useState<Error | null>(null)
const [loading, setLoading] = useState(false)
const refetch = useCallback(async () => {
setLoading(true)
setError(null)
try {
const result = await fetcher()
setData(result)
options?.onSuccess?.(result)
} catch (err) {
const error = err as Error
setError(error)
options?.onError?.(error)
} finally {
setLoading(false)
}
}, [fetcher, options])
useEffect(() => {
if (options?.enabled !== false) {
refetch()
}
}, [key, refetch, options?.enabled])
return { data, error, loading, refetch }
}
// Usage
const { data: markets, loading, error, refetch } = useQuery(
'markets',
() => fetch('/api/markets').then(r => r.json()),
{
onSuccess: data => console.log('Fetched', data.length, 'markets'),
onError: err => console.error('Failed:', err)
}
)
```
### デバウンスフック
```typescript
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(handler)
}, [value, delay])
return debouncedValue
}
// Usage
const [searchQuery, setSearchQuery] = useState('')
const debouncedQuery = useDebounce(searchQuery, 500)
useEffect(() => {
if (debouncedQuery) {
performSearch(debouncedQuery)
}
}, [debouncedQuery])
```
## 状態管理パターン
### Context + Reducerパターン
```typescript
interface State {
markets: Market[]
selectedMarket: Market | null
loading: boolean
}
type Action =
| { type: 'SET_MARKETS'; payload: Market[] }
| { type: 'SELECT_MARKET'; payload: Market }
| { type: 'SET_LOADING'; payload: boolean }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'SET_MARKETS':
return { ...state, markets: action.payload }
case 'SELECT_MARKET':
return { ...state, selectedMarket: action.payload }
case 'SET_LOADING':
return { ...state, loading: action.payload }
default:
return state
}
}
const MarketContext = createContext<{
state: State
dispatch: Dispatch<Action>
} | undefined>(undefined)
export function MarketProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, {
markets: [],
selectedMarket: null,
loading: false
})
return (
<MarketContext.Provider value={{ state, dispatch }}>
{children}
</MarketContext.Provider>
)
}
export function useMarkets() {
const context = useContext(MarketContext)
if (!context) throw new Error('useMarkets must be used within MarketProvider')
return context
}
```
## パフォーマンス最適化
### メモ化
```typescript
// ✅ useMemo for expensive computations
const sortedMarkets = useMemo(() => {
return markets.sort((a, b) => b.volume - a.volume)
}, [markets])
// ✅ useCallback for functions passed to children
const handleSearch = useCallback((query: string) => {
setSearchQuery(query)
}, [])
// ✅ React.memo for pure components
export const MarketCard = React.memo<MarketCardProps>(({ market }) => {
return (
<div className="market-card">
<h3>{market.name}</h3>
<p>{market.description}</p>
</div>
)
})
```
### コード分割と遅延読み込み
```typescript
import { lazy, Suspense } from 'react'
// ✅ Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart'))
const ThreeJsBackground = lazy(() => import('./ThreeJsBackground'))
export function Dashboard() {
return (
<div>
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={data} />
</Suspense>
<Suspense fallback={null}>
<ThreeJsBackground />
</Suspense>
</div>
)
}
```
### 長いリストの仮想化
```typescript
import { useVirtualizer } from '@tanstack/react-virtual'
export function VirtualMarketList({ markets }: { markets: Market[] }) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: markets.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100, // Estimated row height
overscan: 5 // Extra items to render
})
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative'
}}
>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`
}}
>
<MarketCard market={markets[virtualRow.index]} />
</div>
))}
</div>
</div>
)
}
```
## フォーム処理パターン
### バリデーション付き制御フォーム
```typescript
interface FormData {
name: string
description: string
endDate: string
}
interface FormErrors {
name?: string
description?: string
endDate?: string
}
export function CreateMarketForm() {
const [formData, setFormData] = useState<FormData>({
name: '',
description: '',
endDate: ''
})
const [errors, setErrors] = useState<FormErrors>({})
const validate = (): boolean => {
const newErrors: FormErrors = {}
if (!formData.name.trim()) {
newErrors.name = 'Name is required'
} else if (formData.name.length > 200) {
newErrors.name = 'Name must be under 200 characters'
}
if (!formData.description.trim()) {
newErrors.description = 'Description is required'
}
if (!formData.endDate) {
newErrors.endDate = 'End date is required'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!validate()) return
try {
await createMarket(formData)
// Success handling
} catch (error) {
// Error handling
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={formData.name}
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Market name"
/>
{errors.name && <span className="error">{errors.name}</span>}
{/* Other fields */}
<button type="submit">Create Market</button>
</form>
)
}
```
## エラーバウンダリパターン
```typescript
interface ErrorBoundaryState {
hasError: boolean
error: Error | null
}
export class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
ErrorBoundaryState
> {
state: ErrorBoundaryState = {
hasError: false,
error: null
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error boundary caught:', error, errorInfo)
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
)
}
return this.props.children
}
}
// Usage
<ErrorBoundary>
<App />
</ErrorBoundary>
```
## アニメーションパターン
### Framer Motionアニメーション
```typescript
import { motion, AnimatePresence } from 'framer-motion'
// ✅ List animations
export function AnimatedMarketList({ markets }: { markets: Market[] }) {
return (
<AnimatePresence>
{markets.map(market => (
<motion.div
key={market.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
<MarketCard market={market} />
</motion.div>
))}
</AnimatePresence>
)
}
// ✅ Modal animations
export function Modal({ isOpen, onClose, children }: ModalProps) {
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
className="modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
<motion.div
className="modal-content"
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
)
}
```
## アクセシビリティパターン
### キーボードナビゲーション
```typescript
export function Dropdown({ options, onSelect }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false)
const [activeIndex, setActiveIndex] = useState(0)
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setActiveIndex(i => Math.min(i + 1, options.length - 1))
break
case 'ArrowUp':
e.preventDefault()
setActiveIndex(i => Math.max(i - 1, 0))
break
case 'Enter':
e.preventDefault()
onSelect(options[activeIndex])
setIsOpen(false)
break
case 'Escape':
setIsOpen(false)
break
}
}
return (
<div
role="combobox"
aria-expanded={isOpen}
aria-haspopup="listbox"
onKeyDown={handleKeyDown}
>
{/* Dropdown implementation */}
</div>
)
}
```
### フォーカス管理
```typescript
export function Modal({ isOpen, onClose, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null)
const previousFocusRef = useRef<HTMLElement | null>(null)
useEffect(() => {
if (isOpen) {
// Save currently focused element
previousFocusRef.current = document.activeElement as HTMLElement
// Focus modal
modalRef.current?.focus()
} else {
// Restore focus when closing
previousFocusRef.current?.focus()
}
}, [isOpen])
return isOpen ? (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
tabIndex={-1}
onKeyDown={e => e.key === 'Escape' && onClose()}
>
{children}
</div>
) : null
}
```
**覚えておいてください**: モダンなフロントエンドパターンにより、保守可能で高性能なユーザーインターフェースを実装できます。プロジェクトの複雑さに適したパターンを選択してください。

View File

@@ -1,673 +0,0 @@
---
name: golang-patterns
description: 堅牢で効率的かつ保守可能なGoアプリケーションを構築するための慣用的なGoパターン、ベストプラクティス、規約。
---
# Go開発パターン
堅牢で効率的かつ保守可能なアプリケーションを構築するための慣用的なGoパターンとベストプラクティス。
## いつ有効化するか
- 新しいGoコードを書くとき
- Goコードをレビューするとき
- 既存のGoコードをリファクタリングするとき
- Goパッケージ/モジュールを設計するとき
## 核となる原則
### 1. シンプルさと明確さ
Goは巧妙さよりもシンプルさを好みます。コードは明白で読みやすいものであるべきです。
```go
// Good: Clear and direct
func GetUser(id string) (*User, error) {
user, err := db.FindUser(id)
if err != nil {
return nil, fmt.Errorf("get user %s: %w", id, err)
}
return user, nil
}
// Bad: Overly clever
func GetUser(id string) (*User, error) {
return func() (*User, error) {
if u, e := db.FindUser(id); e == nil {
return u, nil
} else {
return nil, e
}
}()
}
```
### 2. ゼロ値を有用にする
型を設計する際、そのゼロ値が初期化なしですぐに使用できるようにします。
```go
// Good: Zero value is useful
type Counter struct {
mu sync.Mutex
count int // zero value is 0, ready to use
}
func (c *Counter) Inc() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
// Good: bytes.Buffer works with zero value
var buf bytes.Buffer
buf.WriteString("hello")
// Bad: Requires initialization
type BadCounter struct {
counts map[string]int // nil map will panic
}
```
### 3. インターフェースを受け取り、構造体を返す
関数はインターフェースパラメータを受け取り、具体的な型を返すべきです。
```go
// Good: Accepts interface, returns concrete type
func ProcessData(r io.Reader) (*Result, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return &Result{Data: data}, nil
}
// Bad: Returns interface (hides implementation details unnecessarily)
func ProcessData(r io.Reader) (io.Reader, error) {
// ...
}
```
## エラーハンドリングパターン
### コンテキスト付きエラーラッピング
```go
// Good: Wrap errors with context
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load config %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config %s: %w", path, err)
}
return &cfg, nil
}
```
### カスタムエラー型
```go
// Define domain-specific errors
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
// Sentinel errors for common cases
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
```
### errors.IsとErrors.Asを使用したエラーチェック
```go
func HandleError(err error) {
// Check for specific error
if errors.Is(err, sql.ErrNoRows) {
log.Println("No records found")
return
}
// Check for error type
var validationErr *ValidationError
if errors.As(err, &validationErr) {
log.Printf("Validation error on field %s: %s",
validationErr.Field, validationErr.Message)
return
}
// Unknown error
log.Printf("Unexpected error: %v", err)
}
```
### エラーを決して無視しない
```go
// Bad: Ignoring error with blank identifier
result, _ := doSomething()
// Good: Handle or explicitly document why it's safe to ignore
result, err := doSomething()
if err != nil {
return err
}
// Acceptable: When error truly doesn't matter (rare)
_ = writer.Close() // Best-effort cleanup, error logged elsewhere
```
## 並行処理パターン
### ワーカープール
```go
func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
wg.Wait()
close(results)
}
```
### キャンセルとタイムアウト用のContext
```go
func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
```
### グレースフルシャットダウン
```go
func GracefulShutdown(server *http.Server) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited")
}
```
### 協調的なGoroutine用のerrgroup
```go
import "golang.org/x/sync/errgroup"
func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([][]byte, len(urls))
for i, url := range urls {
i, url := i, url // Capture loop variables
g.Go(func() error {
data, err := FetchWithTimeout(ctx, url)
if err != nil {
return err
}
results[i] = data
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
```
### Goroutineリークの回避
```go
// Bad: Goroutine leak if context is cancelled
func leakyFetch(ctx context.Context, url string) <-chan []byte {
ch := make(chan []byte)
go func() {
data, _ := fetch(url)
ch <- data // Blocks forever if no receiver
}()
return ch
}
// Good: Properly handles cancellation
func safeFetch(ctx context.Context, url string) <-chan []byte {
ch := make(chan []byte, 1) // Buffered channel
go func() {
data, err := fetch(url)
if err != nil {
return
}
select {
case ch <- data:
case <-ctx.Done():
}
}()
return ch
}
```
## インターフェース設計
### 小さく焦点を絞ったインターフェース
```go
// Good: Single-method interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Compose interfaces as needed
type ReadWriteCloser interface {
Reader
Writer
Closer
}
```
### 使用する場所でインターフェースを定義
```go
// In the consumer package, not the provider
package service
// UserStore defines what this service needs
type UserStore interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
type Service struct {
store UserStore
}
// Concrete implementation can be in another package
// It doesn't need to know about this interface
```
### 型アサーションを使用してオプション動作を実装
```go
type Flusher interface {
Flush() error
}
func WriteAndFlush(w io.Writer, data []byte) error {
if _, err := w.Write(data); err != nil {
return err
}
// Flush if supported
if f, ok := w.(Flusher); ok {
return f.Flush()
}
return nil
}
```
## パッケージ構成
### 標準プロジェクトレイアウト
```text
myproject/
├── cmd/
│ └── myapp/
│ └── main.go # Entry point
├── internal/
│ ├── handler/ # HTTP handlers
│ ├── service/ # Business logic
│ ├── repository/ # Data access
│ └── config/ # Configuration
├── pkg/
│ └── client/ # Public API client
├── api/
│ └── v1/ # API definitions (proto, OpenAPI)
├── testdata/ # Test fixtures
├── go.mod
├── go.sum
└── Makefile
```
### パッケージ命名
```go
// Good: Short, lowercase, no underscores
package http
package json
package user
// Bad: Verbose, mixed case, or redundant
package httpHandler
package json_parser
package userService // Redundant 'Service' suffix
```
### パッケージレベルの状態を避ける
```go
// Bad: Global mutable state
var db *sql.DB
func init() {
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}
// Good: Dependency injection
type Server struct {
db *sql.DB
}
func NewServer(db *sql.DB) *Server {
return &Server{db: db}
}
```
## 構造体設計
### 関数型オプションパターン
```go
type Server struct {
addr string
timeout time.Duration
logger *log.Logger
}
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
func WithLogger(l *log.Logger) Option {
return func(s *Server) {
s.logger = l
}
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
timeout: 30 * time.Second, // default
logger: log.Default(), // default
}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
server := NewServer(":8080",
WithTimeout(60*time.Second),
WithLogger(customLogger),
)
```
### コンポジション用の埋め込み
```go
type Logger struct {
prefix string
}
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.prefix, msg)
}
type Server struct {
*Logger // Embedding - Server gets Log method
addr string
}
func NewServer(addr string) *Server {
return &Server{
Logger: &Logger{prefix: "SERVER"},
addr: addr,
}
}
// Usage
s := NewServer(":8080")
s.Log("Starting...") // Calls embedded Logger.Log
```
## メモリとパフォーマンス
### サイズがわかっている場合はスライスを事前割り当て
```go
// Bad: Grows slice multiple times
func processItems(items []Item) []Result {
var results []Result
for _, item := range items {
results = append(results, process(item))
}
return results
}
// Good: Single allocation
func processItems(items []Item) []Result {
results := make([]Result, 0, len(items))
for _, item := range items {
results = append(results, process(item))
}
return results
}
```
### 頻繁な割り当て用のsync.Pool使用
```go
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func ProcessRequest(data []byte) []byte {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
buf.Write(data)
// Process...
return buf.Bytes()
}
```
### ループ内での文字列連結を避ける
```go
// Bad: Creates many string allocations
func join(parts []string) string {
var result string
for _, p := range parts {
result += p + ","
}
return result
}
// Good: Single allocation with strings.Builder
func join(parts []string) string {
var sb strings.Builder
for i, p := range parts {
if i > 0 {
sb.WriteString(",")
}
sb.WriteString(p)
}
return sb.String()
}
// Best: Use standard library
func join(parts []string) string {
return strings.Join(parts, ",")
}
```
## Goツール統合
### 基本コマンド
```bash
# Build and run
go build ./...
go run ./cmd/myapp
# Testing
go test ./...
go test -race ./...
go test -cover ./...
# Static analysis
go vet ./...
staticcheck ./...
golangci-lint run
# Module management
go mod tidy
go mod verify
# Formatting
gofmt -w .
goimports -w .
```
### 推奨リンター設定(.golangci.yml
```yaml
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- goimports
- misspell
- unconvert
- unparam
linters-settings:
errcheck:
check-type-assertions: true
govet:
check-shadowing: true
issues:
exclude-use-default: false
```
## クイックリファレンスGoイディオム
| イディオム | 説明 |
|-------|-------------|
| インターフェースを受け取り、構造体を返す | 関数はインターフェースパラメータを受け取り、具体的な型を返す |
| エラーは値である | エラーを例外ではなく一級値として扱う |
| メモリ共有で通信しない | goroutine間の調整にチャネルを使用 |
| ゼロ値を有用にする | 型は明示的な初期化なしで機能すべき |
| 少しのコピーは少しの依存よりも良い | 不要な外部依存を避ける |
| 明確さは巧妙さよりも良い | 巧妙さよりも可読性を優先 |
| gofmtは誰の好みでもないが皆の友達 | 常にgofmt/goimportsでフォーマット |
| 早期リターン | エラーを最初に処理し、ハッピーパスのインデントを浅く保つ |
## 避けるべきアンチパターン
```go
// Bad: Naked returns in long functions
func process() (result int, err error) {
// ... 50 lines ...
return // What is being returned?
}
// Bad: Using panic for control flow
func GetUser(id string) *User {
user, err := db.Find(id)
if err != nil {
panic(err) // Don't do this
}
return user
}
// Bad: Passing context in struct
type Request struct {
ctx context.Context // Context should be first param
ID string
}
// Good: Context as first parameter
func ProcessRequest(ctx context.Context, id string) error {
// ...
}
// Bad: Mixing value and pointer receivers
type Counter struct{ n int }
func (c Counter) Value() int { return c.n } // Value receiver
func (c *Counter) Increment() { c.n++ } // Pointer receiver
// Pick one style and be consistent
```
**覚えておいてください**: Goコードは最良の意味で退屈であるべきです - 予測可能で、一貫性があり、理解しやすい。迷ったときは、シンプルに保ってください。

View File

@@ -1,959 +0,0 @@
---
name: golang-testing
description: テスト駆動開発とGoコードの高品質を保証するための包括的なテスト戦略。
---
# Go テスト
テスト駆動開発(TDD)とGoコードの高品質を保証するための包括的なテスト戦略。
## いつ有効化するか
- 新しいGoコードを書くとき
- Goコードをレビューするとき
- 既存のテストを改善するとき
- テストカバレッジを向上させるとき
- デバッグとバグ修正時
## 核となる原則
### 1. テスト駆動開発(TDD)ワークフロー
失敗するテストを書き、実装し、リファクタリングするサイクルに従います。
```go
// 1. テストを書く(失敗)
func TestCalculateTotal(t *testing.T) {
total := CalculateTotal([]float64{10.0, 20.0, 30.0})
want := 60.0
if total != want {
t.Errorf("got %f, want %f", total, want)
}
}
// 2. 実装する(テストを通す)
func CalculateTotal(prices []float64) float64 {
var total float64
for _, price := range prices {
total += price
}
return total
}
// 3. リファクタリング
// テストを壊さずにコードを改善
```
### 2. テーブル駆動テスト
複数のケースを体系的にテストします。
```go
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"mixed signs", -2, 3, 1},
{"zeros", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, got, tt.want)
}
})
}
}
```
### 3. サブテスト
サブテストを使用した論理的なテストの構成。
```go
func TestUser(t *testing.T) {
t.Run("validation", func(t *testing.T) {
t.Run("empty email", func(t *testing.T) {
user := User{Email: ""}
if err := user.Validate(); err == nil {
t.Error("expected validation error")
}
})
t.Run("valid email", func(t *testing.T) {
user := User{Email: "test@example.com"}
if err := user.Validate(); err != nil {
t.Errorf("unexpected error: %v", err)
}
})
})
t.Run("serialization", func(t *testing.T) {
// 別のテストグループ
})
}
```
## テスト構成
### ファイル構成
```text
mypackage/
├── user.go
├── user_test.go # ユニットテスト
├── integration_test.go # 統合テスト
├── testdata/ # テストフィクスチャ
│ ├── valid_user.json
│ └── invalid_user.json
└── export_test.go # 内部のテストのための非公開のエクスポート
```
### テストパッケージ
```go
// user_test.go - 同じパッケージ(ホワイトボックステスト)
package user
func TestInternalFunction(t *testing.T) {
// 内部をテストできる
}
// user_external_test.go - 外部パッケージ(ブラックボックステスト)
package user_test
import "myapp/user"
func TestPublicAPI(t *testing.T) {
// 公開APIのみをテスト
}
```
## アサーションとヘルパー
### 基本的なアサーション
```go
func TestBasicAssertions(t *testing.T) {
// 等価性
got := Calculate()
want := 42
if got != want {
t.Errorf("got %d, want %d", got, want)
}
// エラーチェック
_, err := Process()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// nil チェック
result := GetResult()
if result == nil {
t.Fatal("expected non-nil result")
}
}
```
### カスタムヘルパー関数
```go
// ヘルパーとしてマーク(スタックトレースに表示されない)
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper()
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
func assertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
// 使用例
func TestWithHelpers(t *testing.T) {
result, err := Process()
assertNoError(t, err)
assertEqual(t, result.Status, "success")
}
```
### ディープ等価性チェック
```go
import "reflect"
func assertDeepEqual(t *testing.T, got, want interface{}) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("got %+v, want %+v", got, want)
}
}
func TestStructEquality(t *testing.T) {
got := User{Name: "Alice", Age: 30}
want := User{Name: "Alice", Age: 30}
assertDeepEqual(t, got, want)
}
```
## モッキングとスタブ
### インターフェースベースのモック
```go
// 本番コード
type UserStore interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
type UserService struct {
store UserStore
}
// テストコード
type MockUserStore struct {
users map[string]*User
err error
}
func (m *MockUserStore) GetUser(id string) (*User, error) {
if m.err != nil {
return nil, m.err
}
return m.users[id], nil
}
func (m *MockUserStore) SaveUser(user *User) error {
if m.err != nil {
return m.err
}
m.users[user.ID] = user
return nil
}
// テスト
func TestUserService(t *testing.T) {
mock := &MockUserStore{
users: make(map[string]*User),
}
service := &UserService{store: mock}
// サービスをテスト...
}
```
### 時間のモック
```go
// プロダクションコード - 時間を注入可能にする
type TimeProvider interface {
Now() time.Time
}
type RealTime struct{}
func (RealTime) Now() time.Time {
return time.Now()
}
type Service struct {
time TimeProvider
}
// テストコード
type MockTime struct {
current time.Time
}
func (m MockTime) Now() time.Time {
return m.current
}
func TestTimeDependent(t *testing.T) {
mockTime := MockTime{
current: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
}
service := &Service{time: mockTime}
// 固定時間でテスト...
}
```
### HTTP クライアントのモック
```go
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type MockHTTPClient struct {
response *http.Response
err error
}
func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
return m.response, m.err
}
func TestAPICall(t *testing.T) {
mockClient := &MockHTTPClient{
response: &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(`{"status":"ok"}`)),
},
}
api := &APIClient{client: mockClient}
// APIクライアントをテスト...
}
```
## HTTPハンドラーのテスト
### httptest の使用
```go
func TestHandler(t *testing.T) {
handler := http.HandlerFunc(MyHandler)
req := httptest.NewRequest("GET", "/users/123", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
// ステータスコードをチェック
if rec.Code != http.StatusOK {
t.Errorf("got status %d, want %d", rec.Code, http.StatusOK)
}
// レスポンスボディをチェック
var response map[string]interface{}
if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if response["id"] != "123" {
t.Errorf("got id %v, want 123", response["id"])
}
}
```
### ミドルウェアのテスト
```go
func TestAuthMiddleware(t *testing.T) {
// ダミーハンドラー
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
// ミドルウェアでラップ
handler := AuthMiddleware(nextHandler)
tests := []struct {
name string
token string
wantStatus int
}{
{"valid token", "valid-token", http.StatusOK},
{"invalid token", "invalid", http.StatusUnauthorized},
{"no token", "", http.StatusUnauthorized},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
if tt.token != "" {
req.Header.Set("Authorization", "Bearer "+tt.token)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tt.wantStatus {
t.Errorf("got status %d, want %d", rec.Code, tt.wantStatus)
}
})
}
}
```
### テストサーバー
```go
func TestAPIIntegration(t *testing.T) {
// テストサーバーを作成
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{
"message": "hello",
})
}))
defer server.Close()
// 実際のHTTPリクエストを行う
resp, err := http.Get(server.URL)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
// レスポンスを検証
var result map[string]string
json.NewDecoder(resp.Body).Decode(&result)
if result["message"] != "hello" {
t.Errorf("got %s, want hello", result["message"])
}
}
```
## データベーステスト
### トランザクションを使用したテストの分離
```go
func TestUserRepository(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
tests := []struct {
name string
fn func(*testing.T, *sql.DB)
}{
{"create user", testCreateUser},
{"find user", testFindUser},
{"update user", testUpdateUser},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
defer tx.Rollback() // テスト後にロールバック
tt.fn(t, tx)
})
}
}
```
### テストフィクスチャ
```go
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("postgres", "postgres://localhost/test")
if err != nil {
t.Fatalf("failed to connect: %v", err)
}
// スキーマを移行
if err := runMigrations(db); err != nil {
t.Fatalf("migrations failed: %v", err)
}
return db
}
func seedTestData(t *testing.T, db *sql.DB) {
t.Helper()
fixtures := []string{
`INSERT INTO users (id, email) VALUES ('1', 'test@example.com')`,
`INSERT INTO posts (id, user_id, title) VALUES ('1', '1', 'Test Post')`,
}
for _, query := range fixtures {
if _, err := db.Exec(query); err != nil {
t.Fatalf("failed to seed data: %v", err)
}
}
}
```
## ベンチマーク
### 基本的なベンチマーク
```go
func BenchmarkCalculation(b *testing.B) {
for i := 0; i < b.N; i++ {
Calculate(100)
}
}
// メモリ割り当てを報告
func BenchmarkWithAllocs(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ProcessData([]byte("test data"))
}
}
```
### サブベンチマーク
```go
func BenchmarkEncoding(b *testing.B) {
data := generateTestData()
b.Run("json", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
json.Marshal(data)
}
})
b.Run("gob", func(b *testing.B) {
b.ReportAllocs()
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
b.ResetTimer()
for i := 0; i < b.N; i++ {
enc.Encode(data)
buf.Reset()
}
})
}
```
### ベンチマーク比較
```go
// 実行: go test -bench=. -benchmem
func BenchmarkStringConcat(b *testing.B) {
b.Run("operator", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = "hello" + " " + "world"
}
})
b.Run("fmt.Sprintf", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("%s %s", "hello", "world")
}
})
b.Run("strings.Builder", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
sb.WriteString("hello")
sb.WriteString(" ")
sb.WriteString("world")
_ = sb.String()
}
})
}
```
## ファジングテスト
### 基本的なファズテストGo 1.18+
```go
func FuzzParseInput(f *testing.F) {
// シードコーパス
f.Add("hello")
f.Add("world")
f.Add("123")
f.Fuzz(func(t *testing.T, input string) {
// パースがパニックしないことを確認
result, err := ParseInput(input)
// エラーがあっても、nilでないか一貫性があることを確認
if err == nil && result == nil {
t.Error("got nil result with no error")
}
})
}
```
### より複雑なファジング
```go
func FuzzJSONParsing(f *testing.F) {
f.Add([]byte(`{"name":"test","age":30}`))
f.Add([]byte(`{"name":"","age":0}`))
f.Fuzz(func(t *testing.T, data []byte) {
var user User
err := json.Unmarshal(data, &user)
// JSONがデコードされる場合、再度エンコードできるべき
if err == nil {
_, err := json.Marshal(user)
if err != nil {
t.Errorf("marshal failed after successful unmarshal: %v", err)
}
}
})
}
```
## テストカバレッジ
### カバレッジの実行と表示
```bash
# カバレッジを実行してHTMLレポートを生成
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# パッケージごとのカバレッジを表示
go test -cover ./...
# 詳細なカバレッジ
go test -coverprofile=coverage.out -covermode=atomic ./...
```
### カバレッジのベストプラクティス
```go
// Good: テスタブルなコード
func ProcessData(data []byte) (Result, error) {
if len(data) == 0 {
return Result{}, ErrEmptyData
}
// 各分岐をテスト可能
if isValid(data) {
return parseValid(data)
}
return parseInvalid(data)
}
// 対応するテストが全分岐をカバー
func TestProcessData(t *testing.T) {
tests := []struct {
name string
data []byte
wantErr bool
}{
{"empty data", []byte{}, true},
{"valid data", []byte("valid"), false},
{"invalid data", []byte("invalid"), false},
}
// ...
}
```
## 統合テスト
### ビルドタグの使用
```go
//go:build integration
// +build integration
package myapp_test
import "testing"
func TestDatabaseIntegration(t *testing.T) {
// 実際のDBを必要とするテスト
}
```
```bash
# 統合テストを実行
go test -tags=integration ./...
# 統合テストを除外
go test ./...
```
### テストコンテナの使用
```go
import "github.com/testcontainers/testcontainers-go"
func setupPostgres(t *testing.T) *sql.DB {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "postgres:15",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_PASSWORD": "test",
"POSTGRES_DB": "testdb",
},
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
container.Terminate(ctx)
})
// コンテナに接続
// ...
return db
}
```
## テストの並列化
### 並列テスト
```go
func TestParallel(t *testing.T) {
tests := []struct {
name string
fn func(*testing.T)
}{
{"test1", testCase1},
{"test2", testCase2},
{"test3", testCase3},
}
for _, tt := range tests {
tt := tt // ループ変数をキャプチャ
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // このテストを並列実行
tt.fn(t)
})
}
}
```
### 並列実行の制御
```go
func TestWithResourceLimit(t *testing.T) {
// 同時に5つのテストのみ
sem := make(chan struct{}, 5)
tests := generateManyTests()
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
sem <- struct{}{} // 獲得
defer func() { <-sem }() // 解放
tt.fn(t)
})
}
}
```
## Goツール統合
### テストコマンド
```bash
# 基本テスト
go test ./...
go test -v ./... # 詳細出力
go test -run TestSpecific ./... # 特定のテストを実行
# カバレッジ
go test -cover ./...
go test -coverprofile=coverage.out ./...
# レースコンディション
go test -race ./...
# ベンチマーク
go test -bench=. ./...
go test -bench=. -benchmem ./...
go test -bench=. -cpuprofile=cpu.prof ./...
# ファジング
go test -fuzz=FuzzTest
# 統合テスト
go test -tags=integration ./...
# JSONフォーマットCI統合用
go test -json ./...
```
### テスト設定
```bash
# テストタイムアウト
go test -timeout 30s ./...
# 短時間テスト(長時間テストをスキップ)
go test -short ./...
# ビルドキャッシュのクリア
go clean -testcache
go test ./...
```
## ベストプラクティス
### DRYDon't Repeat Yourself原則
```go
// Good: テーブル駆動テストで繰り返しを削減
func TestValidation(t *testing.T) {
tests := []struct {
input string
valid bool
}{
{"valid@email.com", true},
{"invalid-email", false},
{"", false},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
err := Validate(tt.input)
if (err == nil) != tt.valid {
t.Errorf("Validate(%q) error = %v, want valid = %v",
tt.input, err, tt.valid)
}
})
}
}
```
### テストデータの分離
```go
// Good: テストデータを testdata/ ディレクトリに配置
func TestLoadConfig(t *testing.T) {
data, err := os.ReadFile("testdata/config.json")
if err != nil {
t.Fatal(err)
}
config, err := ParseConfig(data)
// ...
}
```
### クリーンアップの使用
```go
func TestWithCleanup(t *testing.T) {
// リソースを設定
file, err := os.CreateTemp("", "test")
if err != nil {
t.Fatal(err)
}
// クリーンアップを登録deferに似ているが、サブテストで動作
t.Cleanup(func() {
os.Remove(file.Name())
})
// テストを続ける...
}
```
### エラーメッセージの明確化
```go
// Bad: 不明確なエラー
if result != expected {
t.Error("wrong result")
}
// Good: コンテキスト付きエラー
if result != expected {
t.Errorf("Calculate(%d) = %d; want %d", input, result, expected)
}
// Better: ヘルパー関数の使用
assertEqual(t, result, expected, "Calculate(%d)", input)
```
## 避けるべきアンチパターン
```go
// Bad: 外部状態に依存
func TestBadDependency(t *testing.T) {
result := GetUserFromDatabase("123") // 実際のDBを使用
// テストが壊れやすく遅い
}
// Good: 依存を注入
func TestGoodDependency(t *testing.T) {
mockDB := &MockDatabase{
users: map[string]User{"123": {ID: "123"}},
}
result := GetUser(mockDB, "123")
}
// Bad: テスト間で状態を共有
var sharedCounter int
func TestShared1(t *testing.T) {
sharedCounter++
// テストの順序に依存
}
// Good: 各テストを独立させる
func TestIndependent(t *testing.T) {
counter := 0
counter++
// 他のテストに影響しない
}
// Bad: エラーを無視
func TestIgnoreError(t *testing.T) {
result, _ := Process()
if result != expected {
t.Error("wrong result")
}
}
// Good: エラーをチェック
func TestCheckError(t *testing.T) {
result, err := Process()
if err != nil {
t.Fatalf("Process() error = %v", err)
}
if result != expected {
t.Errorf("got %v, want %v", result, expected)
}
}
```
## クイックリファレンス
| コマンド/パターン | 目的 |
|--------------|---------|
| `go test ./...` | すべてのテストを実行 |
| `go test -v` | 詳細出力 |
| `go test -cover` | カバレッジレポート |
| `go test -race` | レースコンディション検出 |
| `go test -bench=.` | ベンチマークを実行 |
| `t.Run()` | サブテスト |
| `t.Helper()` | テストヘルパー関数 |
| `t.Parallel()` | テストを並列実行 |
| `t.Cleanup()` | クリーンアップを登録 |
| `testdata/` | テストフィクスチャ用ディレクトリ |
| `-short` | 長時間テストをスキップ |
| `-tags=integration` | ビルドタグでテストを実行 |
**覚えておいてください**: 良いテストは高速で、信頼性があり、保守可能で、明確です。複雑さより明確さを目指してください。

View File

@@ -1,202 +0,0 @@
---
name: iterative-retrieval
description: サブエージェントのコンテキスト問題を解決するために、コンテキスト取得を段階的に洗練するパターン
---
# 反復検索パターン
マルチエージェントワークフローにおける「コンテキスト問題」を解決します。サブエージェントは作業を開始するまで、どのコンテキストが必要かわかりません。
## 問題
サブエージェントは限定的なコンテキストで起動されます。以下を知りません:
- どのファイルに関連するコードが含まれているか
- コードベースにどのようなパターンが存在するか
- プロジェクトがどのような用語を使用しているか
標準的なアプローチは失敗します:
- **すべてを送信**: コンテキスト制限を超える
- **何も送信しない**: エージェントに重要な情報が不足
- **必要なものを推測**: しばしば間違い
## 解決策: 反復検索
コンテキストを段階的に洗練する4フェーズのループ:
```
┌─────────────────────────────────────────────┐
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ DISPATCH │─────▶│ EVALUATE │ │
│ └──────────┘ └──────────┘ │
│ ▲ │ │
│ │ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ LOOP │◀─────│ REFINE │ │
│ └──────────┘ └──────────┘ │
│ │
│ 最大3サイクル、その後続行 │
└─────────────────────────────────────────────┘
```
### フェーズ1: DISPATCH
候補ファイルを収集する初期の広範なクエリ:
```javascript
// 高レベルの意図から開始
const initialQuery = {
patterns: ['src/**/*.ts', 'lib/**/*.ts'],
keywords: ['authentication', 'user', 'session'],
excludes: ['*.test.ts', '*.spec.ts']
};
// 検索エージェントにディスパッチ
const candidates = await retrieveFiles(initialQuery);
```
### フェーズ2: EVALUATE
取得したコンテンツの関連性を評価:
```javascript
function evaluateRelevance(files, task) {
return files.map(file => ({
path: file.path,
relevance: scoreRelevance(file.content, task),
reason: explainRelevance(file.content, task),
missingContext: identifyGaps(file.content, task)
}));
}
```
スコアリング基準:
- **高(0.8-1.0)**: ターゲット機能を直接実装
- **中(0.5-0.7)**: 関連するパターンや型を含む
- **低(0.2-0.4)**: 間接的に関連
- **なし(0-0.2)**: 関連なし、除外
### フェーズ3: REFINE
評価に基づいて検索基準を更新:
```javascript
function refineQuery(evaluation, previousQuery) {
return {
// 高関連性ファイルで発見された新しいパターンを追加
patterns: [...previousQuery.patterns, ...extractPatterns(evaluation)],
// コードベースで見つかった用語を追加
keywords: [...previousQuery.keywords, ...extractKeywords(evaluation)],
// 確認された無関係なパスを除外
excludes: [...previousQuery.excludes, ...evaluation
.filter(e => e.relevance < 0.2)
.map(e => e.path)
],
// 特定のギャップをターゲット
focusAreas: evaluation
.flatMap(e => e.missingContext)
.filter(unique)
};
}
```
### フェーズ4: LOOP
洗練された基準で繰り返す(最大3サイクル):
```javascript
async function iterativeRetrieve(task, maxCycles = 3) {
let query = createInitialQuery(task);
let bestContext = [];
for (let cycle = 0; cycle < maxCycles; cycle++) {
const candidates = await retrieveFiles(query);
const evaluation = evaluateRelevance(candidates, task);
// 十分なコンテキストがあるか確認
const highRelevance = evaluation.filter(e => e.relevance >= 0.7);
if (highRelevance.length >= 3 && !hasCriticalGaps(evaluation)) {
return highRelevance;
}
// 洗練して続行
query = refineQuery(evaluation, query);
bestContext = mergeContext(bestContext, highRelevance);
}
return bestContext;
}
```
## 実践例
### 例1: バグ修正コンテキスト
```
タスク: "認証トークン期限切れバグを修正"
サイクル1:
DISPATCH: src/**で"token"、"auth"、"expiry"を検索
EVALUATE: auth.ts(0.9)、tokens.ts(0.8)、user.ts(0.3)を発見
REFINE: "refresh"、"jwt"キーワードを追加; user.tsを除外
サイクル2:
DISPATCH: 洗練された用語で検索
EVALUATE: session-manager.ts(0.95)、jwt-utils.ts(0.85)を発見
REFINE: 十分なコンテキスト(2つの高関連性ファイル)
結果: auth.ts、tokens.ts、session-manager.ts、jwt-utils.ts
```
### 例2: 機能実装
```
タスク: "APIエンドポイントにレート制限を追加"
サイクル1:
DISPATCH: routes/**で"rate"、"limit"、"api"を検索
EVALUATE: マッチなし - コードベースは"throttle"用語を使用
REFINE: "throttle"、"middleware"キーワードを追加
サイクル2:
DISPATCH: 洗練された用語で検索
EVALUATE: throttle.ts(0.9)、middleware/index.ts(0.7)を発見
REFINE: ルーターパターンが必要
サイクル3:
DISPATCH: "router"、"express"パターンを検索
EVALUATE: router-setup.ts(0.8)を発見
REFINE: 十分なコンテキスト
結果: throttle.ts、middleware/index.ts、router-setup.ts
```
## エージェントとの統合
エージェントプロンプトで使用:
```markdown
このタスクのコンテキストを取得する際:
1. 広範なキーワード検索から開始
2. 各ファイルの関連性を評価(0-1スケール)
3. まだ不足しているコンテキストを特定
4. 検索基準を洗練して繰り返す(最大3サイクル)
5. 関連性が0.7以上のファイルを返す
```
## ベストプラクティス
1. **広く開始し、段階的に絞る** - 初期クエリで過度に指定しない
2. **コードベースの用語を学ぶ** - 最初のサイクルでしばしば命名規則が明らかになる
3. **不足しているものを追跡** - 明示的なギャップ識別が洗練を促進
4. **「十分に良い」で停止** - 3つの高関連性ファイルは10個の平凡なファイルより優れている
5. **確信を持って除外** - 低関連性ファイルは関連性を持つようにならない
## 関連項目
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - サブエージェントオーケストレーションセクション
- `continuous-learning`スキル - 時間とともに改善するパターン用
- `~/.claude/agents/`内のエージェント定義

View File

@@ -1,138 +0,0 @@
---
name: java-coding-standards
description: Spring Bootサービス向けのJavaコーディング標準命名、不変性、Optional使用、ストリーム、例外、ジェネリクス、プロジェクトレイアウト。
---
# Javaコーディング標準
Spring Bootサービスにおける読みやすく保守可能なJava(17+)コードの標準。
## 核となる原則
- 巧妙さよりも明確さを優先
- デフォルトで不変; 共有可変状態を最小化
- 意味のある例外で早期失敗
- 一貫した命名とパッケージ構造
## 命名
```java
// ✅ クラス/レコード: PascalCase
public class MarketService {}
public record Money(BigDecimal amount, Currency currency) {}
// ✅ メソッド/フィールド: camelCase
private final MarketRepository marketRepository;
public Market findBySlug(String slug) {}
// ✅ 定数: UPPER_SNAKE_CASE
private static final int MAX_PAGE_SIZE = 100;
```
## 不変性
```java
// ✅ recordとfinalフィールドを優先
public record MarketDto(Long id, String name, MarketStatus status) {}
public class Market {
private final Long id;
private final String name;
// getterのみ、setterなし
}
```
## Optionalの使用
```java
// ✅ find*メソッドからOptionalを返す
Optional<Market> market = marketRepository.findBySlug(slug);
// ✅ get()の代わりにmap/flatMapを使用
return market
.map(MarketResponse::from)
.orElseThrow(() -> new EntityNotFoundException("Market not found"));
```
## ストリームのベストプラクティス
```java
// ✅ 変換にストリームを使用し、パイプラインを短く保つ
List<String> names = markets.stream()
.map(Market::name)
.filter(Objects::nonNull)
.toList();
// ❌ 複雑なネストされたストリームを避ける; 明確性のためにループを優先
```
## 例外
- ドメインエラーには非チェック例外を使用; 技術的例外はコンテキストとともにラップ
- ドメイン固有の例外を作成(例: `MarketNotFoundException`)
- 広範な`catch (Exception ex)`を避ける(中央でリスロー/ログ記録する場合を除く)
```java
throw new MarketNotFoundException(slug);
```
## ジェネリクスと型安全性
- 生の型を避ける; ジェネリックパラメータを宣言
- 再利用可能なユーティリティには境界付きジェネリクスを優先
```java
public <T extends Identifiable> Map<Long, T> indexById(Collection<T> items) { ... }
```
## プロジェクト構造(Maven/Gradle)
```
src/main/java/com/example/app/
config/
controller/
service/
repository/
domain/
dto/
util/
src/main/resources/
application.yml
src/test/java/... (mainをミラー)
```
## フォーマットとスタイル
- 一貫して2または4スペースを使用(プロジェクト標準)
- ファイルごとに1つのpublicトップレベル型
- メソッドを短く集中的に保つ; ヘルパーを抽出
- メンバーの順序: 定数、フィールド、コンストラクタ、publicメソッド、protected、private
## 避けるべきコードの臭い
- 長いパラメータリスト → DTO/ビルダーを使用
- 深いネスト → 早期リターン
- マジックナンバー → 名前付き定数
- 静的可変状態 → 依存性注入を優先
- サイレントなcatchブロック → ログを記録して行動、または再スロー
## ログ記録
```java
private static final Logger log = LoggerFactory.getLogger(MarketService.class);
log.info("fetch_market slug={}", slug);
log.error("failed_fetch_market slug={}", slug, ex);
```
## Null処理
- やむを得ない場合のみ`@Nullable`を受け入れる; それ以外は`@NonNull`を使用
- 入力にBean Validation(`@NotNull``@NotBlank`)を使用
## テストの期待
- JUnit 5 + AssertJで流暢なアサーション
- モック用のMockito; 可能な限り部分モックを避ける
- 決定論的テストを優先; 隠れたsleepなし
**覚えておく**: コードを意図的、型付き、観察可能に保つ。必要性が証明されない限り、マイクロ最適化よりも保守性を最適化します。

View File

@@ -1,141 +0,0 @@
---
name: jpa-patterns
description: JPA/Hibernate patterns for entity design, relationships, query optimization, transactions, auditing, indexing, pagination, and pooling in Spring Boot.
---
# JPA/Hibernate パターン
Spring Bootでのデータモデリング、リポジトリ、パフォーマンスチューニングに使用します。
## エンティティ設計
```java
@Entity
@Table(name = "markets", indexes = {
@Index(name = "idx_markets_slug", columnList = "slug", unique = true)
})
@EntityListeners(AuditingEntityListener.class)
public class MarketEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false, unique = true, length = 120)
private String slug;
@Enumerated(EnumType.STRING)
private MarketStatus status = MarketStatus.ACTIVE;
@CreatedDate private Instant createdAt;
@LastModifiedDate private Instant updatedAt;
}
```
監査を有効化:
```java
@Configuration
@EnableJpaAuditing
class JpaConfig {}
```
## リレーションシップとN+1防止
```java
@OneToMany(mappedBy = "market", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PositionEntity> positions = new ArrayList<>();
```
- デフォルトで遅延ロード。必要に応じてクエリで `JOIN FETCH` を使用
- コレクションでは `EAGER` を避け、読み取りパスにはDTOプロジェクションを使用
```java
@Query("select m from MarketEntity m left join fetch m.positions where m.id = :id")
Optional<MarketEntity> findWithPositions(@Param("id") Long id);
```
## リポジトリパターン
```java
public interface MarketRepository extends JpaRepository<MarketEntity, Long> {
Optional<MarketEntity> findBySlug(String slug);
@Query("select m from MarketEntity m where m.status = :status")
Page<MarketEntity> findByStatus(@Param("status") MarketStatus status, Pageable pageable);
}
```
- 軽量クエリにはプロジェクションを使用:
```java
public interface MarketSummary {
Long getId();
String getName();
MarketStatus getStatus();
}
Page<MarketSummary> findAllBy(Pageable pageable);
```
## トランザクション
- サービスメソッドに `@Transactional` を付ける
- 読み取りパスを最適化するために `@Transactional(readOnly = true)` を使用
- 伝播を慎重に選択。長時間実行されるトランザクションを避ける
```java
@Transactional
public Market updateStatus(Long id, MarketStatus status) {
MarketEntity entity = repo.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Market"));
entity.setStatus(status);
return Market.from(entity);
}
```
## ページネーション
```java
PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
Page<MarketEntity> markets = repo.findByStatus(MarketStatus.ACTIVE, page);
```
カーソルライクなページネーションには、順序付けでJPQLに `id > :lastId` を含める。
## インデックス作成とパフォーマンス
- 一般的なフィルタ(`status``slug`、外部キー)にインデックスを追加
- クエリパターンに一致する複合インデックスを使用(`status, created_at`
- `select *` を避け、必要な列のみを投影
- `saveAll``hibernate.jdbc.batch_size` でバッチ書き込み
## コネクションプーリングHikariCP
推奨プロパティ:
```
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.validation-timeout=5000
```
PostgreSQL LOB処理には、次を追加:
```
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
```
## キャッシング
- 1次キャッシュはEntityManagerごと。トランザクション間でエンティティを保持しない
- 読み取り集約型エンティティには、2次キャッシュを慎重に検討。退避戦略を検証
## マイグレーション
- FlywayまたはLiquibaseを使用。本番環境でHibernate自動DDLに依存しない
- マイグレーションを冪等かつ追加的に保つ。計画なしに列を削除しない
## データアクセステスト
- 本番環境を反映するために、Testcontainersを使用した `@DataJpaTest` を優先
- ログを使用してSQL効率をアサート: パラメータ値には `logging.level.org.hibernate.SQL=DEBUG``logging.level.org.hibernate.orm.jdbc.bind=TRACE` を設定
**注意**: エンティティを軽量に保ち、クエリを意図的にし、トランザクションを短く保ちます。フェッチ戦略とプロジェクションでN+1を防ぎ、読み取り/書き込みパスにインデックスを作成します。

View File

@@ -1,165 +0,0 @@
---
name: nutrient-document-processing
description: Nutrient DWS API を使用してドキュメントの処理、変換、OCR、抽出、編集、署名、フォーム入力を行います。PDF、DOCX、XLSX、PPTX、HTML、画像に対応しています。
---
# Nutrient Document Processing
[Nutrient DWS Processor API](https://www.nutrient.io/api/) でドキュメントを処理します。フォーマット変換、テキストとテーブルの抽出、スキャンされたドキュメントの OCR、PII の編集、ウォーターマークの追加、デジタル署名、PDF フォームの入力が可能です。
## セットアップ
**[nutrient.io](https://dashboard.nutrient.io/sign_up/?product=processor)** で無料の API キーを取得してください
```bash
export NUTRIENT_API_KEY="pdf_live_..."
```
すべてのリクエストは `https://api.nutrient.io/build``instructions` JSON フィールドを含むマルチパート POST として送信されます。
## 操作
### ドキュメントの変換
```bash
# DOCX から PDF へ
curl -X POST https://api.nutrient.io/build \
-H "Authorization: Bearer $NUTRIENT_API_KEY" \
-F "document.docx=@document.docx" \
-F 'instructions={"parts":[{"file":"document.docx"}]}' \
-o output.pdf
# PDF から DOCX へ
curl -X POST https://api.nutrient.io/build \
-H "Authorization: Bearer $NUTRIENT_API_KEY" \
-F "document.pdf=@document.pdf" \
-F 'instructions={"parts":[{"file":"document.pdf"}],"output":{"type":"docx"}}' \
-o output.docx
# HTML から PDF へ
curl -X POST https://api.nutrient.io/build \
-H "Authorization: Bearer $NUTRIENT_API_KEY" \
-F "index.html=@index.html" \
-F 'instructions={"parts":[{"html":"index.html"}]}' \
-o output.pdf
```
サポートされている入力形式: PDF、DOCX、XLSX、PPTX、DOC、XLS、PPT、PPS、PPSX、ODT、RTF、HTML、JPG、PNG、TIFF、HEIC、GIF、WebP、SVG、TGA、EPS。
### テキストとデータの抽出
```bash
# プレーンテキストの抽出
curl -X POST https://api.nutrient.io/build \
-H "Authorization: Bearer $NUTRIENT_API_KEY" \
-F "document.pdf=@document.pdf" \
-F 'instructions={"parts":[{"file":"document.pdf"}],"output":{"type":"text"}}' \
-o output.txt
# テーブルを Excel として抽出
curl -X POST https://api.nutrient.io/build \
-H "Authorization: Bearer $NUTRIENT_API_KEY" \
-F "document.pdf=@document.pdf" \
-F 'instructions={"parts":[{"file":"document.pdf"}],"output":{"type":"xlsx"}}' \
-o tables.xlsx
```
### スキャンされたドキュメントの OCR
```bash
# 検索可能な PDF への OCR100以上の言語をサポート
curl -X POST https://api.nutrient.io/build \
-H "Authorization: Bearer $NUTRIENT_API_KEY" \
-F "scanned.pdf=@scanned.pdf" \
-F 'instructions={"parts":[{"file":"scanned.pdf"}],"actions":[{"type":"ocr","language":"english"}]}' \
-o searchable.pdf
```
言語: ISO 639-2 コード(例: `eng``deu``fra``spa``jpn``kor``chi_sim``chi_tra``ara``hin``rus`を介して100以上の言語をサポートしています。`english``german` などの完全な言語名も機能します。サポートされているすべてのコードについては、[完全な OCR 言語表](https://www.nutrient.io/guides/document-engine/ocr/language-support/)を参照してください。
### 機密情報の編集
```bash
# パターンベースSSN、メール
curl -X POST https://api.nutrient.io/build \
-H "Authorization: Bearer $NUTRIENT_API_KEY" \
-F "document.pdf=@document.pdf" \
-F 'instructions={"parts":[{"file":"document.pdf"}],"actions":[{"type":"redaction","strategy":"preset","strategyOptions":{"preset":"social-security-number"}},{"type":"redaction","strategy":"preset","strategyOptions":{"preset":"email-address"}}]}' \
-o redacted.pdf
# 正規表現ベース
curl -X POST https://api.nutrient.io/build \
-H "Authorization: Bearer $NUTRIENT_API_KEY" \
-F "document.pdf=@document.pdf" \
-F 'instructions={"parts":[{"file":"document.pdf"}],"actions":[{"type":"redaction","strategy":"regex","strategyOptions":{"regex":"\\b[A-Z]{2}\\d{6}\\b"}}]}' \
-o redacted.pdf
```
プリセット: `social-security-number``email-address``credit-card-number``international-phone-number``north-american-phone-number``date``time``url``ipv4``ipv6``mac-address``us-zip-code``vin`
### ウォーターマークの追加
```bash
curl -X POST https://api.nutrient.io/build \
-H "Authorization: Bearer $NUTRIENT_API_KEY" \
-F "document.pdf=@document.pdf" \
-F 'instructions={"parts":[{"file":"document.pdf"}],"actions":[{"type":"watermark","text":"CONFIDENTIAL","fontSize":72,"opacity":0.3,"rotation":-45}]}' \
-o watermarked.pdf
```
### デジタル署名
```bash
# 自己署名 CMS 署名
curl -X POST https://api.nutrient.io/build \
-H "Authorization: Bearer $NUTRIENT_API_KEY" \
-F "document.pdf=@document.pdf" \
-F 'instructions={"parts":[{"file":"document.pdf"}],"actions":[{"type":"sign","signatureType":"cms"}]}' \
-o signed.pdf
```
### PDF フォームの入力
```bash
curl -X POST https://api.nutrient.io/build \
-H "Authorization: Bearer $NUTRIENT_API_KEY" \
-F "form.pdf=@form.pdf" \
-F 'instructions={"parts":[{"file":"form.pdf"}],"actions":[{"type":"fillForm","formFields":{"name":"Jane Smith","email":"jane@example.com","date":"2026-02-06"}}]}' \
-o filled.pdf
```
## MCP サーバー(代替)
ネイティブツール統合には、curl の代わりに MCP サーバーを使用します:
```json
{
"mcpServers": {
"nutrient-dws": {
"command": "npx",
"args": ["-y", "@nutrient-sdk/dws-mcp-server"],
"env": {
"NUTRIENT_DWS_API_KEY": "YOUR_API_KEY",
"SANDBOX_PATH": "/path/to/working/directory"
}
}
}
}
```
## 使用タイミング
- フォーマット間でのドキュメント変換PDF、DOCX、XLSX、PPTX、HTML、画像
- PDF からテキスト、テーブル、キー値ペアの抽出
- スキャンされたドキュメントまたは画像の OCR
- ドキュメントを共有する前の PII の編集
- ドラフトまたは機密文書へのウォーターマークの追加
- 契約または合意書へのデジタル署名
- プログラムによる PDF フォームの入力
## リンク
- [API Playground](https://dashboard.nutrient.io/processor-api/playground/)
- [完全な API ドキュメント](https://www.nutrient.io/guides/dws-processor/)
- [Agent Skill リポジトリ](https://github.com/PSPDFKit-labs/nutrient-agent-skill)
- [npm MCP サーバー](https://www.npmjs.com/package/@nutrient-sdk/dws-mcp-server)

View File

@@ -1,146 +0,0 @@
---
name: postgres-patterns
description: PostgreSQL database patterns for query optimization, schema design, indexing, and security. Based on Supabase best practices.
---
# PostgreSQL パターン
PostgreSQLベストプラクティスのクイックリファレンス。詳細なガイダンスについては、`database-reviewer` エージェントを使用してください。
## 起動タイミング
- SQLクエリまたはマイグレーションの作成時
- データベーススキーマの設計時
- 低速クエリのトラブルシューティング時
- Row Level Securityの実装時
- コネクションプーリングの設定時
## クイックリファレンス
### インデックスチートシート
| クエリパターン | インデックスタイプ | 例 |
|--------------|------------|---------|
| `WHERE col = value` | B-treeデフォルト | `CREATE INDEX idx ON t (col)` |
| `WHERE col > value` | B-tree | `CREATE INDEX idx ON t (col)` |
| `WHERE a = x AND b > y` | 複合 | `CREATE INDEX idx ON t (a, b)` |
| `WHERE jsonb @> '{}'` | GIN | `CREATE INDEX idx ON t USING gin (col)` |
| `WHERE tsv @@ query` | GIN | `CREATE INDEX idx ON t USING gin (col)` |
| 時系列範囲 | BRIN | `CREATE INDEX idx ON t USING brin (col)` |
### データタイプクイックリファレンス
| 用途 | 正しいタイプ | 避けるべき |
|----------|-------------|-------|
| ID | `bigint` | `int`、ランダムUUID |
| 文字列 | `text` | `varchar(255)` |
| タイムスタンプ | `timestamptz` | `timestamp` |
| 金額 | `numeric(10,2)` | `float` |
| フラグ | `boolean` | `varchar``int` |
### 一般的なパターン
**複合インデックスの順序:**
```sql
-- 等価列を最初に、次に範囲列
CREATE INDEX idx ON orders (status, created_at);
-- 次の場合に機能: WHERE status = 'pending' AND created_at > '2024-01-01'
```
**カバリングインデックス:**
```sql
CREATE INDEX idx ON users (email) INCLUDE (name, created_at);
-- SELECT email, name, created_at のテーブル検索を回避
```
**部分インデックス:**
```sql
CREATE INDEX idx ON users (email) WHERE deleted_at IS NULL;
-- より小さなインデックス、アクティブユーザーのみを含む
```
**RLSポリシー最適化:**
```sql
CREATE POLICY policy ON orders
USING ((SELECT auth.uid()) = user_id); -- SELECTでラップ
```
**UPSERT:**
```sql
INSERT INTO settings (user_id, key, value)
VALUES (123, 'theme', 'dark')
ON CONFLICT (user_id, key)
DO UPDATE SET value = EXCLUDED.value;
```
**カーソルページネーション:**
```sql
SELECT * FROM products WHERE id > $last_id ORDER BY id LIMIT 20;
-- O(1) vs OFFSET は O(n)
```
**キュー処理:**
```sql
UPDATE jobs SET status = 'processing'
WHERE id = (
SELECT id FROM jobs WHERE status = 'pending'
ORDER BY created_at LIMIT 1
FOR UPDATE SKIP LOCKED
) RETURNING *;
```
### アンチパターン検出
```sql
-- インデックスのない外部キーを検索
SELECT conrelid::regclass, a.attname
FROM pg_constraint c
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
WHERE c.contype = 'f'
AND NOT EXISTS (
SELECT 1 FROM pg_index i
WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey)
);
-- 低速クエリを検索
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
WHERE mean_exec_time > 100
ORDER BY mean_exec_time DESC;
-- テーブル肥大化をチェック
SELECT relname, n_dead_tup, last_vacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;
```
### 設定テンプレート
```sql
-- 接続制限RAMに応じて調整
ALTER SYSTEM SET max_connections = 100;
ALTER SYSTEM SET work_mem = '8MB';
-- タイムアウト
ALTER SYSTEM SET idle_in_transaction_session_timeout = '30s';
ALTER SYSTEM SET statement_timeout = '30s';
-- モニタリング
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- セキュリティデフォルト
REVOKE ALL ON SCHEMA public FROM public;
SELECT pg_reload_conf();
```
## 関連
- Agent: `database-reviewer` - 完全なデータベースレビューワークフロー
- Skill: `clickhouse-io` - ClickHouse分析パターン
- Skill: `backend-patterns` - APIとバックエンドパターン
---
*[Supabase Agent Skills](https://github.com/supabase/agent-skills)MITライセンスに基づく*

View File

@@ -1,345 +0,0 @@
# プロジェクトガイドラインスキル(例)
これはプロジェクト固有のスキルの例です。自分のプロジェクトのテンプレートとして使用してください。
実際の本番アプリケーションに基づいています:[Zenith](https://zenith.chat) - AI駆動の顧客発見プラットフォーム。
---
## 使用するタイミング
このスキルが設計された特定のプロジェクトで作業する際に参照してください。プロジェクトスキルには以下が含まれます:
- アーキテクチャの概要
- ファイル構造
- コードパターン
- テスト要件
- デプロイメントワークフロー
---
## アーキテクチャの概要
**技術スタック:**
- **フロントエンド**: Next.js 15 (App Router), TypeScript, React
- **バックエンド**: FastAPI (Python), Pydanticモデル
- **データベース**: Supabase (PostgreSQL)
- **AI**: Claudeツール呼び出しと構造化出力付きAPI
- **デプロイメント**: Google Cloud Run
- **テスト**: Playwright (E2E), pytest (バックエンド), React Testing Library
**サービス:**
```
┌─────────────────────────────────────────────────────────────┐
│ Frontend │
│ Next.js 15 + TypeScript + TailwindCSS │
│ Deployed: Vercel / Cloud Run │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Backend │
│ FastAPI + Python 3.11 + Pydantic │
│ Deployed: Cloud Run │
└─────────────────────────────────────────────────────────────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Supabase │ │ Claude │ │ Redis │
│ Database │ │ API │ │ Cache │
└──────────┘ └──────────┘ └──────────┘
```
---
## ファイル構造
```
project/
├── frontend/
│ └── src/
│ ├── app/ # Next.js app routerページ
│ │ ├── api/ # APIルート
│ │ ├── (auth)/ # 認証保護されたルート
│ │ └── workspace/ # メインアプリワークスペース
│ ├── components/ # Reactコンポーネント
│ │ ├── ui/ # ベースUIコンポーネント
│ │ ├── forms/ # フォームコンポーネント
│ │ └── layouts/ # レイアウトコンポーネント
│ ├── hooks/ # カスタムReactフック
│ ├── lib/ # ユーティリティ
│ ├── types/ # TypeScript定義
│ └── config/ # 設定
├── backend/
│ ├── routers/ # FastAPIルートハンドラ
│ ├── models.py # Pydanticモデル
│ ├── main.py # FastAPIアプリエントリ
│ ├── auth_system.py # 認証
│ ├── database.py # データベース操作
│ ├── services/ # ビジネスロジック
│ └── tests/ # pytestテスト
├── deploy/ # デプロイメント設定
├── docs/ # ドキュメント
└── scripts/ # ユーティリティスクリプト
```
---
## コードパターン
### APIレスポンス形式 (FastAPI)
```python
from pydantic import BaseModel
from typing import Generic, TypeVar, Optional
T = TypeVar('T')
class ApiResponse(BaseModel, Generic[T]):
success: bool
data: Optional[T] = None
error: Optional[str] = None
@classmethod
def ok(cls, data: T) -> "ApiResponse[T]":
return cls(success=True, data=data)
@classmethod
def fail(cls, error: str) -> "ApiResponse[T]":
return cls(success=False, error=error)
```
### フロントエンドAPI呼び出し (TypeScript)
```typescript
interface ApiResponse<T> {
success: boolean
data?: T
error?: string
}
async function fetchApi<T>(
endpoint: string,
options?: RequestInit
): Promise<ApiResponse<T>> {
try {
const response = await fetch(`/api${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
})
if (!response.ok) {
return { success: false, error: `HTTP ${response.status}` }
}
return await response.json()
} catch (error) {
return { success: false, error: String(error) }
}
}
```
### Claude AI統合構造化出力
```python
from anthropic import Anthropic
from pydantic import BaseModel
class AnalysisResult(BaseModel):
summary: str
key_points: list[str]
confidence: float
async def analyze_with_claude(content: str) -> AnalysisResult:
client = Anthropic()
response = client.messages.create(
model="claude-sonnet-4-5-20250514",
max_tokens=1024,
messages=[{"role": "user", "content": content}],
tools=[{
"name": "provide_analysis",
"description": "Provide structured analysis",
"input_schema": AnalysisResult.model_json_schema()
}],
tool_choice={"type": "tool", "name": "provide_analysis"}
)
# Extract tool use result
tool_use = next(
block for block in response.content
if block.type == "tool_use"
)
return AnalysisResult(**tool_use.input)
```
### カスタムフック (React)
```typescript
import { useState, useCallback } from 'react'
interface UseApiState<T> {
data: T | null
loading: boolean
error: string | null
}
export function useApi<T>(
fetchFn: () => Promise<ApiResponse<T>>
) {
const [state, setState] = useState<UseApiState<T>>({
data: null,
loading: false,
error: null,
})
const execute = useCallback(async () => {
setState(prev => ({ ...prev, loading: true, error: null }))
const result = await fetchFn()
if (result.success) {
setState({ data: result.data!, loading: false, error: null })
} else {
setState({ data: null, loading: false, error: result.error! })
}
}, [fetchFn])
return { ...state, execute }
}
```
---
## テスト要件
### バックエンド (pytest)
```bash
# すべてのテストを実行
poetry run pytest tests/
# カバレッジ付きで実行
poetry run pytest tests/ --cov=. --cov-report=html
# 特定のテストファイルを実行
poetry run pytest tests/test_auth.py -v
```
**テスト構造:**
```python
import pytest
from httpx import AsyncClient
from main import app
@pytest.fixture
async def client():
async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac
@pytest.mark.asyncio
async def test_health_check(client: AsyncClient):
response = await client.get("/health")
assert response.status_code == 200
assert response.json()["status"] == "healthy"
```
### フロントエンド (React Testing Library)
```bash
# テストを実行
npm run test
# カバレッジ付きで実行
npm run test -- --coverage
# E2Eテストを実行
npm run test:e2e
```
**テスト構造:**
```typescript
import { render, screen, fireEvent } from '@testing-library/react'
import { WorkspacePanel } from './WorkspacePanel'
describe('WorkspacePanel', () => {
it('renders workspace correctly', () => {
render(<WorkspacePanel />)
expect(screen.getByRole('main')).toBeInTheDocument()
})
it('handles session creation', async () => {
render(<WorkspacePanel />)
fireEvent.click(screen.getByText('New Session'))
expect(await screen.findByText('Session created')).toBeInTheDocument()
})
})
```
---
## デプロイメントワークフロー
### デプロイ前チェックリスト
- [ ] すべてのテストがローカルで成功
- [ ] `npm run build` が成功(フロントエンド)
- [ ] `poetry run pytest` が成功(バックエンド)
- [ ] ハードコードされたシークレットなし
- [ ] 環境変数がドキュメント化されている
- [ ] データベースマイグレーションが準備されている
### デプロイメントコマンド
```bash
# フロントエンドのビルドとデプロイ
cd frontend && npm run build
gcloud run deploy frontend --source .
# バックエンドのビルドとデプロイ
cd backend
gcloud run deploy backend --source .
```
### 環境変数
```bash
# フロントエンド (.env.local)
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
# バックエンド (.env)
DATABASE_URL=postgresql://...
ANTHROPIC_API_KEY=sk-ant-...
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_KEY=eyJ...
```
---
## 重要なルール
1. **絵文字なし** - コード、コメント、ドキュメントに絵文字を使用しない
2. **不変性** - オブジェクトや配列を変更しない
3. **TDD** - 実装前にテストを書く
4. **80%カバレッジ** - 最低基準
5. **小さなファイル多数** - 通常200-400行、最大800行
6. **console.log禁止** - 本番コードには使用しない
7. **適切なエラー処理** - try/catchを使用
8. **入力検証** - Pydantic/Zodを使用
---
## 関連スキル
- `coding-standards.md` - 一般的なコーディングベストプラクティス
- `backend-patterns.md` - APIとデータベースパターン
- `frontend-patterns.md` - ReactとNext.jsパターン
- `tdd-workflow/` - テスト駆動開発の方法論

View File

@@ -1,749 +0,0 @@
---
name: python-patterns
description: Pythonic イディオム、PEP 8標準、型ヒント、堅牢で効率的かつ保守可能なPythonアプリケーションを構築するためのベストプラクティス。
---
# Python開発パターン
堅牢で効率的かつ保守可能なアプリケーションを構築するための慣用的なPythonパターンとベストプラクティス。
## いつ有効化するか
- 新しいPythonコードを書くとき
- Pythonコードをレビューするとき
- 既存のPythonコードをリファクタリングするとき
- Pythonパッケージ/モジュールを設計するとき
## 核となる原則
### 1. 可読性が重要
Pythonは可読性を優先します。コードは明白で理解しやすいものであるべきです。
```python
# Good: Clear and readable
def get_active_users(users: list[User]) -> list[User]:
"""Return only active users from the provided list."""
return [user for user in users if user.is_active]
# Bad: Clever but confusing
def get_active_users(u):
return [x for x in u if x.a]
```
### 2. 明示的は暗黙的より良い
魔法を避け、コードが何をしているかを明確にしましょう。
```python
# Good: Explicit configuration
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Bad: Hidden side effects
import some_module
some_module.setup() # What does this do?
```
### 3. EAFP - 許可を求めるより許しを請う方が簡単
Pythonは条件チェックよりも例外処理を好みます。
```python
# Good: EAFP style
def get_value(dictionary: dict, key: str) -> Any:
try:
return dictionary[key]
except KeyError:
return default_value
# Bad: LBYL (Look Before You Leap) style
def get_value(dictionary: dict, key: str) -> Any:
if key in dictionary:
return dictionary[key]
else:
return default_value
```
## 型ヒント
### 基本的な型アノテーション
```python
from typing import Optional, List, Dict, Any
def process_user(
user_id: str,
data: Dict[str, Any],
active: bool = True
) -> Optional[User]:
"""Process a user and return the updated User or None."""
if not active:
return None
return User(user_id, data)
```
### モダンな型ヒントPython 3.9+
```python
# Python 3.9+ - Use built-in types
def process_items(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
# Python 3.8 and earlier - Use typing module
from typing import List, Dict
def process_items(items: List[str]) -> Dict[str, int]:
return {item: len(item) for item in items}
```
### 型エイリアスとTypeVar
```python
from typing import TypeVar, Union
# Type alias for complex types
JSON = Union[dict[str, Any], list[Any], str, int, float, bool, None]
def parse_json(data: str) -> JSON:
return json.loads(data)
# Generic types
T = TypeVar('T')
def first(items: list[T]) -> T | None:
"""Return the first item or None if list is empty."""
return items[0] if items else None
```
### プロトコルベースのダックタイピング
```python
from typing import Protocol
class Renderable(Protocol):
def render(self) -> str:
"""Render the object to a string."""
def render_all(items: list[Renderable]) -> str:
"""Render all items that implement the Renderable protocol."""
return "\n".join(item.render() for item in items)
```
## エラーハンドリングパターン
### 特定の例外処理
```python
# Good: Catch specific exceptions
def load_config(path: str) -> Config:
try:
with open(path) as f:
return Config.from_json(f.read())
except FileNotFoundError as e:
raise ConfigError(f"Config file not found: {path}") from e
except json.JSONDecodeError as e:
raise ConfigError(f"Invalid JSON in config: {path}") from e
# Bad: Bare except
def load_config(path: str) -> Config:
try:
with open(path) as f:
return Config.from_json(f.read())
except:
return None # Silent failure!
```
### 例外の連鎖
```python
def process_data(data: str) -> Result:
try:
parsed = json.loads(data)
except json.JSONDecodeError as e:
# Chain exceptions to preserve the traceback
raise ValueError(f"Failed to parse data: {data}") from e
```
### カスタム例外階層
```python
class AppError(Exception):
"""Base exception for all application errors."""
pass
class ValidationError(AppError):
"""Raised when input validation fails."""
pass
class NotFoundError(AppError):
"""Raised when a requested resource is not found."""
pass
# Usage
def get_user(user_id: str) -> User:
user = db.find_user(user_id)
if not user:
raise NotFoundError(f"User not found: {user_id}")
return user
```
## コンテキストマネージャ
### リソース管理
```python
# Good: Using context managers
def process_file(path: str) -> str:
with open(path, 'r') as f:
return f.read()
# Bad: Manual resource management
def process_file(path: str) -> str:
f = open(path, 'r')
try:
return f.read()
finally:
f.close()
```
### カスタムコンテキストマネージャ
```python
from contextlib import contextmanager
@contextmanager
def timer(name: str):
"""Context manager to time a block of code."""
start = time.perf_counter()
yield
elapsed = time.perf_counter() - start
print(f"{name} took {elapsed:.4f} seconds")
# Usage
with timer("data processing"):
process_large_dataset()
```
### コンテキストマネージャクラス
```python
class DatabaseTransaction:
def __init__(self, connection):
self.connection = connection
def __enter__(self):
self.connection.begin_transaction()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.connection.commit()
else:
self.connection.rollback()
return False # Don't suppress exceptions
# Usage
with DatabaseTransaction(conn):
user = conn.create_user(user_data)
conn.create_profile(user.id, profile_data)
```
## 内包表記とジェネレータ
### リスト内包表記
```python
# Good: List comprehension for simple transformations
names = [user.name for user in users if user.is_active]
# Bad: Manual loop
names = []
for user in users:
if user.is_active:
names.append(user.name)
# Complex comprehensions should be expanded
# Bad: Too complex
result = [x * 2 for x in items if x > 0 if x % 2 == 0]
# Good: Use a generator function
def filter_and_transform(items: Iterable[int]) -> list[int]:
result = []
for x in items:
if x > 0 and x % 2 == 0:
result.append(x * 2)
return result
```
### ジェネレータ式
```python
# Good: Generator for lazy evaluation
total = sum(x * x for x in range(1_000_000))
# Bad: Creates large intermediate list
total = sum([x * x for x in range(1_000_000)])
```
### ジェネレータ関数
```python
def read_large_file(path: str) -> Iterator[str]:
"""Read a large file line by line."""
with open(path) as f:
for line in f:
yield line.strip()
# Usage
for line in read_large_file("huge.txt"):
process(line)
```
## データクラスと名前付きタプル
### データクラス
```python
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class User:
"""User entity with automatic __init__, __repr__, and __eq__."""
id: str
name: str
email: str
created_at: datetime = field(default_factory=datetime.now)
is_active: bool = True
# Usage
user = User(
id="123",
name="Alice",
email="alice@example.com"
)
```
### バリデーション付きデータクラス
```python
@dataclass
class User:
email: str
age: int
def __post_init__(self):
# Validate email format
if "@" not in self.email:
raise ValueError(f"Invalid email: {self.email}")
# Validate age range
if self.age < 0 or self.age > 150:
raise ValueError(f"Invalid age: {self.age}")
```
### 名前付きタプル
```python
from typing import NamedTuple
class Point(NamedTuple):
"""Immutable 2D point."""
x: float
y: float
def distance(self, other: 'Point') -> float:
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
# Usage
p1 = Point(0, 0)
p2 = Point(3, 4)
print(p1.distance(p2)) # 5.0
```
## デコレータ
### 関数デコレータ
```python
import functools
import time
def timer(func: Callable) -> Callable:
"""Decorator to time function execution."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
# slow_function() prints: slow_function took 1.0012s
```
### パラメータ化デコレータ
```python
def repeat(times: int):
"""Decorator to repeat a function multiple times."""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
results = []
for _ in range(times):
results.append(func(*args, **kwargs))
return results
return wrapper
return decorator
@repeat(times=3)
def greet(name: str) -> str:
return f"Hello, {name}!"
# greet("Alice") returns ["Hello, Alice!", "Hello, Alice!", "Hello, Alice!"]
```
### クラスベースのデコレータ
```python
class CountCalls:
"""Decorator that counts how many times a function is called."""
def __init__(self, func: Callable):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} has been called {self.count} times")
return self.func(*args, **kwargs)
@CountCalls
def process():
pass
# Each call to process() prints the call count
```
## 並行処理パターン
### I/Oバウンドタスク用のスレッド
```python
import concurrent.futures
import threading
def fetch_url(url: str) -> str:
"""Fetch a URL (I/O-bound operation)."""
import urllib.request
with urllib.request.urlopen(url) as response:
return response.read().decode()
def fetch_all_urls(urls: list[str]) -> dict[str, str]:
"""Fetch multiple URLs concurrently using threads."""
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
future_to_url = {executor.submit(fetch_url, url): url for url in urls}
results = {}
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
results[url] = future.result()
except Exception as e:
results[url] = f"Error: {e}"
return results
```
### CPUバウンドタスク用のマルチプロセシング
```python
def process_data(data: list[int]) -> int:
"""CPU-intensive computation."""
return sum(x ** 2 for x in data)
def process_all(datasets: list[list[int]]) -> list[int]:
"""Process multiple datasets using multiple processes."""
with concurrent.futures.ProcessPoolExecutor() as executor:
results = list(executor.map(process_data, datasets))
return results
```
### 並行I/O用のAsync/Await
```python
import asyncio
async def fetch_async(url: str) -> str:
"""Fetch a URL asynchronously."""
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
async def fetch_all(urls: list[str]) -> dict[str, str]:
"""Fetch multiple URLs concurrently."""
tasks = [fetch_async(url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
return dict(zip(urls, results))
```
## パッケージ構成
### 標準プロジェクトレイアウト
```
myproject/
├── src/
│ └── mypackage/
│ ├── __init__.py
│ ├── main.py
│ ├── api/
│ │ ├── __init__.py
│ │ └── routes.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── user.py
│ └── utils/
│ ├── __init__.py
│ └── helpers.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_api.py
│ └── test_models.py
├── pyproject.toml
├── README.md
└── .gitignore
```
### インポート規約
```python
# Good: Import order - stdlib, third-party, local
import os
import sys
from pathlib import Path
import requests
from fastapi import FastAPI
from mypackage.models import User
from mypackage.utils import format_name
# Good: Use isort for automatic import sorting
# pip install isort
```
### パッケージエクスポート用の__init__.py
```python
# mypackage/__init__.py
"""mypackage - A sample Python package."""
__version__ = "1.0.0"
# Export main classes/functions at package level
from mypackage.models import User, Post
from mypackage.utils import format_name
__all__ = ["User", "Post", "format_name"]
```
## メモリとパフォーマンス
### メモリ効率化のための__slots__使用
```python
# Bad: Regular class uses __dict__ (more memory)
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
# Good: __slots__ reduces memory usage
class Point:
__slots__ = ['x', 'y']
def __init__(self, x: float, y: float):
self.x = x
self.y = y
```
### 大量データ用のジェネレータ
```python
# Bad: Returns full list in memory
def read_lines(path: str) -> list[str]:
with open(path) as f:
return [line.strip() for line in f]
# Good: Yields lines one at a time
def read_lines(path: str) -> Iterator[str]:
with open(path) as f:
for line in f:
yield line.strip()
```
### ループ内での文字列連結を避ける
```python
# Bad: O(n²) due to string immutability
result = ""
for item in items:
result += str(item)
# Good: O(n) using join
result = "".join(str(item) for item in items)
# Good: Using StringIO for building
from io import StringIO
buffer = StringIO()
for item in items:
buffer.write(str(item))
result = buffer.getvalue()
```
## Pythonツール統合
### 基本コマンド
```bash
# Code formatting
black .
isort .
# Linting
ruff check .
pylint mypackage/
# Type checking
mypy .
# Testing
pytest --cov=mypackage --cov-report=html
# Security scanning
bandit -r .
# Dependency management
pip-audit
safety check
```
### pyproject.toml設定
```toml
[project]
name = "mypackage"
version = "1.0.0"
requires-python = ">=3.9"
dependencies = [
"requests>=2.31.0",
"pydantic>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.5.0",
]
[tool.black]
line-length = 88
target-version = ['py39']
[tool.ruff]
line-length = 88
select = ["E", "F", "I", "N", "W"]
[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--cov=mypackage --cov-report=term-missing"
```
## クイックリファレンスPythonイディオム
| イディオム | 説明 |
|-------|-------------|
| EAFP | 許可を求めるより許しを請う方が簡単 |
| コンテキストマネージャ | リソース管理には`with`を使用 |
| リスト内包表記 | 簡単な変換用 |
| ジェネレータ | 遅延評価と大規模データセット用 |
| 型ヒント | 関数シグネチャへのアノテーション |
| データクラス | 自動生成メソッド付きデータコンテナ用 |
| `__slots__` | メモリ最適化用 |
| f-strings | 文字列フォーマット用Python 3.6+ |
| `pathlib.Path` | パス操作用Python 3.4+ |
| `enumerate` | ループ内のインデックス-要素ペア用 |
## 避けるべきアンチパターン
```python
# Bad: Mutable default arguments
def append_to(item, items=[]):
items.append(item)
return items
# Good: Use None and create new list
def append_to(item, items=None):
if items is None:
items = []
items.append(item)
return items
# Bad: Checking type with type()
if type(obj) == list:
process(obj)
# Good: Use isinstance
if isinstance(obj, list):
process(obj)
# Bad: Comparing to None with ==
if value == None:
process()
# Good: Use is
if value is None:
process()
# Bad: from module import *
from os.path import *
# Good: Explicit imports
from os.path import join, exists
# Bad: Bare except
try:
risky_operation()
except:
pass
# Good: Specific exception
try:
risky_operation()
except SpecificError as e:
logger.error(f"Operation failed: {e}")
```
**覚えておいてください**: Pythonコードは読みやすく、明示的で、最小の驚きの原則に従うべきです。迷ったときは、巧妙さよりも明確さを優先してください。

View File

@@ -1,815 +0,0 @@
---
name: python-testing
description: pytest、TDD手法、フィクスチャ、モック、パラメータ化、カバレッジ要件を使用したPythonテスト戦略。
---
# Pythonテストパターン
pytest、TDD方法論、ベストプラクティスを使用したPythonアプリケーションの包括的なテスト戦略。
## いつ有効化するか
- 新しいPythonコードを書くときTDDに従う赤、緑、リファクタリング
- Pythonプロジェクトのテストスイートを設計するとき
- Pythonテストカバレッジをレビューするとき
- テストインフラストラクチャをセットアップするとき
## 核となるテスト哲学
### テスト駆動開発TDD
常にTDDサイクルに従います。
1. **赤**: 期待される動作のための失敗するテストを書く
2. **緑**: テストを通過させるための最小限のコードを書く
3. **リファクタリング**: テストを通過させたままコードを改善する
```python
# Step 1: Write failing test (RED)
def test_add_numbers():
result = add(2, 3)
assert result == 5
# Step 2: Write minimal implementation (GREEN)
def add(a, b):
return a + b
# Step 3: Refactor if needed (REFACTOR)
```
### カバレッジ要件
- **目標**: 80%以上のコードカバレッジ
- **クリティカルパス**: 100%のカバレッジが必要
- `pytest --cov`を使用してカバレッジを測定
```bash
pytest --cov=mypackage --cov-report=term-missing --cov-report=html
```
## pytestの基礎
### 基本的なテスト構造
```python
import pytest
def test_addition():
"""Test basic addition."""
assert 2 + 2 == 4
def test_string_uppercase():
"""Test string uppercasing."""
text = "hello"
assert text.upper() == "HELLO"
def test_list_append():
"""Test list append."""
items = [1, 2, 3]
items.append(4)
assert 4 in items
assert len(items) == 4
```
### アサーション
```python
# Equality
assert result == expected
# Inequality
assert result != unexpected
# Truthiness
assert result # Truthy
assert not result # Falsy
assert result is True # Exactly True
assert result is False # Exactly False
assert result is None # Exactly None
# Membership
assert item in collection
assert item not in collection
# Comparisons
assert result > 0
assert 0 <= result <= 100
# Type checking
assert isinstance(result, str)
# Exception testing (preferred approach)
with pytest.raises(ValueError):
raise ValueError("error message")
# Check exception message
with pytest.raises(ValueError, match="invalid input"):
raise ValueError("invalid input provided")
# Check exception attributes
with pytest.raises(ValueError) as exc_info:
raise ValueError("error message")
assert str(exc_info.value) == "error message"
```
## フィクスチャ
### 基本的なフィクスチャ使用
```python
import pytest
@pytest.fixture
def sample_data():
"""Fixture providing sample data."""
return {"name": "Alice", "age": 30}
def test_sample_data(sample_data):
"""Test using the fixture."""
assert sample_data["name"] == "Alice"
assert sample_data["age"] == 30
```
### セットアップ/ティアダウン付きフィクスチャ
```python
@pytest.fixture
def database():
"""Fixture with setup and teardown."""
# Setup
db = Database(":memory:")
db.create_tables()
db.insert_test_data()
yield db # Provide to test
# Teardown
db.close()
def test_database_query(database):
"""Test database operations."""
result = database.query("SELECT * FROM users")
assert len(result) > 0
```
### フィクスチャスコープ
```python
# Function scope (default) - runs for each test
@pytest.fixture
def temp_file():
with open("temp.txt", "w") as f:
yield f
os.remove("temp.txt")
# Module scope - runs once per module
@pytest.fixture(scope="module")
def module_db():
db = Database(":memory:")
db.create_tables()
yield db
db.close()
# Session scope - runs once per test session
@pytest.fixture(scope="session")
def shared_resource():
resource = ExpensiveResource()
yield resource
resource.cleanup()
```
### パラメータ付きフィクスチャ
```python
@pytest.fixture(params=[1, 2, 3])
def number(request):
"""Parameterized fixture."""
return request.param
def test_numbers(number):
"""Test runs 3 times, once for each parameter."""
assert number > 0
```
### 複数のフィクスチャ使用
```python
@pytest.fixture
def user():
return User(id=1, name="Alice")
@pytest.fixture
def admin():
return User(id=2, name="Admin", role="admin")
def test_user_admin_interaction(user, admin):
"""Test using multiple fixtures."""
assert admin.can_manage(user)
```
### 自動使用フィクスチャ
```python
@pytest.fixture(autouse=True)
def reset_config():
"""Automatically runs before every test."""
Config.reset()
yield
Config.cleanup()
def test_without_fixture_call():
# reset_config runs automatically
assert Config.get_setting("debug") is False
```
### 共有フィクスチャ用のConftest.py
```python
# tests/conftest.py
import pytest
@pytest.fixture
def client():
"""Shared fixture for all tests."""
app = create_app(testing=True)
with app.test_client() as client:
yield client
@pytest.fixture
def auth_headers(client):
"""Generate auth headers for API testing."""
response = client.post("/api/login", json={
"username": "test",
"password": "test"
})
token = response.json["token"]
return {"Authorization": f"Bearer {token}"}
```
## パラメータ化
### 基本的なパラメータ化
```python
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("world", "WORLD"),
("PyThOn", "PYTHON"),
])
def test_uppercase(input, expected):
"""Test runs 3 times with different inputs."""
assert input.upper() == expected
```
### 複数パラメータ
```python
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_add(a, b, expected):
"""Test addition with multiple inputs."""
assert add(a, b) == expected
```
### ID付きパラメータ化
```python
@pytest.mark.parametrize("input,expected", [
("valid@email.com", True),
("invalid", False),
("@no-domain.com", False),
], ids=["valid-email", "missing-at", "missing-domain"])
def test_email_validation(input, expected):
"""Test email validation with readable test IDs."""
assert is_valid_email(input) is expected
```
### パラメータ化フィクスチャ
```python
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
def db(request):
"""Test against multiple database backends."""
if request.param == "sqlite":
return Database(":memory:")
elif request.param == "postgresql":
return Database("postgresql://localhost/test")
elif request.param == "mysql":
return Database("mysql://localhost/test")
def test_database_operations(db):
"""Test runs 3 times, once for each database."""
result = db.query("SELECT 1")
assert result is not None
```
## マーカーとテスト選択
### カスタムマーカー
```python
# Mark slow tests
@pytest.mark.slow
def test_slow_operation():
time.sleep(5)
# Mark integration tests
@pytest.mark.integration
def test_api_integration():
response = requests.get("https://api.example.com")
assert response.status_code == 200
# Mark unit tests
@pytest.mark.unit
def test_unit_logic():
assert calculate(2, 3) == 5
```
### 特定のテストを実行
```bash
# Run only fast tests
pytest -m "not slow"
# Run only integration tests
pytest -m integration
# Run integration or slow tests
pytest -m "integration or slow"
# Run tests marked as unit but not slow
pytest -m "unit and not slow"
```
### pytest.iniでマーカーを設定
```ini
[pytest]
markers =
slow: marks tests as slow
integration: marks tests as integration tests
unit: marks tests as unit tests
django: marks tests as requiring Django
```
## モックとパッチ
### 関数のモック
```python
from unittest.mock import patch, Mock
@patch("mypackage.external_api_call")
def test_with_mock(api_call_mock):
"""Test with mocked external API."""
api_call_mock.return_value = {"status": "success"}
result = my_function()
api_call_mock.assert_called_once()
assert result["status"] == "success"
```
### 戻り値のモック
```python
@patch("mypackage.Database.connect")
def test_database_connection(connect_mock):
"""Test with mocked database connection."""
connect_mock.return_value = MockConnection()
db = Database()
db.connect()
connect_mock.assert_called_once_with("localhost")
```
### 例外のモック
```python
@patch("mypackage.api_call")
def test_api_error_handling(api_call_mock):
"""Test error handling with mocked exception."""
api_call_mock.side_effect = ConnectionError("Network error")
with pytest.raises(ConnectionError):
api_call()
api_call_mock.assert_called_once()
```
### コンテキストマネージャのモック
```python
@patch("builtins.open", new_callable=mock_open)
def test_file_reading(mock_file):
"""Test file reading with mocked open."""
mock_file.return_value.read.return_value = "file content"
result = read_file("test.txt")
mock_file.assert_called_once_with("test.txt", "r")
assert result == "file content"
```
### Autospec使用
```python
@patch("mypackage.DBConnection", autospec=True)
def test_autospec(db_mock):
"""Test with autospec to catch API misuse."""
db = db_mock.return_value
db.query("SELECT * FROM users")
# This would fail if DBConnection doesn't have query method
db_mock.assert_called_once()
```
### クラスインスタンスのモック
```python
class TestUserService:
@patch("mypackage.UserRepository")
def test_create_user(self, repo_mock):
"""Test user creation with mocked repository."""
repo_mock.return_value.save.return_value = User(id=1, name="Alice")
service = UserService(repo_mock.return_value)
user = service.create_user(name="Alice")
assert user.name == "Alice"
repo_mock.return_value.save.assert_called_once()
```
### プロパティのモック
```python
@pytest.fixture
def mock_config():
"""Create a mock with a property."""
config = Mock()
type(config).debug = PropertyMock(return_value=True)
type(config).api_key = PropertyMock(return_value="test-key")
return config
def test_with_mock_config(mock_config):
"""Test with mocked config properties."""
assert mock_config.debug is True
assert mock_config.api_key == "test-key"
```
## 非同期コードのテスト
### pytest-asyncioを使用した非同期テスト
```python
import pytest
@pytest.mark.asyncio
async def test_async_function():
"""Test async function."""
result = await async_add(2, 3)
assert result == 5
@pytest.mark.asyncio
async def test_async_with_fixture(async_client):
"""Test async with async fixture."""
response = await async_client.get("/api/users")
assert response.status_code == 200
```
### 非同期フィクスチャ
```python
@pytest.fixture
async def async_client():
"""Async fixture providing async test client."""
app = create_app()
async with app.test_client() as client:
yield client
@pytest.mark.asyncio
async def test_api_endpoint(async_client):
"""Test using async fixture."""
response = await async_client.get("/api/data")
assert response.status_code == 200
```
### 非同期関数のモック
```python
@pytest.mark.asyncio
@patch("mypackage.async_api_call")
async def test_async_mock(api_call_mock):
"""Test async function with mock."""
api_call_mock.return_value = {"status": "ok"}
result = await my_async_function()
api_call_mock.assert_awaited_once()
assert result["status"] == "ok"
```
## 例外のテスト
### 期待される例外のテスト
```python
def test_divide_by_zero():
"""Test that dividing by zero raises ZeroDivisionError."""
with pytest.raises(ZeroDivisionError):
divide(10, 0)
def test_custom_exception():
"""Test custom exception with message."""
with pytest.raises(ValueError, match="invalid input"):
validate_input("invalid")
```
### 例外属性のテスト
```python
def test_exception_with_details():
"""Test exception with custom attributes."""
with pytest.raises(CustomError) as exc_info:
raise CustomError("error", code=400)
assert exc_info.value.code == 400
assert "error" in str(exc_info.value)
```
## 副作用のテスト
### ファイル操作のテスト
```python
import tempfile
import os
def test_file_processing():
"""Test file processing with temp file."""
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
f.write("test content")
temp_path = f.name
try:
result = process_file(temp_path)
assert result == "processed: test content"
finally:
os.unlink(temp_path)
```
### pytestのtmp_pathフィクスチャを使用したテスト
```python
def test_with_tmp_path(tmp_path):
"""Test using pytest's built-in temp path fixture."""
test_file = tmp_path / "test.txt"
test_file.write_text("hello world")
result = process_file(str(test_file))
assert result == "hello world"
# tmp_path automatically cleaned up
```
### tmpdirフィクスチャを使用したテスト
```python
def test_with_tmpdir(tmpdir):
"""Test using pytest's tmpdir fixture."""
test_file = tmpdir.join("test.txt")
test_file.write("data")
result = process_file(str(test_file))
assert result == "data"
```
## テストの整理
### ディレクトリ構造
```
tests/
├── conftest.py # Shared fixtures
├── __init__.py
├── unit/ # Unit tests
│ ├── __init__.py
│ ├── test_models.py
│ ├── test_utils.py
│ └── test_services.py
├── integration/ # Integration tests
│ ├── __init__.py
│ ├── test_api.py
│ └── test_database.py
└── e2e/ # End-to-end tests
├── __init__.py
└── test_user_flow.py
```
### テストクラス
```python
class TestUserService:
"""Group related tests in a class."""
@pytest.fixture(autouse=True)
def setup(self):
"""Setup runs before each test in this class."""
self.service = UserService()
def test_create_user(self):
"""Test user creation."""
user = self.service.create_user("Alice")
assert user.name == "Alice"
def test_delete_user(self):
"""Test user deletion."""
user = User(id=1, name="Bob")
self.service.delete_user(user)
assert not self.service.user_exists(1)
```
## ベストプラクティス
### すべきこと
- **TDDに従う**: コードの前にテストを書く(赤-緑-リファクタリング)
- **一つのことをテスト**: 各テストは単一の動作を検証すべき
- **説明的な名前を使用**: `test_user_login_with_invalid_credentials_fails`
- **フィクスチャを使用**: フィクスチャで重複を排除
- **外部依存をモック**: 外部サービスに依存しない
- **エッジケースをテスト**: 空の入力、None値、境界条件
- **80%以上のカバレッジを目指す**: クリティカルパスに焦点を当てる
- **テストを高速に保つ**: マークを使用して遅いテストを分離
### してはいけないこと
- **実装をテストしない**: 内部ではなく動作をテスト
- **テストで複雑な条件文を使用しない**: テストをシンプルに保つ
- **テスト失敗を無視しない**: すべてのテストは通過する必要がある
- **サードパーティコードをテストしない**: ライブラリが機能することを信頼
- **テスト間で状態を共有しない**: テストは独立すべき
- **テストで例外をキャッチしない**: `pytest.raises`を使用
- **print文を使用しない**: アサーションとpytestの出力を使用
- **脆弱すぎるテストを書かない**: 過度に具体的なモックを避ける
## 一般的なパターン
### APIエンドポイントのテストFastAPI/Flask
```python
@pytest.fixture
def client():
app = create_app(testing=True)
return app.test_client()
def test_get_user(client):
response = client.get("/api/users/1")
assert response.status_code == 200
assert response.json["id"] == 1
def test_create_user(client):
response = client.post("/api/users", json={
"name": "Alice",
"email": "alice@example.com"
})
assert response.status_code == 201
assert response.json["name"] == "Alice"
```
### データベース操作のテスト
```python
@pytest.fixture
def db_session():
"""Create a test database session."""
session = Session(bind=engine)
session.begin_nested()
yield session
session.rollback()
session.close()
def test_create_user(db_session):
user = User(name="Alice", email="alice@example.com")
db_session.add(user)
db_session.commit()
retrieved = db_session.query(User).filter_by(name="Alice").first()
assert retrieved.email == "alice@example.com"
```
### クラスメソッドのテスト
```python
class TestCalculator:
@pytest.fixture
def calculator(self):
return Calculator()
def test_add(self, calculator):
assert calculator.add(2, 3) == 5
def test_divide_by_zero(self, calculator):
with pytest.raises(ZeroDivisionError):
calculator.divide(10, 0)
```
## pytest設定
### pytest.ini
```ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--strict-markers
--disable-warnings
--cov=mypackage
--cov-report=term-missing
--cov-report=html
markers =
slow: marks tests as slow
integration: marks tests as integration tests
unit: marks tests as unit tests
```
### pyproject.toml
```toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--strict-markers",
"--cov=mypackage",
"--cov-report=term-missing",
"--cov-report=html",
]
markers = [
"slow: marks tests as slow",
"integration: marks tests as integration tests",
"unit: marks tests as unit tests",
]
```
## テストの実行
```bash
# Run all tests
pytest
# Run specific file
pytest tests/test_utils.py
# Run specific test
pytest tests/test_utils.py::test_function
# Run with verbose output
pytest -v
# Run with coverage
pytest --cov=mypackage --cov-report=html
# Run only fast tests
pytest -m "not slow"
# Run until first failure
pytest -x
# Run and stop on N failures
pytest --maxfail=3
# Run last failed tests
pytest --lf
# Run tests with pattern
pytest -k "test_user"
# Run with debugger on failure
pytest --pdb
```
## クイックリファレンス
| パターン | 使用法 |
|---------|-------|
| `pytest.raises()` | 期待される例外をテスト |
| `@pytest.fixture()` | 再利用可能なテストフィクスチャを作成 |
| `@pytest.mark.parametrize()` | 複数の入力でテストを実行 |
| `@pytest.mark.slow` | 遅いテストをマーク |
| `pytest -m "not slow"` | 遅いテストをスキップ |
| `@patch()` | 関数とクラスをモック |
| `tmp_path`フィクスチャ | 自動一時ディレクトリ |
| `pytest --cov` | カバレッジレポートを生成 |
| `assert` | シンプルで読みやすいアサーション |
**覚えておいてください**: テストもコードです。それらをクリーンで、読みやすく、保守可能に保ちましょう。良いテストはバグをキャッチし、優れたテストはそれらを防ぎます。

View File

@@ -1,494 +0,0 @@
---
name: security-review
description: 認証の追加、ユーザー入力の処理、シークレットの操作、APIエンドポイントの作成、支払い/機密機能の実装時にこのスキルを使用します。包括的なセキュリティチェックリストとパターンを提供します。
---
# セキュリティレビュースキル
このスキルは、すべてのコードがセキュリティのベストプラクティスに従い、潜在的な脆弱性を特定することを保証します。
## 有効化するタイミング
- 認証または認可の実装
- ユーザー入力またはファイルアップロードの処理
- 新しいAPIエンドポイントの作成
- シークレットまたは資格情報の操作
- 支払い機能の実装
- 機密データの保存または送信
- サードパーティAPIの統合
## セキュリティチェックリスト
### 1. シークレット管理
#### ❌ 絶対にしないこと
```typescript
const apiKey = "sk-proj-xxxxx" // ハードコードされたシークレット
const dbPassword = "password123" // ソースコード内
```
#### ✅ 常にすること
```typescript
const apiKey = process.env.OPENAI_API_KEY
const dbUrl = process.env.DATABASE_URL
// シークレットが存在することを確認
if (!apiKey) {
throw new Error('OPENAI_API_KEY not configured')
}
```
#### 検証ステップ
- [ ] ハードコードされたAPIキー、トークン、パスワードなし
- [ ] すべてのシークレットを環境変数に
- [ ] `.env.local`を.gitignoreに
- [ ] git履歴にシークレットなし
- [ ] 本番シークレットはホスティングプラットフォームVercel、Railway
### 2. 入力検証
#### 常にユーザー入力を検証
```typescript
import { z } from 'zod'
// 検証スキーマを定義
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(0).max(150)
})
// 処理前に検証
export async function createUser(input: unknown) {
try {
const validated = CreateUserSchema.parse(input)
return await db.users.create(validated)
} catch (error) {
if (error instanceof z.ZodError) {
return { success: false, errors: error.errors }
}
throw error
}
}
```
#### ファイルアップロード検証
```typescript
function validateFileUpload(file: File) {
// サイズチェック最大5MB
const maxSize = 5 * 1024 * 1024
if (file.size > maxSize) {
throw new Error('File too large (max 5MB)')
}
// タイプチェック
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid file type')
}
// 拡張子チェック
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif']
const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0]
if (!extension || !allowedExtensions.includes(extension)) {
throw new Error('Invalid file extension')
}
return true
}
```
#### 検証ステップ
- [ ] すべてのユーザー入力をスキーマで検証
- [ ] ファイルアップロードを制限(サイズ、タイプ、拡張子)
- [ ] クエリでのユーザー入力の直接使用なし
- [ ] ホワイトリスト検証(ブラックリストではなく)
- [ ] エラーメッセージが機密情報を漏らさない
### 3. SQLインジェクション防止
#### ❌ 絶対にSQLを連結しない
```typescript
// 危険 - SQLインジェクションの脆弱性
const query = `SELECT * FROM users WHERE email = '${userEmail}'`
await db.query(query)
```
#### ✅ 常にパラメータ化されたクエリを使用
```typescript
// 安全 - パラメータ化されたクエリ
const { data } = await supabase
.from('users')
.select('*')
.eq('email', userEmail)
// または生のSQLで
await db.query(
'SELECT * FROM users WHERE email = $1',
[userEmail]
)
```
#### 検証ステップ
- [ ] すべてのデータベースクエリがパラメータ化されたクエリを使用
- [ ] SQLでの文字列連結なし
- [ ] ORM/クエリビルダーを正しく使用
- [ ] Supabaseクエリが適切にサニタイズされている
### 4. 認証と認可
#### JWTトークン処理
```typescript
// ❌ 誤りlocalStorageXSSに脆弱
localStorage.setItem('token', token)
// ✅ 正解httpOnly Cookie
res.setHeader('Set-Cookie',
`token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`)
```
#### 認可チェック
```typescript
export async function deleteUser(userId: string, requesterId: string) {
// 常に最初に認可を確認
const requester = await db.users.findUnique({
where: { id: requesterId }
})
if (requester.role !== 'admin') {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 403 }
)
}
// 削除を続行
await db.users.delete({ where: { id: userId } })
}
```
#### 行レベルセキュリティ (Supabase)
```sql
-- すべてのテーブルでRLSを有効化
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
-- ユーザーは自分のデータのみを表示できる
CREATE POLICY "Users view own data"
ON users FOR SELECT
USING (auth.uid() = id);
-- ユーザーは自分のデータのみを更新できる
CREATE POLICY "Users update own data"
ON users FOR UPDATE
USING (auth.uid() = id);
```
#### 検証ステップ
- [ ] トークンはhttpOnly Cookieに保存localStorageではなく
- [ ] 機密操作前の認可チェック
- [ ] SupabaseでRow Level Securityを有効化
- [ ] ロールベースのアクセス制御を実装
- [ ] セッション管理が安全
### 5. XSS防止
#### HTMLをサニタイズ
```typescript
import DOMPurify from 'isomorphic-dompurify'
// 常にユーザー提供のHTMLをサニタイズ
function renderUserContent(html: string) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
ALLOWED_ATTR: []
})
return <div dangerouslySetInnerHTML={{ __html: clean }} />
}
```
#### コンテンツセキュリティポリシー
```typescript
// next.config.js
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
`.replace(/\s{2,}/g, ' ').trim()
}
]
```
#### 検証ステップ
- [ ] ユーザー提供のHTMLをサニタイズ
- [ ] CSPヘッダーを設定
- [ ] 検証されていない動的コンテンツのレンダリングなし
- [ ] Reactの組み込みXSS保護を使用
### 6. CSRF保護
#### CSRFトークン
```typescript
import { csrf } from '@/lib/csrf'
export async function POST(request: Request) {
const token = request.headers.get('X-CSRF-Token')
if (!csrf.verify(token)) {
return NextResponse.json(
{ error: 'Invalid CSRF token' },
{ status: 403 }
)
}
// リクエストを処理
}
```
#### SameSite Cookie
```typescript
res.setHeader('Set-Cookie',
`session=${sessionId}; HttpOnly; Secure; SameSite=Strict`)
```
#### 検証ステップ
- [ ] 状態変更操作でCSRFトークン
- [ ] すべてのCookieでSameSite=Strict
- [ ] ダブルサブミットCookieパターンを実装
### 7. レート制限
#### APIレート制限
```typescript
import rateLimit from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分
max: 100, // ウィンドウあたり100リクエスト
message: 'Too many requests'
})
// ルートに適用
app.use('/api/', limiter)
```
#### 高コスト操作
```typescript
// 検索の積極的なレート制限
const searchLimiter = rateLimit({
windowMs: 60 * 1000, // 1分
max: 10, // 1分あたり10リクエスト
message: 'Too many search requests'
})
app.use('/api/search', searchLimiter)
```
#### 検証ステップ
- [ ] すべてのAPIエンドポイントでレート制限
- [ ] 高コスト操作でより厳しい制限
- [ ] IPベースのレート制限
- [ ] ユーザーベースのレート制限(認証済み)
### 8. 機密データの露出
#### ロギング
```typescript
// ❌ 誤り:機密データをログに記録
console.log('User login:', { email, password })
console.log('Payment:', { cardNumber, cvv })
// ✅ 正解:機密データを編集
console.log('User login:', { email, userId })
console.log('Payment:', { last4: card.last4, userId })
```
#### エラーメッセージ
```typescript
// ❌ 誤り:内部詳細を露出
catch (error) {
return NextResponse.json(
{ error: error.message, stack: error.stack },
{ status: 500 }
)
}
// ✅ 正解:一般的なエラーメッセージ
catch (error) {
console.error('Internal error:', error)
return NextResponse.json(
{ error: 'An error occurred. Please try again.' },
{ status: 500 }
)
}
```
#### 検証ステップ
- [ ] ログにパスワード、トークン、シークレットなし
- [ ] ユーザー向けの一般的なエラーメッセージ
- [ ] 詳細なエラーはサーバーログのみ
- [ ] ユーザーにスタックトレースを露出しない
### 9. ブロックチェーンセキュリティ (Solana)
#### ウォレット検証
```typescript
import { verify } from '@solana/web3.js'
async function verifyWalletOwnership(
publicKey: string,
signature: string,
message: string
) {
try {
const isValid = verify(
Buffer.from(message),
Buffer.from(signature, 'base64'),
Buffer.from(publicKey, 'base64')
)
return isValid
} catch (error) {
return false
}
}
```
#### トランザクション検証
```typescript
async function verifyTransaction(transaction: Transaction) {
// 受信者を検証
if (transaction.to !== expectedRecipient) {
throw new Error('Invalid recipient')
}
// 金額を検証
if (transaction.amount > maxAmount) {
throw new Error('Amount exceeds limit')
}
// ユーザーに十分な残高があることを確認
const balance = await getBalance(transaction.from)
if (balance < transaction.amount) {
throw new Error('Insufficient balance')
}
return true
}
```
#### 検証ステップ
- [ ] ウォレット署名を検証
- [ ] トランザクション詳細を検証
- [ ] トランザクション前の残高チェック
- [ ] ブラインドトランザクション署名なし
### 10. 依存関係セキュリティ
#### 定期的な更新
```bash
# 脆弱性をチェック
npm audit
# 自動修正可能な問題を修正
npm audit fix
# 依存関係を更新
npm update
# 古いパッケージをチェック
npm outdated
```
#### ロックファイル
```bash
# 常にロックファイルをコミット
git add package-lock.json
# CI/CDで再現可能なビルドに使用
npm ci # npm installの代わりに
```
#### 検証ステップ
- [ ] 依存関係が最新
- [ ] 既知の脆弱性なしnpm auditクリーン
- [ ] ロックファイルをコミット
- [ ] GitHubでDependabotを有効化
- [ ] 定期的なセキュリティ更新
## セキュリティテスト
### 自動セキュリティテスト
```typescript
// 認証をテスト
test('requires authentication', async () => {
const response = await fetch('/api/protected')
expect(response.status).toBe(401)
})
// 認可をテスト
test('requires admin role', async () => {
const response = await fetch('/api/admin', {
headers: { Authorization: `Bearer ${userToken}` }
})
expect(response.status).toBe(403)
})
// 入力検証をテスト
test('rejects invalid input', async () => {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ email: 'not-an-email' })
})
expect(response.status).toBe(400)
})
// レート制限をテスト
test('enforces rate limits', async () => {
const requests = Array(101).fill(null).map(() =>
fetch('/api/endpoint')
)
const responses = await Promise.all(requests)
const tooManyRequests = responses.filter(r => r.status === 429)
expect(tooManyRequests.length).toBeGreaterThan(0)
})
```
## デプロイ前セキュリティチェックリスト
すべての本番デプロイメントの前に:
- [ ] **シークレット**:ハードコードされたシークレットなし、すべて環境変数に
- [ ] **入力検証**:すべてのユーザー入力を検証
- [ ] **SQLインジェクション**:すべてのクエリをパラメータ化
- [ ] **XSS**:ユーザーコンテンツをサニタイズ
- [ ] **CSRF**:保護を有効化
- [ ] **認証**:適切なトークン処理
- [ ] **認可**:ロールチェックを配置
- [ ] **レート制限**:すべてのエンドポイントで有効化
- [ ] **HTTPS**:本番で強制
- [ ] **セキュリティヘッダー**CSP、X-Frame-Optionsを設定
- [ ] **エラー処理**:エラーに機密データなし
- [ ] **ロギング**:ログに機密データなし
- [ ] **依存関係**:最新、脆弱性なし
- [ ] **Row Level Security**Supabaseで有効化
- [ ] **CORS**:適切に設定
- [ ] **ファイルアップロード**:検証済み(サイズ、タイプ)
- [ ] **ウォレット署名**:検証済み(ブロックチェーンの場合)
## リソース
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [Next.js Security](https://nextjs.org/docs/security)
- [Supabase Security](https://supabase.com/docs/guides/auth)
- [Web Security Academy](https://portswigger.net/web-security)
---
**覚えておいてください**セキュリティはオプションではありません。1つの脆弱性がプラットフォーム全体を危険にさらす可能性があります。疑わしい場合は、慎重に判断してください。

View File

@@ -1,361 +0,0 @@
| name | description |
|------|-------------|
| cloud-infrastructure-security | クラウドプラットフォームへのデプロイ、インフラストラクチャの設定、IAMポリシーの管理、ロギング/モニタリングの設定、CI/CDパイプラインの実装時にこのスキルを使用します。ベストプラクティスに沿ったクラウドセキュリティチェックリストを提供します。 |
# クラウドおよびインフラストラクチャセキュリティスキル
このスキルは、クラウドインフラストラクチャ、CI/CDパイプライン、デプロイメント設定がセキュリティのベストプラクティスに従い、業界標準に準拠することを保証します。
## 有効化するタイミング
- クラウドプラットフォームAWS、Vercel、Railway、Cloudflareへのアプリケーションのデプロイ
- IAMロールと権限の設定
- CI/CDパイプラインの設定
- インフラストラクチャをコードとして実装Terraform、CloudFormation
- ロギングとモニタリングの設定
- クラウド環境でのシークレット管理
- CDNとエッジセキュリティの設定
- 災害復旧とバックアップ戦略の実装
## クラウドセキュリティチェックリスト
### 1. IAMとアクセス制御
#### 最小権限の原則
```yaml
# ✅ 正解:最小限の権限
iam_role:
permissions:
- s3:GetObject # 読み取りアクセスのみ
- s3:ListBucket
resources:
- arn:aws:s3:::my-bucket/* # 特定のバケットのみ
# ❌ 誤り:過度に広範な権限
iam_role:
permissions:
- s3:* # すべてのS3アクション
resources:
- "*" # すべてのリソース
```
#### 多要素認証MFA
```bash
# 常にroot/adminアカウントでMFAを有効化
aws iam enable-mfa-device \
--user-name admin \
--serial-number arn:aws:iam::123456789:mfa/admin \
--authentication-code1 123456 \
--authentication-code2 789012
```
#### 検証ステップ
- [ ] 本番環境でrootアカウントを使用しない
- [ ] すべての特権アカウントでMFAを有効化
- [ ] サービスアカウントは長期資格情報ではなくロールを使用
- [ ] IAMポリシーは最小権限に従う
- [ ] 定期的なアクセスレビューを実施
- [ ] 未使用の資格情報をローテーションまたは削除
### 2. シークレット管理
#### クラウドシークレットマネージャー
```typescript
// ✅ 正解:クラウドシークレットマネージャーを使用
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManager({ region: 'us-east-1' });
const secret = await client.getSecretValue({ SecretId: 'prod/api-key' });
const apiKey = JSON.parse(secret.SecretString).key;
// ❌ 誤り:ハードコードまたは環境変数のみ
const apiKey = process.env.API_KEY; // ローテーションされず、監査されない
```
#### シークレットローテーション
```bash
# データベース資格情報の自動ローテーションを設定
aws secretsmanager rotate-secret \
--secret-id prod/db-password \
--rotation-lambda-arn arn:aws:lambda:region:account:function:rotate \
--rotation-rules AutomaticallyAfterDays=30
```
#### 検証ステップ
- [ ] すべてのシークレットをクラウドシークレットマネージャーに保存AWS Secrets Manager、Vercel Secrets
- [ ] データベース資格情報の自動ローテーションを有効化
- [ ] APIキーを少なくとも四半期ごとにローテーション
- [ ] コード、ログ、エラーメッセージにシークレットなし
- [ ] シークレットアクセスの監査ログを有効化
### 3. ネットワークセキュリティ
#### VPCとファイアウォール設定
```terraform
# ✅ 正解:制限されたセキュリティグループ
resource "aws_security_group" "app" {
name = "app-sg"
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"] # 内部VPCのみ
}
egress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # HTTPS送信のみ
}
}
# ❌ 誤り:インターネットに公開
resource "aws_security_group" "bad" {
ingress {
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # すべてのポート、すべてのIP
}
}
```
#### 検証ステップ
- [ ] データベースは公開アクセス不可
- [ ] SSH/RDPポートはVPN/bastionのみに制限
- [ ] セキュリティグループは最小権限に従う
- [ ] ネットワークACLを設定
- [ ] VPCフローログを有効化
### 4. ロギングとモニタリング
#### CloudWatch/ロギング設定
```typescript
// ✅ 正解:包括的なロギング
import { CloudWatchLogsClient, CreateLogStreamCommand } from '@aws-sdk/client-cloudwatch-logs';
const logSecurityEvent = async (event: SecurityEvent) => {
await cloudwatch.putLogEvents({
logGroupName: '/aws/security/events',
logStreamName: 'authentication',
logEvents: [{
timestamp: Date.now(),
message: JSON.stringify({
type: event.type,
userId: event.userId,
ip: event.ip,
result: event.result,
// 機密データをログに記録しない
})
}]
});
};
```
#### 検証ステップ
- [ ] すべてのサービスでCloudWatch/ロギングを有効化
- [ ] 失敗した認証試行をログに記録
- [ ] 管理者アクションを監査
- [ ] ログ保持を設定コンプライアンスのため90日以上
- [ ] 疑わしいアクティビティのアラートを設定
- [ ] ログを一元化し、改ざん防止
### 5. CI/CDパイプラインセキュリティ
#### 安全なパイプライン設定
```yaml
# ✅ 正解安全なGitHub Actionsワークフロー
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read # 最小限の権限
steps:
- uses: actions/checkout@v4
# シークレットをスキャン
- name: Secret scanning
uses: trufflesecurity/trufflehog@main
# 依存関係監査
- name: Audit dependencies
run: npm audit --audit-level=high
# 長期トークンではなくOIDCを使用
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
aws-region: us-east-1
```
#### サプライチェーンセキュリティ
```json
// package.json - ロックファイルと整合性チェックを使用
{
"scripts": {
"install": "npm ci", // 再現可能なビルドにciを使用
"audit": "npm audit --audit-level=moderate",
"check": "npm outdated"
}
}
```
#### 検証ステップ
- [ ] 長期資格情報ではなくOIDCを使用
- [ ] パイプラインでシークレットスキャン
- [ ] 依存関係の脆弱性スキャン
- [ ] コンテナイメージスキャン(該当する場合)
- [ ] ブランチ保護ルールを強制
- [ ] マージ前にコードレビューが必要
- [ ] 署名付きコミットを強制
### 6. CloudflareとCDNセキュリティ
#### Cloudflareセキュリティ設定
```typescript
// ✅ 正解セキュリティヘッダー付きCloudflare Workers
export default {
async fetch(request: Request): Promise<Response> {
const response = await fetch(request);
// セキュリティヘッダーを追加
const headers = new Headers(response.headers);
headers.set('X-Frame-Options', 'DENY');
headers.set('X-Content-Type-Options', 'nosniff');
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
headers.set('Permissions-Policy', 'geolocation=(), microphone=()');
return new Response(response.body, {
status: response.status,
headers
});
}
};
```
#### WAFルール
```bash
# Cloudflare WAF管理ルールを有効化
# - OWASP Core Ruleset
# - Cloudflare Managed Ruleset
# - レート制限ルール
# - ボット保護
```
#### 検証ステップ
- [ ] OWASPルール付きWAFを有効化
- [ ] レート制限を設定
- [ ] ボット保護を有効化
- [ ] DDoS保護を有効化
- [ ] セキュリティヘッダーを設定
- [ ] SSL/TLS厳格モードを有効化
### 7. バックアップと災害復旧
#### 自動バックアップ
```terraform
# ✅ 正解自動RDSバックアップ
resource "aws_db_instance" "main" {
allocated_storage = 20
engine = "postgres"
backup_retention_period = 30 # 30日間保持
backup_window = "03:00-04:00"
maintenance_window = "mon:04:00-mon:05:00"
enabled_cloudwatch_logs_exports = ["postgresql"]
deletion_protection = true # 偶発的な削除を防止
}
```
#### 検証ステップ
- [ ] 自動日次バックアップを設定
- [ ] バックアップ保持がコンプライアンス要件を満たす
- [ ] ポイントインタイムリカバリを有効化
- [ ] 四半期ごとにバックアップテストを実施
- [ ] 災害復旧計画を文書化
- [ ] RPOとRTOを定義してテスト
## デプロイ前クラウドセキュリティチェックリスト
すべての本番クラウドデプロイメントの前に:
- [ ] **IAM**rootアカウントを使用しない、MFAを有効化、最小権限ポリシー
- [ ] **シークレット**:すべてのシークレットをローテーション付きクラウドシークレットマネージャーに
- [ ] **ネットワーク**:セキュリティグループを制限、公開データベースなし
- [ ] **ロギング**保持付きCloudWatch/ロギングを有効化
- [ ] **モニタリング**:異常のアラートを設定
- [ ] **CI/CD**OIDC認証、シークレットスキャン、依存関係監査
- [ ] **CDN/WAF**OWASPルール付きCloudflare WAFを有効化
- [ ] **暗号化**:静止時および転送中のデータを暗号化
- [ ] **バックアップ**:テスト済みリカバリ付き自動バックアップ
- [ ] **コンプライアンス**GDPR/HIPAA要件を満たす該当する場合
- [ ] **ドキュメント**:インフラストラクチャを文書化、ランブックを作成
- [ ] **インシデント対応**:セキュリティインシデント計画を配置
## 一般的なクラウドセキュリティ設定ミス
### S3バケットの露出
```bash
# ❌ 誤り:公開バケット
aws s3api put-bucket-acl --bucket my-bucket --acl public-read
# ✅ 正解:特定のアクセス付きプライベートバケット
aws s3api put-bucket-acl --bucket my-bucket --acl private
aws s3api put-bucket-policy --bucket my-bucket --policy file://policy.json
```
### RDS公開アクセス
```terraform
# ❌ 誤り
resource "aws_db_instance" "bad" {
publicly_accessible = true # 絶対にこれをしない!
}
# ✅ 正解
resource "aws_db_instance" "good" {
publicly_accessible = false
vpc_security_group_ids = [aws_security_group.db.id]
}
```
## リソース
- [AWS Security Best Practices](https://aws.amazon.com/security/best-practices/)
- [CIS AWS Foundations Benchmark](https://www.cisecurity.org/benchmark/amazon_web_services)
- [Cloudflare Security Documentation](https://developers.cloudflare.com/security/)
- [OWASP Cloud Security](https://owasp.org/www-project-cloud-security/)
- [Terraform Security Best Practices](https://www.terraform.io/docs/cloud/guides/recommended-practices/)
**覚えておいてください**クラウドの設定ミスはデータ侵害の主要な原因です。1つの露出したS3バケットまたは過度に許容されたIAMポリシーは、インフラストラクチャ全体を危険にさらす可能性があります。常に最小権限の原則と多層防御に従ってください。

View File

@@ -1,164 +0,0 @@
---
name: security-scan
description: AgentShield を使用して、Claude Code の設定(.claude/ ディレクトリのセキュリティ脆弱性、設定ミス、インジェクションリスクをスキャンします。CLAUDE.md、settings.json、MCP サーバー、フック、エージェント定義をチェックします。
---
# Security Scan Skill
[AgentShield](https://github.com/affaan-m/agentshield) を使用して、Claude Code の設定のセキュリティ問題を監査します。
## 起動タイミング
- 新しい Claude Code プロジェクトのセットアップ時
- `.claude/settings.json``CLAUDE.md`、または MCP 設定の変更後
- 設定変更をコミットする前
- 既存の Claude Code 設定を持つ新しいリポジトリにオンボーディングする際
- 定期的なセキュリティ衛生チェック
## スキャン対象
| ファイル | チェック内容 |
|------|--------|
| `CLAUDE.md` | ハードコードされたシークレット、自動実行命令、プロンプトインジェクションパターン |
| `settings.json` | 過度に寛容な許可リスト、欠落した拒否リスト、危険なバイパスフラグ |
| `mcp.json` | リスクのある MCP サーバー、ハードコードされた環境シークレット、npx サプライチェーンリスク |
| `hooks/` | 補間によるコマンドインジェクション、データ流出、サイレントエラー抑制 |
| `agents/*.md` | 無制限のツールアクセス、プロンプトインジェクション表面、欠落したモデル仕様 |
## 前提条件
AgentShield がインストールされている必要があります。確認し、必要に応じてインストールします:
```bash
# インストール済みか確認
npx ecc-agentshield --version
# グローバルにインストール(推奨)
npm install -g ecc-agentshield
# または npx 経由で直接実行(インストール不要)
npx ecc-agentshield scan .
```
## 使用方法
### 基本スキャン
現在のプロジェクトの `.claude/` ディレクトリに対して実行します:
```bash
# 現在のプロジェクトをスキャン
npx ecc-agentshield scan
# 特定のパスをスキャン
npx ecc-agentshield scan --path /path/to/.claude
# 最小深刻度フィルタでスキャン
npx ecc-agentshield scan --min-severity medium
```
### 出力フォーマット
```bash
# ターミナル出力(デフォルト) — グレード付きのカラーレポート
npx ecc-agentshield scan
# JSON — CI/CD 統合用
npx ecc-agentshield scan --format json
# Markdown — ドキュメント用
npx ecc-agentshield scan --format markdown
# HTML — 自己完結型のダークテーマレポート
npx ecc-agentshield scan --format html > security-report.html
```
### 自動修正
安全な修正を自動的に適用します(自動修正可能とマークされた修正のみ):
```bash
npx ecc-agentshield scan --fix
```
これにより以下が実行されます:
- ハードコードされたシークレットを環境変数参照に置き換え
- ワイルドカード権限をスコープ付き代替に厳格化
- 手動のみの提案は変更しない
### Opus 4.6 ディープ分析
より深い分析のために敵対的な3エージェントパイプラインを実行します
```bash
# ANTHROPIC_API_KEY が必要
export ANTHROPIC_API_KEY=your-key
npx ecc-agentshield scan --opus --stream
```
これにより以下が実行されます:
1. **攻撃者(レッドチーム)** — 攻撃ベクトルを発見
2. **防御者(ブルーチーム)** — 強化を推奨
3. **監査人(最終判定)** — 両方の観点を統合
### 安全な設定の初期化
新しい安全な `.claude/` 設定をゼロから構築します:
```bash
npx ecc-agentshield init
```
作成されるもの:
- スコープ付き権限と拒否リストを持つ `settings.json`
- セキュリティベストプラクティスを含む `CLAUDE.md`
- `mcp.json` プレースホルダー
### GitHub Action
CI パイプラインに追加します:
```yaml
- uses: affaan-m/agentshield@v1
with:
path: '.'
min-severity: 'medium'
fail-on-findings: true
```
## 深刻度レベル
| グレード | スコア | 意味 |
|-------|-------|---------|
| A | 90-100 | 安全な設定 |
| B | 75-89 | 軽微な問題 |
| C | 60-74 | 注意が必要 |
| D | 40-59 | 重大なリスク |
| F | 0-39 | クリティカルな脆弱性 |
## 結果の解釈
### クリティカルな発見(即座に修正)
- 設定ファイル内のハードコードされた API キーまたはトークン
- 許可リスト内の `Bash(*)`(無制限のシェルアクセス)
- `${file}` 補間によるフック内のコマンドインジェクション
- シェルを実行する MCP サーバー
### 高い発見(本番前に修正)
- CLAUDE.md 内の自動実行命令(プロンプトインジェクションベクトル)
- 権限内の欠落した拒否リスト
- 不要な Bash アクセスを持つエージェント
### 中程度の発見(推奨)
- フック内のサイレントエラー抑制(`2>/dev/null``|| true`
- 欠落した PreToolUse セキュリティフック
- MCP サーバー設定内の `npx -y` 自動インストール
### 情報の発見(認識)
- MCP サーバーの欠落した説明
- 正しくフラグ付けされた禁止命令(グッドプラクティス)
## リンク
- **GitHub**: [github.com/affaan-m/agentshield](https://github.com/affaan-m/agentshield)
- **npm**: [npmjs.com/package/ecc-agentshield](https://www.npmjs.com/package/ecc-agentshield)

View File

@@ -1,304 +0,0 @@
---
name: springboot-patterns
description: Spring Boot architecture patterns, REST API design, layered services, data access, caching, async processing, and logging. Use for Java Spring Boot backend work.
---
# Spring Boot 開発パターン
スケーラブルで本番グレードのサービスのためのSpring BootアーキテクチャとAPIパターン。
## REST API構造
```java
@RestController
@RequestMapping("/api/markets")
@Validated
class MarketController {
private final MarketService marketService;
MarketController(MarketService marketService) {
this.marketService = marketService;
}
@GetMapping
ResponseEntity<Page<MarketResponse>> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Page<Market> markets = marketService.list(PageRequest.of(page, size));
return ResponseEntity.ok(markets.map(MarketResponse::from));
}
@PostMapping
ResponseEntity<MarketResponse> create(@Valid @RequestBody CreateMarketRequest request) {
Market market = marketService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(MarketResponse::from(market));
}
}
```
## リポジトリパターンSpring Data JPA
```java
public interface MarketRepository extends JpaRepository<MarketEntity, Long> {
@Query("select m from MarketEntity m where m.status = :status order by m.volume desc")
List<MarketEntity> findActive(@Param("status") MarketStatus status, Pageable pageable);
}
```
## トランザクション付きサービスレイヤー
```java
@Service
public class MarketService {
private final MarketRepository repo;
public MarketService(MarketRepository repo) {
this.repo = repo;
}
@Transactional
public Market create(CreateMarketRequest request) {
MarketEntity entity = MarketEntity.from(request);
MarketEntity saved = repo.save(entity);
return Market.from(saved);
}
}
```
## DTOと検証
```java
public record CreateMarketRequest(
@NotBlank @Size(max = 200) String name,
@NotBlank @Size(max = 2000) String description,
@NotNull @FutureOrPresent Instant endDate,
@NotEmpty List<@NotBlank String> categories) {}
public record MarketResponse(Long id, String name, MarketStatus status) {
static MarketResponse from(Market market) {
return new MarketResponse(market.id(), market.name(), market.status());
}
}
```
## 例外ハンドリング
```java
@ControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(ApiError.validation(message));
}
@ExceptionHandler(AccessDeniedException.class)
ResponseEntity<ApiError> handleAccessDenied() {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiError.of("Forbidden"));
}
@ExceptionHandler(Exception.class)
ResponseEntity<ApiError> handleGeneric(Exception ex) {
// スタックトレース付きで予期しないエラーをログ
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiError.of("Internal server error"));
}
}
```
## キャッシング
構成クラスで`@EnableCaching`が必要です。
```java
@Service
public class MarketCacheService {
private final MarketRepository repo;
public MarketCacheService(MarketRepository repo) {
this.repo = repo;
}
@Cacheable(value = "market", key = "#id")
public Market getById(Long id) {
return repo.findById(id)
.map(Market::from)
.orElseThrow(() -> new EntityNotFoundException("Market not found"));
}
@CacheEvict(value = "market", key = "#id")
public void evict(Long id) {}
}
```
## 非同期処理
構成クラスで`@EnableAsync`が必要です。
```java
@Service
public class NotificationService {
@Async
public CompletableFuture<Void> sendAsync(Notification notification) {
// メール/SMS送信
return CompletableFuture.completedFuture(null);
}
}
```
## ロギングSLF4J
```java
@Service
public class ReportService {
private static final Logger log = LoggerFactory.getLogger(ReportService.class);
public Report generate(Long marketId) {
log.info("generate_report marketId={}", marketId);
try {
// ロジック
} catch (Exception ex) {
log.error("generate_report_failed marketId={}", marketId, ex);
throw ex;
}
return new Report();
}
}
```
## ミドルウェア / フィルター
```java
@Component
public class RequestLoggingFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
long start = System.currentTimeMillis();
try {
filterChain.doFilter(request, response);
} finally {
long duration = System.currentTimeMillis() - start;
log.info("req method={} uri={} status={} durationMs={}",
request.getMethod(), request.getRequestURI(), response.getStatus(), duration);
}
}
}
```
## ページネーションとソート
```java
PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
Page<Market> results = marketService.list(page);
```
## エラー回復力のある外部呼び出し
```java
public <T> T withRetry(Supplier<T> supplier, int maxRetries) {
int attempts = 0;
while (true) {
try {
return supplier.get();
} catch (Exception ex) {
attempts++;
if (attempts >= maxRetries) {
throw ex;
}
try {
Thread.sleep((long) Math.pow(2, attempts) * 100L);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw ex;
}
}
}
}
```
## レート制限Filter + Bucket4j
**セキュリティノート**: `X-Forwarded-For`ヘッダーはデフォルトでは信頼できません。クライアントがそれを偽装できるためです。
転送ヘッダーは次の場合のみ使用してください:
1. アプリが信頼できるリバースプロキシnginx、AWS ALBなどの背後にある
2. `ForwardedHeaderFilter`をBeanとして登録済み
3. application propertiesで`server.forward-headers-strategy=NATIVE`または`FRAMEWORK`を設定済み
4. プロキシが`X-Forwarded-For`ヘッダーを上書き(追加ではなく)するよう設定済み
`ForwardedHeaderFilter`が適切に構成されている場合、`request.getRemoteAddr()`は転送ヘッダーから正しいクライアントIPを自動的に返します。この構成がない場合は、`request.getRemoteAddr()`を直接使用してください。これは直接接続IPを返し、唯一信頼できる値です。
```java
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
/*
* セキュリティ: このフィルターはレート制限のためにクライアントを識別するために
* request.getRemoteAddr()を使用します。
*
* アプリケーションがリバースプロキシnginx、AWS ALBなどの背後にある場合、
* 正確なクライアントIP検出のために転送ヘッダーを適切に処理するようSpringを
* 設定する必要があります:
*
* 1. application.properties/yamlで server.forward-headers-strategy=NATIVE
* クラウドプラットフォーム用またはFRAMEWORKを設定
* 2. FRAMEWORK戦略を使用する場合、ForwardedHeaderFilterを登録:
*
* @Bean
* ForwardedHeaderFilter forwardedHeaderFilter() {
* return new ForwardedHeaderFilter();
* }
*
* 3. プロキシが偽装を防ぐためにX-Forwarded-Forヘッダーを上書き追加ではなく
* することを確認
* 4. コンテナに応じてserver.tomcat.remoteip.trusted-proxiesまたは同等を設定
*
* この構成なしでは、request.getRemoteAddr()はクライアントIPではなくプロキシIPを返します。
* X-Forwarded-Forを直接読み取らないでください。信頼できるプロキシ処理なしでは簡単に偽装できます。
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// ForwardedHeaderFilterが構成されている場合は正しいクライアントIPを返す
// getRemoteAddr()を使用。そうでなければ直接接続IPを返す。
// X-Forwarded-Forヘッダーを適切なプロキシ構成なしで直接信頼しない。
String clientIp = request.getRemoteAddr();
Bucket bucket = buckets.computeIfAbsent(clientIp,
k -> Bucket.builder()
.addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1))))
.build());
if (bucket.tryConsume(1)) {
filterChain.doFilter(request, response);
} else {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
}
}
}
```
## バックグラウンドジョブ
Springの`@Scheduled`を使用するか、キューKafka、SQS、RabbitMQなどと統合します。ハンドラーをべき等かつ観測可能に保ちます。
## 可観測性
- 構造化ロギングJSONvia Logbackエンコーダー
- メトリクス: Micrometer + Prometheus/OTel
- トレーシング: Micrometer TracingとOpenTelemetryまたはBraveバックエンド
## 本番デフォルト
- コンストラクタインジェクションを優先、フィールドインジェクションを避ける
- RFC 7807エラーのために`spring.mvc.problemdetails.enabled=true`を有効化Spring Boot 3+
- ワークロードに応じてHikariCPプールサイズを構成、タイムアウトを設定
- クエリに`@Transactional(readOnly = true)`を使用
- `@NonNull``Optional`で適切にnull安全性を強制
**覚えておいてください**: コントローラーは薄く、サービスは焦点を絞り、リポジトリはシンプルに、エラーは集中的に処理します。保守性とテスト可能性のために最適化してください。

View File

@@ -1,119 +0,0 @@
---
name: springboot-security
description: Spring Security best practices for authn/authz, validation, CSRF, secrets, headers, rate limiting, and dependency security in Java Spring Boot services.
---
# Spring Boot セキュリティレビュー
認証の追加、入力処理、エンドポイント作成、またはシークレット処理時に使用します。
## 認証
- ステートレスJWTまたは失効リスト付き不透明トークンを優先
- セッションには `httpOnly``Secure``SameSite=Strict` クッキーを使用
- `OncePerRequestFilter` またはリソースサーバーでトークンを検証
```java
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
public JwtAuthFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
Authentication auth = jwtService.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(request, response);
}
}
```
## 認可
- メソッドセキュリティを有効化: `@EnableMethodSecurity`
- `@PreAuthorize("hasRole('ADMIN')")` または `@PreAuthorize("@authz.canEdit(#id)")` を使用
- デフォルトで拒否し、必要なスコープのみ公開
## 入力検証
- `@Valid` を使用してコントローラーでBean Validationを使用
- DTOに制約を適用: `@NotBlank``@Email``@Size`、カスタムバリデーター
- レンダリング前にホワイトリストでHTMLをサニタイズ
## SQLインジェクション防止
- Spring Dataリポジトリまたはパラメータ化クエリを使用
- ネイティブクエリには `:param` バインディングを使用し、文字列を連結しない
## CSRF保護
- ブラウザセッションアプリの場合はCSRFを有効にし、フォーム/ヘッダーにトークンを含める
- Bearerトークンを使用する純粋なAPIの場合は、CSRFを無効にしてステートレス認証に依存
```java
http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
```
## シークレット管理
- ソースコードにシークレットを含めない。環境変数またはvaultから読み込む
- `application.yml` を認証情報から解放し、プレースホルダーを使用
- トークンとDB認証情報を定期的にローテーション
## セキュリティヘッダー
```java
http
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'"))
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
.xssProtection(Customizer.withDefaults())
.referrerPolicy(rp -> rp.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER)));
```
## レート制限
- 高コストなエンドポイントにBucket4jまたはゲートウェイレベルの制限を適用
- バーストをログに記録してアラートを送信し、リトライヒント付きで429を返す
## 依存関係のセキュリティ
- CIでOWASP Dependency Check / Snykを実行
- Spring BootとSpring Securityをサポートされているバージョンに保つ
- 既知のCVEでビルドを失敗させる
## ロギングとPII
- シークレット、トークン、パスワード、完全なPANデータをログに記録しない
- 機密フィールドを編集し、構造化JSONロギングを使用
## ファイルアップロード
- サイズ、コンテンツタイプ、拡張子を検証
- Webルート外に保存し、必要に応じてスキャン
## リリース前チェックリスト
- [ ] 認証トークンが正しく検証され、期限切れになっている
- [ ] すべての機密パスに認可ガードがある
- [ ] すべての入力が検証およびサニタイズされている
- [ ] 文字列連結されたSQLがない
- [ ] アプリケーションタイプに対してCSRF対策が正しい
- [ ] シークレットが外部化され、コミットされていない
- [ ] セキュリティヘッダーが設定されている
- [ ] APIにレート制限がある
- [ ] 依存関係がスキャンされ、最新である
- [ ] ログに機密データがない
**注意**: デフォルトで拒否し、入力を検証し、最小権限を適用し、設定によるセキュリティを優先します。

View File

@@ -1,157 +0,0 @@
---
name: springboot-tdd
description: Test-driven development for Spring Boot using JUnit 5, Mockito, MockMvc, Testcontainers, and JaCoCo. Use when adding features, fixing bugs, or refactoring.
---
# Spring Boot TDD ワークフロー
80%以上のカバレッジ(ユニット+統合を持つSpring Bootサービスのためのテスト駆動開発ガイダンス。
## いつ使用するか
- 新機能やエンドポイント
- バグ修正やリファクタリング
- データアクセスロジックやセキュリティルールの追加
## ワークフロー
1) テストを最初に書く(失敗すべき)
2) テストを通すための最小限のコードを実装
3) テストをグリーンに保ちながらリファクタリング
4) カバレッジを強制JaCoCo
## ユニットテストJUnit 5 + Mockito
```java
@ExtendWith(MockitoExtension.class)
class MarketServiceTest {
@Mock MarketRepository repo;
@InjectMocks MarketService service;
@Test
void createsMarket() {
CreateMarketRequest req = new CreateMarketRequest("name", "desc", Instant.now(), List.of("cat"));
when(repo.save(any())).thenAnswer(inv -> inv.getArgument(0));
Market result = service.create(req);
assertThat(result.name()).isEqualTo("name");
verify(repo).save(any());
}
}
```
パターン:
- Arrange-Act-Assert
- 部分モックを避ける。明示的なスタビングを優先
- バリエーションに`@ParameterizedTest`を使用
## WebレイヤーテストMockMvc
```java
@WebMvcTest(MarketController.class)
class MarketControllerTest {
@Autowired MockMvc mockMvc;
@MockBean MarketService marketService;
@Test
void returnsMarkets() throws Exception {
when(marketService.list(any())).thenReturn(Page.empty());
mockMvc.perform(get("/api/markets"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").isArray());
}
}
```
## 統合テストSpringBootTest
```java
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class MarketIntegrationTest {
@Autowired MockMvc mockMvc;
@Test
void createsMarket() throws Exception {
mockMvc.perform(post("/api/markets")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name":"Test","description":"Desc","endDate":"2030-01-01T00:00:00Z","categories":["general"]}
"""))
.andExpect(status().isCreated());
}
}
```
## 永続化テストDataJpaTest
```java
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(TestContainersConfig.class)
class MarketRepositoryTest {
@Autowired MarketRepository repo;
@Test
void savesAndFinds() {
MarketEntity entity = new MarketEntity();
entity.setName("Test");
repo.save(entity);
Optional<MarketEntity> found = repo.findByName("Test");
assertThat(found).isPresent();
}
}
```
## Testcontainers
- 本番環境を反映するためにPostgres/Redis用の再利用可能なコンテナを使用
- `@DynamicPropertySource`経由でJDBC URLをSpringコンテキストに注入
## カバレッジJaCoCo
Mavenスニペット:
```xml
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.14</version>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals><goal>report</goal></goals>
</execution>
</executions>
</plugin>
```
## アサーション
- 可読性のためにAssertJ`assertThat`)を優先
- JSONレスポンスには`jsonPath`を使用
- 例外には: `assertThatThrownBy(...)`
## テストデータビルダー
```java
class MarketBuilder {
private String name = "Test";
MarketBuilder withName(String name) { this.name = name; return this; }
Market build() { return new Market(null, name, MarketStatus.ACTIVE); }
}
```
## CIコマンド
- Maven: `mvn -T 4 test` または `mvn verify`
- Gradle: `./gradlew test jacocoTestReport`
**覚えておいてください**: テストは高速で、分離され、決定論的に保ちます。実装の詳細ではなく、動作をテストします。

View File

@@ -1,100 +0,0 @@
---
name: springboot-verification
description: Verification loop for Spring Boot projects: build, static analysis, tests with coverage, security scans, and diff review before release or PR.
---
# Spring Boot 検証ループ
PR前、大きな変更後、デプロイ前に実行します。
## フェーズ1: ビルド
```bash
mvn -T 4 clean verify -DskipTests
# または
./gradlew clean assemble -x test
```
ビルドが失敗した場合は、停止して修正します。
## フェーズ2: 静的解析
Maven一般的なプラグイン:
```bash
mvn -T 4 spotbugs:check pmd:check checkstyle:check
```
Gradle設定されている場合:
```bash
./gradlew checkstyleMain pmdMain spotbugsMain
```
## フェーズ3: テスト + カバレッジ
```bash
mvn -T 4 test
mvn jacoco:report # 80%以上のカバレッジを確認
# または
./gradlew test jacocoTestReport
```
レポート:
- 総テスト数、合格/失敗
- カバレッジ%(行/分岐)
## フェーズ4: セキュリティスキャン
```bash
# 依存関係のCVE
mvn org.owasp:dependency-check-maven:check
# または
./gradlew dependencyCheckAnalyze
# シークレットgit
git secrets --scan # 設定されている場合
```
## フェーズ5: Lint/Formatオプションゲート
```bash
mvn spotless:apply # Spotlessプラグインを使用している場合
./gradlew spotlessApply
```
## フェーズ6: 差分レビュー
```bash
git diff --stat
git diff
```
チェックリスト:
- デバッグログが残っていない(`System.out`、ガードなしの `log.debug`
- 意味のあるエラーとHTTPステータス
- 必要な場所にトランザクションと検証がある
- 設定変更が文書化されている
## 出力テンプレート
```
検証レポート
===================
ビルド: [合格/不合格]
静的解析: [合格/不合格] (spotbugs/pmd/checkstyle)
テスト: [合格/不合格] (X/Y 合格, Z% カバレッジ)
セキュリティ: [合格/不合格] (CVE発見: N)
差分: [X ファイル変更]
全体: [準備完了 / 未完了]
修正が必要な問題:
1. ...
2. ...
```
## 継続モード
- 大きな変更があった場合、または長いセッションで30〜60分ごとにフェーズを再実行
- 短いループを維持: `mvn -T 4 test` + spotbugs で迅速なフィードバック
**注意**: 迅速なフィードバックは遅い驚きに勝ります。ゲートを厳格に保ち、本番システムでは警告を欠陥として扱います。

View File

@@ -1,63 +0,0 @@
---
name: strategic-compact
description: 任意の自動コンパクションではなく、タスクフェーズを通じてコンテキストを保持するための論理的な間隔での手動コンパクションを提案します。
---
# Strategic Compactスキル
任意の自動コンパクションに依存するのではなく、ワークフローの戦略的なポイントで手動の`/compact`を提案します。
## なぜ戦略的コンパクションか?
自動コンパクションは任意のポイントでトリガーされます:
- 多くの場合タスクの途中で、重要なコンテキストを失う
- タスクの論理的な境界を認識しない
- 複雑な複数ステップの操作を中断する可能性がある
論理的な境界での戦略的コンパクション:
- **探索後、実行前** - 研究コンテキストをコンパクト、実装計画を保持
- **マイルストーン完了後** - 次のフェーズのために新しいスタート
- **主要なコンテキストシフト前** - 異なるタスクの前に探索コンテキストをクリア
## 仕組み
`suggest-compact.sh`スクリプトはPreToolUseEdit/Writeで実行され
1. **ツール呼び出しを追跡** - セッション内のツール呼び出しをカウント
2. **閾値検出** - 設定可能な閾値で提案デフォルト50回
3. **定期的なリマインダー** - 閾値後25回ごとにリマインド
## フック設定
`~/.claude/settings.json`に追加:
```json
{
"hooks": {
"PreToolUse": [{
"matcher": "tool == \"Edit\" || tool == \"Write\"",
"hooks": [{
"type": "command",
"command": "~/.claude/skills/strategic-compact/suggest-compact.sh"
}]
}]
}
}
```
## 設定
環境変数:
- `COMPACT_THRESHOLD` - 最初の提案前のツール呼び出しデフォルト50
## ベストプラクティス
1. **計画後にコンパクト** - 計画が確定したら、コンパクトして新しくスタート
2. **デバッグ後にコンパクト** - 続行前にエラー解決コンテキストをクリア
3. **実装中はコンパクトしない** - 関連する変更のためにコンテキストを保持
4. **提案を読む** - フックは*いつ*を教えてくれますが、*するかどうか*は自分で決める
## 関連
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - トークン最適化セクション
- メモリ永続化フック - コンパクションを超えて存続する状態用

View File

@@ -1,409 +0,0 @@
---
name: tdd-workflow
description: 新機能の作成、バグ修正、コードのリファクタリング時にこのスキルを使用します。ユニット、統合、E2Eテストを含む80%以上のカバレッジでテスト駆動開発を強制します。
---
# テスト駆動開発ワークフロー
このスキルは、すべてのコード開発が包括的なテストカバレッジを備えたTDDの原則に従うことを保証します。
## 有効化するタイミング
- 新機能や機能の作成
- バグや問題の修正
- 既存コードのリファクタリング
- APIエンドポイントの追加
- 新しいコンポーネントの作成
## コア原則
### 1. コードの前にテスト
常にテストを最初に書き、次にテストに合格するコードを実装します。
### 2. カバレッジ要件
- 最低80%のカバレッジ(ユニット + 統合 + E2E
- すべてのエッジケースをカバー
- エラーシナリオのテスト
- 境界条件の検証
### 3. テストタイプ
#### ユニットテスト
- 個々の関数とユーティリティ
- コンポーネントロジック
- 純粋関数
- ヘルパーとユーティリティ
#### 統合テスト
- APIエンドポイント
- データベース操作
- サービス間相互作用
- 外部API呼び出し
#### E2Eテスト (Playwright)
- クリティカルなユーザーフロー
- 完全なワークフロー
- ブラウザ自動化
- UI相互作用
## TDDワークフローステップ
### ステップ1ユーザージャーニーを書く
```
[役割]として、[行動]をしたい、それによって[利益]を得られるようにするため
例:
ユーザーとして、セマンティックに市場を検索したい、
それによって正確なキーワードなしでも関連する市場を見つけられるようにするため。
```
### ステップ2テストケースを生成
各ユーザージャーニーについて、包括的なテストケースを作成:
```typescript
describe('Semantic Search', () => {
it('returns relevant markets for query', async () => {
// テスト実装
})
it('handles empty query gracefully', async () => {
// エッジケースのテスト
})
it('falls back to substring search when Redis unavailable', async () => {
// フォールバック動作のテスト
})
it('sorts results by similarity score', async () => {
// ソートロジックのテスト
})
})
```
### ステップ3テストを実行失敗するはず
```bash
npm test
# テストは失敗するはず - まだ実装していない
```
### ステップ4コードを実装
テストに合格する最小限のコードを書く:
```typescript
// テストにガイドされた実装
export async function searchMarkets(query: string) {
// 実装はここ
}
```
### ステップ5テストを再実行
```bash
npm test
# テストは今度は成功するはず
```
### ステップ6リファクタリング
テストをグリーンに保ちながらコード品質を向上:
- 重複を削除
- 命名を改善
- パフォーマンスを最適化
- 可読性を向上
### ステップ7カバレッジを確認
```bash
npm run test:coverage
# 80%以上のカバレッジを達成したことを確認
```
## テストパターン
### ユニットテストパターン (Jest/Vitest)
```typescript
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'
describe('Button Component', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('calls onClick when clicked', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click</Button>)
fireEvent.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Click</Button>)
expect(screen.getByRole('button')).toBeDisabled()
})
})
```
### API統合テストパターン
```typescript
import { NextRequest } from 'next/server'
import { GET } from './route'
describe('GET /api/markets', () => {
it('returns markets successfully', async () => {
const request = new NextRequest('http://localhost/api/markets')
const response = await GET(request)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(Array.isArray(data.data)).toBe(true)
})
it('validates query parameters', async () => {
const request = new NextRequest('http://localhost/api/markets?limit=invalid')
const response = await GET(request)
expect(response.status).toBe(400)
})
it('handles database errors gracefully', async () => {
// データベース障害をモック
const request = new NextRequest('http://localhost/api/markets')
// エラー処理のテスト
})
})
```
### E2Eテストパターン (Playwright)
```typescript
import { test, expect } from '@playwright/test'
test('user can search and filter markets', async ({ page }) => {
// 市場ページに移動
await page.goto('/')
await page.click('a[href="/markets"]')
// ページが読み込まれたことを確認
await expect(page.locator('h1')).toContainText('Markets')
// 市場を検索
await page.fill('input[placeholder="Search markets"]', 'election')
// デバウンスと結果を待つ
await page.waitForTimeout(600)
// 検索結果が表示されることを確認
const results = page.locator('[data-testid="market-card"]')
await expect(results).toHaveCount(5, { timeout: 5000 })
// 結果に検索語が含まれることを確認
const firstResult = results.first()
await expect(firstResult).toContainText('election', { ignoreCase: true })
// ステータスでフィルタリング
await page.click('button:has-text("Active")')
// フィルタリングされた結果を確認
await expect(results).toHaveCount(3)
})
test('user can create a new market', async ({ page }) => {
// 最初にログイン
await page.goto('/creator-dashboard')
// 市場作成フォームに入力
await page.fill('input[name="name"]', 'Test Market')
await page.fill('textarea[name="description"]', 'Test description')
await page.fill('input[name="endDate"]', '2025-12-31')
// フォームを送信
await page.click('button[type="submit"]')
// 成功メッセージを確認
await expect(page.locator('text=Market created successfully')).toBeVisible()
// 市場ページへのリダイレクトを確認
await expect(page).toHaveURL(/\/markets\/test-market/)
})
```
## テストファイル構成
```
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx # ユニットテスト
│ │ └── Button.stories.tsx # Storybook
│ └── MarketCard/
│ ├── MarketCard.tsx
│ └── MarketCard.test.tsx
├── app/
│ └── api/
│ └── markets/
│ ├── route.ts
│ └── route.test.ts # 統合テスト
└── e2e/
├── markets.spec.ts # E2Eテスト
├── trading.spec.ts
└── auth.spec.ts
```
## 外部サービスのモック
### Supabaseモック
```typescript
jest.mock('@/lib/supabase', () => ({
supabase: {
from: jest.fn(() => ({
select: jest.fn(() => ({
eq: jest.fn(() => Promise.resolve({
data: [{ id: 1, name: 'Test Market' }],
error: null
}))
}))
}))
}
}))
```
### Redisモック
```typescript
jest.mock('@/lib/redis', () => ({
searchMarketsByVector: jest.fn(() => Promise.resolve([
{ slug: 'test-market', similarity_score: 0.95 }
])),
checkRedisHealth: jest.fn(() => Promise.resolve({ connected: true }))
}))
```
### OpenAIモック
```typescript
jest.mock('@/lib/openai', () => ({
generateEmbedding: jest.fn(() => Promise.resolve(
new Array(1536).fill(0.1) // 1536次元埋め込みをモック
))
}))
```
## テストカバレッジ検証
### カバレッジレポートを実行
```bash
npm run test:coverage
```
### カバレッジ閾値
```json
{
"jest": {
"coverageThresholds": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
```
## 避けるべき一般的なテストの誤り
### ❌ 誤り:実装の詳細をテスト
```typescript
// 内部状態をテストしない
expect(component.state.count).toBe(5)
```
### ✅ 正解:ユーザーに見える動作をテスト
```typescript
// ユーザーが見るものをテスト
expect(screen.getByText('Count: 5')).toBeInTheDocument()
```
### ❌ 誤り:脆弱なセレクタ
```typescript
// 簡単に壊れる
await page.click('.css-class-xyz')
```
### ✅ 正解:セマンティックセレクタ
```typescript
// 変更に強い
await page.click('button:has-text("Submit")')
await page.click('[data-testid="submit-button"]')
```
### ❌ 誤り:テストの分離なし
```typescript
// テストが互いに依存
test('creates user', () => { /* ... */ })
test('updates same user', () => { /* 前のテストに依存 */ })
```
### ✅ 正解:独立したテスト
```typescript
// 各テストが独自のデータをセットアップ
test('creates user', () => {
const user = createTestUser()
// テストロジック
})
test('updates user', () => {
const user = createTestUser()
// 更新ロジック
})
```
## 継続的テスト
### 開発中のウォッチモード
```bash
npm test -- --watch
# ファイル変更時に自動的にテストが実行される
```
### プリコミットフック
```bash
# すべてのコミット前に実行
npm test && npm run lint
```
### CI/CD統合
```yaml
# GitHub Actions
- name: Run Tests
run: npm test -- --coverage
- name: Upload Coverage
uses: codecov/codecov-action@v3
```
## ベストプラクティス
1. **テストを最初に書く** - 常にTDD
2. **テストごとに1つのアサート** - 単一の動作に焦点
3. **説明的なテスト名** - テスト内容を説明
4. **Arrange-Act-Assert** - 明確なテスト構造
5. **外部依存関係をモック** - ユニットテストを分離
6. **エッジケースをテスト** - null、undefined、空、大きい値
7. **エラーパスをテスト** - ハッピーパスだけでなく
8. **テストを高速に保つ** - ユニットテスト各50ms未満
9. **テスト後にクリーンアップ** - 副作用なし
10. **カバレッジレポートをレビュー** - ギャップを特定
## 成功指標
- 80%以上のコードカバレッジを達成
- すべてのテストが成功(グリーン)
- スキップまたは無効化されたテストなし
- 高速なテスト実行ユニットテストは30秒未満
- E2Eテストがクリティカルなユーザーフローをカバー
- テストが本番前にバグを検出
---
**覚えておいてください**:テストはオプションではありません。テストは自信を持ってリファクタリングし、迅速に開発し、本番の信頼性を可能にする安全網です。

View File

@@ -1,120 +0,0 @@
# 検証ループスキル
Claude Codeセッション向けの包括的な検証システム。
## 使用タイミング
このスキルを呼び出す:
- 機能または重要なコード変更を完了した後
- PRを作成する前
- 品質ゲートが通過することを確認したい場合
- リファクタリング後
## 検証フェーズ
### フェーズ1: ビルド検証
```bash
# プロジェクトがビルドできるか確認
npm run build 2>&1 | tail -20
# または
pnpm build 2>&1 | tail -20
```
ビルドが失敗した場合、停止して続行前に修正。
### フェーズ2: 型チェック
```bash
# TypeScriptプロジェクト
npx tsc --noEmit 2>&1 | head -30
# Pythonプロジェクト
pyright . 2>&1 | head -30
```
すべての型エラーを報告。続行前に重要なものを修正。
### フェーズ3: Lintチェック
```bash
# JavaScript/TypeScript
npm run lint 2>&1 | head -30
# Python
ruff check . 2>&1 | head -30
```
### フェーズ4: テストスイート
```bash
# カバレッジ付きでテストを実行
npm run test -- --coverage 2>&1 | tail -50
# カバレッジ閾値を確認
# 目標: 最低80%
```
報告:
- 合計テスト数: X
- 成功: X
- 失敗: X
- カバレッジ: X%
### フェーズ5: セキュリティスキャン
```bash
# シークレットを確認
grep -rn "sk-" --include="*.ts" --include="*.js" . 2>/dev/null | head -10
grep -rn "api_key" --include="*.ts" --include="*.js" . 2>/dev/null | head -10
# console.logを確認
grep -rn "console.log" --include="*.ts" --include="*.tsx" src/ 2>/dev/null | head -10
```
### フェーズ6: 差分レビュー
```bash
# 変更内容を表示
git diff --stat
git diff HEAD~1 --name-only
```
各変更ファイルをレビュー:
- 意図しない変更
- 不足しているエラー処理
- 潜在的なエッジケース
## 出力フォーマット
すべてのフェーズを実行後、検証レポートを作成:
```
検証レポート
==================
ビルド: [成功/失敗]
型: [成功/失敗] (Xエラー)
Lint: [成功/失敗] (X警告)
テスト: [成功/失敗] (X/Y成功、Z%カバレッジ)
セキュリティ: [成功/失敗] (X問題)
差分: [Xファイル変更]
総合: PRの準備[完了/未完了]
修正すべき問題:
1. ...
2. ...
```
## 継続モード
長いセッションの場合、15分ごとまたは主要な変更後に検証を実行:
```markdown
メンタルチェックポイントを設定:
- 各関数を完了した後
- コンポーネントを完了した後
- 次のタスクに移る前
実行: /verify
```
## フックとの統合
このスキルはPostToolUseフックを補完しますが、より深い検証を提供します。
フックは問題を即座に捕捉; このスキルは包括的なレビューを提供。