Data Dictionary · Khác (phần 2/11)
⚙️ Trang này sinh tự động từ source code bằng
scripts/gen-reference.mjs— đừng sửa tay, sửa code rồi chạynpm run gen:reference.
Thuộc Data Dictionary. 25 bảng, content_blocked_targets tới curriculum_lesson_gate_quiz.
content_blocked_targets
Migration 0167 — sổ việc TẮC: chỗ nào sinh đi sinh lại vẫn không đạt (SRC-636). Chỉ đạo chủ dự án 2026-08-28: "có một số chỗ tạo nội dung, nhưng tạo đi tạo lại vẫn không ổn, thì cần tạm dừng, báo cáo vào đâu đó, để sau đó chỉnh lại thuật toán và logic, chứ không được phép tạo đi tạo lại quá nhiều lần". VÌ SAO CẦN BẢNG CHỨ KHÔNG CHỈ CẦN TRẦN SỐ LẦN: SRC-634 đã có trần 3 lượt/bài, nhưng trần chỉ biết NÓI KHÔNG. Nó không nói vì sao tắc, không ai được giao đi sửa, và không có đường mở lại sau khi sửa — tức là một bài chạm trần thì tắc vĩnh viễn trong im lặng. Bảng này biến "bị chặn" thành một việc CÓ HỒ SƠ: tắc ở đâu, trượt đi trượt lại vì tiêu chí nào, ai đã chỉnh gì, mở lại lúc nào. CHẨN ĐOÁN LÀ CỘT QUAN TRỌNG NHẤT: recurring_problems gom những chỗ chưa đạt LẶP LẠI qua các lượt. Ba lượt cùng trượt một tiêu chí không phải là chuyện xui — đó là bằng chứng prompt/rubric/ đầu vào có lỗi hệ thống, và chạy lượt thứ tư chỉ tốn tiền để nghe lại cùng một câu.
migration: 0167_content_blocked_targets.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
course_id | TEXT | NOT NULL |
unit_seq | INTEGER | NOT NULL |
lesson_seq | INTEGER | NOT NULL |
part | TEXT | — |
target_label | TEXT | NOT NULL |
attempts | INTEGER | NOT NULL |
recurring_problems | TEXT | — |
best_score | INTEGER | — |
first_blocked_at | TEXT | NOT NULL |
last_attempt_at | TEXT | — |
status | TEXT | NOT NULL DEFAULT 'open' CHECK (status IN ('open','released','wont_fix')) |
resolution_note | TEXT | — |
released_at | TEXT | — |
updated_at | TEXT | NOT NULL |
| — | table constraint | PRIMARY KEY (course_id, unit_seq, lesson_seq, part) |
Index: idx_content_blocked_status(status, updated_at DESC)
content_feedback
Migration 0168 — sổ nhận xét của người về PROMPT và về CONTENT (SRC-637). Chỉ đạo chủ dự án 2026-08-28: "cần có chỗ để con người (là tôi) vào review các prompt để tạo content và review chính các content nữa. Rồi sau đó, hệ thống sẽ rà soát lại các comment của tôi, để dùng các comment và feedback đó, để nâng cấp các prompt và engine, rồi sau đó tạo lại các content không ổn." MỘT BẢNG CHO CẢ HAI LOẠI REVIEW, phân biệt bằng scope: cả hai đều là "một người đọc một thứ rồi nói nó chưa ổn ở đâu", cùng vòng đời (mở → đã xử lý), và cùng được máy đọc gộp khi soạn đề xuất nâng prompt. Tách hai bảng thì mọi truy vấn gom feedback phải UNION, và đến lúc thêm loại thứ ba (rubric, engine) lại thêm bảng nữa. GẮN VERSION VÀO TỪNG NHẬN XÉT là điều quan trọng nhất ở đây. Feedback "câu chuyện toàn cảnh trường học, chán" chỉ đúng với prompt đã sinh ra nó; sau khi prompt lên v3 có luật đổi bối cảnh thì chính dòng ấy là BẰNG CHỨNG ĐÃ SỬA XONG chứ không còn là việc tồn. Không có version thì sổ feedback chỉ dài ra mà không bao giờ đóng được dòng nào.
migration: 0168_content_feedback.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
scope | TEXT | NOT NULL CHECK (scope IN ('prompt', 'content')) |
course_id | TEXT | — |
unit_seq | INTEGER | — |
lesson_seq | INTEGER | — |
part | TEXT | — |
run_id | TEXT | — |
prompt_version | INTEGER | — |
rubric_version | TEXT | — |
verdict | TEXT | NOT NULL CHECK (verdict IN ('praise', 'fix', 'regenerate')) |
comment | TEXT | NOT NULL |
status | TEXT | NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'applied', 'superseded')) |
resolution | TEXT | — |
created_by | TEXT | — |
created_at | TEXT | NOT NULL |
updated_at | TEXT | NOT NULL |
Index: idx_content_feedback_open(status, scope, created_at DESC) · idx_content_feedback_target(course_id, unit_seq, lesson_seq, status)
content_item_drafts
Migration 0171 — kho câu hỏi do máy sinh, chờ người duyệt (SRC-638, WF-20 ItemForge). VÌ SAO KHÔNG GHI THẲNG VÀO items: cùng luật §1 của SDD-027 và QG-014 — máy sinh, máy chấm, NGƯỜI DUYỆT. Thêm nữa items có trigger chặn publish khi thiếu quality_json (0067), nên câu chưa qua rubric mà nhét vào đó thì hoặc bị chặn, hoặc lọt vào status candidate lẫn với kho thật. Một bảng riêng giữ ranh giới rõ: đây là BẢN THẢO, chưa phải câu hỏi. bucket giữ đúng phép chia của CD-1: 10 câu luyện tập + 6 câu để dành cho bài đo. Chia ngay từ lúc sinh chứ không chia lúc nạp, vì kế hoạch khe mới biết câu nào phân biệt tốt — mà đó chính là thứ một bài đo cần.
migration: 0171_item_drafts.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
run_id | TEXT | NOT NULL REFERENCES content_runs(id) |
seq | INTEGER | NOT NULL |
node_id | TEXT | NOT NULL |
subject_id | TEXT | NOT NULL |
role | TEXT | NOT NULL CHECK (role IN ('canonical','variant','boundary','discriminator')) |
difficulty | REAL | NOT NULL |
bucket | TEXT | NOT NULL CHECK (bucket IN ('practice','assessment')) |
prompt | TEXT | NOT NULL |
options_json | TEXT | NOT NULL |
correct_index | INTEGER | NOT NULL |
misconception | TEXT | — |
attempts | INTEGER | NOT NULL DEFAULT 1 |
usd | REAL | NOT NULL DEFAULT 0 |
prompt_version | INTEGER | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (run_id, seq) |
quality_score | REAL | — thêm ở 0172_item_draft_quality.sql |
quality_json | TEXT | — thêm ở 0172_item_draft_quality.sql |
gate_ok | INTEGER | NOT NULL DEFAULT 0 — thêm ở 0172_item_draft_quality.sql |
gate_reasons | TEXT | — thêm ở 0172_item_draft_quality.sql |
Khóa ngoại: run_id → content_runs
Index: idx_item_drafts_node(subject_id, node_id) · idx_item_drafts_node_gate(node_id, gate_ok)
content_priority
Ưu tiên: MỘT bảng BA chiều thay vì ba bảng, vì cả ba đều chỉ là "sắp thứ tự rồi bật/tắt" và sau này còn thêm chiều nữa (ladder, trường học) mà không phải đổi bảng lần nữa. dimension='subject' → key là subject_id ('math','vietnamese','informatics','english') dimension='level' → key là bậc '1'..'5' dimension='course' → key là course_id. Chiều này là DANH SÁCH NHẮM, không phải bộ lọc thường: hễ có ÍT NHẤT MỘT dòng course enabled=1 thì hàng chờ chỉ lấy việc trong những khoá ấy. Không có dòng nào thì mở cho mọi khoá theo môn/bậc. Đó là cách nói "làm xong sáu khoá này đã, đừng đụng vào khoá khác" mà không phải tắt từng môn một rồi quên bật lại. Hàng chờ xếp việc theo (rank môn, rank bậc, ladder, seq) và BỎ QUA mọi hàng enabled=0.
migration: 0166_content_production_plane.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
dimension | TEXT | NOT NULL CHECK (dimension IN ('subject','level','course')) |
key | TEXT | NOT NULL |
rank | INTEGER | NOT NULL |
enabled | INTEGER | NOT NULL DEFAULT 1 |
note_vi | TEXT | — |
| — | table constraint | PRIMARY KEY (dimension, key) |
content_run_changes
Câu hỏi "mỗi lần chạy thì cái gì được tạo ra hoặc bị thay đổi" cần một bảng riêng, không nhét được vào content_run_trials: trial là "model nào làm được gì", còn đây là "bài đổi ra sao". Một lượt chạy sinh 0..n dòng, mỗi dòng một PHẦN của bài. So với cái gì: bản thảo mới đối chiếu với BẢN ĐANG CÓ trong curriculum_* (nếu bài đã nạp), nếu chưa có gì thì mọi phần là 'created'. Lưu bản xem trước hai đầu chứ không lưu cả nội dung: đủ để đọc lướt trên Coral, còn bản đầy đủ nằm ở R2 theo r2_key của trial.
migration: 0166_content_production_plane.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
run_id | TEXT | NOT NULL REFERENCES content_runs(id) |
seq | INTEGER | NOT NULL |
part | TEXT | NOT NULL |
action | TEXT | NOT NULL CHECK (action IN ('created','updated','unchanged')) |
before_preview | TEXT | — |
after_preview | TEXT | — |
| — | table constraint | PRIMARY KEY (run_id, seq) |
Khóa ngoại: run_id → content_runs
content_run_trials
migration: 0164_content_foundry.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
run_id | TEXT | NOT NULL REFERENCES content_runs(id) |
model_key | TEXT | NOT NULL |
ok | INTEGER | NOT NULL |
score | INTEGER | NOT NULL |
max_score | INTEGER | NOT NULL |
repaired | INTEGER | NOT NULL DEFAULT 0 |
ms | INTEGER | NOT NULL |
tokens_in | INTEGER | NOT NULL |
tokens_out | INTEGER | NOT NULL |
usd | REAL | NOT NULL |
error | TEXT | — |
problems | TEXT | , -- JSON mảng chỗ chưa đạt, để đọc mà không phải mở R2 |
| — | table constraint | PRIMARY KEY (run_id, model_key) |
prompt_version | INTEGER | — thêm ở 0165_content_trials_versions.sql |
rubric_version | TEXT | — thêm ở 0165_content_trials_versions.sql |
usage_estimated | INTEGER | NOT NULL DEFAULT 0 — thêm ở 0165_content_trials_versions.sql |
draft_json | TEXT | — thêm ở 0169_content_trial_draft.sql |
Khóa ngoại: run_id → content_runs
content_runs
Migration 0164 — Sổ của xưởng sinh nội dung (SRC-632). Chỉ đạo chủ dự án 2026-08-28: nội dung course không soạn tay nữa; hệ tự gọi Cloudflare AI Gateway để sinh, tự kiểm chất lượng, rồi tự nâng cấp; gọi nhiều model (DeepSeek…) để so chất lượng và giá. HAI BẢNG, KHÔNG PHẢI MỘT: một lượt chạy sinh ra NHIỀU lượt thử, mỗi model một lượt. Gộp vào một bảng thì câu hỏi chính của cả hệ này — "cùng một bài, model nào cho điểm cao hơn và tốn bao nhiêu" — thành một truy vấn tự nối bảng với chính nó. BẢN THÔ KHÔNG NẰM Ở ĐÂY: nó nằm ở R2 (foundry/<run>/<model>.json), D1 chỉ giữ con số và đường dẫn. Nhét vài chục KB JSON mỗi lượt vào D1 là biến bảng đọc-thường-xuyên thành kho lưu trữ.
migration: 0164_content_foundry.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
kind | TEXT | NOT NULL |
target_label | TEXT | NOT NULL |
model_keys | TEXT | NOT NULL |
status | TEXT | NOT NULL, -- running | done | failed |
best_model_key | TEXT | — |
best_score | INTEGER | — |
started_at | TEXT | NOT NULL |
finished_at | TEXT | — |
course_id | TEXT | — thêm ở 0166_content_production_plane.sql |
unit_seq | INTEGER | — thêm ở 0166_content_production_plane.sql |
lesson_seq | INTEGER | — thêm ở 0166_content_production_plane.sql |
part | TEXT | — thêm ở 0166_content_production_plane.sql |
preflight | TEXT | — thêm ở 0166_content_production_plane.sql |
requested_by | TEXT | — thêm ở 0166_content_production_plane.sql |
constraint_pressure | TEXT | — thêm ở 0171_item_drafts.sql |
Index: idx_content_runs_started(started_at DESC) · idx_content_runs_course(course_id, started_at DESC) · idx_content_runs_target(course_id, unit_seq, lesson_seq, part, started_at DESC)
course_enrollments
Ai học khoá nào. Khoá chính (course_id, learner_id): một đứa con không đăng ký hai lần cùng một khoá, ép ở tầng dữ liệu. enrolled_by_user_id ghi AI BẤM đăng ký — bố mẹ hay chính con. Cần cho hai việc: đối chiếu khi có tranh cãi, và biết bố mẹ có thật sự tham gia quyết định hay không (tín hiệu cho Parent Model).
migration: 0074_courses.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
course_id | TEXT | NOT NULL REFERENCES courses(id) |
learner_id | TEXT | NOT NULL REFERENCES learners(id) |
enrolled_by_user_id | TEXT | NOT NULL REFERENCES users(id) |
enrolled_by_role | TEXT | NOT NULL CHECK (enrolled_by_role IN ('parent','learner')) |
status | TEXT | NOT NULL DEFAULT 'active' CHECK (status IN ('active','cancelled')) |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
updated_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (course_id, learner_id) |
Khóa ngoại: course_id → courses · learner_id → learners · enrolled_by_user_id → users
Index: idx_course_enroll_learner(learner_id, status)
course_sessions
Từng buổi. Sinh sẵn thành DÒNG THẬT chứ không tính bằng quy tắc lặp khi đọc: nghỉ lễ, đổi ngày, huỷ một buổi đều là chuyện sẽ xảy ra, và sửa một dòng thì được, sửa một quy tắc lặp thì kéo theo mọi buổi khác.
migration: 0074_courses.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
course_id | TEXT | NOT NULL REFERENCES courses(id) ON DELETE CASCADE |
session_no | INTEGER | NOT NULL, -- 1, 2, 3… quyết định miễn phí hay tính tiền |
session_date | TEXT | NOT NULL, -- YYYY-MM-DD giờ Việt Nam |
start_time | TEXT | NOT NULL |
end_time | TEXT | NOT NULL |
status | TEXT | NOT NULL DEFAULT 'scheduled' CHECK (status IN ('scheduled','cancelled')) |
note | TEXT | — |
| — | table constraint | UNIQUE (course_id, session_no) |
mode | TEXT | — thêm ở 0201_calendar.sql |
venue | TEXT | — thêm ở 0201_calendar.sql |
address | TEXT | — thêm ở 0201_calendar.sql |
meeting_url | TEXT | — thêm ở 0201_calendar.sql |
mentor_user_id | TEXT | REFERENCES users(id) — thêm ở 0201_calendar.sql |
Khóa ngoại: course_id → courses
Index: idx_course_sessions_date(course_id, session_date) · idx_course_sessions_mentor(mentor_user_id, session_date)
courses
Khoá học có lịch, bố mẹ đăng ký cho con (SRC-530, REQ-PAR-18/REQ-LRN-22). Khác community_events ở chỗ nào, và vì sao KHÔNG dùng lại bảng đó: một sự kiện là MỘT buổi rời rạc, ai cũng tự giữ chỗ cho mình. Khoá học là MỘT CHUỖI buổi có thứ tự, người đăng ký là learner nhưng người bấm đăng ký thường là bố mẹ, và "buổi thứ mấy" quyết định miễn phí hay tính tiền. Nhét vào cùng bảng thì mọi truy vấn sự kiện phải mang theo ba khái niệm nó không cần.
migration: 0074_courses.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY, -- slug, dùng thẳng trên URL |
subject_id | TEXT | NOT NULL, -- math | vietnamese | … |
title | TEXT | NOT NULL |
summary | TEXT | NOT NULL DEFAULT '' |
description | TEXT | NOT NULL DEFAULT '' |
audience | TEXT | , -- "Học sinh cấp 2" |
grade_min | INTEGER | , grade_max INTEGER |
start_time | TEXT | NOT NULL, -- 'HH:MM' giờ Việt Nam |
end_time | TEXT | NOT NULL |
starts_on | TEXT | NOT NULL, -- YYYY-MM-DD, buổi đầu |
ends_on | TEXT | NOT NULL, -- YYYY-MM-DD, hạn cuối |
trial_sessions | INTEGER | NOT NULL DEFAULT 2 |
requires_laptop | INTEGER | NOT NULL DEFAULT 0 |
capacity | INTEGER | , -- NULL = chưa giới hạn |
fee_note | TEXT | — |
status | TEXT | NOT NULL DEFAULT 'active' CHECK (status IN ('active','cancelled','draft')) |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
mode | TEXT | NOT NULL DEFAULT 'offline' — thêm ở 0201_calendar.sql |
venue | TEXT | — thêm ở 0201_calendar.sql |
address | TEXT | — thêm ở 0201_calendar.sql |
meeting_url | TEXT | — thêm ở 0201_calendar.sql |
mentor_user_id | TEXT | REFERENCES users(id) — thêm ở 0201_calendar.sql |
Index: idx_courses_status(status, starts_on) · idx_courses_mentor(mentor_user_id)
curriculum_course_outcomes
migration: 0086_course_specs_cp_english.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
course_id | TEXT | NOT NULL REFERENCES curriculum_courses(id) |
seq | INTEGER | NOT NULL |
text_vi | TEXT | NOT NULL |
| — | table constraint | PRIMARY KEY (course_id, seq) |
text_en | TEXT | — thêm ở 0181_curriculum_bilingual.sql |
Khóa ngoại: course_id → curriculum_courses
curriculum_course_pains
Nỗi đau của bố mẹ, viết bằng GIỌNG CỦA CHÍNH HỌ (ngôi thứ nhất). Đây là thứ đứng đầu trang khoá học: bố mẹ phải nhận ra mình trong đó thì mới đọc tiếp. Bảng riêng chứ không nhét vào curriculum_course_outcomes: nỗi đau là chỗ ĐANG đứng, outcome là chỗ SẼ tới. Gộp một bảng thì không cổng nào kiểm được "khoá này chưa khai nỗi đau nào".
migration: 0203_curriculum_audience.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
course_id | TEXT | NOT NULL REFERENCES curriculum_courses(id) |
seq | INTEGER | NOT NULL |
text_vi | TEXT | NOT NULL |
text_en | TEXT | — |
| — | table constraint | PRIMARY KEY (course_id, seq) |
Khóa ngoại: course_id → curriculum_courses
curriculum_course_prereqs
migration: 0086_course_specs_cp_english.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
course_id | TEXT | NOT NULL REFERENCES curriculum_courses(id) |
seq | INTEGER | NOT NULL |
text_vi | TEXT | NOT NULL |
| — | table constraint | PRIMARY KEY (course_id, seq) |
Khóa ngoại: course_id → curriculum_courses
curriculum_course_sources
Ngữ liệu và nguồn của một course: gdpt (mã trong bảng chuẩn phủ), b21 (unit cây năng lực Pearl), aops (unit bộ AoPS), material (văn bản/ngữ liệu môn Văn), competency (năng lực đích), note (ô trống CÓ CHỦ — khai ✍️ thay vì im lặng, đúng luật S10).
migration: 0084_curriculum_courses.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
course_id | TEXT | NOT NULL REFERENCES curriculum_courses(id) |
kind | TEXT | NOT NULL CHECK (kind IN ('gdpt','b21','aops','material','competency','note')) |
seq | INTEGER | NOT NULL |
ref_code | TEXT | — |
label_vi | TEXT | NOT NULL |
| — | table constraint | PRIMARY KEY (course_id, kind, seq) |
Khóa ngoại: course_id → curriculum_courses
curriculum_course_specs
Migration 0086 — Bảng spec CD-8/9/10 + hai bộ course mới: Tin lập trình thi đấu, Tiếng Anh (SRC-616). Phần 1: chỗ lưu cho các thành phần của ba đặc tả (course-design-standard CD-8/9/10) chưa có nhà sau 0084/0085: purpose/progression/assessment strategy/completion criteria (course), purpose/ progression/mastery criteria + prerequisites dạng chữ + misconception map (unit), misconception/ error/intervention (lesson). Mỗi thứ một bảng vì chúng là thứ ĐƯỢC ĐỐI CHIẾU (triết lý 0085). Phần 2: seed 10 course Tin học hướng lập trình thi đấu + 15 course Tiếng Anh, ladder 'core', status draft. GDPT: hai môn CHƯA có bảng chuẩn phủ — khai note 🚧 thay vì im lặng (luật S10). Idempotent: CREATE IF NOT EXISTS + INSERT ... ON CONFLICT DO UPDATE trên cột sở hữu.
migration: 0086_course_specs_cp_english.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
course_id | TEXT | PRIMARY KEY REFERENCES curriculum_courses(id) |
purpose_vi | TEXT | — |
progression_vi | TEXT | — |
assessment_strategy_vi | TEXT | — |
completion_criteria_vi | TEXT | — |
purpose_en | TEXT | — thêm ở 0181_curriculum_bilingual.sql |
jtbd_json | TEXT | — thêm ở 0203_curriculum_audience.sql |
Khóa ngoại: course_id → curriculum_courses
curriculum_courses
Migration 0084 — Tầng Course của curriculum (SRC-609). BẢNG MỚI TINH, cố ý KHÔNG đụng bảng courses cũ: courses là LỚP HỌC CÓ LỊCH thu phí (SRC-550, migration 0074 — "Toán 1", "Tiếng Việt 1"...), còn đây là tầng cấu trúc curriculum: ladder → level (1-5, thay khái niệm lớp) → course → unit (Big Idea + Essential Question + key concepts + ngữ liệu + khung tư duy) → lesson (1 Guiding Question, 1-2 concepts). Chuẩn số lượng và chất lượng: docs/reference/course-design-standard.md. Mọi course seed ở status 'draft' — nội dung (Experience, câu hỏi) chưa gắn; nâng 'live' là việc của từng đợt soạn nội dung, không phải của migration này. Idempotent: CREATE IF NOT EXISTS + INSERT ... ON CONFLICT DO UPDATE trên đúng cột sở hữu.
migration: 0084_curriculum_courses.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
subject_id | TEXT | NOT NULL |
ladder | TEXT | NOT NULL CHECK (ladder IN ('arithmetic','algebra','geometry','data','core')) |
level | INTEGER | NOT NULL CHECK (level BETWEEN 1 AND 5) |
seq | INTEGER | NOT NULL CHECK (seq BETWEEN 1 AND 3) |
code | TEXT | NOT NULL |
name_vi | TEXT | NOT NULL |
status | TEXT | NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','live','retired')) |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
name_en | TEXT | — thêm ở 0181_curriculum_bilingual.sql |
audience | TEXT | NOT NULL DEFAULT 'nemo' — thêm ở 0203_curriculum_audience.sql |
course_group | TEXT | — thêm ở 0203_curriculum_audience.sql |
display_seq | INTEGER | — thêm ở 0203_curriculum_audience.sql |
Index: idx_curriculum_courses_slot(subject_id, ladder, level, seq) UNIQUE · idx_curriculum_courses_code(subject_id, code) UNIQUE · idx_curriculum_courses_audience(audience, display_seq)
curriculum_entitlements
Học thử lesson đầu, và quyền học phần còn lại (chỉ đạo chủ dự án 2026-09-05). Chỉ đạo: "Các khóa đều cho phép học thử một lesson đầu tiên." Điều làm migration này khác với vẻ ngoài của nó: trước hôm nay curriculum KHÔNG có khoá nào. Mọi route lesson chỉ hỏi "anh có quyền trên learner này không", không hỏi "learner này có được học khoá này không" — nên một learner vào được cả 201 lesson. Vì vậy "mở lesson đầu" thực chất là DỰNG CÁI KHOÁ rồi chừa lesson đầu ra, chứ không phải nới một cửa đã có. Vì sao khoá theo learner chứ không theo user: một phụ huynh có nhiều con, và tiền trả cho đứa này không phải là quyền học của đứa kia. granted_to_user_id giữ lại AI đã trả, để đối soát. Vì sao không có cột expires_at: chưa có luật hết hạn nào được chốt, và một cột không ai đọc là một lời hứa sai — thêm khi nào có luật thật.
migration: 0202_trial_lesson.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
learner_id | TEXT | NOT NULL REFERENCES learners(id) |
course_id | TEXT | NOT NULL REFERENCES curriculum_courses(id) |
source | TEXT | NOT NULL DEFAULT 'purchase' CHECK (source IN ('purchase','grant')) |
order_ref | TEXT | — |
granted_to_user_id | TEXT | REFERENCES users(id) |
note | TEXT | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: learner_id → learners · course_id → curriculum_courses · granted_to_user_id → users
Index: idx_curriculum_entitlements_one(learner_id, course_id) UNIQUE · idx_curriculum_entitlements_course(course_id)
curriculum_lesson_article_versions
Lịch sử phiên bản. Một bài đọc là công sức nhiều buổi, và trình soạn nào cũng có ngày nuốt mất một đoạn: dán đè, chọn nhầm rồi gõ tiếp, một lần Ctrl+A không cố ý. Không có bản cũ thì mất hẳn. Ghi MỘT dòng cho mỗi lần lưu bản nháp; bản render html không lưu lại vì sinh lại được từ doc bất cứ lúc nào, còn doc thì không sinh lại được từ đâu cả.
migration: 0209_lesson_articles.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
article_id | TEXT | NOT NULL REFERENCES curriculum_lesson_articles(id) |
version | INTEGER | NOT NULL |
title_vi | TEXT | NOT NULL |
doc_vi | TEXT | NOT NULL |
doc_en | TEXT | — |
saved_at | TEXT | NOT NULL DEFAULT (datetime('now')) |
saved_by | TEXT | — |
| — | table constraint | PRIMARY KEY (article_id, version) |
Khóa ngoại: article_id → curriculum_lesson_articles
curriculum_lesson_articles
Tài liệu đọc trong bài học, kiểu Substack (SRC-692, SDD-033). Chỉ đạo chủ dự án 2026-09-08: "Trong mọi lesson trong các khóa học, cần có một hoặc vài tài liệu, dạng như của Substack, có thể dùng TipTap format, có thể sửa nội dung, soạn trong phần admin, có thể nhúng ảnh hay video vào." VÌ SAO KHÔNG DÙNG curriculum_lesson_materials. Bảng đó (migration 0085) trỏ RA NGOÀI: một URL YouTube, một PDF, một lab. Nó không có chỗ chứa nội dung, và ràng buộc CHECK của nó nói thẳng điều ấy: mọi kind đều đòi url hoặc ref_id. Tài liệu ở đây là nội dung TỰ SOẠN nằm trong D1, có bản nháp, có lịch sử phiên bản, có bản dịch. Nhét vào bảng cũ thì phải nới CHECK và thêm sáu cột chỉ một kind dùng tới, tức là làm hỏng chính cái ràng buộc đang giữ cho bảng ấy đúng. HAI CỘT CHO MỘT NỘI DUNG (doc_* và html_*), cố ý: · doc là TipTap JSON, thứ trình soạn mở ra để sửa. Nguồn chuẩn. · html là bản render đã lọc, thứ trang học đọc. Sinh ra từ doc Ở SERVER mỗi lần lưu. Render ở client thì apps/learn phải bundle cả TipTap chỉ để đọc, và mỗi app đọc lại tự lọc HTML một kiểu. Render một lần lúc lưu thì chỉ có MỘT bộ lọc trong cả hệ, và nó nằm ở nơi duy nhất người ngoài không sửa được. html không bao giờ được nhận từ client. status: bản nháp KHÔNG tới tay learner. Soạn dở một bài đọc là chuyện của nhiều buổi, và không có nháp thì người soạn buộc phải viết một mạch hoặc để learner nhìn thấy bài viết dở.
migration: 0209_lesson_articles.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
slug | TEXT | NOT NULL |
seq | INTEGER | NOT NULL |
title_vi | TEXT | NOT NULL |
title_en | TEXT | — |
summary_vi | TEXT | — |
summary_en | TEXT | — |
cover_media_id | TEXT | — |
minutes | INTEGER | — |
doc_vi | TEXT | NOT NULL |
html_vi | TEXT | NOT NULL |
doc_en | TEXT | — |
html_en | TEXT | — |
status | TEXT | NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published')) |
created_at | TEXT | NOT NULL DEFAULT (datetime('now')) |
updated_at | TEXT | NOT NULL DEFAULT (datetime('now')) |
published_at | TEXT | — |
updated_by | TEXT | — |
| — | table constraint | UNIQUE (lesson_id, slug) |
Khóa ngoại: lesson_id → curriculum_lessons
Index: idx_lesson_articles_lesson(lesson_id, seq)
curriculum_lesson_checks
Kiểm tra cuối lesson. kind dùng ĐÚNG bộ answer_kind của SDD-022 cộng thêm reflection: năm dạng kia máy chấm được, reflection là bài viết — người hoặc mô hình đọc.
migration: 0085_curriculum_outcomes.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
seq | INTEGER | NOT NULL |
kind | TEXT | NOT NULL CHECK (kind IN ('mcq','numeric','estimate','order_steps','find_error','reflection')) |
prompt_vi | TEXT | NOT NULL |
outcome_seq | INTEGER | — |
| — | table constraint | PRIMARY KEY (lesson_id, seq) |
Khóa ngoại: lesson_id → curriculum_lessons
curriculum_lesson_concepts
Migration 0187 — Key Concepts của Lesson, và một câu hỏi duy nhất cho mỗi tầng (SRC-663). Chỉ đạo chủ dự án 2026-08-31: "Mỗi lesson cần có đúng 1 Guiding Question, và có 1-2 key concepts. Mỗi Unit cần có đúng 1 Essential Question, và có một vài key concepts." ĐÚNG MỘT CÂU HỎI — chỗ này KHÔNG cần bảng mới và cũng không được xoá gì. Cột đơn đã có sẵn từ đầu (curriculum_lessons.guiding_question_vi, curriculum_units.essential_question_vi) và nó chính là bản CHÍNH THỨC: đúng một câu, không thể nhiều hơn. Bảng nhiều câu (0181) giữ nguyên vai trò khác — nó là bộ câu hỏi chủ dự án viết ra lúc thiết kế, tức là NGUỒN, và nguồn thì không xoá. Việc phải sửa nằm ở chỗ hiển thị: trang đang vẽ cả bộ thay vì vẽ bản chính thức. Bảng dưới đây là thứ duy nhất còn thiếu: key concept ở tầng Lesson. Tầng Unit đã có curriculum_unit_concepts (0183); lesson thì mới chỉ có cột chữ concepts_vi dạng một dòng gộp, không tách được từng mục và không có bản tiếng Anh.
migration: 0187_lesson_concepts.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
seq | INTEGER | NOT NULL |
label_vi | TEXT | NOT NULL |
label_en | TEXT | — |
detail_vi | TEXT | — |
detail_en | TEXT | — |
| — | table constraint | PRIMARY KEY (lesson_id, seq) |
Khóa ngoại: lesson_id → curriculum_lessons
curriculum_lesson_errors
migration: 0086_course_specs_cp_english.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
seq | INTEGER | NOT NULL |
text_vi | TEXT | NOT NULL |
| — | table constraint | PRIMARY KEY (lesson_id, seq) |
text_en | TEXT | — thêm ở 0194_lesson_content_bilingual.sql |
Khóa ngoại: lesson_id → curriculum_lessons
curriculum_lesson_examples
Ví dụ và ví dụ DỄ NHẦM của một lesson. Cặp này phải đi cùng nhau và đó là toàn bộ ý nghĩa của bảng: một khái niệm chỉ thật sự rõ khi learner thấy được ĐƯỜNG BIÊN của nó. Ba ví dụ đúng dạy được cái bên trong; ba ví dụ suýt-đúng mới dạy được cái bên ngoài, và biên giới nằm ở giữa.
migration: 0183_unit_concepts_lesson_examples.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
kind | TEXT | NOT NULL CHECK (kind IN ('example', 'near_miss')) |
seq | INTEGER | NOT NULL |
text_vi | TEXT | NOT NULL |
text_en | TEXT | — |
why_vi | TEXT | — |
why_en | TEXT | — |
| — | table constraint | PRIMARY KEY (lesson_id, kind, seq) |
Khóa ngoại: lesson_id → curriculum_lessons
curriculum_lesson_gate_choices
migration: 0335_lesson_gates.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL |
part_seq | INTEGER | NOT NULL |
quiz_seq | INTEGER | NOT NULL |
seq | INTEGER | NOT NULL CHECK (seq BETWEEN 0 AND 2) |
text_vi | TEXT | NOT NULL |
| — | table constraint | PRIMARY KEY (lesson_id, part_seq, quiz_seq, seq) |
Khóa ngoại: lesson_id, part_seq, quiz_seq → curriculum_lesson_gate_quiz.lesson_id, part_seq, seq
curriculum_lesson_gate_quiz
Migration 0335 - lesson gates. SRC-1265. ## Vì sao migration này tồn tại Chủ dự án 06.10.2026: mỗi chỗ học của khoá bố mẹ Marlins được đọc thành NHIỀU PHẦN NGẮN, cuối mỗi phần là một cổng ba câu trắc nghiệm. Chỉ đúng 3/3 mới mở phần sau; sai thì chờ 3 phút mới được làm lại. Hai bảng, hai việc khác nhau: 1. curriculum_lesson_gate_quiz + _choices: ba câu của từng phần, cạnh thân bài lesson đã ở D1 (migration 0204). Nạp bằng seed sinh từ apps/marlins/src/parentLessonGates/ qua scripts/curriculum/gen-parent-gates.mjs. Thiếu bảng này thì nội dung cổng chỉ sống trong bundle của app, và tầng curriculum (Coral, audit) không thấy nó. 2. parent_lesson_gates: lượt làm cổng của từng phụ huynh. Thời gian chờ 3 phút phải nằm ở server: để ở trình duyệt thì tải lại trang, đổi máy hay mở cửa sổ ẩn danh là xoá được nó, và luật "chỉ 100% mới qua" thành luật không ai thi hành. parent_course_progress (0197) chỉ có một dòng cho cả chỗ học, không có chỗ cho từng phần. Bảng lượt làm KHÔNG giữ đáp án đã chọn: chủ dự án cấm cho biết câu nào đúng câu nào sai, nên server chỉ cần biết tổng điểm của lần gần nhất, lúc trượt gần nhất và lúc qua. ## Chạy lại không đổi gì Chỉ có CREATE ... IF NOT EXISTS.
migration: 0335_lesson_gates.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
lesson_id | TEXT | NOT NULL REFERENCES curriculum_lessons(id) |
part_seq | INTEGER | NOT NULL CHECK (part_seq >= 0) |
seq | INTEGER | NOT NULL CHECK (seq BETWEEN 0 AND 2) |
stem_vi | TEXT | NOT NULL |
answer | INTEGER | NOT NULL CHECK (answer BETWEEN 0 AND 2) |
why_vi | TEXT | NOT NULL |
| — | table constraint | PRIMARY KEY (lesson_id, part_seq, seq) |
Khóa ngoại: lesson_id → curriculum_lessons