---
url: https://docs.nemo12.com/architecture/sdd-038/learning-setup.md
description: >-
  Learning Setup thay quy trình chung, ô điểm đã lưu, câu trả lời hiện sẵn, nút
  change, bộ khởi động ba câu, trả nợ ngôn ngữ hai trang khai báo
---

# SDD-038 · Learning Setup

> 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. Các mục §57, §65, §69 nằm ở [Learning Setup: trở lại hub và còn hai chặng](learning-setup-hub-return.md).

## 12. Learning Setup: không ai phải đi qua cùng một quy trình (SRC-860, 2026-09-19)

Chỉ đạo chủ dự án: *"Với Nemo IELTS, tôi sẽ không bắt mọi learner đi qua cùng một quy trình."* Và nguyên tắc rút ra, chép nguyên văn vì nó quyết định mọi thứ dưới đây:

> Nemo doesn't need to test you just because it can.
> Nemo needs enough evidence to know where you are.

### 12c. Evidence Level được SUY RA, không lưu

> Đã thay bằng §65 (chặng Starting Point và Diagnosis đã bỏ, [§65](learning-setup-hub-return.md))

| Mức | Khi nào | Nemo làm gì |
| --- | --- | --- |
| **Known** | `baseline.source = 'official'` | không hỏi lại |
| **Estimated** | `mock_test` hoặc `self_estimate` | dùng được, nhưng một bài mẫu sẽ làm nó sắc hơn |
| **Unknown** | chưa có baseline | chưa biết gì |

Cả ba suy thẳng từ `learner_ielts_baselines` — bảng đã có sẵn từ SRC-717, không cần cột mới. Lưu `evidence_level` thành một cột sẽ lệch khỏi sự thật ngay lần đầu learner sửa điểm từ tự đánh giá thành điểm thi thật; đây đúng lỗi mà `journey.ts` đã tránh khi từ chối lưu "đang ở bước mấy".

**Thứ duy nhất phải lưu mới** (migration `0253_ielts_learning_setup.sql`) là `start_mode` và `focus_skills_json`. Lý do hẹp: learner chưa có điểm vì họ **mới bắt đầu và muốn học luôn**, hay vì họ **chưa chọn gì cả**? Hai trạng thái ấy nhìn từ dữ liệu giống hệt nhau — cùng là "không có dòng baseline" — nhưng màn hình phải đối xử ngược nhau. Thiếu cột này thì learner đã chọn *I'm new* vẫn bị hỏi lại mỗi lần mở trang, tức là đúng thứ chỉ đạo yêu cầu bỏ đi.

### 12g. Chấm Writing theo bốn tiêu chí

`ielts.production-feedback` lên **v2**. v1 trả một band cho cả bài; con số ấy đủ cho bước chẩn đoán nhưng vô dụng với learner đã 6.5 muốn lên 7.5 — họ cần biết mình tụt ở Task Response hay ở Lexical Resource, vì hai chỗ ấy chữa bằng hai loại việc khác hẳn nhau.

v2 bắt chấm riêng đủ **bốn** tiêu chí Writing (Task Response · Coherence and Cohesion · Lexical Resource · Grammatical Range and Accuracy) và **ba** của Speaking (Fluency and Coherence · Lexical Resource · Grammatical Range and Accuracy), mỗi tiêu chí một band nửa bậc và một câu chỉ vào đúng chỗ trong bài. `key` là `enum` chứ không phải chuỗi tự do: model được viết `note_vi` thế nào cũng được, nhưng nếu nó tự đặt tên tiêu chí thì hai lượt chấm của cùng một learner sẽ trả về "Lexical Resource" và "Vốn từ", và không có cách nào ghép chúng thành một đường tiến bộ.

Khuôn JSON in ra **đủ số phần tử** của đúng kỳ năng đang chấm, không in khuôn mẫu một phần tử — bài học đã trả giá bốn lần trong `PROMPT_HISTORY`: ví dụ lấn át lời dặn.

Bản ghi chấm **trước** 2026-09-19 không được backfill: một band tiêu chí bịa ra từ band tổng trông y hệt một band có căn cứ, và mentor không có cách nào phân biệt. Thiếu thì để thiếu (`criteria?` optional trong `report.ts`).

Luật hé lộ (§4) **không đổi**: bốn tiêu chí đi vào `ai_notes_json`, và đường learner gọi cố ý không trả cột ấy.

### 12i. Mặc định ĐỌC, bấm Edit mới sửa (SRC-861, 2026-09-19)

Áp cho cả bốn màn khai báo: `/ielts/settings`, `/ielts/setup` (khối Starting Point), `/ielts/setup/goal`, `/ielts/setup/declarations`.

**Mẫu này đã có sẵn, và đó là lý do làm thành component chung.** Trang Hồ sơ IELTS (§5b) từ lâu bày một `<dl>` rồi có nút "Sửa" mở `DeclarationForm`. Nó làm đúng việc, nhưng phải viết nội dung **hai lần** — một bản đọc, một bản sửa — và hai bản ấy trôi khỏi nhau ngay lần đầu ai thêm một ô: bản sửa có ô mới, bản đọc thì không, và không có lỗi nào hiện ra.

`EditGate` (`apps/learn/src/EditGate.tsx`) gỡ đúng cái giá ấy. Chỉ có **một** bản nội dung; chế độ đọc là chính bản ấy nằm trong `<fieldset disabled>` đã bỏ viền, bỏ nền, ép `opacity` về 1. Ba điều đáng ghi:

* **`<fieldset disabled>` khoá mọi ô lồng bên trong, kể cả ô thêm vào sau này.** Đây là hành vi sẵn có của HTML chứ không phải mã ta tự giữ, nên nó không thể quên ô nào — khác hẳn một danh sách `disabled={...}` rải tay.
* **Ép `opacity: 1`.** shadcn làm mờ ô bị khoá (`disabled:opacity-50`); để nguyên thì cả màn trông như đang hỏng chứ không phải đang ở chế độ đọc.
* **`data-edit-only`** giấu những thứ chỉ có nghĩa khi đang sửa (nút Lưu, câu hướng dẫn điền). Form tự khai thuộc tính này, vì chỉ form mới biết đâu là hành động và đâu là nội dung. Ngoài `EditGate` thì thuộc tính ấy không làm gì — nên `DeclarationForm` dùng chung với trang Hồ sơ **không đổi hành vi**, và có một case Playwright gác đúng điều đó.

**Cancel phải thật sự huỷ.** Các form giữ trạng thái cục bộ đọc từ API lúc mở, nên chỉ đổi cờ `editing` thì giá trị vừa gõ vẫn còn — learner tưởng đã bỏ, và lần bấm Edit sau thấy lại thứ mình tưởng đã vứt. Cancel vì thế đổi `key` của con, tức dựng lại từ đầu, và `useEffect` đọc lại API. Cách này đúng kể cả với form ta không sửa được ruột.

**Một bug lộ ra khi làm, và nó không thuộc `EditGate`.** `IeltsLearningSetup` và `IeltsSettings` trước đây làm mới bằng cách đặt state về `undefined` (khung xám) rồi đọc lại. Đó là **unmount cả cây con**, nên `EditGate` mất chế độ đang sửa: bấm chip Writing xong là khung đóng lại và learner *không chọn nổi kỹ năng thứ hai*. Nay tách hai đường — `load` (xoá màn, cho lần mở đầu và nút thử lại) và `refresh` (đọc lại, không xoá màn, cho các form gọi sau khi lưu).

**Ô rỗng ở chế độ đọc.** Một `input[type=date]` rỗng được trình duyệt vẽ placeholder `dd/mm/yyyy`, đọc ra như lời mời điền trong khi bấm vào không làm gì cả. Hai chỗ có ngày (`GoalForm`, `CurrentScore`) vì thế đọc `useEditing()` và in "chưa đặt ngày thi" / "not recorded". Đây là thứ `<fieldset>` không làm được, nên context tồn tại đúng cho việc chọn **chữ** nào hiện ra — không phải để bật tắt ô nhập.

**Nợ ngôn ngữ ghi ở đây đã trả xong ở §12j.** Khi viết §12i, `/ielts/setup/goal` và `/ielts/setup/declarations` còn tiếng Việt nên nhãn nút phải truyền vào "Sửa"/"Thôi"; nay cả hai trang nói tiếng Anh và nhãn trở về mặc định.

Cổng kiểm: 5 ca Playwright trong `e2e/smoke.spec.ts` — vào là không sửa được, bấm Edit thì sửa được, nút Lưu không hiện ở chế độ đọc, Cancel trả giá trị về, và trang Hồ sơ không đổi hành vi.

### 12j. Trả nợ ngôn ngữ của hai trang khai báo (SRC-865, 2026-09-19)

§12i để lại một khoản nợ: `/ielts/setup/goal` và `/ielts/setup/declarations` còn tiếng Việt, trái luật sân luyện (§8a). Vòng này trả xong.

**Vì sao nó là nợ chứ không phải một lần quên dịch.** `GoalForm` chỉ chạy trong `/ielts/**` nên dịch thẳng là xong. `DeclarationForm` thì khác: nó **dùng chung** với trang Hồ sơ ở `/squid/ielts-profile`, và trang ấy phải là tiếng Việt theo đúng luật mặc định của repo. Một biểu mẫu, hai nơi, hai thứ tiếng bắt buộc.

**Ba cách, và vì sao chọn cách thứ ba.**

| Cách | Vì sao không |
| --- | --- |
| Dùng `t()` của `apps/learn/src/lang.tsx` | Hàm ấy chạy theo **công tắc** ngôn ngữ người dùng tự bật. Ở đây ngôn ngữ do **chỗ đứng** quyết: `/ielts/**` luôn tiếng Anh dù công tắc để gì, `/squid/**` luôn tiếng Việt. Một biểu mẫu đổi tiếng theo công tắc phá cả hai luật cùng lúc. |
| Tách thành hai component | Hai bản trôi khỏi nhau ngay lần đầu ai thêm một ô — đúng cái giá `EditGate` vừa gỡ đi ở §12i. |
| **Tham số `lang`** trên một component | Chọn cách này. |

`DeclarationForm` nhận `lang?: "vi" \| "en"`, **mặc định `"vi"`** — trang Hồ sơ là chỗ dùng lâu đời hơn, và một mặc định tiếng Anh sẽ lặng lẽ đổi ngôn ngữ của nó nếu ai quên truyền. `DeclarationBlock` trong mạch setup truyền `lang="en"` **cố định**, không đọc công tắc nào.

**Hai chi tiết dễ làm hỏng thứ không liên quan:**

* `PURPOSES` và `FREQ` nay có ba cột (mã · vi · en) chứ không phải hai. Cột `[1]` **giữ nguyên vị trí và giữ nguyên tiếng Việt**: phần đọc của trang Hồ sơ tra theo `?.[1]`, và đảo thứ tự sẽ làm trang ấy bày ra mã máy thay vì chữ người đọc.
* `aria-label` tách khỏi nhãn nhìn thấy và cố ý ngắn hơn. Lý do chính không phải thẩm mỹ: ba chuỗi tiếng Việt ("Kỹ năng khó nhất", "Bài viết có ai chữa", "Số giờ mỗi tuần") đang là chỗ bám của các test sẵn có ở trang Hồ sơ, nên đổi chúng là làm đỏ những test không liên quan gì tới việc dịch.

**Nhãn tám việc khai báo** (`IELTS_TASKS`) cũng sang tiếng Anh, và cố ý **trùng** với nhãn server đặt trong `modules/ielts/setup.ts`: learner bấm một dòng ở Learning Setup rồi tới trang con, nên hai chỗ gọi cùng một việc bằng hai cái tên là bắt họ tự đoán xem mình có đi đúng chỗ không.

Cổng kiểm: case Playwright của `/ielts/setup/declarations` nay đòi nhãn tiếng Anh, và case của trang Hồ sơ thêm một dòng đòi nó **vẫn** tiếng Việt — hai chiều, vì chỉ gác một chiều thì một lần đổi mặc định `lang` sẽ kéo trang `/squid/**` sang tiếng Anh mà không ai báo.

### 12k. Ba thẻ ở `/ielts/settings`, và sổ ba lần thi (SRC-871, 2026-09-19)

**Hai thẻ cạnh nhau, không phải một cột dọc.** Điểm hiện tại và mục tiêu chỉ có nghĩa khi đọc cùng nhau — khoảng cách giữa chúng *là* việc phải học. Xếp dọc thì muốn so phải cuộn, và cái phải so là thứ trang này tồn tại để bày ra. Trên điện thoại vẫn một cột (`md:grid-cols-2`), và `items-start` để thẻ trái dài ra vì lịch sử lần thi không kéo thẻ phải cao theo.

**Bốn ô nhỏ, hai ô một dòng, dùng chung một component.** `BandGrid` (`apps/learn/src/IeltsBandGrid.tsx`) vẽ bốn kỹ năng cho *cả hai* thẻ, cộng một dòng Overall chỉ đọc. Viết hai lần thì lần đầu ai đó đổi cỡ chữ ở một bên là hai cột lệch nhau ngay trên cùng một màn, và không test nào bắt được. Ô nhập không tự khoá theo prop: `EditGate` (§12i) đã dùng `<fieldset disabled>`, nên thêm một nguồn trạng thái nữa ở đây là dựng lại đúng thứ §12i gỡ đi.

**Sổ ba lần thi giữ nguyên hình dữ liệu cũ.** Bảng `learner_ielts_baselines` (0217) sinh ra với đúng một dòng `active` mỗi learner, và một chỉ mục duy nhất canh điều đó. Bảy chỗ trong worker đọc baseline bằng `WHERE status='active'` rồi `first()`: Learner Model, `journey.ts`, `layerInputs.ts`, `setup.ts`, ba truy vấn thư gửi phụ huynh. **Nới chỉ mục cho ba dòng active là làm bảy chỗ ấy bốc một dòng bất kỳ trong ba, không chỗ nào báo lỗi.** Nên: một dòng `active` là lần thi mới nhất, tối đa hai dòng `archived` là lịch sử. Không có migration nào trong vòng này.

`baselines.ts` là chỗ **duy nhất** quyết dòng nào active, qua `reconcileBaselines`: sắp theo `assessed_on` giảm dần, giữ ba dòng đầu, hạ tất cả xuống `archived` rồi nâng dòng đầu lên `active` (đúng thứ tự ấy — làm ngược lại thì có một khoảnh khắc hai dòng active và chỉ mục duy nhất ném). Sửa ngày một lần thi cũ thành hôm nay thì *đúng nó* trở thành điểm hiện tại, không phải dòng nào ghi sau.

Ba endpoint mới, và một đường cũ giữ nguyên vai trò: `GET/POST /v1/learners/{id}/ielts-baselines` và `DELETE .../{baselineId}` quản cả sổ; `PUT /ielts-baseline` vẫn **sửa** lần thi đang hiệu lực (giữ nguyên id, nên lịch sử model trỏ tới nó không đứt). Trần 3 canh ở server và trả 409 — client giấu nút thêm, nhưng một tab mở từ hôm qua vẫn gửi được lượt thứ tư. Lần thi cũ chỉ đọc và xoá được, không sửa: sửa lại điểm của một kỳ thi đã qua thì sửa cái gì?

**Skills to improve thành thẻ riêng.** Nó không phải một con số điểm mà là một *lựa chọn*, và nó quyết Diagnosis rút gọn còn mấy bài (§12c). Đứng lẫn trong thẻ điểm thì đọc ra như dòng phụ chú của điểm hiện tại. `FocusPicker` nhận thêm `label` (đặt `null` ở đây) để tiêu đề không in hai lần dưới tiêu đề thẻ.

Cổng kiểm: 7 ca vitest ở `workers/api/src/modules/ielts/baselines.test.ts` (trần 3, đúng một dòng active, active theo ngày chứ không theo thứ tự ghi, xoá dòng active thì dòng kế lên thay, overall của server, 404, và sổ của con nhà khác) + 2 ca Playwright ở `e2e/smoke.spec.ts` (hai thẻ cạnh nhau với bốn ô mỗi thẻ; thêm tới trần rồi nút biến mất kèm lời giải thích).

## 21. Ô điểm hiện tại phải đọc điểm ĐÃ LƯU (SRC-874, 2026-09-19)

Lỗi này lọt ra production ở §12i và chỉ lộ khi mở trang thật bằng mắt — không cổng nào bắt được.

**Triệu chứng nhìn thấy.** Trên `/ielts/setup`, banner nói *"How well does Nemo know your level? **Unknown**"* và *"Taken on: not recorded"*, nhưng ngay bên dưới thẻ điểm vẫn bày **L 6.5 · R 6.5 · W 6.0 · S 6.0**. Learner chưa khai gì mà màn hình vẫn đưa ra bốn con số.

**Nguyên nhân, và vì sao nó nặng hơn một lỗi hiển thị.** `CurrentScore` không bao giờ đọc điểm đã lưu; nó khởi tạo ô nhập bằng một bộ mặc định viết cứng. Ở chế độ SỬA thì bộ mặc định là chỗ đặt con trỏ cho người sắp gõ — vô hại. Ở chế độ ĐỌC (thêm vào ở §12i) thì đúng bốn con số ấy đọc ra như **sự thật về learner**. Và tệ hơn: một learner đã khai 7.5 mở ra sửa sẽ thấy 6.5, bấm Lưu là **ghi đè mất điểm thật** — mất dữ liệu, im lặng.

**Bản vá.**

* `evidence` trong payload `/ielts-setup` nay mang **điểm bốn kỹ năng** (`bands`), không chỉ `overall`. Trả kèm ở đây chứ không để client gọi thêm `ielts-goal`: hai lượt đọc là hai ảnh chụp có thể lệch, và khi lệch thì banner nói một đằng còn ô nhập nói một nẻo — đúng triệu chứng trên, chỉ khác nguồn.
* `CurrentScore` nhận `current` và khởi tạo từ đó; effect đồng bộ lại theo các **giá trị nguyên thuỷ**, không theo tham chiếu đối tượng (mỗi lượt đọc API trả về một đối tượng mới, so theo tham chiếu sẽ giẫm lên thứ learner đang gõ dở).
* Chưa khai gì mà đang ở chế độ đọc thì in thẳng *"No score entered yet."*, tuyệt đối không bày bộ mặc định.

**Phạm vi.** Chỉ `/ielts/setup`. Thẻ điểm ở `/ielts/settings` đã được dựng lại ở [§13](#_13-settings-thanh-ba-the-so-lan-thi-toi-da-ba-src-871-2026-09-19) (SRC-871) và bản ấy vốn đọc đúng bản ghi đã lưu.

**Vì sao không test nào bắt được, và đã sửa gì ở cổng.** Fixture e2e luôn kèm sẵn một bộ điểm cho `has_score`, nên cảnh *"đã chọn là mình có điểm nhưng chưa khai"* chưa từng được dựng. Nay fixture phân biệt hai trạng thái, và có hai ca: chưa khai thì phải thấy *"No score entered yet."*; khai 8.0/7.5 rồi tải lại trang thì ô phải mang đúng 8.0/7.5. Thêm hai ca đơn vị cho `evidence.bands`.

**Bài học đáng giữ**: cổng tự động của chúng ta kiểm *"màn hình có chạy không"*, không kiểm *"con số trên màn hình có thật không"*. Chuỗi trong bundle đúng, test xanh, CI xanh — mà trang vẫn nói dối về dữ liệu của learner. Với các màn bày số liệu cá nhân, phải có ít nhất một ca dựng cảnh **kho rỗng**.

## 41. Câu trả lời hiện sẵn ở Learning Setup, và quiz đi tiếp bằng một cú bấm (SRC-915, 20.09.2026)

**Một dòng ✓ không nói learner đã trả lời gì.** Sau SRC-910, chặng đã xong thì gập lại, nên toàn bộ
màn Learning Setup chỉ còn tên việc cộng dấu xong. Muốn nhớ lại target band của chính mình thì phải
mở `/ielts/setup/goal` rồi quay về — một chuyến đi cho một con số.

Nay mỗi `SetupPart` mang thêm `value`: câu trả lời đã lưu, rút thành một dòng ngắn.

| Việc | `value` |
| --- | --- |
| Target band and test date | `Band 7.0 · 12.06.2026` |
| Study context | `8h a week · Writing is hardest` |
| Where you are starting from | `Already has a score` |
| Your current IELTS score | `Band 7.0 · 20.08.2026` |
| Skills you want to improve | `Writing, Speaking` |

Ba quyết định đi kèm:

* **Tóm tắt dựng ở SERVER** (`modules/ielts/setup.ts`), cùng chỗ với `done`. Dựng ở client thì màn
  hình phải gọi thêm `ielts-goal` và `ielts-declarations` — hai lượt đọc nữa, hai ảnh chụp có thể
  lệch với ảnh này, đúng kiểu lệch mà `evidence.bands` (SRC-874) đã sinh ra để tránh.
* **Study Context chỉ lấy hai mẩu** trong tám trường: số giờ mỗi tuần và kỹ năng khó nhất. Đó là
  hai câu trả lời đổi thật kế hoạch học; `purpose`/`note` là chữ tự do, dài ngắn tuỳ người.
* **Đầu chặng ĐANG GẬP mang theo tối đa hai giá trị**, thay cho phụ đề. Nếu tóm tắt chỉ hiện khi mở
  ra thì nó không giải quyết được đúng điều chỉ đạo vừa nói. Chưa trả lời thì không bày gì — một
  dòng `· ·` trống còn tệ hơn không có.

**Quick Check: chọn xong màn đứng yên.** Bản SRC-792 tự nhảy câu sau 220ms. Nó tiết kiệm một cú bấm
mỗi câu nhưng lấy đi quyền quyết định lúc nào rời câu: bấm nhầm ô bên cạnh là màn đã đổi trước khi
kịp đọc lại, và muốn sửa thì phải lần ngược bằng "Câu trước". Nay có nút **Tiếp →**, khoá tới khi có
đáp án — một nút bấm được trong khi chưa chọn gì sẽ lặng lẽ bỏ qua một câu, và learner chỉ biết ở
màn kết quả, lúc đọc "Con chưa trả lời câu này".

Gác: `modules/ielts/setup.test.ts` (bốn test cho `value`, kể cả trường hợp chưa trả lời thì null),
và hai e2e trong `apps/learn/e2e/smoke.spec.ts` — một cho tóm tắt khi chặng còn gập, một cho nút Tiếp.

## 45. Nút "change" ở Starting Point, và một ngõ cụt một chiều (SRC-922, 2026-09-20)

### 45a. Ba hậu quả nối nhau

`change` gọi thẳng `choose("unsure")`:

1. **Ghi ngay, không hỏi lại.** Câu trả lời cũ mất trước khi có câu trả lời mới.
2. **`unsure` khác `null`.** Màn ba lối vào chỉ hiện khi `start_mode === null`, nên learner không
   được hỏi lại - họ bị ĐẶT sang "I'm not sure about my level".
3. **Không có đường về.** Bấm `change` lần nữa cũng chỉ ra `unsure`, và `start_mode` không được
   đặt ở chỗ nào khác trong cả app (đã grep toàn `apps/learn/src`: chỉ đúng hàm `choose` ấy).
   Learner kẹt vĩnh viễn, và `hasScore` ở `IeltsSettings.tsx` thành false nên thẻ Current score
   biến mất theo.

Dữ liệu trong sổ thi không hề bị xoá bởi đường này. Nhưng learner không nhìn thấy cơ sở dữ liệu -
họ thấy điểm vừa còn đó giờ mất, và kết luận là mình vừa xoá mất điểm của chính mình.

### 45b. Cách sửa, và vì sao không phải là đổi chữ

Nút chỉ **mở** ba lối vào, với lựa chọn hiện tại đánh dấu "Current". Không ghi gì cho tới khi
learner chọn một cái khác; có "Keep what I have" để lùi. `picking` là trạng thái của MÀN HÌNH,
không phải của dữ liệu - mở ra xem rồi đóng lại thì không có request nào đi.

Đổi chữ thành "start over" cũng làm nhãn nói thật, nhưng learner vẫn kẹt ở `unsure`. Ngõ cụt mới
là chỗ hỏng; cái nhãn chỉ là thứ dẫn người ta vào đó.

Đây là họ hàng của SRC-886, khác chiều: ở đó một cái nhãn ("Nguyễn Tất Đắc") là cò đăng xuất; ở
đây một cái nhãn ("change") là cò đặt-lại-và-khoá. Cùng một bài học - cử chỉ vô hại về hình thức
mà hậu quả lớn nhất màn hình.

Gác: một ca e2e trong `apps/learn/e2e/smoke.spec.ts` đi trọn vòng has_score → unsure → has_score,
đúng vòng mà bản cũ khoá lại ở nấc thứ hai.

## 48. Bộ khởi động có đúng ba câu (SRC-925, 20.09.2026)

Rà toàn bộ 51 bộ: **17 bộ sai**. 16 năng lực Viết có 2 câu (migration 0262 nạp "hai câu mỗi chặng"); `reading.main-idea` có 6 câu (migration 0251 gom mọi câu chặng 1 chưa xếp vào một bộ).

Ba câu là con số có việc thật: bộ khởi động để learner **nhận ra** thao tác trước khi vào bộ kỹ thuật, và hai câu thì một câu đoán trúng đã thành một nửa; nhiều hơn ba thì nó thôi làm việc khởi động mà thành một bài tập đầy đủ.

Migration 0268 vá hai chiều và **không xoá câu nào**: thiếu thì soạn thêm (16 câu chặng 1 cho phần Viết), thừa thì chuyển sang một bộ mới cùng bậc (`reading.main-idea.l1.4`). Cổng máy: `workers/api/src/modules/ieltsSkills/warmupShape.test.ts` dựng CSDL từ chính `migrations/` rồi đếm, nên năng lực mới nào thêm vào với hai câu khởi động sẽ đỏ ngay.

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

### Từ mục 12. Learning Setup: không ai phải đi qua cùng một quy trình (SRC-860, 2026-09-19)

Chỉ đạo: *"Khi mới vào page, mọi thông tin luôn là read-only. Có button Edit, click vào thì các element mới trở thành Editable."*

Chỉ đạo, từ một ảnh chụp trang Settings: *"Cần một card Current Score ở bên trái, mỗi dòng thể hiện 2 mini-cards, thể hiện điểm của 2 skills. Còn Target ở bên phải, UI cũng tương tự. Có thể tạo thêm vài lượt nữa, đó là các điểm và các lần thi (tạo tối đa 3 lần). Cụm Skill to improve, cần tách riêng ra card riêng."*

#### 12a. Tám việc phẳng → năm chặng có tên là câu hỏi

> Đã thay bằng §65 (Learning Setup còn hai chặng, [§65a](learning-setup-hub-return.md))

`/ielts/settings` (§5c) là một danh sách tám dòng: Mục tiêu · Hoàn cảnh học · Quiz · Bài đo · Bài viết · Ghi âm · Buổi gặp · Kế hoạch. Ai vào cũng thấy đủ tám, ai cũng phải làm đủ tám. Với learner đã có IELTS 7.0 muốn nâng Writing, năm trong tám dòng ấy là việc họ không có lý do gì phải làm — và cái giá không phải là thời gian, mà là họ đóng tab.

Nay là `/ielts/setup`, năm chặng, mỗi chặng mang chính câu hỏi nó hỏi:

| # | Chặng | Câu hỏi | Gồm |
| --- | --- | --- | --- |
| 01 | Goal | Where do you want to get to? | band mục tiêu + ngày thi, hoàn cảnh học |
| 02 | Starting Point | Where are you starting from? | lối vào, điểm hiện tại, kỹ năng muốn nâng |
| 03 | Diagnosis | What do you need to improve? | Quick Check, Reading & Listening Check, Writing Sample, Speaking Sample |
| 04 | Calibration | What does your teacher confirm? | khung giờ rảnh, buổi gặp |
| 05 | Learning Plan | How will you get there? | các mốc |

Tám việc cũ không mất đi — chúng được xếp vào chặng của mình, và bảng `IELTS_TASKS` vẫn là nguồn duy nhất nối khoá việc với URL.

#### 12b. Starting Point là một chỗ rẽ, không phải một biểu mẫu

> Đã thay bằng §65 (chặng Starting Point đã bỏ, [§65a](learning-setup-hub-return.md))

Đây là quyết định quan trọng nhất của cả vòng. Ba lối vào:

| Lối vào | Nghĩa | Diagnosis đòi gì |
| --- | --- | --- |
| `new` — *I'm new to IELTS* | chưa thi, chưa biết trình độ, muốn học luôn | **không gì cả** |
| `has_score` — *I already have an IELTS score* | đã thi, biết điểm | chỉ bài của kỹ năng họ chọn nâng |
| `unsure` — *I'm not sure about my level* | nhờ Nemo đo | đủ bốn |

Với `has_score`: chọn Writing → Writing Sample; chọn Speaking → Speaking Sample; chọn Reading **hoặc** Listening → bài đo chung (`skill_assessment`), vì hai kỹ năng ấy không có bài đo riêng — cả hai nằm trong một phiên `assessment_sessions`. Chọn cả hai vẫn là **một** bài, không phải hai. Quick Check bỏ: người đã thi thì đã biết kỳ thi vận hành thế nào.

Không chọn kỹ năng nào trong khi đã khai có điểm thì đòi đủ bốn như `unsure` — "chưa chọn" không được phép thành một lối lách qua mọi bài đo.

Với `new`, đây là điểm UX mà chỉ đạo nhấn mạnh: **Learn → Practice → Measure → Adapt**, thay vì Test → Test → Test rồi mới được học. Trình độ lộ ra qua chính các bài luyện, và sổ bằng chứng ở §5b-bis đã nhận bài luyện từ SRC-849 nên đường ấy có thật, không phải lời hứa.

#### 12d. Phần không phải làm vẫn HIỆN ra

> Đã thay bằng §65 (không chặng nào bị khoá nữa, [§65a](learning-setup-hub-return.md))

Quick Check và Reading & Listening Check của learner `has_score` không bị giấu đi — chúng hiện mờ kèm dòng *not needed for you*. Giấu thì trang ngắn hơn, nhưng learner mất cả đường quay lại làm nếu sau này họ muốn, và quan trọng hơn: thấy Nemo đã **cân nhắc** bài này rồi bỏ qua khác hẳn với việc Nemo chưa từng có nó.

Ngoại lệ: khi **cả chặng** là Optional (learner `new`), từng dòng không lặp lại nhãn ấy — bốn lần *not needed for you* liên tiếp vừa thừa vừa đọc ra như một lời từ chối. Nhãn `Optional` ở đầu chặng nói đủ.

Tiến độ đếm trên chặng **bắt buộc**, nên learner `new` thấy "2 of 4" còn learner `unsure` thấy "2 of 5". Viết cứng "of 5" ở client sẽ nói dối với learner thứ nhất về một việc không tồn tại.

#### 12e. Nhãn mềm, và vì sao nó không phải chuyện chữ nghĩa

> Đã thay bằng §65 (năm chặng còn hai, [§65a](learning-setup-hub-return.md))

*Quick Check · Reading & Listening Check · Writing Sample · Speaking Sample* — không có chữ Test hay Assessment nào. Learner 7.0 lên 7.5 đọc thấy "Assessment" sẽ hiểu là bị bắt thi lại IELTS, và phần đông sẽ đóng tab thay vì viết một bài luận. Nhãn do server đặt (`setup.ts`) để một nơi duy nhất quyết, nhưng lý do thuộc về màn hình.

Chữ trên hai trang này là **tiếng Anh**, đúng luật sân luyện `learn.nemo12.com/ielts/**` (SRC-834/835). Trang cũ còn sót tiếng Việt ("Mục tiêu và hoàn cảnh", "Xong 4/4"); vòng này trả về đúng luật.

#### 12f. `/ielts/settings` còn lại là một ngăn kéo

> Đã thay bằng §12k (ba thẻ ở `/ielts/settings`)

Learning Setup có thứ tự, có chỗ khoá, có điểm kết thúc. `/ielts/settings` thì không: nó là ngăn kéo các giá trị đã khai, mở ra sửa một con số rồi đóng lại. Thử thách cho mọi thứ định thêm vào đó chỉ có một câu — **learner có quay lại sửa nó nhiều lần không?** Band mục tiêu thì có (ngày thi lùi, trường đổi yêu cầu). Writing Sample thì không: nộp xong là xong.

Hai địa chỉ, và thứ tự trong `pathToRoute` là điều duy nhất giữ cả hai cùng sống:

* `/ielts/setup` và `/ielts/setup/<task>` — địa chỉ hiện hành, và là dạng DUY NHẤT `routeToPath` sinh ra.
* `/ielts/settings/<task>` — **link cũ** từ SRC-805, vẫn tới đúng việc ấy (luật slug, CLAUDE.md).
* `/ielts/settings` trơ trọi — trang Settings mới. Bookmark cũ tới đây đổi đích; đó là đổi **có chủ ý** theo chỉ đạo, và trang mới có sẵn một đường sang Learning Setup ngay trên màn.

#### 12h. Mã và cổng kiểm

> Đã thay bằng §65b (tám trang con đã bỏ, [§65b](learning-setup-hub-return.md))

| | Ở đâu |
| --- | --- |
| Luật ai-phải-làm-gì | `workers/api/src/modules/ielts/setup.ts` (hàm thuần, không đọc DB) |
| Trạng thái đã-làm-gì | `pathway.ts` — **không đổi**, `setup.ts` phủ lên nó một lớp yêu-cầu |
| Đường API | `GET`/`PUT /v1/learners/{id}/ielts-setup` |
| Lưu trữ | `0253_ielts_learning_setup.sql` |
| Màn hình | `apps/learn/src/IeltsLearningSetup.tsx`, `IeltsSettings.tsx` |
| Địa chỉ | `pathToRoute`/`routeToPath` trong `App.tsx` (đã xuất ra để test được) |

Tách `setup.ts` khỏi `pathway.ts` vì hai câu hỏi hỏng theo hai kiểu: "đã làm gì" sai là đọc sai CSDL, "cần làm gì" sai là sai luật sản phẩm. Gộp lại thì một lần đổi luật sản phẩm phải sửa vào đúng chỗ đang đọc CSDL.

Cổng kiểm: `setup.test.ts` (16 ca, hàm thuần), `setupRoutes.test.ts` (13 ca qua HTTP — quyền, ghi từng phần, đồng bộ Evidence Level), `apps/learn/src/ieltsRoutes.test.ts` (12 ca địa chỉ, gác link cũ khỏi gãy), và 4 ca Playwright trong `e2e/smoke.spec.ts`.

### Từ mục 41. Câu trả lời hiện sẵn ở Learning Setup, và quiz đi tiếp bằng một cú bấm (SRC-915, 20.09.2026)

Chỉ đạo chủ dự án, hai việc rời nhau trong cùng một vòng xem màn hình:

> *"Cần nhìn luôn được 1 thông tin nào đó, ví dụ như target band, thì cần hiển thị luôn lên đây,
> mà không cần click vào mới xem được. Và 1-2 thông tin về Study Context cần hiển thị luôn tại đây."*
>
> *"Chọn đáp án, rồi bấm button gì đó, thì mới đi tiếp."*

### Từ mục 45. Nút "change" ở Starting Point, và một ngõ cụt một chiều (SRC-922, 2026-09-20)

Chủ dự án mở `/ielts/profile` trên production và thấy *Current overall: not set*, trong khi ảnh
chụp `/ielts/settings` một tiếng trước đó còn bày **6.5**. Lần ngược thì hoá ra trang hồ sơ nói
đúng: sổ thi trống, `evidence.level` là `unknown`, `start_mode` là `unsure`. Câu hỏi thật là vì
sao một learner đã khai "I already have an IELTS score" lại đang ở `unsure`.

### Từ mục 48. Bộ khởi động có đúng ba câu (SRC-925, 20.09.2026)

Chỉ đạo chủ dự án 2026-09-20: *"Warm-up Set, luôn có đúng 3 câu. Cái này có mỗi 2 câu. Rà soát nốt các Warm-up set còn lại."*
