---
url: https://docs.nemo12.com/architecture/sdd-013-coral-content-plane.md
description: >-
  Coral (coral.nemo12.com): mặt phẳng nội dung của Learning Knowledge Factory,
  catalog artifact, blueprint registry và các lượt sinh nội dung.
---

# SDD-013 — Coral: Content Plane (coral.nemo12.com)

**Coral** là giao diện vận hành của *Learning Knowledge Factory*: nơi staff quản lý toàn bộ học liệu — Knowledge Package, Learning Experience (LX), Assessment Experience (AX), **LX Blueprint**, **Assessment Blueprint**, các **ngân hàng đề thi** (generated + đề thật), Item Bank, cùng **Rubrics/Quality** để đánh giá hầu hết artifact. Coral **thay thế data.nemo12.com** (Q-091 ✅ 2026-08-14 — ngừng sử dụng data.nemo12.com).

Tham khảo mori.chuyenchon.com (CC-MORI-01..05, FEAT-039..043) về *phạm vi* — nhưng không bắt chước cách làm: Coral sửa tận gốc các lỗi legacy (content-as-migrations RISK-014, cron-poll thay Queues RISK-018, 4 hệ rubric song song RISK-005).

## 1. Vị trí & nguyên tắc

1. **Factory, không phải CMS** (SDD-003): vòng lặp `Model → Build → Review → Score → Detect Problems → Improve → Publish → Observe Learners ↺`. Coral là bề mặt điều khiển + quan sát của vòng lặp đó.
2. **Registry là nguồn sự thật** (SDD-004 §3): Coral thao tác trên các registry D1 + content R2 hiện có — không tạo kho dữ liệu riêng, không copy nội dung vào app.
3. **Content là data qua API/pipeline** — cấm ship content bằng migration SQL (RISK-014); cấm sửa production trực tiếp — mọi thay đổi qua candidate version + review (SDD-003 §10).
4. **Mọi pipeline dài chạy bằng Cloudflare Workflows + Queues** (§8) — cấm cron-poll bảng D1 (RISK-018).
5. **Hai nghĩa "blueprint"**: registry §3 quản lý *content blueprint* (LX Blueprint, Assessment Blueprint — nghĩa SDD-004 §5–8). *Goal blueprint* (SDD-002 §12, `specialized-chuyen`) thuộc Learner Intelligence, **không** nằm trong Coral.
6. **Truy cập**: Cloudflare Access chỉ cho `dac2205@gmail.com` (Q-092 ✅ 2026-08-14) + API vẫn kiểm tra session role `staff`/`admin` (REQ-ACC-05) — defense-in-depth; email nằm trong Access policy (config hạ tầng), **không hardcode trong code** (RISK-013).

## 2. Catalog artifact Coral quản lý (REQ-CNT-01..03, 05)

| Artifact | Registry | Thiết kế gốc | Thao tác trong Coral |
| --- | --- | --- | --- |
| Knowledge Node / Skill | `knowledge_nodes`, `skills` | SDD-003 §2-3 | browse graph, sửa metadata, gaps |
| Knowledge Package | `learning_packages` | SDD-004 §3 | CRUD, gắn node/LX, manifest |
| Learning Experience | `learning_experiences` + R2 | SDD-004 §5-8 | CRUD, version, preview, evidence_contract |
| Assessment Experience | `learning_experiences` (subtype) | SDD-004 §5-8 | CRUD, gắn blueprint, xem instance stats |
| LX Blueprint / Assessment Blueprint | `content_blueprints` (§3, migration 0016) | SDD-013 | CRUD, version, chạy generation run |
| Item Bank | `items` | SDD-004 §9 | browse, sửa, misconception (REQ-CNT-03) |
| Exams (generated + authentic) | `exams`, `exam_questions`, `exam_problems`, `problem_types` | SDD-011 §3, SDD-012 | §5 — ingestion, tách bài, lời giải, publish |
| Labs | `labs` registry | SDD-010 §3 | browse, config version, quality |
| Rubrics / Quality | `rubrics`, `quality_evaluations` | SDD-003 §6-9 | §6 — registry, review queue |

Mọi artifact có: lifecycle `Draft → Review → Published → Deprecated` (SDD-003 §10), version + provenance (SDD-003 §13), quality profile (SDD-003 §6).

## 3. Blueprint registry & generation runs (REQ-CNT-05, 09, 11)

✍️ 2026-08-14: tên bảng chốt là **`content_blueprints`** (bản đầu SDD này viết `blueprints`, nhưng tên đó đã thuộc goal blueprint của SDD-002 §12 từ migration 0002 — đúng cảnh báo "hai nghĩa blueprint" ở §1). Registry mở ở migration `0016_content_blueprints.sql`; seed 14 blueprint đầu qua `scripts/seed-blueprints.mjs` + data `workers/api/src/modules/coral/blueprints-seed.json`: 3 AX nền (exam-problem-v1, diagnostic-mcq-v1, writing-rubric-band-v1), 8 exam blueprint Toán giữa/cuối HK1 lớp 6-9 (từ nghiên cứu đề thật 2023-2025, khung TT22: TN 12 câu 3đ + TL 7đ, 90 phút; spec có topics + thang điểm từng bài + nguồn), 3 LX (micro-v1, practice-set-v1, lab-config-v1). API `GET/PATCH /v1/coral/blueprints*` (staff/admin, sửa tay từng phần trong tab Blueprint của Coral; sửa spec/target thì version +1).

```text
content_blueprints (
  id, kind[lx|assessment],
  name_vi, description,
  target_selector_json,   -- phạm vi áp dụng: subject/area/node list/dạng bài
  spec_json,              -- mẫu: pedagogy, cấu trúc, item mix, measurement layers
                          --   (vd assessment: answer+confidence+justification — SDD-012 §4)
  version, status[draft|active|deprecated],
  created_by, created_at
)
generation_runs (
  id, blueprint_id → blueprints, blueprint_version,
  input_json,             -- target set đã resolve
  status[queued|running|review|done|failed],
  progress_done, progress_total,
  stats_json,             -- sinh bao nhiêu, pass gate bao nhiêu, cost
  started_at, finished_at
)
```

* **LX Blueprint** sinh Learning Experience (micro/practice/lab-config…); **Assessment Blueprint** định nghĩa cách đo của AX (`exam-problem-v1` của SDD-012 §4 là một bản ghi trong registry này).
* Flow: `blueprint + targets → generation run (WF-14, §8) → candidates (Draft) → Quality Engine (SDD-003 §7-8) → Review queue (§6) → Published`.
* Mọi artifact sinh ra lưu provenance `{blueprint_id, blueprint_version, model, prompt_version, input_ref}` (QG-010, SDD-003 §13) — kế thừa ý tưởng mori source-provenance (FEAT-043), làm trong registry thống nhất.

## 4. Dashboard & tổng quan (REQ-CNT-02, 07)

Ba màn, trả lời đúng câu "đang có bao nhiêu, tốt đến đâu, đang tiến bộ thế nào":

1. **Tổng quan**: đếm artifact theo loại × lifecycle status; quality score trung bình theo môn/area; **coverage gaps** (node thiếu item/LX/AX/lab — REQ-CNT-02); exam bank: số đề/bài theo hệ thi × năm × vòng, % bài đã có lời giải reviewed.
2. **Tiến độ** (*status việc tạo và nâng cấp dần về số lượng và chất lượng*): time-series theo tuần — artifact tạo mới / published / nâng version; số generation run + % pass gate; điểm quality trung bình theo thời gian (kỳ vọng đi lên).
3. **Hoạt động**: runs đang chạy (WF-07/13/14) + progress; queue depth + DLQ (QG-009); review backlog (số candidate chờ human review, tuổi già nhất).

## 5. Quản lý ngân hàng đề thi (REQ-CNT-06)

* Danh sách đề theo `exam_system` × năm × vòng × môn (generated lẫn authentic); trạng thái từng đề (draft/review/published, % bài có lời giải).
* **Ingestion đề thật** = UI của WF-13 (SDD-012 §7): upload/paste + nguồn → tách bài → biên tập statement (markdown + KaTeX + hình R2) → gắn `problem_types` + node links → soạn lời giải 3 tầng (AI draft → AI multi-evaluator — AI-first SRC-035) → deterministic checks QG-011 → publish.
* Checklist publish QG-011 hiển thị ngay trong editor (provenance đủ, ≥1 node link, ≥1 dạng bài, lời giải reviewed) — không đạt thì nút publish disabled kèm lý do.

## 6. Quản lý Quality Gates & Rubrics (REQ-CNT-08)

* **Rubric registry** — một nguồn duy nhất (RISK-005), gốc CC-QAF-1.0 (SDD-003 §7); CRUD rubric + version; mapping `rubric ↔ artifact type` (đánh giá "hầu hết artifact": node, package, LX, AX, item, exam problem, lab, solution).
* **Quality profile** per artifact: 8 chiều (SDD-003 §6) + findings của Multi-Evaluator (SDD-003 §8); variance cao → cờ Review.
* **Review queue** (AI-first — SRC-035): AI multi-evaluator là gate mặc định; hàng đợi chỉ chứa item variance cao/bị learner report — owner spot-check approve/needs-work/reject, có audit. Human bắt buộc duy nhất: moderation an toàn trẻ em (QG-008).
* **QG catalog view**: đọc QG-001..011 từ docs làm reference; các gate nội dung (QG-006, QG-011) hiển thị trạng thái checks chạy trong plane.

## 7. App, worker & auth (REQ-CNT-04)

* **App**: `apps/coral` — Vite + React SPA, design system chung (DS-001). **Worker**: `nemo12-coral`, route `coral.nemo12.com` (zone nemo12.com).
* **Access**: Cloudflare Access app "Nemo12 Coral" — policy allow duy nhất email `dac2205@gmail.com` (Q-092 ✅). API routes `/v1/content/*`, `/v1/coral/*` vẫn gate session `staff`/`admin` (pattern admin.nemo12.com).
* **data.nemo12.com ngừng sử dụng** (Q-091 ✅): mã nguồn `apps/data` **đã gỡ** khỏi repo sau khi làm seed cho `apps/coral`; worker `nemo12-data` không phát triển tiếp; gỡ route `data.nemo12.com` khi Coral đạt parity browse (Q-093 ✍️).
* **API**: workers/api — giữ module `content` (browse/coverage hiện có) + module mới `coral`: blueprints CRUD, generation runs, dashboard aggregates, review queue, rubric registry. OpenAPI từ router (QG-003).

## 8. Cloudflare Workflows & Queues (REQ-CNT-10)

| Pipeline | Loại | Mô tả |
| --- | --- | --- |
| WF-07 Content Quality loop | Workflow `content-quality` | evaluate → findings → priority → improvement candidate → re-evaluate (SDD-003 §7-11) |
| WF-13 Real exam ingestion | Workflow `exam-ingest` | các bước SDD-012 §7, có human-review step (chờ approve trong Coral) |
| WF-14 Generation run | Workflow `content-generation` | resolve targets → fan-out generate qua Queue → collect → evaluate → review queue |

* **Queues**: `nemo12-events` (hiện có) cho domain events; queue mới `nemo12-content` (+ DLQ) cho task generation/evaluation fan-out. Consumer idempotent, DLQ bắt buộc (QG-009, SDD-006 §5).
* Trạng thái mọi run ghi ở `generation_runs`/bảng run tương ứng — Coral đọc để hiển thị (§4.3); **không cron-poll** (RISK-018); Workflow tự cập nhật status qua step.
* AI calls trong generation/evaluation đi qua AI Gateway `nemo12` (QG-010, RISK-022).

## 9. Data Quality — rà soát unit/lesson (REQ-CNT-16, SRC-446)

Người phụ trách data vào Coral → chọn môn → tab **Data Quality**. Tổng quan là hub chỉ việc (SRC-447): card "Việc đang chờ" tính từ dữ liệu thật, bấm là nhảy thẳng vào việc — người mới không phải lò mò các tab. Ba mảnh:

1. **Hàng đợi có lý do** (`GET /v1/coral/dq/queue`): mỗi lesson một điểm ưu tiên deterministic, mỗi cộng điểm kèm một câu lý do — đang ẩn +50 · chưa rà +40 · lần trước major +25 · có câu nguồn llama +20 · học sinh đúng <40% (≥5 lượt) +20 · <10 câu +15 · lần rà cuối >60 ngày +10. Ưu tiên phải giải thích được thì người rà mới tin hàng đợi thay vì tự dò 300 dòng.
2. **Học thử không để vết** (`GET /v1/coral/dq/lesson`): endpoint trả toàn bộ câu KÈM đáp án + misconception; người rà bấm chọn ngay trong Coral, đúng/sai hiện client-side. Không có POST evidence nào — không chạm Learner Model của ai *by construction*, không cần cờ "shadow mode" dễ quên.
3. **Đánh dấu 1-2 cú bấm** (`POST /v1/coral/dq/mark`): verdict `ok|minor|major|blocker` ngay trên dòng (unit, lesson, hoặc từng câu); verdict xấu mở thêm chip tag (các lỗi đúc từ đợt review 1.317 câu: sai đáp án, ngoài chương trình, khó hơn lớp…) + note tuỳ chọn. Mỗi confirm là **một dòng `experience_reviews` append-only** (migration 0049, dùng CHUNG với khu rà của mentor trong Dolphin — SRC-206): một người rà nhiều lần, nhiều người rà một bài, lịch sử giữ đủ.

**Ẩn khi nghiêm trọng:** lesson `blocker` → `skill_nodes.hidden_at` + lý do; practice-start của learn chặn với thông điệp "đang bảo trì nội dung"; rà lại `ok` thì mở. Câu `blocker` → `items.status='candidate'` — rút khỏi lưu thông bằng chính cơ chế status sẵn có (AS-10.4.1), không thêm cờ mới.

**Nâng cấp tiếp (vòng khép kín):** verdict ≠ ok + tags + note = **fixlist máy đọc được**. Vòng sửa: fixlist → sinh lại bằng pipeline candidate (§3, kèm Claude review bắt buộc REQ-KNW-17) với tags làm chỉ dẫn ("sai đáp án" → giải lại từ đầu; "khó hơn lớp" → hạ cấp độ) → publish → `review_status` quay về `unreviewed` để được rà lại — người rà chỉ xác nhận, không phải tự sửa từng câu.

## Trace

| REQ | Section |
| --- | --- |
| REQ-CNT-01 (browse registry + Item Bank + Exams) | §2, §5 |
| REQ-CNT-02 (coverage/quality dashboard) | §4 |
| REQ-CNT-03 (chi tiết item + LX gắn node) | §2 |
| REQ-CNT-04 (Coral portal + Access, thay data.nemo12.com) | §1, §7 |
| REQ-CNT-05 (quản lý Package/LX/AX/Blueprints) | §2, §3 |
| REQ-CNT-06 (quản lý ngân hàng đề) | §5 |
| REQ-CNT-07 (dashboard tổng quan + tiến độ) | §4 |
| REQ-CNT-08 (Quality Gates & Rubrics) | §6 |
| REQ-CNT-09 (blueprint → generation runs) | §3 |
| REQ-CNT-10 (Workflows + Queues) | §8 |
| REQ-CNT-11 (provenance + content intelligence) | §3 |

Liên quan: SDD-003 (factory, quality, lifecycle, provenance) · SDD-004 (registry, LX/AX, Item Bank) · SDD-010 (labs) · SDD-011 §3 (exams generated) · SDD-012 (đề thật, WF-13, QG-011) · Q-091..Q-094 · CC-MORI-01..05.

## Tab Prerequisite graph (SRC-396)

Coral quản dữ liệu chương trình, nên `unit_prereqs` thuộc về đây chứ không phải Admin (Admin quản
người: role, giao mentor, thống kê tài khoản).

Tab bày từng Unit và 1-2 Unit đứng trên nó kèm câu lý do, đánh dấu riêng cạnh xuyên Package. Ba hành
động: **Giữ** (`reviewed`), **Sửa lý do** (`edited`), **Bỏ cạnh này** (`rejected`, bỏ được thì dùng
lại được). Không có nút "Duyệt" — cạnh đã chạy từ lúc sinh ra; đặt một nút duyệt ở đây sẽ nói dối về
việc hệ đang chờ ai đó.

API: `GET /v1/coral/unit-prereqs`, `PATCH /v1/coral/unit-prereqs` (staff/admin).

## Hai tab, hai câu hỏi khác nhau — "Độ phủ" và "Khoá học" (SRC-693)

Chúng trùng nhau đúng một chữ "Unit", còn lại là hai cây khác gốc. Ghi ra đây vì đã có một lần đề
xuất gộp chúng chỉ vì cái tên:

| | Tab **Độ phủ** | Tab **Khoá học** |
| --- | --- | --- |
| Bảng | `skill_nodes` · `labs` · `items` | `curriculum_courses/units/lessons` |
| Cây gì | bản đồ tri thức (strand → module → unit) | chương trình học (course → unit → lesson) |
| Trả lời | "unit nào chưa có lab, chỗ nào còn mỏng" | "Big Idea của unit này viết đã đúng chưa" |
| Việc | soát **độ phủ** | soát và sửa **chữ nghĩa** |

Tab Độ phủ trước 2026-09-08 mang nhãn "Unit" — chính cái nhãn ấy sinh ra đề xuất gộp. Nhãn đổi,
**slug URL vẫn là `unit`**: link đã gửi đi thì không thu hồi được.

## Tab Khoá học — một khu vực cho cả Nemo lẫn Marlins (SRC-680)

Cấu trúc khoá của học sinh và khoá của bố mẹ **y hệt nhau**: Course → Unit (Big Idea + Essential
Question + key concept) → Lesson (Guiding Question). Nên trong Coral chúng dùng **một tab**, và
`audience` (`nemo` | `marlin`) chỉ là một công tắc ở đầu trang, không phải một nhánh code. Hai màn
hình cho một cấu trúc nghĩa là mọi luật kiểm ("unit nào chưa có Big Idea") phải viết hai lần, và
lần thứ hai sẽ lệch.

**Trước SRC-680** khoá của bố mẹ nằm cứng trong `apps/marlins/src/parentCourses.ts`: sửa một dấu
phẩy phải build lại và deploy, và Coral không có đường nào chạm tới. Rà soát nội dung khoá học vì
thế chỉ làm được cho một nửa số khoá.

### Lưu ở đâu

Một cây bảng, một cột phân loại — không phải hai bộ bảng song song (migration 0203):

| | Cột / bảng |
| --- | --- |
| Phân loại | `curriculum_courses.audience`, `course_group` (5 chặng hành trình của bố mẹ), `display_seq` |
| Nỗi đau bố mẹ | `curriculum_course_pains` |
| Việc cần làm (JTBD) | `curriculum_course_specs.purpose_vi/_en` + `jtbd_json` |
| Key concept có ví dụ | `curriculum_unit_concepts.example_vi/_en` |

`(level, seq)` với `audience='marlin'` chỉ còn là **ô lưu** cho chỉ mục UNIQUE cũ; thứ tự đọc thật
nằm ở `display_seq`. Không nới CHECK vì SQLite phải dựng lại cả bảng cha, và D1 chặn — đã thử ở
SRC-642/643.

### API (staff/admin)

| Route | Việc |
| --- | --- |
| `GET /v1/coral/courses?audience=` | danh sách khoá kèm `gaps` — số ô người soạn còn để trống |
| `GET /v1/coral/courses/{id}` | cả cây Course → Unit → Lesson |
| `PATCH /v1/coral/course-units/{unitId}` | tên Unit, Big Idea, Essential Question (song ngữ) |
| `PATCH /v1/coral/course-lessons/{lessonId}` | tên Lesson, Guiding Question (song ngữ) |
| `PATCH /v1/coral/unit-concepts` | key concept theo `(unit_id, seq)` |
| `PATCH /v1/coral/courses/{id}` | tên khoá, câu "việc cần làm" (song ngữ) |
| `PUT /v1/coral/courses/{id}/pains` · `/outcomes` | thay TRỌN danh sách |

Nỗi đau và outcome gửi lên trọn danh sách chứ không sửa từng dòng: hai bảng khoá theo
`(course_id, seq)`, mà việc thật của người soạn là "bỏ câu thứ hai đi" — tức đánh số lại cả danh
sách. Sửa từng dòng thì client phải tự dựng lại chuỗi `seq`, và đó đúng chỗ để lệch.

Bộ lọc môn khớp cả **tiền tố** (`subject_id = 'ielts'` hoặc `LIKE 'ielts\_%' ESCAPE`): Coral chọn
môn theo school ("ielts") còn tầng Course tách IELTS thành năm `subject_id`. Khớp đúng chuỗi thì
chọn Squid → IELTS ra danh sách rỗng trong khi D1 có 24 khoá.

Đây là lần đầu Coral **ghi** vào nội dung khoá học — trước đó Coral chỉ soi chất lượng. Nên mọi lần
sửa đều ghi `audit_log`, và ô để trống ghi thành `NULL` để câu đếm chỗ hổng không lệch.

### Thân bài lesson (đợt 3)

Bốn phần của một bài: **câu chuyện mở bài (SCQA)** · **ví dụ** · **chỗ dễ hiểu lầm** · **20 câu
trắc nghiệm**. Ba phần đầu dùng lại bảng có sẵn của tầng Course; phần thứ tư cần bảng mới
(migration 0204):

| Phần | Bảng |
| --- | --- |
| SCQA | `curriculum_lesson_stories` (nhịp thứ tư `answer` → `resolution`) |
| Ví dụ | `curriculum_lesson_examples` kind `example` (title → text, body → why) |
| Chỗ dễ hiểu lầm | `curriculum_lesson_examples` kind `near_miss` (looksLike → text, whyWrong → why) |
| Trắc nghiệm | `curriculum_lesson_quiz` + `curriculum_lesson_quiz_choices` |
| Performance Task | `curriculum_unit_tasks` + `curriculum_task_criteria` |

Trắc nghiệm không nhét vào `items`: `items` là ngân hàng cho học sinh, gắn với knowledge node, do
máy sinh và người duyệt. Câu ở đây do chủ dự án viết, gắn với lesson, đọc cùng bài.

`GET /v1/coral/lesson-body/{lessonId}` tải **riêng**, không đi kèm cây khoá: 162 lesson × 20 câu ×
4 phương án × 2 thứ tiếng là vài megabyte cho một màn hình chỉ để liệt kê. Ba route sửa:
`.../story`, `.../examples/{kind}/{seq}`, `.../quiz/{seq}`.

Đề, đáp án và phương án của một câu đi **cùng một lượt**: `answer` là chỉ số vào danh sách phương
án, tách hai lượt thì có khoảnh khắc chỉ số trỏ vào phương án cũ — và nếu lượt thứ hai không tới
thì khoảnh khắc ấy thành vĩnh viễn, im lặng.

**Không ô nào được chứa dấu `|`.** Thân bài kéo ngược ra `parentLessons/*.ts` dưới dạng chuỗi
`"tiếng Việt | English"`, nên một dấu `|` làm `bi()` ném lỗi lúc nạp module: trang khoá học của bố
mẹ trắng màn hình. Chặn hai lớp — zod ở API, và `pull-parent-bodies.mjs` dừng trước khi ghi file.

### Vòng đời một sửa đổi

D1 là nguồn thật; file trong `apps/marlins` là bản sinh ra:

1. Sửa trong Coral → ghi thẳng D1.
2. `node scripts/curriculum/pull-parent-courses.mjs` → chép ngược ra `parentCoursesData.ts`.
3. `node scripts/curriculum/gen-parent-courses.mjs > scripts/seed-parent-courses.sql` → commit.

Thân bài đi đường song song: `pull-parent-bodies.mjs` rồi `gen-parent-bodies.mjs` (ghi thẳng ra
bốn lô `seed-parent-bodies-*.sql`).

Vì sao Marlins không gọi API lúc chạy: trang khoá học của bố mẹ hiện ra tức thì, và
`parentCompetency.ts` cùng `parentLessons/` đang khớp với khung này theo **chỉ số** unit/lesson.
Đánh đổi đã chọn (2026-09-05): một sửa đổi phải chờ một vòng deploy. Cổng
`scripts/check-parent-courses.mjs` (trong `npm run check:code`) chắn việc hai file lệch nhau.
