---
url: >-
  https://docs.nemo12.com/architecture/sdd-010-lab-platform/selection-and-metadata.md
description: >-
  Một Experience chọn câu thế nào, smoke test giao diện chạy gì, độ phủ lab đo
  ra sao và vùng metadata của lab hiện gì cho ai.
---

# SDD-010 · Luật chọn câu, smoke test, độ phủ lab và vùng metadata

Một phần của [SDD-010](./index.md).

## 12. Luật chọn câu cho một Experience (SRC-141, SRC-143)

### Vấn đề đã sửa

Đo ngày 2026-08-15: **251/252 bài có dưới 12 câu hỏi**, trung bình **8,5 câu/bài**. Luật cũ cho Luyện tập lấy `LIMIT 12` nên learner **thấy gần như toàn bộ kho** ngay lượt đầu, rồi "Đo giữa"/"Đo cuối" lại lấy 6 câu **từ đúng kho đó**. Bài đo hoá ra chỉ đo xem learner có nhớ mấy câu vừa làm cách đó vài phút không — mà Mastery, Readiness và Learning Plan đều ăn con số này.

Cùng lúc, Học vượt chọn câu bằng `ORDER BY role, difficulty DESC LIMIT 10` — **tất định**. Trượt rồi bấm "Làm lại từ đầu" là gặp lại **đúng 10 câu cũ**; thử vài lần là thuộc đáp án và "vượt" cả unit mà không hề vững.

### Luật mới

Chính sách nằm ở `workers/api/src/modules/learning/selection.ts` — **hàm thuần, 17 test**, không trốn trong `ORDER BY`. Route kéo cả kho của node về (chỉ ~8,5 câu nên rất rẻ) rồi áp luật:

| Luật | Nội dung | Vì sao |
| --- | --- | --- |
| **Để dành câu đo** | ~35% kho (tối thiểu 2 câu), ưu tiên câu `discriminator`/`boundary`, thiếu thì lấy câu khó nhất | Luyện tập **không bao giờ** được đụng vào phần này |
| **Ngưỡng tách** | Kho dưới **6 câu** thì không để dành gì cả | Thà cho luyện đủ còn hơn cắt cả hai bên đều thiếu — nhưng khi đó API trả `pool_warning` nói thẳng "kết quả đo chỉ mang tính tham khảo" |
| **Cỡ một lượt luyện** | **Đúng 10 câu** nếu kho cho phép, chưa đủ thì lấy hết phần dùng được (xem §12b) | Nhịp cố định — learner biết trước bao giờ xong |
| **Luyện thêm (vòng ≥2)** | Ưu tiên câu **chưa gặp trong 30 ngày**, hết mới quay lại câu cũ | Luyện lại không phải là gặp lại y hệt |
| **Bài đo / Học vượt** | Lấy câu để dành trước, rồi câu chưa gặp, rồi câu đã gặp; **luôn xáo theo `session_id`** | Thi lại ra đề khác — bịt lỗ học thuộc 10 câu |
| **Chờ giữa hai lần Học vượt** | Trượt rồi phải chờ **3 tiếng** (`SKIP_COOLDOWN_MS`) | Đề xáo vẫn chưa đủ khi kho mỏng: chờ để lần sau là trí nhớ thật, không phải trí nhớ ngắn hạn |

Xáo dùng seed tất định (LCG lấy bit cao) chứ không `Math.random`: cùng một `session_id` thì tái lập được y hệt khi cần điều tra sự cố.

### Hệ quả phải theo dõi

Luật này chỉ chữa được phần chia; **gốc vẫn là kho câu mỏng**. `docs/reference/item-coverage.md` (sinh bằng `node scripts/audit-items.mjs`) liệt kê từng bài đang dưới ngưỡng và cần soạn thêm bao nhiêu câu.

### 12b. Luật 10 câu mỗi Learning Experience (SRC-153)

Mỗi Learning Experience nhắm **đúng 10 câu**. Lý do không phải là tối ưu sư phạm mà là **quyết định bắt đầu**: biết trước "10 câu là xong" thì đứa trẻ mới dám mở bài ra làm. Trước đây số câu nhảy 4, 6, 8 tuỳ kho mà learner không thấy lý do nào cả.

Hai điều **không được phá** để đạt đủ 10:

1. **Không lấn vào phần kho để dành cho bài đo.** Lấn thì bài đo quay lại đo trí nhớ của phiên luyện vừa xong — đúng lỗi §12 vừa sửa.
2. **Không lặp câu trong cùng một phiên cho đủ số.** Lặp là dạy đáp án, không dạy khái niệm.

Kho chưa đủ thì phiên **ngắn hơn 10** và `pool_warning` nói thẳng con số thật ("Bài này hiện có 6 câu luyện (chuẩn là 10) — kho câu đang được soạn thêm"), hiển thị ngay đầu phiên.

**Điều kiện kho**: cần **16 câu/bài** để có 10 câu luyện (16 − 6 câu để dành = 10). Hiện chuẩn là 10 câu/bài → chỉ ra **6 câu luyện**. Toàn hệ 274 bài × 16 = 4.384 câu, đang có 2.314 → **còn thiếu ~2.070 câu**; 50 bài dưới 6 câu là ưu tiên trước ([item-coverage](../../quality/reports/item-coverage.md)).

**Lấy mẫu câu "vì sao"**: 10 câu × 3 lớp đo là 30 lượt bấm — trẻ nhỏ ngán rồi bấm bừa, mà bấm bừa thì lớp đo thứ 3 thành dữ liệu rác. Nên chỉ hỏi "vì sao con chọn" ở **3 câu đầu** · **câu learner tự nhận là chưa chắc** (tự tin ≤ 2) · **câu đang gặp lại sau khi từng sai**. Ba chỗ đó là nơi câu trả lời có giá trị nhất; các câu còn lại đi thẳng tới kết quả. Cắt khoảng 40% thao tác mà giữ nguyên tín hiệu hiệu chuẩn.

## 13. Smoke test giao diện (SRC-141)

Trang learn nằm sau đăng nhập Google nên trong suốt các đợt làm trước, **không ai mở được giao diện để kiểm trước khi deploy** — mọi kiểm chứng dừng ở "typecheck + build sạch", và hai lỗi ngày 2026-08-15 lọt ra production đều thuộc loại mở trang là thấy ngay.

`apps/learn/e2e/` dựng **bản build thật** rồi **chặn toàn bộ request `/v1/**` và trả dữ liệu giả** (`fixtures.ts`) — không cần tài khoản, không đụng dữ liệu thật, chạy trong CI trước bước deploy. 5 luồng đang canh:

1. Trang gốc hiện các school, nút Lighthouse nằm trên navbar (SRC-120).
2. Breadcrumb đủ chặng và **không còn thanh tab** (SRC-129) — `.n12-tab` phải bằng 0.
3. Phòng Lab dựng được đường đi Unit, **không còn bộ đếm "0/9"** (SRC-116).
4. Làm bài: chọn đáp án → Tiếp → hiện lớp đo độ tự tin (SRC-069).
5. Skip/Exit nằm bên phải và **Skip đi thẳng câu sau, không chấm** (SRC-117).

> ⚠️ Fixture phải giữ **đúng hình dạng** response thật. Đổi response của `/labs`, `/practice/start`, `/cockpit` mà quên sửa `fixtures.ts` thì test vẫn xanh còn production thì gãy.

## 14. Độ phủ lab: mỗi Unit đang hiện có đúng MỘT lab live (SRC-407, SRC-439)

REQ-LAB-08 đặt **trần** "tối đa một lab live mỗi Unit" (`idx_labs_one_per_unit`). Chương trình phủ lab (SRC-399 mở màn, kết ở SRC-439) đặt nốt **sàn** đối xứng: mọi Unit learner nhìn thấy phải có một lab — vì lab là chỗ chạm đầu tiên của Unit, Unit không lab là một ô mở ra không có gì để nghịch. Độ phủ hiện tại: **11/11 môn đang hiện, 0 Unit thiếu lab** (Toán SRC-421 · Tiếng Anh SRC-422 · Vật lí SRC-427 · Sinh SRC-428 · Hoá SRC-429 · Ngữ văn SRC-430 · GenAI/AP/IELTS/SAT SRC-434 · Tin học SRC-436 · đợt chốt SRC-439).

Luật của chương trình, rút từ chính các đợt:

* **Khuôn duy nhất là picker** đã chốt ở SRC-388/390 (đoán bằng cách bấm → bấm từng bước xem điều gì lộ ra → câu kiểm), để mọi lab của một môn đọc lên như một bộ và không phải sửa LabPlayer. Nội dung nhắm **quan niệm sai bền nhất** của Unit; với môn ôn thi/kỹ năng (GenAI · AP · IELTS · SAT) lab nhắm **cách chấm và bẫy hay gặp** thay vì nội dung học thuật.
* **Nhãn nút phải nói ra việc mà cú bấm làm** (SRC-400, SRC-421): cấm nhãn đánh số kiểu "Lựa chọn 1"/"Bước 2" (20/39 lab đợt đầu mắc — learner bấm mù), cấm hai nút trùng nhãn, câu kiểm phải có đáp án đúng. Cổng canh nằm ngay trong `scripts/seed-b21-labs.mjs`, chặn **trước khi ghi** — lab vào production dưới dạng dữ liệu, khuôn lệch là màn trắng mà không test nào đỏ.
* **Seed idempotent và không cướp chỗ** (SRC-427): `ON CONFLICT DO UPDATE` chứ không `INSERT OR REPLACE` (REPLACE trong SQLite là xoá-rồi-chèn, làm treo khoá ngoại `lab_content.lab_id` — lỗi chỉ lộ ở lần chạy THỨ HAI); Unit đã có lab live của người khác thì bỏ qua và in ra, script không có tư cách quyết định lab nào tốt hơn.

## 15. Vùng metadata của lab — ba người đọc, hai vùng khác nhau (SRC-511, SRC-532, SRC-544)

Chủ dự án 2026-08-22, sau khi mở một lab Toán: *"xem xong, tôi không hiểu nó xuất hiện ra để làm
gì."* Đó là lỗi thật của thiết kế: mọi lab vào **thẳng câu hỏi**, không chỗ nào nói vì sao lab này
tồn tại. Cột `objective_vi` có sẵn nhưng chỉ phủ **34/161 lab Toán**, và nó trả lời *"lab dạy gì"* —
không trả lời *"vì sao có lab này"*.

### 15.1 Vùng "Về lab này" — cho learner và mentor (SRC-511)

**Tách theo NGƯỜI ĐỌC, không gộp thành một đoạn văn.** Migration **0068** thêm ba cột nội dung + một
cột thời lượng:

| Cột | Trả lời câu hỏi của ai | Nội dung |
|---|---|---|
| `why_vi` | **mentor**: vì sao có lab này | lý do lab tồn tại trong đường học |
| `notice_vi` | **learner**: làm xong thì nhận ra gì | điều learner sẽ thấy |
| `mentor_vi` | **phụ huynh**: nên hỏi lại con câu gì | một câu hỏi để nói chuyện sau lab |
| `minutes` | cả ba | thời lượng dự kiến |

Gộp ba câu thành một đoạn thì **ai đọc cũng phải lọc qua phần không dành cho mình**, và người bỏ
cuộc trước nhất là learner.

**Vùng meta ĐÓNG SẴN**, sau nút *"Lab này để làm gì?"*. Learner tới lab để nghịch; mở sẵn thì chính
cái lab bị đẩy xuống dưới mép màn hình — cùng nguyên tắc ngân sách chữ ở §10c.

### 15.2 Footer QC — dữ liệu thô cho người rà, đứng cuối cùng (SRC-532, SRC-544)

Chủ dự án 2026-08-24 yêu cầu: với riêng tài khoản `dac2205@gmail.com`, dưới cùng mỗi màn hiển thị
**mọi metadata có thể** của Experience.

**Vị trí là một luật, không phải tuỳ chọn: footer `QcExpMeta` đứng CUỐI CÙNG mỗi màn Experience.**
Nó là dữ liệu thô cho người rà, nên không được chen vào trải nghiệm của learner thường.

* **Hai con số nổi lên đầu** (thứ chủ dự án theo dõi thường xuyên): **Lớp G/12** và **Level B21
  x/12**.
* **Quy đổi thang bắt buộc**: D1 lưu level ở thang **0–100** (quy đổi `midpoint/12*100` trong
  `seed-b21-tree.mjs`), nhưng Building 21 dùng thang **1–12**, nên hiển thị phải **đổi ngược về
  1–12**; giá trị `level(0-100)` vẫn in trong phần dump để đối chiếu. Hiện thang thô là bắt người
  đọc tự quy đổi mỗi lần nhìn.
* **Cổng quyền theo màn**: màn practice dùng **cờ `qc` do server cấp theo email** (cùng cổng với
  vùng kiểm định SRC-454); màn lab **không có phiên practice** nên dùng `owner` do `App` truyền
  xuống. Chấp nhận được vì footer **chỉ hiển thị dữ liệu công khai** — quyền thật vẫn do server canh,
  client không tự nới được gì.
* **Bố cục: lưới 3 cột, mỗi trường một CÂU GIẢI NGHĨA đứng ngay dưới giá trị** (SRC-544). Footer này
  là chỗ chủ dự án **học cách hệ vận hành**, không chỉ là chỗ dump. Bảng giải nghĩa phủ **20
  trường** — ví dụ: `strand` kèm ghi chú SH/DS/HH là strand ẩn; `blocks` là số node đang bị chặn vì
  cần node này làm nền (**số càng lớn thì vá chỗ này càng lời**); `mastery 0.00` kèm `not_assessed`
  nghĩa là **chưa đo**, không phải kém.
* **Trường không có trong bảng giải nghĩa vẫn hiện giá trị thô** — hơn là giấu. Giấu thì người rà
  không biết là hệ không có dữ liệu hay tài liệu chưa theo kịp.
