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.
Files changed (36) hide show
  1. package/README.md +25 -7
  2. package/assets/agents/developers/nodejs-backend-dev.md +92 -0
  3. package/assets/agents/developers/react-frontend-dev.md +32 -19
  4. package/assets/architecture/nodejs-nestjs.md +949 -0
  5. package/assets/architecture/react-frontend.md +216 -145
  6. package/assets/skills/deep-debug/SKILL.md +62 -62
  7. package/assets/skills/research/SKILL.md +124 -0
  8. package/assets/skills/review-changes/SKILL.md +312 -0
  9. package/assets/templates/react-vite/CLAUDE.md +204 -128
  10. package/bin/cli.js +12 -1
  11. package/dist/commands/install.d.ts.map +1 -1
  12. package/dist/commands/install.js +219 -38
  13. package/dist/commands/install.js.map +1 -1
  14. package/dist/commands/list.d.ts.map +1 -1
  15. package/dist/commands/list.js +41 -1
  16. package/dist/commands/list.js.map +1 -1
  17. package/dist/commands/postinstall.d.ts.map +1 -1
  18. package/dist/commands/postinstall.js +2 -0
  19. package/dist/commands/postinstall.js.map +1 -1
  20. package/dist/commands/status.d.ts.map +1 -1
  21. package/dist/commands/status.js +31 -1
  22. package/dist/commands/status.js.map +1 -1
  23. package/dist/commands/uninstall.d.ts.map +1 -1
  24. package/dist/commands/uninstall.js +93 -38
  25. package/dist/commands/uninstall.js.map +1 -1
  26. package/dist/commands/upgrade.d.ts +7 -0
  27. package/dist/commands/upgrade.d.ts.map +1 -0
  28. package/dist/commands/upgrade.js +165 -0
  29. package/dist/commands/upgrade.js.map +1 -0
  30. package/dist/types.d.ts +1 -1
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/utils/symlink.d.ts +1 -0
  33. package/dist/utils/symlink.d.ts.map +1 -1
  34. package/dist/utils/symlink.js +18 -0
  35. package/dist/utils/symlink.js.map +1 -1
  36. package/package.json +3 -1
@@ -1,6 +1,6 @@
1
1
  # React Frontend Structure
2
2
 
3
- > MVVM (Model-View-ViewModel) architecture
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
- │ │ ├── mvvm/
17
- │ │ │ └── ViewModel.ts # ViewModel factory
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
- │ │ ├── HttpClient.ts # API client
20
+ │ │ ├── http-client.ts # API client
25
21
  │ │ └── bootstrap.ts # App initialization
26
22
  │ ├── modules/
27
23
  │ │ └── {module}/
28
- │ │ ├── models/ # Types, interfaces, DTOs
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
- ## MVVM Pattern
44
+ ## Layering
50
45
 
51
46
  ```
52
47
  ┌─────────────────────────────────────────────────┐
53
- View
54
- (Pages + Components)
48
+ Pages / Components
49
+ (render UI)
55
50
  ├─────────────────────────────────────────────────┤
56
- ViewModel
57
- (Hooks - state & logic)
51
+ Hooks
52
+ (state, queries, mutations, business logic)
58
53
  ├─────────────────────────────────────────────────┤
59
- Model
60
- (Types + Services)
54
+ Services
55
+ (pure API calls)
56
+ ├─────────────────────────────────────────────────┤
57
+ │ Types │
58
+ │ (models, DTOs, Zod schemas) │
61
59
  └─────────────────────────────────────────────────┘
62
60
  ```
63
61
 
64
- **Data flow:**
65
- 1. View renders and uses ViewModel (hook)
66
- 2. ViewModel manages state and calls Services
67
- 3. Services make API calls
68
- 4. Model defines data types
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
- ├── models/
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
- │ ├── {Module}Table.tsx
87
- │ ├── {Module}Modal.tsx
85
+ │ ├── {module}-table.tsx
86
+ │ ├── {module}-form.tsx
88
87
  │ └── index.ts
89
88
  ├── pages/
90
- │ ├── {Module}ListPage.tsx
89
+ │ ├── {module}-list-page.tsx
91
90
  │ └── index.ts
92
91
  └── index.ts
93
92
  ```
94
93
 
95
94
  ## Key Files
96
95
 
97
- ### models/user.model.ts
98
- ```tsx
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 UserListResponse {
114
- data: User[];
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
- ```tsx
126
- import Client from '@/core/HttpClient';
127
- import { User, CreateUserRequest } from '../models/user.model';
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 createUser = async (data: CreateUserRequest): Promise<User> => {
130
- return await Client.post<User>('/users', {
131
- body: JSON.stringify(data),
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 deleteUser = async (id: string): Promise<void> => {
136
- await Client.delete(`/users/${id}`);
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
- ### view-models/use-user-list.ts
141
- ```tsx
142
- import { User } from '../models/user.model';
143
- import { listViewModelFactory } from '@/core/mvvm/ViewModel';
144
-
145
- export const useUserList = () => {
146
- const viewModel = listViewModelFactory<User>(
147
- undefined, // viewModel config
148
- '/users', // API endpoint
149
- {}, // default filters
150
- { field: 'name', order: 'asc' } // default sorter
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
- return { ...viewModel };
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/UserListPage.tsx
217
+ ### pages/user-list-page.tsx
158
218
  ```tsx
159
219
  import { useState } from 'react';
160
- import { User } from '../models/user.model';
161
- import { useUserList } from '../view-models/use-user-list';
162
- import { UserTable } from '../components/UserTable';
163
- import { UserModal } from '../components/UserModal';
164
-
165
- export const UserListPage: React.FC = () => {
166
- const [modalOpen, setModalOpen] = useState(false);
167
- const [selectedUser, setSelectedUser] = useState<User | null>(null);
168
-
169
- const {
170
- items,
171
- isLoading,
172
- pagination,
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
- <Button onClick={() => { setSelectedUser(null); setModalOpen(true); }}>
186
- Add User
187
- </Button>
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
- dataSource={items}
191
- loading={isLoading}
192
- pagination={pagination}
193
- onChange={handleTableChange}
194
- onEdit={handleEdit}
195
- onReload={reload}
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
- <UserModal
199
- open={modalOpen}
200
- onOpenChange={setModalOpen}
201
- user={selectedUser}
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/HttpClient.ts
210
- ```tsx
262
+ ### core/http-client.ts
263
+ ```ts
211
264
  const BASE_URL = import.meta.env.VITE_API_URL;
212
265
 
213
- const Client = {
214
- get: async <T>(url: string): Promise<T> => {
215
- const res = await fetch(`${BASE_URL}${url}`, {
216
- headers: { 'Authorization': `Bearer ${getToken()}` },
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
- export default Client;
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 | `spam-slug/` |
251
- | Model file | kebab-case.model.ts | `spam-slug.model.ts` |
252
- | Service file | kebab-case.service.ts | `spam-slug.service.ts` |
253
- | ViewModel | use-kebab-case.ts | `use-spam-slug-list.ts` |
254
- | Component | PascalCase.tsx | `SpamSlugTable.tsx` |
255
- | Page | PascalCasePage.tsx | `SpamSlugListPage.tsx` |
256
-
257
- ## ViewModel Factory Returns
258
-
259
- ```tsx
260
- listViewModelFactory<T>() returns {
261
- items: T[],
262
- isLoading: boolean,
263
- pagination: { current, pageSize, total },
264
- handleTableChange: (pagination, filters, sorter) => void,
265
- handleInputSearch: (field, value) => void,
266
- reload: () => void,
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
- Dành cho bug khó, fix nhiều lần không được. KHÔNG đoán — trace từng bước đến root cause.
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: Thu thập evidence
10
+ ## Step 1: Collect evidence
11
11
 
12
- Ghi lại chính xác, KHÔNG diễn giải:
12
+ Record exactly, DO NOT interpret:
13
13
 
14
- - Error message nguyên văn
14
+ - Exact error message
15
15
  - Stack trace: file, line number, call chain
16
- - Environment nào bị (production/staging/local)
17
- - Lỗi mọi lúc hay chỉ một số case
16
+ - Which environment is affected (production/staging/local)
17
+ - Happens every time or only in certain cases
18
18
 
19
- ## Step 2: Verify code đang chạy
19
+ ## Step 2: Verify the code that is actually running
20
20
 
21
- KHÔNG giả định code trên production = code trên local.
21
+ DO NOT assume the code on production = the code on local.
22
22
 
23
- - Xác định chính xác version/commit đang deploy
24
- - So sánh với code đang đọc trên local
25
- - Nếu KHÁC NHAUđọc đúng version đang deploy trước khi phân tích tiếp
23
+ - Identify the exact version/commit currently deployed
24
+ - Compare it against the code you are reading locally
25
+ - If they DIFFERread the deployed version before analyzing further
26
26
 
27
- ## Step 3: Trace execution path
27
+ ## Step 3: Trace the execution path
28
28
 
29
- Đây bước quan trọng nhất. Đi từ entry point → đến dòng lỗi. Trace TỪNG bước, KHÔNG nhảy cóc.
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
- - Request/event/job đi vào từ đâu?
34
- - Function nào gọi function nào? Theo đúng stack trace.
35
- - Data được truyền qua các layer thế nào?
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. Data dòng lỗi đến từ đâu?
37
+ ### 3b. Where does the data at the failing line come from?
38
38
 
39
- - Biến bị lỗi được tạo/load từ đâu?
40
- - Load trực tiếp từ source (DB, API) hay từ cache/session?
41
- - qua serialize → unserialize không?
42
- - qua transform/convert nào không?
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 tại thời điểm lỗi
44
+ ### 3c. Type & state at the moment of failure
45
45
 
46
- - Type thực tế của biến gì? (string, object, null, enum...)
47
- - Code expect type gì?
48
- - Tại sao type thực tế khác type expected?
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 (khi lỗi trong vendor/library)
50
+ ### 3d. Framework internals (when the error is inside vendor/library)
51
51
 
52
- - Đọc source code tại ĐÚNG line number từ stack trace
53
- - Trace ngược: ai gọi method đó, với argument
54
- - Điều kiện nào khiến code đi vào branch gây lỗi
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: Tìm root cause — Trả lời 3 câu
56
+ ## Step 4: Find the root cause — Answer 3 questions
57
57
 
58
- 1. **Tại sao lỗi?** — Nguyên nhân kỹ thuật cụ thể
59
- 2. **Tại sao trước đây không lỗi?** — Cái gì thay đổi
60
- 3. **Điều kiện reproduce?** — Khi nào lỗi, khi nào không
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
- Nếu chưa trả lời được cả 3 → quay lại Step 3, trace thêm.
62
+ If you can't answer all 3 → go back to Step 3, trace further.
63
63
 
64
- ## Step 5: Check các nguồn state ẩn
64
+ ## Step 5: Check hidden state sources
65
65
 
66
- Bug "lúc được lúc không" thường do state ẩn. Check theo thứ tự:
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
- - Object lấy từ cache mất internal state không? (transient fields, lazy-loaded properties, runtime caches)
71
- - Cache chứa data format cũ, code mới expect format mới?
72
- - Serialize/unserialize thay đổi type không? (int↔float, null handling, enum↔string)
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
- - Collation, encoding ảnh hưởng comparison không?
77
- - Default value trong DB match code expectation không?
78
- - Schema đã update trên production chưa?
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
- - compiled/cached config, routes, views chưa clear?
83
- - Bytecode cache (OPcache, compiled assets) serve file cũ?
84
- - CDN/proxy cache serve asset cũ?
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
- - Env vars trên production có đúng/đủ không?
89
- - Version runtime (PHP, Node, Go, Python, etc.) khác local không?
90
- - Dependency version khác không?
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
- Chỉ fix khi đã trả lời được 3 câu Step 4. Fix phải:
94
+ Only fix once you have answered the 3 questions from Step 4. The fix must:
95
95
 
96
- - Xử đúng root cause, không phải symptom
97
- - Handle edge case đã phát hiện (cache stale, type mismatch)
98
- - Defensive data boundary (cache, DB, external API) — không internal logic
99
- - Không phá code path bình thường để fix edge case
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 điều kiện lỗi từ Step 4 → confirm đã fix
104
- - Test code path bình thường vẫn hoạt động
105
- - Nếu liên quan cache → test cả fresh load lẫn cached load
106
- - Verify đúng version đã deploy (lặp lại Step 2)
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
- - **KHÔNG ĐOÁN MÒ** — Trace evidence, không suy luận từ tên biến hay " lẽ là..."
111
- - **KHÔNG FIX TRƯỚC KHI HIỂU** — Fix không hiểu root cause = tạo bug mới
112
- - **VERIFY DEPLOYED CODE** — Luôn check version đang chạy, không giả định production = local
113
- - **CHECK CACHE TRƯỚC** — Phần lớn bug "lúc được lúc không" do stale cached state
114
- - **MỘT ROOT CAUSE** — Mỗi bug chỉ 1 root cause. Nếu còn nhiều khả năng → trace thêm
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