Skip to content

SDD-038 · Trang kỹ năng và thang học ​

Một phần của SDD-038 Cổng IELTS. 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.

5c. Bốn trang kỹ năng và thang ba bậc (SRC-801, chỉ đạo 2026-09-17 vòng 3) ​

/ielts đổi vai, bảy việc cũ lùi về /ielts/settings ​

Danh sách tám việc ở §1 là việc khai một lần rồi thỉnh thoảng sửa. Thứ learner quay lại hằng ngày là luyện. Đặt tám việc khai báo ở cửa vào là bắt một đứa trẻ đi qua một biểu mẫu mỗi lần nó muốn học, nên /ielts nay là cửa vào bốn kỹ năng và tám việc kia lùi một tầng. Luật hé lộ từng bước (pathway.ts) không đổi một dòng — chỗ đứng của trang không phải một quyết định sản phẩm về việc ai được xem gì.

Sổ hoạt động: ghi VIỆC, không ghi kết quả ​

learning_events (migration 0231) đứng cạnh ielts_practice_attempts chứ không thay nó. Bảng kia ghi kết quả (đúng mấy trên mấy); ba con số chủ dự án hỏi lại nằm cả ở chỗ nó cố ý không ghi:

  • "Số phút đã nghe, tính cả những lần nghe lại" — nghe lại lần thứ tư một đoạn không sinh ra lượt làm bài nào. useListeningMinutes đo bằng sự kiện play/pause của chính thẻ <audio>, không nhân số lần bấm với độ dài bài: learner bấm phát rồi dừng ở giây thứ mười là đã nghe mười giây.
  • "Số ngày học liên tục" — một ngày chỉ mở kho từ ra đọc vẫn là một ngày không bỏ. Chuỗi đếm qua mọi kỹ năng; day tính theo giờ Việt Nam ngay lúc ghi, vì học lúc 0h30 sáng thứ Tư ở Hà Nội là 17h30 UTC thứ Ba, và tính theo UTC thì chuỗi mất một mắt.
  • Thời gian ở trong bài — useTimeOnTask dừng đồng hồ khi tab ẩn, bỏ qua các lượt dưới 20 giây, và cắt ở 90 phút. Một tab bỏ quên qua đêm không được thành "hôm nay con học mười bốn tiếng" trên màn hình một đứa trẻ.

Vốn từ đi theo cặp: đã gặp và đã chắc. Một từ thành "đã chắc" khi learner gặp lại nó ở một bài khác — gặp lại trong chính đoạn vừa đọc là nhớ mặt chữ, gặp lại ở ngữ cảnh mới mới là bắt đầu có từ ấy. Cả hai tính cộng dồn cả đời học, không cắt theo cửa sổ bảy ngày: cắt theo cửa sổ thì con số tụt mỗi lần learner nghỉ một tuần, tức là hệ nói với đứa trẻ rằng nó đã quên — điều mà phép đếm này không hề biết.

Learner chưa có gì trong sổ thì trang ẩn hết số liệu, chỉ còn một lời mời. Một bảng toàn số 0 là bản kê những việc learner chưa làm, đặt trước mắt nó vào đúng lúc nó chưa kịp làm gì.

Hai nút, hai thuật toán đã có sẵn ​

"Học tiếp" hỏi bài nào hợp sức con lúc này; "ôn lại" hỏi thứ nào con sắp quên. Gộp hai câu ấy vào một phép xếp hạng là sớm muộn nút "ôn lại" đưa ra một bài chưa từng làm, và lúc ấy cái nút hứa "đỡ tốn năng lượng" lại là cái nút nặng nhất trang.

Đường cong quên lấy thẳng decayedRetention + initialStability của SDD-017. Khác biệt duy nhất là đơn vị: engine gốc tính trên node của chương trình học, ở đây một "thứ để nhớ" là một BÀI (cluster:seq). Bài luyện IELTS chưa gắn vào cây node, và gắn bừa vào một node nào đó chỉ để dùng lại được truy vấn cũ sẽ làm hỏng chính cây ấy. Viết một đường cong quên thứ hai thì tệ hơn nữa: ngày chúng lệch nhau, không ai biết bên nào đúng mà cả hai đều đang nói với learner.

Mỗi nút mang theo lý do của nó, do server viết. Một nút "hệ thống đã chọn giúp con" không nói vì sao là một hộp đen, và một đứa trẻ bấm vào hộp đen vài lần sẽ thôi tin nó.

Câu ghi nhận: thư viện viết sẵn, điều kiện là mã ​

Ghi nhận khác khen. "Con giỏi lắm" nói về đứa trẻ, đúng dù nó có làm gì hay không, nên nó rỗng. "Sáu ngày liền con không bỏ buổi nào" nói về việc, chỉ đúng khi việc ấy có thật, và không thể nói với người chưa làm.

Chủ dự án chọn thư viện câu viết sẵn thay vì để model sinh. Lý do: một model sinh lời khen sẽ có ngày khen một learner vừa tụt điểm, vì nó đọc con số rồi tự dựng một câu nghe cho tử tế. Ở đây điều kiện là mã — câu về chuỗi 6 ngày chỉ chạy khi chuỗi đạt 6. Tối đa ba câu một lần: nhiều hơn thì mắt lướt qua cả cụm, và câu thứ nhất, câu đáng đọc nhất, mất luôn.

Có một câu cho trường hợp tỉ lệ đúng tụt, và nó không phải câu an ủi. Learner tự chọn bài khó hơn thì tỉ lệ đúng tụt là chuyện phải xảy ra; im lặng đúng lúc ấy sẽ dạy đứa trẻ rằng chọn bài dễ thì hệ thống mới vui.

Thang ba bậc cho nói và viết ​

BậcTênChấm cái gìCó band
1Tập làmCó trả lời đúng đề không, người đọc có hiểu không, có đủ dài khôngkhông
2Làm đượcCó một ý chính, có chỗ đỡ cho ý ấy, các câu có nối vào nhaukhông
3Chuẩn IELTSBốn tiêu chí thật của kỳ thicó

Thang band IELTS bắt đầu có nghĩa từ khoảng 4.0. Một learner mới viết được sáu câu đơn chấm theo bốn tiêu chí sẽ nhận 3.0, rồi 3.0, rồi 3.0 — ba tuần liền cùng một con số trong khi bài của nó rõ ràng đã khác. Thang ấy không sai, nó chỉ quá thô ở đoạn đầu để nhìn thấy tiến bộ, mà nhìn thấy tiến bộ là việc trang này sinh ra để làm.

Bậc là của ĐỀ BÀI, không phải của learner. Không có chỗ nào ghi "learner này đang ở bậc 2", và ba bậc bày ra cùng lúc không khoá bậc nào. Gắn bậc vào người là dựng một cái nhãn tự ứng nghiệm: hệ chỉ đưa bài bậc 1 cho người nó đã xếp bậc 1.

Lệnh cấm trả band ở bậc 1-2 nằm trong mã (scoreLadder ép band: null), không chỉ trong prompt. Một model được dặn "đừng chấm điểm" vẫn sẽ chấm điểm vào ngày nó thấy bài giống bài thi — và lúc ấy một đứa trẻ bậc 1 đọc được con số "3.0" cho bài đầu tiên nó dám viết. Hai lớp: prompt dạy, mã chặn.

Nhận xét (ielts.ladder-feedback) viết cho chính learner, khác ielts.production-feedback viết cho mentor. Thứ tự các ô là một quyết định: did_vi (con đã làm được gì) đứng trước one_step_vi. Model viết theo thứ tự nó được hỏi, và hỏi phần làm được trước là cách rẻ nhất để phần ấy không thành câu lót đường cho phần chê. Một việc nên sửa, không phải một danh sách: learner sửa được một thứ mỗi lần.

track tách bài luyện khỏi bài chẩn đoán ​

Migration 0232 thêm ielts_productions.track. Không có nó, ba chỗ đang đọc bảng theo kiểu "lần nộp mới nhất của kỹ năng ấy" sẽ hỏng lặng lẽ: pathway.ts đánh dấu bước chẩn đoán là xong cho learner chưa từng làm bài chẩn đoán; model.ts để một bài luyện đè lên band của bài chẩn đoán; report.ts đưa mentor đọc nhầm bài. Một cột thay vì một bảng thứ hai, vì hai loại bài có cùng vòng đời.

Giới hạn: ba tầng, ba thứ khác nhau (chỉ đạo chủ dự án 2026-09-17) ​

  1. Trần theo từng kỹ năng — viết 8 bài/ngày, nói 6 đoạn/ngày. Nói chặt hơn viết vì một lượt nộp bài nói gọi model hai lần (Whisper phiên âm, rồi chấm). Cái đắt hơn phải chặt hơn.
  2. Trần tổng — 10 lượt chấm/ngày/learner, nhỏ hơn tổng hai trần trên (8+6). Cố ý: hai trần kia lo nhịp học, trần này lo hoá đơn, và nó chặn cả khi learner chia đều giữa hai kỹ năng để lách.
  3. Trần độ dài đầu vào — 300/450/600 từ và 120/180/240 giây theo bậc. Chi phí một lượt chấm tỉ lệ với số token đi vào, nên một bài 4.000 từ đắt bằng hai mươi bài 200 từ mà vẫn chỉ tính là một lượt. Đếm lượt mà không chặn độ dài là chặn đúng một nửa hoá đơn. Vượt trần thì từ chối thẳng, không cắt bớt rồi chấm phần đầu — nhận xét về một bài không phải bài learner viết còn tệ hơn một lời từ chối.

Số lượt còn lại bày ra trước khi learner làm bài, đọc bằng peekRateLimit (đọc mà không tăng bộ đếm — dùng enforceRateLimit ở đường đọc thì mở trang ra xem còn mấy lượt sẽ tiêu mất một lượt). Một cái trần bất ngờ chặn ngang lúc learner vừa viết xong là một lời từ chối; cùng cái trần ấy hiện sẵn thì chỉ là một luật chơi.

Sổ hoạt động có trần riêng và khác loại: 240 lượt ghi/giờ, seconds <= 7200 ở cả cổng API lẫn CHECK của bảng. Đường này không gọi model lần nào — trần ở đây chống bơm sổ, không chống tốn tiền.

5d. Một tên miền: mạch luyện chuyển hẳn về learn (SRC-805, 2026-09-18) ​

Toàn bộ mã của app ielts cũ chuyển về apps/learn/src/ielts/ — 3.942 dòng, không sửa logic. Ba thứ phải chỉnh và mỗi thứ có một cái bẫy riêng:

Tiền tố URL nằm ở đúng một chỗ. router.ts thêm BASE = "/ielts", và cả parse lẫn href đi qua nó. Rải tiền tố vào từng link là ngày đổi tiền tố sẽ có vài chỗ quên — mà một link quên tiền tố vẫn trông y hệt một link bình thường cho tới lúc có người bấm.

Hai họ URL từng chạm nhau. Tám việc khai báo cũ ở /ielts/<task>, và hai trong số đó tên là writing và speaking — đúng tên hai kỹ năng luyện. Chúng lùi xuống /ielts/settings/<task>; link cũ /ielts/<task> vẫn nhận (luật "link đã gửi đi không được gãy"). Danh sách 12 khoá — vốn là trang chủ của app cũ — về /ielts/courses, nhường /ielts cho hub bốn kỹ năng.

Mạch luyện đứng TRƯỚC cổng đăng nhập của learn, và đây là chỗ nguy hiểm nhất của cả lần chuyển. 140 trang nội dung ở đây vốn công khai; đặt chúng sau cổng đăng nhập là lặng lẽ khoá cả trăm trang đang mở, mất phần SEO đã có, và không có lỗi nào hiện ra để ai đó nhận ra. Mạch giữ AuthProvider riêng: chưa đăng nhập thì luyện y như cũ, đăng nhập rồi thì bài vào sổ.

ielts.nemo12.com KHÔNG bị xoá — nó thành một worker chỉ làm một việc: 301 về learn.nemo12.com/ielts<đường dẫn cũ>, giữ nguyên cả path lẫn query. Tắt hẳn là biến mọi link đã gửi đi thành lỗi DNS. 301 chứ không 302: chuyển nhà vĩnh viễn, và chỉ 301 mới dồn giá trị tìm kiếm của URL cũ sang URL mới.

Giao diện đổi theo, không phải sửa từng file. Mạch cũ nền sáng; learn nền tối. Practice.tsx bọc mạch trong .n12-deep, lớp khai lại chính các token mà các trang ấy đang dùng (kể cả --n12-ocean-900 đảo thành màu chữ sáng). Đó đúng là lý do bảng màu đi bằng token chứ không bằng mã màu viết thẳng.

Phạm vi deploy phải theo. .github/app-paths.json: learn nay nuốt cả content/ielts-* vì nó gói nội dung ấy; thiếu vế này thì sửa một bài đọc sẽ không deploy learn, và không ai biết cho tới lúc mở trang thấy bài cũ. ielts rút về đúng ^apps/ielts/ và không còn bước build.

Cổng: scripts/check-one-domain.mjs trong npm run check:code cấm mọi link sang tên miền cũ (href=, location.*, window.open). Nhắc tên miền trong chú thích vẫn được — lịch sử của mạch này nằm ở đó. Một href sai tên miền không hỏng gì mắt nhìn ra được: thẻ vẫn hiện, vẫn bấm được, vẫn mở ra một trang thật; thứ duy nhất sai là learner rời khỏi learn.nemo12.com.

5e. Năng lực nhỏ: học lý thuyết rồi kiểm hiểu (SRC-805, 2026-09-18) ​

35 năng lực, 140 bài tập. Đọc 10 và Nghe 5 lấy đúng danh sách đã có trong dữ liệu bài tập (SKILL_LABEL, LISTENING_SKILL_LABEL), nên mỗi câu hỏi trong 1.560 câu đọc đều trỏ được về một năng lực. Nói và Viết chưa có danh sách nào nên phải soạn từ đầu: đợt đầu 4 mỗi kỹ năng (0234), đợt sau (SRC-810, migration 0235) bổ sung 6 nữa mỗi kỹ năng để bằng Đọc.

Nghe giữ 5 chứ không kéo lên 10, và đó là chủ ý: 5 là đúng số nhóm câu hỏi thật trong dữ liệu bài nghe. Thêm cho tròn số là soạn lý thuyết cho những thứ không có bài tập nào ở phía sau.

Sáu năng lực Nói bổ sung nhắm vào chỗ learner Việt tắc nhiều nhất mà sách ít dạy: diễn đạt vòng khi thiếu từ, nói cụ thể thay vì chung chung, nói một mạch hai phút (Part 2), so sánh và cân nhắc (Part 3), sửa lỗi giữa chừng, giữ nhịp nói. Sáu năng lực Viết phủ phần vòng đời một bài mà đợt đầu bỏ trống: đọc kỹ đề, mở bài, kết bài, thừa nhận phía kia, đổi kiểu câu, và mô tả biểu đồ cho Task 1.

Nội dung nằm trong D1, không nằm trong content/ (quyết định của chủ dự án). Bài đọc ở content/ vì chúng là ngữ liệu ổn định; lý thuyết thì ngược lại, nó là chữ giải thích, và chữ giải thích chỉ hay lên bằng cách sửa đi sửa lại sau khi xem học sinh thật đọc nó. Bắt mỗi lần sửa một câu phải qua một vòng deploy là bảo đảm nó sẽ không được sửa. Đổi lại là mất cổng kiểm của git, nên cột status (nháp/phát hành) gánh phần ấy: đường công khai lọc ngay trong câu SQL.

Năm phần là KHUÔN, thành năm cột riêng (migration 0233): là gì · cách làm · ví dụ có giải · bẫy thường gặp · bài tập. Nhét cả năm vào một cột markdown thì khuôn biến mất ngay bài thứ ba — người soạn sau sẽ bỏ phần "bẫy" vì không có gì nhắc, mà phần bẫy chính là thứ học sinh Việt mất điểm nhiều nhất và sách ít nói. Trên màn hình, bẫy đứng TRƯỚC bài tập: đọc sau khi làm thì nó chỉ còn là lời an ủi; đọc trước thì learner nhận ra chính cái bẫy ấy ngay ở bài kế tiếp.

Bài tập siêu đơn giản, và ràng buộc ấy nằm trong hình dạng bảng. Một bài là 2-3 câu ngữ liệu, một câu hỏi, vài lựa chọn. Không có cột nào cho một bài đọc dài, và cố ý không có: bài dài đo lẫn hai thứ (có hiểu lý thuyết không, và có đọc hết nổi không) rồi trả về một kết quả không tách được. Chấm ngay từng câu, không chấm cả cụm ở cuối — vòng lặp càng ngắn thì lý thuyết càng dính.

Sau khi chấm, chỉ đáp án đúng được tô màu; lựa chọn sai learner vừa chọn chỉ mờ đi và gạch ngang. Hai lý do: trong bảng màu này accent và coral-deep gần nhau nên tô cả hai thì phải đọc chữ mới biết cái nào đúng; và sơn đỏ chỗ một đứa trẻ vừa chọn sai đi ngược lại chính tinh thần ghi nhận của §5c.

Hai lối vào, cho hai lúc khác nhau (chủ dự án chọn cả hai):

  • Một dải tên năng lực trên trang kỹ năng — lúc learner chủ động muốn học.
  • Một link ngay dưới lời giải của câu learner vừa làm sai — lúc con vừa trượt một câu suy luận là lúc chữ "Suy luận là gì" có nghĩa nhất. Câu đúng thì không hiện link: một lời mời học lại sau mỗi câu đúng chỉ thành nền rồi thành thứ bị bỏ qua.

Vào sổ khi làm HẾT bài tập, không phải khi mở trang. Học lý thuyết là một việc đã làm nên nó tính vào chuỗi ngày (§5c); ghi lúc mở trang thì mở ra rồi đóng lại cũng thành "đã học", và chuỗi ngày mất hết ý nghĩa.

Màn admin (apps/admin, tab "Năng lực IELTS") lưu khi rời ô và gửi đúng ô vừa sửa — cùng luật với Courses.tsx và Pricing.tsx. Đáp án chọn bằng danh sách chứ không gõ số: một chỉ số trỏ ra ngoài danh sách lựa chọn thì bài vẫn hiện bình thường mà chấm sai mọi lượt.

5f. Thang bốn chặng: tiếng Việt trước, tiếng Anh sau (SRC-821, 2026-09-18) ​

Vì sao tiếng Việt đi trước. Tìm ý chính là một thao tác tư duy, không phải việc của vốn từ. Cho learner gặp nó lần đầu bằng tiếng Anh là bắt hai việc khó xảy ra cùng lúc: hiểu nghĩa từng chữ, và nhận ra đoạn văn đang nói gì. Sai lúc ấy thì không ai — kể cả learner — tách được là sai vì thiếu từ hay vì chưa biết cách. Học bằng tiếng mẹ đẻ trước thì lúc sang tiếng Anh chỉ còn đúng một việc mới.

Bốn chặng (cột stage, migration 0238), mỗi chặng khác chặng trước ở đúng một điểm:

ChặngNgôn ngữKhác chặng trước ở điểm nào
1ViệtĐoạn 3-5 câu, ý chính rõ. Việc duy nhất là nhận ra.
2ViệtNhiều kiểu đoạn khác nhau, mỗi bài một kỹ thuật.
3AnhCùng loại đoạn đã quen, đổi sang tiếng Anh.
4AnhĐoạn dài hơn, trừu tượng hơn, gần đề thi.

Chia nhỏ hơn bốn thì ranh giới nhoè và người soạn bài sau sẽ nhét bài vào chặng nào cũng được.

Chặng 2 dài nhất, và đó là một ràng buộc có test. Ý chính hiện có 6 · 12 · 6 · 6 bài. Mười hai kỹ thuật: câu chủ đề đầu đoạn · câu chủ đề cuối đoạn · đoạn không có câu chủ đề · đoạn phản đề · đoạn liệt kê ví dụ · đoạn nhân-quả · đoạn so sánh · đoạn có số liệu · đoạn kể chuyện dẫn vào nhận định · chi tiết lặp nhiều lần vẫn không phải ý chính · đoạn mở bằng câu hỏi · đoạn nhượng bộ. Tên kỹ thuật (technique_vi) hiện sau khi chấm, không hiện trước đề: nói trước là nói mất đáp án, còn đọc sau thì learner có một cái tên để gọi đúng thao tác vừa làm.

Màn hình còn đúng ba thứ (thay cho năm mục xếp dọc của §5e): một thanh bốn chặng đầy dần không chữ · một đoạn văn với các lựa chọn của nó · một dòng "Cách làm" gập lại, mặc định đóng. Thứ tự cũ hợp lý khi viết ra nhưng sai khi dùng: learner phải đọc bốn mục chữ trước khi được làm thử điều đang học, mà bốn mục ấy dễ hiểu hơn hẳn sau bài đầu tiên. Lý thuyết không mất đi, nó lùi vào sau một dòng bấm được, và phần lớn learner sẽ mở nó đúng lúc cần: ngay sau khi vừa sai một bài. Dùng <details> của trình duyệt chứ không dựng lại bằng state — nó đã có sẵn bàn phím, aria và nút Tìm trong trang của Chrome mở được cả phần đang đóng.

Cả 35 năng lực đều đã lên thang (migrations 0240-0246, cùng SRC-821). Mỗi năng lực ngoài Ý chính nhận thêm 10 bài: 3 bài Việt chặng 1, 5 bài Việt chặng 2 (mỗi bài một kỹ thuật có tên), 2 bài Anh chặng 4; bốn bài tiếng Anh nạp ở 0234-0235 ở lại chặng 3. Tổng cộng 506 bài, trong đó 290 bài tiếng Việt (108 ở chặng 1, 182 ở chặng 2) và 216 bài tiếng Anh. Phần tiếng Việt nhiều hơn là chủ ý: sang tiếng Anh, learner chỉ còn học một việc mới.

Ba điều đáng nói vì chúng dễ bị hiểu nhầm thành lỗi:

  • Bài NGHE luyện bằng chữ. Bảng này chỉ có ngữ liệu chữ, nên một bài nghe ở đây là đoạn lời thoại đọc bằng mắt. Thứ đang luyện không phải tai mà là thao tác: nhận ra người nói vừa đổi ý, nhận ra con số nào là con số cuối cùng, nhận ra một câu lịch sự đang mang nghĩa từ chối. Chỗ luyện tai thật là dữ liệu bài nghe có âm thanh (16 bài, 160 câu).
  • Bài NÓI và VIẾT là bài chọn đáp án. Chúng đo việc NHẬN RA, không đo việc làm. Trước khi tự nói được một câu trả lời tốt, learner phải phân biệt được câu trả lời tốt với câu nghe trôi chảy mà lạc đề. Chỗ nói và viết thật, chấm ở server, là ielts_productions.
  • Với Viết, chặng tiếng Việt trả lời một hiểu lầm cụ thể: "em viết dở vì vốn từ em kém". Phần lớn thì không phải. Lập trường đổi giữa chừng, đoạn thân bài không có câu chủ đề, bỏ sót một vế của đề, cả ba lỗi ấy viết bằng tiếng Việt cũng mắc y hệt. Cho learner thấy điều đó bằng chính tiếng mẹ đẻ là cách nhanh nhất để họ thôi đổ cho vốn từ và sửa đúng chỗ.

Cổng gác đợt này nằm ở microSkills/routes.test.ts: không năng lực nào được có ít hơn bốn chặng, không bài nào được lệch ngôn ngữ so với chặng, và mọi bài chặng 2 phải có tên kỹ thuật. Ba lỗi ấy đều KHÔNG hiện ra trên màn hình, chúng chỉ lặng lẽ xoá mất lý do thang này tồn tại.

Server chặn cùng chỗ đó, và chặn trên bản sau khi ghép chứ không trên bản gửi lên: người sửa hay đổi mỗi một ô (bớt lựa chọn mà quên đáp án, hoặc ngược lại), nên một nửa thông tin nằm trong yêu cầu và nửa kia trong CSDL. Kiểm riêng từng nửa là bỏ lọt đúng những lần sửa hỏng hay gặp nhất.

Cổng: modules/microSkills/routes.test.ts (16 test) gác bài nháp không lọt ra đường công khai, đường admin chặn cả người chưa đăng nhập lẫn người không có vai, sửa nửa vời bị chặn, và JSON hỏng trong một cột không làm sập cả trang. Hai test riêng kiểm chính nội dung đã nạp: đủ 23 năng lực và 92 bài tập, mọi đáp án trỏ vào một lựa chọn có thật, không bài nào bỏ trống một trong năm phần. Ở apps/learn: 3 case e2e.

Hai phép đếm trong test là RÀNG BUỘC, không phải con số cho vui: thêm một năng lực mà quên nạp bài tập cho nó thì mọi test khác vẫn xanh, còn hai dòng ấy thì đỏ.

5f. Trang kỹ năng là một hub, phần bị lấy đi có URL riêng (SRC-812, 2026-09-18) ​

Vấn đề. Bốn trang kỹ năng dựng ở §5c đều xếp chồng bốn khối khác loại trên một màn: câu ghi nhận kèm số liệu, nút học tiếp, dải năng lực, rồi danh sách bài. Bốn khối ấy đều đúng, nhưng đứng cạnh nhau thì không khối nào còn là lối đi chính — learner phải ĐỌC hết một màn hình mới biết bấm vào đâu.

Luật. Mỗi trang kỹ năng nay chỉ trả lời một câu: đi đâu bây giờ. Tối đa 5 ô, mỗi ô một lối đi.

Thứ bị lấy đi không mất, nó chuyển sang URL của chính nó — và một URL riêng thì gửi link được, mở tab mới được, bấm Back đúng:

Phần bị lấy khỏi trang kỹ năngChỗ ở mới
Danh sách cụm bài đọc + số liệu/ielts/reading/clusters
Danh sách cụm bài nghe/ielts/listening/clusters
Dải năng lực nhỏ/ielts/skills/<kỹ năng> (§5e)
Chọn đề và nộp bài của một bậc/ielts/<kỹ năng>/levels/<n>

Ô nằm ở ĐÚNG MỘT file (apps/learn/src/ielts/Hub.tsx): bốn bản chép tay của cùng một cái thẻ là bốn chỗ sẽ lệch nhau vào ngày ai đó chỉnh một cái. Cùng file ấy giữ useSkillPage() — trang kỹ năng do server dựng, trong đó có bài kế tiếp cho ô "Học tiếp"; chưa đăng nhập hoặc API hỏng thì nó trả null và ô vẫn đứng nguyên với lời mời mặc định, vì một trang luyện mở công khai (§5d) không được phép trống chỉ vì một lời gợi ý vắng mặt.

Bậc vẫn là của ĐỀ BÀI, không phải của learner (§5c): ba ô bậc mở sẵn cả ba, không khoá ô nào.

Số liệu quay lại trang kỹ năng, nhưng gọn trong MỘT DÒNG (chỉ đạo cùng ngày, vòng 2). Bốn ô số liệu ở lại trang danh sách; ngay dưới tiêu đề trang kỹ năng chỉ còn một dòng nhỏ — phút tuần này · từ đã gặp · từ đã chắc · ngày liên tục (SkillLine trong Hub.tsx). Chưa có lịch sử thì không vẽ dòng nào, giữ nguyên luật §5c: một dòng toàn số 0 là bản kê những việc learner CHƯA làm. Cả trang đọc API đúng một lần qua useSkillPage().

Chuỗi ngày là của RIÊNG từng kỹ năng (chỉ đạo cùng ngày, vòng 3 — đảo quyết định cũ ở §5c). Trước đó streak đếm qua mọi kỹ năng trong khi has_history và bảy ô ngày tính riêng từng kỹ năng, nên trang đọc có thể khoe "5 ngày liên tục" kiếm được hoàn toàn bằng việc nghe, ngay cạnh một bảng nói rằng phần đọc chưa có gì. Hai con số cạnh nhau đo hai thứ khác nhau thì con số nào cũng mất nghĩa. Nay buildProgress() đếm chuỗi trên đúng dòng của kỹ năng đang xem, và truy vấn allDays trong modules/ieltsSkills/routes.ts lọc thêm AND skill=?2 để chuỗi dài nhất cũng theo cùng một thước.

Số phút không được mất khi learner đóng tab (chỉ đạo cùng ngày, vòng 4). useTimeOnTask và useListeningMinutes trước đó chỉ gửi mẩu thời gian trong cleanup của React, tức là chỉ khi rời bài BÊN TRONG SPA; đóng tab hay tắt máy thì cleanup không chạy và cả buổi học biến mất — và nó mất đúng những buổi dài nhất, vì buổi càng dài learner càng hay đóng thẳng tab. Nay pagehide gửi mẩu ấy qua beaconLearningEvent(), một chốt sent giữ cho mẩu chỉ đi một lần dù pagehide và cleanup cùng chạy, và pageshow với persisted (trang sống lại từ bfcache) mở chốt cho một lượt mở bài mới.

Đường gửi là fetch(..., { keepalive: true }) chứ không phải navigator.sendBeacon: beacon không đặt được Content-Type: application/json mà không kích hoạt preflight và không nói được credentials: "include", trong khi API ở tên miền khác và xác thực bằng cookie phiên — một beacon như vậy hỏng im lặng. apps/learn/src/ielts/api.test.ts gác ba thuộc tính ấy.

5g. Các phần thi của Nói và Viết (SRC-815, 2026-09-18) ​

Ba tầng, không trộn. Kỳ thi chia bài ra làm phần thi (Writing Task 1/Task 2, Speaking Part 1/2/3); trong một phần thi, đề rơi vào vài dạng đề quen thuộc; và cắt ngang mọi dạng đề là các năng lực nhỏ của §5e. Ba tầng ba chỗ ở: hai tầng đầu nằm trong bundle (apps/learn/src/ielts/data/examParts.ts), tầng thứ ba nằm trong D1.

13 dạng đề: Task 1 có 4 (biểu đồ có thời gian, bảng/tròn/cột không thời gian, quy trình, bản đồ), Task 2 có 4 (nêu quan điểm, bàn hai phía, vấn đề và cách giải, đề hỏi hai câu), Speaking Part 1 có 3, Part 2 có 4, Part 3 có 4. Mỗi dạng có ba thứ: dấu hiệu nhận ra nó trong đề, bố cục bài, và phần bẫy đứng ở cấp phần thi.

Vì sao nội dung nằm trong bundle chứ không đi qua API. Trước đợt này, /ielts/writing và /ielts/speaking lấy MỌI ô từ fetchSkillPage, nên người chưa đăng nhập mở ra thấy một trang trắng — trong khi /ielts/reading và /ielts/listening bày sẵn toàn bộ nội dung cho mọi người. Nay hai trang ấy đi theo đúng hình của Đọc và Nghe: ô có sẵn trong bundle đứng trước, phần cần tài khoản lùi về URL riêng /ielts/<kỹ năng>/levels.

Một lỗi điều hướng được sửa cùng lúc. §5d đẩy bảy việc khai báo xuống /ielts/settings/<task> để /ielts/writing chỉ còn một nghĩa, nhưng nhánh giữ link cũ (/ielts/<task>) vẫn đứng trước nhánh luyện và vẫn khớp hai khoá writing, speaking. Hệ quả: bấm WRITING trên hub bốn kỹ năng thì rơi vào bài viết chẩn đoán, không phải trang luyện viết. Nhánh giữ link cũ nay loại trừ đúng hai khoá ấy; hai việc khai báo vẫn tới được bằng địa chỉ thật của chúng.

Cổng: apps/learn/e2e/ieltsExamParts.spec.ts (5 case) chạy không mock API, tức là chạy như một người lạ mở link — đó là điều đáng gác nhất ở đây, vì trang trắng khi chưa đăng nhập là lỗi không ai thấy trên máy mình.

5h. Đọc và Nghe cũng có tầng đó, nhưng chia theo hai lối khác nhau (SRC-825, 2026-09-18) ​

Nghe chia theo PHẦN THI, Đọc chia theo DẠNG CÂU HỎI, và đó không phải là sự thiếu nhất quán.

Bài nghe có bốn phần thật, khác nhau thật: Part 1 hội thoại đời thường, Part 2 một người nói về nơi chốn (chỗ hay có bản đồ), Part 3 hai ba người bàn bài vở, Part 4 bài giảng một mạch. Learner gặp đúng bốn phần ấy, theo đúng thứ tự ấy, và mỗi phần đòi một cách chuẩn bị riêng.

Bài đọc thì không. Nó có ba passage, nhưng ba passage chỉ khác nhau về ĐỘ KHÓ, không khác về cách làm; thứ quyết định cách làm là dạng câu hỏi, và một dạng câu hỏi rơi vào passage nào cũng được. Dựng ra "Passage 1 / 2 / 3" rồi gán dạng câu hỏi cho từng passage là dạy một điều không đúng với đề thi thật. Nên với Đọc, mỗi mục là một NHÓM DẠNG CÂU HỎI:

NhómKiểu bên trongSố câu điển hình
Đúng / Sai / Không cóTFNG (sự việc), YNNG (quan điểm)5-7
Nối và ghéptiêu đề đoạn, tìm đoạn chứa thông tin, ghép người, nối nửa câu10-14
Điền vào chỗ trốngcâu rời, tóm tắt/bảng/sơ đồ, ghi chú vào hình, trả lời ngắn10-14
Trắc nghiệmchọn một, chọn hai trở lên5-8

Một cờ trong dữ liệu (GROUPS_QUESTIONS) nói rõ trang đang bày cái nào, và giao diện gọi tên theo đó: "Các dạng câu hỏi" cho Đọc, "Các phần thi" cho ba kỹ năng kia. Gọi tất cả là "phần thi" cho tiện là nói sai với learner ngay ở nhãn.

Lối ra cuối trang khác nhau theo kỹ năng, vì chỗ LÀM THẬT của chúng khác nhau: Đọc và Nghe có cụm bài nằm sẵn trong bundle (/reading/clusters, /listening/clusters), còn Nói và Viết thì bài nộp sống sau đăng nhập (/levels). Một lối ra chung cho cả bốn là một lối ra sai với hai trong bốn.

Thứ tự regex trong router là chỗ dễ sai. /reading/parts cũng khớp khuôn [a-z0-9-]+ của nhánh cụm, nên nếu đứng sau thì nó bị đọc thành "cụm tên là parts" — đúng cái bẫy mà /reading/clusters đã gặp một lần ở SRC-812. Nhánh parts vì thế đặt TRƯỚC.

Cổng: apps/learn/e2e/ieltsExamParts.spec.ts lên 9 case, thêm bốn case cho Đọc và Nghe, vẫn chạy không mock API.

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

Từ mục 5c. Bốn trang kỹ năng và thang ba bậc (SRC-801, chỉ đạo 2026-09-17 vòng 3) ​

Chỉ đạo: "Gom các thông tin trên vào trang kiểu như là settings. Tạo ra các trang riêng cho 4 kỹ năng... Trong mỗi trang đó, learner nhìn thấy dữ liệu, để có động lực... Xuyên suốt là những câu nói động viên, ghi nhận, khen ngợi."

Bốn thẻ kỹ năng là <a href> thật sang ielts.nemo12.com/{reading,listening,speaking,writing}, không phải trang dựng lại ở learn: dựng lại là có hai bản của cùng một thứ và bản thứ hai lạc hậu từ tuần sau. Phiên dùng chung cookie .nemo12.com nên không ai đăng nhập lần thứ hai.

"Đặt các giới hạn, ví dụ limit theo ngày, để learner không dùng quá nhiều, hoặc quá phí API khi gọi vào các hàm LLM."

Từ mục 5d. Một tên miền: mạch luyện chuyển hẳn về learn (SRC-805, 2026-09-18) ​

Chỉ đạo chủ dự án 2026-09-18: "Không được phép link sang ielts.nemo12.com. Cần dùng mọi url trên learn.nemo12.com." Điều này đảo quyết định ở §5c (bốn thẻ kỹ năng trỏ sang tên miền riêng).

Từ mục 5e. Năng lực nhỏ: học lý thuyết rồi kiểm hiểu (SRC-805, 2026-09-18) ​

Chỉ đạo chủ dự án: "Trong mỗi kỹ năng nghe, nói, đọc, viết, thì có các năng lực nhỏ hơn. Ví dụ: xác định main idea, hay inference... thì cần có một chỗ nào đó để 'học lý thuyết', rồi làm các bài tập siêu đơn giản, để đảm bảo là learner hiểu được lý thuyết đó phần nào."

Từ mục 5f. Thang bốn chặng: tiếng Việt trước, tiếng Anh sau (SRC-821, 2026-09-18) ​

Chỉ đạo chủ dự án: "Tôi muốn học sinh được trải nghiệm việc tìm ra ý chính của các đoạn cực ngắn, tầm 3-5 câu thôi, nhưng bằng tiếng Việt. Rồi sau đó luyện tập tiếp rất nhiều kỹ thuật tìm ý chính trên tài liệu tiếng Việt. Sau đó mới chuyển sang tiếng Anh, từ dễ (3-5 câu), rồi nâng dần lên." Và: "Màn hình này cần cực kỳ minimal."

Từ mục 5f. Trang kỹ năng là một hub, phần bị lấy đi có URL riêng (SRC-812, 2026-09-18) ​

Chỉ đạo chủ dự án 2026-09-18: "nhiều thứ lằng nhằng quá. Cần chỉ để lại vài mục chính to to thôi (tối đa 5 mục), click vào nó thì mới đi tiếp tới các trang tiếp theo." — và ngay sau đó, cho ba kỹ năng còn lại: "Cần siêu gọn, cần minimal."

Từ mục 5g. Các phần thi của Nói và Viết (SRC-815, 2026-09-18) ​

Chỉ đạo chủ dự án 2026-09-18: "Writing và Speaking cũng cần làm tương tự như Reading và Listening. Cũng cần tạo ra vài sub-skill trong Writing khi thi IELTS, và vài sub-skill trong Speaking."

Từ mục 5h. Đọc và Nghe cũng có tầng đó, nhưng chia theo hai lối khác nhau (SRC-825, 2026-09-18) ​

Chỉ đạo chủ dự án 2026-09-18: "thêm sub-skill cho Reading và Listening luôn".