moicle 1.4.0 → 1.7.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/README.md +25 -7
- package/assets/agents/developers/nodejs-backend-dev.md +92 -0
- package/assets/agents/developers/react-frontend-dev.md +32 -19
- package/assets/architecture/nodejs-nestjs.md +949 -0
- package/assets/architecture/react-frontend.md +216 -145
- package/assets/skills/deep-debug/SKILL.md +62 -62
- package/assets/skills/research/SKILL.md +124 -0
- package/assets/skills/review-changes/SKILL.md +312 -0
- package/assets/templates/react-vite/CLAUDE.md +204 -128
- package/bin/cli.js +12 -1
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +219 -38
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +41 -1
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/postinstall.d.ts.map +1 -1
- package/dist/commands/postinstall.js +2 -0
- package/dist/commands/postinstall.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +31 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +93 -38
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +165 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/symlink.d.ts +1 -0
- package/dist/utils/symlink.d.ts.map +1 -1
- package/dist/utils/symlink.js +18 -0
- package/dist/utils/symlink.js.map +1 -1
- package/package.json +3 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# React Frontend Structure
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Module-based architecture with hooks + services. Idiomatic React — no MVVM ceremony.
|
|
4
4
|
|
|
5
5
|
## Project Structure
|
|
6
6
|
|
|
@@ -13,26 +13,21 @@
|
|
|
13
13
|
│ │ ├── app.config.ts
|
|
14
14
|
│ │ └── routes.config.ts
|
|
15
15
|
│ ├── core/
|
|
16
|
-
│ │ ├──
|
|
17
|
-
│ │
|
|
18
|
-
│ │ ├── context/ # Global contexts
|
|
19
|
-
│ │ ├── hooks/ # Core hooks
|
|
20
|
-
│ │ ├── interfaces/ # Core interfaces
|
|
21
|
-
│ │ ├── enums/ # Core enums
|
|
16
|
+
│ │ ├── context/ # Global contexts (auth, theme)
|
|
17
|
+
│ │ ├── hooks/ # Core reusable hooks
|
|
22
18
|
│ │ ├── errors/ # Error handling
|
|
23
19
|
│ │ ├── utils/ # Core utilities
|
|
24
|
-
│ │ ├──
|
|
20
|
+
│ │ ├── http-client.ts # API client
|
|
25
21
|
│ │ └── bootstrap.ts # App initialization
|
|
26
22
|
│ ├── modules/
|
|
27
23
|
│ │ └── {module}/
|
|
28
|
-
│ │ ├──
|
|
29
|
-
│ │ ├── view-models/ # Hooks (state management)
|
|
24
|
+
│ │ ├── types/ # Types, interfaces, DTOs, schemas
|
|
30
25
|
│ │ ├── services/ # API calls
|
|
26
|
+
│ │ ├── hooks/ # Module hooks (state + data fetching)
|
|
31
27
|
│ │ ├── components/ # Module components
|
|
32
28
|
│ │ ├── pages/ # Page components
|
|
33
29
|
│ │ └── index.ts
|
|
34
30
|
│ ├── lib/ # Third-party integrations
|
|
35
|
-
│ ├── services/ # Shared services
|
|
36
31
|
│ ├── utils/ # Shared utilities
|
|
37
32
|
│ ├── router.tsx # Route definitions
|
|
38
33
|
│ ├── index.tsx # Entry point
|
|
@@ -46,56 +41,60 @@
|
|
|
46
41
|
└── tailwind.config.js
|
|
47
42
|
```
|
|
48
43
|
|
|
49
|
-
##
|
|
44
|
+
## Layering
|
|
50
45
|
|
|
51
46
|
```
|
|
52
47
|
┌─────────────────────────────────────────────────┐
|
|
53
|
-
│
|
|
54
|
-
│
|
|
48
|
+
│ Pages / Components │
|
|
49
|
+
│ (render UI) │
|
|
55
50
|
├─────────────────────────────────────────────────┤
|
|
56
|
-
│
|
|
57
|
-
│
|
|
51
|
+
│ Hooks │
|
|
52
|
+
│ (state, queries, mutations, business logic) │
|
|
58
53
|
├─────────────────────────────────────────────────┤
|
|
59
|
-
│
|
|
60
|
-
│
|
|
54
|
+
│ Services │
|
|
55
|
+
│ (pure API calls) │
|
|
56
|
+
├─────────────────────────────────────────────────┤
|
|
57
|
+
│ Types │
|
|
58
|
+
│ (models, DTOs, Zod schemas) │
|
|
61
59
|
└─────────────────────────────────────────────────┘
|
|
62
60
|
```
|
|
63
61
|
|
|
64
|
-
**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
**Rules:**
|
|
63
|
+
- Components consume hooks, not services directly
|
|
64
|
+
- Hooks orchestrate services + state (TanStack Query, Zustand, useState)
|
|
65
|
+
- Services are pure functions: input → API → output
|
|
66
|
+
- Types are the contract shared across all layers
|
|
69
67
|
|
|
70
68
|
## Module Structure
|
|
71
69
|
|
|
72
70
|
```
|
|
73
71
|
modules/{module}/
|
|
74
|
-
├──
|
|
72
|
+
├── types/
|
|
75
73
|
│ ├── {module}.model.ts # Types & interfaces
|
|
76
74
|
│ ├── {module}.schema.ts # Zod schemas (validation)
|
|
77
75
|
│ └── index.ts
|
|
78
|
-
├── view-models/
|
|
79
|
-
│ ├── use-{module}-list.ts # List ViewModel
|
|
80
|
-
│ ├── use-{module}-form.ts # Form ViewModel (optional)
|
|
81
|
-
│ └── index.ts
|
|
82
76
|
├── services/
|
|
83
77
|
│ ├── {module}.service.ts # API functions
|
|
84
78
|
│ └── index.ts
|
|
79
|
+
├── hooks/
|
|
80
|
+
│ ├── use-{module}-list.ts # List query hook
|
|
81
|
+
│ ├── use-{module}-detail.ts # Detail query hook
|
|
82
|
+
│ ├── use-{module}-mutations.ts # Create/update/delete mutations
|
|
83
|
+
│ └── index.ts
|
|
85
84
|
├── components/
|
|
86
|
-
│ ├── {
|
|
87
|
-
│ ├── {
|
|
85
|
+
│ ├── {module}-table.tsx
|
|
86
|
+
│ ├── {module}-form.tsx
|
|
88
87
|
│ └── index.ts
|
|
89
88
|
├── pages/
|
|
90
|
-
│ ├── {
|
|
89
|
+
│ ├── {module}-list-page.tsx
|
|
91
90
|
│ └── index.ts
|
|
92
91
|
└── index.ts
|
|
93
92
|
```
|
|
94
93
|
|
|
95
94
|
## Key Files
|
|
96
95
|
|
|
97
|
-
###
|
|
98
|
-
```
|
|
96
|
+
### types/user.model.ts
|
|
97
|
+
```ts
|
|
99
98
|
export interface User {
|
|
100
99
|
id: string;
|
|
101
100
|
name: string;
|
|
@@ -110,8 +109,8 @@ export interface CreateUserRequest {
|
|
|
110
109
|
password: string;
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
export interface
|
|
114
|
-
data:
|
|
112
|
+
export interface Paginated<T> {
|
|
113
|
+
data: T[];
|
|
115
114
|
meta: {
|
|
116
115
|
currentPage: number;
|
|
117
116
|
lastPage: number;
|
|
@@ -119,150 +118,222 @@ export interface UserListResponse {
|
|
|
119
118
|
total: number;
|
|
120
119
|
};
|
|
121
120
|
}
|
|
121
|
+
|
|
122
|
+
export interface UserListParams {
|
|
123
|
+
page?: number;
|
|
124
|
+
pageSize?: number;
|
|
125
|
+
search?: string;
|
|
126
|
+
sort?: { field: keyof User; order: 'asc' | 'desc' };
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### types/user.schema.ts
|
|
131
|
+
```ts
|
|
132
|
+
import { z } from 'zod';
|
|
133
|
+
|
|
134
|
+
export const createUserSchema = z.object({
|
|
135
|
+
name: z.string().min(1, 'Name is required'),
|
|
136
|
+
email: z.string().email('Invalid email'),
|
|
137
|
+
password: z.string().min(8, 'Min 8 characters'),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export type CreateUserFormData = z.infer<typeof createUserSchema>;
|
|
122
141
|
```
|
|
123
142
|
|
|
124
143
|
### services/user.service.ts
|
|
125
|
-
```
|
|
126
|
-
import
|
|
127
|
-
import { User, CreateUserRequest } from '../
|
|
144
|
+
```ts
|
|
145
|
+
import { httpClient } from '@/core/http-client';
|
|
146
|
+
import type { User, CreateUserRequest, Paginated, UserListParams } from '../types/user.model';
|
|
128
147
|
|
|
129
|
-
export const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
export const userService = {
|
|
149
|
+
list: (params: UserListParams) =>
|
|
150
|
+
httpClient.get<Paginated<User>>('/users', { params }),
|
|
151
|
+
|
|
152
|
+
detail: (id: string) =>
|
|
153
|
+
httpClient.get<User>(`/users/${id}`),
|
|
154
|
+
|
|
155
|
+
create: (data: CreateUserRequest) =>
|
|
156
|
+
httpClient.post<User>('/users', data),
|
|
157
|
+
|
|
158
|
+
update: (id: string, data: Partial<CreateUserRequest>) =>
|
|
159
|
+
httpClient.patch<User>(`/users/${id}`, data),
|
|
160
|
+
|
|
161
|
+
remove: (id: string) =>
|
|
162
|
+
httpClient.delete<void>(`/users/${id}`),
|
|
163
|
+
};
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### hooks/use-user-list.ts
|
|
167
|
+
```ts
|
|
168
|
+
import { useQuery } from '@tanstack/react-query';
|
|
169
|
+
import { userService } from '../services/user.service';
|
|
170
|
+
import type { UserListParams } from '../types/user.model';
|
|
171
|
+
|
|
172
|
+
export const userKeys = {
|
|
173
|
+
all: ['users'] as const,
|
|
174
|
+
list: (params: UserListParams) => [...userKeys.all, 'list', params] as const,
|
|
175
|
+
detail: (id: string) => [...userKeys.all, 'detail', id] as const,
|
|
133
176
|
};
|
|
134
177
|
|
|
135
|
-
export const
|
|
136
|
-
|
|
178
|
+
export const useUserList = (params: UserListParams) => {
|
|
179
|
+
return useQuery({
|
|
180
|
+
queryKey: userKeys.list(params),
|
|
181
|
+
queryFn: () => userService.list(params),
|
|
182
|
+
placeholderData: (prev) => prev,
|
|
183
|
+
});
|
|
137
184
|
};
|
|
138
185
|
```
|
|
139
186
|
|
|
140
|
-
###
|
|
141
|
-
```
|
|
142
|
-
import {
|
|
143
|
-
import {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
187
|
+
### hooks/use-user-mutations.ts
|
|
188
|
+
```ts
|
|
189
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
190
|
+
import { userService } from '../services/user.service';
|
|
191
|
+
import { userKeys } from './use-user-list';
|
|
192
|
+
import type { CreateUserRequest } from '../types/user.model';
|
|
193
|
+
|
|
194
|
+
export const useCreateUser = () => {
|
|
195
|
+
const queryClient = useQueryClient();
|
|
196
|
+
|
|
197
|
+
return useMutation({
|
|
198
|
+
mutationFn: (data: CreateUserRequest) => userService.create(data),
|
|
199
|
+
onSuccess: () => {
|
|
200
|
+
queryClient.invalidateQueries({ queryKey: userKeys.all });
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
};
|
|
152
204
|
|
|
153
|
-
|
|
205
|
+
export const useDeleteUser = () => {
|
|
206
|
+
const queryClient = useQueryClient();
|
|
207
|
+
|
|
208
|
+
return useMutation({
|
|
209
|
+
mutationFn: (id: string) => userService.remove(id),
|
|
210
|
+
onSuccess: () => {
|
|
211
|
+
queryClient.invalidateQueries({ queryKey: userKeys.all });
|
|
212
|
+
},
|
|
213
|
+
});
|
|
154
214
|
};
|
|
155
215
|
```
|
|
156
216
|
|
|
157
|
-
### pages/
|
|
217
|
+
### pages/user-list-page.tsx
|
|
158
218
|
```tsx
|
|
159
219
|
import { useState } from 'react';
|
|
160
|
-
import {
|
|
161
|
-
import {
|
|
162
|
-
import { UserTable } from '../components/
|
|
163
|
-
import {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const [
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
handleTableChange,
|
|
174
|
-
reload,
|
|
175
|
-
handleInputSearch,
|
|
176
|
-
} = useUserList();
|
|
177
|
-
|
|
178
|
-
const handleEdit = (record: User) => {
|
|
179
|
-
setSelectedUser(record);
|
|
180
|
-
setModalOpen(true);
|
|
181
|
-
};
|
|
220
|
+
import { useUserList } from '../hooks/use-user-list';
|
|
221
|
+
import { useDeleteUser } from '../hooks/use-user-mutations';
|
|
222
|
+
import { UserTable } from '../components/user-table';
|
|
223
|
+
import { UserFormDialog } from '../components/user-form-dialog';
|
|
224
|
+
import type { User } from '../types/user.model';
|
|
225
|
+
|
|
226
|
+
export default function UserListPage() {
|
|
227
|
+
const [params, setParams] = useState({ page: 1, pageSize: 20 });
|
|
228
|
+
const [editing, setEditing] = useState<User | null>(null);
|
|
229
|
+
const [formOpen, setFormOpen] = useState(false);
|
|
230
|
+
|
|
231
|
+
const { data, isLoading } = useUserList(params);
|
|
232
|
+
const deleteUser = useDeleteUser();
|
|
182
233
|
|
|
183
234
|
return (
|
|
184
|
-
<div>
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
235
|
+
<div className="container mx-auto p-6">
|
|
236
|
+
<div className="flex justify-between mb-6">
|
|
237
|
+
<h1 className="text-2xl font-bold">Users</h1>
|
|
238
|
+
<Button onClick={() => { setEditing(null); setFormOpen(true); }}>
|
|
239
|
+
Add User
|
|
240
|
+
</Button>
|
|
241
|
+
</div>
|
|
188
242
|
|
|
189
243
|
<UserTable
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
244
|
+
data={data?.data ?? []}
|
|
245
|
+
meta={data?.meta}
|
|
246
|
+
isLoading={isLoading}
|
|
247
|
+
onEdit={(user) => { setEditing(user); setFormOpen(true); }}
|
|
248
|
+
onDelete={(user) => deleteUser.mutate(user.id)}
|
|
249
|
+
onPageChange={(page) => setParams((p) => ({ ...p, page }))}
|
|
196
250
|
/>
|
|
197
251
|
|
|
198
|
-
<
|
|
199
|
-
open={
|
|
200
|
-
onOpenChange={
|
|
201
|
-
user={
|
|
202
|
-
onSuccess={reload}
|
|
252
|
+
<UserFormDialog
|
|
253
|
+
open={formOpen}
|
|
254
|
+
onOpenChange={setFormOpen}
|
|
255
|
+
user={editing}
|
|
203
256
|
/>
|
|
204
257
|
</div>
|
|
205
258
|
);
|
|
206
|
-
}
|
|
259
|
+
}
|
|
207
260
|
```
|
|
208
261
|
|
|
209
|
-
### core/
|
|
210
|
-
```
|
|
262
|
+
### core/http-client.ts
|
|
263
|
+
```ts
|
|
211
264
|
const BASE_URL = import.meta.env.VITE_API_URL;
|
|
212
265
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (!res.ok) throw new Error(res.statusText);
|
|
219
|
-
return res.json();
|
|
220
|
-
},
|
|
221
|
-
|
|
222
|
-
post: async <T>(url: string, options?: RequestInit): Promise<T> => {
|
|
223
|
-
const res = await fetch(`${BASE_URL}${url}`, {
|
|
224
|
-
method: 'POST',
|
|
225
|
-
headers: {
|
|
226
|
-
'Content-Type': 'application/json',
|
|
227
|
-
'Authorization': `Bearer ${getToken()}`,
|
|
228
|
-
},
|
|
229
|
-
...options,
|
|
230
|
-
});
|
|
231
|
-
if (!res.ok) throw new Error(res.statusText);
|
|
232
|
-
return res.json();
|
|
233
|
-
},
|
|
234
|
-
|
|
235
|
-
delete: async (url: string): Promise<void> => {
|
|
236
|
-
await fetch(`${BASE_URL}${url}`, {
|
|
237
|
-
method: 'DELETE',
|
|
238
|
-
headers: { 'Authorization': `Bearer ${getToken()}` },
|
|
266
|
+
const buildUrl = (path: string, params?: Record<string, unknown>) => {
|
|
267
|
+
const url = new URL(`${BASE_URL}${path}`);
|
|
268
|
+
if (params) {
|
|
269
|
+
Object.entries(params).forEach(([k, v]) => {
|
|
270
|
+
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
|
239
271
|
});
|
|
240
|
-
}
|
|
272
|
+
}
|
|
273
|
+
return url.toString();
|
|
241
274
|
};
|
|
242
275
|
|
|
243
|
-
|
|
276
|
+
const request = async <T>(path: string, init: RequestInit & { params?: Record<string, unknown> } = {}): Promise<T> => {
|
|
277
|
+
const { params, ...rest } = init;
|
|
278
|
+
const token = getToken();
|
|
279
|
+
const res = await fetch(buildUrl(path, params), {
|
|
280
|
+
...rest,
|
|
281
|
+
headers: {
|
|
282
|
+
'Content-Type': 'application/json',
|
|
283
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
284
|
+
...rest.headers,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (!res.ok) {
|
|
289
|
+
const body = await res.json().catch(() => ({}));
|
|
290
|
+
throw new HttpError(res.status, body?.message ?? res.statusText, body);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return res.status === 204 ? (undefined as T) : res.json();
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
export const httpClient = {
|
|
297
|
+
get: <T>(path: string, opts?: { params?: Record<string, unknown> }) => request<T>(path, { method: 'GET', ...opts }),
|
|
298
|
+
post: <T>(path: string, body?: unknown) => request<T>(path, { method: 'POST', body: JSON.stringify(body) }),
|
|
299
|
+
patch: <T>(path: string, body?: unknown) => request<T>(path, { method: 'PATCH', body: JSON.stringify(body) }),
|
|
300
|
+
put: <T>(path: string, body?: unknown) => request<T>(path, { method: 'PUT', body: JSON.stringify(body) }),
|
|
301
|
+
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
|
302
|
+
};
|
|
244
303
|
```
|
|
245
304
|
|
|
305
|
+
## State Management
|
|
306
|
+
|
|
307
|
+
Pick the right tool per scope:
|
|
308
|
+
|
|
309
|
+
| Scope | Tool |
|
|
310
|
+
|-------|------|
|
|
311
|
+
| Server state (API data) | TanStack Query / SWR |
|
|
312
|
+
| Component-local state | `useState` / `useReducer` |
|
|
313
|
+
| Cross-component UI state | Context API |
|
|
314
|
+
| Global client state (cart, filters) | Zustand |
|
|
315
|
+
| Form state | React Hook Form + Zod |
|
|
316
|
+
|
|
317
|
+
Do NOT dump everything into Redux/Zustand — server state belongs in a query cache.
|
|
318
|
+
|
|
246
319
|
## Conventions
|
|
247
320
|
|
|
248
321
|
| Item | Convention | Example |
|
|
249
322
|
|------|------------|---------|
|
|
250
|
-
| Module folder | kebab-case | `
|
|
251
|
-
|
|
|
252
|
-
|
|
|
253
|
-
|
|
|
254
|
-
|
|
|
255
|
-
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
```
|
|
323
|
+
| Module folder | kebab-case | `user-profile/` |
|
|
324
|
+
| Type file | kebab-case.model.ts | `user.model.ts` |
|
|
325
|
+
| Schema file | kebab-case.schema.ts | `user.schema.ts` |
|
|
326
|
+
| Service file | kebab-case.service.ts | `user.service.ts` |
|
|
327
|
+
| Hook file | use-kebab-case.ts | `use-user-list.ts` |
|
|
328
|
+
| Component file | kebab-case.tsx | `user-table.tsx` |
|
|
329
|
+
| Component export | PascalCase named export | `export const UserTable` |
|
|
330
|
+
| Page file | kebab-case-page.tsx | `user-list-page.tsx` |
|
|
331
|
+
| Page export | default export | `export default UserListPage` |
|
|
332
|
+
|
|
333
|
+
## Rules
|
|
334
|
+
|
|
335
|
+
- Components do NOT call services directly — always go through a hook
|
|
336
|
+
- Services are pure: no React, no state, just `fetch` + types
|
|
337
|
+
- Query keys live next to hooks and are exported for invalidation
|
|
338
|
+
- Validate external input at the boundary with Zod — trust types inside the app
|
|
339
|
+
- One module = one feature domain; shared code goes to `core/` or `components/ui/`
|
|
@@ -5,110 +5,110 @@ description: Deep bug investigation workflow for hard-to-trace errors. Systemati
|
|
|
5
5
|
|
|
6
6
|
# Deep Bug Investigation Workflow
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
For hard bugs that have been "fixed" multiple times without success. DO NOT guess — trace step by step to the root cause.
|
|
9
9
|
|
|
10
|
-
## Step 1:
|
|
10
|
+
## Step 1: Collect evidence
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Record exactly, DO NOT interpret:
|
|
13
13
|
|
|
14
|
-
-
|
|
14
|
+
- Exact error message
|
|
15
15
|
- Stack trace: file, line number, call chain
|
|
16
|
-
-
|
|
17
|
-
-
|
|
16
|
+
- Which environment is affected (production/staging/local)
|
|
17
|
+
- Happens every time or only in certain cases
|
|
18
18
|
|
|
19
|
-
## Step 2: Verify code
|
|
19
|
+
## Step 2: Verify the code that is actually running
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
DO NOT assume the code on production = the code on local.
|
|
22
22
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
23
|
+
- Identify the exact version/commit currently deployed
|
|
24
|
+
- Compare it against the code you are reading locally
|
|
25
|
+
- If they DIFFER → read the deployed version before analyzing further
|
|
26
26
|
|
|
27
|
-
## Step 3: Trace execution path
|
|
27
|
+
## Step 3: Trace the execution path
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
This is the most important step. Go from entry point → to the failing line. Trace EVERY step, DO NOT skip.
|
|
30
30
|
|
|
31
31
|
### 3a. Entry point → Error line
|
|
32
32
|
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
33
|
+
- Where does the request/event/job enter from?
|
|
34
|
+
- Which function calls which function? Follow the stack trace exactly.
|
|
35
|
+
- How is data passed through each layer?
|
|
36
36
|
|
|
37
|
-
### 3b.
|
|
37
|
+
### 3b. Where does the data at the failing line come from?
|
|
38
38
|
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
39
|
+
- Where is the faulty variable created/loaded from?
|
|
40
|
+
- Loaded directly from source (DB, API) or from cache/session?
|
|
41
|
+
- Does it go through serialize → unserialize?
|
|
42
|
+
- Does it go through any transform/convert?
|
|
43
43
|
|
|
44
|
-
### 3c. Type & state
|
|
44
|
+
### 3c. Type & state at the moment of failure
|
|
45
45
|
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
46
|
+
- What is the actual type of the variable? (string, object, null, enum...)
|
|
47
|
+
- What type does the code expect?
|
|
48
|
+
- Why does the actual type differ from the expected one?
|
|
49
49
|
|
|
50
|
-
### 3d. Framework internals (
|
|
50
|
+
### 3d. Framework internals (when the error is inside vendor/library)
|
|
51
51
|
|
|
52
|
-
-
|
|
53
|
-
- Trace
|
|
54
|
-
-
|
|
52
|
+
- Read the source code at the EXACT line number from the stack trace
|
|
53
|
+
- Trace backwards: who calls that method, and with what arguments
|
|
54
|
+
- What condition drives the code into the failing branch
|
|
55
55
|
|
|
56
|
-
## Step 4:
|
|
56
|
+
## Step 4: Find the root cause — Answer 3 questions
|
|
57
57
|
|
|
58
|
-
1. **
|
|
59
|
-
2. **
|
|
60
|
-
3.
|
|
58
|
+
1. **Why does it fail?** — The specific technical cause
|
|
59
|
+
2. **Why didn't it fail before?** — What changed
|
|
60
|
+
3. **Reproduction conditions?** — When it fails, when it doesn't
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
If you can't answer all 3 → go back to Step 3, trace further.
|
|
63
63
|
|
|
64
|
-
## Step 5: Check
|
|
64
|
+
## Step 5: Check hidden state sources
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
"Sometimes works, sometimes doesn't" bugs are usually caused by hidden state. Check in this order:
|
|
67
67
|
|
|
68
68
|
### Cache / Serialization
|
|
69
69
|
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
70
|
+
- Does the object pulled from cache lose any internal state? (transient fields, lazy-loaded properties, runtime caches)
|
|
71
|
+
- Does stale cache contain the old data format while new code expects the new format?
|
|
72
|
+
- Does serialize/unserialize change the type? (int↔float, null handling, enum↔string)
|
|
73
73
|
|
|
74
74
|
### Database / Storage
|
|
75
75
|
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
76
|
+
- Do collation/encoding affect comparisons?
|
|
77
|
+
- Do default values in the DB match the code's expectations?
|
|
78
|
+
- Has the schema been updated on production yet?
|
|
79
79
|
|
|
80
80
|
### Runtime cache / Compiled cache
|
|
81
81
|
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
- CDN/proxy cache serve asset
|
|
82
|
+
- Any compiled/cached config, routes, or views that haven't been cleared?
|
|
83
|
+
- Does the bytecode cache (OPcache, compiled assets) serve the old file?
|
|
84
|
+
- Does CDN/proxy cache serve a stale asset?
|
|
85
85
|
|
|
86
86
|
### Environment
|
|
87
87
|
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
88
|
+
- Are env vars on production correct/complete?
|
|
89
|
+
- Does the runtime version (PHP, Node, Go, Python, etc.) differ from local?
|
|
90
|
+
- Do dependency versions differ?
|
|
91
91
|
|
|
92
92
|
## Step 6: Fix
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
Only fix once you have answered the 3 questions from Step 4. The fix must:
|
|
95
95
|
|
|
96
|
-
-
|
|
97
|
-
- Handle edge case
|
|
98
|
-
-
|
|
99
|
-
-
|
|
96
|
+
- Address the root cause, not the symptom
|
|
97
|
+
- Handle the edge case discovered (stale cache, type mismatch)
|
|
98
|
+
- Be defensive at data boundaries (cache, DB, external API) — not in internal logic
|
|
99
|
+
- Not break the normal code path in order to patch an edge case
|
|
100
100
|
|
|
101
101
|
## Step 7: Verify
|
|
102
102
|
|
|
103
|
-
- Reproduce
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
- Verify
|
|
103
|
+
- Reproduce the failure conditions from Step 4 → confirm the fix works
|
|
104
|
+
- Check the normal code path still works
|
|
105
|
+
- If cache-related → test both fresh load and cached load
|
|
106
|
+
- Verify against the actually deployed version (repeat Step 2)
|
|
107
107
|
|
|
108
108
|
## IMPORTANT
|
|
109
109
|
|
|
110
|
-
- **
|
|
111
|
-
- **
|
|
112
|
-
- **VERIFY DEPLOYED CODE** —
|
|
113
|
-
- **CHECK CACHE
|
|
114
|
-
- **
|
|
110
|
+
- **DO NOT GUESS** — Trace evidence, do not infer from variable names or "maybe it's..."
|
|
111
|
+
- **DO NOT FIX BEFORE UNDERSTANDING** — Fixing without knowing the root cause = creating a new bug
|
|
112
|
+
- **VERIFY DEPLOYED CODE** — Always check the running version, never assume production = local
|
|
113
|
+
- **CHECK CACHE FIRST** — Most "sometimes works, sometimes doesn't" bugs come from stale cached state
|
|
114
|
+
- **ONE ROOT CAUSE** — Every bug has one root cause. If multiple possibilities remain → trace further
|