---
url: https://docs.nemo12.com/reference/constraint-model.md
description: >-
  Constraint Model: learner có bao nhiêu thời gian và bị gì chiếm chỗ, cơ chế
  cập nhật và cách dùng (SDD-007).
---

# Constraint Model

> "Học sinh này có bao nhiêu thời gian, và đang bị gì chiếm chỗ?"

Thiết kế: [SDD-007](../architecture/sdd-007-learner-orchestration.md) (Constraint split), [SDD-002](../architecture/sdd-002-learner-intelligence/models-and-engines.md) §3 · Code: `modules/models/constraint.ts` · Snapshot: `learner_model_versions` (`model_kind='constraint'`)

***

## 1. Vì sao tách khỏi Context Model

Trước đây `available_minutes_per_week` và `energy` nằm trong [Context Model](learner-context-model.md#62-capacity-đang-trú-tạm). Tách ra vì một lý do rất cụ thể:

> **Để Planning không phải đoán capacity từ hai nguồn khác nhau.**

Quỹ thời gian có thể đến từ hồ sơ (`learner_context_models`) **hoặc** từ lời khai nhất thời ("tuần này con chỉ học được 20 phút/ngày" — `learner_context_events`). Khi hai nguồn cùng nói về một thứ mà không ai là chủ, mỗi nơi tiêu thụ sẽ tự chọn một cách khác nhau và không ai biết vì sao kế hoạch lại ra thế.

Constraint Model là **một chỗ duy nhất** trả lời câu hỏi đó, kèm `source` nói rõ con số đến từ đâu.

## 2. Luật nền: rỗng là hợp lệ, và **phải được ghi lại đàng hoàng**

```ts
declared = available_minutes_per_week != null || energy != null
```

> **"Chưa ai khai thời gian rảnh" khác hẳn "đã khai và bằng 0".**

Engine **luôn sinh version kể cả khi chưa có dữ liệu nào** — model tồn tại với `declared: false`, thay vì để trống trong admin và không ai biết là engine chưa chạy hay là không có gì (SRC-130).

Đây là cùng nguyên tắc với [Goal Model](goal-model.md#1-luật-nền-goal-rỗng-là-hợp-lệ), nhưng hệ quả khác: goal rỗng thì hệ thống **không làm gì**; capacity rỗng thì Planning **vẫn phải lập kế hoạch** — chỉ là phải biết mình đang dùng giá trị mặc định.

***

## 3. Model chứa gì

```jsonc
{
  "algorithm_version": "constraint-v1",
  "computed_at": "2026-08-15T…",
  "declared": true,
  "capacity": {
    "available_minutes_per_week": 180,
    "energy": "medium",
    "source": "learner_declaration"        // hoặc "unknown"
  },
  "commitments": [
    { "kind": "semester_exam", "label": "Thi học kỳ 1 — Toán", "date": "2026-09-20", "days_away": 36 }
  ],
  "confidence": 0.9,
  "summary": {
    "empty": false,
    "commitments": 1,
    "note": "Có khai thời gian/năng lượng."
  }
}
```

### `capacity.source` — trường quan trọng nhất

| Giá trị | Nghĩa | Planning làm gì |
| --- | --- | --- |
| `learner_declaration` | Có người thật khai | Dùng con số đó |
| `unknown` | Chưa ai khai | Dùng mặc định **và nói rõ là mặc định** |

Không có trường này thì Planning nhận `available_minutes_per_week: null` và không phân biệt được "chưa biết" với "bằng 0" — hai thứ dẫn tới hai kế hoạch hoàn toàn khác nhau.

### `commitments` — thứ đang chiếm chỗ thời gian

Kỳ thi học kỳ trong **60 ngày tới** (`COMMITMENT_HORIZON_DAYS`), xếp theo ngày gần trước.

Lọc bỏ kỳ thi đã qua và kỳ thi xa hơn 60 ngày: một kỳ thi tháng 6 không chiếm chỗ thời gian của tuần này. Ngưỡng 60 ngày đủ rộng để thấy kỳ thi sắp tới mà không biến mọi lịch thi cả năm thành "ràng buộc".

### `confidence` — 0.9 hoặc 0.1, không có ở giữa

```ts
confidence: declared ? 0.9 : 0.1
```

Nhị phân có chủ đích: hoặc có người khai quỹ thời gian, hoặc không. Không có mức "khai một nửa" — `energy` mà thiếu `minutes` vẫn là có người nói chuyện với hệ thống.

`0.1` (không phải 0) vì `commitments` vẫn có thể có thật dù capacity chưa khai.

### `summary.note` — nói thẳng bằng tiếng người

```
"Chưa ai khai thời gian rảnh — Planning dùng mặc định, không phải suy ra learner rảnh 0 phút."
```

Câu này nằm **trong model**, không phải trong code. Người mở admin đọc được ngay vì sao model rỗng, không phải đi đọc source để đoán.

***

## 4. Cơ chế CẬP NHẬT

### Đầu vào — hai truy vấn

| Nguồn | Lấy gì |
| --- | --- |
| `learner_context_models` | `available_minutes_per_week`, `energy` |
| `semester_exam_schedule` | mọi kỳ thi (engine tự lọc theo 60 ngày) |

`buildConstraintModel()` là **hàm thuần** — không I/O, không LLM, test được thẳng.

### Chạy ở đâu

[WF-04 step 6](workflows.md), sau Retention và trước Planning. Thứ tự bắt buộc: Planning cần Constraint để biết quỹ thời gian.

Trigger giống WF-04: `AssessmentCompleted`, `GoalChanged`, `EXAM_RESCHEDULED`, `admin_recompute`. Cũng nằm trong `CORE_MODEL_KINDS` nên [`ensureAllModels()`](learner-model.md#4-bảo-đảm-mọi-learner-đều-có-model-src-130) dựng bù nếu thiếu.

::: warning `commitments.days_away` phụ thuộc thời gian
Số ngày tới kỳ thi giảm mỗi ngày, nhưng model chỉ tính lại khi có sự kiện. Một kỳ thi 61 ngày nữa **không** nằm trong `commitments`; hôm sau nó phải nằm trong — nhưng không có gì kích hoạt.

Cùng khoảng trống với [Goal Model](goal-model.md#5-trigger--khi-nào-model-được-tính-lại). Thực tế nhẹ hơn vì Planning đọc `semester_exam_schedule` **trực tiếp** cho phần mode blend, không hoàn toàn phụ thuộc snapshot này.
:::

***

## 5. Cách DÙNG

| Nơi | Đọc gì |
| --- | --- |
| [Planning Engine](learning-plan-model.md) | capacity → phút/ngày; commitments → ngữ cảnh |
| `GET /v1/learners/{id}/model-versions` | Danh sách version (admin/mentor) |
| `admin.nemo12.com` | Xem lại lịch sử ràng buộc |

Hôm nay **chỉ Planning** thật sự tiêu thụ. Đó là đúng vai: Constraint tồn tại để trả lời một câu hỏi cho một người dùng, không phải để làm đẹp sơ đồ.

***

## 6. Bất biến — vi phạm là bug

1. Engine **luôn** sinh version, kể cả khi rỗng (`declared: false`).
2. `capacity.source` phải phân biệt `learner_declaration` vs `unknown` — không được để `null` mà không nói rõ.
3. `confidence` thấp khi chưa khai — **không** giả vờ chắc chắn.
4. Chỉ lấy commitment trong 60 ngày tới, bỏ kỳ thi đã qua.
5. Engine **chỉ đọc**; không ghi `learner_context_models`.
6. Chạy **trước** Planning trong WF-04.

## 7. Khoảng trống đã biết

| Việc | Trạng thái |
| --- | --- |
| Gộp hẳn capacity khỏi Context Model | ⏳ hai model vẫn cùng nói về quỹ thời gian ([context §6.2](learner-context-model.md#62-capacity-đang-trú-tạm)) |
| Đọc `time_budget` từ `learner_context_events` | ⏳ hiện **Planning** đọc thẳng event, Constraint chưa gộp vào |
| Commitment ngoài kỳ thi (học thêm, việc nhà, đi chơi xa) | 🕓 `kind` đã tổng quát, mới chỉ sinh `semester_exam` |
| Test hành vi riêng cho `buildConstraintModel` | ⏳ Goal/Planning/Retention đã có; constraint chưa |

## Trace

* REQ-INT-20 (Planning), REQ-INT-29.
* Nguồn: SRC-105, SRC-130 · Q-096 (tách Constraint khỏi Context).
* Thiết kế: [SDD-007](../architecture/sdd-007-learner-orchestration.md), [SDD-002](../architecture/sdd-002-learner-intelligence/models-and-engines.md) §3.
* Kiểm chứng: QG-005.
* Liên quan: [Learning Plan Model](learning-plan-model.md) · [Learner Context Model](learner-context-model.md) · [Workflows](workflows.md).
