---
url: https://docs.nemo12.com/reference/readiness-model.md
description: >-
  Readiness Model: nếu thi đích cụ thể này hôm nay thì sao, hàm
  computeReadiness, cơ chế cập nhật và cách dùng (SDD-002 §17).
---

# Readiness Model

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

Thiết kế: [SDD-002](../architecture/sdd-002-learner-intelligence/models-and-engines.md) §17, [SDD-008](../architecture/sdd-008-overview-views.md) §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.

| | Mastery | Readiness |
| --- | --- | --- |
| Câu hỏi | Em **biết** tới đâu? | Em **đủ cho kỳ thi này** chưa? |
| Phạm vi | Một node | Một **blueprint** (đề thi) |
| Đổi khi | Có bằng chứng mới | Bằng chứng mới **hoặc** đổi mục tiêu |
| Cùng một học sinh | Một giá trị mỗi node | **Nhiề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](goal-model.md#1-luật-nền-goal-rỗng-là-hợp-lệ).

***

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

| Bảng | Nội dung |
| --- | --- |
| `blueprints` | `subject_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.**

| Node | Trọng số | Thiếu | `weight × deficit` |
| --- | --- | --- | --- |
| A — hổng nặng, đề hỏi ít | 1 | 0.7 | **0.7** |
| B — yếu nhẹ, đề hỏi nhiều | 5 | 0.2 | **1.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:

| readiness | confidence | Nghĩa thật |
| --- | --- | --- |
| 0.30 | 0.85 | Đã đo kỹ — **thật sự đang thiếu** |
| 0.30 | 0.10 | Hầu như chưa đo gì — **chưa biết**, đừng hoảng |
| 0.80 | 0.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_map` | `largest_uncertainty` |
| --- | --- | --- |
| Trả lời | Biết là **đang yếu** | **Chưa biết** có yếu hay không |
| Hành động | Học / luyện | **Làm bài chẩn đoán ngắn** |
| Xếp theo | `weight × deficit` | `weight × (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ồn | Là gì |
| --- | --- |
| `learner_goals` | Bảng cũ, learner tự chọn blueprint mục tiêu |
| `learner_context_models.goal_blueprint_id` | Blueprint theo bối cảnh school/lớp |
| `learner_goal_entries` | **Projection của [Goal Model](goal-model.md#4-projection-xuống-learner_goal_entries)** |

`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](workflows.md#4-step-thứ-tự-bắt-buộc) — **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

::: warning Đọ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ào | Sau sự kiện, lưu version | **Mỗi lần đọc**, không lưu |
| Blueprint | Cả 3 nguồn, **mọi** target | **Một** blueprint của môn đang xem |
| Fallback | không | có — blueprint `kind='conditional'` mặc định của môn |
| Dùng cho | Model lịch sử, Planning, báo cáo | Mà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](parent-model.md#5-cách-dùng-2-parent-recommendation--wf-12) 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](retention-model.md) đượ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ơi | Dùng gì |
| --- | --- |
| Planning Engine | version 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` cockpit | readiness + 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ệc | Trạ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

* REQ-PAR-03 (Goal → Current → Gap → Trajectory → Expected).
* Nguồn: SRC-032, SRC-105.
* Thiết kế: [SDD-002](../architecture/sdd-002-learner-intelligence/models-and-engines.md) §17, [SDD-008](../architecture/sdd-008-overview-views.md) §5; mode switching [SDD-011](../architecture/sdd-011-school-models.md) §6 (REQ-ADAPT-01).
* Kiểm chứng: QG-005.
* Liên quan: [Goal Model](goal-model.md) · [Engines §1/§5](engines.md#5-readiness-engine) · [Retention](retention-model.md) · [Parent Model](parent-model.md).
