- Add feature-dev-test.sh: full 10-step E2E test for SDLC + Claude Code workflow - Update feature-development.md cookbook with complete workflow documentation - Fix SDLC orchestrator and project management handler improvements - Update scaffold-test.sh with minor fixes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1111 lines
32 KiB
Markdown
1111 lines
32 KiB
Markdown
# Feature Development Cookbook
|
|
|
|
> Complete workflow for building features using rdev composable monorepo templates with chassis patterns, design system, and auth integration.
|
|
|
|
## Overview
|
|
|
|
This cookbook documents the end-to-end workflow for developing features in a composable monorepo project. There are two approaches:
|
|
|
|
1. **SDLC-Driven** (Recommended) - Structured workflow using the SDLC system and Claude Code
|
|
2. **Manual** - Traditional development with manual planning and implementation
|
|
|
|
The SDLC-driven approach uses skeleton commands that Claude executes inside the project pod, producing structured artifacts (spec, design, tasks) that can be approved and tracked.
|
|
|
|
```
|
|
SDLC Feature Development Workflow
|
|
──────────────────────────────────
|
|
1. Create Feature → POST /sdlc/features
|
|
2. Generate Spec → Claude runs /spec-feature
|
|
3. Approve Spec → POST /sdlc/features/{slug}/artifacts/spec/approve
|
|
4. Generate Design → Claude runs /design-feature
|
|
5. Approve Design → POST /sdlc/features/{slug}/artifacts/design/approve
|
|
6. Break Down Tasks → Claude runs /breakdown-feature
|
|
7. Approve Tasks → POST /sdlc/features/{slug}/artifacts/tasks/approve
|
|
8. Implement Tasks → Claude runs /implement-task for each
|
|
9. Review & Merge → Claude runs /review-feature, /merge-feature
|
|
```
|
|
|
|
---
|
|
|
|
## Quick Start: API-Driven Feature Development
|
|
|
|
This section shows the minimal API calls to develop a feature using Claude Code and the SDLC system.
|
|
|
|
### Prerequisites
|
|
|
|
```bash
|
|
export RDEV_API_URL="https://rdev.masq-ops.orchard9.ai"
|
|
export RDEV_API_KEY="<your-api-key>"
|
|
```
|
|
|
|
### 1. Create Feature
|
|
|
|
```bash
|
|
curl -X POST "$RDEV_API_URL/projects/myapp/sdlc/features" \
|
|
-H "X-API-Key: $RDEV_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"slug": "add-hello", "title": "Add /hello endpoint"}'
|
|
```
|
|
|
|
### 2. Generate Spec with Claude
|
|
|
|
```bash
|
|
curl -X POST "$RDEV_API_URL/projects/myapp/builds" \
|
|
-H "X-API-Key: $RDEV_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"prompt": "/spec-feature add-hello",
|
|
"auto_commit": true,
|
|
"auto_push": true,
|
|
"git_clone_url": "https://git.threesix.ai/jordan/myapp.git"
|
|
}'
|
|
# Returns: {"data": {"task_id": "abc123", ...}}
|
|
```
|
|
|
|
### 3. Check Build Status
|
|
|
|
```bash
|
|
curl "$RDEV_API_URL/builds/abc123" \
|
|
-H "X-API-Key: $RDEV_API_KEY"
|
|
# Wait for status: "completed"
|
|
```
|
|
|
|
### 4. Check Classifier for Next Action
|
|
|
|
```bash
|
|
curl "$RDEV_API_URL/projects/myapp/sdlc/next?feature=add-hello" \
|
|
-H "X-API-Key: $RDEV_API_KEY"
|
|
# Returns: {"action": "AWAIT_APPROVAL", "artifact": "spec", ...}
|
|
```
|
|
|
|
### 5. Approve Spec
|
|
|
|
```bash
|
|
curl -X POST "$RDEV_API_URL/projects/myapp/sdlc/features/add-hello/artifacts/spec/approve" \
|
|
-H "X-API-Key: $RDEV_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{}'
|
|
```
|
|
|
|
### 6. Continue Through Phases
|
|
|
|
Repeat the pattern: **Generate artifact** → **Check classifier** → **Approve** → **Next phase**
|
|
|
|
```bash
|
|
# Generate design
|
|
curl -X POST "$RDEV_API_URL/projects/myapp/builds" \
|
|
-H "X-API-Key: $RDEV_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"prompt": "/design-feature add-hello", "auto_commit": true, "auto_push": true, "git_clone_url": "https://git.threesix.ai/jordan/myapp.git"}'
|
|
|
|
# Approve design
|
|
curl -X POST "$RDEV_API_URL/projects/myapp/sdlc/features/add-hello/artifacts/design/approve" \
|
|
-H "X-API-Key: $RDEV_API_KEY" -H "Content-Type: application/json" -d '{}'
|
|
|
|
# Generate tasks
|
|
curl -X POST "$RDEV_API_URL/projects/myapp/builds" \
|
|
-H "X-API-Key: $RDEV_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"prompt": "/breakdown-feature add-hello", "auto_commit": true, "auto_push": true, "git_clone_url": "https://git.threesix.ai/jordan/myapp.git"}'
|
|
|
|
# Approve tasks
|
|
curl -X POST "$RDEV_API_URL/projects/myapp/sdlc/features/add-hello/artifacts/tasks/approve" \
|
|
-H "X-API-Key: $RDEV_API_KEY" -H "Content-Type: application/json" -d '{}'
|
|
|
|
# Implement first task
|
|
curl -X POST "$RDEV_API_URL/projects/myapp/builds" \
|
|
-H "X-API-Key: $RDEV_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"prompt": "/implement-task add-hello T1", "auto_commit": true, "auto_push": true, "git_clone_url": "https://git.threesix.ai/jordan/myapp.git"}'
|
|
```
|
|
|
|
### E2E Test Script
|
|
|
|
Run the full workflow automatically:
|
|
|
|
```bash
|
|
./cookbooks/scripts/feature-dev-test.sh run my-test-project
|
|
./cookbooks/scripts/feature-dev-test.sh status my-test-project
|
|
./cookbooks/scripts/feature-dev-test.sh teardown my-test-project
|
|
```
|
|
|
|
---
|
|
|
|
## Skeleton Commands Reference
|
|
|
|
These commands are installed in every project's `.claude/commands/` directory and can be invoked via the `/builds` endpoint.
|
|
|
|
| Command | Description | Artifacts |
|
|
|---------|-------------|-----------|
|
|
| `/spec-feature <slug>` | Create specification document | `.sdlc/features/<slug>/spec.md` |
|
|
| `/design-feature <slug>` | Create technical design | `.sdlc/features/<slug>/design.md` |
|
|
| `/breakdown-feature <slug>` | Break feature into tasks | `.sdlc/features/<slug>/tasks.md` |
|
|
| `/implement-task <slug> <task-id>` | Implement a specific task | Code changes |
|
|
| `/create-qa-plan <slug>` | Create QA test plan | `.sdlc/features/<slug>/qa-plan.md` |
|
|
| `/run-qa <slug>` | Execute QA tests | Test results |
|
|
| `/review-feature <slug>` | Code review all changes | Review report |
|
|
| `/audit-feature <slug>` | Audit feature for compliance | Audit report |
|
|
| `/fix-review-issues <slug>` | Fix issues from review | Code changes |
|
|
| `/remediate-audit <slug>` | Fix audit findings | Code changes |
|
|
| `/fix-qa-failures <slug>` | Fix failing QA tests | Code changes |
|
|
| `/merge-feature <slug>` | Merge feature to main | Merged branch |
|
|
| `/archive-feature <slug>` | Archive completed feature | Archived state |
|
|
| `/next <slug>` | Get classifier recommendation | Next action |
|
|
| `/deliver <slug>` | Run full delivery pipeline | All artifacts |
|
|
|
|
---
|
|
|
|
## SDLC API Reference
|
|
|
|
### Feature Management
|
|
|
|
| Endpoint | Method | Description |
|
|
|----------|--------|-------------|
|
|
| `/projects/{id}/sdlc/state` | GET | Get SDLC state for project |
|
|
| `/projects/{id}/sdlc/features` | GET | List all features |
|
|
| `/projects/{id}/sdlc/features` | POST | Create new feature |
|
|
| `/projects/{id}/sdlc/features/{slug}` | GET | Get feature details |
|
|
| `/projects/{id}/sdlc/features/{slug}` | DELETE | Delete feature |
|
|
|
|
### Artifacts
|
|
|
|
| Endpoint | Method | Description |
|
|
|----------|--------|-------------|
|
|
| `/projects/{id}/sdlc/features/{slug}/artifacts` | GET | List artifact status |
|
|
| `/projects/{id}/sdlc/features/{slug}/artifacts/{type}/approve` | POST | Approve artifact |
|
|
|
|
### Tasks
|
|
|
|
| Endpoint | Method | Description |
|
|
|----------|--------|-------------|
|
|
| `/projects/{id}/sdlc/features/{slug}/tasks` | GET | List tasks |
|
|
| `/projects/{id}/sdlc/features/{slug}/tasks` | POST | Add task |
|
|
|
|
### Workflow Control
|
|
|
|
| Endpoint | Method | Description |
|
|
|----------|--------|-------------|
|
|
| `/projects/{id}/sdlc/features/{slug}/block` | POST | Block feature |
|
|
| `/projects/{id}/sdlc/features/{slug}/unblock` | POST | Unblock feature |
|
|
| `/projects/{id}/sdlc/next` | GET | Get classifier recommendation |
|
|
|
|
### Queries
|
|
|
|
| Endpoint | Method | Description |
|
|
|----------|--------|-------------|
|
|
| `/projects/{id}/sdlc/query/blocked` | GET | List blocked features |
|
|
| `/projects/{id}/sdlc/query/ready` | GET | List ready features |
|
|
| `/projects/{id}/sdlc/query/needs-approval` | GET | List features awaiting approval |
|
|
|
|
---
|
|
|
|
## Manual Development Workflow
|
|
|
|
For cases where you want direct control without the SDLC system, you can follow the traditional development workflow below.
|
|
|
|
```
|
|
Manual Feature Development Workflow
|
|
────────────────────────────────────
|
|
1. Planning → Define requirements, break into tasks
|
|
2. API Dev → Bind + Wrap patterns, OpenAPI annotations
|
|
3. Frontend Dev → Auth hooks, design system, API client
|
|
4. Testing → Handler tests, integration tests
|
|
5. Review → /review-code, quality checks
|
|
6. Deployment → Git push, Woodpecker CI, verify
|
|
```
|
|
|
|
This approach assumes you have:
|
|
|
|
- An rdev project with monorepo skeleton (created via `POST /project`)
|
|
- At least one API service component (`services/api`)
|
|
- A Next.js or React app component (`apps/dashboard`)
|
|
|
|
### Project Structure
|
|
|
|
Your composable monorepo should have:
|
|
|
|
```
|
|
my-project/
|
|
├── pkg/ # Shared Go packages
|
|
│ ├── app/ # Service bootstrapper
|
|
│ │ ├── app.go # NewApp, Run, RegisterRoutes
|
|
│ │ ├── handler.go # Wrap pattern
|
|
│ │ ├── bind.go # Bind + BindAndValidate
|
|
│ │ └── health.go # Health probes
|
|
│ ├── httperror/ # Typed HTTP errors
|
|
│ ├── httpresponse/ # JSON response envelope
|
|
│ ├── httpvalidation/ # go-playground/validator
|
|
│ └── auth/ # JWT + API key validation
|
|
├── packages/ # Shared TS packages
|
|
│ ├── ui/ # Design system components
|
|
│ ├── layout/ # DashboardShell, Sidebar
|
|
│ ├── auth/ # AuthProvider, useAuth
|
|
│ ├── logger/ # Structured logging
|
|
│ └── api-client/ # Generated TypeScript client
|
|
├── services/ # Go backend services
|
|
│ └── api/
|
|
│ ├── cmd/server/main.go
|
|
│ └── internal/api/routes.go
|
|
└── apps/ # Frontend applications
|
|
└── dashboard/
|
|
└── src/app/ # Next.js App Router
|
|
```
|
|
|
|
---
|
|
|
|
## Step 1: Feature Planning
|
|
|
|
### Create Feature Branch
|
|
```bash
|
|
git checkout -b feature/user-profile
|
|
```
|
|
|
|
### Define Requirements
|
|
|
|
Example: User Profile Feature
|
|
- **Backend:** GET/PUT /api/users/me endpoints
|
|
- **Frontend:** Profile page with edit form
|
|
- **Validation:** Email format, name required
|
|
- **Auth:** Require authenticated user
|
|
|
|
### Break Into Tasks
|
|
|
|
```markdown
|
|
## Tasks
|
|
|
|
- [ ] Add User domain model with validation
|
|
- [ ] Implement GET /api/users/me handler
|
|
- [ ] Implement PUT /api/users/me handler
|
|
- [ ] Add OpenAPI annotations
|
|
- [ ] Create profile page component
|
|
- [ ] Add profile edit form
|
|
- [ ] Connect to API with generated client
|
|
- [ ] Write handler tests
|
|
- [ ] Run quality checks
|
|
```
|
|
|
|
---
|
|
|
|
## Step 2: API Development
|
|
|
|
### 2.1 Add Domain Model
|
|
|
|
Create the domain model in `services/api/internal/domain/`:
|
|
|
|
```go
|
|
// services/api/internal/domain/user.go
|
|
package domain
|
|
|
|
import "time"
|
|
|
|
// User represents an authenticated user's profile.
|
|
type User struct {
|
|
ID string `json:"id" db:"id"`
|
|
Email string `json:"email" db:"email"`
|
|
Name string `json:"name" db:"name"`
|
|
AvatarURL string `json:"avatar_url,omitempty" db:"avatar_url"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
```
|
|
|
|
### 2.2 Create Handler with Wrap Pattern
|
|
|
|
The `Wrap` pattern converts error-returning handlers to `http.HandlerFunc`:
|
|
|
|
```go
|
|
// services/api/internal/api/handlers/user.go
|
|
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"my-project/pkg/app"
|
|
"my-project/pkg/auth"
|
|
"my-project/pkg/httperror"
|
|
"my-project/pkg/httpresponse"
|
|
"my-project/services/api/internal/service"
|
|
)
|
|
|
|
type UserHandler struct {
|
|
svc *service.UserService
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewUserHandler(svc *service.UserService, logger *slog.Logger) *UserHandler {
|
|
return &UserHandler{svc: svc, logger: logger}
|
|
}
|
|
|
|
// GetMe returns the authenticated user's profile.
|
|
// Uses app.Wrap to convert error-returning handler to http.HandlerFunc.
|
|
func (h *UserHandler) GetMe() http.HandlerFunc {
|
|
return app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
|
|
// Get user from auth context
|
|
user, ok := auth.GetUser(r.Context())
|
|
if !ok {
|
|
return httperror.Unauthorized("not authenticated")
|
|
}
|
|
|
|
// Fetch full profile
|
|
profile, err := h.svc.GetByID(r.Context(), user.ID)
|
|
if err != nil {
|
|
return httperror.WrapError(err, "failed to get profile")
|
|
}
|
|
|
|
httpresponse.JSON(w, http.StatusOK, profile)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// UpdateMe updates the authenticated user's profile.
|
|
// Uses app.BindAndValidate for request parsing and validation.
|
|
func (h *UserHandler) UpdateMe() http.HandlerFunc {
|
|
type request struct {
|
|
Name string `json:"name" validate:"required,min=1,max=100"`
|
|
AvatarURL string `json:"avatar_url" validate:"omitempty,url"`
|
|
}
|
|
|
|
return app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
|
|
// Get user from auth context
|
|
user, ok := auth.GetUser(r.Context())
|
|
if !ok {
|
|
return httperror.Unauthorized("not authenticated")
|
|
}
|
|
|
|
// Bind and validate request body
|
|
var req request
|
|
if err := app.BindAndValidate(r, &req); err != nil {
|
|
return err // Already an HTTPError with validation details
|
|
}
|
|
|
|
// Update profile
|
|
profile, err := h.svc.Update(r.Context(), user.ID, req.Name, req.AvatarURL)
|
|
if err != nil {
|
|
return httperror.WrapError(err, "failed to update profile")
|
|
}
|
|
|
|
httpresponse.JSON(w, http.StatusOK, profile)
|
|
return nil
|
|
})
|
|
}
|
|
```
|
|
|
|
### 2.3 Register Routes
|
|
|
|
```go
|
|
// services/api/internal/api/routes.go
|
|
func (s *Server) RegisterRoutes() {
|
|
userHandler := handlers.NewUserHandler(s.userService, s.logger)
|
|
|
|
// Protected routes (require auth)
|
|
s.router.Group(func(r chi.Router) {
|
|
r.Use(auth.Middleware(s.authValidator))
|
|
|
|
r.Get("/api/users/me", userHandler.GetMe())
|
|
r.Put("/api/users/me", userHandler.UpdateMe())
|
|
})
|
|
}
|
|
```
|
|
|
|
### 2.4 Add OpenAPI Annotations
|
|
|
|
```go
|
|
// services/api/cmd/server/main.go
|
|
func buildOpenAPISpec() *api.OpenAPISpec {
|
|
spec := api.NewOpenAPISpec("My Project API", "1.0.0", "/api")
|
|
|
|
// Security schemes
|
|
spec.WithBearerSecurity("bearerAuth", "JWT Bearer token")
|
|
spec.WithGlobalSecurity("bearerAuth")
|
|
|
|
// User profile endpoints
|
|
spec.Op("GET", "/users/me", "Get current user profile",
|
|
api.OpResponses(
|
|
api.OpResponse(200, "User profile", api.Ref("User")),
|
|
api.OpStandardResponses()...,
|
|
),
|
|
)
|
|
|
|
spec.OpWithBody("PUT", "/users/me", "Update current user profile",
|
|
api.Object(map[string]*api.Schema{
|
|
"name": api.String().Description("Display name"),
|
|
"avatar_url": api.String().Format("uri").Description("Avatar URL"),
|
|
}, "name"),
|
|
api.OpResponses(
|
|
api.OpResponse(200, "Updated user profile", api.Ref("User")),
|
|
api.OpStandardResponses()...,
|
|
),
|
|
)
|
|
|
|
// Schema definitions
|
|
spec.Schema("User", api.Object(map[string]*api.Schema{
|
|
"id": api.UUID(),
|
|
"email": api.Email(),
|
|
"name": api.String(),
|
|
"avatar_url": api.String().Format("uri"),
|
|
"created_at": api.DateTime(),
|
|
"updated_at": api.DateTime(),
|
|
}))
|
|
|
|
return spec
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Step 3: Frontend Development
|
|
|
|
### 3.1 Generate TypeScript Client
|
|
|
|
After adding OpenAPI annotations, regenerate the client:
|
|
|
|
```bash
|
|
./scripts/generate-client.sh
|
|
```
|
|
|
|
This updates `packages/api-client/src/schema.d.ts` with typed endpoints.
|
|
|
|
### 3.2 Create Profile Page
|
|
|
|
```tsx
|
|
// apps/dashboard/src/app/(dashboard)/profile/page.tsx
|
|
import { ProfileForm } from '@/components/profile-form';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@my-project/ui';
|
|
|
|
export default function ProfilePage() {
|
|
return (
|
|
<div className="p-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Profile Settings</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ProfileForm />
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 3.3 Create Profile Form with Auth Hook
|
|
|
|
```tsx
|
|
// apps/dashboard/src/components/profile-form.tsx
|
|
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useAuth } from '@my-project/auth';
|
|
import { Button, Input, Label } from '@my-project/ui';
|
|
import { updateProfile } from '@/actions/profile';
|
|
|
|
export function ProfileForm() {
|
|
const { user, refreshUser } = useAuth();
|
|
const [name, setName] = useState(user?.name ?? '');
|
|
const [avatarUrl, setAvatarUrl] = useState(user?.avatar_url ?? '');
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
if (!user) {
|
|
return <div>Loading...</div>;
|
|
}
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setIsSubmitting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const result = await updateProfile({ name, avatar_url: avatarUrl });
|
|
if (result.success) {
|
|
await refreshUser();
|
|
} else {
|
|
setError(result.error ?? 'Failed to update profile');
|
|
}
|
|
} catch (err) {
|
|
setError('An unexpected error occurred');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">Email</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
value={user.email}
|
|
disabled
|
|
className="bg-surface-50"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="name">Display Name</Label>
|
|
<Input
|
|
id="name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Enter your name"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="avatar">Avatar URL</Label>
|
|
<Input
|
|
id="avatar"
|
|
type="url"
|
|
value={avatarUrl}
|
|
onChange={(e) => setAvatarUrl(e.target.value)}
|
|
placeholder="https://example.com/avatar.png"
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="text-sm text-error">{error}</div>
|
|
)}
|
|
|
|
<Button type="submit" disabled={isSubmitting}>
|
|
{isSubmitting ? 'Saving...' : 'Save Changes'}
|
|
</Button>
|
|
</form>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 3.4 Create Server Action
|
|
|
|
```typescript
|
|
// apps/dashboard/src/actions/profile.ts
|
|
'use server';
|
|
|
|
import { revalidatePath } from 'next/cache';
|
|
import { cookies } from 'next/headers';
|
|
|
|
interface UpdateProfileInput {
|
|
name: string;
|
|
avatar_url?: string;
|
|
}
|
|
|
|
interface UpdateProfileResult {
|
|
success: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
export async function updateProfile(input: UpdateProfileInput): Promise<UpdateProfileResult> {
|
|
const token = cookies().get('auth_token')?.value;
|
|
|
|
if (!token) {
|
|
return { success: false, error: 'Not authenticated' };
|
|
}
|
|
|
|
const response = await fetch(`${process.env.API_URL}/api/users/me`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify(input),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
return {
|
|
success: false,
|
|
error: error.error?.message ?? 'Failed to update profile',
|
|
};
|
|
}
|
|
|
|
revalidatePath('/profile');
|
|
return { success: true };
|
|
}
|
|
```
|
|
|
|
### 3.5 Add Navigation Link
|
|
|
|
```tsx
|
|
// apps/dashboard/src/components/sidebar-nav.tsx
|
|
import { User, Settings, Home } from 'lucide-react';
|
|
|
|
const navItems = [
|
|
{ label: 'Dashboard', href: '/dashboard', icon: Home },
|
|
{ label: 'Profile', href: '/profile', icon: User },
|
|
{ label: 'Settings', href: '/settings', icon: Settings },
|
|
];
|
|
```
|
|
|
|
---
|
|
|
|
## Step 4: Testing
|
|
|
|
### 4.1 Write Handler Tests
|
|
|
|
```go
|
|
// services/api/internal/api/handlers/user_test.go
|
|
package handlers_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"my-project/pkg/auth"
|
|
"my-project/services/api/internal/api/handlers"
|
|
"my-project/services/api/internal/service"
|
|
)
|
|
|
|
func TestUserHandler_GetMe(t *testing.T) {
|
|
svc := service.NewUserService(/* mock deps */)
|
|
handler := handlers.NewUserHandler(svc, slog.Default())
|
|
|
|
t.Run("returns user profile", func(t *testing.T) {
|
|
// Create request with auth context
|
|
req := httptest.NewRequest("GET", "/api/users/me", nil)
|
|
req = req.WithContext(auth.SetUser(req.Context(), &auth.User{
|
|
ID: "user-123",
|
|
Email: "test@example.com",
|
|
}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.GetMe().ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
json.Unmarshal(rr.Body.Bytes(), &resp)
|
|
|
|
data := resp["data"].(map[string]any)
|
|
if data["id"] != "user-123" {
|
|
t.Errorf("expected user-123, got %v", data["id"])
|
|
}
|
|
})
|
|
|
|
t.Run("returns 401 without auth", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/users/me", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler.GetMe().ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected 401, got %d", rr.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUserHandler_UpdateMe(t *testing.T) {
|
|
svc := service.NewUserService(/* mock deps */)
|
|
handler := handlers.NewUserHandler(svc, slog.Default())
|
|
|
|
t.Run("updates profile", func(t *testing.T) {
|
|
body := `{"name": "New Name", "avatar_url": "https://example.com/avatar.png"}`
|
|
req := httptest.NewRequest("PUT", "/api/users/me", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req = req.WithContext(auth.SetUser(req.Context(), &auth.User{
|
|
ID: "user-123",
|
|
}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.UpdateMe().ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("validates required name", func(t *testing.T) {
|
|
body := `{"name": ""}`
|
|
req := httptest.NewRequest("PUT", "/api/users/me", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req = req.WithContext(auth.SetUser(req.Context(), &auth.User{
|
|
ID: "user-123",
|
|
}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.UpdateMe().ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400, got %d", rr.Code)
|
|
}
|
|
})
|
|
}
|
|
```
|
|
|
|
### 4.2 Run Tests
|
|
|
|
```bash
|
|
# Run all tests
|
|
./scripts/quality.sh
|
|
|
|
# Run specific service tests
|
|
go test ./services/api/...
|
|
|
|
# Run with coverage
|
|
go test -cover ./services/api/...
|
|
```
|
|
|
|
---
|
|
|
|
## Step 5: Review & Quality Checks
|
|
|
|
### 5.1 Run Code Review
|
|
|
|
Use the built-in review command:
|
|
|
|
```bash
|
|
# In Claude Code
|
|
/review-code
|
|
```
|
|
|
|
This checks for:
|
|
- Completeness and accuracy
|
|
- Tech debt indicators
|
|
- Maintainability issues
|
|
- Extensibility concerns
|
|
- DRY/CLEAN violations
|
|
|
|
### 5.2 Run Quality Checks
|
|
|
|
```bash
|
|
# Lint Go code
|
|
golangci-lint run ./...
|
|
|
|
# Lint frontend
|
|
cd apps/dashboard && npm run lint
|
|
|
|
# Type check
|
|
cd apps/dashboard && npm run typecheck
|
|
```
|
|
|
|
### 5.3 Verify OpenAPI Spec
|
|
|
|
```bash
|
|
# Start the service locally
|
|
cd services/api && make run
|
|
|
|
# Check the spec
|
|
curl http://localhost:8001/openapi.json | jq '.paths["/users/me"]'
|
|
|
|
# View docs
|
|
open http://localhost:8001/docs
|
|
```
|
|
|
|
---
|
|
|
|
## Step 6: Deployment
|
|
|
|
### 6.1 Commit Changes
|
|
|
|
```bash
|
|
# Stage changes
|
|
git add services/api/internal/api/handlers/user.go
|
|
git add services/api/internal/api/routes.go
|
|
git add apps/dashboard/src/app/\(dashboard\)/profile/
|
|
git add apps/dashboard/src/components/profile-form.tsx
|
|
git add apps/dashboard/src/actions/profile.ts
|
|
|
|
# Commit
|
|
git commit -m "feat: add user profile feature
|
|
|
|
- Add GET/PUT /api/users/me endpoints with Wrap pattern
|
|
- Add profile page with edit form
|
|
- Add server action for profile updates
|
|
- Add handler tests"
|
|
```
|
|
|
|
### 6.2 Push and Monitor CI
|
|
|
|
```bash
|
|
# Push feature branch
|
|
git push -u origin feature/user-profile
|
|
|
|
# Monitor pipeline
|
|
./cookbooks/scripts/feature-test.sh status my-project
|
|
```
|
|
|
|
### 6.3 Create Pull Request
|
|
|
|
```bash
|
|
gh pr create --title "feat: add user profile feature" --body "
|
|
## Summary
|
|
- Added user profile endpoints (GET/PUT /api/users/me)
|
|
- Added profile page with edit form
|
|
- Integrated with auth system
|
|
|
|
## Testing
|
|
- Handler tests added
|
|
- Manual testing complete
|
|
|
|
## Checklist
|
|
- [x] Handler tests pass
|
|
- [x] Quality checks pass
|
|
- [x] OpenAPI spec updated
|
|
"
|
|
```
|
|
|
|
### 6.4 Verify Deployment
|
|
|
|
After merge to main:
|
|
|
|
```bash
|
|
# Check deployment
|
|
curl https://my-project.threesix.ai/api/users/me \
|
|
-H "Authorization: Bearer $TOKEN" | jq .
|
|
|
|
# Open the app
|
|
open https://my-project.threesix.ai/profile
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern Quick Reference
|
|
|
|
### Wrap Pattern
|
|
```go
|
|
app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
|
|
// Return errors, they become proper HTTP responses
|
|
if err := doThing(); err != nil {
|
|
return httperror.WrapError(err, "failed to do thing")
|
|
}
|
|
httpresponse.JSON(w, http.StatusOK, result)
|
|
return nil
|
|
})
|
|
```
|
|
|
|
### Bind Pattern
|
|
```go
|
|
var req RequestType
|
|
if err := app.BindAndValidate(r, &req); err != nil {
|
|
return err // Validation errors become 400 with details
|
|
}
|
|
```
|
|
|
|
### HTTPError Sentinels
|
|
```go
|
|
httperror.BadRequest("invalid input")
|
|
httperror.NotFound("user not found")
|
|
httperror.Unauthorized("not authenticated")
|
|
httperror.Forbidden("access denied")
|
|
httperror.Conflict("already exists")
|
|
httperror.Internal("something went wrong")
|
|
httperror.Validation("validation failed")
|
|
```
|
|
|
|
### Auth Context
|
|
```go
|
|
user, ok := auth.GetUser(r.Context())
|
|
if !ok {
|
|
return httperror.Unauthorized("not authenticated")
|
|
}
|
|
```
|
|
|
|
### React Auth Hook
|
|
```tsx
|
|
const { user, isLoading, login, logout } = useAuth();
|
|
```
|
|
|
|
### Design System Components
|
|
```tsx
|
|
import { Button, Card, Input, Label, Badge } from '@my-project/ui';
|
|
import { DashboardShell, Sidebar, Header } from '@my-project/layout';
|
|
```
|
|
|
|
---
|
|
|
|
## E2E Test Scripts
|
|
|
|
### Feature Development (Claude + SDLC)
|
|
|
|
Test the complete feature development flow with Claude Code:
|
|
|
|
```bash
|
|
# Run full SDLC feature development flow
|
|
./cookbooks/scripts/feature-dev-test.sh run test-feature
|
|
|
|
# Check status
|
|
./cookbooks/scripts/feature-dev-test.sh status test-feature
|
|
|
|
# Cleanup
|
|
./cookbooks/scripts/feature-dev-test.sh teardown test-feature
|
|
```
|
|
|
|
### Scaffold Validation
|
|
|
|
Test that project scaffolding creates correct patterns:
|
|
|
|
```bash
|
|
# Create project and verify patterns
|
|
./cookbooks/scripts/scaffold-test.sh run test-scaffold
|
|
|
|
# Verify specific patterns
|
|
./cookbooks/scripts/scaffold-test.sh verify-patterns test-scaffold
|
|
|
|
# Cleanup
|
|
./cookbooks/scripts/scaffold-test.sh teardown test-scaffold
|
|
```
|
|
|
|
---
|
|
|
|
## Manual Troubleshooting
|
|
|
|
### Handler returns 500 instead of proper error
|
|
Check that you're using `httperror.*` functions, not raw `errors.New()`.
|
|
|
|
### Validation errors not showing details
|
|
Ensure you're using `app.BindAndValidate()` which includes validation details.
|
|
|
|
### Auth middleware not applying
|
|
Verify route is inside the `r.Group()` with `auth.Middleware()`.
|
|
|
|
### Generated client out of sync
|
|
Run `./scripts/generate-client.sh` after OpenAPI changes.
|
|
|
|
### Frontend not seeing auth user
|
|
Check that `AuthProvider` wraps your app in `providers.tsx`.
|
|
|
|
---
|
|
|
|
## Chassis Framework
|
|
|
|
The `pkg/chassis` package provides a convenience facade over `pkg/app`:
|
|
|
|
```go
|
|
import "my-project/pkg/chassis"
|
|
|
|
svc := chassis.New("my-service", chassis.WithDefaultPort(8080))
|
|
```
|
|
|
|
It re-exports: `New`, `Wrap`, `WrapWithLogger`, `Bind`, `BindAndValidate`, `BindStrict`, `NewHealthHandler`, `PingChecker`, `HTTPChecker`.
|
|
|
|
## OpenAPI Documentation
|
|
|
|
Each service defines its spec in `internal/api/spec.go`:
|
|
|
|
```go
|
|
spec := openapi.NewOpenAPISpec("My API", "1.0.0").
|
|
WithBearerSecurity("bearer", "JWT token")
|
|
|
|
spec.WithSchema("User", openapi.Object(map[string]openapi.Schema{
|
|
"id": openapi.UUID(),
|
|
"email": openapi.Email(),
|
|
}, "id", "email"))
|
|
|
|
spec.AddPath("/api/v1/users/{id}", "get", map[string]any{
|
|
"summary": "Get user",
|
|
"tags": []string{"Users"},
|
|
"parameters": []any{openapi.IDParam()},
|
|
"responses": map[string]any{
|
|
"200": openapi.OpResponse("Success", openapi.ResponseSchema(openapi.Ref("User"))),
|
|
"404": openapi.OpResponse("Not found", openapi.ErrorResponseSchema()),
|
|
},
|
|
})
|
|
|
|
application.EnableDocs(spec) // Mounts /docs (Scalar UI) and /openapi.json
|
|
```
|
|
|
|
## Design System Components
|
|
|
|
Available from `@project/ui`:
|
|
|
|
| Component | Usage |
|
|
|-----------|-------|
|
|
| Button | Primary actions, variants: default, destructive, outline, ghost |
|
|
| Card | Content containers with CardHeader, CardContent, CardFooter |
|
|
| Input, Label | Form fields |
|
|
| Badge | Status indicators, variants: success, warning, error, info |
|
|
| Dialog | Modal dialogs |
|
|
| Table | Data tables |
|
|
| Select | Dropdowns |
|
|
| Alert | Notification banners, variants: default, destructive, success, warning |
|
|
| Textarea | Multiline input |
|
|
| DropdownMenu | Context menus with items, checkboxes, radio groups |
|
|
| Sheet | Slide-in panels (side: top, right, bottom, left) |
|
|
|
|
All use CSS custom properties: `var(--background)`, `var(--accent)`, `var(--border)`, etc.
|
|
|
|
---
|
|
|
|
## SDLC Troubleshooting
|
|
|
|
### Build returns "completed" but no artifacts created
|
|
|
|
The skeleton command may have failed silently. Check the build output:
|
|
|
|
```bash
|
|
curl "$RDEV_API_URL/builds/<task-id>" -H "X-API-Key: $RDEV_API_KEY" | jq '.data.result'
|
|
```
|
|
|
|
Common causes:
|
|
- Feature doesn't exist (create it first via `/sdlc/features`)
|
|
- Previous artifacts missing (spec must exist before design)
|
|
- SDLC CLI not installed in pod
|
|
|
|
### Classifier returns AWAIT_ARTIFACT but artifact exists
|
|
|
|
The artifact may exist in the file system but not registered with SDLC:
|
|
|
|
```bash
|
|
# Register artifact manually
|
|
curl -X POST "$RDEV_API_URL/projects/$PROJECT/builds" \
|
|
-H "X-API-Key: $RDEV_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"prompt": "sdlc artifact create <slug> <type>", "auto_commit": true, "auto_push": true, "git_clone_url": "<url>"}'
|
|
```
|
|
|
|
### Task implementation doesn't commit changes
|
|
|
|
Check that `auto_commit` and `auto_push` are set to `true`, and `git_clone_url` is provided:
|
|
|
|
```bash
|
|
curl -X POST "$RDEV_API_URL/projects/$PROJECT/builds" \
|
|
-H "X-API-Key: $RDEV_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"prompt": "/implement-task <slug> <task-id>",
|
|
"auto_commit": true,
|
|
"auto_push": true,
|
|
"git_clone_url": "https://git.threesix.ai/jordan/<project>.git"
|
|
}'
|
|
```
|
|
|
|
### Build times out
|
|
|
|
Builds have a default timeout of 10 minutes. Complex implementations may need longer. Check the build status periodically:
|
|
|
|
```bash
|
|
# Poll status
|
|
while true; do
|
|
status=$(curl -s "$RDEV_API_URL/builds/<task-id>" -H "X-API-Key: $RDEV_API_KEY" | jq -r '.data.status // .status')
|
|
echo "Status: $status"
|
|
[[ "$status" == "completed" || "$status" == "failed" ]] && break
|
|
sleep 10
|
|
done
|
|
```
|
|
|
|
### Feature stuck in wrong phase
|
|
|
|
Check the classifier recommendation to understand what's blocking:
|
|
|
|
```bash
|
|
curl "$RDEV_API_URL/projects/$PROJECT/sdlc/next?feature=<slug>" \
|
|
-H "X-API-Key: $RDEV_API_KEY" | jq '.'
|
|
```
|
|
|
|
If needed, you can manually advance the phase by approving the pending artifact.
|
|
|
|
---
|
|
|
|
## Related
|
|
|
|
- [Composable App Cookbook](./composable-app.md) - Creating projects with components
|
|
- [Landing Page Cookbook](./landing-page.md) - Simple single-component sites
|
|
- [Composable Monorepo Guide](../.claude/guides/services/composable-monorepo.md)
|
|
- [API Framework Guide](../.claude/guides/packages/api-framework.md)
|
|
- [SDLC Guide](../.claude/guides/services/sdlc.md) - Full SDLC orchestration documentation
|