@buivietphi/skill-mobile-mt 2.1.0 → 2.2.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.
@@ -0,0 +1,656 @@
1
+ # Code Generation Templates — Production-Ready Patterns
2
+
3
+ > On-demand module. Loaded when building new features, setting up state management, API clients, or forms.
4
+ > Contains COMPLETE, copy-and-adapt code templates — not snippets.
5
+
6
+ ---
7
+
8
+ ## State Management Templates
9
+
10
+ ### Zustand (React Native) — Advanced Store
11
+
12
+ ```typescript
13
+ // stores/useAuthStore.ts
14
+ import { create } from 'zustand';
15
+ import { persist, createJSONStorage } from 'zustand/middleware';
16
+ import { immer } from 'zustand/middleware/immer';
17
+ import AsyncStorage from '@react-native-async-storage/async-storage';
18
+
19
+ interface User { id: string; email: string; name: string; role: 'user' | 'admin'; }
20
+
21
+ interface AuthState {
22
+ user: User | null;
23
+ token: string | null;
24
+ refreshToken: string | null;
25
+ isLoading: boolean;
26
+ error: string | null;
27
+ // Actions
28
+ login: (email: string, password: string) => Promise<void>;
29
+ logout: () => void;
30
+ refreshSession: () => Promise<boolean>;
31
+ clearError: () => void;
32
+ }
33
+
34
+ export const useAuthStore = create<AuthState>()(
35
+ persist(
36
+ immer((set, get) => ({
37
+ user: null,
38
+ token: null,
39
+ refreshToken: null,
40
+ isLoading: false,
41
+ error: null,
42
+
43
+ login: async (email, password) => {
44
+ set(state => { state.isLoading = true; state.error = null; });
45
+ try {
46
+ const res = await authService.login({ email, password });
47
+ set(state => {
48
+ state.user = res.user;
49
+ state.token = res.token;
50
+ state.refreshToken = res.refreshToken;
51
+ state.isLoading = false;
52
+ });
53
+ } catch (e) {
54
+ set(state => {
55
+ state.isLoading = false;
56
+ state.error = e instanceof Error ? e.message : 'Login failed';
57
+ });
58
+ }
59
+ },
60
+
61
+ logout: () => {
62
+ set(state => {
63
+ state.user = null;
64
+ state.token = null;
65
+ state.refreshToken = null;
66
+ });
67
+ },
68
+
69
+ refreshSession: async () => {
70
+ const { refreshToken } = get();
71
+ if (!refreshToken) return false;
72
+ try {
73
+ const res = await authService.refresh(refreshToken);
74
+ set(state => { state.token = res.token; state.refreshToken = res.refreshToken; });
75
+ return true;
76
+ } catch {
77
+ get().logout();
78
+ return false;
79
+ }
80
+ },
81
+
82
+ clearError: () => set(state => { state.error = null; }),
83
+ })),
84
+ {
85
+ name: 'auth-storage',
86
+ storage: createJSONStorage(() => AsyncStorage),
87
+ partialize: (state) => ({ token: state.token, refreshToken: state.refreshToken, user: state.user }),
88
+ }
89
+ )
90
+ );
91
+
92
+ // Selectors (memoized — prevent re-renders)
93
+ export const useIsLoggedIn = () => useAuthStore(state => !!state.token);
94
+ export const useUserRole = () => useAuthStore(state => state.user?.role);
95
+ ```
96
+
97
+ ### Zustand Store Composition
98
+
99
+ ```typescript
100
+ // stores/index.ts — composing multiple stores
101
+ export { useAuthStore, useIsLoggedIn } from './useAuthStore';
102
+ export { useCartStore } from './useCartStore';
103
+ export { useSettingsStore } from './useSettingsStore';
104
+
105
+ // Usage: each store is independent, no single god-store
106
+ // Components subscribe to ONLY the store they need
107
+ ```
108
+
109
+ ### Redux Toolkit — Entity Adapter for Collections
110
+
111
+ ```typescript
112
+ // features/products/productsSlice.ts
113
+ import { createSlice, createAsyncThunk, createEntityAdapter, PayloadAction } from '@reduxjs/toolkit';
114
+ import { Product } from './product.types';
115
+ import { productService } from './productService';
116
+
117
+ const productsAdapter = createEntityAdapter<Product>({
118
+ selectId: (product) => product.id,
119
+ sortComparer: (a, b) => b.createdAt.localeCompare(a.createdAt),
120
+ });
121
+
122
+ interface ProductsExtra { loading: boolean; error: string | null; page: number; hasMore: boolean; }
123
+
124
+ const initialState = productsAdapter.getInitialState<ProductsExtra>({
125
+ loading: false, error: null, page: 1, hasMore: true,
126
+ });
127
+
128
+ export const fetchProducts = createAsyncThunk(
129
+ 'products/fetch',
130
+ async (page: number, { rejectWithValue }) => {
131
+ try {
132
+ return await productService.getProducts(page);
133
+ } catch (e) {
134
+ return rejectWithValue(e instanceof Error ? e.message : 'Failed to fetch');
135
+ }
136
+ }
137
+ );
138
+
139
+ const productsSlice = createSlice({
140
+ name: 'products',
141
+ initialState,
142
+ reducers: {
143
+ productUpdated: productsAdapter.updateOne,
144
+ productRemoved: productsAdapter.removeOne,
145
+ productsReset: () => initialState,
146
+ },
147
+ extraReducers: (builder) => {
148
+ builder
149
+ .addCase(fetchProducts.pending, (state) => { state.loading = true; state.error = null; })
150
+ .addCase(fetchProducts.fulfilled, (state, action) => {
151
+ state.loading = false;
152
+ state.page += 1;
153
+ state.hasMore = action.payload.length >= 20;
154
+ productsAdapter.upsertMany(state, action.payload);
155
+ })
156
+ .addCase(fetchProducts.rejected, (state, action) => {
157
+ state.loading = false;
158
+ state.error = action.payload as string;
159
+ });
160
+ },
161
+ });
162
+
163
+ export const { productUpdated, productRemoved, productsReset } = productsSlice.actions;
164
+ export default productsSlice.reducer;
165
+
166
+ // Typed selectors
167
+ export const {
168
+ selectAll: selectAllProducts,
169
+ selectById: selectProductById,
170
+ selectTotal: selectProductCount,
171
+ } = productsAdapter.getSelectors((state: RootState) => state.products);
172
+ ```
173
+
174
+ ### Riverpod (Flutter) — Async + Family
175
+
176
+ ```dart
177
+ // providers/product_provider.dart
178
+ import 'package:riverpod_annotation/riverpod_annotation.dart';
179
+ part 'product_provider.g.dart';
180
+
181
+ @riverpod
182
+ class ProductList extends _$ProductList {
183
+ int _page = 1;
184
+ bool _hasMore = true;
185
+
186
+ @override
187
+ FutureOr<List<Product>> build() => _fetch();
188
+
189
+ Future<List<Product>> _fetch() async {
190
+ final repo = ref.watch(productRepositoryProvider);
191
+ return repo.getProducts(page: 1);
192
+ }
193
+
194
+ Future<void> loadMore() async {
195
+ if (!_hasMore) return;
196
+ final repo = ref.read(productRepositoryProvider);
197
+ final next = await repo.getProducts(page: _page + 1);
198
+ _hasMore = next.length >= 20;
199
+ _page++;
200
+ state = AsyncData([...state.value ?? [], ...next]);
201
+ }
202
+
203
+ Future<void> refresh() async {
204
+ _page = 1;
205
+ _hasMore = true;
206
+ ref.invalidateSelf();
207
+ }
208
+ }
209
+
210
+ // Family provider — parameterized by ID
211
+ @riverpod
212
+ Future<Product> productDetail(ProductDetailRef ref, String id) async {
213
+ final repo = ref.watch(productRepositoryProvider);
214
+ return repo.getProductById(id);
215
+ }
216
+
217
+ // Usage in widget:
218
+ // final products = ref.watch(productListProvider);
219
+ // products.when(data: (list) => ..., error: (e, _) => ..., loading: () => ...);
220
+ //
221
+ // final detail = ref.watch(productDetailProvider('abc-123'));
222
+ ```
223
+
224
+ ---
225
+
226
+ ## API Client Templates
227
+
228
+ ### React Native — Axios with Retry + Token Queue
229
+
230
+ ```typescript
231
+ // services/api.ts
232
+ import axios, { AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
233
+ import { useAuthStore } from '@/stores/useAuthStore';
234
+
235
+ const api = axios.create({
236
+ baseURL: process.env.EXPO_PUBLIC_API_URL,
237
+ timeout: 15000,
238
+ headers: { 'Content-Type': 'application/json' },
239
+ });
240
+
241
+ // Token refresh queue — prevents concurrent refresh calls
242
+ let isRefreshing = false;
243
+ let failedQueue: Array<{ resolve: (token: string) => void; reject: (error: Error) => void }> = [];
244
+
245
+ const processQueue = (error: Error | null, token: string | null) => {
246
+ failedQueue.forEach(({ resolve, reject }) => {
247
+ error ? reject(error) : resolve(token!);
248
+ });
249
+ failedQueue = [];
250
+ };
251
+
252
+ // Request interceptor — attach token
253
+ api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
254
+ const token = useAuthStore.getState().token;
255
+ if (token) config.headers.Authorization = `Bearer ${token}`;
256
+ return config;
257
+ });
258
+
259
+ // Response interceptor — auto-refresh on 401
260
+ api.interceptors.response.use(
261
+ (response) => response,
262
+ async (error: AxiosError) => {
263
+ const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
264
+
265
+ if (error.response?.status === 401 && !originalRequest._retry) {
266
+ if (isRefreshing) {
267
+ return new Promise((resolve, reject) => {
268
+ failedQueue.push({
269
+ resolve: (token) => {
270
+ originalRequest.headers = { ...originalRequest.headers, Authorization: `Bearer ${token}` };
271
+ resolve(api(originalRequest));
272
+ },
273
+ reject,
274
+ });
275
+ });
276
+ }
277
+
278
+ originalRequest._retry = true;
279
+ isRefreshing = true;
280
+
281
+ try {
282
+ const success = await useAuthStore.getState().refreshSession();
283
+ if (success) {
284
+ const newToken = useAuthStore.getState().token!;
285
+ processQueue(null, newToken);
286
+ originalRequest.headers = { ...originalRequest.headers, Authorization: `Bearer ${newToken}` };
287
+ return api(originalRequest);
288
+ }
289
+ } catch (refreshError) {
290
+ processQueue(refreshError as Error, null);
291
+ useAuthStore.getState().logout();
292
+ } finally {
293
+ isRefreshing = false;
294
+ }
295
+ }
296
+
297
+ return Promise.reject(normalizeError(error));
298
+ }
299
+ );
300
+
301
+ // Retry with exponential backoff
302
+ export async function apiWithRetry<T>(
303
+ fn: () => Promise<T>,
304
+ maxRetries = 3,
305
+ baseDelay = 1000
306
+ ): Promise<T> {
307
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
308
+ try {
309
+ return await fn();
310
+ } catch (error) {
311
+ const isRetryable = error instanceof AppError && error.isRetryable;
312
+ if (!isRetryable || attempt === maxRetries) throw error;
313
+ const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 500; // jitter
314
+ await new Promise(r => setTimeout(r, delay));
315
+ }
316
+ }
317
+ throw new Error('Unreachable');
318
+ }
319
+
320
+ export default api;
321
+ ```
322
+
323
+ ### Flutter — Dio with Interceptors
324
+
325
+ ```dart
326
+ // services/api_client.dart
327
+ import 'package:dio/dio.dart';
328
+
329
+ class ApiClient {
330
+ late final Dio _dio;
331
+ final TokenStorage _tokenStorage;
332
+
333
+ ApiClient({required TokenStorage tokenStorage}) : _tokenStorage = tokenStorage {
334
+ _dio = Dio(BaseOptions(
335
+ baseUrl: const String.fromEnvironment('API_URL'),
336
+ connectTimeout: const Duration(seconds: 15),
337
+ receiveTimeout: const Duration(seconds: 15),
338
+ ));
339
+
340
+ _dio.interceptors.addAll([
341
+ _AuthInterceptor(_tokenStorage, _dio),
342
+ _RetryInterceptor(maxRetries: 3),
343
+ LogInterceptor(requestBody: true, responseBody: true),
344
+ ]);
345
+ }
346
+
347
+ Future<T> get<T>(String path, {Map<String, dynamic>? params, T Function(dynamic)? fromJson}) async {
348
+ final res = await _dio.get(path, queryParameters: params);
349
+ return fromJson != null ? fromJson(res.data) : res.data as T;
350
+ }
351
+
352
+ Future<T> post<T>(String path, {dynamic data, T Function(dynamic)? fromJson}) async {
353
+ final res = await _dio.post(path, data: data);
354
+ return fromJson != null ? fromJson(res.data) : res.data as T;
355
+ }
356
+ }
357
+
358
+ class _AuthInterceptor extends Interceptor {
359
+ final TokenStorage _storage;
360
+ final Dio _dio;
361
+
362
+ _AuthInterceptor(this._storage, this._dio);
363
+
364
+ @override
365
+ void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
366
+ final token = await _storage.getAccessToken();
367
+ if (token != null) options.headers['Authorization'] = 'Bearer $token';
368
+ handler.next(options);
369
+ }
370
+
371
+ @override
372
+ void onError(DioException err, ErrorInterceptorHandler handler) async {
373
+ if (err.response?.statusCode == 401) {
374
+ try {
375
+ final newToken = await _refreshToken();
376
+ err.requestOptions.headers['Authorization'] = 'Bearer $newToken';
377
+ final res = await _dio.fetch(err.requestOptions);
378
+ handler.resolve(res);
379
+ return;
380
+ } catch (_) {
381
+ await _storage.clear();
382
+ }
383
+ }
384
+ handler.next(err);
385
+ }
386
+
387
+ Future<String> _refreshToken() async {
388
+ final refresh = await _storage.getRefreshToken();
389
+ final res = await _dio.post('/auth/refresh', data: {'refreshToken': refresh});
390
+ final token = res.data['accessToken'] as String;
391
+ await _storage.saveAccessToken(token);
392
+ return token;
393
+ }
394
+ }
395
+ ```
396
+
397
+ ---
398
+
399
+ ## Form Templates
400
+
401
+ ### React Native — TanStack Form + Zod
402
+
403
+ ```typescript
404
+ // forms/useLoginForm.ts
405
+ import { useForm } from '@tanstack/react-form';
406
+ import { zodValidator } from '@tanstack/zod-form-adapter';
407
+ import { z } from 'zod';
408
+
409
+ const loginSchema = z.object({
410
+ email: z.string().email('Invalid email address'),
411
+ password: z.string().min(8, 'Password must be at least 8 characters'),
412
+ });
413
+
414
+ export function useLoginForm(onSubmit: (data: z.infer<typeof loginSchema>) => Promise<void>) {
415
+ return useForm({
416
+ defaultValues: { email: '', password: '' },
417
+ validatorAdapter: zodValidator(),
418
+ validators: { onChange: loginSchema },
419
+ onSubmit: async ({ value }) => {
420
+ await onSubmit(value);
421
+ },
422
+ });
423
+ }
424
+
425
+ // LoginScreen.tsx — usage
426
+ function LoginScreen() {
427
+ const form = useLoginForm(async (data) => {
428
+ await authService.login(data.email, data.password);
429
+ });
430
+
431
+ return (
432
+ <form.Provider>
433
+ <form.Field name="email">
434
+ {(field) => (
435
+ <View>
436
+ <TextInput
437
+ value={field.state.value}
438
+ onChangeText={field.handleChange}
439
+ onBlur={field.handleBlur}
440
+ placeholder="Email"
441
+ keyboardType="email-address"
442
+ autoCapitalize="none"
443
+ />
444
+ {field.state.meta.errors.length > 0 && (
445
+ <Text style={styles.error}>{field.state.meta.errors[0]}</Text>
446
+ )}
447
+ </View>
448
+ )}
449
+ </form.Field>
450
+
451
+ <form.Field name="password">
452
+ {(field) => (
453
+ <View>
454
+ <TextInput
455
+ value={field.state.value}
456
+ onChangeText={field.handleChange}
457
+ onBlur={field.handleBlur}
458
+ placeholder="Password"
459
+ secureTextEntry
460
+ />
461
+ {field.state.meta.errors.length > 0 && (
462
+ <Text style={styles.error}>{field.state.meta.errors[0]}</Text>
463
+ )}
464
+ </View>
465
+ )}
466
+ </form.Field>
467
+
468
+ <form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]}>
469
+ {([canSubmit, isSubmitting]) => (
470
+ <Button
471
+ title={isSubmitting ? 'Logging in...' : 'Login'}
472
+ onPress={form.handleSubmit}
473
+ disabled={!canSubmit || isSubmitting}
474
+ />
475
+ )}
476
+ </form.Subscribe>
477
+ </form.Provider>
478
+ );
479
+ }
480
+ ```
481
+
482
+ ### Multi-Step Form Pattern
483
+
484
+ ```typescript
485
+ // forms/useMultiStepForm.ts
486
+ import { useState, useCallback } from 'react';
487
+
488
+ interface StepConfig<T> {
489
+ validate: (data: Partial<T>) => Record<string, string> | null;
490
+ }
491
+
492
+ export function useMultiStepForm<T extends Record<string, unknown>>(
493
+ steps: StepConfig<T>[],
494
+ initialData: Partial<T>
495
+ ) {
496
+ const [currentStep, setCurrentStep] = useState(0);
497
+ const [data, setData] = useState<Partial<T>>(initialData);
498
+ const [errors, setErrors] = useState<Record<string, string>>({});
499
+
500
+ const updateField = useCallback(<K extends keyof T>(key: K, value: T[K]) => {
501
+ setData(prev => ({ ...prev, [key]: value }));
502
+ setErrors(prev => { const next = { ...prev }; delete next[key as string]; return next; });
503
+ }, []);
504
+
505
+ const next = useCallback(() => {
506
+ const validationErrors = steps[currentStep].validate(data);
507
+ if (validationErrors) { setErrors(validationErrors); return false; }
508
+ setCurrentStep(prev => Math.min(prev + 1, steps.length - 1));
509
+ return true;
510
+ }, [currentStep, data, steps]);
511
+
512
+ const back = useCallback(() => {
513
+ setCurrentStep(prev => Math.max(prev - 1, 0));
514
+ }, []);
515
+
516
+ return {
517
+ currentStep,
518
+ totalSteps: steps.length,
519
+ data,
520
+ errors,
521
+ updateField,
522
+ next,
523
+ back,
524
+ isFirst: currentStep === 0,
525
+ isLast: currentStep === steps.length - 1,
526
+ progress: (currentStep + 1) / steps.length,
527
+ };
528
+ }
529
+ ```
530
+
531
+ ### File Upload with Progress
532
+
533
+ ```typescript
534
+ // services/uploadService.ts
535
+ import * as ImagePicker from 'expo-image-picker';
536
+ import api from './api';
537
+
538
+ export async function pickAndUploadImage(
539
+ onProgress?: (percent: number) => void
540
+ ): Promise<{ url: string }> {
541
+ // 1. Pick image
542
+ const result = await ImagePicker.launchImageLibraryAsync({
543
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
544
+ allowsEditing: true,
545
+ quality: 0.8,
546
+ base64: false,
547
+ });
548
+
549
+ if (result.canceled) throw new Error('User cancelled');
550
+ const asset = result.assets[0];
551
+
552
+ // 2. Validate
553
+ if (asset.fileSize && asset.fileSize > 5 * 1024 * 1024) {
554
+ throw new Error('Image must be under 5MB');
555
+ }
556
+
557
+ // 3. Upload with progress
558
+ const formData = new FormData();
559
+ formData.append('file', {
560
+ uri: asset.uri,
561
+ type: asset.mimeType ?? 'image/jpeg',
562
+ name: asset.fileName ?? 'photo.jpg',
563
+ } as unknown as Blob);
564
+
565
+ const { data } = await api.post('/upload', formData, {
566
+ headers: { 'Content-Type': 'multipart/form-data' },
567
+ onUploadProgress: (e) => {
568
+ if (e.total) onProgress?.(Math.round((e.loaded / e.total) * 100));
569
+ },
570
+ });
571
+
572
+ return data;
573
+ }
574
+ ```
575
+
576
+ ---
577
+
578
+ ## Type Generation Patterns
579
+
580
+ ### API Response → TypeScript Interfaces
581
+
582
+ ```typescript
583
+ // types/api.types.ts — Branded types for type safety
584
+ type Brand<T, B> = T & { __brand: B };
585
+ export type UserId = Brand<string, 'UserId'>;
586
+ export type ProductId = Brand<string, 'ProductId'>;
587
+
588
+ // API response interfaces — generated from response shape
589
+ export interface ApiResponse<T> {
590
+ data: T;
591
+ meta?: { page: number; totalPages: number; totalItems: number };
592
+ }
593
+
594
+ export interface ApiError {
595
+ code: string;
596
+ message: string;
597
+ details?: Record<string, string[]>;
598
+ }
599
+
600
+ // Domain entities
601
+ export interface User {
602
+ id: UserId;
603
+ email: string;
604
+ name: string;
605
+ avatarUrl: string | null;
606
+ role: 'user' | 'admin';
607
+ createdAt: string;
608
+ }
609
+
610
+ export interface Product {
611
+ id: ProductId;
612
+ title: string;
613
+ description: string;
614
+ price: number;
615
+ images: string[];
616
+ category: string;
617
+ inStock: boolean;
618
+ }
619
+
620
+ // Discriminated union for async states
621
+ export type AsyncState<T> =
622
+ | { status: 'idle' }
623
+ | { status: 'loading' }
624
+ | { status: 'success'; data: T }
625
+ | { status: 'error'; error: string };
626
+
627
+ // Type guard
628
+ export function isSuccess<T>(state: AsyncState<T>): state is { status: 'success'; data: T } {
629
+ return state.status === 'success';
630
+ }
631
+ ```
632
+
633
+ ### Navigation Params Typing
634
+
635
+ ```typescript
636
+ // navigation/types.ts
637
+ import { NativeStackScreenProps } from '@react-navigation/native-stack';
638
+ import { ProductId, UserId } from '@/types/api.types';
639
+
640
+ export type RootStackParamList = {
641
+ Home: undefined;
642
+ ProductDetail: { productId: ProductId };
643
+ Profile: { userId: UserId };
644
+ EditProfile: undefined;
645
+ Settings: undefined;
646
+ };
647
+
648
+ // Screen props — use in each screen
649
+ export type ProductDetailProps = NativeStackScreenProps<RootStackParamList, 'ProductDetail'>;
650
+ export type ProfileProps = NativeStackScreenProps<RootStackParamList, 'Profile'>;
651
+
652
+ // Type-safe navigation hook
653
+ import { useNavigation } from '@react-navigation/native';
654
+ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
655
+ export const useAppNavigation = () => useNavigation<NativeStackNavigationProp<RootStackParamList>>();
656
+ ```
@@ -50,14 +50,14 @@ CROSS-PLATFORM patterns (API contract, token expiry, navigation params):
50
50
 
51
51
  USER SAYS → MODE → SCOPE
52
52
  ──────────────────────────────────────────────────────────────────────────────
53
- "Review full code / review toàn bộ" → FULL → Read ALL src/ files → 12-category checklist
54
- "Review thay đổi / review changes" → CHANGES → git diff (unstaged + staged) → review only changed code
55
- "Review file X / review file này" → FILE → Read specific file(s) → 12-category checklist on those files only
56
- "Review function X / review hàm này" → FUNCTION → Find function → trace callers/callees → deep review that function
53
+ "Review full code" → FULL → Read ALL src/ files → 12-category checklist
54
+ "Review changes / review diff" → CHANGES → git diff (unstaged + staged) → review only changed code
55
+ "Review file X / review this file" → FILE → Read specific file(s) → 12-category checklist on those files only
56
+ "Review function X / review this function" → FUNCTION → Find function → trace callers/callees → deep review that function
57
57
  "Review PR / review pull request" → PR → git diff [base]..HEAD → Step 0 PR-Level + 12-category on diff
58
- "Review các file đã sửa / modified files" → MODIFIED → git status (modified only) → read + review modified files
59
- "Review commit / review các commit" → COMMITS → git log → git show [commit] → review each commit's changes
60
- "Check PR / check review PR" → PR-CHECK → Step 0 ONLY (size, scope, tests, commits) → quick pass/fail
58
+ "Review modified files" → MODIFIED → git status (modified only) → read + review modified files
59
+ "Review commits" → COMMITS → git log → git show [commit] → review each commit's changes
60
+ "Check PR" → PR-CHECK → Step 0 ONLY (size, scope, tests, commits) → quick pass/fail
61
61
 
62
62
  ═══ MODE DETAILS ═══
63
63
 
@@ -124,7 +124,7 @@ MODE: PR-CHECK (quick — no code review)
124
124
  ⛔ DEFAULT: If user says just "review" without specifying:
125
125
  → Check git status first
126
126
  → If changes exist → MODE: CHANGES (review what changed)
127
- → If no changes → ASK: "Review full codebase hoặc file cụ thể nào?"
127
+ → If no changes → ASK: "Review full codebase or a specific file?"
128
128
  ```
129
129
 
130
130
  ---