Skip to content

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 theo apps/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_attempts chỉ 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) ​

sql
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)
sectiondomain (khoá)Tỉ lệ câu trong một đề
rwcraft_structure · information_ideas · conventions · expression_ideas28 · 26 · 26 · 20 %
mathalgebra · advanced_math · psda · geometry_trig35 · 35 · 15 · 15 %
  • source cấm chứa collegeboard / 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âu approved.
  • 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ào draft, người duyệt đẩy lên approved; nạp production qua seed-data.

3. Cổng và mục tiêu (REQ-SAT-01..03) ​

  • Router: apps/learn/src/App.tsx chuyển mọi /sat và /sat/* sang apps/learn/src/sat/Sat.tsx (cùng khuôn aiTeen/AiTeen.tsx): /sat, /sat/goal, /sat/test/{id}, /sat/report/{id}. SatHub cũ đã 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 qua requireLearner như /v1/ielts/** (SDD-038 §24); authCoverage.test.ts thê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òng learner_goals kind sat để Goal engine có sẵn dùng được.
  • Đề xuất mục tiêu: bảng SCHOOL_GROUPS dùng chung với apps/web/src/site/SatGraphics.tsx (chuyển vào packages/ để 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âu approved củ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 trong sat_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 = 2 cách ≥ 3 ngày thì cleared_at.
  • Lời giải chỉ trả về sau khi câu đã khoá đáp án (REQ-SAT-14); GET câu hỏi không bao giờ mang answer_json hay rationale_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ên domain theo miền, 1 phiên module, và một retest mỗ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|retest gần nhất đã nộp đủ; cả hai ≥ target_total.
  • Dashboard /sat: một lời gọi GET /v1/sat/home trả đủ (đ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_answers ghi một dòng learner_evidence (subject_id='sat', node_id = node của câu, source='sat', type = kind của phiên, observed_performance 0/1, reliability 0.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ạng ielts-effort; dashboard sat-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/sat trả 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.

MethodPath (sau /v1/learners/{learnerId}/sat)ViệcĐợt
GET/PUT/goalđọc / lưu mục tiêu1
GET/homedashboard: mục tiêu, bài đang dở, 5 lần đo gần nhất, đã có đề chưa1
POST/sessionsmở bài đo `{kind: diagnosticretest}`; 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ạn1
PUT/sessions/{id}/answers/{itemId}lưu câu trả lời / đánh dấu; 409 sau hạn + 5 giây1
POST/sessions/{id}/submitnộp module; M1 thì server chọn M21
GET/sessions/{id}/reportbáo cáo + lời giải, chỉ sau khi nộp cả bài1
PUT/answers/{sessionId}/{itemId}/causegắn nguyên nhân sai (phiên chứa câu, kiểm thuộc learner)2
POST/practicemở phiên luyện `{kind: domainmodule
GET/practice/{id}câu của phiên; đáp án chỉ kèm câu đã khoá hoặc khi phiên đã nộp2
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 thi2
POST/practice/{id}/submitnộp; module thì chấm, ghi bằng chứng sat_test, câu sai vào sổ lỗi2
GET/mistakessổ lỗi: câu đang mở, số tới hạn, số đã ra khỏi sổ2
GET/PUT/planlộ 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ần3
GET/effortphú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ày3
GET/summarybả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âu3

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: validateForm chạy trong CI cho mọi form approved.

10. Chia đợt ​

ĐợtGồmREQ
1Khung /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âu01, 02, 04, 05, 07, 19, 23, 24, 25
2Nguyên nhân sai, luyện miền, module có giờ, sổ lỗi, dashboard08, 09, 10, 11, 12, 14, 18
3Lộ trình, đo lại, readiness, ôn giãn cách, effort, phụ huynh, bàn SAT03, 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 ​

REQMụ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