---
url: https://docs.nemo12.com/architecture/sdd-038/reading-writing.md
description: >-
  Sub-skill Viết theo bốn tiêu chí, hai chiều của Reading, gắn thẻ năng lực,
  phân bố đáp án và lựa chọn sai
---

# SDD-038 · Kho bài Đọc và Viết

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

## 25. Dap an thoi luon dung dau, va file JSON noi dung bi go han (SRC-882, 2026-09-19)

### 25a. Sap lai thu tu lua chon cua 128 cau

16 bai nghe soan dau tien co 123 tren 128 cau dat dap an dung o lua chon thu nhat (muc 23). Hai dot
sau da rai san luc sinh seed; dot dau thi chua. Nay ca 128 cau duoc sap lai: 33 / 31 / 28 / 31.

**Cai gia da chap nhan, noi ra cho ro:** bang `ielts_practice_attempts` ghi CHI SO lua chon chu
khong ghi noi dung, nen nhung lan tra loi truoc ngay hom nay khong con doi chieu duoc voi de. Diem
dung/sai da luu van dung; chi rieng viec "learner da chon phuong an nao" la mat nghia. Chu du an
quyet dinh danh doi dieu do de de thoi du doan duoc.

**Sap TAT DINH chu khong xoay.** Xoay di k cho phu thuoc vao thu tu hien tai, nen chay lai lan hai
la xoay tiep - ma workflow seed khong co nut hoan tac va chay nham hai lan la chuyen se xay ra
(QG-004). Thu tu moi vi the suy tu chinh NOI DUNG tung lua chon: sap theo khoa bam cua chuoi lua
chon cong ma cau. Chay bao nhieu lan cung ra dung mot thu tu.

**Doc tu D1, khong doc tu file.** `scripts/gen-listening-answer-order.mjs` lay bai qua duong doc
cong khai cua API. Mot ban sao trong repo se lech voi ban dang chay dung vao ngay co nguoi sua mot
cau o man admin, va khi ay no se lang le ghi de ban sua ay.

### 25b. File JSON noi dung da go han

Da go: 12 file JSON, va script sinh seed cua hai dot ay - no chi chay duoc khi con JSON nen khong
con ly do ton tai. Ca hai deu tim lai duoc trong lich su git neu can doi chieu.

**Giu lai hai file `.sql` da nap** (`seed-ielts-listening-batch2.sql`, `batch3.sql`) va file sap
lai thu tu. Chung khong phai ban thu hai cua noi dung ma la SO GHI viec da chay: luat seed-data noi
ro file nap phai nam trong repo, vi "chay roi ma khong co trong repo thi lan sau khong ai dung lai
duoc". Chung khong bao gio duoc doc lai de dung lai noi dung.

**Soan dot sau o dau:** man admin (`apps/admin`, muc 18), hoac mot script doc tu D1 roi sinh SQL
nhu `gen-listening-answer-order.mjs`. Khong soan trong repo nua.

## 27. Bai doc cung thoi dan dap an ve lua chon dau (SRC-899, 2026-09-20)

Chi dao chu du an: *"sua luon 16 bai cu di"* - noi sau khi 16 bai nghe da sua xong, nen hieu la sua
not phan con lai. Do lai thi lo ra con so: 52 bai doc co **1336 tren 1422** cau mot dap an dat dap
an dung o lua chon thu nhat, tuc 94 phan tram - te hon bai nghe nhieu (123/128 nhung chi tren 128
cau). Sau khi sap lai: 375 / 361 / 349 / 337.

**Phep sap nam TRONG SQL, khac lan sua bai nghe.** Lan truoc mot script Node doc bai qua API roi
sinh san tung cau UPDATE. Nay khong lam the duoc: duong doc noi dung da vao sau cong dang nhap
(SRC-885), va token wrangler tren may soan da het han - may soan khong con duong nao doc D1.

Doi lai duoc mot thu dang gia hon ca su tien loi: cau UPDATE sinh san mang theo CA NOI DUNG bon lua
chon, nen neu co nguoi vua sua mot cau o man admin thi lan nap ay ghi de ban sua. Ban SQL chi dao
THU TU cua chinh nhung lua chon dang co, nen noi dung moi nhat duoc giu nguyen du no la ban nao.
Tu nay moi lan sua hang loat noi dung nen theo loi ay.

**Van TAT DINH chu khong xoay**, cung ly do voi muc 25a: khoa sap suy tu noi dung tung lua chon
(`length(value) * 131 + unicode(value)`, lay du 1000), nen chay lai bao nhieu lan cung ra mot thu
tu. Khoa nay khong phai ham bam tot, va khong can tot - viec duy nhat no phai lam la khong tuong
quan voi thu tu nguoi soan go vao.

**Kiem TRUOC khi chay tren production.** Cau SQL nay phuc tap hon moi file seed truoc do, nen no
duoc chay thu tren SQLite cuc bo voi noi dung 52 bai doc THAT lay tu lich su git (commit 87f4c53e
da xoa cac file JSON, nhung `git show` van doc duoc). Bon bat bien duoc kiem: dap an van tro vao
dung chuoi lua chon cu; tap lua chon khong mat khong them; chay lan hai khong doi gi; va cau ngoai
pham vi khong bi dung toi.

**Pham vi:** chi cau trac nghiem MOT dap an. Cau nhieu dap an (dang "Which THREE statements") dung
ngoai vi dao chung can anh xa nhieu chi so cung luc, va chung chi chiem vai chuc cau.

**Cai gia** giong muc 25a va da duoc chap nhan lan thu hai: `ielts_practice_attempts` ghi CHI SO
lua chon, nen nhung lan tra loi bai doc truoc hom nay khong con doi chieu duoc voi de.

## 32. Sub-skills của phần Viết, xếp theo bốn tiêu chí chấm (SRC-896, 2026-09-20)

| Cụm (tiêu chí chấm) | Sub-skills |
| --- | --- |
| Task Response / Task Achievement | Reading the task · Holding a position · Developing ideas · Supporting ideas · Addressing all parts · Describing key features |
| Coherence & Cohesion | Building a paragraph · Linking ideas · Logical progression · Paragraph organisation · Referencing · Cohesive devices |
| Lexical Resource | Choosing words · Vocabulary range · Precision · Appropriacy · Word formation · Spelling |
| Grammatical Range & Accuracy | Varying sentences · Sentence structures · Complex sentences · Grammatical accuracy · Punctuation |

### a. Cụm là một cột trong CSDL, không phải một bảng tra trong mã

`ielts_micro_skills.cluster` (migration 0264). Để phép chia này trong một hằng số ở app thì cùng
một danh sách sống ở hai nơi, và cái ở màn admin, nơi nội dung được sửa thật, sẽ là cái không có.

Cột **rỗng** với reading, listening, speaking: ba kỹ năng ấy không được chấm theo tiêu chí, nên gán
cụm cho chúng là bịa ra một tầng không có thật. Thẻ Mastery gom nhóm khi cột có chữ và xếp phẳng
khi không, nên vẫn chỉ có một bản thẻ cho cả bốn kỹ năng.

Vì sao chia cụm lại đáng làm: một danh sách phẳng hai mươi ba dòng đọc ra "hai mươi ba thứ phải
giỏi". Cùng danh sách ấy cắt theo cụm thì đọc ra "bốn tiêu chí được chấm, mỗi tiêu chí gồm mấy thao
tác" — đúng hình dạng thật của bài thi, và là câu trả lời cho câu learner hỏi ngay sau khi nhận
điểm: *mất điểm ở tiêu chí nào*.

### b. Bốn mục cũ nằm ngoài danh sách mới

* **`describe-data` đổi TÊN** thành *Describing key features* (chữ của chính bản mô tả thang điểm
  Task 1), **giữ nguyên mã**. Mã là đoạn URL và là khoá đếm mastery: đổi mã đi thì link learner đã
  lưu gãy, và mọi câu họ đã làm dưới mã cũ biến khỏi thẻ.
* **`introduction`, `conclusion`, `concede` rút xuống `draft`** (chỉ đạo chủ dự án 2026-09-20, khi
  được hỏi thẳng). Không xoá: lý thuyết và bài tập đã soạn xong vẫn nằm nguyên trong CSDL và bật
  lại ở màn admin bằng một cú bấm. Xoá dòng thì mất cả nội dung lẫn bằng chứng learner đã làm dưới
  mã ấy, mà cái được chỉ là vài dòng sạch hơn.

### c. Mười sáu mục mới sinh ra đã có chỗ học và chỗ luyện, và đủ thang bốn chặng

Mỗi mục mới nạp đủ **năm phần lý thuyết** (là gì · các bước · ví dụ có giải · bẫy) và bài tập theo
đúng **thang bốn chặng** của SRC-821:

| Chặng | Nội dung | Số câu mỗi năng lực |
| --- | --- | --- |
| 1 | tiếng Việt, tình huống thẳng thớm | 2 |
| 2 | tiếng Việt, mỗi câu mang tên kỹ thuật | 2 |
| 3 | tiếng Anh, đoạn ngắn | 4 |
| 4 | tiếng Anh, đoạn dài gần đề thật | 2 |

Migration 0260 nạp chặng 3; **0262 nạp ba chặng còn lại**. Hai lần vì lần đầu dừng ở chặng 3 và
cổng `microSkills/routes.test.ts` đỏ ngay: thang bốn chặng là ràng buộc chứ không phải gợi ý, và
với phần Viết nó còn đúng hơn phần Đọc, vì "triển khai một ý" hay "mạch ý tiến lên" là thao tác
của người viết bất kỳ thứ tiếng nào. Gặp nó lần đầu bằng tiếng Anh là trộn hai việc khó vào một.

Một ngoại lệ có chủ ý: `spelling`, `word-formation`, `punctuation` giữ **ngữ liệu tiếng Anh ở mọi
chặng**, vì thứ được luyện ở đó chính là mặt chữ tiếng Anh; phần hỏi và phần giải thích của chặng
1-2 vẫn tiếng Việt.

Một dòng hiện trên thẻ mastery mà bấm vào chỉ thấy *No exercises published yet* là lời mời dẫn vào
phòng trống: learner bấm đúng chỗ họ yếu nhất rồi không có gì làm.

### d. Thang năm bậc cho cả mười sáu mục (migration 0266)

Chỉ đạo chủ dự án 2026-09-20, vòng ba: *"soạn tiếp thang năm bậc cho 16 mục mới"*. Mỗi mục nay có
**40 câu**, đúng khuôn mà `scripts/check-micro-content.mjs` gác:

| Bậc | Thành phần | Số câu |
| --- | --- | --- |
| 1 | tiếng Việt, tình huống thẳng thớm | 5 |
| 2 | 5 cặp Việt-Anh, đoạn 30-62 từ | 10 |
| 3 | tiếng Anh, đoạn ngắn | 10 |
| 4 | 5 cặp Việt-Anh, đoạn 65-110 từ | 10 |
| 5 | tiếng Anh, đoạn 110-200 từ như đề thi | 5 |

Tổng: **640 câu, 80 bài tập**. Nội dung soạn trong `content/ielts-micro/writing/` (một file một năng
lực, theo luật nhiều-phiên-song-song) và sinh ra SQL bằng `scripts/micro-seed.mjs`, cùng đường với
phần Reading.

Một sửa trong chính trình sinh đi kèm bản này: mặc định `task_type_id` là `<skill>.multiple-choice`
chỉ còn áp cho **Reading**. `ielts_task_types` hiện chỉ có bộ dạng của Reading (0264 nói rõ vì sao),
và cột ấy là khoá ngoại, nên gán `writing.multiple-choice` không phải một nhãn sai mà là một dòng
không nạp được: cả migration dừng ở lỗi FOREIGN KEY. Kỹ năng chưa có bộ dạng thì để `NULL`, đúng như
cột đã cho phép từ đầu.

### e. Còn mở

* Kho bài Writing chưa gắn thẻ sub-skill ở mức câu hỏi như Reading và Listening, nên mastery của
  phần Viết vẫn đếm từ **bài tập năng lực** là chính.
* Phần Viết chưa có **chiều thứ hai theo dạng đề** như Reading (SRC-895). Bốn tiêu chí chấm là một
  trục khác với dạng đề, nên nếu sau này thêm trục ấy thì nó là Task 1 và Task 2, không phải các
  dạng câu hỏi trắc nghiệm.

## 35. Reading có HAI chiều, và chúng cắt cùng một kho bài (SRC-895, 2026-09-20)

### 35a. Vì sao MỘT kho hai nhãn, không phải hai kho

Sơ đồ chủ dự án gửi đặt SKILLS và TASK TYPES cạnh nhau như hai nhánh song song, và cách đọc thẳng nhất là dựng cho mỗi nhánh một kho bài riêng. Chủ dự án đã bác cách ấy (2026-09-20), và lý do nằm ở chỗ hai chiều này không độc lập với nhau:

* **Mọi bài Matching Headings đều đang luyện Main Idea.** Đó là định nghĩa của dạng ấy, không phải một sự trùng hợp. Tách hai kho là soạn lại cùng một thao tác tư duy hai lần, rồi đo nó bằng hai cái thước không cộng được với nhau.
* **Bảng "Where am I strong?" cộng theo năng lực.** Một kho bài tập thứ hai không mang năng lực thì mỗi lần learner luyện Matching Headings là một lần thanh Main Idea đứng yên, trong khi learner biết rõ mình vừa luyện đúng cái đó. Sai lệch ấy không ai đọc được từ màn hình.

Nên một bài tập mang **đúng một năng lực và đúng một dạng câu hỏi**. Lọc theo chiều nào cũng cắt trên cùng một kho, và mọi lần làm bài đều rơi vào đúng một ô của cả hai thước đo.

Hệ quả trên giao diện, và nó là thứ phải giữ: **thẻ bài ở trang năng lực mang nhãn DẠNG, thẻ bài ở trang dạng mang nhãn NĂNG LỰC.** Chiều nào cũng bày ra nhãn của chiều kia trước khi learner bấm. Bỏ nhãn chéo ấy đi là hai chiều thành hai thế giới rời nhau, và learner sẽ làm lại đúng một bài tưởng là hai.

### 35b. Bài tập vẫn chỉ có MỘT địa chỉ

Hai nhánh URL cho hai lối vào — `/ielts/skills/reading/<năng lực>` và `/ielts/tasks/reading/<dạng>` — vì mỗi lối vào là chỗ learner sẽ quay lại và sẽ gửi link cho nhau.

Nhưng **không có** `/ielts/tasks/reading/matching-headings/levels/2/1`. Thẻ ở trang dạng trỏ thẳng về địa chỉ dưới nhánh `skills`. Cho một bài hai URL là để sổ hoạt động ghi hai `ref` khác nhau cho cùng một lần làm bài, rồi mọi con số đếm lượt sai đi một cách không ai đọc ra được từ màn hình. `taskTypeRoutes.test.ts` khoá quyết định này lại bằng một ca kiểm riêng.

### 35c. Mười hai dạng, và vì sao chúng chưa có kiểu tương tác thật

Bảng `ielts_task_types` (migration 0264) giữ mười hai dạng của Reading theo đúng danh sách chủ dự án gửi. Tên **tiếng Anh là tên chính**, chặt hơn luật chung của sân luyện một bậc: đây là dòng chữ in nguyên văn trên đề thi, và một learner chỉ quen "Ghép tiêu đề" thì lúc cầm đề sẽ phải dịch ngược trong khi đang tính giờ.

Bảy trong mười hai dạng — Matching Headings, bốn dạng Completion, Short Answer, Matching Sentence Endings — trong phòng thi đòi learner **nhập chữ** hoặc **ghép cặp**, chứ không chọn trong bốn ô. Đợt này **chưa** dựng hai kiểu tương tác ấy (quyết định chủ dự án 2026-09-20: "trắc nghiệm trước, kiểu thật ở đợt sau").

Điều đó **không** làm các bài mới thành trắc nghiệm chung chung: ngữ liệu và đề bài vẫn mang đúng hình dạng của dạng ấy — TFNG chỉ có ba lựa chọn True/False/Not Given, Matching Information hỏi "đoạn nào", Sentence Completion in sẵn giới hạn số từ và các lựa chọn đều là chữ lấy nguyên văn từ bài. Thứ còn thiếu là ô nhập, không phải thao tác tư duy. Việc còn mở: mở rộng lược đồ cho đáp án nhập chữ và đáp án ghép cặp, cùng phần chấm điểm của chúng.

### 35d. Evidence đổi tên thành Logical Argument, giữ nguyên mã

Sơ đồ chủ dự án ghi "Logical Argument" ở chỗ hệ đang chạy "Evidence". Đổi **nhãn**, giữ nguyên **mã** `evidence`:

* `learn.nemo12.com/ielts/skills/reading/evidence` đã là URL công khai, và luật CLAUDE.md là link người dùng đã lưu không được gãy.
* Mã còn là tiền tố của id bài tập (`reading.evidence.l2.1`) và của `ref` trong sổ hoạt động (`exercise:reading.evidence.l2.1`). Đổi mã là phải viết lại cả lịch sử làm bài của learner, và một lần viết lại như thế hỏng giữa chừng thì không có đường lùi.

Tên Việt đổi theo: "Bằng chứng" nói về thứ learner **tìm**, còn "Lập luận" nói về thứ learner **xét** — và chính cái sau mới là điều năng lực này dạy.

### 35e. Nội dung soạn theo trục nào thì xếp theo trục ấy

`content/ielts-micro/reading/<năng lực>.json` xoay theo năng lực; `content/ielts-micro/reading-tasks/<dạng>.json` xoay theo dạng, mỗi bài khai `micro` của mình. Hai thư mục cùng đổ vào một bảng qua `scripts/micro-seed.mjs`.

Hai cách xếp vì cách xếp phải theo trục mà **người soạn đang nghĩ**: soạn năm bài Matching Headings một lượt thì chúng phải nằm cạnh nhau để đọc được ra là năm bài ấy có lặp ý nhau không. Bắt xé chúng ra năm file năng lực khác nhau là bỏ đúng phép kiểm ấy.

Đợt này thêm **55 bài · 275 câu** cho 11 dạng chưa có (Multiple Choice đã được 100 bài cũ phủ). Kết quả hai chiều: mười năng lực có 12–23 bài mỗi cái, mười hai dạng có ít nhất 5 bài mỗi cái — vượt mức sàn chủ dự án đặt ra cho cả hai chiều.

### 35f. Bốn chỗ hỏng im lặng mà bộ test gác

Cả bốn đều biểu hiện bằng một màn hình **trông bình thường**:

1. **Đường mới không thừa hưởng cổng đăng nhập.** Cổng SRC-885 gác theo mẫu `micro-skills/*`, mà `task-types` không khớp mẫu ấy — hai đường mới mở toang vào đúng kho nội dung vừa khoá. Bộ test bắt được chỗ này trước khi nó kịp ra production.
2. **`LEFT JOIN` sang bảng dạng, không `INNER`.** Bài chưa gán dạng phải vẫn hiện; một `INNER JOIN` làm cả trang Listening trống trơn vào đúng ngày cột mới thêm mà nội dung chưa kịp gán.
3. **Dạng rỗng phải trả `exercises: 0`, không biến mất.** Ô xám nói với learner "có chỗ này, chưa tới lượt"; một dạng vắng mặt thì không nói gì, và bản đồ kỳ thi thủng một lỗ. Vì thế số đếm dùng truy vấn con tương quan chứ không dùng `JOIN ... GROUP BY`, vốn sẽ lặng lẽ đánh rơi đúng những dòng ấy.
4. **Hai chiều phải soi vào cùng một kho.** Nếu một bài đếm được ở chiều này mà mất ở chiều kia thì mô hình một-kho đã hỏng, và hỏng theo cách chỉ lộ ra khi ai đó ngồi đếm tay.

## 38. Gắn thẻ năng lực nhỏ cho bài viết, ở mức câu hỏi (SRC-913, 2026-09-20)

### a. Vì sao phần Viết không gắn thẻ được theo cách của phần Đọc

Với Reading và Listening, mỗi câu hỏi mang sẵn một thẻ năng lực và learner trả lời đúng hoặc sai,
nên bằng chứng rơi ra từ chính phép chấm. Một đề Viết thì **không có câu hỏi nào bên trong**: nó có
một bài văn, và bài văn ấy được chấm bằng bốn band tiêu chí. Dừng ở đó thì thẻ "Where am I strong?"
của phần Viết mãi mãi chỉ đếm được bài tập năng lực, trong khi thứ learner bỏ nhiều công nhất lại
là những bài viết thật.

Nên **chỗ gắn thẻ là lúc chấm**, không phải lúc soạn đề: một lượt đọc riêng trả lời câu hỏi "bài
này cho thấy gì về từng thao tác viết", và kết quả đi vào đúng bảng mà hai kỹ năng kia đang dùng
(`ielts_subskill_results`).

### b. Một prompt riêng, không gộp vào hai prompt đã có

`ielts.writing-subskill-tag@1`. Hai prompt sẵn có viết cho hai người đọc (mentor và learner) và cả
hai trả về **lời văn**; thứ cần ở đây là một **bảng dữ liệu**. Gộp là buộc một prompt làm hai việc,
và tới lượt sửa thứ hai thì phần dữ liệu sẽ bị lời văn lấn át — bài học đã trả giá bốn lần trong
`PROMPT_HISTORY`. Tách riêng còn cho phép phần chấm hỏng mà phần thẻ vẫn chạy, và ngược lại.

**Danh mục 23 năng lực là ĐẦU VÀO, đọc từ D1**, không viết cứng trong prompt: nội dung sửa được ở
màn admin (SRC-896), nên một bản sao trong mã là một nguồn sự thật thứ hai mà không ai nhớ cập nhật.

### c. Trần tám thẻ mỗi bài, và đó là ràng buộc chứ không phải con số cho đẹp

`MIN_ITEMS` của thang mastery là 8. Một bài viết được phép gắn hai mươi ba thẻ sẽ tự mình đẩy mọi
năng lực qua ngưỡng xếp bậc chỉ sau ba lần nộp, và bậc ấy dựng trên phán đoán của model về một bài
duy nhất. Prompt vì thế bị cấm nói về năng lực mà đề bài không đòi tới, và đường gọi loại bỏ mã lạ,
mã trùng rồi cắt trần trước khi ghi (`writingTags.ts`, có test riêng).

### d. Đếm chung một thanh với bài tập (chỉ đạo chủ dự án 2026-09-20)

Bằng chứng ở đây do **model phán đoán**, còn bằng chứng của bài tập là **đúng sai khách quan**. Chủ
dự án chọn cộng chung: cùng một thao tác thì cùng một thước, và learner không phải cộng nhẩm hai
con số trên một thẻ vốn đã có hai mươi ba dòng. Cột `ref` giữ nguồn (`writing:<production_id>`) để
khi một con số trông sai thì còn truy ngược được về đúng bài đã sinh ra nó.

### e. Cả hai đường nộp đều gắn thẻ

Bài **chẩn đoán** (`ielts/routes.ts#finishScoring`) và bài **thang luyện**
(`ieltsSkills/routes.ts#finishLadder`) đều là bài viết thật. Chỉ gắn thẻ một đường là dựng lại đúng
chỗ lệch mà SRC-849 đã phải đi sửa một lần rồi. Lỗi gắn thẻ bị nuốt ở cả hai đường: learner đã nộp
bài và đã có nhận xét trong tay, một lượt gắn thẻ hỏng là việc của hệ thống chứ không phải của họ.

### f. Phần Nói dùng chung đúng bộ máy ấy (SRC-914)

Hai kỹ năng sản sinh chỉ khác nhau ở **đầu vào** (chữ hay bản phiên âm) và ở **danh mục năng lực**;
phép lọc, trần tám thẻ và cách dựng dòng bằng chứng thì giống hệt. Nên chúng đi chung một đường:
prompt `ielts.writing-subskill-tag@1` nghỉ hưu sau vài giờ, thay bằng
`ielts.production-subskill-tag@1` có nhánh theo kỹ năng, cùng cách mà `ielts.production-feedback`
đã làm. Dựng một prompt thứ hai cho Nói thì hai bản sẽ lệch nhau ngay lượt sửa sau, và lệch im lặng.

**Bản phiên âm giới hạn thẻ nào nói được, và giới hạn ấy nằm trong prompt.** Phát âm, trọng âm,
ngữ điệu đã bị lọc mất khi tiếng thành chữ, nên cấm gắn thẻ dựa vào chúng (cùng lệnh cấm mà
SRC-860 đã mang vào đường chấm). Hai thứ vẫn còn trong chữ:

* **Chỗ tự sửa giữa câu** để lại nguyên vệt trong bản phiên âm, nên `self-correct` gắn thẻ được
  bình thường.
* **Nhịp nói** suy được từ `seconds` cộng số chữ, cộng các chỗ ngắc ngứ còn sót lại
  (*"I think, er, the main reason"*). Không có hai thứ ấy thì prompt được dặn bỏ qua năng lực này
  thay vì đoán.

**Ba đường nộp đều gắn thẻ**, không phải hai: bài chẩn đoán, bài thang luyện, và **lượt nói buộc
vào một năng lực** (`speakingTurn.ts`). Đường thứ ba dễ bị bỏ sót vì nó đã mang sẵn một mã năng
lực, nhưng mã ấy nói learner ĐỊNH luyện gì chứ không nói đoạn nói CHO THẤY gì: một lượt luyện
"Extending an answer" bốn mươi giây vẫn để lộ việc thì bị trôi hay một chỗ tự sửa rất gọn.

### g. Còn mở

* **Mentor chưa sửa được thẻ**. Khi mentor chốt một band khác model, phần thẻ vẫn giữ nguyên phán
  đoán cũ. Một đường cho mentor gỡ một thẻ sai là việc đáng làm, và nó cũng là dữ liệu để đo xem
  model gắn thẻ đúng tới đâu.
* **Bảng bằng chứng không ghi prompt nào đã sinh ra thẻ.** `ielts_productions.ai_prompt_ref` chỉ
  giữ prompt CHẤM; `ielts_subskill_results` không có cột tương đương. Ngày nào đổi prompt gắn thẻ
  thì không tách được số liệu trước và sau, nên một cột `prompt_ref` ở đó là việc nên làm trước
  lần đổi tiếp theo chứ không phải sau.

## 84. Reading: hai lối khoá lại, và bộ chọn lĩnh vực dọn về đúng chỗ (SRC-982, 22.09.2026)

**Vì sao khoá lối "theo dạng đề".** Hai chiều cắt cùng một kho bài, nhưng chúng không ngang giá
với người mới: chia theo NĂNG LỰC nói cho learner biết mình yếu ở đâu, còn chia theo DẠNG ĐỀ chỉ
có nghĩa khi đã biết mình yếu ở đâu rồi. Mở cả hai lối từ ngày đầu là mời họ đi lối thứ hai trước,
rồi luyện hai mươi bài matching mà không hiểu vì sao mình sai.

Điều kiện mở: **mọi** năng lực nhỏ đã có bậc (`level !== null`), tức là đã làm đủ câu để hệ thống
xếp được bậc. Cố ý KHÔNG đòi một bậc tối thiểu: "học xong" ở đây là đã đi qua, không phải đã giỏi,
và một cổng đòi bậc cao sẽ khoá vĩnh viễn đúng những learner cần đổi cách luyện nhất.

Khoá **hiện ra** chứ không giấu, và nói rõ còn thiếu mấy ("3 of 10 sub-skills done"). Một lối đi
biến mất thì learner không biết nó tồn tại; một lối đi xám kèm điều kiện thì nó thành một cái
đích. Cổng đứng ở CẢ HAI chỗ - cái nút và chính trang `TaskTypesPage` - vì một lối khoá bằng cách
bỏ nút đi thì vẫn mở với bất kỳ ai gõ URL hoặc còn giữ một đường dẫn đã lưu.

**Word list** khoá cùng điều kiện: học thuộc từ rời trước khi gặp chúng trong bài đọc là cách học
ngược, và chủ dự án khoá cả hai lối trong cùng một chỉ đạo.

**Bộ chọn lĩnh vực** rời header về ngay trên danh sách cụm. Chip cũ theo learner qua mọi trang, kể
cả những trang mà lĩnh vực không đổi được gì - ở đó nó chỉ là một nút gây tò mò dẫn ra khỏi việc
đang làm. Bộ chọn mới có cả nhãn "All fields": bỏ lọc phải dễ y như đặt lọc, nếu không learner
chọn nhầm một lĩnh vực là mắc kẹt trong đó mà không biết đường ra. Trang `/ielts/domain` vẫn còn
cho những đường dẫn đã lưu.

## 91. Đáp án đúng không còn là đáp án dài nhất (SRC-990, 22.09.2026)

Chủ dự án chụp màn hình một bài Main Idea và chỉ ra một dấu hiệu chạy suốt kho bài: đáp án đúng
gần như luôn là lựa chọn dài nhất. Đo lại trên toàn bộ `content/ielts-micro/` thì đúng như vậy:
**621/1000 câu** có đáp án đúng là lựa chọn dài nhất một cách duy nhất, trong khi mức ngẫu nhiên
của bộ bốn lựa chọn là 25%. Có những file gần như tuyệt đối: `writing/organisation` 100%,
`writing/referencing` 100%, `writing/support-ideas` 100%, `reading/main-idea` 96%,
`reading/summary` 95%.

**Vì sao đây là hỏng chứ không phải chuyện nhỏ về hình thức.** Một learner đếm chữ rồi chọn câu
dài nhất sẽ đúng hai phần ba số câu mà không đọc đoạn văn. Bài đo khi ấy không còn đo kỹ năng
đọc: nó đo khả năng nhận ra thói quen soạn đề. Tệ hơn, thói quen ấy KHÔNG có trong phòng thi
IELTS thật, nên learner mang chiến thuật này tới ngày thi và mất điểm ở đúng chỗ họ tưởng mình
mạnh. Một chỉ báo sai còn hại hơn không có chỉ báo nào.

**Nguyên nhân là ở bản chất của đáp án đúng, không phải ở sự cẩu thả.** Với Main Idea hay
Summary, đáp án đúng phải ôm cả đoạn, nên nó tự nhiên dài hơn ba lựa chọn nhiễu vốn chỉ đỡ một
chi tiết. Vì vậy cách sửa KHÔNG phải rút ngắn đáp án đúng - làm thế là phá chính cái đúng của nó -
mà là viết lại các lựa chọn nhiễu cho đủ dài, đúng như đề IELTS thật vẫn làm: nhiễu là một câu
hoàn chỉnh, đủ sức nặng, sai ở NỘI DUNG chứ không sai ở kích cỡ.

Đã viết lại **khoảng 300 lựa chọn nhiễu** trên 24 file. Mỗi lần sửa chỉ chạm vào nhiễu, không
chạm vào đáp án đúng (script áp bản vá từ chối mọi thay đổi vào chỉ số `answer`), và nội dung sai
của nhiễu được giữ nguyên - nó chỉ được viết thành câu đầy đủ hơn.

**Cổng `check-option-length-cue`** (`scripts/check-option-length-cue.mjs`, nằm trong
`npm run check:code`) giữ chỗ này: mỗi file phải có tỉ lệ "đáp án đúng là lựa chọn dài nhất duy
nhất" không quá 40%, và tỉ lệ "ngắn nhất duy nhất" cũng không quá 40% - vì một kho bài mà đáp án
đúng không bao giờ dài nhất thì cũng là một dấu hiệu đoán được, chỉ đảo chiều. Cổng bỏ qua hai
loại câu mà độ dài không mang tin: bộ lựa chọn ngắn dưới 24 ký tự (một con số, một từ, một liên
từ như `On the other hand`) và file có dưới mười câu chấm được.

Hiện trạng sau khi sửa: **30% trên 689 câu chấm được**, không file nào quá ngưỡng.

Nội dung chạy trên production nằm trong D1 chứ không phải trong repo, nên bản vá này đi kèm
migration `0280_reload_ielts_micro_content_after_option_length_rebalance.sql`, sinh bằng
`node scripts/micro-seed.mjs --all`. Migration là `INSERT OR REPLACE` trên id cố định nên chạy lại
bao nhiêu lần cũng được.

## 117. Sub-skill trước, task type sau (SRC-1050, 25.09.2026)

Chủ dự án: *"Cần giúp learners hiểu rằng hệ thống đang ưu tiên để nâng sub-skills, chứ mình chưa
quan tâm lắm tới task-types... Cần nói về việc này ở vài chỗ."* Bốn chỗ, tiếng Việt (chữ giải
thích, SRC-953): dưới "Where am I strong?" ở trang kỹ năng; dưới tên sub-skill; trang task type
đang khoá (lý do đầy đủ); và cụm "Vì sao luyện sub-skill trước?" trong popover hướng dẫn.

## 120. Bù câu "Not Given" cho các bài T/F/NG và Y/N/NG (SRC-1056, 25.09.2026)

Trả món nợ đã ghi ở §113: lượt nạp kho 0294 không có câu mới nào đáp án Not Given, vì bộ kiểm độ dài
chặn nhãn dài nhất. Migration 0298 thêm 2 câu Not Given cho mỗi bài trong 8 bài (reading.detail.l3.3,
evidence.l3.3, evidence.l3.4, inference.l5.3, inference.l5.4, summary.l5.3, tone.l3.3, tone.l5.3): kho mỗi bài
từ 1-2 lên 3-4 câu Not Given trên 10, gần tỉ lệ đề thật. Mỗi câu nói về một điều đoạn văn KHÔNG nhắc tới
(không phải điều nó nói ngược lại), nên Not Given là đáp án duy nhất; why_vi nói rõ vì sao không phải
True/False. Luật SRC-990 về độ dài đáp án không áp dụng cho nhãn cố định, vì nhãn giống nhau ở mọi câu.

## 138. Viết lại lựa chọn sai trong kho đọc/nghe (SRC-1126, 29.09.2026)

P0 nội dung của lượt audit 2 (§132): đáp án đúng thường là lựa chọn dài nhất, nên learner đoán được bằng độ dài.
Chủ dự án cho làm ngày 29.09.2026.

* **Nguồn**: nội dung chỉ nằm trong D1 (`ielts_content_questions`), không có file nguồn trong repo. Xuất bằng
  `seed-data.yml` + `scripts/noop-readonly.sql` (câu `verify` chỉ đọc): 2.560 câu trắc nghiệm (Reading 1.920,
  Listening 640); trong 2.230 câu có lựa chọn đủ dài để độ dài thành gợi ý, **1.990 câu** có đáp án là lựa chọn dài
  nhất (Reading 89%, Listening 74%, thước đo cùng luật với `check-option-length-cue`).
* **Cách sửa**: chỉ viết lại LỰA CHỌN SAI thành một ý sai cụ thể về nội dung (sai con số, sai người, sai nguyên
  nhân, khái quát quá). Chữ và vị trí đáp án đúng giữ nguyên, nên lời giải `why` vẫn khớp. Mười agent viết song
  song; mỗi lô qua một script kiểm: đáp án không đổi, không còn dài nhất, không trùng lựa chọn, không gạch dài,
  và (sau khi một lô đầu làm sai) không có câu đệm tự tố cáo kiểu "which is not what the speaker says" hay đuôi
  câu lặp lại.
* **Cân tỉ lệ**: sửa hết thì lựa chọn dài nhất gần như luôn SAI, tức một mẹo ngược. Giữ bản gốc cho 25% số câu,
  chọn ngẫu nhiên, để tỉ lệ đáp án là lựa chọn dài nhất về **25%** (Reading 26%, Listening 22%), đúng mức đoán
  bừa với bốn lựa chọn. Còn một gợi ý yếu: vài lựa chọn sai mới dài hẳn hơn phần còn lại.
* **Nạp**: `scripts/seed-ielts-option-rebalance-2026-09-29.sql`, 1.432 câu UPDATE, mỗi câu gác bằng
  `AND options_json = <bản đã xuất>` để không đè câu admin sửa sau ngày xuất và chạy lại không đổi gì.

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

### Từ mục 25. Dap an thoi luon dung dau, va file JSON noi dung bi go han (SRC-882, 2026-09-19)

Chi dao chu du an: *"sua luon 16 bai cu di, va dua vao D1 Cloudflare database di, dung de o JSON
nua"*.

Dot 2 va dot 3 giu ban da gui di o `scripts/content/ielts-listening-batch*.json`, lap luan luc ay
la "ban DA NAP, cung loai voi moi scripts/seed-\*.sql". Chu du an bac lap luan ay, va bac dung: hai
ban cua cung mot noi dung thi mot ban se cu. Ngay co nguoi sua mot cau o man admin, file JSON thanh
sai ma khong co dau hieu nao bao la no sai.

### Từ mục 32. Sub-skills của phần Viết, xếp theo bốn tiêu chí chấm (SRC-896, 2026-09-20)

Chỉ đạo chủ dự án 2026-09-20, kèm ảnh chụp thẻ "Where am I strong?" của trang Writing: dùng đúng
bộ sub-skills dưới đây, **chia theo bốn cụm**. Và ngay sau đó: *"Điều chỉnh lại các tasks để mỗi
sub-skills đều có cái để learners có thể học và luyện tập."*

### Từ mục 35. Reading có HAI chiều, và chúng cắt cùng một kho bài (SRC-895, 2026-09-20)

Chỉ đạo chủ dự án 2026-09-20: *"Cần chia lại theo 2 chiều sau, nghĩa là learner có thể lọc theo cả sub-skills lẫn theo task types để luyện tập, và cần tạo đủ mỗi sub skill cần có 5 tasks, và mỗi task types cũng vậy."* Kèm sơ đồ mười năng lực và mười hai dạng câu hỏi.

### Từ mục 38. Gắn thẻ năng lực nhỏ cho bài viết, ở mức câu hỏi (SRC-913, 2026-09-20)

Chỉ đạo chủ dự án 2026-09-20: *"gắn thẻ sub-skill cho kho bài Writing ở mức câu hỏi"* — đóng đúng
cái lỗ mà §32e vừa ghi ra.

Chỉ đạo chủ dự án 2026-09-20, ngay sau khi phần Viết xong: *"gắn thẻ cho Speaking luôn"*.

### Từ mục 84. Reading: hai lối khoá lại, và bộ chọn lĩnh vực dọn về đúng chỗ (SRC-982, 22.09.2026)

Chỉ đạo: *"Không cho Learners vào Practice by Task Types. Cái đó sau khi học xong hết cả 10 phần
trên thì mới được Unlock Practice By Task Type. Word List cũng bị locked lại. Tính năng chọn field
hay domain, cần đưa vào bên trong của Reading Clusters."*
