Skip to content

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ạy npm 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ộtKiểuRàng buộc / ghi chú
course_idTEXTNOT NULL
unit_seqINTEGERNOT NULL
lesson_seqINTEGERNOT NULL
partTEXT—
target_labelTEXTNOT NULL
attemptsINTEGERNOT NULL
recurring_problemsTEXT—
best_scoreINTEGER—
first_blocked_atTEXTNOT NULL
last_attempt_atTEXT—
statusTEXTNOT NULL DEFAULT 'open' CHECK (status IN ('open','released','wont_fix'))
resolution_noteTEXT—
released_atTEXT—
updated_atTEXTNOT NULL
—table constraintPRIMARY 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ộtKiểuRàng buộc / ghi chú
idTEXTPRIMARY KEY
scopeTEXTNOT NULL CHECK (scope IN ('prompt', 'content'))
course_idTEXT—
unit_seqINTEGER—
lesson_seqINTEGER—
partTEXT—
run_idTEXT—
prompt_versionINTEGER—
rubric_versionTEXT—
verdictTEXTNOT NULL CHECK (verdict IN ('praise', 'fix', 'regenerate'))
commentTEXTNOT NULL
statusTEXTNOT NULL DEFAULT 'open' CHECK (status IN ('open', 'applied', 'superseded'))
resolutionTEXT—
created_byTEXT—
created_atTEXTNOT NULL
updated_atTEXTNOT 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ộtKiểuRàng buộc / ghi chú
run_idTEXTNOT NULL REFERENCES content_runs(id)
seqINTEGERNOT NULL
node_idTEXTNOT NULL
subject_idTEXTNOT NULL
roleTEXTNOT NULL CHECK (role IN ('canonical','variant','boundary','discriminator'))
difficultyREALNOT NULL
bucketTEXTNOT NULL CHECK (bucket IN ('practice','assessment'))
promptTEXTNOT NULL
options_jsonTEXTNOT NULL
correct_indexINTEGERNOT NULL
misconceptionTEXT—
attemptsINTEGERNOT NULL DEFAULT 1
usdREALNOT NULL DEFAULT 0
prompt_versionINTEGER—
created_atTEXTNOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
—table constraintPRIMARY KEY (run_id, seq)
quality_scoreREAL— thêm ở 0172_item_draft_quality.sql
quality_jsonTEXT— thêm ở 0172_item_draft_quality.sql
gate_okINTEGERNOT NULL DEFAULT 0 — thêm ở 0172_item_draft_quality.sql
gate_reasonsTEXT— 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ộtKiểuRàng buộc / ghi chú
dimensionTEXTNOT NULL CHECK (dimension IN ('subject','level','course'))
keyTEXTNOT NULL
rankINTEGERNOT NULL
enabledINTEGERNOT NULL DEFAULT 1
note_viTEXT—
—table constraintPRIMARY 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ộtKiểuRàng buộc / ghi chú
run_idTEXTNOT NULL REFERENCES content_runs(id)
seqINTEGERNOT NULL
partTEXTNOT NULL
actionTEXTNOT NULL CHECK (action IN ('created','updated','unchanged'))
before_previewTEXT—
after_previewTEXT—
—table constraintPRIMARY KEY (run_id, seq)

Khóa ngoại: run_id → content_runs

content_run_trials ​

migration: 0164_content_foundry.sql

CộtKiểuRàng buộc / ghi chú
run_idTEXTNOT NULL REFERENCES content_runs(id)
model_keyTEXTNOT NULL
okINTEGERNOT NULL
scoreINTEGERNOT NULL
max_scoreINTEGERNOT NULL
repairedINTEGERNOT NULL DEFAULT 0
msINTEGERNOT NULL
tokens_inINTEGERNOT NULL
tokens_outINTEGERNOT NULL
usdREALNOT NULL
errorTEXT—
problemsTEXT, -- JSON mảng chỗ chưa đạt, để đọc mà không phải mở R2
—table constraintPRIMARY KEY (run_id, model_key)
prompt_versionINTEGER— thêm ở 0165_content_trials_versions.sql
rubric_versionTEXT— thêm ở 0165_content_trials_versions.sql
usage_estimatedINTEGERNOT NULL DEFAULT 0 — thêm ở 0165_content_trials_versions.sql
draft_jsonTEXT— 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ộtKiểuRàng buộc / ghi chú
idTEXTPRIMARY KEY
kindTEXTNOT NULL
target_labelTEXTNOT NULL
model_keysTEXTNOT NULL
statusTEXTNOT NULL, -- running | done | failed
best_model_keyTEXT—
best_scoreINTEGER—
started_atTEXTNOT NULL
finished_atTEXT—
course_idTEXT— thêm ở 0166_content_production_plane.sql
unit_seqINTEGER— thêm ở 0166_content_production_plane.sql
lesson_seqINTEGER— thêm ở 0166_content_production_plane.sql
partTEXT— thêm ở 0166_content_production_plane.sql
preflightTEXT— thêm ở 0166_content_production_plane.sql
requested_byTEXT— thêm ở 0166_content_production_plane.sql
constraint_pressureTEXT— 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ộtKiểuRàng buộc / ghi chú
course_idTEXTNOT NULL REFERENCES courses(id)
learner_idTEXTNOT NULL REFERENCES learners(id)
enrolled_by_user_idTEXTNOT NULL REFERENCES users(id)
enrolled_by_roleTEXTNOT NULL CHECK (enrolled_by_role IN ('parent','learner'))
statusTEXTNOT NULL DEFAULT 'active' CHECK (status IN ('active','cancelled'))
created_atTEXTNOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
updated_atTEXTNOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
—table constraintPRIMARY 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ộtKiểuRàng buộc / ghi chú
idTEXTPRIMARY KEY
course_idTEXTNOT NULL REFERENCES courses(id) ON DELETE CASCADE
session_noINTEGERNOT NULL, -- 1, 2, 3… quyết định miễn phí hay tính tiền
session_dateTEXTNOT NULL, -- YYYY-MM-DD giờ Việt Nam
start_timeTEXTNOT NULL
end_timeTEXTNOT NULL
statusTEXTNOT NULL DEFAULT 'scheduled' CHECK (status IN ('scheduled','cancelled'))
noteTEXT—
—table constraintUNIQUE (course_id, session_no)
modeTEXT— thêm ở 0201_calendar.sql
venueTEXT— thêm ở 0201_calendar.sql
addressTEXT— thêm ở 0201_calendar.sql
meeting_urlTEXT— thêm ở 0201_calendar.sql
mentor_user_idTEXTREFERENCES 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ộtKiểuRàng buộc / ghi chú
idTEXTPRIMARY KEY, -- slug, dùng thẳng trên URL
subject_idTEXTNOT NULL, -- math | vietnamese | …
titleTEXTNOT NULL
summaryTEXTNOT NULL DEFAULT ''
descriptionTEXTNOT NULL DEFAULT ''
audienceTEXT, -- "Học sinh cấp 2"
grade_minINTEGER, grade_max INTEGER
start_timeTEXTNOT NULL, -- 'HH:MM' giờ Việt Nam
end_timeTEXTNOT NULL
starts_onTEXTNOT NULL, -- YYYY-MM-DD, buổi đầu
ends_onTEXTNOT NULL, -- YYYY-MM-DD, hạn cuối
trial_sessionsINTEGERNOT NULL DEFAULT 2
requires_laptopINTEGERNOT NULL DEFAULT 0
capacityINTEGER, -- NULL = chưa giới hạn
fee_noteTEXT—
statusTEXTNOT NULL DEFAULT 'active' CHECK (status IN ('active','cancelled','draft'))
created_atTEXTNOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
modeTEXTNOT NULL DEFAULT 'offline' — thêm ở 0201_calendar.sql
venueTEXT— thêm ở 0201_calendar.sql
addressTEXT— thêm ở 0201_calendar.sql
meeting_urlTEXT— thêm ở 0201_calendar.sql
mentor_user_idTEXTREFERENCES 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ộtKiểuRàng buộc / ghi chú
course_idTEXTNOT NULL REFERENCES curriculum_courses(id)
seqINTEGERNOT NULL
text_viTEXTNOT NULL
—table constraintPRIMARY KEY (course_id, seq)
text_enTEXT— 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ộtKiểuRàng buộc / ghi chú
course_idTEXTNOT NULL REFERENCES curriculum_courses(id)
seqINTEGERNOT NULL
text_viTEXTNOT NULL
text_enTEXT—
—table constraintPRIMARY KEY (course_id, seq)

Khóa ngoại: course_id → curriculum_courses

curriculum_course_prereqs ​

migration: 0086_course_specs_cp_english.sql

CộtKiểuRàng buộc / ghi chú
course_idTEXTNOT NULL REFERENCES curriculum_courses(id)
seqINTEGERNOT NULL
text_viTEXTNOT NULL
—table constraintPRIMARY 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ộtKiểuRàng buộc / ghi chú
course_idTEXTNOT NULL REFERENCES curriculum_courses(id)
kindTEXTNOT NULL CHECK (kind IN ('gdpt','b21','aops','material','competency','note'))
seqINTEGERNOT NULL
ref_codeTEXT—
label_viTEXTNOT NULL
—table constraintPRIMARY 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ộtKiểuRàng buộc / ghi chú
course_idTEXTPRIMARY KEY REFERENCES curriculum_courses(id)
purpose_viTEXT—
progression_viTEXT—
assessment_strategy_viTEXT—
completion_criteria_viTEXT—
purpose_enTEXT— thêm ở 0181_curriculum_bilingual.sql
jtbd_jsonTEXT— 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ộtKiểuRàng buộc / ghi chú
idTEXTPRIMARY KEY
subject_idTEXTNOT NULL
ladderTEXTNOT NULL CHECK (ladder IN ('arithmetic','algebra','geometry','data','core'))
levelINTEGERNOT NULL CHECK (level BETWEEN 1 AND 5)
seqINTEGERNOT NULL CHECK (seq BETWEEN 1 AND 3)
codeTEXTNOT NULL
name_viTEXTNOT NULL
statusTEXTNOT NULL DEFAULT 'draft' CHECK (status IN ('draft','live','retired'))
created_atTEXTNOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
name_enTEXT— thêm ở 0181_curriculum_bilingual.sql
audienceTEXTNOT NULL DEFAULT 'nemo' — thêm ở 0203_curriculum_audience.sql
course_groupTEXT— thêm ở 0203_curriculum_audience.sql
display_seqINTEGER— 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ộtKiểuRàng buộc / ghi chú
idTEXTPRIMARY KEY
learner_idTEXTNOT NULL REFERENCES learners(id)
course_idTEXTNOT NULL REFERENCES curriculum_courses(id)
sourceTEXTNOT NULL DEFAULT 'purchase' CHECK (source IN ('purchase','grant'))
order_refTEXT—
granted_to_user_idTEXTREFERENCES users(id)
noteTEXT—
created_atTEXTNOT 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ộtKiểuRàng buộc / ghi chú
article_idTEXTNOT NULL REFERENCES curriculum_lesson_articles(id)
versionINTEGERNOT NULL
title_viTEXTNOT NULL
doc_viTEXTNOT NULL
doc_enTEXT—
saved_atTEXTNOT NULL DEFAULT (datetime('now'))
saved_byTEXT—
—table constraintPRIMARY 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ộtKiểuRàng buộc / ghi chú
idTEXTPRIMARY KEY
lesson_idTEXTNOT NULL REFERENCES curriculum_lessons(id)
slugTEXTNOT NULL
seqINTEGERNOT NULL
title_viTEXTNOT NULL
title_enTEXT—
summary_viTEXT—
summary_enTEXT—
cover_media_idTEXT—
minutesINTEGER—
doc_viTEXTNOT NULL
html_viTEXTNOT NULL
doc_enTEXT—
html_enTEXT—
statusTEXTNOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'published'))
created_atTEXTNOT NULL DEFAULT (datetime('now'))
updated_atTEXTNOT NULL DEFAULT (datetime('now'))
published_atTEXT—
updated_byTEXT—
—table constraintUNIQUE (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ộtKiểuRàng buộc / ghi chú
lesson_idTEXTNOT NULL REFERENCES curriculum_lessons(id)
seqINTEGERNOT NULL
kindTEXTNOT NULL CHECK (kind IN ('mcq','numeric','estimate','order_steps','find_error','reflection'))
prompt_viTEXTNOT NULL
outcome_seqINTEGER—
—table constraintPRIMARY 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ộtKiểuRàng buộc / ghi chú
lesson_idTEXTNOT NULL REFERENCES curriculum_lessons(id)
seqINTEGERNOT NULL
label_viTEXTNOT NULL
label_enTEXT—
detail_viTEXT—
detail_enTEXT—
—table constraintPRIMARY KEY (lesson_id, seq)

Khóa ngoại: lesson_id → curriculum_lessons

curriculum_lesson_errors ​

migration: 0086_course_specs_cp_english.sql

CộtKiểuRàng buộc / ghi chú
lesson_idTEXTNOT NULL REFERENCES curriculum_lessons(id)
seqINTEGERNOT NULL
text_viTEXTNOT NULL
—table constraintPRIMARY KEY (lesson_id, seq)
text_enTEXT— 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ộtKiểuRàng buộc / ghi chú
lesson_idTEXTNOT NULL REFERENCES curriculum_lessons(id)
kindTEXTNOT NULL CHECK (kind IN ('example', 'near_miss'))
seqINTEGERNOT NULL
text_viTEXTNOT NULL
text_enTEXT—
why_viTEXT—
why_enTEXT—
—table constraintPRIMARY KEY (lesson_id, kind, seq)

Khóa ngoại: lesson_id → curriculum_lessons

curriculum_lesson_gate_choices ​

migration: 0335_lesson_gates.sql

CộtKiểuRàng buộc / ghi chú
lesson_idTEXTNOT NULL
part_seqINTEGERNOT NULL
quiz_seqINTEGERNOT NULL
seqINTEGERNOT NULL CHECK (seq BETWEEN 0 AND 2)
text_viTEXTNOT NULL
—table constraintPRIMARY 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ộtKiểuRàng buộc / ghi chú
lesson_idTEXTNOT NULL REFERENCES curriculum_lessons(id)
part_seqINTEGERNOT NULL CHECK (part_seq >= 0)
seqINTEGERNOT NULL CHECK (seq BETWEEN 0 AND 2)
stem_viTEXTNOT NULL
answerINTEGERNOT NULL CHECK (answer BETWEEN 0 AND 2)
why_viTEXTNOT NULL
—table constraintPRIMARY KEY (lesson_id, part_seq, seq)

Khóa ngoại: lesson_id → curriculum_lessons