@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,269 @@
1
+ # Next.js App Router — Server Components, Server Actions & DAL Patterns
2
+
3
+ ## Data Access Layer (server-only)
4
+
5
+ ```typescript
6
+ // lib/dal/orders.dal.ts
7
+ // @trace.source=specs/bdd/order/ORD-UC1.feature
8
+ import 'server-only'; // prevents accidental import on client
9
+ import { db } from '@/lib/db';
10
+ import type { Order } from '@/types/order';
11
+
12
+ export async function getOrderById(id: number): Promise<Order | null> {
13
+ return db.order.findUnique({
14
+ where: { id },
15
+ include: { items: { include: { product: true } }, customer: true },
16
+ });
17
+ }
18
+
19
+ export async function listOrdersByCustomer(customerId: number): Promise<Order[]> {
20
+ return db.order.findMany({
21
+ where: { customerId },
22
+ include: { items: true },
23
+ orderBy: { createdAt: 'desc' },
24
+ });
25
+ }
26
+ ```
27
+
28
+ ## Server Component (Data Fetching + Layout)
29
+
30
+ ```tsx
31
+ // app/(dashboard)/orders/page.tsx
32
+ // @trace.implements=ORD-UC1-SC1
33
+ // @trace.source=specs/bdd/order/ORD-UC1.feature
34
+ import { listOrdersByCustomer } from '@/lib/dal/orders.dal';
35
+ import { getCurrentUser } from '@/lib/auth';
36
+ import { OrderCard } from './_components/OrderCard';
37
+ import { EmptyState } from '@/components/ui/EmptyState';
38
+
39
+ export default async function OrdersPage() {
40
+ const user = await getCurrentUser();
41
+ const orders = await listOrdersByCustomer(user.id);
42
+
43
+ return (
44
+ <section>
45
+ <h1>My Orders</h1>
46
+ {orders.length === 0 ? (
47
+ <EmptyState message="No orders yet." />
48
+ ) : (
49
+ <ul>
50
+ {orders.map(order => (
51
+ <OrderCard key={order.id} order={order} />
52
+ ))}
53
+ </ul>
54
+ )}
55
+ </section>
56
+ );
57
+ }
58
+ ```
59
+
60
+ ## Zod Schema (shared server + client)
61
+
62
+ ```typescript
63
+ // lib/validations/order.schema.ts
64
+ import { z } from 'zod';
65
+
66
+ export const createOrderSchema = z.object({
67
+ customerId: z.number().positive(),
68
+ items: z.array(z.object({
69
+ productId: z.number().positive(),
70
+ quantity: z.number().int().min(1, 'Quantity must be at least 1'),
71
+ })).min(1, 'At least one item is required'),
72
+ });
73
+
74
+ export type CreateOrderInput = z.infer<typeof createOrderSchema>;
75
+ ```
76
+
77
+ ## Server Action (Mutation)
78
+
79
+ ```typescript
80
+ // lib/actions/order.actions.ts
81
+ 'use server';
82
+ // @trace.implements=ORD-UC2-SC1
83
+ // @trace.source=specs/bdd/order/ORD-UC2.feature
84
+
85
+ import { revalidatePath } from 'next/cache';
86
+ import { redirect } from 'next/navigation';
87
+ import { createOrderSchema } from '@/lib/validations/order.schema';
88
+ import { db } from '@/lib/db';
89
+ import { getCurrentUser } from '@/lib/auth';
90
+
91
+ // Returns { error } on failure, redirects on success
92
+ export async function createOrder(formData: FormData) {
93
+ const user = await getCurrentUser();
94
+
95
+ const parsed = createOrderSchema.safeParse({
96
+ customerId: user.id,
97
+ items: JSON.parse(formData.get('items') as string),
98
+ });
99
+
100
+ if (!parsed.success) {
101
+ return { error: parsed.error.flatten().fieldErrors };
102
+ }
103
+
104
+ try {
105
+ await db.order.create({
106
+ data: {
107
+ customerId: parsed.data.customerId,
108
+ status: 'pending',
109
+ items: {
110
+ create: parsed.data.items.map(item => ({
111
+ productId: item.productId,
112
+ quantity: item.quantity,
113
+ })),
114
+ },
115
+ },
116
+ });
117
+ } catch {
118
+ return { error: { _form: ['Failed to create order. Please try again.'] } };
119
+ }
120
+
121
+ revalidatePath('/orders');
122
+ redirect('/orders');
123
+ }
124
+
125
+ export async function cancelOrder(orderId: number) {
126
+ const user = await getCurrentUser();
127
+
128
+ const order = await db.order.findFirst({
129
+ where: { id: orderId, customerId: user.id },
130
+ });
131
+
132
+ if (!order) return { error: 'Order not found.' };
133
+ if (order.status !== 'pending') return { error: 'Only pending orders can be cancelled.' };
134
+
135
+ await db.order.update({ where: { id: orderId }, data: { status: 'cancelled' } });
136
+
137
+ revalidatePath('/orders');
138
+ return { success: true };
139
+ }
140
+ ```
141
+
142
+ ## Client Component (Interactive UI)
143
+
144
+ ```tsx
145
+ // app/(dashboard)/orders/_components/CancelOrderButton.tsx
146
+ 'use client';
147
+ // @trace.implements=ORD-UC3-SC1
148
+ // @trace.source=specs/bdd/order/ORD-UC3.feature
149
+ import { useTransition } from 'react';
150
+ import { cancelOrder } from '@/lib/actions/order.actions';
151
+
152
+ interface Props {
153
+ orderId: number;
154
+ disabled?: boolean;
155
+ }
156
+
157
+ export function CancelOrderButton({ orderId, disabled }: Props) {
158
+ const [isPending, startTransition] = useTransition();
159
+
160
+ const handleCancel = () => {
161
+ startTransition(async () => {
162
+ const result = await cancelOrder(orderId);
163
+ if (result?.error) {
164
+ alert(result.error); // replace with toast in real app
165
+ }
166
+ });
167
+ };
168
+
169
+ return (
170
+ <button
171
+ onClick={handleCancel}
172
+ disabled={disabled || isPending}
173
+ className="btn-danger"
174
+ >
175
+ {isPending ? 'Cancelling...' : 'Cancel Order'}
176
+ </button>
177
+ );
178
+ }
179
+ ```
180
+
181
+ ## API Route (for webhooks / external consumers)
182
+
183
+ ```typescript
184
+ // app/api/orders/webhook/route.ts
185
+ import { NextRequest, NextResponse } from 'next/server';
186
+ import { verifyWebhookSignature } from '@/lib/webhook';
187
+ import { db } from '@/lib/db';
188
+
189
+ export async function POST(request: NextRequest) {
190
+ const body = await request.json();
191
+
192
+ if (!verifyWebhookSignature(request, body)) {
193
+ return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
194
+ }
195
+
196
+ if (body.event === 'payment.completed') {
197
+ await db.order.update({
198
+ where: { id: body.orderId },
199
+ data: { status: 'paid', paidAt: new Date() },
200
+ });
201
+ }
202
+
203
+ return NextResponse.json({ received: true });
204
+ }
205
+ ```
206
+
207
+ ## Loading + Error Boundaries
208
+
209
+ ```tsx
210
+ // app/(dashboard)/orders/loading.tsx
211
+ export default function OrdersLoading() {
212
+ return (
213
+ <div className="order-skeleton">
214
+ {Array.from({ length: 3 }).map((_, i) => (
215
+ <div key={i} className="skeleton-card animate-pulse" />
216
+ ))}
217
+ </div>
218
+ );
219
+ }
220
+ ```
221
+
222
+ ```tsx
223
+ // app/(dashboard)/orders/error.tsx
224
+ 'use client';
225
+ export default function OrdersError({ error, reset }: {
226
+ error: Error & { digest?: string };
227
+ reset: () => void;
228
+ }) {
229
+ return (
230
+ <div className="error-state">
231
+ <p>Failed to load orders: {error.message}</p>
232
+ <button onClick={reset}>Try again</button>
233
+ </div>
234
+ );
235
+ }
236
+ ```
237
+
238
+ ## Playwright E2E Test
239
+
240
+ ```typescript
241
+ // e2e/orders/create-order.spec.ts
242
+ // @trace.verifies=ORD-UC2
243
+ // @trace.test_type=e2e
244
+ import { test, expect } from '@playwright/test';
245
+ import { loginAs } from '../fixtures/auth';
246
+
247
+ test.describe('Create Order', () => {
248
+ test.use({ storageState: 'e2e/.auth/customer.json' }); // reuse auth state
249
+
250
+ test('customer creates an order successfully', async ({ page }) => {
251
+ await page.goto('/orders/new');
252
+
253
+ await page.getByLabel('Product').selectOption({ label: 'Wireless Headphones' });
254
+ await page.getByLabel('Quantity').fill('2');
255
+ await page.getByRole('button', { name: 'Add to Cart' }).click();
256
+ await page.getByRole('button', { name: 'Place Order' }).click();
257
+
258
+ await expect(page).toHaveURL(/\/orders\/\d+/);
259
+ await expect(page.getByText('Order placed successfully')).toBeVisible();
260
+ });
261
+
262
+ test('shows validation error when cart is empty', async ({ page }) => {
263
+ await page.goto('/orders/new');
264
+ await page.getByRole('button', { name: 'Place Order' }).click();
265
+
266
+ await expect(page.getByText('At least one item is required')).toBeVisible();
267
+ });
268
+ });
269
+ ```
@@ -0,0 +1,14 @@
1
+ name: "Next.js"
2
+ version: "1.0.0"
3
+ description: "Next.js 14+ App Router with Server Components, Server Actions, and React Query"
4
+ language: "TypeScript"
5
+ framework: "Next.js"
6
+ stack_type: "fullstack"
7
+ default_layer_order:
8
+ - Types / interfaces
9
+ - Server Actions (mutations)
10
+ - Data access layer (DAL) — server-side DB/API calls
11
+ - Server Components (data fetching + layout)
12
+ - Client Components (interactive UI)
13
+ - API Routes (for external consumers / webhooks)
14
+ test_framework: "Vitest + React Testing Library + Playwright (E2E)"
@@ -0,0 +1,74 @@
1
+ build:
2
+ compile: "npm run build"
3
+ test: "npm test"
4
+ run: "npm run dev"
5
+ lint: "npm run lint"
6
+ e2e: "npx playwright test"
7
+
8
+ architecture:
9
+ style: "App Router — Server Components + Server Actions + Client Components"
10
+ key_rules:
11
+ - "Default to Server Components — only add 'use client' when interactivity is needed"
12
+ - "Data fetching happens in Server Components or Server Actions — never in useEffect"
13
+ - "Server Actions handle all mutations (form submits, data writes)"
14
+ - "Data Access Layer (DAL) abstracts all DB/external API calls — used only on server"
15
+ - "Client Components are 'leaf nodes' — keep them small and focused on interactivity"
16
+ - "API Routes (/api) used only for external consumers or webhooks, not for own UI"
17
+ - "Use Next.js cache() and unstable_cache for data memoization"
18
+ folder_structure: |
19
+ app/
20
+ ├── (auth)/ ← route group: auth pages
21
+ ├── (dashboard)/ ← route group: authenticated pages
22
+ │ └── orders/
23
+ │ ├── page.tsx ← Server Component (list page)
24
+ │ ├── [id]/
25
+ │ │ └── page.tsx ← Server Component (detail page)
26
+ │ └── _components/ ← feature-scoped Client Components
27
+ ├── api/ ← API Routes (webhooks, external consumers)
28
+ └── layout.tsx
29
+ lib/
30
+ ├── dal/ ← Data Access Layer (server-only DB/API calls)
31
+ │ └── orders.dal.ts
32
+ ├── actions/ ← Server Actions
33
+ │ └── order.actions.ts
34
+ └── validations/ ← Zod schemas (shared server+client)
35
+ └── order.schema.ts
36
+ components/
37
+ └── ui/ ← shared primitive Client Components
38
+
39
+ coding_standards:
40
+ naming:
41
+ server_components: "PascalCase, no suffix — co-locate in app/ route folder"
42
+ client_components: "PascalCase + 'use client' at top — in _components/ folders"
43
+ server_actions: "camelCase verb + noun (e.g., createOrder, cancelOrder) in lib/actions/"
44
+ dal_functions: "camelCase (e.g., getOrderById, listOrdersByCustomer) in lib/dal/"
45
+ files:
46
+ page: "app/{route}/page.tsx"
47
+ layout: "app/{route}/layout.tsx"
48
+ server_action: "lib/actions/{domain}.actions.ts"
49
+ dal: "lib/dal/{domain}.dal.ts"
50
+ schema: "lib/validations/{domain}.schema.ts"
51
+ patterns:
52
+ mutations: "Server Actions with useFormState / useTransition"
53
+ data_fetching: "Server Component async/await + fetch with revalidate options"
54
+ client_state: "Zustand or Context API (avoid for server-derived data)"
55
+ forms: "React Hook Form (client) OR native <form> with Server Actions"
56
+ error_handling: "error.tsx (route-level) + try/catch in Server Actions returning {error}"
57
+ loading: "loading.tsx (Suspense boundary) per route segment"
58
+ auth: "next-auth v5 (Auth.js) or middleware-based session check"
59
+
60
+ testing:
61
+ unit: "Vitest + React Testing Library for Client Components"
62
+ integration: "Vitest + MSW for Server Actions"
63
+ e2e: "Playwright — full browser tests"
64
+ patterns:
65
+ - "Test Server Components by rendering with async render utilities"
66
+ - "Mock fetch/DB calls for Server Action tests"
67
+ - "Playwright tests cover full user flows (auth → action → result)"
68
+ - "Use @playwright/test fixtures for auth state reuse"
69
+
70
+ trace_tags:
71
+ implements: "// @trace.implements={UC-ID}-SC{N}"
72
+ source: "// @trace.source=specs/bdd/{domain}/{UC-ID}.feature"
73
+ verifies: "// @trace.verifies={UC-ID}"
74
+ test_type: "// @trace.test_type=unit|e2e"
@@ -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"