---
url: https://docs.nemo12.com/engineering/incident-log/incidents-09-plus.md
description: >-
  Sự cố từ mục 9 trở đi (git add cuốn file phiên khác, cron chung nhịp, test ăn
  đĩa, nhánh treo) và luật sinh ra sau đó.
---

# Sổ sự cố vận hành · Mục 9 trở đi (26.08.2026 trở đi)

Xem cách đọc sổ ở [trang tổng](index.md). Mục mới thêm vào cuối trang này.

## 9. SRC-603 — `git add` một file DÙNG CHUNG cuốn theo bó việc dở của phiên khác (2026-08-26)

Biến thể mới của họ lỗi "máy local nói dối", và lần này local nói dối theo hướng **ngược lại**:
local thấy NHIỀU hơn CI, nên cái sai lọt qua mọi cổng chạy tại chỗ.

* **Triệu chứng.** `npm run check:docs` xanh trên máy. Cũng đúng commit ấy, chạy trên một bản
  checkout sạch thì đỏ ngay dòng đầu: `Cannot find module scripts/check-english-identifiers.mjs`.
* **Nguyên nhân gốc.** Một phiên khác đang làm một **bó ba mảnh** đi liền nhau: script cổng mới,
  trang `coding-conventions.md` chứa danh sách ngoại lệ mà cổng ấy đọc, và dòng gọi cổng trong
  `package.json`. Hai mảnh đầu còn là file chưa commit; mảnh thứ ba nằm trong `package.json` là
  **file dùng chung**. Phiên này sửa `package.json` để cắm cổng `check-req-canonical` của mình, rồi
  `git add package.json` cả file, nên **cuốn luôn dòng gọi của họ vào commit của mình**. HEAD từ đó
  gọi một script không tồn tại. Trên máy vẫn xanh vì hai mảnh kia có mặt trong cây làm việc.
* **Cách sửa.** Gỡ dòng gọi ấy khỏi commit của mình mà **không đụng file trên đĩa** (sao lưu bản
  cây làm việc, sửa, `git commit --amend`, rồi chép trả lại), để phiên kia giữ nguyên thứ họ đang
  làm dở. Cấm commit nửa bó: commit script mà thiếu trang ngoại lệ thì cổng chạy không có ngoại lệ
  và báo 232 vi phạm, tức là còn tệ hơn không commit gì.
* **Cổng/luật.**
  1. **Luật "chỉ `git add` đúng đường dẫn mình sửa" chưa đủ** khi đường dẫn ấy là file dùng chung.
     Với `package.json`, `docs/index.md`, cấu hình sidebar và mọi file nhiều phiên cùng đụng:
     **xem `git diff --cached <file>` trước khi commit**, và nếu thấy dòng không phải của mình thì
     hoặc gỡ ra, hoặc commit hộ trọn bó và nói rõ, chứ không cuốn theo nửa vời.
  2. **Kiểm chứng như CI kiểm, trước khi đẩy**: `git worktree add --detach <tmp> HEAD` rồi chạy cổng
     trong đó. Cây ấy chỉ có thứ đã commit, đúng thứ CI nhìn thấy. Đây là cách duy nhất bắt được
     loại lỗi này, vì mọi cổng chạy tại chỗ đều mù với nó.
  3. **Đọc đúng mã thoát.** Lần này cái sai còn suýt lọt thêm một nhịp nữa vì câu lệnh dạng
     `npm run check:docs | grep -E "✗|error"; echo "rc=$?"` — `$?` ở đó là mã thoát của `grep`, không
     phải của `npm`, và `grep` thì khớp được chữ "error" trong một dòng báo THÀNH CÔNG. Muốn lấy mã
     thoát thật thì `set -o pipefail`, hoặc chạy lệnh trần rồi mới lọc bản ghi.

## 10. SRC-603 — đánh ✅ trước khi tài liệu kịp vào kho, và cái giá của nó (2026-08-26)

Cùng ngày, cùng một cổng, vấp **hai lần liên tiếp** với hai mã khác nhau. Lần thứ hai mới thấy đây
không phải xui, mà là một lỗ hổng quy trình có thật.

* **Triệu chứng.** Cổng SRC-596 (trần nợ nội dung = 0) làm đỏ CI ngay sau khi một phiên commit
  `docs/intake.md`. Mã bị bắt lần lượt là SRC-600, SRC-604, rồi SRC-605 — **không mã nào thuộc về
  phiên vừa commit**.
* **Nguyên nhân gốc.** Sổ intake định nghĩa `✅` là *"Đã canonical hóa"*, nhưng thực tế nhiều phiên
  đánh ✅ **ngay khi làm xong code**, rồi mới viết tài liệu sau, hoặc để tài liệu nằm ở file chưa
  commit. Trong khoảng giữa ấy, dòng sổ **nói dối**: nó khai đã có tài liệu trong khi chưa. Cổng
  chỉ đơn giản là thứ đầu tiên đọc ra lời nói dối đó.
  Cái làm nó đau là **ai chịu hậu quả**: dòng sổ nằm trong file dùng chung, nên người làm đỏ CI là
  *phiên kế tiếp commit `intake.md`*, chứ không phải phiên đã đánh ✅ sớm.
* **Cách sửa.** Với SRC-600/604 (tài liệu đã viết xong, chỉ chưa commit): commit hộ trọn bó và nói
  rõ. Với SRC-605 (tài liệu **chưa tồn tại ở bất kỳ đâu**): hạ trạng thái dòng đó về `🔵` — sửa một
  ô, không đụng nội dung, để sổ nói đúng sự thật tại thời điểm ấy. Phiên sở hữu nó nâng lại thành ✅
  khi tài liệu vào kho.
* **Cổng/luật.**
  1. **`✅` nghĩa là tài liệu ĐÃ COMMIT, không phải "đã làm xong code".** Chưa commit tài liệu thì
     dòng đó là `🔵`. Đây không phải luật mới, chỉ là luật cũ nay có cổng đọc được.
  2. **Đừng nâng trần để đi tiếp.** Trần nợ nội dung là 0 và giữ nguyên 0. Cách đúng khi cổng đỏ vì
     mã của phiên khác: hoặc commit hộ phần đã trọn vẹn, hoặc hạ trạng thái dòng sổ cho khớp thực
     tế. Cả hai đều mất vài phút; nâng trần thì mất luôn cái cổng.
  3. **Bó việc phải commit trọn.** SRC-600/604 gồm **năm** mảnh dính nhau: file code, migration đổi
     enum, trang tài liệu, dòng cắm cổng trong `package.json`, và **trang reference sinh lại**.
     Commit thiếu mảnh nào cũng hỏng theo một kiểu riêng — thiếu migration thì production từ chối
     ghi, thiếu trang tài liệu thì cổng chạy không có danh sách ngoại lệ và báo 232 vi phạm, thiếu
     dòng cắm cổng thì luật không được canh, thiếu trang sinh lại thì AS-01.4.1 đỏ. Trước khi commit
     một bó, liệt kê đủ các mảnh rồi mới stage.
  4. **Thêm migration thì chạy `npm run gen:reference` trong CÙNG commit.** `data-dictionary.md`
     sinh từ thư mục `migrations/`, nên mọi thay đổi schema đều làm trang ấy lệch. Mảnh này dễ quên
     nhất vì nó không nằm trong đầu người viết migration — đã quên đúng một lần ở chính đợt này.
  5. **Một cái đỏ đúng chỗ rẻ hơn nhiều một cái xanh sai chỗ.** Lần này `ci` đỏ ở bước sinh lại
     reference, nên bước deploy trong `ci.yml` **không chạy**, và `deploy-api` cũng dừng ở cổng
     "chờ ci xanh". Nhờ vậy code ghi enum tiếng Anh **chưa** lên production trong khi bảng còn CHECK
     cũ — đúng khoảng lệch mà SRC-464 đã trả giá. Hai cổng nối tiếp nhau đã làm đúng việc của chúng.

## 11. `git add -A <thư mục>` vẫn là `git add -A` (2026-08-27)

Ca thật thứ ba của cùng một họ lỗi, và lần này nó dạy một điều mà hai ca cũ không dạy được.

* **Triệu chứng.** Một phiên commit bó việc của mình, nhưng commit ấy cuốn theo **8 file đang dở
  của phiên khác** (4 file JSON nội dung course + 4 migration). Phiên bị cuốn chỉ phát hiện khi
  thấy `git log` của file mình đang viết đã có commit lạ.
* **Nguyên nhân gốc.** Lệnh chạy là `git add -A scripts migrations`. Người gõ hiểu nó là *"thêm
  những file CỦA TÔI trong hai thư mục này"*; thực tế nó là *"thêm MỌI thứ trong hai thư mục này"*,
  kể cả file chưa theo dõi của phiên khác. Thêm tên thư mục vào **thu hẹp phạm vi, không thu hẹp
  quyền**.
* **Vì sao luật cũ không đỡ được.** `CLAUDE.md` cấm `git add -A`, nhưng cả hai ca trước đều là
  `git add -A` **trần**. Người đọc dễ suy ra rằng nguy hiểm nằm ở chỗ "không giới hạn", nên thêm
  thư mục vào là đã an toàn. Suy luận đó sai, và nó sai một cách im lặng.
* **Cách sửa.** Liệt kê đúng đường dẫn từng file trong mỗi `git add`. Phiên gây lỗi đã chuyển hẳn
  sang cách này; bên bị cuốn commit đè lại bản đã sửa của mình nên nội dung cuối vẫn đúng.
* **Cổng/luật.**
  1. **`-A` là cấm ở MỌI dạng**, kể cả có đường dẫn theo sau. Cũng đừng dùng `git add <thư mục>` hay
     `git add .` vì lý do y hệt.
  2. **Xem `git diff --cached --stat` trước mỗi commit.** Thấy tên file mình không đụng tới thì gỡ
     ra (`git restore --staged <file>`), đừng commit rồi tính sau.
  3. Lỡ cuốn rồi thì **nói ra**: nêu đúng tên file đã cuốn, để phiên chủ biết mà đè lại bản đúng.
     Cuốn nhầm còn cứu được; cuốn nhầm mà im lặng thì bản dở lên main và không ai truy ra.

## 12. File dở của phiên khác bị cuốn lên main làm ĐỎ CỔNG của mọi phiên (2026-08-30)

Cùng họ với ca 11, nhưng hậu quả khác hẳn nên ghi riêng.

* **Triệu chứng.** `ci` trên `main` đỏ ở bước typecheck với bốn lỗi `TS7006` trong
  `workers/api/src/modules/b21/authoring.ts`. Commit làm đỏ là một commit về **bảng giá IELTS**,
  không liên quan gì tới file đó. `deploy-api` mà một phiên khác vừa bấm cũng dừng ở cổng "chờ ci
  xanh", tức là **ba phiên đứng vì một cái quét nhầm**.
* **Nguyên nhân gốc, hai nửa.**
  1. Phiên A commit bó việc của mình và cuốn theo `authoring.ts` — file phiên B **đang viết dở**,
     chưa sửa xong kiểu, chưa chạy `tsc`.
  2. Phiên B (chính phiên này) làm một việc dài, nhiều file, **ngay trên cây làm việc chung** thay
     vì mở worktree riêng như `CLAUDE.md` đã dặn. Một file dở nằm trong cây chung là một quả mìn
     đặt sẵn cho mọi lệnh `git add` rộng tay của phiên khác.
* **Điều ca 11 chưa dạy.** Ca 11 nói về **mất công và sai quy công**: bản dở lên main, phiên chủ
  đè lại là xong. Ca này cho thấy cái giá thật lớn hơn: file dở là **code không biên dịch được**,
  nên nó không chỉ sai chủ mà còn **chặn deploy của toàn repo** cho tới khi ai đó chịu đọc log.
  Nội dung dở thì im lặng; code dở thì đỏ cả cổng.
* **Cách sửa.** Phiên B đẩy bản đã sửa kiểu, `main` xanh lại. Không ai phải revert.
* **Luật rút thêm.**
  1. **Việc dài hoặc nhiều file thì mở worktree** (`npm run wt -- <tên-việc>`). Luật này đã có
     trong `CLAUDE.md`; ca này là cái giá của việc bỏ qua nó.
  2. **Đừng để file code chưa biên dịch được nằm qua đêm trong cây chung.** Chưa xong thì hoặc
     `tsc` cho sạch, hoặc để ngoài repo, hoặc commit vào nhánh riêng.
  3. **CI đỏ trên `main` thì đọc log trước khi kết luận là lỗi của commit mới nhất.** Ở đây tên
     commit làm đỏ ("bảng giá IELTS") không dính dáng gì tới file gây lỗi.

## 13. Ba máy chung một nhịp cron: lượt bị giết giữa chừng và MẤT SẠCH LOG (2026-08-30)

* **Triệu chứng.** Xưởng tầng Course vừa lên production đứng im hai mươi phút: không unit nào được
  dựng. `wrangler tail` bắt trọn một nhịp cron và về **đúng không dòng nào** — không log lỗi, không
  cả dòng "đã chạy". Trong khi đó máy sinh khung năng lực b21 vẫn tiến đều, nên cron rõ ràng có nổ.
* **ĐÍNH CHÍNH (viết lại sau khi có bằng chứng).** "Không dòng nào" hoá ra có hai nguyên nhân chồng
  lên nhau, và bản đầu của mục này chỉ đoán đúng một nửa:
  1. `wrangler tail` trên máy chẩn đoán **tự nó hỏng** (`fetch failed`, lỗi mạng phía máy), nên
     nhiều lượt bắt log về rỗng không phải vì worker im lặng.
  2. Thứ thật sự chặn xưởng là **prompt trượt schema**, không phải lượt cron bị giết: model trả
     mảng ít phần tử hơn mức schema đòi, và mỗi lượt đều ghi `curriculum_unit_failed` đầy đủ —
     chỉ là chưa ai đọc được vì công cụ đang hỏng.
     Bài học không đổi nhưng lý do thì đổi: **đừng kết luận từ việc KHÔNG THẤY gì cho tới khi biết
     chắc công cụ quan sát còn sống.** Một dòng log ngay đầu lượt vẫn đáng có, và chính nó về sau là
     thứ chứng minh lượt cron chạy trọn vẹn (`cron_ai_tick_start` rồi `cron_ai_tick_done`).
* **Nguyên nhân gốc.** Ba máy dùng chung nhịp `*/5` và mỗi máy tự đặt trần riêng: b21 sáu skill,
  máy dịch mười sáu lô, xưởng tầng Course năm chặng. Cộng lại một lượt cron xin tới vài chục lượt
  gọi model, mỗi lượt 8 tới 20 giây. Lượt `scheduled` bị cắt trước khi chạy hết.
* **Vì sao im lặng.** Log gom trong `ctx.waitUntil` chỉ xả khi lượt kết thúc bình thường. Bị cắt
  giữa chừng thì mất sạch. Máy đầu tiên trong chuỗi vẫn kịp ghi vào D1 nên nhìn vào dữ liệu thấy
  "có tiến", còn máy cuối chuỗi thì không bao giờ tới lượt — và không có một dòng nào nói ra điều
  đó. Đây đúng loại hỏng nguy hiểm nhất: hệ trông như đang chạy.
* **Cách sửa.** `shared/aiBudget.ts`: MỘT ngân sách lượt gọi cho cả nhịp, ba máy tiêu chung theo
  thứ tự ưu tiên, hết phần thì nhường nhịp sau. Trần nằm ở chỗ có thật (thời gian một lượt cron),
  không nằm rải rác trong từng máy.
* **Luật rút ra.**
  1. **Trần phải đặt ở chỗ giới hạn thật sự tồn tại.** Mỗi máy tự đặt trần riêng là mỗi máy tự
     tính một mình, còn cái bị cạn là tài nguyên chung.
  2. **Thêm một máy vào nhịp có sẵn là đổi ngân sách của mọi máy đang ở đó**, kể cả khi không sửa
     một dòng nào của chúng.
  3. **Không log không có nghĩa là không chạy.** Lượt bị cắt trông y hệt lượt không nổ. Muốn phân
     biệt thì phải nhìn dấu vết ghi trong D1, hoặc ghi một dòng NGAY ĐẦU lượt trước khi làm gì.

## 14. Vá một dòng fixture để qua cổng, và cả bộ e2e lặng lẽ đổi vai (2026-09-18)

* **Chuyện gì.** Cổng ra mắt (`apps/learn/src/LaunchNotice.tsx`, SRC-833) chặn mọi tài khoản trừ
  chủ dự án cho tới 08:00 ngày 20.09.2026. Bộ e2e của learn đăng nhập bằng `demo@nemo12.com` nên
  139/160 case dừng ở trang thông báo. Bản vá được đề nghị và áp là **một dòng**: đổi email trong
  `apps/learn/e2e/fixtures.ts` sang `dac2205@gmail.com`. Bộ test xanh lại — trừ một case.
* **Vì sao nó hỏng.** Email ấy không chỉ mở cổng ra mắt. `SchoolHome.tsx:216` đặt
  `allSubjects = (viewerEmail === OWNER_EMAIL)`, nên cả bộ e2e chuyển sang chạy **dưới vai chủ dự
  án**: mọi môn đều mở, bấm "IELTS Reading" là điều hướng chứ không hiện hộp *"Sẵn sàng để học vào
  ngày…"*, và case gác đúng điều đó (`smoke.spec.ts:626`) đỏ. Chú thích của chính case ấy đã nói
  trước: *"Tài khoản trong fixture là demo@nemo12.com, tức KHÔNG phải chủ dự án, nên đây đúng là
  đường đi của học sinh thường."*
* **Chẩn sai lần đầu, và vì sao.** Phiên gây ra đã dựng một worktree ở commit TRƯỚC thay đổi của
  mình để kiểm, thấy vẫn đỏ, và kết luận "case này có từ trước". Nhưng trong worktree ấy họ đã áp
  chính dòng vá email để chạy được test — tức là **mang theo đúng biến gây lỗi rồi kết luận biến ấy
  vô can**. Phép đo tự làm nhiễm chính nó.
* **Cách sửa** (`172b0673`). Trả fixture về `demo@nemo12.com`; vượt cổng bằng
  `page.clock.install({ time: <sau LAUNCH_AT> })` rồi `resume()` — thời gian vẫn chạy nên animation
  và timeout của app không bị đóng băng. Cả bộ: 174 passed.
* **Luật rút ra.**
  1. **Fixture e2e phải giữ đúng vai của người dùng thường.** Cho nó mượn vai đặc quyền là tắt
     lặng lẽ mọi nhánh rẽ theo vai — `allSubjects` chỉ là nhánh may mắn có test gác; những nhánh
     khác thì không có chuông nào cả.
  2. **Vượt một cổng thì vượt bằng đúng thứ cổng ấy đo.** Cổng ra mắt là cổng THỜI GIAN, nên vượt
     bằng đồng hồ. Vượt bằng danh tính là đổi một biến thứ hai mà không ai khai.
  3. **Một giá trị trong fixture có thể là khoá của nhiều nhánh.** Trước khi đổi, `grep` xem nó còn
     được so sánh ở đâu nữa (`OWNER_EMAIL` ở đây nằm tại hai file khác nhau).
  4. **Muốn tách nguyên nhân thì phép đo không được mang theo nghi phạm.** Chạy ở commit cũ mà vẫn
     áp bản vá đang nghi ngờ thì kết quả không nói lên điều gì.

## 15. Bộ test ăn hết đĩa: 64 GB file tạm, và ba lớp rò khác nhau (2026-09-20)

* **Chuyện gì.** Đĩa máy chủ dự án còn **7,9 GB / 460 GB — 99%**. Thủ phạm không nằm trong repo:
  **60 GB trong thư mục tạm** của macOS, gồm **2.931 file `nemo12-schema-*.sqlite`** (~11 MB/file)
  và **2.722 file `nemo12-test-*.sqlite`** (~12 MB/file), tích từ 16.09 tới 01:04 sáng 20.09. Dọn
  xong thu về **64 GB**: 99% → 84%, trống 7,9 GB → 71 GB.

* **Vì sao tìm lâu.** Ba lần đo sai trước khi trúng:
  1. `df -h` rồi đọc **dòng đầu** — đó là volume hệ thống chỉ-đọc (12Gi/60%), không phải
     `/System/Volumes/Data` (428Gi/99%) nơi chứa `/Users`. Năm volume APFS chia chung phần trống
     nên cả năm dòng cùng hiện ~8 GB avail, và con số "60% mà còn 8 GB" trông vô lý — tôi đã dựng
     một lời giải thích (snapshot đang thu hồi) thay vì coi sự vô lý ấy là dấu hiệu mình đọc nhầm
     bảng. **Cách đúng: `df -h <đường dẫn mình quan tâm>`, đừng đọc bảng rồi chọn dòng.**
  2. `timeout 90 du -sh ~/Downloads` trả về rỗng. macOS **không có lệnh `timeout`** (exit 127), và
     tôi đọc kết quả rỗng thành "bị chặn quyền". Rỗng vì lệnh không tồn tại, không phải vì bị chặn.
  3. `du` thư viện Photos trả `Operation not permitted` — cái này thì chặn thật (TCC), và là chỗ
     duy nhất trong cả đợt đo bị chặn thật.

* **Ba lớp rò, ba nguyên nhân khác nhau** trong `workers/api/src/shared/runlog.testkit.ts`:

  | Rò | Nguyên nhân | Chữa |
  | --- | --- | --- |
  | File khuôn `nemo12-schema-*` | đặt tên bằng `randomUUID()` và **không có chỗ nào xoá** — mỗi worker mỗi lượt để lại một file | đặt tên bằng **vân tay SHA-1 của schema**: cùng schema thì cả máy dùng chung ĐÚNG MỘT file. Dựng vào tên tạm rồi `renameSync` (nguyên tử) để hai worker cùng lúc không đọc phải file viết dở |
  | File test `nemo12-test-*` | chỉ xoá trong `close()`; test quên gọi hoặc lượt chạy bị giết là để lại | thêm `afterAll` của vitest |
  | Bundle của cổng SEO | `scripts/check-seo-content.mjs` dựng 1,3 MB mỗi lượt, không xoá; cổng chạy cho mọi commit có code | `unlinkSync` NGAY sau `import` |

* **Vòng hai: rà cả kho xem còn script nào cùng bệnh.** Quét 16 file có đụng `tmpdir()`, đối chiếu
  "chỗ tạo" với "chỗ xoá", rồi ĐO bằng cách đếm — và đo mới là thứ tìm ra: **mỗi lần build web để
  lại thêm 2 file**. Ba chỗ nữa:

  | Script | Chạy khi nào | Rò |
  | --- | --- | --- |
  | `apps/web/scripts/prerender.mjs` | **mỗi lần build web**, cả CI lẫn máy | `nemo12-seo-<pid>.mjs`, 1,3 MB |
  | `apps/web/scripts/gen-sitemap.mjs` | **mỗi lần build web** | `nemo12-routes-<pid>.mjs` |
  | `scripts/curriculum/pull-parent-bodies.mjs` | khi kéo nội dung khoá bố mẹ | `nemo12-pc-<pid>.mjs` |

  Cả ba cùng một khuôn: `esbuild` gói TypeScript thành `.mjs` tạm rồi `import`, và không ai xoá.
  Chữa giống nhau: `unlinkSync` ngay sau `import` — Node đã nạp module vào bộ nhớ, file trên đĩa
  không còn ai cần. Đo lại sau khi vá: build đầy đủ + cổng SEO để lại **0 file**.

  Ba `scripts/seed-*.mjs` khớp lưới tìm nhưng KHÔNG rò: chúng chỉ ĐỌC `/tmp/<x>.json` làm đường dẫn
  mặc định. Lưới tìm theo chuỗi luôn bắt oan vài chỗ; đó là lý do bước đếm không bỏ được.

* **Bài học đắt nhất: `process.on("exit")` không đủ, và "đăng ký muộn" thì im lặng vô hiệu.**
  Bản vá đầu dùng `process.on("exit")` — vitest 4 kết thúc worker bằng cách giết tiến trình nên hook
  của Node không chạy: đo lại vẫn **58 file / 711 MB** sau một lượt đủ bộ. Bản thứ hai đăng ký
  `afterAll` nhưng đăng ký **bên trong `createTestDb()`**, tức khi vitest đã qua pha thu thập — hook
  rơi vào hư không, vẫn **59 file**. Chỉ khi đăng ký ở **mức module** mới xuống **1 file**. Cả ba
  lần đều "chạy xong không lỗi"; chỉ có phép đếm file mới phân biệt được.

* **Luật rút ra.**
  1. **Script nào tạo file trong `tmpdir()` thì phải xoá nó**, và xoá ở chỗ mọi đường thoát đều đi
     qua — ngay sau khi dùng xong, không phải ở cuối file có nhiều `process.exit()`.
  2. **File tạm dùng lại được thì đặt tên theo NỘI DUNG, không theo UUID.** Tên ngẫu nhiên biến một
     cache thành một đống rác tăng tuyến tính theo số lượt chạy.
  3. **Kiểm một bản vá dọn rác bằng cách ĐẾM, không bằng cách đọc mã.** `rm -f` sạch → chạy → đếm.
     Ba bản vá liên tiếp ở đây đều trông đúng khi đọc.
  4. **Dọn rác không bao giờ được làm đổ việc chính**: mọi lối xoá bọc `try/catch`, và một phiên
     song song đang chạy test thì không được giật file của nó (ngưỡng tuổi 6 giờ khi quét).

## 2026-09-22 — Một nhánh treo 12 ngày suýt xoá trọn một bản thiết kế (SRC-949)

**Bối cảnh.** PR #21 (`session/mentors-ui`, mở 10.09.2026) sửa một lỗi thật: `.n12-card` trên
`www` giữ nền trắng trong khi chữ bên trong đã bị kéo sang màu sáng, nên năm thẻ mentor ở
`/mentors` gần như trống. Bản vá thêm 22 dòng CSS. Tới 22.09 nhánh ấy **sau `main` 656 commit**.

**Hai điều lộ ra khi rà, và cả hai đều không nhìn thấy từ trang PR.**

1. **Lỗi đã hết sống.** Ngày 16.09, SRC-767 thay `.theme-www` biển-sâu bằng hệ dải sáng/tối, và
   trong đó `.n12-band-dark .n12-card` đã được xử sẵn (`apps/web/src/index.css:119`). Bản vá của
   nhánh vá một thứ không còn tồn tại, theo một công thức màu đã bị thay.
2. **Gộp nó vào là một phép XOÁ.** `apps/web/src/index.css` trên nhánh có 158 dòng, trên `main`
   có 378. Lấy file từ nhánh — kể cả bằng `git checkout <nhánh> -- <file>`, vốn trông như một
   thao tác hẹp — là quay ngược trọn bản thiết kế 16.09. Trong lượt rà này thao tác ấy **đã chạy
   thật** và chỉ lộ ra vì có bước đếm số dòng sau đó; `git diff --stat` in ra rỗng nên không cảnh
   báo gì.

**Xử lý.** Không vá lại. Đóng PR #21. Số SRC cũ của nó (SRC-704) đã bị cấp cho việc khác ngày
12.09 trong lúc nhánh treo, nên việc này nhận số mới.

**Luật rút ra.**

1. **Trước khi vá lại một thay đổi cũ, kiểm lỗi còn sống không** — mở file trên `main` và tìm
   đúng luật CSS/đoạn mã ấy, đừng tin mô tả trong PR. Mô tả đúng vào ngày nó được viết.
2. **`git checkout <ref> -- <đường dẫn>` KHÔNG phải một phép lấy hẹp.** Nó lấy TRỌN file ở phiên
   bản ấy, nên trên một nhánh cũ nó là một phép hoàn nguyên đội lốt một phép vá. Muốn lấy đúng
   phần thêm thì `git cherry-pick`, hoặc đọc `git diff` rồi áp tay.
3. **Đếm sau mỗi thao tác lấy file từ ref khác** (`wc -l`, `git diff --stat` so với `HEAD`). Ở đây
   `git status` chỉ nói "M" và không nói rằng chữ M ấy là 220 dòng biến mất.
4. **Nhánh treo quá một tuần thì cherry-pick, đừng gộp** — và kiểm lại xem số SRC của nó có còn là
   của nó không.
