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 (Constraint split), SDD-002 §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. 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
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, 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ì
{
"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
confidence: declared ? 0.9 : 0.1Nhị 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, 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() dựng bù nếu thiếu.
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. 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 | 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
- Engine luôn sinh version, kể cả khi rỗng (
declared: false). capacity.sourcephải phân biệtlearner_declarationvsunknown— không được đểnullmà không nói rõ.confidencethấp khi chưa khai — không giả vờ chắc chắn.- Chỉ lấy commitment trong 60 ngày tới, bỏ kỳ thi đã qua.
- Engine chỉ đọc; không ghi
learner_context_models. - 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) |
Đọ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, SDD-002 §3.
- Kiểm chứng: QG-005.
- Liên quan: Learning Plan Model · Learner Context Model · Workflows.