Skip to content

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 ​

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, 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ĩaPlanning làm gì
learner_declarationCó người thật khaiDùng con số đó
unknownChưa ai khaiDù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ồnLấy gì
learner_context_modelsavailable_minutes_per_week, energy
semester_exam_schedulemọ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 Enginecapacity → phút/ngày; commitments → ngữ cảnh
GET /v1/learners/{id}/model-versionsDanh sách version (admin/mentor)
admin.nemo12.comXem 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ệcTrạ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 ​