mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
feat: add Cursor, Codex, and OpenCode harnesses — maximize every AI coding tool
- AGENTS.md: universal cross-tool file read by Claude Code, Cursor, Codex, and OpenCode - .cursor/: 15 hook events via hooks.json, 16 hook scripts with DRY adapter pattern, 29 rules (9 common + 20 language-specific) with Cursor YAML frontmatter - .codex/: reference config.toml, Codex-specific AGENTS.md supplement, 10 skills ported to .agents/skills/ with openai.yaml metadata - .opencode/: 3 new tools (format-code, lint-check, git-summary), 3 new hooks (shell.env, experimental.session.compacting, permission.ask), expanded instructions, version bumped to 1.6.0 - README: fixed Cursor section, added Codex section, added cross-tool parity table - install.sh: now copies hooks.json + hooks/ for --target cursor
This commit is contained in:
523
.agents/skills/api-design/SKILL.md
Normal file
523
.agents/skills/api-design/SKILL.md
Normal file
@@ -0,0 +1,523 @@
|
||||
---
|
||||
name: api-design
|
||||
description: REST API design patterns including resource naming, status codes, pagination, filtering, error responses, versioning, and rate limiting for production APIs.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# API Design Patterns
|
||||
|
||||
Conventions and best practices for designing consistent, developer-friendly REST APIs.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Designing new API endpoints
|
||||
- Reviewing existing API contracts
|
||||
- Adding pagination, filtering, or sorting
|
||||
- Implementing error handling for APIs
|
||||
- Planning API versioning strategy
|
||||
- Building public or partner-facing APIs
|
||||
|
||||
## Resource Design
|
||||
|
||||
### URL Structure
|
||||
|
||||
```
|
||||
# Resources are nouns, plural, lowercase, kebab-case
|
||||
GET /api/v1/users
|
||||
GET /api/v1/users/:id
|
||||
POST /api/v1/users
|
||||
PUT /api/v1/users/:id
|
||||
PATCH /api/v1/users/:id
|
||||
DELETE /api/v1/users/:id
|
||||
|
||||
# Sub-resources for relationships
|
||||
GET /api/v1/users/:id/orders
|
||||
POST /api/v1/users/:id/orders
|
||||
|
||||
# Actions that don't map to CRUD (use verbs sparingly)
|
||||
POST /api/v1/orders/:id/cancel
|
||||
POST /api/v1/auth/login
|
||||
POST /api/v1/auth/refresh
|
||||
```
|
||||
|
||||
### Naming Rules
|
||||
|
||||
```
|
||||
# GOOD
|
||||
/api/v1/team-members # kebab-case for multi-word resources
|
||||
/api/v1/orders?status=active # query params for filtering
|
||||
/api/v1/users/123/orders # nested resources for ownership
|
||||
|
||||
# BAD
|
||||
/api/v1/getUsers # verb in URL
|
||||
/api/v1/user # singular (use plural)
|
||||
/api/v1/team_members # snake_case in URLs
|
||||
/api/v1/users/123/getOrders # verb in nested resource
|
||||
```
|
||||
|
||||
## HTTP Methods and Status Codes
|
||||
|
||||
### Method Semantics
|
||||
|
||||
| Method | Idempotent | Safe | Use For |
|
||||
|--------|-----------|------|---------|
|
||||
| GET | Yes | Yes | Retrieve resources |
|
||||
| POST | No | No | Create resources, trigger actions |
|
||||
| PUT | Yes | No | Full replacement of a resource |
|
||||
| PATCH | No* | No | Partial update of a resource |
|
||||
| DELETE | Yes | No | Remove a resource |
|
||||
|
||||
*PATCH can be made idempotent with proper implementation
|
||||
|
||||
### Status Code Reference
|
||||
|
||||
```
|
||||
# Success
|
||||
200 OK — GET, PUT, PATCH (with response body)
|
||||
201 Created — POST (include Location header)
|
||||
204 No Content — DELETE, PUT (no response body)
|
||||
|
||||
# Client Errors
|
||||
400 Bad Request — Validation failure, malformed JSON
|
||||
401 Unauthorized — Missing or invalid authentication
|
||||
403 Forbidden — Authenticated but not authorized
|
||||
404 Not Found — Resource doesn't exist
|
||||
409 Conflict — Duplicate entry, state conflict
|
||||
422 Unprocessable Entity — Semantically invalid (valid JSON, bad data)
|
||||
429 Too Many Requests — Rate limit exceeded
|
||||
|
||||
# Server Errors
|
||||
500 Internal Server Error — Unexpected failure (never expose details)
|
||||
502 Bad Gateway — Upstream service failed
|
||||
503 Service Unavailable — Temporary overload, include Retry-After
|
||||
```
|
||||
|
||||
### Common Mistakes
|
||||
|
||||
```
|
||||
# BAD: 200 for everything
|
||||
{ "status": 200, "success": false, "error": "Not found" }
|
||||
|
||||
# GOOD: Use HTTP status codes semantically
|
||||
HTTP/1.1 404 Not Found
|
||||
{ "error": { "code": "not_found", "message": "User not found" } }
|
||||
|
||||
# BAD: 500 for validation errors
|
||||
# GOOD: 400 or 422 with field-level details
|
||||
|
||||
# BAD: 200 for created resources
|
||||
# GOOD: 201 with Location header
|
||||
HTTP/1.1 201 Created
|
||||
Location: /api/v1/users/abc-123
|
||||
```
|
||||
|
||||
## Response Format
|
||||
|
||||
### Success Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "abc-123",
|
||||
"email": "alice@example.com",
|
||||
"name": "Alice",
|
||||
"created_at": "2025-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Collection Response (with Pagination)
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{ "id": "abc-123", "name": "Alice" },
|
||||
{ "id": "def-456", "name": "Bob" }
|
||||
],
|
||||
"meta": {
|
||||
"total": 142,
|
||||
"page": 1,
|
||||
"per_page": 20,
|
||||
"total_pages": 8
|
||||
},
|
||||
"links": {
|
||||
"self": "/api/v1/users?page=1&per_page=20",
|
||||
"next": "/api/v1/users?page=2&per_page=20",
|
||||
"last": "/api/v1/users?page=8&per_page=20"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "validation_error",
|
||||
"message": "Request validation failed",
|
||||
"details": [
|
||||
{
|
||||
"field": "email",
|
||||
"message": "Must be a valid email address",
|
||||
"code": "invalid_format"
|
||||
},
|
||||
{
|
||||
"field": "age",
|
||||
"message": "Must be between 0 and 150",
|
||||
"code": "out_of_range"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response Envelope Variants
|
||||
|
||||
```typescript
|
||||
// Option A: Envelope with data wrapper (recommended for public APIs)
|
||||
interface ApiResponse<T> {
|
||||
data: T;
|
||||
meta?: PaginationMeta;
|
||||
links?: PaginationLinks;
|
||||
}
|
||||
|
||||
interface ApiError {
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: FieldError[];
|
||||
};
|
||||
}
|
||||
|
||||
// Option B: Flat response (simpler, common for internal APIs)
|
||||
// Success: just return the resource directly
|
||||
// Error: return error object
|
||||
// Distinguish by HTTP status code
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
### Offset-Based (Simple)
|
||||
|
||||
```
|
||||
GET /api/v1/users?page=2&per_page=20
|
||||
|
||||
# Implementation
|
||||
SELECT * FROM users
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20 OFFSET 20;
|
||||
```
|
||||
|
||||
**Pros:** Easy to implement, supports "jump to page N"
|
||||
**Cons:** Slow on large offsets (OFFSET 100000), inconsistent with concurrent inserts
|
||||
|
||||
### Cursor-Based (Scalable)
|
||||
|
||||
```
|
||||
GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20
|
||||
|
||||
# Implementation
|
||||
SELECT * FROM users
|
||||
WHERE id > :cursor_id
|
||||
ORDER BY id ASC
|
||||
LIMIT 21; -- fetch one extra to determine has_next
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [...],
|
||||
"meta": {
|
||||
"has_next": true,
|
||||
"next_cursor": "eyJpZCI6MTQzfQ"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:** Consistent performance regardless of position, stable with concurrent inserts
|
||||
**Cons:** Cannot jump to arbitrary page, cursor is opaque
|
||||
|
||||
### When to Use Which
|
||||
|
||||
| Use Case | Pagination Type |
|
||||
|----------|----------------|
|
||||
| Admin dashboards, small datasets (<10K) | Offset |
|
||||
| Infinite scroll, feeds, large datasets | Cursor |
|
||||
| Public APIs | Cursor (default) with offset (optional) |
|
||||
| Search results | Offset (users expect page numbers) |
|
||||
|
||||
## Filtering, Sorting, and Search
|
||||
|
||||
### Filtering
|
||||
|
||||
```
|
||||
# Simple equality
|
||||
GET /api/v1/orders?status=active&customer_id=abc-123
|
||||
|
||||
# Comparison operators (use bracket notation)
|
||||
GET /api/v1/products?price[gte]=10&price[lte]=100
|
||||
GET /api/v1/orders?created_at[after]=2025-01-01
|
||||
|
||||
# Multiple values (comma-separated)
|
||||
GET /api/v1/products?category=electronics,clothing
|
||||
|
||||
# Nested fields (dot notation)
|
||||
GET /api/v1/orders?customer.country=US
|
||||
```
|
||||
|
||||
### Sorting
|
||||
|
||||
```
|
||||
# Single field (prefix - for descending)
|
||||
GET /api/v1/products?sort=-created_at
|
||||
|
||||
# Multiple fields (comma-separated)
|
||||
GET /api/v1/products?sort=-featured,price,-created_at
|
||||
```
|
||||
|
||||
### Full-Text Search
|
||||
|
||||
```
|
||||
# Search query parameter
|
||||
GET /api/v1/products?q=wireless+headphones
|
||||
|
||||
# Field-specific search
|
||||
GET /api/v1/users?email=alice
|
||||
```
|
||||
|
||||
### Sparse Fieldsets
|
||||
|
||||
```
|
||||
# Return only specified fields (reduces payload)
|
||||
GET /api/v1/users?fields=id,name,email
|
||||
GET /api/v1/orders?fields=id,total,status&include=customer.name
|
||||
```
|
||||
|
||||
## Authentication and Authorization
|
||||
|
||||
### Token-Based Auth
|
||||
|
||||
```
|
||||
# Bearer token in Authorization header
|
||||
GET /api/v1/users
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||
|
||||
# API key (for server-to-server)
|
||||
GET /api/v1/data
|
||||
X-API-Key: sk_live_abc123
|
||||
```
|
||||
|
||||
### Authorization Patterns
|
||||
|
||||
```typescript
|
||||
// Resource-level: check ownership
|
||||
app.get("/api/v1/orders/:id", async (req, res) => {
|
||||
const order = await Order.findById(req.params.id);
|
||||
if (!order) return res.status(404).json({ error: { code: "not_found" } });
|
||||
if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } });
|
||||
return res.json({ data: order });
|
||||
});
|
||||
|
||||
// Role-based: check permissions
|
||||
app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => {
|
||||
await User.delete(req.params.id);
|
||||
return res.status(204).send();
|
||||
});
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Headers
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 95
|
||||
X-RateLimit-Reset: 1640000000
|
||||
|
||||
# When exceeded
|
||||
HTTP/1.1 429 Too Many Requests
|
||||
Retry-After: 60
|
||||
{
|
||||
"error": {
|
||||
"code": "rate_limit_exceeded",
|
||||
"message": "Rate limit exceeded. Try again in 60 seconds."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limit Tiers
|
||||
|
||||
| Tier | Limit | Window | Use Case |
|
||||
|------|-------|--------|----------|
|
||||
| Anonymous | 30/min | Per IP | Public endpoints |
|
||||
| Authenticated | 100/min | Per user | Standard API access |
|
||||
| Premium | 1000/min | Per API key | Paid API plans |
|
||||
| Internal | 10000/min | Per service | Service-to-service |
|
||||
|
||||
## Versioning
|
||||
|
||||
### URL Path Versioning (Recommended)
|
||||
|
||||
```
|
||||
/api/v1/users
|
||||
/api/v2/users
|
||||
```
|
||||
|
||||
**Pros:** Explicit, easy to route, cacheable
|
||||
**Cons:** URL changes between versions
|
||||
|
||||
### Header Versioning
|
||||
|
||||
```
|
||||
GET /api/users
|
||||
Accept: application/vnd.myapp.v2+json
|
||||
```
|
||||
|
||||
**Pros:** Clean URLs
|
||||
**Cons:** Harder to test, easy to forget
|
||||
|
||||
### Versioning Strategy
|
||||
|
||||
```
|
||||
1. Start with /api/v1/ — don't version until you need to
|
||||
2. Maintain at most 2 active versions (current + previous)
|
||||
3. Deprecation timeline:
|
||||
- Announce deprecation (6 months notice for public APIs)
|
||||
- Add Sunset header: Sunset: Sat, 01 Jan 2026 00:00:00 GMT
|
||||
- Return 410 Gone after sunset date
|
||||
4. Non-breaking changes don't need a new version:
|
||||
- Adding new fields to responses
|
||||
- Adding new optional query parameters
|
||||
- Adding new endpoints
|
||||
5. Breaking changes require a new version:
|
||||
- Removing or renaming fields
|
||||
- Changing field types
|
||||
- Changing URL structure
|
||||
- Changing authentication method
|
||||
```
|
||||
|
||||
## Implementation Patterns
|
||||
|
||||
### TypeScript (Next.js API Route)
|
||||
|
||||
```typescript
|
||||
import { z } from "zod";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const createUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1).max(100),
|
||||
});
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json();
|
||||
const parsed = createUserSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({
|
||||
error: {
|
||||
code: "validation_error",
|
||||
message: "Request validation failed",
|
||||
details: parsed.error.issues.map(i => ({
|
||||
field: i.path.join("."),
|
||||
message: i.message,
|
||||
code: i.code,
|
||||
})),
|
||||
},
|
||||
}, { status: 422 });
|
||||
}
|
||||
|
||||
const user = await createUser(parsed.data);
|
||||
|
||||
return NextResponse.json(
|
||||
{ data: user },
|
||||
{
|
||||
status: 201,
|
||||
headers: { Location: `/api/v1/users/${user.id}` },
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Python (Django REST Framework)
|
||||
|
||||
```python
|
||||
from rest_framework import serializers, viewsets, status
|
||||
from rest_framework.response import Response
|
||||
|
||||
class CreateUserSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
name = serializers.CharField(max_length=100)
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "email", "name", "created_at"]
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
return CreateUserSerializer
|
||||
return UserSerializer
|
||||
|
||||
def create(self, request):
|
||||
serializer = CreateUserSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = UserService.create(**serializer.validated_data)
|
||||
return Response(
|
||||
{"data": UserSerializer(user).data},
|
||||
status=status.HTTP_201_CREATED,
|
||||
headers={"Location": f"/api/v1/users/{user.id}"},
|
||||
)
|
||||
```
|
||||
|
||||
### Go (net/http)
|
||||
|
||||
```go
|
||||
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req CreateUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := req.Validate(); err != nil {
|
||||
writeError(w, http.StatusUnprocessableEntity, "validation_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.service.Create(r.Context(), req)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrEmailTaken):
|
||||
writeError(w, http.StatusConflict, "email_taken", "Email already registered")
|
||||
default:
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "Internal error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Location", fmt.Sprintf("/api/v1/users/%s", user.ID))
|
||||
writeJSON(w, http.StatusCreated, map[string]any{"data": user})
|
||||
}
|
||||
```
|
||||
|
||||
## API Design Checklist
|
||||
|
||||
Before shipping a new endpoint:
|
||||
|
||||
- [ ] Resource URL follows naming conventions (plural, kebab-case, no verbs)
|
||||
- [ ] Correct HTTP method used (GET for reads, POST for creates, etc.)
|
||||
- [ ] Appropriate status codes returned (not 200 for everything)
|
||||
- [ ] Input validated with schema (Zod, Pydantic, Bean Validation)
|
||||
- [ ] Error responses follow standard format with codes and messages
|
||||
- [ ] Pagination implemented for list endpoints (cursor or offset)
|
||||
- [ ] Authentication required (or explicitly marked as public)
|
||||
- [ ] Authorization checked (user can only access their own resources)
|
||||
- [ ] Rate limiting configured
|
||||
- [ ] Response does not leak internal details (stack traces, SQL errors)
|
||||
- [ ] Consistent naming with existing endpoints (camelCase vs snake_case)
|
||||
- [ ] Documented (OpenAPI/Swagger spec updated)
|
||||
7
.agents/skills/api-design/agents/openai.yaml
Normal file
7
.agents/skills/api-design/agents/openai.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "API Design"
|
||||
short_description: "REST API design patterns and best practices"
|
||||
brand_color: "#F97316"
|
||||
default_prompt: "Design REST API: resources, status codes, pagination"
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
598
.agents/skills/backend-patterns/SKILL.md
Normal file
598
.agents/skills/backend-patterns/SKILL.md
Normal file
@@ -0,0 +1,598 @@
|
||||
---
|
||||
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.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Backend Development Patterns
|
||||
|
||||
Backend architecture patterns and best practices for scalable server-side applications.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Designing REST or GraphQL API endpoints
|
||||
- Implementing repository, service, or controller layers
|
||||
- Optimizing database queries (N+1, indexing, connection pooling)
|
||||
- Adding caching (Redis, in-memory, HTTP cache headers)
|
||||
- Setting up background jobs or async processing
|
||||
- Structuring error handling and validation for APIs
|
||||
- Building middleware (auth, logging, rate limiting)
|
||||
|
||||
## API Design Patterns
|
||||
|
||||
### RESTful API Structure
|
||||
|
||||
```typescript
|
||||
// ✅ Resource-based URLs
|
||||
GET /api/markets # List resources
|
||||
GET /api/markets/:id # Get single resource
|
||||
POST /api/markets # Create resource
|
||||
PUT /api/markets/:id # Replace resource
|
||||
PATCH /api/markets/:id # Update resource
|
||||
DELETE /api/markets/:id # Delete resource
|
||||
|
||||
// ✅ Query parameters for filtering, sorting, pagination
|
||||
GET /api/markets?status=active&sort=volume&limit=20&offset=0
|
||||
```
|
||||
|
||||
### Repository Pattern
|
||||
|
||||
```typescript
|
||||
// Abstract data access logic
|
||||
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
|
||||
}
|
||||
|
||||
// Other methods...
|
||||
}
|
||||
```
|
||||
|
||||
### Service Layer Pattern
|
||||
|
||||
```typescript
|
||||
// Business logic separated from data access
|
||||
class MarketService {
|
||||
constructor(private marketRepo: MarketRepository) {}
|
||||
|
||||
async searchMarkets(query: string, limit: number = 10): Promise<Market[]> {
|
||||
// Business logic
|
||||
const embedding = await generateEmbedding(query)
|
||||
const results = await this.vectorSearch(embedding, limit)
|
||||
|
||||
// Fetch full data
|
||||
const markets = await this.marketRepo.findByIds(results.map(r => r.id))
|
||||
|
||||
// Sort by similarity
|
||||
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) {
|
||||
// Vector search implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware Pattern
|
||||
|
||||
```typescript
|
||||
// Request/response processing pipeline
|
||||
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' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
export default withAuth(async (req, res) => {
|
||||
// Handler has access to req.user
|
||||
})
|
||||
```
|
||||
|
||||
## Database Patterns
|
||||
|
||||
### Query Optimization
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD: Select only needed columns
|
||||
const { data } = await supabase
|
||||
.from('markets')
|
||||
.select('id, name, status, volume')
|
||||
.eq('status', 'active')
|
||||
.order('volume', { ascending: false })
|
||||
.limit(10)
|
||||
|
||||
// ❌ BAD: Select everything
|
||||
const { data } = await supabase
|
||||
.from('markets')
|
||||
.select('*')
|
||||
```
|
||||
|
||||
### N+1 Query Prevention
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: N+1 query problem
|
||||
const markets = await getMarkets()
|
||||
for (const market of markets) {
|
||||
market.creator = await getUser(market.creator_id) // N queries
|
||||
}
|
||||
|
||||
// ✅ GOOD: Batch fetch
|
||||
const markets = await getMarkets()
|
||||
const creatorIds = markets.map(m => m.creator_id)
|
||||
const creators = await getUsers(creatorIds) // 1 query
|
||||
const creatorMap = new Map(creators.map(c => [c.id, c]))
|
||||
|
||||
markets.forEach(market => {
|
||||
market.creator = creatorMap.get(market.creator_id)
|
||||
})
|
||||
```
|
||||
|
||||
### Transaction Pattern
|
||||
|
||||
```typescript
|
||||
async function createMarketWithPosition(
|
||||
marketData: CreateMarketDto,
|
||||
positionData: CreatePositionDto
|
||||
) {
|
||||
// Use Supabase transaction
|
||||
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
|
||||
}
|
||||
|
||||
// SQL function in Supabase
|
||||
CREATE OR REPLACE FUNCTION create_market_with_position(
|
||||
market_data jsonb,
|
||||
position_data jsonb
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
-- Start transaction automatically
|
||||
INSERT INTO markets VALUES (market_data);
|
||||
INSERT INTO positions VALUES (position_data);
|
||||
RETURN jsonb_build_object('success', true);
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
-- Rollback happens automatically
|
||||
RETURN jsonb_build_object('success', false, 'error', SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
```
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
### Redis Caching Layer
|
||||
|
||||
```typescript
|
||||
class CachedMarketRepository implements MarketRepository {
|
||||
constructor(
|
||||
private baseRepo: MarketRepository,
|
||||
private redis: RedisClient
|
||||
) {}
|
||||
|
||||
async findById(id: string): Promise<Market | null> {
|
||||
// Check cache first
|
||||
const cached = await this.redis.get(`market:${id}`)
|
||||
|
||||
if (cached) {
|
||||
return JSON.parse(cached)
|
||||
}
|
||||
|
||||
// Cache miss - fetch from database
|
||||
const market = await this.baseRepo.findById(id)
|
||||
|
||||
if (market) {
|
||||
// Cache for 5 minutes
|
||||
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 Pattern
|
||||
|
||||
```typescript
|
||||
async function getMarketWithCache(id: string): Promise<Market> {
|
||||
const cacheKey = `market:${id}`
|
||||
|
||||
// Try cache
|
||||
const cached = await redis.get(cacheKey)
|
||||
if (cached) return JSON.parse(cached)
|
||||
|
||||
// Cache miss - fetch from DB
|
||||
const market = await db.markets.findUnique({ where: { id } })
|
||||
|
||||
if (!market) throw new Error('Market not found')
|
||||
|
||||
// Update cache
|
||||
await redis.setex(cacheKey, 300, JSON.stringify(market))
|
||||
|
||||
return market
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
### Centralized Error Handler
|
||||
|
||||
```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 })
|
||||
}
|
||||
|
||||
// Log unexpected errors
|
||||
console.error('Unexpected error:', error)
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
}, { status: 500 })
|
||||
}
|
||||
|
||||
// Usage
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const data = await fetchData()
|
||||
return NextResponse.json({ success: true, data })
|
||||
} catch (error) {
|
||||
return errorHandler(error, request)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Retry with Exponential Backoff
|
||||
|
||||
```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) {
|
||||
// Exponential backoff: 1s, 2s, 4s
|
||||
const delay = Math.pow(2, i) * 1000
|
||||
await new Promise(resolve => setTimeout(resolve, delay))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!
|
||||
}
|
||||
|
||||
// Usage
|
||||
const data = await fetchWithRetry(() => fetchFromAPI())
|
||||
```
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
### JWT Token Validation
|
||||
|
||||
```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)
|
||||
}
|
||||
|
||||
// Usage in API route
|
||||
export async function GET(request: Request) {
|
||||
const user = await requireAuth(request)
|
||||
|
||||
const data = await getDataForUser(user.userId)
|
||||
|
||||
return NextResponse.json({ success: true, data })
|
||||
}
|
||||
```
|
||||
|
||||
### Role-Based Access Control
|
||||
|
||||
```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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage - HOF wraps the handler
|
||||
export const DELETE = requirePermission('delete')(
|
||||
async (request: Request, user: User) => {
|
||||
// Handler receives authenticated user with verified permission
|
||||
return new Response('Deleted', { status: 200 })
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Simple In-Memory Rate Limiter
|
||||
|
||||
```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) || []
|
||||
|
||||
// Remove old requests outside window
|
||||
const recentRequests = requests.filter(time => now - time < windowMs)
|
||||
|
||||
if (recentRequests.length >= maxRequests) {
|
||||
return false // Rate limit exceeded
|
||||
}
|
||||
|
||||
// Add current request
|
||||
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/min
|
||||
|
||||
if (!allowed) {
|
||||
return NextResponse.json({
|
||||
error: 'Rate limit exceeded'
|
||||
}, { status: 429 })
|
||||
}
|
||||
|
||||
// Continue with request
|
||||
}
|
||||
```
|
||||
|
||||
## Background Jobs & Queues
|
||||
|
||||
### Simple Queue Pattern
|
||||
|
||||
```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> {
|
||||
// Job execution logic
|
||||
}
|
||||
}
|
||||
|
||||
// Usage for indexing markets
|
||||
interface IndexJob {
|
||||
marketId: string
|
||||
}
|
||||
|
||||
const indexQueue = new JobQueue<IndexJob>()
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { marketId } = await request.json()
|
||||
|
||||
// Add to queue instead of blocking
|
||||
await indexQueue.add({ marketId })
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Job queued' })
|
||||
}
|
||||
```
|
||||
|
||||
## Logging & Monitoring
|
||||
|
||||
### Structured Logging
|
||||
|
||||
```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()
|
||||
|
||||
// Usage
|
||||
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 })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Remember**: Backend patterns enable scalable, maintainable server-side applications. Choose patterns that fit your complexity level.
|
||||
7
.agents/skills/backend-patterns/agents/openai.yaml
Normal file
7
.agents/skills/backend-patterns/agents/openai.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Backend Patterns"
|
||||
short_description: "API design, database, and server-side patterns"
|
||||
brand_color: "#F59E0B"
|
||||
default_prompt: "Apply backend patterns: API design, repository, caching"
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
530
.agents/skills/coding-standards/SKILL.md
Normal file
530
.agents/skills/coding-standards/SKILL.md
Normal file
@@ -0,0 +1,530 @@
|
||||
---
|
||||
name: coding-standards
|
||||
description: Universal coding standards, best practices, and patterns for TypeScript, JavaScript, React, and Node.js development.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Coding Standards & Best Practices
|
||||
|
||||
Universal coding standards applicable across all projects.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Starting a new project or module
|
||||
- Reviewing code for quality and maintainability
|
||||
- Refactoring existing code to follow conventions
|
||||
- Enforcing naming, formatting, or structural consistency
|
||||
- Setting up linting, formatting, or type-checking rules
|
||||
- Onboarding new contributors to coding conventions
|
||||
|
||||
## Code Quality Principles
|
||||
|
||||
### 1. Readability First
|
||||
- Code is read more than written
|
||||
- Clear variable and function names
|
||||
- Self-documenting code preferred over comments
|
||||
- Consistent formatting
|
||||
|
||||
### 2. KISS (Keep It Simple, Stupid)
|
||||
- Simplest solution that works
|
||||
- Avoid over-engineering
|
||||
- No premature optimization
|
||||
- Easy to understand > clever code
|
||||
|
||||
### 3. DRY (Don't Repeat Yourself)
|
||||
- Extract common logic into functions
|
||||
- Create reusable components
|
||||
- Share utilities across modules
|
||||
- Avoid copy-paste programming
|
||||
|
||||
### 4. YAGNI (You Aren't Gonna Need It)
|
||||
- Don't build features before they're needed
|
||||
- Avoid speculative generality
|
||||
- Add complexity only when required
|
||||
- Start simple, refactor when needed
|
||||
|
||||
## TypeScript/JavaScript Standards
|
||||
|
||||
### Variable Naming
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Function Naming
|
||||
|
||||
```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) { }
|
||||
```
|
||||
|
||||
### Immutability Pattern (CRITICAL)
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```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 Best Practices
|
||||
|
||||
```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()
|
||||
```
|
||||
|
||||
### Type Safety
|
||||
|
||||
```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 Best Practices
|
||||
|
||||
### Component Structure
|
||||
|
||||
```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>
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Hooks
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Conditional Rendering
|
||||
|
||||
```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 Design Standards
|
||||
|
||||
### REST API Conventions
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
### Response Format
|
||||
|
||||
```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 })
|
||||
```
|
||||
|
||||
### Input Validation
|
||||
|
||||
```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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## File Organization
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
### File Naming
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
## Comments & Documentation
|
||||
|
||||
### When to Comment
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### JSDoc for Public APIs
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Best Practices
|
||||
|
||||
### Memoization
|
||||
|
||||
```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)
|
||||
}, [])
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```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>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Database Queries
|
||||
|
||||
```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('*')
|
||||
```
|
||||
|
||||
## Testing Standards
|
||||
|
||||
### Test Structure (AAA Pattern)
|
||||
|
||||
```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)
|
||||
})
|
||||
```
|
||||
|
||||
### Test Naming
|
||||
|
||||
```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', () => { })
|
||||
```
|
||||
|
||||
## Code Smell Detection
|
||||
|
||||
Watch for these anti-patterns:
|
||||
|
||||
### 1. Long Functions
|
||||
```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. Deep Nesting
|
||||
```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. Magic Numbers
|
||||
```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)
|
||||
```
|
||||
|
||||
**Remember**: Code quality is not negotiable. Clear, maintainable code enables rapid development and confident refactoring.
|
||||
7
.agents/skills/coding-standards/agents/openai.yaml
Normal file
7
.agents/skills/coding-standards/agents/openai.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Coding Standards"
|
||||
short_description: "Universal coding standards and best practices"
|
||||
brand_color: "#3B82F6"
|
||||
default_prompt: "Apply standards: immutability, error handling, type safety"
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
326
.agents/skills/e2e-testing/SKILL.md
Normal file
326
.agents/skills/e2e-testing/SKILL.md
Normal file
@@ -0,0 +1,326 @@
|
||||
---
|
||||
name: e2e-testing
|
||||
description: Playwright E2E testing patterns, Page Object Model, configuration, CI/CD integration, artifact management, and flaky test strategies.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# E2E Testing Patterns
|
||||
|
||||
Comprehensive Playwright patterns for building stable, fast, and maintainable E2E test suites.
|
||||
|
||||
## Test File Organization
|
||||
|
||||
```
|
||||
tests/
|
||||
├── e2e/
|
||||
│ ├── auth/
|
||||
│ │ ├── login.spec.ts
|
||||
│ │ ├── logout.spec.ts
|
||||
│ │ └── register.spec.ts
|
||||
│ ├── features/
|
||||
│ │ ├── browse.spec.ts
|
||||
│ │ ├── search.spec.ts
|
||||
│ │ └── create.spec.ts
|
||||
│ └── api/
|
||||
│ └── endpoints.spec.ts
|
||||
├── fixtures/
|
||||
│ ├── auth.ts
|
||||
│ └── data.ts
|
||||
└── playwright.config.ts
|
||||
```
|
||||
|
||||
## Page Object Model (POM)
|
||||
|
||||
```typescript
|
||||
import { Page, Locator } from '@playwright/test'
|
||||
|
||||
export class ItemsPage {
|
||||
readonly page: Page
|
||||
readonly searchInput: Locator
|
||||
readonly itemCards: Locator
|
||||
readonly createButton: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
this.searchInput = page.locator('[data-testid="search-input"]')
|
||||
this.itemCards = page.locator('[data-testid="item-card"]')
|
||||
this.createButton = page.locator('[data-testid="create-btn"]')
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/items')
|
||||
await this.page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
async search(query: string) {
|
||||
await this.searchInput.fill(query)
|
||||
await this.page.waitForResponse(resp => resp.url().includes('/api/search'))
|
||||
await this.page.waitForLoadState('networkidle')
|
||||
}
|
||||
|
||||
async getItemCount() {
|
||||
return await this.itemCards.count()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { ItemsPage } from '../../pages/ItemsPage'
|
||||
|
||||
test.describe('Item Search', () => {
|
||||
let itemsPage: ItemsPage
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
itemsPage = new ItemsPage(page)
|
||||
await itemsPage.goto()
|
||||
})
|
||||
|
||||
test('should search by keyword', async ({ page }) => {
|
||||
await itemsPage.search('test')
|
||||
|
||||
const count = await itemsPage.getItemCount()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
|
||||
await expect(itemsPage.itemCards.first()).toContainText(/test/i)
|
||||
await page.screenshot({ path: 'artifacts/search-results.png' })
|
||||
})
|
||||
|
||||
test('should handle no results', async ({ page }) => {
|
||||
await itemsPage.search('xyznonexistent123')
|
||||
|
||||
await expect(page.locator('[data-testid="no-results"]')).toBeVisible()
|
||||
expect(await itemsPage.getItemCount()).toBe(0)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Playwright Configuration
|
||||
|
||||
```typescript
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: [
|
||||
['html', { outputFolder: 'playwright-report' }],
|
||||
['junit', { outputFile: 'playwright-results.xml' }],
|
||||
['json', { outputFile: 'playwright-results.json' }]
|
||||
],
|
||||
use: {
|
||||
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
actionTimeout: 10000,
|
||||
navigationTimeout: 30000,
|
||||
},
|
||||
projects: [
|
||||
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
||||
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
||||
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
|
||||
],
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120000,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Flaky Test Patterns
|
||||
|
||||
### Quarantine
|
||||
|
||||
```typescript
|
||||
test('flaky: complex search', async ({ page }) => {
|
||||
test.fixme(true, 'Flaky - Issue #123')
|
||||
// test code...
|
||||
})
|
||||
|
||||
test('conditional skip', async ({ page }) => {
|
||||
test.skip(process.env.CI, 'Flaky in CI - Issue #123')
|
||||
// test code...
|
||||
})
|
||||
```
|
||||
|
||||
### Identify Flakiness
|
||||
|
||||
```bash
|
||||
npx playwright test tests/search.spec.ts --repeat-each=10
|
||||
npx playwright test tests/search.spec.ts --retries=3
|
||||
```
|
||||
|
||||
### Common Causes & Fixes
|
||||
|
||||
**Race conditions:**
|
||||
```typescript
|
||||
// Bad: assumes element is ready
|
||||
await page.click('[data-testid="button"]')
|
||||
|
||||
// Good: auto-wait locator
|
||||
await page.locator('[data-testid="button"]').click()
|
||||
```
|
||||
|
||||
**Network timing:**
|
||||
```typescript
|
||||
// Bad: arbitrary timeout
|
||||
await page.waitForTimeout(5000)
|
||||
|
||||
// Good: wait for specific condition
|
||||
await page.waitForResponse(resp => resp.url().includes('/api/data'))
|
||||
```
|
||||
|
||||
**Animation timing:**
|
||||
```typescript
|
||||
// Bad: click during animation
|
||||
await page.click('[data-testid="menu-item"]')
|
||||
|
||||
// Good: wait for stability
|
||||
await page.locator('[data-testid="menu-item"]').waitFor({ state: 'visible' })
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.locator('[data-testid="menu-item"]').click()
|
||||
```
|
||||
|
||||
## Artifact Management
|
||||
|
||||
### Screenshots
|
||||
|
||||
```typescript
|
||||
await page.screenshot({ path: 'artifacts/after-login.png' })
|
||||
await page.screenshot({ path: 'artifacts/full-page.png', fullPage: true })
|
||||
await page.locator('[data-testid="chart"]').screenshot({ path: 'artifacts/chart.png' })
|
||||
```
|
||||
|
||||
### Traces
|
||||
|
||||
```typescript
|
||||
await browser.startTracing(page, {
|
||||
path: 'artifacts/trace.json',
|
||||
screenshots: true,
|
||||
snapshots: true,
|
||||
})
|
||||
// ... test actions ...
|
||||
await browser.stopTracing()
|
||||
```
|
||||
|
||||
### Video
|
||||
|
||||
```typescript
|
||||
// In playwright.config.ts
|
||||
use: {
|
||||
video: 'retain-on-failure',
|
||||
videosPath: 'artifacts/videos/'
|
||||
}
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
```yaml
|
||||
# .github/workflows/e2e.yml
|
||||
name: E2E Tests
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: npx playwright install --with-deps
|
||||
- run: npx playwright test
|
||||
env:
|
||||
BASE_URL: ${{ vars.STAGING_URL }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
```
|
||||
|
||||
## Test Report Template
|
||||
|
||||
```markdown
|
||||
# E2E Test Report
|
||||
|
||||
**Date:** YYYY-MM-DD HH:MM
|
||||
**Duration:** Xm Ys
|
||||
**Status:** PASSING / FAILING
|
||||
|
||||
## Summary
|
||||
- Total: X | Passed: Y (Z%) | Failed: A | Flaky: B | Skipped: C
|
||||
|
||||
## Failed Tests
|
||||
|
||||
### test-name
|
||||
**File:** `tests/e2e/feature.spec.ts:45`
|
||||
**Error:** Expected element to be visible
|
||||
**Screenshot:** artifacts/failed.png
|
||||
**Recommended Fix:** [description]
|
||||
|
||||
## Artifacts
|
||||
- HTML Report: playwright-report/index.html
|
||||
- Screenshots: artifacts/*.png
|
||||
- Videos: artifacts/videos/*.webm
|
||||
- Traces: artifacts/*.zip
|
||||
```
|
||||
|
||||
## Wallet / Web3 Testing
|
||||
|
||||
```typescript
|
||||
test('wallet connection', async ({ page, context }) => {
|
||||
// Mock wallet provider
|
||||
await context.addInitScript(() => {
|
||||
window.ethereum = {
|
||||
isMetaMask: true,
|
||||
request: async ({ method }) => {
|
||||
if (method === 'eth_requestAccounts')
|
||||
return ['0x1234567890123456789012345678901234567890']
|
||||
if (method === 'eth_chainId') return '0x1'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await page.goto('/')
|
||||
await page.locator('[data-testid="connect-wallet"]').click()
|
||||
await expect(page.locator('[data-testid="wallet-address"]')).toContainText('0x1234')
|
||||
})
|
||||
```
|
||||
|
||||
## Financial / Critical Flow Testing
|
||||
|
||||
```typescript
|
||||
test('trade execution', async ({ page }) => {
|
||||
// Skip on production — real money
|
||||
test.skip(process.env.NODE_ENV === 'production', 'Skip on production')
|
||||
|
||||
await page.goto('/markets/test-market')
|
||||
await page.locator('[data-testid="position-yes"]').click()
|
||||
await page.locator('[data-testid="trade-amount"]').fill('1.0')
|
||||
|
||||
// Verify preview
|
||||
const preview = page.locator('[data-testid="trade-preview"]')
|
||||
await expect(preview).toContainText('1.0')
|
||||
|
||||
// Confirm and wait for blockchain
|
||||
await page.locator('[data-testid="confirm-trade"]').click()
|
||||
await page.waitForResponse(
|
||||
resp => resp.url().includes('/api/trade') && resp.status() === 200,
|
||||
{ timeout: 30000 }
|
||||
)
|
||||
|
||||
await expect(page.locator('[data-testid="trade-success"]')).toBeVisible()
|
||||
})
|
||||
```
|
||||
7
.agents/skills/e2e-testing/agents/openai.yaml
Normal file
7
.agents/skills/e2e-testing/agents/openai.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "E2E Testing"
|
||||
short_description: "Playwright end-to-end testing"
|
||||
brand_color: "#06B6D4"
|
||||
default_prompt: "Generate Playwright E2E tests with Page Object Model"
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
236
.agents/skills/eval-harness/SKILL.md
Normal file
236
.agents/skills/eval-harness/SKILL.md
Normal file
@@ -0,0 +1,236 @@
|
||||
---
|
||||
name: eval-harness
|
||||
description: Formal evaluation framework for Claude Code sessions implementing eval-driven development (EDD) principles
|
||||
origin: ECC
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
# Eval Harness Skill
|
||||
|
||||
A formal evaluation framework for Claude Code sessions, implementing eval-driven development (EDD) principles.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Setting up eval-driven development (EDD) for AI-assisted workflows
|
||||
- Defining pass/fail criteria for Claude Code task completion
|
||||
- Measuring agent reliability with pass@k metrics
|
||||
- Creating regression test suites for prompt or agent changes
|
||||
- Benchmarking agent performance across model versions
|
||||
|
||||
## Philosophy
|
||||
|
||||
Eval-Driven Development treats evals as the "unit tests of AI development":
|
||||
- Define expected behavior BEFORE implementation
|
||||
- Run evals continuously during development
|
||||
- Track regressions with each change
|
||||
- Use pass@k metrics for reliability measurement
|
||||
|
||||
## Eval Types
|
||||
|
||||
### Capability Evals
|
||||
Test if Claude can do something it couldn't before:
|
||||
```markdown
|
||||
[CAPABILITY EVAL: feature-name]
|
||||
Task: Description of what Claude should accomplish
|
||||
Success Criteria:
|
||||
- [ ] Criterion 1
|
||||
- [ ] Criterion 2
|
||||
- [ ] Criterion 3
|
||||
Expected Output: Description of expected result
|
||||
```
|
||||
|
||||
### Regression Evals
|
||||
Ensure changes don't break existing functionality:
|
||||
```markdown
|
||||
[REGRESSION EVAL: feature-name]
|
||||
Baseline: SHA or checkpoint name
|
||||
Tests:
|
||||
- existing-test-1: PASS/FAIL
|
||||
- existing-test-2: PASS/FAIL
|
||||
- existing-test-3: PASS/FAIL
|
||||
Result: X/Y passed (previously Y/Y)
|
||||
```
|
||||
|
||||
## Grader Types
|
||||
|
||||
### 1. Code-Based Grader
|
||||
Deterministic checks using code:
|
||||
```bash
|
||||
# Check if file contains expected pattern
|
||||
grep -q "export function handleAuth" src/auth.ts && echo "PASS" || echo "FAIL"
|
||||
|
||||
# Check if tests pass
|
||||
npm test -- --testPathPattern="auth" && echo "PASS" || echo "FAIL"
|
||||
|
||||
# Check if build succeeds
|
||||
npm run build && echo "PASS" || echo "FAIL"
|
||||
```
|
||||
|
||||
### 2. Model-Based Grader
|
||||
Use Claude to evaluate open-ended outputs:
|
||||
```markdown
|
||||
[MODEL GRADER PROMPT]
|
||||
Evaluate the following code change:
|
||||
1. Does it solve the stated problem?
|
||||
2. Is it well-structured?
|
||||
3. Are edge cases handled?
|
||||
4. Is error handling appropriate?
|
||||
|
||||
Score: 1-5 (1=poor, 5=excellent)
|
||||
Reasoning: [explanation]
|
||||
```
|
||||
|
||||
### 3. Human Grader
|
||||
Flag for manual review:
|
||||
```markdown
|
||||
[HUMAN REVIEW REQUIRED]
|
||||
Change: Description of what changed
|
||||
Reason: Why human review is needed
|
||||
Risk Level: LOW/MEDIUM/HIGH
|
||||
```
|
||||
|
||||
## Metrics
|
||||
|
||||
### pass@k
|
||||
"At least one success in k attempts"
|
||||
- pass@1: First attempt success rate
|
||||
- pass@3: Success within 3 attempts
|
||||
- Typical target: pass@3 > 90%
|
||||
|
||||
### pass^k
|
||||
"All k trials succeed"
|
||||
- Higher bar for reliability
|
||||
- pass^3: 3 consecutive successes
|
||||
- Use for critical paths
|
||||
|
||||
## Eval Workflow
|
||||
|
||||
### 1. Define (Before Coding)
|
||||
```markdown
|
||||
## EVAL DEFINITION: feature-xyz
|
||||
|
||||
### Capability Evals
|
||||
1. Can create new user account
|
||||
2. Can validate email format
|
||||
3. Can hash password securely
|
||||
|
||||
### Regression Evals
|
||||
1. Existing login still works
|
||||
2. Session management unchanged
|
||||
3. Logout flow intact
|
||||
|
||||
### Success Metrics
|
||||
- pass@3 > 90% for capability evals
|
||||
- pass^3 = 100% for regression evals
|
||||
```
|
||||
|
||||
### 2. Implement
|
||||
Write code to pass the defined evals.
|
||||
|
||||
### 3. Evaluate
|
||||
```bash
|
||||
# Run capability evals
|
||||
[Run each capability eval, record PASS/FAIL]
|
||||
|
||||
# Run regression evals
|
||||
npm test -- --testPathPattern="existing"
|
||||
|
||||
# Generate report
|
||||
```
|
||||
|
||||
### 4. Report
|
||||
```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
|
||||
```
|
||||
|
||||
## Integration Patterns
|
||||
|
||||
### Pre-Implementation
|
||||
```
|
||||
/eval define feature-name
|
||||
```
|
||||
Creates eval definition file at `.claude/evals/feature-name.md`
|
||||
|
||||
### During Implementation
|
||||
```
|
||||
/eval check feature-name
|
||||
```
|
||||
Runs current evals and reports status
|
||||
|
||||
### Post-Implementation
|
||||
```
|
||||
/eval report feature-name
|
||||
```
|
||||
Generates full eval report
|
||||
|
||||
## Eval Storage
|
||||
|
||||
Store evals in project:
|
||||
```
|
||||
.claude/
|
||||
evals/
|
||||
feature-xyz.md # Eval definition
|
||||
feature-xyz.log # Eval run history
|
||||
baseline.json # Regression baselines
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Define evals BEFORE coding** - Forces clear thinking about success criteria
|
||||
2. **Run evals frequently** - Catch regressions early
|
||||
3. **Track pass@k over time** - Monitor reliability trends
|
||||
4. **Use code graders when possible** - Deterministic > probabilistic
|
||||
5. **Human review for security** - Never fully automate security checks
|
||||
6. **Keep evals fast** - Slow evals don't get run
|
||||
7. **Version evals with code** - Evals are first-class artifacts
|
||||
|
||||
## Example: Adding Authentication
|
||||
|
||||
```markdown
|
||||
## EVAL: add-authentication
|
||||
|
||||
### Phase 1: Define (10 min)
|
||||
Capability Evals:
|
||||
- [ ] User can register with email/password
|
||||
- [ ] User can login with valid credentials
|
||||
- [ ] Invalid credentials rejected with proper error
|
||||
- [ ] Sessions persist across page reloads
|
||||
- [ ] Logout clears session
|
||||
|
||||
Regression Evals:
|
||||
- [ ] Public routes still accessible
|
||||
- [ ] API responses unchanged
|
||||
- [ ] Database schema compatible
|
||||
|
||||
### Phase 2: Implement (varies)
|
||||
[Write code]
|
||||
|
||||
### 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
|
||||
```
|
||||
7
.agents/skills/eval-harness/agents/openai.yaml
Normal file
7
.agents/skills/eval-harness/agents/openai.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Eval Harness"
|
||||
short_description: "Eval-driven development with pass/fail criteria"
|
||||
brand_color: "#EC4899"
|
||||
default_prompt: "Set up eval-driven development with pass/fail criteria"
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
642
.agents/skills/frontend-patterns/SKILL.md
Normal file
642
.agents/skills/frontend-patterns/SKILL.md
Normal file
@@ -0,0 +1,642 @@
|
||||
---
|
||||
name: frontend-patterns
|
||||
description: Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Frontend Development Patterns
|
||||
|
||||
Modern frontend patterns for React, Next.js, and performant user interfaces.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Building React components (composition, props, rendering)
|
||||
- Managing state (useState, useReducer, Zustand, Context)
|
||||
- Implementing data fetching (SWR, React Query, server components)
|
||||
- Optimizing performance (memoization, virtualization, code splitting)
|
||||
- Working with forms (validation, controlled inputs, Zod schemas)
|
||||
- Handling client-side routing and navigation
|
||||
- Building accessible, responsive UI patterns
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Composition Over Inheritance
|
||||
|
||||
```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>
|
||||
```
|
||||
|
||||
### Compound Components
|
||||
|
||||
```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>
|
||||
```
|
||||
|
||||
### Render Props Pattern
|
||||
|
||||
```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>
|
||||
```
|
||||
|
||||
## Custom Hooks Patterns
|
||||
|
||||
### State Management Hook
|
||||
|
||||
```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()
|
||||
```
|
||||
|
||||
### Async Data Fetching Hook
|
||||
|
||||
```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)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Debounce Hook
|
||||
|
||||
```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])
|
||||
```
|
||||
|
||||
## State Management Patterns
|
||||
|
||||
### Context + Reducer Pattern
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Memoization
|
||||
|
||||
```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>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
### Code Splitting & Lazy Loading
|
||||
|
||||
```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>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Virtualization for Long Lists
|
||||
|
||||
```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>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Form Handling Patterns
|
||||
|
||||
### Controlled Form with Validation
|
||||
|
||||
```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>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Boundary Pattern
|
||||
|
||||
```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>
|
||||
```
|
||||
|
||||
## Animation Patterns
|
||||
|
||||
### Framer Motion Animations
|
||||
|
||||
```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>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Accessibility Patterns
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
```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>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Focus Management
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
**Remember**: Modern frontend patterns enable maintainable, performant user interfaces. Choose patterns that fit your project complexity.
|
||||
7
.agents/skills/frontend-patterns/agents/openai.yaml
Normal file
7
.agents/skills/frontend-patterns/agents/openai.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Frontend Patterns"
|
||||
short_description: "React and Next.js patterns and best practices"
|
||||
brand_color: "#8B5CF6"
|
||||
default_prompt: "Apply React/Next.js patterns and best practices"
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
495
.agents/skills/security-review/SKILL.md
Normal file
495
.agents/skills/security-review/SKILL.md
Normal file
@@ -0,0 +1,495 @@
|
||||
---
|
||||
name: security-review
|
||||
description: Use this skill when adding authentication, handling user input, working with secrets, creating API endpoints, or implementing payment/sensitive features. Provides comprehensive security checklist and patterns.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Security Review Skill
|
||||
|
||||
This skill ensures all code follows security best practices and identifies potential vulnerabilities.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Implementing authentication or authorization
|
||||
- Handling user input or file uploads
|
||||
- Creating new API endpoints
|
||||
- Working with secrets or credentials
|
||||
- Implementing payment features
|
||||
- Storing or transmitting sensitive data
|
||||
- Integrating third-party APIs
|
||||
|
||||
## Security Checklist
|
||||
|
||||
### 1. Secrets Management
|
||||
|
||||
#### ❌ NEVER Do This
|
||||
```typescript
|
||||
const apiKey = "sk-proj-xxxxx" // Hardcoded secret
|
||||
const dbPassword = "password123" // In source code
|
||||
```
|
||||
|
||||
#### ✅ ALWAYS Do This
|
||||
```typescript
|
||||
const apiKey = process.env.OPENAI_API_KEY
|
||||
const dbUrl = process.env.DATABASE_URL
|
||||
|
||||
// Verify secrets exist
|
||||
if (!apiKey) {
|
||||
throw new Error('OPENAI_API_KEY not configured')
|
||||
}
|
||||
```
|
||||
|
||||
#### Verification Steps
|
||||
- [ ] No hardcoded API keys, tokens, or passwords
|
||||
- [ ] All secrets in environment variables
|
||||
- [ ] `.env.local` in .gitignore
|
||||
- [ ] No secrets in git history
|
||||
- [ ] Production secrets in hosting platform (Vercel, Railway)
|
||||
|
||||
### 2. Input Validation
|
||||
|
||||
#### Always Validate User Input
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
// Define validation schema
|
||||
const CreateUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1).max(100),
|
||||
age: z.number().int().min(0).max(150)
|
||||
})
|
||||
|
||||
// Validate before processing
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### File Upload Validation
|
||||
```typescript
|
||||
function validateFileUpload(file: File) {
|
||||
// Size check (5MB max)
|
||||
const maxSize = 5 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('File too large (max 5MB)')
|
||||
}
|
||||
|
||||
// Type check
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
throw new Error('Invalid file type')
|
||||
}
|
||||
|
||||
// Extension check
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
#### Verification Steps
|
||||
- [ ] All user inputs validated with schemas
|
||||
- [ ] File uploads restricted (size, type, extension)
|
||||
- [ ] No direct use of user input in queries
|
||||
- [ ] Whitelist validation (not blacklist)
|
||||
- [ ] Error messages don't leak sensitive info
|
||||
|
||||
### 3. SQL Injection Prevention
|
||||
|
||||
#### ❌ NEVER Concatenate SQL
|
||||
```typescript
|
||||
// DANGEROUS - SQL Injection vulnerability
|
||||
const query = `SELECT * FROM users WHERE email = '${userEmail}'`
|
||||
await db.query(query)
|
||||
```
|
||||
|
||||
#### ✅ ALWAYS Use Parameterized Queries
|
||||
```typescript
|
||||
// Safe - parameterized query
|
||||
const { data } = await supabase
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('email', userEmail)
|
||||
|
||||
// Or with raw SQL
|
||||
await db.query(
|
||||
'SELECT * FROM users WHERE email = $1',
|
||||
[userEmail]
|
||||
)
|
||||
```
|
||||
|
||||
#### Verification Steps
|
||||
- [ ] All database queries use parameterized queries
|
||||
- [ ] No string concatenation in SQL
|
||||
- [ ] ORM/query builder used correctly
|
||||
- [ ] Supabase queries properly sanitized
|
||||
|
||||
### 4. Authentication & Authorization
|
||||
|
||||
#### JWT Token Handling
|
||||
```typescript
|
||||
// ❌ WRONG: localStorage (vulnerable to XSS)
|
||||
localStorage.setItem('token', token)
|
||||
|
||||
// ✅ CORRECT: httpOnly cookies
|
||||
res.setHeader('Set-Cookie',
|
||||
`token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`)
|
||||
```
|
||||
|
||||
#### Authorization Checks
|
||||
```typescript
|
||||
export async function deleteUser(userId: string, requesterId: string) {
|
||||
// ALWAYS verify authorization first
|
||||
const requester = await db.users.findUnique({
|
||||
where: { id: requesterId }
|
||||
})
|
||||
|
||||
if (requester.role !== 'admin') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// Proceed with deletion
|
||||
await db.users.delete({ where: { id: userId } })
|
||||
}
|
||||
```
|
||||
|
||||
#### Row Level Security (Supabase)
|
||||
```sql
|
||||
-- Enable RLS on all tables
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Users can only view their own data
|
||||
CREATE POLICY "Users view own data"
|
||||
ON users FOR SELECT
|
||||
USING (auth.uid() = id);
|
||||
|
||||
-- Users can only update their own data
|
||||
CREATE POLICY "Users update own data"
|
||||
ON users FOR UPDATE
|
||||
USING (auth.uid() = id);
|
||||
```
|
||||
|
||||
#### Verification Steps
|
||||
- [ ] Tokens stored in httpOnly cookies (not localStorage)
|
||||
- [ ] Authorization checks before sensitive operations
|
||||
- [ ] Row Level Security enabled in Supabase
|
||||
- [ ] Role-based access control implemented
|
||||
- [ ] Session management secure
|
||||
|
||||
### 5. XSS Prevention
|
||||
|
||||
#### Sanitize HTML
|
||||
```typescript
|
||||
import DOMPurify from 'isomorphic-dompurify'
|
||||
|
||||
// ALWAYS sanitize user-provided HTML
|
||||
function renderUserContent(html: string) {
|
||||
const clean = DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
|
||||
ALLOWED_ATTR: []
|
||||
})
|
||||
return <div dangerouslySetInnerHTML={{ __html: clean }} />
|
||||
}
|
||||
```
|
||||
|
||||
#### Content Security Policy
|
||||
```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()
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Verification Steps
|
||||
- [ ] User-provided HTML sanitized
|
||||
- [ ] CSP headers configured
|
||||
- [ ] No unvalidated dynamic content rendering
|
||||
- [ ] React's built-in XSS protection used
|
||||
|
||||
### 6. CSRF Protection
|
||||
|
||||
#### CSRF Tokens
|
||||
```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 }
|
||||
)
|
||||
}
|
||||
|
||||
// Process request
|
||||
}
|
||||
```
|
||||
|
||||
#### SameSite Cookies
|
||||
```typescript
|
||||
res.setHeader('Set-Cookie',
|
||||
`session=${sessionId}; HttpOnly; Secure; SameSite=Strict`)
|
||||
```
|
||||
|
||||
#### Verification Steps
|
||||
- [ ] CSRF tokens on state-changing operations
|
||||
- [ ] SameSite=Strict on all cookies
|
||||
- [ ] Double-submit cookie pattern implemented
|
||||
|
||||
### 7. Rate Limiting
|
||||
|
||||
#### API Rate Limiting
|
||||
```typescript
|
||||
import rateLimit from 'express-rate-limit'
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // 100 requests per window
|
||||
message: 'Too many requests'
|
||||
})
|
||||
|
||||
// Apply to routes
|
||||
app.use('/api/', limiter)
|
||||
```
|
||||
|
||||
#### Expensive Operations
|
||||
```typescript
|
||||
// Aggressive rate limiting for searches
|
||||
const searchLimiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 10, // 10 requests per minute
|
||||
message: 'Too many search requests'
|
||||
})
|
||||
|
||||
app.use('/api/search', searchLimiter)
|
||||
```
|
||||
|
||||
#### Verification Steps
|
||||
- [ ] Rate limiting on all API endpoints
|
||||
- [ ] Stricter limits on expensive operations
|
||||
- [ ] IP-based rate limiting
|
||||
- [ ] User-based rate limiting (authenticated)
|
||||
|
||||
### 8. Sensitive Data Exposure
|
||||
|
||||
#### Logging
|
||||
```typescript
|
||||
// ❌ WRONG: Logging sensitive data
|
||||
console.log('User login:', { email, password })
|
||||
console.log('Payment:', { cardNumber, cvv })
|
||||
|
||||
// ✅ CORRECT: Redact sensitive data
|
||||
console.log('User login:', { email, userId })
|
||||
console.log('Payment:', { last4: card.last4, userId })
|
||||
```
|
||||
|
||||
#### Error Messages
|
||||
```typescript
|
||||
// ❌ WRONG: Exposing internal details
|
||||
catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message, stack: error.stack },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Generic error messages
|
||||
catch (error) {
|
||||
console.error('Internal error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'An error occurred. Please try again.' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### Verification Steps
|
||||
- [ ] No passwords, tokens, or secrets in logs
|
||||
- [ ] Error messages generic for users
|
||||
- [ ] Detailed errors only in server logs
|
||||
- [ ] No stack traces exposed to users
|
||||
|
||||
### 9. Blockchain Security (Solana)
|
||||
|
||||
#### Wallet Verification
|
||||
```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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Transaction Verification
|
||||
```typescript
|
||||
async function verifyTransaction(transaction: Transaction) {
|
||||
// Verify recipient
|
||||
if (transaction.to !== expectedRecipient) {
|
||||
throw new Error('Invalid recipient')
|
||||
}
|
||||
|
||||
// Verify amount
|
||||
if (transaction.amount > maxAmount) {
|
||||
throw new Error('Amount exceeds limit')
|
||||
}
|
||||
|
||||
// Verify user has sufficient balance
|
||||
const balance = await getBalance(transaction.from)
|
||||
if (balance < transaction.amount) {
|
||||
throw new Error('Insufficient balance')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
#### Verification Steps
|
||||
- [ ] Wallet signatures verified
|
||||
- [ ] Transaction details validated
|
||||
- [ ] Balance checks before transactions
|
||||
- [ ] No blind transaction signing
|
||||
|
||||
### 10. Dependency Security
|
||||
|
||||
#### Regular Updates
|
||||
```bash
|
||||
# Check for vulnerabilities
|
||||
npm audit
|
||||
|
||||
# Fix automatically fixable issues
|
||||
npm audit fix
|
||||
|
||||
# Update dependencies
|
||||
npm update
|
||||
|
||||
# Check for outdated packages
|
||||
npm outdated
|
||||
```
|
||||
|
||||
#### Lock Files
|
||||
```bash
|
||||
# ALWAYS commit lock files
|
||||
git add package-lock.json
|
||||
|
||||
# Use in CI/CD for reproducible builds
|
||||
npm ci # Instead of npm install
|
||||
```
|
||||
|
||||
#### Verification Steps
|
||||
- [ ] Dependencies up to date
|
||||
- [ ] No known vulnerabilities (npm audit clean)
|
||||
- [ ] Lock files committed
|
||||
- [ ] Dependabot enabled on GitHub
|
||||
- [ ] Regular security updates
|
||||
|
||||
## Security Testing
|
||||
|
||||
### Automated Security Tests
|
||||
```typescript
|
||||
// Test authentication
|
||||
test('requires authentication', async () => {
|
||||
const response = await fetch('/api/protected')
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
|
||||
// Test authorization
|
||||
test('requires admin role', async () => {
|
||||
const response = await fetch('/api/admin', {
|
||||
headers: { Authorization: `Bearer ${userToken}` }
|
||||
})
|
||||
expect(response.status).toBe(403)
|
||||
})
|
||||
|
||||
// Test input validation
|
||||
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 rate limiting
|
||||
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)
|
||||
})
|
||||
```
|
||||
|
||||
## Pre-Deployment Security Checklist
|
||||
|
||||
Before ANY production deployment:
|
||||
|
||||
- [ ] **Secrets**: No hardcoded secrets, all in env vars
|
||||
- [ ] **Input Validation**: All user inputs validated
|
||||
- [ ] **SQL Injection**: All queries parameterized
|
||||
- [ ] **XSS**: User content sanitized
|
||||
- [ ] **CSRF**: Protection enabled
|
||||
- [ ] **Authentication**: Proper token handling
|
||||
- [ ] **Authorization**: Role checks in place
|
||||
- [ ] **Rate Limiting**: Enabled on all endpoints
|
||||
- [ ] **HTTPS**: Enforced in production
|
||||
- [ ] **Security Headers**: CSP, X-Frame-Options configured
|
||||
- [ ] **Error Handling**: No sensitive data in errors
|
||||
- [ ] **Logging**: No sensitive data logged
|
||||
- [ ] **Dependencies**: Up to date, no vulnerabilities
|
||||
- [ ] **Row Level Security**: Enabled in Supabase
|
||||
- [ ] **CORS**: Properly configured
|
||||
- [ ] **File Uploads**: Validated (size, type)
|
||||
- [ ] **Wallet Signatures**: Verified (if blockchain)
|
||||
|
||||
## Resources
|
||||
|
||||
- [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)
|
||||
|
||||
---
|
||||
|
||||
**Remember**: Security is not optional. One vulnerability can compromise the entire platform. When in doubt, err on the side of caution.
|
||||
7
.agents/skills/security-review/agents/openai.yaml
Normal file
7
.agents/skills/security-review/agents/openai.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Security Review"
|
||||
short_description: "Comprehensive security checklist and vulnerability detection"
|
||||
brand_color: "#EF4444"
|
||||
default_prompt: "Run security checklist: secrets, input validation, injection prevention"
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
103
.agents/skills/strategic-compact/SKILL.md
Normal file
103
.agents/skills/strategic-compact/SKILL.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
name: strategic-compact
|
||||
description: Suggests manual context compaction at logical intervals to preserve context through task phases rather than arbitrary auto-compaction.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Strategic Compact Skill
|
||||
|
||||
Suggests manual `/compact` at strategic points in your workflow rather than relying on arbitrary auto-compaction.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Running long sessions that approach context limits (200K+ tokens)
|
||||
- Working on multi-phase tasks (research → plan → implement → test)
|
||||
- Switching between unrelated tasks within the same session
|
||||
- After completing a major milestone and starting new work
|
||||
- When responses slow down or become less coherent (context pressure)
|
||||
|
||||
## Why Strategic Compaction?
|
||||
|
||||
Auto-compaction triggers at arbitrary points:
|
||||
- Often mid-task, losing important context
|
||||
- No awareness of logical task boundaries
|
||||
- Can interrupt complex multi-step operations
|
||||
|
||||
Strategic compaction at logical boundaries:
|
||||
- **After exploration, before execution** — Compact research context, keep implementation plan
|
||||
- **After completing a milestone** — Fresh start for next phase
|
||||
- **Before major context shifts** — Clear exploration context before different task
|
||||
|
||||
## How It Works
|
||||
|
||||
The `suggest-compact.js` script runs on PreToolUse (Edit/Write) and:
|
||||
|
||||
1. **Tracks tool calls** — Counts tool invocations in session
|
||||
2. **Threshold detection** — Suggests at configurable threshold (default: 50 calls)
|
||||
3. **Periodic reminders** — Reminds every 25 calls after threshold
|
||||
|
||||
## Hook Setup
|
||||
|
||||
Add to your `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Edit",
|
||||
"hooks": [{ "type": "command", "command": "node ~/.claude/skills/strategic-compact/suggest-compact.js" }]
|
||||
},
|
||||
{
|
||||
"matcher": "Write",
|
||||
"hooks": [{ "type": "command", "command": "node ~/.claude/skills/strategic-compact/suggest-compact.js" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Environment variables:
|
||||
- `COMPACT_THRESHOLD` — Tool calls before first suggestion (default: 50)
|
||||
|
||||
## Compaction Decision Guide
|
||||
|
||||
Use this table to decide when to compact:
|
||||
|
||||
| Phase Transition | Compact? | Why |
|
||||
|-----------------|----------|-----|
|
||||
| Research → Planning | Yes | Research context is bulky; plan is the distilled output |
|
||||
| Planning → Implementation | Yes | Plan is in TodoWrite or a file; free up context for code |
|
||||
| Implementation → Testing | Maybe | Keep if tests reference recent code; compact if switching focus |
|
||||
| Debugging → Next feature | Yes | Debug traces pollute context for unrelated work |
|
||||
| Mid-implementation | No | Losing variable names, file paths, and partial state is costly |
|
||||
| After a failed approach | Yes | Clear the dead-end reasoning before trying a new approach |
|
||||
|
||||
## What Survives Compaction
|
||||
|
||||
Understanding what persists helps you compact with confidence:
|
||||
|
||||
| Persists | Lost |
|
||||
|----------|------|
|
||||
| CLAUDE.md instructions | Intermediate reasoning and analysis |
|
||||
| TodoWrite task list | File contents you previously read |
|
||||
| Memory files (`~/.claude/memory/`) | Multi-step conversation context |
|
||||
| Git state (commits, branches) | Tool call history and counts |
|
||||
| Files on disk | Nuanced user preferences stated verbally |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Compact after planning** — Once plan is finalized in TodoWrite, compact to start fresh
|
||||
2. **Compact after debugging** — Clear error-resolution context before continuing
|
||||
3. **Don't compact mid-implementation** — Preserve context for related changes
|
||||
4. **Read the suggestion** — The hook tells you *when*, you decide *if*
|
||||
5. **Write before compacting** — Save important context to files or memory before compacting
|
||||
6. **Use `/compact` with a summary** — Add a custom message: `/compact Focus on implementing auth middleware next`
|
||||
|
||||
## Related
|
||||
|
||||
- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) — Token optimization section
|
||||
- Memory persistence hooks — For state that survives compaction
|
||||
- `continuous-learning` skill — Extracts patterns before session ends
|
||||
7
.agents/skills/strategic-compact/agents/openai.yaml
Normal file
7
.agents/skills/strategic-compact/agents/openai.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Strategic Compact"
|
||||
short_description: "Context management via strategic compaction"
|
||||
brand_color: "#14B8A6"
|
||||
default_prompt: "Suggest task boundary compaction for context management"
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
410
.agents/skills/tdd-workflow/SKILL.md
Normal file
410
.agents/skills/tdd-workflow/SKILL.md
Normal file
@@ -0,0 +1,410 @@
|
||||
---
|
||||
name: tdd-workflow
|
||||
description: Use this skill when writing new features, fixing bugs, or refactoring code. Enforces test-driven development with 80%+ coverage including unit, integration, and E2E tests.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Test-Driven Development Workflow
|
||||
|
||||
This skill ensures all code development follows TDD principles with comprehensive test coverage.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Writing new features or functionality
|
||||
- Fixing bugs or issues
|
||||
- Refactoring existing code
|
||||
- Adding API endpoints
|
||||
- Creating new components
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Tests BEFORE Code
|
||||
ALWAYS write tests first, then implement code to make tests pass.
|
||||
|
||||
### 2. Coverage Requirements
|
||||
- Minimum 80% coverage (unit + integration + E2E)
|
||||
- All edge cases covered
|
||||
- Error scenarios tested
|
||||
- Boundary conditions verified
|
||||
|
||||
### 3. Test Types
|
||||
|
||||
#### Unit Tests
|
||||
- Individual functions and utilities
|
||||
- Component logic
|
||||
- Pure functions
|
||||
- Helpers and utilities
|
||||
|
||||
#### Integration Tests
|
||||
- API endpoints
|
||||
- Database operations
|
||||
- Service interactions
|
||||
- External API calls
|
||||
|
||||
#### E2E Tests (Playwright)
|
||||
- Critical user flows
|
||||
- Complete workflows
|
||||
- Browser automation
|
||||
- UI interactions
|
||||
|
||||
## TDD Workflow Steps
|
||||
|
||||
### Step 1: Write User Journeys
|
||||
```
|
||||
As a [role], I want to [action], so that [benefit]
|
||||
|
||||
Example:
|
||||
As a user, I want to search for markets semantically,
|
||||
so that I can find relevant markets even without exact keywords.
|
||||
```
|
||||
|
||||
### Step 2: Generate Test Cases
|
||||
For each user journey, create comprehensive test cases:
|
||||
|
||||
```typescript
|
||||
describe('Semantic Search', () => {
|
||||
it('returns relevant markets for query', async () => {
|
||||
// Test implementation
|
||||
})
|
||||
|
||||
it('handles empty query gracefully', async () => {
|
||||
// Test edge case
|
||||
})
|
||||
|
||||
it('falls back to substring search when Redis unavailable', async () => {
|
||||
// Test fallback behavior
|
||||
})
|
||||
|
||||
it('sorts results by similarity score', async () => {
|
||||
// Test sorting logic
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Step 3: Run Tests (They Should Fail)
|
||||
```bash
|
||||
npm test
|
||||
# Tests should fail - we haven't implemented yet
|
||||
```
|
||||
|
||||
### Step 4: Implement Code
|
||||
Write minimal code to make tests pass:
|
||||
|
||||
```typescript
|
||||
// Implementation guided by tests
|
||||
export async function searchMarkets(query: string) {
|
||||
// Implementation here
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Run Tests Again
|
||||
```bash
|
||||
npm test
|
||||
# Tests should now pass
|
||||
```
|
||||
|
||||
### Step 6: Refactor
|
||||
Improve code quality while keeping tests green:
|
||||
- Remove duplication
|
||||
- Improve naming
|
||||
- Optimize performance
|
||||
- Enhance readability
|
||||
|
||||
### Step 7: Verify Coverage
|
||||
```bash
|
||||
npm run test:coverage
|
||||
# Verify 80%+ coverage achieved
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Unit Test Pattern (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 Integration Test Pattern
|
||||
```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 () => {
|
||||
// Mock database failure
|
||||
const request = new NextRequest('http://localhost/api/markets')
|
||||
// Test error handling
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### E2E Test Pattern (Playwright)
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('user can search and filter markets', async ({ page }) => {
|
||||
// Navigate to markets page
|
||||
await page.goto('/')
|
||||
await page.click('a[href="/markets"]')
|
||||
|
||||
// Verify page loaded
|
||||
await expect(page.locator('h1')).toContainText('Markets')
|
||||
|
||||
// Search for markets
|
||||
await page.fill('input[placeholder="Search markets"]', 'election')
|
||||
|
||||
// Wait for debounce and results
|
||||
await page.waitForTimeout(600)
|
||||
|
||||
// Verify search results displayed
|
||||
const results = page.locator('[data-testid="market-card"]')
|
||||
await expect(results).toHaveCount(5, { timeout: 5000 })
|
||||
|
||||
// Verify results contain search term
|
||||
const firstResult = results.first()
|
||||
await expect(firstResult).toContainText('election', { ignoreCase: true })
|
||||
|
||||
// Filter by status
|
||||
await page.click('button:has-text("Active")')
|
||||
|
||||
// Verify filtered results
|
||||
await expect(results).toHaveCount(3)
|
||||
})
|
||||
|
||||
test('user can create a new market', async ({ page }) => {
|
||||
// Login first
|
||||
await page.goto('/creator-dashboard')
|
||||
|
||||
// Fill market creation form
|
||||
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')
|
||||
|
||||
// Submit form
|
||||
await page.click('button[type="submit"]')
|
||||
|
||||
// Verify success message
|
||||
await expect(page.locator('text=Market created successfully')).toBeVisible()
|
||||
|
||||
// Verify redirect to market page
|
||||
await expect(page).toHaveURL(/\/markets\/test-market/)
|
||||
})
|
||||
```
|
||||
|
||||
## Test File Organization
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── Button/
|
||||
│ │ ├── Button.tsx
|
||||
│ │ ├── Button.test.tsx # Unit tests
|
||||
│ │ └── Button.stories.tsx # Storybook
|
||||
│ └── MarketCard/
|
||||
│ ├── MarketCard.tsx
|
||||
│ └── MarketCard.test.tsx
|
||||
├── app/
|
||||
│ └── api/
|
||||
│ └── markets/
|
||||
│ ├── route.ts
|
||||
│ └── route.test.ts # Integration tests
|
||||
└── e2e/
|
||||
├── markets.spec.ts # E2E tests
|
||||
├── trading.spec.ts
|
||||
└── auth.spec.ts
|
||||
```
|
||||
|
||||
## Mocking External Services
|
||||
|
||||
### Supabase Mock
|
||||
```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 Mock
|
||||
```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 Mock
|
||||
```typescript
|
||||
jest.mock('@/lib/openai', () => ({
|
||||
generateEmbedding: jest.fn(() => Promise.resolve(
|
||||
new Array(1536).fill(0.1) // Mock 1536-dim embedding
|
||||
))
|
||||
}))
|
||||
```
|
||||
|
||||
## Test Coverage Verification
|
||||
|
||||
### Run Coverage Report
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
### Coverage Thresholds
|
||||
```json
|
||||
{
|
||||
"jest": {
|
||||
"coverageThresholds": {
|
||||
"global": {
|
||||
"branches": 80,
|
||||
"functions": 80,
|
||||
"lines": 80,
|
||||
"statements": 80
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Testing Mistakes to Avoid
|
||||
|
||||
### ❌ WRONG: Testing Implementation Details
|
||||
```typescript
|
||||
// Don't test internal state
|
||||
expect(component.state.count).toBe(5)
|
||||
```
|
||||
|
||||
### ✅ CORRECT: Test User-Visible Behavior
|
||||
```typescript
|
||||
// Test what users see
|
||||
expect(screen.getByText('Count: 5')).toBeInTheDocument()
|
||||
```
|
||||
|
||||
### ❌ WRONG: Brittle Selectors
|
||||
```typescript
|
||||
// Breaks easily
|
||||
await page.click('.css-class-xyz')
|
||||
```
|
||||
|
||||
### ✅ CORRECT: Semantic Selectors
|
||||
```typescript
|
||||
// Resilient to changes
|
||||
await page.click('button:has-text("Submit")')
|
||||
await page.click('[data-testid="submit-button"]')
|
||||
```
|
||||
|
||||
### ❌ WRONG: No Test Isolation
|
||||
```typescript
|
||||
// Tests depend on each other
|
||||
test('creates user', () => { /* ... */ })
|
||||
test('updates same user', () => { /* depends on previous test */ })
|
||||
```
|
||||
|
||||
### ✅ CORRECT: Independent Tests
|
||||
```typescript
|
||||
// Each test sets up its own data
|
||||
test('creates user', () => {
|
||||
const user = createTestUser()
|
||||
// Test logic
|
||||
})
|
||||
|
||||
test('updates user', () => {
|
||||
const user = createTestUser()
|
||||
// Update logic
|
||||
})
|
||||
```
|
||||
|
||||
## Continuous Testing
|
||||
|
||||
### Watch Mode During Development
|
||||
```bash
|
||||
npm test -- --watch
|
||||
# Tests run automatically on file changes
|
||||
```
|
||||
|
||||
### Pre-Commit Hook
|
||||
```bash
|
||||
# Runs before every commit
|
||||
npm test && npm run lint
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
- name: Run Tests
|
||||
run: npm test -- --coverage
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Write Tests First** - Always TDD
|
||||
2. **One Assert Per Test** - Focus on single behavior
|
||||
3. **Descriptive Test Names** - Explain what's tested
|
||||
4. **Arrange-Act-Assert** - Clear test structure
|
||||
5. **Mock External Dependencies** - Isolate unit tests
|
||||
6. **Test Edge Cases** - Null, undefined, empty, large
|
||||
7. **Test Error Paths** - Not just happy paths
|
||||
8. **Keep Tests Fast** - Unit tests < 50ms each
|
||||
9. **Clean Up After Tests** - No side effects
|
||||
10. **Review Coverage Reports** - Identify gaps
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- 80%+ code coverage achieved
|
||||
- All tests passing (green)
|
||||
- No skipped or disabled tests
|
||||
- Fast test execution (< 30s for unit tests)
|
||||
- E2E tests cover critical user flows
|
||||
- Tests catch bugs before production
|
||||
|
||||
---
|
||||
|
||||
**Remember**: Tests are not optional. They are the safety net that enables confident refactoring, rapid development, and production reliability.
|
||||
7
.agents/skills/tdd-workflow/agents/openai.yaml
Normal file
7
.agents/skills/tdd-workflow/agents/openai.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "TDD Workflow"
|
||||
short_description: "Test-driven development with 80%+ coverage"
|
||||
brand_color: "#22C55E"
|
||||
default_prompt: "Follow TDD: write tests first, implement, verify 80%+ coverage"
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
126
.agents/skills/verification-loop/SKILL.md
Normal file
126
.agents/skills/verification-loop/SKILL.md
Normal file
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: verification-loop
|
||||
description: "A comprehensive verification system for Claude Code sessions."
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Verification Loop Skill
|
||||
|
||||
A comprehensive verification system for Claude Code sessions.
|
||||
|
||||
## When to Use
|
||||
|
||||
Invoke this skill:
|
||||
- After completing a feature or significant code change
|
||||
- Before creating a PR
|
||||
- When you want to ensure quality gates pass
|
||||
- After refactoring
|
||||
|
||||
## Verification Phases
|
||||
|
||||
### Phase 1: Build Verification
|
||||
```bash
|
||||
# Check if project builds
|
||||
npm run build 2>&1 | tail -20
|
||||
# OR
|
||||
pnpm build 2>&1 | tail -20
|
||||
```
|
||||
|
||||
If build fails, STOP and fix before continuing.
|
||||
|
||||
### Phase 2: Type Check
|
||||
```bash
|
||||
# TypeScript projects
|
||||
npx tsc --noEmit 2>&1 | head -30
|
||||
|
||||
# Python projects
|
||||
pyright . 2>&1 | head -30
|
||||
```
|
||||
|
||||
Report all type errors. Fix critical ones before continuing.
|
||||
|
||||
### Phase 3: Lint Check
|
||||
```bash
|
||||
# JavaScript/TypeScript
|
||||
npm run lint 2>&1 | head -30
|
||||
|
||||
# Python
|
||||
ruff check . 2>&1 | head -30
|
||||
```
|
||||
|
||||
### Phase 4: Test Suite
|
||||
```bash
|
||||
# Run tests with coverage
|
||||
npm run test -- --coverage 2>&1 | tail -50
|
||||
|
||||
# Check coverage threshold
|
||||
# Target: 80% minimum
|
||||
```
|
||||
|
||||
Report:
|
||||
- Total tests: X
|
||||
- Passed: X
|
||||
- Failed: X
|
||||
- Coverage: X%
|
||||
|
||||
### Phase 5: Security Scan
|
||||
```bash
|
||||
# Check for secrets
|
||||
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
|
||||
|
||||
# Check for console.log
|
||||
grep -rn "console.log" --include="*.ts" --include="*.tsx" src/ 2>/dev/null | head -10
|
||||
```
|
||||
|
||||
### Phase 6: Diff Review
|
||||
```bash
|
||||
# Show what changed
|
||||
git diff --stat
|
||||
git diff HEAD~1 --name-only
|
||||
```
|
||||
|
||||
Review each changed file for:
|
||||
- Unintended changes
|
||||
- Missing error handling
|
||||
- Potential edge cases
|
||||
|
||||
## Output Format
|
||||
|
||||
After running all phases, produce a verification report:
|
||||
|
||||
```
|
||||
VERIFICATION REPORT
|
||||
==================
|
||||
|
||||
Build: [PASS/FAIL]
|
||||
Types: [PASS/FAIL] (X errors)
|
||||
Lint: [PASS/FAIL] (X warnings)
|
||||
Tests: [PASS/FAIL] (X/Y passed, Z% coverage)
|
||||
Security: [PASS/FAIL] (X issues)
|
||||
Diff: [X files changed]
|
||||
|
||||
Overall: [READY/NOT READY] for PR
|
||||
|
||||
Issues to Fix:
|
||||
1. ...
|
||||
2. ...
|
||||
```
|
||||
|
||||
## Continuous Mode
|
||||
|
||||
For long sessions, run verification every 15 minutes or after major changes:
|
||||
|
||||
```markdown
|
||||
Set a mental checkpoint:
|
||||
- After completing each function
|
||||
- After finishing a component
|
||||
- Before moving to next task
|
||||
|
||||
Run: /verify
|
||||
```
|
||||
|
||||
## Integration with Hooks
|
||||
|
||||
This skill complements PostToolUse hooks but provides deeper verification.
|
||||
Hooks catch issues immediately; this skill provides comprehensive review.
|
||||
7
.agents/skills/verification-loop/agents/openai.yaml
Normal file
7
.agents/skills/verification-loop/agents/openai.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Verification Loop"
|
||||
short_description: "Build, test, lint, typecheck verification"
|
||||
brand_color: "#10B981"
|
||||
default_prompt: "Run verification: build, test, lint, typecheck, security"
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
Reference in New Issue
Block a user