Skip to content

CoursePack: soạn tay trọn một khoá, biên dịch ra migration ​

Từ 03.09.2026 nội dung khoá học ai viết cũng được. CoursePack là định dạng để viết tay một khoá trọn vẹn, đủ mô hình Trải nghiệm học, rồi biên dịch thành migration bằng máy, thay vì gõ tay INSERT INTO.

Một pack là JSON trong git, và đó là điểm cốt lõi: nội dung viết tay không tái tạo được bằng cách chạy lại workflow, nên file trong repo là nguồn sự thật duy nhất của nó.

Hai công cụ, đừng lẫn ​

course_std_to_sql.pycourse_pack_to_sql.py
Dùng khiKhoá đã có trong D1, muốn bồi thêm chuẩn SRC-609Tạo khoá mới, hoặc soạn trọn một khoá
Lấy idTra trong D1 (wrangler d1 execute --remote)Sinh bằng công thức từ course_id
Chạy offlineKhôngCó, nên chạy được trong CI
Phủ Trải nghiệm họcKhông (có trước mô hình đó)Có: SCQA, ví dụ, quiz 20 câu

Cấu trúc thư mục ​

api.conan.school/content/courses/
  catalog/industry.json      # đặc tả khung: 8 cộng đồng × 10 khoá
  catalog/occupation.json    # đặc tả khung: 10 cộng đồng × 10 khoá
  packs/<slug>.json          # pack: phần cấp khoá + units_from
  packs/<slug>.u1.json       # một unit một file

units_from cho phép mỗi unit nằm một file riêng. Một khoá đầy đủ là hàng chục nghìn chữ; nhét cả vào một JSON thì không ai diff nổi một thay đổi nhỏ, và hai người không sửa hai unit khác nhau cùng lúc được.

Đường đi ​

sửa JSON  →  balance_quiz_answers.py  →  course_pack_to_sql.py > migrations/<ngày><chữ>_*.sql
          →  push  →  CI áp migration TRƯỚC deploy  →  POST /ops/audit/<slug>
bash
python3 scripts/course_pack_to_sql.py --check content/courses/packs/<slug>.json
python3 scripts/course_pack_to_sql.py content/courses/packs/<slug>.json > migrations/20260904a_content_sales.sql

Migration sinh ra dùng INSERT ... ON CONFLICT DO UPDATE trên đúng những cột pack sở hữu, không dùng INSERT OR IGNORE: nội dung phải sửa được bằng cách sửa JSON rồi sinh lại. OR IGNORE thì lần áp thứ hai im lặng không đổi gì, và người sửa tưởng đã sửa xong.

--check bắt được gì, và không bắt được gì ​

Nội dung viết tay đi vòng qua bốn lớp lọc câu hỏi, lượt kiểm chéo đáp án bằng model khác, và các assert văn phong của engine. --check không thay được lớp nào trong số đó. Nó chỉ bắt loại lỗi đếm được:

  • thiếu câu quiz (phải đúng 20), thiếu ví dụ (3 mỗi nhóm), thiếu bài viết (3 góc đúng thứ tự);
  • correct_index trỏ ra ngoài mảng, phương án trùng nhau, câu hỏi trùng nhau;
  • key concept không song ngữ, thiếu SCQA, thiếu Performance Task;
  • course_seed dưới 80 ký tự, community_id bỏ trống;
  • đáp án đúng dồn vào ít vị trí, xem mục dưới.

Đó đúng là loại lỗi người soát tay bỏ sót nhiều nhất, vì nội dung vẫn trông đầy đủ. Kiểm nội dung có đúng hay không thì vẫn phải POST /ops/audit/:slug sau khi CI áp xong, đó là lớp kiểm duy nhất còn với tới được nội dung viết tay.

--check chạy trong ci-checks mỗi lượt PR, ở job api.conan.school.

Bẫy: vị trí đáp án đúng ​

Người viết nội dung, kể cả model, có thiên hướng đặt đáp án đúng vào vị trí thứ hai, nó nghe tự nhiên nhất khi đọc thành câu. Đo thật trên khoá đầu tiên soạn bằng định dạng này: một lesson có 16/20 câu đáp án ở vị trí 1, một lesson khác 15/20.

Hậu quả là người học tinh ý đếm ra quy luật trước khi đọc hết đề, và bài quiz thôi không còn dò được lỗ hổng nào nữa, trong khi mọi màn hình vẫn hiện đủ 20 câu và chấm điểm bình thường.

scripts/balance_quiz_answers.py xoay vòng vị trí theo chỉ số câu hỏi (nên kết quả cố định, chạy lại ra đúng cùng thứ tự). Câu nào mà vị trí mang nghĩa thì đánh dấu "fixed_order": true.

bash
python3 scripts/balance_quiz_answers.py content/courses/packs/<slug>.u*.json

Ba việc phải tự nhớ ​

  1. Chạy POST /ops/audit/:slug sau khi CI áp. Không chạy thì không ai từng đọc nội dung đó ngoài người viết.
  2. course_content_runs sẽ có lỗ. Bảng đó là cơ sở so chất lượng giữa các model; artefact viết tay không có dòng nào ở đó. Đừng đọc biểu đồ so model như thể nó đầy đủ.
  3. Repo trước, D1 sau. Không wrangler d1 execute chèn nội dung từ máy. Xem deploy.

Danh mục 180 khoá ​

content/courses/catalog/ giữ đặc tả khung: 18 cộng đồng × 10 khoá, mỗi khoá có slug, tên song ngữ và một câu premise nói rõ dạy ai và làm được việc gì. Đây là thiết kế chương trình, không phải nội dung giảng dạy và không phải chất liệu nền.

premise là thứ giữ cho mười khoá của một cộng đồng không trùng nhau, và là chỗ người soạn nhìn vào để viết course_seed.

Tình trạng tính tới 05.09.2026: 80/180 khoá đã soạn đầy đủ, tổng 525 lesson và 10.500 câu quiz. Hai hình dạng cùng tồn tại: 65 khoá đầu theo khuôn 3 unit / 6 lesson / 120 câu, và 15 khoá từ drink-peak-throughput trở đi theo khuôn 3 unit / 9 lesson / 180 câu, khuôn sau là khuôn hiện hành, vì mỗi unit ba lesson mới đủ chỗ cho một chặng có mở, có luyện và có chốt.

Ba cộng đồng đã đủ 10 khoá: cm-ind-food, cm-ind-drink, cm-ind-education. Danh sách đầy đủ từng khoá kèm pack và migration nằm ở api.conan.school/content/courses/catalog/*.json, trường status của mỗi khoá, đó là nguồn sự thật, không chép lại vào đây vì nó đổi mỗi phiên soạn.

Chữ cái trong tên migration phải kiểm trước khi đặt. Ngày 04.09 có hai file từ nhánh khác đã dùng a và d; đặt trùng chữ cái không làm CI hỏng ngay (sổ schema_migrations ghi theo tên đầy đủ) nhưng làm thứ tự áp trong cùng một ngày thành mơ hồ. Chạy ls migrations/<ngày>* trước khi đặt tên.

Câu hỏi cốt lõi của khoá ​

Mỗi pack bắt buộc có essential_question_vi và essential_question_en, một câu hỏi xuyên suốt cả khoá, hiện ngay dưới tên khoá ở /courses/:slug như phụ đề (quyết định của người soạn 07.09.2026). Cột courses.essential_question_vi / _en thêm ở migration 20260907d.

Đừng lẫn với EQ của unit: EQ unit hỏi về một chặng, EQ khoá hỏi về thứ người học đi tìm suốt cả khoá. Một khoá có ba EQ unit và một EQ khoá.

--check chặn ba thứ: thiếu bản tiếng Việt, thiếu bản tiếng Anh, và câu không kết thúc bằng dấu hỏi. Bắt buộc cả hai thứ tiếng vì cùng lý do với name_en: thiếu bản tiếng Anh thì trang EN hiện tên khoá tiếng Anh với một dòng phụ đề tiếng Việt ngay dưới, hỏng mà màn hình trông như đang chạy.

Cùng lượt đó, đoạn description dạng "Ba unit: ..." đã gỡ khỏi mục What you'll learn trên giao diện. Nó mô tả cách khoá được chia, không nói người học đi tìm gì; trường dữ liệu vẫn giữ nguyên, chỉ giao diện không vẽ nó nữa.

Ba mức soạn ​

Pack khai "level": 1|2|3. --check siết theo mức đã khai; pack không khai level là nội dung soạn trước hệ này và được kiểm như mức 3.

MứcCó gìDùng để
1Khung: tên song ngữ, Essential Question, Big Idea, Performance Task, SCQA cấp unit; mỗi unit ≥ 3 lesson, mỗi lesson có tên, definition, guiding_question và 1–2 key concept song ngữ. Chưa có quiz, bài viết, ví dụNhìn thấy hình dạng khoá và sửa hướng, làm trong một lượt ngắn
2Thêm SCQA từng lesson, ba bài viết, chín ví dụĐọc được, học được, chưa tự kiểm được
3Thêm ba chỗ dừng, quiz 20 câu mỗi lesson, errors, interventions, materialsĐưa ra cho người học thật

Bộ sinh từ chối sinh SQL cho pack mức 1 và 2, chúng là trạng thái đang soạn, sinh ra sẽ là một khoá cụt trong D1.

Mỗi unit tối thiểu ba lesson ​

Áp cho mọi mức. Hai lesson không đủ cho một chặng có mở, có luyện, có chốt.

Nợ tính tới 05.09.2026: 65 trên 108 pack có unit chỉ hai lesson, toàn bộ là khoá soạn trước luật này. --check liệt kê chúng ở dạng cảnh báo mỗi lần chạy, không làm đỏ CI. Trả nợ bằng cách thêm một lesson vào mỗi unit rồi khai "level": 3.

Key concept khai ở lesson, unit gộp lại ​

Mỗi lesson khai 1 key concept, tối đa 2, song ngữ. Unit không khai riêng: bộ biên dịch gộp từ các lesson, giữ thứ tự, bỏ trùng theo tên tiếng Việt. Số học kéo theo: 3 lesson × 1–2 nên unit có 3–6 key concept, và --check siết đúng khoảng đó.

Lý do không khai hai chỗ: hai danh sách phải khớp nhau bằng tay thì sẽ trôi khỏi nhau. Sửa một khái niệm ở lesson mà quên sửa ở unit thì trang unit dạy một đằng, bài dạy một nẻo, không lỗi, không màn hình nào đỏ. Một nguồn sự thật: lesson.

Ràng buộc này còn ép một điều đáng giá về sư phạm: một bài dạy một khái niệm. Lesson cần tới ba key concept thường là hai bài bị gộp làm một, và nên tách.

Mỗi key concept có vi, en, detail, và tuỳ chọn examples (ba ví dụ). Giao diện bày chúng thành chip luôn mang nhãn tiếng Anh, bấm vào mở popover có tên tiếng Việt, detail, và ba ví dụ. Nhãn cố định tiếng Anh vì khái niệm là từ khoá tra cứu được, người học gặp lại nó trong sách và trong bảng tính bằng tiếng Anh, không bằng bản dịch.

Ba ví dụ chứ không phải một: một ví dụ đủ để nhận ra khái niệm, ba ví dụ mới đủ để thấy đường biên của nó. Ví dụ nằm ở bảng course_unit_element_examples.

Pack cũ khai key_concepts thẳng ở unit thì bộ biên dịch tôn trọng phần khai đó, 108 pack soạn trước luật này đều thế. Pack mới khai ở lesson.

Xem thêm ​

DeepBook viết tay: khung trước, nội dung sau ​

Một DeepBook sống ở hai bảng: legacy_ebooks (quyển) và legacy_ebook_chapters (chương, mỗi chương có big_idea_vi và essential_question_vi). Nội dung thật nằm sâu hơn một tầng nữa, ebook_sections và các bảng con của nó (SCQA, khái niệm, ví dụ, bài tập, quiz).

Vì thế khung sách soạn được riêng, trước khi có nội dung: đợt 05.09.2026 chèn 12 quyển × 5 chương cho 12 cộng đồng (api.conan.school/migrations/20260907l_deepbooks_12_outlines.sql), mỗi chương chỉ có tên + một Essential Question. Hai điều cần nhớ về mức sơ bộ này:

  • Các quyển vào D1 với is_published = 0. Chương chưa có section thì trang đọc trả 404 ở /ebooks/:slug/chapters/:n; xuất bản trước khi có section là dựng một cái cửa mở vào phòng trống.
  • community_id phải điền ngay lúc chèn. DeepBook không gắn cộng đồng là nội dung mở cho tất cả (xem gác trong content/index.ts), nên bỏ trống không phải "để sau" mà là "mở khoá".

Thời gian học ước lượng (13.09.2026) ​

Ba cấp, và hai nguồn khác nhau, đây là chỗ dễ làm sai nhất:

CấpNguồnCột
Bài họcBộ biên dịch TÍNHcourse_concepts.estimated_minutes
Performance taskNgười soạn GHI ở task.minutescourse_unit_tasks.estimated_minutes
Unittổng bài + taskcourse_learning_units.estimated_minutes
Khoátổng các unitcourses.estimated_minutes

Phút của bài tính từ nội dung có thật: tổng minutes của ba bài viết, cộng 0,5 phút mỗi ví dụ, 1 phút mỗi chỗ dừng, 0,75 phút mỗi câu quiz. Không gõ tay, gõ tay thì con số lệch khỏi nội dung ngay lần sửa đầu tiên và không ai biết. Một bài mức 3 ra khoảng 29 phút, mức 2 khoảng 11.

Phút của task thì không tính được: task là việc làm ngoài ứng dụng (đi lấy ba tháng sổ thu chi, hỏi năm người khách), và không dấu hiệu nào trong văn bản nói nó mất bao lâu. --check bắt buộc task.minutes nằm trong khoảng 15..480.

Vòng đầu của 570 task được suy từ số bước đánh số trong đề bài, hai mươi phút một bước, sàn 60, làm tròn 15 phút. Đây là điểm xuất phát để người soạn chỉnh, không phải kết luận: phân bố hiện tại là 60 phút ×13, 75 ×246, 105 ×310, 120 ×1.

Con số được cộng sẵn và lưu, không cộng lúc đọc: thẻ khoá ở trang cộng đồng vẽ hàng chục khoá một lúc, cộng qua ba bảng cho mỗi thẻ là ba mươi câu truy vấn cho một dòng chữ. Cái giá là con số lệch nếu ai sửa nội dung thẳng trong D1, chấp nhận được, vì luật của repo là nội dung vào repo trước rồi mới biên dịch.

Giao diện không vẽ "0 phút": cột NULL nghĩa là chưa biên dịch lại, và 0 phút là một lời khẳng định sai trong khi không vẽ gì thì đúng là chưa biết.

Tài liệu nội bộ nền tảng Conan School.