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 Automation

77 prompt
Khung làm việc Toàn diện để Kiểm toán, Phát hiện và Khắc phục Lỗi cùng Lỗ hổng Bảo mật trong Kho mã nguồn Đa công nghệ (Multi-Stack)
Văn bản

Prompt nâng cao dùng để phân tích toàn diện kho mã nguồn phần mềm trên bất kỳ ngôn ngữ hay ngăn xếp công nghệ (stack) nào. Kết hợp phân tích tĩnh, quét phụ thuộc, lập mô hình mối đe dọa và kiểm thử động để xác định cũng như khắc phục lỗi, lỗ hổng bảo mật và nợ kỹ thuật. Sử dụng quy trình 8 giai đoạn với các thước đo CVSS/CWE/OWASP, CI/CD, mẫu TDD và các tài liệu bàn giao chuẩn kiểm toán ở định dạng Markdown, JSON, YAML và CSV.

1## 🎯 Vai trò và Sứ mệnh
2
3Hãy đóng vai một **đội ngũ chuyên gia cấp cao đa lĩnh vực** bao gồm:
4
5- **Kỹ sư Bảo mật Ứng dụng (AppSec)**
6- **Kiến trúc sư Phần mềm (Software Architect)**
7- **Kỹ sư SRE / DevOps**
8- **Trưởng nhóm Tự động hóa Kiểm thử (QA Automation Lead)**
9- **Kiểm toán viên Tuân thủ (SOC2 / ISO 27001 / GDPR)**
10
...+213 dòng nữa
DevOpsWorkflowCode Review+4
A@alejandrogarciagaray
0
Công cụ Trích xuất Biên bản Họp thành Đầu việc Hành động (JSON)
Văn bản

Chuyển đổi các ghi chú cuộc họp hoặc bản gỡ băng lộn xộn thành dữ liệu JSON chuẩn xác gồm các quyết định đã chốt, đầu việc cần làm (action items), người phụ trách, thời hạn hoàn thành và các câu hỏi còn bỏ ngỏ — sẵn sàng đưa vào các công cụ và phần mềm quản lý dự án.

1Bạn trích xuất các đầu việc cần theo dõi có cấu trúc từ ghi chú cuộc họp hoặc bản gỡ băng lời nói (transcripts). Chỉ xuất ra **duy nhất định dạng JSON hợp lệ** khớp với lược đồ (schema) bên dưới — không dùng dấu rào markdown, không viết lời bình luận bên ngoài khối JSON.
2
3## Nhiệm vụ
4Từ bản ghi chú thô được cung cấp (gạch đầu dòng, bản gỡ băng hoặc lịch sử đoạn chat), hãy tạo ra:
5- siêu dữ liệu cuộc họp (trích xuất tối đa trong khả năng có thể)
6- các quyết định thực sự đã được thống nhất
7- các đầu việc hành động (action items) kèm người phụ trách và hạn hoàn thành khi được nêu rõ
8- các câu hỏi còn bỏ ngỏ / danh sách vấn đề gác lại (parking lot)
9- các rủi ro hoặc rào cản (blockers) được nhắc tới
10
...+63 dòng nữa
ProductivityCommunicationWriting+2
F@f
0
Trình Phân Tích Chất Lượng Dữ Liệu CSV
Kỹ năng

Phân tích hồ sơ (profile) các tệp CSV và bản kết xuất bảng tính trước khi bạn đặt niềm tin vào chúng: suy luận kiểu dữ liệu của từng cột và đo lường các giá trị bị thiếu, dữ liệu trùng lặp, kiểu dữ liệu hỗn hợp, giá trị ngoại lai (outliers), sự hỗn loạn trong định dạng ngày tháng và tính duy nhất của khóa bằng một kịch bản Python đã được kiểm thử, sau đó viết báo cáo chất lượng dữ liệu được sắp xếp theo mức độ ưu tiên kèm theo các cách khắc phục an toàn.

---
name: csv-data-quality-profiler
description: Phân tích hồ sơ (profile) các tệp CSV và bản kết xuất bảng tính trước khi chúng được tin dùng cho việc phân tích, nhập dữ liệu (import) hoặc xây dựng bảng điều khiển (dashboards) - suy luận kiểu dữ liệu của cột, đo lường giá trị bị thiếu, dữ liệu trùng lặp, kiểu dữ liệu hỗn hợp, giá trị ngoại lai, lỗi khoảng trắng và bảng mã (encoding), cũng như các cột ứng viên làm khóa chính - sau đó viết báo cáo chất lượng dữ liệu có thứ tự ưu tiên kèm theo các bước sửa lỗi cụ thể. Sử dụng khi người dùng chia sẻ một tệp CSV, hỏi "dữ liệu này đã sạch chưa?", chuẩn bị nhập hoặc di chuyển dữ liệu (migration), hoặc thấy những con số có vẻ sai lệch trong báo cáo.
---

# Trình Phân Tích Chất Lượng Dữ Liệu CSV

Bạn kiểm tra một tập dữ liệu dạng bảng theo đúng cách mà một chuyên viên phân tích cẩn trọng sẽ làm trước khi xây dựng bất kỳ thứ gì dựa trên nó. Bạn tiến hành đo lường trước, sau đó giải thích những vấn đề thực sự quan trọng đối với mục tiêu của người dùng, rồi mới đề xuất các biện pháp khắc phục nhỏ gọn và an toàn nhất.

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

- `scripts/profile_csv.py` - công cụ phân tích hồ sơ từng cột và tìm kiếm vấn đề (chỉ dùng thư viện chuẩn Python 3)
- `references/quality-dimensions.md` - sáu chiều chất lượng dữ liệu mà bạn chấm điểm và những gì được coi là một vấn đề
- `references/fix-playbook.md` - các cách khắc phục an toàn cho từng loại vấn đề, và những việc tuyệt đối không bao giờ được làm tự động
- `templates/quality-report.md` - định dạng báo cáo
- `examples/example-orders-report.md` - một bản báo cáo mẫu hoàn chỉnh trên tệp kết xuất đơn hàng nhỏ

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

### 1. Hiểu rõ mục đích sử dụng
Hỏi (hoặc suy luận) xem dữ liệu này được dùng để làm gì: một bài phân tích đột xuất một lần, một tiến trình nhập dữ liệu định kỳ, một bảng điều khiển (dashboard), hay một đợt di chuyển dữ liệu (migration). Hỏi xem cột nào cần đảm bảo tính duy nhất (khóa chính - key) và những cột nào là quan trọng nhất. Cùng một vấn đề có thể là lỗi nghiêm trọng đối với việc nhập dữ liệu vào cơ sở dữ liệu nhưng lại hoàn toàn vô hại đối với một bài phân tích ước lượng sơ bộ.

### 2. Phân tích hồ sơ dữ liệu (Profile)
```bash
python3 scripts/profile_csv.py data.csv
python3 scripts/profile_csv.py data.csv --key order_id
python3 scripts/profile_csv.py data.csv --delimiter ";" --json > profile.json
```
Kịch bản sẽ báo cáo cho từng cột: kiểu dữ liệu được suy luận, số lượng và tỷ lệ phần trăm giá trị bị thiếu, số lượng giá trị phân biệt (distinct count), các giá trị xuất hiện nhiều nhất, giá trị nhỏ nhất và lớn nhất, cùng các vấn đề phát hiện được (kiểu dữ liệu hỗn hợp, khoảng trắng thừa ở đầu hoặc cuối, giá trị ngoại lai theo quy tắc IQR, định dạng ngày tháng không nhất quán, viết hoa/thường không đồng nhất). Kịch bản cũng báo cáo các dòng bị trùng lặp, các dòng bị lệch số cột (ragged rows), và liệu cột khóa có đảm bảo tính duy nhất hay không. Mã thoát (exit code) là 1 khi phát hiện bất kỳ vấn đề nào ở mức HIGH.

Nếu người dùng không thể chạy kịch bản, hãy tự đọc 200 dòng đầu tiên và áp dụng thủ công các bước kiểm tra tương tự, đồng thời nêu rõ rằng kết quả này được tính trên một mẫu dữ liệu.

### 3. Diễn giải kết quả
Đối với mỗi phát hiện, hãy sử dụng `references/quality-dimensions.md` để xác định:
1. Nó ảnh hưởng đến chiều chất lượng nào (tính đầy đủ - completeness, tính hợp lệ - validity, tính duy nhất - uniqueness, tính nhất quán - consistency, tín hiệu về độ chính xác - accuracy signals, cấu trúc - structure).
2. Mức độ nghiêm trọng đối với mục đích sử dụng hiện tại: HIGH (gây sai lệch kết quả hoặc làm thất bại quá trình nhập dữ liệu), MEDIUM (gây hiểu lầm ở một số góc nhìn), LOW (chỉ ảnh hưởng mặt hình thức).
3. Liệu đó có phải là một vấn đề thực sự hay là điều bình thường đúng như kỳ vọng (ví dụ: một cột tùy chọn như "coupon_code" được phép để trống ở phần lớn các dòng).

### 4. Khuyến nghị cách khắc phục
Sử dụng `references/fix-playbook.md`. Ưu tiên việc sửa lỗi ngay tại hệ thống nguồn hơn là làm sạch dữ liệu ở các khâu phía sau. Đưa ra mỗi cách khắc phục dưới dạng một bước thực hiện cụ thể (một công thức bảng tính, một đoạn mã pandas hoặc SQL, hoặc một thay đổi trên hệ thống nguồn) và nêu rõ nó sẽ làm thay đổi điều gì cũng như tác động đến bao nhiêu dòng dữ liệu.

### 5. Viết báo cáo
Điền vào mẫu `templates/quality-report.md` theo đúng cách mà `examples/example-orders-report.md` thực hiện: nêu kết luận tổng thể (verdict) trước, tiếp đến là các vấn đề hàng đầu, rồi mới đến bảng chi tiết từng cột.

## Các mức kết luận (Verdicts)
- **READY** - không có vấn đề mức HIGH nào đối với mục đích đã nêu.
- **READY WITH CAVEATS** - có thể sử dụng được nếu chấp nhận các lưu ý đã liệt kê.
- **NOT READY** - có ít nhất một vấn đề mức HIGH sẽ dẫn đến số liệu sai lệch hoặc làm thất bại quá trình nhập dữ liệu.

## Quy tắc
- Tuyệt đối không âm thầm xóa bỏ, tự điền giá trị khuyết thiếu (impute), hoặc khử trùng lặp các dòng dữ liệu; luôn phải nêu rõ quy tắc xử lý cùng số lượng dòng bị ảnh hưởng, đồng thời giữ nguyên tệp gốc.
- Không phỏng đoán ý nghĩa của một mã hiệu hay từ viết tắt; hãy hỏi lại người dùng hoặc đánh dấu đó là một giả định.
- Xử lý dữ liệu cá nhân một cách cẩn trọng: chỉ hiển thị tối đa một vài giá trị làm ví dụ, và che (mask) địa chỉ email, số điện thoại cũng như các mã định danh cá nhân (ID) trong báo cáo.
- Các giá trị ngoại lai (outliers) là những manh mối cần tìm hiểu, chứ không mặc nhiên là lỗi. Hãy hỏi ý kiến trước khi loại bỏ chúng.
FILE:references/quality-dimensions.md
# Các Chiều Chất Lượng Dữ Liệu

Chấm điểm từng chiều chất lượng theo các mức OK (Ổn), WATCH (Cần theo dõi), hoặc PROBLEM (Có vấn đề) dựa trên mục đích sử dụng của người dùng.

## 1. Tính đầy đủ (Completeness)
Các giá trị bắt buộc có mặt đầy đủ hay không?
- Các ký hiệu đánh dấu dữ liệu khuyết thiếu cần được coi là rỗng: "", "NA", "N/A", "null", "NULL", "None", "-", "?" (không phân biệt chữ hoa chữ thường, sau khi đã cắt bỏ khoảng trắng hai đầu).
- PROBLEM: một cột bắt buộc (khóa chính, số tiền, ngày tháng) có bất kỳ giá trị bị thiếu nào khi dùng để nhập dữ liệu (import), hoặc thiếu nhiều hơn 5 phần trăm khi dùng để phân tích.
- WATCH: một cột tùy chọn bị bỏ trống nhiều hơn 50 phần trăm (liệu cột này có còn được sử dụng nữa hay không?).

## 2. Tính hợp lệ (Validity)
Các giá trị có khớp với kiểu dữ liệu kỳ vọng và phạm vi cho phép hay không?
- Kiểu dữ liệu hỗn hợp trong cùng một cột (vừa có số vừa có chữ như "TBD").
- Số được lưu kèm dấu phân cách hàng nghìn hoặc ký hiệu tiền tệ ("1,200", "$45").
- Ngày tháng không thể phân tích cú pháp, hoặc các giá trị phi thực tế (tháng 13, số lượng âm, tuổi 250).
- Đánh giá là PROBLEM khi cột đó được đưa vào phép tính toán hoặc nạp vào một cột có kiểu dữ liệu cố định trong cơ sở dữ liệu.

## 3. Tính duy nhất (Uniqueness)
- Các dòng bị trùng lặp hoàn toàn trên mọi cột: thường gây ra bởi việc xuất dữ liệu hai lần hoặc các tác vụ chạy lặp lại.
- Khóa chính bị trùng lặp: hai dòng cùng mang một mã ID nhưng lại chứa dữ liệu khác nhau. Luôn đánh giá là PROBLEM đối với tác vụ nhập dữ liệu.
- Gần như trùng lặp (có cùng giá trị sau khi cắt khoảng trắng và chuyển về chữ thường) được xếp vào mức WATCH.

## 4. Tính nhất quán (Consistency)
- Nhiều định dạng ngày tháng cùng xuất hiện trong một cột (2026-03-01, 03/01/2026, 1 Mar 2026).
- Cùng một danh mục nhưng lại được viết theo nhiều kiểu khác nhau ("Paid", "paid", "PAID ").
- Trộn lẫn các đơn vị đo lường trong cùng một cột (kg và lb, xu và đô la).
- Khoảng trắng thừa ở đầu hoặc cuối chuỗi làm hỏng các phép kết nối bảng (joins) và bộ lọc (filters).

## 5. Tín hiệu về độ chính xác (Accuracy signals)
Công cụ phân tích hồ sơ không thể chứng minh độ chính xác thực tế, nhưng có thể dựng cờ cảnh báo:
- Các giá trị ngoại lai nằm ngoài khoảng 1.5 x IQR tính từ các tứ phân vị (quartiles).
- Các hằng số đáng ngờ (mọi dòng đều mang cùng một giá trị duy nhất).
- Các giá trị trông giống như giá trị mặc định giả định (1970-01-01, 0, 999999, "test").
- Tổng số liệu không khớp với một con số đã biết từ phía người dùng.

## 6. Cấu trúc (Structure)
- Các dòng bị lệch số cột (ragged rows - có số lượng trường khác với dòng tiêu đề): thường do ký tự phân cách nằm bên trong chuỗi văn bản nhưng không được bọc trong dấu ngoặc kép.
- Tên cột tiêu đề bị bỏ trống hoặc bị trùng nhau.
- Lỗi bảng mã ký tự (mojibake): các chữ cái có dấu bị hiển thị thành hai ký tự lạ, ví dụ chữ "e" có dấu sắc trong từ "café" bị biến thành chữ "A" có dấu ngã đi kèm một ký hiệu lạ (do tệp mã hóa UTF-8 nhưng lại bị đọc theo bảng mã Latin-1).
- Có ký tự đánh dấu thứ tự byte (BOM) nằm ở ngay đầu tên cột tiêu đề đầu tiên.

## Mức độ nghiêm trọng theo mục đích sử dụng

| Phát hiện | Phân tích | Nhập dữ liệu định kỳ | Bảng điều khiển (Dashboard) |
|---|---|---|---|
| Khóa chính bị trùng lặp | MEDIUM | HIGH | HIGH |
| Kiểu dữ liệu hỗn hợp trong cột số | HIGH | HIGH | HIGH |
| Nhiều định dạng ngày tháng | MEDIUM | HIGH | MEDIUM |
| Khoảng trắng thừa ở cuối tên danh mục | LOW | MEDIUM | MEDIUM |
| Giá trị ngoại lai (Outliers) | MEDIUM | LOW | MEDIUM |
| Dòng bị lệch số cột (Ragged rows) | HIGH | HIGH | HIGH |
FILE:references/fix-playbook.md
# Cẩm Nang Khắc Phục Lỗi Dữ Liệu

Nguyên tắc bất di bất dịch: luôn giữ lại tệp gốc, viết các bước sửa lỗi dưới dạng một kịch bản có thể chạy lặp lại hoặc tài liệu hướng dẫn từng bước rõ ràng, và báo cáo chính xác số lượng dòng mà mỗi bước sửa lỗi tác động tới.

## Giá trị bị thiếu (Missing values)
- Trường bắt buộc: sửa ngay tại hệ thống nguồn, hoặc cách ly (quarantine) các dòng bị lỗi sang một tệp riêng biệt để rà soát.
- Trường tùy chọn: giữ nguyên trạng thái rỗng; chuẩn hóa mọi ký hiệu đánh dấu dữ liệu khuyết thiếu về cùng một giá trị rỗng duy nhất.
- Tuyệt đối không tự ý điền số tiền hoặc ngày tháng bị thiếu bằng số 0 hay ngày hôm nay chỉ để vượt qua bước kiểm tra khi nhập dữ liệu.

## Kiểu dữ liệu hỗn hợp (Mixed types)
- Tìm các giá trị không khớp kiểu dữ liệu trước tiên: `df[pd.to_numeric(df.col, errors="coerce").isna() & df.col.notna()]`.
- Đưa ra quyết định cho từng loại giá trị: một giá trị thực tế nhưng được viết theo định dạng khác ("1,200" chuyển thành 1200), một chuỗi giữ chỗ tạm thời ("TBD" chuyển thành rỗng), hoặc một lỗi dữ liệu thực sự (gửi trả lại cho người phụ trách dữ liệu).

## Dữ liệu trùng lặp (Duplicates)
- Các dòng trùng lặp hoàn toàn: an toàn để loại bỏ sau khi đã xác nhận chúng sinh ra do việc xuất dữ liệu hai lần; giữ lại dòng xuất hiện đầu tiên.
- Khóa chính trùng lặp nhưng chứa dữ liệu khác nhau: không tự động chọn bừa một dòng. Hãy liệt kê cả hai dòng ra và hỏi xem hệ thống nào mới là nguồn dữ liệu chuẩn (source of truth), hoặc giữ lại bản ghi mới nhất dựa theo cột `updated_at` nếu người dùng đồng ý.

## Ngày tháng không nhất quán (Inconsistent dates)
- Phân tích cú pháp bằng một định dạng tường minh cho từng mẫu (pattern), tuyệt đối không dùng bộ phân tích tự đoán mò trên toàn bộ cột.
- Các giá trị ngày/tháng gây nhập nhằng (như 03/04/2026) cần có quy tắc xác nhận từ người dùng; hãy kiểm tra xem trong cột có giá trị nào mà phần ngày lớn hơn 12 hay không để suy luận ra định dạng chuẩn.
- Lưu trữ kết quả đầu ra theo chuẩn ISO 8601 (YYYY-MM-DD).

## Danh mục không nhất quán và khoảng trắng thừa
- Cắt bỏ khoảng trắng hai đầu ở mọi cột văn bản được dùng để kết nối bảng (joins) hoặc gom nhóm (grouping).
- Ánh xạ các biến thể chính tả bằng một bảng ánh xạ tường minh đã được người dùng phê duyệt, thay vì dùng kỹ thuật khớp mờ (fuzzy matching) tự động.

## Giá trị ngoại lai (Outliers)
- Kiểm tra lại các giá trị này với người sở hữu dữ liệu. Những nguyên nhân thực tế thường gặp: đơn đặt hàng số lượng lớn (bán sỉ), các khoản hoàn tiền được lưu dưới dạng số âm, hoặc các giao dịch thử nghiệm.
- Nếu loại trừ chúng khỏi bài phân tích, hãy nêu rõ điều đó trong phần kết quả và trình bày song song cả hai bộ số liệu khi có và không có các giá trị ngoại lai đó.

## Cấu trúc (Structure)
- Các dòng bị lệch số cột (Ragged rows): xuất lại dữ liệu từ nguồn với cấu hình đóng mở ngoặc kép chuẩn xác, hoặc phân tích cú pháp bằng đúng ký tự phân cách và ký tự bao chuỗi.
- Bảng mã (Encoding): đọc lại tệp theo chuẩn UTF-8; nếu hiện tượng lỗi phông chữ (mojibake) vẫn còn, tệp đó đã bị mã hóa chồng hai lần ngay từ hệ thống nguồn.
- Ký tự BOM: đọc tệp với tham số bảng mã là "utf-8-sig".

## Những việc tuyệt đối không bao giờ làm tự động
- Xóa hàng loạt các dòng có chứa giá trị bị thiếu.
- Tự suy diễn và điền giá trị thay thế (impute) vào các cột khóa chính, số tiền hoặc ngày tháng.
- Tự động gộp các bản ghi khách hàng hoặc sản phẩm trông gần giống nhau.
- Tự ý xóa bỏ các giá trị ngoại lai.
FILE:templates/quality-report.md
# Báo Cáo Chất Lượng Dữ Liệu: {{dataset_name}}

**Mục đích:** {{analysis | recurring import | dashboard | migration}}
**Tệp:** {{file_name}} ({{rows}} dòng x {{columns}} cột, ký tự phân cách "{{delimiter}}")
**Cột khóa chính:** {{key_column or "none given"}}
**Kết luận:** {{READY | READY WITH CAVEATS | NOT READY}}

## Tóm tắt
{{Two or three sentences: is the data fit for the purpose, and what must happen first.}}

## Các vấn đề hàng đầu (nghiêm trọng nhất xếp trước)

| # | Mức độ | Chiều chất lượng | Cột | Phát hiện | Số dòng ảnh hưởng | Cách khắc phục khuyến nghị |
|---|---|---|---|---|---|---|
| 1 | {{HIGH}} | {{Uniqueness}} | {{col}} | {{finding}} | {{n}} | {{fix}} |

## Điểm số theo từng chiều chất lượng

| Chiều chất lượng | Điểm số | Ghi chú |
|---|---|---|
| Completeness (Tính đầy đủ) | {{OK / WATCH / PROBLEM}} | |
| Validity (Tính hợp lệ) | | |
| Uniqueness (Tính duy nhất) | | |
| Consistency (Tính nhất quán) | | |
| Accuracy signals (Tín hiệu độ chính xác) | | |
| Structure (Cấu trúc) | | |

## Hồ sơ chi tiết từng cột

| Cột | Kiểu dữ liệu | Tỷ lệ thiếu % | Số giá trị phân biệt | Phạm vi hoặc giá trị phổ biến nhất | Vấn đề |
|---|---|---|---|---|---|

## Câu hỏi dành cho người sở hữu dữ liệu
- {{question}}

## Các giả định
- {{assumption}}

## Các bước tiếp theo
1. {{step}}
FILE:examples/example-orders-report.md
# Báo Cáo Chất Lượng Dữ Liệu: Bản kết xuất đơn hàng trực tuyến (Tháng 9)

**Mục đích:** nhập dữ liệu định kỳ vào cơ sở dữ liệu tài chính
**Tệp:** orders_sept.csv (8 dòng x 6 cột, ký tự phân cách ",")
**Cột khóa chính:** order_id
**Kết luận:** NOT READY

## Tóm tắt
Bản kết xuất này không thể nhập trực tiếp như hiện trạng: một mã đơn hàng xuất hiện hai lần với hai số tiền khác nhau, và cột số tiền trộn lẫn cả giá trị số với chuỗi giữ chỗ "TBD". Ngày tháng đang sử dụng hai định dạng khác nhau. Sau khi thực hiện ba bước khắc phục dưới đây, tệp dữ liệu sẽ sẵn sàng để nhập.

## Các vấn đề hàng đầu (nghiêm trọng nhất xếp trước)

| # | Mức độ | Chiều chất lượng | Cột | Phát hiện | Số dòng ảnh hưởng | Cách khắc phục khuyến nghị |
|---|---|---|---|---|---|---|
| 1 | HIGH | Uniqueness | order_id | Mã khóa A-1003 xuất hiện hai lần (số tiền 45.00 và 54.00) | 2 | Hỏi bộ phận tài chính xem dòng nào mới đúng; không tự động chọn |
| 2 | HIGH | Validity | amount | Kiểu dữ liệu hỗn hợp: 1 giá trị không phải số ("TBD") | 1 | Thay thế bằng số tiền thực tế từ hệ thống cửa hàng, hoặc cách ly dòng này |
| 3 | MEDIUM | Consistency | order_date | Hai định dạng ngày tháng (YYYY-MM-DD và DD/MM/YYYY) | 2 | Phân tích cú pháp tường minh cho từng mẫu, lưu trữ theo chuẩn ISO 8601 |
| 4 | MEDIUM | Consistency | status | Khoảng trắng thừa ở đầu hoặc cuối ("paid ") | 1 | Cắt bỏ khoảng trắng ở tất cả các cột danh mục trước khi nhập dữ liệu |
| 5 | LOW | Accuracy signals | amount | Giá trị ngoại lai 1250.00 (theo quy tắc IQR) | 1 | Xác nhận lại với nhóm vận hành cửa hàng; nhiều khả năng là đơn hàng mua sỉ |

## Điểm số theo từng chiều chất lượng

| Chiều chất lượng | Điểm số | Ghi chú |
|---|---|---|
| Completeness (Tính đầy đủ) | WATCH | cột coupon trống 87.5 phần trăm, đúng như kỳ vọng đối với một trường tùy chọn |
| Validity (Tính hợp lệ) | PROBLEM | có giá trị "TBD" trong cột amount |
| Uniqueness (Tính duy nhất) | PROBLEM | trùng mã khóa A-1003 |
| Consistency (Tính nhất quán) | WATCH | định dạng ngày tháng, khoảng trắng thừa ở cuối trong cột status |
| Accuracy signals (Tín hiệu độ chính xác) | WATCH | có một đơn hàng giá trị lớn |
| Structure (Cấu trúc) | OK | không có dòng lệch số cột, dòng tiêu đề sạch sẽ |

## Hồ sơ chi tiết từng cột

| Cột | Kiểu dữ liệu | Tỷ lệ thiếu % | Số giá trị phân biệt | Phạm vi hoặc giá trị phổ biến nhất | Vấn đề |
|---|---|---|---|---|---|
| order_id | string | 0.0 | 7 | A-1003 (2), A-1001 (1), A-1002 (1) | trùng khóa chính |
| order_date | date | 0.0 | 7 | 2026-09-01 đến 2026-09-28 | 2 định dạng |
| customer_email | string | 0.0 | 6 | (đã ẩn) | không có |
| amount | float | 0.0 | 8 | 18.5 đến 1250.0 | kiểu hỗn hợp, giá trị ngoại lai |
| status | string | 0.0 | 2 | paid (7), refunded (1) | khoảng trắng bao quanh |
| coupon | string | 87.5 | 1 | FALL10 (1) | phần lớn để trống (đúng kỳ vọng) |

## Câu hỏi dành cho người sở hữu dữ liệu
- Dòng A-1003 nào mới là dòng chính xác, và tại sao đơn hàng này lại bị xuất ra hai lần?
- Số tiền thực tế của đơn hàng A-1006 là bao nhiêu?

## Các giả định
- Định dạng DD/MM/YYYY được sử dụng cho các ngày tháng có dấu gạch chéo (vì có một giá trị mang ngày 28, nên không thể là định dạng MM/DD).

## Các bước tiếp theo
1. Giải quyết dứt điểm hai đơn hàng A-1003 và A-1006 với bộ phận tài chính.
2. Bổ sung bước cắt khoảng trắng và chuẩn hóa ngày tháng vào tác vụ xuất dữ liệu.
3. Chạy lại lệnh `python3 scripts/profile_csv.py orders_sept.csv --key order_id` và tiến hành nhập dữ liệu khi lệnh trả về mã thoát 0.
FILE:scripts/profile_csv.py
#!/usr/bin/env python3
"""Profile a CSV file for data quality problems (Python 3 standard library only).

Usage:
  python3 profile_csv.py FILE.csv [--key COLUMN] [--delimiter ","] [--json]
  python3 profile_csv.py - < FILE.csv          (read from stdin)

Reports per column: inferred type (int, float, numtext = numbers stored with
separators or currency symbols, date, bool, string), missing values, distinct count, top values,
min/max, and issues (mixed types, surrounding spaces, IQR outliers, several
date formats, inconsistent casing). Also reports ragged rows, duplicate rows,
blank or duplicate headers, and key uniqueness when --key is given.
Exit code: 0 = no HIGH issues, 1 = at least one HIGH issue, 2 = usage error.
"""
import argparse
import csv
import io
import json
import re
import statistics
import sys
from collections import Counter

MISSING = {"", "na", "n/a", "null", "none", "nan", "-", "?"}
DATE_PATTERNS = [
    ("YYYY-MM-DD", re.compile(r"^\d{4}-\d{2}-\d{2}$")),
    ("YYYY-MM-DD HH:MM", re.compile(r"^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2})?$")),
    ("DD/MM/YYYY or MM/DD/YYYY", re.compile(r"^\d{1,2}/\d{1,2}/\d{4}$")),
    ("DD.MM.YYYY", re.compile(r"^\d{1,2}\.\d{1,2}\.\d{4}$")),
    ("D Mon YYYY", re.compile(r"^\d{1,2} [A-Za-z]{3,9} \d{4}$")),
]
INT_RE = re.compile(r"^[+-]?\d+$")
FLOAT_RE = re.compile(r"^[+-]?(\d+\.\d*|\.\d+|\d+)([eE][+-]?\d+)?$")
FORMATTED_NUM_RE = re.compile(r"^[+-]?[$\u20ac\u00a3]?\d{1,3}(,\d{3})+(\.\d+)?$|^[$\u20ac\u00a3]\d+(\.\d+)?$")
BOOL_VALUES = {"true", "false", "yes", "no", "y", "n"}


def classify(value):
    v = value.strip()
    if INT_RE.match(v):
        return "int"
    if FLOAT_RE.match(v):
        return "float"
    if v.lower() in BOOL_VALUES:
        return "bool"
    for name, rx in DATE_PATTERNS:
        if rx.match(v):
            return "date:" + name
    if FORMATTED_NUM_RE.match(v):
        return "formatted_number"
    return "string"


def quartiles(nums):
    q = statistics.quantiles(nums, n=4, method="inclusive")
    return q[0], q[2]


def profile(rows, header, key=None):
    issues = []  # (severity, column, code, message)
    ncols = len(header)
    if header and header[0].startswith("\ufeff"):
        header[0] = header[0].lstrip("\ufeff")
        issues.append(("LOW", header[0], "bom", "File starts with a byte order mark; read with encoding utf-8-sig"))
    names = Counter(h.strip() for h in header)
    for h in header:
        if not h.strip():
            issues.append(("MEDIUM", "(header)", "blank-header", "A header cell is blank"))
    for h, c in names.items():
        if h and c > 1:
            issues.append(("HIGH", h, "duplicate-header", f"Header '{h}' appears {c} times"))

    ragged = [i + 2 for i, r in enumerate(rows) if len(r) != ncols]
    if ragged:
        issues.append(("HIGH", "(rows)", "ragged-rows",
                       f"{len(ragged)} row(s) have a field count different from the header ({ncols}); first at line(s) {ragged[:5]}"))
    good = [r for r in rows if len(r) == ncols]

    dup_counter = Counter(tuple(c.strip() for c in r) for r in good)
    dup_rows = sum(c - 1 for c in dup_counter.values() if c > 1)
    if dup_rows:
        issues.append(("MEDIUM", "(rows)", "duplicate-rows", f"{dup_rows} fully duplicated row(s)"))

    columns = []
    for idx, name in enumerate(header):
        raw = [r[idx] for r in good]
        present = [v for v in raw if v.strip().lower() not in MISSING]
        missing = len(raw) - len(present)
        kinds = Counter(classify(v) for v in present)
        base = Counter()
        for k, c in kinds.items():
            base["date" if k.startswith("date:") else k] += c
        if base:
            top_kind, top_n = base.most_common(1)[0]
        else:
            top_kind, top_n = "empty", 0
        if set(base) <= {"int", "float"} and base:
            ctype = "float" if "float" in base else "int"
        elif set(base) <= {"int", "float", "formatted_number"} and base:
            ctype = "numtext"
        else:
            ctype = top_kind
        col = {
            "name": name, "type": ctype, "rows": len(raw), "missing": missing,
            "missing_pct": round(100.0 * missing / len(raw), 1) if raw else 0.0,
            "distinct": len(set(v.strip() for v in present)),
            "top_values": Counter(v.strip() for v in present).most_common(3),
            "issues": [],
        }

        def add(sev, code, msg):
            col["issues"].append(code)
            issues.append((sev, name, code, msg))

        numeric_like = base.get("int", 0) + base.get("float", 0)
        others = {k: c for k, c in kinds.items() if k not in ("int", "float")}
        if numeric_like and others and numeric_like >= sum(others.values()):
            bad = [v.strip() for v in present if classify(v) not in ("int", "float")]
            add("HIGH", "mixed-types",
                f"Mostly numeric but {len(bad)} non-numeric value(s), e.g. {bad[:3]}")
        elif base.get("formatted_number") and ctype in ("numtext", "formatted_number"):
            add("MEDIUM", "formatted-numbers",
                f"{base['formatted_number']} value(s) use thousands separators or currency symbols")
        date_formats = {k[5:] for k in kinds if k.startswith("date:")}
        if len(date_formats) > 1:
            add("MEDIUM", "date-formats", f"Several date formats: {sorted(date_formats)}")
        if ctype == "date" and "DD/MM/YYYY or MM/DD/YYYY" in date_formats:
            add("LOW", "ambiguous-dates", "Slash dates are ambiguous (day/month order); confirm the format")
        spaced = [v for v in present if v != v.strip()]
        if spaced:
            add("MEDIUM", "surrounding-spaces", f"{len(spaced)} value(s) have leading or trailing spaces")
        if ctype == "string" and present:
            groups = {}
            for v in present:
                groups.setdefault(v.strip().lower(), set()).add(v.strip())
            variants = [sorted(s) for s in groups.values() if len(s) > 1]
            if variants:
                add("LOW", "case-variants", f"Same value with different casing: {variants[:3]}")
        if col["missing_pct"] > 50:
            add("LOW", "mostly-empty", f"{col['missing_pct']}% missing (fine if optional)")
        elif missing:
            add("LOW", "missing", f"{missing} missing value(s)")
        if ctype in ("int", "float"):
            nums = [float(v) for v in present if classify(v) in ("int", "float")]
            if nums:
                fmt = int if ctype == "int" else float
                col["min"], col["max"] = fmt(min(nums)), fmt(max(nums))
            if len(nums) >= 4:
                q1, q3 = quartiles(nums)
                iqr = q3 - q1
                lo, hi = q1 - 1.5 * iqr, q3 + 1.5 * iqr
                outliers = [n for n in nums if n < lo or n > hi]
                if outliers:
                    add("LOW", "outliers", f"{len(outliers)} outlier(s) outside [{lo:g}, {hi:g}]: {outliers[:3]}")
        elif ctype == "date" and present:
            iso = sorted(v.strip()[:10] for v in present if classify(v).startswith("date:YYYY"))
            if iso:
                col["min"], col["max"] = iso[0], iso[-1]
        if len(present) > 1 and col["distinct"] == 1:
            add("LOW", "constant", "Every non-missing value is the same")
        columns.append(col)

    key_report = None
    if key:
        names_clean = [h.strip() for h in header]
        if key not in names_clean:
            issues.append(("HIGH", key, "key-missing", f"Key column '{key}' not found in header"))
        else:
            k = names_clean.index(key)
            vals = [r[k].strip() for r in good]
            empty = sum(1 for v in vals if v.lower() in MISSING)
            dups = {v: c for v, c in Counter(vals).items() if c > 1 and v.lower() not in MISSING}
            key_report = {"column": key, "empty": empty, "duplicate_keys": dups}
            if empty:
                issues.append(("HIGH", key, "key-empty", f"{empty} row(s) have an empty key"))
            if dups:
                issues.append(("HIGH", key, "key-duplicate", f"{len(dups)} key value(s) repeat: {dict(list(dups.items())[:5])}"))
    return {"rows": len(rows), "columns": len(header), "column_profiles": columns,
            "key": key_report, "issues": [dict(zip(("severity", "column", "code", "message"), i)) for i in issues]}


def render(report):
    out = [f"Rows: {report['rows']}  Columns: {report['columns']}", "",
           "Column            Type     Missing%  Distinct  Range / top values"]
    for c in report["column_profiles"]:
        rng = f"{c['min']} .. {c['max']}" if "min" in c else ", ".join(f"{v} ({n})" for v, n in c["top_values"])
        out.append(f"{c['name'][:17]:<17} {c['type'][:8]:<8} {c['missing_pct']:>8}  {c['distinct']:>8}  {rng[:60]}")
    out.append("")
    order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
    for i in sorted(report["issues"], key=lambda x: order[x["severity"]]):
        out.append(f"[{i['severity']}] {i['column']}: {i['code']}: {i['message']}")
    counts = Counter(i["severity"] for i in report["issues"])
    out.append("")
    out.append(f"{counts.get('HIGH', 0)} HIGH, {counts.get('MEDIUM', 0)} MEDIUM, {counts.get('LOW', 0)} LOW")
    out.append("Heuristic profile: confirm findings with references/quality-dimensions.md.")
    return "\n".join(out)


def main(argv=None):
    ap = argparse.ArgumentParser(description="Profile a CSV file for data quality problems.")
    ap.add_argument("file", help="CSV path, or - for stdin")
    ap.add_argument("--key", help="column that should be unique and non-empty")
    ap.add_argument("--delimiter", default=None, help="field delimiter (default: sniffed)")
    ap.add_argument("--json", action="store_true", help="print JSON instead of text")
    a = ap.parse_args(argv)
    try:
        text = sys.stdin.read() if a.file == "-" else open(a.file, encoding="utf-8", newline="").read()
    except (OSError, UnicodeDecodeError) as e:
        print(f"error: cannot read {a.file}: {e}", file=sys.stderr)
        return 2
    if not text.strip():
        print("error: file is empty", file=sys.stderr)
        return 2
    delim = a.delimiter
    if delim is None:
        try:
            delim = csv.Sniffer().sniff(text[:4096], delimiters=",;\t|").delimiter
        except csv.Error:
            delim = ","
    all_rows = [r for r in csv.reader(io.StringIO(text), delimiter=delim) if any(c.strip() for c in r)]
    header, rows = all_rows[0], all_rows[1:]
    report = profile(rows, header, a.key)
    report["delimiter"] = delim
    print(json.dumps(report, indent=2) if a.json else render(report))
    return 1 if any(i["severity"] == "HIGH" for i in report["issues"]) else 0


if __name__ == "__main__":
    sys.exit(main())
PythonData AnalysisData Quality+2
F@f
0
Trình Giải Thích và Xác Thực Lịch Trình Cron
Kỹ năng

Giải thích bất kỳ biểu thức cron nào bằng ngôn ngữ tự nhiên dễ hiểu, liệt kê các thời điểm chạy tiếp theo, và cảnh báo những cạm bẫy phổ biến như quy tắc ngày-trong-tháng HOẶC thứ-trong-tuần, các ngày không bao giờ xảy ra, khoảng trống giờ mùa hè (DST), sự nhầm lẫn giữa giờ UTC và giờ địa phương, cùng các tác vụ bị chạy chồng chéo. Bao gồm công cụ kiểm tra tệp crontab viết bằng thư viện chuẩn Python đã được kiểm thử.

---
name: cron-schedule-explainer
description: Giải thích, xác thực và viết các lịch trình cron - dịch một biểu thức cron sang ngôn ngữ tự nhiên dễ hiểu, liệt kê các thời điểm chạy tiếp theo, đồng thời cảnh báo những cạm bẫy như quy tắc ngày-trong-tháng HOẶC thứ-trong-tuần, các ngày không bao giờ xảy ra, khoảng trống giờ mùa hè (DST), sự nhầm lẫn về múi giờ, và các tác vụ chạy chồng chéo hoặc quá dày đặc. Sử dụng khi người dùng dán một dòng crontab, Kubernetes CronJob, lịch chạy GitHub Actions, hoặc hỏi "khi nào tác vụ này sẽ chạy?" hay "hãy viết một biểu thức cron chạy vào thứ Ba tuần thứ hai của mỗi tháng".
---

# Trình Giải Thích Lịch Trình Cron

Bạn giúp cho các tác vụ được lập lịch trở nên dễ đoán định. Đối với mỗi lịch trình, bạn đưa ra ý nghĩa bằng ngôn ngữ tự nhiên dễ hiểu, các thời điểm chạy cụ thể tiếp theo, và những rủi ro có thể khiến ai đó bị bất ngờ lúc 2 giờ sáng.

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

- `scripts/cron_explain.py` - trình phân tích cú pháp, giải thích, tính toán lần chạy tiếp theo và kiểm tra cạm bẫy (chỉ dùng thư viện chuẩn Python 3)
- `references/cron-syntax.md` - phạm vi các trường, ký tự đặc biệt, các macro và sự khác biệt giữa các nền tảng
- `references/scheduling-pitfalls.md` - những sai lầm phổ biến và cách phòng tránh chúng
- `templates/schedule-review.md` - định dạng báo cáo rà soát
- `examples/example-backup-review.md` - bài rà soát mẫu hoàn chỉnh cho ba dòng crontab

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

### 1. Xác định nền tảng
Chuẩn cron 5 trường (Vixie cron, cronie, Kubernetes CronJob, GitHub Actions) là mặc định. Hãy hỏi hoặc kiểm tra xem liệu người dùng có đang nói đến Quartz (6 hoặc 7 trường có thêm giây và ký tự `?`), AWS EventBridge (6 trường có thêm năm), hay systemd timers hay không; xem chi tiết trong `references/cron-syntax.md`. Lưu ý về múi giờ: GitHub Actions luôn sử dụng múi giờ UTC; Kubernetes sử dụng múi giờ của trình điều khiển (controller) trừ khi trường `timeZone` được thiết lập.

### 2. Chạy công cụ kiểm tra
```bash
python3 scripts/cron_explain.py "30 2 * * 1-5"
python3 scripts/cron_explain.py "0 9 1 * MON" --count 8 --from "2026-10-08 10:00"
python3 scripts/cron_explain.py --file crontab.txt
```
Kịch bản sẽ in ra lời giải thích bằng ngôn ngữ dễ hiểu, N thời điểm chạy tiếp theo (theo giờ địa phương không gắn múi giờ của máy chủ), và các cảnh báo. Mã thoát (exit code) là 1 khi một biểu thức không hợp lệ hoặc không bao giờ chạy.

Nếu bạn không thể chạy kịch bản, hãy tự áp dụng đúng các quy tắc tương tự một cách thủ công và nói rõ điều đó.

### 3. Giải thích
Đối với mỗi lịch trình, hãy cung cấp:
1. Ý nghĩa bằng một câu ngôn ngữ tự nhiên dễ hiểu.
2. Từ 3 đến 5 lần chạy tiếp theo kèm theo múi giờ được nêu rõ.
3. Các cảnh báo từ kịch bản và từ tệp `references/scheduling-pitfalls.md` có liên quan (chạy chồng chéo với các tác vụ kéo dài, giờ mùa hè DST, giờ UTC so với giờ địa phương, các lần chạy bị bỏ lỡ khi máy tắt).

### 4. Viết hoặc sửa lịch trình
Khi người dùng mô tả một lịch trình bằng lời, hãy viết biểu thức cron tương ứng, sau đó đưa nó qua công cụ kiểm tra để xác nhận rằng các lần chạy tiếp theo khớp đúng với ý định của họ. Đối với những lịch trình mà cron không thể diễn đạt trực tiếp (chẳng hạn như thứ Ba tuần thứ hai của tháng, hoặc ngày làm việc cuối cùng trong tháng), hãy đưa ra một biểu thức cron kết hợp với một điều kiện bảo vệ (guard) trong câu lệnh, ví dụ như `[ "$(date +\%d)" -le 07 ] && run-job`, và giải thích rõ lý do tại sao.

### 5. Báo cáo
Sử dụng mẫu `templates/schedule-review.md`, giống như cách làm trong `examples/example-backup-review.md`.

## Quy tắc
- Luôn nêu rõ múi giờ mà bạn đang giả định.
- Hãy nhớ rằng ký tự `%` bắt buộc phải được thoát (escape) thành `\%` bên trong phần câu lệnh của tệp crontab.
- Tuyệt đối không trực tiếp chỉnh sửa tệp crontab đang chạy của người dùng; hãy hiển thị dòng cần thêm vào và hướng dẫn bước chạy `crontab -e`.
- Luôn khuyến nghị sử dụng cơ chế khóa (ví dụ: `flock -n /tmp/job.lock cmd`) bất cứ khi nào một tác vụ có khả năng chạy lâu hơn khoảng thời gian lặp lại của nó.
FILE:references/cron-syntax.md
# Tài Liệu Tham Chiếu Cú Pháp Cron (Chuẩn 5 trường)

```
+------------- phút (0-59)
| +----------- giờ (0-23)
| | +--------- ngày trong tháng (1-31)
| | | +------- tháng (1-12 hoặc JAN-DEC)
| | | | +----- thứ trong tuần (0-7 hoặc SUN-SAT; cả 0 và 7 đều là Chủ nhật)
| | | | |
* * * * *  command
```

## Các ký tự đặc biệt
| Ký hiệu | Ý nghĩa | Ví dụ |
|---|---|---|
| `*` | mọi giá trị | `* * * * *` mỗi phút |
| `,` | danh sách | `0 8,12,18 * * *` vào lúc 08:00, 12:00, 18:00 |
| `-` | khoảng giá trị | `0 9 * * 1-5` 09:00 từ thứ Hai đến thứ Sáu |
| `/` | bước nhảy | `*/15 * * * *` mỗi 15 phút; `10-50/20` = 10, 30, 50 |

Tên tháng và thứ không phân biệt chữ hoa chữ thường. Khoảng tên viết tắt (`MON-FRI`) hoạt động trên hầu hết các bản triển khai cron, còn danh sách các tên viết tắt thì hoạt động ở mọi nơi.

## Các macro
| Macro | Biểu thức tương đương |
|---|---|
| `@yearly` / `@annually` | `0 0 1 1 *` |
| `@monthly` | `0 0 1 * *` |
| `@weekly` | `0 0 * * 0` |
| `@daily` / `@midnight` | `0 0 * * *` |
| `@hourly` | `0 * * * *` |
| `@reboot` | chạy một lần khi khởi động máy (không dựa theo thời gian) |

## Quy tắc kết hợp trường ngày
Nếu cả trường ngày trong tháng và trường thứ trong tuần đều bị giới hạn (tức là không trường nào mang dấu `*`), tác vụ sẽ chạy khi MỘT TRONG HAI điều kiện khớp. Biểu thức `0 9 1 * MON` sẽ chạy vào ngày mùng 1 hàng tháng VÀ vào mọi ngày thứ Hai.

## Sự khác biệt giữa các nền tảng
| Nền tảng | Số trường | Múi giờ | Ghi chú |
|---|---|---|---|
| Linux cron (cronie, Vixie) | 5 | giờ địa phương của hệ thống | biến `CRON_TZ=` được cronie hỗ trợ |
| Kubernetes CronJob | 5 | múi giờ của controller, hoặc `spec.timeZone` | sử dụng `concurrencyPolicy: Forbid` để ngăn chạy chồng chéo |
| GitHub Actions `schedule` | 5 | luôn là UTC | các lần chạy có thể bị trễ khi hệ thống quá tải; khoảng cách tối thiểu là 5 phút |
| Quartz (Java) | 6-7 (giây đứng đầu, năm là tùy chọn) | có thể cấu hình | dùng `?` cho "không chỉ định giá trị cụ thể", hỗ trợ `L`, `W`, `#` |
| AWS EventBridge | 6 (có trường năm) | UTC trừ khi múi giờ của bộ lập lịch được thiết lập | một trong hai trường ngày-trong-tháng hoặc thứ-trong-tuần bắt buộc phải là `?` |

Kịch bản trong kỹ năng này hỗ trợ chuẩn 5 trường cùng các macro nêu trên (ngoại trừ `@reboot`, được kịch bản báo cáo là không dựa theo thời gian).
FILE:references/scheduling-pitfalls.md
# Những Cạm Bẫy Khi Lập Lịch

## 1. Ngày trong tháng HOẶC thứ trong tuần
Biểu thức `0 0 13 * 5` KHÔNG PHẢI là "thứ Sáu ngày 13". Nó sẽ chạy vào mọi ngày 13 hàng tháng và vào mọi ngày thứ Sáu. Hãy sử dụng `0 0 13 * *` kết hợp với một điều kiện bảo vệ: `[ "$(date +\%u)" = 5 ] && cmd`.

## 2. những ngày không bao giờ hoặc hiếm khi xảy ra
- `0 0 30 2 *` không bao giờ chạy (tháng Hai không có ngày 30).
- `0 0 31 * *` chỉ chạy trong 7 tháng có 31 ngày của năm.
- `0 0 29 2 *` chỉ chạy vào các năm nhuận.
Đối với nhu cầu chạy vào "ngày cuối cùng của tháng", hãy dùng `0 0 28-31 * *` kèm điều kiện bảo vệ: `[ "$(date -d tomorrow +\%d)" = 01 ] && cmd`.

## 3. Giờ tiết kiệm ánh sáng ban ngày / Giờ mùa hè (DST)
Ở các múi giờ địa phương có áp dụng DST, các mốc thời gian trong khoảng từ 01:00 đến 03:00 có thể bị bỏ qua (khi vặn đồng hồ tiến lên vào mùa xuân) hoặc bị chạy hai lần (khi vặn đồng hồ lùi lại vào mùa thu), tùy thuộc vào bản triển khai cron. Hãy xếp lịch cho các tác vụ quan trọng nằm ngoài khung giờ đó, hoặc cho cron chạy theo múi giờ UTC.

## 4. Giờ UTC so với giờ địa phương
GitHub Actions và nhiều bộ lập lịch trên đám mây sử dụng giờ UTC. Lịch chạy "Mỗi ngày vào lúc 09:00" đối với một nhóm làm việc tại Istanbul (UTC+3) sẽ là `0 6 * * *` theo giờ UTC. Hãy luôn ghi chú rõ múi giờ ngay bên cạnh biểu thức cron trong tài liệu và trong bình luận mã nguồn (code comments).

## 5. Chạy quá dày đặc hoặc chạy chồng chéo
- `* * * * *` chạy tới 1440 lần mỗi ngày. Hãy đảm bảo đó thực sự là điều bạn muốn.
- Trường phút để là `*` đi kèm với một giờ cố định (`* 3 * * *`) sẽ chạy 60 lần liên tục từ 03:00 đến 03:59; thông thường ý định thực sự của người viết là `0 3 * * *`.
- Nếu một tác vụ có thể mất nhiều thời gian thực thi hơn khoảng cách giữa các lần chạy của nó, hãy sử dụng cơ chế khóa (`flock -n`) hoặc `concurrencyPolicy: Forbid`.

## 6. Giá trị bước nhảy (step) không chia chẵn vòng lặp
Biểu thức `*/7` ở trường phút sẽ chạy vào các phút 0, 7, ..., 56, rồi chạy lại ngay ở phút 0 của giờ tiếp theo (chỉ cách nhau 4 phút). Biểu thức `*/25` chạy vào các phút 0, 25, 50. Các bước nhảy luôn bắt đầu đếm lại từ đầu sau mỗi giờ, mỗi ngày hoặc mỗi tháng.

## 7. Hiệu ứng bầy đàn (Thundering herd)
Rất nhiều nhóm cùng chọn `0 0 * * *` hoặc `0 * * * *`. Hãy dịch chuyển các tác vụ sang một số phút lẻ (ví dụ: `17 2 * * *`) để tránh gây đột biến tải trên các hệ thống dùng chung và các API có giới hạn tần suất gọi (rate-limited).

## 8. Biến môi trường và kết quả đầu ra
Cron chạy với biến môi trường PATH tối thiểu và không có login shell. Hãy sử dụng các đường dẫn tuyệt đối, khai báo các biến cần thiết ngay trong tệp crontab, và chuyển hướng kết quả đầu ra (`>> /var/log/job.log 2>&1`) để có thể nhìn thấy lỗi khi tác vụ thất bại. Hãy thoát ký tự `%` thành `\%`.

## 9. Các lần chạy bị bỏ lỡ
Cron thuần túy không tự động chạy bù các lần thực thi bị bỏ lỡ trong lúc máy tính đang tắt. Hãy sử dụng anacron, systemd timers với thiết lập `Persistent=true`, hoặc `startingDeadlineSeconds` của Kubernetes khi việc bỏ lỡ một lần chạy gây ảnh hưởng quan trọng.
FILE:templates/schedule-review.md
# Báo Cáo Rà Soát Lịch Trình: {{system_or_repo}}

**Nền tảng:** {{Linux cron | Kubernetes CronJob | GitHub Actions | other}}
**Múi giờ giả định:** {{time_zone}}
**Ngày rà soát:** {{date}}

## Tóm tắt
{{One or two sentences: are the schedules doing what the team expects, and what must change.}}

## Các lịch trình

### {{n}}. `{{expression}}` - {{job name}}
- **Ý nghĩa:** {{plain-English explanation}}
- **Các lần chạy tiếp theo:** {{run 1}}, {{run 2}}, {{run 3}}
- **Kết luận:** {{OK | FIX | CLARIFY}}
- **Cảnh báo:**
  - {{warning}}
- **Dòng cấu hình đề xuất:**
  ```
  {{corrected crontab line}}
  ```

## Câu hỏi
- {{question for the team}}
FILE:examples/example-backup-review.md
# Báo Cáo Rà Soát Lịch Trình: crontab của máy chủ vận hành (ops server)

**Nền tảng:** Linux cron (cronie)
**Múi giờ giả định:** Europe/Berlin (giờ địa phương của máy chủ, có áp dụng DST)
**Ngày rà soát:** 2026-10-08

## Tóm tắt
Hai trong số ba dòng cấu hình không hoạt động đúng như nội dung ghi chú. Tác vụ sao lưu chạy bên trong khung giờ chuyển đổi giờ mùa hè (DST), còn báo cáo "thứ Sáu ngày 13" thực chất lại chạy vào mọi ngày thứ Sáu và mọi ngày 13 hàng tháng.

## Các lịch trình

### 1. `30 2 * * *` - sao lưu cơ sở dữ liệu hàng đêm
- **Ý nghĩa:** Vào lúc 02:30 mỗi ngày.
- **Các lần chạy tiếp theo:** 2026-10-09 02:30, 2026-10-10 02:30, 2026-10-11 02:30
- **Kết luận:** FIX
- **Cảnh báo:**
  - Mốc 02:30 nằm trong khung giờ thay đổi DST; vào đêm chỉnh đồng hồ tiến lên trong mùa xuân, tác vụ có thể bị bỏ qua, còn vào mùa thu nó có thể bị chạy hai lần.
  - Quá trình sao lưu có thể mất hơn một giờ vào dịp cuối tháng; hiện chưa có cơ chế khóa (lock).
- **Dòng cấu hình đề xuất:**
  ```
  17 4 * * * flock -n /tmp/db-backup.lock /opt/scripts/db-backup.sh >> /var/log/db-backup.log 2>&1
  ```

### 2. `0 9 13 * FRI` - báo cáo vui "thứ Sáu ngày 13"
- **Ý nghĩa:** Vào lúc 09:00 ngày 13 của tháng HOẶC vào mọi ngày thứ Sáu (theo quy tắc kết hợp ngày của cron).
- **Các lần chạy tiếp theo:** 2026-10-09 09:00 (Thứ Sáu), 2026-10-13 09:00 (Thứ Ba, ngày 13), 2026-10-16 09:00 (Thứ Sáu)
- **Kết luận:** FIX
- **Cảnh báo:**
  - Cả hai trường ngày đều bị giới hạn, vì vậy cron áp dụng phép toán HOẶC (OR) chứ không phải VÀ (AND).
- **Dòng cấu hình đề xuất:**
  ```
  0 9 13 * * [ "$(date +\%u)" = 5 ] && /opt/scripts/fun-report.sh
  ```

### 3. `*/20 8-18 * * 1-5` - đồng bộ phiếu yêu cầu (tickets) từ hệ thống hỗ trợ
- **Ý nghĩa:** Mỗi 20 phút một lần (vào các phút :00, :20, :40) từ 08:00 đến 18:59, từ thứ Hai đến thứ Sáu.
- **Các lần chạy tiếp theo:** 2026-10-08 10:20, 2026-10-08 10:40, 2026-10-08 11:00
- **Kết luận:** CLARIFY
- **Cảnh báo:**
  - Lần chạy cuối cùng trong ngày là lúc 18:40 chứ không phải 18:00. Hãy sử dụng `8-17` kết hợp với một dòng `0 18 * * 1-5` riêng biệt nếu quá trình đồng bộ cần dừng lại đúng vào lúc 18:00.

## Câu hỏi
- Máy chủ có nên chạy cron theo múi giờ UTC để tránh hoàn toàn các rắc rối liên quan đến giờ mùa hè (DST) hay không?
FILE:scripts/cron_explain.py
#!/usr/bin/env python3
"""Explain, validate, and preview standard 5-field cron expressions (stdlib only).

Usage:
  python3 cron_explain.py "EXPR" [--count N] [--from "YYYY-MM-DD HH:MM"]
  python3 cron_explain.py --file crontab.txt [--count N] [--from ...]

For each expression: a plain-English explanation, the next N run times
(naive server-local time), and pitfall warnings. In --file mode, crontab
lines are read; comments, blank lines and VAR=value lines are skipped and the
first five fields (or a leading @macro) are taken as the schedule.
Exit code: 0 = all valid, 1 = an expression is invalid or never runs, 2 = usage.
"""
import argparse
import calendar
import datetime as dt
import sys

MONTHS = {m.lower(): i for i, m in enumerate(calendar.month_abbr) if m}
DAYS = {"sun": 0, "mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6}
MACROS = {
    "@yearly": "0 0 1 1 *", "@annually": "0 0 1 1 *", "@monthly": "0 0 1 * *",
    "@weekly": "0 0 * * 0", "@daily": "0 0 * * *", "@midnight": "0 0 * * *",
    "@hourly": "0 * * * *",
}
FIELDS = [("minute", 0, 59, {}), ("hour", 0, 23, {}), ("day of month", 1, 31, {}),
          ("month", 1, 12, MONTHS), ("day of week", 0, 7, DAYS)]
DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]


def compress(values, fmt=str):
    """[1,2,3,5] -> '1-3, 5' using fmt for each number."""
    vals, out, i = sorted(values), [], 0
    while i < len(vals):
        j = i
        while j + 1 < len(vals) and vals[j + 1] == vals[j] + 1:
            j += 1
        out.append(fmt(vals[i]) if j - i < 2 else f"{fmt(vals[i])}-{fmt(vals[j])}")
        if 0 < j - i < 2:
            out.append(fmt(vals[j]))
        i = j + 1
    return ", ".join(out)


class CronError(ValueError):
    pass


def _num(token, lo, hi, names, field):
    t = token.lower()
    if t in names:
        return names[t]
    if not t.isdigit():
        raise CronError(f"{field}: '{token}' is not a number or known name")
    v = int(t)
    if not lo <= v <= hi:
        raise CronError(f"{field}: {v} is outside {lo}-{hi}")
    return v


def parse_field(text, lo, hi, names, field):
    values = set()
    for part in text.split(","):
        if not part:
            raise CronError(f"{field}: empty list item in '{text}'")
        step = 1
        if "/" in part:
            part, step_s = part.split("/", 1)
            if not step_s.isdigit() or int(step_s) == 0:
                raise CronError(f"{field}: bad step '/{step_s}'")
            step = int(step_s)
        if part == "*":
            start, end = lo, hi
        elif "-" in part:
            a, b = part.split("-", 1)
            start, end = _num(a, lo, hi, names, field), _num(b, lo, hi, names, field)
            if start > end:
                raise CronError(f"{field}: range {a}-{b} is reversed")
        else:
            start = _num(part, lo, hi, names, field)
            end = hi if step > 1 else start
        values.update(range(start, end + 1, step))
    return values


def parse(expr):
    expr = expr.strip()
    if expr.lower() == "@reboot":
        raise CronError("@reboot runs once at startup and is not time based")
    expr = MACROS.get(expr.lower(), expr)
    parts = expr.split()
    if len(parts) != 5:
        hint = " (6-7 fields look like Quartz or EventBridge; see references/cron-syntax.md)" if len(parts) in (6, 7) else ""
        raise CronError(f"expected 5 fields, got {len(parts)}{hint}")
    for p in parts:
        bare = p.lower()
        for n in list(MONTHS) + list(DAYS):
            bare = bare.replace(n, "")
        if any(c in bare for c in "?lw#"):
            raise CronError(f"'{p}': '?', 'L', 'W' and '#' are Quartz extensions, not standard cron")
    sets = [parse_field(p, lo, hi, names, name) for p, (name, lo, hi, names) in zip(parts, FIELDS)]
    if 7 in sets[4]:
        sets[4].discard(7)
        sets[4].add(0)
    return parts, sets


def describe_set(values, lo, hi, field, raw):
    vals = sorted(values)
    if raw == "*":
        return None
    if field == "day of week":
        names = [DAY_NAMES[v] for v in vals]
        if vals == [1, 2, 3, 4, 5]:
            return "Monday to Friday"
        if vals == [0, 6]:
            return "on weekends"
        return ", ".join(names)
    if field == "month":
        return ", ".join(calendar.month_name[v] for v in vals)
    return compress(vals)


def explain(parts, sets):
    minute, hour, dom, month, dow = sets
    rm, rh, rdom, rmon, rdow = parts
    if rm == "*" and rh == "*":
        time_txt = "every minute"
    elif rm.startswith("*/") and rh == "*":
        time_txt = f"every {rm[2:]} minutes"
    elif rm.startswith("*/"):
        time_txt = (f"every {rm[2:]} minutes (at minute " + ", ".join(str(m) for m in sorted(minute)) +
                    ") during hour(s) " + compress(hour, lambda h: f"{h:02d}"))
    elif rh == "*":
        time_txt = "at minute " + ", ".join(str(m) for m in sorted(minute)) + " of every hour"
    elif rm == "*":
        time_txt = "every minute during hour(s) " + compress(hour, lambda h: f"{h:02d}")
    elif len(minute) * len(hour) <= 6:
        time_txt = "at " + ", ".join(f"{h:02d}:{m:02d}" for h in sorted(hour) for m in sorted(minute))
    else:
        time_txt = ("at minute(s) " + ", ".join(str(m) for m in sorted(minute)) +
                    " past hour(s) " + compress(hour, lambda h: f"{h:02d}"))
    day_txt = []
    d_dom = describe_set(dom, 1, 31, "day of month", rdom)
    d_dow = describe_set(dow, 0, 6, "day of week", rdow)
    if d_dom and d_dow:
        day_txt.append(f"on day(s) {d_dom} of the month OR on {d_dow}")
    elif d_dom:
        day_txt.append(f"on day(s) {d_dom} of the month")
    elif d_dow:
        day_txt.append(d_dow if d_dow.startswith("on ") else f"on {d_dow}")
    else:
        day_txt.append("every day")
    d_mon = describe_set(month, 1, 12, "month", rmon)
    if d_mon:
        day_txt.append(f"in {d_mon}")
    text = f"{time_txt}, {' '.join(day_txt)}"
    return text[0].upper() + text[1:] + "."


def day_matches(d, parts, sets):
    _, _, dom, month, dow = sets
    if d.month not in month:
        return False
    cron_dow = (d.weekday() + 1) % 7
    dom_r, dow_r = parts[2] != "*", parts[4] != "*"
    if dom_r and dow_r:
        return d.day in dom or cron_dow in dow
    if dom_r:
        return d.day in dom
    if dow_r:
        return cron_dow in dow
    return True


def next_runs(parts, sets, start, count, max_days=366 * 8):
    minute, hour = sorted(sets[0]), sorted(sets[1])
    runs = []
    day = start.date()
    for _ in range(max_days):
        if day_matches(day, parts, sets):
            for h in hour:
                for m in minute:
                    t = dt.datetime(day.year, day.month, day.day, h, m)
                    if t > start:
                        runs.append(t)
                        if len(runs) >= count:
                            return runs
        day += dt.timedelta(days=1)
    return runs


def warnings(parts, sets, runs):
    minute, hour, dom, month, dow = sets
    out = []
    if parts[2] != "*" and parts[4] != "*":
        out.append("Day of month AND day of week are both set: cron runs when EITHER matches (OR, not AND).")
    if parts[2] != "*" and parts[4] == "*":
        max_days = {m: (29 if m == 2 else calendar.monthrange(2026, m)[1]) for m in month}
        if not any(d <= max_days[m] for m in month for d in dom):
            out.append("Never runs: the chosen day(s) of month do not exist in the chosen month(s).")
        elif any(d > 28 for d in dom):
            out.append("Some chosen days (29-31) do not exist in every month, so some months are skipped.")
    if parts[0] == "*" and parts[1] != "*":
        out.append("Minute is '*': runs every minute of the chosen hour(s); did you mean minute 0?")
    runs_per_day = len(minute) * len(hour)
    if runs_per_day >= 288:
        out.append(f"Runs {runs_per_day} times a day; make sure that is intended and add a lock against overlap.")
    if any(1 <= h <= 2 for h in hour) and parts[1] != "*":
        out.append("Runs between 01:00 and 02:59: in local time zones with DST this can be skipped or run twice.")
    for i, raw in ((0, parts[0]), (1, parts[1])):
        if "/" in raw:
            step = int(raw.split("/")[1])
            span = 60 if i == 0 else 24
            if span % step:
                out.append(f"Step /{step} does not divide {span}: the gap is uneven where the {'hour' if i == 0 else 'day'} wraps.")
    if parts[0] == "0" and parts[1] in ("*", "0"):
        out.append("Minute 0 at the top of the hour is a popular slot; consider an odd minute to avoid load spikes.")
    return out


def check(expr, start, count):
    print(f"Expression: {expr}")
    try:
        parts, sets = parse(expr)
    except CronError as e:
        print(f"  INVALID: {e}\n")
        return False
    print(f"  Meaning: {explain(parts, sets)}")
    runs = next_runs(parts, sets, start, count)
    ok = True
    if runs:
        print(f"  Next {len(runs)} run(s) after {start:%Y-%m-%d %H:%M} (server local time):")
        for r in runs:
            print(f"    {r:%Y-%m-%d %H:%M} {r:%a}")
    else:
        print("  Next runs: none found in the next 8 years")
        ok = False
    for w in warnings(parts, sets, runs):
        print(f"  WARNING: {w}")
    print()
    return ok


def crontab_schedules(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            s = line.strip()
            if not s or s.startswith("#"):
                continue
            first = s.split()[0]
            if "=" in first and not first.startswith("@"):
                continue
            yield first if first.startswith("@") else " ".join(s.split()[:5])


def main(argv=None):
    ap = argparse.ArgumentParser(description="Explain and validate cron expressions.")
    ap.add_argument("expr", nargs="?", help='cron expression in quotes, e.g. "*/15 9-17 * * 1-5"')
    ap.add_argument("--file", help="read schedules from a crontab file")
    ap.add_argument("--count", type=int, default=5, help="number of next runs to show (default 5)")
    ap.add_argument("--from", dest="start", help='start time "YYYY-MM-DD HH:MM" (default: now)')
    a = ap.parse_args(argv)
    if bool(a.expr) == bool(a.file):
        ap.print_usage(sys.stderr)
        print("error: give exactly one of EXPR or --file", file=sys.stderr)
        return 2
    try:
        start = dt.datetime.strptime(a.start, "%Y-%m-%d %H:%M") if a.start else dt.datetime.now().replace(second=0, microsecond=0)
    except ValueError:
        print("error: --from must look like 2026-10-08 10:00", file=sys.stderr)
        return 2
    exprs = list(crontab_schedules(a.file)) if a.file else [a.expr]
    results = [check(e, start, max(1, a.count)) for e in exprs]
    print(f"{sum(results)} of {len(results)} schedule(s) valid and runnable.")
    return 0 if all(results) else 1


if __name__ == "__main__":
    sys.exit(main())
DevOpsDebuggingSRE+2
F@f
0
Viết Kịch Bản Shell taste
Văn bản

Các quy ước định kiến để viết kịch bản Bash an toàn, dễ đọc và dễ bảo trì.

# Phong cách (taste) viết kịch bản Shell của một kỹ sư DevOps
- Bắt đầu mọi kịch bản với #!/usr/bin/env bash và set -euo pipefail. Độ tin cậy: 0.90
- Chuyển sang sử dụng Python hoặc Go một khi kịch bản vượt quá khoảng 100 dòng hoặc cần đến các cấu trúc dữ liệu thực thụ. Độ tin cậy: 0.85
- Chạy ShellCheck trong CI và sửa các cảnh báo thay vì tắt chúng đi. Độ tin cậy: 0.95
- Định dạng kịch bản bằng shfmt để các bản so sánh thay đổi (diff) luôn nhỏ gọn. Độ tin cậy: 0.75
- Đặt mọi biến khi khai triển trong dấu ngoặc kép ("$var") trừ khi việc tách từ (word splitting) là có chủ đích và đã được chú thích rõ. Độ tin cậy: 0.95
- Sử dụng [[ ]] cho các phép kiểm tra điều kiện và $(...) để thay thế lệnh, tuyệt đối không dùng dấu huyền (` `). Độ tin cậy: 0.90
- Khai báo các biến bên trong hàm bằng từ khóa local. Độ tin cậy: 0.85
- Đặt phần xử lý logic vào trong các hàm và gọi hàm main "$@" ở cuối tệp. Độ tin cậy: 0.80
- Dọn dẹp các tệp tạm bằng mktemp và bẫy sự kiện trap '...' EXIT. Độ tin cậy: 0.90
- Ghi các lỗi và nhật ký (log) ra luồng stderr và giữ luồng stdout dành riêng cho dữ liệu để các công cụ khác có thể nối ống (pipe). Độ tin cậy: 0.85
- Thoát chương trình với mã trạng thái khác không (non-zero) cùng thông báo rõ ràng khi có lỗi xảy ra. Độ tin cậy: 0.90
- Cung cấp hàm hướng dẫn sử dụng (usage) và xử lý các cờ -h cũng như --help. Độ tin cậy: 0.80
- Phân tích cú pháp các tùy chọn bằng getopts hoặc vòng lặp case đơn giản, không kiểm tra tham số vị trí một cách tùy tiện. Độ tin cậy: 0.75
- Kiểm tra trước các lệnh bắt buộc phải có bằng command -v và báo lỗi dừng sớm ngay khi thiếu một lệnh. Độ tin cậy: 0.85
- Đọc cấu hình từ các biến môi trường kèm theo giá trị mặc định, chẳng hạn như -8080. Độ tin cậy: 0.80
- Không bao giờ phân tích cú pháp (parse) đầu ra của lệnh ls; hãy dùng mẫu glob hoặc find -print0 kết hợp với xargs -0. Độ tin cậy: 0.90
- Đọc tệp từng dòng một bằng cú pháp while IFS= read -r line. Độ tin cậy: 0.85
- Sử dụng mảng (array) cho danh sách đối số thay vì ghép nối các chuỗi lệnh. Độ tin cậy: 0.85
- Tránh sử dụng eval, và tuyệt đối không truyền dữ liệu đầu vào không đáng tin cậy vào đó. Độ tin cậy: 0.95
- Thiết kế các kịch bản có tính lũy đẳng (idempotent) để việc chạy chúng hai lần vẫn đảm bảo an toàn. Độ tin cậy: 0.85
- Thêm cờ --dry-run vào bất kỳ tác vụ nào thực hiện xóa hoặc thay đổi trạng thái từ xa. Độ tin cậy: 0.80
- Chỉ bó buộc trong chuẩn POSIX sh khi kịch bản bắt buộc phải chạy trong các container tối giản; còn lại hãy nhắm tới mục tiêu bash 4+. Độ tin cậy: 0.70
DevOpsLinuxCLI+2
F@f
0
Trước4 / 4