---
url: https://docs.nemo12.com/architecture/sdd-038/content-bank.md
description: >-
  Nội dung luyện chuyển từ JSON về D1, màn soạn nội dung, kho từ, tiêu chuẩn đủ
  đề, kho câu bốc ngẫu nhiên, trang lý thuyết
---

# SDD-038 · Kho nội dung trong D1

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

## 14. Kho từ nói về CỤM, không còn là một cuốn từ điển (SRC-863, 2026-09-19)

`/ielts/lexicon` trước đây là họ từ của lĩnh vực đang chọn, ba bậc, mỗi thẻ mở ra collocation và ví
dụ (SRC-784). Một từ điển trả lời "từ này còn dạng nào" — câu hỏi chỉ nảy ra khi learner đang viết.
Câu được hỏi ở đây khác hẳn: "tôi đã học được bao nhiêu rồi", và một danh sách bốn mươi họ từ không
trả lời được nó.

**Ba con số một cụm** (`workers/api/src/modules/ieltsSkills/lexicon.ts`):

| | Nghĩa | Vì sao tách ra |
| --- | --- | --- |
| `total` | cụm dạy bao nhiêu từ | mẫu số, không phụ thuộc learner |
| `met` | đã gặp | |
| `secured` | đã gặp LẠI ở một bài khác | gộp vào `met` là khen learner vì lướt nhiều bài — đúng cách quên nhanh nhất |

Thanh tiến độ vì thế có hai lớp, không phải một phần trăm.

**"So với bảy ngày trước" tính bằng cách chạy lại phép đếm trên quá khứ**, không lưu ảnh chụp hằng
ngày. Cách này luôn cùng mẫu số với con số hôm nay, kể cả khi cụm được bổ sung từ về sau — còn một
ảnh chụp cũ thì đóng băng một mẫu số đã đổi, và hai con số cạnh nhau sẽ tính trên hai kho từ khác
nhau mà không ai nhìn thấy. Dòng "+6 this week" chỉ hiện khi dương: không có "+0", không có mũi tên
đi xuống (cùng luật với §8).

**Trang một cụm** (`/ielts/lexicon/{cluster}`) là MỘT danh sách phẳng: từ, nghĩa, và một chấm nói đã
gặp chưa (● đã chắc · ○ mới gặp một lần · trống là chưa). Bỏ ba khối bậc có câu dẫn riêng, bỏ thẻ mở
ra đóng vào, bỏ hộp collocation. Thứ tự KHÔNG xếp theo trạng thái: xếp theo trạng thái nghĩa là mỗi
lần learner học thêm một từ thì cả danh sách nhảy chỗ.

Endpoint `GET /v1/learners/{id}/ielts-vocabulary` trả thêm `done_refs` (những bài của cụm đã làm) để
app tự đánh dấu từng từ từ bảng nghĩa có sẵn trong bundle — thay vì gửi về hàng nghìn từ mỗi lần mở
trang.

**Nợ để lại, nói rõ ở đây**: dữ liệu họ từ (`data/lexicon/*.json`, ba bậc + collocation + ví dụ) nay
KHÔNG còn màn hình nào đọc tới. Chưa xoá vì nó là nội dung đã soạn và còn dùng được cho một trang
"tra từ" về sau; chủ dự án quyết gộp lại hay bỏ.

## 17. Nội dung luyện rời file JSON, về D1 (SRC-868, 2026-09-19)

**Đánh đổi phải nói thẳng**: app nay phụ thuộc mạng ở những trang trước đây mở là có ngay. Mọi trang
nội dung vì thế phải có trạng thái chờ và trạng thái hỏng thật sự dùng được — một trang trắng khi
API chậm là bước lùi so với bản JSON.

**Schema** (migration 0255): một bảng `ielts_content_items` cho CẢ BA loại bài (passage, clip,
warmup) vì chúng chỉ khác nhau ở phần thân; `ielts_content_clusters` cho cụm; `ielts_content_questions`
tách riêng vì `sub_skill` của từng câu là thứ thật sự được truy vấn (thẻ Mastery ở §13 đếm theo đúng
cột ấy). Mảng luôn-đọc-trọn-gói (`paragraphs`, `options`, `glossary`) nằm trong cột JSON: tách ra
bảng con là thêm ba phép JOIN cho mọi lượt mở bài, đổi lấy một khả năng truy vấn không ai cần.

Số đã nạp: 16 cụm · 71 bài (52 đọc, 16 nghe, 3 khởi động) · 1729 câu hỏi · 200 họ từ của 8 lĩnh vực.

**Chặng 2 — đường đọc công khai** (`modules/ieltsContent`): bốn endpoint, đều public có chủ ý (tầng
luyện vốn mở cho người chưa đăng nhập, SRC-792, và dữ liệu trả về không có dòng nào của learner).
Một lượt gọi trả CẢ CỤM, vì learner đi từ cụm vào bài trong vài giây — đó là cách giành lại phần lớn
thứ mất khi rời bundle. Ngược lại, danh sách cụm cố ý KHÔNG mang theo bài.

**Chặng 3 — app đọc qua API** (`data/store.ts`): ba lớp để lượt chờ chỉ xảy ra một lần — nhớ trong
bộ nhớ tiến trình, gộp các lượt gọi trùng, và cả cụm về trong một lượt. Mọi trang nội dung nay có ba
nhánh: đang tải, hỏng (kèm nút thử lại), có dữ liệu. Ba nhánh ấy KHÔNG gộp được với nhánh "gõ sai
địa chỉ": gộp lại là báo với learner rằng bài không tồn tại trong khi chỉ có mạng vừa chập.

Phần đọc thẳng file JSON dồn vào `data/bundled.ts` và chỉ còn TEST với cổng kiểm dùng — chúng kiểm
chính các file soạn thảo, nên chúng phải đọc file chứ không đọc API: một phép kiểm đi hỏi API là
kiểm bản đã nạp, tức là bắt lỗi sau khi lỗi đã lên production.

E2E cũng đọc từ `content/` qua một lớp chặn trong `e2e/fixtures.ts`. Để test đi thẳng ra
api.nemo12.com thì vừa phụ thuộc mạng của máy chạy CI, vừa kiểm dữ liệu ĐANG CHẠY trên production
thay vì dữ liệu của chính commit đang kiểm.

**Chặng 4 — worker cũng rời JSON** (`ieltsContent/read.ts`). Trước đây `ieltsPractice/content.ts`
nêu tên TỪNG file JSON, vì esbuild của wrangler gói theo đồ thị import tĩnh và worker không có
`import.meta.glob`: thêm một bài mới phải sửa hai chỗ, quên chỗ thứ hai thì endpoint đọc thành tiếng
im lặng không biết bài ấy tồn tại. Nay cả hai file nội dung của worker biến mất cùng cái bẫy ấy.

Phép đếm vốn từ vẫn là hàm THUẦN: worker đọc một bảng tra cho cả kỹ năng trong một lượt rồi tra đồng
bộ, thay vì hỏi D1 cho từng bài (bảy chục round-trip cho một lần mở trang).

`itemsFor` trong `catalog.ts` nay chỉ còn Nói và Viết. Đó không phải thiếu nhất quán: "bài" của hai
kỹ năng ấy là một ĐỀ trong `PRODUCTION_LADDER`, nằm trong mã vì nó là KHUÔN của tính năng chứ không
phải nội dung người soạn sửa hằng tuần — cùng lẽ với năm bậc ở `levels.ts`.

**Đổi luôn một cổng kiểm.** Cổng nội dung cũ từng bắt "bài này đã được nêu trong file worker chưa";
phép ấy hết nghĩa. Nó đổi thành phép so file seed với `content/`, rồi tới §19 thì cả hai bên của phép
so ấy đều biến mất và nó thành `scripts/audit-ielts-content.mjs`.

## 18. Màn soạn nội dung, và chiều ngược từ D1 về repo (SRC-870, 2026-09-19)

`admin.nemo12.com` → tab **Nội dung IELTS**: cây cụm/bài bên trái, bài bên phải; sửa tên bài, đoạn
văn, bảng nghĩa, từng câu hỏi, và bật tắt nháp/phát hành. Lưu khi RỜI Ô và chỉ gửi ô vừa đổi — cùng
luật với `MicroSkills` và `Courses`: gửi lại cả bài là hai người sửa hai ô khác nhau cùng lúc thì
người lưu sau xoá mất ô của người kia.

**Luật nội dung chuyển về server** (`ieltsContent/validate.ts`): từ trong bảng nghĩa phải xuất hiện
nguyên văn trong bài, đáp án phải trỏ vào một lựa chọn có thật, không em dash. Khi nội dung còn trong
repo, ba luật ấy sống ở cổng CI vì mọi lần sửa đều đi qua một commit. Từ nay có đường sửa KHÔNG qua
commit nào, nên chúng phải chạy trước khi ghi. Chúng kiểm trên trạng thái SAU KHI GHÉP: rút ngắn mảng
lựa chọn có thể làm đáp án cũ trỏ ra ngoài, và lúc ấy phần vừa gửi thì hợp lệ còn câu hỏi thì hỏng.

**Hai thứ phải sửa theo, và cả hai là bẫy thật:**

1. **File seed không còn xoá trước khi nạp.** Bản đầu có bốn câu `DELETE`, đúng khi repo là bản
   chính. Từ khi có màn admin, chính bốn câu ấy là thứ nguy hiểm nhất trong file: một lượt chạy lại
   xoá sạch mọi bài vừa soạn, và workflow seed không có nút hoàn tác. Gỡ một bài khỏi learner nay là
   đổi trạng thái sang `draft`.
2. **Có chiều ngược**: một script kéo bản đang chạy từ đường công khai về `content/`, rồi bộ sinh
   seed dựng lại file seed từ đó. Sau một lượt như vậy, `git diff` chính là bản ghi những gì đã sửa
   từ màn admin. §19 xoá cả hai script này cùng các file chúng ghi vào, nên đường review ấy hiện
   KHÔNG còn — đó là cái mất đã ghi rõ ở cuối §19.

Vì diff của lần kéo ấy là bản ghi duy nhất, thứ tự trường trong `shape.ts` khớp đúng thứ tự trong
file soạn thảo — lệch thứ tự thì mọi file đều "đổi" và bản ghi vô dụng.

**Một lỗi của SRC-868 lộ ra nhờ chính chiều ngược này**: bộ sinh seed bỏ quên bảng nghĩa của bài
KHỞI ĐỘNG, mà trang Warmup đọc thẳng `item.glossary`. Đã vá cả ba tầng (bộ sinh, `shape.ts`, và một
lớp đỡ ở app để một trường thiếu không làm trắng cả trang), và nạp lại dữ liệu.

**Số phận các file JSON**: xem §19 — chúng đã bị xoá.

## 19. Xoá hẳn file JSON: D1 là bản duy nhất (SRC-875, 2026-09-19)

`content/ielts-reading`, `content/ielts-listening`, `content/ielts-warmup` và
`apps/learn/src/ielts/data/lexicon/*.json` đã xoá. Bộ sinh file seed, chính file seed, và script
export cũng đi theo: cả ba chỉ tồn tại để nối repo với D1, mà nay repo không còn giữ nội dung nào.

**Ba thứ dựa vào các file ấy, và chỗ đứng mới của chúng:**

| Thứ cũ | Nay |
| --- | --- |
| `clusterVocab.ts` — kho từ 50 của cụm, nêu tên tám cụm bằng tay | đọc `vocabulary_json` của cụm ĐỌC cùng slug, ngay trong `loadGlossaries` |
| `data/bundled.ts` + `e2e/fixtures.ts` đọc `content/` | một file `apps/learn/e2e/content.fixture.json` đã commit, sinh bằng `scripts/gen-ielts-test-fixture.mjs` |
| `check-ielts-content.mjs` trong `npm run check:code` | `scripts/audit-ielts-content.mjs`, chạy tay: `npm run audit:ielts-content` |

**Vì sao phép rà không còn chắn CI.** Thứ đáng rà nay nằm sau một lượt gọi mạng, và một cổng phụ
thuộc mạng mà đứng chắn deploy thì đến ngày mạng chập là mọi phiên đứng — cùng lý do
`check:ui-latest` cố ý không chắn CI (DS-001 §0). Phần gác thường trực đã chuyển vào chính lúc ghi
(`ieltsContent/validate.ts`), nơi nó chặn TRƯỚC khi nội dung hỏng kịp tới learner; phép rà là lưới
thứ hai, chạy sau một đợt soạn.

**Fixture của test không phải nguồn nội dung.** Nó là một bản chụp để test có thứ mà dựng màn hình,
giữ mọi cụm nhưng mỗi cụm chỉ ba bài đầu. Test vẫn không gọi ra mạng: đi thẳng api.nemo12.com thì
vừa phụ thuộc mạng của máy chạy CI, vừa kiểm dữ liệu đang chạy trên production thay vì dữ liệu của
chính commit đang kiểm.

**Cái mất, nói thẳng.** Repo không còn bản sao nội dung nào, nên:

* Một lần sửa từ màn admin KHÔNG để lại diff nào để review. Chiều ngược (`export-ielts-content.mjs`)
  đã xoá cùng các file nó ghi vào.
* Khôi phục khi mất dữ liệu nay trông vào Time Travel của chính D1 (Cloudflare giữ 30 ngày), không
  còn một file seed trong repo để nạp lại.

Hai điều này là hệ quả trực tiếp của chỉ đạo, không phải sơ suất. Muốn lấy lại đường review thì
cách rẻ nhất là một lượt chạy định kỳ ghi bản chụp vào một kho riêng — chưa dựng, và chưa ai yêu
cầu.

## 26. Ba ky nang con lai du 20 de (SRC-888, 2026-09-20)

### 26a. Reading: 12 bai moi, khong dong den code

Mot de Doc la BA bai cung mot cum, nen so de bang tong `floor(so bai / 3)` cua tung cum. Bon cum
day (10 bai) cho 3 de moi cum; bon cum mong (3 bai) chi cho 1 de. Dot nay them 3 bai cho moi cum
mong: 8 cum, 64 bai, va dung 20 de. Khong co dong code nao doi - cong thuc ghep de von da dung.

Moi bai giu nguyen rang buoc cu: 5 doan chung 400 tu, 30 cau hoi chia dung 10 nhom moi nhom 3 cau,
20 muc tu va muc nao cung phai XUAT HIEN NGUYEN VAN trong bai (trang to tu bang cach do chuoi).

### 26b. Writing va Speaking: de la NOI DUNG, nen phai co cho chua

Hai ky nang nay khong ghep de tu bai nao ca. Mot de Writing LA hai de bai, mot de Speaking LA ba
chum cau hoi. Truoc dot nay chung nam trong `PRODUCTION_LADDER` - mot mang trong ma nguon worker,
dung cho ma muc 25b vua chot la khong duoc de noi dung nua: sua mot de bai phai sua code, deploy
lai worker, va man admin khong voi toi.

Migration 0257 dung hai bang: `ielts_exam_papers` va `ielts_exam_parts`. Hai bang chu khong phai
mot cot JSON, vi PHAN moi la thu learner mo ra lam, va man admin sua mot phan thi khong nen ghi lai
ca de.

**Task 1 dung dang TABLE.** Task 1 Academic von mo ta BIEU DO, ma he chua co cho chua hinh. Table
la mot dang Task 1 co that trong ky thi, nen day khong phai do thay the tam - so lieu viet bang
chu trong `body_md`, learner mo ta y het khi mo ta mot bieu do.

### 26c. Mot phan de co URL rieng, va van dung o lam bai cu

Truoc: ca hai phan cua de Writing deu tro ve bac 3, nen bam Task 1 hay Task 2 cung ra dung mot
trang. Nay moi phan mot dia chi: `/ielts/writing/tests/paper-2/1`.

Ten route la `paperPart` chu KHONG phai `examPart`. `examPart` da co nghia khac tu SRC-815 - no la
mot DANG de trong phan thi (`/writing/parts/task-2`), khong phai phan thu n cua mot de cu the. Hai
thu nghe giong nhau toi muc dung chung mot ten la dat bay cho nguoi sua sau.

`PaperPartPage` dung lai dung `WritingBox` va `SpeakingBox` da co. Hai o ay mang theo nhung thu chi
lo ra sau khi co nguoi dung that: ban nhap giu trong localStorage theo ma de, tran do dai bay ra
TRUOC khi lam bai, duong nop va duong nhan xet. Dung mot o thu ba la dung lai ca bon thu ay, va cai
dung lai se thieu dung nhung cho nguoi ta chi nho ra khi da mat bai mot lan.

### 26d. Hai kho de, mot duong nop

Server cham theo `prompt_id`. Thang luyen tra trong ma nguon; de thi tra o D1 qua
`examPartPrompt()`. Duong nop thu kho trong ma truoc vi no khong ton luot doc D1 nao.

De thi cham o BAC 3 (gat nhat, co diem band): mot de dung dang phong thi ma cham o bac 1 thi nhan
xet de dai hon thuc te, va learner mang cai de dai ay vao ngay thi. Nhung SAN DO DAI khong lay theo
bac ma lay tu chinh phan de: Task 1 can 150 tu, Task 2 can 250, hai phan cung bac.

Bang so lieu va cue card DI THEO de toi model cham. Thieu chung, model doc "Describe the table" ma
khong co bang nao, roi che learner bia so lieu.

## 49. Khu luyện đề: thẻ một dòng, gom theo lĩnh vực, có dấu đã làm (SRC-926, 20.09.2026)

### 49a. Thông tin không phân biệt được thẻ này với thẻ kia thì không giúp chọn

Mọi đề Đọc đều 60 phút và ba phần, và câu "Three passages from one field..." lặp nguyên văn trên mọi thẻ. Ba dòng ấy bắt **tên đề** - thứ duy nhất learner chọn theo - cạnh tranh chỗ đứng với hai dòng nói cùng một điều. Số phút và số phần không mất: chúng ở trang đề, nơi learner sắp bắt đầu và lúc ấy mới cần biết mình cam kết bao lâu.

### 49b. Gom theo lĩnh vực thay cho bộ lọc độ khó

Thang Level 1-5 (SRC-850) là thang của **bài tập năng lực**: nó nói ngôn ngữ và độ dài đoạn. Khu luyện đề thì mọi đề đều ở độ khó phòng thi - đó là định nghĩa của nó, nên một bộ lọc năm bậc ở đây sẽ có bốn bậc rỗng và learner bấm vào Level 2 thấy trống sẽ kết luận sai rằng nội dung còn thiếu.

Thay vào đó, thẻ gom theo **lĩnh vực** của cụm, tên nhóm in một lần, và tên thẻ rút còn "Paper 1". Nhóm giữ thứ tự xuất hiện chứ không xếp theo bảng chữ cái, nên các vỏ đề chưa mở tự rơi xuống cuối thành một nhóm riêng thay vì rải ô xám vào mọi nhóm.

### 49c. Đã làm / đang dở, đọc từ sổ công sức

`GET /v1/learners/{id}/ielts-paper-parts?skill=` trả về `ref` thô của những bài learner đã mở (`<cụm>:<số bài>`), chỉ tính lượt có đo thời gian - `useTimeOnTask` bỏ qua mọi lượt dưới 20 giây, nên bấm nhầm rồi quay ra không làm một đề sáng lên.

Máy chủ **không** bóc ref thành "đề số mấy": phép ghép ba passage hoặc bốn clip thành một đề sống ở `data/examTests.ts` và chỉ ở đó. Bóc hộ ở phía máy chủ là chép luật ghép đề sang chỗ thứ hai, rồi hai bên lệch nhau vào ngày đổi luật.

Ba trạng thái dùng đúng ngôn ngữ hình của SRC-919: đang dở (viền coral đặc, kèm "2/3 parts") nổi hơn đã xong (coral nhạt, mang dấu tích - xem §52, nơi cả hai khu dùng chung một bộ dấu). Viết và Nói chưa có địa chỉ riêng cho từng phần nên không mang nhãn nào - một nhãn ở đó sẽ nói về thứ khác với thứ learner nghĩ.

## 50. Tiêu chuẩn "đủ đề để luyện", và đợt một làm cho đạt (SRC-930, 20.09.2026)

### 50.1 Kho đề đo được lúc nhận việc

Đếm trên D1 production, không đếm trên repo - nội dung sống ở D1 từ SRC-875 và màn admin vẫn nạp
thêm, nên mọi con số suy ra từ file seed trong repo đều thấp hơn sự thật:

| Kỹ năng | Bài trong kho | Đề ghép được | Câu một đề | Dạng câu hỏi |
| --- | --- | --- | --- | --- |
| Reading | 64 passage / 8 cụm | 20 | 90 | 1 (multiple choice) |
| Listening | 80 clip / 8 cụm | 20 | 40 (32 choice + 8 gap) | 2 |
| Writing | 20 đề soạn riêng | 20 | Task 1 + Task 2 | 2 |
| Speaking | 20 đề soạn riêng | 20 | Part 1-2-3 | 3 |

Tức là lời hứa "hai mươi đề mỗi kỹ năng" của SRC-862 **đã đạt**. Thứ còn thiếu không phải số lượng.

### 50.2 Tiêu chuẩn: năm điều, điều nào cũng đếm được bằng máy

1. **Khối lượng** - 20 đề mỗi kỹ năng, trong đó 4 đề **niêm phong** làm đề đo. Một learner luyện
   mười tuần thi một đề mỗi tuần; hết đề trước ngày thi thì tuần cuối không còn gì để đo.
2. **Hình dạng đúng đề thật** - Reading 40 câu / 60 phút, Listening 40 câu / 30 phút, Writing
   150 + 250 từ / 60 phút, Speaking Part 1-2-3 / 14 phút.
3. **Phủ dạng câu hỏi** - mỗi dạng có thật của kỳ thi xuất hiện ở ít nhất 3 đề, mỗi đề có ít nhất
   5 dạng.
4. **Phủ ngữ cảnh theo luật của từng kỹ năng** - Listening bốn part xen kẽ hội thoại và độc thoại;
   Reading ba passage cùng lĩnh vực, tăng dần độ khó.
5. **Làm xong là có điểm** - một đồng hồ cho cả đề, một phiếu điểm, một band.

Vì sao tiêu chuẩn KHÔNG phải một con số: một kho hai mươi đề mà đề nào cũng trắc nghiệm thì learner
luyện hết vẫn chưa từng gặp dạng câu chiếm nhiều điểm nhất của đề thật. Đếm đề là đếm thứ dễ đếm
nhất chứ không phải thứ quyết định.

### 50.3 Đợt một: điều 1, 2, 4, 5

Điều 3 là soạn nội dung mới (hàng trăm câu, nhiều đợt) nên tách ra làm sau; bốn điều còn lại là
việc của mã nguồn và làm trọn trong đợt này.

**Một trang riêng để NGỒI LÀM cả đề** (`ExamSit.tsx`, `/ielts/{kỹ năng}/tests/{đề}/sit`). Đứng
riêng khỏi trang đề vì ba luật của nó ngược hẳn với luyện tập, và cả ba phải đúng cùng lúc thì con
số cuối cùng mới có nghĩa: không chấm câu nào cho tới lúc nộp; một đồng hồ cho cả đề chứ không
phải mỗi phần một cái; đúng số câu của đề thật. Trang đề vẫn giữ các ô mở từng phần ra chỗ luyện -
hai cách mở cùng một đề, và sự khác nhau nói thẳng trên nút.

**Reading về đúng 40 câu** (`examSit.pickQuestions`). Ba passage 90 câu rút thành 14/13/13. Phép
chọn đi vòng tròn qua mười nhóm năng lực chứ không cắt đầu dãy: câu trong bài xếp theo nhóm, nên
`slice(0, 14)` cho ra một đề chỉ hỏi bốn nhóm đầu bảng - và learner yếu Inference vẫn ra band cao.
Phép chọn TẤT ĐỊNH, vì hai lần thi thử phải so được với nhau.

**Listening xen kẽ hội thoại và độc thoại** (`examSit.listeningPapers`). Bốn part lấy từ bốn lĩnh
vực khác nhau như cũ, nhưng nay đúng kiểu: hội thoại, độc thoại, hội thoại, độc thoại. Kho hết
clip đúng kiểu thì DỪNG, không ghép một đề bốn độc thoại cho đủ số - một đề sai hình dạng nằm
trong danh sách tệ hơn một ô khoá, vì ô khoá thì learner biết mà chờ. Kiểu của từng clip đi kèm
danh sách cụm (`clip_kinds`, kèm `seq` chứ không phải một dãy theo thứ tự: một bài nháp giữa cụm
sẽ làm đề trỏ lệch một nấc).

**Phiếu điểm quy band** (`data/band.ts`). Bảng quy đổi chép nguyên, không nội suy: band không
tuyến tính theo số câu đúng, và mọi công thức đều sai ở đâu đó. Đề rút gọn quy về thang 40 trước
khi tra bảng. Viết lấy (Task 1 + 2 x Task 2) / 3 đúng luật chấm thật; Nói lấy trung bình ba part.
Mọi chỗ hiện band đều nói kèm đây là ƯỚC LƯỢNG - bảng thật xê dịch vài câu giữa các kỳ thi.

**Bốn đề cuối niêm phong** (`examSit.isSealed`). Tính theo vị trí cuối dãy chứ không đánh dấu tay
từng đề: kho đề còn dài ra theo từng đợt soạn, và một danh sách id viết tay sẽ trỏ vào giữa dãy
ngay đợt sau. Kho dưới bốn đề thì không niêm phong đề nào - niêm phong hết kho là khoá learner ra
ngoài chính thứ họ vào đây để làm.

**Viết và Nói đi đường khác, và nói thẳng vì sao.** Hai kỹ năng ấy không có đáp án để đối chiếu:
mỗi task nộp lên và model chấm. Nên trang ngồi làm đề giữ ĐỒNG HỒ CỦA CẢ ĐỀ (thứ đang thiếu), còn
việc chấm vẫn ở trang của từng phần, và phiếu điểm cộng lại từ band server đã ghi -
`GET /v1/learners/{id}/ielts-paper-bands?skill=`. Đường ấy trả **lượt mới nhất** của mỗi phần chứ
không phải lượt cao nhất: ghép những lần làm tốt nhất của từng phần ở những ngày khác nhau ra một
điểm tổng là dựng một con số không mô tả buổi thi nào, mà nó lại trông y như một band thật.

### 50.4 Cái chưa có, nói thẳng

* **Điều 3 chưa đạt**: Reading vẫn 100% trắc nghiệm. Matching Headings, True/False/Not Given,
  Summary completion và các dạng còn lại là nội dung phải soạn, và đó là đợt tiếp theo.
* **Kho clip Nghe thiếu một hội thoại**: 39 hội thoại / 41 độc thoại, mà 20 đề cần 40/40. Đề cuối
  vì vậy rơi về ô khoá cho tới khi có thêm một clip hội thoại.
* **Hết giờ thì nộp, không khoá màn hình**: đúng như phòng thi, và cũng để một tab bị treo không
  xoá trắng công một tiếng của learner.

## 82. Trang lý thuyết: tên tiếng Anh dễ, và đưa ra từng mẩu (SRC-980, 22.09.2026)

Tên trang: "Cách làm" → **"How to do this"**. Các nhãn mục cũng vậy: "Step by step", "One worked
example", "Traps to watch for".

**Vì sao đưa ra từng mẩu.** Trước bản này cả bốn bước, ví dụ và các bẫy đổ ra cùng lúc - khoảng ba
trăm chữ trong một màn. Với một learner vừa làm sai một bài thì đó là một bức tường, và bức tường
thì người ta cuộn qua chứ không đọc.

Hai điều giữ lại có chủ ý: mẩu đã mở thì **ở lại** chứ không bị thay, vì bước 3 chỉ có nghĩa khi
còn nhìn thấy bước 2 ngay trên nó - thứ được kiểm soát là tốc độ đưa ra, không phải trí nhớ của
learner. Và nút Next **đếm số** ("Next step · 2 of 4"): một nút Next trơ trọi không nói được còn
bao nhiêu, và không biết còn bao nhiêu là lý do người ta bỏ giữa chừng.

Riêng ví dụ có giải thì cắt đúng một chỗ: đề trước, chờ, rồi mới tới lời giải. Đọc lời giải cùng
lúc với đề thì learner không bao giờ tự nghĩ, và phần này mất đúng chỗ đáng giá nhất của nó.

## 109. Nút Report trong mỗi bài luyện (SRC-1029, 24.09.2026)

* **Màn hình** (`ielts/ReportIssue.tsx`): lá cờ nhỏ "Report" dưới cùng bài, bấm mở hộp thoại tick
  được nhiều lý do (audio chỉ hiện ở Listening), thêm ghi chú tuỳ chọn, gửi xong cảm ơn và learner
  ở nguyên bài. Báo cáo gắn với ĐÚNG câu đang hiện.
* **Server**: `POST /v1/learners/{id}/ielts-reports` ghi vào `content_reports` và
  `content_review_queue` (target_type `ielts_exercise`, target_id `<skill>.<code>.l<level>.<seq>#<item>`),
  cùng hai sổ mà báo cáo câu hỏi của learn vẫn dùng, nên Coral thấy ngay. Không cần migration.

## 110. Kho câu lớn hơn lượt làm, bốc ngẫu nhiên, né câu bị báo (SRC-1031, 24.09.2026)

* **Hai con số tách ra** (migration 0290): `ielts_micro_exercises.show_count` là số câu MỘT lượt,
  điền sẵn bằng số câu đang phát hành. Kho lớn lên (thêm câu), lượt làm giữ độ dài cũ. Thẻ bài nói
  số câu một lượt (`items`), kèm `bank_items` là cỡ kho.
* **Cờ báo lỗi**: bài trả mỗi câu kèm `flagged` = có review `ielts_exercise` còn mở (§109). Review
  đã xử xong thì câu quay lại vòng bốc.
* **Bốc** (`drawItems`, trên máy learner): đơn vị là CẶP ở bậc 2 và 4; ưu tiên câu đã làm trong
  lượt dở, rồi câu sạch ngẫu nhiên, rồi mới tới câu bị báo khi kho không đủ câu sạch. "Try again"
  bốc lượt mới.
* **Nội dung**: phần máy xong trước; ba câu mới cho mỗi bài (khoảng 1.900 câu cho 632 bài) là việc
  soạn nội dung riêng, nạp bằng migration nội dung, không đổi `show_count`.

## 113. Nạp kho câu: +3 câu mỗi bài (SRC-1038, 25.09.2026)

Phần nội dung của §110. Migration 0294 nạp 1.969 câu cho 632 bài (Writing 854, Reading 635,
Speaking 300, Listening 180): bài thường +3, bài cặp +2 cặp. `show_count` giữ nguyên nên lượt làm
không dài ra; bộ khởi động nay kho 6, mỗi lượt 3 (test `warmupShape` đo `show_count`).

Soạn bằng workflow 56 agent (28 soạn, 28 rà độc lập) cộng bộ kiểm máy. Giới hạn đã biết: ở các bài
True/False/Not Given, bộ kiểm độ dài chặn "Not Given" làm đáp án (nhãn ấy dài nhất), nên câu MỚI chỉ
dùng True/False làm đáp án; câu cũ vẫn có Not Given. Cần một ngoại lệ cho nhãn cố định trong luật
SRC-990 rồi soạn bù.

## 131. "Có vẻ thiếu dữ liệu" ở trang How to do this, và audit nội dung (SRC-1092, 27.09.2026)

* **Chấm điểm**: `scripts/audit-content.mjs` (skill `/audit-content`) chấm 1.030 đơn vị ở 6 khu: lý
  thuyết sub-skill (51), bài luyện trong D1 (632, cả bốn kỹ năng), bài luyện nguồn JSON (237), Grammar
  (36), Speak together (11), popover thuật ngữ (63). Kết quả: **100/100 mọi đơn vị**. Dữ liệu KHÔNG thiếu.
* **Thủ phạm là giao diện**: trang hiện TỪNG MỤC MỘT (`OneByOne`): một bước rồi nút "Next step · 1 of
  4", đề ví dụ rồi nút "Show the answer". Kiểm trên Chrome của chủ dự án (chỉ đọc): nút có và hiện rõ,
  nhưng nằm dưới mép màn hình thì trang trông như cụt.
* **Sửa**: `MicroTheory.tsx` hiện ĐỦ các bước, lời giải ví dụ và các bẫy ngay khi qua cổng; bỏ
  `OneByOne`. Ba cổng đọc hiểu (SDD-039) giữ nguyên. Đảo một phần SRC-980 ("đề trước, chờ, rồi lời
  giải"): cái giá của việc trông như thiếu dữ liệu lớn hơn cái lợi của một nhịp chờ.
* Ca e2e mới trong `ieltsMicroSkill.spec.ts`.

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

### Từ mục 14. Kho từ nói về CỤM, không còn là một cuốn từ điển (SRC-863, 2026-09-19)

Chỉ đạo chủ dự án: *"cần xem được danh sách các cụm, và độ mastery với từng cụm. Đại ý là learner
thấy được là 'à, tôi đã học được từng đây, tôi đã tiến bộ hơn so với 7 ngày qua như thế nào'."* Và
với trang chi tiết: *"cần lược bớt rất nhiều element thừa thãi, làm rối loạn cả trang."*

### Từ mục 17. Nội dung luyện rời file JSON, về D1 (SRC-868, 2026-09-19)

Chỉ đạo chủ dự án: *"Chuyển dữ liệu từ JSON vào trong D1 Cloudflare đi, ko dùng JSON nữa."* Và khi
được hỏi app nên đọc từ đâu, chủ dự án chọn **gọi API lúc chạy**, cho cả bốn kho: bài đọc, bài nghe,
kho họ từ, bài khởi động.

**Đây là một lần đảo quyết định cũ**, ghi rõ để người sau khỏi đoán. SRC-783 (2026-09-16) chốt ngược
lại — "Data lưu ra JSON files" — vì lúc ấy tầng luyện là chỗ thử nghiệm, nội dung thay mỗi ngày, và
sửa một file rồi deploy nhanh hơn chạy một migration. Nay nội dung đã ổn định, còn cái giá của JSON
thì hiện rõ: sửa một chữ trong một bài là build lại và deploy lại cả app, và không có màn admin nào
sửa được nội dung mà không qua một commit.

**Nạp** (lúc ấy): một bộ sinh đọc các file JSON rồi xuất một file seed, chạy qua `seed-data.yml`.
Cả bộ sinh lẫn file seed đã XOÁ ở §19 cùng các file JSON — nhắc lại ở đây chỉ để đọc được lịch sử,
đừng đi tìm chúng trong repo.

**Số phận các file JSON**: chúng nay là BẢN SAO để review, không còn là bản chính. Bản chính là D1.

### Từ mục 18. Màn soạn nội dung, và chiều ngược từ D1 về repo (SRC-870, 2026-09-19)

Chỉ đạo chủ dự án: *"Làm màn admin soạn nội dung trước đi"* — trước khi bỏ các file JSON.

### Từ mục 19. Xoá hẳn file JSON: D1 là bản duy nhất (SRC-875, 2026-09-19)

Chỉ đạo chủ dự án: *"xoá file JSON trong content/ đi"*, sau khi màn admin đã chạy.

### Từ mục 26. Ba ky nang con lai du 20 de (SRC-888, 2026-09-20)

Chi dao chu du an: *"gio soan Reading, Writing, Speaking cho du 20 de luon"*.

### Từ mục 49. Khu luyện đề: thẻ một dòng, gom theo lĩnh vực, có dấu đã làm (SRC-926, 20.09.2026)

Chỉ đạo chủ dự án 2026-09-20, trên ảnh chụp `/ielts/reading/tests`: *"Bỏ hết mấy thông tin thừa như là '3 parts' hay câu phụ đề, và '60 mins'."*

### Từ mục 50. Tiêu chuẩn "đủ đề để luyện", và đợt một làm cho đạt (SRC-930, 20.09.2026)

Chủ dự án 2026-09-20, kèm ảnh trang `Reading papers`: *"Như thế nào thì được coi là đủ đề để
luyện? Cho tôi một tiêu chuẩn phù hợp. Rồi làm cho đủ tiêu chuẩn đó, cho cả 4 kỹ năng."*

### Từ mục 82. Trang lý thuyết: tên tiếng Anh dễ, và đưa ra từng mẩu (SRC-980, 22.09.2026)

Chỉ đạo: *"Không hiểu từ 'cách làm' nghĩa là sao... cần dùng từ tiếng Anh mà siêu dễ hiểu, cho học
sinh 10 tuổi vẫn hiểu được. Nội dung... cần trình bày theo kiểu đưa ra từ từ, mỗi lúc chỉ nên là 1
câu... Next, next thì mới tới câu tiếp. Hãy dẫn dắt từng bước như đang dạy trẻ con lớp 5."*

### Từ mục 109. Nút Report trong mỗi bài luyện (SRC-1029, 24.09.2026)

Chủ dự án: *"Trong mỗi bài luyện tập, dưới cùng cần có chỗ để Learner có thể Report. Cần làm hơi
giống như Duolingo tính năng Report."*

### Từ mục 110. Kho câu lớn hơn lượt làm, bốc ngẫu nhiên, né câu bị báo (SRC-1031, 24.09.2026)

Chủ dự án: *"Trong mỗi bài tập cho các sub-skills, cần bổ sung thêm 3 câu nữa, sau đó hệ thống sẽ
lấy ngẫu nhiên ra... Thi thoảng learners sẽ đánh dấu câu này hay câu kia có vấn đề, thì hệ thống sẽ
không ưu tiên lấy những câu đã bị đánh dấu."*

### Từ mục 131. "Có vẻ thiếu dữ liệu" ở trang How to do this, và audit nội dung (SRC-1092, 27.09.2026)

Chủ dự án gửi ảnh "One worked example" chỉ có đề, không lời giải, và "Step by step" chỉ một bước:
"có vẻ bị thiếu dữ liệu", yêu cầu tạo skill audit khu nội dung, chấm điểm, rồi sửa.
