@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.
Files changed (152) hide show
  1. package/ARCHITECTURE.md +243 -0
  2. package/bin/build.js +230 -0
  3. package/bin/index.js +311 -0
  4. package/commands/debug.md +374 -0
  5. package/commands/debug.tmpl +77 -0
  6. package/commands/define-product.md +451 -0
  7. package/commands/define-product.tmpl +154 -0
  8. package/commands/fix-bug.md +379 -0
  9. package/commands/fix-bug.tmpl +82 -0
  10. package/commands/generate-bdd.md +591 -0
  11. package/commands/generate-bdd.tmpl +294 -0
  12. package/commands/generate-code.md +395 -0
  13. package/commands/generate-code.tmpl +98 -0
  14. package/commands/generate-prd.md +488 -0
  15. package/commands/generate-prd.tmpl +191 -0
  16. package/commands/generate-tech-docs.md +362 -0
  17. package/commands/generate-tech-docs.tmpl +65 -0
  18. package/commands/generate-tests.md +377 -0
  19. package/commands/generate-tests.tmpl +80 -0
  20. package/commands/refine-prd.md +408 -0
  21. package/commands/refine-prd.tmpl +111 -0
  22. package/commands/review-code.md +354 -0
  23. package/commands/review-code.tmpl +57 -0
  24. package/commands/review-context.md +646 -0
  25. package/commands/review-context.tmpl +349 -0
  26. package/commands/review-tech-docs.md +518 -0
  27. package/commands/review-tech-docs.tmpl +221 -0
  28. package/commands/run-tests.md +343 -0
  29. package/commands/run-tests.tmpl +46 -0
  30. package/commands/setup-ai-first.md +278 -0
  31. package/commands/setup-ai-first.tmpl +197 -0
  32. package/commands/smoke-test.md +366 -0
  33. package/commands/smoke-test.tmpl +69 -0
  34. package/commands/validate-traces.md +529 -0
  35. package/commands/validate-traces.tmpl +232 -0
  36. package/core/FRAMEWORK_VERSION +1 -0
  37. package/core/commands/debug.md +374 -0
  38. package/core/commands/define-product.md +451 -0
  39. package/core/commands/fix-bug.md +379 -0
  40. package/core/commands/generate-bdd.md +591 -0
  41. package/core/commands/generate-code.md +395 -0
  42. package/core/commands/generate-prd.md +488 -0
  43. package/core/commands/generate-tech-docs.md +362 -0
  44. package/core/commands/generate-tests.md +377 -0
  45. package/core/commands/refine-prd.md +408 -0
  46. package/core/commands/review-code.md +354 -0
  47. package/core/commands/review-context.md +646 -0
  48. package/core/commands/review-tech-docs.md +518 -0
  49. package/core/commands/run-tests.md +343 -0
  50. package/core/commands/setup-ai-first.md +278 -0
  51. package/core/commands/smoke-test.md +366 -0
  52. package/core/commands/validate-traces.md +529 -0
  53. package/core/hooks/data-guard.js +141 -0
  54. package/core/hooks/settings.json +18 -0
  55. package/core/modules/angular/architecture-snippets/component-patterns.md +187 -0
  56. package/core/modules/angular/module.yaml +6 -0
  57. package/core/modules/angular/stack-profile.yaml +38 -0
  58. package/core/modules/context-engineering/architecture-snippets/context-design.md +119 -0
  59. package/core/modules/context-engineering/module.yaml +9 -0
  60. package/core/modules/context-engineering/stack-profile.yaml +61 -0
  61. package/core/modules/dotnet/architecture-snippets/clean-arch.md +160 -0
  62. package/core/modules/dotnet/module.yaml +6 -0
  63. package/core/modules/dotnet/stack-profile.yaml +50 -0
  64. package/core/modules/golang/architecture-snippets/domain-layout.md +283 -0
  65. package/core/modules/golang/module.yaml +6 -0
  66. package/core/modules/golang/stack-profile.yaml +40 -0
  67. package/core/modules/java-spring/architecture-snippets/layered-arch.md +201 -0
  68. package/core/modules/java-spring/module.yaml +15 -0
  69. package/core/modules/java-spring/stack-profile.yaml +28 -0
  70. package/core/modules/nextjs/architecture-snippets/app-router-patterns.md +269 -0
  71. package/core/modules/nextjs/module.yaml +14 -0
  72. package/core/modules/nextjs/stack-profile.yaml +74 -0
  73. package/core/modules/php-laravel/architecture-snippets/service-repository.md +302 -0
  74. package/core/modules/php-laravel/module.yaml +15 -0
  75. package/core/modules/php-laravel/stack-profile.yaml +56 -0
  76. package/core/modules/react/architecture-snippets/hooks-query-patterns.md +254 -0
  77. package/core/modules/react/module.yaml +14 -0
  78. package/core/modules/react/stack-profile.yaml +63 -0
  79. package/core/rules/data-protection.md +80 -0
  80. package/core/rules/workflow.md +44 -0
  81. package/core/skills/code/SKILL.md +526 -0
  82. package/core/skills/debug/SKILL.md +584 -0
  83. package/core/skills/discovery/SKILL.md +363 -0
  84. package/core/skills/prd/SKILL.md +456 -0
  85. package/core/skills/setup-ai-first/SKILL.md +160 -0
  86. package/core/skills/spec/SKILL.md +361 -0
  87. package/core/skills/test/SKILL.md +862 -0
  88. package/core/steps/context-loader.md +163 -0
  89. package/core/steps/gate.md +81 -0
  90. package/core/steps/report-footer.md +53 -0
  91. package/core/steps/spawn-agent.md +123 -0
  92. package/core/templates/architecture.template.md +113 -0
  93. package/core/templates/feature.template +259 -0
  94. package/core/templates/platform-guide.template.md +145 -0
  95. package/core/templates/prd.template.md +312 -0
  96. package/core/templates/product-definition.template.md +168 -0
  97. package/core/templates/project-context.yaml +78 -0
  98. package/hooks/data-guard.js +141 -0
  99. package/hooks/settings.json +18 -0
  100. package/modules/angular/architecture-snippets/component-patterns.md +187 -0
  101. package/modules/angular/module.yaml +6 -0
  102. package/modules/angular/stack-profile.yaml +38 -0
  103. package/modules/context-engineering/architecture-snippets/context-design.md +119 -0
  104. package/modules/context-engineering/module.yaml +9 -0
  105. package/modules/context-engineering/stack-profile.yaml +61 -0
  106. package/modules/dotnet/architecture-snippets/clean-arch.md +160 -0
  107. package/modules/dotnet/module.yaml +6 -0
  108. package/modules/dotnet/stack-profile.yaml +50 -0
  109. package/modules/golang/architecture-snippets/domain-layout.md +283 -0
  110. package/modules/golang/module.yaml +6 -0
  111. package/modules/golang/stack-profile.yaml +40 -0
  112. package/modules/java-spring/architecture-snippets/layered-arch.md +201 -0
  113. package/modules/java-spring/module.yaml +15 -0
  114. package/modules/java-spring/stack-profile.yaml +28 -0
  115. package/modules/nextjs/architecture-snippets/app-router-patterns.md +269 -0
  116. package/modules/nextjs/module.yaml +14 -0
  117. package/modules/nextjs/stack-profile.yaml +74 -0
  118. package/modules/php-laravel/architecture-snippets/service-repository.md +302 -0
  119. package/modules/php-laravel/module.yaml +15 -0
  120. package/modules/php-laravel/stack-profile.yaml +56 -0
  121. package/modules/react/architecture-snippets/hooks-query-patterns.md +254 -0
  122. package/modules/react/module.yaml +14 -0
  123. package/modules/react/stack-profile.yaml +63 -0
  124. package/package.json +42 -0
  125. package/rules/data-protection.md +80 -0
  126. package/rules/workflow.md +44 -0
  127. package/scripts/init.sh +49 -0
  128. package/scripts/upgrade.sh +94 -0
  129. package/skills/code/SKILL.md +526 -0
  130. package/skills/code/SKILL.tmpl +176 -0
  131. package/skills/debug/SKILL.md +584 -0
  132. package/skills/debug/SKILL.tmpl +262 -0
  133. package/skills/discovery/SKILL.md +363 -0
  134. package/skills/discovery/SKILL.tmpl +147 -0
  135. package/skills/prd/SKILL.md +456 -0
  136. package/skills/prd/SKILL.tmpl +188 -0
  137. package/skills/setup-ai-first/SKILL.md +160 -0
  138. package/skills/setup-ai-first/SKILL.tmpl +107 -0
  139. package/skills/spec/SKILL.md +361 -0
  140. package/skills/spec/SKILL.tmpl +174 -0
  141. package/skills/test/SKILL.md +862 -0
  142. package/skills/test/SKILL.tmpl +296 -0
  143. package/steps/context-loader.md +163 -0
  144. package/steps/gate.md +81 -0
  145. package/steps/report-footer.md +53 -0
  146. package/steps/spawn-agent.md +123 -0
  147. package/templates/architecture.template.md +113 -0
  148. package/templates/feature.template +259 -0
  149. package/templates/platform-guide.template.md +145 -0
  150. package/templates/prd.template.md +312 -0
  151. package/templates/product-definition.template.md +168 -0
  152. package/templates/project-context.yaml +78 -0
@@ -0,0 +1,302 @@
1
+ # PHP Laravel — Service-Repository Architecture Patterns
2
+
3
+ ## Form Request (Validation Layer)
4
+
5
+ ```php
6
+ <?php
7
+ // app/Http/Requests/Order/CreateOrderRequest.php
8
+
9
+ namespace App\Http\Requests\Order;
10
+
11
+ use Illuminate\Foundation\Http\FormRequest;
12
+
13
+ class CreateOrderRequest extends FormRequest
14
+ {
15
+ public function authorize(): bool
16
+ {
17
+ return true; // auth handled by middleware
18
+ }
19
+
20
+ public function rules(): array
21
+ {
22
+ return [
23
+ 'customer_id' => ['required', 'integer', 'exists:customers,id'],
24
+ 'items' => ['required', 'array', 'min:1'],
25
+ 'items.*.product_id' => ['required', 'integer', 'exists:products,id'],
26
+ 'items.*.quantity' => ['required', 'integer', 'min:1'],
27
+ ];
28
+ }
29
+
30
+ public function messages(): array
31
+ {
32
+ return [
33
+ 'items.required' => 'At least one order item is required.',
34
+ ];
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## Repository Interface + Eloquent Implementation
40
+
41
+ ```php
42
+ <?php
43
+ // app/Repositories/OrderRepositoryInterface.php
44
+
45
+ namespace App\Repositories;
46
+
47
+ use App\Models\Order;
48
+ use Illuminate\Contracts\Pagination\LengthAwarePaginator;
49
+
50
+ interface OrderRepositoryInterface
51
+ {
52
+ public function findById(int $id): ?Order;
53
+ public function findByCustomer(int $customerId, int $perPage = 15): LengthAwarePaginator;
54
+ public function create(array $data): Order;
55
+ public function updateStatus(int $id, string $status): bool;
56
+ }
57
+ ```
58
+
59
+ ```php
60
+ <?php
61
+ // app/Repositories/Eloquent/OrderRepository.php
62
+
63
+ namespace App\Repositories\Eloquent;
64
+
65
+ use App\Models\Order;
66
+ use App\Repositories\OrderRepositoryInterface;
67
+ use Illuminate\Contracts\Pagination\LengthAwarePaginator;
68
+
69
+ class OrderRepository implements OrderRepositoryInterface
70
+ {
71
+ public function findById(int $id): ?Order
72
+ {
73
+ return Order::with(['items.product', 'customer'])->find($id);
74
+ }
75
+
76
+ public function findByCustomer(int $customerId, int $perPage = 15): LengthAwarePaginator
77
+ {
78
+ return Order::where('customer_id', $customerId)
79
+ ->with('items')
80
+ ->latest()
81
+ ->paginate($perPage);
82
+ }
83
+
84
+ public function create(array $data): Order
85
+ {
86
+ return Order::create($data);
87
+ }
88
+
89
+ public function updateStatus(int $id, string $status): bool
90
+ {
91
+ return Order::where('id', $id)->update(['status' => $status]) > 0;
92
+ }
93
+ }
94
+ ```
95
+
96
+ ## Service Layer
97
+
98
+ ```php
99
+ <?php
100
+ // app/Services/Order/OrderService.php
101
+
102
+ namespace App\Services\Order;
103
+
104
+ use App\Exceptions\BusinessException;
105
+ use App\Models\Order;
106
+ use App\Repositories\OrderRepositoryInterface;
107
+ use Illuminate\Support\Facades\DB;
108
+
109
+ class OrderService
110
+ {
111
+ // @trace.implements=ORD-UC1-SC1
112
+ // @trace.source=specs/bdd/order/ORD-UC1.feature
113
+ public function __construct(
114
+ private readonly OrderRepositoryInterface $orderRepository
115
+ ) {}
116
+
117
+ public function createOrder(int $customerId, array $items): Order
118
+ {
119
+ return DB::transaction(function () use ($customerId, $items) {
120
+ $order = $this->orderRepository->create([
121
+ 'customer_id' => $customerId,
122
+ 'status' => 'pending',
123
+ ]);
124
+
125
+ foreach ($items as $item) {
126
+ $order->items()->create([
127
+ 'product_id' => $item['product_id'],
128
+ 'quantity' => $item['quantity'],
129
+ ]);
130
+ }
131
+
132
+ return $order->load('items.product');
133
+ });
134
+ }
135
+
136
+ public function cancelOrder(int $orderId): void
137
+ {
138
+ $order = $this->orderRepository->findById($orderId);
139
+
140
+ if (!$order) {
141
+ throw new BusinessException("Order #{$orderId} not found.", 404);
142
+ }
143
+
144
+ if ($order->status !== 'pending') {
145
+ throw new BusinessException("Only pending orders can be cancelled.", 422);
146
+ }
147
+
148
+ $this->orderRepository->updateStatus($orderId, 'cancelled');
149
+ }
150
+ }
151
+ ```
152
+
153
+ ## Resource Controller
154
+
155
+ ```php
156
+ <?php
157
+ // app/Http/Controllers/Order/OrderController.php
158
+
159
+ namespace App\Http\Controllers\Order;
160
+
161
+ use App\Http\Controllers\Controller;
162
+ use App\Http\Requests\Order\CreateOrderRequest;
163
+ use App\Http\Resources\Order\OrderResource;
164
+ use App\Services\Order\OrderService;
165
+ use Illuminate\Http\JsonResponse;
166
+ use Illuminate\Http\Request;
167
+
168
+ class OrderController extends Controller
169
+ {
170
+ public function __construct(
171
+ private readonly OrderService $orderService
172
+ ) {}
173
+
174
+ // @trace.implements=ORD-UC1-SC1
175
+ // @trace.source=specs/bdd/order/ORD-UC1.feature
176
+ public function store(CreateOrderRequest $request): JsonResponse
177
+ {
178
+ $order = $this->orderService->createOrder(
179
+ customerId: $request->validated('customer_id'),
180
+ items: $request->validated('items')
181
+ );
182
+
183
+ return response()->json([
184
+ 'status' => 'success',
185
+ 'message' => 'Order created successfully.',
186
+ 'data' => new OrderResource($order),
187
+ ], 201);
188
+ }
189
+
190
+ // @trace.implements=ORD-UC2-SC1
191
+ // @trace.source=specs/bdd/order/ORD-UC2.feature
192
+ public function destroy(int $id): JsonResponse
193
+ {
194
+ $this->orderService->cancelOrder($id);
195
+
196
+ return response()->json([
197
+ 'status' => 'success',
198
+ 'message' => 'Order cancelled.',
199
+ ]);
200
+ }
201
+ }
202
+ ```
203
+
204
+ ## API Resource (Response Transformer)
205
+
206
+ ```php
207
+ <?php
208
+ // app/Http/Resources/Order/OrderResource.php
209
+
210
+ namespace App\Http\Resources\Order;
211
+
212
+ use Illuminate\Http\Request;
213
+ use Illuminate\Http\Resources\Json\JsonResource;
214
+
215
+ class OrderResource extends JsonResource
216
+ {
217
+ public function toArray(Request $request): array
218
+ {
219
+ return [
220
+ 'id' => $this->id,
221
+ 'status' => $this->status,
222
+ 'customer_id' => $this->customer_id,
223
+ 'items' => OrderItemResource::collection($this->whenLoaded('items')),
224
+ 'created_at' => $this->created_at->toISOString(),
225
+ ];
226
+ }
227
+ }
228
+ ```
229
+
230
+ ## Feature Test (HTTP Layer)
231
+
232
+ ```php
233
+ <?php
234
+ // tests/Feature/Order/CreateOrderTest.php
235
+
236
+ namespace Tests\Feature\Order;
237
+
238
+ use App\Models\Customer;
239
+ use App\Models\Product;
240
+ use Illuminate\Foundation\Testing\RefreshDatabase;
241
+ use Tests\TestCase;
242
+
243
+ // @trace.verifies=ORD-UC1
244
+ // @trace.test_type=feature
245
+ class CreateOrderTest extends TestCase
246
+ {
247
+ use RefreshDatabase;
248
+
249
+ /** @test */
250
+ public function it_creates_order_successfully(): void
251
+ {
252
+ $customer = Customer::factory()->create();
253
+ $product = Product::factory()->create(['stock' => 10]);
254
+
255
+ $response = $this->actingAs($customer)
256
+ ->postJson('/api/v1/orders', [
257
+ 'customer_id' => $customer->id,
258
+ 'items' => [
259
+ ['product_id' => $product->id, 'quantity' => 2],
260
+ ],
261
+ ]);
262
+
263
+ $response->assertStatus(201)
264
+ ->assertJsonStructure(['status', 'data' => ['id', 'status', 'items']]);
265
+
266
+ $this->assertDatabaseHas('orders', ['customer_id' => $customer->id, 'status' => 'pending']);
267
+ }
268
+
269
+ /** @test */
270
+ public function it_returns_422_when_items_are_empty(): void
271
+ {
272
+ $customer = Customer::factory()->create();
273
+
274
+ $response = $this->actingAs($customer)
275
+ ->postJson('/api/v1/orders', ['customer_id' => $customer->id, 'items' => []]);
276
+
277
+ $response->assertStatus(422)
278
+ ->assertJsonValidationErrors(['items']);
279
+ }
280
+ }
281
+ ```
282
+
283
+ ## Service Provider Binding
284
+
285
+ ```php
286
+ <?php
287
+ // app/Providers/RepositoryServiceProvider.php
288
+
289
+ namespace App\Providers;
290
+
291
+ use App\Repositories\Eloquent\OrderRepository;
292
+ use App\Repositories\OrderRepositoryInterface;
293
+ use Illuminate\Support\ServiceProvider;
294
+
295
+ class RepositoryServiceProvider extends ServiceProvider
296
+ {
297
+ public function register(): void
298
+ {
299
+ $this->app->bind(OrderRepositoryInterface::class, OrderRepository::class);
300
+ }
301
+ }
302
+ ```
@@ -0,0 +1,15 @@
1
+ name: "PHP Laravel"
2
+ version: "1.0.0"
3
+ description: "Laravel 11 backend with service-repository pattern"
4
+ language: "PHP"
5
+ framework: "Laravel"
6
+ stack_type: "backend"
7
+ default_layer_order:
8
+ - Form Request (validation)
9
+ - Model / Eloquent
10
+ - Repository interface
11
+ - Repository impl
12
+ - Service
13
+ - Controller (Resource Controller)
14
+ - Resource (API response transformer)
15
+ test_framework: "PHPUnit + Pest"
@@ -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"