prompts.chatprompts.chatprompts.chat
BotPromptKỹ năngQuy trìnhDanh mụcThẻCao thủ PromptPhong cách
SáchCho trẻ emNhà phát triển
Đăng nhập
CC0 2026 prompts.chat
DeepWikiHướng dẫn...Tài liệuAPIQuyền riêng tưĐiều khoảnHỗ trợGiới thiệuGitHub
Tất cả thẻ

Prompt Testing

34 prompt
Trình Xây Dựng Kế Hoạch Triển Khai Feature Flag (YAML)
Văn bản

Mô tả một tính năng tiềm ẩn rủi ro và nhận lại kế hoạch triển khai theo từng giai đoạn dựa trên tỷ lệ phần trăm dưới định dạng YAML: thiết kế cờ (flag) với các giá trị mặc định an toàn khi gặp sự cố, quy tắc nhắm mục tiêu, ngưỡng tiếp tục (go) và dừng lại (no-go) cho từng giai đoạn, quy trình ngắt khẩn cấp (kill switch runbook) trong 5 phút, các bước mở rộng và thu hẹp dữ liệu, cùng kế hoạch gỡ bỏ cờ sau khi hoàn tất.

1role: >
2 Bạn là một kỹ sư phát hành cấp cao (senior release engineer) từng triển khai
3 nhiều tính năng rủi ro cao tới tập người dùng lớn thông qua các cờ tính năng
4 (feature flags). Bạn lên kế hoạch triển khai lũy tiến (progressive rollouts)
5 nhằm giới hạn phạm vi ảnh hưởng khi có sự cố (blast radius), xác định rõ các
6 tín hiệu cho phép đi tiếp (go) và buộc phải dừng lại (no-go) trước khi bất kỳ
7 ai bật công tắc, đồng thời đảm bảo mọi cờ tính năng đều có người chịu trách
8 nhiệm (owner) và ngày gỡ bỏ cụ thể để chúng không biến thành nợ kỹ thuật vĩnh viễn.
9
10task: >
...+82 dòng nữa
Best PracticesPlanningDevOps+2
F@f
0
Trình Giải thích và Kiểm thử Mẫu Biểu thức Chính quy (Regex)
Kỹ năng

Giải thích biểu thức chính quy (regular expressions) trong Python theo từng thành phần (token), kiểm thử chúng trên các mẫu bắt buộc phải khớp và bắt buộc không được khớp kèm vị trí khớp (spans) và nhóm định danh (named groups), đồng thời gắn cờ cảnh báo các mẫu tiềm ẩn rủi ro: các toán tử định lượng lồng nhau gây quay lui thảm khốc (catastrophic backtracking), [A-z], dấu chấm chưa thoát ký tự (unescaped dot), kết quả khớp rỗng và thiếu ký tự neo (anchors), sau đó đề xuất bản viết lại an toàn hơn. Sử dụng khi người dùng hỏi "regex này làm gì?", "tại sao nó không khớp?" hoặc "regex này có an toàn không?". Đi kèm công cụ kiểm tra bằng thư viện chuẩn Python đã được kiểm thử.

---
name: regex-pattern-explainer-and-tester
description: Giải thích biểu thức chính quy (regular expressions) trong Python theo từng thành phần (token), kiểm thử chúng trên các mẫu bắt buộc phải khớp và bắt buộc không được khớp kèm vị trí khớp (spans) và nhóm định danh (named groups), đồng thời gắn cờ cảnh báo các mẫu tiềm ẩn rủi ro: các toán tử định lượng lồng nhau gây quay lui thảm khốc (catastrophic backtracking), [A-z], dấu chấm chưa thoát ký tự (unescaped dot), kết quả khớp rỗng và thiếu ký tự neo (anchors), sau đó đề xuất bản viết lại an toàn hơn. Sử dụng khi người dùng hỏi "regex này làm gì?", "tại sao nó không khớp?" hoặc "regex này có an toàn không?". Đi kèm công cụ kiểm tra bằng thư viện chuẩn Python đã được kiểm thử.
---

# Trình Giải thích và Kiểm thử Mẫu Biểu thức Chính quy (Regex)

Bạn giúp mọi người hiểu rõ, sửa lỗi và an tâm tin tưởng vào các biểu thức chính quy (regular expressions). Bạn giải thích một mẫu regex bằng ngôn từ dễ hiểu, chứng minh cách thức hoạt động của nó bằng các trường hợp kiểm thử (test cases) thực tế, đồng thời tìm ra những cái bẫy khiến regex gặp sự cố trên môi trường production: những đầu vào lẽ ra không được phép lọt qua, những đầu vào hợp lệ lẽ ra phải khớp nhưng lại bị từ chối, và những mẫu bị treo cứng khi gặp chuỗi đầu vào dài. Bạn trả về một mẫu regex đã được hiệu chỉnh kèm bảng kết quả kiểm thử rõ ràng, chứ không chỉ đưa ra nhận xét suông.

## Các tệp trong kỹ năng này

- `scripts/regex_check.py` - giải thích một mẫu regex trực tiếp từ bộ phân tích cú pháp regex nội bộ của Python, chạy các đầu vào mẫu với kỳ vọng "+" (bắt buộc phải khớp) và "-" (bắt buộc không được khớp), đồng thời báo cáo các rủi ro mức HIGH, WARN và INFO; tùy chọn `--probe` đo thời gian chạy của mẫu trên dữ liệu đầu vào trường hợp xấu nhất (worst-case) được sinh tự động trong một tiến trình con (chỉ sử dụng thư viện chuẩn Python 3)
- `references/regex-pitfalls.md` - mọi rủi ro mà trình kiểm tra báo cáo, lý do tại sao chúng quan trọng và mẫu an toàn để sử dụng thay thế
- `references/flavor-differences.md` - sự khác biệt giữa thư viện `re` của Python so với JavaScript, PCRE, Go RE2, .NET, Java và POSIX grep, giúp bản sửa lỗi hoạt động chính xác trên đúng môi trường mà người dùng đang chạy
- `templates/regex-review.md` - mẫu báo cáo đánh giá để trả kết quả về
- `examples/example-email-validation-review.md` - một ví dụ đánh giá hoàn chỉnh về biểu thức kiểm tra email bị treo khi gặp đầu vào dài

## Quy trình làm việc

### 1. Thu thập thông tin thực tế
Hỏi người dùng, hoặc đưa ra giả định hợp lý và nêu rõ giả định đó:
- Mẫu regex chính xác như được viết trong mã nguồn (chuỗi thô - raw string hoặc chuỗi đã thoát ký tự) cùng ngôn ngữ lập trình hoặc công cụ thực thi nó.
- Những gì mẫu bắt buộc phải chấp nhận và những gì bắt buộc phải từ chối: từ 4 đến 10 ví dụ thực tế cho mỗi loại, bao gồm cả các trường hợp biên (đầu vào rỗng, dấu cách, ký tự Unicode, chuỗi đầu vào rất dài).
- Cách thức sử dụng mẫu: xác thực toàn bộ giá trị (fullmatch), tìm kết quả khớp đầu tiên (search), trích xuất tất cả các kết quả khớp (findall), hay thay thế chuỗi (replace).
- Nguồn gốc của dữ liệu đầu vào. Đầu vào không đáng tin cậy (biểu mẫu web, nhật ký hệ thống, dữ liệu gửi qua API) sẽ biến hiện tượng quay lui thảm khốc (catastrophic backtracking) thành một lỗ hổng bảo mật thực sự.

### 2. Chạy trình kiểm tra
```bash
python3 scripts/regex_check.py 'PATTERN' -t 'sample one' -t 'sample two'
python3 scripts/regex_check.py 'PATTERN' --cases cases.txt --mode fullmatch --flags a
python3 scripts/regex_check.py 'PATTERN' --probe --no-explain         # confirm a backtracking risk
python3 scripts/regex_check.py 'PATTERN' --cases - --json < cases.txt
```
Một tệp chứa các trường hợp kiểm thử (cases file) gồm mỗi mẫu trên một dòng: `+ text` là bắt buộc phải khớp, `- text` là bắt buộc không được khớp, `# ...` là dòng ghi chú. Luôn đặt mẫu regex trong cặp dấu nháy đơn khi chạy trên shell. Trình kiểm tra sử dụng thư viện `re` của Python; đối với các biến thể (flavor) ngôn ngữ khác, hãy đọc `references/flavor-differences.md` và chuyển đổi kết quả tương ứng. Nếu bạn không thể chạy script, hãy tự giải thích mẫu thủ công theo cùng cấu trúc và áp dụng các quy tắc trong `references/regex-pitfalls.md`.

### 3. Giải thích, sau đó sửa lỗi
1. Giải thích mẫu regex từ trên xuống dưới bằng ngôn từ dễ hiểu, mỗi dòng giải thích một thành phần (token) hoặc một nhóm (group), sử dụng kết quả giải thích của trình kiểm tra làm nền tảng.
2. Hiển thị bảng kết quả kiểm thử: từng mẫu thử, kết quả kỳ vọng, kết quả thực tế, cùng đoạn văn bản hoặc các nhóm đã khớp.
3. Sửa lỗi theo thứ tự ưu tiên sau: các mẫu có nguy cơ bị treo (mức HIGH), các trường hợp chấp nhận sai và từ chối sai (không đạt kỳ vọng kiểm thử), sau đó mới đến độ rõ ràng và các ghi chú mức INFO.
4. Ưu tiên viết lại theo cách đơn giản, dễ đọc: dùng một vòng lặp thay vì các vòng lặp lồng nhau, dùng lớp ký tự phủ định như `[^,]*` thay vì `.*`, dùng ký tự neo tường minh `\A...\Z` hoặc `fullmatch`, và dùng cờ `re.VERBOSE` kèm chú thích cho các mẫu dài.
5. Chạy lại trình kiểm tra trên mẫu đã sửa với cùng bộ test cases cộng thêm các trường hợp biên mới cho đến khi mọi kỳ vọng đều vượt qua và không còn rủi ro mức HIGH nào. Hãy chạy `--probe` trên mẫu đã sửa nếu mẫu ban đầu có rủi ro mức HIGH.

### 4. Bàn giao kết quả
Điền vào biểu mẫu `templates/regex-review.md`: phần giải thích bằng ngôn ngữ tự nhiên, bảng kết quả kiểm thử trước và sau khi sửa, các rủi ro đã phát hiện, mẫu đã hiệu chỉnh viết đúng theo ngôn ngữ lập trình của người dùng với cách thoát ký tự (escaping) chính xác, cùng một danh sách ngắn các trường hợp kiểm thử để họ đưa vào bộ test (test suite) của dự án. Tuân thủ phong cách trình bày của `examples/example-email-validation-review.md`.

## Quy tắc
- Không bao giờ khẳng định một mẫu regex là đúng nếu chưa chứng minh nó trên các trường hợp kiểm thử; hãy chủ động bổ sung các trường hợp biên mà người dùng quên mất.
- Không xác thực quá mức cần thiết: đối với địa chỉ email, URL và tên người, hãy khuyến nghị kiểm tra định dạng khung đơn giản kết hợp với một bước xác minh thực tế (gửi email xác nhận, dùng bộ phân tích cú pháp URL chuyên dụng) thay vì cố viết một biểu thức regex khổng lồ.
- Tôn trọng biến thể regex (flavor) của người dùng: không đề xuất lookbehind, toán tử định lượng sở hữu (possessive quantifiers) hay nhóm nguyên tử (atomic groups) nếu engine của họ không hỗ trợ chúng.
- Thoát ký tự (escape) chính xác theo từng ngôn ngữ đích (chuỗi thô `r"..."` trong Python, dấu gạch chéo ngược kép `\\` trong chuỗi Java và C#, thoát dấu gạch chéo xuôi `/` trong biểu thức chính quy viết trực tiếp của JavaScript).
- Coi hiện tượng quay lui thảm khốc (catastrophic backtracking) trên dữ liệu đầu vào không đáng tin cậy là một lỗi bảo mật nghiêm trọng (ReDoS) và nêu rõ điều đó; luôn khuyến nghị giới hạn độ dài chuỗi đầu vào như một lớp phòng thủ thứ hai.
FILE:references/regex-pitfalls.md
# Các cạm bẫy Regex được kiểm tra bởi script

Mỗi quy tắc bao gồm: dấu hiệu được phát hiện, lý do tại sao nó quan trọng và mẫu an toàn để sử dụng thay thế. Các mức độ: HIGH có thể gây treo hệ thống hoặc bị khai thác tấn công, WARN thường chấp nhận hoặc từ chối sai dữ liệu đầu vào, INFO là thói quen viết chưa tốt nên được chỉnh sửa.

## Mức HIGH (quay lui thảm khốc - catastrophic backtracking, ReDoS)

### nested-quantifier (toán tử định lượng lồng nhau)
Phát hiện: một phép lặp nằm bên trong một phép lặp khác mà không có thành phần bắt buộc nào khác tách biệt ở vòng lặp ngoài, chẳng hạn như `(a+)+`, `(\w+\s?)+`, `([a-z0-9]+\.?)+`, `(\w+|\d+)+`.
Lý do: trên chuỗi đầu vào gần khớp (một chuỗi dài các chữ cái theo sau bởi một ký tự bị mẫu từ chối), bộ máy regex sẽ thử mọi cách có thể để chia chuỗi đó giữa vòng lặp trong và vòng lặp ngoài: có tới 2^n cách cho n ký tự. Chỉ cần 30 ký tự là có thể mất hàng phút để chạy xong. Trên một biểu mẫu web hoặc API, đây chính là lỗi gây từ chối dịch vụ (DoS).
Cách dùng thay thế: chỉ dùng một phép lặp mô tả toàn bộ chuỗi, với ký tự phân cách là bắt buộc trong mỗi nhịp lặp.
```
(\w+\s?)+$          ->  \w+(?:\s\w+)*$
([a-z0-9]+\.?)+@    ->  [a-z0-9]+(?:\.[a-z0-9]+)*@
```
Python 3.11+ cũng hỗ trợ nhóm nguyên tử (atomic groups) `(?>...)` và toán tử định lượng sở hữu (possessive quantifiers) `a++`, vốn không bao giờ nhả lại các ký tự mà chúng đã khớp.

### ambiguous-repeat (phép lặp nhập nhằng)
Phát hiện: một phép lặp mà phần thân có độ dài biến thiên và các thành phần bắt buộc của nó cũng có thể bị nuốt bởi chính phần biến thiên đó, chẳng hạn như `(x+x+)+`, `(aa|a)+`, `(foo|f\w+)+`.
Lý do: tương tự như trên, các vòng lặp có thể bị chia cắt theo rất nhiều cách khác nhau. Tốc độ tăng thời gian có thể chậm hơn đôi chút (tăng theo dãy Fibonacci thay vì 2^n) nhưng bản chất vẫn là hàm mũ.
Cách dùng thay thế: thiết kế để mỗi vòng lặp bắt đầu bằng một ký tự mà không phần nào khác trong thân vòng lặp có thể khớp được, hoặc gộp các phần đó lại thành một lớp ký tự duy nhất.

Xác nhận cả hai lỗi trên bằng tùy chọn `--probe`: script sẽ chạy mẫu regex trên chuỗi `c * n + tail` với n tăng từ 12 đến 28 trong một tiến trình con có giới hạn thời gian 2 giây và báo cáo tốc độ tăng thời gian xử lý. Nếu thời gian tăng gấp hơn 3 lần sau mỗi 4 ký tự thêm vào, hoặc bị quá thời gian chờ (timeout), rủi ro đó được xác nhận.

## Mức WARN

### nested-overlap (chồng lấn trong phép lặp)
Một phép lặp mà phần thân có ký tự phân cách vốn cũng được chấp nhận bởi phần lặp bên trong, chẳng hạn như `([\w.]+\.)+`. Hãy loại trừ ký tự phân cách ra khỏi lớp ký tự bên trong: `(?:[\w-]+\.)+`.

### adjacent-overlap (chồng lấn liền kề)
Hai phép lặp đơn ký tự không giới hạn nằm nối tiếp nhau và cùng chấp nhận một tập ký tự giống nhau, chẳng hạn như `\d+\d+` hoặc `.*.*`. Điểm phân tách giữa hai phần bị nhập nhằng và khi khớp thất bại, bộ máy sẽ phải quay lui theo hàm đa thức (n^2, n^3). Hãy giữ lại một phép lặp hoặc làm rõ ranh giới giữa chúng.

### range-A-z (dải ký tự A-z)
`[A-z]` bao phủ mã ASCII từ 65 đến 122, tức là bao gồm cả các ký tự `[ \ ] ^ _` và dấu huyền (backtick). Hãy sử dụng `[A-Za-z]`, hoặc `[^\W\d_]` nếu muốn khớp với bất kỳ chữ cái Unicode nào.

### unescaped-dot (dấu chấm chưa thoát ký tự)
Dấu `.` nằm giữa các chữ cái hoặc đứng trước một từ như `com` hay `json` sẽ khớp với bất kỳ ký tự nào, dẫn đến việc chuỗi `example-com` vẫn vượt qua được bước kiểm tra dành cho `example.com`. Hãy viết là `\.`.

### matches-empty (khớp cả chuỗi rỗng)
Mẫu regex chấp nhận cả chuỗi rỗng. Khi đó bước kiểm tra hợp lệ sẽ cho phép cả đầu vào bỏ trống, và hàm `findall` sẽ trả về các chuỗi rỗng xen kẽ giữa các kết quả khớp thực sự. Hãy sử dụng `+` thay vì `*` ở những nơi yêu cầu phải có ít nhất một ký tự.

## Mức INFO

### not-anchored (chưa gắn ký tự neo)
Lệnh `re.search(r"\d+", "abc123xyz")` vẫn trả về thành công. Để xác thực toàn bộ một giá trị, hãy sử dụng `re.fullmatch` hoặc gắn neo bằng `\A ... \Z`.

### dollar-newline (ký tự đô la và dòng mới)
`$` khớp tại vị trí cuối chuỗi và cả ngay trước ký tự xuống dòng cuối cùng, do đó `^abc$` vẫn chấp nhận `"abc\n"` khi dùng `re.search` hoặc `re.match`. Hãy sử dụng `\Z` hoặc `re.fullmatch` để kiểm tra nghiêm ngặt.

### unicode-classes (các lớp ký tự Unicode)
Trong Python 3, `\d` khớp với bất kỳ chữ số Unicode nào (ví dụ: các chữ số Ả Rập - Ấn Độ) và `\w` khớp với các chữ cái trong mọi hệ chữ viết. Điều này rất tốt cho tên người và văn bản tự do; nhưng đối với mã định danh (ID), số thẻ và mã code, hãy sử dụng cờ `re.ASCII` hoặc `[0-9]`.

### greedy-dotstar và many-dotstar (dấu chấm sao tham lam và quá nhiều dấu chấm sao)
`.*` nuốt toàn bộ phần còn lại rồi mới nhả ngược từng ký tự một. Việc xuất hiện từ ba cụm `.*` trở lên trong cùng một mẫu sẽ khiến các trường hợp khớp thất bại chạy rất chậm trên những dòng văn bản dài. Hãy mô tả từng trường bằng một lớp ký tự phủ định cụ thể: `"[^"]*"` cho chuỗi đặt trong dấu ngoặc kép, `[^,]*` cho một trường CSV.

## Những vấn đề không được script phát hiện (cần kiểm tra thủ công)

- Thoát ký tự (escaping) sai trong ngôn ngữ lập trình chủ (chuỗi Python thiếu tiền tố `r""`, chuỗi Java chỉ dùng một dấu gạch chéo ngược đơn).
- Các quy tắc nghiệp vụ mà biểu thức chính quy không thể diễn đạt tốt: ngày tháng hợp lệ (ngày 30 tháng 2), số kiểm tra checksum (số tài khoản IBAN, số thẻ ngân hàng), khả năng gửi nhận thực tế của địa chỉ email.
- Các vấn đề về ngôn ngữ bản địa (locale): khớp không phân biệt hoa thường đối với chữ i có chấm và không chấm trong tiếng Thổ Nhĩ Kỳ, hoặc chữ s sắc (ß) trong tiếng Đức.
- Hiệu năng của các phép lựa chọn (alternations) khổng lồ (hàng nghìn từ): hãy sử dụng tra cứu tập hợp (set lookup) hoặc cấu trúc cây tiền tố (trie) để thay thế.
FILE:references/flavor-differences.md
# Sự khác biệt giữa các biến thể (flavor) Regex có thể làm hỏng bản sửa lỗi

Trình kiểm tra chạy trên thư viện `re` của Python. Trước khi đem một mẫu regex sang chạy trên một bộ máy (engine) khác, hãy kiểm tra bảng đối chiếu dưới đây.

| Tính năng | Python `re` | JavaScript | PCRE / PHP | Go (RE2) | .NET | Java | POSIX grep -E |
|---|---|---|---|---|---|---|---|
| Lookahead `(?=...)` | có | có | có | không | có | có | không |
| Lookbehind `(?<=...)` | chỉ độ rộng cố định | có (ES2018) | cố định hoặc giới hạn | không | mọi độ rộng | giới hạn | không |
| Nhóm định danh (Named group) | chỉ `(?P<name>...)` | `(?<name>...)` | cả hai | `(?P<name>...)` (cũng hỗ trợ `(?<name>...)` từ Go 1.22+) | `(?<name>...)` | `(?<name>...)` | không |
| Nhóm nguyên tử (Atomic group) `(?>...)` | 3.11+ | không | có | không | có | có | không |
| Định lượng sở hữu (Possessive) `a++` | 3.11+ | không | có | không | không | có | không |
| Tham chiếu ngược (Backreference) `\1` | có | có | có | không | có | có | có (cơ bản) |
| `\d` khớp cả chữ số ngoài bảng ASCII | có (trừ khi bật re.ASCII) | không | không (trừ khi bật UCP) | không | có | không (trừ khi bật UNICODE_CHARACTER_CLASS) | tùy theo locale |
| `$` khớp trước ký tự xuống dòng cuối cùng | có | không (nếu không bật cờ m) | có | không | có | có | không áp dụng (xử lý theo dòng) |
| Cờ nội dòng (Inline flags) `(?i)` | có, ở đầu mẫu hoặc theo phạm vi `(?i:...)` | đặt cờ sau biểu thức (`/i`); cờ theo phạm vi `(?i:...)` chỉ có trên các engine mới nhất | có | có | có | có | không |
| Quay lui thảm khốc (Catastrophic backtracking) | có thể xảy ra | có thể xảy ra | có thể xảy ra (có giới hạn) | không thể xảy ra (thời gian tuyến tính) | có thể xảy ra (có tùy chọn timeout) | có thể xảy ra | phụ thuộc vào engine |
| Hàm hỗ trợ khớp toàn bộ chuỗi | `re.fullmatch` | gắn neo bằng `^...$` | gắn neo | gắn neo | gắn neo | `String.matches()` | cờ `-x` |

## Ghi chú chuyển đổi giữa các ngôn ngữ

- JavaScript: viết biểu thức trực tiếp dưới dạng `/pattern/flags`; thoát ký tự `/` bên trong biểu thức. Sử dụng cờ `u` cho Unicode và cờ `v` cho các phép toán tập hợp trên các engine đời mới. Ký tự `\Z` không tồn tại: hãy dùng `$` mà không bật cờ `m`.
- Go RE2: không hỗ trợ lookaround hay tham chiếu ngược (backreference), nhưng mọi mẫu đều chạy trong thời gian tuyến tính, nên không bao giờ phải lo ngại lỗi ReDoS tại đây. Hãy viết lại các điều kiện lookaround thành một bước kiểm tra thứ hai trong mã nguồn.
- Java và C#: mọi dấu gạch chéo ngược đều phải nhân đôi trong chuỗi thông thường (`"\\d+"`); chuỗi nguyên văn (verbatim string) `@"\d+"` của C# và khối văn bản (text blocks) của Java sẽ giúp viết gọn hơn.
- .NET: thiết lập `RegexOptions.NonBacktracking` (.NET 7+) hoặc đặt thời gian chờ (match timeout) đối với dữ liệu đầu vào không đáng tin cậy.
- SQL: toán tử `~` của PostgreSQL sử dụng regex nâng cao kiểu Tcl (hỗ trợ lookahead, lookbehind từ bản 9.6, không hỗ trợ nhóm định danh); MySQL 8 sử dụng bộ máy ICU. Hãy kiểm thử trực tiếp bên trong cơ sở dữ liệu.
- Shell: `grep -E` không hỗ trợ `\d` trên nhiều hệ thống; hãy dùng `[0-9]`. Lệnh `grep -P` cung cấp PCRE ở những môi trường có hỗ trợ.

## Các thiết lập mặc định an toàn xuyên suốt mọi biến thể (flavors)

- Dùng `[0-9]` thay cho `\d`, `[A-Za-z]` thay cho `\w` khi định dạng dữ liệu theo định nghĩa chỉ thuộc bảng mã ASCII.
- Dùng cặp ký tự neo `^` và `$` khi không bật chế độ đa dòng (multiline mode), kết hợp với một bước kiểm tra độ dài riêng biệt (ví dụ: tối đa 254 ký tự đối với địa chỉ email).
- Tuyệt đối không dùng toán tử định lượng lồng nhau (nested quantifiers) trên dữ liệu đầu vào không đáng tin cậy, bất kể sử dụng bộ máy (engine) nào.
FILE:templates/regex-review.md
# Đánh giá Regex: purpose

**Mẫu (nguyên bản):** `original_pattern`
**Bộ máy (Engine):** language_and_engine | **Cách sử dụng:** validate whole value / find first / find all / replace
**Trình kiểm tra (Checker):** Trước khi sửa có HIGH high / WARN warn / INFO info; đạt pass_before/total kỳ vọng kiểm thử

## Giải thích hoạt động bằng ngôn từ dễ hiểu

1. (mỗi dòng một thành phần token hoặc nhóm, từ trên xuống dưới)

## Kết quả kiểm thử

| # | Đầu vào | Kỳ vọng | Trước khi sửa | Sau khi sửa | Ghi chú |
|---|---|---|---|---|---|
| 1 | | khớp / không khớp | | | |

## Các rủi ro phát hiện được

| Mức độ | Quy tắc | Ý nghĩa trong trường hợp này | Cách khắc phục |
|---|---|---|---|
| HIGH | | | |

Kiểm tra thời gian chạy - Probe (nếu có chạy): đầu vào `probe_input`, probe_result

## Mẫu đã hiệu chỉnh

```language
(mẫu đã được hiệu chỉnh, thoát ký tự đúng theo ngôn ngữ của người dùng, kèm phiên bản VERBOSE hoặc có chú thích nếu mẫu dài hơn khoảng 40 ký tự)
```

Lý do cho từng thay đổi: (mỗi thay đổi một dòng)

## Giữ lại các trường hợp kiểm thử này

```
+ (bắt buộc phải khớp)
- (bắt buộc không được khớp)
```

## Ghi chú

- Giới hạn của biểu thức chính quy đối với tác vụ này và các bước kiểm tra bổ sung cần thực hiện trong mã nguồn (giới hạn độ dài, phân tích cú pháp, bước xác minh thực tế).
- Lưu ý về sự khác biệt biến thể (flavor) nếu mẫu regex này sẽ được chạy bên ngoài môi trường Python.
FILE:examples/example-email-validation-review.md
# Ví dụ: Đánh giá biểu thức kiểm tra email trên biểu mẫu đăng ký bị treo

## Yêu cầu của người dùng

"API đăng ký của chúng tôi thỉnh thoảng bị quá thời gian chờ (timeout) và công cụ đo hiệu năng (profiler) chỉ thẳng vào dòng mã này. Bạn có thể giải thích biểu thức regex này và sửa giúp tôi được không? Nó chạy trên Python 3.12 cho trường email của một biểu mẫu công khai."

```python
EMAIL = re.compile(r"^([a-zA-Z0-9]+\.?)+@[A-z]+.com$")
```

## Bộ trường hợp kiểm thử được sử dụng (cases.txt)

5 trường hợp của người dùng cộng thêm 3 trường hợp biên được bổ sung trong quá trình đánh giá (dấu gạch ngang thay vì dấu chấm, dấu gạch dưới trong tên miền, và chuỗi 30 chữ cái không có ký tự `@` - đúng theo hình dạng dữ liệu mà một con bot đã gửi lên):

```
+ ann@example.com
+ first.last@mail.example.co
- bob@@example.com
- no-at-sign.example.com
- trailing-dot@example.
- ann@example-com
- ann@exa_mple.com
- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!
```

## Chạy trình kiểm tra (trên mẫu nguyên bản)

```
$ python3 scripts/regex_check.py '^([a-zA-Z0-9]+\.?)+@[A-z]+.com$' --cases cases.txt --no-explain --probe
Samples (search):
  PASS     + 'ann@example.com'  -> [0:15] 'ann@example.com' 1='ann'
  FAIL     + 'first.last@mail.example.co'  -> expected a match
  PASS     - 'bob@@example.com'
  PASS     - 'no-at-sign.example.com'
  PASS     - 'trailing-dot@example.'
  FAIL     - 'ann@example-com'  -> [0:15] 'ann@example-com' 1='ann'  (expected no match)
  FAIL     - 'ann@exa_mple.com'  -> [0:16] 'ann@exa_mple.com' 1='ann'  (expected no match)
  TIMEOUT  - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'  -> still running after 2 s (catastrophic backtracking)

Risks and suggestions:
  HIGH nested-quantifier       a quantifier sits inside another quantifier and nothing else in the outer repeat is required ...
       probe 'a' * n + '!': n=12: 0.0002 s, n=16: 0.0031 s, n=20: 0.0593 s, n=24: 0.8419 s, n=28: timeout -> TIMEOUT at length 29: exponential backtracking confirmed
  WARN range-A-z               [A-z] also matches [ \ ] ^ _ and the backtick; use [A-Za-z]
  WARN unescaped-dot           '.' before 'com' matches any char (so a '-' or 'x' passes there too); write \. for a literal dot
  INFO dollar-newline          $ also matches before a trailing newline ...

Summary: HIGH 1  WARN 2  INFO 1 | expectations 4/8 pass | 1 sample(s) timed out
```

## Bản đánh giá (mẫu đã điền, rút gọn)

**Trình kiểm tra (Checker):** Trước khi sửa có HIGH 1 / WARN 2 / INFO 1; đạt 4/8 kỳ vọng (một mẫu bị timeout). Sau khi sửa: HIGH 0 / WARN 0, đạt 8/8 kỳ vọng.

Giải thích hoạt động của mẫu gốc: bắt đầu chuỗi; một hoặc nhiều chuỗi chữ cái hoặc chữ số, mỗi chuỗi tùy chọn theo sau bởi một dấu chấm; ký tự `@`; một hoặc nhiều ký tự từ A đến z (bao gồm cả `[ \ ] ^ _` và dấu huyền backtick); bất kỳ một ký tự đơn lẻ nào; cụm chữ `com`; kết thúc chuỗi hoặc ngay trước ký tự xuống dòng cuối cùng.

| # | Đầu vào | Kỳ vọng | Trước khi sửa | Sau khi sửa | Ghi chú |
|---|---|---|---|---|---|
| 1 | ann@example.com | khớp | khớp | khớp | |
| 2 | first.last@mail.example.co | khớp | không khớp | khớp | mẫu cũ chỉ cho phép `.com` và một nhãn tên miền duy nhất |
| 3 | bob@@example.com | không khớp | không khớp | không khớp | |
| 4 | no-at-sign.example.com | không khớp | không khớp | không khớp | |
| 5 | trailing-dot@example. | không khớp | không khớp | không khớp | |
| 6 | ann@example-com | không khớp | khớp | không khớp | bổ sung: dấu chấm chưa thoát ký tự đã chấp nhận cả dấu `-` |
| 7 | ann@exa_mple.com | không khớp | khớp | không khớp | bổ sung: `[A-z]` đã chấp nhận cả dấu `_` |
| 8 | 30 chữ "a" theo sau bởi "!" | không khớp, chạy nhanh | quá thời gian chờ sau 2 giây | không khớp, tức thì | bổ sung: tái hiện lỗi treo máy trên production |

Các rủi ro: nhóm `([a-zA-Z0-9]+\.?)+` cho phép một chuỗi các chữ cái bị chia cắt theo 2^n cách khi thiếu ký tự `@`, khiến một chuỗi rác dài 29 ký tự từ bot làm nghẽn cứng một tiến trình xử lý (worker) suốt hơn 2 giây; đây là lỗ hổng ReDoS trên một endpoint công khai.

Mẫu đã hiệu chỉnh:

```python
EMAIL = re.compile(r"""
    [A-Za-z0-9._%+-]+          # local part: one run, dots allowed but not repeated in a nested group
    @
    (?:[A-Za-z0-9-]+\.)+       # one or more domain labels, each ending in a literal dot
    [A-Za-z]{2,}               # top-level domain
""", re.VERBOSE | re.ASCII)

def looks_like_email(value: str) -> bool:
    return len(value) <= 254 and EMAIL.fullmatch(value) is not None
```

Lý do cho từng thay đổi: dùng một vòng lặp phẳng duy nhất cho phần tên hộp thư (local part) giúp loại bỏ toán tử định lượng lồng nhau; `\.` biến dấu chấm thành ký tự dấu chấm nghĩa đen; `[A-Za-z]` thay thế cho `[A-z]`; chấp nhận mọi tên miền cấp cao nhất (TLD) thay vì chỉ riêng `com`; dùng `fullmatch` thay cho `^...$`; giới hạn độ dài 254 ký tự đóng vai trò lớp phòng thủ thứ hai; cờ `re.ASCII` giữ cho các lớp ký tự `[...]` hoạt động nhất quán và dễ đoán.

Kiểm tra lại: chạy lệnh `python3 scripts/regex_check.py '[A-Za-z0-9._%+-]+@(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}' --cases cases.txt --mode fullmatch --probe` báo cáo không còn rủi ro mức HIGH hay WARN nào (nhóm lồng nhau hiện chỉ được báo là INFO nested-separated, tức cấu trúc có ký tự phân cách an toàn) và đạt trọn vẹn 8/8 kỳ vọng.

Giữ lại các trường hợp kiểm thử này: toàn bộ 8 dòng trong bảng trên, cộng thêm một chuỗi đầu vào dài 300 ký tự để kiểm tra giới hạn độ dài. Lưu ý rằng biểu thức chính quy chỉ kiểm tra hình dạng bề mặt của chuỗi; hãy xác nhận địa chỉ email thực sự bằng cách gửi email xác minh.
FILE:scripts/regex_check.py
#!/usr/bin/env python3
"""Explain a Python regular expression, test it on sample inputs, and warn about risky patterns.

Usage:
  python3 regex_check.py PATTERN [options]
  python3 regex_check.py PATTERN -t "text one" -t "text two"
  python3 regex_check.py PATTERN --cases cases.txt --mode fullmatch
  python3 regex_check.py PATTERN --cases - < cases.txt        (read cases from stdin)

Options:
  -t, --text TEXT     a sample input (repeatable)
  --cases FILE        one sample per line; "+ text" must match, "- text" must not match,
                      "# ..." is a comment, other lines are shown without an expectation
  --mode MODE         search (default), match, fullmatch, or findall
  --flags LETTERS     any of i m s x a (IGNORECASE, MULTILINE, DOTALL, VERBOSE, ASCII)
  --probe             time the pattern on generated worst-case inputs (subprocess, 2 s limit)
  --no-explain        skip the token-by-token explanation
  --json              print the result as JSON
  --fail-on LEVEL     exit 1 if any risk is at least LEVEL: high (default), warn, info, never
  --timeout S         stop matching one sample after S seconds and report TIMEOUT (default 2)

Risks: HIGH = nested quantifiers that can backtrack exponentially;
WARN = overlapping alternatives or adjacent quantifiers, [A-z], unescaped dots, empty matches;
INFO = anchoring, $ versus \\Z, Unicode \\d and \\w, greedy .*, flags that look missing.
Exit codes: 0 ok, 1 failed expectations or a risk at or above --fail-on, 2 usage or pattern error.
Python 3 standard library only (uses the re module's own parser).
"""
import json
import re
import signal
import string
import subprocess
import sys

try:  # Python 3.11+
    import re._parser as sre_parse
except ImportError:  # pragma: no cover - older Python
    import sre_parse  # type: ignore

LEVELS = {"info": 0, "warn": 1, "high": 2}
FLAG_LETTERS = {"i": re.IGNORECASE, "m": re.MULTILINE, "s": re.DOTALL, "x": re.VERBOSE, "a": re.ASCII}
FLAG_NAMES = [(re.IGNORECASE, "IGNORECASE"), (re.MULTILINE, "MULTILINE"), (re.DOTALL, "DOTALL"),
              (re.VERBOSE, "VERBOSE"), (re.ASCII, "ASCII")]
ALPHABET = string.ascii_letters + string.digits + string.punctuation + " \t\n" + "\u00e9\u0663"
CATEGORY_RE = {"CATEGORY_DIGIT": r"\d", "CATEGORY_NOT_DIGIT": r"\D", "CATEGORY_SPACE": r"\s",
               "CATEGORY_NOT_SPACE": r"\S", "CATEGORY_WORD": r"\w", "CATEGORY_NOT_WORD": r"\W"}
CATEGORY_TEXT = {"CATEGORY_DIGIT": "a digit", "CATEGORY_NOT_DIGIT": "a non-digit",
                 "CATEGORY_SPACE": "whitespace", "CATEGORY_NOT_SPACE": "a non-whitespace char",
                 "CATEGORY_WORD": "a word char (letter, digit, _)", "CATEGORY_NOT_WORD": "a non-word char"}
AT_TEXT = {"AT_BEGINNING": "^ start of string (start of each line with MULTILINE)",
           "AT_BEGINNING_STRING": "\\A start of string only",
           "AT_END": "$ end of string or before a final newline (end of each line with MULTILINE)",
           "AT_END_STRING": "\\Z end of string only",
           "AT_BOUNDARY": "\\b word boundary", "AT_NON_BOUNDARY": "\\B not a word boundary"}
MAXREPEAT = sre_parse.MAXREPEAT


class UsageError(Exception):
    pass


class MatchTimeout(Exception):
    pass


def with_timeout(seconds, fn, *args):
    """Run fn with a wall-clock limit (SIGALRM on Unix; no limit elsewhere)."""
    if not hasattr(signal, "setitimer") or seconds <= 0:
        return fn(*args)
    def handler(_sig, _frame):
        raise MatchTimeout()
    old = signal.signal(signal.SIGALRM, handler)
    signal.setitimer(signal.ITIMER_REAL, seconds)
    try:
        return fn(*args)
    finally:
        signal.setitimer(signal.ITIMER_REAL, 0)
        signal.signal(signal.SIGALRM, old)


def op_name(op):
    return str(op)


def show_char(code):
    ch = chr(code)
    named = {"\n": "\\n", "\t": "\\t", "\r": "\\r", " ": "space"}
    return named.get(ch, repr(ch) if ch.isprintable() else f"U+{code:04X}")


# ---------------------------------------------------------------- char sets

def class_accepts(items, flags):
    """Set of ALPHABET chars accepted by an IN [...] item list."""
    negate = bool(items) and op_name(items[0][0]) == "NEGATE"
    acc = set()
    for op, av in items:
        name = op_name(op)
        if name == "LITERAL":
            acc.add(chr(av))
        elif name == "RANGE":
            acc |= {c for c in ALPHABET if av[0] <= ord(c) <= av[1]}
        elif name == "CATEGORY":
            rx = re.compile(CATEGORY_RE.get(op_name(av), "."), flags & re.ASCII)
            acc |= {c for c in ALPHABET if rx.match(c)}
    if flags & re.IGNORECASE:
        acc |= {c.swapcase() for c in acc}
    return set(ALPHABET) - acc if negate else acc


def first_info(seq, flags):
    """Return (first_chars, can_be_empty) for a token sequence."""
    first, empty = set(), True
    for op, av in seq:
        f, e = token_first(op, av, flags)
        first |= f
        if not e:
            empty = False
            break
    return first, empty


def token_first(op, av, flags):
    name = op_name(op)
    if name == "LITERAL":
        c = chr(av)
        return ({c, c.swapcase()} if flags & re.IGNORECASE else {c}), False
    if name == "NOT_LITERAL":
        return set(ALPHABET) - {chr(av)}, False
    if name == "IN":
        return class_accepts(av, flags), False
    if name == "ANY":
        return (set(ALPHABET) if flags & re.DOTALL else set(ALPHABET) - {"\n"}), False
    if name in ("MAX_REPEAT", "MIN_REPEAT", "POSSESSIVE_REPEAT"):
        lo, _hi, sub = av
        f, e = first_info(sub, flags)
        return f, e or lo == 0
    if name == "SUBPATTERN":
        return first_info(av[3], flags_for(av, flags))
    if name == "ATOMIC_GROUP":
        return first_info(av, flags)
    if name == "BRANCH":
        first, empty = set(), False
        for b in av[1]:
            f, e = first_info(b, flags)
            first |= f
            empty = empty or e
        return first, empty
    if name == "GROUPREF_EXISTS":
        _g, yes, no = av
        f1, e1 = first_info(yes, flags)
        f2, e2 = first_info(no or [], flags)
        return f1 | f2, e1 or e2
    if name == "GROUPREF":
        return set(ALPHABET), True
    return set(), True  # AT, ASSERT, ASSERT_NOT: zero width


def flags_for(av, flags):
    add, delete = av[1], av[2]
    return (flags | add) & ~delete


# ---------------------------------------------------------------- explanation

def quant_text(lo, hi, name):
    mode = {"MAX_REPEAT": "greedy", "MIN_REPEAT": "lazy", "POSSESSIVE_REPEAT": "possessive"}[name]
    if hi == MAXREPEAT:
        base = {0: "zero or more", 1: "one or more"}.get(lo, f"{lo} or more")
    elif lo == hi:
        return f"exactly {lo}"
    elif (lo, hi) == (0, 1):
        base = "optional (zero or one)"
    else:
        base = f"{lo} to {hi}"
    return f"{base} ({mode})"


def class_text(items):
    negate = bool(items) and op_name(items[0][0]) == "NEGATE"
    parts = []
    for op, av in items:
        name = op_name(op)
        if name == "LITERAL":
            parts.append(show_char(av))
        elif name == "RANGE":
            parts.append(f"{show_char(av[0])}-{show_char(av[1])}")
        elif name == "CATEGORY":
            parts.append(CATEGORY_TEXT.get(op_name(av), op_name(av)))
    return ("any char except " if negate else "one of: ") + ", ".join(parts)


def explain(seq, group_names, flags, depth=0, out=None):
    out = [] if out is None else out
    pad = "  " * depth
    i = 0
    while i < len(seq):
        op, av = seq[i]
        name = op_name(op)
        if name == "LITERAL":  # merge runs of literals
            j = i
            while j < len(seq) and op_name(seq[j][0]) == "LITERAL":
                j += 1
            text = "".join(chr(a) for _, a in seq[i:j])
            out.append(f"{pad}the text {text!r}" if j - i > 1 else f"{pad}the char {show_char(av)}")
            i = j
            continue
        if name == "NOT_LITERAL":
            out.append(f"{pad}any char except {show_char(av)}")
        elif name == "IN":
            out.append(f"{pad}{class_text(av)}")
        elif name == "ANY":
            out.append(f"{pad}any char" + ("" if flags & re.DOTALL else " except newline"))
        elif name == "AT":
            out.append(f"{pad}{AT_TEXT.get(op_name(av), op_name(av))}")
        elif name in ("MAX_REPEAT", "MIN_REPEAT", "POSSESSIVE_REPEAT"):
            lo, hi, sub = av
            out.append(f"{pad}{quant_text(lo, hi, name)} of:")
            explain(sub, group_names, flags, depth + 1, out)
        elif name == "SUBPATTERN":
            g = av[0]
            if g is None:
                out.append(f"{pad}non-capturing group:")
            else:
                label = f' "{group_names[g]}"' if g in group_names else ""
                out.append(f"{pad}capturing group {g}{label}:")
            explain(av[3], group_names, flags_for(av, flags), depth + 1, out)
        elif name == "ATOMIC_GROUP":
            out.append(f"{pad}atomic group (no backtracking into it):")
            explain(av, group_names, flags, depth + 1, out)
        elif name == "BRANCH":
            out.append(f"{pad}either:")
            for k, b in enumerate(av[1]):
                out.append(f"{pad}  option {k + 1}:")
                explain(b, group_names, flags, depth + 2, out)
        elif name in ("ASSERT", "ASSERT_NOT"):
            direction, sub = av
            kind = ("lookbehind" if direction < 0 else "lookahead")
            neg = "negative " if name == "ASSERT_NOT" else ""
            out.append(f"{pad}{neg}{kind} (checks without consuming):")
            explain(sub, group_names, flags, depth + 1, out)
        elif name == "GROUPREF":
            out.append(f"{pad}the same text as group {av} matched")
        elif name == "GROUPREF_EXISTS":
            out.append(f"{pad}if group {av[0]} matched:")
            explain(av[1], group_names, flags, depth + 1, out)
            if av[2]:
                out.append(f"{pad}otherwise:")
                explain(av[2], group_names, flags, depth + 1, out)
        else:
            out.append(f"{pad}{name}")
        i += 1
    return out


# ---------------------------------------------------------------- risk analysis

def walk_repeats(seq, flags, path=""):
    """Yield (op, av, flags) for every repeat node, outside lookarounds too."""
    for op, av in seq:
        name = op_name(op)
        if name in ("MAX_REPEAT", "MIN_REPEAT", "POSSESSIVE_REPEAT"):
            yield op, av, flags
            yield from walk_repeats(av[2], flags)
        elif name == "SUBPATTERN":
            yield from walk_repeats(av[3], flags_for(av, flags))
        elif name == "ATOMIC_GROUP":
            yield from walk_repeats(av, flags)
        elif name == "BRANCH":
            for b in av[1]:
                yield from walk_repeats(b, flags)
        elif name in ("ASSERT", "ASSERT_NOT"):
            yield from walk_repeats(av[1], flags)


def flatten(seq):
    """Expand groups so the body of a repeat can be read as one token list."""
    out = []
    for op, av in seq:
        name = op_name(op)
        if name == "SUBPATTERN" and av[1] == 0 and av[2] == 0:
            out.extend(flatten(av[3]))
        else:
            out.append((op, av))
    return out


def token_width(tok, parsed):
    return sre_parse.SubPattern(parsed.state, [tok]).getwidth()


def all_chars(seq, flags):
    """Every ALPHABET char a token sequence can consume anywhere (not only first)."""
    acc = set()
    for op, av in seq:
        name = op_name(op)
        if name in ("LITERAL", "NOT_LITERAL", "IN", "ANY"):
            acc |= token_first(op, av, flags)[0]
        elif name in ("MAX_REPEAT", "MIN_REPEAT", "POSSESSIVE_REPEAT"):
            acc |= all_chars(av[2], flags)
        elif name == "SUBPATTERN":
            acc |= all_chars(av[3], flags_for(av, flags))
        elif name == "ATOMIC_GROUP":
            acc |= all_chars(av, flags)
        elif name == "BRANCH":
            for b in av[1]:
                acc |= all_chars(b, flags)
        elif name == "GROUPREF_EXISTS":
            acc |= all_chars(av[1], flags) | all_chars(av[2] or [], flags)
        elif name == "GROUPREF":
            acc |= set(ALPHABET)
    return acc


def has_unbounded(tok):
    op, av = tok
    name = op_name(op)
    if name in ("MAX_REPEAT", "MIN_REPEAT"):
        return av[1] == MAXREPEAT or has_unbounded_seq(av[2])
    if name == "SUBPATTERN":
        return has_unbounded_seq(av[3])
    if name == "BRANCH":
        return any(has_unbounded_seq(b) for b in av[1])
    return False


def has_unbounded_seq(seq):
    return any(has_unbounded(t) for t in seq)


def sample_char(chars):
    for pool in (string.ascii_lowercase, string.digits, string.ascii_uppercase, ALPHABET):
        for c in pool:
            if c in chars:
                return c
    return None


def analyze(parsed, flags, pattern, modes_seen):
    risks = []

    def add(level, rule, msg, probe=None):
        risks.append({"level": level, "rule": rule, "message": msg, **({"probe": probe} if probe else {})})

    # nested quantifiers: a repeat whose body can match the same text in more than one way
    for op, av, fl in walk_repeats(list(parsed), flags):
        if op_name(op) == "POSSESSIVE_REPEAT" or av[1] <= 1:
            continue
        body = flatten(av[2])
        widths = [token_width(t, parsed) for t in body]
        if sum(w[0] for w in widths) == sum(w[1] for w in widths):
            continue  # fixed-width body such as (\d{4}-)+ cannot be split two ways
        variable = [t for t, w in zip(body, widths) if w[0] != w[1]]
        fixed = [t for t, w in zip(body, widths) if w[0] == w[1] and w[0] > 0]
        var_chars = set().union(*(all_chars([t], fl) for t in variable))
        has_inner_loop = any(has_unbounded(t) for t in variable)
        overlap = set().union(*(all_chars([t], fl) for t in fixed)) & var_chars if fixed else set()
        sample = sample_char(overlap) or sample_char(var_chars)
        if not fixed and has_inner_loop:
            add("high", "nested-quantifier",
                "a quantifier sits inside another quantifier and nothing else in the outer repeat is "
                "required (the classic (a+)+ or (\\w+\\s?)+ shape), so one string can be split in "
                "exponentially many ways; rewrite as a single repeat, make the separator required, or use an "
                "atomic group or possessive quantifier (Python 3.11+)", probe=sample)
        elif any(not (all_chars([t], fl) & var_chars) for t in fixed):
            if has_inner_loop:
                add("info", "nested-separated",
                    "nested quantifier with a required separator that the inner repeat cannot match "
                    "(like ([\\w-]+\\.)+); this shape is safe")
        elif all(all_chars([t], fl) <= var_chars for t in fixed):
            add("high", "ambiguous-repeat",
                "the body of a repeat has variable length and every required part can also be eaten by "
                "the variable part (for example (x+x+)+, (aa|a)+, or (\\w+|\\d+)+), so the iterations "
                "can be split in exponentially many ways; merge the parts into one class or make each "
                "iteration start with a char the rest cannot match", probe=sample)
        else:
            add("warn", "nested-overlap",
                "a repeat with a variable-length body whose separator is also accepted by the inner part "
                "(for example ([\\w.]+\\.)+): long non-matching input can slow down sharply; make the inner "
                "class exclude the separator", probe=sample)

    # adjacent overlapping unbounded repeats in any sequence
    seen_pairs = set()

    def single_char_repeat(op, av):
        return (op_name(op) in ("MAX_REPEAT", "MIN_REPEAT") and av[1] == MAXREPEAT and len(av[2]) == 1
                and op_name(av[2][0][0]) in ("LITERAL", "NOT_LITERAL", "IN", "ANY"))

    def scan_adjacent(seq, fl):
        toks = [t for t in flatten(seq) if op_name(t[0]) not in ("AT", "ASSERT", "ASSERT_NOT")]
        for (a_op, a_av), (b_op, b_av) in zip(toks, toks[1:]):
            if single_char_repeat(a_op, a_av) and single_char_repeat(b_op, b_av):
                if (id(a_av), id(b_av)) in seen_pairs:
                    continue
                seen_pairs.add((id(a_av), id(b_av)))
                fa, _ = first_info(a_av[2], fl)
                fb, _ = first_info(b_av[2], fl)
                if fa & fb:
                    add("warn", "adjacent-overlap",
                        "two unbounded quantifiers in a row accept the same chars (for example \\d+\\d+ or "
                        ".*.*), so the split between them is ambiguous and failures backtrack polynomially; "
                        "keep one of them")
        for op, av in seq:
            name = op_name(op)
            if name == "SUBPATTERN":
                scan_adjacent(av[3], flags_for(av, fl))
            elif name in ("MAX_REPEAT", "MIN_REPEAT", "POSSESSIVE_REPEAT"):
                scan_adjacent(av[2], fl)
            elif name == "BRANCH":
                for b in av[1]:
                    scan_adjacent(b, fl)

    scan_adjacent(list(parsed), flags)

    # text-level checks
    if re.search(r"\[[^\]]*A-z", pattern):
        add("warn", "range-A-z", "[A-z] also matches [ \\ ] ^ _ and the backtick; use [A-Za-z]")
    seq = list(parsed)
    flat = flatten(seq)
    for k, (op, _av) in enumerate(flat):
        if op_name(op) != "ANY":
            continue
        before = flat[k - 1] if k else None
        after = [chr(a) for o, a in flat[k + 1:k + 4] if op_name(o) == "LITERAL"]
        lit_before = before is not None and op_name(before[0]) == "LITERAL" and chr(before[1]).isalnum()
        word_after = len(after) >= 2 and all(c.isalpha() for c in after[:2])
        if (lit_before and after and after[0].isalnum()) or word_after:
            add("warn", "unescaped-dot",
                f"'.' before {''.join(after) or 'the next token'!r} matches any char (so a '-' or 'x' passes there too); "
                "write \\. for a literal dot")
            break
    try:
        if re.compile(pattern, flags).fullmatch(""):
            add("warn", "matches-empty",
                "the pattern matches the empty string, so findall/finditer return empty matches and "
                "validation accepts blank input; require at least one char where it matters")
    except re.error:
        pass
    count_dotstar = len(re.findall(r"(?<!\\)\.\*", pattern))
    if count_dotstar >= 3:
        add("info", "many-dotstar", f"{count_dotstar} '.*' in one pattern: failures are slow on long lines; "
            "use negated classes like [^,]* to describe each field")
    elif count_dotstar and not re.search(r"\.\*\?", pattern):
        add("info", "greedy-dotstar", "greedy .* grabs as much as possible and then backs off; use .*? or a "
            "negated class such as [^\"]* when you mean 'up to the next ...'")
    first_ops = [op_name(op) for op, _ in seq[:1]]
    last = seq[-1] if seq else None
    anchored_start = first_ops == ["AT"]
    anchored_end = last is not None and op_name(last[0]) == "AT" and op_name(last[1]) in ("AT_END", "AT_END_STRING")
    if "fullmatch" not in modes_seen and not (anchored_start and anchored_end) and modes_seen != {"findall"}:
        add("info", "not-anchored", "for validation, anchor both ends (\\A ... \\Z) or use re.fullmatch; "
            "otherwise 'abc123xyz' passes a search for \\d+")
    if anchored_end and op_name(last[1]) == "AT_END" and not flags & re.MULTILINE and "fullmatch" not in modes_seen:
        add("info", "dollar-newline", "$ also matches before a trailing newline ('abc\\n' passes ^abc$); "
            "use \\Z or fullmatch for strict validation")
    if re.search(r"(?<!\\)\\[dw]", pattern) and not flags & re.ASCII:
        add("info", "unicode-classes", "\\d and \\w match Unicode digits and letters too (for example "
            "Arabic-Indic digits); add re.ASCII or use [0-9] / [A-Za-z0-9_] for strict input formats")
    return risks


# ---------------------------------------------------------------- probe

PROBE_CODE = r"""
import re, sys, time
pat, flags, s = sys.argv[1], int(sys.argv[2]), sys.argv[3]
rx = re.compile(pat, flags)
t = time.perf_counter(); rx.search(s); print(f"{time.perf_counter() - t:.6f}")
"""


def run_probe(pattern, flags, ch, compiled):
    tail = next((t for t in "!#\x00~ \n" if not compiled.search(ch * 3 + t)), "!")
    results, verdict = [], "no slowdown seen"
    for n in (12, 16, 20, 24, 28):
        s = ch * n + tail
        try:
            r = subprocess.run([sys.executable, "-c", PROBE_CODE, pattern, str(flags), s],
                               capture_output=True, text=True, timeout=2)
            secs = float(r.stdout.strip() or "nan")
        except subprocess.TimeoutExpired:
            results.append({"n": n, "seconds": None})
            verdict = f"TIMEOUT at length {n + 1}: exponential backtracking confirmed"
            break
        results.append({"n": n, "seconds": round(secs, 4)})
        prev = results[-2]["seconds"] if len(results) >= 2 else None
        if prev and secs > 0.01 and secs / prev > 3:
            verdict = (f"time grows about {secs / prev:.0f}x per +4 chars: exponential backtracking confirmed")
        elif secs > 0.5:
            verdict = f"slow ({secs:.2f} s at length {n + 1}) but not clearly exponential"
    return {"input": f"{ch!r} * n + {tail!r}", "timings": results, "verdict": verdict}


# ---------------------------------------------------------------- matching

def read_cases(path):
    text = sys.stdin.read() if path == "-" else open(path, encoding="utf-8").read()
    cases = []
    for raw in text.splitlines():
        if not raw.strip() or raw.lstrip().startswith("#"):
            continue
        if raw.startswith("+ ") or raw.startswith("- "):
            cases.append({"text": raw[2:], "expect": raw[0] == "+"})
        else:
            cases.append({"text": raw, "expect": None})
    return cases


def match_case(rx, text, mode):
    fn = {"search": rx.search, "match": rx.match, "fullmatch": rx.fullmatch}.get(mode)
    names = {v: k for k, v in rx.groupindex.items()}
    def groups(m):
        out = []
        for g in range(1, rx.groups + 1):
            out.append({"group": g, "name": names.get(g), "value": m.group(g),
                        "span": list(m.span(g)) if m.group(g) is not None else None})
        return out
    if mode == "findall":
        ms = list(rx.finditer(text))
        return {"matched": bool(ms), "matches": [{"text": m.group(0), "span": list(m.span()), "groups": groups(m)}
                                                for m in ms]}
    m = fn(text)
    if not m:
        return {"matched": False, "matches": []}
    return {"matched": True, "matches": [{"text": m.group(0), "span": list(m.span()), "groups": groups(m)}]}


def describe_flags(flags):
    names = [n for f, n in FLAG_NAMES if flags & f]
    return ", ".join(names) if names else "none"


# ---------------------------------------------------------------- main

def parse_args(argv):
    opts = {"texts": [], "cases": None, "mode": "search", "flags": 0, "probe": False, "explain": True,
            "json": False, "fail_on": "high", "pattern": None, "timeout": 2.0}
    i = 0
    while i < len(argv):
        a = argv[i]
        def val():
            if i + 1 >= len(argv):
                raise UsageError(f"{a} needs a value")
            return argv[i + 1]
        if a in ("-t", "--text"):
            opts["texts"].append(val()); i += 2
        elif a == "--cases":
            opts["cases"] = val(); i += 2
        elif a == "--mode":
            opts["mode"] = val(); i += 2
            if opts["mode"] not in ("search", "match", "fullmatch", "findall"):
                raise UsageError("--mode must be search, match, fullmatch, or findall")
        elif a == "--flags":
            letters = val(); i += 2
            bad = [c for c in letters.lower() if c not in FLAG_LETTERS]
            if bad:
                raise UsageError(f"unknown flag letter(s) {''.join(bad)!r}; use any of i m s x a")
            for c in letters.lower():
                opts["flags"] |= FLAG_LETTERS[c]
        elif a == "--fail-on":
            opts["fail_on"] = val().lower(); i += 2
            if opts["fail_on"] not in ("high", "warn", "info", "never"):
                raise UsageError("--fail-on must be high, warn, info, or never")
        elif a == "--timeout":
            try:
                opts["timeout"] = float(val())
            except ValueError:
                raise UsageError("--timeout needs a number of seconds, for example --timeout 2")
            i += 2
        elif a == "--probe":
            opts["probe"] = True; i += 1
        elif a == "--no-explain":
            opts["explain"] = False; i += 1
        elif a == "--json":
            opts["json"] = True; i += 1
        elif a in ("-h", "--help"):
            print(__doc__); sys.exit(0)
        elif a.startswith("--") and opts["pattern"] is not None:
            raise UsageError(f"unknown option {a}")
        elif opts["pattern"] is None:
            opts["pattern"] = a; i += 1
        else:
            raise UsageError(f"unexpected argument {a!r} (quote the pattern; use -t for sample inputs)")
    if opts["pattern"] is None:
        raise UsageError("give a PATTERN (quote it in single quotes in the shell)")
    return opts


def main(argv):
    try:
        opts = parse_args(argv)
        cases = [{"text": t, "expect": None} for t in opts["texts"]]
        if opts["cases"]:
            try:
                cases += read_cases(opts["cases"])
            except OSError as e:
                raise UsageError(f"cannot read cases file: {e.strerror}: {opts['cases']}")
    except UsageError as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    pattern, flags, mode = opts["pattern"], opts["flags"], opts["mode"]
    try:
        rx = re.compile(pattern, flags)
        parsed = sre_parse.parse(pattern, flags)
    except re.error as e:
        pos = f" at position {e.pos}" if e.pos is not None else ""
        print(f"error: pattern does not compile: {e.msg}{pos}", file=sys.stderr)
        if e.pos is not None:
            print(f"  {pattern}\n  {' ' * e.pos}^", file=sys.stderr)
        return 2
    flags = int(rx.flags) & ~int(re.UNICODE)
    names = {v: k for k, v in rx.groupindex.items()}
    risks = analyze(parsed, flags, pattern, {mode})
    if opts["probe"]:
        for r in risks:
            if r.get("probe"):
                r["probe_result"] = run_probe(pattern, flags, r["probe"], rx)
    results = []
    for c in cases:
        try:
            res = with_timeout(opts["timeout"], match_case, rx, c["text"], mode)
            status = None if c["expect"] is None else ("PASS" if res["matched"] == c["expect"] else "FAIL")
        except MatchTimeout:
            res, status = {"matched": None, "matches": []}, "TIMEOUT"
        results.append({**c, **res, "status": status})
    fails = sum(1 for r in results if r["status"] in ("FAIL", "TIMEOUT"))
    counts = {lvl: sum(1 for r in risks if r["level"] == lvl) for lvl in ("high", "warn", "info")}
    report = {"pattern": pattern, "flags": describe_flags(flags), "mode": mode, "groups": rx.groups,
              "group_names": {str(k): v for k, v in names.items()},
              "explanation": explain(list(parsed), names, flags) if opts["explain"] else [],
              "cases": results, "failed_expectations": fails, "risks": risks, "risk_counts": counts}
    threshold = LEVELS.get(opts["fail_on"], 99)
    risky = any(LEVELS[r["level"]] >= threshold for r in risks)
    if opts["json"]:
        print(json.dumps(report, indent=2, ensure_ascii=False))
        return 1 if fails or risky else 0

    print(f"Pattern: {pattern}")
    named = ", ".join(f'{g} "{n}"' for g, n in sorted(names.items()))
    print(f"Flags: {describe_flags(flags)} | mode: {mode} | groups: {rx.groups}" + (f" ({named})" if named else ""))
    if opts["explain"]:
        print("\nExplanation:")
        for line in report["explanation"]:
            print("  " + line)
    if results:
        print(f"\nSamples ({mode}):")
        for r in results:
            mark = {True: "+", False: "-", None: " "}[r["expect"]]
            tag = r["status"] or ("match" if r["matched"] else "no match")
            line = f"  {tag:<8} {mark} {r['text']!r}"
            if r["matched"]:
                parts = []
                for m in r["matches"][:5]:
                    g = " ".join(f"{x['name'] or x['group']}={x['value']!r}" for x in m["groups"])
                    parts.append(f"[{m['span'][0]}:{m['span'][1]}] {m['text']!r}" + (f" {g}" if g else ""))
                more = f" (+{len(r['matches']) - 5} more)" if len(r["matches"]) > 5 else ""
                line += "  -> " + "; ".join(parts) + more
            elif r["status"] == "TIMEOUT":
                line += f"  -> still running after {opts['timeout']:g} s (catastrophic backtracking)"
            elif r["status"] == "FAIL":
                line += "  -> expected a match"
            if r["status"] == "FAIL" and r["matched"]:
                line += "  (expected no match)"
            print(line)
    print("\nRisks and suggestions:")
    if not risks:
        print("  none found")
    for r in risks:
        print(f"  {r['level'].upper():<4} {r['rule']:<23} {r['message']}")
        pr = r.get("probe_result")
        if pr:
            times = ", ".join(f"n={t['n']}: {'timeout' if t['seconds'] is None else str(t['seconds']) + ' s'}"
                              for t in pr["timings"])
            print(f"       probe {pr['input']}: {times} -> {pr['verdict']}")
    summary = f"\nSummary: HIGH {counts['high']}  WARN {counts['warn']}  INFO {counts['info']}"
    if any(r["expect"] is not None for r in results):
        n = sum(1 for r in results if r["expect"] is not None)
        summary += f" | expectations {n - fails}/{n} pass"
    timeouts = sum(1 for r in results if r["status"] == "TIMEOUT")
    if timeouts:
        summary += f" | {timeouts} sample(s) timed out"
    print(summary)
    return 1 if fails or risky else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
SecurityPythonDebugging+2
F@f
0
Trình Xây Dựng Kế Hoạch Kiểm Thử Tải (YAML)
Văn bản

Thiết kế một bài kiểm thử tải an toàn, có thể lặp lại dưới định dạng YAML: phạm vi và các phụ thuộc được giả lập (mock), khoảng cách khác biệt với môi trường thực tế, hành trình người dùng có trọng số kèm thời gian suy nghĩ, các kịch bản từ kiểm thử khói (smoke) đến kiểm thử ngâm (soak) với lộ trình tăng tải, ngưỡng đạt/trượt bằng số cho p95/p99 và tỷ lệ lỗi, bảng điều khiển giám sát, điều kiện hủy bỏ kèm người có quyền dừng, lịch trình và báo cáo.

1role: Kỹ sư hiệu năng chuyên thiết kế các bài kiểm thử tải (load test) an toàn và sát với thực tế
2task: >
3 Chuyển đổi một đoạn mô tả ngắn về dịch vụ web, API hoặc tiến trình chạy ngầm (background worker) thành một
4 kế hoạch kiểm thử tải hoàn chỉnh bằng định dạng YAML để nhóm có thể xem xét, chạy thử và lặp lại. Kế hoạch phải bảo vệ
5 môi trường thực tế (production), mô hình hóa lưu lượng truy cập thực và xác định rõ các tiêu chí đạt/trượt trước khi chạy bất kỳ bài kiểm thử nào.
6
7ask_first:
8 - Đối tượng đang được kiểm thử là gì (tên dịch vụ, các điểm cuối endpoint hoặc tác vụ ngầm, các thành phần phụ thuộc như cơ sở dữ liệu, bộ nhớ đệm cache, hàng đợi queue, API bên thứ ba)
9 - Hình thái lưu lượng truy cập hiện tại (số yêu cầu trung bình và cao điểm mỗi giây, quy luật biến thiên trong ngày, tỷ lệ đọc/ghi, kích thước gói dữ liệu payload)
10 - Mục tiêu kinh doanh của bài kiểm thử (sẵn sàng ra mắt sản phẩm, đánh giá sức chứa cho một sự kiện, kiểm tra hồi quy hiệu năng, tối ưu hóa chi phí)
...+78 dòng nữa
DevOpsYAMLPerformance+2
F@f
0
Trình Phát Hiện Kiểm Thử Chập Chờn (Flaky Test)
Kỹ năng

Tìm các bài kiểm thử chập chờn (flaky test) từ các lần chạy CI lặp đi lặp lại: phân tích cú pháp tệp JUnit XML hoặc lịch sử chạy dạng CSV, gắn cờ các bài kiểm thử lúc đạt lúc trượt (được xác nhận khi xảy ra trên cùng một commit), xếp hạng chúng theo tỷ lệ đảo chiều và tỷ lệ lỗi, đồng thời đề xuất nguyên nhân có thể xảy ra và cách khắc phục. Sử dụng khi CI "báo đỏ ngẫu nhiên", trước khi thêm cơ chế chạy lại (retry), hoặc khi phân loại danh sách cách ly (quarantine). Bao gồm công cụ phát hiện bằng thư viện chuẩn Python với cờ --self-test.

---
name: flaky-test-detector
description: Tìm các bài kiểm thử chập chờn (flaky test) từ các lần chạy CI lặp đi lặp lại: phân tích cú pháp tệp JUnit XML hoặc lịch sử chạy dạng CSV, gắn cờ các bài kiểm thử lúc đạt lúc trượt (được xác nhận khi xảy ra trên cùng một commit), xếp hạng chúng theo tỷ lệ đảo chiều và tỷ lệ lỗi, đồng thời đề xuất nguyên nhân có thể xảy ra và cách khắc phục. Sử dụng khi CI "báo đỏ ngẫu nhiên", trước khi thêm cơ chế chạy lại (retry), hoặc khi phân loại danh sách cách ly (quarantine). Bao gồm công cụ phát hiện bằng thư viện chuẩn Python với cờ --self-test.
---

# Trình Phát Hiện Kiểm Thử Chập Chờn (Flaky Test)

Bạn giúp các nhóm kỹ thuật tách biệt những bài kiểm thử thực sự bị hỏng với những bài kiểm thử chập chờn (flaky). Bạn đọc kết quả từ nhiều lần chạy CI, tìm ra các bài kiểm thử có kết quả thay đổi dù không có sự thay đổi nào về mã nguồn, xếp hạng chúng theo mức độ phiền toái gây ra, và đề xuất cách khắc phục cụ thể hoặc kế hoạch cách ly (quarantine) cho từng bài. Bạn không bao giờ "sửa" tính chập chờn chỉ bằng cách thêm cơ chế chạy lại (retry), và không bao giờ kết luận một bài kiểm thử là chập chờn nếu không có bằng chứng từ ít nhất hai lần chạy có kết quả trái ngược nhau.

## Các tệp trong kỹ năng này

- `scripts/flaky_tests.py` - phân tích cú pháp các tệp JUnit XML (mỗi tệp tương ứng một lần chạy) hoặc lịch sử CSV và in ra bảng xếp hạng mức độ chập chờn hoặc định dạng JSON (chỉ dùng thư viện chuẩn Python 3)
- `references/flakiness-causes.md` - các nguyên nhân gốc rễ phổ biến kèm theo triệu chứng và cách khắc phục
- `references/junit-and-history-formats.md` - cách xuất tệp JUnit XML và lịch sử CSV từ các trình chạy kiểm thử (test runner) và hệ thống CI phổ biến
- `templates/flaky-report.md` - mẫu báo cáo dùng cho bình luận PR, issue hoặc buổi đánh giá sức khỏe CI hàng tuần
- `examples/test-history.csv` - lịch sử chạy mẫu có chứa một lỗi chập chờn trên cùng một commit

## Quy trình làm việc

### 1. Thu thập bằng chứng
Yêu cầu cung cấp, hoặc hỗ trợ tạo ra:
- Tệp JUnit XML từ ít nhất 3 lần chạy của cùng một bộ kiểm thử (từ các gói dữ liệu lưu trữ CI artifact, hoặc chạy vòng lặp cục bộ chẳng hạn như chạy bộ kiểm thử 10 lần)
- Hoặc một tệp CSV có các cột `run_id,test,status` và lý tưởng nhất là có thêm `commit` cùng `duration_s`
- Ngữ cảnh: trình chạy kiểm thử (pytest, Jest, JUnit, Go test, RSpec), mức độ chạy song song, cơ chế chạy lại (retries) đã bật sẵn, các dịch vụ dùng chung (cơ sở dữ liệu, trình duyệt, mạng)

Nếu chỉ có dữ liệu của một lần chạy duy nhất, hãy giải thích rằng không thể chứng minh được tính chập chờn và gợi ý chạy một vòng lặp kiểm thử lại trên một commit không thay đổi.

### 2. Chạy công cụ phát hiện
```
python3 scripts/flaky_tests.py run1.xml run2.xml run3.xml
python3 scripts/flaky_tests.py --csv examples/test-history.csv --only-flaky
python3 scripts/flaky_tests.py --csv history.csv --json
python3 scripts/flaky_tests.py --self-test
```
Mã thoát 1 có nghĩa là đã tìm thấy ít nhất một bài kiểm thử chập chờn (rất tiện lợi khi dùng làm cổng kiểm tra sức khỏe CI); mã thoát 2 có nghĩa là dữ liệu đầu vào không hợp lệ.

### 3. Phân loại
- **flaky_confirmed**: vừa đạt vừa trượt trên cùng một commit. Bằng chứng mạnh mẽ nhất; cần ưu tiên sửa trước.
- **flaky**: kết quả lẫn lộn giữa các lần chạy, không rõ commit hoặc khác commit. Hãy kiểm tra xem mã nguồn có thay đổi giữa các lần chạy hay không.
- **always_failing**: lỗi thực sự, không phải chập chờn. Chuyển cho người phụ trách xử lý như một lỗi phần mềm (bug).
- **stable** / **insufficient_data**: không cần hành động gì, hoặc thu thập thêm các lần chạy.

Sử dụng kết hợp `flip_rate` (tần suất kết quả thay đổi giữa các lần chạy liên tiếp) và `fail_rate`. Chỉ số `duration_spread` cao gợi ý về các vấn đề quá thời gian chờ (timeout), lệnh gọi mạng hoặc tranh chấp tài nguyên.

### 4. Chẩn đoán
Đối với mỗi bài kiểm thử chập chờn, hãy đọc mã kiểm thử và thông báo lỗi, sau đó đối chiếu các triệu chứng với `references/flakiness-causes.md`: căn thời gian và lệnh sleep, phụ thuộc vào thứ tự chạy kiểm thử, trạng thái dùng chung, múi giờ và đồng hồ hệ thống, tính ngẫu nhiên không có hạt giống (seed), mạng, xử lý đồng thời, giới hạn tài nguyên.

### 5. Đưa ra khuyến nghị
Đối với mỗi bài kiểm thử, hãy nêu rõ: nguyên nhân có khả năng cao nhất, bằng chứng, cách khắc phục cụ thể (ví dụ: "thay thế sleep(2) bằng việc chờ sự kiện cập nhật trạng thái đơn hàng"), người phụ trách, và có nên cách ly tạm thời hay không. Điền thông tin vào `templates/flaky-report.md`.

## Quy tắc

- Cơ chế chạy lại (retries) chỉ là băng cá nhân tạm thời có ngày hết hạn, không bao giờ là cách khắc phục thực sự.
- Các bài kiểm thử bị cách ly cần có người phụ trách và một ticket theo dõi; hãy liệt kê chúng vào báo cáo.
- Không xóa một bài kiểm thử chập chờn nếu chưa có sự đồng ý của người phụ trách.
- Giữ số liệu thống kê trung thực: nêu rõ kết luận được đưa ra dựa trên bao nhiêu lần chạy.
- Không bao giờ dán thông tin bí mật (secrets) hoặc toàn bộ nhật ký có chứa token vào báo cáo; chỉ cắt lấy phần kiểm tra điều kiện (assertion) có liên quan.
FILE:references/flakiness-causes.md
# Các nguyên nhân phổ biến gây ra kiểm thử chập chờn

| Nguyên nhân | Triệu chứng điển hình | Cách khắc phục |
|---|---|---|
| Lệnh sleep cố định và căn thời gian | Trượt nhiều hơn trên các tác nhân (agent) CI đang bận; độ lệch thời gian chạy cao | Chờ một điều kiện hoặc sự kiện kèm thời gian chờ tối đa (timeout) thay vì dùng `sleep` |
| Phụ thuộc vào thứ tự kiểm thử | Chạy riêng lẻ thì đạt, chạy trong toàn bộ bộ kiểm thử hoặc theo thứ tự ngẫu nhiên thì trượt | Đặt lại trạng thái dùng chung trong bước setup/teardown; chạy với thứ tự ngẫu nhiên để tái hiện lỗi |
| Trạng thái khả biến dùng chung | Biến toàn cục, singleton, bộ nhớ đệm (cache), biến môi trường bị rò rỉ giữa các bài kiểm thử | Cô lập dữ liệu mẫu (fixtures); khôi phục biến môi trường; tránh lưu trạng thái ở cấp độ mô-đun |
| Tài nguyên bên ngoài dùng chung | Các luồng chạy song song dùng chung các dòng cơ sở dữ liệu, cổng mạng, tệp tạm hoặc hàng đợi | Đặt tên duy nhất cho mỗi luồng xử lý (worker); hoàn tác giao dịch (rollback transaction) sau mỗi bài kiểm thử; dùng cổng trống ngẫu nhiên |
| Đồng hồ và múi giờ | Trượt vào gần nửa đêm, cuối tháng, khi đổi giờ mùa hè (DST), hoặc trên các máy chạy ở múi giờ khác | Đóng băng hoặc tiêm (inject) đồng hồ giả lập; sử dụng múi giờ UTC; tránh dùng thời gian "hiện tại" trong câu lệnh kiểm tra |
| Tính ngẫu nhiên không có hạt giống (seed) | Hiếm khi trượt với dữ liệu kỳ lạ | Gắn hạt giống (seed) cho bộ sinh ngẫu nhiên và in giá trị hạt giống ra khi bài kiểm thử bị trượt |
| Lệnh gọi mạng và dịch vụ bên thứ ba | Lỗi quá thời gian chờ (timeout), lỗi 429, lỗi DNS | Giả lập (mock) hoặc ghi lại phản hồi; tách các lệnh gọi thực tế sang một bộ kiểm thử riêng không chặn luồng chính |
| Xử lý đồng thời và bất đồng bộ | Điều kiện tranh chấp (race conditions), promise chưa được await, luồng (thread) chưa được join | Await mọi tác vụ bất đồng bộ; sử dụng cơ chế đồng bộ hóa phù hợp; thêm các điểm móc (hook) tất định |
| Giới hạn tài nguyên | Hết bộ nhớ (OOM), cạn kiệt tệp mở (file handle), container chạy chậm | Giảm mức độ chạy song song; dọn dẹp tài nguyên; chủ động nâng giới hạn một cách có tính toán |
| Kiểm thử giao diện (UI) và trình duyệt | Không tìm thấy phần tử, thời gian chạy hiệu ứng chuyển động, khác biệt kích thước khung nhìn (viewport) | Sử dụng bộ chọn (selector) ổn định và bộ định vị tự động chờ; tắt hiệu ứng chuyển động trong chế độ kiểm thử |
| Số dấu phẩy động và thứ tự | Kiểm tra trực tiếp trên các tập hợp không có thứ tự hoặc số thực dấu phẩy động chính xác tuyệt đối | So sánh các giá trị đã được sắp xếp; sử dụng độ lệch sai số cho phép (tolerance) |

## Câu hỏi phân loại
1. Nó có bị trượt trên cùng một commit khi chạy lại không? (flaky_confirmed)
2. Nó bị trượt khi chạy một mình, hay chỉ khi chạy cùng các bài kiểm thử khác?
3. Việc bị trượt có tương quan với một máy chạy (agent) cụ thể, thời điểm trong ngày, hay luồng chạy song song nào không?
4. Thông báo lỗi có luôn giống hệt nhau không?
5. Trước đây cơ chế chạy lại (retries) có che giấu lỗi này không? Hãy kiểm tra nhật ký chạy lại.
FILE:references/junit-and-history-formats.md
# Cách lấy tệp JUnit XML và lịch sử chạy

## Xuất JUnit XML cho mỗi lần chạy
- pytest: `pytest --junitxml=reports/run-$N.xml`
- Jest: trình báo cáo `jest-junit` (`JEST_JUNIT_OUTPUT_FILE=reports/run-$N.xml`)
- Go: `go test -v ./... 2>&1 | go-junit-report > reports/run-$N.xml`
- JUnit / Maven Surefire: `target/surefire-reports/*.xml` (gộp lại cho mỗi lần chạy)
- RSpec: `rspec_junit_formatter`
- Playwright: `--reporter=junit`

Vòng lặp chạy lại trên một commit không thay đổi:
```
for N in $(seq 1 10); do pytest --junitxml=reports/run-$N.xml || true; done
python3 scripts/flaky_tests.py reports/run-*.xml
```
Truyền các tệp theo đúng thứ tự chạy (thứ tự khớp mẫu glob là ổn nếu tên tệp được sắp xếp đúng, ví dụ: run-01..run-10).

## Cách kịch bản đọc dữ liệu JUnit
- Mã định danh bài kiểm thử (Test id) = `classname::name`
- Thẻ con `<failure>` hoặc `<error>` = trượt (fail); `<skipped>` = bỏ qua (skip); các trường hợp còn lại = đạt (pass)
- Thuộc tính `time` = thời lượng chạy tính bằng giây

## Lịch sử dạng CSV
Các cột: `run_id,test,status` cùng các cột tùy chọn `commit,duration_s`. Các trạng thái được chấp nhận: pass/passed/ok/success, fail/failed/failure/error/broken, skip/skipped/ignored/pending. Các dòng phải được sắp xếp theo đúng thứ tự lần chạy.

Ý tưởng xuất dữ liệu: sử dụng API phân tích kiểm thử của hệ thống CI, một tác vụ nhỏ nối thêm một dòng cho mỗi bài kiểm thử sau mỗi lần chạy vào tệp lưu trữ CSV, hoặc truy vấn trực tiếp từ cơ sở dữ liệu lưu kết quả kiểm thử của bạn.
FILE:templates/flaky-report.md
# Báo Cáo Kiểm Thử Chập Chờn (Flaky Test)

**Bộ kiểm thử:** <name>  **Số lần chạy đã phân tích:** <n> (<khoảng ngày hoặc các commit>)  **Trình chạy:** <pytest / Jest / ...>

## Tóm tắt
- flaky_confirmed: <n>  flaky: <n>  always_failing: <n>  stable: <n>  insufficient_data: <n>
- Cơ chế chạy lại (retries) hiện đang bật: <có/không, số lần>

## Ưu tiên sửa trước
| Bài kiểm thử | Phân loại | Tỷ lệ lỗi | Tỷ lệ đảo chiều | Nguyên nhân có thể | Bằng chứng | Cách khắc phục đề xuất | Người phụ trách | Cách ly? |
|---|---|---|---|---|---|---|---|---|
| | | | | | | | | |

## Lỗi thực sự (không phải chập chờn)
| Bài kiểm thử | Lỗi từ khi nào | Thông báo lỗi (đã cắt gọn) | Người phụ trách |
|---|---|---|---|

## Danh sách cách ly (Quarantine)
| Bài kiểm thử | Ticket | Người phụ trách | Ngày hết hạn |
|---|---|---|---|

## Các bước tiếp theo
1. 
2. 
FILE:examples/test-history.csv
run_id,test,status,commit,duration_s
1,checkout::test_apply_coupon,pass,a1b2c3,0.42
1,checkout::test_payment_timeout,pass,a1b2c3,1.10
1,search::test_autocomplete,pass,a1b2c3,0.31
1,profile::test_avatar_upload,failed,a1b2c3,2.05
2,checkout::test_apply_coupon,pass,a1b2c3,0.40
2,checkout::test_payment_timeout,failed,a1b2c3,5.02
2,search::test_autocomplete,pass,a1b2c3,0.29
2,profile::test_avatar_upload,failed,a1b2c3,2.10
3,checkout::test_apply_coupon,pass,d4e5f6,0.44
3,checkout::test_payment_timeout,pass,d4e5f6,1.07
3,search::test_autocomplete,skipped,d4e5f6,
3,profile::test_avatar_upload,failed,d4e5f6,2.01
4,checkout::test_apply_coupon,pass,d4e5f6,0.41
4,checkout::test_payment_timeout,failed,d4e5f6,5.01
4,search::test_autocomplete,pass,d4e5f6,0.30
4,profile::test_avatar_upload,failed,d4e5f6,2.07
FILE:scripts/flaky_tests.py
#!/usr/bin/env python3
"""Detect flaky tests from repeated CI runs (Python 3 standard library only).

Inputs (any mix):
  * JUnit XML files, one per run, in run order:  flaky_tests.py run1.xml run2.xml run3.xml
  * A CSV history file with columns run_id,test,status[,commit][,duration_s]:  flaky_tests.py --csv history.csv

Statuses: pass, fail (failure or error), skip. Skips are ignored for flakiness math.
Classifications per test:
  flaky_confirmed   - passed AND failed on the same commit
  flaky             - passed AND failed across runs (commit unknown or different)
  always_failing    - failed in every non-skipped run (>= --min-runs)
  stable            - passed in every non-skipped run (>= --min-runs)
  insufficient_data - fewer than --min-runs non-skipped results

Exit code: 1 if any flaky/flaky_confirmed test is found, else 0 (2 on input errors).
"""
import argparse
import csv
import json
import sys
import xml.etree.ElementTree as ET
from collections import OrderedDict

STATUS_ALIASES = {
    "pass": "pass", "passed": "pass", "ok": "pass", "success": "pass",
    "fail": "fail", "failed": "fail", "failure": "fail", "error": "fail", "broken": "fail",
    "skip": "skip", "skipped": "skip", "ignored": "skip", "pending": "skip",
}


def norm_status(raw):
    s = STATUS_ALIASES.get(str(raw).strip().lower())
    if s is None:
        raise ValueError(f"unknown status: {raw!r}")
    return s


def parse_junit_text(text, run_id):
    """Return list of (run_id, test_id, status, commit, duration) from JUnit XML text."""
    root = ET.fromstring(text)
    rows = []
    for case in root.iter("testcase"):
        cls = case.get("classname") or ""
        name = case.get("name") or "?"
        test_id = f"{cls}::{name}" if cls else name
        if case.find("failure") is not None or case.find("error") is not None:
            status = "fail"
        elif case.find("skipped") is not None:
            status = "skip"
        else:
            status = "pass"
        try:
            dur = float(case.get("time", "") or 0.0)
        except ValueError:
            dur = 0.0
        rows.append((run_id, test_id, status, None, dur))
    return rows


def parse_csv_text(text):
    rows = []
    reader = csv.DictReader(text.splitlines())
    need = {"run_id", "test", "status"}
    if not reader.fieldnames or not need.issubset({f.strip() for f in reader.fieldnames}):
        raise ValueError("CSV needs columns run_id,test,status (optional commit,duration_s)")
    for r in reader:
        r = {k.strip(): (v or "").strip() for k, v in r.items() if k}
        dur = float(r["duration_s"]) if r.get("duration_s") else 0.0
        rows.append((r["run_id"], r["test"], norm_status(r["status"]), r.get("commit") or None, dur))
    return rows


def analyze(rows, min_runs=3):
    """rows in run order. Returns OrderedDict test_id -> stats dict."""
    tests = OrderedDict()
    for run_id, test_id, status, commit, dur in rows:
        t = tests.setdefault(test_id, {"results": [], "durations": []})
        t["results"].append((run_id, status, commit))
        if status != "skip" and dur:
            t["durations"].append(dur)
    out = OrderedDict()
    for test_id, t in tests.items():
        seq = [s for _, s, _ in t["results"] if s != "skip"]
        passes = seq.count("pass")
        fails = seq.count("fail")
        skips = sum(1 for _, s, _ in t["results"] if s == "skip")
        flips = sum(1 for a, b in zip(seq, seq[1:]) if a != b)
        by_commit = {}
        for _, s, c in t["results"]:
            if c and s != "skip":
                by_commit.setdefault(c, set()).add(s)
        mixed_commits = sorted(c for c, ss in by_commit.items() if ss == {"pass", "fail"})
        n = len(seq)
        if passes and fails:
            cls = "flaky_confirmed" if mixed_commits else "flaky"
        elif n < min_runs:
            cls = "insufficient_data"
        elif fails == n:
            cls = "always_failing"
        else:
            cls = "stable"
        durs = t["durations"]
        out[test_id] = {
            "runs": n,
            "passes": passes,
            "fails": fails,
            "skips": skips,
            "fail_rate": round(fails / n, 3) if n else 0.0,
            "flips": flips,
            "flip_rate": round(flips / (n - 1), 3) if n > 1 else 0.0,
            "mixed_commits": mixed_commits,
            "max_duration_s": round(max(durs), 3) if durs else None,
            "duration_spread": round(max(durs) / min(durs), 2) if durs and min(durs) > 0 else None,
            "classification": cls,
        }
    return out


def priority(stats):
    """Higher = fix first. Confirmed flakiness and high flip rates rank highest."""
    base = {"flaky_confirmed": 3.0, "flaky": 2.0, "always_failing": 1.0}.get(stats["classification"], 0.0)
    return round(base + stats["flip_rate"] + stats["fail_rate"], 3)


def render_table(results):
    lines = ["| Test | Class | Runs | Fails | Fail rate | Flip rate | Same-commit mix | Priority |",
             "|---|---|---|---|---|---|---|---|"]
    ranked = sorted(results.items(), key=lambda kv: (-priority(kv[1]), kv[0]))
    for tid, s in ranked:
        lines.append(f"| {tid} | {s['classification']} | {s['runs']} | {s['fails']} | "
                     f"{s['fail_rate']:.0%} | {s['flip_rate']:.0%} | {', '.join(s['mixed_commits']) or '-'} | {priority(s)} |")
    return "\n".join(lines)


def summary(results):
    counts = {}
    for s in results.values():
        counts[s["classification"]] = counts.get(s["classification"], 0) + 1
    return counts


JUNIT_RUN = """<testsuites><testsuite name="api">
<testcase classname="api.test_orders" name="test_create" time="0.12"/>
<testcase classname="api.test_orders" name="test_refund" time="{refund_time}">{refund}</testcase>
<testcase classname="api.test_auth" name="test_expired_token" time="0.05">{auth}</testcase>
<testcase classname="api.test_search" name="test_reindex" time="1.0"><skipped/></testcase>
</testsuite></testsuites>"""


def _run(refund_fail, auth_fail, refund_time="0.30"):
    return JUNIT_RUN.format(
        refund='<failure message="timeout"/>' if refund_fail else "",
        auth='<error message="boom"/>' if auth_fail else "",
        refund_time=refund_time)


def self_test():
    rows = []
    pattern = [(False, True), (True, True), (False, True), (True, True), (False, True)]
    for i, (rf, af) in enumerate(pattern, 1):
        rows += parse_junit_text(_run(rf, af, "0.30" if not rf else "3.10"), f"run{i}")
    res = analyze(rows, min_runs=3)
    csv_text = ("run_id,test,status,commit,duration_s\n"
                "1,ui::login,pass,abc,1.0\n2,ui::login,failed,abc,1.2\n3,ui::login,pass,def,0.9\n"
                "1,ui::cart,pass,abc,\n2,ui::cart,pass,abc,\n"
                "1,ui::banner,skipped,abc,\n2,ui::banner,ok,abc,\n3,ui::banner,ok,def,\n4,ui::banner,ok,def,\n")
    res_csv = analyze(parse_csv_text(csv_text), min_runs=3)
    bad_status = False
    try:
        norm_status("weird")
    except ValueError:
        bad_status = True
    bad_csv = False
    try:
        parse_csv_text("a,b\n1,2\n")
    except ValueError:
        bad_csv = True
    cases = [
        ("junit_case_count", len(res), 4),
        ("create_stable", res["api.test_orders::test_create"]["classification"], "stable"),
        ("refund_flaky", res["api.test_orders::test_refund"]["classification"], "flaky"),
        ("refund_flips", res["api.test_orders::test_refund"]["flips"], 4),
        ("refund_flip_rate", res["api.test_orders::test_refund"]["flip_rate"], 1.0),
        ("refund_fail_rate", res["api.test_orders::test_refund"]["fail_rate"], 0.4),
        ("refund_duration_spread", res["api.test_orders::test_refund"]["duration_spread"], 10.33),
        ("error_counts_as_fail", res["api.test_auth::test_expired_token"]["classification"], "always_failing"),
        ("skip_only_insufficient", res["api.test_search::test_reindex"]["classification"], "insufficient_data"),
        ("skip_counted", res["api.test_search::test_reindex"]["skips"], 5),
        ("csv_same_commit_confirmed", res_csv["ui::login"]["classification"], "flaky_confirmed"),
        ("csv_mixed_commit_listed", res_csv["ui::login"]["mixed_commits"], ["abc"]),
        ("csv_two_runs_insufficient", res_csv["ui::cart"]["classification"], "insufficient_data"),
        ("csv_skip_ignored_stable", res_csv["ui::banner"]["classification"], "stable"),
        ("priority_order", priority(res_csv["ui::login"]) > priority(res["api.test_orders::test_refund"]), True),
        ("unknown_status_rejected", bad_status, True),
        ("bad_csv_rejected", bad_csv, True),
        ("summary_counts", summary(res), {"stable": 1, "flaky": 1, "always_failing": 1, "insufficient_data": 1}),
    ]
    failed = 0
    for name, got, expected in cases:
        ok = abs(got - expected) < 0.01 if isinstance(expected, float) else got == expected
        failed += not ok
        print(f"{'PASS' if ok else 'FAIL'}  {name}: got={got!r} expected={expected!r}")
    print(f"\n{len(cases) - failed}/{len(cases)} passed")
    return 1 if failed else 0


def main(argv=None):
    p = argparse.ArgumentParser(description="Detect flaky tests from repeated JUnit XML runs or a CSV history.")
    p.add_argument("junit", nargs="*", help="JUnit XML files in run order")
    p.add_argument("--csv", help="CSV history: run_id,test,status[,commit][,duration_s]")
    p.add_argument("--min-runs", type=int, default=3)
    p.add_argument("--json", action="store_true", help="print JSON instead of a markdown table")
    p.add_argument("--only-flaky", action="store_true")
    p.add_argument("--self-test", action="store_true")
    a = p.parse_args(argv)
    if a.self_test:
        return self_test()
    rows = []
    try:
        for i, path in enumerate(a.junit, 1):
            with open(path, encoding="utf-8") as fh:
                rows += parse_junit_text(fh.read(), f"run{i}:{path}")
        if a.csv:
            with open(a.csv, encoding="utf-8") as fh:
                rows += parse_csv_text(fh.read())
    except (OSError, ValueError, ET.ParseError) as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    if not rows:
        p.error("give JUnit XML files, --csv, or --self-test")
    res = analyze(rows, a.min_runs)
    if a.only_flaky:
        res = OrderedDict((k, v) for k, v in res.items() if v["classification"].startswith("flaky"))
    if a.json:
        print(json.dumps({"summary": summary(res), "tests": res}, indent=2))
    else:
        print(render_table(res))
        print("\nSummary:", ", ".join(f"{k}={v}" for k, v in sorted(summary(res).items())))
    return 1 if any(v["classification"].startswith("flaky") for v in res.values()) else 0


if __name__ == "__main__":
    sys.exit(main())
PythonDevOpsQuality+3
F@f
0
Kiểm thử taste
Văn bản

Các ưu tiên mang tính định hướng rõ ràng để viết các bài kiểm thử tự động nhanh, đáng tin cậy trong các dự án TypeScript và JavaScript.

# Gu kiểm thử của một nhóm phát triển TypeScript thực dụng
- Sử dụng vitest làm trình chạy kiểm thử mặc định cho cả kiểm thử đơn vị (unit test) và kiểm thử tích hợp (integration test). Độ tin cậy: 0.90
- Đặt các tệp kiểm thử ngay cạnh mã nguồn mà chúng kiểm tra với tên *.test.ts. Độ tin cậy: 0.85
- Đặt tên các bài kiểm thử bằng những câu văn rõ ràng mô tả hành vi, chứ không phải tên phương thức. Độ tin cậy: 0.90
- Tuân theo cấu trúc chuẩn bị (arrange), thực thi (act), khẳng định (assert) với một dòng trống ngăn cách giữa mỗi giai đoạn. Độ tin cậy: 0.75
- Kiểm thử hành vi công khai thông qua API của mô-đun thay vì kiểm thử các hàm nội bộ (private). Độ tin cậy: 0.90
- Ưu tiên sử dụng các thành phần phối hợp thực tế và các bản giả lập trong bộ nhớ (in-memory fakes) hơn là dùng mock. Độ tin cậy: 0.85
- Chỉ làm giả (mock) tại các ranh giới tiến trình chẳng hạn như mạng, đồng hồ hệ thống và bộ sinh số ngẫu nhiên. Độ tin cậy: 0.90
- Chặn bắt các yêu cầu HTTP bằng msw thay vì tự viết hàm giả lập (stub) cho fetch bằng tay. Độ tin cậy: 0.80
- Chạy các bài kiểm thử cơ sở dữ liệu với một Postgres thực sự trong Testcontainers, chứ không dùng SQLite. Độ tin cậy: 0.80
- Đóng băng thời gian bằng bộ đếm thời gian giả (fake timers) và truyền giá trị mầm (seed) cho bất kỳ thứ gì mang tính ngẫu nhiên. Độ tin cậy: 0.90
- Tạo dữ liệu kiểm thử bằng các hàm khởi tạo (factory functions) nhỏ cho phép ghi đè giá trị. Độ tin cậy: 0.85
- Giữ cho các bài kiểm thử chụp nhanh (snapshot tests) ít và nhỏ gọn, đồng thời xem xét kỹ mọi thay đổi khác biệt của snapshot. Độ tin cậy: 0.75
- Sử dụng Playwright cho một lớp mỏng các bài kiểm thử đầu-cuối (end-to-end) chỉ tập trung vào những luồng người dùng quan trọng. Độ tin cậy: 0.85
- Chọn các phần tử theo vai trò (role) và tên hỗ trợ tiếp cận (accessible name) trong các bài kiểm thử giao diện, không bao giờ chọn theo class CSS. Độ tin cậy: 0.90
- Coi một bài kiểm thử chập chờn (flaky test) là một lỗi: cách ly nó ngay trong ngày và sửa hoặc xóa nó trong vòng một tuần. Độ tin cậy: 0.85
- Không bao giờ dùng lệnh sleep trong kiểm thử; thay vào đó hãy chờ một điều kiện cụ thể xảy ra. Độ tin cậy: 0.95
- Giữ cho toàn bộ bộ kiểm thử đơn vị chạy xong dưới một phút trên máy tính xách tay. Độ tin cậy: 0.75
- Theo dõi độ bao phủ mã nguồn (coverage) như một xu hướng trong CI nhưng không chặn việc gộp mã (merge) dựa trên một tỷ lệ phần trăm cố định. Độ tin cậy: 0.70
- Bổ sung kiểm thử dựa trên thuộc tính (property-based tests) bằng fast-check cho các bộ phân tích cú pháp (parsers) và logic thuần túy. Độ tin cậy: 0.65
- Viết một bài kiểm thử hồi quy báo lỗi trước khi sửa bất kỳ lỗi nào được báo cáo. Độ tin cậy: 0.90
- Chạy các bài kiểm thử theo thứ tự ngẫu nhiên để phát hiện trạng thái dùng chung bị ẩn. Độ tin cậy: 0.70
JavaScriptTypeScriptQuality+2
F@f
0
Dịch Vụ FastAPI taste
Văn bản

Các quy ước mang tính định hướng rõ ràng để xây dựng và vận hành các dịch vụ FastAPI trên môi trường production bằng Python.

# Dịch Vụ FastAPI taste của một kỹ sư API Python
- Định nghĩa phần thân của yêu cầu (request) và phản hồi (response) bằng các mô hình Pydantic v2 và luôn thiết lập response_model để không vô tình làm rò rỉ dữ liệu ngoài ý muốn. Độ tin cậy: 0.95
- Tách riêng các lược đồ (schemas) cho các thao tác tạo mới (create), cập nhật (update) và đọc (read) thay vì tái sử dụng mô hình ORM làm cấu trúc dữ liệu trả về của API. Độ tin cậy: 0.90
- Chỉ sử dụng các endpoint khai báo với async def khi mọi lệnh gọi bên trong đều thực sự là bất đồng bộ (async); nếu không, hãy viết bằng def thông thường và để threadpool tự xử lý. Độ tin cậy: 0.85
- Tiêm (inject) các phiên kết nối cơ sở dữ liệu (database sessions), cài đặt cấu hình và người dùng hiện tại thông qua Depends, tuyệt đối không dùng các biến toàn cục ở cấp độ module. Độ tin cậy: 0.90
- Tải cấu hình bằng pydantic-settings từ các biến môi trường và cho dừng ứng dụng báo lỗi ngay lúc khởi động (fail fast) nếu thiếu bất kỳ thông tin cấu hình nào. Độ tin cậy: 0.90
- Chia nhỏ các tuyến đường (routes) thành các module APIRouter theo từng miền nghiệp vụ (domain) và giữ cho tệp main.py chỉ giới hạn ở việc khởi tạo ứng dụng và kết nối các thành phần lại với nhau. Độ tin cậy: 0.85
- Đặt logic nghiệp vụ vào trong các hàm dịch vụ (service functions) hoàn toàn tách biệt và không biết gì về giao thức HTTP, để có thể tái sử dụng chúng từ các tiến trình chạy ngầm (workers) và trong kiểm thử (tests). Độ tin cậy: 0.85
- Sử dụng cú pháp SQLAlchemy 2.0 với các phiên bất đồng bộ (async sessions) và quản lý lược đồ cơ sở dữ liệu bằng các bản di chuyển (migrations) của Alembic. Độ tin cậy: 0.80
- Sử dụng trình quản lý ngữ cảnh lifespan cho quá trình khởi động (startup) và tắt ứng dụng (shutdown) thay vì các hook on_event đã lỗi thời. Độ tin cậy: 0.85
- Chỉ phát sinh ngoại lệ HTTPException ở tầng giao tiếp ngoài cùng và ánh xạ các lỗi nghiệp vụ (domain errors) sang mã trạng thái HTTP bằng các trình xử lý ngoại lệ (exception handlers) đã đăng ký. Độ tin cậy: 0.80
- Trả về cấu trúc lỗi nhất quán gồm mã lỗi máy có thể đọc được (machine-readable code), thông báo lỗi và thông tin chi tiết tùy chọn. Độ tin cậy: 0.85
- Đánh phiên bản cho API ngay trên đường dẫn (/v1) và coi lược đồ OpenAPI được tạo ra như một bản hợp đồng giao tiếp được kiểm tra tự động trong quy trình CI. Độ tin cậy: 0.75
- Sử dụng phân trang dựa trên con trỏ (cursor-based pagination) cho các danh sách lớn và giới hạn kích thước trang tối đa ở phía máy chủ. Độ tin cậy: 0.80
- Kiểm thử bằng pytest và httpx.AsyncClient, ghi đè các phụ thuộc (overriding dependencies) thay vì dùng kỹ thuật monkey-patch vào các thành phần nội bộ. Độ tin cậy: 0.90
- Chạy các tiến trình con (workers) của uvicorn phía sau gunicorn hoặc một trình quản lý tiến trình trên môi trường production, và chỉ dùng cờ --reload trong môi trường phát triển (development). Độ tin cậy: 0.80
- Chuyển các tác vụ xử lý chậm sang một hàng đợi tác vụ (task queue) thực thụ như Celery, Arq hoặc Dramatiq; BackgroundTasks chỉ dành cho các công việc nhỏ chạy một lần rồi thôi (fire-and-forget). Độ tin cậy: 0.85
- Thêm ID yêu cầu (request IDs), nhật ký định dạng JSON có cấu trúc và middleware truy vết OpenTelemetry ngay từ ngày đầu tiên. Độ tin cậy: 0.80
- Bảo vệ các endpoint bằng OAuth2 bearer token hoặc khóa API thông qua các phụ thuộc bảo mật (security dependencies) để trang tài liệu tự động hiển thị cơ chế xác thực. Độ tin cậy: 0.80
- Cung cấp riêng biệt hai endpoint /healthz và /readyz, trong đó bước kiểm tra mức độ sẵn sàng (readiness) sẽ kiểm tra kết nối cơ sở dữ liệu và các phụ thuộc cứng khác. Độ tin cậy: 0.85
- Quản lý các thư viện phụ thuộc bằng uv kèm theo tệp khóa (lock file), đồng thời kiểm tra lỗi cú pháp (lint) và định dạng mã nguồn bằng Ruff. Độ tin cậy: 0.75
- Kiểm tra kiểu dữ liệu (type check) bằng mypy hoặc pyright ở chế độ nghiêm ngặt (strict mode) trên gói dịch vụ. Độ tin cậy: 0.70
BackendBest PracticesAPI+2
F@f
0
Triển Khai Feature Flag taste
Văn bản

Các quy ước mang tính định hướng rõ ràng để phát hành các thay đổi một cách an toàn bằng feature flag và triển khai tăng dần theo giai đoạn.

# Triển Khai Feature Flag taste của một kỹ sư phát hành (release engineer)
- Tách biệt việc triển khai (deploy) khỏi việc phát hành (release): đẩy mã nguồn ở trạng thái ẩn phía sau một cờ (flag) và bật nó lên một cách độc lập với quá trình triển khai. Độ tin cậy: 0.95
- Sử dụng dịch vụ quản lý cờ chuyên dụng hoặc SDK tương thích với OpenFeature thay vì tự viết các câu lệnh if thủ công dựa trên biến môi trường. Độ tin cậy: 0.80
- Gán cho mỗi cờ một người phụ trách (owner), phần mô tả, phân loại (release, experiment, ops hoặc permission) và ngày dự kiến gỡ bỏ. Độ tin cậy: 0.90
- Đặt tên các cờ theo hành vi mà chúng kiểm soát, chẳng hạn như checkout-new-payment-form, chứ không đặt theo mã ticket hay tên người. Độ tin cậy: 0.85
- Mặc định mọi cờ về hành vi hiện tại, an toàn khi không thể kết nối tới dịch vụ quản lý cờ. Độ tin cậy: 0.95
- Đánh giá giá trị của các cờ một lần duy nhất cho mỗi yêu cầu (request) ngay tại điểm đầu của luồng thực thi mã nguồn và truyền kết quả xuống dưới, không đánh giá sâu bên trong các vòng lặp. Độ tin cậy: 0.80
- Triển khai tăng dần từng bước: người dùng nội bộ trước, sau đó đến 1%, 10%, 50% và 100%, đồng thời theo dõi các chỉ số đo lường ở từng bước. Độ tin cậy: 0.90
- Sử dụng cơ chế phân nhóm cố định dựa trên hàm băm (sticky, hash-based bucketing) theo ID người dùng hoặc ID tài khoản ổn định để người dùng không bị nhảy qua lại giữa các biến thể khác nhau. Độ tin cậy: 0.90
- Xác định rõ các chỉ số đánh giá thành công và chỉ số bảo vệ (guardrail metrics) trước khi bắt đầu triển khai, chứ không phải sau khi nhìn vào bảng điều khiển (dashboard). Độ tin cậy: 0.85
- Kết nối cơ chế tự động hoàn tác (rollback) với các cảnh báo về tỷ lệ lỗi và độ trễ đối với những thay đổi tiềm ẩn nhiều rủi ro. Độ tin cậy: 0.75
- Giữ một công tắc ngắt khẩn cấp (kill switch) cho mọi phụ thuộc bên ngoài hoặc tác vụ tốn kém tài nguyên để bộ phận vận hành có thể tắt ngay mà không cần triển khai lại mã nguồn. Độ tin cậy: 0.90
- Gỡ bỏ các cờ phát hành (release flags) trong vòng vài tuần sau khi đạt mức 100% và cho phép quy trình CI báo lỗi thất bại khi một cờ đã quá hạn ngày gỡ bỏ. Độ tin cậy: 0.80
- Kiểm thử cả hai luồng bật (on) và tắt (off) trong CI đối với mọi cờ đang hoạt động. Độ tin cậy: 0.85
- Tránh lồng các cờ bên trong những cờ khác; nếu hai cờ có tương tác với nhau, hãy gộp chúng lại hoặc viết tài liệu ghi rõ các tổ hợp kết hợp. Độ tin cậy: 0.80
- Ghi nhật ký (log) kết quả đánh giá cờ kèm theo biến thể (variant) vào trong các dấu vết (traces) để khi xảy ra sự cố có thể truy vết về một đợt triển khai cụ thể. Độ tin cậy: 0.80
- Giới hạn quyền của những người có thể thay đổi các cờ trên môi trường production và lưu nhật ký kiểm toán (audit log) cho mọi thay đổi kèm theo lý do. Độ tin cậy: 0.85
- Tuyệt đối không sử dụng feature flag để thay thế cho các bước kiểm tra phân quyền (authorization checks). Độ tin cậy: 0.90
- Thông báo các đợt triển khai lớn trong một kênh chung kèm theo tên cờ, kế hoạch triển khai và các bước hoàn tác (rollback). Độ tin cậy: 0.70
- Giữ các cờ phân quyền (permission) và cờ vận hành (ops) tồn tại lâu dài trong một danh sách riêng biệt để chúng không bị nhầm lẫn với các cờ phát hành đã cũ. Độ tin cậy: 0.75
- Thực hiện một buổi đánh giá ngắn sau mỗi đợt triển khai lớn và ghi nhận lại những chỉ số nào đã thực sự phát hiện ra vấn đề. Độ tin cậy: 0.65
Best PracticesDevOpsArchitecture+1
F@f
0
Thử nghiệm Học máy taste
Văn bản

Các quy ước mang tính định kiến để chạy các thử nghiệm học máy có khả năng tái lập, từ mô hình cơ sở đến môi trường production.

# Thử nghiệm Học máy taste của một kỹ sư ML ứng dụng
- Viết chỉ số đánh giá và một mô hình cơ sở (baseline) đơn giản trước khi huấn luyện bất kỳ mô hình thực tế nào. Độ tin cậy: 0.95
- Chia dữ liệu theo thời gian hoặc theo thực thể khi có khả năng xảy ra rò rỉ dữ liệu (data leakage), tuyệt đối không chia ngẫu nhiên theo mặc định. Độ tin cậy: 0.90
- Đóng băng tập kiểm thử (test set) từ sớm và chỉ xem nó khi đưa ra các quyết định cuối cùng, đồng thời sử dụng một tập xác thực (validation set) riêng biệt để tinh chỉnh. Độ tin cậy: 0.95
- Theo dõi mọi lần chạy bằng MLflow hoặc Weights & Biases, ghi nhật ký (log) cấu hình, các chỉ số, phiên bản mã nguồn và phiên bản tập dữ liệu. Độ tin cậy: 0.90
- Lưu cấu hình thử nghiệm trong các tệp YAML được quản lý phiên bản, nạp bằng Hydra hoặc Pydantic, thay vì dùng các hằng số gán cứng (hardcoded). Độ tin cậy: 0.80
- Thiết lập và ghi lại các hạt giống ngẫu nhiên (random seeds) cho Python, NumPy cùng framework đang dùng, đồng thời báo cáo phương sai qua nhiều seed khác nhau. Độ tin cậy: 0.85
- Quản lý phiên bản các tập dữ liệu bằng DVC hoặc các đường dẫn lưu trữ đối tượng bất biến (immutable object storage) để mọi kết quả đều có thể tái lập. Độ tin cậy: 0.80
- Tạo mẫu thử nghiệm (prototype) trong các notebook nhưng hãy chuyển mã nguồn có thể tái sử dụng vào một gói Python đã được kiểm thử ngay khi nó được dùng đến lần thứ hai. Độ tin cậy: 0.85
- Cố định các thư viện phụ thuộc bằng tệp khóa (lock file) và ghi nhận lại phiên bản CUDA cùng trình điều khiển (driver) trong mỗi lần chạy. Độ tin cậy: 0.75
- Kiểm tra thủ công các mẫu dữ liệu thô và các lỗi gán nhãn trước khi tin tưởng vào bất kỳ chỉ số tổng hợp nào. Độ tin cậy: 0.90
- Ưu tiên các mô hình cây tăng cường độ dốc (gradient boosted trees) cho dữ liệu dạng bảng và chỉ tìm đến học sâu (deep learning) khi chúng tỏ ra kém hiệu quả rõ rệt. Độ tin cậy: 0.80
- Sử dụng PyTorch với một vòng lặp huấn luyện gọn nhẹ hoặc dùng Lightning thay vì các framework tùy chỉnh cồng kềnh. Độ tin cậy: 0.70
- Báo cáo các chỉ số theo từng phân khúc (slice) chẳng hạn như khu vực, thiết bị hoặc lớp (class), chứ không chỉ dừng lại ở một con số tổng thể duy nhất. Độ tin cậy: 0.90
- Hiệu chuẩn (calibrate) các xác suất dự đoán khi mã nguồn ở khâu sau đưa ra quyết định dựa trên ngưỡng (threshold). Độ tin cậy: 0.75
- Tinh chỉnh siêu tham số (hyperparameters) bằng Optuna hoặc tìm kiếm ngẫu nhiên (random search) đơn giản trong phạm vi ngân sách tính toán cố định. Độ tin cậy: 0.75
- Viết kiểm thử đơn vị (unit tests) cho các bước biến đổi đặc trưng (feature transforms) và bộ nạp dữ liệu (data loaders), đồng thời kiểm tra xác nhận (assert) kích thước (shapes) cùng kiểu dữ liệu (dtypes) kỳ vọng. Độ tin cậy: 0.85
- Xác thực dữ liệu đầu vào bằng các bước kiểm tra lược đồ (schema) như Pandera hoặc Great Expectations trước khi huấn luyện. Độ tin cậy: 0.80
- Lưu mô hình cùng với luồng tiền xử lý (preprocessing pipeline) của nó thành một tạo phẩm (artifact) duy nhất để quá trình huấn luyện và phục vụ (serving) không bị lệch nhau. Độ tin cậy: 0.90
- So sánh mô hình mới với mô hình đang chạy trên môi trường production ở chế độ ngoại tuyến (offline) và sau đó thông qua triển khai chạy ngầm (shadow) hoặc thử nghiệm A/B. Độ tin cậy: 0.85
- Giám sát sự trôi dạt đặc trưng (feature drift) và phân phối dự đoán sau khi ra mắt, đồng thời cảnh báo khi có sự dịch chuyển lớn. Độ tin cậy: 0.80
- Viết một ghi chú thử nghiệm ngắn cho mỗi lần chạy quan trọng, nêu rõ giả thuyết, kết quả và bước tiếp theo. Độ tin cậy: 0.85
- Xóa hoặc lưu trữ các nhánh công việc thất bại thay vì để lại hàng tá các notebook làm dở dang. Độ tin cậy: 0.65
Best PracticesPythonTesting+2
F@f
0
Công cụ Kiểm thử và Đánh giá Mẫu Gitignore
Kỹ năng

Giải thích từng dòng của tệp .gitignore, kiểm thử tệp đó với các đường dẫn bắt buộc phải bỏ qua hoặc bắt buộc phải giữ lại để có thể commit, chỉ rõ quy tắc quyết định cho từng đường dẫn, đồng thời cảnh báo các lỗi sai như quy tắc phủ định bị chặn bởi thư mục cha đã bị loại trừ, chú thích nội dòng (inline comment), khoảng trắng ở đầu dòng, dấu gạch chéo ngược (backslash), ký tự ** thừa, thông tin bí mật (secrets) chưa được bao phủ và tệp .env.example bị bỏ qua. Trả về một tệp đã được sửa lỗi. Bao gồm một trình kiểm tra viết bằng thư viện chuẩn Python có hỗ trợ --self-test, đối chiếu khớp với git thực tế.

---
name: gitignore-pattern-tester-and-reviewer
description: Giải thích từng dòng của tệp .gitignore, kiểm thử tệp đó với các đường dẫn bắt buộc phải bỏ qua hoặc bắt buộc phải giữ lại để có thể commit, chỉ rõ quy tắc quyết định cho từng đường dẫn, đồng thời cảnh báo các lỗi sai như quy tắc phủ định bị chặn bởi thư mục cha đã bị loại trừ, chú thích nội dòng (inline comment), khoảng trắng ở đầu dòng, dấu gạch chéo ngược (backslash), ký tự ** thừa, thông tin bí mật (secrets) chưa được bao phủ và tệp .env.example bị bỏ qua. Trả về một tệp đã được sửa lỗi. Bao gồm một trình kiểm tra viết bằng thư viện chuẩn Python có hỗ trợ --self-test, đối chiếu khớp với git thực tế.
---

# Công cụ Kiểm thử và Đánh giá Mẫu Gitignore

Bạn giúp các lập trình viên hiểu và sửa lỗi các tệp `.gitignore`. Bạn giải thích những gì mà mỗi mẫu (pattern) thực sự khớp, chứng minh điều đó bằng các bài kiểm thử đường dẫn, và tìm ra những sai lầm khiến git bỏ qua các tệp mã nguồn hoặc vô tình commit các thông tin bí mật (secrets) cùng kết quả biên dịch (build output). Bạn trả về một tệp đã được hiệu chỉnh kèm theo các bài kiểm thử vượt qua thành công, chứ không chỉ đưa ra lời khuyên suông.

## Các tệp trong kỹ năng này

- `scripts/gitignore_check.py` - giải thích từng quy tắc, chạy các bài kiểm thử "+ path" (bắt buộc phải bỏ qua) và "- path" (bắt buộc phải giữ lại), chỉ rõ dòng quyết định cho từng đường dẫn, và báo cáo các phát hiện ở mức HIGH, MEDIUM, LOW và INFO (chỉ dùng thư viện chuẩn Python 3)
- `references/gitignore-rules.md` - cách git đọc các tệp ignore: các nguồn và thứ tự ưu tiên, cú pháp dòng, neo đường dẫn (anchoring), ký tự đại diện (wildcards), quy tắc thư mục cha, các tệp đã được theo dõi (tracked files) và các lệnh để xác nhận
- `references/common-pitfalls.md` - mọi mã phát hiện kèm cách khắc phục, các bẫy thường gặp mà kịch bản không thể nhìn thấy (tên quá rộng, tệp đã được theo dõi, tệp lồng nhau, phân biệt chữ hoa chữ thường, tệp khóa lock files, sự khác biệt với .dockerignore) và các khối mẫu khởi đầu tốt
- `templates/gitignore-review.md` - bản đánh giá để bàn giao lại cho người dùng
- `examples/webapp.gitignore` - một tệp ignore của ứng dụng web chứa sáu lỗi thực tế
- `examples/webapp-fixed.gitignore` - tệp đã được sửa lỗi (tất cả các bài kiểm thử đều vượt qua)
- `examples/webapp-paths.txt` - 19 bài kiểm thử đường dẫn được sử dụng với cả hai tệp trên

## Quy trình làm việc

### 1. Thu thập dữ liệu đầu vào
Yêu cầu cung cấp tệp `.gitignore` (và bất kỳ tệp `.gitignore` lồng nhau nào), ngăn xếp công nghệ (stack), và các đường dẫn quan trọng: những gì bắt buộc phải bỏ qua (kết quả biên dịch, các gói phụ thuộc, nhật ký log, thông tin bí mật) và những gì bắt buộc phải giữ lại để có thể commit (các thư mục mã nguồn có tên phổ biến như `build/` hoặc `lib/`, các tệp `.gitkeep`, `.env.example`, các tệp khóa lock files). Nếu người dùng chỉ hỏi "tại sao X lại bị bỏ qua?", hãy hỏi đường dẫn và sử dụng kết quả đầu ra của lệnh `git check-ignore -v X` nếu họ có sẵn.

### 2. Chạy trình kiểm tra
```
python3 scripts/gitignore_check.py examples/webapp.gitignore --test examples/webapp-paths.txt
python3 scripts/gitignore_check.py .gitignore --test paths.txt --json
python3 scripts/gitignore_check.py --self-test
```
Tệp đường dẫn (Paths file): `+ path` là bắt buộc phải bỏ qua, `- path` là bắt buộc phải giữ lại, kết thúc tên thư mục bằng dấu `/`. Mã thoát (Exit) 1 có nghĩa là có phát hiện mức HIGH hoặc có bài kiểm thử thất bại, mã thoát 2 có nghĩa là dữ liệu đầu vào không thể đọc được. Trình kiểm tra mô hình hóa một tệp ignore ở thư mục gốc của kho lưu trữ (repo root); đối với tệp lồng bên trong, hãy cung cấp các đường dẫn tương đối tính từ thư mục chứa tệp đó.

### 3. Giải thích và sửa lỗi
- Đi qua bảng Quy tắc (Rules table) bằng ngôn từ dễ hiểu, đặc biệt là quy tắc neo đường dẫn (dấu gạch chéo ở đầu hoặc ở giữa = tương đối so với thư mục chứa `.gitignore`) và quy tắc thư mục cha (xem `references/gitignore-rules.md`).
- Đối với mỗi trường hợp FAIL, hãy trích dẫn dòng quyết định từ cột "Decided by" và đưa ra thay đổi nhỏ nhất để khắc phục nó.
- Sửa mọi phát hiện ở mức HIGH và MEDIUM; giải thích bất kỳ phát hiện mức INFO nào mà bạn giữ nguyên.
- Thêm các bài kiểm thử `- path` cho các thư mục mã nguồn có tên phổ biến để làm lộ diện các quy tắc quá rộng.
- Chạy lại trình kiểm tra trên tệp đã sửa cho đến khi tất cả các bài kiểm thử đều vượt qua, và hiển thị phần khác biệt (diff).

### 4. Bàn giao kết quả
Điền vào mẫu `templates/gitignore-review.md`: tệp hiện tại đang làm gì, bảng kiểm thử, các phát hiện, toàn bộ tệp đã sửa lỗi, bản so sánh khác biệt (diff), và các câu lệnh để xác nhận (`git check-ignore -v`, `git status --ignored`) cũng như để bỏ theo dõi (untrack) các tệp đã lỡ được commit trước đó (`git rm --cached`).

## Kết quả ví dụ
Đối với `examples/webapp.gitignore` chạy cùng `examples/webapp-paths.txt`: 13/19 bài kiểm thử vượt qua, 1 phát hiện mức HIGH, 4 mức MEDIUM, 2 mức LOW, 1 mức INFO. Mức HIGH NEGATION_BLOCKED: `logs/` loại trừ cả thư mục, do đó `!logs/.gitkeep` không bao giờ có thể được áp dụng (cách sửa: `logs/*`). Mức MEDIUM: `config/secrets.yml # never commit` bị hiểu là một mẫu duy nhất, vì vậy tệp chứa thông tin bí mật này sẽ bị commit; ` Thumbs.db` có một khoảng trắng ở đầu dòng; `src\generated` bị đọc thành `srcgenerated`; và các tệp `*.key`, `*.pem` cùng `id_rsa` chưa được bao phủ. Mức LOW: `.env*` làm ẩn mất `.env.example`; `*.log` bị liệt kê hai lần. Mẫu `build` không được neo cũng làm ẩn luôn `src/build/README.md` (cách sửa: `/build/`). Tệp `examples/webapp-fixed.gitignore` vượt qua 19/19 bài kiểm thử mà không có bất kỳ phát hiện nào ở mức HIGH, MEDIUM hay LOW.

## Quy tắc

- Dòng khớp cuối cùng sẽ chiến thắng; một tệp nằm bên trong một thư mục đã bị loại trừ thì không bao giờ có thể được đưa vào lại (re-included). Hãy luôn nhắc lại điều này bất cứ khi nào có liên quan đến một dòng bắt đầu bằng `!`.
- Tuyệt đối không đề xuất bỏ qua các tệp khóa (`package-lock.json`, `poetry.lock`, `go.sum`, `Cargo.lock` của ứng dụng) hoặc cấu hình dùng chung mà nhóm chủ đích commit.
- Nếu một thông tin bí mật (secret) đã lỡ bị commit, hãy nói rõ rằng việc thêm nó vào ignore là chưa đủ: phải xoay vòng (rotate) khóa đó và cân nhắc viết lại lịch sử git.
- Ưu tiên các mẫu được neo ở thư mục gốc (`/dist/`, `/build/`) cho các thư mục đầu ra, và đặt các tệp cá nhân của trình soạn thảo cũng như hệ điều hành vào tệp ignore toàn cục (global ignore file) khi cả nhóm đồng thuận.
- Xác nhận trên kho lưu trữ thực tế bằng lệnh `git check-ignore -v`: kịch bản tuân theo các quy tắc trong tài liệu của git cho một tệp đơn lẻ, nhưng các tệp lồng nhau, `.git/info/exclude`, tệp toàn cục và thiết lập `core.ignorecase` có thể làm thay đổi kết quả.
FILE:references/gitignore-rules.md
# Cách git đọc tệp .gitignore

Nguồn: tài liệu hướng dẫn gitignore(5) (`git help gitignore`). Trình kiểm tra triển khai các quy tắc này và đã được đối chiếu với git 2.47 thực tế trên 6,956 lượt kiểm tra đường dẫn mà không có bất kỳ sự khác biệt nào.

## Nguồn gốc của các quy tắc (ưu tiên từ cao nhất trở xuống)
1. Các tùy chọn `--exclude` trên dòng lệnh (hiếm gặp; được sử dụng bởi một số công cụ).
2. Các tệp `.gitignore`: tệp nằm trong cùng thư mục với đường dẫn đang xét sẽ ưu tiên hơn các tệp nằm ở thư mục cấp trên. Các mẫu (patterns) có tính tương đối so với thư mục chứa tệp `.gitignore` đó.
3. `.git/info/exclude` - cục bộ trên một bản sao (clone) duy nhất, không bao giờ được commit. Rất phù hợp cho các tệp nháp cá nhân.
4. Tệp toàn cục trong `core.excludesFile` (mặc định là `~/.config/git/ignore`) - rất phù hợp cho các tệp của trình soạn thảo và hệ điều hành như `.DS_Store` và `*.swp`.

Bên trong một tệp, dòng khớp CUỐI CÙNG sẽ quyết định kết quả.

## Cú pháp dòng
| Dòng | Ý nghĩa |
|---|---|
| dòng trống | được bỏ qua, dùng làm khoảng phân cách |
| `# text` | chú thích (comment); chỉ có tác dụng khi nằm ngay đầu dòng. `foo # bar` được hiểu là mẫu có tên `foo # bar` |
| `\#file` | một tên tệp bắt đầu bằng ký tự `#` |
| `!pattern` | đưa trở lại (re-include) một đối tượng đã bị loại trừ bởi một dòng trước đó |
| `\!file` | một tên tệp bắt đầu bằng ký tự `!` |
| `name   ` | các khoảng trắng ở cuối dòng sẽ bị loại bỏ trừ khi được thoát bằng dấu gạch chéo ngược (`name\ `) |
| `  name` | các khoảng trắng ở đầu dòng ĐƯỢC GIỮ LẠI và trở thành một phần của tên |
| `\` | thoát (escape) ký tự kế tiếp; nó không bao giờ được dùng làm dấu phân cách đường dẫn |

## Dấu gạch chéo (slash) quyết định vị trí mà mẫu sẽ khớp
| Mẫu | Khớp với | Không khớp với |
|---|---|---|
| `build` | `build`, `src/build`, `a/b/build/x.js` (bất kỳ tệp hoặc thư mục nào có tên là build) | `build.sh` |
| `build/` | các thư mục có tên là build ở bất kỳ độ sâu nào | một TỆP (FILE) có tên là build |
| `/build` | chỉ khớp với `build` nằm ngay cạnh tệp .gitignore | `src/build` |
| `doc/frotz` | `doc/frotz` nằm ngay cạnh tệp .gitignore (dấu gạch chéo ở giữa có tác dụng neo giống như dấu gạch chéo ở đầu) | `a/doc/frotz` |
| `**/doc/frotz` | `doc/frotz` ở bất kỳ độ sâu nào | |

Quy tắc nằm lòng: không có dấu gạch chéo (hoặc chỉ có một dấu gạch chéo ở cuối) = khớp ở bất kỳ độ sâu nào; có dấu gạch chéo ở đầu hoặc ở giữa = tương đối so với thư mục chứa tệp .gitignore.

## Ký tự đại diện (Wildcards)
| Ký tự đại diện | Ý nghĩa |
|---|---|
| `*` | bất kỳ chuỗi ký tự nào ngoại trừ `/` (`*.log`, `/a*z` không khớp với `ab/cz`) |
| `?` | chính xác một ký tự bất kỳ ngoại trừ `/` |
| `[abc]`, `[a-z]`, `[!0-9]`, `[^0-9]` | một ký tự nằm trong (hoặc không nằm trong) tập hợp; các lớp ký tự kiểu `[[:digit:]]` đều hoạt động |
| `**/name` | `name` nằm trong bất kỳ thư mục nào, bao gồm cả thư mục gốc trên cùng |
| `dir/**` | mọi thứ nằm bên trong `dir`, nhưng không bao gồm chính bản thân `dir` |
| `a/**/b` | `a/b`, `a/x/b`, `a/x/y/b` |
| `foo**`, `**bar` | `**` không nằm cạnh dấu gạch chéo sẽ hoạt động giống như một dấu `*` đơn lẻ |

## Quy tắc thư mục cha (điều gây bất ngờ phổ biến nhất)
"Không thể đưa một tệp vào lại (re-include) nếu thư mục cha của tệp đó đã bị loại trừ." Git thậm chí còn không liệt kê nội dung bên trong một thư mục đã bị loại trừ, vì vậy một dòng `!` phía sau sẽ không bao giờ có cơ hội được xét tới.

```gitignore
# KHÔNG hoạt động: logs/ đã bị loại trừ dưới dạng cả một thư mục
logs/
!logs/.gitkeep

# hoạt động: loại trừ nội dung bên trong thư mục, giữ cho bản thân thư mục vẫn hiển thị
logs/*
!logs/.gitkeep

# hoạt động đối với các tệp nằm sâu hơn: đưa vào lại (re-include) từng cấp thư mục
/assets/*
!/assets/icons/
/assets/icons/*
!/assets/icons/app.svg
```

## Phong cách danh sách trắng (Whitelist style)
Bỏ qua mọi thứ, sau đó chỉ cho phép những gì bạn muốn:
```gitignore
*
!*/
!*.md
!src/**
```
`!*/` đưa mọi thư mục vào lại để các dòng `!` phía sau có thể chạm tới các tệp nằm bên trong chúng.

## Các tệp đã được theo dõi (Already tracked files)
.gitignore chỉ ảnh hưởng đến các tệp chưa được theo dõi (untracked files). Một tệp đã được commit sẽ vẫn tiếp tục được theo dõi:
```
git rm --cached path/to/file        # giữ tệp trên ổ đĩa, ngừng theo dõi nó
git rm -r --cached build/           # tương tự đối với một thư mục
```
Nếu đó là một thông tin bí mật (secret), việc xóa nó khỏi danh sách theo dõi là chưa đủ: hãy xoay vòng (rotate) khóa bí mật đó, bởi vì nó vẫn còn nằm trong lịch sử git.

## Các câu lệnh để xác nhận
```
git check-ignore -v path/to/file          # in ra tệp, dòng và mẫu đã bỏ qua đường dẫn đó (không in gì = không bị bỏ qua)
git check-ignore -v --no-index path       # dùng được cả cho các tệp đang được theo dõi (tracked)
git status --ignored --short              # liệt kê các tệp bị bỏ qua với ký hiệu "!!"
git ls-files --others --ignored --exclude-standard   # mọi tệp chưa được theo dõi đang bị bỏ qua
```
FILE:references/common-pitfalls.md
# Các bẫy .gitignore thường gặp và mã lỗi của trình kiểm tra

| Mã | Mức độ nghiêm trọng | Vấn đề xảy ra | Cách khắc phục |
|---|---|---|---|
| NEGATION_BLOCKED | HIGH | `!path/file` nằm sau một quy tắc loại trừ thư mục cha (`logs/`, `dist`, `a/*` đối với `a/b/c`) sẽ không bao giờ có hiệu lực, khiến tệp bạn định commit vẫn bị bỏ qua | Loại trừ nội dung bên trong (`logs/*` hoặc `logs/**`) thay vì cả thư mục, hoặc đưa vào lại từng cấp thư mục (`!a/b/`) |
| SECRETS_NOT_COVERED | MEDIUM | các tệp bí mật điển hình (`.env`, `*.pem`, `*.key`, `id_rsa`) sẽ bị commit khi chạy `git add .` | Thêm `.env`, `.env.*`, `*.pem`, `*.key`, `id_rsa*`; đồng thời sử dụng trình quét thông tin bí mật (secret scanner) trong CI |
| INLINE_COMMENT | MEDIUM | `secrets.yml # never commit` được hiểu là một mẫu duy nhất bao gồm cả phần văn bản chú thích, do đó `secrets.yml` KHÔNG hề bị bỏ qua | Đặt các chú thích trên dòng riêng của chúng |
| LEADING_SPACE | MEDIUM | ` Thumbs.db` chỉ khớp với một cái tên bắt đầu bằng dấu cách | Xóa dấu cách đó đi |
| BACKSLASH_SEPARATOR | MEDIUM | `src\generated` có nghĩa là "src" + ký tự "g" được thoát + "enerated" = `srcgenerated` | Luôn sử dụng `/`, kể cả trên Windows |
| ENV_EXAMPLE_IGNORED | LOW | `.env*` cũng làm ẩn luôn `.env.example`, tệp mẫu mà các lập trình viên khác cần dùng | Thêm `!.env.example` ở dòng phía sau nó |
| DUPLICATE | LOW | cùng một dòng xuất hiện hai lần; vô hại nhưng là dấu hiệu của việc gộp mã (merge) bằng cách sao chép-dán | Xóa bớt một dòng |
| DOUBLE_STAR | LOW | `foo**` thực chất chỉ là `foo*`; rất có thể tác giả định viết là `foo/**` | Chỉ sử dụng `**` dưới dạng `**/x`, `x/**` hoặc `a/**/b` |
| TRAILING_SPACE | LOW | các khoảng trắng ở cuối dòng bị âm thầm lược bỏ; nếu đó là chủ ý thì tệp sẽ không khớp | Xóa hoặc thoát (escape) chúng |
| CRLF | LOW | ký tự xuống dòng kiểu Windows trong tệp .gitignore | Lưu dưới dạng LF; thêm `* text=auto` vào tệp .gitattributes |
| ANCHORED_BY_SLASH | INFO | `src/gen` chỉ khớp ngay cạnh tệp .gitignore, chứ không khớp với `packages/a/src/gen` | Thêm `/` ở đầu để thể hiện rõ chủ ý hoặc thêm `**/` để khớp ở bất kỳ độ sâu nào |

## Các bẫy mà kịch bản không thể nhìn thấy (hãy kiểm tra thủ công)
- **Các tên không được neo nhưng quá rộng.** `build` cũng làm ẩn cả `src/build/` và `docs/build/`; `lib` làm ẩn mọi thư mục `lib` bao gồm cả mã nguồn. Hãy sử dụng `/build/` cho thư mục đầu ra ở cấp gốc. Hãy viết một bài kiểm thử `- path` cho bất kỳ thư mục mã nguồn nào có tên phổ biến.
- **Các tệp đã được theo dõi (Tracked files).** Việc thêm một quy tắc không làm bỏ theo dõi (untrack) các tệp đã được commit trước đó. Hãy chạy `git ls-files -ci --exclude-standard` để liệt kê các tệp đang được theo dõi nhưng khớp với quy tắc ignore, sau đó chạy `git rm --cached`.
- **Các tệp .gitignore lồng nhau.** Một tệp `.gitignore` nằm trong thư mục con sẽ thêm các quy tắc tương đối so với thư mục con đó và có thể đưa vào lại những gì mà tệp ở thư mục gốc đã loại trừ (trừ khi một thư mục cha đã bị loại trừ). Hãy chạy trình kiểm tra trên từng tệp với các đường dẫn tương đối so với thư mục của nó, hoặc xác nhận bằng lệnh `git check-ignore -v`.
- **Phân biệt chữ hoa chữ thường (Case sensitivity).** Trên macOS và Windows, thiết lập `core.ignorecase=true` khiến `*.JPG` khớp với cả `photo.jpg`; nhưng trên môi trường Linux CI thì không. Hãy viết cả hai dạng chữ hoa/thường hoặc sử dụng `*.[jJ][pP][gG]`.
- **Các tệp cá nhân nằm trong tệp dùng chung.** `.idea/`, `.vscode/`, `*.swp`, `.DS_Store` thuộc về tệp ignore toàn cục (`core.excludesFile`), mặc dù nhiều nhóm vẫn giữ chúng trong tệp của kho lưu trữ (repo) để tiện lợi. Đừng bỏ qua `.vscode/settings.json` nếu nhóm chủ đích chia sẻ tệp này.
- **Các tệp khóa (Lock files).** Đừng bỏ qua `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `poetry.lock`, `Cargo.lock` (đối với các ứng dụng) hoặc `go.sum`: chúng giúp quá trình biên dịch (build) có thể tái lập chính xác.
- **Đã thêm vào ignore nhưng vẫn ép commit.** `git add -f` sẽ ghi đè lên các quy tắc ignore; người đánh giá mã (reviewer) nên kiểm tra xem có kết quả biên dịch hoặc thông tin bí mật nào bị ép thêm vào hay không.
- **Các tệp ignore khác sử dụng quy tắc khác.** `.dockerignore` sử dụng `filepath.Match` của Go cộng với `**` và luôn được neo tại thư mục gốc của ngữ cảnh biên dịch (build context root - không có khái niệm "bất kỳ độ sâu nào" cho các tên thông thường). `.npmignore`, `.eslintignore` và `.prettierignore` tuân theo các quy tắc tương tự gitignore nhưng không phải lúc nào cũng giống ở mọi chi tiết. Hãy kiểm thử chúng bằng công cụ riêng của chúng.

## Các khối mẫu khởi đầu tốt
```gitignore
# Node
node_modules/
/dist/
/build/
npm-debug.log*
.npm/

# Python
__pycache__/
*.py[cod]
.venv/
.pytest_cache/
/dist/
*.egg-info/

# Secrets
.env
.env.*
!.env.example
*.pem
*.key
id_rsa*

# Logs that keep their folder
logs/*
!logs/.gitkeep
```
FILE:templates/gitignore-review.md
# Đánh giá .gitignore: <kho lưu trữ hoặc thư mục>

**Ngăn xếp (Stack):** <Node 20 + Vite / Python 3.12 / monorepo với pnpm workspaces>
**Các tệp đã đánh giá:** <.gitignore tại thư mục gốc, packages/api/.gitignore>
**Tóm tắt:** <một câu: vấn đề quan trọng nhất và bản sửa lỗi sẽ thay đổi điều gì>

## Tệp hiện tại đang làm gì
| Dòng | Mẫu (Pattern) | Ý nghĩa |
|---|---|---|
| <9> | <`logs/`> | <bỏ qua mọi thư mục có tên là logs, ở bất kỳ độ sâu nào, và mọi thứ bên trong nó> |

## Kiểm thử đường dẫn (Path tests)
| Kết quả | Đường dẫn | Kỳ vọng | Thực tế | Quyết định bởi |
|---|---|---|---|---|
| <FAIL> | <`logs/.gitkeep`> | <giữ lại (kept)> | <bỏ qua (ignored)> | <thư mục cha logs/ bị loại trừ bởi L9; L10 không thể đưa nó vào lại> |

## Các phát hiện (Findings)
| # | Mức độ nghiêm trọng | Mã | Dòng | Vấn đề | Cách khắc phục |
|---|---|---|---|---|---|
| 1 | <HIGH> | <NEGATION_BLOCKED> | <10> | <...> | <sử dụng logs/* thay vì logs/> |

## Tệp .gitignore đề xuất
```gitignore
<toàn bộ tệp đã sửa lỗi, với các chú thích nằm trên dòng riêng của chúng>
```

## Các thay đổi
```diff
- logs/
+ logs/*
```

## Áp dụng và xác nhận
1. Thay thế tệp, sau đó chạy: `git check-ignore -v <từng đường dẫn từ bài kiểm thử>`
2. Chạy `git status --ignored --short` và kiểm tra xem không có tệp quan trọng nào hiển thị dưới dạng `!!`
3. Bỏ theo dõi (untrack) các tệp hiện đã được bỏ qua nhưng từng bị commit trước đó: `git rm --cached <path>` (xoay vòng bất kỳ khóa bí mật nào đã từng bị commit)
4. Commit: `chore: fix .gitignore (<lý do ngắn gọn>)`

## Các việc cần làm tiếp theo (Follow-ups)
- <chuyển các tệp của trình soạn thảo sang tệp ignore toàn cục>
- <thêm trình quét thông tin bí mật (gitleaks, trufflehog) vào CI>
FILE:examples/webapp.gitignore
# Dependencies
node_modules
/vendor/

# Build output
dist/
build
*.log
logs/
!logs/.gitkeep

# Environment
.env*
config/secrets.yml # never commit

# OS and editor files
.DS_Store
.idea/
*.swp
 Thumbs.db

# Test coverage
coverage/**
!coverage/README.md

# Generated code
src\generated
*.log
FILE:examples/webapp-fixed.gitignore
# Dependencies
node_modules/
/vendor/

# Build output (root only, so src/build/ stays tracked)
/dist/
/build/
*.log
# ignore the contents of logs/, not the folder, so the negation can work
logs/*
!logs/.gitkeep

# Environment and secrets
.env
.env.*
!.env.example
*.pem
*.key
id_rsa*
# never commit
config/secrets.yml

# OS and editor files
.DS_Store
Thumbs.db
.idea/
*.swp

# Test coverage
coverage/**
!coverage/README.md

# Generated code
/src/generated/
FILE:examples/webapp-paths.txt
# Paths for examples/webapp.gitignore and webapp-fixed.gitignore
# "+ path" = must be ignored, "- path" = must stay committable
+ node_modules/react/index.js
+ packages/api/node_modules/lodash/lodash.js
+ vendor/autoload.php
- packages/vendor/README.md
+ dist/app.js
+ build/output.js
- src/build/README.md
+ logs/app.log
- logs/.gitkeep
+ .env
+ .env.local
- .env.example
+ config/secrets.yml
+ .DS_Store
+ Thumbs.db
+ coverage/lcov.info
- coverage/README.md
+ src/generated/types.ts
- src/index.ts
FILE:scripts/gitignore_check.py
#!/usr/bin/env python3
"""Gitignore pattern tester and reviewer (Python 3 standard library only).

Explains every rule in a .gitignore file, tests the file against paths that must be
ignored ("+ path") or must stay committable ("- path"), names the rule that decides
each path, and flags common mistakes: negations that cannot work because a parent
directory is excluded, inline comments, leading spaces, Windows backslashes, stray
"**", duplicates, secrets that are not covered and .env.example being ignored.

Matching follows git's documented gitignore rules for a single .gitignore at the
repository root: last matching rule wins, "!" re-includes, a trailing "/" matches
directories only, a "/" at the start or in the middle anchors the pattern to the
.gitignore's directory, "*" "?" "[...]" never match "/", "**" spans directories,
and a file can never be re-included if a parent directory is excluded.

Usage:
  python3 gitignore_check.py .gitignore [--test paths.txt] [--json]
  python3 gitignore_check.py --self-test
Paths file: one path per line, "+ path" = must be ignored, "- path" = must NOT be
ignored, end a path with "/" if it is a directory, "#" starts a comment.
Exit codes: 0 = no HIGH findings and all tests pass, 1 = HIGH finding or failed test,
2 = unreadable input.
"""
import argparse, json, re, sys

SEV_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "INFO": 3}
SECRET_PROBES = [".env", "config/.env.production", "certs/server.key", "deploy/id_rsa",
                 "secrets/service-account.pem"]
POSIX_CLASSES = {"alnum": "a-zA-Z0-9", "alpha": "a-zA-Z", "digit": "0-9", "lower": "a-z",
                 "upper": "A-Z", "space": " \\t\\n\\r\\f\\v", "xdigit": "0-9A-Fa-f",
                 "punct": re.escape("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"), "blank": " \\t"}


class Rule:
    def __init__(self, line_no, raw, negate, dir_only, anchored, body, regex):
        self.line_no, self.raw, self.negate, self.dir_only = line_no, raw, negate, dir_only
        self.anchored, self.body, self.regex = anchored, body, regex

    def matches(self, rel_path, is_dir):
        if self.dir_only and not is_dir:
            return False
        target = rel_path if self.anchored else rel_path.rsplit("/", 1)[-1]
        return self.regex.fullmatch(target) is not None

    def label(self):
        return f"L{self.line_no} `{self.raw}`"


def strip_trailing_spaces(s):
    """git drops trailing spaces unless the last one is escaped with a backslash."""
    end = len(s)
    while end > 0 and s[end - 1] == " ":
        bs, k = 0, end - 2
        while k >= 0 and s[k] == "\\":
            bs += 1; k -= 1
        if bs % 2 == 1:
            break
        end -= 1
    return s[:end]


def class_to_regex(body):
    out, m = "", 0
    while m < len(body):
        ch = body[m]
        if ch == "[" and body.startswith("[:", m):
            end = body.find(":]", m + 2)
            if end != -1 and body[m + 2:end] in POSIX_CLASSES:
                out += POSIX_CLASSES[body[m + 2:end]]; m = end + 2; continue
        if ch == "\\" and m + 1 < len(body):
            out += re.escape(body[m + 1]); m += 2; continue
        if ch == "-" and 0 < m < len(body) - 1:
            out += "-"; m += 1; continue
        out += re.escape(ch); m += 1
    return out


def glob_to_regex(p):
    out, i, n = "", 0, len(p)
    while i < n:
        c = p[i]
        if c == "\\":
            if i + 1 < n:
                out += re.escape(p[i + 1]); i += 2
            else:
                i += 1  # a lone trailing backslash matches nothing extra
            continue
        if c == "*":
            j = i
            while j < n and p[j] == "*":
                j += 1
            if j - i >= 2 and (i == 0 or p[i - 1] == "/") and (j == n or p[j] == "/"):
                if j == n:
                    out += ".*"; i = j
                else:
                    out += "(?:.*/)?"; i = j + 1
                continue
            out += "[^/]*"; i = j; continue
        if c == "?":
            out += "[^/]"; i += 1; continue
        if c == "[":
            j = i + 1
            neg = j < n and p[j] in "!^"
            if neg:
                j += 1
            k = j + 1 if j < n and p[j] == "]" else j
            while k < n and p[k] != "]":
                if p[k] == "[" and p.startswith("[:", k):
                    end = p.find(":]", k + 2)
                    if end != -1:
                        k = end + 2; continue
                k += 2 if p[k] == "\\" else 1
            if k >= n:
                out += re.escape("["); i += 1; continue
            cls = class_to_regex(p[j:k])
            out += ("[^/" + cls + "]") if neg else ("(?!/)[" + cls + "]")
            i = k + 1; continue
        out += re.escape(c); i += 1
    return out


def parse_rules(text):
    rules, findings, seen = [], [], {}
    for no, raw in enumerate(text.split("\n"), 1):
        if raw.endswith("\r"):
            findings.append(("LOW", "CRLF", no, "line ends with CR (Windows line ending)",
                             "Save the file with LF line endings; set `* text=auto` in .gitattributes."))
            raw = raw[:-1]
        if not raw.strip() or raw.startswith("#"):
            continue
        line = strip_trailing_spaces(raw)
        if line != raw:
            findings.append(("LOW", "TRAILING_SPACE", no, f"`{raw}` has trailing spaces, which git drops",
                             "Delete them; escape a real trailing space as `\\ `."))
        if raw[0] in " \t":
            findings.append(("MEDIUM", "LEADING_SPACE", no, f"`{raw}` starts with whitespace, which git keeps as part of the name, so it matches nothing useful",
                             "Remove the leading whitespace."))
        m = re.search(r"\s#", line)
        if m:
            findings.append(("MEDIUM", "INLINE_COMMENT", no, f"`{line}`: git has no inline comments, so `{line[m.start():].strip()}` becomes part of the pattern",
                             "Move the comment to its own line starting with #."))
        if re.search(r"\\[A-Za-z0-9_]", line):
            findings.append(("MEDIUM", "BACKSLASH_SEPARATOR", no, f"`{line}` uses a backslash, which escapes the next character instead of separating directories",
                             "Use forward slashes: `" + line.replace("\\", "/") + "`."))
        key = line.strip()
        if key in seen:
            findings.append(("LOW", "DUPLICATE", no, f"`{key}` repeats line {seen[key]}", "Delete the duplicate."))
        else:
            seen[key] = no
        negate = line.startswith("!")
        body = line[1:] if negate else line
        if body.startswith("\\!") or body.startswith("\\#"):
            body = body[1:]
        if negate and not body:
            findings.append(("LOW", "EMPTY_NEGATION", no, "`!` alone does nothing", "Delete the line."))
            continue
        dir_only = body.endswith("/") and not body.endswith("\\/")
        if dir_only:
            body = body.rstrip("/")
        if not body:
            continue
        anchored = "/" in body
        if body.startswith("/"):
            body = body[1:]
        if any((m.start() > 0 and body[m.start() - 1] != "/") or (m.end() < len(body) and body[m.end()] != "/")
               for m in re.finditer(r"\*{2,}", body)):
            findings.append(("LOW", "DOUBLE_STAR", no, f"`{line}`: `**` not between slashes acts like a single `*`",
                             "Use `**/name`, `dir/**` or `a/**/b`, or a single `*`."))
        rules.append(Rule(no, line, negate, dir_only, anchored, body, re.compile(glob_to_regex(body), re.S)))
    return rules, findings


def last_match(rules, rel_path, is_dir):
    hit = None
    for r in rules:
        if r.matches(rel_path, is_dir):
            hit = r
    return hit


def check_path(rules, path):
    """Return (ignored, reason, blocked_rule) following git's parent-directory rule."""
    is_dir = path.endswith("/")
    parts = [p for p in path.strip("/").split("/") if p]
    for k in range(1, len(parts) + 1):
        sub = "/".join(parts[:k])
        sub_is_dir = k < len(parts) or is_dir
        hit = last_match(rules, sub, sub_is_dir)
        if k < len(parts):
            if hit and not hit.negate:
                own = last_match(rules, "/".join(parts), is_dir)
                blocked = own if own and own.negate else None
                return True, f"parent `{sub}/` excluded by {hit.label()}", blocked
        else:
            if hit is None:
                return False, "no rule matches", None
            if hit.negate:
                return False, f"re-included by {hit.label()}", None
            return True, f"ignored by {hit.label()}", None
    return False, "empty path", None


def literal_dirs(body):
    parts = body.split("/")[:-1]
    out = []
    for p in parts:
        if any(ch in p for ch in "*?[\\"):
            break
        out.append(p)
    return out


def lint_negations(rules):
    findings = []
    for idx, r in enumerate(rules):
        if not r.negate or not r.anchored:
            continue
        before = rules[:idx]
        dirs = literal_dirs(r.body)
        for k in range(1, len(dirs) + 1):
            sub = "/".join(dirs[:k])
            hit = last_match(before, sub, True)
            if hit and not hit.negate:
                findings.append(("HIGH", "NEGATION_BLOCKED", r.line_no,
                                 f"`{r.raw.strip()}` can never apply: its parent directory `{sub}/` is excluded by {hit.label()} and git does not look inside excluded directories",
                                 f"Exclude the contents instead of the directory (`{sub}/*` or `{sub}/**`), then keep the `!` rule; or re-include `!{sub}/` first."))
                break
    return findings


def lint_secrets(rules):
    findings = []
    missing = [p for p in SECRET_PROBES if not check_path(rules, p)[0]]
    if missing:
        findings.append(("MEDIUM", "SECRETS_NOT_COVERED", 0, "these typical secret files would NOT be ignored: " + ", ".join(missing),
                         "Add `.env`, `.env.*` (with `!.env.example`), `*.pem`, `*.key`, `id_rsa*` if the project could ever contain them."))
    if check_path(rules, ".env.example")[0]:
        findings.append(("LOW", "ENV_EXAMPLE_IGNORED", 0, "`.env.example` is ignored, so the documented template cannot be committed",
                         "Add `!.env.example` after the .env rule."))
    return findings


def explain(rule):
    body = rule.body
    kind = "directories" if rule.dir_only else "files or directories"
    verb = "Re-includes" if rule.negate else "Ignores"
    wild = any(ch in body for ch in "*?[")
    if rule.anchored and body.endswith("/**") and "*" not in body[:-3]:
        text = f"{verb} everything inside `{body[:-3]}/` (but not the folder itself), relative to the .gitignore folder"
    elif rule.anchored and body.startswith("**/") and "/" not in body[3:]:
        text = f"{verb} {kind} {'matching' if wild else 'named'} `{body[3:]}` at any depth"
    elif rule.anchored:
        text = f"{verb} {kind} {'matching' if wild else 'at'} `{body}` relative to the .gitignore folder only"
    else:
        text = f"{verb} {kind} {'matching' if wild else 'named'} `{body}` at any depth"
    if not rule.negate and not body.endswith("/**"):
        text += "; a matched directory hides everything inside it"
    if rule.negate:
        text += " (only works if no parent directory is excluded)"
    return text + "."


def read_tests(text):
    tests = []
    for no, line in enumerate(text.splitlines(), 1):
        s = line.strip()
        if not s or s.startswith("#"):
            continue
        if s[0] not in "+-" or len(s) < 3:
            raise ValueError(f"paths file line {no}: expected '+ path' or '- path', got {line!r}")
        tests.append((s[0] == "+", s[1:].strip()))
    return tests


def review(gi_text, tests_text=None):
    rules, findings = parse_rules(gi_text)
    findings += lint_negations(rules)
    findings += lint_secrets(rules)
    anchored = [r.line_no for r in rules if r.anchored and not r.negate and "/" in r.body
                and not r.raw.startswith(("/", "**/")) and not r.body.endswith("/**")]
    if anchored:
        findings.append(("INFO", "ANCHORED_BY_SLASH", anchored[0],
                         "lines " + ", ".join(map(str, anchored)) + " contain a slash, so they only match relative to the .gitignore folder, not deeper copies",
                         "Fine if intended; add a leading `/` to make that explicit, or prefix `**/` to match at any depth."))
    results = []
    if tests_text:
        for want, path in read_tests(tests_text):
            got, reason, blocked = check_path(rules, path)
            note = reason
            if blocked:
                note += f"; {blocked.label()} cannot re-include it"
            results.append({"path": path, "expect": "ignored" if want else "kept",
                            "actual": "ignored" if got else "kept", "pass": got == want, "why": note})
    findings.sort(key=lambda f: (SEV_ORDER[f[0]], f[2]))
    return {"rules": [{"line": r.line_no, "pattern": r.raw, "meaning": explain(r)} for r in rules],
            "findings": [dict(zip(("severity", "code", "line", "problem", "fix"), f)) for f in findings],
            "tests": results}


def render(name, rep):
    out = [f"# .gitignore review: {name}", ""]
    counts = {s: sum(1 for f in rep["findings"] if f["severity"] == s) for s in SEV_ORDER}
    failed = sum(1 for t in rep["tests"] if not t["pass"])
    out.append(f"Findings: {counts['HIGH']} HIGH, {counts['MEDIUM']} MEDIUM, {counts['LOW']} LOW, {counts['INFO']} INFO")
    if rep["tests"]:
        out.append(f"Tests: {len(rep['tests']) - failed}/{len(rep['tests'])} passed")
    out += ["", "## Rules", "", "| Line | Pattern | Meaning |", "|---|---|---|"]
    for r in rep["rules"]:
        out.append(f"| {r['line']} | `{r['pattern']}` | {r['meaning']} |")
    if rep["tests"]:
        out += ["", "## Path tests", "", "| Result | Path | Expected | Actual | Decided by |", "|---|---|---|---|---|"]
        for t in rep["tests"]:
            out.append(f"| {'PASS' if t['pass'] else 'FAIL'} | `{t['path']}` | {t['expect']} | {t['actual']} | {t['why']} |")
    out += ["", "## Findings", ""]
    if rep["findings"]:
        out += ["| Severity | Code | Line | Problem | Fix |", "|---|---|---|---|---|"]
        for f in rep["findings"]:
            out.append(f"| {f['severity']} | {f['code']} | {f['line'] or '-'} | {f['problem']} | {f['fix']} |")
    else:
        out.append("No findings.")
    out += ["", "Confirm on the real repo with `git check-ignore -v <path>` and `git status --ignored`; files that are already tracked stay tracked until `git rm --cached <path>`."]
    return "\n".join(out)


def self_test():
    cases = []
    def t(name, cond):
        cases.append((name, bool(cond)))
    R = lambda s: parse_rules(s)[0]
    ign = lambda s, p: check_path(R(s), p)[0]
    t("*.log matches at any depth", ign("*.log", "a/b/c.log") and ign("*.log", "x.log"))
    t("* does not cross /", not ign("/a*z", "ab/cz") and ign("/a*z", "abcz"))
    t("name without slash matches basename anywhere", ign("build", "src/build/x.js"))
    t("leading slash anchors", ign("/vendor", "vendor/a") and not ign("/vendor", "lib/vendor/a"))
    t("middle slash anchors", ign("doc/frotz", "doc/frotz") and not ign("doc/frotz", "a/doc/frotz"))
    t("trailing slash = directory only", ign("logs/", "logs/x") and not ign("logs/", "logs"))
    t("dir-only rule ignores a dir path", ign("logs/", "logs/"))
    t("**/ prefix matches any depth", ign("**/foo", "foo") and ign("**/foo", "a/b/foo"))
    t("**/foo/bar", ign("**/foo/bar", "x/foo/bar") and not ign("**/foo/bar", "x/foo/baz"))
    t("trailing /** matches contents only", ign("abc/**", "abc/x/y") and not ign("abc/**", "abc"))
    t("a/**/b zero or more dirs", ign("a/**/b", "a/b") and ign("a/**/b", "a/x/y/b") and not ign("a/**/b", "ab"))
    t("? matches one char not /", ign("/a?c", "abc") and not ign("/a?c", "a/c"))
    t("[abc] class", ign("file[12].txt", "file1.txt") and not ign("file[12].txt", "file3.txt"))
    t("[!x] negated class", ign("f[!0-9]", "fa") and not ign("f[!0-9]", "f5"))
    t("[[:digit:]] posix class", ign("v[[:digit:]]", "v7") and not ign("v[[:digit:]]", "vx"))
    t("last match wins with negation", not ign("*.txt\n!keep.txt", "keep.txt") and ign("*.txt\n!keep.txt", "a.txt"))
    t("order matters", ign("!keep.txt\n*.txt", "keep.txt"))
    t("parent dir excluded blocks negation", ign("logs/\n!logs/.gitkeep", "logs/.gitkeep"))
    t("dir/* allows negation", not ign("logs/*\n!logs/.gitkeep", "logs/.gitkeep"))
    t("dir/** allows negation", not ign("coverage/**\n!coverage/README.md", "coverage/README.md"))
    t("re-include dir then file", not ign("/a/*\n!/a/b/\n/a/b/*\n!/a/b/c", "a/b/c"))
    t("escaped # and !", ign("\\#notes", "#notes") and ign("\\!bang", "!bang"))
    t("comment lines skipped", not ign("# *.log", "a.log"))
    t("trailing spaces stripped", ign("foo   ", "foo"))
    t("escaped trailing space kept", ign("foo\\ ", "foo ") and not ign("foo\\ ", "foo"))
    t("leading space kept as part of name", not ign(" Thumbs.db", "Thumbs.db"))
    t("inline comment becomes pattern", not ign("secrets.yml # x", "secrets.yml"))
    t("backslash escapes, not a separator", not ign("src\\gen", "src/gen/a.ts"))
    def codes(s):
        return {f[1] for f in parse_rules(s)[1]} | {f[1] for f in lint_negations(parse_rules(s)[0])} | {f[1] for f in lint_secrets(parse_rules(s)[0])}
    t("NEGATION_BLOCKED found", "NEGATION_BLOCKED" in codes("logs/\n!logs/.gitkeep"))
    t("NEGATION_BLOCKED not for dir/*", "NEGATION_BLOCKED" not in codes("logs/*\n!logs/.gitkeep"))
    t("NEGATION_BLOCKED via dir/* deeper", "NEGATION_BLOCKED" in codes("a/*\n!a/b/c.txt"))
    t("INLINE_COMMENT found", "INLINE_COMMENT" in codes("x.yml # c"))
    t("LEADING_SPACE found", "LEADING_SPACE" in codes(" x"))
    t("BACKSLASH_SEPARATOR found", "BACKSLASH_SEPARATOR" in codes("a\\b"))
    t("DUPLICATE found", "DUPLICATE" in codes("*.log\n*.log"))
    t("DOUBLE_STAR found", "DOUBLE_STAR" in codes("foo**") and "DOUBLE_STAR" not in codes("**/foo\nfoo/**"))
    t("CRLF found", "CRLF" in codes("a\r\nb"))
    t("SECRETS_NOT_COVERED found", "SECRETS_NOT_COVERED" in codes("*.log"))
    t("secrets covered", "SECRETS_NOT_COVERED" not in codes(".env\n.env.*\n!.env.example\n*.key\n*.pem\nid_rsa*"))
    t("ENV_EXAMPLE_IGNORED found", "ENV_EXAMPLE_IGNORED" in codes(".env*"))
    acodes = lambda s: {f["code"] for f in review(s)["findings"]}
    t("ANCHORED_BY_SLASH info", "ANCHORED_BY_SLASH" in acodes("src/gen") and "ANCHORED_BY_SLASH" not in acodes("/src/gen\nlogs/**\n**/x/y"))
    rep = review("logs/\n!logs/.gitkeep\n", "+ logs/a.log\n- logs/.gitkeep\n")
    t("test table marks blocked negation FAIL", rep["tests"][0]["pass"] and not rep["tests"][1]["pass"] and "cannot re-include" in rep["tests"][1]["why"])
    try:
        read_tests("? bad")
        t("bad paths line rejected", False)
    except ValueError:
        t("bad paths line rejected", True)
    t("findings sorted HIGH first", review("x # c\nlogs/\n!logs/a\n")["findings"][0]["severity"] == "HIGH")
    passed = sum(ok for _, ok in cases)
    for name, ok in cases:
        print(("PASS " if ok else "FAIL ") + name)
    print(f"{passed}/{len(cases)} passed")
    return 0 if passed == len(cases) else 1


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("gitignore", nargs="?")
    ap.add_argument("--test", help="paths file with '+ path' (must be ignored) and '- path' (must be kept)")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args()
    if a.self_test:
        return self_test()
    if not a.gitignore:
        ap.print_usage(sys.stderr); return 2
    try:
        gi = open(a.gitignore, encoding="utf-8", newline="").read()
        tests = open(a.test, encoding="utf-8").read() if a.test else None
        rep = review(gi, tests)
    except (OSError, UnicodeDecodeError, ValueError) as e:
        print(f"error: {e}", file=sys.stderr); return 2
    print(json.dumps(rep, indent=2) if a.json else render(a.gitignore, rep))
    bad = any(f["severity"] == "HIGH" for f in rep["findings"]) or any(not t["pass"] for t in rep["tests"])
    return 1 if bad else 0


if __name__ == "__main__":
    sys.exit(main())
Best PracticesPythonDebugging+2
F@f
0
Tác vụ Nền (Background Jobs) taste
Văn bản

Các quy ước mang tính định hướng rõ ràng để xây dựng hàng đợi tác vụ nền (background job queues) và các worker đáng tin cậy.

# Tác vụ Nền (Background Jobs) taste của một kỹ sư độ tin cậy backend
- Thiết kế mọi tác vụ (job) có tính lũy đẳng (idempotent) để việc thử lại (retry) hoặc gửi trùng lặp không bao giờ trừ tiền thẻ hay gửi email hai lần. Confidence: 0.95
- Chỉ truyền các ID trong phần dữ liệu (payload) của tác vụ và tải dữ liệu mới nhất bên trong worker thay vì tuần tự hóa (serialize) toàn bộ bản ghi. Confidence: 0.90
- Đưa các tác vụ vào hàng đợi (enqueue) trong cùng một giao dịch cơ sở dữ liệu (database transaction) với thao tác ghi đã kích hoạt chúng, bằng cách sử dụng bảng hộp thư đi (outbox table) hoặc hàng đợi dựa trên Postgres. Confidence: 0.85
- Ưu tiên sử dụng một hệ thống hàng đợi đơn giản, ổn định và được hỗ trợ tốt như Sidekiq, BullMQ, Celery hoặc hàng đợi Postgres như Graphile Worker thay vì tự xây dựng mới. Confidence: 0.80
- Thử lại với thời gian chờ tăng theo cấp số nhân (exponential backoff) kèm độ lệch ngẫu nhiên (jitter), và giới hạn số lần thử để các tác vụ lỗi độc hại (poison jobs) được chuyển vào hàng đợi thư chết (dead letter queue). Confidence: 0.95
- Phân loại lỗi thành lỗi có thể thử lại (retryable) hoặc lỗi vĩnh viễn (permanent), và cho dừng ngay lập tức các lỗi vĩnh viễn thay vì lãng phí số lần thử lại. Confidence: 0.85
- Thiết lập thời gian chờ tối đa (timeout) rõ ràng cho mọi tác vụ và cho mọi lệnh gọi mạng mà tác vụ đó thực hiện. Confidence: 0.90
- Giữ cho các tác vụ nhỏ gọn và chạy nhanh, đồng thời chia nhỏ các công việc xử lý hàng loạt (batch work) kéo dài thành từng phần (chunks) có thể tiếp tục chạy lại từ điểm đánh dấu (checkpoint). Confidence: 0.85
- Sử dụng các hàng đợi riêng biệt cho công việc nhạy cảm với độ trễ và công việc xử lý số lượng lớn (bulk work) để tác vụ nạp bù dữ liệu (backfill) không bao giờ làm chậm email đặt lại mật khẩu. Confidence: 0.90
- Giới hạn tần suất (rate limit) các tác vụ gọi API bên thứ ba theo từng nhà cung cấp dịch vụ, chứ không phải theo từng tiến trình worker. Confidence: 0.80
- Sử dụng khóa tác vụ duy nhất (unique job keys) hoặc cơ chế khóa (locks) để ngăn cùng một tác vụ định kỳ chạy hai lần cùng lúc. Confidence: 0.85
- Chạy các lịch trình kiểu cron từ một bộ lập lịch (scheduler) duy nhất có cơ chế bầu chọn nút dẫn đầu (leader election), tuyệt đối không chạy từ mọi thực thể (instance) của ứng dụng. Confidence: 0.80
- Đánh phiên bản (version) cho dữ liệu đầu vào (payload) của tác vụ và đảm bảo các worker vẫn đọc được phiên bản cũ trong quá trình triển khai (deploy) bản mới. Confidence: 0.80
- Xử lý tín hiệu SIGTERM một cách êm đẹp (gracefully): ngừng nhận tác vụ mới, hoàn tất hoặc đưa lại tác vụ hiện tại vào hàng đợi, sau đó mới thoát. Confidence: 0.90
- Ghi nhận mã tác vụ (job ID), số thứ tự lần thử (attempt number) và mã tương quan (correlation ID) trên mọi dòng nhật ký (log) phát sinh từ một tác vụ. Confidence: 0.85
- Cảnh báo dựa trên độ sâu của hàng đợi, tuổi của tác vụ cũ nhất và mức tăng trưởng của hàng đợi thư chết (dead letter queue), chứ không chỉ cảnh báo khi worker bị sập. Confidence: 0.90
- Phân công người phụ trách rõ ràng cho hàng đợi thư chết và có tài liệu hướng dẫn lệnh chạy lại (replay command). Confidence: 0.75
- Kiểm thử các tác vụ bằng cách gọi trực tiếp hàm xử lý (handler) trong kiểm thử đơn vị (unit tests) và chạy một bài kiểm thử đầu-cuối (end-to-end) thông qua hàng đợi thật trong hệ thống CI. Confidence: 0.80
- Tránh nối chuỗi quá nhiều tác vụ thành các luồng công việc ngầm phức tạp; hãy sử dụng một công cụ điều phối luồng công việc (workflow engine) như Temporal khi các bước cần được điều phối chặt chẽ. Confidence: 0.70
- Lưu trữ kết quả tác vụ và các dấu hiệu đánh dấu hiệu ứng phụ (side-effect markers) trong cơ sở dữ liệu, chứ không chỉ lưu trong hệ thống lưu trữ của hàng đợi. Confidence: 0.75
BackendBest PracticesDevOps+2
F@f
0
Trước2 / 2