Skip to content

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. 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ânChữ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ạithêm afterAll của vitest
    Bundle của cổng SEOscripts/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ó codeunlinkSync 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:

    ScriptChạy khi nàoRò
    apps/web/scripts/prerender.mjsmỗi lần build web, cả CI lẫn máynemo12-seo-<pid>.mjs, 1,3 MB
    apps/web/scripts/gen-sitemap.mjsmỗi lần build webnemo12-routes-<pid>.mjs
    scripts/curriculum/pull-parent-bodies.mjskhi 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.