Skip to content

Readiness Model ​

"Nếu thi cái đích cụ thể này vào hôm nay thì sao?"

Thiết kế: SDD-002 §17, SDD-008 §5 · Hàm thuần: modules/knowledge/mastery.ts → computeReadiness() · I/O: modules/models/service.ts → runReadinessEngine()


1. Luật nền: Readiness luôn gắn một Target ​

Không có "readiness chung chung". Câu "con sẵn sàng 68%" mà không nói sẵn sàng cho cái gì là câu vô nghĩa.

MasteryReadiness
Câu hỏiEm biết tới đâu?Em đủ cho kỳ thi này chưa?
Phạm viMột nodeMột blueprint (đề thi)
Đổi khiCó bằng chứng mớiBằng chứng mới hoặc đổi mục tiêu
Cùng một học sinhMột giá trị mỗi nodeNhiều giá trị — mỗi target một điểm

Cùng một đứa trẻ có thể sẵn sàng 85% cho thi học kỳ và 40% cho thi chuyên tại cùng một thời điểm, vì hai đề hỏi những thứ khác nhau với trọng số khác nhau. Gộp thành một con số là xoá mất chính thông tin cần dùng.

Hệ quả: không có Readiness Model nếu không có goal. summary.empty = true khi learner chưa khai đích nào — và đó là hợp lệ, đúng như Goal Model.


2. Blueprint — cái đích được mô tả bằng gì ​

BảngNội dung
blueprintssubject_id, title_vi, kind (conditional/specialized), cut_score, max_score
blueprint_weights(blueprint_id, node_id) → weight + required_state

required_state là mức cần đạt của node đó trong đề này, quy ra ngưỡng mastery:

ts
REQUIRED_THRESHOLD = { chac: 0.8, lung_lay: 0.5, hong: 0, unknown: 0 }

hong/unknown → ngưỡng 0 = đề này không yêu cầu node đó. Một node có thể là cốt lõi trong đề chuyên nhưng không xuất hiện trong đề học kỳ.


3. Thuật toán computeReadiness() ​

với mỗi node trong blueprint:
    threshold = REQUIRED_THRESHOLD[required_state]
    m         = mastery của learner (0 nếu chưa đo)
    achieved  = threshold > 0 ? clamp01(m / threshold) : 1
    achievedWeight += weight × achieved
    totalWeight    += weight
    nếu m < threshold → ghi vào gaps { weight, deficit = threshold − m }

ratio           = achievedWeight / totalWeight
estimated_score = ratio × max_score
on_track        = estimated_score ≥ cut_score

Ba chi tiết quyết định:

clamp01(m / threshold) — vượt ngưỡng không được cộng thêm. Học sinh đạt mastery 0.95 ở node chỉ cần 0.5 vẫn tính là achieved = 1, không phải 1.9. Giỏi vượt mức ở một chỗ không bù được lỗ hổng ở chỗ khác — đúng như đề thi thật: làm hoàn hảo câu 1 không giúp gì cho câu 5.

Node chưa đo tính m = 0. Bi quan có chủ đích: chưa có bằng chứng thì không được giả định biết. Nhưng vì thế confidence (§4) trở thành bắt buộc — nếu không, learner mới sẽ thấy readiness 5% và tưởng mình dốt, trong khi sự thật là hệ thống chưa đo gì.

Gap xếp theo weight × deficit, không theo mức yếu.

NodeTrọng sốThiếuweight × deficit
A — hổng nặng, đề hỏi ít10.70.7
B — yếu nhẹ, đề hỏi nhiều50.21.0 ← ưu tiên hơn

Vá node B trước có lợi hơn cho điểm số dù nó "ít hổng" hơn. Xếp hạng theo mức yếu là bản năng tự nhiên nhưng sai về mặt tối ưu.

Chia ba mức: top 1/3 = critical, giữa = important, còn lại = nice_to_have.


4. Confidence — điểm chỉ đáng tin bằng phần đã thật sự đo ​

target.confidence = Σ(weight × confidence_của_node) / Σ(weight)      // node chưa đo tính 0
model.confidence  = trung bình confidence của các target
                  = 0 nếu không có target nào

Trung bình có trọng số theo weight: chưa đo node chiếm 30% số điểm của đề nguy hiểm hơn nhiều so với chưa đo node chiếm 2%.

targets.length === 0 → confidence = 0, không phải 1. Ghi rõ trong code: "chưa biết" khác "chắc chắn đủ".

Đây là cặp số phải luôn đọc cùng nhau:

readinessconfidenceNghĩa thật
0.300.85Đã đo kỹ — thật sự đang thiếu
0.300.10Hầu như chưa đo gì — chưa biết, đừng hoảng
0.800.20Đoán lạc quan trên vài node — cần đo thêm trước khi tin

Hiện readiness và confidence đều nằm trong model, nhưng UI chưa buộc phải hiển thị cặp — xem §8.


5. largest_uncertainty — Readiness sinh ra nhu cầu đo ​

Đây là phần khiến Readiness khác một bản báo cáo thuần tuý.

lọc node có confidence < 0.5
xếp theo weight × (1 − confidence) giảm dần
lấy top 5 → next_assessment_priority = 3 node đầu

Kết quả là danh sách "cần đo", hoàn toàn khác danh sách "cần học" (gap_map):

gap_maplargest_uncertainty
Trả lờiBiết là đang yếuChưa biết có yếu hay không
Hành độngHọc / luyệnLàm bài chẩn đoán ngắn
Xếp theoweight × deficitweight × (1 − confidence)

Một node quan trọng mà chưa đo là rủi ro lớn hơn một node quan trọng đã biết là yếu — vì cái đã biết thì có kế hoạch, cái chưa biết thì không. Vì vậy model chủ động chỉ ra "nên đo cái gì tiếp theo", và cockpit biến nó thành hành động assess.


6. Cơ chế CẬP NHẬT ​

Blueprint nào được chấm — ba nguồn ​

sql
WHERE b.id IN (SELECT blueprint_id FROM learner_goals WHERE learner_id=?1 AND status='active')
   OR b.id IN (SELECT goal_blueprint_id FROM learner_context_models WHERE learner_id=?1 …)
   OR b.id IN (SELECT blueprint_id FROM learner_goal_entries WHERE learner_id=?1 AND status='active' …)
NguồnLà gì
learner_goalsBảng cũ, learner tự chọn blueprint mục tiêu
learner_context_models.goal_blueprint_idBlueprint theo bối cảnh school/lớp
learner_goal_entriesProjection của Goal Model

DISTINCT khử trùng khi cùng một blueprint đến từ nhiều nguồn. Ba nguồn là dấu vết của quá trình phát triển — learner_goals có trước Goal Model. Hợp nhất được thì tốt, nhưng bỏ nguồn cũ khi chưa migrate dữ liệu sẽ làm learner cũ mất mục tiêu.

Chạy ở đâu ​

WF-04 step 4 — step cuối, sau Goal → Context → Learner. Phải sau Goal vì cần biết goal trỏ tới blueprint nào.

Trigger giống WF-04: AssessmentCompleted, GoalChanged, EXAM_RESCHEDULED, admin_recompute.

Lưu version qua saveModelVersion() (model_kind='readiness') — chỉ tăng version khi hash nội dung đổi. Nộp bài mà không node nào trong blueprint đổi → engine vẫn chạy (có dòng engine_runs) nhưng version giữ nguyên.

Output ​

jsonc
{
  "algorithm_version": "readiness-v1",
  "confidence": 0.62,
  "targets": [{
    "blueprint_id": "…", "title": "Thi vào 10 — Toán điều kiện", "subject_id": "math",
    "score": 0.68, "estimated_score": 6.8, "cut_score": 5, "max_score": 10,
    "on_track": true, "confidence": 0.62,
    "gap_map": [{ "node_id": "…", "required": 0.8, "current": 0.35, "gap": 0.45, "severity": "critical" }],
    "largest_uncertainty": [{ "node_id": "…", "weight": 3, "confidence": 0.12 }],
    "next_assessment_priority": ["…", "…", "…"]
  }],
  "summary": { "targets": 2, "best": 0.68, "on_track": 1, "empty": false }
}

gap_map cắt còn 20 node — đủ để hành động, không phình snapshot.


7. Cách DÙNG ​

7.1 Hai đường tính readiness — cố ý khác nhau ​

Đọc kỹ chỗ này

computeReadiness() được gọi ở hai nơi, và chúng không luôn cho cùng kết quả:

Readiness Engine (WF-04)buildCockpit()
Khi nàoSau sự kiện, lưu versionMỗi lần đọc, không lưu
BlueprintCả 3 nguồn, mọi targetMột blueprint của môn đang xem
Fallbackkhôngcó — blueprint kind='conditional' mặc định của môn
Dùng choModel lịch sử, Planning, báo cáoMàn hình học hôm nay

Cockpit có fallback: learner chưa khai mục tiêu nào vẫn thấy readiness theo blueprint mặc định của môn, để màn hình học không trống trơn. Readiness Model thì không fallback — nó chỉ ghi lại sự thật, và sự thật là chưa có target nào.

Vì vậy con số trên màn hình học và con số trong model có thể lệch — đó là có chủ đích, không phải bug. Khi cần đối chiếu lịch sử thì model là nguồn.

7.2 Cockpit — gap → hành động cụ thể ​

Với top 6 gap, cockpit chọn loại hành động theo trạng thái thật:

gap.node có prereq chưa đạt (mastery < 0.5)   → fix_prerequisite   "Cần vững X trước — đang chặn Y"
gap.node chưa từng đo                          → assess             "Chưa đủ bằng chứng — làm bài chẩn đoán ngắn"
gap severity = critical                        → learn
còn lại                                        → practice
priority = weight × deficit

Đề xuất prerequisite trước là điểm quan trọng: nếu node yếu vì cái gốc của nó chưa vững, luyện thẳng node đó là lãng phí. Đây cũng là câu mà Parent Recommendation dùng để nói với bố mẹ "gốc đang chặn nhiều phần phía sau".

Sau đó review candidates từ Retention được xếp cạnh (cùng thang điểm, trần ~3.2) chứ không đè bẹp — rồi dedupe theo node, lấy top 5.

7.3 Study mode — cùng dữ liệu, đổi cách dùng ​

computeStudyMode() (modules/knowledge/mode.ts), tính từ lịch thi học kỳ gần nhất:

≤ 30 ngày   → exam_prep 0.8
≥ 90 ngày   → exam_prep 0.2  (deep_learning 0.8)
30–90       → nội suy tuyến tính: 0.8 − ((d−30)/60) × 0.6
không có / đã qua → 0.2
mode = exam_prep_weight ≥ 0.5 ? "exam_prep" : "deep_learning"

Khi exam_prep, cockpit đẩy "Luyện đề học kỳ" lên đầu danh sách việc. Cùng một readiness, cùng một gap map, nhưng còn 10 ngày thi thì việc đúng là luyện đề chứ không phải đào sâu bản chất.

7.4 Nơi tiêu thụ khác ​

NơiDùng gì
Planning Engineversion mới nhất của readiness model
Parent Recommendation (WF-12)gaps critical + on_track → lời khuyên tuần
marlins #/child/{id}Goal → Current → Gap → Trajectory (REQ-PAR-03)
apps/learn cockpitreadiness + next actions

8. Bất biến — vi phạm là bug ​

  1. Readiness luôn gắn một blueprint. Không có readiness không target.
  2. Vượt ngưỡng không cộng thêm (clamp01) — giỏi chỗ này không bù chỗ khác.
  3. Node chưa đo tính mastery = 0, và bắt buộc phản ánh vào confidence.
  4. Không target → confidence = 0, không phải 1.
  5. Gap xếp theo weight × deficit, không theo mức yếu.
  6. largest_uncertainty là danh sách cần đo, không phải cần học.
  7. Readiness không bao giờ ghi vào learner_skill_state — nó chỉ đọc mastery.

9. Khoảng trống đã biết ​

ViệcTrạng thái
Hợp nhất 3 nguồn blueprint về learner_goal_entries⏳ cần migrate learner_goals trước
UI buộc hiện cặp readiness + confidence⏳ model đã có confidence, hiển thị chưa bắt buộc
Test hành vi riêng cho computeReadiness⏳ Goal/Retention đã có file test riêng; readiness chưa
Trajectory của readiness theo thời gian (REQ-PAR-03 "Trajectory")🕓 dựng được từ learner_model_versions nhưng chưa có view

Trace ​