---
url: https://docs.nemo12.com/reference/goal-model.md
description: >-
  Goal Model: learner đang cố đạt điều gì và cái nào gấp hơn, engine thuần
  goals/engine.ts, cách cập nhật và dùng (SDD-002 §16).
---

# 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](../architecture/sdd-002-learner-intelligence/models-and-engines.md) §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 |
| --- | --- |
| `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](workflows.md#4-step-thứ-tự-bắt-buộc) — 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**

```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ạ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 |

::: warning 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](retention-model.md) đã 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](schedules.md) — đâ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](models.md#3-learner-context-model-context-v1) | `learner_goal_entries` `ORDER BY urgency DESC, priority ASC LIMIT 1` | `active_goal_ref` |
| [Learner Model](models.md#2-learner-model-learner-v2) | version + `summary` của Goal Model | `goals_summary` — **tham chiếu, không sao chép** |
| [Readiness Model](readiness-model.md) | `learner_goal_entries.blueprint_id` | chọn target để chấm |
| Planning (`models/planning.ts`) | goal urgent nhất | dựng Learning Plan |
| [Retention](retention-model.md#61-review-queue--get-v1learnersidretentionreview-queue) | 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

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ệ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](../architecture/sdd-002-learner-intelligence/models-and-engines.md) §16.
* Liên quan: [Readiness Model](readiness-model.md) · [Engines §2](engines.md#2-goal-engine) · [Workflows WF-04/WF-15](workflows.md) · [State Machines §5](state-machines.md#5-goal).
