---
url: >-
  https://docs.nemo12.com/architecture/sdd-027-content-foundry/production-plane.md
description: >-
  Xưởng nhận việc, xếp hàng, ưu tiên ra sao, kiểm đầu vào, vá từng phần, ghi
  changeset, chống chạy loạn và theo dõi trên Coral thế nào.
---

# SDD-027 · Mặt phẳng sản xuất, preflight, vá từng phần và màn theo dõi

Một phần của [SDD-027](./index.md).

## 9. Mặt phẳng sản xuất — địa chỉ, hàng chờ, ưu tiên (SRC-634)

Chỉ đạo chủ dự án 2026-08-28 gồm năm việc; §9-§13 là chỗ ở của chúng.

**Địa chỉ thật thay cho nhãn chữ.** `content_runs` mang `course_id` · `unit_seq` · `lesson_seq` ·
`part` (migration 0166). Trước đó chỉ có `target_label` là một chuỗi tiếng Việt, nên câu hỏi "khoá
này đã chạy những lượt nào" phải `LIKE` trên chuỗi — sai một dấu cách là mất dòng. Địa chỉ cũng
chính là khoá của mọi luật chống chạy loạn ở §13.

**Brief dựng từ D1, không gõ tay.** `db.ts` đọc course · unit · key concepts · Misconception Map ·
các bài anh em theo địa chỉ rồi dựng `LessonBrief`. Trước SRC-634 brief nằm trong body của
`POST /runs`: gõ thiếu key concepts thì model không tuân được CD-4.4 mà không ai biết, gõ sai tên
unit thì bài sinh ra lạc khỏi khoá. Nay **dữ liệu vào xưởng là đúng dữ liệu learner đang đọc**.

**Hàng chờ được TÍNH RA, không lưu.** `priority.ts` xếp mọi bài còn thiếu nội dung theo
`(rank môn, rank bậc, ladder, seq khoá, seq unit, seq bài)`. Một bảng hàng chờ lưu sẵn sẽ ôi thiu
ngay khi người soạn bổ sung một unit hoặc một lượt chạy xong, và không ai biết. Đổi lại là một
truy vấn nặng hơn — ở quy mô vài trăm bài đây là đổi chác đúng.

**Việc là gì:** một bài **đã có tên trong kế hoạch unit** mà nội dung chưa đủ sàn CD-8 (chưa có
story, dưới 3 hiểu lầm, dưới 2 lỗi, dưới 3 hướng gỡ, chưa có học liệu, dưới 2 câu kiểm). Xưởng
**không tự đẻ ra unit và không tự đặt tên bài** — đó là việc của người soạn unit (`/course-design`).

**Ưu tiên là dữ liệu, không phải code.** Bảng `content_priority(dimension, key, rank, enabled)`,
mặc định theo đúng chỉ đạo: `math` 1 · `vietnamese` 2 · `informatics` 3 · `english` 4; bậc 1→5 theo
thứ tự tăng dần (bậc nền trước). Hàng chờ `JOIN` vào bảng này, nên **`enabled=0` là biến mất khỏi
hàng chờ** — tắt một môn là một thao tác trên Coral, không phải một lần deploy.

## 10. Báo cáo kiểm tra đầu vào (preflight)

Chỉ đạo: *"mỗi workflow, rất cần có báo cáo kiểm tra xem những dữ liệu đầu vào đã đủ hay chưa, hay
là cần bổ sung thêm"*. `preflight.ts` chấm chín mục và phân biệt **chặn** với **nhắc**:

| Mục | Thiếu thì | Vì sao |
| --- | --- | --- |
| Khoá · Unit · Big Idea · Essential Question · tên bài · key concepts 3-5 | **CHẶN** | Model không thể đúng khi thiếu: không có key concepts thì CD-4.4 (concepts ⊆ key concepts) là bất khả thi, bài chắc chắn trượt L16 |
| Misconception Map của unit · Unit Outcomes · các bài anh em | nhắc | Bài vẫn ra được nhưng đứt mối nối (`unit_code` rỗng) hoặc dễ trùng phạm vi |

Một hàm, hai người gọi — và đó là chủ đích: **Coral hỏi trước** ("khoá này bấm sinh được chưa"),
**xưởng dùng để chặn** (chưa đủ thì không khởi động workflow). Đây là lớp chống tốn tiền rẻ nhất
trong cả hệ: một lượt chạy trên brief thiếu key concepts là trả tiền cho kết quả đã biết trước là
hỏng. Bản báo cáo tại thời điểm chạy được **chụp lại** vào cột `content_runs.preflight` — tính lại
tươi thì mọi lượt chạy cũ đều hoá ra "đủ" sau khi người ta bổ sung dữ liệu.

## 11. Vá từng phần, không sinh lại từ đầu

Chỉ đạo: *"làm sao để fix được từng phần, từng content, thay vì lại phải sinh lại từ đầu"*.

`POST /runs` nhận thêm `part` ∈ {guiding_question, concepts, outcomes, story, misconceptions,
errors, interventions, materials, checks} kèm `reasons` (lời người chê). Khi đó:

1. Bài gốc đọc từ D1 (`loadBaselineDraft`) — vá là sửa cái đang có, không phải sinh cái mới.
2. Prompt mang **cả bài** để model biết ngữ cảnh, kèm lệnh **cấm sửa phần khác**; model chỉ trả về
   `{"<part>": …}`. Bảy phần kia được ghép lại **bằng code**, model không có cơ hội chạm vào.
3. Mảnh vá bị ép **đúng ràng buộc của chính trường ấy** trong `lessonDraftSchema` (một nguồn sự
   thật: `partSpec` lấy schema con từ schema bài, không chép tay lần hai).
4. **Chấm lại CẢ BÀI sau khi ghép**, không chấm trên mảnh: một mảnh đúng vẫn có thể phá bất biến
   toàn bài (vá `checks` làm tụt số mcq xuống dưới số misconception — L13).
5. Rẻ hơn hẳn: trần output 2048 thay vì 8192, và mặc định **một model rẻ nhất** thay vì cả sổ —
   vá là việc nhỏ có đúng/sai rõ ràng, không phải cuộc thi giữa các model.

`curriculum_lessons` không có nội dung nào thì **không vá được**: xưởng trả 409 và bảo sinh cả bài
trước, thay vì lặng lẽ sinh cả bài (người bấm "sửa câu chuyện" mà nhận về một bài khác hẳn là kiểu
bất ngờ tệ nhất).

## 12. Lượt chạy này đổi những gì (changeset)

Chỉ đạo: *"mỗi lần chạy thì cái gì được tạo ra hoặc bị thay đổi"*. Bảng `content_run_changes`
(0166): mỗi lượt sinh 9 dòng, mỗi dòng một phần của bài, `action` ∈ created | updated | unchanged.

* **So với bản ĐANG CÓ trong `curriculum_*`**, không so với lượt chạy trước: người vận hành hỏi
  "chạy xong thì bài của con đổi gì", không hỏi "hai bản nháp khác nhau ra sao".
* **Ghi cả phần KHÔNG đổi.** Một lượt vá mà bảng chỉ hiện một dòng thì không phân biệt được với
  một lượt sinh cả bài đã làm hỏng tám phần kia; nhìn thấy tám dòng "giữ nguyên" mới là bằng chứng.
* So bằng JSON đã chuẩn hoá khoảng trắng: báo đổi oan thì mọi lượt đều hiện 9 dòng đỏ và bảng mất
  sạch giá trị.

## 13. Năm chốt chống chạy loạn

Chỉ đạo: *"làm sao đừng để hệ thống liên tục chạy toán loạn workflows, vì như vậy rất là tốn tiền"*.
Mọi lượt chạy — bấm tay hay do hàng chờ lấy việc — đi qua **cùng một hàm** `startRun`; hai đường
riêng là hai bộ chốt và bộ nào đó sẽ thiếu một chốt. Xếp **rẻ trước**: chốt nào không cần gọi model
để biết là sai thì phải chặn trước.

| # | Chốt | Trả về | Trần (vars, đổi phải deploy) |
| ---: | --- | --- | --- |
| 1 | Đầu vào chưa đủ (§10) | 422 kèm báo cáo | — |
| 2 | Bài này đang có lượt chạy | 409 | — |
| 3 | Vừa chạy, chưa hết nghỉ | 429 | `FOUNDRY_TARGET_COOLDOWN_MIN` = 30 |
| 4 | Đã thử quá số lần mà vẫn thiếu | 409, đòi người xem | `FOUNDRY_MAX_ATTEMPTS_PER_TARGET` = 3 |
| 5 | Trần song song / trần chi phí ngày | 429 | `FOUNDRY_MAX_CONCURRENT_RUNS` = 2 · `FOUNDRY_DAILY_USD_CAP` = 5 |

Thêm hai lớp nữa: **hàng chờ tự loại** việc đang chạy / vừa chạy / thử quá nhiều lần (nên bấm "lấy
việc" liên tục cũng không sinh hai lượt cho cùng một bài), và **trần lô** `FOUNDRY_MAX_BATCH` = 5
cho `POST /runs/batch` — chỗ duy nhất một cú bấm sinh ra nhiều lượt. Trần để ở `vars` chứ không ở
D1 là cố ý: **trần sửa được từ giao diện thì không còn là trần**.

**Cron kéo việc — có chặn, tắt mặc định.** Một đợt 80 bài mà trần lô là 5 thì người vận hành phải
bấm 16 lần; ma sát ấy không làm hệ an toàn hơn, chỉ làm người ta bấm bừa. Nên có `scheduled` chạy
mỗi 15 phút, kéo tối đa `FOUNDRY_MAX_BATCH` việc — nhưng **không có đường tắt nào so với bấm tay**:
mỗi việc vẫn qua đủ năm chốt, chạm 429 là dừng cả lượt kéo, và nó chỉ làm việc trong danh sách nhắm
nếu có. Tắt mặc định trong code (`FOUNDRY_AUTO_DRAIN` khác `"1"`); bật là một quyết định ghi trong
`wrangler.jsonc`, và kịch bản xấu nhất vẫn bị trần chi phí ngày chặn.

## 14. Màn hình theo dõi trên Coral

`coral.nemo12.com` → tab **Xưởng nội dung** (`apps/coral/src/Foundry.tsx`, SDD-013). Thứ tự khối
theo đúng thứ tự câu hỏi người vận hành hỏi: trần đang hiệu lực → ưu tiên môn/bậc → bảng từng khoá
(còn bao nhiêu bài thiếu, **đã chạy mấy lượt**, tốn bao nhiêu) → mở một khoá thấy việc của nó kèm
báo cáo đầu vào và nút *sinh cả bài* / *chỉ vá phần X* → mở một lượt chạy thấy **nó đổi những gì**.

Màn này **không giữ luật nào**: đủ đầu vào chưa, được chạy hay không, ưu tiên ra sao — tất cả do
xưởng quyết, Coral chỉ hiển thị và bấm. Chín route `/v1/coral/foundry/*` chỉ chuyển tiếp
(staff/admin gate + audit log cho hai route tiêu tiền).

**Trạng thái hiện tại:** service binding `FOUNDRY` trong `workers/api/wrangler.jsonc` **đang tắt**
vì `nemo12-foundry` chưa deploy lần nào (token CI thiếu quyền R2 — ghi tại chỗ trong wrangler.jsonc
từ SRC-632). Cho tới khi mở lại, mọi route foundry trả 404 có thông điệp rõ và màn Coral hiện đúng
lý do. Thứ tự bắt buộc: **deploy `nemo12-foundry` trước, mở binding sau** — CI đã xếp bước deploy
foundry trước bước deploy api đúng vì lẽ đó.
