Skip to content

Sổ sự cố vận hành · Mục 1 đến 8 (21.08 đến 25.08.2026) ​

Xem cách đọc sổ ở trang tổng.

1. SRC-431 — deploy-docs đỏ, chặn deploy tài liệu (2026-08-21) ​

  • Triệu chứng. CI job deploy-docs đỏ ở commit 2c8e8e1; tài liệu không xuất bản được. Chạy cùng cổng ấy trên máy thì xanh.
  • Nguyên nhân gốc. Hai câu trong SDD-013 và trang open-questions còn nói apps/data như mã đang có, trong khi thư mục đó đã gỡ khỏi repo. Cổng kiểm bằng existsSync. Trên máy vẫn còn một apps/data rơi rớt (đã gỡ khỏi Git, chỉ sót .env và dist không được theo dõi) nên existsSync trả về đúng; chỉ bản checkout sạch của CI mới thấy sự thật.
  • Cách sửa. Sửa câu văn cho khớp thực tế (2c760f4), rồi kiểm lại bằng cách tạm chuyển thư mục rơi rớt đi và chạy lại cổng.
  • Cổng/luật. Kết quả cổng chỉ có giá trị khi vùng liên quan sạch trong Git. Trước khi tin một lần chạy xanh, chạy git status --short <thư-mục> — còn dòng ?? nào là kết quả chưa nói lên gì. Cách kiểm chứng cho lỗi họ này: gỡ tạm thứ chỉ-có-trên-máy rồi chạy lại, đừng chạy lại y nguyên.

2. SRC-457 — nhiều phiên chạy song song đè nhau (2026-08-21) ​

Hai tai nạn riêng biệt, cùng một nguyên nhân họ: nhiều phiên Claude làm việc cùng lúc trên cùng một cây làm việc, và cả hai lần đều im lặng.

  • Triệu chứng (a) — số SRC cấp trùng. Hai phiên cùng đọc con trỏ > Nguồn tiếp theo: SRC-450 rồi cùng lấy 450; SRC-448/450 bị lấy trùng.
  • Triệu chứng (b) — dòng sổ chung biến mất. Một phiên đọc cả docs/intake.md vào bộ nhớ, sửa, rồi ghi cả file ra — dòng mà phiên khác vừa chèn giữa chừng biến mất không dấu vết.
  • Nguyên nhân gốc. Cấp số dựa trên con trỏ đọc trước rồi +1 là thao tác không nguyên tử. Và ghi-cả-file là phép "last writer wins" trên một sổ chung chỉ được phép chèn.
  • Cách sửa (7537cbe, a17aaa3).
    • scripts/src-new.mjs cấp số nguyên tử: khoá bằng mkdir, số = max đang có + 1 (không tin con trỏ), đọc lại ngay trước khi ghi, rename nguyên tử.
    • QG-001 thêm hai bất biến: không trùng số và con trỏ phải đi trước max.
    • scripts/check-no-clobber.mjs chặn commit làm mất một dòng SRC/REQ đang có trong ref gốc.
    • .githooks/pre-commit chạy check-no-clobber + check-docs cho mọi commit đụng docs/, và scripts/install-hooks.mjs chạy qua vòng đời prepare để npm install tự bật hook — quên bật tay là hàng rào biến mất mà không báo gì, dạng hỏng tệ nhất.
    • CI chặn lần hai (so với HEAD~1), nên --no-verify hoặc máy chưa bật hook cũng không lọt.
    • scripts/worktree.mjs (npm run wt) cho mỗi phiên một cây riêng trên nhánh session/<tên>.
  • Cổng/luật (bản đầy đủ ở CLAUDE.md tại gốc repo, cố ý KHÔNG đặt link vì file đó nằm ngoài srcDir của VitePress nên link sẽ làm đỏ cả build — đây là bản rút gọn để tra):
    1. Cấp số SRC chỉ bằng node scripts/src-new.mjs "<nguồn>" "<canonical>"; không bao giờ đọc con trỏ rồi +1.
    2. Sổ chung (docs/intake.md, docs/traceability.md, bảng REQ trong PRD-001) chỉ được CHÈN. Dùng Edit trên đúng đoạn; cấm đọc cả file rồi ghi cả file.
    3. git add -A là cấm — nó cuốn theo file đang dở của phiên khác. Chỉ git add đúng đường dẫn mình sửa.
    4. Đọc lại file ngay trước khi sửa nếu giữa chừng có gọi tool khác. Hệ thống báo "changed on disk" thì bản trên đĩa là gốc, đừng khôi phục bản cũ của mình.
    5. Cố ý xoá dòng sổ chung phải nói ra: NEMO12_ALLOW_LEDGER_DELETE=1 hoặc [ledger-delete] trong message commit — escape hatch để lại vết đọc được.
    6. Hook không cứu được file code. Sổ chung mất một dòng là dấu hiệu máy đọc được; App.tsx bị đè thì không ai biết (đã xảy ra với ProgressViews.tsx ngày 2026-08-21, phải áp lại tay). Việc dài hoặc đụng nhiều file code → làm trong worktree riêng.
  • Kiểm chứng đã chạy. 8 tiến trình cấp số đồng thời ra 8 số liên tiếp không mất dòng; tạo dòng trùng → QG-001 FAIL đúng; --no-verify xoá dòng SRC-446 → CI-mode bắt được; thêm [ledger-delete] → cho qua.

3. SRC-464 — "no such column" trên production dù deploy-api xanh (2026-08-21) ​

  • Triệu chứng. Cột mới báo no such column trên production, trong khi workflow deploy-api báo xanh. Lộ ra tình cờ khi làm SRC-463.
  • Nguyên nhân gốc. Bước áp migration trong deploy-api.yml chỉ chạy khi dispatch tay (04929dd, cố ý — để token deploy thường không cần quyền D1). Push thường chỉ deploy code. Hệ quả: 0064_referrals và 0065 nằm PENDING nhiều giờ mà không ai biết, vì mọi chỉ dấu đều xanh.
  • Cách sửa. Dispatch deploy-api qua GitHub (đúng luật cấm wrangler thẳng lên production), áp cả hai migration; sửa con trỏ số trong migrations/README.md (0038 → 0066, lệch từ lâu) và ghi thành luật (f802cf8).
  • Cổng/luật.
    • Merge migration xong PHẢI dispatch deploy-api bằng tay, rồi kiểm wrangler d1 migrations list --remote cho ra "No migrations to apply". Xanh của push thường không chứng minh migration đã áp.
    • Đây là mặt còn lại của QG-004: "migration tương thích deploy trước code". Chính vì code có thể lên trước migration nên migration phải idempotent và code phải chịu được cột chưa tồn tại — nếu không, khoảng trễ này là downtime. Quy trình an toàn ở skill d1-migrate.
    • Con trỏ số migration là thứ lệch âm thầm; kiểm lại nó mỗi lần đụng thư mục migrations.

4. SRC-490 & SRC-491 — canonical hoá là gì, và tại sao mọi trao đổi phải vào docs ​

Không phải sự cố, nhưng nằm ở đây vì nó là cách phòng cho cả họ lỗi "kiến thức chỉ còn trong commit message hoặc trong lịch sử chat" — đúng thứ khiến file này phải được viết.

  • Bối cảnh (SRC-490). Chủ dự án hỏi: "chuyển toàn bộ những gì đã trao đổi vào docs.nemo12.com. Việc này gọi là tạo các canonical à?" — đúng tên gọi, nhưng có hai bước khác nhau:
    1. Ghi intake: docs/intake.md giữ nguyên văn yêu cầu và quyết định, kèm mã SRC-xxx. Đây là lịch sử, không phải thiết kế.
    2. Canonical hoá: đưa nội dung ấy thành REQ trong PRD và thiết kế trong SDD, có trace hai chiều. Đích đến là: đọc PRD/SDD là đủ, không phải lần lại lịch sử chat.
  • Việc đã làm. Đợt SRC-490 canonical hoá SRC-476..489: thêm REQ-VIS-11..15 và REQ-LRN-41..44 vào PRD-001; viết SDD-008 §8–§12, SDD-010 §5.1, SDD-021 §7.1–§7.2 (cd18f53).
  • SRC-491 là mặt thi hành của cùng nguyên tắc: "mô tả và gợi ý thêm về các việc còn sót, quyết luôn giúp tôi rồi làm luôn càng tốt". Hai việc treo từ 2026-08-17 được chốt và làm ngay — ô "Nhắc lại" trong màn làm bài (55c0c27), và chốt không dựng cột hint_vi rỗng mà dùng chữ Pearl đã có (812/822 node), thiếu chữ thì ẩn nút.
  • Luật. Quy trình sáu bước ở conventions.md §6 là bắt buộc: ghi intake → xác định PRD/SDD → thêm/cập nhật REQ → cập nhật sources/satisfies → cập nhật traceability. Hai hệ quả thực hành rút từ SRC-490/491:
    • Một dòng intake chưa canonical hoá thì việc chưa xong. Commit message không phải nơi lưu thiết kế; nó không có trace, không grep được theo REQ, và không ai đọc lại.
    • Chốt bằng cách không dựng cột rỗng. Khi dữ liệu đã có ở chỗ khác, dùng nó và ẩn UI khi thiếu — thêm một cột rỗng là thêm một nguồn sự thật thứ hai phải nuôi.

5. SRC-500 — docs.nemo12.com build ĐỎ sau khi đảo chiều nguồn dữ liệu (2026-08-22) ​

  • Triệu chứng. Ngay sau khi cây năng lực chuyển vào docs/ để docs/ thành nguồn, toàn bộ docs.nemo12.com không build được.
  • Nguyên nhân gốc. VitePress chặn cả bản build khi có link chết. Các trang curriculum vừa chuyển vào trỏ sang những chùm bài luận chỉ tồn tại trên site Pearl (/ideas/…, /mental-models/…, /decisions/…, /hieu/…, /learning/…) — trên docs chúng là liên kết ngoài site. Hệ quả méo mó: một chùm bài luận thiếu chỗ ở kho khác làm sập toàn bộ docs, đúng lúc docs vừa thành nguồn.
  • Cách sửa (8d36e93). Khai ignoreDeadLinks trong apps/docs/.vitepress/config.mts theo tiền tố (regex trên các nhánh Pearl), không tắt kiểm link.
  • Cổng/luật.
    • Khi một cổng chặn quá rộng, thu hẹp phạm vi cổng, đừng tắt cổng. Link gãy trong tài liệu quản trị vẫn phải đỏ như cũ — đó là giá trị còn lại của cổng.
    • Chuyển một cây nội dung vào docs/ là đổi tập link hợp lệ. Chạy build docs trước khi coi việc chuyển nguồn là xong.

6. SRC-504 — 8 test e2e gãy trên main sau thay đổi của phiên khác (2026-08-21) ​

  • Triệu chứng. CI trên main đỏ: 8 test e2e gãy, sau khi phiên khác đổi bản đồ (SRC-499) và popover (SRC-503) mà không cập nhật test.
  • Nguyên nhân gốc. Hành vi đổi có chủ ý, test bị bỏ lại. Nó chỉ nổ ở main vì mỗi phiên chỉ chạy phần của mình.
  • Cách sửa (0563d43). Sửa TEST cho khớp luật mới, không sửa giao diện của phiên kia:
    • Bản đồ nay mặc định chỉ bày Package → test nào chạm Module/Unit phải mở sâu trước; gói thành một hàm moBanDoToiUnit khớp cả nhãn VI lẫn EN, để lần sau đổi mức xem chỉ sửa một chỗ.
    • Nhãn ngôi sao Focus đổi theo mức xem.
    • Popover xác nhận bỏ link "Ở lại đây" → cách đóng nay là bấm lớp phủ, và phải bấm vào góc lớp phủ chứ không phải giữa, vì giữa là chính popover.
    • Test "Dashboard bày sẵn cả ba tầng" viết lại thành "mặc định chỉ Package, mở sâu mới tới Unit": luật cũ SRC-445 (một cách bày) vẫn đúng, cái đổi là bày TỚI ĐÂU.
  • Cổng/luật.
    • Đổi hành vi UI thì cập nhật test trong cùng lần đổi. Test gãy vì hành vi đổi có chủ ý là nợ, không phải nhiễu.
    • Sửa test cho khớp luật mới, không sửa giao diện của phiên khác cho khớp test cũ — trừ khi xác định được luật mới là sai.
    • Gói thao tác điều hướng lặp lại thành một helper. Tám test gãy vì tám chỗ tự mở bản đồ.
    • CI đỏ trên main là việc của mọi phiên, không phải của riêng ai gây ra: cổng chặn deploy nên một phiên để đỏ là mọi phiên đứng (đã có lần năm SRC của ba phiên bị kẹt).

7. SRC-546 — cách làm một đợt "còn việc gì nữa": liệt kê → xếp ưu tiên → làm P1 (2026-08-24) ​

Ghi ở đây như quy trình mẫu, vì nó là cách phát hiện ra loại nợ không ai báo cáo.

  • Yêu cầu. "còn việc gì cần làm nữa, liệt kê, đánh giá ưu tiên rồi làm dần" → đợt P1 gồm ba mảnh khép vòng chuẩn phủ (dd8f3af, b9ac303).
  • Ba mảnh và vì sao chúng là P1.
    1. 10 dây unit_prereqs cho node mới của SRC-537 (dấu hiệu chia hết làm nền cho ƯCLN-BCNN và đồng dư; chuỗi hình khối bình hành → tam giác-thang → hộp → lăng trụ → chóp → trụ-nón-cầu; xác suất cần cả chắc-chắn-có-thể lẫn kiểm đếm). Không có dây thì trang Chỗ hổng và lối cứu giữa bài không trỏ về được unit mới — chúng tồn tại mà bộ kê đơn không biết đường.
    2. 14 dòng chuẩn phủ đổi 🚧 → ✅ sau khi 106 câu qua cổng chất lượng. Bảng Toán 1–9 còn 81 ✅ · 0 🚧 · 1 ✍️ (số La Mã, cố ý bỏ).
    3. Dòng sót phát hiện khi rà lại — kí hiệu khoa học lớp 7 — chèn thêm, kèm lộ ba node luỹ thừa ra khỏi strand ẩn.
  • Cổng/luật.
    • Nội dung mới chưa nối dây prereq là nội dung vô hình. Seed node xong mà không seed unit_prereqs thì bộ kê đơn không bao giờ trỏ tới — không có lỗi nào nổ, chỉ là im lặng.
    • Bảng chuẩn phủ sót dòng thì CHÈN, không xoá và viết lại. Cùng luật chỉ-chèn với sổ chung (mục 2).
    • Ô cố ý bỏ phải mang dấu riêng (✍️), khác ô chưa làm (🚧) — nếu không, mỗi đợt rà lại tốn một lần điều tra cùng một ô.
    • Sau seed phải xuất lại dữ liệu tri thức để tài liệu khớp D1 (b9ac303).

8. SRC-583 — gọi API ít thôi, hit database ít thôi (2026-08-25) ​

  • Triệu chứng. Chủ dự án: "tối ưu đi, gọi API ít thôi, hit vào database ít thôi". Bảng chỉ số ghép ở client: progress + cockpit + retention/summary cho TỪNG môn, tức 1 + 2N request HTTP, mỗi request lại vài truy vấn D1. Với 5 môn là 11 request và hơn 30 lượt chạm D1 chỉ để về 20 con số — mà số môn đang tăng (đã 11 môn).
  • Nguyên nhân gốc. Chi phí tuyến tính theo số môn, giấu trong tầng ghép ở client, nên nó lớn dần một cách âm thầm — không lần nào đủ chậm để ai báo.
  • Cách sửa (23b2451). GET /v1/learners/{id}/metrics dùng số truy vấn cố định (6), không phụ thuộc số môn: thống kê gộp theo môn · mục tiêu mỗi môn · blueprint mặc định cho môn chưa có mục tiêu · trọng số một câu IN · bảng mastery theo node · hàng retention mỗi môn. Thêm môn thứ 12 không làm bảng chậm thêm một nhịp. Tiện thể gỡ N+1 có sẵn trong /progress (đọc danh sách môn rồi chạy thêm một truy vấn cho từng môn) bằng GROUP BY + HAVING.
  • Cổng/luật hiệu năng đã áp.
    1. Số truy vấn phải cố định, không tăng theo số môn / số learner / số node. Đây là bất biến cần phát biểu ra khi thiết kế endpoint, không phải chỉ số đo sau.
    2. Không ghép dữ liệu ở client bằng nhiều request. Ghép ở server thành một endpoint; tầng ghép ở client là nơi N+1 trốn giỏi nhất vì không log nào cho thấy nó.
    3. Soát N+1 quanh mọi vòng lặp có truy vấn bên trong. GROUP BY + HAVING thường thay được nguyên vòng lặp.
    4. Endpoint gộp phải dùng lại chính hàm tính của engine — computeReadiness, cùng luật chọn blueprint với buildCockpit, nhãn retention từ chính groupLabel. Công thức chỉ sống ở một nơi; nếu không, hai màn sẽ nói hai con số khác nhau.
    5. Placeholder đánh số ?1..?n, không dùng ? tràn. Testkit của repo chỉ hiểu dạng đánh số: ? tràn chạy được trên D1 thật mà trống trong test — tức là mất lưới an toàn, đúng họ "local nói dối" ở đầu file.
    6. Test một endpoint gộp phải khoá đúng những chỗ dễ sai của việc gộp (7 test đã viết): coverage không đếm node thuộc Package tạm ẩn · mastery không pha loãng bởi bài chưa học · chưa đạt mục tiêu thì readiness là null chứ không phải 0 · bài chưa từng vùng không lọt vào mẫu số retention · 401 khi không có phiên.
    7. Đổi API là đổi hợp đồng: sinh lại contract snapshot (3020e68, 109a532).