Data Dictionary · Khác (phần 5/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. 24 bảng, grammar_live_signups tới ielts_micro_progress.
grammar_live_signups
Migration 0340 - Đăng ký buổi Live của NEMO GRAMMAR và bản ghi từng buổi. SRC-1307. ## Vì sao migration này tồn tại Chủ dự án 09.10.2026: NEMO GRAMMAR có buổi Live qua Zoom mỗi tối thứ Hai 20:00-21:30 (giờ VN), từ 12.10.2026. Learner bấm tham gia thì mới thấy link Zoom, và mỗi buổi có bản ghi để xem lại. Buổi là lịch cố định nên máy chủ tính ra từ một mốc (grammarLive/schedule.ts), giống Speak together (migration 0303): không có bảng buổi, không có việc định kỳ phải sinh lịch trước. Chỉ hai thứ phải nhớ: ai đã tham gia buổi nào, và link bản ghi (YouTube không công khai) của buổi đã xong. Không có bảng đăng ký thì nút tham gia không nhớ được gì và link Zoom lộ cho mọi người. ## Chạy lại không đổi gì CREATE TABLE / INDEX IF NOT EXISTS.
migration: 0340_grammar_live_signups_and_recordings.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
session_id | TEXT | NOT NULL |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (session_id, learner_id) |
Khóa ngoại: learner_id → learners
Index: idx_grammar_live_signups_learner(learner_id, session_id)
gull_entities
Migration 0306 - Gull, customer understanding. SRC-1094, SDD-047. ## Vì sao migration này tồn tại Gull (gull.nemo12.com) là System of Record for Customer Understanding (PRD-007). Mười hai loại thực thể (Program, Stakeholder, Persona, JTBD, Need, Pain, Journey, Stage, Step, Touchpoint, Evidence, Opportunity) có chung phần lớn trường quản trị, nên chúng nằm trong MỘT bảng gull_entities với cột kind; trường riêng của từng loại nằm trong fields (JSON). Giá trị của Gull nằm ở đường nối, nên quan hệ là bảng riêng gull_links, có thứ tự (position, cho Stage trong Journey và Step trong Stage) và có thể gắn một chương trình (program_id: Program Context). Không có giao diện sửa (Q-187): chủ dự án sửa bằng script (scripts/gull-build-seed.mjs rồi workflow seed-data). Lịch sử phiên bản vẫn phải có (PRD-007 §4.4), nên trigger ghi bản cũ vào gull_history trước mỗi lần sửa hoặc xoá: người sửa không phải nhớ. ## Chạy lại không đổi gì Mọi câu IF NOT EXISTS.
migration: 0306_gull_customer_understanding.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY, -- slug tiếng Anh ổn định, vd persona-g11-hanoi-ielts |
kind | TEXT | NOT NULL CHECK (kind IN ('program','stakeholder','persona','jtbd','need','pain' |
name | TEXT | NOT NULL |
summary | TEXT | NOT NULL DEFAULT '' |
fields | TEXT | NOT NULL DEFAULT '{}', -- JSON, trường riêng của từng loại (SDD-047 §3) |
status | TEXT | NOT NULL DEFAULT 'draft' CHECK (status IN ('hypothesis','draft','evidence_backed','validated','deprecated')) |
important | INTEGER | NOT NULL DEFAULT 0, -- 1 = nhận định quan trọng, phải có Evidence trừ khi là hypothesis |
tags | TEXT | NOT NULL DEFAULT '[]', -- JSON mảng chuỗi |
owner | TEXT | — |
reviewer | TEXT | — |
source | TEXT | , -- vd docs/product/parent-jtbd.md |
generated_by | TEXT | NOT NULL DEFAULT 'human', -- 'human' hoặc 'ai:<model>' |
confidence | REAL | CHECK (confidence IS NULL OR (confidence >= 0 AND confidence <= 1)) |
human_verified_at | TEXT | — |
version | INTEGER | NOT NULL DEFAULT 1 |
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')) |
Index: idx_gull_entities_kind(kind, status)
gull_history
migration: 0306_gull_customer_understanding.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | INTEGER | PRIMARY KEY AUTOINCREMENT |
entity_id | TEXT | NOT NULL |
version | INTEGER | NOT NULL |
snapshot | TEXT | NOT NULL, -- JSON của dòng TRƯỚC khi đổi |
change | TEXT | NOT NULL CHECK (change IN ('update','delete')) |
changed_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Index: idx_gull_history_entity(entity_id, version)
gull_links
migration: 0306_gull_customer_understanding.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
from_id | TEXT | NOT NULL REFERENCES gull_entities(id) |
to_id | TEXT | NOT NULL REFERENCES gull_entities(id) |
rel | TEXT | NOT NULL, -- 'contains' (journey→stage→step), 'has_pain', 'supports' (evidence→x)… SDD-047 §3.2 |
program_id | TEXT | NOT NULL DEFAULT '', -- '' = quan hệ toàn cục; khác '' = chỉ đúng trong chương trình ấy |
position | INTEGER | — |
note | TEXT | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (from_id, to_id, rel, program_id) |
Khóa ngoại: from_id → gull_entities · to_id → gull_entities
Index: idx_gull_links_to(to_id)
gull_program_context
Program Context: phần khác biệt của một thực thể toàn cục trong một chương trình.
migration: 0306_gull_customer_understanding.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
entity_id | TEXT | NOT NULL REFERENCES gull_entities(id) |
program_id | TEXT | NOT NULL REFERENCES gull_entities(id) |
summary | TEXT | NOT NULL DEFAULT '' |
fields | TEXT | NOT NULL DEFAULT '{}' |
| — | table constraint | PRIMARY KEY (entity_id, program_id) |
Khóa ngoại: entity_id → gull_entities · program_id → gull_entities
ielts_content_clusters
Cụm nội dung: 8 cụm đọc + 8 cụm nghe. Bài khởi động không thuộc cụm nào.
migration: 0255_ielts_content.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
skill | TEXT | NOT NULL CHECK (skill IN ('reading','listening')) |
slug | TEXT | NOT NULL |
unit | INTEGER | NOT NULL DEFAULT 1 |
domain | TEXT | NOT NULL |
name_en | TEXT | NOT NULL |
name_vi | TEXT | NOT NULL |
field_en | TEXT | NOT NULL DEFAULT '' |
field_vi | TEXT | NOT NULL DEFAULT '' |
note_vi | TEXT | NOT NULL DEFAULT '' |
vocabulary_json | TEXT | NOT NULL DEFAULT '[]' |
status | TEXT | NOT NULL DEFAULT 'published' CHECK (status IN ('draft','published')) |
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')) |
Index: idx_content_clusters_slug(skill, slug) UNIQUE
ielts_content_items
Một bài: passage đọc, clip nghe, hoặc bài khởi động.
migration: 0255_ielts_content.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
skill | TEXT | NOT NULL CHECK (skill IN ('reading','listening','warmup')) |
cluster_id | TEXT | REFERENCES ielts_content_clusters(id) ON DELETE CASCADE |
seq | INTEGER | NOT NULL |
slug | TEXT | NOT NULL DEFAULT '' |
title | TEXT | NOT NULL |
kind | TEXT | NOT NULL DEFAULT '' |
domain | TEXT | NOT NULL DEFAULT '' |
body_json | TEXT | NOT NULL |
extra_json | TEXT | NOT NULL DEFAULT '{}' |
status | TEXT | NOT NULL DEFAULT 'published' CHECK (status IN ('draft','published')) |
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')) |
Khóa ngoại: cluster_id → ielts_content_clusters
Index: idx_content_items_cluster(cluster_id, seq) · idx_content_items_skill(skill, status, seq)
ielts_content_keys_seen
Migration 0331 - ielts content keys seen. SRC-1229. ## Vì sao migration này tồn tại Đề đầy đủ ghép từ CHÍNH những câu của kho luyện, và đường chấm luyện (POST .../ielts-content/check) trả đáp án của mọi câu learner gửi lên. Không có sổ này thì server không biết learner đã cầm đáp án của câu nào, nên lúc ghép đề nó có thể đưa đúng những câu learner vừa xem đáp án hôm qua vào bài đo hôm nay, và band trả về đo trí nhớ chứ không đo sức. Mỗi dòng là "learner này đã nhận đáp án của câu này, lần đầu lúc nào"; lúc ghép đề server chọn bài có ít câu đã lộ nhất. ## Chạy lại không đổi gì CREATE TABLE IF NOT EXISTS + index IF NOT EXISTS. Không nạp dữ liệu cũ: những đáp án đã trả trước ngày có sổ không còn dấu vết nào để dựng lại.
migration: 0331_ielts_content_keys_seen.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
learner_id | TEXT | NOT NULL |
question_id | TEXT | NOT NULL |
seen_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (learner_id, question_id) |
Index: idx_ielts_content_keys_seen_question(question_id)
ielts_content_questions
Câu hỏi của một bài. Bảng riêng: xem đoạn đầu file.
migration: 0255_ielts_content.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
item_id | TEXT | NOT NULL REFERENCES ielts_content_items(id) ON DELETE CASCADE |
seq | INTEGER | NOT NULL |
sub_skill | TEXT | NOT NULL |
kind | TEXT | NOT NULL DEFAULT 'choice' CHECK (kind IN ('choice','gap')) |
prompt | TEXT | NOT NULL |
quote | TEXT | NOT NULL DEFAULT '' |
options_json | TEXT | NOT NULL DEFAULT '[]' |
answer_json | TEXT | NOT NULL DEFAULT '[]' |
accept_json | TEXT | NOT NULL DEFAULT '[]' |
why | TEXT | NOT NULL DEFAULT '' |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: item_id → ielts_content_items
Index: idx_content_questions_item(item_id, seq)
ielts_diag4_attempts
Migration 0307 - Chẩn đoán 4 kỹ năng IELTS, mỗi kỹ năng một bài (SRC-1099). Chủ dự án 28.09.2026: mỗi kỹ năng lớn một bài test riêng; mỗi sub-skill 3 câu; sau mỗi câu learner trả lời thêm "chắc chắn đến đâu" để hệ thống phân biệt hiểu thật với đoán; làm xong cả 4 bài thì hệ thống tạo báo cáo và learner xem lại được. Thay bài đo 6 câu mỗi kỹ năng cũ (các phiên cũ giữ nguyên). ## Hai bảng ielts_diag4_attempts: một lượt làm MỘT kỹ năng. items_json chụp lại đề đã phát (mã câu, sub-skill, đáp án) lúc bắt đầu, để kho câu có đổi sau đó thì lượt đã làm vẫn chấm đúng đề learner đã thấy. answers_json lưu dần (thoát ra làm tiếp được), result_json là kết quả chấm lúc nộp. ielts_diag4_reports: một báo cáo mỗi khi đủ bốn kỹ năng đã nộp tính từ báo cáo trước. Lưu thành ảnh chụp để xem lại lần chẩn đoán cũ đúng như lúc ấy, và so sánh các lần với nhau. ## Chạy lại không đổi gì CREATE TABLE / INDEX IF NOT EXISTS.
migration: 0307_ielts_diag4.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
skill | TEXT | NOT NULL CHECK (skill IN ('listening','reading','speaking','writing')) |
items_json | TEXT | NOT NULL |
answers_json | TEXT | NOT NULL DEFAULT '{}' |
result_json | TEXT | — |
report_id | TEXT | — |
started_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
submitted_at | TEXT | — |
Khóa ngoại: learner_id → learners
Index: idx_ielts_diag4_attempts_learner(learner_id, skill, started_at DESC)
ielts_diag4_reports
migration: 0307_ielts_diag4.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
report_json | TEXT | NOT NULL |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: learner_id → learners
Index: idx_ielts_diag4_reports_learner(learner_id, created_at DESC)
ielts_diagnostic_items
Bài đo ngắn IELTS dùng chung bảng assessment_sessions (kind='diagnostic', subject_id='ielts'), không dựng bảng phiên thứ hai. Nhưng một phiên đo IELTS cần biết nó đã hỏi những STRAND nào để tầng suy luận quy kết quả về đúng kỹ năng; item_ids_json chỉ có id câu.
migration: 0218_ielts_declarations.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
session_id | TEXT | NOT NULL |
item_id | TEXT | NOT NULL |
strand | TEXT | NOT NULL, -- listening · reading · writing · speaking · language |
node_id | TEXT | NOT NULL |
position | INTEGER | NOT NULL |
| — | table constraint | PRIMARY KEY (session_id, item_id) |
Index: idx_ielts_diag_session(session_id, position)
ielts_exam_papers
Migration 0257 - Đề thi Writing và Speaking vào D1. SRC-888. Chỉ đạo chủ dự án 2026-09-20: soạn Reading, Writing, Speaking cho đủ 20 đề mỗi kỹ năng. ## Vì sao hai kỹ năng này cần bảng riêng, còn Đọc và Nghe thì không Đề Đọc và đề Nghe được GHÉP từ bài có sẵn: ba passage cùng cụm, bốn clip khác lĩnh vực. Chúng không cần chỗ chứa nào vì chúng không phải nội dung, chúng là một cách xếp nội dung (SRC-862). Đề Viết và Nói thì ngược lại: không có "bài" nào để ghép. Một đề Writing LÀ hai đề bài, một đề Speaking LÀ ba chùm câu hỏi. Nội dung ấy hiện nằm trong mã nguồn worker (PRODUCTION_LADDER), đúng chỗ mà SRC-875 đã chốt là không được để nội dung nữa: sửa một đề bài phải sửa code, deploy lại worker, và màn admin không với tới. ## Hai bảng, không phải một Một đề có nhiều phần, và phần mới là thứ learner mở ra làm. Nhét các phần vào một cột JSON của bảng đề thì mọi chỗ đọc một phần đều phải tải cả đề rồi tự tách - và màn admin sẽ phải ghi lại cả đề khi sửa một dòng, đúng kiểu ghi đè làm mất việc của người khác. ## body_md giữ bảng số liệu của Task 1 Task 1 Academic vốn mô tả BIỂU ĐỒ. Hệ chưa có chỗ chứa hình, nên đề ở đây dùng dạng TABLE - một dạng Task 1 có thật trong kỳ thi, không phải đồ thay thế tạm. Số liệu viết bằng chữ trong body_md, và learner mô tả đúng như mô tả một biểu đồ.
migration: 0257_ielts_exam_papers.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
skill | TEXT | NOT NULL CHECK (skill IN ('writing','speaking')) |
slug | TEXT | NOT NULL |
seq | INTEGER | NOT NULL |
name | TEXT | NOT NULL |
note | TEXT | NOT NULL DEFAULT '' |
minutes | INTEGER | NOT NULL |
status | TEXT | NOT NULL DEFAULT 'published' CHECK (status IN ('draft','published')) |
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')) |
Index: idx_exam_papers_slug(skill, slug) UNIQUE
ielts_exam_parts
migration: 0257_ielts_exam_papers.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
paper_id | TEXT | NOT NULL REFERENCES ielts_exam_papers(id) ON DELETE CASCADE |
seq | INTEGER | NOT NULL |
label | TEXT | NOT NULL |
title | TEXT | NOT NULL |
minutes | INTEGER | NOT NULL |
prompt_en | TEXT | NOT NULL |
body_md | TEXT | NOT NULL DEFAULT '' |
questions_json | TEXT | NOT NULL DEFAULT '[]' |
min_words | INTEGER | — |
min_seconds | INTEGER | — |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: paper_id → ielts_exam_papers
Index: idx_exam_parts_paper(paper_id, seq)
ielts_exam_quiz_attempts
Migration 0224 — Quiz "con hiểu gì về kỳ thi IELTS". SRC-738 (chỉ đạo chủ dự án 2026-09-15). ── Vì sao KHÔNG dùng lại assessment_sessions như bài đo bốn kỹ năng ────────────────────── Hai thứ trông giống nhau (đều là câu hỏi trắc nghiệm) nhưng là hai loại việc khác hẳn: · Bài đo bốn kỹ năng ĐO NĂNG LỰC. Câu nằm trong items, gắn skill_nodes, kết quả thành learner_evidence và đi vào Learner Model. Chấm sau khi nộp cả bài, không hiện đáp án giữa chừng, vì hiện đáp án làm hỏng chính phép đo. · Quiz này DẠY. Nó hỏi về LUẬT CHƠI của kỳ thi (mấy kỹ năng, band tính thế nào, Task 2 nặng hơn Task 1 bao nhiêu) — những thứ learner hiểu sai thì luyện sai suốt nhiều tháng. Nó chấm NGAY và hiện giải thích, vì mục đích là sửa chỗ hiểu sai chứ không phải cho điểm. Nhét quiz này vào assessment_sessions sẽ bơm bằng chứng "làm sai 3 câu" vào Learner Model về một thứ KHÔNG PHẢI năng lực tiếng Anh: hiểu nhầm cách tính band không làm Reading của một đứa trẻ yếu đi. Đó là lý do có bảng riêng, và là lý do bảng này không nối vào skill_nodes. Câu hỏi nằm trong MÃ NGUỒN (modules/ielts/examQuiz.ts), không nằm trong CSDL: chúng là kiến thức về một kỳ thi ổn định nhiều năm, sửa chúng là một commit có review, không phải một lượt INSERT. Bảng này chỉ lưu LƯỢT LÀM.
migration: 0224_ielts_exam_quiz.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
quiz_version | INTEGER | NOT NULL DEFAULT 1 |
answered | INTEGER | NOT NULL |
correct | INTEGER | NOT NULL |
answers_json | TEXT | NOT NULL DEFAULT '[]' |
misconceptions_json | TEXT | NOT NULL DEFAULT '[]' |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: learner_id → learners
Index: idx_ielts_quiz_learner(learner_id, created_at DESC)
ielts_full_test_attempts
Migration 0316 - ielts full test ladder. SRC-1146, SDD-038 §149. ## Vì sao migration này tồn tại Chủ dự án 30.09.2026: learn.nemo12.com/ielts có một khu làm ĐỀ ĐẦY ĐỦ (Listening, Reading, Writing, Speaking), 50 đề, mở khoá tuần tự. Đề k+1 chỉ mở khi đề k có một lượt làm XONG và NGHIÊM TÚC. "Nghiêm túc" phải do server quyết từ dữ liệu đã lưu, không phải do app tự khai, nên server cần giữ chính những thứ nó dùng để quyết: mốc giờ bắt đầu và nộp của từng phần (đóng dấu bằng đồng hồ SERVER), câu trả lời, số lần rời tab, và kết luận hợp lệ kèm lý do. Bốn bảng: · ielts_full_test_attempts - một lượt làm một đề. valid NULL khi chưa nộp hết. · ielts_full_test_sections - một phần của một lượt. PK (attempt_id, skill): nộp lại một phần là ghi đè đúng dòng ấy, không đẻ thêm dòng. · ielts_full_test_comments - nhận xét chung do AI viết, MỘT dòng mỗi lượt (PK attempt_id), sinh một lần rồi giữ nguyên. INSERT OR IGNORE trên PK là thứ bảo đảm hai request chạy chồng không sinh hai bản. · ielts_full_test_unlocks - admin mở tay một đề cho một learner. Dấu vết đầy đủ nằm ở audit_log; bảng này chỉ giữ trạng thái đang có hiệu lực. ## Lượt cũ Trước migration này KHÔNG có lượt làm đề đầy đủ nào được lưu: các đề từng kỹ năng (SRC-930) chỉ ghi điểm thô vào sổ công sức, không giữ mốc giờ hay câu trả lời. Không có dữ liệu để chứng minh một lượt cũ là nghiêm túc, nên không có gì để "cho qua" (grandfather); mọi learner bắt đầu từ đề 1. Xem SDD-038 §149. ## Chạy lại không đổi gì Toàn bộ là CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS.
migration: 0316_ielts_full_test_ladder.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
test_no | INTEGER | NOT NULL CHECK (test_no BETWEEN 1 AND 50) |
papers_json | TEXT | NOT NULL DEFAULT '{}' |
status | TEXT | NOT NULL DEFAULT 'in_progress' CHECK (status IN ('in_progress','finished')) |
started_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
finished_at | TEXT | — |
valid | INTEGER | — |
reasons_json | TEXT | NOT NULL DEFAULT '[]' |
warnings_json | TEXT | NOT NULL DEFAULT '[]' |
report_json | TEXT | — |
Khóa ngoại: learner_id → learners
Index: idx_full_test_attempts_learner(learner_id, test_no)
ielts_full_test_comments
migration: 0316_ielts_full_test_ladder.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
attempt_id | TEXT | PRIMARY KEY REFERENCES ielts_full_test_attempts(id) ON DELETE CASCADE |
text_vi | TEXT | NOT NULL |
source | TEXT | NOT NULL CHECK (source IN ('ai','template')) |
model | TEXT | — |
prompt_ref | TEXT | — |
usd | REAL | NOT NULL DEFAULT 0 |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: attempt_id → ielts_full_test_attempts
ielts_full_test_sections
migration: 0316_ielts_full_test_ladder.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
attempt_id | TEXT | NOT NULL REFERENCES ielts_full_test_attempts(id) ON DELETE CASCADE |
skill | TEXT | NOT NULL CHECK (skill IN ('listening','reading','writing','speaking')) |
started_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
submitted_at | TEXT | — |
answers_json | TEXT | NOT NULL DEFAULT '[]' |
result_json | TEXT | — |
blur_count | INTEGER | NOT NULL DEFAULT 0 |
| — | table constraint | PRIMARY KEY (attempt_id, skill) |
Khóa ngoại: attempt_id → ielts_full_test_attempts
ielts_full_test_unlocks
migration: 0316_ielts_full_test_ladder.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
test_no | INTEGER | NOT NULL CHECK (test_no BETWEEN 1 AND 50) |
unlocked_by | TEXT | NOT NULL |
reason | TEXT | NOT NULL DEFAULT '' |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
| — | table constraint | PRIMARY KEY (learner_id, test_no) |
Khóa ngoại: learner_id → learners
ielts_learner_models
Migration 0229 — Nơi IELTS Learner Model được LƯU, không chỉ được tính. SRC-792 (nhịp 3/3). ## Vì sao cần lưu, khi đã có hàm tính modules/ielts/layer.ts dựng lát cắt IELTS từ lời khai cộng bằng chứng, và dựng lại từ đầu mỗi lần có ai mở trang. Cách ấy đúng ở một điểm quan trọng và sai ở ba điểm khác: ĐÚNG: kết quả không bao giờ cũ hơn dữ liệu. Không có cache nào để lệch. SAI 1: không trả lời được "tháng trước con ở đâu". Mỗi lượt tính xong là vứt đi, nên hệ không bao giờ nói được "Reading của con đã đi từ 5.5 lên 6.0" — mà đó đúng là câu một đứa trẻ luyện sáu tháng cần nghe nhất. SAI 2: không ai kiểm được hệ đã từng nói gì. Mentor nhớ buổi trước hệ xếp Writing là chỗ yếu nhất, giờ mở ra thấy Speaking, và không có cách nào biết vì dữ liệu đổi hay vì thuật toán đổi. SAI 3: mọi màn đọc đều trả giá tính lại, kể cả khi chẳng có bằng chứng mới nào. Bảng này KHÔNG thay layer.ts. Nó lưu lại ẢNH CHỤP của từng lần tính, và engine_version nói ảnh ấy do bản thuật toán nào sinh ra. Đổi thuật toán là tăng số ấy, và những ảnh cũ vẫn đọc được đúng như lúc chúng ra đời. ## Vì sao một bảng RIÊNG, không dùng learner_skill_state learner_skill_state giữ mastery 0..1 cho mỗi node kỹ năng, chung cho mọi môn. IELTS cần một hình dạng khác hẳn: bốn kỹ năng cố định, thang band 0..9 nửa bậc, một mức tin cậy cho cả lát cắt, và quan trọng nhất là CHỖ LỆCH giữa điều learner tin về mình với điều bài làm nói ra. Không ô nào trong learner_skill_state chứa được chỗ lệch ấy, mà đó là thứ có giá trị nhất của lát cắt. ## Mỗi lần tính một dòng, không ghi đè INSERT mới chứ không UPDATE dòng cũ: lịch sử là lý do bảng này tồn tại. Dòng mới nhất của một learner là lát cắt đang dùng; các dòng trước là thứ trả lời được câu "đã đi được tới đâu". Số dòng không phình: engine chỉ ghi khi có bằng chứng mới hoặc lời khai mới (xem model.ts).
migration: 0229_ielts_learner_model.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
learner_id | TEXT | NOT NULL REFERENCES learners(id) ON DELETE CASCADE |
engine_version | INTEGER | NOT NULL |
evidence_count | INTEGER | NOT NULL DEFAULT 0 |
evidence_through | TEXT | — |
layer_json | TEXT | NOT NULL |
listening | REAL | , reading REAL, writing REAL, speaking REAL |
confidence | REAL | NOT NULL DEFAULT 0 |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Khóa ngoại: learner_id → learners
Index: idx_ielts_models_learner(learner_id, created_at DESC)
ielts_lexicon_domains
Một dòng dẫn cho kho từ của mỗi lĩnh vực.
migration: 0255_ielts_content.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
domain | TEXT | PRIMARY KEY |
note_vi | TEXT | NOT NULL DEFAULT '' |
ielts_lexicon_families
Họ từ của một lĩnh vực (SRC-784, nay rời khỏi bundle theo cùng chỉ đạo).
migration: 0255_ielts_content.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
domain | TEXT | NOT NULL |
head | TEXT | NOT NULL |
tier | INTEGER | NOT NULL CHECK (tier BETWEEN 1 AND 3) |
vi | TEXT | NOT NULL |
members_json | TEXT | NOT NULL DEFAULT '[]' |
collocations_json | TEXT | NOT NULL DEFAULT '[]' |
example | TEXT | NOT NULL DEFAULT '' |
created_at | TEXT | NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) |
Index: idx_lexicon_families_domain(domain, tier)
ielts_micro_exercises
Migration 0250 — Tầng BÀI TẬP giữa năng lực nhỏ và câu hỏi. SRC-850. Chỉ đạo chủ dự án 2026-09-19: "Click vào một trong các card (ví dụ như Main Idea hay Detail) thì cần vào một trang mới. Trong đó có nhiều bài tập, và chia theo 5 levels. Mỗi levels có khá nhiều bài tập. Learners có thể filter để chọn ra bài tập để làm bài. Mỗi bài tập đều có URL riêng biệt." ## Vì sao cần một tầng nữa, chứ không chỉ thêm cột level vào câu hỏi Trước bản này, một năng lực là MỘT thang dài: mở /ielts/skills/reading/main-idea ra là đi từ câu đầu tới câu cuối, không có chỗ dừng và không có gì để gửi cho nhau. Đặt level lên từng câu thì vẫn không giải được hai việc chủ dự án vừa nêu: · "Mỗi bài tập đều có URL riêng" — URL phải trỏ vào một THỨ CÓ RANH GIỚI. Một câu hỏi thì quá nhỏ (URL cho từng câu là 3.500 URL không ai gửi cho ai), còn cả một level thì quá to. · "Learners có thể filter để chọn ra bài tập" — lọc chỉ có nghĩa khi có nhiều VẬT THỂ để lọc. Lọc ra 350 câu rời thì learner vẫn đứng trước cùng một danh sách không biết bắt đầu từ đâu. Một bài tập ở đây là một buổi làm việc trọn vẹn: 5–10 đoạn, làm hết trong chừng mười phút, có điểm kết và có câu nói con vừa làm được gì. ## Năm bậc, và composition của từng bậc nằm trong dữ liệu chứ không trong đầu người soạn Chỉ đạo, nguyên văn rút gọn: 1 — 5 đoạn, đều tiếng Việt. 2 — 5 đoạn tiếng Việt + 5 đoạn tiếng Anh, trong đó bản Việt DỊCH TỪ bản Anh. 3 — 10 đoạn ngắn, đều tiếng Anh. 4 — 5 Việt + 5 Anh, nhưng dài hơn bậc 2. 5 — gần như đề thi thật. Bậc 2 và 4 là chỗ duy nhất trong cả hệ có QUAN HỆ giữa hai câu hỏi: đoạn Việt số 1 và đoạn Anh số 1 là cùng một đoạn văn qua hai thứ tiếng. Quan hệ ấy phải nằm trong cột (pair), không nằm trong quy ước "cứ seq n và seq n+5 thì là một cặp": quy ước ấy đúng cho tới bài đầu tiên có 6 đoạn Việt, và lúc sai thì nó sai im lặng — trang ghép nhầm hai đoạn không liên quan rồi nói với learner rằng đây là bản dịch của nhau. Vì sao cặp ấy đáng có: learner làm đoạn tiếng Việt trước, hiểu thao tác, rồi gặp LẠI ĐÚNG đoạn ấy bằng tiếng Anh. Phần khó duy nhất còn lại là chữ — đúng tinh thần thang bốn chặng ở 0238, nay làm chặt hơn một bậc.
migration: 0250_micro_skill_exercises.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
id | TEXT | PRIMARY KEY |
micro_skill_id | TEXT | NOT NULL REFERENCES ielts_micro_skills(id) ON DELETE CASCADE |
level | INTEGER | NOT NULL CHECK (level BETWEEN 1 AND 5) |
seq | INTEGER | NOT NULL |
title_en | TEXT | NOT NULL |
title_vi | TEXT | NOT NULL |
blurb_vi | TEXT | NOT NULL |
status | TEXT | NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','published')) |
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')) |
task_type_id | TEXT | REFERENCES ielts_task_types(id) — thêm ở 0264_reading_task_types.sql |
show_count | INTEGER | — thêm ở 0290_micro_exercise_show_count.sql |
minutes | INTEGER | — thêm ở 0292_micro_exercise_minutes.sql |
Khóa ngoại: micro_skill_id → ielts_micro_skills
Index: idx_micro_exercises_addr(micro_skill_id, level, seq) UNIQUE · idx_micro_exercises_list(micro_skill_id, status, level, seq) · idx_micro_exercises_task_type(task_type_id, status, level, seq)
ielts_micro_progress
Bài đang làm dở: giữ chỗ learner dừng lại — SRC-919. Chỉ đạo chủ dự án 2026-09-20: "Hệ thống cần đánh dấu 'bài nào đang làm dở', 'bài nào đã làm xong rồi'." "Đã xong" đã đọc được từ learning_events (SRC-917): mỗi bài làm hết ghi một dòng action='practice'. "Đang làm dở" thì KHÔNG đọc được từ đó, vì theo đúng thiết kế ấy một bài bỏ giữa chừng không để lại dòng nào — và đó là chủ ý, không phải thiếu sót: ghi lúc mở trang thì mở ra rồi đóng lại cũng thành "đã học" và chuỗi ngày (SRC-801) mất nghĩa. Nên trạng thái dở nằm ở bảng riêng này, và nó giữ CẢ các lựa chọn đã chấm chứ không chỉ một con số. Lý do: một thẻ báo "đang dở 3/5" mà bấm vào lại phải làm lại từ câu 1 thì cái nhãn ấy chỉ tố cáo learner chứ không giúp gì. Có picks thì bấm vào là về đúng chỗ đang đứng. Một dòng cho mỗi (learner, bài): đây là VỊ TRÍ HIỆN TẠI, không phải lịch sử — lịch sử là việc của learning_events. Làm xong bài thì dòng này bị xoá, nên bảng luôn nhỏ bằng số bài đang dở.
migration: 0267_ielts_micro_progress.sql
| Cột | Kiểu | Ràng buộc / ghi chú |
|---|---|---|
learner_id | TEXT | NOT NULL |
ref | TEXT | NOT NULL, -- 'exercise:<skill>.<code>.l<level>.<seq>', cùng quy ước learning_events |
answered | INTEGER | NOT NULL DEFAULT 0 |
total | INTEGER | NOT NULL DEFAULT 0 |
picks | TEXT | NOT NULL DEFAULT '{}', -- JSON |
updated_at | TEXT | NOT NULL DEFAULT (datetime('now')) |
| — | table constraint | PRIMARY KEY (learner_id, ref) |
Index: idx_micro_progress_learner_ref(learner_id, ref)