Phase 02: Blueprint Creation
| INPUT | PROCESSING | OUTPUT | ||
|
⮕ |
|
⮕ |
|
Overview
Duc (Architecture agent) creates a technical blueprint from the validated WRD. This is Phase 2 of the 13-phase TDD workflow.
This phase: 1. Analyzes requirements from Phase 1 (WRD Intake) 2. Designs system architecture 3. Defines components, files, APIs, and data models 4. Undergoes two-layer validation (deterministic + Baron LLM)
Note: Duc uses read-only analysis tools (
Read,Glob,Grep) to understand the existing codebase before designing.
Assignment
| Issue Type | Assignee | Reason |
|---|---|---|
| Feature | Duc | Architect designs system changes |
| Tech | Duc | Architect understands technical changes |
| Bug | Duc | Architect can trace through codebase |
Document Format
All blueprints MUST be valid YAML files.
| Requirement | Description |
|---|---|
| Format | YAML 1.2 |
| Extension | .yaml |
| Naming | BP-{NNN}-{domain}-v{N}.yaml |
| Encoding | UTF-8 |
| Validation | Baron validates against domain-specific JSON Schema |
Examples:
- BP-001-backend-v1.yaml
- BP-002-frontend-v3.yaml
- BP-003-devops-v1.yaml
Blueprint Fundamentals
What is a Blueprint?
A blueprint is a self-contained YAML specification for a single domain. It tells the implementing agent:
- WHAT artifact to produce
- WHERE it lives (infra, service, domain, file path)
- WHY (acceptance criteria)
- INTERFACES (inputs, outputs, contracts)
A blueprint does NOT specify HOW to implement - that's the implementer's job.
Language Principle: Speak the Implementer's Language
Issue documents use business language. Blueprints use technical language.
The blueprint bridges business requirements to implementation. Duc must write artifact descriptions using the vocabulary of the target technology stack so the implementer knows exactly what to create.
| Domain | Technology | Blueprint Language Examples |
|---|---|---|
| Backend | Python/FastAPI | "Create a FastAPI APIRouter", "Define a Pydantic BaseModel", "Add an async def endpoint" |
| Backend | Node/Express | "Create an Express Router", "Define a Zod schema", "Add a middleware function" |
| Backend | Go/Gin | "Create a Gin RouterGroup", "Define a struct with JSON tags", "Add a handler function" |
| Frontend | React/TypeScript | "Create a React functional component", "Create a custom hook using useState", "Define a TypeScript interface" |
| Frontend | Vue | "Create a Vue SFC (Single File Component)", "Add a composable using ref and computed" |
| DevOps | Kubernetes | "Update the Deployment manifest", "Add an Ingress rule", "Create a ConfigMap" |
| DevOps | Terraform | "Add an aws_iam_policy resource", "Create a module for the service" |
| Tests | pytest | "Create a pytest test module", "Add a @pytest.fixture", "Use pytest.mark.parametrize" |
| Tests | Jest | "Create a Jest test suite with describe", "Add a beforeEach setup", "Mock using jest.fn()" |
| Tests | Playwright | "Create a Playwright test spec", "Add a test.beforeEach hook", "Use page.locator()" |
Bad (too vague):
- path: src/domains/refunds/service.py
description: Refund business logic
Good (technology-specific):
- path: src/domains/refunds/service.py
description: |
Create a RefundService class with:
- async def create_refund(order_id, amount_cents) -> RefundResult
- async def get_refund(refund_id) -> Refund | None
- Inject StripeClient and OrderRepository via __init__
One Blueprint per Domain
Each blueprint targets exactly one domain owner:
| Domain | Owner Agent | Role | Stack Examples |
|---|---|---|---|
tests |
Marie | QA Engineer - writes tests FIRST | pytest, Jest, Playwright |
backend |
Dede | Backend Developer | Python/FastAPI, Node/Express, Go/Gin |
frontend |
Dali | Frontend Developer | React, Vue, Angular, Svelte |
infra |
Gus | DevOps / GitOps | Kubernetes, Terraform, Helm, GitHub Actions |
sre |
Maigret | SRE - journey-level observability | Prometheus, Grafana, OpenTelemetry |
Blueprint Versioning
Blueprints are versioned in the architect's sub-issue:
ISSUE-123/sub-issues/duc/
├── BP-001-tests-v1.yaml # Initial draft
├── BP-001-tests-v2.yaml # After Baron's feedback
├── BP-001-tests-v3.yaml # Approved ✓
├── BP-002-backend-v1.yaml # Approved ✓
├── BP-003-frontend-v1.yaml # Approved ✓
├── BP-004-infra-v1.yaml # Approved ✓
└── BP-005-sre-v1.yaml # Approved ✓
Only the approved version gets copied to the domain agent's sub-issue.
Blueprint Schemas
Common Header (All Blueprints)
blueprint_id: BP-{NNN} # Unique within issue
version: v{N} # Increments on revision
issue_id: ISSUE-{ID} # Parent issue reference
owner: string # Domain: backend | frontend | devops | tests | security | sre | finops
status: draft | review | approved | rejected
created_at: datetime
updated_at: datetime
# Technology context - helps implementer understand the stack
stack:
language: string # python | typescript | go | etc.
framework: string # fastapi | express | react | vue | etc.
version: string # 3.11 | 18.2 | 1.21 | etc.
Backend Blueprint
For API endpoints, services, business logic, data models.
Schema
blueprint_id: BP-{NNN}
version: v{N}
issue_id: ISSUE-{ID}
owner: backend
status: draft | review | approved | rejected
# STACK
stack:
language: python | typescript | go | java | etc.
framework: fastapi | express | gin | spring | etc.
version: string
# WHERE - Location context
service:
name: string # Service name
status: existing | new # Is this service new?
repo: string # Repository URL
domain:
name: string # Domain/module name
status: existing | new # Is this domain new?
path: string # Path within repo (e.g., src/domains/payments/)
# WHAT - Artifact definition (use framework-specific language!)
artifacts:
- path: string # Exact file path
type: create | modify | delete
description: string # Technical description using framework vocabulary
# INTERFACES - Contracts
interfaces:
- type: rest | grpc | graphql | event | internal
method: string # GET, POST, etc. (for REST)
endpoint: string # /api/v1/users/{id}
request:
headers: object | null
params: object | null
body: object | null
response:
success: object
errors: [object]
# WHY - Acceptance criteria
acceptance_criteria:
- string
- string
# DEPENDENCIES
dependencies:
blueprints: [string] # Other blueprint IDs this depends on
services: [string] # External service dependencies
packages: [string] # Required packages with versions
Example: Payment Refund Endpoint (Python/FastAPI)
blueprint_id: BP-001
version: v1
issue_id: ISSUE-123
owner: backend
status: draft
stack:
language: python
framework: fastapi
version: "0.109"
service:
name: payment-service
status: existing
repo: github.com/acme/payment-service
domain:
name: refunds
status: new
path: src/domains/refunds/
artifacts:
- path: src/domains/refunds/__init__.py
type: create
description: |
Python package init. Export public API:
- from .service import RefundService
- from .models import RefundRequest, RefundResult, RefundStatus
- from .router import router
- path: src/domains/refunds/models.py
type: create
description: |
Define Pydantic models:
- class RefundRequest(BaseModel): order_id (str), amount_cents (int), reason (str | None)
- class RefundResult(BaseModel): refund_id (str), order_id (str), amount_cents (int), status (RefundStatus), created_at (datetime)
- class RefundStatus(str, Enum): PENDING, COMPLETED, FAILED
Add field validators:
- amount_cents must be > 0
- order_id must match pattern ^ord_[a-zA-Z0-9]+$
- path: src/domains/refunds/service.py
type: create
description: |
Create RefundService class:
- __init__(self, stripe_client: StripeClient, order_repo: OrderRepository, refund_repo: RefundRepository)
- async def create_refund(self, request: RefundRequest, idempotency_key: str) -> RefundResult
- Validate order exists via order_repo.get_by_id()
- Validate amount <= order.amount_cents
- Check idempotency via refund_repo.get_by_idempotency_key()
- Call stripe_client.refunds.create() with Stripe idempotency key
- Persist refund via refund_repo.create()
- Publish RefundCompletedEvent via event bus
- async def get_refund(self, refund_id: str) -> Refund | None
- path: src/domains/refunds/router.py
type: create
description: |
Create FastAPI APIRouter with prefix="/api/v1/refunds":
- @router.post("/", response_model=RefundResult, status_code=201)
async def create_refund(request: RefundRequest, idempotency_key: str = Header(...), service: RefundService = Depends(get_refund_service))
- Use HTTPException for error responses with detail dict containing 'code' and 'message'
- Add OpenAPI tags=["refunds"] and summary/description for docs
- path: src/domains/refunds/repository.py
type: create
description: |
Create RefundRepository class:
- __init__(self, db: AsyncSession)
- async def create(self, refund: Refund) -> Refund
- async def get_by_id(self, refund_id: str) -> Refund | None
- async def get_by_idempotency_key(self, key: str) -> Refund | None
Use SQLAlchemy async queries with select() and insert()
- path: src/domains/refunds/events.py
type: create
description: |
Define event schemas:
- class RefundCompletedEvent(BaseEvent):
topic = "payments.refund.completed"
refund_id: str
order_id: str
amount_cents: int
completed_at: datetime
- path: src/api/main.py
type: modify
description: |
Add to existing FastAPI app:
- from src.domains.refunds import router as refunds_router
- app.include_router(refunds_router)
interfaces:
- type: rest
method: POST
endpoint: /api/v1/refunds
request:
headers:
Authorization: Bearer {token}
Idempotency-Key: string (required, UUID format)
body:
order_id: string (required, pattern: ^ord_[a-zA-Z0-9]+$)
amount_cents: integer (required, min: 1, max: order.amount_cents)
reason: string (optional, max: 500 chars)
response:
success:
status: 201
body:
refund_id: string
order_id: string
amount_cents: integer
status: "pending" | "completed" | "failed"
created_at: datetime (ISO 8601)
errors:
- status: 400
body:
code: "INVALID_AMOUNT"
message: "Refund amount exceeds original order amount"
- status: 404
body:
code: "ORDER_NOT_FOUND"
message: "Order does not exist"
- status: 409
body:
code: "DUPLICATE_REQUEST"
message: "Refund already processed for this idempotency key"
refund: { existing refund object }
- status: 422
body:
code: "ORDER_NOT_REFUNDABLE"
message: "Order status does not allow refunds"
- type: event
method: publish
endpoint: payments.refund.completed
request:
body:
refund_id: string
order_id: string
amount_cents: integer
completed_at: datetime
acceptance_criteria:
- Refund endpoint validates order exists and belongs to authenticated user
- Refund amount cannot exceed original order amount
- Idempotency key prevents duplicate refunds (returns existing refund with 409)
- Refund integrates with Stripe Refund API
- Event published on successful refund for downstream consumers
- All errors return structured JSON with error code and message
dependencies:
blueprints: []
services:
- stripe-api
- order-service (gRPC for order validation)
packages:
- stripe>=7.0.0
- pydantic>=2.0.0
- sqlalchemy[asyncio]>=2.0.0
Frontend Blueprint
For UI components, pages, client-side logic.
Schema
blueprint_id: BP-{NNN}
version: v{N}
issue_id: ISSUE-{ID}
owner: frontend
status: draft | review | approved | rejected
# STACK
stack:
language: typescript | javascript
framework: react | vue | angular | svelte
version: string
styling: tailwind | styled-components | css-modules | etc.
# WHERE
application:
name: string
status: existing | new
repo: string
component:
name: string
status: existing | new
path: string # e.g., src/components/RefundDialog/
# WHAT (use React/Vue/etc. vocabulary!)
artifacts:
- path: string
type: create | modify | delete
description: string
# INTERFACES - API contracts this component uses
api_contracts:
- endpoint: string
method: string
request: object
response: object
# UI SPECIFICATIONS
ui_specs:
wireframe: string | null # Link to design
states: # UI states to handle
- name: string
description: string
interactions: # User interactions
- trigger: string
action: string
# WHY
acceptance_criteria:
- string
# DEPENDENCIES
dependencies:
blueprints: [string]
components: [string] # Existing components to use
packages: [string]
Example: Refund Dialog Component (React/TypeScript)
blueprint_id: BP-002
version: v1
issue_id: ISSUE-123
owner: frontend
status: draft
stack:
language: typescript
framework: react
version: "18.2"
styling: tailwind
application:
name: merchant-dashboard
status: existing
repo: github.com/acme/merchant-dashboard
component:
name: RefundDialog
status: new
path: src/components/RefundDialog/
artifacts:
- path: src/components/RefundDialog/index.tsx
type: create
description: |
Create React functional component RefundDialog:
- Props interface: { orderId: string; maxAmount: number; isOpen: boolean; onClose: () => void; onSuccess: (refund: Refund) => void }
- Use useState for form state and dialog step (form | confirm | loading | success | error)
- Use Dialog component from @/components/ui/Dialog (Radix-based)
- Render RefundForm when step === 'form'
- Render RefundConfirmation when step === 'confirm' | 'success' | 'error'
- Handle onClose with dirty form warning using window.confirm()
- path: src/components/RefundDialog/RefundForm.tsx
type: create
description: |
Create React functional component RefundForm:
- Props interface: { maxAmount: number; onSubmit: (data: RefundFormData) => void; onCancel: () => void }
- Use react-hook-form with zodResolver for form handling
- Define Zod schema: z.object({ amount: z.number().min(1).max(maxAmount), reason: z.string().optional() })
- Render:
- Input type="number" for amount with {...register('amount')}
- Show max refundable amount as helper text
- Textarea for optional reason
- Button type="submit" "Continue to Review"
- Button type="button" onClick={onCancel} "Cancel"
- Display validation errors from formState.errors
- path: src/components/RefundDialog/RefundConfirmation.tsx
type: create
description: |
Create React functional component RefundConfirmation:
- Props interface: {
step: 'confirm' | 'loading' | 'success' | 'error';
amount: number;
refund?: Refund;
error?: string;
onConfirm: () => void;
onRetry: () => void;
onClose: () => void;
}
- Conditional rendering based on step:
- 'confirm': Show amount summary, "Confirm Refund" button, "Go Back" button
- 'loading': Show Spinner component with "Processing refund..." text
- 'success': Show CheckCircle icon, refund.refund_id, amount, auto-close after 3s using useEffect
- 'error': Show XCircle icon, error message, "Try Again" button
- path: src/components/RefundDialog/useRefund.ts
type: create
description: |
Create custom hook useRefund:
- Signature: (orderId: string) => { mutate, isLoading, error, data }
- Use useMutation from @tanstack/react-query
- mutationFn: async (data: { amount_cents: number; reason?: string }) => {
const response = await fetch('/api/v1/refunds', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID()
},
body: JSON.stringify({ order_id: orderId, ...data })
});
if (!response.ok) throw await response.json();
return response.json();
}
- On success: invalidate ['orders', orderId] query
- path: src/components/RefundDialog/types.ts
type: create
description: |
Define TypeScript interfaces:
- interface RefundFormData { amount: number; reason?: string }
- interface Refund { refund_id: string; order_id: string; amount_cents: number; status: 'pending' | 'completed' | 'failed'; created_at: string }
- interface RefundError { code: string; message: string }
- path: src/components/RefundDialog/RefundDialog.test.tsx
type: create
description: |
Create Jest + React Testing Library tests:
- describe('RefundDialog', () => { ... })
- test('renders form when open')
- test('validates amount does not exceed max')
- test('shows confirmation step after form submit')
- test('calls onSuccess with refund data after successful API call')
- test('shows error message on API failure')
- test('closes dialog on backdrop click with dirty form warning')
- Mock fetch using jest.fn()
- Use renderHook for testing useRefund
- path: src/pages/OrderDetail/index.tsx
type: modify
description: |
Add to existing OrderDetail component:
- Import RefundDialog from '@/components/RefundDialog'
- Add useState<boolean> for isRefundDialogOpen
- Add Button onClick={() => setIsRefundDialogOpen(true)} "Issue Refund"
- Only show if order.status === 'completed' && order.refundable_amount > 0
- Render <RefundDialog
orderId={order.id}
maxAmount={order.refundable_amount}
isOpen={isRefundDialogOpen}
onClose={() => setIsRefundDialogOpen(false)}
onSuccess={(refund) => { toast.success('Refund processed'); refetch(); }}
/>
api_contracts:
- endpoint: /api/v1/refunds
method: POST
request:
order_id: string
amount_cents: integer
reason: string | null
response:
refund_id: string
order_id: string
amount_cents: integer
status: "pending" | "completed" | "failed"
created_at: string
ui_specs:
wireframe: https://figma.com/file/xxx/refund-dialog
states:
- name: idle
description: Dialog closed, no state
- name: form
description: Dialog open showing amount input and reason textarea
- name: confirm
description: User clicked "Continue", showing confirmation with final amount
- name: loading
description: API request in progress, spinner visible, buttons disabled
- name: success
description: Refund completed, showing green checkmark and refund ID
- name: error
description: Refund failed, showing error message with retry option
interactions:
- trigger: Click "Issue Refund" button on order detail page
action: Open dialog in 'form' state
- trigger: Submit form with valid amount
action: Transition to 'confirm' state
- trigger: Click "Confirm Refund" in confirmation view
action: Transition to 'loading', call API, then 'success' or 'error'
- trigger: Click "Go Back" in confirmation view
action: Return to 'form' state with preserved values
- trigger: Click outside dialog or press Escape
action: If form dirty, show confirm("Discard changes?"). If confirmed or not dirty, close dialog.
- trigger: Success state shown for 3 seconds
action: Auto-close dialog and call onSuccess
acceptance_criteria:
- Dialog opens from order detail page when clicking "Issue Refund"
- Amount field validates: required, positive, max = order.refundable_amount
- Confirmation step shows amount before API call
- Loading state disables all buttons and shows spinner
- Success state shows refund ID and closes after 3s
- Error state shows error.message and allows retry
- Form state preserved when going back from confirmation
- Escape key and backdrop click close with dirty form warning
- Fully keyboard accessible (tab order, enter to submit)
- Screen reader announces dialog title and state changes
dependencies:
blueprints:
- BP-001 # Backend refund endpoint must exist
components:
- Dialog from @/components/ui/Dialog (Radix)
- Button from @/components/ui/Button
- Input from @/components/ui/Input
- Spinner from @/components/ui/Spinner
packages:
- react-hook-form@^7.0.0
- @hookform/resolvers@^3.0.0
- zod@^3.0.0
- @tanstack/react-query@^5.0.0
DevOps Blueprint
For infrastructure, deployment, CI/CD changes.
Schema
blueprint_id: BP-{NNN}
version: v{N}
issue_id: ISSUE-{ID}
owner: devops
status: draft | review | approved | rejected
# STACK
stack:
orchestration: kubernetes | ecs | docker-compose
iac: terraform | pulumi | cloudformation
ci: github-actions | gitlab-ci | jenkins
# WHERE - Infrastructure context
infrastructure:
provider: aws | gcp | azure
account: string
region: string
cluster: string | null # EKS, GKE, etc.
# WHAT - Deployment context
deployment:
namespace: string
service: string
type: update | new | delete
# ARTIFACTS (use K8s/Terraform vocabulary!)
artifacts:
- path: string
type: create | modify | delete
description: string
# REQUIREMENTS
requirements:
compute:
cpu: string
memory: string
replicas: integer
autoscaling: object | null
networking:
ingress: [object]
egress: [object]
storage:
volumes: [object]
secrets:
- name: string
source: string
# WHY
acceptance_criteria:
- string
# DEPENDENCIES
dependencies:
blueprints: [string]
infrastructure: [string]
Example: Payment Service Deployment Update (Kubernetes/Terraform)
blueprint_id: BP-003
version: v1
issue_id: ISSUE-123
owner: devops
status: draft
stack:
orchestration: kubernetes
iac: terraform
ci: github-actions
infrastructure:
provider: aws
account: "123456789012"
region: eu-west-1
cluster: eks-production
deployment:
namespace: payments
service: payment-service
type: update
artifacts:
- path: k8s/payment-service/deployment.yaml
type: modify
description: |
Modify existing Deployment resource:
- Add to spec.template.spec.containers[0].env:
- name: STRIPE_REFUND_ENABLED
value: "true"
- name: REFUND_MAX_AMOUNT_CENTS
valueFrom:
configMapKeyRef:
name: payment-service-config
key: refund.max_amount_cents
- Add to spec.template.spec.containers[0].envFrom:
- secretRef:
name: stripe-refund-secrets
- path: k8s/payment-service/configmap.yaml
type: modify
description: |
Add to existing ConfigMap data:
refund.max_amount_cents: "1000000" # $10,000 max
refund.rate_limit_per_minute: "10"
refund.idempotency_ttl_hours: "24"
- path: k8s/payment-service/ingress.yaml
type: modify
description: |
Add to existing Ingress spec.rules[0].http.paths:
- path: /api/v1/refunds
pathType: Prefix
backend:
service:
name: payment-service
port:
number: 8080
- path: k8s/payment-service/secrets.yaml
type: create
description: |
Create ExternalSecret resource (external-secrets-operator):
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: stripe-refund-secrets
namespace: payments
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: stripe-refund-secrets
data:
- secretKey: STRIPE_REFUND_API_KEY
remoteRef:
key: payments/stripe/refund-api-key
- path: terraform/modules/payment-service/iam.tf
type: modify
description: |
Add to existing aws_iam_policy.payment_service_secrets:
- In statement[0].resources, add:
"arn:aws:secretsmanager:eu-west-1:123456789012:secret:payments/stripe/refund-api-key-*"
- path: terraform/modules/payment-service/variables.tf
type: modify
description: |
Add variable:
variable "refund_enabled" {
description = "Enable refund functionality"
type = bool
default = false
}
- path: .github/workflows/deploy-payment-service.yaml
type: modify
description: |
Add to existing GitHub Actions workflow jobs.deploy.steps:
- name: Verify refund endpoint health
run: |
kubectl wait --for=condition=ready pod -l app=payment-service -n payments --timeout=120s
curl -f http://payment-service.payments.svc.cluster.local:8080/api/v1/refunds/health || exit 1
requirements:
compute:
cpu: "500m"
memory: "1Gi"
replicas: 3
autoscaling:
minReplicas: 3
maxReplicas: 10
targetCPUUtilization: 70
networking:
ingress:
- path: /api/v1/refunds
service: payment-service
port: 8080
annotations:
nginx.ingress.kubernetes.io/rate-limit: "10"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
egress:
- destination: api.stripe.com
port: 443
protocol: HTTPS
storage:
volumes: []
secrets:
- name: STRIPE_REFUND_API_KEY
source: aws-secrets-manager:payments/stripe/refund-api-key
acceptance_criteria:
- Endpoint /api/v1/refunds accessible via ingress
- ExternalSecret syncs Stripe key from AWS Secrets Manager
- IAM policy allows payment-service to read refund secrets
- Zero-downtime deployment via rolling update
- Health check endpoint responds within 120s of deploy
- Rollback possible via kubectl rollout undo
dependencies:
blueprints:
- BP-001 # Backend code must be merged first
infrastructure:
- eks-production cluster (existing)
- external-secrets-operator (existing)
- aws-secrets-manager ClusterSecretStore (existing)
- nginx-ingress-controller (existing)
Tests Blueprint
For test suites, QA automation.
Schema
blueprint_id: BP-{NNN}
version: v{N}
issue_id: ISSUE-{ID}
owner: tests
status: draft | review | approved | rejected
# STACK
stack:
language: python | typescript | etc.
test_framework: pytest | jest | playwright | k6
version: string
# SCOPE
test_scope:
service: string
domain: string
type: unit | integration | e2e | performance | security
# WHAT (use pytest/jest vocabulary!)
artifacts:
- path: string
type: create | modify | delete
description: string
# TEST SCENARIOS
scenarios:
- name: string
type: unit | integration | e2e
given: string
when: string
then: string
priority: critical | high | medium | low
# WHY
acceptance_criteria:
- string
# DEPENDENCIES
dependencies:
blueprints: [string]
fixtures: [string]
Example: Refund API Tests (Python/pytest)
blueprint_id: BP-004
version: v1
issue_id: ISSUE-123
owner: tests
status: draft
stack:
language: python
test_framework: pytest
version: "8.0"
test_scope:
service: payment-service
domain: refunds
type: integration
artifacts:
- path: tests/integration/refunds/test_refund_api.py
type: create
description: |
Create pytest test module with async tests:
- Use @pytest.mark.asyncio decorator on all tests
- Use httpx.AsyncClient for API calls
- Import fixtures from conftest.py
Test functions:
- async def test_create_refund_full_amount(client, completed_order, auth_headers):
response = await client.post("/api/v1/refunds", json={...}, headers={...})
assert response.status_code == 201
assert response.json()["status"] == "completed"
- async def test_create_refund_partial_amount(...)
- async def test_refund_exceeds_order_amount_returns_400(...)
- async def test_refund_nonexistent_order_returns_404(...)
- async def test_duplicate_idempotency_key_returns_existing_refund(...)
- async def test_refund_other_users_order_returns_403(...)
- async def test_refund_pending_order_returns_422(...)
- path: tests/integration/refunds/conftest.py
type: create
description: |
Create pytest fixtures:
@pytest.fixture
async def completed_order(db_session, test_user) -> Order:
"""Create a completed order with known amount for refund testing."""
order = Order(user_id=test_user.id, amount_cents=10000, status="completed")
db_session.add(order)
await db_session.commit()
return order
@pytest.fixture
async def pending_order(db_session, test_user) -> Order:
"""Create a pending order that cannot be refunded."""
...
@pytest.fixture
def stripe_mock(mocker) -> MagicMock:
"""Mock Stripe refund API calls."""
mock = mocker.patch("stripe.Refund.create")
mock.return_value = {"id": "re_test123", "status": "succeeded"}
return mock
@pytest.fixture
def idempotency_key() -> str:
return str(uuid.uuid4())
- path: tests/unit/domains/refunds/test_service.py
type: create
description: |
Create pytest unit tests for RefundService:
- class TestRefundService:
@pytest.fixture
def service(self, mock_stripe, mock_order_repo, mock_refund_repo):
return RefundService(mock_stripe, mock_order_repo, mock_refund_repo)
async def test_create_refund_validates_amount_not_exceeding_order(self, service):
...
async def test_create_refund_checks_idempotency_key(self, service):
...
async def test_create_refund_publishes_event_on_success(self, service, mock_event_bus):
...
- path: tests/fixtures/orders.py
type: modify
description: |
Add to existing fixtures module:
def refundable_order_factory(user_id: str, amount_cents: int = 10000) -> Order:
"""Factory for creating refundable orders in tests."""
return Order(
id=f"ord_{uuid.uuid4().hex[:12]}",
user_id=user_id,
amount_cents=amount_cents,
status="completed",
created_at=datetime.utcnow()
)
scenarios:
- name: Successful full refund
type: integration
given: A completed order exists with amount_cents=10000
when: POST /api/v1/refunds with amount_cents=10000
then: Returns 201 with refund_id and status="completed"
priority: critical
- name: Successful partial refund
type: integration
given: A completed order exists with amount_cents=10000
when: POST /api/v1/refunds with amount_cents=3000
then: Returns 201 with refund_id and amount_cents=3000
priority: critical
- name: Refund exceeds order amount
type: integration
given: A completed order exists with amount_cents=10000
when: POST /api/v1/refunds with amount_cents=15000
then: Returns 400 with code="INVALID_AMOUNT"
priority: high
- name: Refund for non-existent order
type: integration
given: No order with id="ord_nonexistent" exists
when: POST /api/v1/refunds with order_id="ord_nonexistent"
then: Returns 404 with code="ORDER_NOT_FOUND"
priority: high
- name: Duplicate refund via idempotency key
type: integration
given: A refund already exists with idempotency_key="idem_123"
when: POST /api/v1/refunds with same idempotency_key
then: Returns 200 (not 201) with existing refund object
priority: critical
- name: Refund other user's order
type: integration
given: Order belongs to user_id="user_other"
when: POST /api/v1/refunds authenticated as user_id="user_me"
then: Returns 403 with code="FORBIDDEN"
priority: high
- name: Refund non-refundable order
type: integration
given: Order exists with status="pending"
when: POST /api/v1/refunds
then: Returns 422 with code="ORDER_NOT_REFUNDABLE"
priority: medium
acceptance_criteria:
- All 7 scenarios have passing pytest tests
- Integration tests use testcontainers for PostgreSQL
- Stripe API mocked using pytest-mock
- pytest-cov reports >= 80% coverage for src/domains/refunds/
- Tests run in CI via pytest -v --cov=src/domains/refunds tests/
- All tests use @pytest.mark.asyncio for async support
dependencies:
blueprints:
- BP-001 # Backend implementation must exist to test
fixtures:
- Test database with order table
- Test user with authentication token
- Stripe mock responses for success/failure
Security Blueprint
For authentication, authorization, security hardening.
Example: Refund Endpoint Security (Python/FastAPI)
blueprint_id: BP-005
version: v1
issue_id: ISSUE-123
owner: security
status: draft
stack:
language: python
framework: fastapi
auth: jwt
scope:
service: payment-service
domain: refunds
artifacts:
- path: src/domains/refunds/permissions.py
type: create
description: |
Define permission constants and checks:
class RefundPermissions(str, Enum):
CREATE = "refund:create"
READ = "refund:read"
APPROVE = "refund:approve"
def can_refund_order(user: User, order: Order) -> bool:
"""Check if user can refund this specific order."""
if user.has_role("admin"):
return True
if user.has_role("merchant") and order.merchant_id == user.merchant_id:
return True
return False
- path: src/domains/refunds/dependencies.py
type: create
description: |
Create FastAPI dependencies for auth:
async def require_refund_permission(
current_user: User = Depends(get_current_user),
order: Order = Depends(get_order_from_request)
) -> User:
if not can_refund_order(current_user, order):
raise HTTPException(status_code=403, detail={"code": "FORBIDDEN", "message": "Cannot refund this order"})
return current_user
- path: src/middleware/rate_limit.py
type: modify
description: |
Add to existing rate limiter configuration:
RATE_LIMITS = {
...existing...,
"/api/v1/refunds": RateLimitConfig(
requests_per_minute=10,
requests_per_hour=100,
key_func=lambda req: f"refund:{req.state.user.id}" # per-user limit
)
}
- path: src/domains/refunds/audit.py
type: create
description: |
Create audit logging for refund operations:
async def log_refund_attempt(
user_id: str,
order_id: str,
amount_cents: int,
success: bool,
error_code: str | None = None
) -> None:
"""Log all refund attempts for security audit trail."""
await audit_logger.log(
event_type="refund.attempt",
user_id=user_id,
metadata={
"order_id": order_id,
"amount_cents": amount_cents,
"success": success,
"error_code": error_code,
"ip_address": get_client_ip(),
"user_agent": get_user_agent()
}
)
security_requirements:
authentication:
type: jwt
requirements:
- Valid JWT in Authorization header (Bearer scheme)
- Token must not be expired (exp claim)
- Token must contain user_id and roles claims
- Token signature verified against JWKS endpoint
authorization:
type: rbac
rules:
- role: admin
permissions: [refund:create, refund:read, refund:approve]
constraints: none
- role: merchant
permissions: [refund:create, refund:read]
constraints:
- order.merchant_id == user.merchant_id
- role: support
permissions: [refund:read]
constraints:
- read-only access
data_protection:
encryption:
- Refund amounts encrypted at rest (AES-256)
- All API traffic over TLS 1.3
pii_fields:
- customer_email excluded from refund records
- customer_name excluded from refund records
rate_limiting:
enabled: true
limits:
per_user: 10 requests/minute, 100 requests/hour
per_ip: 30 requests/minute
per_order: 3 refund attempts lifetime
exposure_points:
- endpoint: POST /api/v1/refunds
risk_level: critical
mitigations:
- JWT authentication required
- RBAC permission check (can_refund_order)
- Idempotency key prevents replay attacks
- Amount validated server-side against order total
- Audit log for all attempts (success and failure)
- Rate limit per user and per IP
- Alert on >5 failed refunds per user per hour
acceptance_criteria:
- All refund endpoints require valid JWT
- Users cannot refund orders they don't own (403 Forbidden)
- Rate limiter returns 429 when exceeded
- All refund attempts logged to audit trail
- No PII in error messages or logs
- OWASP ZAP scan passes with no high/critical findings
dependencies:
blueprints:
- BP-001 # Backend implementation
SRE Blueprint
For observability, alerting at user journey level, reliability.
Key Principle: Maigret creates alerts tied to user journey IDs from the issue definition. This enables business-level monitoring ("Is AUTH-001 working?") not just technical metrics.
Schema
blueprint_id: BP-{NNN}
version: v{N}
issue_id: ISSUE-{ID}
owner: sre
status: draft | review | approved | rejected
# STACK
stack:
monitoring: prometheus | datadog | cloudwatch
dashboards: grafana | datadog
alerting: alertmanager | pagerduty | opsgenie
tracing: opentelemetry | jaeger | zipkin
# SCOPE
service:
name: string
criticality: P1 | P2 | P3
# USER JOURNEY OBSERVABILITY - Links to issue's user_journeys
user_journey_monitoring:
- journey_id: string # e.g., AUTH-001, REFUND-001
slo:
availability: string # e.g., "99.9%"
latency_p99: string # e.g., "< 2s"
alerts:
- name: string
condition: string # PromQL or similar
severity: critical | warning | info
runbook: string
# WHAT (use Prometheus/Grafana vocabulary!)
artifacts:
- path: string
type: create | modify | delete
description: string
# METRICS
monitoring:
metrics:
- name: string
type: Counter | Gauge | Histogram | Summary
description: string
labels: [string]
journey_ids: [string] # Which journeys this metric supports
# DASHBOARDS
dashboards:
- name: string
panels: [string]
# ALERTING
alerting:
rules:
- name: string
journey_id: string | null # Link to user journey
condition: string
severity: critical | warning | info
runbook: string | null
# SLOs (Service Level Objectives)
slos:
- journey_id: string # Journey-level SLO
availability: string
latency_p99: string
error_rate: string
# WHY
acceptance_criteria:
- string
# DEPENDENCIES
dependencies:
blueprints: [string]
Example: Refund Feature Observability (Prometheus/Grafana)
blueprint_id: BP-006
version: v1
issue_id: ISSUE-123
owner: sre
status: draft
stack:
monitoring: prometheus
dashboards: grafana
alerting: alertmanager
tracing: opentelemetry
oncall: pagerduty
service:
name: payment-service
criticality: P1
# USER JOURNEY OBSERVABILITY
# Links directly to journeys defined in the feature request
user_journey_monitoring:
- journey_id: REFUND-001 # "Request refund" journey from issue
slo:
availability: "99.9%"
latency_p99: "< 5s"
alerts:
- name: REFUND001_HighFailureRate
condition: |
sum(rate(journey_outcome_total{journey="REFUND-001", outcome="failure"}[5m]))
/ sum(rate(journey_outcome_total{journey="REFUND-001"}[5m])) > 0.01
severity: critical
runbook: monitoring/runbooks/refund-001-failures.md
- name: REFUND001_LatencyHigh
condition: |
histogram_quantile(0.99,
sum(rate(journey_duration_seconds_bucket{journey="REFUND-001"}[5m])) by (le)
) > 5
severity: warning
runbook: monitoring/runbooks/refund-001-latency.md
- journey_id: REFUND-002 # "Check refund status" journey
slo:
availability: "99.95%"
latency_p99: "< 500ms"
alerts:
- name: REFUND002_LatencyHigh
condition: |
histogram_quantile(0.99,
sum(rate(journey_duration_seconds_bucket{journey="REFUND-002"}[5m])) by (le)
) > 0.5
severity: warning
runbook: monitoring/runbooks/refund-002-latency.md
artifacts:
- path: monitoring/dashboards/refunds.json
type: create
description: |
Create Grafana dashboard JSON:
- Title: "Refunds - User Journey Health"
- Datasource: Prometheus
- Row 1: Journey Health Overview
1. Stat: journey_outcome_total{journey=~"REFUND-.*", outcome="success"} / journey_outcome_total{journey=~"REFUND-.*"}
Title: "REFUND-001 Success Rate"
2. Stat: Same for REFUND-002
Title: "REFUND-002 Success Rate"
- Row 2: Journey Latency
3. Heatmap: journey_duration_seconds_bucket{journey="REFUND-001"}
Title: "REFUND-001 Latency Distribution"
4. Graph: histogram_quantile(0.99, journey_duration_seconds_bucket{journey=~"REFUND-.*"})
Title: "P99 Latency by Journey"
- Row 3: Technical Metrics
5. Graph: rate(refund_requests_total[5m]) by (status)
Title: "Request Rate"
6. Graph: refund_stripe_api_duration_seconds
Title: "Stripe API Latency"
- Variables:
- $journey: REFUND-001, REFUND-002
- path: monitoring/alerts/refunds.yaml
type: create
description: |
Create PrometheusRule custom resource with journey-level alerts:
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: refund-journey-alerts
namespace: monitoring
labels:
journey_domain: REFUND
spec:
groups:
- name: refund-journeys
rules:
# REFUND-001: Request refund journey
- alert: REFUND001_HighFailureRate
expr: |
sum(rate(journey_outcome_total{journey="REFUND-001", outcome="failure"}[5m]))
/ sum(rate(journey_outcome_total{journey="REFUND-001"}[5m])) > 0.01
for: 5m
labels:
severity: critical
journey_id: REFUND-001
team: payments
annotations:
summary: "User journey REFUND-001 failure rate above 1%"
description: "Users are unable to complete refund requests"
runbook_url: "https://runbooks.acme.com/refund-001-failures"
- alert: REFUND001_LatencyP99High
expr: |
histogram_quantile(0.99,
sum(rate(journey_duration_seconds_bucket{journey="REFUND-001"}[5m])) by (le)
) > 5
for: 10m
labels:
severity: warning
journey_id: REFUND-001
annotations:
summary: "REFUND-001 journey P99 latency above 5s"
# REFUND-002: Check refund status journey
- alert: REFUND002_LatencyHigh
expr: |
histogram_quantile(0.99,
sum(rate(journey_duration_seconds_bucket{journey="REFUND-002"}[5m])) by (le)
) > 0.5
for: 5m
labels:
severity: warning
journey_id: REFUND-002
annotations:
summary: "REFUND-002 journey P99 latency above 500ms"
- name: refund-technical
rules:
- alert: StripeAPIErrors
expr: sum(rate(refund_stripe_api_errors_total[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "Stripe API errors detected"
- path: monitoring/runbooks/refund-001-failures.md
type: create
description: |
Create journey-specific runbook:
# REFUND-001: Request Refund - Failures Runbook
## Journey Definition
User wants to request a refund for a completed order.
## Alert: REFUND001_HighFailureRate
### Symptoms
- Journey failure rate exceeds 1% over 5 minutes
- Users unable to complete refund requests
### Investigation Steps
1. Check journey dashboard: monitoring/dashboards/refunds (filter journey=REFUND-001)
2. Identify failure step: `kubectl logs -n payments -l app=payment-service | grep "REFUND-001" | grep "error"`
3. Check dependent services:
- Stripe API: https://status.stripe.com
- Order service: /api/v1/orders/health
4. Check recent deployments: `kubectl rollout history deployment/payment-service -n payments`
### Common Causes
- Stripe API rate limiting or outage
- Order service unavailable
- Invalid Stripe API key
### Resolution
- If Stripe issue: Enable circuit breaker, notify users
- If order service: Check order-service pods, restart if needed
- If API key: Rotate in AWS Secrets Manager, restart pods
- path: src/domains/refunds/observability.py
type: create
description: |
Create journey instrumentation helpers:
from prometheus_client import Counter, Histogram
from opentelemetry import trace
# Journey-level metrics (business outcome)
JOURNEY_OUTCOME = Counter(
"journey_outcome_total",
"User journey outcomes",
["journey", "outcome"] # outcome: success, failure, abandoned
)
JOURNEY_DURATION = Histogram(
"journey_duration_seconds",
"User journey duration",
["journey"],
buckets=[0.1, 0.5, 1, 2, 5, 10, 30]
)
@contextmanager
def track_journey(journey_id: str):
"""Context manager to track user journey execution."""
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span(f"journey:{journey_id}") as span:
span.set_attribute("journey.id", journey_id)
start = time.time()
try:
yield span
JOURNEY_OUTCOME.labels(journey=journey_id, outcome="success").inc()
except Exception as e:
JOURNEY_OUTCOME.labels(journey=journey_id, outcome="failure").inc()
span.set_attribute("journey.error", str(e))
raise
finally:
JOURNEY_DURATION.labels(journey=journey_id).observe(time.time() - start)
monitoring:
metrics:
# Journey-level metrics (business outcomes)
- name: journey_outcome_total
type: Counter
description: User journey outcomes (success/failure/abandoned)
labels: [journey, outcome]
journey_ids: [REFUND-001, REFUND-002]
instrumentation: |
JOURNEY_OUTCOME = Counter(
"journey_outcome_total",
"User journey outcomes",
["journey", "outcome"]
)
- name: journey_duration_seconds
type: Histogram
description: End-to-end user journey duration
labels: [journey]
buckets: [0.1, 0.5, 1, 2, 5, 10, 30]
journey_ids: [REFUND-001, REFUND-002]
# Technical metrics (implementation details)
- name: refund_requests_total
type: Counter
description: Total refund API requests
labels: [status, error_code]
journey_ids: [REFUND-001]
- name: refund_stripe_api_duration_seconds
type: Histogram
description: Stripe API call latency
labels: [endpoint, status_code]
journey_ids: [REFUND-001]
# Journey-level SLOs
slos:
- journey_id: REFUND-001
name: "Request Refund"
availability: "99.9% of refund requests succeed"
latency_p99: "< 5s end-to-end"
error_rate: "< 1% non-user-error failures"
- journey_id: REFUND-002
name: "Check Refund Status"
availability: "99.95% of status checks succeed"
latency_p99: "< 500ms"
error_rate: "< 0.1%"
acceptance_criteria:
- Journey-level dashboard deployed showing REFUND-001 and REFUND-002 health
- PrometheusRule with journey-labeled alerts created
- Journey-specific runbooks reviewed by on-call team
- PagerDuty integration routes journey alerts to payments team
- SLO tracking enabled per journey in Prometheus
- OpenTelemetry tracing spans include journey_id attribute
dependencies:
blueprints:
- BP-001 # Metrics instrumented in backend code
- BP-003 # ServiceMonitor configured in deployment
FinOps Blueprint
[Schema and example remain the same as before - FinOps vocabulary is already specific to AWS/cost tools]
Blueprint Creation Process
Step 1: Architect Receives Issue
Baron assigns the issue to Veuve (features) or Duc (tech/bugs):
{
"id": "c001",
"timestamp": "2026-01-13T10:00:00Z",
"agent": "baron",
"type": "assignment",
"message": "Issue ISSUE-123 assigned for blueprint creation",
"refs": ["issue.md"]
}
Step 2: Architect Analyzes & Produces Blueprints
The architect: 1. Reads the issue acceptance criteria and user journeys 2. Identifies affected domains and their technology stacks 3. Creates one blueprint per domain using framework-specific language 4. Posts delivery comment
{
"id": "c002",
"timestamp": "2026-01-13T12:00:00Z",
"agent": "duc",
"type": "delivery",
"message": "Created 5 blueprints for refund feature (TDD order)",
"refs": [
"BP-001-tests-v1.yaml",
"BP-002-backend-v1.yaml",
"BP-003-frontend-v1.yaml",
"BP-004-infra-v1.yaml",
"BP-005-sre-v1.yaml"
]
}
Step 3: Baron Validates
Baron reviews each blueprint against validation criteria (see below).
Step 4: Iterate or Approve
If rejected, architect creates new version. If approved, Baron copies to domain sub-issues.
Two-Layer Validation
Blueprint Creation uses the same two-layer validation as all phases:
Layer 1: Deterministic Validators (Fast, Code-Based)
| Validator | Purpose |
|---|---|
SchemaValidator |
Blueprint matches BlueprintCreationOutput schema |
RequiredFieldsValidator |
Required fields present (status, result, feedback) |
ValueInSetValidator |
Status is success, blocked, or needs_clarification |
Layer 2: Baron LLM Validation (Semantic Quality)
After deterministic checks pass, Baron validates semantic quality:
Validate Blueprint Creation output:
1. COMPLETENESS: Are all components defined?
2. FEASIBILITY: Is the architecture implementable?
3. TRACEABILITY: Does blueprint address WRD acceptance criteria?
4. QUALITY: Are interfaces well-defined?
Return: pass | flag | escalate
| Verdict | Action |
|---|---|
pass |
Continue to Phase 3 (Blueprint Review) |
flag |
Log warning, continue |
escalate |
Abort run, human must review |
Baron's Validation Criteria
| Check | Description |
|---|---|
| Schema compliance | All required fields present and valid |
| Stack declaration | Technology stack clearly specified |
| Technical specificity | Artifact descriptions use framework vocabulary, not vague language |
| Scope alignment | Blueprint addresses issue acceptance criteria |
| Domain correctness | Owner matches the content (e.g., infra in devops, not backend) |
| Interface consistency | Contracts match across blueprints (e.g., frontend uses backend's API) |
| Dependency validity | Referenced blueprints exist |
| Completeness | No obvious gaps in coverage |
Validation Output
{
"id": "c003",
"timestamp": "2026-01-13T14:00:00Z",
"agent": "baron",
"type": "review",
"status": "rejected",
"message": "BP-001 rejected: Artifact descriptions too vague",
"refs": ["BP-001-backend-v1.yaml"],
"details": {
"blueprint": "BP-001",
"errors": [
{
"field": "artifacts[1].description",
"issue": "Too vague: 'Refund business logic'",
"suggestion": "Specify class name, method signatures, dependencies (e.g., 'Create RefundService class with async def create_refund(...)')"
}
]
}
}
Output
On successful validation, Duc produces structured output:
Phase Result
class BlueprintCreationResult(BaseModel):
overview: str
components: list[ComponentDefinition]
interfaces: list[InterfaceDefinition] = []
data_models: list[DataModelDefinition] = []
files_to_create: list[FileDefinition] = []
files_to_modify: list[FileModification] = []
Agent Feedback (For RL)
Every phase output includes feedback scores:
class AgentFeedback(BaseModel):
scores: FeedbackScores # 4 dimensions (0.0-1.0)
traceability: list[TraceabilityItem]
gaps: list[Gap]
suggestions: list[Suggestion]
What Happens Next
- Phase output JSON saved to
data/wrds/{wrd_id}/runs/{run_id}/phases/blueprint_creation.json - Proceeds to Phase 3 (Blueprint Review) where Baron reviews the blueprint
- After approval, domain agents receive their specific instructions
Phase 03 (Blueprint Review) documentation is coming soon.
TDD Workflow Context
After Blueprint Review (Phase 3), the workflow follows TDD order:
| Phase | Agent | Description |
|---|---|---|
| 4-5 | Marie | Test Planning + Implementation (tests written FIRST) |
| 6-7 | Dede | Backend Planning + Implementation |
| 8-9 | Dali | Frontend Planning + Implementation |
| 10-11 | Maigret | SRE Planning + Implementation |
| 12-13 | Gustave | DevOps Planning + Implementation |
Version History
| Version | Date | Changes |
|---|---|---|
| 0.1 | 2026-01-13 | Initial blueprint schemas with examples |
| 0.2 | 2026-01-13 | Added "Speak the Implementer's Language" principle with technology-specific examples |
| 0.3 | 2026-01-13 | Made YAML format mandatory for all blueprints |
| 0.4 | 2026-01-13 | Added Maigret (SRE) with user journey-level observability |
| 0.5 | 2026-01-13 | Corrected agent roles, TDD order (Marie first), removed FinOps |
| 1.0 | 2026-01-17 | Added two-layer validation (deterministic + Baron LLM), feedback structure for RL, 13-phase TDD context |