---
url: https://docs.nemo12.com/reference/data-semantics.md
description: >-
  Ý nghĩa nghiệp vụ của từng bảng và cột D1 với một đứa trẻ và gia đình thật, bổ
  sung cho Data Dictionary (AS-04.3.2, AS-04.5.4).
---

# Data Semantics

[Data Dictionary](data-dictionary.md) sinh tự động từ `migrations/*.sql`: nó nói **kiểu dữ liệu và ràng buộc**. Nó không nói được cột đó **nghĩa là gì với một đứa trẻ và một gia đình thật**. Trang này trả lời phần còn lại (AS-04.3.2, AS-04.5.4):

* một bảng **tồn tại để làm gì**, ai được ghi, ai đọc;
* một cột hay bị hiểu nhầm thì **nghĩa đúng là gì**;
* trường nào là **PII** và **vì sao nó cần tồn tại** — không có lý do thì phải xoá cột, không phải viết thêm chú thích;
* dữ liệu đó **giữ bao lâu** và **xoá bằng đường nào**.

Ba luật đọc trang này:

1. **Data dictionary là cơ khí, trang này là phán đoán.** Bảng đổi thì `npm run gen:reference` tự cập nhật dictionary; còn nghĩa nghiệp vụ thì phải người viết. Thêm bảng mới mà không thêm dòng ở đây là nợ.
2. **Trạng thái vẽ ở nơi khác.** Mọi enum `status` và đường chuyển tiếp nằm trong [State Machines](state-machines.md); ở đây chỉ nói giá trị đó *có ý nghĩa gì*.
3. **PII được kê khai, không được suy đoán.** §5 là danh sách đóng. Trường không có trong §5 thì không được coi là an toàn mặc định — phải bổ sung vào §5 khi thêm.

***

## 1. Bản đồ domain

76 bảng D1 chia theo ngôn ngữ sản phẩm, không theo thứ tự migration:

| Domain | Bảng | Câu hỏi nghiệp vụ domain này trả lời |
| --- | --- | --- |
| Danh tính & gia đình | `users`, `auth_identities`, `sessions`, `families`, `family_members`, `learners`, `guardians`, `invitations`, `role_assignments`, `audit_log` | Ai là ai, ai là người nhà của ai, ai được xem dữ liệu của ai |
| Quyền riêng tư & vòng đời dữ liệu | `consents`, `data_deletion_requests`, `retention_policies` | Bố mẹ đã đồng ý những gì, xoá dữ liệu bằng đường nào, giữ bao lâu |
| Tri thức (bản đồ môn) | `subjects`, `strands`, `skill_nodes`, `skill_edges`, `items`, `item_versions`, `labs`, `lab_content` | Môn học gồm những kỹ năng nào, học liệu nào phục vụ kỹ năng nào |
| Learner Model & bằng chứng | `learner_skill_state`, `learner_evidence`, `learner_models`, `learner_model_versions`, `learner_retention`, `learner_goals`, `learner_goal_entries`, `learner_context_models`, `learner_context_events` | Con đang vững/lung lay ở đâu, vì bằng chứng nào, và hệ thống đã nghĩ gì ở thời điểm nào |
| Học & luyện | `assessment_sessions`, `assessment_responses`, `learning_experiences`, `learner_experience_state` | Con đã làm gì, làm tới đâu, kết quả ra sao |
| Ngân hàng đề & lịch thi | `exams`, `exam_questions`, `exam_attempts`, `exam_attempt_answers`, `learner_exam_targets`, `semester_exam_schedule`, `target_schools`, `school_enrollments` | Đề nào có thật, con thi gì, khi nào |
| Nội dung (Coral) | `blueprints`, `blueprint_weights`, `content_blueprints`, `generation_runs`, `content_reports`, `content_review_queue` | Nội dung được soạn theo khuôn nào, ai duyệt, báo sai thì đi đâu |
| Tương tác & cộng đồng | `forum_topics`, `interactions`, `interaction_votes`, `interaction_votes_rebuilt` | Ai nói gì với ai, nội dung nào bị ẩn |
| Student Portrait | `portraits`, `portrait_versions`, `portrait_tracks`, `portrait_learner_sections`, `portrait_reactions`, `milestones`, `external_activities`, `parent_beliefs` | Bức tranh gia đình vẽ về con — và tiếng nói của chính con |
| Mentor | `mentor_assignments` | Dolphin nào theo sát Nemo nào |
| Whale (du học) | `whale_countries`, `whale_universities`, `whale_scholarships`, `whale_rankings`, `whale_preferences`, `opportunities`, `success_stories` | Cơ hội học ngoài Việt Nam |
| Orca (thi đấu) | `competitions`, `competition_editions`, `competition_actions` | Sân thi đấu và bước con đã đi |
| Run log & vận hành | `engine_runs`, `workflow_runs`, `workflow_run_steps`, `queue_dead_letters`, `rate_limit_buckets` | Hệ thống đã thật sự chạy chưa, chạy hỏng ở đâu |

Bảng `interaction_votes_rebuilt` là tàn dư của một lần dựng lại bảng vote; giữ để không mất phiếu cũ. Không code nào ghi mới vào đó.

***

## 2. Danh tính & gia đình — bảng lõi

### `users` — một con người có tài khoản đăng nhập

| Cột | Nghĩa nghiệp vụ (ngoài kiểu dữ liệu) |
| --- | --- |
| `id` | Khoá danh tính **duy nhất**. Mọi quyền, mọi nhật ký đều trỏ về đây. |
| `email` | Chỉ để **liên lạc và đối chiếu với Google**, tuyệt đối **không** để phân quyền (RISK-013 — email đổi được, chuyển chủ được). Cấm mọi câu SQL kiểu `WHERE email = 'admin@…'`. |
| `display_name` | Tên hiển thị do Google trả về hoặc người dùng tự đặt. Không phải tên pháp lý, không dùng để định danh. |
| `avatar_media_id` / `avatar_url` | Ảnh đại diện. Hai cột cùng mục đích, tồn tại vì `avatar_url` thêm sau (0022) khi bỏ tầng media id. Code mới ghi `avatar_url`. |
| `status` | `active` | `suspended` | `deleted`. `deleted` là **xoá mềm** — hàng còn để nhật ký và đồng thuận không trỏ vào khoảng không; xoá cứng đi qua `data_deletion_requests` (§7). |

### `learners` — một đứa trẻ đang học, KHÔNG bắt buộc có tài khoản

Đây là bảng dễ hiểu sai nhất trong hệ. **`learners` không phải `users`.** Một Nemo lớp 6 có thể chưa có tài khoản Google nào: bố mẹ tạo hồ sơ học trước, con đăng nhập sau.

| Cột | Nghĩa nghiệp vụ |
| --- | --- |
| `user_id` | **NULL nghĩa là con chưa có tài khoản riêng** — không phải dữ liệu lỗi. Khi con nhận lời mời và đăng nhập, cột này mới được nối. |
| `family_id` | Gốc của mọi quyết định quyền: người nhà = thành viên của chính family này (`shared/authz.ts`). |
| `display_name` | Tên gọi trong nhà ("Bin", "Nem"), không phải tên khai sinh. Đây là lý do nó **không** được đưa vào context AI (§5). |
| `birth_year` / `birth_date` | Chỉ để suy lớp và chọn độ khó phù hợp lứa tuổi. `birth_date` thêm ở 0003 khi cần chính xác hơn cho lịch thi chuyển cấp. |
| `grade` | Lớp đang học — **dữ liệu vận hành chính**, quyết định nội dung nào hiện ra. Đây là trường tuổi tác duy nhất Context Builder được đọc. |
| `status` | `invited` (đã tạo, chưa nhận) → `active` → `paused` → `archived`. Xem [state machines §8](state-machines.md#_8-learner). |

### `family_members` vs `guardians` — vì sao có hai bảng

Không trùng nhau, và đây là chỗ hay bị nhầm:

* `family_members(family_id, user_id, role)` — **quan hệ quyền**: ai thuộc gia đình nào với vai gì (`owner`/`guardian`/`supporter`/`learner`). `shared/authz.ts` chỉ đọc bảng này.
* `guardians(learner_id, user_id, relationship)` — **quan hệ con người**: người này là bố, mẹ, ông bà hay người giám hộ của **đứa trẻ cụ thể nào**. Dùng để xưng hô và hiển thị, **không** dùng để chốt quyền.

Đặt quyền vào bảng quan hệ con người sẽ khiến "ông ngoại" và "người giám hộ hợp pháp" có quyền khác nhau vì lý do tình cảm chứ không phải lý do bảo mật. Tách ra là có chủ đích.

### `role_assignments` — vai nội bộ

`(user_id, role, scope_type, scope_id)`. Role ∈ `learner`/`guardian`/`mentor`/`staff`/`admin`. `scope_type` mặc định `global` với `scope_id = '*'`: **hôm nay hệ thống chưa thu hẹp phạm vi theo trường/gia đình**, cột này là chỗ trống đã chuẩn bị cho ngày cần. Xem [permissions](permissions.md).

### `sessions` — một lần đăng nhập, không phải một thiết bị

| Cột | Nghĩa nghiệp vụ |
| --- | --- |
| `token_hash` | **SHA-256 của token**, không bao giờ là token thô. Đọc trộm được bảng này cũng không đăng nhập được. |
| `rotated_from` | Chuỗi xoay vòng: session mới trỏ về session cũ nó thay thế. Dùng để phát hiện token cũ bị dùng lại. |
| `revoked_at` | Đăng xuất / thu hồi. Session hợp lệ = `revoked_at IS NULL` **và** `expires_at > now`. |
| `client` | `web` (cookie) hay `mobile` (bearer) — quyết định luật CSRF nào áp dụng. |

### `audit_log` — ai đã mở hồ sơ của con

Bảng này **chỉ nối thêm**, không có đường sửa/xoá qua API (`shared/audit.ts`).

| Cột | Nghĩa nghiệp vụ |
| --- | --- |
| `occurred_at` | **Cột thời gian duy nhất** của bảng. Bảng này cố ý **không có `created_at`** — hai cột thời gian cho cùng một sự kiện là lỗi dữ liệu. |
| `actor_role` | Vai **lúc hành động**, không phải vai hiện tại: một người bị gỡ role mentor hôm nay không làm thay đổi nhật ký hôm qua. |
| `ip`, `user_agent` | Phần "từ đâu". PII — xem §5. |
| `request_id` | `cf-ray` của Cloudflare; nối một dòng nhật ký với một request cụ thể và với log của Cloudflare. |
| `detail_json` | **Chỉ tham chiếu, không nội dung.** `sanitiseDetail()` thay mọi khoá nhạy cảm bằng `[redacted]` và chuỗi dài bằng `[len:n]`. Nhật ký sống lâu hơn dữ liệu gốc, nên nội dung nguyên văn trong log là một lần lộ dữ liệu nữa mỗi lần ai đó mở log ra đọc. |

Truy cập **bằng quyền mượn** (mentor/staff/admin) bắt buộc sinh một dòng; truy cập của chính em ấy và của người nhà thì không — nếu ghi cả thì nhật ký toàn tiếng ồn và chìm mất đúng thứ cần thấy.

***

## 3. Quyền riêng tư & vòng đời dữ liệu

### `consents` — bằng chứng bố mẹ đã đồng ý, theo từng việc

Đồng thuận **không phải một cờ boolean**. Nó là bản ghi có thời điểm, có phiên bản chính sách, rút lại được.

| Cột | Nghĩa nghiệp vụ |
| --- | --- |
| `learner_id` | **NULL = đồng thuận ở mức gia đình** (áp cho mọi con). Index dùng `COALESCE(learner_id,'')` vì trong SQLite hai `NULL` là khác nhau — không có `COALESCE` thì đẻ ra nhiều dòng trùng ở mức gia đình. |
| `guardian_user_id` | Người lớn **đã thật sự bấm nút**, không phải chủ tài khoản trên danh nghĩa. |
| `scope` | 7 việc tách rời, mỗi việc hỏi riêng: `account`, `learning_data`, `ai_processing`, `portrait`, `community`, `mentor_access`, `research`. Chữ hiện cho phụ huynh nằm ở `shared/privacy.ts` (`SCOPE_PURPOSE`) — mục đích viết bằng tiếng người, không phải chú thích kỹ thuật (AS-07.3.1). |
| `granted` | `1` = đang đồng ý. Rút lại là đặt `0` + `revoked_at`, **không xoá dòng**. |
| `policy_version` | Bản chính sách lúc bấm (`POLICY_VERSION`, hiện `2026-08-15`). Đổi nội dung chính sách phải tăng số này; đồng thuận cũ giữ nguyên phiên bản của nó, nên luôn trả lời được "bố mẹ đã đồng ý với bản nào". |
| `evidence_json` | IP, user-agent, màn hình nào, nút nào. Đây là phần biến "chúng tôi nghĩ là có đồng ý" thành bằng chứng. |

Luật thi hành: **không có dòng `granted=1` còn hiệu lực cho một scope thì tính năng của scope đó không được chạy.** `account` và `learning_data` là bắt buộc (không có thì không có tài khoản); 5 scope còn lại tuỳ chọn.

### `data_deletion_requests` — đường xoá dữ liệu

| Cột | Nghĩa nghiệp vụ |
| --- | --- |
| `subject_type` / `subject_id` | Xoá cái gì: một `learner`, một `user`, hay cả `family`. |
| `requested_by` | Phải là phụ huynh/chủ tài khoản — trẻ không tự yêu cầu xoá hồ sơ của mình. |
| `status` | `pending` → `in_progress` → `completed` | `rejected`. |
| `note` | **Đã xoá những gì và giữ lại gì, vì sao.** Đây là cột quan trọng nhất: có nghĩa vụ giữ lại một số bản ghi (đồng thuận, nhật ký), và phải nói ra được. |

### `retention_policies` — chính sách giữ dữ liệu khai báo được

Bảng khai báo, không phải bảng thi hành: nó nói **nên** giữ bao lâu; việc dọn thật là job vận hành. Cột `rationale` bắt buộc — một chính sách không nói được vì sao thì không phải chính sách. 10 dòng đã seed nằm ở §6.

***

## 4. Learner Model — cột hay bị hiểu nhầm

### `learner_skill_state` — trạng thái hiện tại của một kỹ năng

| Cột | Nghĩa nghiệp vụ |
| --- | --- |
| `mastery` | 0..1 — **mức làm được**, suy từ bằng chứng. **Không bao giờ tự giảm theo thời gian.** Quên là chuyện của `learner_retention`, không phải của mastery. |
| `confidence` | 0..1 — **hệ thống chắc bao nhiêu về con số mastery kia**. Hai cột này khác nhau hoàn toàn: `mastery=0.2, confidence=0.1` nghĩa là "chưa biết", không phải "hổng". |
| `trajectory` | Xu hướng gần đây (`rising`/`flat`/`falling`) — dùng để chọn lời nói với phụ huynh, không dùng để chọn bài. |

### `learner_evidence` — mỗi dòng là một lần quan sát

| Cột | Nghĩa nghiệp vụ |
| --- | --- |
| `observed_performance` | 0..1. Với trắc nghiệm là đúng/sai; với bài dài là điểm chuẩn hoá. |
| `reliability` | Bằng chứng này **đáng tin bao nhiêu**: học sinh tự khai < làm bài có kiểm < thi có giám sát. Đây là chỗ chặn "tự chấm 10 điểm rồi model tin ngay". |
| `occurred_at` | Thời điểm **quan sát**, không phải thời điểm ghi. Nhập bù dữ liệu cũ vẫn phải đúng thứ tự lịch sử. |

### `learner_model_versions` — hôm đó hệ thống nghĩ gì

Version chỉ sinh khi `content_hash` đổi: engine chạy mà kết quả y hệt thì **không** đẻ version mới (vẫn có dòng `engine_runs`). Bản `superseded` là **bất biến vĩnh viễn** — đó là thứ làm cho câu "vì sao tháng trước hệ thống bảo con nên ôn Hằng đẳng thức" trả lời được.

### `learner_context_events` — lời khai bối cảnh

Không phải log, mà là **lời khai có thời hạn** của con hoặc bố mẹ ("tuần này con ốm", "mỗi ngày học được 30 phút").

| Cột | Nghĩa nghiệp vụ |
| --- | --- |
| `statement` | Nguyên văn tiếng người — **hiện lại đúng câu này** khi giải thích vì sao kế hoạch đổi. Vì thế nó là PII (§5). |
| `effective_from` / `effective_to` | Khoảng còn hiệu lực. `effective_to IS NULL` = còn hiệu lực tới khi bị supersede. |
| `source_role` | `learner` hay `parent` — cùng một lời khai, hai người nói thì trọng số khác nhau. |
| `confidence` | Độ tin của lời khai, mặc định 0.8. |
| `status` / `superseded_by` | Lời khai mới **không ghi đè** lời cũ, nó supersede. Lịch sử giữ nguyên để giải thích quyết định cũ. |

### `learner_experience_state` — đã làm xong từng chặng chưa

`PRIMARY KEY (learner_id, subject_id, unit_key, exp_key)`. `unit_key = "<strand>|<module>|<unit>"`, `exp_key` ∈ `skip` | `mid` | `final` | `explore:<node>` | `practice:<node>` | `practice<n>:<node>`. Mastery theo node **không đủ** để tô đúng ba màu của Unit kiểu Duolingo — phải biết từng Experience đã làm chưa, nên mới có bảng này.

`status` chỉ có `completed` và `failed`: **không có `in_progress`**. Đang làm dở là trạng thái ở client, không phải sự kiện đáng ghi vào D1.

***

## 5. Kiểm kê PII — mỗi trường một lý do tồn tại

AS-04.5.4 (data minimization). Danh sách **đóng**: trường PII không có trong bảng này là trường chưa được duyệt.

| Bảng.cột | Loại | Vì sao trường này phải tồn tại | Nếu bỏ thì mất gì |
| --- | --- | --- | --- |
| `users.email` | Định danh liên lạc | Khớp tài khoản Google khi đăng nhập; là đường duy nhất liên lạc với phụ huynh khi có sự cố tài khoản | Không đăng nhập lại được, không báo được sự cố |
| `users.display_name` | Tên | Xưng hô trong giao diện người lớn | Giao diện gọi phụ huynh bằng id |
| `users.avatar_url` | Ảnh | Nhận ra đúng tài khoản khi một máy có nhiều người dùng | Nhầm tài khoản trong gia đình nhiều con |
| `auth_identities.email` | Định danh liên lạc | Email **tại thời điểm liên kết** với provider; giữ riêng vì `users.email` đổi được | Không phát hiện được khi Google đổi email dưới cùng một `subject` |
| `learners.display_name` | Tên trẻ | Con phải thấy tên mình, bố mẹ phải phân biệt được các con | Không phân biệt được con trong cùng gia đình |
| `learners.birth_year`, `birth_date` | Tuổi | Suy lớp và độ khó phù hợp lứa tuổi; lịch thi chuyển cấp phụ thuộc năm sinh | Chọn sai độ khó, sai mốc thi |
| `learners.grade` | Lớp | Quyết định nội dung nào hiện ra — dữ liệu vận hành chính | Không biết dạy gì |
| `invitations.email` | Email người được mời | Gửi lời mời tới đúng người | Chỉ mời được bằng mã dán tay |
| `guardians.relationship` | Quan hệ gia đình | Xưng hô đúng (bố/mẹ/ông bà) | Giao diện gọi mọi người lớn là "phụ huynh" |
| `audit_log.ip` | Địa chỉ mạng | Trả lời "từ đâu" khi điều tra truy cập bất thường vào hồ sơ trẻ (AS-07.4.2) | Biết ai xem nhưng không biết từ đâu — không đủ để điều tra |
| `audit_log.user_agent` | Thiết bị | Phân biệt truy cập từ trình duyệt người thật với script | Không phát hiện được truy cập tự động |
| `consents.evidence_json` | IP + ngữ cảnh bấm | Biến đồng thuận thành bằng chứng pháp lý có thể trình ra | Còn lời khẳng định, hết bằng chứng |
| `learner_context_events.statement` | Lời khai nguyên văn | Hiện lại đúng câu bố mẹ/con đã nói khi giải thích vì sao kế hoạch đổi | Giải thích bằng lời máy, gia đình không nhận ra tiếng nói của mình |
| `portrait_learner_sections.items_json` | Lời con tự viết | Là **tiếng nói của đứa trẻ** trong bức tranh về chính nó (SDD-015) | Chỉ còn góc nhìn của bố mẹ |
| `parent_beliefs.*` | Nhận định của phụ huynh | Ghi lại "bố mẹ nghĩ con thế nào" để đối chiếu với dữ liệu thật | Mất một nửa cuộc đối thoại gia đình |
| `portraits.sections_json` | Chân dung do bố mẹ viết | Nội dung cốt lõi của Student Portrait | Không có sản phẩm |
| `sessions.token_hash` | Bí mật (đã băm) | Xác thực từng request | Không có phiên đăng nhập |

**Trường cố tình KHÔNG có trong hệ:** số điện thoại, địa chỉ nhà, tên trường của con, số căn cước, ảnh chụp trẻ do trẻ tải lên, dữ liệu định vị. Không thu thập thì không phải bảo vệ.

**Ranh giới với AI:** không trường nào trong bảng trên đi vào prompt. `shared/context-builder.ts` chỉ đọc `learners.grade` cộng dữ liệu học tập đã bỏ định danh, gọi trẻ bằng bí danh `nemo_<hash>`, và có cổng chặn `assertNoIdentifiers()` chạy trên context đã dựng xong. Xem [AI Registry §5](ai-registry.md).

***

## 6. Retention — giữ bao lâu, vì sao chừng đó

Khớp đúng 10 dòng đã seed trong `retention_policies` (migration 0033):

| Nhóm dữ liệu | Bảng | Giữ | Lý do chọn đúng con số đó |
| --- | --- | --- | --- |
| Danh tính | `users` | tới khi yêu cầu xoá | Cần để đăng nhập và để phụ huynh quản lý con — không có mốc thời gian tự nhiên nào để cắt |
| Hồ sơ học sinh | `learners` | tới khi yêu cầu xoá | Là gốc của mọi tiến độ; xoá hồ sơ là xoá cả lịch sử học |
| Bằng chứng học tập | `learner_evidence` | 1095 ngày (3 năm) | Đủ dài để nhìn tiến bộ **qua một cấp học**, đủ ngắn để không giữ vô hạn |
| Phiên bản model | `learner_model_versions` | 365 ngày | Đủ để giải thích vì sao kế hoạch học đổi; cũ hơn một năm thì không ai còn hỏi |
| Lịch sử chạy engine | `engine_runs` | 90 ngày | Phục vụ điều tra sự cố và đối chiếu hoá đơn AI theo quý |
| Lịch sử chạy workflow | `workflow_runs` | 90 ngày | Cùng chu kỳ với `engine_runs` để hai bảng đối chiếu được với nhau |
| Lời khai bối cảnh | `learner_context_events` | 730 ngày (2 năm) | Lời khai cũ vẫn giải thích được quyết định cũ của Planning Engine |
| Nhật ký truy cập | `audit_log` | 730 ngày | Sự cố quyền riêng tư thường bị phát hiện muộn; 2 năm là khoảng điều tra thực tế |
| Đồng thuận | `consents` | vĩnh viễn | Bằng chứng pháp lý cho việc thu thập dữ liệu trẻ em — xoá bằng chứng đồng ý là tự bỏ chỗ đứng |
| Yêu cầu xoá dữ liệu | `data_deletion_requests` | vĩnh viễn | Bằng chứng đã thực hiện đúng yêu cầu xoá |

**Nhóm chưa có dòng chính sách** (nói thẳng thay vì im lặng): nội dung học (`items`, `item_versions`, `learning_experiences`) giữ vô thời hạn vì nó là tài sản chung, không gắn với một đứa trẻ; `sessions` hết hạn tự nhiên theo `expires_at`; `rate_limit_buckets` là dữ liệu tạm, dọn theo cron; `queue_dead_letters` chưa khai hạn. Bốn nhóm này cần bổ sung vào `retention_policies` khi có job dọn thật.

**Model version giữ bao nhiêu bản** (AS-04.5.3): không giới hạn số bản, giới hạn theo thời gian (365 ngày). Version chỉ sinh khi nội dung đổi nên số bản tăng theo tốc độ thay đổi thật của learner, không theo số lần engine chạy.

***

## 7. Đường xoá dữ liệu

```
Phụ huynh gửi POST /v1/privacy/deletion-requests
        │
        ├── ghi data_deletion_requests (status = pending)
        ├── ghi audit_log (ai yêu cầu, khi nào, từ đâu)
        │
        ▼
Vận hành xử lý (status = in_progress)
        │
        ├── xoá: bằng chứng học, model version, lời khai bối cảnh, portrait, nội dung con viết
        ├── ẩn danh: audit_log giữ dòng nhưng dữ liệu trỏ tới đã biến mất
        ├── GIỮ: consents + chính dòng data_deletion_requests (là bằng chứng đã làm đúng)
        │
        ▼
status = completed, ghi `note` = đã xoá gì / giữ gì / vì sao
```

Ba thứ **không** bị xoá và phải nói rõ với gia đình khi họ hỏi: bản ghi đồng thuận, bản ghi chính yêu cầu xoá, và nhật ký truy cập đã xảy ra trước đó. Lý do: cả ba tồn tại để bảo vệ chính đứa trẻ, không phải để phục vụ hệ thống.

Trạng thái: đường ghi nhận yêu cầu và đường tra cứu đã chạy (`POST` / `GET /v1/privacy/deletion-requests/{id}`); **bước xoá thật hiện là thao tác vận hành có người làm**, chưa tự động. Ghi ra đây vì im lặng về chỗ này là nói dối.

***

## 8. Cột JSON — bên trong có gì

Nguyên tắc chung (AS-04.2.5): dữ liệu **cần truy vấn thường xuyên** phải có cột riêng và index; JSON chỉ chứa thứ đọc nguyên khối.

| Cột | Bên trong | Vì sao là JSON chứ không phải bảng |
| --- | --- | --- |
| `items.quality_json` | Hồ sơ chất lượng 8 chiều CC-QAF, mỗi chiều `{score, evidence}` | Đọc nguyên khối khi soát nội dung; điểm tổng hợp đã tách ra `quality_score` để lọc và index |
| `items.screen_flags_json` | Mảng mã lỗi máy sàng lọc, vd `["no_node","dup_prompt"]` | Tập mã mở, thay đổi theo bộ sàng lọc |
| `item_versions.payload_json` | Snapshot **đầy đủ** nội dung tại version đó | Bản chụp bất biến — tách cột sẽ khiến schema đổi làm hỏng bản chụp cũ |
| `audit_log.detail_json` | Chỉ tham chiếu đã lọc qua `sanitiseDetail()` | Hình dạng khác nhau theo từng loại hành động |
| `consents.evidence_json` | IP, user-agent, màn hình, nút | Bằng chứng, đọc nguyên khối khi cần trình ra |
| `portraits.sections_json` | Các mục bố mẹ viết | Cấu trúc mục do sản phẩm định nghĩa, đổi mà không cần migration |
| `portrait_learner_sections.items_json` | Các mục **con** viết | Cùng lý do; tách bảng riêng với `portraits` vì quyền ghi khác nhau (chỉ role learner) |
| `learner_models.*_json` | Ảnh chụp model | Bất biến theo version |
| `engine_runs.input_json` / `output_json` | Đầu vào/đầu ra một lượt chạy | Bằng chứng, không truy vấn theo trường bên trong |
| `blueprints`/`content_blueprints.spec_json` | Khuôn soạn nội dung | Là cấu hình do người soạn viết, không phải dữ liệu quan hệ |

***

## 9. Bắt buộc khi thêm bảng hoặc cột mới

1. Viết comment `-- …` **ngay trên** `CREATE TABLE` trong migration — `gen-reference.mjs` bốc nó vào data-dictionary.
2. Thêm dòng vào §1 (bản đồ domain) của trang này.
3. Cột có ý nghĩa vượt quá cái tên → thêm vào §2/§3/§4.
4. Cột chạm tới con người → **bắt buộc** thêm vào §5 kèm lý do tồn tại. Không viết được lý do thì không thêm cột.
5. Có `status` → vẽ vào [State Machines](state-machines.md).
6. Nhóm dữ liệu mới → thêm dòng `retention_policies` **trong migration**, không để lại sau.

## Trace

* REQ-DOC-04 (reference), REQ-SEC-03/04 (dữ liệu trẻ em, retention).
* Rủi ro: RISK-001 (email không làm khoá), RISK-013 (cấm phân quyền bằng email).
* Thiết kế: [SDD-001](../architecture/sdd-001-platform.md) §6–7, [SDD-002](../architecture/sdd-002-learner-intelligence/index.md), [SDD-015](../architecture/sdd-015-student-portrait.md).
* Trang anh em: [Data Dictionary](data-dictionary.md) (sinh tự động), [State Machines](state-machines.md), [Permissions](permissions.md), [AI Registry](ai-registry.md).
* Kiểm chứng: QG-004, QG-008.
