---
url: https://docs.nemo12.com/architecture/sdd-038/skill-pages.md
description: >-
  Bốn trang kỹ năng, năng lực nhỏ, thang bốn chặng Việt trước Anh sau, các phần
  thi Nói Viết Đọc Nghe
---

# SDD-038 · Trang kỹ năng và thang học

> Một phần của [SDD-038 Cổng IELTS](index.md). Phần đầu là đặc tả đang áp dụng, mỗi mục giữ số § cũ; diễn biến và lý do các vòng quyết định nằm ở mục **Lịch sử quyết định** cuối trang.

## 5c. Bốn trang kỹ năng và thang ba bậc (SRC-801, chỉ đạo 2026-09-17 vòng 3)

### `/ielts` đổi vai, bảy việc cũ lùi về `/ielts/settings`

Danh sách tám việc ở §1 là việc **khai một lần rồi thỉnh thoảng sửa**. Thứ learner quay lại hằng ngày là **luyện**. Đặt tám việc khai báo ở cửa vào là bắt một đứa trẻ đi qua một biểu mẫu mỗi lần nó muốn học, nên `/ielts` nay là cửa vào bốn kỹ năng và tám việc kia lùi một tầng. Luật hé lộ từng bước (`pathway.ts`) **không đổi một dòng** — chỗ đứng của trang không phải một quyết định sản phẩm về việc ai được xem gì.

### Sổ hoạt động: ghi VIỆC, không ghi kết quả

`learning_events` (migration 0231) đứng cạnh `ielts_practice_attempts` chứ không thay nó. Bảng kia ghi **kết quả** (đúng mấy trên mấy); ba con số chủ dự án hỏi lại nằm cả ở chỗ nó cố ý không ghi:

* **"Số phút đã nghe, tính cả những lần nghe lại"** — nghe lại lần thứ tư một đoạn không sinh ra lượt làm bài nào. `useListeningMinutes` đo bằng sự kiện `play`/`pause` của chính thẻ `<audio>`, không nhân số lần bấm với độ dài bài: learner bấm phát rồi dừng ở giây thứ mười là đã nghe mười giây.
* **"Số ngày học liên tục"** — một ngày chỉ mở kho từ ra đọc vẫn là một ngày không bỏ. Chuỗi đếm qua **mọi** kỹ năng; `day` tính theo **giờ Việt Nam ngay lúc ghi**, vì học lúc 0h30 sáng thứ Tư ở Hà Nội là 17h30 UTC thứ Ba, và tính theo UTC thì chuỗi mất một mắt.
* **Thời gian ở trong bài** — `useTimeOnTask` dừng đồng hồ khi tab ẩn, bỏ qua các lượt dưới 20 giây, và cắt ở 90 phút. Một tab bỏ quên qua đêm không được thành *"hôm nay con học mười bốn tiếng"* trên màn hình một đứa trẻ.

Vốn từ đi theo **cặp**: *đã gặp* và *đã chắc*. Một từ thành "đã chắc" khi learner gặp lại nó ở **một bài khác** — gặp lại trong chính đoạn vừa đọc là nhớ mặt chữ, gặp lại ở ngữ cảnh mới mới là bắt đầu có từ ấy. Cả hai tính **cộng dồn cả đời học**, không cắt theo cửa sổ bảy ngày: cắt theo cửa sổ thì con số tụt mỗi lần learner nghỉ một tuần, tức là hệ nói với đứa trẻ rằng nó đã quên — điều mà phép đếm này không hề biết.

Learner chưa có gì trong sổ thì trang **ẩn hết số liệu**, chỉ còn một lời mời. Một bảng toàn số 0 là bản kê những việc learner chưa làm, đặt trước mắt nó vào đúng lúc nó chưa kịp làm gì.

### Hai nút, hai thuật toán đã có sẵn

"Học tiếp" hỏi *bài nào hợp sức con lúc này*; "ôn lại" hỏi *thứ nào con sắp quên*. Gộp hai câu ấy vào một phép xếp hạng là sớm muộn nút "ôn lại" đưa ra một bài chưa từng làm, và lúc ấy cái nút hứa "đỡ tốn năng lượng" lại là cái nút nặng nhất trang.

Đường cong quên lấy thẳng `decayedRetention` + `initialStability` của [SDD-017](../sdd-017-retention.md). Khác biệt duy nhất là **đơn vị**: engine gốc tính trên node của chương trình học, ở đây một "thứ để nhớ" là một BÀI (`cluster:seq`). Bài luyện IELTS chưa gắn vào cây node, và gắn bừa vào một node nào đó chỉ để dùng lại được truy vấn cũ sẽ làm hỏng chính cây ấy. Viết một đường cong quên thứ hai thì tệ hơn nữa: ngày chúng lệch nhau, không ai biết bên nào đúng mà cả hai đều đang nói với learner.

Mỗi nút mang theo **lý do** của nó, do server viết. Một nút "hệ thống đã chọn giúp con" không nói vì sao là một hộp đen, và một đứa trẻ bấm vào hộp đen vài lần sẽ thôi tin nó.

### Câu ghi nhận: thư viện viết sẵn, điều kiện là mã

Ghi nhận khác khen. *"Con giỏi lắm"* nói về đứa trẻ, đúng dù nó có làm gì hay không, nên nó rỗng. *"Sáu ngày liền con không bỏ buổi nào"* nói về **việc**, chỉ đúng khi việc ấy có thật, và không thể nói với người chưa làm.

Chủ dự án chọn thư viện câu viết sẵn thay vì để model sinh. Lý do: một model sinh lời khen sẽ có ngày khen một learner vừa tụt điểm, vì nó đọc con số rồi tự dựng một câu nghe cho tử tế. Ở đây điều kiện là mã — câu về chuỗi 6 ngày **chỉ** chạy khi chuỗi đạt 6. Tối đa ba câu một lần: nhiều hơn thì mắt lướt qua cả cụm, và câu thứ nhất, câu đáng đọc nhất, mất luôn.

Có một câu cho trường hợp **tỉ lệ đúng tụt**, và nó không phải câu an ủi. Learner tự chọn bài khó hơn thì tỉ lệ đúng tụt là chuyện phải xảy ra; im lặng đúng lúc ấy sẽ dạy đứa trẻ rằng chọn bài dễ thì hệ thống mới vui.

### Thang ba bậc cho nói và viết

| Bậc | Tên | Chấm cái gì | Có band |
| --- | --- | --- | --- |
| 1 | Tập làm | Có trả lời đúng đề không, người đọc có hiểu không, có đủ dài không | không |
| 2 | Làm được | Có một ý chính, có chỗ đỡ cho ý ấy, các câu có nối vào nhau | không |
| 3 | Chuẩn IELTS | Bốn tiêu chí thật của kỳ thi | có |

Thang band IELTS bắt đầu có nghĩa từ khoảng 4.0. Một learner mới viết được sáu câu đơn chấm theo bốn tiêu chí sẽ nhận 3.0, rồi 3.0, rồi 3.0 — ba tuần liền cùng một con số trong khi bài của nó rõ ràng đã khác. Thang ấy không sai, nó chỉ quá thô ở đoạn đầu để nhìn thấy tiến bộ, mà nhìn thấy tiến bộ là việc trang này sinh ra để làm.

**Bậc là của ĐỀ BÀI, không phải của learner.** Không có chỗ nào ghi "learner này đang ở bậc 2", và ba bậc bày ra cùng lúc không khoá bậc nào. Gắn bậc vào người là dựng một cái nhãn tự ứng nghiệm: hệ chỉ đưa bài bậc 1 cho người nó đã xếp bậc 1.

Lệnh cấm trả band ở bậc 1-2 nằm **trong mã** (`scoreLadder` ép `band: null`), không chỉ trong prompt. Một model được dặn "đừng chấm điểm" vẫn sẽ chấm điểm vào ngày nó thấy bài giống bài thi — và lúc ấy một đứa trẻ bậc 1 đọc được con số "3.0" cho bài đầu tiên nó dám viết. Hai lớp: prompt dạy, mã chặn.

Nhận xét (`ielts.ladder-feedback`) viết cho **chính learner**, khác `ielts.production-feedback` viết cho mentor. Thứ tự các ô là một quyết định: `did_vi` (con đã làm được gì) đứng **trước** `one_step_vi`. Model viết theo thứ tự nó được hỏi, và hỏi phần làm được trước là cách rẻ nhất để phần ấy không thành câu lót đường cho phần chê. **Một** việc nên sửa, không phải một danh sách: learner sửa được một thứ mỗi lần.

### `track` tách bài luyện khỏi bài chẩn đoán

Migration 0232 thêm `ielts_productions.track`. Không có nó, ba chỗ đang đọc bảng theo kiểu "lần nộp mới nhất của kỹ năng ấy" sẽ hỏng **lặng lẽ**: `pathway.ts` đánh dấu bước chẩn đoán là xong cho learner chưa từng làm bài chẩn đoán; `model.ts` để một bài luyện đè lên band của bài chẩn đoán; `report.ts` đưa mentor đọc nhầm bài. Một cột thay vì một bảng thứ hai, vì hai loại bài có cùng vòng đời.

### Giới hạn: ba tầng, ba thứ khác nhau (chỉ đạo chủ dự án 2026-09-17)

1. **Trần theo từng kỹ năng** — viết 8 bài/ngày, nói 6 đoạn/ngày. Nói chặt hơn viết vì một lượt nộp bài nói gọi model **hai** lần (Whisper phiên âm, rồi chấm). Cái đắt hơn phải chặt hơn.
2. **Trần tổng** — 10 lượt chấm/ngày/learner, nhỏ hơn tổng hai trần trên (8+6). Cố ý: hai trần kia lo nhịp học, trần này lo hoá đơn, và nó chặn cả khi learner chia đều giữa hai kỹ năng để lách.
3. **Trần độ dài đầu vào** — 300/450/600 từ và 120/180/240 giây theo bậc. Chi phí một lượt chấm tỉ lệ với số token đi vào, nên một bài 4.000 từ đắt bằng hai mươi bài 200 từ mà vẫn chỉ tính là **một** lượt. Đếm lượt mà không chặn độ dài là chặn đúng một nửa hoá đơn. Vượt trần thì **từ chối thẳng**, không cắt bớt rồi chấm phần đầu — nhận xét về một bài không phải bài learner viết còn tệ hơn một lời từ chối.

Số lượt còn lại **bày ra trước khi learner làm bài**, đọc bằng `peekRateLimit` (đọc mà không tăng bộ đếm — dùng `enforceRateLimit` ở đường đọc thì mở trang ra xem còn mấy lượt sẽ tiêu mất một lượt). Một cái trần bất ngờ chặn ngang lúc learner vừa viết xong là một lời từ chối; cùng cái trần ấy hiện sẵn thì chỉ là một luật chơi.

Sổ hoạt động có trần riêng và khác loại: 240 lượt ghi/giờ, `seconds <= 7200` ở cả cổng API lẫn CHECK của bảng. Đường này không gọi model lần nào — trần ở đây chống **bơm sổ**, không chống tốn tiền.

## 5d. Một tên miền: mạch luyện chuyển hẳn về learn (SRC-805, 2026-09-18)

Toàn bộ mã của app `ielts` cũ chuyển về `apps/learn/src/ielts/` — 3.942 dòng, không sửa logic. Ba thứ
phải chỉnh và mỗi thứ có một cái bẫy riêng:

**Tiền tố URL nằm ở đúng một chỗ.** `router.ts` thêm `BASE = "/ielts"`, và cả `parse` lẫn `href`
đi qua nó. Rải tiền tố vào từng link là ngày đổi tiền tố sẽ có vài chỗ quên — mà một link quên
tiền tố vẫn trông y hệt một link bình thường cho tới lúc có người bấm.

**Hai họ URL từng chạm nhau.** Tám việc khai báo cũ ở `/ielts/<task>`, và hai trong số đó tên là
`writing` và `speaking` — đúng tên hai kỹ năng luyện. Chúng lùi xuống `/ielts/settings/<task>`;
link cũ `/ielts/<task>` vẫn nhận (luật "link đã gửi đi không được gãy"). Danh sách 12 khoá — vốn
là trang chủ của app cũ — về `/ielts/courses`, nhường `/ielts` cho hub bốn kỹ năng.

**Mạch luyện đứng TRƯỚC cổng đăng nhập của learn**, và đây là chỗ nguy hiểm nhất của cả lần chuyển.
140 trang nội dung ở đây vốn công khai; đặt chúng sau cổng đăng nhập là lặng lẽ khoá cả trăm trang
đang mở, mất phần SEO đã có, và **không có lỗi nào hiện ra** để ai đó nhận ra. Mạch giữ
`AuthProvider` riêng: chưa đăng nhập thì luyện y như cũ, đăng nhập rồi thì bài vào sổ.

**`ielts.nemo12.com` KHÔNG bị xoá** — nó thành một worker chỉ làm một việc: 301 về
`learn.nemo12.com/ielts<đường dẫn cũ>`, giữ nguyên cả path lẫn query. Tắt hẳn là biến mọi link đã
gửi đi thành lỗi DNS. 301 chứ không 302: chuyển nhà vĩnh viễn, và chỉ 301 mới dồn giá trị tìm kiếm
của URL cũ sang URL mới.

**Giao diện đổi theo, không phải sửa từng file.** Mạch cũ nền sáng; learn nền tối. `Practice.tsx`
bọc mạch trong `.n12-deep`, lớp khai lại chính các token mà các trang ấy đang dùng (kể cả
`--n12-ocean-900` đảo thành màu chữ sáng). Đó đúng là lý do bảng màu đi bằng token chứ không bằng
mã màu viết thẳng.

**Phạm vi deploy phải theo.** `.github/app-paths.json`: `learn` nay nuốt cả `content/ielts-*` vì nó
gói nội dung ấy; thiếu vế này thì sửa một bài đọc sẽ không deploy learn, và không ai biết cho tới
lúc mở trang thấy bài cũ. `ielts` rút về đúng `^apps/ielts/` và không còn bước build.

Cổng: `scripts/check-one-domain.mjs` trong `npm run check:code` cấm mọi **link** sang tên miền cũ
(`href=`, `location.*`, `window.open`). Nhắc tên miền trong chú thích vẫn được — lịch sử của mạch
này nằm ở đó. Một `href` sai tên miền không hỏng gì mắt nhìn ra được: thẻ vẫn hiện, vẫn bấm được,
vẫn mở ra một trang thật; thứ duy nhất sai là learner rời khỏi learn.nemo12.com.

## 5e. Năng lực nhỏ: học lý thuyết rồi kiểm hiểu (SRC-805, 2026-09-18)

**35 năng lực, 140 bài tập.** Đọc 10 và Nghe 5 lấy đúng danh sách đã có trong dữ liệu bài tập
(`SKILL_LABEL`, `LISTENING_SKILL_LABEL`), nên mỗi câu hỏi trong 1.560 câu đọc đều trỏ được về một
năng lực. Nói và Viết chưa có danh sách nào nên phải soạn từ đầu: đợt đầu 4 mỗi kỹ năng (0234),
đợt sau (SRC-810, migration 0235) bổ sung 6 nữa mỗi kỹ năng để bằng Đọc.

Nghe giữ 5 chứ không kéo lên 10, và đó là chủ ý: 5 là đúng số nhóm câu hỏi thật trong dữ liệu bài
nghe. Thêm cho tròn số là soạn lý thuyết cho những thứ không có bài tập nào ở phía sau.

Sáu năng lực Nói bổ sung nhắm vào chỗ learner Việt tắc nhiều nhất mà sách ít dạy: *diễn đạt vòng
khi thiếu từ*, *nói cụ thể thay vì chung chung*, *nói một mạch hai phút* (Part 2), *so sánh và cân
nhắc* (Part 3), *sửa lỗi giữa chừng*, *giữ nhịp nói*. Sáu năng lực Viết phủ phần vòng đời một bài
mà đợt đầu bỏ trống: *đọc kỹ đề*, *mở bài*, *kết bài*, *thừa nhận phía kia*, *đổi kiểu câu*, và
*mô tả biểu đồ* cho Task 1.

**Nội dung nằm trong D1, không nằm trong `content/`** (quyết định của chủ dự án). Bài đọc ở
`content/` vì chúng là ngữ liệu ổn định; lý thuyết thì ngược lại, nó là **chữ giải thích**, và chữ
giải thích chỉ hay lên bằng cách sửa đi sửa lại sau khi xem học sinh thật đọc nó. Bắt mỗi lần sửa
một câu phải qua một vòng deploy là bảo đảm nó sẽ không được sửa. Đổi lại là mất cổng kiểm của git,
nên cột `status` (nháp/phát hành) gánh phần ấy: đường công khai lọc ngay trong câu SQL.

**Năm phần là KHUÔN, thành năm cột riêng** (migration 0233): là gì · cách làm · ví dụ có giải ·
bẫy thường gặp · bài tập. Nhét cả năm vào một cột markdown thì khuôn biến mất ngay bài thứ ba —
người soạn sau sẽ bỏ phần "bẫy" vì không có gì nhắc, mà phần bẫy chính là thứ học sinh Việt mất
điểm nhiều nhất và sách ít nói. Trên màn hình, **bẫy đứng TRƯỚC bài tập**: đọc sau khi làm thì nó
chỉ còn là lời an ủi; đọc trước thì learner nhận ra chính cái bẫy ấy ngay ở bài kế tiếp.

**Bài tập siêu đơn giản, và ràng buộc ấy nằm trong hình dạng bảng.** Một bài là 2-3 câu ngữ liệu,
một câu hỏi, vài lựa chọn. Không có cột nào cho một bài đọc dài, và cố ý không có: bài dài đo lẫn
hai thứ (có hiểu lý thuyết không, và có đọc hết nổi không) rồi trả về một kết quả không tách được.
Chấm **ngay từng câu**, không chấm cả cụm ở cuối — vòng lặp càng ngắn thì lý thuyết càng dính.

Sau khi chấm, chỉ **đáp án đúng** được tô màu; lựa chọn sai learner vừa chọn chỉ mờ đi và gạch
ngang. Hai lý do: trong bảng màu này `accent` và `coral-deep` gần nhau nên tô cả hai thì phải đọc
chữ mới biết cái nào đúng; và sơn đỏ chỗ một đứa trẻ vừa chọn sai đi ngược lại chính tinh thần ghi
nhận của §5c.

**Hai lối vào, cho hai lúc khác nhau** (chủ dự án chọn cả hai):

* Một dải tên năng lực trên trang kỹ năng — lúc learner **chủ động** muốn học.
* Một link ngay dưới lời giải của câu learner **vừa làm sai** — lúc con vừa trượt một câu suy luận
  là lúc chữ "Suy luận là gì" có nghĩa nhất. Câu đúng thì không hiện link: một lời mời học lại sau
  mỗi câu đúng chỉ thành nền rồi thành thứ bị bỏ qua.

**Vào sổ khi làm HẾT bài tập, không phải khi mở trang.** Học lý thuyết là một việc đã làm nên nó
tính vào chuỗi ngày (§5c); ghi lúc mở trang thì mở ra rồi đóng lại cũng thành "đã học", và chuỗi
ngày mất hết ý nghĩa.

**Màn admin** (`apps/admin`, tab "Năng lực IELTS") lưu khi **rời ô** và gửi **đúng ô vừa sửa** —
cùng luật với `Courses.tsx` và `Pricing.tsx`. Đáp án chọn bằng danh sách chứ không gõ số: một chỉ
số trỏ ra ngoài danh sách lựa chọn thì bài vẫn hiện bình thường mà chấm sai mọi lượt.

## 5f. Thang bốn chặng: tiếng Việt trước, tiếng Anh sau (SRC-821, 2026-09-18)

**Vì sao tiếng Việt đi trước.** Tìm ý chính là một thao tác **tư duy**, không phải việc của vốn từ.
Cho learner gặp nó lần đầu bằng tiếng Anh là bắt hai việc khó xảy ra cùng lúc: hiểu nghĩa từng chữ,
và nhận ra đoạn văn đang nói gì. Sai lúc ấy thì không ai — kể cả learner — tách được là sai vì
thiếu từ hay vì chưa biết cách. Học bằng tiếng mẹ đẻ trước thì lúc sang tiếng Anh chỉ còn **đúng
một** việc mới.

**Bốn chặng** (cột `stage`, migration 0238), mỗi chặng khác chặng trước ở đúng một điểm:

| Chặng | Ngôn ngữ | Khác chặng trước ở điểm nào |
| --- | --- | --- |
| 1 | Việt | Đoạn 3-5 câu, ý chính rõ. Việc duy nhất là nhận ra. |
| 2 | Việt | Nhiều **kiểu đoạn** khác nhau, mỗi bài một kỹ thuật. |
| 3 | Anh | Cùng loại đoạn đã quen, đổi sang tiếng Anh. |
| 4 | Anh | Đoạn dài hơn, trừu tượng hơn, gần đề thi. |

Chia nhỏ hơn bốn thì ranh giới nhoè và người soạn bài sau sẽ nhét bài vào chặng nào cũng được.

**Chặng 2 dài nhất, và đó là một ràng buộc có test.** Ý chính hiện có 6 · 12 · 6 · 6 bài. Mười hai
kỹ thuật: câu chủ đề đầu đoạn · câu chủ đề cuối đoạn · đoạn không có câu chủ đề · đoạn phản đề ·
đoạn liệt kê ví dụ · đoạn nhân-quả · đoạn so sánh · đoạn có số liệu · đoạn kể chuyện dẫn vào nhận
định · chi tiết lặp nhiều lần vẫn không phải ý chính · đoạn mở bằng câu hỏi · đoạn nhượng bộ. Tên
kỹ thuật (`technique_vi`) hiện **sau khi chấm**, không hiện trước đề: nói trước là nói mất đáp án,
còn đọc sau thì learner có một cái tên để gọi đúng thao tác vừa làm.

**Màn hình còn đúng ba thứ** (thay cho năm mục xếp dọc của §5e): một thanh bốn chặng đầy dần không
chữ · **một** đoạn văn với các lựa chọn của nó · một dòng "Cách làm" gập lại, mặc định đóng. Thứ
tự cũ hợp lý khi viết ra nhưng sai khi dùng: learner phải đọc bốn mục chữ trước khi được làm thử
điều đang học, mà bốn mục ấy dễ hiểu hơn hẳn **sau** bài đầu tiên. Lý thuyết không mất đi, nó lùi
vào sau một dòng bấm được, và phần lớn learner sẽ mở nó đúng lúc cần: ngay sau khi vừa sai một bài.
Dùng `<details>` của trình duyệt chứ không dựng lại bằng state — nó đã có sẵn bàn phím, aria và
nút Tìm trong trang của Chrome mở được cả phần đang đóng.

**Cả 35 năng lực đều đã lên thang** (migrations 0240-0246, cùng SRC-821). Mỗi năng lực ngoài Ý
chính nhận thêm 10 bài: 3 bài Việt chặng 1, 5 bài Việt chặng 2 (mỗi bài một kỹ thuật có tên), 2 bài
Anh chặng 4; bốn bài tiếng Anh nạp ở 0234-0235 ở lại chặng 3. Tổng cộng 506 bài, trong đó 290 bài
tiếng Việt (108 ở chặng 1, 182 ở chặng 2) và 216 bài tiếng Anh. Phần tiếng Việt nhiều hơn là chủ ý:
sang tiếng Anh, learner chỉ còn học **một** việc mới.

Ba điều đáng nói vì chúng dễ bị hiểu nhầm thành lỗi:

* **Bài NGHE luyện bằng chữ.** Bảng này chỉ có ngữ liệu chữ, nên một bài nghe ở đây là đoạn lời
  thoại đọc bằng mắt. Thứ đang luyện không phải tai mà là thao tác: nhận ra người nói vừa đổi ý,
  nhận ra con số nào là con số cuối cùng, nhận ra một câu lịch sự đang mang nghĩa từ chối. Chỗ
  luyện tai thật là dữ liệu bài nghe có âm thanh (16 bài, 160 câu).
* **Bài NÓI và VIẾT là bài chọn đáp án.** Chúng đo việc NHẬN RA, không đo việc làm. Trước khi tự
  nói được một câu trả lời tốt, learner phải phân biệt được câu trả lời tốt với câu nghe trôi chảy
  mà lạc đề. Chỗ nói và viết thật, chấm ở server, là `ielts_productions`.
* **Với Viết, chặng tiếng Việt trả lời một hiểu lầm cụ thể**: "em viết dở vì vốn từ em kém". Phần
  lớn thì không phải. Lập trường đổi giữa chừng, đoạn thân bài không có câu chủ đề, bỏ sót một vế
  của đề, cả ba lỗi ấy viết bằng tiếng Việt cũng mắc y hệt. Cho learner thấy điều đó bằng chính
  tiếng mẹ đẻ là cách nhanh nhất để họ thôi đổ cho vốn từ và sửa đúng chỗ.

Cổng gác đợt này nằm ở `microSkills/routes.test.ts`: không năng lực nào được có ít hơn bốn chặng,
không bài nào được lệch ngôn ngữ so với chặng, và mọi bài chặng 2 phải có tên kỹ thuật. Ba lỗi ấy
đều KHÔNG hiện ra trên màn hình, chúng chỉ lặng lẽ xoá mất lý do thang này tồn tại.

Server chặn cùng chỗ đó, và chặn trên **bản sau khi ghép** chứ không trên bản gửi lên: người sửa
hay đổi mỗi một ô (bớt lựa chọn mà quên đáp án, hoặc ngược lại), nên một nửa thông tin nằm trong
yêu cầu và nửa kia trong CSDL. Kiểm riêng từng nửa là bỏ lọt đúng những lần sửa hỏng hay gặp nhất.

Cổng: `modules/microSkills/routes.test.ts` (16 test) gác bài nháp không lọt ra đường công khai,
đường admin chặn cả người chưa đăng nhập lẫn người không có vai, sửa nửa vời bị chặn, và JSON hỏng
trong một cột không làm sập cả trang. Hai test riêng kiểm chính **nội dung đã nạp**: đủ 23 năng lực
và 92 bài tập, mọi đáp án trỏ vào một lựa chọn có thật, không bài nào bỏ trống một trong năm phần.
Ở `apps/learn`: 3 case e2e.

Hai phép đếm trong test là RÀNG BUỘC, không phải con số cho vui: thêm một năng lực mà quên nạp bài
tập cho nó thì mọi test khác vẫn xanh, còn hai dòng ấy thì đỏ.

## 5f. Trang kỹ năng là một hub, phần bị lấy đi có URL riêng (SRC-812, 2026-09-18)

**Vấn đề.** Bốn trang kỹ năng dựng ở §5c đều xếp chồng bốn khối khác loại trên một màn: câu ghi
nhận kèm số liệu, nút học tiếp, dải năng lực, rồi danh sách bài. Bốn khối ấy đều đúng, nhưng đứng
cạnh nhau thì không khối nào còn là lối đi chính — learner phải ĐỌC hết một màn hình mới biết bấm
vào đâu.

**Luật.** Mỗi trang kỹ năng nay chỉ trả lời một câu: *đi đâu bây giờ*. Tối đa 5 ô, mỗi ô một lối đi.

**Thứ bị lấy đi không mất, nó chuyển sang URL của chính nó** — và một URL riêng thì gửi link được,
mở tab mới được, bấm Back đúng:

| Phần bị lấy khỏi trang kỹ năng | Chỗ ở mới |
| --- | --- |
| Danh sách cụm bài đọc + số liệu | `/ielts/reading/clusters` |
| Danh sách cụm bài nghe | `/ielts/listening/clusters` |
| Dải năng lực nhỏ | `/ielts/skills/<kỹ năng>` (§5e) |
| Chọn đề và nộp bài của một bậc | `/ielts/<kỹ năng>/levels/<n>` |

**Ô nằm ở ĐÚNG MỘT file** (`apps/learn/src/ielts/Hub.tsx`): bốn bản chép tay của cùng một cái thẻ
là bốn chỗ sẽ lệch nhau vào ngày ai đó chỉnh một cái. Cùng file ấy giữ `useSkillPage()` — trang kỹ năng
do server dựng, trong đó có bài kế tiếp cho ô "Học tiếp"; chưa đăng nhập hoặc API hỏng thì nó trả
`null` và ô vẫn đứng nguyên
với lời mời mặc định, vì một trang luyện mở công khai (§5d) không được phép trống chỉ vì một lời gợi
ý vắng mặt.

**Bậc vẫn là của ĐỀ BÀI, không phải của learner** (§5c): ba ô bậc mở sẵn cả ba, không khoá ô nào.

**Số liệu quay lại trang kỹ năng, nhưng gọn trong MỘT DÒNG** (chỉ đạo cùng ngày, vòng 2). Bốn ô số
liệu ở lại trang danh sách; ngay dưới tiêu đề trang kỹ năng chỉ còn một dòng nhỏ — phút tuần này ·
từ đã gặp · từ đã chắc · ngày liên tục (`SkillLine` trong `Hub.tsx`). Chưa có lịch sử thì không vẽ
dòng nào, giữ nguyên luật §5c: một dòng toàn số 0 là bản kê những việc learner CHƯA làm. Cả trang
đọc API đúng một lần qua `useSkillPage()`.

**Chuỗi ngày là của RIÊNG từng kỹ năng** (chỉ đạo cùng ngày, vòng 3 — đảo quyết định cũ ở §5c).
Trước đó `streak` đếm qua mọi kỹ năng trong khi `has_history` và bảy ô ngày tính riêng từng kỹ
năng, nên trang đọc có thể khoe "5 ngày liên tục" kiếm được hoàn toàn bằng việc nghe, ngay cạnh
một bảng nói rằng phần đọc chưa có gì. Hai con số cạnh nhau đo hai thứ khác nhau thì con số nào
cũng mất nghĩa. Nay `buildProgress()` đếm chuỗi trên đúng dòng của kỹ năng đang xem, và truy vấn
`allDays` trong `modules/ieltsSkills/routes.ts` lọc thêm `AND skill=?2` để chuỗi dài nhất cũng
theo cùng một thước.

**Số phút không được mất khi learner đóng tab** (chỉ đạo cùng ngày, vòng 4). `useTimeOnTask` và
`useListeningMinutes` trước đó chỉ gửi mẩu thời gian trong cleanup của React, tức là chỉ khi rời bài
BÊN TRONG SPA; đóng tab hay tắt máy thì cleanup không chạy và cả buổi học biến mất — và nó mất đúng
những buổi dài nhất, vì buổi càng dài learner càng hay đóng thẳng tab. Nay `pagehide` gửi mẩu ấy qua
`beaconLearningEvent()`, một chốt `sent` giữ cho mẩu chỉ đi một lần dù `pagehide` và cleanup cùng
chạy, và `pageshow` với `persisted` (trang sống lại từ bfcache) mở chốt cho một lượt mở bài mới.

Đường gửi là `fetch(..., { keepalive: true })` chứ **không** phải `navigator.sendBeacon`: beacon
không đặt được `Content-Type: application/json` mà không kích hoạt preflight và không nói được
`credentials: "include"`, trong khi API ở tên miền khác và xác thực bằng cookie phiên — một beacon
như vậy hỏng im lặng. `apps/learn/src/ielts/api.test.ts` gác ba thuộc tính ấy.

## 5g. Các phần thi của Nói và Viết (SRC-815, 2026-09-18)

**Ba tầng, không trộn.** Kỳ thi chia bài ra làm **phần thi** (Writing Task 1/Task 2, Speaking Part
1/2/3); trong một phần thi, đề rơi vào vài **dạng đề** quen thuộc; và cắt ngang mọi dạng đề là các
**năng lực nhỏ** của §5e. Ba tầng ba chỗ ở: hai tầng đầu nằm trong bundle
(`apps/learn/src/ielts/data/examParts.ts`), tầng thứ ba nằm trong D1.

**13 dạng đề**: Task 1 có 4 (biểu đồ có thời gian, bảng/tròn/cột không thời gian, quy trình, bản đồ),
Task 2 có 4 (nêu quan điểm, bàn hai phía, vấn đề và cách giải, đề hỏi hai câu), Speaking Part 1 có 3,
Part 2 có 4, Part 3 có 4. Mỗi dạng có ba thứ: **dấu hiệu nhận ra** nó trong đề, **bố cục** bài, và
phần bẫy đứng ở cấp phần thi.

**Vì sao nội dung nằm trong bundle chứ không đi qua API.** Trước đợt này, `/ielts/writing` và
`/ielts/speaking` lấy MỌI ô từ `fetchSkillPage`, nên người chưa đăng nhập mở ra thấy một trang
trắng — trong khi `/ielts/reading` và `/ielts/listening` bày sẵn toàn bộ nội dung cho mọi người. Nay
hai trang ấy đi theo đúng hình của Đọc và Nghe: ô có sẵn trong bundle đứng trước, phần cần tài khoản
lùi về URL riêng `/ielts/<kỹ năng>/levels`.

**Một lỗi điều hướng được sửa cùng lúc.** §5d đẩy bảy việc khai báo xuống `/ielts/settings/<task>`
để `/ielts/writing` chỉ còn một nghĩa, nhưng nhánh giữ link cũ (`/ielts/<task>`) vẫn đứng trước nhánh
luyện và vẫn khớp hai khoá `writing`, `speaking`. Hệ quả: bấm WRITING trên hub bốn kỹ năng thì rơi
vào **bài viết chẩn đoán**, không phải trang luyện viết. Nhánh giữ link cũ nay loại trừ đúng hai khoá
ấy; hai việc khai báo vẫn tới được bằng địa chỉ thật của chúng.

Cổng: `apps/learn/e2e/ieltsExamParts.spec.ts` (5 case) chạy **không** mock API, tức là chạy như một
người lạ mở link — đó là điều đáng gác nhất ở đây, vì trang trắng khi chưa đăng nhập là lỗi không ai
thấy trên máy mình.

## 5h. Đọc và Nghe cũng có tầng đó, nhưng chia theo hai lối khác nhau (SRC-825, 2026-09-18)

**Nghe chia theo PHẦN THI, Đọc chia theo DẠNG CÂU HỎI, và đó không phải là sự thiếu nhất quán.**

Bài nghe có bốn phần thật, khác nhau thật: Part 1 hội thoại đời thường, Part 2 một người nói về
nơi chốn (chỗ hay có bản đồ), Part 3 hai ba người bàn bài vở, Part 4 bài giảng một mạch. Learner
gặp đúng bốn phần ấy, theo đúng thứ tự ấy, và mỗi phần đòi một cách chuẩn bị riêng.

Bài đọc thì không. Nó có ba passage, nhưng ba passage chỉ khác nhau về ĐỘ KHÓ, không khác về cách
làm; thứ quyết định cách làm là dạng câu hỏi, và một dạng câu hỏi rơi vào passage nào cũng được.
Dựng ra "Passage 1 / 2 / 3" rồi gán dạng câu hỏi cho từng passage là dạy một điều không đúng với
đề thi thật. Nên với Đọc, mỗi mục là một NHÓM DẠNG CÂU HỎI:

| Nhóm | Kiểu bên trong | Số câu điển hình |
| --- | --- | --- |
| Đúng / Sai / Không có | TFNG (sự việc), YNNG (quan điểm) | 5-7 |
| Nối và ghép | tiêu đề đoạn, tìm đoạn chứa thông tin, ghép người, nối nửa câu | 10-14 |
| Điền vào chỗ trống | câu rời, tóm tắt/bảng/sơ đồ, ghi chú vào hình, trả lời ngắn | 10-14 |
| Trắc nghiệm | chọn một, chọn hai trở lên | 5-8 |

Một cờ trong dữ liệu (`GROUPS_QUESTIONS`) nói rõ trang đang bày cái nào, và giao diện gọi tên theo
đó: "Các dạng câu hỏi" cho Đọc, "Các phần thi" cho ba kỹ năng kia. Gọi tất cả là "phần thi" cho
tiện là nói sai với learner ngay ở nhãn.

**Lối ra cuối trang khác nhau theo kỹ năng**, vì chỗ LÀM THẬT của chúng khác nhau: Đọc và Nghe có
cụm bài nằm sẵn trong bundle (`/reading/clusters`, `/listening/clusters`), còn Nói và Viết thì bài
nộp sống sau đăng nhập (`/levels`). Một lối ra chung cho cả bốn là một lối ra sai với hai trong
bốn.

**Thứ tự regex trong router là chỗ dễ sai.** `/reading/parts` cũng khớp khuôn `[a-z0-9-]+` của
nhánh cụm, nên nếu đứng sau thì nó bị đọc thành "cụm tên là parts" — đúng cái bẫy mà
`/reading/clusters` đã gặp một lần ở SRC-812. Nhánh `parts` vì thế đặt TRƯỚC.

Cổng: `apps/learn/e2e/ieltsExamParts.spec.ts` lên 9 case, thêm bốn case cho Đọc và Nghe, vẫn chạy
không mock API.

## Lịch sử quyết định

### Từ mục 5c. Bốn trang kỹ năng và thang ba bậc (SRC-801, chỉ đạo 2026-09-17 vòng 3)

Chỉ đạo: *"Gom các thông tin trên vào trang kiểu như là settings. Tạo ra các trang riêng cho 4 kỹ năng... Trong mỗi trang đó, learner nhìn thấy dữ liệu, để có động lực... Xuyên suốt là những câu nói động viên, ghi nhận, khen ngợi."*

Bốn thẻ kỹ năng là `<a href>` thật sang `ielts.nemo12.com/{reading,listening,speaking,writing}`, không phải trang dựng lại ở learn: dựng lại là có hai bản của cùng một thứ và bản thứ hai lạc hậu từ tuần sau. Phiên dùng chung cookie `.nemo12.com` nên không ai đăng nhập lần thứ hai.

*"Đặt các giới hạn, ví dụ limit theo ngày, để learner không dùng quá nhiều, hoặc quá phí API khi gọi vào các hàm LLM."*

### Từ mục 5d. Một tên miền: mạch luyện chuyển hẳn về learn (SRC-805, 2026-09-18)

Chỉ đạo chủ dự án 2026-09-18: *"Không được phép link sang ielts.nemo12.com. Cần dùng mọi url trên
learn.nemo12.com."* Điều này **đảo** quyết định ở §5c (bốn thẻ kỹ năng trỏ sang tên miền riêng).

### Từ mục 5e. Năng lực nhỏ: học lý thuyết rồi kiểm hiểu (SRC-805, 2026-09-18)

Chỉ đạo chủ dự án: *"Trong mỗi kỹ năng nghe, nói, đọc, viết, thì có các năng lực nhỏ hơn. Ví dụ:
xác định main idea, hay inference... thì cần có một chỗ nào đó để 'học lý thuyết', rồi làm các bài
tập siêu đơn giản, để đảm bảo là learner hiểu được lý thuyết đó phần nào."*

### Từ mục 5f. Thang bốn chặng: tiếng Việt trước, tiếng Anh sau (SRC-821, 2026-09-18)

Chỉ đạo chủ dự án: *"Tôi muốn học sinh được trải nghiệm việc tìm ra ý chính của các đoạn cực ngắn,
tầm 3-5 câu thôi, nhưng bằng tiếng Việt. Rồi sau đó luyện tập tiếp rất nhiều kỹ thuật tìm ý chính
trên tài liệu tiếng Việt. Sau đó mới chuyển sang tiếng Anh, từ dễ (3-5 câu), rồi nâng dần lên."*
Và: *"Màn hình này cần cực kỳ minimal."*

### Từ mục 5f. Trang kỹ năng là một hub, phần bị lấy đi có URL riêng (SRC-812, 2026-09-18)

Chỉ đạo chủ dự án 2026-09-18: *"nhiều thứ lằng nhằng quá. Cần chỉ để lại vài mục chính to to thôi
(tối đa 5 mục), click vào nó thì mới đi tiếp tới các trang tiếp theo."* — và ngay sau đó, cho ba kỹ
năng còn lại: *"Cần siêu gọn, cần minimal."*

### Từ mục 5g. Các phần thi của Nói và Viết (SRC-815, 2026-09-18)

Chỉ đạo chủ dự án 2026-09-18: *"Writing và Speaking cũng cần làm tương tự như Reading và Listening.
Cũng cần tạo ra vài sub-skill trong Writing khi thi IELTS, và vài sub-skill trong Speaking."*

### Từ mục 5h. Đọc và Nghe cũng có tầng đó, nhưng chia theo hai lối khác nhau (SRC-825, 2026-09-18)

Chỉ đạo chủ dự án 2026-09-18: *"thêm sub-skill cho Reading và Listening luôn"*.
