SDD-044 — NEMO SAT
Yêu cầu: PRD-004. Trạng thái draft, quyết định của chủ dự án ngày 24.09.2026 ở PRD-004 §10 đã áp vào thiết kế này.
1. Hình dạng tổng thể
learn.nemo12.com/sat/** apps/learn/src/sat/ (sub-app, cùng khuôn apps/learn/src/ielts/)
│ fetch, cookie phiên
api.nemo12.com/v1/sat/** workers/api/src/modules/sat/ learner: goal, tests, practice, report
api.nemo12.com/v1/admin/sat/** workers/api/src/modules/satContent/ staff: soạn, duyệt, ghép đề
│
D1 nemo12-platform sat_items · sat_passages · sat_forms · sat_form_modules
sat_goals · sat_sessions · sat_answers · sat_mistakes
→ learner_evidence (có sẵn) · learner_goals (có sẵn, kind 'sat')Nguyên tắc chọn:
- Tái dùng khuôn IELTS, không tái dùng mã IELTS. Khung (
Shell,Chrome, cổng đăng nhập, router con) chép theoapps/learn/src/ielts/; logic đề và điểm viết riêng vì luật SAT khác hẳn (module theo độ khó, thang 200-800, đáp án tự điền). - Một bảng phiên, một bảng câu trả lời cho mọi ngữ cảnh (bài đo, luyện miền, module có giờ, ôn sổ lỗi).
ielts_practice_attemptschỉ lưu tổng đúng/sai nên không dựng lại được báo cáo theo miền; SAT cần từng câu. - Đánh số migration bằng
npm run migration:new, không tự +1.
2. Mô hình nội dung (REQ-SAT-23..26)
sat_passages (id, section CHECK IN ('rw','math'), body_md, source, license, created_by, ...)
sat_items (
id, section 'rw'|'math', domain TEXT, -- 8 miền, xem bảng dưới
skill TEXT, node_id TEXT, -- node SA1-SA5 (knowledge/sat.md)
difficulty INTEGER CHECK (1..3),
passage_id NULL REFERENCES sat_passages,
stem_md, format CHECK IN ('mcq','spr'),
choices_json NULL, -- 4 lựa chọn khi mcq
answer_json, -- mcq: "B"; spr: ["3/4","0.75",".75"]
rationale_md, source, license,
status CHECK IN ('draft','review','approved','retired'),
author_id, reviewer_id, -- reviewer <> author (REQ-SAT-24, CHECK)
calibrated_p REAL NULL, attempts_n INTEGER DEFAULT 0
)
sat_forms (id, title, status, routing_json) -- ngưỡng module 1 → M2 dễ/khó theo phần
sat_form_modules (form_id, section, stage CHECK IN ('m1','m2_easy','m2_hard'), position, item_id)| section | domain (khoá) | Tỉ lệ câu trong một đề |
|---|---|---|
| rw | craft_structure · information_ideas · conventions · expression_ideas | 28 · 26 · 26 · 20 % |
| math | algebra · advanced_math · psda · geometry_trig | 35 · 35 · 15 · 15 % |
sourcecấm chứacollegeboard/bluebook(CHECK + test), đúng REQ-SAT-24.- Kiểm ghép đề (
satContent/validateForm.ts): đủ 27/22 câu mỗi module, phân bố miền lệch ≤ 1 câu so với bảng, không câu nào lặp trong một đề, mọi câuapproved. - Kho câu soạn mới (PRD-004 §10, quyết định 5: không dùng lại
scripts/items-sat-pearl.json). AI soạn nháp vàodraft, người duyệt đẩy lênapproved; nạp production quaseed-data.
3. Cổng và mục tiêu (REQ-SAT-01..03)
- Router:
apps/learn/src/App.tsxchuyển mọi/satvà/sat/*sangapps/learn/src/sat/Sat.tsx(cùng khuônaiTeen/AiTeen.tsx):/sat,/sat/goal,/sat/test/{id},/sat/report/{id}.SatHubcũ đã bỏ; bốn lối vào nội dung Squid nay là khối Courses trên trang chủ/sat. - API
/v1/sat/**đi quarequireLearnernhư/v1/ielts/**(SDD-038 §24);authCoverage.test.tsthêm tiền tố. sat_goals (learner_id PK, target_total, target_rw, target_math, test_date, deadline, school_group, updated_at)và đồng bộ một dònglearner_goalskindsatđể Goal engine có sẵn dùng được.- Đề xuất mục tiêu: bảng
SCHOOL_GROUPSdùng chung vớiapps/web/src/site/SatGraphics.tsx(chuyển vàopackages/để một nguồn); mục tiêu = cận trên của dải nhóm, chia hai phần theo tỉ lệ hiện tại của learner (mặc định 50/50).
4. Bài đo và điểm (REQ-SAT-04..09)
Phiên: sat_sessions (id, learner_id, kind CHECK IN ('diagnostic','retest','domain','module','review'), form_id NULL, section NULL, stage NULL, started_at, deadline_at, submitted_at, result_json). Mỗi module của bài đo là một phiên con (parent_id) để hết giờ một module không mất cả bài.
Định tuyến: nộp M1 → server đếm câu đúng → so routing_json (mặc định: R&W ≥ 15/27, Math ≥ 13/22 → M2 khó) → trả về M2. Server quyết, không để client quyết, vì client nói dối được.
Đồng hồ: deadline_at do server đặt; client đếm ngược từ đó; câu trả lời nộp sau deadline_at + 5s bị bỏ. Mất mạng giữa chừng: câu trả lời lưu cục bộ và gửi lại, server nhận theo answered_at.
Điểm ước lượng: không có bảng quy đổi chính thức (PRD-004 §3.3). v1 dùng bảng nội bộ theo (section, stage m2, số câu đúng tổng) → điểm 200-800, dựng từ phân bố độ khó của đề và hiệu chỉnh khi có điểm thi thật learner tự khai. Kết quả luôn mang estimate: true, band: ±40.
Mức 7 bậc mỗi miền: tỉ lệ đúng có trọng số độ khó (1/1.5/2) của các câu thuộc miền trong phiên, cắt thành 7 bậc theo ngưỡng cố định trong modules/sat/scoring.ts.
Nguyên nhân sai (REQ-SAT-08): sat_answers.cause CHECK IN ('knowledge','misread','pacing','careless') NULL; gợi ý mặc định: pacing nếu bỏ trống hoặc trả lời trong 10 giây cuối module; careless nếu cùng skill đã đúng ≥ 2 lần trước đó; còn lại knowledge. Learner đổi được.
5. Luyện (REQ-SAT-10..14)
domain: chọn 10 câuapprovedcủa miền/node, ưu tiên câu chưa làm, độ khó quanh mức hiện tại.module: lấy một module từ đề chưa dùng, đúng giờ thi.review: câu trongsat_mistakes (learner_id, item_id, cause, due_at, streak, cleared_at); lịch 1-3-7-14 ngày lấy từ Retention (SDD-017);streak = 2cách ≥ 3 ngày thìcleared_at.- Lời giải chỉ trả về sau khi câu đã khoá đáp án (REQ-SAT-14);
GETcâu hỏi không bao giờ manganswer_jsonhayrationale_md.
6. Lộ trình, đo lại, sẵn sàng (REQ-SAT-15..18)
modules/sat/plan.ts: đầu vào (ngày thi, giờ/tuần, 3 miền hổng, điểm hiện tại, mục tiêu) → danh sách tuần; mỗi tuần: số phiêndomaintheo miền, 1 phiênmodule, và mộtretestmỗi 3-4 tuần. Điểm mong đợi mỗi mốc: nội suy tuyến tính hiện tại → mục tiêu, trừ 2 tuần đệm cuối.- Lưu
sat_plans (learner_id, plan_json, version, created_at); kéo dời mốc tạo version mới. - Readiness: truy vấn 2 phiên
diagnostic|retestgần nhất đã nộp đủ; cả hai ≥target_total. - Dashboard
/sat: một lời gọiGET /v1/sat/hometrả đủ (điểm gần nhất, mục tiêu, ngày còn lại, ≤ 3 việc hôm nay, chuỗi ngày) để màn đầu không chờ nhiều lượt gọi (bài học SRC-992).
7. Bằng chứng, phụ huynh, mentor (REQ-SAT-19..22)
- Mỗi
sat_answersghi một dònglearner_evidence(subject_id='sat',node_id= node của câu,source='sat',type= kind của phiên,observed_performance0/1,reliability0.9 cho diagnostic/module, 0.6 cho domain/review,difficulty). Ghi trong cùng batch D1. GET /v1/sat/effort(REQ-SAT-20), cùng hình dạngielts-effort; dashboardsat-learners(SDD-037 §7.8) đọc từ đây, và bàn SAT thêm cột "21 ngày không có attempt".- Marlins:
GET /v1/parent/learners/:id/sattrả mục tiêu, mốc, điểm các lần đo, readiness; kiểm quan hệ guardian như các route parent hiện có.
8. API
Mọi đường learner nằm dưới /v1/learners/{learnerId}/sat (đổi từ /v1/sat của bản nháp 0.1 khi code đợt 1, 24.09.2026): một phụ huynh có nhiều con, và quyền kiểm theo từng learner bằng canAccessLearner như sân IELTS. Code: workers/api/src/modules/sat/routes.ts, admin.ts, luật chấm thuần trong scoring.ts.
| Method | Path (sau /v1/learners/{learnerId}/sat) | Việc | Đợt |
|---|---|---|---|
| GET/PUT | /goal | đọc / lưu mục tiêu | 1 |
| GET | /home | dashboard: mục tiêu, bài đang dở, 5 lần đo gần nhất, đã có đề chưa | 1 |
| POST | /sessions | mở bài đo `{kind: diagnostic | retest}`; bài đang dở thì trả lại chính nó |
| GET | /sessions/{id} | module đang mở (không đáp án, không độ khó); tự nộp module quá hạn | 1 |
| PUT | /sessions/{id}/answers/{itemId} | lưu câu trả lời / đánh dấu; 409 sau hạn + 5 giây | 1 |
| POST | /sessions/{id}/submit | nộp module; M1 thì server chọn M2 | 1 |
| GET | /sessions/{id}/report | báo cáo + lời giải, chỉ sau khi nộp cả bài | 1 |
| PUT | /answers/{sessionId}/{itemId}/cause | gắn nguyên nhân sai (phiên chứa câu, kiểm thuộc learner) | 2 |
| POST | /practice | mở phiên luyện `{kind: domain | module |
| GET | /practice/{id} | câu của phiên; đáp án chỉ kèm câu đã khoá hoặc khi phiên đã nộp | 2 |
| PUT | /practice/{id}/answers/{itemId} | domain/review: chốt đáp án, trả lời giải ngay, khoá câu (409 nếu gửi lại); module: lưu như phòng thi | 2 |
| POST | /practice/{id}/submit | nộp; module thì chấm, ghi bằng chứng sat_test, câu sai vào sổ lỗi | 2 |
| GET | /mistakes | sổ lỗi: câu đang mở, số tới hạn, số đã ra khỏi sổ | 2 |
| GET/PUT | /plan | lộ trình hiện hành kèm slip và readiness / dựng lại theo hours_per_week (tạo phiên bản mới trong sat_plans) | 3 |
| PUT | /plan/weeks/{week} | learner dời mốc: bật/tắt bài đo lại của một tuần | 3 |
| GET | /effort | phút (phiên đã nộp, mỗi phiên tối đa 75), số câu, độ chính xác 7/30 ngày, số câu theo ngày 14 ngày | 3 |
| GET | /summary | bản cho phụ huynh (Marlins): mục tiêu, điểm các lần đo, mốc, sẵn sàng, mốc trượt; không có từng câu | 3 |
Staff (/v1/admin/sat, vai admin): GET/POST /items, GET /items/{id}, POST /items/{id}/review (approve/reject/retire, người duyệt khác người soạn), GET/POST /forms, PUT /forms/{id}/modules/{section}/{stage}, GET /forms/{id}/validate, POST /forms/{id}/publish.
Đợt 2 (code 25.09.2026, migration 0291). Phiên luyện lấy câu từ sat_session_items (bài đo lấy từ đề). sat_answers.revealed_at là khoá lời giải (REQ-SAT-14). Sổ lỗi thêm last_correct_at để thi hành "đúng hai lần cách nhau ≥ 3 ngày thì ra khỏi sổ". Lịch ôn 1-3-7-14 ngày tính trong scoring.ts (nextDue, afterRetry), CHƯA đi qua engine Retention (SDD-017): REQ-SAT-13 vì vậy còn ⏳. Báo cáo bài đo trả thêm causes (đã gắn hoặc gợi ý) và priorities (ba miền mất nhiều điểm nhất, nhân tỉ trọng miền); /home trả tasks (≤ 3), streak (ngày giờ Việt Nam) và mistakes_due. Giao diện: apps/learn/src/sat/Practice.tsx (/sat/practice, /sat/practice/{id}, /sat/mistakes).
Đợt 3 (code 25.09.2026). Luật lộ trình, mốc trượt và sẵn sàng là hàm thuần trong workers/api/src/modules/sat/plan.ts; đường đi trong progress.ts. Hạn ôn sổ lỗi nay tính bằng engine Retention (reviewWindowDates + initialStability, cùng cách ieltsSkills/next.ts dùng), thay cho bảng 1-3-7-14 viết tay. Phụ huynh: thẻ "SAT của con" trong Portrait của Marlins (apps/marlins/src/Portrait.tsx), gọi /summary. Bàn SAT: dashboard sat-learners thêm hai cột sat_score (điểm ước lượng lần đo gần nhất) và sat_weakest (miền mất nhiều điểm nhất); tín hiệu "21 ngày không có attempt" đã có sẵn qua stalledAfterDays: 21, vì mọi câu trả lời SAT đều ghi learner_evidence. Giao diện learner: apps/learn/src/sat/PlanPage.tsx (/sat/plan).
Giờ nghỉ (thêm khi code): module đầu của phần Math mở với deadline_at = NULL; lượt GET đầu tiên sau giờ nghỉ mới đặt hạn 35 phút. Nhờ vậy 10 phút nghỉ không ăn vào giờ làm bài, và learner bỏ đi giữa giờ nghỉ thì bài vẫn chờ nguyên ở đó.
9. Kiểm chứng
- Behavior test (
/api-test) trên D1 thật: định tuyến M2 theo ngưỡng, hết giờ bỏ câu muộn, không lộ đáp án trước khi khoá, reviewer ≠ author, nguồn College Board bị chặn, học sinh A không đọc được phiên của B, phụ huynh chỉ đọc con mình. - E2E (
/e2e-check): làm hết một bài đo rút gọn (form test 4 × 3 câu) tới báo cáo. - Cổng nội dung:
validateFormchạy trong CI cho mọi formapproved.
10. Chia đợt
| Đợt | Gồm | REQ |
|---|---|---|
| 1 | Khung /sat/**, mục tiêu, bảng nội dung, người chơi đề, bài đo + báo cáo, admin nạp câu | 01, 02, 04, 05, 07, 19, 23, 24, 25 |
| 2 | Nguyên nhân sai, luyện miền, module có giờ, sổ lỗi, dashboard | 08, 09, 10, 11, 12, 14, 18 |
| 3 | Lộ trình, đo lại, readiness, ôn giãn cách, effort, phụ huynh, bàn SAT | 03, 06, 13, 15, 16, 17, 20, 21, 22, 26 |
11. Cố ý không làm
- Không dùng IRT ở v1: cần hàng nghìn attempt mỗi câu mới hiệu chỉnh được; bảng quy đổi nội bộ + nhãn "ước lượng" trung thực hơn một mô hình trông khoa học mà thiếu dữ liệu.
- Không cho AI tự đẩy câu vào
approved: AI được soạn nháp, người duyệt quyết. - Không dựng ứng dụng thi mô phỏng Bluebook toàn màn hình khoá trình duyệt.
12. Trace
| REQ | Mục |
|---|---|
| REQ-SAT-01..03 | §3 |
| REQ-SAT-04..09 | §4 |
| REQ-SAT-10..14 | §5 |
| REQ-SAT-15..18 | §6 |
| REQ-SAT-19..22 | §7 |
| REQ-SAT-23..26 | §2 |