Goal Model
"Học sinh này đang cố đạt điều gì, và cái nào gấp hơn?"
Thiết kế: SDD-002 §16 · Engine thuần: workers/api/src/modules/goals/engine.ts · I/O: modules/models/service.ts · Test: goals/engine.test.ts (14 ca) · Bảng: learner_goal_entries (migration 0027)
1. Luật nền: goal RỖNG là hợp lệ
Đây là bất biến quan trọng nhất của model này, và nó đi ngược trực giác kỹ thuật thông thường.
summary.empty === true // trạng thái hợp lệ, KHÔNG phải lỗi, KHÔNG phải "chưa khởi tạo"REQ-ONB-05, RISK-009. Hệ thống không bịa mục tiêu. Một đứa trẻ lớp 6 mới vào có thể chưa biết mình muốn gì — đó là bình thường và đúng lứa tuổi. Điền hộ một mục tiêu (kiểu "đạt 8.0 Toán học kỳ này") để bảng đỡ trống là tự bịa ra áp lực rồi gán cho đứa trẻ, và mọi thứ hạ nguồn — readiness, kế hoạch học, lời khuyên cho bố mẹ — sẽ được tính trên một cái đích không ai chọn.
Hệ quả trong code: bước 3 của engine (§3) chỉ chạy khi chưa có goal nào khác, và confidence = 0 khi rỗng thay vì mặc định 1.
2. Model chứa gì
GoalModel — computed model, có version + hash, lưu tại learner_model_versions (model_kind='goal').
{
"algorithm_version": "goal-v1",
"computed_at": "2026-08-15T…",
"confidence": 0.9,
"confidence_basis": "mục tiêu do con/bố mẹ khai, có đủ mốc thời gian",
"goals": [ /* cây goal, xem dưới */ ],
"conflicts": [ /* xung đột phát hiện được */ ],
"summary": {
"total": 3, "roots": 2,
"operational": 1, "tactical": 1, "strategic": 1, "undated": 0,
"next_deadline": "2026-09-20", "next_goal": "Thi học kỳ 1 — Toán",
"empty": false
}
}GoalNode
| Trường | Ý nghĩa |
|---|---|
key | Khoá ổn định theo nguồn gốc (exam_target:{id}) — chạy lại engine không nhân bản goal |
parent_key | Cây goal: null = goal gốc |
kind | exam_school · graduation · ielts · sat · ap · semester_exam · subject_readiness · foundation_repair · enrollment · custom |
horizon | operational ≤14 ngày · tactical ≤90 · strategic >90 · undated |
urgency | 0–1, tính từ số ngày còn lại |
priority | Do người khai đặt (số nhỏ = quan trọng hơn) |
deadline / days_to_deadline | Ngày thi. Đây là nguồn duy nhất — không có bảng lịch thi thứ hai |
source | declaration (người khai) · enrollment (suy ra) · system (goal con) · parent · learner_self |
origin_kind / origin_id | Truy về bản ghi gốc sinh ra goal này |
conflict_flags | Cờ xung đột gắn ngay trên goal |
3. Cơ chế CẬP NHẬT — 6 bước
Engine thuần buildGoalModel(input): không I/O, không LLM, deterministic. Chạy trong WF-04 step 1 — luôn là step đầu tiên, vì Context/Learner/Readiness Model đều tham chiếu nó.
Đầu vào
| Nguồn | Bảng | Lọc |
|---|---|---|
| Đích thi đã khai | learner_exam_targets | status='active' |
| Lịch thi học kỳ | semester_exam_schedule | tất cả |
| Enrollment | school_enrollments | status='active' |
| Blueprint bối cảnh | learner_context_models.goal_blueprint_id | — |
Bước 1 — Exam target → hai goal
Mỗi đích thi sinh một goal gốc và một goal con subject_readiness cho môn chuyên:
exam_target:{id} "Chuyên Toán KHTN — 8.0" (source: declaration)
└── exam_target_subject:{id} "math readiness" (source: system)Môn không chuyên là thuộc tính của goal gốc, không sinh goal riêng (Q-102). Nếu sinh riêng, một đích thi 4 môn sẽ nở thành 5 goal và làm loãng hoàn toàn thứ tự ưu tiên.
Bước 2 — Lịch thi học kỳ → goal vận hành
priority = 1 (cao nhất). Thi học kỳ là việc chắc chắn xảy ra và sát ngày, khác với đích thi chuyên còn xa.
Bước 3 — Enrollment → goal nền, chỉ khi chưa có goal nào
if (goals.length === 0 && input.contextBlueprint) { … }Điều kiện goals.length === 0 là hiện thân của luật §1. Có mục tiêu thật rồi thì không thêm mục tiêu giả; chưa có gì thì tạm lấy chương trình của lớp đang học làm mốc — nhưng đánh dấu source: "enrollment" để mọi thứ hạ nguồn biết đây là hệ thống đoán hộ, không phải lời khai.
Bước 4 — Horizon & urgency
horizon: ≤14 ngày → operational · ≤90 → tactical · >90 → strategic · không deadline → undated
urgency: quá hạn → 1 · ≥365 ngày → 0.05 · không deadline → 0.1 · còn lại → 1/(1 + days/30)Hàm 1/(1+d/30) không tuyến tính — dốc hẳn lên trong 30 ngày cuối:
| Còn lại | urgency |
|---|---|
| 5 ngày | 0.86 |
| 30 ngày | 0.5 |
| 90 ngày | 0.25 |
| 180 ngày | 0.14 |
Đây là lý do thi học kỳ 5 ngày nữa thắng kỳ thi chuyên 6 tháng nữa, dù kỳ thi chuyên "to" hơn nhiều. Một hệ thống xếp theo tầm quan trọng danh nghĩa sẽ bắt đứa trẻ ôn thi chuyên trong tuần thi học kỳ — đúng cái sai mà con người cũng hay mắc.
Bước 5 — Phát hiện xung đột, không tự giải quyết
| Loại | Điều kiện |
|---|---|
deadline_passed | days_to_deadline < 0 |
time_competition | hai goal gốc có deadline cách nhau ≤ 7 ngày |
Engine gắn cờ và escalate cho Planning/phụ huynh (Q-099). Nó không tự bỏ goal nào.
Vì sao không tự quyết: khi hai kỳ thi chen nhau trong một tuần, việc bỏ cái nào là quyết định của gia đình, có yếu tố mà hệ thống không thấy (tài chính, sức khoẻ, nguyện vọng thật). Một thuật toán tự chọn hộ ở đây là vượt quyền.
Bước 6 — Sắp xếp
urgency giảm dần → priority tăng dần → key (ổn định)Có test khoá riêng: "thi học kỳ gần thắng goal xa về thứ tự — không phải 'goal lớn nhất thắng'".
Confidence — "ta có dám nói đây đúng là mục tiêu của con không"
Không phải xác suất thống kê (AS-05.5.4):
rỗng → 0 "chưa ai khai mục tiêu nào"
chỉ suy từ enrollment → 0.3 "suy ra từ lớp đang học, con/bố mẹ chưa xác nhận"
có lời khai → 0.6 + 0.3 × (số goal có deadline / số goal khai)Rỗng cho confidence = 0 chứ không phải 1: rỗng là hợp lệ, nhưng không được phép giả vờ chắc chắn về một thứ chưa ai nói. Goal khai mà thiếu ngày thì bị trừ — lập kế hoạch cho một mục tiêu không có mốc thời gian là lập kế hoạch mù.
4. Projection xuống learner_goal_entries
Sau khi tính, model được chiếu xuống bảng để các module khác truy vấn được bằng SQL thay vì phải parse JSON.
UNIQUE (learner_id, origin_kind, origin_id) ← khoá idempotent| Tình huống | Kết quả |
|---|---|
| Goal mới | INSERT |
| Goal đã có, nội dung đổi | UPDATE (ON CONFLICT) |
| Goal biến mất khỏi model | status='dropped' — không xoá |
| Goal xuất hiện lại | quay về status='active' |
Ghi hai lượt: goal gốc trước, goal con sau — vì parent_goal_id cần id thật của cha.
Không hard-delete để giữ lịch sử: "hồi tháng 3 con từng nhắm trường này" là thông tin thật về hành trình của gia đình.
5. Trigger — khi nào model được tính lại
trigger | Từ đâu | Mã workflow |
|---|---|---|
GoalChanged | Khai/sửa đích thi, thêm lịch thi học kỳ | WF-15 Declarations Intake |
EXAM_RESCHEDULED | Đổi ngày thi | WF-15 |
AssessmentCompleted | Nộp bài chẩn đoán / đề thi | WF-04 |
admin_recompute | Admin bấm tính lại | WF-04 |
Goal Model có phụ thuộc thời gian nhưng không có cron
urgency, horizon, days_to_deadline đều tính từ now. Một goal cách 20 ngày hôm nay là tactical; 6 ngày sau nó phải là operational — nhưng không có gì kích hoạt tính lại nếu học sinh không làm bài và không ai sửa mục tiêu.
Khác với Retention đã có cron WF-17, Goal Model hiện chỉ tính lại theo sự kiện. Thực tế ít gây hại vì learner có hoạt động thì WF-04 chạy thường xuyên, nhưng với learner ngừng học hẳn thì horizon trong bảng sẽ lỗi thời. Xem schedules — đây là ứng viên cron tiếp theo.
6. Cách DÙNG — ai đọc Goal Model
Goal Model ─┬─▶ Context Model active_goal_ref (goal urgency cao nhất)
├─▶ Learner Model goals_summary (tham chiếu, không sao chép)
├─▶ Readiness Model chọn blueprint làm target
├─▶ Planning Engine goal đang urgent nhất → kế hoạch học
└─▶ Retention Engine inGoal → +1 bậc urgency ôn tập| Nơi dùng | Đọc gì | Để làm gì |
|---|---|---|
| Context Model | learner_goal_entries ORDER BY urgency DESC, priority ASC LIMIT 1 | active_goal_ref |
| Learner Model | version + summary của Goal Model | goals_summary — tham chiếu, không sao chép |
| Readiness Model | learner_goal_entries.blueprint_id | chọn target để chấm |
Planning (models/planning.ts) | goal urgent nhất | dựng Learning Plan |
| Retention | node có trong blueprint của goal | inGoal → nâng mức gấp |
Goal chỉ có một nguồn sự thật. Learner Model nhúng goals_summary như một ref kèm goal_model_version, không copy nội dung goal. Đây là lý do thứ tự chạy trong WF-04 bắt buộc phải là Goal → Context → Learner → Readiness.
7. Bất biến — vi phạm là bug
- Goal rỗng (
summary.empty) là hợp lệ, không phải lỗi. - Goal nền từ enrollment chỉ sinh khi chưa có goal nào khác.
keyổn định theoorigin— chạy lại engine không nhân bản goal (có test).- Engine phát hiện xung đột, không tự giải quyết.
- Goal biến mất khỏi model →
dropped, không xoá. - Deadline chỉ có một nguồn:
learner_goal_entries.deadline. - Không module nào được sao chép nội dung goal — chỉ tham chiếu.
confidencephản ánh nguồn gốc goal, không phải khả năng đạt được.
8. Kiểm chứng
goals/engine.test.ts — 14 ca test hành vi (cấm test bằng regex source, RISK-015):
- goal rỗng là hợp lệ (RISK-009)
- exam target sinh goal gốc + goal con
- horizon: 5 ngày = vận hành, 300 ngày = chiến lược
- urgency: quá hạn = 1, càng gần càng cao
- thi học kỳ gần thắng goal xa — không phải "goal lớn nhất thắng"
- hai deadline chen ≤7 ngày → flag, engine không tự quyết
- deadline đã qua được đánh dấu, không im lặng bỏ qua
- goal nền từ enrollment chỉ khi chưa khai gì
- key ổn định — chạy lại không nhân bản
- 5 ca cho
confidence
Gate: QG-005.
9. Khoảng trống đã biết
| Việc | Trạng thái |
|---|---|
Cron tính lại horizon/urgency theo thời gian | ⏳ xem cảnh báo §5 |
foundation_repair, custom trong GoalKind | 🕓 đã khai kiểu, engine chưa sinh |
source: parent / learner_self | 🕓 đã khai, hiện mọi lời khai đều vào declaration |
Goal achieved (đã đạt) | 🕓 CHECK constraint có, engine chưa tự chuyển |
Trace
- REQ-INT-16 (Goal Model), REQ-ONB-05 (goal rỗng hợp lệ), RISK-009.
- Nguồn: SRC-032 §9-10, SRC-105 · Q-099 (không tự giải xung đột), Q-102 (môn không chuyên là attribute).
- Thiết kế: SDD-002 §16.
- Liên quan: Readiness Model · Engines §2 · Workflows WF-04/WF-15 · State Machines §5.