---
url: https://docs.nemo12.com/architecture/sdd-039-evidence-gated-reading.md
description: >-
  Trang nhiều nội dung theo bằng chứng (SDD-039): nội dung tuôn ra chậm, mỗi mẩu
  đổi lấy một động tác, learner trả bằng chứng đọc hiểu.
---

# SDD-039 — Trang nhiều nội dung: tuôn chậm, và phải trả bằng chứng

Chỉ đạo chủ dự án 2026-09-18: *"Cần một tiêu chuẩn chung về 'cách hiển thị các trang có nhiều nội dung', như trang trong ảnh và các trang tương tự. Đó là nội dung tuôn ra thật chậm, thật ít, còn learner cần xác nhận việc đọc và hiểu, bằng cách trả lời câu hỏi True/False, hoặc chọn 1 trong 3 từ, hoặc Quiz cực ngắn, hoặc Điền từ. Đại ý là không cho phép user xem lướt, mà user bắt buộc phải làm nhiều động tác, để hệ thống có evidence rằng 'Learner có xem kỹ, có hiểu'. Hệ thống sẽ lưu lại vết, khi learner làm từng trang (cho dù chỉ đọc và đưa ra evidence như trang này)."*

Tài liệu này là **tiêu chuẩn chung**, không phải mô tả một trang. Nó áp cho mọi trang trong sân IELTS mà nội dung dài hơn một màn hình và việc của learner là ĐỌC: trang phần thi (`/ielts/{skill}/parts/{part}`), trang năng lực nhỏ (`/ielts/skills/...`), trang lý thuyết trong cụm bài, và mọi trang cùng loại sinh ra sau này.

## 1. Vấn đề: một trang đọc không để lại dấu vết nào

Trang `/ielts/speaking/parts/part-2` hiện bày trọn vẹn: mô tả phần thi, ba gạch đầu dòng "phần này đo cái gì", rồi các dạng đề với bố cục từng bước — tất cả xuất hiện cùng lúc, cuộn một mạch tới đáy. Hai hệ quả, và cả hai đều im lặng:

1. **Learner lướt qua trong sáu giây rồi tin rằng mình đã đọc.** Không có chỗ nào trên trang buộc dừng lại, nên thứ rẻ nhất là cuộn tới cuối. Đây không phải lỗi của learner: một trang chữ trên màn hình là lời mời lướt.
2. **Hệ thống không biết gì hết.** `learning_events` chỉ nhận dòng khi learner làm bài; một buổi đọc lý thuyết ba mươi phút để lại đúng con số 0. Nên mọi thứ dựng trên dữ liệu ấy — Learner Model, phút học, dự đoán 200 giờ (SDD-038, SRC-835) — đều bỏ sót đúng phần learner vừa bỏ công.

Điều thứ hai là điều đáng sửa hơn: nó làm hệ nói sai về chính đứa trẻ.

## 2. Nguyên tắc: mỗi mẩu nội dung đổi lấy một động tác

Trang chia thành các **mẩu** (`segment`). Một mẩu là một ý trọn vẹn, ngắn: một đoạn văn, một danh sách ba gạch đầu dòng, một bảng nhỏ. Sau mỗi mẩu (hoặc mỗi cụm 2-3 mẩu khi nội dung quá vụn) là một **cổng** (`checkpoint`) — một động tác nhỏ learner phải làm để mẩu tiếp theo hiện ra.

Bốn loại cổng, và đây là danh sách đóng — thêm loại thứ năm là một quyết định sản phẩm, không phải một lựa chọn lúc soạn trang:

| Loại | Learner làm gì | Dùng khi |
| --- | --- | --- |
| `true_false` | Một mệnh đề về mẩu vừa đọc, chọn Đúng / Sai | Ý vừa đọc có một khẳng định rõ ràng để soi lại |
| `pick_word` | Một câu thiếu một từ, chọn 1 trong 3 | Mẩu vừa đọc đưa ra một thuật ngữ hoặc một phân biệt từ |
| `micro_quiz` | 1-2 câu trắc nghiệm cực ngắn | Mẩu chứa một quy trình hoặc một danh sách có thứ tự |
| `cloze` | Gõ một từ vào chỗ trống | Từ ấy đáng nhớ mặt chữ (thuật ngữ IELTS, cụm cố định) |

**Ba luật ràng buộc mọi cổng:**

* **Cổng hỏi về mẩu VỪA đọc, không hỏi về kiến thức nền.** Cổng là bằng chứng đọc, không phải một bài kiểm tra chen ngang. Một câu hỏi learner trả lời được mà không cần đọc mẩu vừa rồi thì nó không đo gì cả.
* **Sai không phải là cửa đóng.** Sai thì hiện lại đúng chỗ trong mẩu vừa đọc, kèm một câu nói vì sao, rồi cho trả lời lại. Cổng đo *có đọc kỹ không*, và một learner đọc lại rồi trả lời đúng chính là đã làm đúng việc phải làm. Chặn cứng sau n lần sai chỉ dạy được một điều: đoán cho nhanh để qua.
* **Không đếm ngược, không tính giờ.** Đây là trang đọc. Một đồng hồ chạy biến việc đọc kỹ thành việc phải vội.

## 3. Nội dung hiện ra thế nào

* Mở trang: hiện **mẩu đầu tiên** cộng phần tiêu đề. Không hiện bóng mờ của phần dưới, không thanh cuộn dài tới đáy — learner không cần biết còn bao nhiêu chữ nữa, họ cần biết còn bao nhiêu **bước** (xem dưới).
* Qua một cổng: mẩu tiếp theo hiện ra bằng một chuyển động ngắn của `motion` (preset trong `packages/design-system/ui/Motion.tsx`, DS-001 §0 — không kéo gói thứ tư về), và trang cuộn tới đầu mẩu mới, không cuộn tới cổng.
* **Luôn có một thước tiến độ đếm BƯỚC, không đếm phần trăm chữ**: "3 / 7". Learner biết mình đang ở đâu trong một việc có kết thúc.
* **Quay lại thì mọi mẩu đã mở vẫn mở.** Trang đã đi qua không bao giờ đóng lại; đọc lại toàn bộ là quyền của learner, và một trang tự gập lại sau khi đã mở là trang cướp mất thứ learner vừa trả tiền bằng công sức để có.
* **Máy tìm kiếm và người chưa đăng nhập đọc được TOÀN BỘ.** Cổng là một lớp của trải nghiệm học, không phải một lớp che nội dung: 140 trang nội dung IELTS đang là nguồn SEO chính (SRC-694, SRC-829) và chúng phải ở lại trong HTML tĩnh. Với khách chưa đăng nhập, trang hiện thẳng, không cổng, không ghi vết — không có learner nào để ghi vết cho.

## 4. Vết để lại: mỗi trang một dòng, mỗi cổng một mẩu bằng chứng

Dùng lại `learning_events` (migration 0231, SDD-038), **không thêm bảng mới**: đây vẫn là "learner đã bỏ ra bao nhiêu phút vào việc gì", cùng loại với luyện đọc và nghe. Thêm bảng thứ hai là chia đôi câu trả lời cho câu hỏi "hôm nay con học bao lâu".

| Cột | Giá trị cho một trang đọc |
| --- | --- |
| `action` | `read` — hành động mới, đứng cạnh `practice`/`review`/`replay`/`vocab`/`submit` |
| `ref` | Khoá trang, ví dụ `part:speaking/part-2`, `micro:reading/inference` |
| `seconds` | Thời gian thật trên trang, đo bằng `useTimeOnTask` đã có (dừng khi tab ẩn) |
| `items` | Số cổng của trang |
| `correct` | Số cổng trả lời đúng **ngay lần đầu** |

`correct` cố ý đếm lần đầu, trong khi màn hình cho trả lời lại: hai con số ấy nói hai chuyện. Với learner, việc đọc lại rồi trả lời đúng là xong. Với Learner Model, "đúng ngay lần đầu" mới là tín hiệu về mức hiểu — và REQ-PED-07 đã chốt rằng bằng chứng phải khai đúng độ tin cậy của nó, không khai vống lên.

**Một trang ghi một dòng, lúc learner đi qua cổng cuối cùng** (không phải lúc rời trang, không phải mỗi cổng một dòng): một dòng cho một việc trọn vẹn thì đọc sổ ra là đọc được lịch sử học, còn bảy dòng vụn cho một trang thì mọi phép đếm "bao nhiêu bài" sau này đều sai. Trang đọc dở không ghi gì — và đó là câu trả lời đúng: learner chưa đọc xong trang ấy thật.

## 5. Nội dung soạn thế nào

Cổng là **một phần của nội dung**, không phải thứ máy sinh lúc hiển thị. Chúng nằm cạnh mẩu chữ trong cùng một file dữ liệu (`apps/learn/src/ielts/data/*.ts` cho nội dung trong bundle, hoặc bảng nội dung tương ứng cho nội dung trong D1), và người soạn viết chúng cùng lúc với việc viết mẩu ấy.

Lý do không sinh tự động: một câu True/False sinh máy từ một đoạn văn gần như luôn hỏi vào mệnh đề *dễ trích nhất*, không phải mệnh đề *đáng dừng lại nhất*. Chỗ đáng dừng là chỗ người soạn biết learner hay hiểu nhầm — đúng thứ đã có sẵn trong khuôn soạn bài của Nemo12 (`nearMisses`, "ví dụ dễ nhầm").

**Giới hạn số cổng**: một trang có từ 3 tới 8 cổng. Dưới 3 thì trang ấy không phải trang nhiều nội dung, để nguyên. Trên 8 thì trang ấy nên tách làm hai trang có URL riêng (luật đã có ở SRC-812: mỗi việc một URL).

## 6. Đã hiện thực tới đâu

**Đợt 1 (2026-09-18) — cả 13 trang phần thi.** Mọi trang `/ielts/{skill}/parts/{part}` nay chạy đúng tiêu chuẩn này, mỗi trang 4 cổng.

| Thứ | Ở đâu |
| --- | --- |
| Luật chấm + luật soạn bài, hàm thuần | `apps/learn/src/ielts/gates.ts` |
| Nội dung cổng (13 trang × 4 cổng) | `apps/learn/src/ielts/data/checkpoints.ts` |
| Component tuôn chậm + ghi vết | `apps/learn/src/ielts/GatedContent.tsx` |
| Trang dùng nó | `apps/learn/src/ielts/ExamPart.tsx` |
| `action: 'read'` trong sổ | migration `0249`, `workers/api/src/modules/ieltsSkills/routes.ts` |
| Test | `gates.test.ts` (13 case), `e2e/ieltsGatedReading.spec.ts` (5 case), `routes.test.ts` (2 case) |

Bốn mẩu của một trang phần thi: mở bài (tên, mô tả, thời lượng) → "phần này đo cái gì" → các dạng đề → các bẫy thường gặp. Mẩu mở bài luôn có mặt, vì cổng số một hỏi về chính nó.

Ba điều đáng ghi vì chúng là quyết định, không phải chi tiết:

* **Lối ra khỏi trang (làm thử một bài, xem năng lực nhỏ) chỉ hiện khi ĐÃ ĐỌC XONG.** Bày sẵn lúc learner mới ở cổng thứ nhất là đặt một cửa thoát ngay cạnh việc đang làm dở — với một trang dựng ra để không cho lướt thì đó là tự mở lại đúng cái cửa vừa đóng.
* **Phần chưa mở KHÔNG có trong DOM**, không phải bị che bằng CSS: che bằng CSS thì cuộn xuống vẫn đọc được và mọi thứ còn lại chỉ là trang trí. Có một e2e gác đúng điều này.
* **Dòng `read` không vào tỉ lệ đúng của kỹ năng** (`progress.ts`): cổng đọc hỏi về mẩu vừa đọc xong, còn tỉ lệ ấy nói về việc làm bài thi thật. Trộn vào là cho phép kéo con số ấy lên bằng cách đọc trang lý thuyết — đo sai đúng thứ nó sinh ra để đo. Phút học thì VẪN tính, vì đọc cũng là học.

**Đợt 2 (2026-09-19) — 35 trang năng lực nhỏ.** Cổng gắn vào PHẦN LÝ THUYẾT trong khối "Cách làm", mỗi trang 3 cổng (105 cổng).

| Thứ | Ở đâu |
| --- | --- |
| Nội dung cổng (35 năng lực × 3) | `apps/learn/src/ielts/data/microCheckpoints.ts` |
| Khối lý thuyết, tách khỏi trang chọn bài | `apps/learn/src/ielts/MicroTheory.tsx` |
| Cổng độ phủ (chạy trong `npm run check:code`) | `scripts/check-ielts-gates.mjs` |
| Test | `microCheckpoints.test.ts`, `e2e/ieltsMicroTheory.spec.ts` (4 case) |

Ba quyết định của đợt này:

* **Không gắn cổng vào danh sách bài tập.** Bài tập tự nó đã là bằng chứng: learner chọn đáp án, hệ chấm, sổ ghi. Thêm một lớp cổng lên trên là bắt trả lời hai lần cho cùng một việc. Luật "làm trước, đọc lý thuyết sau" (SRC-821) giữ nguyên: mở trang ra vẫn là bài tập, lý thuyết vẫn gập lại.
* **Đồng hồ chỉ chạy sau khi learner MỞ khối lý thuyết.** `GatedContent` đếm giây từ lúc được dựng; dựng sẵn bên trong một `<details>` đang đóng thì mọi learner chỉ ghé qua trang chọn bài cũng bị tính giờ đọc — một con số sai đi thẳng vào sổ và vào phép đếm 200 giờ.
* **Khối "Cách làm" giữ nguyên bản redesign của SRC-851** (diagram + thẻ bốn bước + thẻ ví dụ + thẻ bẫy). Nó suýt mất một lần: bản redesign chỉ tồn tại dưới dạng file CHƯA COMMIT trong cây làm việc chung, nên nhánh dựng `MicroTheory.tsx` mọc từ `origin/main` không nhìn thấy và render lại phần ấy trơn. Nay `e2e/ieltsMicroTheory.spec.ts` gác cả HÌNH lẫn THẺ, không chỉ gác chữ.
* **Mẩu mở đầu (`what_vi`) đứng NGOÀI cổng**, vì cổng số một hỏi về chính nó. Cùng luật với trang phần thi. Đây là lỗi thật đã mắc lúc ráp lần đầu và bị e2e bắt: cổng hỏi về một câu learner chưa được đọc.

Một lỗi nữa của đợt này, đáng ghi vì nó im lặng: cổng từng được bọc bằng preset `FadeIn`, mà preset ấy dùng `whileInView` - một khối nằm dưới mép màn hình đứng nguyên ở `opacity: 0` cho tới khi learner tự cuộn xuống. Learner trả lời đúng, phần tiếp theo đã có trong DOM, mà màn hình như không có gì xảy ra. Nay cổng dùng preset `Reveal` (chạy lúc dựng) và trang **tự cuộn tới đầu mẩu mới** - đúng điều §3 đã viết mà bản đầu chưa làm.

**Phạm vi của lỗi ấy hẹp hơn vẻ ngoài, và điều đó quyết định cách gác nó.** Nó chỉ xảy ra khi khối mới nằm XA chỗ learner vừa bấm: bấm một nút làm trình duyệt kéo chính nút ấy vào tầm nhìn, nên một khối phản hồi nằm ngay dưới nút thì vẫn lọt vào `whileInView` và vẫn hiện bình thường (phiên SRC-850 đã đo điều này trên trang bài tập của họ: `top=275` trên màn cao 360). Với cổng đọc hiểu thì khác, vì mẩu vừa mở có thể dài cả nghìn pixel.

Hệ quả cho TEST: một e2e dựng trên màn rộng với mẩu ngắn sẽ **xanh cả khi dùng `FadeIn`** - nó gác phép cuộn chứ không gác preset. Case thật phải là mẩu DÀI trên màn THẤP (900×400, mẩu "các dạng đề" của Task 2): lúc ấy cổng nằm cách mép dưới hơn một nghìn pixel. Đã kiểm hai chiều: `FadeIn` cho `opacity: 0` và test ĐỎ, `Reveal` cho `opacity: 1` và test xanh. Một test không đỏ được khi lật bản sửa thì không gác gì cả.

Ghi vết: `ref` là `micro:{skill}/{code}`, cùng bảng và cùng `action: 'read'` với trang phần thi.

**Nội dung lý thuyết sống ở D1, cổng sống trong bundle** — nên `check-ielts-gates.mjs` đọc thẳng migration `0234`/`0235` làm danh sách sự thật: nạp thêm một năng lực mà quên soạn cổng thì CI đỏ. Cổng ấy KHÔNG phát hiện được việc ai đó sửa chữ trong lý thuyết mà quên sửa cổng theo — đó là việc của người sửa.

**Đợt 3 (2026-09-19) — kho từ của 8 cụm bài đọc.** 50 từ mỗi cụm nay tuôn ra từng mẩu mười từ, mỗi mẩu đổi lấy một câu hỏi nghĩa (5 cổng). Ghi vết: `ref` là `cluster:{id}`.

| Thứ | Ở đâu |
| --- | --- |
| Chia mẩu + ghép cổng, hàm thuần | `apps/learn/src/ielts/wordGates.ts` |
| Trang cụm dùng nó | `apps/learn/src/ielts/Reading.tsx` (`ClusterView`) |
| Test | `wordGates.test.ts` (11 case), `e2e/ieltsClusterVocab.spec.ts` (5 case) |

**Đây là ngoại lệ DUY NHẤT của luật "cổng do người soạn viết" (§5), và ranh giới của nó rất hẹp.**

Với một đoạn văn, §5 đúng: máy sinh câu hỏi từ văn xuôi sẽ hỏi vào mệnh đề *dễ trích nhất*, không phải mệnh đề *đáng dừng lại nhất*. Với một DANH SÁCH mà mỗi mục tự nó là một đơn vị học thì khác: thứ đáng dừng lại chính là từng mục, và nghĩa của nó đã do người soạn viết sẵn trong dữ liệu. Máy không chọn điều gì đáng hỏi — danh sách đã chọn thay nó; máy chỉ ghép một câu hỏi quanh một mục có sẵn, với **phương án nhiễu là nghĩa thật của những từ đứng cạnh trong chính mẩu vừa đọc**.

Lý do thực tế đứng sau ranh giới ấy: soạn tay 8 cụm × 50 từ = 400 cổng thì không ai duy trì nổi, và mỗi lần thêm một từ lại phải soạn thêm một cổng — tức là luật sẽ bị bỏ qua ngay lần nội dung mở rộng đầu tiên. Ngoại lệ này **không** mở cho văn xuôi.

Hai ràng buộc riêng của cổng ghép máy, cả hai đều có test:

* **Ổn định**: chọn từ và chọn vị trí đáp án đi bằng một hàm băm của (khoá trang + số mẩu), không bằng `Math.random`. React vẽ lại nhiều lần trong một lượt; random thì câu hỏi đổi giữa lúc learner đang đọc. Tải lại trang gặp đúng bộ câu cũ là điều ĐÚNG.
* **Hỏi bằng đúng bản nghĩa đang hiện trên màn** (tiếng Anh, theo SRC-834). Bản đầu hỏi bằng nghĩa tiếng Việt trong khi dòng từ hiện nghĩa tiếng Anh — cổng hoá ra đo trí nhớ về một bản learner không nhìn thấy. E2E bắt được.

**Cụm bài NGHE để nguyên.** Trang cụm nghe chỉ có một đoạn ghi chú và danh sách bài nghe; vốn từ của nó dùng lại kho từ của cụm đọc cùng lĩnh vực. Dưới 3 cổng thì §5 nói rõ: để nguyên là hơn.

## 7. Phần chưa chốt

* **Ngưỡng "đã hiểu"**: bao nhiêu cổng đúng ngay lần đầu thì Learner Model được coi là có bằng chứng hiểu trang này. Cần chủ dự án chốt — xem Q-153. Hiện sổ chỉ LƯU hai con số, chưa có chỗ nào đặt ngưỡng.
* **Các đợt sau**: trang Word list theo lĩnh vực (`/ielts/lexicon`) là bề mặt danh sách còn lại chưa có cổng; nó dùng lại được `wordGates` nhưng dữ liệu có hình dạng khác (họ từ theo bậc), nên cần một vòng riêng.

## 8. Trace

| Mục | Ở đâu |
| --- | --- |
| Yêu cầu | REQ-PED-08 (PRD-001 §5) |
| Nguồn | SRC-836 |
| Cổng chất lượng | QG-005 (bằng chứng và độ tin cậy), QG-006 (chuẩn sư phạm) |
| Dữ liệu | `learning_events` (migration 0231), action `read` |
| Liên quan | SDD-038 §3 (phút học và dự đoán 200 giờ đọc chính sổ này), SDD-022 (các dạng câu hỏi), REQ-PED-07 (độ tin cậy theo dạng) |
