Skip to content

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ạiLearner làm gìDùng khi
true_falseMộ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_wordMột câu thiếu một từ, chọn 1 trong 3Mẩu vừa đọc đưa ra một thuật ngữ hoặc một phân biệt từ
micro_quiz1-2 câu trắc nghiệm cực ngắnMẩu chứa một quy trình hoặc một danh sách có thứ tự
clozeGõ một từ vào chỗ trốngTừ ấ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ộtGiá trị cho một trang đọc
actionread — hành động mới, đứng cạnh practice/review/replay/vocab/submit
refKhoá trang, ví dụ part:speaking/part-2, micro:reading/inference
secondsThời gian thật trên trang, đo bằng useTimeOnTask đã có (dừng khi tab ẩn)
itemsSố cổng của trang
correctSố 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ầnapps/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ếtapps/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
Testgates.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àiapps/learn/src/ielts/MicroTheory.tsx
Cổng độ phủ (chạy trong npm run check:code)scripts/check-ielts-gates.mjs
TestmicroCheckpoints.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ầnapps/learn/src/ielts/wordGates.ts
Trang cụm dùng nóapps/learn/src/ielts/Reading.tsx (ClusterView)
TestwordGates.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ầuREQ-PED-08 (PRD-001 §5)
NguồnSRC-836
Cổng chất lượngQG-005 (bằng chứng và độ tin cậy), QG-006 (chuẩn sư phạm)
Dữ liệulearning_events (migration 0231), action read
Liên quanSDD-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)