Skip to content

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.

ts
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').

jsonc
{
  "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
keyKhoá ổn định theo nguồn gốc (exam_target:{id}) — chạy lại engine không nhân bản goal
parent_keyCây goal: null = goal gốc
kindexam_school · graduation · ielts · sat · ap · semester_exam · subject_readiness · foundation_repair · enrollment · custom
horizonoperational ≤14 ngày · tactical ≤90 · strategic >90 · undated
urgency0–1, tính từ số ngày còn lại
priorityDo người khai đặt (số nhỏ = quan trọng hơn)
deadline / days_to_deadlineNgày thi. Đây là nguồn duy nhất — không có bảng lịch thi thứ hai
sourcedeclaration (người khai) · enrollment (suy ra) · system (goal con) · parent · learner_self
origin_kind / origin_idTruy về bản ghi gốc sinh ra goal này
conflict_flagsCờ 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ồnBảngLọc
Đích thi đã khailearner_exam_targetsstatus='active'
Lịch thi học kỳsemester_exam_scheduletất cả
Enrollmentschool_enrollmentsstatus='active'
Blueprint bối cảnhlearner_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 ​

ts
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ạiurgency
5 ngày0.86
30 ngày0.5
90 ngày0.25
180 ngày0.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_passeddays_to_deadline < 0
time_competitionhai 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ốngKết quả
Goal mớiINSERT
Goal đã có, nội dung đổiUPDATE (ON CONFLICT)
Goal biến mất khỏi modelstatus='dropped' — không xoá
Goal xuất hiện lạiquay 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 ​

triggerTừ đâuMã workflow
GoalChangedKhai/sửa đích thi, thêm lịch thi học kỳWF-15 Declarations Intake
EXAM_RESCHEDULEDĐổi ngày thiWF-15
AssessmentCompletedNộp bài chẩn đoán / đề thiWF-04
admin_recomputeAdmin bấm tính lạiWF-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 Modellearner_goal_entries ORDER BY urgency DESC, priority ASC LIMIT 1active_goal_ref
Learner Modelversion + summary của Goal Modelgoals_summary — tham chiếu, không sao chép
Readiness Modellearner_goal_entries.blueprint_idchọn target để chấm
Planning (models/planning.ts)goal urgent nhấtdựng Learning Plan
Retentionnode có trong blueprint của goalinGoal → 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 ​

  1. Goal rỗng (summary.empty) là hợp lệ, không phải lỗi.
  2. Goal nền từ enrollment chỉ sinh khi chưa có goal nào khác.
  3. key ổn định theo origin — chạy lại engine không nhân bản goal (có test).
  4. Engine phát hiện xung đột, không tự giải quyết.
  5. Goal biến mất khỏi model → dropped, không xoá.
  6. Deadline chỉ có một nguồn: learner_goal_entries.deadline.
  7. Không module nào được sao chép nội dung goal — chỉ tham chiếu.
  8. confidence phả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ệcTrạ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 ​