Rà soát content-ops 28.08.2026
Rà soát đối kháng toàn phần hệ tự sinh và tự nâng chất lượng nội dung (ba Cloudflare Workflow + tầng authoring + bộ kiểm + chuẩn @conan/course-standard + docs), đúng ngày hệ ra đời, trước khi nó chạy đủ lâu để các lỗi nền kịp làm bẩn dữ liệu.
Phương pháp: 6 reviewer độc lập theo 6 lát cắt (ngữ nghĩa Workflows · SQL/D1 · prompt & sư phạm · tầng lọc/assert · bảo mật & chi phí · drift docs-mã), mỗi lát một reviewer đối kháng đọc lại từng finding để gỡ; 21 nghi vấn của reviewer chính được kiểm chứng riêng; một critic soát độ phủ (đường sinh→hiển thị, rollback, observability, vòng phản hồi người học). Chỉ giữ finding có bằng chứng file:line.
Kết quả: 57 finding qua kiểm chứng (5 blocker · 31 major · 21 minor/upgrade). Toàn bộ blocker và phần lớn major đã sửa cùng ngày; phần còn lại nằm ở [Roadmap](#roadmap-, -ghi-nhan-chua-lam-đot-nay).
Bản vá sau đó được đưa qua đợt review đối kháng thứ hai (3 reviewer + 3 verifier trên chính diff), bắt thêm 15 lỗi trong bản vá (đều đã sửa), đáng kể nhất: luật "câu đầu có tên riêng" vừa hồi sinh lại bác oan chính câu mở đúng bằng tên nhân vật; [Oo]ng bắt oan "làm xong A"; audit chạy no-retell thiếu ignore-set cast nên phạt SCQA đã qua cửa sinh đúng luật; id con cắt 8 ký tự làm hai target trùng tiền tố sập về một; comparator ưu tiên bị đảo (visible đè level); và mọi đường đọc/ghi cột owner_* nay tự rơi về lược đồ cũ khi migration 20260828j chưa kịp áp, vòng cron không chết vì thứ tự deploy.
Ba phát hiện đắt nhất
1. So sánh model: mục đích tồn tại của hệ, toàn dữ liệu bịa
Ba lỗi độc lập cùng đánh vào một chỗ:
| Lỗi | Hệ quả |
|---|---|
Orchestrator xoay vòng model, ghi vào course_content_runs.model, nhưng không hàm authoring nào nhận model, callAI luôn chạy llama-70b | DB nói "deepseek-r1 sinh bài này", sự thật là llama-70b. /ops/models so sánh những thứ chưa từng chạy |
Bước score lọc finding theo f.target_id === targetId, nhưng finding trỏ vào dòng con (câu hỏi, ví dụ) còn target của job là concept/unit | mine luôn rỗng → mọi run được 1.0 bất kể chất lượng |
runId = crypto.randomUUID() sinh ngoài step, engine replay run() từ đầu khi resume, UUID đổi, UPDATE đóng run trượt hết | Run dài/retry (đúng những run đáng xem nhất) treo running vĩnh viễn |
Đã sửa: model xuyên suốt mọi hàm authoring xuống callAI; finding mang owner_kind/owner_id (migration 20260828j) và score chấm theo owner với mẫu số là số item của chính artefact; runId = event.instanceId; score hỏng → NULL chứ không đánh fail một run đã sinh xong.
2. Cron chéo: mọi job 6 tiếng lặng lẽ chạy dày gấp đôi
Một hàm scheduled() chạy cho CẢ HAI cron (17 */6 và 41 */3) mà không rẽ theo controller.cron, thêm cron 3 tiếng cho course-ops đồng nghĩa reflection, product-reward, book-heartbeat, mọi concept-evolution và mentor-model chạy mỗi 3 tiếng thay vì 6. Kèm theo: một lỗi tạo instance ở đầu danh sách làm rơi toàn bộ phần còn lại của lượt cron.
Đã sửa: rẽ theo controller.cron; mỗi block tự lo lỗi của mình (log rồi đi tiếp).
3. Sinh lại ngân hàng giữa lúc người học đang làm quiz → 500, mất trọn lượt làm
Lượt quiz chốt chỉ lưu id câu hỏi; máy sinh nền sinh lại ngân hàng bằng DELETE+INSERT (id mới). Người học start lúc 8:00, orchestrator regen lúc 8:05, submit lúc 8:10 → byId.get(itemId)! nổ TypeError → 500, không nộp lại được. Cùng lúc, phát hiện đáp án không hề được xáo dù assessment.md tuyên bố có.
Đã sửa: items_json giờ là snapshot trọn câu hỏi (đề + đáp án ĐÃ xáo + ô đúng sau xáo), vừa miễn nhiễm với regen, vừa làm claim "xáo đáp án mỗi lượt" thành sự thật; run cũ dạng mảng id được chấm theo đường cũ nhưng bỏ qua câu đã mất thay vì nổ.
Blocker còn lại đã sửa
| # | Finding | Sửa |
|---|---|---|
| B4 | /ops/* chỉ sau auth member thường: tài khoản học thử cũng bật được mẻ sinh với budget không trần, model không validate (kể cả __proto__ lọt qua lookup) | Cả cụm sau Cloudflare Access; budget kẹp 1–24; model kiểm bằng hasOwnProperty; slugs chỉ nhận chuỗi |
| B5 | engine.ts lọc leaksAnswer TRƯỚC khi bilingualizeDiagnosticPrompt chèn tên tiếng Việt của khái niệm (thường chính là đáp án) vào đề, tự tạo lại đúng cái leak vừa lọc | Rewrite trước, lọc sau (cả diagnostic lẫn checkpoint) |
Major đã sửa (chọn lọc)
Orchestrator & Workflows
- Id con chứa
Date.now()→ "already exists" không bao giờ khớp: trùng target trong một lượt là sinh đôi, retry của step là sinh lại cả mẻ. → Id dựng từevent.instanceId, thêm dedup theokind:target. sleep 20 phút< trần retry của con (~47–63 phút) → audit chấm nội dung đang ghi dở, finding trỏ vào dòng sắp bị xoá. → 65 phút.- Blocker không lọc theo
course_slugs; finding mồ côi (artefact đã đổi id) chiếm đầu hàng đợiLIMITvĩnh viễn. → Lọc theo scope; mồ côi tự đánhresolved. unit-specđược giao cho unit chưa có khái niệm → fail lặp mỗi 3 tiếng, đốt budget. → Chỉ pick unit có concept.- Không bao giờ sinh
unit-big-idea/unit-bilingual(mức 3 theo chuẩn v2) và không thấy bài mất summative. → Ba counter mới trongmissingCounts+ ba pick mới; mức 3 gác big-idea quiz, mức 4 gác cả hai ngân hàng. - Ưu tiên "khoá hiển thị trước khoá ẩn" có trong docs nhưng không có trong mã. → Cột
visible(legacy_courses.is_active) + sort. open-runkhông idempotent khi step retry;persist-findingsmột batch không chặn trên;course_quality_reportsghi trùng khi replay. → ON CONFLICT, chia lô 80, id theo instanceId.
Tầng lọc & prompt
- Regex dựng từ tên khái niệm trong DB không escape +
\bASCII: tên chứa ký tự đặc biệt làm crash cả lượt audit/assert, tên có dấu làm luật tắt im lặng. →wordBound()(escape + lookaround Unicode) dùng chung. countSentencesđếm dấu chấm ngăn nghìn ("1.000.000 đồng" = +3 câu ảo); nhãn[A-E]với/ibắt oan "chị e rằng"; luật "câu đầu có tên riêng" chết im vì[A-ZÀ-Ỹ]phủ cả chữ thường có dấu. → Sửa cả ba trongcontent-rules.ts.- Người kiểm đáp án là CHÍNH model ra đề,
temperature 0.5; chỉ số trả về không chặn trên (đánh số 1-based là loại oan hàng loạt); lượt kiểm hỏng thì giữ lô không log. → Kiểm chéoVERIFY_PARTNER+ temp 0; schema kẹp 0–9; trả lời dưới nửa số câu coi như hỏng; log mọi lô bỏ kiểm. authorQuizBanksđo pool TRƯỚC kiểm (loại ~40%) → ngân hàng 8–11 câu được tính "đủ mức 4" vĩnh viễn. → Vòng sinh–kiểm–bù tối đa 5 lượt đo bằng số câu ĐÃ QUA kiểm, sàn 12/ngân hàng (minAccept v2.1).course_metadata(anti-scope, thuật ngữ cấm mượn, scenes, giọng văn) sinh ra để làm hàng rào nhưng không máy sinh nào đọc; tham sốbannedcủa blueprint không call site nào truyền. →courseAuthoringContext()bơm fence vào mọi prompt; scenes của khoá đè danh sách công ty chung; blueprint nhận danh sách cấm + retry + trần token 10000.evaluateUnitTaskbỏ qua trọng số tiêu chí không khớp tên → bài thiếu hẳn một phần vẫn qua cổng 80%. → Mọi tiêu chí vào mẫu số, không khớp = mức 0; temp 0.2.authorLessonPack(đã bị basics+quiz-banks thay) vẫn sống: chạy là xoá CẢ HAI ngân hàng rồi ghi 20 câu không cộtbank→ tất cả rơi vềsummative, formative trống, tụt mức 4. → Gỡ hẳn hàm + endpoint.- Ngân hàng unit 25 câu (đường legacy nhiều người học gặp nhất) chỉ lọc 1/4 lớp. → Đủ bốn lớp.
assertJourneyInvariantsquétJSON.stringifynên khái niệm trùng tên key schema (Option, Question, Select…) bị bắt oan vĩnh viễn. → Chỉ gom giá trị chuỗi.collectLog: truecho cả lượt chấm reflection/bài nộp/inquiry → log gateway thành kho lưu thứ cấp dữ liệu người học. → Tắt log ba engine đó.
Bộ kiểm (auditCourse)
- Chỉ cài 6/9 luật chuẩn v2, bỏ trắng SCQA đã lưu, ngân hàng unit, thư viện Big Idea, outcome cấp lesson; hai luật có trong mã lại vắng trong chuẩn; ba helper chép tay lệch chuẩn hoá với cửa sinh;
JSON.parsekhông guard làm một dòng hỏng giết cả lượt audit. → Viết lại: dùng chungcontent-rules.ts/diagnostic-language.ts, phủ mọi bảng, guard mọi parse, thêmno-foreign-chars,foreign-term-borrowed,bank-missing-summative, vàsuspect-answer-key, vòng phản hồi từ dữ liệu trả lời thật (≥5 lượt, >60% dồn một ô sai).
Docs & chuẩn: 16 chỗ drift đã đồng bộ
Đáng kể nhất: index.md còn dạy cơ chế 3-vòng-95% và thư viện ~20 câu (đã bị hai-cổng/hai-ngân-hàng thay); course-levels.md coi TEST_MODE là đường duy nhất; assessment.md nói lô kiểm 5 (mã: 10), "bỏ mọi câu bất đồng" (mã: hai lần bất đồng cùng phương án), "6 lượt mỗi ngân hàng" (mã: pool chung), "xáo đáp án" (mã: chưa, nay đã); content-ops.md khoe 5 model xoay vòng (mã: 3, nay 4 + llama-8b đứng ngoài có chủ đích); README nói ngưỡng PT thuộc profile nhưng chuẩn hardcode 0.8.
Chuẩn ra bản v2.1 (file mới, v2.json không đụng, đúng chính sách "đã công bố thì không sửa tại chỗ"): +6 luật chất lượng từ lỗi thật, bannedPhrases máy đọc được cho outcome-observable (một nguồn cho cả sinh lẫn kiểm), đặc tả lớp kiểm đáp án (lô 10 · kiểm chéo model · hai lần bất đồng), minAccept: 12, ba counter xếp mức mới chỉ gác khi có đếm, repo cũ không tụt mức oan.
Roadmap: ghi nhận, chưa làm đợt này
| Việc | Vì sao đáng làm | Cỡ |
|---|---|---|
| Versioning nội dung + chấm-trước-hiển-thị | Máy sinh vẫn ghi đè thẳng bảng đang phục vụ người học; lượt sinh tệ thay mất bản tốt, không đường rollback | Kiến trúc, cần bảng version cho họ course_* |
| Entitlement + school-scope + unit-lock cho nhóm endpoint học liệu mới | learn-overview/lesson-quiz/task/inquiry/reflection bỏ qua các cổng mà nhánh 5E cũ enforce, gọi thẳng API là vượt | Ngoài phạm vi content-ops, đã tách task riêng |
| Rate-limit các endpoint AI của người học | Vòng inquiry→view tạo được lượt gọi AI không giới hạn | Đã tách task riêng |
| Lớp thẩm định chuyên môn bằng model mạnh | Bộ kiểm bắt lỗi hình thức + đáp án, chưa bắt được giảng sai tinh vi | Một workflow mới, dùng deepseek-r1/model ngoài |
| Dọn finding resolved + progress trỏ item đã xoá | Bảng chỉ lớn dần; course_material_progress đếm cả dòng mồ côi sau regen | Nhỏ, làm cùng đợt versioning |
Đo thật retries.limit của Workflows (3 hay 4 lượt) | Tài liệu Cloudflare tự mâu thuẫn; ảnh hưởng ngân sách thời gian | Log attempt một đợt rồi chốt số |
Bài học nạp lại vào triết lý
- Đường ống đo chất lượng phải tự chứng minh được nó đang đo, ba lỗi độc lập cùng làm cột score thành 1.0 mà không ai hay; một truy vấn
HAVING score < 1ngày đầu đã lộ ngay. - Cùng một luật, một cài đặt, luật chép tay ở hai cửa (sinh/kiểm) là hai luật khác nhau chờ ngày cãi nhau; nay văn phong SCQA, cụm outcome rỗng, ngưỡng bài viết đều một nguồn.
- Metadata sinh ra để dùng, không phải để có, hàng rào anti-drift nằm trong DB tám ngày mà không prompt nào đọc.
- Dữ liệu người học là lớp kiểm cuối và rẻ nhất, mọi lớp kiểm lúc sinh đều thua được; đa số người thật cùng chọn một ô sai là bằng chứng mạnh hơn mọi model.
- Mã ngoài
step.dolà mã chạy lại, mọi giá trị định danh của một run phải bám vàoevent.instanceId.