@anhth2/spec-driven-dev-plugin 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +243 -0
- package/bin/build.js +230 -0
- package/bin/index.js +311 -0
- package/commands/debug.md +374 -0
- package/commands/debug.tmpl +77 -0
- package/commands/define-product.md +451 -0
- package/commands/define-product.tmpl +154 -0
- package/commands/fix-bug.md +379 -0
- package/commands/fix-bug.tmpl +82 -0
- package/commands/generate-bdd.md +591 -0
- package/commands/generate-bdd.tmpl +294 -0
- package/commands/generate-code.md +395 -0
- package/commands/generate-code.tmpl +98 -0
- package/commands/generate-prd.md +488 -0
- package/commands/generate-prd.tmpl +191 -0
- package/commands/generate-tech-docs.md +362 -0
- package/commands/generate-tech-docs.tmpl +65 -0
- package/commands/generate-tests.md +377 -0
- package/commands/generate-tests.tmpl +80 -0
- package/commands/refine-prd.md +408 -0
- package/commands/refine-prd.tmpl +111 -0
- package/commands/review-code.md +354 -0
- package/commands/review-code.tmpl +57 -0
- package/commands/review-context.md +646 -0
- package/commands/review-context.tmpl +349 -0
- package/commands/review-tech-docs.md +518 -0
- package/commands/review-tech-docs.tmpl +221 -0
- package/commands/run-tests.md +343 -0
- package/commands/run-tests.tmpl +46 -0
- package/commands/setup-ai-first.md +278 -0
- package/commands/setup-ai-first.tmpl +197 -0
- package/commands/smoke-test.md +366 -0
- package/commands/smoke-test.tmpl +69 -0
- package/commands/validate-traces.md +529 -0
- package/commands/validate-traces.tmpl +232 -0
- package/core/FRAMEWORK_VERSION +1 -0
- package/core/commands/debug.md +374 -0
- package/core/commands/define-product.md +451 -0
- package/core/commands/fix-bug.md +379 -0
- package/core/commands/generate-bdd.md +591 -0
- package/core/commands/generate-code.md +395 -0
- package/core/commands/generate-prd.md +488 -0
- package/core/commands/generate-tech-docs.md +362 -0
- package/core/commands/generate-tests.md +377 -0
- package/core/commands/refine-prd.md +408 -0
- package/core/commands/review-code.md +354 -0
- package/core/commands/review-context.md +646 -0
- package/core/commands/review-tech-docs.md +518 -0
- package/core/commands/run-tests.md +343 -0
- package/core/commands/setup-ai-first.md +278 -0
- package/core/commands/smoke-test.md +366 -0
- package/core/commands/validate-traces.md +529 -0
- package/core/hooks/data-guard.js +141 -0
- package/core/hooks/settings.json +18 -0
- package/core/modules/angular/architecture-snippets/component-patterns.md +187 -0
- package/core/modules/angular/module.yaml +6 -0
- package/core/modules/angular/stack-profile.yaml +38 -0
- package/core/modules/context-engineering/architecture-snippets/context-design.md +119 -0
- package/core/modules/context-engineering/module.yaml +9 -0
- package/core/modules/context-engineering/stack-profile.yaml +61 -0
- package/core/modules/dotnet/architecture-snippets/clean-arch.md +160 -0
- package/core/modules/dotnet/module.yaml +6 -0
- package/core/modules/dotnet/stack-profile.yaml +50 -0
- package/core/modules/golang/architecture-snippets/domain-layout.md +283 -0
- package/core/modules/golang/module.yaml +6 -0
- package/core/modules/golang/stack-profile.yaml +40 -0
- package/core/modules/java-spring/architecture-snippets/layered-arch.md +201 -0
- package/core/modules/java-spring/module.yaml +15 -0
- package/core/modules/java-spring/stack-profile.yaml +28 -0
- package/core/modules/nextjs/architecture-snippets/app-router-patterns.md +269 -0
- package/core/modules/nextjs/module.yaml +14 -0
- package/core/modules/nextjs/stack-profile.yaml +74 -0
- package/core/modules/php-laravel/architecture-snippets/service-repository.md +302 -0
- package/core/modules/php-laravel/module.yaml +15 -0
- package/core/modules/php-laravel/stack-profile.yaml +56 -0
- package/core/modules/react/architecture-snippets/hooks-query-patterns.md +254 -0
- package/core/modules/react/module.yaml +14 -0
- package/core/modules/react/stack-profile.yaml +63 -0
- package/core/rules/data-protection.md +80 -0
- package/core/rules/workflow.md +44 -0
- package/core/skills/code/SKILL.md +526 -0
- package/core/skills/debug/SKILL.md +584 -0
- package/core/skills/discovery/SKILL.md +363 -0
- package/core/skills/prd/SKILL.md +456 -0
- package/core/skills/setup-ai-first/SKILL.md +160 -0
- package/core/skills/spec/SKILL.md +361 -0
- package/core/skills/test/SKILL.md +862 -0
- package/core/steps/context-loader.md +163 -0
- package/core/steps/gate.md +81 -0
- package/core/steps/report-footer.md +53 -0
- package/core/steps/spawn-agent.md +123 -0
- package/core/templates/architecture.template.md +113 -0
- package/core/templates/feature.template +259 -0
- package/core/templates/platform-guide.template.md +145 -0
- package/core/templates/prd.template.md +312 -0
- package/core/templates/product-definition.template.md +168 -0
- package/core/templates/project-context.yaml +78 -0
- package/hooks/data-guard.js +141 -0
- package/hooks/settings.json +18 -0
- package/modules/angular/architecture-snippets/component-patterns.md +187 -0
- package/modules/angular/module.yaml +6 -0
- package/modules/angular/stack-profile.yaml +38 -0
- package/modules/context-engineering/architecture-snippets/context-design.md +119 -0
- package/modules/context-engineering/module.yaml +9 -0
- package/modules/context-engineering/stack-profile.yaml +61 -0
- package/modules/dotnet/architecture-snippets/clean-arch.md +160 -0
- package/modules/dotnet/module.yaml +6 -0
- package/modules/dotnet/stack-profile.yaml +50 -0
- package/modules/golang/architecture-snippets/domain-layout.md +283 -0
- package/modules/golang/module.yaml +6 -0
- package/modules/golang/stack-profile.yaml +40 -0
- package/modules/java-spring/architecture-snippets/layered-arch.md +201 -0
- package/modules/java-spring/module.yaml +15 -0
- package/modules/java-spring/stack-profile.yaml +28 -0
- package/modules/nextjs/architecture-snippets/app-router-patterns.md +269 -0
- package/modules/nextjs/module.yaml +14 -0
- package/modules/nextjs/stack-profile.yaml +74 -0
- package/modules/php-laravel/architecture-snippets/service-repository.md +302 -0
- package/modules/php-laravel/module.yaml +15 -0
- package/modules/php-laravel/stack-profile.yaml +56 -0
- package/modules/react/architecture-snippets/hooks-query-patterns.md +254 -0
- package/modules/react/module.yaml +14 -0
- package/modules/react/stack-profile.yaml +63 -0
- package/package.json +42 -0
- package/rules/data-protection.md +80 -0
- package/rules/workflow.md +44 -0
- package/scripts/init.sh +49 -0
- package/scripts/upgrade.sh +94 -0
- package/skills/code/SKILL.md +526 -0
- package/skills/code/SKILL.tmpl +176 -0
- package/skills/debug/SKILL.md +584 -0
- package/skills/debug/SKILL.tmpl +262 -0
- package/skills/discovery/SKILL.md +363 -0
- package/skills/discovery/SKILL.tmpl +147 -0
- package/skills/prd/SKILL.md +456 -0
- package/skills/prd/SKILL.tmpl +188 -0
- package/skills/setup-ai-first/SKILL.md +160 -0
- package/skills/setup-ai-first/SKILL.tmpl +107 -0
- package/skills/spec/SKILL.md +361 -0
- package/skills/spec/SKILL.tmpl +174 -0
- package/skills/test/SKILL.md +862 -0
- package/skills/test/SKILL.tmpl +296 -0
- package/steps/context-loader.md +163 -0
- package/steps/gate.md +81 -0
- package/steps/report-footer.md +53 -0
- package/steps/spawn-agent.md +123 -0
- package/templates/architecture.template.md +113 -0
- package/templates/feature.template +259 -0
- package/templates/platform-guide.template.md +145 -0
- package/templates/prd.template.md +312 -0
- package/templates/product-definition.template.md +168 -0
- package/templates/project-context.yaml +78 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
build:
|
|
2
|
+
compile: "composer install"
|
|
3
|
+
test: "php artisan test"
|
|
4
|
+
run: "php artisan serve"
|
|
5
|
+
migrate: "php artisan migrate"
|
|
6
|
+
seed: "php artisan db:seed"
|
|
7
|
+
|
|
8
|
+
architecture:
|
|
9
|
+
style: "Service-Repository (Controller → Service → Repository → Model)"
|
|
10
|
+
key_rules:
|
|
11
|
+
- "Controllers must not contain business logic — delegate to Services"
|
|
12
|
+
- "Services own business logic and transaction boundaries"
|
|
13
|
+
- "Repositories abstract all Eloquent/DB queries"
|
|
14
|
+
- "Form Requests handle all input validation (never validate in controller)"
|
|
15
|
+
- "API Resources transform Model output (never return raw Model/array)"
|
|
16
|
+
- "Use dependency injection via constructor, not Facades in classes"
|
|
17
|
+
|
|
18
|
+
coding_standards:
|
|
19
|
+
naming:
|
|
20
|
+
controllers: "PascalCase + Controller suffix — Resource style (e.g., OrderController)"
|
|
21
|
+
services: "PascalCase + Service suffix (e.g., OrderService)"
|
|
22
|
+
repositories:
|
|
23
|
+
interface: "PascalCase + RepositoryInterface (e.g., OrderRepositoryInterface)"
|
|
24
|
+
impl: "PascalCase + Repository (e.g., EloquentOrderRepository)"
|
|
25
|
+
models: "PascalCase singular (e.g., Order)"
|
|
26
|
+
form_requests: "PascalCase + Request suffix (e.g., CreateOrderRequest)"
|
|
27
|
+
resources: "PascalCase + Resource suffix (e.g., OrderResource)"
|
|
28
|
+
files:
|
|
29
|
+
controller: "app/Http/Controllers/{Domain}/{Name}Controller.php"
|
|
30
|
+
service: "app/Services/{Domain}/{Name}Service.php"
|
|
31
|
+
repository_interface: "app/Repositories/{Name}RepositoryInterface.php"
|
|
32
|
+
repository_impl: "app/Repositories/Eloquent/{Name}Repository.php"
|
|
33
|
+
model: "app/Models/{Name}.php"
|
|
34
|
+
form_request: "app/Http/Requests/{Domain}/{Action}{Name}Request.php"
|
|
35
|
+
resource: "app/Http/Resources/{Domain}/{Name}Resource.php"
|
|
36
|
+
patterns:
|
|
37
|
+
response_wrapper: "JsonResponse with status/data/message structure"
|
|
38
|
+
pagination: "Laravel paginate() — returns LengthAwarePaginator"
|
|
39
|
+
exception_base: "App\\Exceptions\\BusinessException"
|
|
40
|
+
transactions: "DB::transaction(fn () => ...) in Service layer"
|
|
41
|
+
events: "Laravel Events + Listeners for domain events"
|
|
42
|
+
|
|
43
|
+
testing:
|
|
44
|
+
unit: "PHPUnit or Pest"
|
|
45
|
+
feature: "Laravel Feature Tests (HTTP layer with database)"
|
|
46
|
+
patterns:
|
|
47
|
+
- "Use RefreshDatabase or DatabaseTransactions trait"
|
|
48
|
+
- "Fake external services with Http::fake() or mock()"
|
|
49
|
+
- "Use actingAs(\$user) for auth context"
|
|
50
|
+
- "Assert JSON structure with assertJson() / assertJsonStructure()"
|
|
51
|
+
|
|
52
|
+
trace_tags:
|
|
53
|
+
implements: "// @trace.implements={UC-ID}-SC{N}"
|
|
54
|
+
source: "// @trace.source=specs/bdd/{domain}/{UC-ID}.feature"
|
|
55
|
+
verifies: "// @trace.verifies={UC-ID}"
|
|
56
|
+
test_type: "// @trace.test_type=unit|feature"
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# React — Hooks, React Query & Zustand Patterns
|
|
2
|
+
|
|
3
|
+
## React Query — Query Hook
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// features/order/queries/order.queries.ts
|
|
7
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
8
|
+
import { orderApi } from '@/api/order.api';
|
|
9
|
+
import type { CreateOrderPayload, Order } from '../types';
|
|
10
|
+
|
|
11
|
+
// Query key factory — single source of truth for cache invalidation
|
|
12
|
+
export const orderKeys = {
|
|
13
|
+
all: ['ORDERS'] as const,
|
|
14
|
+
lists: () => [...orderKeys.all, 'list'] as const,
|
|
15
|
+
list: (customerId: number) => [...orderKeys.lists(), { customerId }] as const,
|
|
16
|
+
detail: (id: number) => [...orderKeys.all, 'detail', id] as const,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// @trace.implements=ORD-UC1-SC1
|
|
20
|
+
// @trace.source=specs/bdd/order/ORD-UC1.feature
|
|
21
|
+
export function useOrderList(customerId: number) {
|
|
22
|
+
return useQuery({
|
|
23
|
+
queryKey: orderKeys.list(customerId),
|
|
24
|
+
queryFn: () => orderApi.getByCustomer(customerId),
|
|
25
|
+
staleTime: 30_000,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// @trace.implements=ORD-UC2-SC1
|
|
30
|
+
// @trace.source=specs/bdd/order/ORD-UC2.feature
|
|
31
|
+
export function useCreateOrder() {
|
|
32
|
+
const queryClient = useQueryClient();
|
|
33
|
+
|
|
34
|
+
return useMutation({
|
|
35
|
+
mutationFn: (payload: CreateOrderPayload) => orderApi.create(payload),
|
|
36
|
+
onSuccess: (_, variables) => {
|
|
37
|
+
// Invalidate order list for this customer
|
|
38
|
+
queryClient.invalidateQueries({ queryKey: orderKeys.list(variables.customerId) });
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## API Client (Axios)
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// api/order.api.ts
|
|
48
|
+
import { apiClient } from '@/api/client';
|
|
49
|
+
import type { CreateOrderPayload, Order, PaginatedResponse } from '@/features/order/types';
|
|
50
|
+
|
|
51
|
+
export const orderApi = {
|
|
52
|
+
getByCustomer: (customerId: number) =>
|
|
53
|
+
apiClient.get<PaginatedResponse<Order>>('/v1/orders', { params: { customerId } })
|
|
54
|
+
.then(res => res.data),
|
|
55
|
+
|
|
56
|
+
getById: (id: number) =>
|
|
57
|
+
apiClient.get<Order>(`/v1/orders/${id}`).then(res => res.data),
|
|
58
|
+
|
|
59
|
+
create: (payload: CreateOrderPayload) =>
|
|
60
|
+
apiClient.post<Order>('/v1/orders', payload).then(res => res.data),
|
|
61
|
+
|
|
62
|
+
cancel: (id: number) =>
|
|
63
|
+
apiClient.patch<void>(`/v1/orders/${id}/cancel`).then(res => res.data),
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// api/client.ts — Axios instance with auth interceptor
|
|
69
|
+
import axios from 'axios';
|
|
70
|
+
|
|
71
|
+
export const apiClient = axios.create({
|
|
72
|
+
baseURL: import.meta.env.VITE_API_URL,
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
apiClient.interceptors.request.use(config => {
|
|
77
|
+
const token = localStorage.getItem('access_token');
|
|
78
|
+
if (token) config.headers.Authorization = `Bearer ${token}`;
|
|
79
|
+
return config;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
apiClient.interceptors.response.use(
|
|
83
|
+
res => res,
|
|
84
|
+
error => {
|
|
85
|
+
if (error.response?.status === 401) {
|
|
86
|
+
// redirect to login
|
|
87
|
+
window.location.href = '/login';
|
|
88
|
+
}
|
|
89
|
+
return Promise.reject(error);
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Zustand Store (Client State)
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// features/cart/store/cart.store.ts
|
|
98
|
+
import { create } from 'zustand';
|
|
99
|
+
import { persist } from 'zustand/middleware';
|
|
100
|
+
import type { CartItem } from '../types';
|
|
101
|
+
|
|
102
|
+
interface CartStore {
|
|
103
|
+
items: CartItem[];
|
|
104
|
+
addItem: (item: CartItem) => void;
|
|
105
|
+
removeItem: (productId: number) => void;
|
|
106
|
+
clearCart: () => void;
|
|
107
|
+
total: () => number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const useCartStore = create<CartStore>()(
|
|
111
|
+
persist(
|
|
112
|
+
(set, get) => ({
|
|
113
|
+
items: [],
|
|
114
|
+
|
|
115
|
+
addItem: (item) =>
|
|
116
|
+
set(state => {
|
|
117
|
+
const existing = state.items.find(i => i.productId === item.productId);
|
|
118
|
+
if (existing) {
|
|
119
|
+
return {
|
|
120
|
+
items: state.items.map(i =>
|
|
121
|
+
i.productId === item.productId
|
|
122
|
+
? { ...i, quantity: i.quantity + item.quantity }
|
|
123
|
+
: i
|
|
124
|
+
),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return { items: [...state.items, item] };
|
|
128
|
+
}),
|
|
129
|
+
|
|
130
|
+
removeItem: (productId) =>
|
|
131
|
+
set(state => ({ items: state.items.filter(i => i.productId !== productId) })),
|
|
132
|
+
|
|
133
|
+
clearCart: () => set({ items: [] }),
|
|
134
|
+
|
|
135
|
+
total: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
|
|
136
|
+
}),
|
|
137
|
+
{ name: 'cart-storage' }
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Feature Component (Container)
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// features/order/components/OrderListPage.tsx
|
|
146
|
+
// @trace.implements=ORD-UC1-SC1
|
|
147
|
+
// @trace.source=specs/bdd/order/ORD-UC1.feature
|
|
148
|
+
import { useAuth } from '@/hooks/useAuth';
|
|
149
|
+
import { useOrderList } from '../queries/order.queries';
|
|
150
|
+
import { OrderCard } from './OrderCard';
|
|
151
|
+
|
|
152
|
+
export function OrderListPage() {
|
|
153
|
+
const { user } = useAuth();
|
|
154
|
+
const { data: orders, isLoading, isError } = useOrderList(user.id);
|
|
155
|
+
|
|
156
|
+
if (isLoading) return <div>Loading...</div>;
|
|
157
|
+
if (isError) return <div>Failed to load orders.</div>;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div className="order-list">
|
|
161
|
+
{orders?.map(order => (
|
|
162
|
+
<OrderCard key={order.id} order={order} />
|
|
163
|
+
))}
|
|
164
|
+
{orders?.length === 0 && <p>No orders yet.</p>}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## React Hook Form + Zod
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
// features/order/components/CreateOrderForm.tsx
|
|
174
|
+
import { useForm } from 'react-hook-form';
|
|
175
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
176
|
+
import { z } from 'zod';
|
|
177
|
+
import { useCreateOrder } from '../queries/order.queries';
|
|
178
|
+
|
|
179
|
+
const createOrderSchema = z.object({
|
|
180
|
+
customerId: z.number().positive(),
|
|
181
|
+
items: z.array(z.object({
|
|
182
|
+
productId: z.number().positive(),
|
|
183
|
+
quantity: z.number().int().min(1),
|
|
184
|
+
})).min(1, 'At least one item required'),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
type CreateOrderForm = z.infer<typeof createOrderSchema>;
|
|
188
|
+
|
|
189
|
+
export function CreateOrderForm({ customerId }: { customerId: number }) {
|
|
190
|
+
const { mutate: createOrder, isPending } = useCreateOrder();
|
|
191
|
+
|
|
192
|
+
const { register, handleSubmit, formState: { errors } } = useForm<CreateOrderForm>({
|
|
193
|
+
resolver: zodResolver(createOrderSchema),
|
|
194
|
+
defaultValues: { customerId, items: [] },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const onSubmit = (data: CreateOrderForm) => createOrder(data);
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
201
|
+
{/* form fields */}
|
|
202
|
+
{errors.items && <p className="error">{errors.items.message}</p>}
|
|
203
|
+
<button type="submit" disabled={isPending}>
|
|
204
|
+
{isPending ? 'Creating...' : 'Create Order'}
|
|
205
|
+
</button>
|
|
206
|
+
</form>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Test with React Testing Library + MSW
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
// features/order/__tests__/OrderListPage.test.tsx
|
|
215
|
+
// @trace.verifies=ORD-UC1
|
|
216
|
+
// @trace.test_type=integration
|
|
217
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
218
|
+
import { http, HttpResponse } from 'msw';
|
|
219
|
+
import { server } from '@/test/server';
|
|
220
|
+
import { renderWithProviders } from '@/test/utils';
|
|
221
|
+
import { OrderListPage } from '../components/OrderListPage';
|
|
222
|
+
|
|
223
|
+
describe('OrderListPage', () => {
|
|
224
|
+
it('displays orders after successful fetch', async () => {
|
|
225
|
+
server.use(
|
|
226
|
+
http.get('/v1/orders', () =>
|
|
227
|
+
HttpResponse.json([
|
|
228
|
+
{ id: 1, status: 'pending', customerId: 42, items: [] },
|
|
229
|
+
])
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
renderWithProviders(<OrderListPage />, { user: { id: 42 } });
|
|
234
|
+
|
|
235
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
236
|
+
|
|
237
|
+
await waitFor(() =>
|
|
238
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument()
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
expect(screen.getByText('Order #1')).toBeInTheDocument();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('shows empty state when no orders exist', async () => {
|
|
245
|
+
server.use(http.get('/v1/orders', () => HttpResponse.json([])));
|
|
246
|
+
|
|
247
|
+
renderWithProviders(<OrderListPage />, { user: { id: 42 } });
|
|
248
|
+
|
|
249
|
+
await waitFor(() =>
|
|
250
|
+
expect(screen.getByText('No orders yet.')).toBeInTheDocument()
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name: "React"
|
|
2
|
+
version: "1.0.0"
|
|
3
|
+
description: "React 18 SPA with hooks, React Query, and Zustand"
|
|
4
|
+
language: "TypeScript"
|
|
5
|
+
framework: "React"
|
|
6
|
+
stack_type: "frontend"
|
|
7
|
+
default_layer_order:
|
|
8
|
+
- Types / interfaces
|
|
9
|
+
- API client (React Query hooks)
|
|
10
|
+
- State store (Zustand)
|
|
11
|
+
- Custom hooks (business logic)
|
|
12
|
+
- UI components (presentational)
|
|
13
|
+
- Page / feature components (container)
|
|
14
|
+
test_framework: "Vitest + React Testing Library"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
build:
|
|
2
|
+
compile: "npm run build"
|
|
3
|
+
test: "npm test"
|
|
4
|
+
run: "npm run dev"
|
|
5
|
+
lint: "npm run lint"
|
|
6
|
+
|
|
7
|
+
architecture:
|
|
8
|
+
style: "Feature-based (components → hooks → queries → store)"
|
|
9
|
+
key_rules:
|
|
10
|
+
- "Business logic lives in custom hooks, not in components"
|
|
11
|
+
- "Server state managed by React Query (useQuery / useMutation)"
|
|
12
|
+
- "Client-only UI state managed by Zustand or useState"
|
|
13
|
+
- "Components must be pure functions — no direct API calls inside JSX"
|
|
14
|
+
- "Shared UI primitives in components/ui/, feature logic in features/{name}/"
|
|
15
|
+
- "Never fetch data directly in a component — use a custom hook"
|
|
16
|
+
folder_structure: |
|
|
17
|
+
src/
|
|
18
|
+
├── api/ ← axios instance, interceptors
|
|
19
|
+
├── components/
|
|
20
|
+
│ └── ui/ ← reusable primitives (Button, Modal, Table...)
|
|
21
|
+
├── features/
|
|
22
|
+
│ └── {domain}/
|
|
23
|
+
│ ├── components/ ← feature-specific UI
|
|
24
|
+
│ ├── hooks/ ← useOrderList, useCreateOrder...
|
|
25
|
+
│ ├── queries/ ← React Query definitions
|
|
26
|
+
│ ├── store/ ← Zustand slice (if needed)
|
|
27
|
+
│ └── types.ts
|
|
28
|
+
├── pages/ ← route-level page components
|
|
29
|
+
└── lib/ ← utilities, formatters, constants
|
|
30
|
+
|
|
31
|
+
coding_standards:
|
|
32
|
+
naming:
|
|
33
|
+
components: "PascalCase (e.g., OrderList, CreateOrderModal)"
|
|
34
|
+
hooks: "camelCase with 'use' prefix (e.g., useOrderList, useCreateOrder)"
|
|
35
|
+
query_keys: "SCREAMING_SNAKE_CASE array (e.g., ['ORDER_LIST', customerId])"
|
|
36
|
+
stores: "camelCase + Store suffix (e.g., useCartStore)"
|
|
37
|
+
files:
|
|
38
|
+
component: "{Feature}.tsx"
|
|
39
|
+
hook: "use{Feature}.ts"
|
|
40
|
+
query: "{feature}.queries.ts"
|
|
41
|
+
store: "{feature}.store.ts"
|
|
42
|
+
types: "{feature}.types.ts"
|
|
43
|
+
patterns:
|
|
44
|
+
data_fetching: "React Query (TanStack Query v5)"
|
|
45
|
+
global_state: "Zustand slices"
|
|
46
|
+
forms: "React Hook Form + Zod validation"
|
|
47
|
+
api_client: "Axios with interceptors for auth + error handling"
|
|
48
|
+
error_boundary: "React ErrorBoundary wraps each feature route"
|
|
49
|
+
|
|
50
|
+
testing:
|
|
51
|
+
unit: "Vitest + React Testing Library"
|
|
52
|
+
e2e: "Playwright or Cypress"
|
|
53
|
+
patterns:
|
|
54
|
+
- "Test behavior, not implementation — query by role/label, not className"
|
|
55
|
+
- "Mock React Query with a QueryClient wrapper in test setup"
|
|
56
|
+
- "Use MSW (Mock Service Worker) to mock API calls in tests"
|
|
57
|
+
- "renderWithProviders() helper wraps component with QueryClient + Router"
|
|
58
|
+
|
|
59
|
+
trace_tags:
|
|
60
|
+
implements: "// @trace.implements={UC-ID}-SC{N}"
|
|
61
|
+
source: "// @trace.source=specs/bdd/{domain}/{UC-ID}.feature"
|
|
62
|
+
verifies: "// @trace.verifies={UC-ID}"
|
|
63
|
+
test_type: "// @trace.test_type=unit|integration"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Data Protection Rules
|
|
2
|
+
|
|
3
|
+
> These rules are loaded by `steps/context-loader.md` at the start of every command.
|
|
4
|
+
> AI agents MUST follow these rules without exception.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## NEVER Read — Sensitive Files
|
|
9
|
+
|
|
10
|
+
The following file patterns contain credentials, secrets, or private keys.
|
|
11
|
+
**Do NOT read, display, log, or reference their contents under any circumstance.**
|
|
12
|
+
|
|
13
|
+
### Environment & Secrets
|
|
14
|
+
- `.env`
|
|
15
|
+
- `.env.*` (e.g., `.env.local`, `.env.production`, `.env.staging`)
|
|
16
|
+
- `*.secret`
|
|
17
|
+
- `secrets/` (entire directory)
|
|
18
|
+
- `.secrets/` (entire directory)
|
|
19
|
+
- `*credentials*`
|
|
20
|
+
|
|
21
|
+
### Cryptographic Keys & Certificates
|
|
22
|
+
- `*.key`
|
|
23
|
+
- `*.pem`
|
|
24
|
+
- `*.p12`
|
|
25
|
+
- `*.pfx`
|
|
26
|
+
- `*.jks`
|
|
27
|
+
- `*.keystore`
|
|
28
|
+
- `*.crt` / `*.cert`
|
|
29
|
+
|
|
30
|
+
### Framework-Specific Config Files (may contain DB passwords, API keys)
|
|
31
|
+
- `application-prod.yml` / `application-prod.properties`
|
|
32
|
+
- `application-production.yml`
|
|
33
|
+
- `appsettings.Production.json`
|
|
34
|
+
- `appsettings.Staging.json`
|
|
35
|
+
- `database.yml` (Rails)
|
|
36
|
+
- `config/master.key` (Rails)
|
|
37
|
+
- `storage/oauth-private.key`
|
|
38
|
+
|
|
39
|
+
### Files Matching Dangerous Keywords
|
|
40
|
+
Any file whose name contains (case-insensitive):
|
|
41
|
+
- `password`
|
|
42
|
+
- `passwd`
|
|
43
|
+
- `secret`
|
|
44
|
+
- `private_key`
|
|
45
|
+
- `api_key`
|
|
46
|
+
- `access_token`
|
|
47
|
+
- `auth_token`
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## NEVER Write or Modify
|
|
52
|
+
|
|
53
|
+
- Any file listed above
|
|
54
|
+
- `*.lock` files that are not package lock files (e.g., `*.lock` outside of `package-lock.json`, `yarn.lock`, `composer.lock`)
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## NEVER Execute via Bash
|
|
59
|
+
|
|
60
|
+
- Commands that print secrets: `printenv`, `env | grep -i secret`, `cat .env`
|
|
61
|
+
- Commands that expose credentials: `docker inspect`, `kubectl get secret -o yaml`
|
|
62
|
+
- Git commands that may expose history of secrets: `git show`, `git log -p` on config files
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Safe Alternatives
|
|
67
|
+
|
|
68
|
+
If context about environment configuration is needed:
|
|
69
|
+
|
|
70
|
+
1. Ask the user to describe the configuration **without sharing actual values**.
|
|
71
|
+
2. Reference the **structure** of config (keys, not values): "I see you use `DATABASE_URL` — I'll generate code that reads from that variable."
|
|
72
|
+
3. Use placeholder values in generated code: `process.env.DATABASE_URL` or `${DATABASE_URL}`.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## If a Sensitive File is Accidentally Accessed
|
|
77
|
+
|
|
78
|
+
1. Do NOT display or repeat any content from the file.
|
|
79
|
+
2. Immediately stop and notify the user: "I've detected a sensitive file. I will not read or use its contents."
|
|
80
|
+
3. Ask the user what they actually need (usually it's the structure, not the values).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Workflow Rules
|
|
2
|
+
|
|
3
|
+
> General AI behavior rules for all spec-driven-dev commands.
|
|
4
|
+
> Loaded by `steps/context-loader.md` at the start of every command.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Checkpoints
|
|
9
|
+
|
|
10
|
+
- **Always** show a CHECKPOINT before making significant changes.
|
|
11
|
+
- A CHECKPOINT must include: what will be done, which files will be created/modified, estimated scope.
|
|
12
|
+
- Wait for explicit "Y" or user confirmation before proceeding.
|
|
13
|
+
- Exception: read-only analysis commands (`/review-code`, `/validate-traces`, `/debug`) may skip CHECKPOINT.
|
|
14
|
+
|
|
15
|
+
## Scope Control
|
|
16
|
+
|
|
17
|
+
- Work only within the scope explicitly confirmed at CHECKPOINT.
|
|
18
|
+
- Do NOT create files outside the directories specified in `project-context.yaml → paths`.
|
|
19
|
+
- If new scope is discovered mid-command, STOP and ask: "I found additional scope [{description}]. Should I include it? (Y/N)"
|
|
20
|
+
|
|
21
|
+
## Code Generation
|
|
22
|
+
|
|
23
|
+
- Never generate code for files not backed by a `.feature` spec (unless `/fix-bug` or `/debug`).
|
|
24
|
+
- Always add `@trace.implements` tags on controller-level methods.
|
|
25
|
+
- Never overwrite existing business logic without explicit confirmation.
|
|
26
|
+
- Build must pass before committing: run `{conventions.build_command}` and fix errors (max 3 retries).
|
|
27
|
+
|
|
28
|
+
## File Operations
|
|
29
|
+
|
|
30
|
+
- Prefer **editing** existing files over replacing them entirely.
|
|
31
|
+
- When creating new files, check if a similar file already exists first.
|
|
32
|
+
- Never delete files unless explicitly instructed.
|
|
33
|
+
|
|
34
|
+
## Communication
|
|
35
|
+
|
|
36
|
+
- Report in the language the user writes in (Vietnamese if user uses Vietnamese, English otherwise).
|
|
37
|
+
- Keep reports structured: status, artifacts created, next recommended command.
|
|
38
|
+
- If unsure about business intent, ask — do not guess and generate wrong spec.
|
|
39
|
+
|
|
40
|
+
## Error Handling
|
|
41
|
+
|
|
42
|
+
- If a tool call fails (file not found, build error, etc.), report the specific error clearly.
|
|
43
|
+
- Do NOT silently skip errors or pretend success.
|
|
44
|
+
- Suggest a concrete fix, not just "please check the error".
|