Coding Conventions — đặt tên trong code
Documentation Conventions quy định cách viết tài liệu. File này quy định cách đặt tên trong code, và hiện chỉ có một luật, nhưng là luật áp cho toàn bộ repo.
1. Luật nền: định danh tiếng Anh, nội dung tiếng Việt
Chủ dự án chốt 2026-08-26 (SRC-600):
Mọi hằng số đều cần là tiếng Anh, mọi slug đều cần là tiếng Anh.
Ranh giới là định danh ↔ nội dung, không phải code ↔ không-code:
| Ngôn ngữ | Vì sao | |
|---|---|---|
| Định danh — hằng số, biến, hàm, type, key, slug, tên file | tiếng Anh | Định danh là thứ người khác tìm kiếm và ghép nối. LECH_LOP_TOI_DA không grep được cùng grade, lechTamLop() không đứng cạnh gradeWindow() trong autocomplete, và một người mới (hay một AI) đọc nó phải dịch trước khi hiểu. |
Nội dung — nhãn trên màn hình, rationale_vi, name_vi, thông báo lỗi cho learner | tiếng Việt (qua lang.tsx) | Đây là sản phẩm nói với người Việt. Dịch sang tiếng Anh trong code rồi dịch ngược lúc render là thêm một tầng mất mát. |
Luật này không động tới chuỗi hiển thị. Một hằng số tiếng Anh giữ một câu tiếng Việt là đúng:
const GRADE_SPAN_MAX = 2; // ✅ tên Anh
const label = "lệch quá 2 lớp"; // ✅ nội dung Việt2. Bốn loại slug, cùng một luật
"Slug" ở đây gồm mọi chuỗi định danh dạng kebab-case:
| Loại | Ví dụ sai | Ví dụ đúng |
|---|---|---|
| Đường dẫn URL | /{school}/toan-canh/{subject} | /{school}/overview/{subject} |
| Key nội bộ / giá trị enum | tab: "ngan-hang-de" | tab: "exam-bank" |
| Class CSS | .n12-cua-hong | .n12-gate-red |
| Tên file / thư mục code | HocLieu.tsx | Materials.tsx |
3. Ba ngoại lệ, và chỉ ba
Ngoại lệ phải nằm ở đây; không có ngoại lệ "ngầm hiểu".
- Danh từ riêng tiếng Việt trong dữ liệu: tên người, tên trường (
thpt-chuyen-ha-noi-amsterdam), tên địa danh. Đó là dữ liệu, không phải định danh do ta đặt ra. - ID nội dung chương trình học (
ch-phan-ung-hoa-hoc,sp-do-duoc-cai-gi-tren-vat-song-1): chúng là tên khái niệm trong chương trình Việt Nam, đang nằm trong D1 và trong hàng trăm file seed. Đổi là một cuộc migration nội dung riêng, không phải việc đặt tên. - Mã môn học (
toan,van,su,dia): đã là khoá ngoại ở nhiều bảng và xuất hiện trong URL công khai; giữ nguyên cho tới khi có đợt migration riêng.
Ba ngoại lệ này có một điểm chung: chúng là dữ liệu đã lưu, không phải tên do lập trình viên đặt lúc viết code. Luật §1 áp trọn vẹn cho phần lập trình viên tự đặt.
4. Đổi tên một thứ đã chạy thì phải làm gì
| Đổi cái gì | Bắt buộc kèm |
|---|---|
| Định danh trong code | không gì thêm — đổi hết một lượt, npm run typecheck xanh |
| Slug URL công khai | alias đường cũ (301/rewrite) giữ ít nhất 6 tháng, sửa link trong docs/, sửa e2e |
| Giá trị đã lưu trong D1 | migration đánh số theo skill d1-migrate, idempotent, chạy được cả khi code deploy trước |
Đường link đã chia sẻ là thứ ta không thu hồi được. Bỏ alias là im lặng làm hỏng link của người khác.
4b. Một ca đã làm: hai enum trạng thái (2026-08-26)
blueprint_weights.required_state và parent_beliefs.perceived_state từng là enum tiếng Việt. Migration 0082_english_state_enums.sql đổi chúng thành:
| Cũ | Mới | Cũ | Mới | |
|---|---|---|---|---|
chac | solid | on_dinh | steady | |
lung_lay | shaky | dang_chac/nhac_lai/nguy_co_quen/nen_on_lai (nhãn Retention) | solid/refresh/at_risk/relearn | |
hong | gap |
Ba chi tiết đáng lấy làm mẫu cho lần sau:
CHECKmới nhận CẢ HAI bộ tên. CI không chạy migration khi push thường nên code lên trước, bảng lên sau; chỉ nhận tên mới thì bản deploy cũ bị chặn ghi, chỉ nhận tên cũ thì bản mới bị chặn. Siết còn bốn tên tiếng Anh là việc của một migration sau.- Đọc thì quy về tên mới, đừng so tên thô.
normalizeState()trongmodules/knowledge/mastery.tslà chỗ duy nhất biết bảng tên cũ. Không có nó thì mọi blueprint chưa migrate tụt ngưỡng về mặc định — hỏng lặng lẽ, không ai thấy. - Snapshot đã lưu không migrate được.
learner_model_versionsgiữ JSON của những lần chạy trước; chúng còn nguyên tên cũ mãi mãi. Vì thế bảng nhãn ở app giữ luôn cả khoá cũ.
5. Cổng kiểm
node scripts/check-english-identifiers.mjs — nằm trong npm run check:docs, nên CI đã chạy sẵn. Nó quét định danh khai báo (const/function/type/…) và slug trong string literal, tách theo camelCase/snake/kebab, rồi báo đỏ khi gặp âm tiết tiếng Việt.
Ba cách khai ngoại lệ, theo thứ tự nên dùng:
| Cách | Viết thế nào | Dùng khi |
|---|---|---|
Khối LEGACY_* | đặt slug cũ trong một object tên có LEGACY | bảng tra slug cũ → slug mới (§4) |
| Pragma cả file | // slug-exempt: <lý do> trong ~2000 ký tự đầu | file chứa ID nội dung hay mã môn (§3.2, §3.3) |
| Đánh dấu một dòng | legacy-slug trong chú thích cuối dòng | một chỗ lẻ |
Phần quét định danh khai báo không bao giờ tắt được — mọi ngoại lệ chỉ tắt phần slug, vì ngoại lệ §3 đều nói về dữ liệu, không về tên do lập trình viên đặt.
Cổng chạy trên cây làm việc nên vẫn dính bẫy "máy local nói dối" ghi ở CLAUDE.md gốc repo (cố ý không đặt link vì file đó nằm ngoài srcDir của VitePress, link sẽ làm đỏ cả build) — git status --short sạch rồi mới tin kết quả xanh. Xem thêm sổ sự cố §9.
6. Dữ liệu dùng chung cho nhiều app thì nằm ở packages/, không chép sang từng app (SRC-867)
Ngày 2026-09-19 hai file packages.ts — một ở apps/web/src, một ở apps/marlins/src — được gộp làm một, tức chuyển từ hai bản sang một: packages/catalog. Chúng là cùng một danh mục gói học và giá, và cả hai đều mở đầu bằng đúng câu "nguồn duy nhất cho trang công khai" — trong khi có hai bản.
Cái giá đã trả, không phải giả định. Một đợt sửa chữ ngày 2026-09-19 chỉ chạm bản web, nên gần một ngày bố mẹ trên marlins đọc "Nemo Dive dành cho learner", "xây dựng Learner Model" trong khi nemo12.com đã nói khác. Không cổng nào bắt được: mỗi bản tự nó đều hợp lệ, typecheck xanh, test xanh. Lần rà sau mới lộ ra, và lộ ra vì có người đi tìm chứ không vì máy báo.
Luật: một dữ liệu mà hai app trở lên cùng hiển thị thì đặt ở packages/<tên> và khai làm workspace dependency ("@nemo12/<tên>": "*"), không cp sang app thứ hai. Chép sang là tạo hai nguồn, và hai nguồn chỉ khác nhau vào ngày ai đó sửa một bên.
Dấu hiệu nhận ra sớm: hai file cùng tên ở hai app. find apps -name '<tên>.ts' | wc -l ra số lớn hơn 1 là đáng mở ra xem — đôi khi trùng tên mà khác việc, nhưng trùng cả nội dung thì không.
Không áp cho dữ liệu SINH RA từ nguồn khác. apps/marlins/src/parentCoursesData.ts được sinh từ D1 và có dòng "ĐỪNG SỬA TAY" ngay đầu file; ở đó nguồn thật là D1, bản trong repo chỉ là ảnh chụp. Luật này nói về dữ liệu mà repo là nguồn.
7. Khối JSX phải nằm TRONG thân component (SRC-935)
Một phần tử JSX đứng một mình ở cấp module là cú pháp hợp lệ trong .tsx, và đó chính là chỗ nguy hiểm: <a>...</a> trên một dòng riêng là một expression statement, còn {/* ... */} là một block statement chứa chú thích. tsc không phàn nàn, eslint không đỏ, vite build chạy trót lọt. React dựng phần tử ấy đúng một lần lúc nạp module rồi vứt đi, nên nó không bao giờ được vẽ.
Đã xảy ra thật ngày 20.09.2026 (Audit #015, D-4): một lượt gộp nhánh đặt khối lối vào "Join a live class" ra ngoài thân IeltsTaskPage. /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 đỏ. Nó chỉ lộ ra khi có người đọc lại cấu trúc file bằng mắt.
Cổng scripts/check-orphan-jsx.mjs (trong npm run check:code) quét bằng parser của TypeScript, không bằng biểu thức chính quy: JSX nhiều dòng không dùng {} nên mọi cách đếm ngoặc đều đọc nhầm các dòng nối tiếp của một phần tử bình thường thành "cấp module" — bản thử đầu tiên bằng regex cho ra 55 chỗ nghi ngờ mà cả 55 đều sai.
Cổng này không bắt được lớp lỗi anh em: một component khai báo đúng nhưng không ai gọi. Chỗ ấy phải gác bằng e2e hỏi TRANG ĐÃ DỰNG chứ không hỏi mã nguồn — xem apps/learn/e2e/ieltsDashboard.spec.ts.
Trace
- Nguồn: SRC-600 (chủ dự án 2026-08-26) · SRC-935 (tự phát hiện khi rà lại Audit #015, 21.09.2026).
- Liên quan: Documentation Conventions · Dependency Hygiene.