Skip to content

SDD-038 · Rà soát và audit sân luyện ​

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.

51. Một lượt rà cả sân luyện: mã chết, một bài bị đè, và hai cổng nói dối (SRC-931, 21.09.2026) ​

Mạch tiến độ bài tập (bốn trạng thái, hai cuốn sổ, quy ước ref, luật mở khoá và các ca hỏng) được mô tả gọn ở một chỗ: Tiến độ bài tập IELTS. Mục này và các mục §42 · §46 · §53 giữ lý do của từng quyết định; trang kia nói cái gì đang chạy.

51a. 104 nhánh tiếng Việt không có cửa nào mở ra được ​

SRC-834/835 đã bỏ công tắc VI/EN khỏi sân luyện, và từ hôm ấy useLang() trả về đúng một hằng số "en" (apps/learn/src/ielts/lang.tsx). Nhưng 109 chỗ trong mười hai file vẫn viết lang === "vi" ? <bản Việt> : <bản Anh> — mỗi màn hình vẫn cõng theo trọn một bản tiếng Việt mà không thao tác nào của learner mở ra được.

Đã gỡ nhánh chết ở 104 chỗ. Năm chỗ còn lại đọc item.lang của NỘI DUNG (thang bốn chặng §5f), không phải công tắc giao diện, nên chúng ở lại. Phép biến đổi chạy bằng parser TypeScript chứ không bằng regex: ternary ở đây có cái trải bốn dòng và có cái lồng trong cái khác, và một regex sẽ cắt nhầm đúng những chỗ ấy.

Không một pixel nào đổi. useLang() vốn đã trả "en", nên thứ bị xoá đúng bằng thứ chưa bao giờ hiện ra; 345 test của learn và 1.937 test của api đều xanh trước và sau. Kèm theo là 67 cặp ngoặc JSX thừa mà phép gỡ để lại ({"Passages"} → Passages), mười khai báo lang không còn ai đọc, và năm dòng import { useLang } theo sau chúng.

pick() và t() ở lại, và đó không phải sót tay: chúng chọn giữa *_vi và *_en của DỮ LIỆU nội dung, nơi bản tiếng Anh còn đang lấp dần, và chúng rơi về bản còn lại khi bản kia trống. Gỡ chúng là biến mỗi ô chưa dịch xong thành một chỗ trống không ai hiểu vì sao trống.

51b. Sáu ký hiệu không còn ai gọi ​

useNextOf (Hub.tsx), levelInfo (levels.ts), findTest (data/examTests.ts), useLexicon (data/store.ts), fetchMicroSkills (ielts/api.ts) ở app learn, và parseRef (ieltsSkills/catalog.ts) ở api. Cả sáu là dấu vết của một trang hoặc một lối đi đã bỏ: fetchMicroSkills chẳng hạn là lời gọi của trang danh sách năng lực mà SRC-921 đã gỡ, còn parseRef là nghịch đảo của refOf cho một đường đọc ngược chưa bao giờ dựng.

Phép đo dùng ở đây, ghi lại để lần sau đo cùng cách: một ký hiệu là CHẾT khi không file nào khác nhắc tới nó và trong chính file của nó nó chỉ xuất hiện đúng một lần, tức là chỉ có dòng định nghĩa. Thiếu vế thứ hai thì mọi hàm nội bộ đều bị kết tội oan.

51c. Một bài tập bị đè mất, một bài chưa bao giờ được nạp ​

Đây là chỗ nặng nhất của lượt rà, và nó không nhìn thấy được từ màn hình.

Hai trục soạn bài (§35e) cùng đổ vào MỘT không gian id: scripts/micro-seed.mjs đặt tên bài là <kỹ năng>.<năng lực>.l<bậc>.<thứ tự>, lấy micro của bài nếu có, không thì lấy code của file. Nên một bài ở trục dạng khai micro: "main-idea", bậc 1, thứ tự 3 sinh ra đúng cái id mà bài thứ ba bậc 1 của reading/main-idea.json sinh ra.

Chuyện ấy đã xảy ra:

BàiIdKết cục
reading-tasks/matching-headings.json "Give the paragraph a name"reading.main-idea.l1.3Nạp ở migration 0265, đang chạy
reading/main-idea.json "Five more, same job"reading.main-idea.l1.3Bị đè, chưa từng tới production
reading/main-idea.json "Same five, two languages"reading.main-idea.l2.3Không migration nào nạp, chưa từng tới production

INSERT OR REPLACE không báo lỗi khi trùng khoá, nó chỉ đè. Nên Main Idea chạy trên production với mười bài trong khi kho có mười hai, và dấu hiệu duy nhất là một danh sách ngắn hơn kho hai dòng — không ai đọc ra điều đó từ giao diện.

Đã sửa ở ba chỗ:

  1. reading/main-idea.json đánh lại bài va chạm thành seq: 5. Bài ở trục dạng GIỮ số 3, vì nó đã nằm trên production và /ielts/skills/reading/main-idea/levels/1/3 đã là một URL learner có thể đã lưu — luật CLAUDE.md là link đã gửi đi thì không được gãy.
  2. Migration 0269_main_idea_two_missing_exercises.sql nạp đúng hai bài còn thiếu, sinh bằng micro-seed rồi cắt lấy hai bài (chứ không nạp lại cả file: một diff nhỏ thì đọc được).
  3. scripts/check-micro-content.mjs nay dựng id y như micro-seed dựng và bắt trùng, nêu tên cả hai file. Đã thử ngược: đặt lại va chạm thì cổng đỏ đúng một dòng.

Vì sao seq: 5 chứ không phải seq: 4, và bài học thứ hai của cùng một cái bẫy. Bản sửa đầu tiên chọn số 4, vì trong content/ số 4 còn trống. Nhưng không gian id này còn một người dùng nữa mà content/ không nhìn thấy: migration viết tay. Migration 0268 (SRC-925, hôm trước) đã tự tay dựng reading.main-idea.l1.4 để gom ba câu khởi động lẻ vào một bài, và bài ấy chỉ sống trong SQL. Nạp đè lên nó là INSERT OR REPLACE xoá dòng cũ, rồi khoá ngoại ON DELETE CASCADE cuốn theo cả ba câu — mất nội dung, vẫn không một dòng lỗi nào.

Thứ duy nhất phản đối là phép đếm tổng số câu trong microSkills/routes.test.ts: mong 2.429, nhận 2.426. Đó đúng là công dụng mà chú thích của phép đếm ấy tự nhận, và nó đã làm được việc — nhưng nó chỉ nói "lệch ba", không nói lệch ở đâu. Nên cổng nay đối chiếu thêm với id do migration KHÔNG sinh từ content/ dựng (nhận ra bằng tên bộ sinh nằm trong file), và nói thẳng tên file đã lấy số ấy. Thử ngược: đặt lại seq: 4 thì cổng đỏ đúng một dòng, nêu tên 0268.

51d. Hai cổng nói dối theo hai kiểu khác nhau ​

check-micro-content không chạy ở đâu cả. content/ielts-micro/README.md ghi rõ cổng này gác thành phần từng bậc, nhưng nó không có trong npm run check:code, không có trong CI, không có trong hook. Một cổng như thế tệ hơn không có cổng: người soạn đọc README rồi tin rằng máy đang gác hộ. Chạy tay nó ra 164 dòng lỗi, tất cả rơi vào 11 file của trục DẠNG CÂU HỎI — trục sinh sau nó (SRC-895) và nó chưa biết mặt: nó đòi code trong khi file khai task_type, đòi mười câu ở bậc 3 trong khi một đề Matching Headings là năm tiêu đề, đòi ngữ liệu 110 từ ở bậc 5 trong khi độ dài ở trục này do chính dạng câu hỏi quyết định.

Cổng nay biết cả hai trục, mỗi trục một khuôn, và khuôn của trục dạng đọc từ chính 55 bài đã soạn (năm bài một dạng, năm câu một bài, chỉ bậc 1 · 3 · 5) đúng cách mà dải độ dài cũ đọc từ reading/main-idea. Trần độ dài KHÔNG áp cho trục dạng — dải bậc 3 (25-92 từ) và bậc 5 (48-139 từ) chồng lên nhau thật — nhưng sàn thì giữ, vì một "ngữ liệu" mười từ thì ở dạng nào cũng không còn là bài đọc. Xanh rồi mới nối vào npm run check:code: nối một cổng đang đỏ là chặn deploy của mọi phiên vì một việc không phải của họ.

audit:ielts-content trả 401 kể từ SRC-885. Phép rà nội dung gọi /v1/public/ielts/content/* trần, mà §27 đã khoá cả nhóm ấy sau requireSession. Nó không đọc mã lỗi nên thứ người chạy nhìn thấy là một vệt stack trace của fetch — một câu trả lời sai cho câu hỏi "nội dung có hỏng không". Nay nó nhận token qua NEMO12_SESSION, gửi bằng Authorization: Bearer, và khi thiếu token thì in ra đúng câu lệnh phải gõ thay vì stack trace.

51e. Hai trang chưa từng được nhắc trong docs ​

/ielts/warmup (bộ khởi động ba câu) và /ielts/domain (chọn lĩnh vực của learner) đều là trang sống, đều có lối vào thật — Shell.tsx trỏ tới domain, trang /ielts/courses trỏ tới warmup — nhưng /ielts/domain không xuất hiện ở bất kỳ dòng nào của docs/, còn /ielts/warmup chỉ được nhắc gián tiếp qua nội dung của nó (§17, §48). Ghi lại ở đây để lần sau đọc docs là thấy:

TrangViệc của nóNội dung đến từ
/ielts/warmupBa câu mở đầu, không chấm điểm, dành cho người vừa mở trangielts_content_items kiểu warmup (§17)
/ielts/domainLearner chọn lĩnh vực; kho từ và cụm bài theo lựa chọn ấyielts_content_clusters + hồ sơ learner

51f. Còn mở, và đây là chỗ cần chủ dự án quyết ​

  • Ranh giới thật của §8a hẹp hơn câu chữ của nó. Rà xong thì thấy sân luyện đang theo một luật tinh hơn "mọi trang chỉ tiếng Anh": TÊN và NHÃN tiếng Anh, THÂN BÀI DẠY vẫn tiếng Việt.ExamPart.tsx ghi thẳng điều đó trong một comment (name_en ở tiêu đề, note_vi/measures_vi/ tell_vi/shape_vi ở thân); levels.ts làm đúng thế (short: "Level 1" tiếng Anh, title và why tiếng Việt); microCheckpoints.ts và checkpoints.ts chỉ có bản tiếng Việt và không có trường *_en nào. Ba chỗ ấy nhất quán với nhau và nhất quán với thang bốn chặng §5f — thứ lệch là §8a, chứ không phải mã. Đã thêm một đoạn vào §8a nói rõ ranh giới; nếu chủ dự án muốn thân bài cũng sang tiếng Anh thì đó là một đợt SOẠN LẠI nội dung dạy học, không phải một lượt dịch.
  • Trục dạng câu hỏi mới chỉ có ở Reading. content/ielts-micro/ có reading/ (10 năng lực), writing/ (16 năng lực) và reading-tasks/ (11 dạng). Nghe và Nói chưa có trục dạng nào, nên hai kỹ năng ấy chỉ lọc được theo năng lực. Đó là việc soạn nội dung, không phải việc của mã.
  • NEMO_TO_VND không ai gọi. Hằng số ấy ghi luật "1 Nemo = 1.000đ" (§34b) nhưng việc chuyển khoản diễn ra ngoài đời, nên nó là chữ chứ không phải mã. Để lại có chủ ý, ghi ra đây để lần rà sau không phải điều tra lại.

71. Rà soát cuối phiên 22.09.2026 (SRC-965) ​

  • IeltsLatestTest.tsx đã xoá. Ba lối vào "đã thi / tự đoán / không đoán" (SRC-956) không còn màn nào dẫn tới từ khi trang Goal bỏ đi ở SRC-961. Một file không ai gọi sẽ lệch khỏi phần còn lại mà không ai biết. Lấy lại được: git show 85c9c930:apps/learn/src/IeltsLatestTest.tsx.
  • Learning Plan có ca e2e cho sợi dây lưu. Sửa một ô, bấm Lưu, mở lại trang. Nếu dây đứt ở bất cứ đâu — nút Save không nối, cells gửi lên rỗng, trang không đọc lại bản đã lưu — màn hình vẫn vẽ ra một bảng trông đúng, và learner chỉ phát hiện ở lần mở sau.
  • Một lỗi nữa lộ ra khi viết ca ấy: nhánh "thiếu mục tiêu hoặc thiếu điểm" của Learning Plan KHÔNG có tiêu đề, vì tiêu đề nằm trong header của EditGate ở nhánh kia. Learner mở trang ra và không biết mình đang ở đâu.
  • Hai bài học đã ghi vào trí nhớ dự án: xoa-test-bang-script.md và useregistersave-vong-ve-vo-tan.md.

129. Lượt audit đầu tiên và bộ skill audit (SRC-1086, 27.09.2026) ​

Chủ dự án: test và audit một lượt về UI, logic và nhiều khía cạnh khác; tạo skill chuyên để audit; rồi "fix hết P0 và P1". Bộ skill: /audit (điều phối) và bốn góc /audit-ui, /audit-logic, /audit-language, /audit-security-perf trong .claude/skills/. Bốn agent chạy song song, chỉ đọc và chạy test; phát hiện có bằng chứng, xếp P0 (hỏng/sai dữ liệu), P1 (người dùng gặp), P2 (nâng cấp).

Đã sửa (P0, P1):

  • Speak together cộng giờ sau khi đã rời phòng (P1, chứng minh bằng test): /done nay từ chối khi ended_at có giá trị, và UPDATE đóng buổi thêm AND ended_at IS NULL để một lượt Rời đua với Xong cũng không lọt. Test mới trong speakPair/routes.test.ts.
  • Dò mã phòng 4 số (P1): /speak-pairs/join có rate limit riêng speakPairJoin (20 lần/10 phút), dò hết 9.000 mã mất hơn ba ngày; /done thêm rate limit learningEvent.
  • Bundle learn 2,08 MB (P1): sân luyện IELTS (ielts/Practice) tải lười khi vào /ielts/**; bundle chính còn 1,60 MB, IELTS thành gói riêng 479 KB kèm nội dung Grammar và Speak together.
  • Câu giới hạn của phép đo bằng tiếng Anh (P0, SRC-953): phiếu điểm đề luyện nay nói tiếng Việt và nói rõ "không phải band IELTS chính thức"; ba dòng dặn dò trước khi làm đề cũng tiếng Việt.
  • Lời tour và câu dẫn Learning Plan bằng tiếng Anh (P1, SRC-953): năm lời tour ở trang /ielts và trang kỹ năng, và câu "Đặt mục tiêu, Nemo sẽ lên kế hoạch..." nay tiếng Việt.
  • Dấu "?" 24px trên điện thoại (P1): vùng bấm nới lên 40px bằng một lớp giả trong suốt, hình tròn giữ nguyên (CardHint, TaskTypes, TermTitle).

Không làm, có lý do:

  • Agent UI đề xuất đổi nút Speak together sang tiếng Việt: ngược chỉ đạo SRC-1084 (trang ấy tiếng Anh đơn giản toàn bộ), bỏ.
  • Nhãn nút hành động trong sân luyện đang lẫn Anh và Việt (ExamSit "Back to papers", Grammar "Kiểm tra"): SRC-953 nói nhãn nút tiếng Việt, nhưng chủ dự án cần chốt một chiều trước khi sửa hàng loạt.
  • P2 (bậc Grammar chỉ kiểm ở client, cờ credited sai khi đã cộng trong ngày, polling khi tab ẩn, NavStats không làm mới, h1 ẩn cho trang kỹ năng, sitemap thiếu bài What's new...) để lượt sau.
  • Ca vitest practiceEvidence "xu hướng đổi thì vẫn ghi" đỏ cả khi bỏ mọi thay đổi của lượt này: phụ thuộc thời điểm chạy, không thuộc phạm vi.

132. Audit toàn sân luyện IELTS, lượt 2 (SRC-1097, 28.09.2026) ​

Năm góc (UI, logic, ngôn ngữ, bảo mật-hiệu năng, nội dung) chạy song song trên toàn /ielts/**. Không có P0 ở mã; một P0 ở NỘI DUNG (đáp án đúng thường là lựa chọn dài nhất: 89% ở Reading, 78% ở Listening) chờ chủ dự án duyệt một lượt viết lại bằng workflow. Chủ dự án: "Fix hết P1".

Đã sửa (P1):

  • Cộng giờ không giới hạn: POST /learning-events nay có trần NGÀY 7200 giây cho mỗi learner, cộng mọi nguồn trong sổ (DAILY_CAP_SECONDS); dòng vẫn ghi, chỉ số giây bị cắt về phần còn lại.
  • "Try again" cộng lại trọn phút: số phút cố định của bài chỉ cộng ở lần làm xong đầu tiên; lượt sau tính theo giây thật client đo, không vượt số phút của bài. Hai test mới trong ieltsSkills/routes.test.ts.
  • Bảng xếp hạng quét cả sổ mỗi lần mở: phép tính đệm 60 giây theo isolate, gắn theo binding D1 (leaderboard/service.ts); chậm tối đa một phút.
  • Polling khi tab ẩn: Speak together (3 giây) và lớp Live (2,5 giây) thôi hỏi khi document.hidden, hỏi lại ngay khi quay về tab; đồng hồ 1 giây cũng dừng khi ẩn.
  • Ngôn ngữ (SRC-953/834): đoạn giới thiệu bốn phần thi (ExamPart.tsx) sang tiếng Việt; "Các buổi học" → "Sessions", "Bài nói của con" → "Your recordings", nhãn "Bài viết của con" → "Your writing", lỗi micro sang tiếng Anh, tiêu đề gập của Grammar sang tiếng Anh.
  • Kèm: trang buổi Speak together coi trả lời thiếu start là không có buổi (từng hiện NaN).

Còn chờ: P0 nội dung (viết lại lựa chọn sai), các P2 (rate limit hai route nộp bài, lộ mã learner ở bảng xếp hạng, bundle chính 1,6 MB, đáp án Grammar lệch vị trí, audit-content bỏ qua migration lỗi, chủ đề Grammar không kiểm ở máy chủ).

126g. Ai đã đăng ký buổi Speak together (SRC-1098, 28.09.2026) ​

Trang chi tiết buổi có thẻ "Who is coming (n)": ảnh đại diện hoặc chữ cái đầu và tên hiển thị của từng người đã đăng ký, theo thứ tự đăng ký, đánh dấu "(you)" cho chính mình. API chi tiết trả attendees chỉ gồm tên, ảnh, cờ me (không email, không mã nội bộ), tối đa 60 người. Đăng ký hoặc huỷ xong thì trang tải lại chi tiết để danh sách cập nhật.

137. Làm nốt việc tồn (SRC-1125, 28.09.2026) ​

Chủ dự án: "Còn nhiều việc vẫn chưa được xử lý. Hãy làm nốt." Các P2 của lượt audit 2 (§132) và các chỗ hụt ghi lại trong những lượt sau:

  • Rate limit hai route nộp bài có chấm điểm: ielts-exam-quiz/submit và ielts-diagnostic/submit nay gọi enforceRateLimit(RATE_LIMITS.submit) như ieltsPractice.
  • Bảng xếp hạng không lộ mã learner khác: ba route đọc (/v1/ielts/leaderboard, /weeks, /weeks/{w}) thay learner_id của người khác bằng anon-<n>; chỉ mã của người xem giữ nguyên để màn tô dòng của mình.
  • Chủ đề Grammar kiểm ở máy chủ: POST /ielts-grammar chỉ cộng cho mã có trong GRAMMAR_TOPIC_IDS (sinh bởi scripts/gen-grammar-topics.mjs từ file nội dung; test gác khi quên sinh lại). Trước đây mã bịa vẫn ăn 120 giây.
  • Đáp án Grammar lệch về vị trí đầu: TaskCard xáo lựa chọn khi hiển thị, cố định theo bài (shuffled); test gác tỉ lệ đáp án đứng đầu dưới 50%.
  • audit-content không bỏ qua im lặng migration không chạy được trên sqlite3: in tên ra.
  • Vân tay video: src/catalog.ts (danh sách import sinh tự động) ra khỏi vân tay mã dựng hình, để thêm một video không làm mọi video cũ bị coi là đã đổi. Lượt này đổi vân tay của cả kho MỘT lần.
  • Tài liệu API: tham số program của speak-schedule (SRC-1120) nay khai trong OpenAPI.
  • Trang Grammar công khai: câu "đang được soạn dần" thay bằng "cả 103 bài đã có nội dung".
  • Bài đo 6 câu cũ trên trang hồ sơ IELTS của cổng learn thay bằng lối vào bài chẩn đoán mới (/ielts/diagnostic); kết quả cũ vẫn hiện ở cột "Bài đo".

Còn chờ chủ dự án: P0 nội dung (đáp án đúng thường là lựa chọn dài nhất trong kho đọc/nghe; nội dung chỉ nằm trong D1 nên cần một lượt riêng), link Zoom, bốn link nhóm Zalo, video cho 62 bài Grammar, thương hiệu video Grammar, và việc sau tháng 10/2026 của program Speaking.

164. Bốn luật từ audit 03.10.2026 (SRC-1204) ​

Audit toàn bộ code ngày 03.10.2026 tìm ra bốn chỗ hành vi sản phẩm lệch với ý định. Chủ dự án giao "làm nốt", nên đây là phương án đã chọn - đổi được, nhưng đổi thì sửa ở đây trước.

  • Đồng hồ dùng thử chỉ chạy khi chính learner mở. Đọc gói IELTS/Grammar hay thưởng học tập từ phiên bố mẹ, mentor, admin trước đó trả not_started: true và không ghi dòng nào; trước đó lượt đọc của người khác bắt đầu 14 ngày thử thay learner.

  • Ngày thi gợi ý không phải hạn chót. Chỉ ngày learner đã xác nhận (confirmed = 1, hoặc lịch học kỳ đã khai) mới tạo mục tiêu, cam kết, chế độ luyện thi và đếm ngược. Kèm theo: Planning trước đó không đọc được ngày thi nào vì câu truy vấn lỗi bị nuốt im lặng; đã sửa.

  • Giới hạn học thử Speaking do server quyết. Mọi learner chưa là thành viên IELTS trả phí còn hạn đều chịu giới hạn buổi thử, không còn phụ thuộc tham số ?program do client gửi.

  • Trần 2 giờ học mỗi ngày áp cho mọi nguồn ghi giờ (ngữ pháp, lượt nói, bài thang bậc, buổi nói đôi) qua một hàm ghi có kẹp trần, không chỉ đường ghi hoạt động chung.

  • Mã phòng Speak together giữ 3 chữ số (quyết định 30.09.2026), nhưng mỗi tài khoản chỉ được nhập sai 10 lần một ngày; nhập đúng không tốn lượt.

  • Đáp án không còn xuống trình duyệt trước khi làm bài. Đề full test ghép từ chính câu luyện tập (useComposedTests), nên trước đây đáp án nằm sẵn trong tab mạng. Chủ dự án chọn giữ MỘT ngân hàng câu và chấm luyện tập ở server:

    • Đường nội dung (/v1/public/ielts/content/*) bỏ answer/accept/why, thêm uid (mã câu đầy đủ) và picks (số phương án phải chọn).
    • POST /v1/learners/{id}/ielts-content/check chấm từng lô câu SAU khi learner trả lời, trả correct kèm đáp án và lời giải; gác bằng phiên + quyền với learner, trần 150 lượt/ngày. Logic đúng/sai dùng chung với scoreObjective (ieltsContent/grade.ts).
    • Reading, Listening, Warmup, ExamSit gọi đường chấm, có trạng thái chờ và nút thử lại.
    • Script soát nội dung và script sinh fixture e2e lấy khoá bằng ?keys=1 với phiên staff/admin; learner gửi keys=1 nhận đúng bản không khoá. Bản có khoá trả no-store.

    Còn hở có chủ ý: learner gọi tay đường chấm vẫn lấy được khoá từng câu, trong giới hạn 150 lượt một ngày. Đóng hẳn đòi server tự ghép đề (biết câu nào thuộc lượt thi đang mở). Hai phép chuẩn hoá câu điền từ: từ SRC-1225 trang luyện chấm đúng nếu khớp theo MỘT trong hai phép (bản luyện cũ hoặc bản của đề đầy đủ), nên "half-past six" không còn sai khi luyện mà đúng khi thi, và không câu nào đang đúng lại thành sai. Đề đầy đủ giữ nguyên normaliseStrict, điểm đã chấm không đổi.

  • Server tự ghép đề và giữ tập câu của lượt (SRC-1229). Đóng chỗ hở ở đoạn trên bằng ba lớp:

    • (a) POST .../ielts-full-tests/{n}/attempts tự ghép đề n (ieltsFullTests/compose.ts, hàm thuần chép phép xếp của FullTests.tsx/examTests.ts; test fullTestCompose.test.ts ở app so hai bên trên cùng dữ liệu) và lưu vào papers_json bốn mã đề kèm question_uids (40 câu Listening, 40 câu Reading đúng như trang làm bài rút). Mã đề app gửi lên bị bỏ qua. Khi chấm, scoreObjective chỉ tính câu trong tập: câu ngoài tập bị bỏ và đếm vào ignored, câu trong tập không gửi tính là bỏ trống. Lượt mở trước SRC-1229 được dựng tập câu lúc bắt đầu phần.
    • (b) Đường chấm luyện ghi mọi câu đã trả đáp án vào ielts_content_keys_seen (migration 0331). Lúc ghép, trong các đề của một kỹ năng server chọn đề có ít câu đã lộ nhất; hoà thì theo vị trí bắt đầu từ đề thứ n, và không lặp đề learner đã làm ở ô khác trừ khi hết đề. Không bao giờ chặn đề vì mọi đề đều đã lộ. Learner chưa xem đáp án câu nào thì nhận đúng đề như app ghép.
    • (c) Trong lúc phần L/R của một lượt in_progress chưa nộp, đường chấm luyện trả 403 TEST_IN_PROGRESS cho câu thuộc tập của phần ấy. Nộp phần xong thì hỏi lại được, nên trang làm đề vẫn lấy đáp án ngay sau khi nộp như cũ. Chỉ khoá với lượt mở chưa quá 24 giờ: lượt bỏ dở không có cơ chế hết hạn, và khoá theo trạng thái thì câu của nó bị khoá ở trang luyện mãi mãi.
    • Cùng đợt: uuid trong @cloudflare/realtimekit ép lên 11.1.1 bằng overrides (lỗ hổng GHSA-w5hq-g745-h8pq ở bản 8.3.2; realtimekit chỉ dùng v4 và validate).

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

Từ mục 51. Một lượt rà cả sân luyện: mã chết, một bài bị đè, và hai cổng nói dối (SRC-931, 21.09.2026) ​

Chỉ đạo chủ dự án 2026-09-20: "Rà soát và dọn dẹp code của learn.nemo12.com/ielts cho gọn gàng nhé, và đối chiếu với docs.nemo12.com xem sao. Rồi đồng bộ cả hai bên. Docs thiếu thì bổ sung docs, chỉ liên quan tới IELTS thôi, còn tính năng thiếu hoặc data thiếu thì chỉnh data."