---
url: https://docs.nemo12.com/engineering/incident-log/incidents-01-08.md
description: >-
  Tám sự cố đầu tiên (deploy-docs đỏ, phiên song song, cột thiếu, e2e gãy, gọi
  API) và cổng sinh ra sau mỗi lần.
---

# 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](index.md).

## 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](../../quality/quality-gates.md): "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](../../conventions.md#_6-quy-trinh-tiep-nhan-tai-lieu-moi)
  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`).
