---
url: https://docs.nemo12.com/architecture/sdd-038/pathway.md
description: >-
  Cổng /ielts bốn bước, bài chẩn đoán bốn kỹ năng, luật hé lộ, màn mentor,
  Skills Check và cổng SAT dùng chung khuôn
---

# SDD-038 · Cổng bốn bước và chẩn đoán

> 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.

## 1. `/ielts` là một cổng, không phải một hub

Giờ `/ielts` chỉ là một **danh sách tám việc**, mỗi việc một dòng, bấm sang trang riêng: `/ielts/goal`, `/ielts/declarations`, `/ielts/quiz`, `/ielts/assessment`, `/ielts/writing`, `/ielts/speaking`, `/ielts/booking`, `/ielts/plan`. Được ba thứ mà bản gộp không có: gửi được link tới đúng một việc, bấm back ra đúng chỗ vừa đứng, và mỗi màn chỉ hỏi một câu. Danh sách khoá của trang con rút thẳng từ bảng `IELTS_TASKS`, không chép tay lần hai — thêm trang mà quên khai thì URL của nó lặng lẽ đổ về hub.

**Ít chữ là ràng buộc, không phải khẩu vị** (SRC-048 + chỉ đạo "siêu siêu minimal"): mỗi dòng ở hub là tên việc cộng một dấu xong. Câu "vì sao có bước này" đã bỏ khỏi màn hình; lý do vẫn cần cho người sửa mã nên nó ở trong chú thích và trong tài liệu này, không ở trước mắt đứa trẻ.

`/ielts` là một route cấp cao nhất (`view: "ieltsPathway"` trong `apps/learn/src/App.tsx`), khung tối giản: logo, đổi ngôn ngữ, menu tài khoản. Không Lighthouse, không Khoá học, không Rủ bạn, không lưới school. Mỗi nút thêm vào là một lối ra khỏi bốn bước, và bốn bước chỉ có nghĩa khi đi hết. Logo vẫn bấm về trang chủ được: một trang không có đường ra là một cái bẫy, khác hẳn một trang không mời gọi đi chỗ khác.

## 2. Bốn bước này khác gì năm bước của SDD-035

[SDD-035](../sdd-035-ielts-onboarding.md) mô tả **hành trình học** (Mục tiêu → Chẩn đoán → Khoảng cách → Lộ trình → Sẵn sàng), đúng năm bước của trang công khai, và không có điểm dừng. SDD-038 mô tả **việc nhận một người mới**, và nó kết thúc ở một buổi gặp người thật.

Hai thứ cùng đọc một kho dữ liệu nhưng kể hai câu chuyện, nên là hai file (`journey.ts` và `pathway.ts`). Cả hai đều **suy ra** trạng thái từ dữ liệu của chính từng bước; không file nào ghi cột "đang ở bước mấy".

| Bước | Xong khi | Làm ở đâu |
| --- | --- | --- |
| 1. Đặt mục tiêu | có mục tiêu đang hoạt động **và** có lời khai hoàn cảnh học | `/ielts/goal`, `/ielts/declarations` |
| 2. Chẩn đoán | quiz **và** bài bốn kỹ năng **và** một bài viết **và** một đoạn ghi âm | `/ielts/quiz`, `/ielts/assessment`, `/ielts/writing`, `/ielts/speaking` |
| 3. Gặp thầy cô | đã khai nguyện vọng giờ **và** ghi tên một buổi | `/ielts/booking`, dùng lại Học cùng thầy cô ([SDD-034](../sdd-034-mentor-slots.md)) |
| 4. Kế hoạch học | có ít nhất một mốc trung gian | `/ielts/plan` |

**Khoá theo dây chuyền**: bước N chỉ mở khi mọi bước trước đã xong, và bước khoá vẫn **hiện ra** (mờ) chứ không bị giấu. Một learner đặt lịch gặp trước khi làm bài chẩn đoán sẽ tới buổi gặp mà không có gì để nói; còn giấu các bước sau thì mỗi lần bấm Tiếp là một lần bất ngờ, và người ta không giao việc học của mình cho thứ mình chưa nhìn hết.

## 3. Hai kỹ năng sản sinh: một bài viết, một đoạn nói

Bảng `ielts_productions` (migration 0228), **không** dùng lại `assessment_responses`: bài trắc nghiệm có `selected_index` và chảy vào Learner Model dưới dạng `learner_evidence`; một đoạn văn và một tệp âm thanh thì không có "đúng/sai", và điểm của chúng là một **nhận định** — của model trước, của mentor sau. Ép chúng vào bảng trắc nghiệm sẽ bơm nhận định của máy vào Learner Model như thể là bằng chứng đo được.

* **Đề bài nằm trong mã nguồn** (`modules/ielts/production.ts`), một đề mỗi kỹ năng. Cố ý một đề: ở bước chẩn đoán, hai learner làm hai đề khác nhau thì hai bản nhận định không so được với nhau.
* **Âm thanh ở R2**, D1 chỉ giữ khoá. Trần: 3 phút (chỉ đạo), sàn 20 giây, 8 MB. Giao diện tự dừng ở trần — learner đang kể chuyện thì không nhìn đồng hồ.
* **Speaking: phiên âm rồi mới chấm.** Model chấm là model chữ, nên prompt `ielts.production-feedback` **cấm thẳng** mọi nhận xét về phát âm, trọng âm, ngữ điệu. Một nhận xét về trọng âm sinh ra từ chữ nghe rất thật và sai hoàn toàn; mentor đọc rồi nhắc lại với learner là hệ đã nói dối qua miệng một người thật. Bản phiên âm được **giữ lại**: mentor đọc nhanh hơn nghe, và khi nhận định có vẻ lệch thì đó là chỗ đầu tiên để soi.
* **Nhận lưu trước, chấm sau.** Learner đã nói xong ba phút thì không được mất bài vì một model bận: `status='failed'` là kết cục hợp lệ, bước vẫn tính là xong.

## 4. Luật hé lộ

Chỉ đạo: hệ ra báo cáo nhưng **không** đưa hết cho learner trước buổi gặp, chỉ hé lộ một phần.

* `learnerReveal()` trong `modules/ielts/pathway.ts` là chỗ **duy nhất** quyết định phần hé lộ, và nó **dựng một đối tượng mới** chứ không lọc bớt báo cáo đầy đủ. Lọc bớt nghĩa là mọi trường thêm vào sau này mặc định **lộ** và người thêm phải nhớ đi che — thứ chắc chắn sẽ quên. Dựng mới nghĩa là mặc định **kín**.
* Phần hé lộ **không mang con số nào**: một learner cầm "ước lượng 5.5" đi trước buổi gặp sẽ dành cả buổi để cãi hoặc để buồn về con số ấy. Câu hé lộ là `teaser_vi` do chính model viết dưới lệnh cấm chứa số.
* Trang **nói thẳng là đang giữ lại, và nói vì sao**. Giấu mà không nói là giấu thì learner tự kết luận theo hướng xấu hơn sự thật.
* Đường learner (`ielts-pathway`, `ielts-productions`) cố ý **không SELECT** các cột nhận định. Báo cáo đầy đủ chỉ mở ở `/v1/mentor/learners/{id}/ielts-report`, sau cổng vai mentor, và đoạn ghi âm phát qua worker chứ không ký URL R2 công khai — một URL đã ký sống tiếp sau khi mentor mất vai.

## 3b. Hai màn được làm lại theo chỉ đạo 2026-09-17 (vòng 3)

**Trang Mục tiêu chỉ hỏi một câu.** Các mốc trung gian đã rời khỏi `/ielts/goal` sang `/ielts/plan`, cùng với ô khai điểm hiện tại — điểm hiện tại không phải một cái đích, nó là đầu vào của phép chia mốc (`suggest` từ chối thẳng khi thiếu). Đặt nó cạnh band mục tiêu là bắt learner khai hai con số trông giống hệt nhau ở cùng một màn, đúng cách để họ gõ nhầm ô. `/ielts/goal` vì thế dùng một biểu mẫu riêng chứ không dùng lại `IeltsGoalCard` — card ấy còn phục vụ trang Hồ sơ, sửa nó cho vừa màn này sẽ làm hỏng màn kia.

**Quiz: một câu một màn.** Bản cũ đổ cả mười câu với bốn mươi phương án vào một cột cuộn. Ba thứ hỏng cùng lúc: mắt phải tự tìm mình đang ở câu nào, phương án dài phải co chữ cho vừa, và learner thấy ngay còn chín câu nữa nên đọc lướt cho xong — mà quiz này tồn tại để **sửa hiểu biết**, đọc lướt thì không sửa được gì. Giờ chữ to, phương án là ô bấm rộng, chọn xong tự sang câu sau (không có nút "Tiếp" — một cú bấm thừa nhân cho mười câu), vẫn quay lại đổi ý được. Tiến độ là một vạch mảnh cộng `3/10`, thay cho một dòng chữ.

Màn giới thiệu quiz cố ý **lớn**: nó là chỗ duy nhất nói "đây không phải bài kiểm tra tiếng Anh", và một câu quan trọng in cỡ chữ phụ đề thì không ai đọc. Lớn ở đây là cỡ chữ và khoảng thở, không phải thêm element — đúng hướng với luật tối giản, không ngược.

## 4b. Màn mentor: nơi báo cáo được mở ra

Tab **IELTS** trong cổng Dolphin (`apps/mentors/src/IeltsReport.tsx`), đứng ngay sau About: với learner đi qua cổng bốn bước, buổi gặp đầu tiên sinh ra để mở đúng bản báo cáo này.

* **Mỗi phần mở riêng, mặc định đóng.** Không đổ cả bảng điểm ra một lúc. Mentor chọn thứ tự mở, và thứ tự ấy là một phần của việc dạy: mở chỗ mạnh trước hay chỗ hụt trước là hai buổi gặp khác nhau.
* **Lời cảnh báo đứng đầu, không gấp lại được**: học sinh chưa từng thấy những con số này. Mentor quên điều đó sẽ hỏi "em thấy điểm sao" và làm hỏng đúng buổi gặp mà cả luồng bốn bước tồn tại để dẫn tới.
* **Hai cột band không bao giờ gộp**: `ai_band` (máy đoán) và `mentor_band` (người chốt). `PATCH /v1/mentor/ielts-productions/{id}` là đường ghi cột thứ hai — không có nó thì cột ấy vĩnh viễn NULL và bản báo cáo rút lại còn đúng con số của model. Đường này **không** đụng tới bài viết, bản phiên âm hay nhận định của model: mentor chốt điểm và ghi một câu của mình, không sửa thứ learner đã nộp. Trường không gửi thì giữ nguyên, gửi `null` thì xoá — gộp hai ý ấy sẽ khiến một lần sửa ghi chú lặng lẽ xoá mất band đã chốt.
* **Bài của learner hiện nguyên văn**, không cắt, không "xem thêm". Với speaking, nhãn nói rõ đây là **bản phiên âm** và máy chưa từng nghe tiếng — một mentor tưởng mình đang đọc bài viết sẽ nhận xét sai loại.

## 5. Bước 4 là bản nháp, và nói ra điều đó

Kế hoạch sơ bộ dùng lại `ielts-goal/milestones/suggest` ([SDD-035](../sdd-035-ielts-onboarding.md), `planner.ts`). Giao diện nói ngay dòng đầu rằng đây là **bản sơ bộ** để buổi gặp bắt đầu từ việc sửa thay vì từ trang giấy trắng. Learner tin nhầm ở đây thì mọi câu mentor sửa trong buổi gặp đều nghe như một lần bị đổi ý.

## 6. Cổng kiểm

SRC-801 thêm 39 test: `modules/ieltsSkills/progress.test.ts` (chuỗi ngày khi hôm nay chưa học, phút không bị làm tròn nuốt mất, vốn từ không tụt khi nghỉ tuần, tuần trước bằng 0 thì không chia), `encouragement.test.ts` (learner chưa có gì thì **tuyệt đối không** nhận câu khen nào), `next.test.ts` (nút "ôn lại" không bao giờ đưa ra bài chưa từng làm), `routes.test.ts` (quyền, trần theo ngày, và **xem số lượt còn lại không tiêu mất lượt nào**). Ở `apps/learn`: 2 case e2e gác bốn thẻ kỹ năng trỏ đúng `ielts.nemo12.com` và tám việc cũ vẫn tới được ở `/ielts/settings`.

`workers/api/src/modules/ielts/pathway.test.ts` — 13 test qua HTTP thật, phủ năm thứ hỏng im lặng (cộng 8 test cho Evidence Registry ở `shared/evidence.test.ts` và 8 test cho Learner Model ở `modules/ielts/model.test.ts`): luật hé lộ (learner không thấy band hay nhận xét, **kể cả sau khi mentor đã chốt**), khoá theo dây chuyền, cổng độ dài bài viết, "chấm hỏng không làm mất bài", và "sửa ghi chú không xoá mất band đã chốt".

## 9. Cổng SAT dùng lại đúng khuôn này (SRC-839, 2026-09-19)

Phần SAT nằm trong tài liệu này chứ không tách SDD riêng vì nó **không có thiết kế riêng nào**:
nó là cùng một quyết định sản phẩm — một kỳ thi là một cổng CẤP CAO NHẤT, khung tối giản, một
thẻ chứa các lối vào — áp cho một tên khác. Tách ra một tài liệu thứ hai là hẹn ngày hai bản mô
tả cùng một khuôn trôi khỏi nhau.

Khác `/ielts` ở đúng ba chỗ, và mỗi chỗ có lý do:

| | `/ielts` | `/sat` |
| --- | --- | --- |
| Dashboard nỗ lực | có, đọc `ielts-effort` | **chưa có**: không có endpoint `sat-effort`, và bốn thẻ toàn số 0 là bản kê những việc learner chưa làm |
| Các dòng trong thẻ | bốn kỹ năng của kỳ thi | bốn lối vào nội dung SAT đang có ở Squid: Khoá học · Luyện đề · Labs · Toàn cảnh |
| Ngôn ngữ | tiếng Anh (chỉ đạo 2026-09-18, chỉ áp cho sân IELTS) | **tiếng Việt**, theo luật chung của CLAUDE.md |

Cổng này KHÔNG sinh ra màn hình mới: bốn dòng trỏ vào `/squid/<trang>/sat` đang chạy. Việc nó làm
là thôi bắt người tới vì SAT phải chọn một con vật trước khi được nói về kỳ thi mình đang lo.

Chỗ của dashboard, khi có `sat-effort`, là ngay dưới `h1` — đúng vị trí `EffortDashboard` đứng ở
`/ielts`.

Mã: từ SRC-1024 cổng này là trang chủ khu NEMO SAT, `apps/learn/src/sat/Sat.tsx` (SDD-044 §3); nhánh route `sat` trong `apps/learn/src/App.tsx`.
Cổng kiểm: e2e `smoke.spec.ts` — "cổng SAT đứng riêng ở /sat, không rơi về hub school".

## 10. Khu IELTS mở cổng ra mắt sớm (SRC-843, 2026-09-19)

Trang công khai đưa người đọc từ khu IELTS thẳng vào `learn.nemo12.com/ielts` (SRC-841, SDD-029). Cổng ra
mắt chặn ở đó thì cả một luồng vừa dựng xong kết thúc bằng một trang "hẹn ngày mai" — trong khi 140 trang
luyện IELTS vốn đã công khai, nên chặn phần còn lại của sân ấy không giữ kín được gì. `LaunchNotice.tsx`
vì thế mở **riêng khu IELTS** (đường dẫn đọc từ thanh địa chỉ; tham số `path` có để test hỏi "ở đường dẫn
này thì sao"); phần còn lại của learn vẫn đóng tới 08:00 20.09 vì chỉ đạo nói về khu IELTS, không nói về cả
app. Test chốt cả hai hướng, vì cả hai đều hỏng im lặng: mở hụt thì luồng trên kết thúc bằng trang hẹn ngày
mai, mở quá tay thì cả app ra mắt sớm hơn ngày đã hẹn với phụ huynh. `App.tsx` không đổi một dòng.

## 33. Ba lượt nói chẩn đoán, và learner chọn lượt ưng nhất (SRC-890, 2026-09-20)

### a. Ba lượt, không phải một và cũng không phải vô hạn

Tới §29 bài nói chẩn đoán chỉ có **một** lượt: nộp xong là xong. Một lượt duy nhất biến một tai nạn
kỹ thuật — micro để xa, ngoài phòng có tiếng, lạc đề ở giây thứ năm rồi biết nhưng đã lỡ — thành
**phép đo chính thức** của một đứa trẻ, và phép đo ấy đi tiếp vào Learner Model lẫn báo cáo mentor.

Trần **ba** chứ không mở vô hạn, vì bài này là một phép ĐO. Cho nói lại không giới hạn thì learner
sẽ đọc lại cùng một đoạn tới lúc trơn, và thứ đo được sau đó là trí nhớ chứ không phải năng lực
nói. Ba lần đủ vá tai nạn, không đủ học thuộc.

Trần canh ở **cả hai phía**: màn hình giấu nút ghi âm khi hết lượt, và server đếm lại trước khi đọc
thân yêu cầu (`SPEAKING_MAX_TAKES` trong `production.ts`). Một cổng chỉ nằm trong trình duyệt thì
không phải cổng; và đếm trước khi đọc tệp để learner hết lượt không phải đẩy xong vài trăm KB mới
nghe lời từ chối.

### b. Learner chọn, máy không chọn hộ

Luật cũ — "lấy lượt mới nhất" — sai ở đúng trường hợp hay xảy ra nhất: learner nói lần hai để thử
một cách mở bài khác, nghe lại rồi thấy lần một vẫn hơn. Máy không nghe được điều ấy.

Bước chọn không phải thủ tục đặt thêm vào cuối: nó buộc learner **nghe lại chính mình ba lần rồi
nói ra lần nào tốt hơn** — tức là tự đánh giá, thứ mà không chỗ nào khác trong bài chẩn đoán hỏi
tới.

Cột `ielts_productions.chosen` (migration 0257) mặc định 0, và ba chỗ đọc bảng đổi sang
`ORDER BY chosen DESC, created_at DESC`: `pathway.ts` (câu hé lộ learner đọc), `model.ts` (band vào
Learner Model), `report.ts` (báo cáo mentor, nay có thêm cờ `chosen` để mentor biết lượt nào là lời
khai của chính learner). Chưa ai chọn thì mọi `chosen` bằng 0 và thứ tự rơi về đúng luật cũ, nên
mọi dòng có trước 0257 giữ nguyên cách hiểu — không cần backfill.

`POST /v1/learners/{id}/ielts-productions/speaking/{productionId}/choose` ghi cả hai vế trong **một
câu lệnh** (`chosen = CASE WHEN id=?1 THEN 1 ELSE 0 END`): hai câu — một bỏ chọn hết, một chọn dòng
mới — có một khoảnh khắc ở giữa nơi learner không có lượt nào được chọn. Mệnh đề WHERE giữ đủ
`learner_id`, `skill`, `track`; thiếu `track` thì một cú bấm ở đây bỏ chọn cả lượt luyện của thang
ba bậc, cùng bảng mà khác việc. Id lạ trả 404 **trước khi** ghi, nếu không nó sẽ lặng lẽ bỏ chọn
sạch cả ba lượt rồi trả về 200.

Màn nộp bài không còn tự rời trang sau khi nộp: việc của learner chưa hết — còn nghe lại rồi chọn.

### c. Màn "Bài nói của con": nhóm theo việc, màu mang nghĩa

Bản §29 xếp mọi lượt nói thành một dãy thẻ giống hệt nhau, mỗi thẻ mang ba nhãn chữ đầy đủ
("Nhận xét lần ấy", "Một việc cho lần sau", "Lời con đã nói, viết ra chữ"). Ba chục lượt thành chín
chục dòng nhãn lặp — mà chữ lặp thì mắt không đọc, nó lướt; thứ đáng đọc chìm trong thứ không đáng.

Ba phép sửa, theo đúng thứ tự quan trọng:

1. **Nhóm theo việc**, không phải một dãy phẳng theo thời gian: bài chẩn đoán · thang ba bậc · bước
   nói của bộ bài. Đó đúng là ba việc khác nhau trong đầu learner, và một tiêu đề nhóm nói một lần
   thay cho ba chục nhãn lặp trên từng dòng.
2. **Màu mang nghĩa, không trang trí**: mỗi nhóm một token màu (coral · ocean · sun) đứng ở vạch
   bên trái mỗi dòng. Liếc một cái là biết đang ở nhóm nào. Ba màu, hết — màu thứ tư là quay lại
   chỗ cũ, chỉ khác là bằng màu.
3. **Một dòng chữ mỗi lượt khi chưa mở**: ngày, độ dài, và câu ghi nhận. Nhận xét đầy đủ cùng bản
   phiên âm gộp vào **một** chỗ mở ra, không phải hai — learner mở chúng cùng lúc, vì cùng một lý do.

Ranh giới hé lộ của §29 không đổi: `learnerNotes` vẫn lọc ở server, và trang này vẫn chỉ đọc thứ
server đã lọc.

### d. Còn mở

* Trần ba lượt chỉ áp cho bài nói **chẩn đoán**. Thang ba bậc và bước nói của bộ bài vẫn nộp lại
  không giới hạn (chúng là bài luyện, không phải phép đo) — nhưng cũng chưa có đường chọn.
* Bài **viết** chẩn đoán vẫn một lượt. Chủ dự án chỉ nói về bài nói, và một bài viết thì learner đã
  sửa được trước khi bấm nộp, nên nhu cầu khác hẳn.

## 59. IELTS Skills Check: bốn ô, một trang riêng, ba trạng thái (SRC-950, 22.09.2026)

### 59a. Số thứ tự nằm trong tiêu đề, không nằm ở dấu tích

Năm chặng vốn đã có `position`, nhưng màn hình chỉ vẽ nó vào ô tròn bên trái — và ô ấy đổi thành
một nét check ngay khi chặng xong. Hậu quả đúng bằng ảnh chủ dự án gửi: một trang đã xong gần hết
thì không còn con số nào, và năm chặng đọc ra như năm mục rời rạc chứ không phải một trình tự.

Nay số in trong chính tiêu đề (`1. Goal`), nên nó ở lại dù chặng xong hay chưa.

### 59b. Chặng Diagnosis còn hai dòng

| Trước | Sau |
| --- | --- |
| Quick Check | **IELTS Knowledge Check** |
| Reading & Listening Check | **IELTS Skills Check** → `/ielts/setup/skills` |
| Writing Sample | (vào trang con) |
| Speaking Sample | (vào trang con) |

"Quick Check" nói bài ấy NHANH, trong khi điều learner cần biết là nó hỏi về **cái gì**: hiểu biết
chung về kỳ thi, không phải năng lực tiếng Anh. Và "Quick" đứng cạnh "Skills Check" thì hai cái tên
chỉ khác nhau ở tốc độ — tức là không nói được chúng khác nhau ở chỗ nào thật.

### 59c. Reading tách khỏi Listening, và đó là một đổi luật chứ không phải đổi nhãn

Nay:

* `POST /ielts-diagnostic?skills=reading` dựng một lượt đo **chỉ** phần Reading (`pickDiagnosticItems`
  nhận danh sách kỹ năng; `poolShortfall` chỉ soi các kỹ năng được yêu cầu).
* `GET /ielts-assessment-brief?skills=reading` trả lời giới thiệu khớp đúng bài ấy — số câu, số
  phút, và một câu giới hạn khác: một lượt đo một kỹ năng **không** so được kỹ năng ấy với ba kỹ
  năng kia trong cùng một lần ngồi.
* Trạng thái từng ô đọc từ `ielts_diagnostic_items.strand` nối với `assessment_sessions.status`
  (`readDiagnosticStrands`), nên một lượt đo bốn kỹ năng cũ vẫn đánh dấu xong cả hai ô — đúng như
  nó phải thế: learner ấy ĐÃ làm phần Reading rồi.

### 59d. Ba trạng thái, và vì sao không phải một cờ `done`

`not_checked` · `in_progress` · `checked`. Cùng lý do `SetupPhaseStatus` ra đời ở §57: một learner
mở bài rồi bỏ ngang mà màn hình nói "chưa kiểm" là xoá đi một việc họ đã làm thật. Writing và
Speaking đọc theo bài đã nộp (`ielts_productions`), không theo phiên trắc nghiệm — một câu trắc
nghiệm phần writing trong lượt đo cũ **không** phải một bài viết đã nộp.

### 59e. Xác nhận trước khi test, xem lại sau khi test

`SkillCheckGate` đứng trước mọi ô trắc nghiệm: chưa kiểm thì phải bấm "Start this check" mới vào
bài (và trong bài còn một nấc giới thiệu nữa, SRC-719); đã kiểm thì mở ra là thấy kết quả — tỉ lệ
đúng, số câu đã làm, câu nhận định của Learner Model — kèm dòng nói thẳng đây **không** phải band
IELTS. Nút làm lại đứng **sau** kết quả, không thay chỗ nó.

### 59f. Chỗ dễ sai

* **Khoá việc trùng tên địa chỉ mạch luyện.** `reading`, `listening` và `skills` đều là đoạn URL đã
  có nghĩa khác (`/ielts/reading`, `/ielts/skills/reading`). Nhánh legacy `/ielts/<task>` trong
  `pathToRoute` đứng trước, nên thiếu một dòng loại trừ là mạch luyện bị nuốt — e2e bắt đúng lỗi ấy
  ở lượt đầu. Ba ô kiểm chỉ tới được bằng địa chỉ thật, `/ielts/setup/<task>`.
* **`/ielts/setup/assessment` là link cũ, không được gãy.** Nó vẫn mở bài bốn kỹ năng.
* **Trạng thái chặng.** Việc thật của Diagnosis nằm trong bốn ô ở trang con, nên `statusOf` nhận
  thêm một dấu hiệu `touched`; thiếu nó thì learner đã kiểm xong Reading vẫn bị gọi là "Not started".

## 61. Chữ hướng dẫn của các bài check nói tiếng Việt (SRC-953, 22.09.2026)

### 61a. Đường ranh, và vì sao nó không mâu thuẫn với SRC-834/835

| Ở `/ielts/**` | Thứ tiếng |
| --- | --- |
| Tên trang (`Reading Check`), tên kỹ năng, nhãn trạng thái (`Not checked` · `In progress` · `Checked`), nội dung đề | Tiếng Anh |
| Chữ Nemo GIẢI THÍCH bài: giới thiệu, giới hạn phép đo, dặn dò, tóm tắt kết quả, nhãn nút | Tiếng Việt |

SRC-834/835 nói sân luyện tiếng Anh, với lý do: learner vào đây luyện một kỳ thi tiếng Anh, và một
lớp dịch bọc quanh bài thi là lớp họ phải tự gỡ vào ngày thi. Lý do ấy áp cho thứ learner **gặp
trong phòng thi**. Nó không áp cho Nemo khi Nemo đang nói VỀ bài thi.

Chỗ này quan trọng nhất ở đúng một câu: *"Bài này KHÔNG cho ra band IELTS."* Một learner band 5.0
đọc câu ấy bằng tiếng Anh sẽ hiểu lỗ chỗ, và một lời cảnh báo hiểu sai thì thà đừng cảnh báo — họ
sẽ đem con số 17% đi so với mục tiêu du học của chính mình.

### 61b. Trình bày mượn khuôn trang luyện

Ba màn (cửa xác nhận · giới thiệu · làm bài) nay dùng đúng khuôn `MicroExercise`: cột `max-w-3xl`
căn giữa, chữ cỡ đọc được với `leading-8`, lựa chọn là thẻ rộng hết cột (`rounded-card border
p-3.5`), nút hành động là pill lớn. Trước đó chúng là `text-sm` dính mép trái của một trang
full-width, đọc ra như phần chân của một biểu mẫu chứ không như một việc sắp ngồi xuống làm.

Vạch tiến độ chia đoạn thay cho dòng `Question 3 / 6`: một cái vạch nói "còn dài bao nhiêu" nhanh
hơn mọi con số, và learner đang làm bài thì không nên phải đọc thêm chữ.

### 61c. Một thứ KHÔNG mượn

Trang luyện chấm ngay từng câu và mở lời giải. Bài đo thì không — và không phải vì tiếc công: hiện
đúng/sai giữa chừng là làm hỏng chính phép đo (§ đầu `diagnostic.ts`). Thẻ lựa chọn ở bài đo vì thế
chỉ có hai trạng thái, đã chọn và chưa chọn; không có màu đúng, không có gạch ngang sai.

## 133. Chẩn đoán 4 kỹ năng, mỗi kỹ năng một bài (SRC-1099, 28.09.2026)

Yêu cầu và mười quyết định: [prd-ielts-diagnostic](../../product/prd-ielts-diagnostic.md).

**Dữ liệu** (migration 0307): `ielts_diag4_attempts` là một lượt làm MỘT kỹ năng; `items_json`
chụp lại đề kèm đáp án lúc bắt đầu, để kho câu có đổi sau đó thì lượt đã làm vẫn chấm đúng đề
learner đã thấy. `ielts_diag4_reports` là ảnh chụp báo cáo, để xem lại lần cũ đúng như lúc ấy.

**Nguồn câu:** Reading lấy MỘT bài đọc đã publish có đủ 10 kỹ năng con và 30 câu trắc nghiệm, 3 câu
đầu mỗi kỹ năng con. Listening lấy 3 đoạn nghe có đủ 5 kỹ năng con, mỗi đoạn 1 câu mỗi kỹ năng con.
Chọn bài theo băm (learner, kỹ năng, số lượt) để lần làm lại gặp bài khác. Speaking/Writing lấy từ
`diagnosticBank/` (60 câu, không câu nào có đáp án là lựa chọn dài nhất - bài học từ P0 nội dung ở §132).
Kho chưa đủ thì trả 503 thay vì phát một đề rỗng.

**API** (`modules/ielts/diag4Routes.ts`): `GET /ielts-diag4` (trạng thái + báo cáo),
`POST /ielts-diag4/{skill}/start` (làm tiếp lượt dở nếu có; 400 nếu chưa đủ 30 ngày),
`PUT /attempts/{id}` (lưu dần), `POST /attempts/{id}/submit`, `GET /reports/{id}`. Đề phát ra không
mang đáp án hay lời giải. Nộp dùng `UPDATE ... WHERE submitted_at IS NULL` để hai lượt nộp cùng lúc
không chấm hai lần. Báo cáo sinh khi lượt MỚI NHẤT của cả bốn kỹ năng đều chưa thuộc báo cáo nào.

**Chấm** (`diag4.ts`, hàm thuần có test): bốn loại kết quả, mức Strong/Developing/Needs work, ba
việc nên luyện trước (hiểu sai trước, rồi tỉ lệ đúng thấp nhất, bỏ qua kỹ năng con Strong).

**Giao diện** (`apps/learn/src/ielts/Diagnostic.tsx`): hub bốn thẻ, trang làm bài từng câu một,
trang báo cáo; thẻ "Find your level" trên `/ielts`. Bài đo 6 câu cũ chưa gỡ khỏi Learning Setup ở
vòng này: gỡ nó đụng tới luật `requiredDiagnosisParts` của cổng bốn bước, nên để thành việc riêng.

**Mentor đọc báo cáo** (SRC-1246, 05.10.2026, trả nợ của mục này): cổng Dolphin
(`mentors.nemo12.com`), màn một learner, tab **IELTS**, khối đầu tiên "4-skill diagnostic". Khối ấy
gọi đúng hai đường của learner (`GET /v1/learners/{id}/ielts-diag4` và `GET .../reports/{id}`):
`canAccessLearner` đã cho vai mentor đọc, nên không dựng một route `/v1/mentor/...` thứ hai trả cùng
dữ liệu. Mentor thấy trạng thái bốn kỹ năng (đã nộp ngày nào, band ước lượng), danh sách báo cáo
(mới nhất mở sẵn) và nội dung báo cáo: overall, bốn band, ba việc nên luyện trước, sub-skill từng
kỹ năng, và chỗ hiểu sai kèm đáp án learner chọn. Chỉ đọc: cổng mentor không có nút bắt đầu, lưu
hay nộp. Không import `Diag4ReportView` của learn vì nó gắn với router và chữ hướng dẫn của learn;
phần thuần (`apps/mentors/src/diag4.ts`) có test riêng.

## 163. Báo cáo mẫu khi chưa làm đủ bài chẩn đoán (SRC-1217, 03.10.2026)

Chủ dự án 03.10.2026: learner mở báo cáo khi chưa làm đủ bốn bài chẩn đoán thì thấy một báo cáo MẪU, số liệu giả nhưng thông tin của chính learner, để hình dung báo cáo cuối cùng.

* Trang: `learn.nemo12.com/ielts/diagnostic/report` (`Diag4ReportPage`). Có báo cáo thật thì không đổi gì. Chưa có thì trên cùng là thẻ "còn thiếu gì": trạng thái từng bài (`Checked` / `In progress` / `Not checked`) và MỘT nút chính đi tiếp bài đang dở (hoặc bài đầu chưa làm). Thẻ Report ở `/ielts/diagnostic` thêm link "Xem báo cáo mẫu".
* Một bộ vẽ: `Diag4ReportView` vẽ cả báo cáo thật lẫn báo cáo mẫu, nên mẫu không thể lệch bố cục thật. Mẫu chỉ thêm: huy hiệu "Sample report" trên MỌI phần, nền sọc chìm và độ mờ nhẹ, dòng ngữ cảnh (tên, Goal ≈ band · ngày thi, trường + lớp), và tên mục "Focus first" không thành link.
* Dữ liệu mẫu: `apps/learn/src/ielts/diagnosticSample.ts`, hàm thuần `buildSampleReport(context, variant)`. Không fetch, không đồng hồ, không ngẫu nhiên: cùng đầu vào ra cùng báo cáo. Kiểu `Diag4SampleReport` mang `sample: true`; trang chỉ GỌI ĐỌC (`ielts-diag4`, `ielts-goal`, `walk-entry`), không ghi gì, không vào Evidence.
* Ba biến thể, learner bấm qua lại: "Nếu gần mục tiêu", "Nếu một kỹ năng tụt lại" (Writing thấp hơn mục tiêu 2 band), "Nếu dưới mục tiêu". Band = mục tiêu learner (mặc định 6.5 khi chưa đặt) cộng bảng lệch cố định `OFFSETS`, kẹp 3..9, làm tròn nửa bậc.
* Ngôn ngữ theo SRC-953: nhãn, tên kỹ năng, huy hiệu tiếng Anh; chữ giải thích và nhãn nút tiếng Việt. Trình đọc màn hình: vùng báo cáo có `aria-label` "Sample report, số liệu giả", mỗi huy hiệu có chữ ẩn "số liệu giả, không phải kết quả của bạn".
* Ngoài phạm vi: báo cáo SAT chỉ mở theo id một bài đã nộp (không có trạng thái "chưa đủ"); Diagnosis Report trường học cố ý không có điểm số và không bịa danh sách (SDD-021 §4).
* Kiểm: `diagnosticSample.test.ts` (dùng goal thật, tất định, cờ sample, ba câu chuyện khác nhau), `e2e/ieltsDiagSample.spec.ts` (chưa đủ bài: thấy mẫu có tên + CTA và không có lượt ghi; đủ bài: báo cáo thật không có huy hiệu; 375px không cuộn ngang).

## 165. Gap to Target: band theo tiêu chí cho Writing/Speaking, bậc cho Reading/Listening (SRC-1188, 02.10.2026)

Chủ dự án chốt 02.10.2026 cho trang `learn.nemo12.com/ielts/gap`. Luật 19.09.2026 (mục "Bằng chứng luyện tập KHÔNG đẩy band" ở §5b-bis) **đứng nguyên**: tỉ lệ đúng của bài luyện không bao giờ thành band, không có phép quy đổi tỉ lệ ra band ở bất cứ đâu.

* **Writing và Speaking có band thật theo từng tiêu chí** (Writing: Task Response, Coherence and Cohesion, Lexical Resource, Grammatical Range and Accuracy; Speaking: Fluency and Coherence, Lexical Resource, Grammatical Range and Accuracy, Pronunciation chỉ khi dữ liệu có). Nguồn là band tiêu chí do `ielts.production-feedback` v2 ghi vào `ielts_productions.ai_notes_json` (§12g).
* **Luật gom: trung vị của tối đa 3 bài đã chấm gần nhất** cho từng tiêu chí. Hai bài thì lấy trung bình làm tròn lên nửa bậc. Màn bày kèm số bài làm căn cứ và ngày của bài mới nhất. Trung vị để một lượt chấm lệch không kéo cả con số; ba bài để con số đi theo bài gần đây.
* **Bài nào được đếm**: chỉ bài `scored` có mảng `criteria` hợp lệ. Bài chấm trước 19.09.2026 không có tiêu chí thì bỏ qua (không bịa từ band tổng, như §12g). `ai_notes_json` hỏng thì bỏ qua bài ấy. Bài **chẩn đoán** không được đếm, để trang này không thành cửa sau của luật hé lộ SRC-792 (kết quả chẩn đoán để dành cho buổi gặp mentor). Chỉ `key` và `band` rời khỏi worker; `note_vi` và các ô dành cho mentor thì không.
* **Nguồn thứ hai: phần Writing/Speaking của thi đầy đủ** (chủ dự án chốt 04.10.2026). Bài chẩn đoán đứng ngoài nên bài luyện gần như chưa bao giờ mang band tiêu chí, và danh sách sẽ rỗng với hầu hết learner. Thi đầy đủ thì đã chấm từng tiêu chí bằng cùng prompt v2 và learner đã thấy các band ấy trên phiếu kết quả, nên đưa vào đây không hé thêm điều gì. Điều kiện đếm khớp đúng điều kiện route phiếu kết quả mở cho learner: lượt `finished`, phần đã nộp, đọc `ielts_full_test_sections.result_json` (`subskills[{code, band}]`; Pronunciation `band: null` bị bỏ). Hai nguồn gom chung theo ngày, luật trung vị 3 bài giữ nguyên, và mỗi tiêu chí trả thêm `sources: {practice, full_test}` để màn ghi rõ "từ 2 bài thi thử, 1 bài luyện".
* **Reading và Listening không có band.** Hai kỹ năng ấy không có tiêu chí chấm, và thứ duy nhất có là tỉ lệ đúng của bài luyện, đúng thứ luật 19.09 cấm quy ra band. Trang bày bốn bậc của server (`mastery.ts`: Building, Getting there, Solid, Strong; dưới 8 câu là "Not checked"). Mốc 75% cũ ở client (lệch với 70% của server) đã gỡ; nút "Đóng chỗ hở lớn nhất" chọn kỹ năng con có bậc thấp nhất, tỉ lệ đúng thấp hơn phá hoà.
* Trên màn có một câu tiếng Việt nói rõ hai điều này: band theo tiêu chí lấy từ bài đã chấm, còn Reading/Listening chỉ có bậc, không phải band IELTS.

API: thêm trường `criteria` vào `GET /v1/learners/{learnerId}/ielts-skills/{skill}` (đường learner đã gọi sẵn cho trang này, cùng cổng quyền `canAccessLearner`): mảng `{key, band, pieces, latest_at, cluster, sources}` với writing/speaking, `null` với reading/listening. `cluster` nối tiêu chí Writing với cụm kỹ năng con (migration 0260) để trang xếp kỹ năng con dưới tiêu chí của nó. Mã: `workers/api/src/modules/ieltsSkills/criteria.ts`; test: `routes.test.ts` khối "criteria".

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

### Từ mục 1. `/ielts` là một cổng, không phải một hub

Trước SRC-792, `learn.nemo12.com/ielts` không khớp route nào nên rơi về hub sáu school. Người tới vì IELTS phải tự tìm đường giữa Turtle, Shark, Octopus — tức là phải chọn một con vật trước khi được nói về kỳ thi mình đang lo.

**Vòng 2 cùng ngày — một tính năng, một trang, một URL.** Bản đầu nhét cả bốn bước vào một trang, mỗi bước mở ra tại chỗ; chủ dự án xem xong nói "lằng nhằng phức tạp quá". Đúng: một bước mở ra kéo theo cả card mục tiêu, biểu mẫu lời khai, bài quiz và hai ô nộp bài trong cùng một cột — cái stepper lẽ ra để nói *còn mấy việc nữa* lại thành chỗ chứa mọi việc.

### Từ mục 9. Cổng SAT dùng lại đúng khuôn này (SRC-839, 2026-09-19)

Chỉ đạo chủ dự án 2026-09-19, kèm ảnh chụp `learn.nemo12.com/sat`: *"làm lại toàn bộ trang này,
tương tự như trang ielts"*. Ảnh ấy bày hub sáu school, vì `/sat` **không khớp route nào** —
đúng cái hỏng mà §1 mô tả cho `/ielts` trước SRC-792, chỉ khác tên kỳ thi.

### Từ mục 10. Khu IELTS mở cổng ra mắt sớm (SRC-843, 2026-09-19)

*(Ghi hộ từ commit dac83a0f khi cổng SRC-596 đỏ trên `main`; phiên làm SRC-843 sửa lời nếu cần.)*

### Từ mục 33. Ba lượt nói chẩn đoán, và learner chọn lượt ưng nhất (SRC-890, 2026-09-20)

Chỉ đạo chủ dự án 20.09.2026: *"Có thể đưa tối đa 3 lần bài nói chẩn đoán lên. Rồi cho phép lựa
chọn bài nói mà mình thấy ưng nhất."* — và, cùng ngày, về màn `/ielts/recordings` vừa dựng ở §29:
*"Màn hình này quá nhiều text cứ na ná nhau, cần chỉnh lại Information Architect và màu sắc cho dễ
đọc thông tin."*

### Từ mục 59. IELTS Skills Check: bốn ô, một trang riêng, ba trạng thái (SRC-950, 22.09.2026)

Chỉ đạo chủ dự án 22.09.2026, kèm hai ảnh chụp `/ielts/setup`:

> *"Đánh số ở đầu các cụm, từ 1 tới 5. Ảnh 2, gồm "IELTS Check" (thay vì Quick Check), hoặc đổi tên
> thành gì đó mà Learner hiểu rằng "kiểm tra hiểu biết về IELTS nói chung". Rồi IELTS Skills Check
> (bạn có thể đổi tên). Click vào thì vào trang bên trong gồm 4 kỹ năng, có tiến độ check (chưa
> kiểm tra, đang làm dở, đã kiểm tra; đều là tiếng Anh): Reading Check, rồi tách Listening Check
> ra. Writing Check và cuối cùng là Speaking Check. Đó đều là các bài Diagnosis. Mọi bước, learner
> đều cần xác nhận, rồi mới đi tiếp, hoặc rồi mới test. Test rồi thì trong trang nhỏ đó, có thể xem
> lại được."*

Trước đây hai kỹ năng ấy nằm chung **một** phiên `assessment_sessions(kind='diagnostic')` gồm 24
câu. Luật cũ ở `requiredDiagnosisParts`: chọn Reading là bị đòi cả `skill_assessment`, tức là ngồi
hết sáu câu Listening cho một phép đo mình không xin.

### Từ mục 61. Chữ hướng dẫn của các bài check nói tiếng Việt (SRC-953, 22.09.2026)

Chỉ đạo chủ dự án 22.09.2026, kèm bốn ảnh chụp: ba ảnh `/ielts/setup/reading` và một ảnh trang
luyện `/ielts/skills/reading/inference`.

> *"Với ảnh 1, 2, 3, cần dùng tiếng Việt cho phần hướng dẫn, trình bày thoáng ra và đẹp đẹp như
> ảnh 4. Quiz cần trình bày như ảnh 4."*

Ảnh 1 chụp đúng chỗ luật cũ gãy trong thực tế: một thẻ tiếng Anh (`What this check found`,
`17% correct · 6 questions answered`) có một dòng tiếng Việt lọt vào giữa (`bài đo đúng 1/6`) — câu
nhận định do Learner Model sinh ra, vốn luôn tiếng Việt. Thẻ ấy chưa bao giờ thuần tiếng Anh được.
