Audit #015 — Ổn định sau khi gộp năm mạch việc song song (2026-09-20, commit cb51b14a)
Chỉ đạo SRC-904 của chủ dự án: push code, test và audit một lượt, ra báo cáo về tính ổn định — ít lỗi giao diện, ít lỗi logic, ít lỗi authentication, và chạy tốt trên điện thoại.
Bản này khác các audit trước ở một điểm, và điểm ấy là kết luận quan trọng nhất: phần lớn lỗi nặng tìm được không nằm trong code ai viết ra, mà sinh ra từ LƯỢT GỘP. Năm mạch việc chạy song song trên cùng một cây làm việc, và lúc gộp lại, ba khối mã bị đặt sai chỗ theo đúng một kiểu: vẫn biên dịch được, vẫn qua mọi cổng, và vẫn sai.
1. Phương pháp và phạm vi
| Bước | Cách làm | Kết quả |
|---|---|---|
| Chốt phạm vi | origin/main khi bắt đầu là f69a0e60; cây làm việc chung có 11 commit local chưa đẩy + một mạch leaderboard chưa commit | 43 module API · 263 migration · 204 file test · 489 file docs |
| Dọn nhánh trước khi đẩy | So từng commit local với origin theo NỘI DUNG (không theo hash) | 6/11 commit là bản làm TRÙNG việc đã có trên origin; chỉ 5 commit mang việc thật |
| Máy chạy trước | check:docs · check:code · typecheck · test · contrast · cổng số migration | 30 cổng docs/code PASS · typecheck ✓ · 2.484 test PASS (209 file) · contrast 231/231 |
| e2e | apps/learn build thật + Playwright, gồm 320px và 375px | 232 PASS (thêm 4 phép gác mới trong phiên) |
| Rà bằng agent | 5 lens song song (auth · tiền · lớp live · giao diện/mobile · migration), mỗi phát hiện qua 3 người phản biện độc lập với nhiệm vụ BÁC BỎ | 56 agent · 17 phát hiện · 13 sống, 4 bị bác |
| Đối chiếu chéo | Phát hiện nào được cả người lẫn agent tìm thấy độc lập thì ghi một dòng | 2 dòng trùng (D-4 lối vào lớp live, D-1 nguyên tử của grant) |
Ngưỡng của vòng phản biện đặt nghiêm: không chắc thì coi như bị bác. 4 phát hiện bị loại đều vì chỉ 1/3 người giữ lại.
2. Ba lỗi do chính lượt GỘP sinh ra, và vì sao không cổng nào đỏ
Đây là lớp lỗi đáng ghi nhớ nhất của phiên này.
G-1 🔴 Hai nhánh route /live bị đặt ra ngoài thân switch
Lượt gộp tự động đưa ba dòng /live của parse() vào giữa khối /reading, và hai nhánh case của path() ra ngoài thân switch. Cái thứ hai là lỗi cú pháp nên tsc bắt được. Cái thứ nhất thì biên dịch sạch mà vẫn sai: /ielts/live chỉ đọc ra đúng khi đường dẫn đồng thời khớp regex /reading, tức là không bao giờ.
Sửa ở 3eab3752.
G-2 🔴 Lối vào "Join a live class" nằm ngoài component, không bao giờ hiện
IeltsTaskPage đóng lại ở dòng 1209 của apps/learn/src/IeltsPathway.tsx, còn khối JSX của lối vào lớp live nằm ở 1210–1223, tức là ở cấp module.
Vì sao không cổng nào đỏ: trong một file .tsx thì đó vẫn là cú pháp hợp lệ. {/* ... */} là một block statement, còn <a>...</a> đứng một mình là một expression statement. tsc xanh, lint xanh, build xanh. React dựng phần tử ấy đúng một lần lúc nạp module rồi vứt đi.
Hậu quả: /ielts/live vẫn chạy nếu gõ thẳng địa chỉ, nhưng không còn đường nào đi tới nó từ giao diện. Một tính năng biến mất mà không một cổng nào đỏ. Cả agent lens giao diện lẫn phép rà tay đều tìm ra độc lập.
Sửa ở 27683b98, kèm phép gác hỏi TRANG ĐÃ DỰNG chứ không hỏi mã nguồn.
G-3 🔴 Hai cặp migration trùng số
| Số | Hai file cùng số | Xử lý |
|---|---|---|
| 0259 | 0259_listening_subskills_five.sql (đã trên main) và 0259_nemo_leaderboard.sql (chưa commit) | Đổi bản chưa đẩy thành 0261 |
| 0257 | 0257_ielts_exam_papers.sql (đã áp production) và 0257_ielts_diagnostic_speaking_takes.sql (chưa đẩy) | Đổi bản chưa đẩy thành 0263 |
wrangler d1 migrations apply sắp theo TÊN FILE, nên số trùng là thứ tự áp không xác định (migrations/README.md §1). Luật "migration đã áp lên production thì không đổi tên" được giữ: chỉ bản chưa từng lên main bị đánh số lại.
Cổng AS-04.1.1 bắt được lớp lỗi này, nhưng nó chỉ chạy trong CI. Nếu đẩy nguyên trạng thì CI đỏ và chặn deploy của mọi phiên. Sau khi sửa: dãy liền mạch 0001..0263, không trùng.
3. Phát hiện 🔴 — chặn phát hành hoặc động tới tiền
| # | Phát hiện | Bằng chứng | Trạng thái |
|---|---|---|---|
| D-1 | grant() ghi sổ và cộng ví bằng hai câu lệnh rời. INSERT OR IGNORE vào nemo_ledger rồi mới UPDATE số dư, không bọc DB.batch. Chết giữa hai câu là ví thiếu vĩnh viễn: chạy lại thì meta.changes === 0 nên thoát sớm ở dòng 152, câu cộng ví không bao giờ chạy lại. readWallet đọc số dư đã lưu còn unpaid cộng từ sổ, nên trang ví hiện balance = 0 cạnh unpaid = 100. | workers/api/src/modules/leaderboard/service.ts:148 | ✅ sửa 7d4cf64e |
| D-2 | Điểm tốc độ của ván đấu tính từ ms do CLIENT gửi. Learner tự đặt ms nhỏ để ăn điểm tối đa mọi câu. | workers/api/src/modules/liveSession/routes.ts:467 | ✅ sửa 643fc870 |
| D-3 | Tải lại trang sau khi mentor kết thúc buổi thì learner mất hẳn đường viết phản tư 4F. | apps/learn/src/ielts/Live.tsx:401 | ✅ sửa d461442f |
| D-4 | Lối vào lớp live không bao giờ hiện (G-2 ở trên) | apps/learn/src/IeltsPathway.tsx | ✅ sửa 27683b98 |
| D-5 | Phòng học treo vĩnh viễn ở "Opening the room..." khi /v1/live/form hỏng, dù phòng và câu hỏi đã về. | apps/learn/src/ielts/Live.tsx:436 | ✅ sửa d461442f |
| D-6 | Tương phản WCAG AA dưới chuẩn ở hai nút đặc của lớp live: text-white trên bg-accent chỉ 3,29:1 (cần 4,5:1) | apps/learn/src/ielts/Live.tsx:113 và :429 | ✅ sửa 37c2a13f |
D-6 là thứ làm CI đỏ thật trong phiên này. Chỗ ở dòng 429 bị cổng bắt; chỗ ở dòng 113 thì cổng KHÔNG bắt được vì lớp nằm trong một ternary bên trong template literal. Cả hai đã sửa.
4. Phát hiện 🟡 — sai mà chưa chặn phát hành
| # | Phát hiện | Bằng chứng | Trạng thái |
|---|---|---|---|
| D-7 | Bảng xếp hạng chỉ kiểm "đã đăng nhập", mà tài khoản thì ai cũng tự tạo được. Tên thật và ảnh của learner lộ ra cho bất kỳ ai lập được một tài khoản. Đây là RANH GIỚI CHÍNH SÁCH, không phải lỗi code: phạm vi mở đã được cân nhắc và ghi rõ trong đầu file. Cần chủ dự án chốt. | workers/api/src/modules/leaderboard/routes.ts:31 | ✅ sửa be939583 |
| D-8 | Route admin /leaderboard/close không chặn chốt tháng đang chạy, và INSERT OR IGNORE khoá vĩnh viễn giải sai người | workers/api/src/modules/leaderboard/routes.ts:145 | ✅ sửa c08cc011 |
| D-9 | Học vắt qua nửa đêm bị dồn hết sang ngày hôm sau, làm hỏng vế "không bỏ ngày nào" của luật thưởng | workers/api/src/modules/ieltsSkills/routes.ts:120 | ✅ sửa c52d38c0 |
| D-10 | Câu quiz check-in learner bỏ trống bị gửi lên thành lựa chọn đầu tiên | apps/learn/src/ielts/Live.tsx:147 | ✅ sửa b8fb5b0d |
| D-11 | /ielts/recordings viết toàn tiếng Việt trong sân luyện tiếng Anh (SRC-834/835) | apps/learn/src/ielts/Recordings.tsx:208 | ✅ sửa 676b91ec |
| D-12 | aria-label của menu tài khoản và nút đóng modal cứng tiếng Việt ở mọi trang /ielts, dù đã truyền cờ english | apps/learn/src/AvatarMenu.tsx:44 | ✅ sửa 676b91ec |
| D-13 | Tên learner bị bóp mất trên bảng xếp hạng ở điện thoại | đo thật, bảng dưới | ✅ sửa 8c37458c |
D-13 đo bằng số, vì mắt không thấy
Hàng của bảng có bốn phần tử, ba trong số đó (hạng, ảnh, cột phải) mang shrink-0, nên ô TÊN là chỗ duy nhất còn co được và nó gánh trọn phần thiếu. Trang không tràn ngang và không báo lỗi — đúng kiểu hỏng im lặng mà DS-001 §4b đã ghi từ SRC-900.
| Khung nhìn | Trước | Sau | Tên cần |
|---|---|---|---|
| 320px | 14px | 113px | 205px |
| 375px | 69px | 168px | 205px |
| 414px | 108px | 207px (đủ chữ) | 207px |
Chữa theo luật 2 của DS-001 §4b: dưới sm, dòng "còn thiếu bao nhiêu" xuống nằm ở dòng phụ dưới tên thay vì đứng ở cột phải. Không mất con số nào, chỉ đổi chỗ.
5. Phát hiện ⚪ — nhỏ, và nợ cũ không thuộc phiên này
| # | Phát hiện | Trạng thái |
|---|---|---|
| D-14 | hours() in ra "11h 60m" vì làm tròn hai vế độc lập. Không vô hại: 12h là đúng mốc ăn thưởng, nên learner còn thiếu một giây lại đọc thấy mình đã đủ. Nay cắt xuống: 43.199 giây ra "11h 59m", đủ 43.200 mới ra "12h". | ✅ sửa cb51b14a |
| D-15 | Bảng THÁNG in cứng of 30 days, nên tháng 31 ngày hiện "31 of 30 days", tháng 2 hiện "28 of 30 days". Nay đếm từ from/to server trả về. | ✅ sửa cb51b14a |
| H-1 | sdd-038 có hai mục "## 26." và hai mục "## 27." | ⚪ nợ cũ, đã có trên origin/main trước phiên |
| H-2 | docs/traceability.md có dòng SRC-587 hai lần | ⚪ nợ cũ, đã có trên origin/main trước phiên |
| H-3 | SRC-895 và SRC-898 đã làm xong và đã lên main, nhưng không có dòng sổ và không tài liệu canonical nào nhắc tới. | ✅ đã đóng 21.09: SRC-895 do một phiên khác ghi, SRC-898 ghi ở SDD-038 §54 bằng những gì đọc được trong mã nguồn |
| H-4 | 6/11 commit local là bản làm trùng việc đã có trên origin (SRC-884, 889, 894, 896, 898, 885) | ⚪ dấu hiệu, xem §7 |
6. Cổng: cái gì bắt được, cái gì không
| Cổng | Ở máy | Trong CI | Ghi chú |
|---|---|---|---|
check:docs (10 script) | ✓ | ✓ | bắt được SRC-904 chưa có canonical |
check:code (20 script) | ✓ | ✓ | |
typecheck | ✓ | ✓ | bắt được một nửa lỗi G-1 |
test (2.484 test) | ✓ | ✓ | |
| Tương phản WCAG AA (AS-09.3.1) | ✗ không có trong npm run check | ✓ | làm CI đỏ trong phiên này |
| Số migration liên tục, không trùng (AS-04.1.1) | ✗ | ✓ | sẽ đỏ nếu không sửa G-3 trước |
| e2e Playwright | ✗ (chạy tay) | ✓ |
Ba điểm mù đáng ghi:
npm run checkở máy KHÔNG gọi cổng tương phản. Chạy tay bằngnpm run contrast --workspace @nemo12/design-system.- Cổng tương phản đọc lớp bằng biểu thức trên mã nguồn, nên nó không thấy lớp nằm trong một ternary bên trong template literal — đúng chỗ D-6 ở dòng 113 nấp.
- Không cổng nào bắt được JSX mồ côi ngoài thân component (G-2), vì đó là cú pháp hợp lệ. Phép gác duy nhất có tác dụng là hỏi trang đã dựng, tức là e2e.
7. Điều rút ra về việc chạy nhiều phiên song song
Ba số liệu của phiên này nói cùng một chuyện:
- 6/11 commit local làm trùng việc đã có trên origin. Hai phiên khác nhau làm cùng một SRC, bằng hai bản vá khác nhau, rồi cùng tồn tại.
- Ba khối mã bị lượt gộp đặt sai chỗ (G-1, G-2), và chỉ một nửa bị
tscbắt. - Giữa lúc audit chạy, một phiên khác đang sửa cùng file và đã đẩy một bản vá chỉ sửa một trong hai chỗ của D-6; phần còn lại nằm trong cây làm việc chung chưa commit.
CLAUDE.md đã có luật worktree riêng cho việc dài. Phiên này là bằng chứng nữa rằng chi phí của lượt gộp không nằm ở xung đột git — git báo xung đột thì người ta sửa. Chi phí nằm ở chỗ git KHÔNG báo gì: hai khối mã hợp lệ, đặt cạnh nhau sai chỗ, và mọi cổng vẫn xanh.
8. Việc còn lại, xếp theo thứ tự nên làm
D-1— đã sửa trong phiên. Hai câu nay nằm trong mộtgrant()chưa nguyên tửDB.batch, và thứ tự bị ĐẢO: cộng ví trước, ghi sổ sau. Gộp thẳng theo thứ tự cũ là phá mất chống trùng, vì tới lượt câu thứ hai thì dòng sổ đã có rồi; ở thứ tự mới,WHERE NOT EXISTSđọc sổ khi nó còn chưa mang dòng ấy. Thêm phép gác cho bất biến mà lỗi này phá: số dư luôn bằng tổng sổ, đúng cả sau nhiều lượt chốt và nhiều loại thưởng.D-2 điểm tốc độ do client gửi— đã sửa trong phiên.msbị gỡ khỏi thân request; server đo từquestion_started_atcủa chính nó. Chữa luôn một lỗi thứ hai lộ ra khi sửa: đồng hồ đếm ngược trên màn learner đo từ lúc MÁY ẤY mở câu, nên ai vào muộn cũng thấy đủ 25 giây trong khi server đã tính gần hết giờ. Nay trạng thái phòng mangelapsed_ms— một KHOẢNG chứ không phải một MỐC, để máy sai giờ không làm ván đấu hiện ra đã hết giờ từ giây đầu.D-5 và D-3 hai đường cụt của phòng học live— đã sửa trong phiên. Buổi đã đóng nay mở lại được cho người đã dự (và chỉ cho người đã dự), nên biểu mẫu 4F không còn biến mất vì một lượt tải lại trang. Cổng render chỉ còn đòi trạng thái phòng, nên một lượt/v1/live/formhỏng không khoá luôn ván đấu; chỗ nào thiếu bộ câu hỏi thì nói ra và cho bấm lại tại chỗ.D-7 ranh giới ai xem được bảng xếp hạng— đã quyết và đã sửa (21.09.2026, SRC-938). Chủ dự án giao toàn quyền quyết. Chọn hướng HẸP: chỉ learner đang học và người của nhà trường;guardiankhông nằm trong danh sách. Lý do đầy đủ ở SDD-038 §34d. Hẹp thì đảo lại được bằng một chuỗi thêm vào mảng vai, còn rộng thì không.D-8 chốt tháng đang chạy— đã sửa trong phiên. Một kỳ chỉ chốt được sau khi nó kết thúc; phép gác nằm trongcloseWeek/closeMonthnên cron và nút bấm tay đều đi qua, và route trả 400 kèm ngày kỳ ấy đóng lại. Tiện thể vá nốt vệt sót của D-9: bảng xếp hạng nay cũng đọc theo ngày học.D-9 ranh giới nửa đêm của "không bỏ ngày nào"— đã sửa trong phiên. Ngày học nay bắt đầu lúc 04:00 giờ VN (learningDay), dùng cho cả đường ghi lẫn đường đọc. Lượt chốt tuần tách sang cron riêng lúc 05:00 giờ VN, và hằng số cron nằm cạnh hằng số lằn ranh nên dời sai là test đỏ chứ không phải sổ chốt nhầm tuần trong im lặng.D-11, D-12 chữ tiếng Việt còn sót trong sân luyện tiếng Anh— đã sửa trong phiên. 14 chuỗi giao diện của/ielts/recordingsvà haiaria-labelcủa menu tài khoản. Nội dung bài (*_vi) giữ nguyên tiếng Việt vì nó thuộc thang bốn chặng SRC-821, và có một phép gác riêng giữ đúng ranh giới ấy.Đưa cổng tương phản vào
npm run checkđể nó không còn là thứ chỉ CI mới biết.H-3 viết mục canonical cho SRC-895 và SRC-898 rồi mới ghi sổ.