---
url: https://docs.nemo12.com/architecture/sdd-054-parent-model.md
description: >-
  Bản nháp Parent Model (SDD-054): sổ bằng chứng riêng của phụ huynh trong
  Marlins và bốn chiều suy ra, vì sao không dùng learner_evidence.
---

# SDD-054 - Parent Model

> Nguồn: SRC-1172, chủ dự án 01.10.2026: *"Cần rà soát các evidence của các Parents trong Marlins,
> rồi đưa vào Evidence Registry. Rồi từ đó tính toán và update Parent Model. Trong
> admin.nemo12.com, cần có chỗ để tôi xem được chi tiết các model của từng parent."*
> Migration: `0328_parent_evidence_registry_and_parent_model.sql`,
> `0330_parent_ebook_reading_progress.sql`.
> Mã: `workers/api/src/modules/parentModel/`, và trang `Parents` của admin.
> Cùng khuôn với lát cắt IELTS của learner (SRC-849, [SDD-038](sdd-038/evidence.md)).

## 1. Rà soát: phụ huynh đang để lại những gì

Đo trên D1 production ngày 01.10.2026. Dấu vết của phụ huynh nằm rải ở tám bảng, mỗi bảng một hình
dạng, và không bảng nào biết về bảng kia:

| Nguồn | Dòng | Việc nó ghi lại |
| --- | ---: | --- |
| `milestones` | 66 | đặt mốc cho con |
| `invitations` | 48 | mời con vào học, mời thêm người giám hộ |
| `family_notes` | 37 | ghi chú về gia đình |
| `learner_context_events` | 15 | báo hoàn cảnh của con |
| `parent_course_progress` | 6 | đọc xong chỗ học, làm quiz trong khoá CHO BỐ MẸ |
| `portraits` | 6 | dựng Student Portrait |
| `community_event_registrations` | 5 | đăng ký sự kiện |
| `parent_beliefs` | 0 | tự khai mức lo và dự đoán về con |

Tổng khoảng 183 mẩu. Hai bảng rỗng (`parent_beliefs`, `portrait_reactions`) vẫn được nối vào vì
đường ghi đã có trên Marlins, chỉ là chưa ai dùng; nối sẵn thì ngày đầu tiên có người dùng là mẩu
ấy vào sổ ngay, không phải chờ một lượt sửa mã.

Kết luận của bước rà soát: **không có câu truy vấn nào trả lời được "phụ huynh này đã để lại
những gì"**, nên cũng không có cách nào tính ra một model về họ.

## 2. Vì sao một sổ RIÊNG, không dùng `learner_evidence`

Khoá ngoại của sổ cũ là `learner_id REFERENCES learners(id)`. Một phụ huynh là một dòng `users`.
Nhét `user_id` vào cột ấy thì hoặc vi phạm khoá ngoại, hoặc phải nới nó ra - mà nới là mất đúng
thứ đang giữ cho sổ của learner không lẫn người lạ.

Sổ riêng còn giữ được một khác biệt thật: bằng chứng của learner nói về **năng lực của chính họ**,
còn bằng chứng của phụ huynh phần lớn nói về **việc họ làm cho một người khác**. Vì thế
`parent_evidence` có thêm `about_learner_id`, thứ không có nghĩa gì trong sổ của learner.

Giữ nguyên ba tính chất của Evidence Registry (SDD-002 §5): **bất biến**, có **khoá chống ghi
trùng**, và mang **mốc thời điểm việc xảy ra** chứ không phải lúc ghi sổ.

## 3. Bốn chiều

Marlins đứng trên một câu: *"Mục tiêu của việc làm bố mẹ không phải là khiến con phụ thuộc vào
những bố mẹ tốt hơn. Mà là giúp con đủ sức sống mà không cần mình."* Model vì thế đo **họ đang ở
đâu trên đường đó**, không đo họ chăm chỉ tới mức nào.

| Chiều | Trả lời câu | Nguồn nuôi nó |
| --- | --- | --- |
| `learning` | chính bố mẹ có đang học không | `parent_course_progress` |
| `understanding` | họ hiểu con tới đâu | `portraits`, `parent_beliefs`, `learner_context_events` |
| `acting` | hiểu biết đã thành việc làm chưa | `milestones`, `invitations` |
| `presence` | họ có quay lại đều đặn không | `family_notes`, `community_event_registrations` |

Bốn chiều này là **một quyết định sản phẩm**, không phải một sự thật toán học. Chúng được chọn vì
mỗi chiều có ít nhất một nguồn CÓ THẬT trong bảng §1: một chiều không nguồn chỉ là ô trống làm màn
admin trông đầy đặn hơn sự thật. Đổi bộ chiều thì phải tăng `PARENT_MODEL_VERSION`.

## 4. Phép tính

Điểm một chiều là **trung bình có trọng số**, trọng số = `reliability × freshness`.

* **Độ tin cậy ba bậc**, cùng thang với sổ learner: `.90` việc máy đo được (đọc xong chỗ học, làm
  quiz, ghi danh) · `.70` việc quan sát được nhưng tự khai nội dung (viết ghi chú, đặt mốc, dựng
  chân dung) · `.50` lời tự khai thuần.
* **Độ mới** giảm theo nửa đời **120 ngày**. Đủ dài để một phụ huynh bận ba tháng không tụt về 0,
  đủ ngắn để model không khoe mãi một lần hoạt động duy nhất từ năm ngoái.
* Hai thứ **nhân** nhau chứ không cộng: một lời tự khai từ năm ngoái phải nhẹ ở cả hai chiều, mà
  phép cộng vẫn cho nó một nửa tiếng nói.

**`null` không phải `0`.** Chiều chưa có mẩu nào trả về `null`, và chiều mà MỌI mẩu đã quá cũ cũng
trả về `null`: khi không còn dám nói gì về hôm nay thì im lặng là câu trả lời đúng. Một số 0 ở đây
sẽ bị đọc thành "phụ huynh này tệ", trong khi sự thật là "ta không biết".

**Không có điểm tổng**, và đó là chủ ý. Một con số duy nhất sẽ xếp hạng con người, giấu mất chiều
đang yếu (thứ duy nhất đáng hành động), và mời người đọc so sánh hai gia đình không so sánh được.

**Xu hướng cần ít nhất 4 mẩu.** Với 2 hay 3 mẩu thì "xu hướng" chỉ là một mẩu lẻ đứng một mình, và
một mũi tên đi xuống vì một lần duy nhất là một lời cáo buộc chứ không phải một quan sát.

`confidence` của cả lát cắt tính **cả chiều rỗng**: dày dữ liệu ở đúng một chiều không có nghĩa là
ta hiểu người này.

## 5. Ảnh chụp, không phải một ô ghi đè

`parent_models` lưu từng ảnh chụp như `ielts_learner_models` (0229). Chỉ ghi dòng mới khi **có gì
đó đổi**: thêm bằng chứng, đổi bản thuật toán, hoặc một trong bốn chiều đổi số. Mở trang mười lần
trong một buổi không được đẻ ra mười ảnh giống hệt nhau - lịch sử toàn dòng trùng thì không đọc ra
được đường đi nào, mà đọc đường đi đúng là lý do lưu lịch sử.

## 6. Đường chạy

| Đường | Việc |
| --- | --- |
| `POST /v1/admin/parents/backfill-evidence` | rà tám nguồn, nạp mẩu còn thiếu; chạy lại không sinh bản trùng |
| `POST /v1/admin/parents/recompute` | tính lại model cho mọi phụ huynh có bằng chứng |
| `GET /v1/admin/parents` | danh sách kèm ảnh chụp mới nhất |
| `GET /v1/admin/parents/{userId}` | lát cắt đang hiệu lực + lịch sử |
| `GET /v1/admin/parents/{userId}/evidence` | sổ bằng chứng, mới nhất trước |
| `POST /v1/admin/parents/{userId}/recompute` | tính lại một người |

Tất cả đòi vai `admin` (QG-008): đây là dữ liệu về NGƯỜI, không phải dữ liệu của chính người đang
đăng nhập. Mở một phụ huynh để lại một dòng `ParentModelViewed` trong nhật ký truy cập.

Nạp bù là **một đường chạy riêng, không nằm trong migration**: nó đọc tám bảng và phải chạy lại
được bất cứ lúc nào, kể cả sau khi đã có dữ liệu mới.

## 7. Chưa mở cho chính phụ huynh, và vì sao

Bốn con số 0..1 về một người, hiện thẳng cho chính họ, sẽ đọc thành một **bảng điểm làm-bố-mẹ** -
đúng thứ Marlins nói là không nên làm với trẻ con, nên cũng không nên làm với bố mẹ. Nếu sau này
mở, nó phải mở dưới dạng **gợi ý việc làm** ("chỗ học tiếp theo cho bạn") chứ không dưới dạng điểm.

## 8. Lượt chạy đầu, và ba nguồn thêm sau nó (SRC-1228)

Lượt nạp bù và tính model đầu tiên chạy 04.10.2026: **182 mẩu, 32 phụ huynh, không nguồn nào lỗi**.
Kết quả đọc ra một điều mà thiết kế không đoán trước được:

| Chiều | Số phụ huynh KHÔNG có mẩu nào |
| --- | ---: |
| Có mặt | 27 / 32 |
| Tự học | 26 / 32 |
| Hiểu con | 24 / 32 |
| Hành động | 2 / 32 |

Gần như mọi người chỉ có đúng một chiều, và đó là chiều **Hành động** - vì đặt mốc cho con và mời
con vào học là việc ai cũng làm ngay khi lập tài khoản. Ba chiều còn lại trống không phải vì phụ
huynh không làm gì, mà vì **cổng này không ghi lại việc họ làm**.

Chỉ đạo chủ dự án 04.10.2026: *"Thêm đường ghi cho 'có mặt' và 'tự học' đi."* Ba nguồn thêm vào:

| Nguồn | Chiều | Tin cậy | Ghi chú |
| --- | --- | --- | --- |
| `sessions` (phiên đăng nhập) | Có mặt | `.90` | chỉ phiên GỐC |
| `email_opens` (mở thư tuần) | Có mặt | `.50` | chỉ lượt `direct` |
| `parent_ebook_progress` (đọc sách) | Tự học | `.90` | mở `.4`, đọc xong `1.0` |

**Chỉ phiên gốc** (`rotated_from IS NULL`): token tự xoay trong lúc dùng nên một lần đăng nhập đẻ
ra nhiều dòng. Đếm cả dòng xoay là đo tuổi phiên chứ không đo số lần quay lại - trên production
04.10.2026, 209 dòng chỉ ứng với **178 lần đăng nhập thật**.

**Chỉ lượt mở `direct`**: ảnh theo dõi trong thư bị máy chủ thư tải hộ trước khi người ta mở, và
Gmail tải qua proxy của họ. Một lượt `proxy` chứng minh máy chủ Google đã chạm vào thư, không
chứng minh người nào đã đọc. Trên production **363/442 lượt là proxy**: đếm cả chúng là dựng một
chiều "có mặt" đo hạ tầng của Gmail. Ngay cả `direct` cũng yếu (khung xem trước cũng tải ảnh), nên
nó mang độ tin cậy bậc thấp nhất và không bao giờ nặng bằng một lần đăng nhập.

**Đọc sách tách MỞ khỏi ĐỌC XONG.** Thư viện Marlins có 20 quyển và trang đọc chạy từ SRC-673,
nhưng đọc xong một chương không để lại dấu vết nào. Nay `parent_ebook_progress` ghi hai mốc:
`opened_at` (lần đầu mở) và `finished_at` (lần đầu cuộn hết chương, đo bằng `IntersectionObserver`
ở khối cuối trang). Gộp cả hai thành "đã đọc" là biến một cú bấm nhầm thành bằng chứng ngang với
một buổi ngồi đọc thật. Cả hai mốc chỉ đặt MỘT lần: mở lại hay đọc lại không đẩy mốc về hôm nay,
vì mốc phải trả lời "họ bắt đầu học khi nào".

## 9. Còn nợ

* Chưa có đường ghi **trực tiếp**: mẩu mới chỉ vào sổ khi chạy nạp bù. Đường ghi thẳng lúc việc
  xảy ra là bước sau, và `event_id` đã dựng tất định nên hai đường không đụng nhau.
* Chưa nối `portrait_reactions` và `family_journey` (cả hai đang 0 dòng).
* Chưa có lịch chạy lại định kỳ; hiện là hai nút trong admin.
