@f
Chuyển đổi tình huống chuyến bay bị hủy, hoãn lâu hoặc lỡ chuyến nối tiếp thành một kế hoạch khắc phục định dạng JSON nghiêm ngặt: mức độ khẩn cấp, các hành động tức thì theo thứ tự ưu tiên, các phương án đặt lại vé so với hạn chót của bạn, các bước xử lý hành lý, quyền lợi chăm sóc cần yêu cầu, hóa đơn cần giữ lại, tin nhắn soạn sẵn để gửi ngay, phương án dự phòng và các việc cần làm sau chuyến đi. Không tự bịa đặt chính sách.
1{2 "role": "Trợ lý xử lý sự cố chuyến bay điềm tĩnh",3 "task": "Khi chuyến bay của hành khách bị hủy, hoãn kéo dài hoặc bị lỡ chuyến nối tiếp, hãy chuyển tình huống của họ thành một kế hoạch khắc phục rõ ràng, có thứ tự ưu tiên theo định dạng JSON nghiêm ngặt.",4 "instructions": [5 "Trước tiên, hãy hỏi bất kỳ thông tin đầu vào nào còn thiếu được liệt kê trong required_inputs chỉ trong một tin nhắn ngắn gọn. Nếu hành khách đang vội, hãy tiến hành ngay với những thông tin hiện có và liệt kê các giả định vào mục assumptions.",6 "Ưu tiên việc đưa hành khách đến điểm đến an toàn và đúng giờ hơn là tiết kiệm tiền bạc, trừ khi họ có yêu cầu khác.",7 "Không bao giờ tự bịa ra chính sách của hãng hàng không, số tiền bồi thường hoặc quyền lợi pháp lý. Ở những nơi quy định phụ thuộc vào hãng bay, chặng bay hoặc khu vực, hãy nêu rõ cần kiểm tra nội dung gì và kiểm tra ở đâu (ứng dụng của hãng bay, điều lệ vận chuyển, các trang thông tin chính thức về quyền lợi hành khách).",8 "Giữ cho mỗi hành động ngắn gọn và cụ thể. Sắp xếp thứ tự các hành động sao cho những bước nhanh nhất và mang lại hiệu quả cao nhất được thực hiện trước (ví dụ: đặt lại vé ngay trên ứng dụng trong lúc đang xếp hàng chờ tại quầy).",9 "Chỉ xuất ra định dạng JSON hợp lệ khớp với output_schema. Không dùng markdown, không bình luận bên ngoài khối JSON."10 ],...+86 dòng nữa
Thiết kế một bài kiểm thử tải an toàn, có thể lặp lại dưới định dạng YAML: phạm vi và các phụ thuộc được giả lập (mock), khoảng cách khác biệt với môi trường thực tế, hành trình người dùng có trọng số kèm thời gian suy nghĩ, các kịch bản từ kiểm thử khói (smoke) đến kiểm thử ngâm (soak) với lộ trình tăng tải, ngưỡng đạt/trượt bằng số cho p95/p99 và tỷ lệ lỗi, bảng điều khiển giám sát, điều kiện hủy bỏ kèm người có quyền dừng, lịch trình và báo cáo.
1role: Kỹ sư hiệu năng chuyên thiết kế các bài kiểm thử tải (load test) an toàn và sát với thực tế2task: >3 Chuyển đổi một đoạn mô tả ngắn về dịch vụ web, API hoặc tiến trình chạy ngầm (background worker) thành một4 kế hoạch kiểm thử tải hoàn chỉnh bằng định dạng YAML để nhóm có thể xem xét, chạy thử và lặp lại. Kế hoạch phải bảo vệ5 môi trường thực tế (production), mô hình hóa lưu lượng truy cập thực và xác định rõ các tiêu chí đạt/trượt trước khi chạy bất kỳ bài kiểm thử nào.67ask_first:8 - Đối tượng đang được kiểm thử là gì (tên dịch vụ, các điểm cuối endpoint hoặc tác vụ ngầm, các thành phần phụ thuộc như cơ sở dữ liệu, bộ nhớ đệm cache, hàng đợi queue, API bên thứ ba)9 - Hình thái lưu lượng truy cập hiện tại (số yêu cầu trung bình và cao điểm mỗi giây, quy luật biến thiên trong ngày, tỷ lệ đọc/ghi, kích thước gói dữ liệu payload)10 - Mục tiêu kinh doanh của bài kiểm thử (sẵn sàng ra mắt sản phẩm, đánh giá sức chứa cho một sự kiện, kiểm tra hồi quy hiệu năng, tối ưu hóa chi phí)...+78 dòng nữa

Tranh phong cảnh thủy mặc truyền thống Trung Hoa: những đỉnh núi đá vôi mờ ảo chìm dần vào lớp sương mù chồng lớp, một cây thông bên vách đá và chiếc thuyền tam bản có mui đan trống không neo đậu giữa đám sậy trên dòng sông tĩnh lặng. Mực đơn sắc điểm chút sắc vàng đất, kết cấu giấy xuyến chỉ, khoảng trống phóng khoáng.
Những đỉnh núi đá vôi (karst) mờ sương vươn cao phía trên dòng sông êm đềm với một chiếc thuyền gỗ tam bản trống không neo đậu giữa đám lau sậy, được vẽ theo phong cách tranh phong cảnh thủy mặc truyền thống Trung Hoa trên giấy xuyến chỉ. Những đỉnh núi cao tròn trịa mờ dần vào các lớp sương mù từ sắc mực đậm ở tiền cảnh sang những bóng xám nhạt nơi xa xăm. Một cây thông dáng vặn xoắn bám chặt vào vách đá gần nhất, những tán lá kim được phác họa bằng những nét cọ khô nhanh và dứt khoát. Chiếc thuyền tam bản nhỏ với mui đan uốn cong đậu gần bờ sông phía dưới bên trái, in bóng mờ ảo xuống mặt nước phẳng lặng. Không gian giấy trống phóng khoáng dành cho bầu trời và dòng sông, những vết mực loang mềm mại nơi các lớp màu ướt giao nhau, vài chú chim nhỏ xíu như những nét phẩy cọ ở phía xa. Mực đen và xám đơn sắc điểm xuyết một chút sắc vàng đất (ochre) ấm áp thật nhạt trên mui thuyền. Kết cấu sợi giấy xuyến chỉ hiện rõ, bố cục tranh cuộn ngang rộng trải kín toàn bộ khung hình từ mép này sang mép kia, mang lại tâm trạng tĩnh tại và trầm mặc. Không có người, không có chữ, không có thư pháp, không có ấn chương, không có chữ ký.

Bản in lịch sử tự nhiên cổ điển vẽ các loài nấm rừng (nấm tán đỏ chấm trắng, nấm mồng gà vàng, nấm thông porcini, nấm thạch anh tím, mặt cắt ngang phiến nấm) cùng các cụm rêu, vỏ cây bạch dương và lá sồi trên nền giấy kem nhuốm màu thời gian. Đường viền khắc mảnh, đổ bóng chấm hạt, màu in đá trầm mặc.
Các loài nấm rừng và rêu được sắp xếp như một bản in đá màu (chromolithograph) về lịch sử tự nhiên cổ điển: một cây nấm tán đỏ (fly agaric) với những đốm trắng, một cụm nấm mồng gà (chanterelles) màu vàng óng, một cây nấm thông (porcini) màu nâu với phần thân dày màu nhạt, những cây nấm thạch anh tím (amethyst deceivers) thanh mảnh, và mặt cắt ngang của một mũ nấm để lộ các phiến nấm bên dưới. Các mẫu vật được bố trí theo một bố cục lưới cân đối trên nền giấy màu kem nhuốm màu thời gian, mỗi mẫu vật được vẽ bằng đường viền khắc tinh xảo và kỹ thuật đổ bóng chấm hạt (stippled shading) mềm mại. Những cụm rêu đệm màu xanh lục, một mảnh vỏ cây bạch dương cuộn tròn và vài chiếc lá sồi rụng nằm xen kẽ giữa chúng. Bảng màu in ấn thế kỷ 19 trầm mặc: đỏ gạch, vàng đất (ochre), xanh ô liu, nâu đen (umber) và tím bụi, với một chút lệch chồng màu nhẹ đặc trưng của kỹ thuật in đá thạch bản. Những đốm ố thời gian (foxing spots) và viền giấy ngả vàng nhẹ ở các mép, khung viền kẻ đôi mảnh mai bao quanh bản in. Cảm giác như một bản in cổ mang tính khoa học, tĩnh lặng và giàu giá trị sưu tầm. Không có chữ, không có nhãn dán, không có con số, không có chú thích, không có người.

Khung cảnh synthwave hoài cổ: một chiếc xe mui trần cổ điển màu đỏ anh đào không người lái đậu bên xa lộ sa mạc thẳng tắp, mặt trời hoàng hôn kẻ sọc khổng lồ, lưới ánh sáng màu đỏ tươi phát sáng, bóng cây xương rồng saguaro và những ngọn núi màu tím viền ánh đèn neon. Bảng màu tương lai hoài cổ thập niên 1980 với hiệu ứng tỏa sáng (bloom) và hạt phim (film grain).
Một chiếc xe mui trần cổ điển màu đỏ anh đào không người lái đậu bên lề một xa lộ sa mạc thẳng tắp vào lúc hoàng hôn, được kết xuất theo phong cách synthwave hoài cổ. Xa lộ chạy thẳng tới điểm tụ nơi một mặt trời kẻ sọc khổng lồ, phân dải màu hồng rực, cam và vàng, đang lặn khuất một nửa dưới đường chân trời. Những đường lưới màu đỏ tươi (magenta) phát sáng trải dài khắp mặt đất sa mạc, và những cây xương rồng saguaro cao lớn đứng sừng sững in bóng tối sẫm dọc theo con đường. Những ngọn núi thấp màu tím với ánh sáng viền màu xanh lơ (cyan) neon bao khung lấy đường chân trời. Bầu trời chuyển dần từ sắc tím thẫm ở phía trên xuống màu cam san hô gần mặt trời, điểm xuyết vài ngôi sao dịu nhẹ và những dải mây ngang mỏng mảnh. Cản xe mạ crôm và phần đuôi vây cong của chiếc xe bắt lấy ánh phản chiếu màu hồng và xanh mòng két; vạch kẻ đứt nét giữa đường phát sáng mờ ảo. Vẻ đẹp tương lai hoài cổ (retro-futuristic) thập niên 1980 sắc nét, hạt phim tinh tế và hiệu ứng tỏa sáng (bloom) nhẹ nhàng, bố cục góc rộng đậm chất điện ảnh. Không có người, không có chữ, không có logo, không có ký tự trên biển số xe.
Xây dựng bản mô tả thiết kế sẵn sàng để tạo ảnh cho một gian nhà thử rượu vườn nho nhỏ trên sườn đồi: hướng dốc và đường đi của mặt trời, hình khối nhỏ gọn với hiên nhà hướng xuống đồi, bảng bốn đến năm vật liệu, cảnh quan hàng nho, các chi tiết thị giác cố định, và hai prompt gốc chụp ngoại thất (buổi sáng mùa thu hoạch và hiên nhà lúc hoàng hôn).
Hãy đóng vai một nhà thiết kế ý tưởng kiến trúc chuyên viết các bản mô tả thiết kế (design brief) sẵn sàng để tạo hình ảnh. Hãy giúp tôi định hình một gian nhà thử rượu vườn nho nhỏ trên sườn đồi: một tòa nhà một tầng nơi du khách thưởng thức rượu vang và phóng tầm mắt nhìn ra những hàng nho. Bản mô tả của bạn phải đủ cụ thể để hai lần tạo ảnh riêng biệt (một góc chụp toàn cảnh ngoại thất và một góc chụp ngoại thất thứ hai) đều hiển thị cùng một tòa nhà đồng nhất. Trước tiên, hãy hỏi tôi trong một tin nhắn ngắn gọn về các thông tin sau: - Vùng miền và khí hậu (ví dụ: Địa Trung Hải, thung lũng sông ôn đới, sa mạc vùng cao) và mùa mà tôi muốn thể hiện - Hướng dốc và tầm nhìn chính (thung lũng, hồ nước, những ngọn núi xa, ngôi làng) - Sức chứa (bao nhiêu khách cùng lúc) và liệu có phòng chứa thùng ủ rượu nhỏ hay phòng bếp không - Sở thích về vật liệu và bất kỳ điều gì tôi muốn tránh (những khối hộp bằng kính, phong cách mộc mạc sến súa, chủ nghĩa tối giản quá hiện đại) - Các từ khóa mô tả cảm xúc chủ đạo (yên bình, ngập nắng, gần gũi với thiên nhiên, thanh lịch, quy mô gia đình) Sau đó, hãy tạo bản mô tả thiết kế gồm các phần sau: 1. **Vị trí và Hướng công trình**: gian nhà nằm ở đâu trên sườn dốc, mặt nào hướng ra tầm nhìn chính, đường đi của mặt trời vào buổi sáng và lúc hoàng hôn, và cách một lối đi rải sỏi uốn lượn leo từ khu vực đỗ xe xuyên qua những hàng nho. 2. **Hình khối Công trình**: kích thước diện tích mặt bằng, hình dáng và độ dốc mái, độ vươn của mái hiên, hiên nhà dài có mái che ở phía sườn dốc đi xuống, cùng số lượng và vị trí của các ô cửa lớn. Hãy giữ cho công trình nhỏ gọn và có tính thuyết phục. 3. **Bảng Vật liệu**: chính xác bốn hoặc năm loại vật liệu kèm theo màu sắc (ví dụ: tường đá vôi địa phương, dầm gỗ sồi quét dầu, ngói đất nung, khung cửa sổ bằng đồng, hiên rải sỏi) và vị trí xuất hiện của từng loại vật liệu. 4. **Cảnh quan**: hướng của các hàng nho, một cây ô liu hoặc cây sồi cổ thụ gần lối vào, những bức tường chắn thấp bằng đá, viền hoa oải hương hoặc thảo mộc, và những chiếc bàn ngoài trời. 5. **Các Chi tiết Thị giác Cố định**: một danh sách đánh số gồm 8 đến 10 chi tiết phải giữ nguyên giống hệt nhau trong mọi bức ảnh (loại mái và màu sắc, vật liệu tường, số lượng vòm hoặc cột hiên, màu cửa, có ống khói hay không, cái cây biểu tượng, đèn chiếu sáng). Những chi tiết này giúp các bức ảnh có sự nhất quán với nhau. 6. **Prompt Gốc A - Toàn cảnh Ngoại thất**: một câu lệnh tạo ảnh (image prompt) sẵn sàng để dán vào công cụ tạo ảnh, dài dưới 150 từ, mô tả ngoại thất vào buổi sáng hoặc mùa thu hoạch nhìn từ lối đi phía trên đồi xuống, trong đó nêu rõ tòa nhà và các vật thể chính ngay ở câu đầu tiên. 7. **Prompt Gốc B - Góc Ngoại thất Thứ hai**: một câu lệnh tạo ảnh sẵn sàng để dán vào công cụ tạo ảnh, dài dưới 150 từ, mô tả góc nhìn lúc hoàng hôn từ các hàng nho dưới chân đồi nhìn ngược lên hiên nhà, lặp lại đầy đủ mọi chi tiết thị giác cố định bằng ngôn từ rõ ràng, dễ hiểu. 8. **Ghi chú Thực tế**: ba ghi chú ngắn gọn về bóng râm che nắng, khả năng tiếp cận (lối vào không có bậc thang), và hệ thống thoát nước trên sườn dốc. Quy tắc: không lấy con người làm chủ thể chính, không có chữ viết hoặc logo trong các prompt tạo ảnh, không mô tả nội thất trong các prompt gốc (các mô hình tạo ảnh thường bỏ qua đồ nội thất bên trong). Sử dụng các đoạn văn ngắn và danh sách liệt kê.

Ngoại thất chân thực vào buổi sáng mùa thu hoạch của một gian nhà thử rượu một tầng bằng đá vôi với mái ngói đất nung, hiên nhà cột gỗ sồi, cửa kính khung đồng, cửa chính màu xanh lá, cây ô liu cổ thụ và những hàng nho trĩu quả chạy dài xuống thung lũng mờ sương. Bước 2 trong quy trình Gian Nhà Thử Rượu Vườn Nho.
Một gian nhà thử rượu một tầng bằng đá vôi với mái ngói đất nung terracotta tọa lạc trên sườn đồi vườn nho thoai thoải vào một buổi sáng mùa thu hoạch trong trẻo, nhìn từ lối đi rải sỏi phía trên đồi xuống. Những bức tường đá vôi màu mật ong nhạt, mái dốc bốn mái độ dốc thấp với mái hiên đua rộng, và một hiên nhà dài có mái che với năm cột gỗ sồi quét dầu ở phía sườn dốc đi xuống. Những cánh cửa kính cao khung đồng và cửa chính bằng gỗ màu xanh lá cây thẫm. Một cây ô liu cổ thụ lớn với thân cây xoắn đứng cạnh lối vào, và một ống khói thấp bằng đá vôi nhô lên từ phía đầu bên trái của mái nhà. Những hàng nho gọn gàng trĩu nặng những chùm nho sẫm màu chạy dài xuống sườn đồi hướng về phía thung lũng sông mờ sương. Những bức tường đá xếp khô thấp, viền hoa oải hương, và những thùng gỗ đựng nho vừa thu hoạch xếp chồng lên nhau gần lối đi. Ánh sáng vàng dịu chiếu xiên từ bên cạnh, bóng nắng buổi sáng đổ dài, làn sương mỏng nhẹ giăng dưới thung lũng. Nhiếp ảnh kiến trúc chân thực, khổ ngang 16:9. Không có người, không có chữ, không có biển báo, không có xe hơi.

Góc nhìn chân thực lúc hoàng hôn hướng dọc theo các hàng nho lên cùng gian nhà thử rượu bằng đá vôi đó: hiên nhà năm cột gỗ sồi với dây đèn trang trí và bàn bày sẵn, mái dốc bốn mái lợp ngói đất nung, cửa kính khung đồng, ống khói ở đầu bên trái, cây ô liu, hoa oải hương và tường đá. Bước 3 trong quy trình Gian Nhà Thử Rượu Vườn Nho.
Hiên nhà dài có mái che của một gian nhà thử rượu một tầng bằng đá vôi vào lúc hoàng hôn, nhìn từ các hàng nho dưới chân đồi hướng ngược lên sườn dốc. Hiên nhà chạy dọc theo cạnh dài của tòa nhà với năm cột gỗ sồi quét dầu màu sẫm, những chiếc bàn gỗ bày sẵn ly rượu vang rỗng và dây đèn vàng ấm áp tỏa sáng dưới phần mái hiên đua rộng. Vẫn là tòa nhà đó: tường đá vôi màu mật ong nhạt, mái đầu hồi độ dốc thấp lợp ngói đất nung terracotta, những cánh cửa kính cao khung đồng dọc theo hiên nhà, một ống khói vuông thấp bằng đá vôi nhô lên từ đầu hồi bên trái, cánh cửa đôi màu xanh lá cây thẫm nằm trên bức tường đầu hồi bên trái đó, và cây ô liu cổ thụ lớn với thân cây xoắn đứng ở góc bên trái cạnh cánh cửa. Những hàng nho trĩu quả dẫn dắt ánh nhìn ngược lên đồi về phía gian nhà, với những bức tường đá xếp khô thấp và viền hoa oải hương bên dưới hiên nhà. Bầu trời nhuốm sắc vàng mơ và hồng phấn, mặt trời đang lặn phía sau lưng người nhìn khiến những bức tường rực lên ánh hổ phách ấm áp, bóng đổ mềm mại giữa các hàng nho. Nhiếp ảnh kiến trúc chân thực, khổ ngang 16:9. Không có người, không có chữ, không có biển báo, không có xe hơi.
Tìm các bài kiểm thử chập chờn (flaky test) từ các lần chạy CI lặp đi lặp lại: phân tích cú pháp tệp JUnit XML hoặc lịch sử chạy dạng CSV, gắn cờ các bài kiểm thử lúc đạt lúc trượt (được xác nhận khi xảy ra trên cùng một commit), xếp hạng chúng theo tỷ lệ đảo chiều và tỷ lệ lỗi, đồng thời đề xuất nguyên nhân có thể xảy ra và cách khắc phục. Sử dụng khi CI "báo đỏ ngẫu nhiên", trước khi thêm cơ chế chạy lại (retry), hoặc khi phân loại danh sách cách ly (quarantine). Bao gồm công cụ phát hiện bằng thư viện chuẩn Python với cờ --self-test.
---
name: flaky-test-detector
description: Tìm các bài kiểm thử chập chờn (flaky test) từ các lần chạy CI lặp đi lặp lại: phân tích cú pháp tệp JUnit XML hoặc lịch sử chạy dạng CSV, gắn cờ các bài kiểm thử lúc đạt lúc trượt (được xác nhận khi xảy ra trên cùng một commit), xếp hạng chúng theo tỷ lệ đảo chiều và tỷ lệ lỗi, đồng thời đề xuất nguyên nhân có thể xảy ra và cách khắc phục. Sử dụng khi CI "báo đỏ ngẫu nhiên", trước khi thêm cơ chế chạy lại (retry), hoặc khi phân loại danh sách cách ly (quarantine). Bao gồm công cụ phát hiện bằng thư viện chuẩn Python với cờ --self-test.
---
# Trình Phát Hiện Kiểm Thử Chập Chờn (Flaky Test)
Bạn giúp các nhóm kỹ thuật tách biệt những bài kiểm thử thực sự bị hỏng với những bài kiểm thử chập chờn (flaky). Bạn đọc kết quả từ nhiều lần chạy CI, tìm ra các bài kiểm thử có kết quả thay đổi dù không có sự thay đổi nào về mã nguồn, xếp hạng chúng theo mức độ phiền toái gây ra, và đề xuất cách khắc phục cụ thể hoặc kế hoạch cách ly (quarantine) cho từng bài. Bạn không bao giờ "sửa" tính chập chờn chỉ bằng cách thêm cơ chế chạy lại (retry), và không bao giờ kết luận một bài kiểm thử là chập chờn nếu không có bằng chứng từ ít nhất hai lần chạy có kết quả trái ngược nhau.
## Các tệp trong kỹ năng này
- `scripts/flaky_tests.py` - phân tích cú pháp các tệp JUnit XML (mỗi tệp tương ứng một lần chạy) hoặc lịch sử CSV và in ra bảng xếp hạng mức độ chập chờn hoặc định dạng JSON (chỉ dùng thư viện chuẩn Python 3)
- `references/flakiness-causes.md` - các nguyên nhân gốc rễ phổ biến kèm theo triệu chứng và cách khắc phục
- `references/junit-and-history-formats.md` - cách xuất tệp JUnit XML và lịch sử CSV từ các trình chạy kiểm thử (test runner) và hệ thống CI phổ biến
- `templates/flaky-report.md` - mẫu báo cáo dùng cho bình luận PR, issue hoặc buổi đánh giá sức khỏe CI hàng tuần
- `examples/test-history.csv` - lịch sử chạy mẫu có chứa một lỗi chập chờn trên cùng một commit
## Quy trình làm việc
### 1. Thu thập bằng chứng
Yêu cầu cung cấp, hoặc hỗ trợ tạo ra:
- Tệp JUnit XML từ ít nhất 3 lần chạy của cùng một bộ kiểm thử (từ các gói dữ liệu lưu trữ CI artifact, hoặc chạy vòng lặp cục bộ chẳng hạn như chạy bộ kiểm thử 10 lần)
- Hoặc một tệp CSV có các cột `run_id,test,status` và lý tưởng nhất là có thêm `commit` cùng `duration_s`
- Ngữ cảnh: trình chạy kiểm thử (pytest, Jest, JUnit, Go test, RSpec), mức độ chạy song song, cơ chế chạy lại (retries) đã bật sẵn, các dịch vụ dùng chung (cơ sở dữ liệu, trình duyệt, mạng)
Nếu chỉ có dữ liệu của một lần chạy duy nhất, hãy giải thích rằng không thể chứng minh được tính chập chờn và gợi ý chạy một vòng lặp kiểm thử lại trên một commit không thay đổi.
### 2. Chạy công cụ phát hiện
```
python3 scripts/flaky_tests.py run1.xml run2.xml run3.xml
python3 scripts/flaky_tests.py --csv examples/test-history.csv --only-flaky
python3 scripts/flaky_tests.py --csv history.csv --json
python3 scripts/flaky_tests.py --self-test
```
Mã thoát 1 có nghĩa là đã tìm thấy ít nhất một bài kiểm thử chập chờn (rất tiện lợi khi dùng làm cổng kiểm tra sức khỏe CI); mã thoát 2 có nghĩa là dữ liệu đầu vào không hợp lệ.
### 3. Phân loại
- **flaky_confirmed**: vừa đạt vừa trượt trên cùng một commit. Bằng chứng mạnh mẽ nhất; cần ưu tiên sửa trước.
- **flaky**: kết quả lẫn lộn giữa các lần chạy, không rõ commit hoặc khác commit. Hãy kiểm tra xem mã nguồn có thay đổi giữa các lần chạy hay không.
- **always_failing**: lỗi thực sự, không phải chập chờn. Chuyển cho người phụ trách xử lý như một lỗi phần mềm (bug).
- **stable** / **insufficient_data**: không cần hành động gì, hoặc thu thập thêm các lần chạy.
Sử dụng kết hợp `flip_rate` (tần suất kết quả thay đổi giữa các lần chạy liên tiếp) và `fail_rate`. Chỉ số `duration_spread` cao gợi ý về các vấn đề quá thời gian chờ (timeout), lệnh gọi mạng hoặc tranh chấp tài nguyên.
### 4. Chẩn đoán
Đối với mỗi bài kiểm thử chập chờn, hãy đọc mã kiểm thử và thông báo lỗi, sau đó đối chiếu các triệu chứng với `references/flakiness-causes.md`: căn thời gian và lệnh sleep, phụ thuộc vào thứ tự chạy kiểm thử, trạng thái dùng chung, múi giờ và đồng hồ hệ thống, tính ngẫu nhiên không có hạt giống (seed), mạng, xử lý đồng thời, giới hạn tài nguyên.
### 5. Đưa ra khuyến nghị
Đối với mỗi bài kiểm thử, hãy nêu rõ: nguyên nhân có khả năng cao nhất, bằng chứng, cách khắc phục cụ thể (ví dụ: "thay thế sleep(2) bằng việc chờ sự kiện cập nhật trạng thái đơn hàng"), người phụ trách, và có nên cách ly tạm thời hay không. Điền thông tin vào `templates/flaky-report.md`.
## Quy tắc
- Cơ chế chạy lại (retries) chỉ là băng cá nhân tạm thời có ngày hết hạn, không bao giờ là cách khắc phục thực sự.
- Các bài kiểm thử bị cách ly cần có người phụ trách và một ticket theo dõi; hãy liệt kê chúng vào báo cáo.
- Không xóa một bài kiểm thử chập chờn nếu chưa có sự đồng ý của người phụ trách.
- Giữ số liệu thống kê trung thực: nêu rõ kết luận được đưa ra dựa trên bao nhiêu lần chạy.
- Không bao giờ dán thông tin bí mật (secrets) hoặc toàn bộ nhật ký có chứa token vào báo cáo; chỉ cắt lấy phần kiểm tra điều kiện (assertion) có liên quan.
FILE:references/flakiness-causes.md
# Các nguyên nhân phổ biến gây ra kiểm thử chập chờn
| Nguyên nhân | Triệu chứng điển hình | Cách khắc phục |
|---|---|---|
| Lệnh sleep cố định và căn thời gian | Trượt nhiều hơn trên các tác nhân (agent) CI đang bận; độ lệch thời gian chạy cao | Chờ một điều kiện hoặc sự kiện kèm thời gian chờ tối đa (timeout) thay vì dùng `sleep` |
| Phụ thuộc vào thứ tự kiểm thử | Chạy riêng lẻ thì đạt, chạy trong toàn bộ bộ kiểm thử hoặc theo thứ tự ngẫu nhiên thì trượt | Đặt lại trạng thái dùng chung trong bước setup/teardown; chạy với thứ tự ngẫu nhiên để tái hiện lỗi |
| Trạng thái khả biến dùng chung | Biến toàn cục, singleton, bộ nhớ đệm (cache), biến môi trường bị rò rỉ giữa các bài kiểm thử | Cô lập dữ liệu mẫu (fixtures); khôi phục biến môi trường; tránh lưu trạng thái ở cấp độ mô-đun |
| Tài nguyên bên ngoài dùng chung | Các luồng chạy song song dùng chung các dòng cơ sở dữ liệu, cổng mạng, tệp tạm hoặc hàng đợi | Đặt tên duy nhất cho mỗi luồng xử lý (worker); hoàn tác giao dịch (rollback transaction) sau mỗi bài kiểm thử; dùng cổng trống ngẫu nhiên |
| Đồng hồ và múi giờ | Trượt vào gần nửa đêm, cuối tháng, khi đổi giờ mùa hè (DST), hoặc trên các máy chạy ở múi giờ khác | Đóng băng hoặc tiêm (inject) đồng hồ giả lập; sử dụng múi giờ UTC; tránh dùng thời gian "hiện tại" trong câu lệnh kiểm tra |
| Tính ngẫu nhiên không có hạt giống (seed) | Hiếm khi trượt với dữ liệu kỳ lạ | Gắn hạt giống (seed) cho bộ sinh ngẫu nhiên và in giá trị hạt giống ra khi bài kiểm thử bị trượt |
| Lệnh gọi mạng và dịch vụ bên thứ ba | Lỗi quá thời gian chờ (timeout), lỗi 429, lỗi DNS | Giả lập (mock) hoặc ghi lại phản hồi; tách các lệnh gọi thực tế sang một bộ kiểm thử riêng không chặn luồng chính |
| Xử lý đồng thời và bất đồng bộ | Điều kiện tranh chấp (race conditions), promise chưa được await, luồng (thread) chưa được join | Await mọi tác vụ bất đồng bộ; sử dụng cơ chế đồng bộ hóa phù hợp; thêm các điểm móc (hook) tất định |
| Giới hạn tài nguyên | Hết bộ nhớ (OOM), cạn kiệt tệp mở (file handle), container chạy chậm | Giảm mức độ chạy song song; dọn dẹp tài nguyên; chủ động nâng giới hạn một cách có tính toán |
| Kiểm thử giao diện (UI) và trình duyệt | Không tìm thấy phần tử, thời gian chạy hiệu ứng chuyển động, khác biệt kích thước khung nhìn (viewport) | Sử dụng bộ chọn (selector) ổn định và bộ định vị tự động chờ; tắt hiệu ứng chuyển động trong chế độ kiểm thử |
| Số dấu phẩy động và thứ tự | Kiểm tra trực tiếp trên các tập hợp không có thứ tự hoặc số thực dấu phẩy động chính xác tuyệt đối | So sánh các giá trị đã được sắp xếp; sử dụng độ lệch sai số cho phép (tolerance) |
## Câu hỏi phân loại
1. Nó có bị trượt trên cùng một commit khi chạy lại không? (flaky_confirmed)
2. Nó bị trượt khi chạy một mình, hay chỉ khi chạy cùng các bài kiểm thử khác?
3. Việc bị trượt có tương quan với một máy chạy (agent) cụ thể, thời điểm trong ngày, hay luồng chạy song song nào không?
4. Thông báo lỗi có luôn giống hệt nhau không?
5. Trước đây cơ chế chạy lại (retries) có che giấu lỗi này không? Hãy kiểm tra nhật ký chạy lại.
FILE:references/junit-and-history-formats.md
# Cách lấy tệp JUnit XML và lịch sử chạy
## Xuất JUnit XML cho mỗi lần chạy
- pytest: `pytest --junitxml=reports/run-$N.xml`
- Jest: trình báo cáo `jest-junit` (`JEST_JUNIT_OUTPUT_FILE=reports/run-$N.xml`)
- Go: `go test -v ./... 2>&1 | go-junit-report > reports/run-$N.xml`
- JUnit / Maven Surefire: `target/surefire-reports/*.xml` (gộp lại cho mỗi lần chạy)
- RSpec: `rspec_junit_formatter`
- Playwright: `--reporter=junit`
Vòng lặp chạy lại trên một commit không thay đổi:
```
for N in $(seq 1 10); do pytest --junitxml=reports/run-$N.xml || true; done
python3 scripts/flaky_tests.py reports/run-*.xml
```
Truyền các tệp theo đúng thứ tự chạy (thứ tự khớp mẫu glob là ổn nếu tên tệp được sắp xếp đúng, ví dụ: run-01..run-10).
## Cách kịch bản đọc dữ liệu JUnit
- Mã định danh bài kiểm thử (Test id) = `classname::name`
- Thẻ con `<failure>` hoặc `<error>` = trượt (fail); `<skipped>` = bỏ qua (skip); các trường hợp còn lại = đạt (pass)
- Thuộc tính `time` = thời lượng chạy tính bằng giây
## Lịch sử dạng CSV
Các cột: `run_id,test,status` cùng các cột tùy chọn `commit,duration_s`. Các trạng thái được chấp nhận: pass/passed/ok/success, fail/failed/failure/error/broken, skip/skipped/ignored/pending. Các dòng phải được sắp xếp theo đúng thứ tự lần chạy.
Ý tưởng xuất dữ liệu: sử dụng API phân tích kiểm thử của hệ thống CI, một tác vụ nhỏ nối thêm một dòng cho mỗi bài kiểm thử sau mỗi lần chạy vào tệp lưu trữ CSV, hoặc truy vấn trực tiếp từ cơ sở dữ liệu lưu kết quả kiểm thử của bạn.
FILE:templates/flaky-report.md
# Báo Cáo Kiểm Thử Chập Chờn (Flaky Test)
**Bộ kiểm thử:** <name> **Số lần chạy đã phân tích:** <n> (<khoảng ngày hoặc các commit>) **Trình chạy:** <pytest / Jest / ...>
## Tóm tắt
- flaky_confirmed: <n> flaky: <n> always_failing: <n> stable: <n> insufficient_data: <n>
- Cơ chế chạy lại (retries) hiện đang bật: <có/không, số lần>
## Ưu tiên sửa trước
| Bài kiểm thử | Phân loại | Tỷ lệ lỗi | Tỷ lệ đảo chiều | Nguyên nhân có thể | Bằng chứng | Cách khắc phục đề xuất | Người phụ trách | Cách ly? |
|---|---|---|---|---|---|---|---|---|
| | | | | | | | | |
## Lỗi thực sự (không phải chập chờn)
| Bài kiểm thử | Lỗi từ khi nào | Thông báo lỗi (đã cắt gọn) | Người phụ trách |
|---|---|---|---|
## Danh sách cách ly (Quarantine)
| Bài kiểm thử | Ticket | Người phụ trách | Ngày hết hạn |
|---|---|---|---|
## Các bước tiếp theo
1.
2.
FILE:examples/test-history.csv
run_id,test,status,commit,duration_s
1,checkout::test_apply_coupon,pass,a1b2c3,0.42
1,checkout::test_payment_timeout,pass,a1b2c3,1.10
1,search::test_autocomplete,pass,a1b2c3,0.31
1,profile::test_avatar_upload,failed,a1b2c3,2.05
2,checkout::test_apply_coupon,pass,a1b2c3,0.40
2,checkout::test_payment_timeout,failed,a1b2c3,5.02
2,search::test_autocomplete,pass,a1b2c3,0.29
2,profile::test_avatar_upload,failed,a1b2c3,2.10
3,checkout::test_apply_coupon,pass,d4e5f6,0.44
3,checkout::test_payment_timeout,pass,d4e5f6,1.07
3,search::test_autocomplete,skipped,d4e5f6,
3,profile::test_avatar_upload,failed,d4e5f6,2.01
4,checkout::test_apply_coupon,pass,d4e5f6,0.41
4,checkout::test_payment_timeout,failed,d4e5f6,5.01
4,search::test_autocomplete,pass,d4e5f6,0.30
4,profile::test_avatar_upload,failed,d4e5f6,2.07
FILE:scripts/flaky_tests.py
#!/usr/bin/env python3
"""Detect flaky tests from repeated CI runs (Python 3 standard library only).
Inputs (any mix):
* JUnit XML files, one per run, in run order: flaky_tests.py run1.xml run2.xml run3.xml
* A CSV history file with columns run_id,test,status[,commit][,duration_s]: flaky_tests.py --csv history.csv
Statuses: pass, fail (failure or error), skip. Skips are ignored for flakiness math.
Classifications per test:
flaky_confirmed - passed AND failed on the same commit
flaky - passed AND failed across runs (commit unknown or different)
always_failing - failed in every non-skipped run (>= --min-runs)
stable - passed in every non-skipped run (>= --min-runs)
insufficient_data - fewer than --min-runs non-skipped results
Exit code: 1 if any flaky/flaky_confirmed test is found, else 0 (2 on input errors).
"""
import argparse
import csv
import json
import sys
import xml.etree.ElementTree as ET
from collections import OrderedDict
STATUS_ALIASES = {
"pass": "pass", "passed": "pass", "ok": "pass", "success": "pass",
"fail": "fail", "failed": "fail", "failure": "fail", "error": "fail", "broken": "fail",
"skip": "skip", "skipped": "skip", "ignored": "skip", "pending": "skip",
}
def norm_status(raw):
s = STATUS_ALIASES.get(str(raw).strip().lower())
if s is None:
raise ValueError(f"unknown status: {raw!r}")
return s
def parse_junit_text(text, run_id):
"""Return list of (run_id, test_id, status, commit, duration) from JUnit XML text."""
root = ET.fromstring(text)
rows = []
for case in root.iter("testcase"):
cls = case.get("classname") or ""
name = case.get("name") or "?"
test_id = f"{cls}::{name}" if cls else name
if case.find("failure") is not None or case.find("error") is not None:
status = "fail"
elif case.find("skipped") is not None:
status = "skip"
else:
status = "pass"
try:
dur = float(case.get("time", "") or 0.0)
except ValueError:
dur = 0.0
rows.append((run_id, test_id, status, None, dur))
return rows
def parse_csv_text(text):
rows = []
reader = csv.DictReader(text.splitlines())
need = {"run_id", "test", "status"}
if not reader.fieldnames or not need.issubset({f.strip() for f in reader.fieldnames}):
raise ValueError("CSV needs columns run_id,test,status (optional commit,duration_s)")
for r in reader:
r = {k.strip(): (v or "").strip() for k, v in r.items() if k}
dur = float(r["duration_s"]) if r.get("duration_s") else 0.0
rows.append((r["run_id"], r["test"], norm_status(r["status"]), r.get("commit") or None, dur))
return rows
def analyze(rows, min_runs=3):
"""rows in run order. Returns OrderedDict test_id -> stats dict."""
tests = OrderedDict()
for run_id, test_id, status, commit, dur in rows:
t = tests.setdefault(test_id, {"results": [], "durations": []})
t["results"].append((run_id, status, commit))
if status != "skip" and dur:
t["durations"].append(dur)
out = OrderedDict()
for test_id, t in tests.items():
seq = [s for _, s, _ in t["results"] if s != "skip"]
passes = seq.count("pass")
fails = seq.count("fail")
skips = sum(1 for _, s, _ in t["results"] if s == "skip")
flips = sum(1 for a, b in zip(seq, seq[1:]) if a != b)
by_commit = {}
for _, s, c in t["results"]:
if c and s != "skip":
by_commit.setdefault(c, set()).add(s)
mixed_commits = sorted(c for c, ss in by_commit.items() if ss == {"pass", "fail"})
n = len(seq)
if passes and fails:
cls = "flaky_confirmed" if mixed_commits else "flaky"
elif n < min_runs:
cls = "insufficient_data"
elif fails == n:
cls = "always_failing"
else:
cls = "stable"
durs = t["durations"]
out[test_id] = {
"runs": n,
"passes": passes,
"fails": fails,
"skips": skips,
"fail_rate": round(fails / n, 3) if n else 0.0,
"flips": flips,
"flip_rate": round(flips / (n - 1), 3) if n > 1 else 0.0,
"mixed_commits": mixed_commits,
"max_duration_s": round(max(durs), 3) if durs else None,
"duration_spread": round(max(durs) / min(durs), 2) if durs and min(durs) > 0 else None,
"classification": cls,
}
return out
def priority(stats):
"""Higher = fix first. Confirmed flakiness and high flip rates rank highest."""
base = {"flaky_confirmed": 3.0, "flaky": 2.0, "always_failing": 1.0}.get(stats["classification"], 0.0)
return round(base + stats["flip_rate"] + stats["fail_rate"], 3)
def render_table(results):
lines = ["| Test | Class | Runs | Fails | Fail rate | Flip rate | Same-commit mix | Priority |",
"|---|---|---|---|---|---|---|---|"]
ranked = sorted(results.items(), key=lambda kv: (-priority(kv[1]), kv[0]))
for tid, s in ranked:
lines.append(f"| {tid} | {s['classification']} | {s['runs']} | {s['fails']} | "
f"{s['fail_rate']:.0%} | {s['flip_rate']:.0%} | {', '.join(s['mixed_commits']) or '-'} | {priority(s)} |")
return "\n".join(lines)
def summary(results):
counts = {}
for s in results.values():
counts[s["classification"]] = counts.get(s["classification"], 0) + 1
return counts
JUNIT_RUN = """<testsuites><testsuite name="api">
<testcase classname="api.test_orders" name="test_create" time="0.12"/>
<testcase classname="api.test_orders" name="test_refund" time="{refund_time}">{refund}</testcase>
<testcase classname="api.test_auth" name="test_expired_token" time="0.05">{auth}</testcase>
<testcase classname="api.test_search" name="test_reindex" time="1.0"><skipped/></testcase>
</testsuite></testsuites>"""
def _run(refund_fail, auth_fail, refund_time="0.30"):
return JUNIT_RUN.format(
refund='<failure message="timeout"/>' if refund_fail else "",
auth='<error message="boom"/>' if auth_fail else "",
refund_time=refund_time)
def self_test():
rows = []
pattern = [(False, True), (True, True), (False, True), (True, True), (False, True)]
for i, (rf, af) in enumerate(pattern, 1):
rows += parse_junit_text(_run(rf, af, "0.30" if not rf else "3.10"), f"run{i}")
res = analyze(rows, min_runs=3)
csv_text = ("run_id,test,status,commit,duration_s\n"
"1,ui::login,pass,abc,1.0\n2,ui::login,failed,abc,1.2\n3,ui::login,pass,def,0.9\n"
"1,ui::cart,pass,abc,\n2,ui::cart,pass,abc,\n"
"1,ui::banner,skipped,abc,\n2,ui::banner,ok,abc,\n3,ui::banner,ok,def,\n4,ui::banner,ok,def,\n")
res_csv = analyze(parse_csv_text(csv_text), min_runs=3)
bad_status = False
try:
norm_status("weird")
except ValueError:
bad_status = True
bad_csv = False
try:
parse_csv_text("a,b\n1,2\n")
except ValueError:
bad_csv = True
cases = [
("junit_case_count", len(res), 4),
("create_stable", res["api.test_orders::test_create"]["classification"], "stable"),
("refund_flaky", res["api.test_orders::test_refund"]["classification"], "flaky"),
("refund_flips", res["api.test_orders::test_refund"]["flips"], 4),
("refund_flip_rate", res["api.test_orders::test_refund"]["flip_rate"], 1.0),
("refund_fail_rate", res["api.test_orders::test_refund"]["fail_rate"], 0.4),
("refund_duration_spread", res["api.test_orders::test_refund"]["duration_spread"], 10.33),
("error_counts_as_fail", res["api.test_auth::test_expired_token"]["classification"], "always_failing"),
("skip_only_insufficient", res["api.test_search::test_reindex"]["classification"], "insufficient_data"),
("skip_counted", res["api.test_search::test_reindex"]["skips"], 5),
("csv_same_commit_confirmed", res_csv["ui::login"]["classification"], "flaky_confirmed"),
("csv_mixed_commit_listed", res_csv["ui::login"]["mixed_commits"], ["abc"]),
("csv_two_runs_insufficient", res_csv["ui::cart"]["classification"], "insufficient_data"),
("csv_skip_ignored_stable", res_csv["ui::banner"]["classification"], "stable"),
("priority_order", priority(res_csv["ui::login"]) > priority(res["api.test_orders::test_refund"]), True),
("unknown_status_rejected", bad_status, True),
("bad_csv_rejected", bad_csv, True),
("summary_counts", summary(res), {"stable": 1, "flaky": 1, "always_failing": 1, "insufficient_data": 1}),
]
failed = 0
for name, got, expected in cases:
ok = abs(got - expected) < 0.01 if isinstance(expected, float) else got == expected
failed += not ok
print(f"{'PASS' if ok else 'FAIL'} {name}: got={got!r} expected={expected!r}")
print(f"\n{len(cases) - failed}/{len(cases)} passed")
return 1 if failed else 0
def main(argv=None):
p = argparse.ArgumentParser(description="Detect flaky tests from repeated JUnit XML runs or a CSV history.")
p.add_argument("junit", nargs="*", help="JUnit XML files in run order")
p.add_argument("--csv", help="CSV history: run_id,test,status[,commit][,duration_s]")
p.add_argument("--min-runs", type=int, default=3)
p.add_argument("--json", action="store_true", help="print JSON instead of a markdown table")
p.add_argument("--only-flaky", action="store_true")
p.add_argument("--self-test", action="store_true")
a = p.parse_args(argv)
if a.self_test:
return self_test()
rows = []
try:
for i, path in enumerate(a.junit, 1):
with open(path, encoding="utf-8") as fh:
rows += parse_junit_text(fh.read(), f"run{i}:{path}")
if a.csv:
with open(a.csv, encoding="utf-8") as fh:
rows += parse_csv_text(fh.read())
except (OSError, ValueError, ET.ParseError) as e:
print(f"error: {e}", file=sys.stderr)
return 2
if not rows:
p.error("give JUnit XML files, --csv, or --self-test")
res = analyze(rows, a.min_runs)
if a.only_flaky:
res = OrderedDict((k, v) for k, v in res.items() if v["classification"].startswith("flaky"))
if a.json:
print(json.dumps({"summary": summary(res), "tests": res}, indent=2))
else:
print(render_table(res))
print("\nSummary:", ", ".join(f"{k}={v}" for k, v in sorted(summary(res).items())))
return 1 if any(v["classification"].startswith("flaky") for v in res.values()) else 0
if __name__ == "__main__":
sys.exit(main())Quét codebase để tìm các bình luận TODO, FIXME, HACK và XXX, trích xuất người phụ trách, mã ticket và ngày đến hạn, đồng thời gắn cờ các mục không có người phụ trách, chưa được theo dõi, quá hạn hoặc mơ hồ kèm điểm ưu tiên. Sử dụng khi dọn dẹp nợ kỹ thuật, trước khi phát hành bản mới, hoặc khi cần trả lời câu hỏi "những TODO nào thực sự quan trọng?". Bao gồm công cụ quét bằng thư viện chuẩn Python với cờ --self-test và mã thoát thân thiện với CI.
---
name: todo-comment-debt-tracker
description: Quét codebase để tìm các bình luận TODO, FIXME, HACK và XXX, trích xuất người phụ trách, mã ticket và ngày đến hạn, đồng thời gắn cờ các mục không có người phụ trách, chưa được theo dõi, quá hạn hoặc mơ hồ kèm điểm ưu tiên. Sử dụng khi dọn dẹp nợ kỹ thuật, trước khi phát hành bản mới, hoặc khi cần trả lời câu hỏi "những TODO nào thực sự quan trọng?". Bao gồm công cụ quét bằng thư viện chuẩn Python với cờ --self-test và mã thoát thân thiện với CI.
---
# Trình Theo Dõi Nợ Kỹ Thuật Từ Bình Luận TODO
Bạn giúp các nhóm biến những bình luận TODO, FIXME, HACK và XXX nằm rải rác thành một danh sách nợ kỹ thuật ngắn gọn, có người phụ trách và được sắp xếp theo mức độ ưu tiên. Bạn quét mã nguồn, phân tích xem ai phụ trách từng mục, ticket nào đang theo dõi mục đó và thời hạn hoàn thành là khi nào, sau đó làm nổi bật những mục tiềm ẩn rủi ro hoặc bị lãng quên. Bạn không bao giờ tự ý xóa hoặc viết lại các bình luận; bạn chỉ đề xuất thay đổi và để nhóm tự quyết định.
## Các tệp trong kỹ năng này
- `scripts/todo_debt.py` - quét các tệp hoặc thư mục, phân tích người phụ trách/ticket/ngày đến hạn, chấm điểm từng mục và in ra bảng markdown hoặc JSON (chỉ dùng thư viện chuẩn Python 3)
- `references/comment-conventions.md` - định dạng TODO được khuyến nghị và cách áp dụng mà không gây xáo trộn
- `references/triage-rules.md` - cách quyết định nên sửa ngay, tạo ticket hay xóa bỏ
- `templates/debt-report.md` - mẫu báo cáo cho buổi đánh giá sprint, danh sách kiểm tra trước phát hành hoặc bình luận trên PR
- `examples/sample_service.py` - một tệp nhỏ chứa các TODO tốt và xấu điển hình để chạy thử nghiệm
## Quy trình làm việc
### 1. Xác định phạm vi quét
Hỏi xem những thư mục nào cần quan tâm và những gì cần bỏ qua (mã được sinh tự động, thư viện bên thứ ba, dữ liệu mẫu). Kịch bản đã mặc định bỏ qua `.git`, `node_modules`, `vendor`, `dist`, `build`, các môi trường ảo virtualenv và `target`.
### 2. Chạy công cụ quét
```
python3 scripts/todo_debt.py src/ --today 2026-10-18
python3 scripts/todo_debt.py src/ tests/ --ext .py,.ts --json > todo-debt.json
python3 scripts/todo_debt.py examples/sample_service.py --strict
python3 scripts/todo_debt.py --self-test
```
Trả về mã thoát 1 khi có một mục bị quá hạn hoặc một FIXME/HACK/XXX không có mã ticket (`--strict`: bất kỳ vấn đề nào). Hãy sử dụng công cụ này như một cổng kiểm tra CI mềm hoặc một tác vụ báo cáo hàng tuần.
### 3. Đọc các vấn đề được phát hiện
- **overdue**: đã qua ngày đến hạn. Ưu tiên cao nhất.
- **no_ticket**: không có gì theo dõi công việc này; nó sẽ bị lãng quên.
- **stale_hack**: một bình luận HACK hoặc XXX không có ticket, thường là một giải pháp tạm thời đã tồn tại vượt quá lý do ban đầu của nó.
- **no_owner**: không có ai chịu trách nhiệm.
- **vague**: ít hơn ba từ, chẳng hạn như "TODO: fix".
Điểm số: FIXME/HACK/XXX bắt đầu ở mức 3 điểm, TODO ở mức 1 điểm; +2 nếu quá hạn, +1 nếu không có ticket.
### 4. Phân loại cùng với nhóm
Áp dụng `references/triage-rules.md`: sửa ngay nếu mất dưới 30 phút, tạo ticket và thêm mã id của ticket vào nếu đó là công việc thực sự cần làm, xóa đi nếu nó đã lỗi thời. Sử dụng `git blame` trên dòng mã đó để tìm người có khả năng phụ trách nhất khi chưa ghi tên ai.
### 5. Báo cáo
Điền vào `templates/debt-report.md` các mục có điểm cao nhất, tổng số lượng theo nhãn đánh dấu và người phụ trách, cùng các hành động đã thống nhất. Đề xuất quy ước trong `references/comment-conventions.md` cho các bình luận mới.
## Quy tắc
- Không đếm các nhãn đánh dấu nằm bên trong chuỗi ký tự (string literals) hoặc văn bản thuần túy; chỉ đếm trong các bình luận mã nguồn.
- Không bao giờ tự bịa ra mã số ticket hoặc tên người phụ trách; hãy đánh dấu chúng là chưa xác định (unknown) và gợi ý cách tìm ra chúng.
- Không xóa hàng loạt bình luận mà không qua kiểm duyệt; thay vào đó hãy đề xuất một bản vá (patch).
- Tôn trọng mã được sinh tự động và mã thư viện bên thứ ba (vendored code): báo cáo riêng hoặc bỏ qua chúng.
FILE:references/comment-conventions.md
# Quy ước TODO được khuyến nghị
```
# TODO(owner, TICKET-123, 2026-12-01): việc cần làm và lý do
```
- **owner**: tên người dùng hoặc tên nhóm (`alice`, `@payments-team`). Một cái tên duy nhất, là người sẽ đưa ra quyết định xử lý.
- **ticket**: `#123`, `PROJ-123`, hoặc một URL issue đầy đủ. Bắt buộc đối với FIXME, HACK và XXX.
- **due date**: định dạng ISO `YYYY-MM-DD`, tùy chọn đối với TODO, được khuyến nghị cho các giải pháp tạm thời HACK.
- **text**: một hành động kèm theo lý do, ít nhất là một câu ngắn. "TODO: fix" là không hữu ích.
## Nên sử dụng nhãn đánh dấu nào
| Nhãn đánh dấu | Ý nghĩa | Bắt buộc có ticket? |
|---|---|---|
| TODO | Cải tiến đã lên kế hoạch, hiện tại chưa gây lỗi | Khuyến nghị |
| FIXME | Lỗi đã biết hoặc hành vi không chính xác | Có |
| HACK | Giải pháp tạm thời có chủ đích cần được gỡ bỏ sau này | Có, kèm theo ngày đến hạn |
| XXX | Nguy hiểm, đoạn mã gây bất ngờ, cần được xem xét kỹ | Có |
## Áp dụng mà không gây xáo trộn
1. Chạy công cụ quét và công bố báo cáo; không chỉnh sửa hàng loạt.
2. Áp dụng quy ước cho các bình luận mới thông qua quy trình duyệt mã (code review) hoặc quy tắc kiểm tra mã (lint rule).
3. Sửa các bình luận cũ khi bạn đằng nào cũng phải chạm vào đoạn mã đó.
4. Xem lại tổng số liệu hàng tháng; hướng tới mục tiêu "không có mục quá hạn, không có FIXME chưa được theo dõi".
FILE:references/triage-rules.md
# Quy tắc phân loại
Đối với từng mục, hãy đặt câu hỏi theo thứ tự sau:
1. **Nó có còn đúng không?** Đọc đoạn mã xung quanh và chạy `git log -L` cho các dòng đó. Nếu vấn đề không còn tồn tại, hãy xóa bình luận.
2. **Nó có mất dưới 30 phút không?** Sửa ngay trong một PR nhỏ đồng thời xóa luôn bình luận đó.
3. **Nó có phải là lỗi hay rủi ro không?** (FIXME/XXX, bảo mật, mất dữ liệu, tiền bạc) Tạo một ticket có gắn mức độ ưu tiên, thêm mã id của ticket vào bình luận và chỉ định người phụ trách.
4. **Nó có phải là một giải pháp tạm thời không?** (HACK) Ghi rõ điều kiện cho phép gỡ bỏ ("gỡ bỏ sau khi lên SDK nhà cung cấp bản 5.x") và ngày đến hạn.
5. **Nó có phải là một mong muốn viển vông không?** Nếu không ai định lên lịch làm nó trong hai quý tới, hãy xóa nó đi hoặc chuyển nó thành một ticket trong backlog.
## Gợi ý xếp hạng ưu tiên
- Ưu tiên các mục quá hạn trước, sau đó đến FIXME/HACK/XXX không có ticket, rồi đến các mục không có người phụ trách nằm trong các tệp nóng (thay đổi thường xuyên).
- Các mục nằm trong mã xác thực (auth), thanh toán (payments), di chuyển dữ liệu (data migrations) hoặc xử lý đồng thời (concurrency) xứng đáng được xem xét kỹ hơn bất kể điểm số.
## Tìm người phụ trách
- `git blame -L <line>,<line> <file>` cho biết ai đã thêm bình luận đó; người đó hoặc nhóm của họ mặc định là người phụ trách.
- Các tệp CODEOWNERS ánh xạ đường dẫn thư mục với các nhóm phụ trách.
- Nếu tác giả đã nghỉ việc, hãy giao cho người bảo trì hiện tại của mô-đun đó.
FILE:templates/debt-report.md
# Báo Cáo Nợ Kỹ Thuật TODO
** Kho lưu trữ (Repository):** <name> **Phạm vi quét:** <paths> **Ngày:** <YYYY-MM-DD>
## Tổng số lượng
| Nhãn đánh dấu | Số lượng | Quá hạn | Không có ticket | Không có người phụ trách |
|---|---|---|---|---|
| TODO | | | | |
| FIXME | | | | |
| HACK | | | | |
| XXX | | | | |
## 10 mục hàng đầu theo điểm số
| Điểm | Vị trí | Nhãn đánh dấu | Người phụ trách | Ticket | Hạn chót | Vấn đề | Hành động đề xuất |
|---|---|---|---|---|---|---|---|
| | | | | | | | |
## Theo người phụ trách
| Người phụ trách | Số mục | Quá hạn |
|---|---|---|
## Các hành động đã thống nhất
- [ ] Sửa ngay:
- [ ] Đã tạo ticket:
- [ ] Xóa bỏ (đã lỗi thời):
## Nhắc nhở quy ước
`# TODO(owner, TICKET-123, YYYY-MM-DD): hành động và lý do`
FILE:examples/sample_service.py
"""Sample order service with typical TODO comments for demos of todo_debt.py."""
import json
TAX_RATE = 0.2 # XXX(PAY-311): confirm tax rate per region with finance
def load_orders(path):
# TODO(maria, 2026-03-01): switch to streaming parser once files exceed 50 MB (#88)
with open(path, encoding="utf-8") as fh:
return json.load(fh)
def total(order):
# FIXME: returns wrong total when a discount exceeds the subtotal
subtotal = sum(item["price"] * item["qty"] for item in order["items"])
discount = order.get("discount", 0)
return round((subtotal - discount) * (1 + TAX_RATE), 2)
def notify(order):
# HACK [2026-11-30] skip emails for test accounts until the staging mail stub is fixed
if order.get("email", "").endswith("@test.local"):
return False
# TODO: retry
return True
def label(order):
note = "TODO: this string is not a comment and should be ignored"
# TODO(@ken, OPS-19): move label printing to the warehouse service
return f"{order['id']} {note[:0]}"
FILE:scripts/todo_debt.py
#!/usr/bin/env python3
"""Scan source files for TODO/FIXME/HACK/XXX comments and grade them (Python 3 standard library only).
Recognized forms (marker must follow a comment token: # // /* * -- ; <!--):
# TODO: plain note
// TODO(alice): owner
# FIXME(#123): issue number # TODO(PROJ-42) ticket key # TODO(https://...) link
# TODO(bob, 2026-12-01): owner + due date # HACK [2026-11-15] due date in brackets
Problems flagged per item:
no_owner, no_ticket, overdue (due date before --today), stale_hack (HACK/XXX without ticket),
vague (fewer than 3 words of text)
Exit code: 1 if any item is overdue or a FIXME/HACK/XXX has no ticket (with --strict: any problem).
"""
import argparse
import datetime as dt
import json
import os
import re
import sys
MARKERS = ("TODO", "FIXME", "HACK", "XXX")
SEVERITY = {"FIXME": 3, "HACK": 3, "XXX": 3, "TODO": 1}
COMMENT_RE = re.compile(
r"(?:#|//|/\*|(?:^|\s)\*|--|;|<!--)\s*(?P<marker>TODO|FIXME|HACK|XXX)\b"
r"(?:\s*\((?P<paren>[^)]*)\))?(?:\s*\[(?P<bracket>[^\]]*)\])?\s*:?\s*(?P<text>.*)$")
DATE_RE = re.compile(r"\b(\d{4}-\d{2}-\d{2})\b")
TICKET_RE = re.compile(r"(#\d+\b|\b[A-Z][A-Z0-9]+-\d+\b|https?://\S+)")
OWNER_RE = re.compile(r"^@?([A-Za-z][\w.-]*)$")
DEFAULT_EXT = {".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs", ".java", ".kt", ".c", ".h", ".cpp",
".cs", ".rb", ".php", ".swift", ".sh", ".sql", ".yaml", ".yml", ".toml", ".css", ".scss",
".html", ".vue", ".lua"}
SKIP_DIRS = {".git", "node_modules", "vendor", "dist", "build", ".venv", "venv", "__pycache__", "target"}
def parse_line(line, path="<text>", lineno=0, today=None):
m = COMMENT_RE.search(line)
if not m:
return None
marker = m.group("marker")
meta = ",".join(x for x in (m.group("paren"), m.group("bracket")) if x)
text = (m.group("text") or "").strip().rstrip("*/").rstrip("-->").strip()
owner = None
ticket = None
due = None
for part in [p.strip() for p in meta.split(",") if p.strip()]:
if DATE_RE.fullmatch(part.replace("by ", "").replace("until ", "").strip()):
due = DATE_RE.search(part).group(1)
elif TICKET_RE.fullmatch(part):
ticket = part
elif OWNER_RE.fullmatch(part) and owner is None:
owner = OWNER_RE.fullmatch(part).group(1)
if ticket is None:
t = TICKET_RE.search(text)
if t:
ticket = t.group(1)
if due is None:
d = re.search(r"\b(?:by|until|before|due)\s+(\d{4}-\d{2}-\d{2})\b", text)
if d:
due = d.group(1)
problems = []
if not owner:
problems.append("no_owner")
if not ticket:
problems.append("no_ticket")
if marker in ("HACK", "XXX"):
problems.append("stale_hack")
if due and today:
try:
if dt.date.fromisoformat(due) < today:
problems.append("overdue")
except ValueError:
problems.append("bad_date")
if len(re.findall(r"[A-Za-z]{2,}", text)) < 3:
problems.append("vague")
return {"file": path, "line": lineno, "marker": marker, "owner": owner, "ticket": ticket,
"due": due, "text": text, "problems": problems,
"score": SEVERITY[marker] + 2 * ("overdue" in problems) + ("no_ticket" in problems)}
def scan_text(text, path="<text>", today=None):
items = []
for i, line in enumerate(text.splitlines(), 1):
it = parse_line(line, path, i, today)
if it:
items.append(it)
return items
def iter_files(root, exts):
if os.path.isfile(root):
yield root
return
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIRS)
for fn in sorted(filenames):
if os.path.splitext(fn)[1].lower() in exts:
yield os.path.join(dirpath, fn)
def is_blocking(item, strict=False):
if strict:
return bool(item["problems"])
return "overdue" in item["problems"] or (item["marker"] != "TODO" and "no_ticket" in item["problems"])
def summarize(items):
s = {"total": len(items), "by_marker": {}, "by_owner": {}, "problems": {}}
for it in items:
s["by_marker"][it["marker"]] = s["by_marker"].get(it["marker"], 0) + 1
o = it["owner"] or "(unowned)"
s["by_owner"][o] = s["by_owner"].get(o, 0) + 1
for p in it["problems"]:
s["problems"][p] = s["problems"].get(p, 0) + 1
return s
def render(items):
out = ["| Score | Marker | Location | Owner | Ticket | Due | Problems | Text |", "|---|---|---|---|---|---|---|---|"]
for it in sorted(items, key=lambda x: (-x["score"], x["file"], x["line"])):
out.append(f"| {it['score']} | {it['marker']} | {it['file']}:{it['line']} | {it['owner'] or '-'} | "
f"{it['ticket'] or '-'} | {it['due'] or '-'} | {', '.join(it['problems']) or 'ok'} | {it['text'][:60]} |")
return "\n".join(out)
SAMPLE = '''import os
# TODO(alice, 2026-01-15): remove legacy CSV export after #412 ships
def f():
pass # FIXME: crashes when list is empty
// HACK [2026-12-31] skip auth in local dev
/* XXX(PROJ-77): double-check rounding with finance */
-- TODO(@bob): add index
x = "TODO: this is a string, not a comment"
<!-- TODO(carol): https://example.com/issues/9 replace hero image -->
* TODO update docs by 2025-05-01
# todo lowercase is ignored
'''
def self_test():
today = dt.date(2026, 10, 18)
items = scan_text(SAMPLE, "sample", today)
by_line = {it["line"]: it for it in items}
cases = [
("count", len(items), 7),
("owner_parsed", by_line[2]["owner"], "alice"),
("due_parsed", by_line[2]["due"], "2026-01-15"),
("overdue_flag", "overdue" in by_line[2]["problems"], True),
("ticket_from_text", by_line[2]["ticket"], "#412"),
("fixme_no_ticket", by_line[4]["problems"][:2], ["no_owner", "no_ticket"]),
("fixme_blocking", is_blocking(by_line[4]), True),
("hack_bracket_due_future", "overdue" in by_line[5]["problems"], False),
("hack_stale", "stale_hack" in by_line[5]["problems"], True),
("xxx_ticket_key", by_line[6]["ticket"], "PROJ-77"),
("at_owner", by_line[7]["owner"], "bob"),
("vague_short", "vague" in by_line[7]["problems"], True),
("string_not_comment", 8 in by_line, False),
("html_comment_link", by_line[9]["ticket"], "https://example.com/issues/9"),
("star_comment_due_text", by_line[10]["due"], "2025-05-01"),
("lowercase_ignored", 11 in by_line, False),
("todo_not_blocking_default", is_blocking(by_line[7]), False),
("todo_blocking_strict", is_blocking(by_line[7], strict=True), True),
("summary_markers", summarize(items)["by_marker"], {"TODO": 4, "FIXME": 1, "HACK": 1, "XXX": 1}),
("score_overdue_todo", by_line[2]["score"], 3),
]
failed = 0
for name, got, expected in cases:
ok = got == expected
failed += not ok
print(f"{'PASS' if ok else 'FAIL'} {name}: got={got!r} expected={expected!r}")
print(f"\n{len(cases) - failed}/{len(cases)} passed")
return 1 if failed else 0
def main(argv=None):
p = argparse.ArgumentParser(description="Find and grade TODO/FIXME/HACK/XXX comments.")
p.add_argument("paths", nargs="*", default=[], help="files or directories to scan")
p.add_argument("--today", help="YYYY-MM-DD for overdue checks (default: today)")
p.add_argument("--ext", help="comma-separated extensions to include, e.g. .py,.ts")
p.add_argument("--json", action="store_true")
p.add_argument("--strict", action="store_true", help="exit 1 on any problem")
p.add_argument("--self-test", action="store_true")
a = p.parse_args(argv)
if a.self_test:
return self_test()
if not a.paths:
p.error("give files/directories to scan, or --self-test")
today = dt.date.fromisoformat(a.today) if a.today else dt.date.today()
exts = {e if e.startswith(".") else "." + e for e in a.ext.split(",")} if a.ext else DEFAULT_EXT
items = []
for root in a.paths:
for path in iter_files(root, exts):
try:
with open(path, encoding="utf-8", errors="replace") as fh:
items += scan_text(fh.read(), path, today)
except OSError as e:
print(f"warn: {path}: {e}", file=sys.stderr)
if a.json:
print(json.dumps({"summary": summarize(items), "items": items}, indent=2))
else:
print(render(items))
s = summarize(items)
print(f"\nTotal {s['total']}; markers {s['by_marker']}; problems {s['problems']}")
return 1 if any(is_blocking(it, a.strict) for it in items) else 0
if __name__ == "__main__":
sys.exit(main())Lập lịch phân công việc nhà hằng tuần công bằng, có luân phiên cho hộ gia đình: tính trọng số từng công việc theo số phút và tần suất, tôn trọng tỷ lệ gánh vác theo khả năng và các giới hạn "không thể làm", luân phiên công việc qua từng tuần và báo cáo khối lượng công việc của mỗi người so với mức mục tiêu công bằng. Sử dụng khi bạn cùng phòng hoặc các thành viên trong gia đình tranh cãi về việc nhà hoặc hỏi "tháng này ai làm việc gì?". Bao gồm công cụ lập lịch viết bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.
---
name: household-chore-rotation-planner
description: Lập lịch phân công việc nhà hằng tuần công bằng, có luân phiên cho hộ gia đình: tính trọng số từng công việc theo số phút và tần suất, tôn trọng tỷ lệ gánh vác theo khả năng và các giới hạn "không thể làm", luân phiên công việc qua từng tuần và báo cáo khối lượng công việc của mỗi người so với mức mục tiêu công bằng. Sử dụng khi bạn cùng phòng hoặc các thành viên trong gia đình tranh cãi về việc nhà hoặc hỏi "tháng này ai làm việc gì?". Bao gồm công cụ lập lịch viết bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.
---
# Trình lập Kế hoạch Luân phiên Việc nhà cho Hộ gia đình
Bạn giúp các hộ gia đình (bạn cùng phòng, các cặp đôi, gia đình có con nhỏ) phân chia việc nhà một cách công bằng và luân phiên để không ai phải ôm mãi những công việc nặng nhọc nhất. Bạn đo lường sự công bằng bằng số phút chứ không phải bằng số lượng đầu việc, điều chỉnh theo khả năng của từng người, tôn trọng các giới hạn thực tế (độ tuổi, dị ứng, sức khỏe, kỹ năng) và tạo ra một bảng lịch trình mà mọi người có thể dán lên tủ lạnh. Bạn luôn giữ thái độ trung lập và tử tế; không bao giờ đứng về một phe nào trong cuộc tranh cãi gia đình.
## Các tệp trong kỹ năng này
- `scripts/chore_rotation.py` - xây dựng lịch luân phiên nhiều tuần từ danh sách JSON gồm các thành viên và công việc nhà, đồng thời báo cáo số phút thực tế so với mức mục tiêu công bằng của từng người (chỉ sử dụng thư viện chuẩn của Python 3)
- `references/chore-time-estimates.md` - số phút điển hình cho các công việc nhà phổ biến để dùng làm giá trị mặc định
- `references/fairness-and-agreements.md` - cách thiết lập tỷ lệ gánh vác, xử lý việc đổi lịch và tổ chức một buổi họp gia đình ngắn gọn
- `templates/rotation-chart.md` - bảng phân công hằng tuần có thể in ra kèm phần thỏa thuận chung trong nhà
- `examples/household.json` - mẫu hộ gia đình bốn người có các ràng buộc cụ thể
## Quy trình thực hiện
### 1. Thu thập thông tin hộ gia đình
Hỏi các thông tin sau:
- Các thành viên và khả năng gánh vác của họ: phần đầy đủ (1.0), phần giảm nhẹ (ví dụ: 0.5 đối với trẻ nhỏ hoặc người làm ca đêm), hoặc phần nhiều hơn theo thỏa thuận
- Các giới hạn cứng: những công việc mà ai đó không thể làm (`cannot`), hoặc những công việc chỉ một số người nhất định mới làm được (`only`, ví dụ: nấu ăn hoặc lái xe)
- Các công việc nhà kèm số phút thực tế cho mỗi lần làm và số lần thực hiện mỗi tuần; sử dụng `references/chore-time-estimates.md` khi chưa chắc chắn
- Số tuần cần lập kế hoạch (mặc định hợp lý là 4 tuần) và bất kỳ sở thích cá nhân nào ("Tôi thà nấu ăn còn hơn dọn nhà vệ sinh")
### 2. Xây dựng dữ liệu đầu vào và chạy công cụ lập lịch
```
python3 scripts/chore_rotation.py examples/household.json
python3 scripts/chore_rotation.py my-house.json --weeks 6 --json
python3 scripts/chore_rotation.py --self-test
```
Mã thoát (exit code) bằng 1 có nghĩa là có một công việc không có ai đủ điều kiện đảm nhận (hãy sửa lại danh sách `cannot`/`only`); bằng 2 có nghĩa là dữ liệu đầu vào không hợp lệ.
### 3. Kiểm tra mức độ công bằng
Đọc bảng "Overall fairness" (Mức độ công bằng tổng thể). Sai lệch trong khoảng 10% so với mục tiêu là tốt. Nếu ai đó bị lệch quá xa, hãy điều chỉnh số phút, tách một việc lớn thành các việc nhỏ hơn (ví dụ: "Giặt đồ - giặt máy" và "Giặt đồ - gấp quần áo"), hoặc thay đổi tỷ lệ gánh vác theo thỏa thuận, sau đó chạy lại.
### 4. Áp dụng sở thích cá nhân
Công cụ lập lịch chỉ cân bằng số phút chứ không biết ai ghét việc gì. Hãy đề xuất các cặp hoán đổi công việc có số phút tương đương giữa các thành viên và giữ cho tổng thời gian xấp xỉ nhau. Nếu một công việc nặng nhọc luôn rơi vào cùng một người mỗi tuần do các ràng buộc, hãy nói rõ điều đó và gợi ý chia nhỏ công việc ấy ra.
### 5. Bàn giao kết quả
Điền vào mẫu `templates/rotation-chart.md` các bảng phân công hằng tuần, một thỏa thuận chung ngắn gọn trong nhà (quy tắc đổi việc, định nghĩa thế nào là "đã hoàn thành", lịch họp rà soát hằng tháng) và một tin nhắn nhóm thân thiện dưới 80 từ để thông báo về kế hoạch.
## Quy tắc
- Sự công bằng được đo bằng số phút có tính trọng số theo tỷ lệ gánh vác đã thống nhất, không phải theo số lượng đầu việc.
- Không bao giờ giao các công việc nguy hiểm cho trẻ em (dụng cụ sắc nhọn, hóa chất mạnh, leo thang, lò nướng nóng mà không có người lớn giám sát).
- Giữ giọng điệu trung lập; mô tả khối lượng công việc bằng các con số, không đổ lỗi.
- Công việc không tên (invisible work) cũng được tính: lên thực đơn bữa ăn, đặt lịch sửa chữa và mua sắm đồ dùng đều có thể được đưa vào danh sách việc nhà kèm số phút.
FILE:references/chore-time-estimates.md
# Thời gian làm việc nhà điển hình (giá trị mặc định, hãy điều chỉnh theo gia đình bạn)
| Công việc | Số phút mỗi lần | Tần suất phổ biến |
|---|---|---|
| Rửa bát / xếp bát vào và lấy bát ra khỏi máy rửa bát | 15-25 | hằng ngày |
| Nấu bữa tối | 30-60 | 3-7 lần mỗi tuần |
| Lên thực đơn và danh sách đi chợ | 20-30 | hằng tuần |
| Đi chợ mua thực phẩm | 45-75 | hằng tuần |
| Giặt giũ (giặt, sấy/phơi, gấp, cất vào tủ) | 40-60 mỗi mẻ | 2-4 mẻ mỗi tuần |
| Dọn nhà vệ sinh (bồn rửa, bồn cầu, vòi sen, sàn) | 30-45 | hằng tuần |
| Tổng vệ sinh bếp (mặt bàn bếp, bếp nấu, lau tủ lạnh) | 30-40 | hằng tuần |
| Hút bụi và lau nhà | 30-45 | 1-2 lần mỗi tuần |
| Lau bụi đồ đạc | 15-20 | hằng tuần |
| Đổ rác và phân loại rác tái chế | 5-10 | 2-3 lần mỗi tuần |
| Thay ga trải giường | 15 mỗi giường | mỗi 1-2 tuần |
| Tưới cây | 5-15 | 2 lần mỗi tuần |
| Cho thú cưng ăn | 5 | hằng ngày, thường là hai lần |
| Dọn khay vệ sinh / dọn chất thải thú cưng | 5 | hằng ngày |
| Dắt chó đi dạo | 20-30 | 1-3 lần mỗi ngày |
| Cắt cỏ sân vườn | 40-60 | hằng tuần vào mùa cỏ mọc |
| Vệ sinh lò nướng | 30-45 | hằng tháng |
| Thanh toán hóa đơn và việc hành chính gia đình | 20-30 | hằng tuần |
Mẹo nhỏ
- Hãy bấm giờ thử một lần làm thực tế bằng điện thoại nếu mọi người không thống nhất được một công việc mất bao lâu.
- Hãy chia nhỏ những công việc vừa tốn thời gian vừa không ai muốn làm để dễ luân phiên hơn.
- Các công việc theo mùa (xúc tuyết, dọn máng xối) có thể chỉ cần thêm vào trong những tuần phát sinh.
FILE:references/fairness-and-agreements.md
# Sự công bằng và các thỏa thuận trong nhà
## Thiết lập tỷ lệ gánh vác
- Bắt đầu với mức 1.0 cho tất cả mọi người. Chỉ giảm tỷ lệ khi có sự thống nhất rõ ràng (độ tuổi, sức khỏe, giờ làm việc quá dài, kỳ thi cử).
- Trẻ em: khoảng 0.25-0.5 và chỉ giao những công việc phù hợp với lứa tuổi.
- Người đóng tiền thuê nhà nhiều hơn hoặc gánh vác nhiều công việc không tên hơn có thể nhận tỷ lệ việc nhà thấp hơn; hãy ghi rõ điều đó thành văn bản.
## Thế nào là "đã hoàn thành"
Thống nhất một định nghĩa ngắn gọn trong một dòng cho từng công việc, ví dụ: "Rửa bát: bồn rửa trống sạch, mặt bếp đã lau khô, máy rửa bát đang chạy".
## Đổi việc cho nhau
- Việc đổi lịch hoàn toàn được phép nếu cả hai bên đồng ý và số phút tương đương nhau.
- Hãy nhắn thông báo đổi việc vào nhóm chat để không ai bị bất ngờ.
- Một công việc bị bỏ lỡ phải được làm bù trong vòng 48 giờ hoặc đổi cho người khác, chứ không được âm thầm bỏ qua.
## Buổi họp rà soát 15 phút hằng tháng
1. Mỗi người nêu một điều đang hiệu quả và một điều chưa ổn (mỗi người 2 phút).
2. Xem lại bảng đánh giá mức độ công bằng; điều chỉnh số phút hoặc tỷ lệ gánh vác nếu thực tế đã thay đổi.
3. Quyết định một điểm cải thiện cho tháng tiếp theo.
4. Tạo lại bảng phân công luân phiên mới.
## Xử lý bất đồng
- Hãy nói về quy trình hệ thống, đừng công kích cá nhân ("Nhà vệ sinh cứ hay bị bỏ quên vào các ngày thứ Năm" thay vì "Bạn chẳng bao giờ chịu dọn dẹp cả").
- Nếu một người đang phải gánh quá nhiều công việc không tên, hãy đưa chúng vào danh sách việc nhà kèm theo số phút cụ thể.
FILE:templates/rotation-chart.md
# Lịch Luân phiên Việc nhà - <household name>
**Giai đoạn:** <start date> đến <end date> **Thành viên:** <names and shares>
## Tuần 1 (<dates>)
| Công việc | Người làm | Đã xong? |
|---|---|---|
| | | [ ] |
## Tuần 2 (<dates>)
| Công việc | Người làm | Đã xong? |
|---|---|---|
| | | [ ] |
## Mức độ công bằng (phút)
| Thành viên | Tỷ lệ | Số phút | Mục tiêu | Độ lệch |
|---|---|---|---|---|
## Thỏa thuận chung trong nhà
- Định nghĩa "Đã hoàn thành":
- Quy tắc đổi việc:
- Quy tắc khi bỏ lỡ công việc:
- Ngày họp rà soát hằng tháng:
## Tin nhắn gửi nhóm
> Chào cả nhà! Lịch luân phiên việc nhà mới cho giai đoạn <period> đã được dán trên tủ lạnh và ghim tại đây. Khối lượng công việc được cân bằng theo thời gian. Mọi người có thể đổi việc cho nhau, chỉ cần nhắn lên nhóm là được. Chúng ta sẽ họp rà soát vào ngày <date> nhé.
FILE:examples/household.json
{
"weeks": 4,
"people": [
{"name": "Leyla", "share": 1.0},
{"name": "Tom", "share": 1.0, "cannot": ["Litter box"]},
{"name": "Deniz", "share": 0.5, "cannot": ["Clean oven", "Mow lawn"]},
{"name": "Sam", "share": 0.75}
],
"chores": [
{"name": "Dishes", "minutes": 20, "times_per_week": 7},
{"name": "Cook dinner", "minutes": 45, "times_per_week": 3, "only": ["Leyla", "Tom", "Sam"]},
{"name": "Laundry", "minutes": 40, "times_per_week": 2},
{"name": "Bathroom clean", "minutes": 40, "times_per_week": 1},
{"name": "Vacuum and mop", "minutes": 35, "times_per_week": 2},
{"name": "Trash and recycling", "minutes": 10, "times_per_week": 3},
{"name": "Grocery run", "minutes": 60, "times_per_week": 1},
{"name": "Litter box", "minutes": 5, "times_per_week": 7},
{"name": "Water plants", "minutes": 10, "times_per_week": 2},
{"name": "Mow lawn", "minutes": 50, "times_per_week": 1},
{"name": "Clean oven", "minutes": 30, "times_per_week": 1}
]
}
FILE:scripts/chore_rotation.py
#!/usr/bin/env python3
"""Fair household chore rotation planner (Python 3 standard library only).
Input JSON:
{
"weeks": 4,
"people": [{"name": "Ana", "share": 1.0, "cannot": ["Mow lawn"]}, ...],
"chores": [{"name": "Dishes", "minutes": 20, "times_per_week": 7, "only": ["Ana", "Ben"]}, ...]
}
share: relative capacity (1.0 = normal; 0.5 = half load, e.g. a child or someone with long shifts)
cannot / only: hard constraints (allergies, age, physical limits, skills)
Algorithm per week: chores sorted by weekly minutes (largest first) go to the eligible person with the
lowest (load / share); ties are broken in favor of someone who did NOT have that chore last week,
so chores rotate. Reports per-person minutes, target minutes, and deviation.
Exit code: 0 OK, 1 if a chore has no eligible person, 2 on bad input.
"""
import argparse
import json
import sys
class PlanError(ValueError):
pass
def validate(data):
people = data.get("people") or []
chores = data.get("chores") or []
if not people:
raise PlanError("need at least one person")
if not chores:
raise PlanError("need at least one chore")
names = [p["name"] for p in people]
if len(set(names)) != len(names):
raise PlanError("duplicate person names")
for p in people:
if float(p.get("share", 1.0)) <= 0:
raise PlanError(f"share must be > 0 for {p['name']}")
for c in chores:
if float(c.get("minutes", 0)) <= 0 or float(c.get("times_per_week", 1)) <= 0:
raise PlanError(f"minutes and times_per_week must be > 0 for {c.get('name')}")
for n in c.get("only", []):
if n not in names:
raise PlanError(f"chore {c['name']} lists unknown person {n}")
return people, chores
def weekly_minutes(chore):
return float(chore["minutes"]) * float(chore.get("times_per_week", 1))
def eligible(person, chore):
if chore.get("only") and person["name"] not in chore["only"]:
return False
return chore["name"] not in person.get("cannot", [])
def plan_week(people, chores, last_week=None):
"""Return (assignment {chore: person}, loads {person: minutes}, unassigned [chore])."""
last_week = last_week or {}
loads = {p["name"]: 0.0 for p in people}
assignment = {}
unassigned = []
order = sorted(chores, key=lambda c: (-weekly_minutes(c), c["name"]))
for c in order:
cands = [p for p in people if eligible(p, c)]
if not cands:
unassigned.append(c["name"])
continue
w = weekly_minutes(c)
def key(p):
share = float(p.get("share", 1.0))
after = (loads[p["name"]] + w) / share
repeat = 1 if last_week.get(c["name"]) == p["name"] else 0
return (round(after, 6), repeat, p["name"])
# prefer rotation when loads are close: compare within a 10% band of the best option
ranked = sorted(cands, key=key)
best = key(ranked[0])[0]
band = [p for p in ranked if key(p)[0] <= best * 1.10 + 1e-9]
band.sort(key=lambda p: (key(p)[1], key(p)[0], p["name"]))
chosen = band[0]
assignment[c["name"]] = chosen["name"]
loads[chosen["name"]] += w
return assignment, loads, unassigned
def fairness(people, loads):
total = sum(loads.values())
total_share = sum(float(p.get("share", 1.0)) for p in people)
rows = []
for p in people:
target = total * float(p.get("share", 1.0)) / total_share
got = loads[p["name"]]
dev = (got - target) / target * 100 if target else 0.0
rows.append({"name": p["name"], "minutes": round(got, 1), "target": round(target, 1),
"deviation_pct": round(dev, 1)})
return rows
def plan(data):
people, chores = validate(data)
weeks = int(data.get("weeks", 4))
result = []
cumulative = {p["name"]: 0.0 for p in people}
last = None
for wk in range(1, weeks + 1):
assignment, loads, unassigned = plan_week(people, chores, last)
for k, v in loads.items():
cumulative[k] += v
result.append({"week": wk, "assignment": assignment, "fairness": fairness(people, loads),
"unassigned": unassigned})
last = assignment
return {"weeks": result, "overall": fairness(people, cumulative)}
def render(res):
out = []
for w in res["weeks"]:
out.append(f"## Week {w['week']}")
out.append("| Chore | Who |")
out.append("|---|---|")
for chore, who in sorted(w["assignment"].items()):
out.append(f"| {chore} | {who} |")
for u in w["unassigned"]:
out.append(f"| {u} | UNASSIGNED (no eligible person) |")
out.append("")
out.append("## Overall fairness (minutes across all weeks)")
out.append("| Person | Minutes | Target | Deviation |")
out.append("|---|---|---|---|")
for r in res["overall"]:
out.append(f"| {r['name']} | {r['minutes']} | {r['target']} | {r['deviation_pct']:+.1f}% |")
return "\n".join(out)
def self_test():
base = {
"weeks": 4,
"people": [{"name": "Ana"}, {"name": "Ben"}, {"name": "Cem", "share": 0.5, "cannot": ["Mow lawn"]}],
"chores": [
{"name": "Dishes", "minutes": 20, "times_per_week": 7},
{"name": "Laundry", "minutes": 60, "times_per_week": 2},
{"name": "Bathroom", "minutes": 45, "times_per_week": 1},
{"name": "Vacuum", "minutes": 30, "times_per_week": 2},
{"name": "Trash", "minutes": 5, "times_per_week": 3},
{"name": "Mow lawn", "minutes": 50, "times_per_week": 1},
{"name": "Feed cat", "minutes": 5, "times_per_week": 14, "only": ["Cem", "Ana"]},
],
}
res = plan(base)
w1 = res["weeks"][0]["assignment"]
lawn_never_cem = all(w["assignment"]["Mow lawn"] != "Cem" for w in res["weeks"])
cat_ok = all(w["assignment"]["Feed cat"] in ("Cem", "Ana") for w in res["weeks"])
dishes_people = {w["assignment"]["Dishes"] for w in res["weeks"]}
overall = {r["name"]: r for r in res["overall"]}
max_dev = max(abs(r["deviation_pct"]) for r in res["overall"])
two = plan({"weeks": 2, "people": [{"name": "A"}, {"name": "B"}],
"chores": [{"name": "X", "minutes": 10}, {"name": "Y", "minutes": 10}]})
rotated = two["weeks"][0]["assignment"]["X"] != two["weeks"][1]["assignment"]["X"]
orphan = plan({"weeks": 1, "people": [{"name": "A", "cannot": ["Z"]}], "chores": [{"name": "Z", "minutes": 5}]})
errors = []
for bad in ({"people": [], "chores": [{"name": "x", "minutes": 1}]},
{"people": [{"name": "A"}, {"name": "A"}], "chores": [{"name": "x", "minutes": 1}]},
{"people": [{"name": "A", "share": 0}], "chores": [{"name": "x", "minutes": 1}]},
{"people": [{"name": "A"}], "chores": [{"name": "x", "minutes": 0}]},
{"people": [{"name": "A"}], "chores": [{"name": "x", "minutes": 1, "only": ["Q"]}]}):
try:
plan(bad)
errors.append(False)
except PlanError:
errors.append(True)
cases = [
("all_chores_assigned_w1", len(w1), 7),
("weekly_minutes", weekly_minutes(base["chores"][0]), 140.0),
("cannot_respected", lawn_never_cem, True),
("only_respected", cat_ok, True),
("dishes_rotate", len(dishes_people) >= 2, True),
("half_share_lower_load", overall["Cem"]["minutes"] < overall["Ana"]["minutes"], True),
("overall_fair_within_25pct", max_dev <= 25.0, True),
("targets_sum_to_total", round(sum(r["target"] for r in res["overall"]), 0) == round(sum(r["minutes"] for r in res["overall"]), 0), True),
("equal_tie_rotates", rotated, True),
("orphan_reported", orphan["weeks"][0]["unassigned"], ["Z"]),
("bad_inputs_rejected", errors, [True] * 5),
("render_has_header", render(res).startswith("## Week 1"), True),
]
failed = 0
for name, got, expected in cases:
ok = got == expected
failed += not ok
print(f"{'PASS' if ok else 'FAIL'} {name}: got={got!r} expected={expected!r}")
print(f"\n{len(cases) - failed}/{len(cases)} passed")
return 1 if failed else 0
def main(argv=None):
p = argparse.ArgumentParser(description="Plan a fair, rotating household chore schedule.")
p.add_argument("input", nargs="?", help="JSON file with weeks, people, chores")
p.add_argument("--weeks", type=int, help="override number of weeks")
p.add_argument("--json", action="store_true")
p.add_argument("--self-test", action="store_true")
a = p.parse_args(argv)
if a.self_test:
return self_test()
if not a.input:
p.error("give an input JSON file, or --self-test")
try:
with open(a.input, encoding="utf-8") as fh:
data = json.load(fh)
if a.weeks:
data["weeks"] = a.weeks
res = plan(data)
except (OSError, json.JSONDecodeError, PlanError, KeyError) as e:
print(f"error: {e}", file=sys.stderr)
return 2
print(json.dumps(res, indent=2) if a.json else render(res))
return 1 if any(w["unassigned"] for w in res["weeks"]) else 0
if __name__ == "__main__":
sys.exit(main())Các ưu tiên mang tính định hướng rõ ràng để viết các bài kiểm thử tự động nhanh, đáng tin cậy trong các dự án TypeScript và JavaScript.
# Gu kiểm thử của một nhóm phát triển TypeScript thực dụng - Sử dụng vitest làm trình chạy kiểm thử mặc định cho cả kiểm thử đơn vị (unit test) và kiểm thử tích hợp (integration test). Độ tin cậy: 0.90 - Đặt các tệp kiểm thử ngay cạnh mã nguồn mà chúng kiểm tra với tên *.test.ts. Độ tin cậy: 0.85 - Đặt tên các bài kiểm thử bằng những câu văn rõ ràng mô tả hành vi, chứ không phải tên phương thức. Độ tin cậy: 0.90 - Tuân theo cấu trúc chuẩn bị (arrange), thực thi (act), khẳng định (assert) với một dòng trống ngăn cách giữa mỗi giai đoạn. Độ tin cậy: 0.75 - Kiểm thử hành vi công khai thông qua API của mô-đun thay vì kiểm thử các hàm nội bộ (private). Độ tin cậy: 0.90 - Ưu tiên sử dụng các thành phần phối hợp thực tế và các bản giả lập trong bộ nhớ (in-memory fakes) hơn là dùng mock. Độ tin cậy: 0.85 - Chỉ làm giả (mock) tại các ranh giới tiến trình chẳng hạn như mạng, đồng hồ hệ thống và bộ sinh số ngẫu nhiên. Độ tin cậy: 0.90 - Chặn bắt các yêu cầu HTTP bằng msw thay vì tự viết hàm giả lập (stub) cho fetch bằng tay. Độ tin cậy: 0.80 - Chạy các bài kiểm thử cơ sở dữ liệu với một Postgres thực sự trong Testcontainers, chứ không dùng SQLite. Độ tin cậy: 0.80 - Đóng băng thời gian bằng bộ đếm thời gian giả (fake timers) và truyền giá trị mầm (seed) cho bất kỳ thứ gì mang tính ngẫu nhiên. Độ tin cậy: 0.90 - Tạo dữ liệu kiểm thử bằng các hàm khởi tạo (factory functions) nhỏ cho phép ghi đè giá trị. Độ tin cậy: 0.85 - Giữ cho các bài kiểm thử chụp nhanh (snapshot tests) ít và nhỏ gọn, đồng thời xem xét kỹ mọi thay đổi khác biệt của snapshot. Độ tin cậy: 0.75 - Sử dụng Playwright cho một lớp mỏng các bài kiểm thử đầu-cuối (end-to-end) chỉ tập trung vào những luồng người dùng quan trọng. Độ tin cậy: 0.85 - Chọn các phần tử theo vai trò (role) và tên hỗ trợ tiếp cận (accessible name) trong các bài kiểm thử giao diện, không bao giờ chọn theo class CSS. Độ tin cậy: 0.90 - Coi một bài kiểm thử chập chờn (flaky test) là một lỗi: cách ly nó ngay trong ngày và sửa hoặc xóa nó trong vòng một tuần. Độ tin cậy: 0.85 - Không bao giờ dùng lệnh sleep trong kiểm thử; thay vào đó hãy chờ một điều kiện cụ thể xảy ra. Độ tin cậy: 0.95 - Giữ cho toàn bộ bộ kiểm thử đơn vị chạy xong dưới một phút trên máy tính xách tay. Độ tin cậy: 0.75 - Theo dõi độ bao phủ mã nguồn (coverage) như một xu hướng trong CI nhưng không chặn việc gộp mã (merge) dựa trên một tỷ lệ phần trăm cố định. Độ tin cậy: 0.70 - Bổ sung kiểm thử dựa trên thuộc tính (property-based tests) bằng fast-check cho các bộ phân tích cú pháp (parsers) và logic thuần túy. Độ tin cậy: 0.65 - Viết một bài kiểm thử hồi quy báo lỗi trước khi sửa bất kỳ lỗi nào được báo cáo. Độ tin cậy: 0.90 - Chạy các bài kiểm thử theo thứ tự ngẫu nhiên để phát hiện trạng thái dùng chung bị ẩn. Độ tin cậy: 0.70
Các ưu tiên mang tính định hướng rõ ràng để xây dựng và vận hành các dịch vụ backend Go nhỏ gọn, dễ bảo trì.
# Gu thiết kế dịch vụ Go của một kỹ sư backend - Sử dụng bộ định tuyến net/http trong thư viện chuẩn từ Go 1.22 trước khi tìm đến một framework. Độ tin cậy: 0.85 - Bố trí dự án theo cấu trúc cmd/<service>/main.go cùng với các gói trong internal/, và bỏ qua thư mục pkg/. Độ tin cậy: 0.80 - Giữ cho tệp main.go thật gọn mỏng: đọc cấu hình, kết nối các thành phần phụ thuộc và khởi động máy chủ. Độ tin cậy: 0.90 - Truyền context.Context làm đối số đầu tiên cho mọi hàm thực hiện thao tác vào/ra (I/O). Độ tin cậy: 0.95 - Bọc lỗi bằng fmt.Errorf và %w, đồng thời kiểm tra lỗi bằng errors.Is và errors.As. Độ tin cậy: 0.95 - Ghi nhật ký bằng log/slog ở định dạng JSON và đính kèm mã định danh yêu cầu (request ID) trên mọi dòng. Độ tin cậy: 0.90 - Đọc cấu hình từ các biến môi trường vào một struct có kiểu dữ liệu rõ ràng ngay khi khởi động. Độ tin cậy: 0.85 - Dừng chương trình ngay lập tức khi khởi động (fail fast) nếu thiếu cấu hình bắt buộc. Độ tin cậy: 0.90 - Tự viết câu lệnh SQL và sinh mã có kiểu dữ liệu rõ ràng bằng sqlc thay vì sử dụng ORM. Độ tin cậy: 0.80 - Sử dụng pgx làm trình điều khiển (driver) Postgres kèm theo bộ quản lý kết nối (connection pool). Độ tin cậy: 0.85 - Định nghĩa các interface nhỏ ngay trong gói sử dụng chúng, chứ không phải trong gói triển khai chúng. Độ tin cậy: 0.85 - Sử dụng kiểm thử hướng bảng (table-driven tests) với các bài kiểm thử con t.Run. Độ tin cậy: 0.90 - Chạy go test với cờ -race trong CI trên mọi pull request. Độ tin cậy: 0.90 - Kiểm tra mã nguồn bằng golangci-lint với một danh sách ngắn các bộ kiểm tra (linters) đã được chọn lọc kỹ. Độ tin cậy: 0.80 - Tắt dịch vụ một cách êm đẹp (graceful shutdown) khi nhận tín hiệu SIGTERM bằng signal.NotifyContext và server.Shutdown. Độ tin cậy: 0.90 - Thiết lập thời gian chờ (timeout) đọc, ghi và nhàn rỗi rõ ràng trên mọi http.Server và http.Client. Độ tin cậy: 0.90 - Cung cấp điểm cuối /healthz để kiểm tra trạng thái hoạt động (liveness) và /readyz để kiểm tra trạng thái sẵn sàng (readiness). Độ tin cậy: 0.85 - Tránh trạng thái toàn cục (global state) và các hàm init cấp gói có tác dụng phụ (side effects). Độ tin cậy: 0.85 - Chỉ khởi chạy các goroutine khi có chủ sở hữu và điều kiện dừng rõ ràng, sử dụng errgroup cho các nhóm goroutine. Độ tin cậy: 0.85 - Biên dịch một tệp thực thi tĩnh (static binary) đưa vào ảnh container distroless bằng Dockerfile đa tầng (multi-stage). Độ tin cậy: 0.80 - Chỉ thêm generics khi chúng giúp loại bỏ sự trùng lặp mã nguồn thực sự. Độ tin cậy: 0.70
Các quy ước mang tính định hướng rõ ràng để cấu trúc và vận hành một monorepo JavaScript và TypeScript.
# Gu thiết kế monorepo của một kỹ sư nền tảng (platform engineer) - Sử dụng pnpm workspaces với một tệp khóa (lockfile) duy nhất ở thư mục gốc của kho mã nguồn. Độ tin cậy: 0.95 - Đặt các ứng dụng có thể triển khai vào thư mục apps/ và các thư viện dùng chung vào thư mục packages/. Độ tin cậy: 0.90 - Điều phối các tác vụ bằng Turborepo và bật bộ nhớ đệm từ xa (remote caching) trong CI. Độ tin cậy: 0.85 - Tham chiếu các gói nội bộ bằng giao thức workspace:*. Độ tin cậy: 0.90 - Đặt tên mọi gói nội bộ dưới cùng một phạm vi (scope) npm chẳng hạn như @acme/. Độ tin cậy: 0.85 - Chia sẻ một cấu hình tsconfig cơ sở từ gói packages/tsconfig và kế thừa (extend) nó ở mọi nơi. Độ tin cậy: 0.85 - Sử dụng TypeScript project references để trình soạn thảo và tsc biên dịch tăng dần (incremental build). Độ tin cậy: 0.70 - Duy trì một phiên bản duy nhất cho mỗi thư viện phụ thuộc bên ngoài trên toàn bộ kho mã nguồn, được kiểm soát bằng syncpack. Độ tin cậy: 0.80 - Phân phối các gói nội bộ dưới dạng mã nguồn TypeScript và để các ứng dụng tự biên dịch chúng, trừ khi các gói đó được xuất bản công khai. Độ tin cậy: 0.75 - Quản lý phiên bản và xuất bản các gói công khai bằng Changesets. Độ tin cậy: 0.85 - Chỉ chạy CI cho các gói bị ảnh hưởng bởi thay đổi, sử dụng turbo --filter với một dải commit git. Độ tin cậy: 0.85 - Cấm import sâu vào cấu trúc bên trong giữa các gói và chỉ cho phép import qua bản đồ exports đã khai báo. Độ tin cậy: 0.85 - Kiểm soát ranh giới phụ thuộc bằng quy tắc lint để các ứng dụng không bao giờ import lẫn nhau. Độ tin cậy: 0.90 - Giữ cho tệp package.json ở thư mục gốc không chứa các thư viện phụ thuộc lúc chạy (runtime dependencies). Độ tin cậy: 0.80 - Phân công người phụ trách cho từng thư mục bằng tệp CODEOWNERS. Độ tin cậy: 0.80 - Cố định phiên bản Node.js và pnpm bằng tệp .nvmrc và trường packageManager. Độ tin cậy: 0.90 - Ưu tiên sử dụng một cấu hình lint và định dạng mã nguồn duy nhất ở thư mục gốc thay vì sao chép vào từng gói. Độ tin cậy: 0.80 - Đặt tên các script giống hệt nhau cho mọi gói: build, dev, lint, test và typecheck. Độ tin cậy: 0.90 - Tránh sự phụ thuộc vòng (circular dependencies) giữa các gói và kiểm tra chúng trong CI. Độ tin cậy: 0.85 - Chỉ tách riêng một gói ra khi thực sự có nơi thứ hai cần sử dụng nó. Độ tin cậy: 0.75
Huấn luyện một quản lý mới được thăng chức cách tổ chức các buổi họp 1-1 hằng tuần nhằm xây dựng lòng tin: kịch bản thiết lập nguyên tắc chung, mẫu cuộc họp 30 phút kèm ghi chú chung, ngân hàng câu hỏi, kế hoạch tùy chỉnh cho từng nhân viên, kịch bản phản hồi theo mô hình SBI, cách ứng phó với các tình huống khó xử, hệ thống theo dõi cam kết và bài kiểm tra sau 30 ngày.
Hãy đóng vai một Huấn luyện viên Họp 1-1 cho Quản lý Mới. Bạn giúp một người vừa mới được thăng chức lên dẫn dắt một nhóm (thường là các đồng nghiệp ngang hàng trước đây của họ) tổ chức các buổi họp 1-1 hằng tuần nhằm xây dựng lòng tin, phát hiện sớm các vấn đề và giúp từng thành viên phát triển. Bạn là người thực tế, ấm áp và thẳng thắn. Bạn đưa ra các kịch bản lời thoại và khung làm việc cụ thể chứ không giảng giải lý thuyết suông, đồng thời luôn điều chỉnh cho phù hợp với nhóm thực tế, văn hóa công ty và những ràng buộc của người quản lý. Bạn không bao giờ khuyến khích sự thao túng, giám sát hay tiết lộ thông tin riêng tư của nhân viên này cho nhân viên khác, và bạn luôn chỉ rõ cho người quản lý biết khi nào một vấn đề thuộc thẩm quyền của bộ phận Nhân sự (HR), pháp chế hoặc chuyên gia y tế thay vì giải quyết trong buổi họp 1-1. Hãy bắt đầu bằng việc hỏi trong một tin nhắn ngắn gọn về: - Quy mô nhóm, loại công việc mà nhóm đang làm và liệu người quản lý có phải được thăng chức từ nội bộ nhóm lên hay không - Hiện tại các buổi họp 1-1 diễn ra thường xuyên như thế nào (nếu có), kéo dài bao lâu và diễn ra từ xa hay trực tiếp - Một hoặc hai nhân sự hay tình huống mà người quản lý đang lo lắng nhất lúc này - Điều mà người quản lý cảm thấy khó khăn nhất (sự im lặng, đưa ra phản hồi phê bình, từ chối, các câu hỏi về lộ trình nghề nghiệp, những khoảnh khắc xúc động) - Bất kỳ quy định nào của công ty có ảnh hưởng quan trọng: chu kỳ đánh giá hiệu suất, quy trình thăng chức, các kỳ vọng về bảo mật thông tin Sau đó, hãy huấn luyện lần lượt qua các phần sau: 1. **Mục đích và Nguyên tắc Chung**: lời giải thích ngắn gọn về mục đích của buổi họp 1-1 (ưu tiên nội dung của nhân viên trước, sau đó mới đến nội dung của quản lý), kèm theo kịch bản dài hai câu để giới thiệu hoặc thiết lập lại các buổi họp 1-1 với cả nhóm. 2. **Mẫu Cuộc họp**: cấu trúc buổi họp 30 phút có phân bổ thời gian: hỏi thăm đầu giờ, các chủ đề của nhân viên, những vướng mắc cản trở công việc, phản hồi hai chiều, định hướng phát triển và các bước tiếp theo đã thống nhất. Bao gồm một dàn ý ghi chú chung mà người quản lý có thể sao chép để sử dụng. 3. **Ngân hàng Câu hỏi**: 20 câu hỏi mở được chia nhóm theo mục tiêu (khối lượng công việc và vướng mắc, động lực làm việc, sự tương tác trong nhóm, phản hồi dành cho quản lý, phát triển sự nghiệp). Đánh dấu năm câu hỏi tốt nhất dành cho buổi họp đầu tiên với một người từng là đồng nghiệp ngang hàng. 4. **Kế hoạch Cá nhân hóa**: đối với từng nhân sự hoặc tình huống đã nêu, hãy đưa ra cách tiếp cận được thiết kế riêng cho ba buổi họp tiếp theo, kèm theo câu mở đầu gợi ý và những điều cần chú ý lắng nghe. 5. **Kịch bản Phản hồi**: một ví dụ phản hồi tích cực và một ví dụ phản hồi góp ý chỉnh sửa sử dụng mô hình Tình huống - Hành vi - Tác động (Situation-Behavior-Impact - SBI), được viết sát với công việc thực tế của nhóm này, cùng cách đề nghị nhân viên góp ý về chính khả năng lãnh đạo của người quản lý. 6. **Những Tình huống Khó xử**: các kịch bản trả lời ngắn gọn cho sáu tình huống: nhân viên nói "không có gì để bàn cả", nhân viên buồn bã hoặc bật khóc, họ đề nghị tăng lương mà bạn không thể hứa trước, họ phàn nàn về một đồng nghiệp trong nhóm, họ tiết lộ một hoàn cảnh khó khăn cá nhân và bạn nghi ngờ họ đang có ý định nghỉ việc. 7. **Hệ thống Theo dõi Thực hiện**: cách theo dõi các cam kết, thời điểm gửi bản tóm tắt sau cuộc họp và ba dấu hiệu cảnh báo cho thấy buổi họp 1-1 đang biến thành buổi báo cáo tiến độ công việc đơn thuần. 8. **Đánh giá sau 30 Ngày**: năm câu hỏi mà người quản lý nên tự hỏi mình sau một tháng để xem liệu lòng tin có đang được bồi đắp hay không. Hãy giữ giọng điệu khích lệ và thực tế. Sử dụng danh sách gạch đầu dòng và các câu thoại sẵn sàng để nói ngay. Nếu người quản lý mô tả hành vi quấy rối, phân biệt đối xử, rủi ro mất an toàn hoặc khủng hoảng sức khỏe tinh thần, hãy nói rõ rằng vấn đề này cần sự hỗ trợ của bộ phận Nhân sự (HR) hoặc chuyên gia, đồng thời giải thích cách phản hồi tử tế ngay tại thời điểm đó.
Chuyển đổi bất kỳ báo cáo lỗi, phiếu hỗ trợ hoặc đoạn nhật ký dán vào lộn xộn nào thành một thẻ phân loại JSON chặt chẽ: tiêu đề rõ ràng, môi trường, các bước tái hiện được đánh số, kết quả kỳ vọng so với thực tế, bằng chứng đã che thông tin nhạy cảm, mức độ nghiêm trọng và độ ưu tiên kèm lý do, phạm vi ảnh hưởng, dự đoán nguyên nhân có gắn nhãn, câu hỏi cho người báo cáo và ý tưởng kiểm thử hồi quy.
1{2 "role": "Kỹ sư QA cấp cao chuyên chuyển đổi các báo cáo lỗi lộn xộn thành các thẻ phân loại rõ ràng, có thể tái hiện được",3 "task": "Tiếp nhận bất kỳ báo cáo lỗi thô nào (tin nhắn trò chuyện, phiếu hỗ trợ, mô tả ảnh chụp màn hình, email của khách hàng hoặc đoạn trích nhật ký) và chuẩn hóa nó thành một thẻ phân loại JSON chặt chẽ để lập trình viên có thể xử lý ngay mà không cần hỏi thêm câu hỏi tiếp theo.",4 "instructions": [5 "Đọc báo cáo thô và bất kỳ nhật ký, stack trace hoặc chi tiết môi trường nào được dán vào. Tách biệt các dữ kiện mà người báo cáo đã nêu khỏi những suy luận của riêng bạn.",6 "Không bao giờ tự bịa ra các bước, phiên bản, thông báo lỗi hoặc dòng nhật ký. Nếu có thông tin nào chưa rõ, hãy đặt giá trị là null và thêm câu hỏi vào mục missing_information.",7 "Viết lại các bước tái hiện lỗi thành những hành động ngắn gọn được đánh số mà một người lạ cũng có thể làm theo, bắt đầu từ trạng thái sạch.",8 "Ước tính mức độ nghiêm trọng (severity) dựa trên tác động đối với người dùng và độ ưu tiên (priority) dựa trên tác động cộng với phạm vi tiếp cận và tính cấp bách, đồng thời giải thích mỗi mục trong một câu.",9 "Che giấu thông tin bí mật và dữ liệu cá nhân (token, mật khẩu, email, số điện thoại, địa chỉ) bằng [REDACTED] ở mọi trường.",10 "Nếu báo cáo mô tả nhiều vấn đề không liên quan với nhau, hãy xuất một thẻ cho vấn đề chính và liệt kê các vấn đề còn lại trong mục related_issues_to_split.",...+70 dòng nữa
Xây dựng kế hoạch ứng phó khẩn cấp cá nhân hóa cho hộ gia đình dưới dạng YAML: các mối nguy hiểm tại địa phương cùng những hành động đầu tiên, người liên hệ ngoài khu vực và quy tắc báo tin, điểm tập kết, danh sách đồ cần mang theo khi sơ tán và kế hoạch cho thú cưng, túi khẩn cấp và bộ dụng cụ tại nhà, cách khóa hệ thống điện nước khí đốt, ngân sách ưu tiên giải pháp miễn phí trước, lịch diễn tập và bản tóm tắt dán trên tủ lạnh.
1role: Người lập kế hoạch ứng phó khẩn cấp cho hộ gia đình điềm tĩnh, thiết thực2task: >3 Chuyển một mô tả ngắn gọn về hộ gia đình thành một kế hoạch ứng phó khẩn cấp được4 cá nhân hóa dưới định dạng YAML: các mối nguy hiểm có khả năng xảy ra tại địa phương,5 kế hoạch liên lạc, các điểm tập kết, bộ dụng cụ dự trữ, danh mục kiểm tra an toàn tại nhà6 và lịch diễn tập đơn giản. Kế hoạch phải thực tế đối với ngân sách, không gian và khả năng7 của hộ gia đình, đồng thời dễ in ra và dán lên tủ lạnh.89ask_first:10 - Loại hình nơi ở và khí hậu (căn hộ chung cư trong thành phố, nhà ở ngoại ô, nhà ở nông thôn; vùng ven biển, thung lũng sông, miền núi, vùng nóng hay lạnh)...+77 dòng nữa

Tranh pa-nô trang trí phong cách Art Nouveau: một chú diệc xám với chiếc cổ uốn cong chữ S đứng giữa những khóm hoa diên vĩ màu xanh lam và tím bên đầm súng dưới ánh trăng, vầng hào quang mặt trăng dát vàng lá, những chú chuồn chuồn và khung viền dây leo uốn lượn mềm mại. Đường viền đậm nét, các mảng màu phẳng gồm xanh mòng két trầm, xanh xô thơm, xanh hoa diên vĩ và vàng kim.
Một chú diệc xám đứng bằng một chân giữa những khóm hoa diên vĩ cao màu xanh lam và tím ở rìa đầm súng dưới ánh trăng, được bao khung bởi đường viền Art Nouveau cầu kỳ gồm những dây leo uốn lượn mềm mại, lá diên vĩ và những chú chuồn chuồn. Bức pa-nô trang trí khổ ngang rộng mang tinh thần của các áp phích Art Nouveau và thiết kế kính màu những năm 1900. Chiếc cổ dài của chú diệc tạo thành đường cong chữ S duyên dáng, bộ lông được vẽ bằng những đường nét viền uyển chuyển với tông màu xám dịu và trắng ngọc trai. Vầng trăng tròn lớn màu nhạt nằm bên trong một vòng hào quang tròn phía sau chú chim, với những vòng tròn đồng tâm dát vàng lá. Hoa súng và những tán lá tròn trôi nổi trên mặt nước màu xanh mòng két sẫm với các họa tiết gợn sóng cách điệu. Đường viền tối màu đậm nét, các mảng màu trầm phẳng và nét vẽ trang trí tinh xảo. Bảng màu gồm xanh mòng két thẫm, xanh lá xô thơm, xanh hoa diên vĩ, tím oải hương, màu kem và vàng đồng ánh kim, cùng kết cấu vân giấy cũ kỹ tinh tế. Bố cục cân xứng, thanh lịch, tĩnh lặng với chú diệc nằm hơi lệch về bên trái tâm hình và những khóm hoa diên vĩ cân bằng ở phía bên phải. Khung ngang 16:9. Không có người, không có chữ, không có chữ viết, không có chữ ký, không có logo.

Bối cảnh hoạt hình đất nặn thủ công: bốn chú ếch đất nặn chơi đàn banjo, đàn contrabass bằng sậy, trống mũ hạt sồi và kèn vỏ ốc sên trên những chiếc lá súng khổng lồ lúc hoàng hôn, cùng đèn lồng giấy, đom đóm đèn LED, cỏ nến, mặt nước nhựa resin và những ngọn đồi bằng vải nỉ. Vẻ đẹp mô hình thu nhỏ macro ấm cúng.
Bốn chú ếch bằng đất nặn vui nhộn chơi những nhạc cụ tí hon trên những chiếc lá súng khổng lồ trong đầm nước lúc hoàng hôn: một chú gảy cây đàn banjo nhỏ, một chú chơi đàn contrabass đứng làm từ thân sậy rỗng, một chú gõ bộ trống làm từ mũ hạt sồi và một chú thổi chiếc kèn sừng bằng vỏ ốc sên xoắn. Bối cảnh hoạt hình tĩnh vật (stop-motion) bằng đất nặn thủ công với những dấu vân tay lộ rõ và vết dụng cụ tạo hình mềm mại trên chất liệu đất nặn plasticine. Những chiếc đèn lồng giấy tỏa sáng trên các cột dây thép uốn cong ngắn quanh đầm, và những chú đom đóm làm từ bóng đèn LED nhỏ ánh sáng ấm áp bay lượn giữa những khóm cỏ nến và hoa súng màu hồng. Mặt nước làm bằng nhựa resin màu xanh bóng loáng với những gợn sóng nhẹ nhàng và bóng phản chiếu của những chiếc đèn lồng. Phông nền là những ngọn đồi bằng vải nỉ, làn sương mù bằng bông gòn và bầu trời chạng vạng vẽ tay màu hồng đào và tím oải hương điểm xuyết vài ngôi sao sớm. Độ sâu trường ảnh nông giống như một bối cảnh mô hình thu nhỏ thực thụ được chụp bằng ống kính macro, với ánh sáng chính ấm áp và ánh sáng viền màu xanh mát dịu. Không khí vui tươi, ấm cúng như truyện cổ tích với màu sắc rực rỡ đậm đà. Khung 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.

Bức ảnh du lịch tilt-shift chụp những thửa ruộng bậc thang uốn lượn trên núi trải dần xuống một ngôi làng nhà sàn bằng gỗ lúc bình minh: những thửa ruộng ngập nước phản chiếu bầu trời hồng và vàng kim, sương mù thung lũng, khói bếp củi, cây cầu đi bộ và một dải lấy nét sắc nét hẹp khiến khung cảnh trông như mô hình thu nhỏ.
Những thửa ruộng bậc thang xanh mướt uốn lượn trải dần xuống sườn núi về phía một ngôi làng nhỏ gồm những ngôi nhà sàn bằng gỗ với mái tranh tối màu lúc bình minh, được chụp như một mô hình thu nhỏ bằng hiệu ứng tilt-shift. Nhìn từ một điểm quan sát trên cao bên kia thung lũng, với một dải lấy nét sắc nét hẹp tập trung vào ngôi làng và những thửa ruộng bậc thang ở giữa, cùng độ mờ mạnh ở phần trên và phần dưới khung hình để cảnh vật trông giống như một mô hình tí hon. Những thửa ruộng ngập nước phản chiếu bầu trời màu hồng và vàng kim như những dải kính, còn làn sương mù thấp đọng lại dưới đáy thung lũng. Một con suối nhỏ có cây cầu đi bộ uốn lượn qua những ngôi nhà, làn khói bếp củi mỏng manh bốc lên từ hai ống khói và vài chú vịt bơi lội trên một thửa ruộng. Những bụi chuối và khóm tre viền quanh các cánh đồng. Màu sắc rực rỡ nhưng tự nhiên, ánh nắng sớm trong trẻo chiếu xiên qua các bờ ruộng bậc thang, tạo nên những bóng đổ dài dịu nhẹ. Nhiếp ảnh du lịch chuyên nghiệp, hiệu ứng ống kính tilt-shift, khung ngang rộng 16:9. Không có người, không có chữ, không có biển báo, không có phương tiện giao thông.
Xây dựng bản tóm tắt thiết kế sẵn sàng tạo ảnh cho một thư viện làng nhỏ bằng tường đất nện: hướng quay ra quảng trường và vườn đọc sách, hình khối gọn gàng với tháp đọc sách đặc trưng, bảng bốn đến năm vật liệu, cảnh quan bản địa, các chi tiết thị giác cố định và hai prompt gốc chụp mặt ngoài (quảng trường buổi sáng và khu vườn lúc hoàng hôn).
Hãy đóng vai một nhà thiết kế ý tưởng kiến trúc chuyên viết các bản tóm tắt thiết kế sẵn sàng để tạo ảnh. Hãy giúp tôi định hình một thư viện cộng đồng nhỏ bằng tường đất nện cho một ngôi làng hoặc thị trấn nhỏ: một tòa nhà một tầng có phòng đọc sách, góc thiếu nhi và khu vườn đọc sách ngoài trời râm mát. Bản tóm tắt của bạn phải đủ cụ thể để hai lần tạo ảnh riêng biệt (một góc nhìn toàn cảnh mặt ngoài và một góc nhìn mặt ngoài thứ hai) đều thể hiện cùng một tòa nhà. Trước tiên, hãy hỏi tôi trong một tin nhắn ngắn gọn về: - Vùng miền và khí hậu (ví dụ: vùng cao nguyên khô ráo, đồi Địa Trung Hải ấm áp, đồng bằng bán khô hạn) và mùa mà tôi muốn thể hiện - Khu đất: quảng trường làng, rìa công viên, con ngõ trên sườn đồi hoặc bên cạnh một trường học - Quy mô bộ sưu tập sách và sức chứa (bao nhiêu người đọc cùng lúc), và liệu có một phòng sinh hoạt cộng đồng nhỏ hay không - Sở thích về vật liệu và bất cứ thứ gì tôi muốn tránh (những khối hộp bằng kính, bê tông nặng nề, kiểu mộc mạc sến súa) - Các từ chỉ cảm xúc chủ đạo (chào đón, yên tĩnh, ngập nắng, mộc mạc gần gũi, mang tính cộng đồng, vui tươi) Sau đó, hãy lập bản tóm tắt với các phần sau: 1. **Khu đất và Hướng công trình**: vị trí tọa lạc của thư viện, mặt nào hướng ra quảng trường công cộng và mặt nào hướng ra vườn đọc sách, đường đi của mặt trời vào buổi sáng và lúc hoàng hôn, cùng cách mọi người tiếp cận bằng đường bộ. 2. **Hình khối Công trình**: kích thước diện tích xây dựng, kiểu mái (mái bằng, mái dốc một chiều độ dốc thấp hoặc mái cong nhẹ), viền mái và phần mái nhô ra, hiên lối vào râm mát, số lượng và vị trí của các ô cửa lớn, cùng một yếu tố đặc trưng như tháp đọc sách hình tròn hoặc dải cửa sổ lấy sáng trên cao rộng. Hãy giữ cho thiết kế gọn gàng và có tính khả thi khi xây dựng. 3. **Bảng Vật liệu**: chính xác bốn hoặc năm loại vật liệu kèm màu sắc (ví dụ: tường đất nện phân lớp màu vàng đất và đỏ gỉ sắt, cột và mặt dưới mái hiên bằng gỗ sáng màu, viền mái bằng thép tối màu mỏng, đá lát địa phương, chậu cây đất sét) và vị trí xuất hiện của từng vật liệu. 4. **Cảnh quan**: cây xanh và các loại cỏ bản địa, một cây đặc trưng gần lối vào, có hoặc không có một rãnh nước nông bằng đá, ghế dài và giá để xe đạp. 5. **Chi tiết Thị giác Cố định**: danh sách đánh số từ 8 đến 10 chi tiết phải giữ nguyên giống hệt nhau trong mọi bức ảnh (màu tường và các lớp đất lộ rõ, hình dáng mái, số lượng cột hiên, số lượng và hình dáng cửa sổ, màu cửa chính, yếu tố đặc trưng và vị trí của nó, cây đặc trưng và phía đặt cây, thiết bị chiếu sáng). Những chi tiết này giúp các bức ảnh nhất quán với nhau. 6. **Prompt Gốc A - Toàn cảnh Mặt ngoài**: một prompt tạo ảnh sẵn sàng để dán dưới 150 từ cho cảnh mặt ngoài buổi sáng tươi sáng nhìn từ quảng trường công cộng, với tòa nhà và các vật thể chính nằm ngay trong câu đầu tiên. 7. **Prompt Gốc B - Góc nhìn Mặt ngoài Thứ hai**: một prompt tạo ảnh sẵn sàng để dán dưới 150 từ cho cảnh lúc hoàng hôn nhìn từ phía vườn đọc sách với ánh sáng ấm áp tỏa ra từ các ô cửa sổ, lặp lại mọi chi tiết thị giác cố định bằng ngôn từ rõ ràng và nêu rõ từng yếu tố xuất hiện ở bên nào từ góc nhìn này. 8. **Ghi chú Thực tế**: ba ghi chú ngắn gọn về làm mát thụ động và che nắng, lối đi không bậc tam cấp và cách bảo vệ tường đất khỏi mưa (độ vươn của mái, bệ đá). Quy tắc: không lấy con người làm chủ thể chính, không có chữ, biển báo hoặc logo trong các prompt tạo ảnh, không mô tả nội thất trong các prompt gốc (các mô hình tạo ảnh thường bỏ qua đồ nội thất). Sử dụng các đoạn văn ngắn và danh sách.

Mặt ngoài chân thực buổi sáng của ngôi thư viện làng tường đất nện một tầng: tường màu vàng đất và đỏ gỉ sắt phân lớp, tháp đọc sách hình tròn ở đầu bên phải, sáu cột hiên bằng gỗ, cửa màu xanh da trời, cây bách xù cổ thụ và rãnh nước bằng đá. Bước 2 trong quy trình thiết kế Thư viện Làng Tường Đất Nện.
Một ngôi thư viện làng tường đất nện một tầng với tháp đọc sách tường đất nện hình tròn ở đầu bên phải hướng ra quảng trường lát đá ngập nắng vào một buổi sáng trong lành. Những bức tường dài lộ rõ các lớp đất nằm ngang màu vàng đất, màu cát và đỏ gỉ sắt trên bệ đá xám thấp. Mái bằng với viền thép tối màu mỏng vươn ra tạo thành hiên lối vào sâu râm mát được nâng đỡ bởi sáu cột gỗ sáng màu mảnh mai. Phía sau hiên, ba ô cửa sổ cao hẹp có khung đồng tối màu nằm ở mỗi bên của cánh cửa đôi rộng bằng gỗ màu xanh da trời. Tháp hình trụ cao hơn mái chính một chút, với một vòng cửa sổ vuông nhỏ gần đỉnh. Một cây bách xù cổ thụ tán rộng với thân cây xoắn đứng ở bên trái lối vào, và một rãnh nước nông bằng đá chạy ngang qua quảng trường phía trước hiên. Những chậu đất sét trồng hoa oải hương và cỏ khô, hai chiếc ghế dài bằng gỗ và một dãy giá để xe đạp. Ánh sáng chiếu xiên màu vàng dịu nhẹ, bóng đổ sắc nét, bầu trời xanh nhạt phía trên những ngọn đồi khô ráo. Nhiếp ảnh kiến trúc chân thực, khung ngang 16:9. Không có người, không có chữ, không có biển báo, không có xe hơi.

Góc nhìn chân thực lúc hoàng hôn của cùng ngôi thư viện tường đất nện nhìn từ vườn đọc sách: những bức tường đất phân lớp, tháp đọc sách hình tròn sáng lên như chiếc đèn lồng ở đầu bên trái, giàn pergola gỗ sáu cột, hoa oải hương, cỏ dại và đèn lối đi. Bước 3 trong quy trình thiết kế Thư viện Làng Tường Đất Nện.
Mặt sau của cùng ngôi thư viện làng tường đất nện một tầng lúc hoàng hôn, nhìn từ khu vườn đọc sách yên tĩnh, với ánh sáng vàng ấm áp tỏa ra từ những ô cửa sổ rộng và từ tháp đọc sách tường đất nện hình tròn hiện nằm ở đầu bên trái. Cùng một tòa nhà: những bức tường dài với các lớp đất nằm ngang rõ nét màu vàng đất, màu cát và đỏ gỉ sắt trên bệ đá xám thấp, mái bằng với viền thép tối màu mỏng nhô ra sâu, cùng những ô cửa sổ cao hẹp có khung đồng tối màu. Tháp hình trụ cao hơn mái chính một chút, với một dải cửa sổ cao khung tối màu chạy liên tục quanh phần đỉnh, tỏa sáng như một chiếc đèn lồng. Dọc theo phía khu vườn này, một giàn pergola gỗ nông với sáu cột gỗ sáng màu mảnh mai che mát cho hiên đá có những chiếc ghế dài đọc sách thấp bằng gỗ. Khu vườn có những lối đi rải sỏi, cỏ khô bản địa, hoa oải hương trồng trong chậu đất sét và những chiếc đèn lối đi nhỏ ánh sáng ấm áp, còn phần ngọn của cây bách xù cổ thụ tán rộng từ quảng trường phía trước vừa kịp lộ ra phía trên mái nhà ở bên phải. Bầu trời chạng vạng xanh thẫm với vầng trăng lưỡi liềm mảnh và những ngôi sao đầu tiên. Nhiếp ảnh kiến trúc chân thực, khung ngang 16:9. Không có người, không có chữ, không có biển báo, không có xe hơi.
Giải mã một chuỗi JWT mà không mặc định tin tưởng nó, đồng thời rà soát lỗi (lint) phần header và các thông tin xác nhận (claims): thuật toán alg none, khóa jku/x5u từ xa, thiếu hoặc hết hạn exp, lệch đồng hồ nbf/iat, dấu thời gian tính bằng mili-giây, thời hạn sống quá dài, sai lệch iss/aud, dữ liệu nhạy cảm nằm trong payload, và kiểm tra chữ ký HMAC bằng khóa bí mật lấy từ biến môi trường. Sử dụng khi thao tác đăng nhập hoặc lệnh gọi API thất bại với mã lỗi 401, hoặc khi rà soát cấu hình token. Bao gồm công cụ kiểm tra bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.
---
name: jwt-token-inspector
description: Giải mã một chuỗi JWT mà không mặc định tin tưởng nó, đồng thời rà soát lỗi (lint) phần header và các thông tin xác nhận (claims): thuật toán alg none, khóa jku/x5u từ xa, thiếu hoặc hết hạn exp, lệch đồng hồ nbf/iat, dấu thời gian tính bằng mili-giây, thời hạn sống quá dài, sai lệch iss/aud, dữ liệu nhạy cảm nằm trong payload, và kiểm tra chữ ký HMAC bằng khóa bí mật lấy từ biến môi trường. Sử dụng khi thao tác đăng nhập hoặc lệnh gọi API thất bại với mã lỗi 401, hoặc khi rà soát cấu hình token. 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 JWT Token
Bạn giúp các lập trình viên tìm hiểu và gỡ lỗi JSON Web Tokens (JWT) một cách an toàn. Bạn giải mã phần header và payload, chuyển đổi các dấu thời gian sang định dạng giờ UTC dễ đọc, giải thích ý nghĩa của từng claim, và gắn cờ cảnh báo những thiết lập khiến token trở nên mất an toàn, không thể sử dụng hoặc dễ gây lỗi trên môi trường thực tế (production). Bạn luôn coi mọi token là không đáng tin cậy: giải mã (decoding) không phải là xác minh chữ ký (verification), và bạn luôn nêu rõ điều đó.
## Các tệp trong kỹ năng này
- `scripts/jwt_inspect.py` - giải mã một JWS compact token, rà soát lỗi trong header và claims, tùy chọn xác minh chữ ký HS256/HS384/HS512 bằng khóa bí mật, xuất kết quả ra markdown hoặc JSON (chỉ sử dụng thư viện chuẩn Python 3)
- `references/claims-and-validation.md` - các claim đã đăng ký chuẩn, những gì bộ xác minh (verifier) bắt buộc phải kiểm tra và theo thứ tự nào
- `references/common-jwt-pitfalls.md` - những sai lầm đứng sau hầu hết các lỗi và sự cố bảo mật liên quan đến JWT, kèm theo cách khắc phục
- `templates/jwt-review.md` - mẫu báo cáo ngắn gọn cho phiếu báo lỗi (bug ticket), đánh giá PR hoặc rà soát bảo mật
- `examples/sample-tokens.md` - ba token mẫu minh họa (thời hạn quá dài, alg none, khóa jku từ xa kèm thời gian hết hạn exp tính bằng mili-giây) và các phát hiện kỳ vọng
## Quy trình thực hiện
### 1. Xử lý token an toàn
Yêu cầu người dùng cung cấp token từ môi trường thử nghiệm (test) hoặc tiền triển khai (staging). Nếu người dùng dán một token đang hoạt động trên môi trường thực tế (production), hãy cảnh báo họ rằng token đó vẫn cấp quyền truy cập cho đến khi hết hạn và đề nghị họ xoay vòng (rotate)/thu hồi nó sau phiên làm việc. Không bao giờ lặp lại toàn bộ chuỗi token hoặc phần chữ ký trong câu trả lời của bạn.
### 2. Chạy công cụ kiểm tra
```
python3 scripts/jwt_inspect.py "<token>"
python3 scripts/jwt_inspect.py "<token>" --now 2026-10-19T09:00:00Z --leeway 60
python3 scripts/jwt_inspect.py "<token>" --expect-iss https://auth.example.com --expect-aud api://orders
JWT_SECRET=... python3 scripts/jwt_inspect.py "<token>" --secret-env JWT_SECRET
python3 scripts/jwt_inspect.py --self-test
```
Tiền tố `Bearer ` sẽ tự động được lược bỏ. Sử dụng `-` để đọc token từ luồng đầu vào chuẩn (stdin). Mã thoát (exit code) bằng 1 có nghĩa là phát hiện ít nhất một vấn đề mức độ cao (`--strict`: bất kỳ phát hiện nào); mã thoát bằng 2 có nghĩa là không thể giải mã được token.
### 3. Giải thích kết quả
- Hiển thị phần header và các claim đã giải mã cùng thời gian exp/nbf/iat theo cả giờ UTC và múi giờ của người dùng.
- Nhóm các phát hiện theo mức độ nghiêm trọng: **cao (high)** (mất an toàn hoặc bị từ chối: alg none, chữ ký trống, đã hết hạn, chưa đến thời điểm có hiệu lực, sai lệch iss/aud, khóa nhúng trực tiếp hoặc tải từ xa, chữ ký sai, dấu thời gian tính bằng mili-giây), **trung bình (medium)** (không có exp, thời hạn vượt quá chính sách, chứa claim nhạy cảm, khóa bí mật HMAC yếu, iat nằm ở tương lai), **thấp (low)** (thiếu iss, aud, sub hoặc kid).
- Đối với lỗi 401, hãy đối chiếu phát hiện với triệu chứng thực tế bằng cách tham khảo `references/claims-and-validation.md`: lệch đồng hồ hệ thống, sai đối tượng thụ hưởng (audience), sai bên phát hành (issuer) hoặc sai khóa xác minh là những nguyên nhân phổ biến nhất.
### 4. Đề xuất biện pháp khắc phục
Sử dụng `references/common-jwt-pitfalls.md`: ghim cố định danh sách thuật toán cho phép trên bộ xác minh, giữ thời hạn của access token ngắn (5-15 phút) kết hợp với refresh token, chuyển dữ liệu cá nhân ra khỏi payload, cho phép khoảng dung sai thời gian (leeway) từ 30-60 giây, và chỉ tải khóa từ một URL JWKS đã được cấu hình cố định.
### 5. Báo cáo
Điền vào mẫu `templates/jwt-review.md` với dấu vân tay (fingerprint) của token (tuyệt đối không ghi nguyên chuỗi token), các phát hiện, nguyên nhân gốc rễ và biện pháp khắc phục đã thống nhất.
## Quy tắc
- Giải mã không phải là xác minh chữ ký. Không bao giờ khẳng định một token là hợp lệ trừ khi chữ ký của nó đã được kiểm tra bằng đúng khóa tương ứng.
- Không bao giờ in ra hoặc lưu trữ toàn bộ chuỗi token, chữ ký hoặc khóa bí mật. Hãy truyền khóa bí mật thông qua biến môi trường chứ không truyền trực tiếp trên dòng lệnh.
- Không hỗ trợ làm giả (forge), phát lại (replay) hoặc chỉnh sửa trái phép (tamper) các token cho những hệ thống mà người dùng không sở hữu.
- Các chữ ký RS/ES/PS/EdDSA cần có khóa công khai (public key) và thư viện JOSE chuyên dụng; kịch bản đi kèm chỉ xác minh các thuật toán họ HMAC.
FILE:references/claims-and-validation.md
# Các Claim và Thứ tự Xác thực
## Các claim chuẩn đã đăng ký (RFC 7519)
| Claim | Ý nghĩa | Kiểu dữ liệu | Ghi chú |
|---|---|---|---|
| iss | Bên phát hành (Issuer) | chuỗi/URL | Ghim cố định trên bộ xác minh; mỗi miền tin cậy một bên phát hành |
| sub | Chủ thể (ID người dùng hoặc dịch vụ) | chuỗi | ID ổn định, không dùng địa chỉ email vốn có thể thay đổi |
| aud | Đối tượng đích (Audience) | chuỗi hoặc mảng | Dịch vụ API phải tìm thấy ID của chính mình trong trường này |
| exp | Thời điểm hết hạn | NumericDate (giây) | Bắt buộc phải có đối với access token |
| nbf | Không có hiệu lực trước thời điểm | NumericDate | Token sẽ bị từ chối cho đến khi chạm mốc thời gian này |
| iat | Thời điểm phát hành | NumericDate | Dùng để kiểm tra vòng đời và độ mới của token |
| jti | Định danh token (Token ID) | chuỗi | Hỗ trợ danh sách thu hồi (revocation lists) và phát hiện tấn công phát lại (replay) |
NumericDate là số giây tính từ mốc 1970-01-01T00:00:00Z. Một giá trị gồm 13 chữ số là mili-giây và sẽ bị hiểu nhầm thành một ngày cách hiện tại hàng chục nghìn năm trong tương lai.
## Các tham số trong Header
- `alg` - thuật toán ký. Bộ xác minh mới là bên quyết định những giá trị nào được phép sử dụng; bản thân token không bao giờ được quyền quyết định.
- `kid` - định danh khóa (key ID), được dùng để chọn đúng khóa từ tập JWKS của bên phát hành trong quá trình xoay vòng khóa.
- `typ` - thường là `JWT` hoặc `at+jwt` (hồ sơ access token theo chuẩn RFC 9068).
- `jku`, `x5u`, `jwk` - trỏ tới hoặc nhúng trực tiếp các khóa. Hãy bỏ qua chúng trừ khi chúng khớp hoàn toàn với một nguồn đáng tin cậy đã được ghim cố định.
## Những gì bộ xác minh bắt buộc phải kiểm tra, theo đúng thứ tự
1. Token có đúng ba phân đoạn và giải mã ra các đối tượng JSON hợp lệ.
2. `alg` nằm trong danh sách cho phép (allow-list) của bộ xác minh (ví dụ: chỉ chấp nhận RS256). Từ chối thẳng `none`.
3. Chữ ký được xác minh thành công bằng khóa được chọn qua `kid` từ một nguồn JWKS đáng tin cậy, tuyệt đối không lấy khóa trực tiếp từ bên trong token.
4. `exp` nằm ở tương lai và `nbf` nằm ở quá khứ, cả hai đều được áp dụng một khoảng dung sai nhỏ (30-60 giây).
5. `iss` bằng đúng định danh của bên phát hành kỳ vọng.
6. `aud` có chứa ID của dịch vụ hiện tại.
7. Các quy tắc nghiệp vụ của ứng dụng: phạm vi quyền (scopes), vai trò (roles), định danh thuê bao (tenant), và `jti` chưa bị thu hồi.
## Đối chiếu triệu chứng lỗi 401 với nguyên nhân
| Triệu chứng | Nguyên nhân khả dĩ | Điểm cần kiểm tra |
|---|---|---|
| Chạy được ở máy cục bộ (local), lỗi trên prod | Lệch đồng hồ hệ thống hoặc URL của bên phát hành khác nhau | So sánh iat/nbf với giờ máy chủ, kiểm tra trường iss |
| Lỗi ngay lập tức sau khi vừa đăng nhập | nbf hoặc iat nằm ở tương lai | Đồng bộ thời gian trên các máy chủ (NTP) |
| Chỉ bị lỗi ở một API duy nhất | Sai đối tượng đích (audience) | Danh sách aud |
| Bị lỗi sau khi xoay vòng khóa (key rotation) | Không nhận diện được kid mới hoặc JWKS bị lưu đệm (cache) quá lâu | Đối chiếu kid với JWKS, kiểm tra thời gian sống (TTL) của bộ nhớ đệm |
| Không bao giờ hết hạn | Thiếu trường exp hoặc exp đang tính bằng mili-giây | Giá trị của trường exp |
FILE:references/common-jwt-pitfalls.md
# Những Sai lầm Phổ biến khi Sử dụng JWT
1. **Chấp nhận `alg: none`.** Các token không có chữ ký có thể bị làm giả dễ dàng. Cách khắc phục: truyền một danh sách thuật toán cho phép (allow-list) tường minh vào hàm xác minh.
2. **Nhầm lẫn thuật toán (RS256 so với HS256).** Nếu bộ xác minh chấp nhận cả hai, kẻ tấn công có thể ký bằng thuật toán HMAC với việc dùng chính khóa công khai (public key) làm khóa bí mật (secret). Cách khắc phục: mỗi khóa chỉ gắn với một họ thuật toán duy nhất.
3. **Tin tưởng khóa lấy từ bên trong token.** Các trường `jku`, `x5u` hoặc `jwk` nhúng sẵn cho phép người gửi tự ý chọn khóa xác minh. Cách khắc phục: chỉ tải khóa từ một URL JWKS đã được cấu hình cố định từ trước.
4. **Khóa bí mật HMAC yếu.** Các khóa bí mật ngắn hoặc nằm trong từ điển có thể bị dò quét vét cạn (brute-force) ngoại tuyến chỉ từ một token thu được. Cách khắc phục: dùng ít nhất 32 byte ngẫu nhiên cho HS256 (48 byte cho HS384, 64 byte cho HS512) và lưu trữ trong trình quản lý bí mật (secret manager).
5. **Access token có thời hạn quá dài.** Một token bị lộ sẽ tiếp tục có hiệu lực cho đến tận thời điểm exp. Cách khắc phục: đặt thời hạn access token từ 5-15 phút kết hợp với refresh token có xoay vòng; thu hồi theo `jti` nếu cần.
6. **Chứa dữ liệu cá nhân trong payload.** Base64url chỉ là mã hóa định dạng (encoding) chứ không phải mật mã hóa (encryption); bất kỳ ai cầm token đều đọc được nội dung. Cách khắc phục: chỉ lưu các ID và phạm vi quyền (scopes), hoặc sử dụng JWE.
7. **Dùng mili-giây thay vì giây.** Rất hay gặp khi dùng hàm `Date.now()` trong JavaScript. Cách khắc phục: sử dụng `Math.floor(Date.now() / 1000)`.
8. **Không cấu hình khoảng dung sai thời gian (leeway).** Sự chênh lệch nhỏ giữa đồng hồ các máy chủ sẽ gây ra lỗi 401 ngẫu nhiên. Cách khắc phục: cấu hình dung sai 30-60 giây và bật đồng bộ NTP trên tất cả các máy chủ.
9. **Bỏ qua bước kiểm tra aud/iss.** Token cấp cho dịch vụ này có thể bị đem phát lại (replay) để truy cập trái phép vào dịch vụ khác. Cách khắc phục: luôn xác thực cả hai trường này.
10. **Đặt token trên URL hoặc trong nhật ký (logs).** Chúng sẽ bị rò rỉ qua trường Referer, máy chủ proxy và hệ thống tìm kiếm nhật ký. Cách khắc phục: gửi token trong trường header Authorization và che (redact) token khi ghi nhật ký.
FILE:templates/jwt-review.md
# Báo cáo Rà soát JWT - <tên dịch vụ hoặc mã phiếu>
**Dấu vân tay token:** sha256 <12 ký tự hex đầu tiên> **Môi trường:** <dev / staging / prod> **Thời điểm kiểm tra:** <giờ UTC>
**Bên phát hành (Issuer):** <iss> **Đối tượng đích (Audience):** <aud> **Thuật toán:** <alg> **Thời hạn sống:** <exp - iat>
## Các phát hiện
| Mức độ nghiêm trọng | Mã phát hiện | Chi tiết | Cách khắc phục |
|---|---|---|---|
| | | | |
## Nguyên nhân gốc rễ
<một hoặc hai câu liên kết phát hiện trên với triệu chứng lỗi được báo cáo>
## Các thay đổi đã thống nhất
- [ ] Bộ xác minh (Verifier): <danh sách cho phép allow-list, dung sai thời gian leeway, kiểm tra iss/aud>
- [ ] Bên phát hành (Issuer): <thời hạn sống, các claim, kích thước khóa, kid>
- [ ] Xoay vòng token hoặc khóa bí mật bị lộ: <có/không, người phụ trách>
## Ghi chú
Được giải mã bằng scripts/jwt_inspect.py. Bản thân chuỗi token không được lưu lại trong báo cáo này.
FILE:examples/sample-tokens.md
# Các Token Mẫu (chỉ dùng để minh họa, không thuộc hệ thống thực tế)
Chạy từng mẫu bằng lệnh:
```
python3 scripts/jwt_inspect.py "<token>" --now 2026-10-19T09:00:00Z --expect-aud api://orders
```
## 1. Token khách hàng có thời hạn sống dài (HS256, khóa bí mật minh họa "dev-secret")
```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguc2hvcGZyb250LmV4YW1wbGUiLCJzdWIiOiJjdXN0Xzg4NDEiLCJhdWQiOlsiYXBpOi8vb3JkZXJzIiwiYXBpOi8vcHJvZmlsZSJdLCJpYXQiOjE3OTIzOTUwMDAsImV4cCI6MTc5Mjk5OTgwMCwiZW1haWwiOiJtaW5hQGV4YW1wbGUuY29tIiwicm9sZXMiOlsiY3VzdG9tZXIiXSwiY2FyZF9sYXN0NCI6IjQyNDIifQ.xupeoN2LYDYe-FKD_4qOh7KCw6G68Zi1pCms7dtUGh0
```
Kỳ vọng: mã thoát 0; mức trung bình `lifetime_long` (7.0 ngày vượt quá chính sách 1.0 ngày), mức trung bình `sensitive_claim` (card_last4). Khi đặt biến môi trường cho `--secret-env` là "dev-secret": xác minh chữ ký thành công, kèm theo cảnh báo mức trung bình `weak_secret` (10 byte).
## 2. Token quản trị viên không có chữ ký (alg none)
```
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2F1dGguc2hvcGZyb250LmV4YW1wbGUiLCJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc5MjM5NTAwMH0.
```
Kỳ vọng: mã thoát 1; mức cao `alg_none`, mức cao `aud_mismatch` (không có aud trong khi yêu cầu `api://orders`), mức trung bình `exp_missing`.
## 3. URL khóa từ xa và thời gian hết hạn tính bằng mili-giây (RS256)
```
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImprdSI6Imh0dHBzOi8va2V5cy5hdHRhY2tlci5leGFtcGxlL2p3a3MuanNvbiJ9.eyJpc3MiOiJodHRwczovL2F1dGguc2hvcGZyb250LmV4YW1wbGUiLCJzdWIiOiJzdmMtcmVwb3J0cyIsImF1ZCI6ImFwaTovL29yZGVycyIsImlhdCI6MTc5MjM5NTAwMCwiZXhwIjoxNzkyMzk4NjAwMDAwfQ.bm90LWEtcmVhbC1zaWduYXR1cmUtanVzdC1hLWRlbW8
```
Kỳ vọng: mã thoát 1; mức cao `exp_milliseconds`, mức cao `header_jku`, mức thấp `kid_missing`.
FILE:scripts/jwt_inspect.py
#!/usr/bin/env python3
"""JWT token inspector and claim linter (Python 3 standard library only).
Decodes a JSON Web Token (JWS compact form: header.payload.signature) WITHOUT trusting it,
prints the header and claims with human-readable times, and flags risky or broken settings.
Optionally verifies HS256/HS384/HS512 signatures when you pass the shared secret.
Usage:
python3 jwt_inspect.py <token> # or "-" to read the token from stdin
python3 jwt_inspect.py <token> --now 2026-10-19T09:00:00Z --leeway 60
python3 jwt_inspect.py <token> --expect-iss https://auth.example.com --expect-aud api://orders
python3 jwt_inspect.py <token> --secret-env JWT_SECRET # verify HS* with a secret from an env var
python3 jwt_inspect.py <token> --max-lifetime 3600 --json
python3 jwt_inspect.py --self-test
Severity: high (token unsafe or unusable), medium (likely bug or weak policy), low (hygiene).
Exit code: 0 no high findings, 1 at least one high finding (or --strict and any finding), 2 bad input.
The signature itself is never printed; only its length and a short SHA-256 fingerprint of the token.
"""
import argparse
import base64
import datetime as dt
import hashlib
import hmac
import json
import os
import re
import sys
HS_ALGS = {"HS256": hashlib.sha256, "HS384": hashlib.sha384, "HS512": hashlib.sha512}
MIN_HS_SECRET = {"HS256": 32, "HS384": 48, "HS512": 64}
KNOWN_ALGS = set(HS_ALGS) | {"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
"ES256", "ES384", "ES512", "EdDSA"}
SENSITIVE = re.compile(r"(pass(word)?|pwd|secret|ssn|social|credit|card|cvv|iban|private|api_?key|refresh_?token)", re.I)
TIME_CLAIMS = ("exp", "nbf", "iat")
class TokenError(ValueError):
pass
def b64url_decode(part, what):
if not re.fullmatch(r"[A-Za-z0-9_-]*", part):
raise TokenError(f"{what} is not base64url (found characters outside A-Z a-z 0-9 - _ or '=' padding)")
try:
return base64.urlsafe_b64decode(part + "=" * (-len(part) % 4))
except (ValueError, TypeError) as e:
raise TokenError(f"{what} could not be base64url-decoded: {e}")
def b64url_encode(raw):
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
def decode(token):
token = token.strip()
if token.lower().startswith("bearer "):
token = token[7:].strip()
parts = token.split(".")
if len(parts) == 5:
raise TokenError("5 segments: this is an encrypted JWE, not a signed JWS; it cannot be inspected without the key")
if len(parts) != 3:
raise TokenError(f"expected 3 dot-separated segments, found {len(parts)}")
out = {}
for name, part in (("header", parts[0]), ("payload", parts[1])):
raw = b64url_decode(part, name)
try:
obj = json.loads(raw.decode("utf-8"))
except (UnicodeDecodeError, ValueError):
raise TokenError(f"{name} is not valid UTF-8 JSON")
if not isinstance(obj, dict):
raise TokenError(f"{name} must be a JSON object")
out[name] = obj
out["signature"] = b64url_decode(parts[2], "signature")
out["signing_input"] = (parts[0] + "." + parts[1]).encode("ascii")
out["fingerprint"] = hashlib.sha256(token.encode("utf-8")).hexdigest()[:12]
return out
def parse_now(value):
if value is None:
return int(dt.datetime.now(dt.timezone.utc).timestamp())
if re.fullmatch(r"\d+", value):
return int(value)
t = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
if t.tzinfo is None:
t = t.replace(tzinfo=dt.timezone.utc)
return int(t.timestamp())
def iso(ts):
try:
return dt.datetime.fromtimestamp(ts, dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
except (OverflowError, OSError, ValueError, TypeError):
return "invalid"
def human_delta(seconds):
s = abs(int(seconds))
for unit, size in (("d", 86400), ("h", 3600), ("m", 60)):
if s >= size:
return f"{s / size:.1f}{unit}"
return f"{s}s"
def verify_hs(tok, secret):
alg = tok["header"].get("alg")
fn = HS_ALGS.get(alg)
if fn is None:
return None
mac = hmac.new(secret.encode("utf-8"), tok["signing_input"], fn).digest()
return hmac.compare_digest(mac, tok["signature"])
def inspect(token, now=None, leeway=0, expect_iss=None, expect_aud=None, max_lifetime=86400, secret=None):
tok = decode(token)
h, p = tok["header"], tok["payload"]
findings = []
def add(sev, code, msg):
findings.append({"severity": sev, "code": code, "message": msg})
alg = h.get("alg")
if alg is None:
add("high", "alg_missing", "header has no 'alg'; libraries may reject it or guess")
elif str(alg).lower() == "none":
add("high", "alg_none", "alg is 'none': the token is unsigned and anyone can forge it")
if tok["signature"]:
add("medium", "none_with_signature", "alg 'none' but a signature segment is present")
elif alg not in KNOWN_ALGS:
add("medium", "alg_unknown", f"unusual alg '{alg}'")
if alg and str(alg).lower() != "none" and not tok["signature"]:
add("high", "signature_empty", "signature segment is empty although alg requires one")
for key in ("jku", "x5u"):
if key in h:
add("high", f"header_{key}", f"header '{key}' points to a remote key URL ({h[key]}); never fetch keys from a URL inside the token")
if "jwk" in h:
add("high", "header_jwk", "header embeds its own 'jwk'; a verifier that trusts it accepts attacker keys")
if "crit" in h:
add("medium", "header_crit", f"'crit' lists extensions {h['crit']}; verifiers must understand them or reject the token")
if alg in KNOWN_ALGS and alg not in HS_ALGS and "kid" not in h:
add("low", "kid_missing", "asymmetric alg without 'kid'; key rotation will be painful")
now = parse_now(now) if not isinstance(now, int) else now
times = {}
for c in TIME_CLAIMS:
if c in p:
v = p[c]
if isinstance(v, bool) or not isinstance(v, (int, float)):
add("high", f"{c}_not_numeric", f"'{c}' must be NumericDate seconds, got {v!r}")
elif v > 10**11:
add("high", f"{c}_milliseconds", f"'{c}'={v} looks like milliseconds; JWT uses seconds")
else:
times[c] = int(v)
if "exp" not in p:
add("medium", "exp_missing", "no 'exp': the token never expires")
elif "exp" in times and times["exp"] + leeway < now:
add("high", "expired", f"expired at {iso(times['exp'])} ({human_delta(now - times['exp'])} ago)")
if "nbf" in times and times["nbf"] - leeway > now:
add("high", "not_yet_valid", f"not valid before {iso(times['nbf'])} ({human_delta(times['nbf'] - now)} from now)")
if "iat" in times and times["iat"] - leeway > now:
add("medium", "iat_future", f"issued in the future ({iso(times['iat'])}); check clock skew")
if "exp" in times and "iat" in times:
life = times["exp"] - times["iat"]
if life <= 0:
add("high", "exp_before_iat", "exp is not after iat")
elif max_lifetime and life > max_lifetime:
add("medium", "lifetime_long", f"lifetime {human_delta(life)} exceeds policy {human_delta(max_lifetime)}")
if "iss" not in p:
add("low", "iss_missing", "no 'iss'; verifiers cannot pin the issuer")
elif expect_iss and p["iss"] != expect_iss:
add("high", "iss_mismatch", f"iss '{p['iss']}' != expected '{expect_iss}'")
aud = p.get("aud")
auds = aud if isinstance(aud, list) else ([aud] if aud is not None else [])
if aud is None and expect_aud:
add("high", "aud_mismatch", f"no 'aud' claim but '{expect_aud}' is required")
elif aud is None:
add("low", "aud_missing", "no 'aud'; the token could be replayed against another service")
elif expect_aud and expect_aud not in auds:
add("high", "aud_mismatch", f"aud {auds} does not contain expected '{expect_aud}'")
if "sub" not in p:
add("low", "sub_missing", "no 'sub'; who is this token about?")
for k in p:
if SENSITIVE.search(k):
add("medium", "sensitive_claim", f"claim '{k}' looks sensitive; JWT payloads are only encoded, not encrypted")
verified = None
if secret is not None:
if alg not in HS_ALGS:
add("low", "verify_skipped", f"--secret only verifies HS256/384/512; alg is {alg}")
else:
verified = verify_hs(tok, secret)
if not verified:
add("high", "bad_signature", "HMAC signature does NOT match the given secret")
if len(secret.encode("utf-8")) < MIN_HS_SECRET[alg]:
add("medium", "weak_secret", f"{alg} secret is {len(secret.encode('utf-8'))} bytes; use at least {MIN_HS_SECRET[alg]} random bytes")
order = {"high": 0, "medium": 1, "low": 2}
findings.sort(key=lambda f: (order[f["severity"]], f["code"]))
return {
"fingerprint": tok["fingerprint"],
"header": h,
"payload": p,
"times": {c: iso(v) for c, v in times.items()},
"now": iso(now),
"signature_bytes": len(tok["signature"]),
"signature_verified": verified,
"findings": findings,
}
def to_markdown(r):
lines = [f"# JWT inspection (sha256 {r['fingerprint']})", "",
f"Checked at {r['now']}. Signature: {r['signature_bytes']} bytes"
+ ("" if r["signature_verified"] is None else (", verified OK" if r["signature_verified"] else ", NOT verified")), "",
"## Header", "```json", json.dumps(r["header"], indent=2, ensure_ascii=False), "```", "",
"## Claims", "```json", json.dumps(r["payload"], indent=2, ensure_ascii=False), "```", ""]
if r["times"]:
lines += ["| Claim | UTC |", "|---|---|"] + [f"| {k} | {v} |" for k, v in r["times"].items()] + [""]
lines += ["## Findings"]
if not r["findings"]:
lines.append("No findings.")
else:
lines += ["| Severity | Code | Detail |", "|---|---|---|"]
lines += [f"| {f['severity']} | {f['code']} | {f['message']} |" for f in r["findings"]]
return "\n".join(lines)
def make_token(header, payload, secret=None, signature=b""):
si = b64url_encode(json.dumps(header, separators=(",", ":")).encode()) + "." + \
b64url_encode(json.dumps(payload, separators=(",", ":")).encode())
if secret is not None:
signature = hmac.new(secret.encode(), si.encode(), HS_ALGS[header["alg"]]).digest()
return si + "." + b64url_encode(signature)
def self_test():
now = 1760860800 # 2025-10-19T08:00:00Z
strong = "k" * 40
good_claims = {"iss": "https://auth.example.com", "sub": "user-42", "aud": "api://orders",
"iat": now - 60, "exp": now + 840}
good = make_token({"alg": "HS256", "typ": "JWT"}, good_claims, strong)
codes = lambda r: {f["code"] for f in r["findings"]}
cases = []
def check(name, cond):
cases.append((name, bool(cond)))
r = inspect(good, now=now, secret=strong, expect_iss="https://auth.example.com", expect_aud="api://orders")
check("clean token has no findings", r["findings"] == [])
check("clean token signature verified", r["signature_verified"] is True)
check("bearer prefix accepted", inspect("Bearer " + good, now=now)["payload"]["sub"] == "user-42")
check("wrong secret flagged", "bad_signature" in codes(inspect(good, now=now, secret="x" * 40)))
check("weak secret flagged", "weak_secret" in codes(inspect(make_token({"alg": "HS256"}, good_claims, "short"), now=now, secret="short")))
check("alg none flagged high", "alg_none" in codes(inspect(make_token({"alg": "none"}, good_claims), now=now)))
check("expired flagged", "expired" in codes(inspect(good, now=now + 3600)))
check("leeway tolerates small skew", "expired" not in codes(inspect(good, now=now + 850, leeway=30)))
check("nbf in future flagged", "not_yet_valid" in codes(inspect(make_token({"alg": "HS256"}, dict(good_claims, nbf=now + 600), strong), now=now)))
check("exp missing flagged", "exp_missing" in codes(inspect(make_token({"alg": "HS256"}, {"sub": "a", "iat": now}, strong), now=now)))
check("milliseconds exp flagged", "exp_milliseconds" in codes(inspect(make_token({"alg": "HS256"}, dict(good_claims, exp=(now + 600) * 1000), strong), now=now)))
check("long lifetime flagged", "lifetime_long" in codes(inspect(make_token({"alg": "HS256"}, dict(good_claims, exp=now + 30 * 86400), strong), now=now)))
check("iss mismatch flagged", "iss_mismatch" in codes(inspect(good, now=now, expect_iss="https://evil.example")))
check("aud list accepted", "aud_mismatch" not in codes(inspect(make_token({"alg": "HS256"}, dict(good_claims, aud=["a", "api://orders"]), strong), now=now, expect_aud="api://orders")))
check("aud mismatch flagged", "aud_mismatch" in codes(inspect(good, now=now, expect_aud="api://billing")))
check("missing aud with expected aud is high", "aud_mismatch" in codes(inspect(make_token({"alg": "HS256"}, {k: v for k, v in good_claims.items() if k != "aud"}, strong), now=now, expect_aud="api://orders")))
check("jku header flagged", "header_jku" in codes(inspect(make_token({"alg": "RS256", "kid": "1", "jku": "https://x/keys"}, good_claims, signature=b"sig"), now=now)))
check("rs256 without kid is low", "kid_missing" in codes(inspect(make_token({"alg": "RS256"}, good_claims, signature=b"sig"), now=now)))
check("sensitive claim flagged", "sensitive_claim" in codes(inspect(make_token({"alg": "HS256"}, dict(good_claims, password="hunter2"), strong), now=now)))
check("empty signature flagged", "signature_empty" in codes(inspect(make_token({"alg": "HS256"}, good_claims), now=now)))
for bad, label in (("abc.def", "two segments rejected"), ("a.b.c.d.e", "JWE rejected"), ("!!.e30.", "bad base64 rejected")):
try:
inspect(bad, now=now)
check(label, False)
except TokenError:
check(label, True)
check("markdown never prints signature", make_token({"alg": "HS256"}, good_claims, strong).split(".")[2] not in to_markdown(r))
passed = sum(ok for _, ok in cases)
for name, ok in cases:
print(("PASS " if ok else "FAIL ") + name)
print(f"{passed}/{len(cases)} passed")
return 0 if passed == len(cases) else 1
def main(argv=None):
ap = argparse.ArgumentParser(description="Decode and lint a JWT without trusting it.")
ap.add_argument("token", nargs="?", help="JWT string, or '-' to read from stdin")
ap.add_argument("--now", help="reference time: ISO 8601 (Z or offset) or epoch seconds; default: current UTC time")
ap.add_argument("--leeway", type=int, default=0, help="allowed clock skew in seconds (default 0)")
ap.add_argument("--expect-iss")
ap.add_argument("--expect-aud")
ap.add_argument("--max-lifetime", type=int, default=86400, help="max exp-iat seconds before warning (0 = off)")
ap.add_argument("--secret-env", help="name of an environment variable holding the HS* secret (avoids shell history)")
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.token:
ap.error("token required (or --self-test)")
token = sys.stdin.read() if a.token == "-" else a.token
secret = None
if a.secret_env:
secret = os.environ.get(a.secret_env)
if secret is None:
print(f"error: environment variable {a.secret_env} is not set", file=sys.stderr)
return 2
try:
r = inspect(token, now=a.now, leeway=a.leeway, expect_iss=a.expect_iss, expect_aud=a.expect_aud,
max_lifetime=a.max_lifetime, secret=secret)
except (TokenError, 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))
if any(f["severity"] == "high" for f in r["findings"]) or (a.strict and r["findings"]):
return 1
return 0
if __name__ == "__main__":
sys.exit(main())