Các quy ước định hướng rõ ràng để xây dựng hệ thống đường ống dữ liệu và kho dữ liệu phân tích đáng tin cậy.
# Đường ống dữ liệu (Data Pipeline) taste của một kỹ sư dữ liệu - Ưu tiên mô hình ELT: nạp dữ liệu thô nguyên trạng trước, sau đó mới biến đổi bên trong kho dữ liệu (warehouse). Độ tin cậy: 0.85 - Tách biệt rõ ràng các tầng raw, staging và mart, đồng thời tuyệt đối không để các bảng điều khiển (dashboard) đọc trực tiếp từ các bảng raw. Độ tin cậy: 0.90 - Mô hình hóa các phép biến đổi trong dbt (hoặc SQLMesh) dưới dạng mã SQL được quản lý phiên bản và duyệt mã (code review) giống như bất kỳ mã nguồn nào khác. Độ tin cậy: 0.90 - Đảm bảo mọi tác vụ đều có tính lũy đẳng (idempotent): chạy lại dữ liệu của một ngày phải tạo ra cùng một kết quả mà không sinh ra bản ghi trùng lặp. Độ tin cậy: 0.95 - Phân vùng (partition) các bảng lớn theo ngày diễn ra sự kiện và xử lý dữ liệu theo từng khoảng ngày giới hạn. Độ tin cậy: 0.85 - Chạy bù dữ liệu (backfill) phải là một thao tác hạng nhất có lệnh được lập tài liệu rõ ràng, chứ không phải một kịch bản chạy tay tạm bợ một lần. Độ tin cậy: 0.85 - Thêm các bài kiểm thử về tính duy nhất (uniqueness), không rỗng (not-null), giá trị hợp lệ (accepted values) và toàn vẹn tham chiếu trên mọi mô hình quan trọng. Độ tin cậy: 0.90 - Định nghĩa hợp đồng dữ liệu (data contracts) với các hệ thống cung cấp dữ liệu đầu nguồn (upstream producers) để các thay đổi cấu trúc (schema) báo lỗi ngay trong CI của họ chứ không làm gãy đường ống của bạn. Độ tin cậy: 0.80 - Theo dõi độ mới (freshness) của dữ liệu trên từng nguồn và cảnh báo khi dữ liệu về trễ, chứ không chỉ khi một tác vụ chạy thất bại. Độ tin cậy: 0.90 - Điều phối luồng công việc bằng Airflow, Dagster hoặc Prefect, và tách các tác vụ tính toán nặng ra khỏi các worker của trình điều phối. Độ tin cậy: 0.80 - Ưu tiên điều phối dựa trên tài sản dữ liệu (asset-based orchestration) có khả năng nhận biết bảng nào phụ thuộc vào bảng nào hơn là các đồ thị tác vụ thuần túy. Độ tin cậy: 0.70 - Lưu trữ dấu thời gian (timestamps) theo múi giờ UTC và chỉ chuyển đổi sang giờ địa phương ở tầng hiển thị. Độ tin cậy: 0.95 - Sử dụng Parquet hoặc một định dạng lưu trữ dạng cột khác có lược đồ (schema) tường minh cho các tệp trong hồ dữ liệu (data lake). Độ tin cậy: 0.85 - Tách biệt thời gian xảy ra sự kiện (event time) với thời gian xử lý (processing time) và xử lý rõ ràng các dữ liệu đến muộn. Độ tin cậy: 0.85 - Giữ thông tin định danh cá nhân (PII) trong ít bảng nhất có thể, che giấu (mask) hoặc băm (hash) chúng từ sớm và ghi chép quyền truy cập tập trung tại một nơi duy nhất. Độ tin cậy: 0.90 - Chỉ sử dụng các mô hình tăng trưởng (incremental models) sau khi việc làm mới toàn bộ (full refresh) trở nên quá chậm; tính chính xác là trên hết, chi phí đứng thứ hai. Độ tin cậy: 0.75 - Đặt tên các mô hình một cách nhất quán (stg_, int_, fct_, dim_) và viết tài liệu mô tả cho mọi cột mà người dùng thực sự truy vấn. Độ tin cậy: 0.80 - Sử dụng bản chụp nhanh chiều dữ liệu biến đổi chậm loại 2 (slowly changing dimension type 2 snapshots) khi lịch sử thay đổi là yếu tố quan trọng, thay vì dùng các cột kiểm toán tự phát. Độ tin cậy: 0.70 - Chạy thử đường ống dữ liệu trong CI trên một tập dữ liệu mẫu hoặc bản sao không tốn dung lượng (zero-copy clone) trước khi triển khai các thay đổi lên môi trường thực tế (production). Độ tin cậy: 0.80 - Giám sát chi phí kho dữ liệu trên từng mô hình và hủy các truy vấn quét toàn bộ bảng mà không có bộ lọc phân vùng (partition filters). Độ tin cậy: 0.75 - Mỗi chỉ số đo lường (metric) chỉ có một người sở hữu và một định nghĩa duy nhất trong tầng ngữ nghĩa (semantic layer), chứ không phải năm phiên bản khác nhau trên năm bảng điều khiển. Độ tin cậy: 0.80
Các quy ước mang tính định hướng rõ ràng để xây dựng và vận hành các dịch vụ FastAPI trên môi trường production bằng Python.
# Dịch Vụ FastAPI taste của một kỹ sư API Python - Định nghĩa phần thân của yêu cầu (request) và phản hồi (response) bằng các mô hình Pydantic v2 và luôn thiết lập response_model để không vô tình làm rò rỉ dữ liệu ngoài ý muốn. Độ tin cậy: 0.95 - Tách riêng các lược đồ (schemas) cho các thao tác tạo mới (create), cập nhật (update) và đọc (read) thay vì tái sử dụng mô hình ORM làm cấu trúc dữ liệu trả về của API. Độ tin cậy: 0.90 - Chỉ sử dụng các endpoint khai báo với async def khi mọi lệnh gọi bên trong đều thực sự là bất đồng bộ (async); nếu không, hãy viết bằng def thông thường và để threadpool tự xử lý. Độ tin cậy: 0.85 - Tiêm (inject) các phiên kết nối cơ sở dữ liệu (database sessions), cài đặt cấu hình và người dùng hiện tại thông qua Depends, tuyệt đối không dùng các biến toàn cục ở cấp độ module. Độ tin cậy: 0.90 - Tải cấu hình bằng pydantic-settings từ các biến môi trường và cho dừng ứng dụng báo lỗi ngay lúc khởi động (fail fast) nếu thiếu bất kỳ thông tin cấu hình nào. Độ tin cậy: 0.90 - Chia nhỏ các tuyến đường (routes) thành các module APIRouter theo từng miền nghiệp vụ (domain) và giữ cho tệp main.py chỉ giới hạn ở việc khởi tạo ứng dụng và kết nối các thành phần lại với nhau. Độ tin cậy: 0.85 - Đặt logic nghiệp vụ vào trong các hàm dịch vụ (service functions) hoàn toàn tách biệt và không biết gì về giao thức HTTP, để có thể tái sử dụng chúng từ các tiến trình chạy ngầm (workers) và trong kiểm thử (tests). Độ tin cậy: 0.85 - Sử dụng cú pháp SQLAlchemy 2.0 với các phiên bất đồng bộ (async sessions) và quản lý lược đồ cơ sở dữ liệu bằng các bản di chuyển (migrations) của Alembic. Độ tin cậy: 0.80 - Sử dụng trình quản lý ngữ cảnh lifespan cho quá trình khởi động (startup) và tắt ứng dụng (shutdown) thay vì các hook on_event đã lỗi thời. Độ tin cậy: 0.85 - Chỉ phát sinh ngoại lệ HTTPException ở tầng giao tiếp ngoài cùng và ánh xạ các lỗi nghiệp vụ (domain errors) sang mã trạng thái HTTP bằng các trình xử lý ngoại lệ (exception handlers) đã đăng ký. Độ tin cậy: 0.80 - Trả về cấu trúc lỗi nhất quán gồm mã lỗi máy có thể đọc được (machine-readable code), thông báo lỗi và thông tin chi tiết tùy chọn. Độ tin cậy: 0.85 - Đánh phiên bản cho API ngay trên đường dẫn (/v1) và coi lược đồ OpenAPI được tạo ra như một bản hợp đồng giao tiếp được kiểm tra tự động trong quy trình CI. Độ tin cậy: 0.75 - Sử dụng phân trang dựa trên con trỏ (cursor-based pagination) cho các danh sách lớn và giới hạn kích thước trang tối đa ở phía máy chủ. Độ tin cậy: 0.80 - Kiểm thử bằng pytest và httpx.AsyncClient, ghi đè các phụ thuộc (overriding dependencies) thay vì dùng kỹ thuật monkey-patch vào các thành phần nội bộ. Độ tin cậy: 0.90 - Chạy các tiến trình con (workers) của uvicorn phía sau gunicorn hoặc một trình quản lý tiến trình trên môi trường production, và chỉ dùng cờ --reload trong môi trường phát triển (development). Độ tin cậy: 0.80 - Chuyển các tác vụ xử lý chậm sang một hàng đợi tác vụ (task queue) thực thụ như Celery, Arq hoặc Dramatiq; BackgroundTasks chỉ dành cho các công việc nhỏ chạy một lần rồi thôi (fire-and-forget). Độ tin cậy: 0.85 - Thêm ID yêu cầu (request IDs), nhật ký định dạng JSON có cấu trúc và middleware truy vết OpenTelemetry ngay từ ngày đầu tiên. Độ tin cậy: 0.80 - Bảo vệ các endpoint bằng OAuth2 bearer token hoặc khóa API thông qua các phụ thuộc bảo mật (security dependencies) để trang tài liệu tự động hiển thị cơ chế xác thực. Độ tin cậy: 0.80 - Cung cấp riêng biệt hai endpoint /healthz và /readyz, trong đó bước kiểm tra mức độ sẵn sàng (readiness) sẽ kiểm tra kết nối cơ sở dữ liệu và các phụ thuộc cứng khác. Độ tin cậy: 0.85 - Quản lý các thư viện phụ thuộc bằng uv kèm theo tệp khóa (lock file), đồng thời kiểm tra lỗi cú pháp (lint) và định dạng mã nguồn bằng Ruff. Độ tin cậy: 0.75 - Kiểm tra kiểu dữ liệu (type check) bằng mypy hoặc pyright ở chế độ nghiêm ngặt (strict mode) trên gói dịch vụ. Độ tin cậy: 0.70
Các quy ước mang tính định hướng rõ ràng để xây dựng các ứng dụng tích hợp LLM, hệ thống RAG và tác nhân (agents) đáng tin cậy.
# Ứng Dụng LLM taste của một kỹ sư sản phẩm AI - Coi các prompt như mã nguồn (code): lưu trữ chúng trong các tệp được quản lý phiên bản kèm theo tên, phiên bản và người phụ trách, chứ không để dưới dạng các chuỗi ký tự viết trực tiếp (inline strings) nằm rải rác khắp nơi. Độ tin cậy: 0.90 - Xây dựng một bộ dữ liệu đánh giá (evaluation set) từ đầu vào thực tế của người dùng trước khi tinh chỉnh prompt, và chạy lại bộ đánh giá đó mỗi khi thay đổi prompt hoặc mô hình. Độ tin cậy: 0.95 - Yêu cầu đầu ra có cấu trúc bằng lược đồ JSON (JSON schema) hoặc lệnh gọi công cụ (tool calls) và xác thực nó bằng thư viện kiểm tra lược đồ trước khi đưa vào sử dụng. Độ tin cậy: 0.95 - Đặt một lớp cổng kết nối (gateway) hoặc lớp bọc (wrapper) mỏng bao quanh các nhà cung cấp mô hình để việc chuyển đổi mô hình hoặc thêm phương án dự phòng (fallbacks) chỉ đơn giản là một thay đổi trong cấu hình. Độ tin cậy: 0.85 - Ghim cố định các phiên bản mô hình chính xác trên môi trường production và chỉ nâng cấp chúng một cách có chủ đích sau khi vượt qua các bài kiểm tra đánh giá (evals). Độ tin cậy: 0.90 - Truyền phát trực tiếp (stream) phản hồi tới người dùng bất cứ khi nào đầu ra dài hơn một hoặc hai câu. Độ tin cậy: 0.85 - Bắt đầu khâu truy xuất thông tin bằng tìm kiếm kết hợp (hybrid search) đơn giản (từ khóa kết hợp với vector nhúng - embeddings) và thêm bước xếp hạng lại (reranking) trước khi tìm đến các quy trình RAG phức tạp. Độ tin cậy: 0.80 - Chia nhỏ tài liệu (chunk) theo cấu trúc tự nhiên của chúng (tiêu đề, các phần) và giữ lại siêu dữ liệu (metadata) về nguồn gốc để câu trả lời có thể trích dẫn lại. Độ tin cậy: 0.85 - Giữ cho ngữ cảnh (context) nhỏ gọn và có tính liên quan cao; việc truy xuất thêm nhiều văn bản thường làm cho câu trả lời tệ đi chứ không phải tốt hơn. Độ tin cậy: 0.80 - Cung cấp cho các tác nhân (agents) một danh sách ngắn gồm các công cụ chuyên biệt, được mô tả rõ ràng với lược đồ đầu vào nghiêm ngặt thay vì một công cụ ôm đồm làm mọi thứ. Độ tin cậy: 0.85 - Đặt các giới hạn cứng cho các vòng lặp của tác nhân: số bước tối đa, số lượng token tối đa và thời gian chờ tối đa theo đồng hồ thực tế (wall-clock timeout). Độ tin cậy: 0.90 - Yêu cầu xác nhận từ con người trước bất kỳ lệnh gọi công cụ nào thực hiện việc gửi đi, xóa bỏ, thanh toán hoặc thay đổi dữ liệu bên ngoài ứng dụng. Độ tin cậy: 0.90 - Coi tất cả các tài liệu được truy xuất, các trang web và kết quả trả về từ công cụ là đầu vào không đáng tin cậy có thể chứa mã độc tiêm nhiễm prompt (prompt injection). Độ tin cậy: 0.90 - Ghi nhật ký (log) mọi lệnh gọi mô hình kèm theo phiên bản prompt, tên mô hình, độ trễ, số lượng token và chi phí, đồng thời ẩn đi dữ liệu nhạy cảm của người dùng. Độ tin cậy: 0.85 - Lưu vào bộ nhớ đệm (cache) các lệnh gọi có tính xác định (deterministic) và sử dụng tính năng lưu đệm prompt (prompt caching) cho các prompt hệ thống dài và ít thay đổi. Độ tin cậy: 0.75 - Sử dụng mô hình nhỏ nhất vượt qua được các bài đánh giá (evals), và chỉ điều hướng những trường hợp khó sang mô hình đắt tiền. Độ tin cậy: 0.80 - Thử lại (retry) khi gặp giới hạn tần suất (rate limits) và lỗi tạm thời bằng thuật toán lùi lũy thừa có nhiễu ngẫu nhiên (exponential backoff and jitter), tuyệt đối không thử lại trong một vòng lặp khép kín khi gặp lỗi xác thực dữ liệu (validation failures). Độ tin cậy: 0.85 - Giữ tham số nhiệt độ (temperature) ở mức thấp đối với tác vụ trích xuất và phân loại, và chỉ đặt cao hơn đối với việc tạo nội dung sáng tạo. Độ tin cậy: 0.75 - Cho người dùng biết câu trả lời đến từ nguồn nào và giúp họ dễ dàng báo cáo một phản hồi chưa tốt. Độ tin cậy: 0.80 - Thu thập phản hồi thích (thumbs-up) và không thích (thumbs-down), sau đó đưa các trường hợp kết quả kém quay trở lại bộ dữ liệu đánh giá (eval set). Độ tin cậy: 0.85 - Đừng tinh chỉnh mô hình (fine-tune) cho đến khi kỹ thuật viết prompt, truy xuất thông tin và đánh giá đã thực sự chạm tới giới hạn của chúng. Độ tin cậy: 0.70
Các quy ước mang tính định hướng rõ ràng để phát hành các thay đổi một cách an toàn bằng feature flag và triển khai tăng dần theo giai đoạn.
# Triển Khai Feature Flag taste của một kỹ sư phát hành (release engineer) - Tách biệt việc triển khai (deploy) khỏi việc phát hành (release): đẩy mã nguồn ở trạng thái ẩn phía sau một cờ (flag) và bật nó lên một cách độc lập với quá trình triển khai. Độ tin cậy: 0.95 - Sử dụng dịch vụ quản lý cờ chuyên dụng hoặc SDK tương thích với OpenFeature thay vì tự viết các câu lệnh if thủ công dựa trên biến môi trường. Độ tin cậy: 0.80 - Gán cho mỗi cờ một người phụ trách (owner), phần mô tả, phân loại (release, experiment, ops hoặc permission) và ngày dự kiến gỡ bỏ. Độ tin cậy: 0.90 - Đặt tên các cờ theo hành vi mà chúng kiểm soát, chẳng hạn như checkout-new-payment-form, chứ không đặt theo mã ticket hay tên người. Độ tin cậy: 0.85 - Mặc định mọi cờ về hành vi hiện tại, an toàn khi không thể kết nối tới dịch vụ quản lý cờ. Độ tin cậy: 0.95 - Đánh giá giá trị của các cờ một lần duy nhất cho mỗi yêu cầu (request) ngay tại điểm đầu của luồng thực thi mã nguồn và truyền kết quả xuống dưới, không đánh giá sâu bên trong các vòng lặp. Độ tin cậy: 0.80 - Triển khai tăng dần từng bước: người dùng nội bộ trước, sau đó đến 1%, 10%, 50% và 100%, đồng thời theo dõi các chỉ số đo lường ở từng bước. Độ tin cậy: 0.90 - Sử dụng cơ chế phân nhóm cố định dựa trên hàm băm (sticky, hash-based bucketing) theo ID người dùng hoặc ID tài khoản ổn định để người dùng không bị nhảy qua lại giữa các biến thể khác nhau. Độ tin cậy: 0.90 - Xác định rõ các chỉ số đánh giá thành công và chỉ số bảo vệ (guardrail metrics) trước khi bắt đầu triển khai, chứ không phải sau khi nhìn vào bảng điều khiển (dashboard). Độ tin cậy: 0.85 - Kết nối cơ chế tự động hoàn tác (rollback) với các cảnh báo về tỷ lệ lỗi và độ trễ đối với những thay đổi tiềm ẩn nhiều rủi ro. Độ tin cậy: 0.75 - Giữ một công tắc ngắt khẩn cấp (kill switch) cho mọi phụ thuộc bên ngoài hoặc tác vụ tốn kém tài nguyên để bộ phận vận hành có thể tắt ngay mà không cần triển khai lại mã nguồn. Độ tin cậy: 0.90 - Gỡ bỏ các cờ phát hành (release flags) trong vòng vài tuần sau khi đạt mức 100% và cho phép quy trình CI báo lỗi thất bại khi một cờ đã quá hạn ngày gỡ bỏ. Độ tin cậy: 0.80 - Kiểm thử cả hai luồng bật (on) và tắt (off) trong CI đối với mọi cờ đang hoạt động. Độ tin cậy: 0.85 - Tránh lồng các cờ bên trong những cờ khác; nếu hai cờ có tương tác với nhau, hãy gộp chúng lại hoặc viết tài liệu ghi rõ các tổ hợp kết hợp. Độ tin cậy: 0.80 - Ghi nhật ký (log) kết quả đánh giá cờ kèm theo biến thể (variant) vào trong các dấu vết (traces) để khi xảy ra sự cố có thể truy vết về một đợt triển khai cụ thể. Độ tin cậy: 0.80 - Giới hạn quyền của những người có thể thay đổi các cờ trên môi trường production và lưu nhật ký kiểm toán (audit log) cho mọi thay đổi kèm theo lý do. Độ tin cậy: 0.85 - Tuyệt đối không sử dụng feature flag để thay thế cho các bước kiểm tra phân quyền (authorization checks). Độ tin cậy: 0.90 - Thông báo các đợt triển khai lớn trong một kênh chung kèm theo tên cờ, kế hoạch triển khai và các bước hoàn tác (rollback). Độ tin cậy: 0.70 - Giữ các cờ phân quyền (permission) và cờ vận hành (ops) tồn tại lâu dài trong một danh sách riêng biệt để chúng không bị nhầm lẫn với các cờ phát hành đã cũ. Độ tin cậy: 0.75 - Thực hiện một buổi đánh giá ngắn sau mỗi đợt triển khai lớn và ghi nhận lại những chỉ số nào đã thực sự phát hiện ra vấn đề. Độ tin cậy: 0.65
Các quy ước mang tính định kiến để xây dựng ứng dụng di động đa nền tảng bằng React Native và Expo.
# Ứng dụng React Native taste của một kỹ sư di động đa nền tảng - Bắt đầu các ứng dụng mới bằng Expo và quy trình prebuild thay vì một dự án React Native thuần (bare), và chỉ eject khi một mô-đun bản địa (native module) thực sự đòi hỏi điều đó. Độ tin cậy: 0.90 - Viết mọi thứ bằng TypeScript với chế độ nghiêm ngặt (strict mode) được bật, bao gồm cả các tham số điều hướng và các liên kết (bindings) mô-đun bản địa. Độ tin cậy: 0.95 - Sử dụng Expo Router để điều hướng dựa trên tệp và luôn bật định tuyến có kiểu dữ liệu (typed routes) để các liên kết bị hỏng sẽ báo lỗi ngay tại thời điểm biên dịch. Độ tin cậy: 0.80 - Lưu trạng thái máy chủ (server state) trong TanStack Query và giới hạn trạng thái máy khách toàn cục (global client state) trong một kho lưu trữ Zustand nhỏ gọn dành cho phiên đăng nhập và tùy chỉnh cá nhân. Độ tin cậy: 0.85 - Tuyệt đối không lưu trữ token trong AsyncStorage; hãy sử dụng expo-secure-store hoặc Keychain và Keystore cho bất kỳ thông tin nhạy cảm nào. Độ tin cậy: 0.95 - Kết xuất (render) các danh sách dài bằng FlashList hoặc một FlatList đã được tinh chỉnh kỹ với các khóa (keys) ổn định, tuyệt đối không dùng ScrollView kết hợp với hàm map. Độ tin cậy: 0.90 - Chạy các hiệu ứng chuyển động (animations) trên luồng UI bằng Reanimated và xử lý cử chỉ bằng react-native-gesture-handler, thay vì dùng Animated API cũ. Độ tin cậy: 0.85 - Bật Kiến trúc Mới (New Architecture) và Hermes theo mặc định, đồng thời coi những thư viện cản trở việc này là nợ kỹ thuật (tech debt). Độ tin cậy: 0.75 - Duy trì một mô-đun giao diện (theme) duy nhất cho màu sắc, khoảng cách và kiểu chữ, đồng thời hỗ trợ chế độ tối (dark mode) ngay từ màn hình đầu tiên. Độ tin cậy: 0.85 - Tuân thủ các vùng an toàn (safe areas) bằng react-native-safe-area-context thay vì gán cứng khoảng đệm (padding) cho phần tai thỏ (notches). Độ tin cậy: 0.90 - Đặt mã nguồn đặc thù cho từng nền tảng vào các tệp .ios.tsx và .android.tsx thay vì rải rác các câu lệnh kiểm tra Platform.OS. Độ tin cậy: 0.70 - Phát hành các bản vá lỗi JavaScript thông qua EAS Update kèm theo chính sách phiên bản runtime, và sử dụng các bản phát hành trên cửa hàng ứng dụng cho những thay đổi ở tầng native. Độ tin cậy: 0.80 - Dựng (build) và ký số (sign) trong CI bằng EAS Build để không có bản phát hành nào bị phụ thuộc vào một chiếc máy tính cá nhân đơn lẻ. Độ tin cậy: 0.85 - Kiểm thử các thành phần (components) bằng React Native Testing Library và truy vấn theo vai trò (role) cùng văn bản (text), thay vì ưu tiên tìm theo testID trước. Độ tin cậy: 0.80 - Bao phủ các luồng quan trọng từ đầu đến cuối (end-to-end) bằng Maestro hoặc Detox trên các trình giả lập thực tế trong CI. Độ tin cậy: 0.75 - Thêm accessibilityLabel và accessibilityRole vào mọi phần tử có thể chạm (touchable) và giữ kích thước vùng chạm tối thiểu là 44 điểm (points). Độ tin cậy: 0.90 - Xử lý trạng thái ngoại tuyến (offline) như một trạng thái bình thường: hiển thị dữ liệu đã lưu trong bộ nhớ đệm, xếp hàng các thao tác ghi và thử lại khi có kết nối trở lại. Độ tin cậy: 0.80 - Báo cáo các sự cố sập ứng dụng (crashes) và lỗi JavaScript bằng Sentry, bao gồm cả source maps cho mọi bản phát hành. Độ tin cậy: 0.85 - Tối ưu hóa hình ảnh bằng expo-image cùng cơ chế bộ nhớ đệm thay vì dùng thành phần Image mặc định. Độ tin cậy: 0.80 - Đo đạc hiệu năng (profile) bằng React Native DevTools trước khi phỏng đoán các cách khắc phục vấn đề hiệu năng. Độ tin cậy: 0.70 - Chỉ yêu cầu cấp quyền tại đúng thời điểm một tính năng cần đến chúng, kèm theo một màn hình giải thích ngắn gọn trước đó. Độ tin cậy: 0.90
Các quy ước mang tính định kiến để viết và duy trì tài liệu dành cho nhà phát triển luôn giữ được độ chính xác.
# Tài liệu Kỹ thuật taste của một người viết hướng đến nhà phát triển - Giữ tài liệu trong cùng kho lưu trữ (repository) với mã nguồn và xét duyệt (review) chúng trong cùng một pull request với thay đổi đó. Độ tin cậy: 0.95 - Viết bằng Markdown hoặc MDX và xây dựng trang tài liệu bằng trình tạo trang tĩnh như Docusaurus, Starlight hoặc MkDocs. Độ tin cậy: 0.85 - Tổ chức nội dung thành bài hướng dẫn từng bước (tutorials), hướng dẫn thực hiện (how-to guides), tài liệu tham chiếu (reference) và giải thích (explanation) theo mô hình Diataxis. Độ tin cậy: 0.85 - Mở đầu tệp README bằng một câu nêu rõ dự án làm gì, sau đó là hướng dẫn cài đặt và phần bắt đầu nhanh (quick start) hoạt động được ngay trong màn hình đầu tiên. Độ tin cậy: 0.95 - Đảm bảo mọi đoạn mã mẫu đều có thể sao chép-dán và chạy được ngay, bao gồm đầy đủ các câu lệnh import cùng những giá trị thực tế. Độ tin cậy: 0.95 - Kiểm thử các đoạn mã mẫu trong CI hoặc trích xuất chúng từ các tệp ví dụ đã được kiểm thử để chúng không bao giờ bị lỗi thời. Độ tin cậy: 0.80 - Tạo tài liệu tham chiếu API tự động từ mã nguồn, chẳng hạn như OpenAPI, TypeDoc hoặc docstrings, thay vì viết thủ công. Độ tin cậy: 0.90 - Viết ở ngôi thứ hai và dùng thể chủ động với các câu ngắn gọn cùng từ ngữ giản dị. Độ tin cậy: 0.90 - Sử dụng các tiêu đề dựa trên tác vụ như "Rotate an API key" (Xoay vòng khóa API) thay vì những tiêu đề mơ hồ như "Keys" (Khóa). Độ tin cậy: 0.85 - Nêu rõ các điều kiện tiên quyết và phiên bản ở ngay đầu mỗi bài hướng dẫn. Độ tin cậy: 0.85 - Hiển thị kết quả đầu ra kỳ vọng sau mỗi câu lệnh để người đọc biết rằng họ đang làm đúng hướng. Độ tin cậy: 0.80 - Kiểm tra văn phong bằng Vale cùng một bộ quy tắc hành văn chung, đồng thời kiểm tra các đường dẫn liên kết trong mỗi lần dựng (build). Độ tin cậy: 0.80 - Sử dụng các khối lưu ý (admonitions) như Note và Warning một cách tiết chế, chỉ dành cho những thông tin làm thay đổi hành động của người đọc. Độ tin cậy: 0.75 - Ưu tiên vẽ sơ đồ dưới dạng mã nguồn bằng Mermaid để chúng có thể được xét duyệt và cập nhật giống như văn bản. Độ tin cậy: 0.75 - Duy trì nhật ký thay đổi (changelog) theo định dạng Keep a Changelog và viết hướng dẫn chuyển đổi (migration guides) cho mọi thay đổi gây hỏng tương thích (breaking change). Độ tin cậy: 0.90 - Ghi lại các quyết định kiến trúc dưới dạng các bản ghi ADR ngắn gọn giải thích rõ bối cảnh và những sự đánh đổi, chứ không chỉ nêu mỗi lựa chọn cuối cùng. Độ tin cậy: 0.85 - Gán người phụ trách và ngày xét duyệt gần nhất cho mỗi trang, đồng thời gỡ bỏ những trang không còn đúng với thực tế. Độ tin cậy: 0.75 - Thêm ô tìm kiếm và các liên kết sâu (deep links) ổn định trỏ tới từng tiêu đề trước khi bổ sung bất kỳ tính năng nào khác cho trang web. Độ tin cậy: 0.80 - Viết văn bản thay thế (alt text) cho mọi ảnh chụp màn hình và tránh dùng ảnh chụp màn hình chứa văn bản vốn có thể trình bày dưới dạng khối mã lệnh. Độ tin cậy: 0.85 - Thu thập phản hồi bằng một tiện ích đơn giản kiểu "Trang này có hữu ích không?" và xem xét kết quả định kỳ hàng tháng. Độ tin cậy: 0.65 - Ghi lại tài liệu về các thông báo lỗi theo đúng nguyên văn để người dùng có thể tìm thấy cách khắc phục thông qua việc tìm kiếm. Độ tin cậy: 0.85
Các quy ước mang tính định kiến để chạy các thử nghiệm học máy có khả năng tái lập, từ mô hình cơ sở đến môi trường production.
# Thử nghiệm Học máy taste của một kỹ sư ML ứng dụng - Viết chỉ số đánh giá và một mô hình cơ sở (baseline) đơn giản trước khi huấn luyện bất kỳ mô hình thực tế nào. Độ tin cậy: 0.95 - Chia dữ liệu theo thời gian hoặc theo thực thể khi có khả năng xảy ra rò rỉ dữ liệu (data leakage), tuyệt đối không chia ngẫu nhiên theo mặc định. Độ tin cậy: 0.90 - Đóng băng tập kiểm thử (test set) từ sớm và chỉ xem nó khi đưa ra các quyết định cuối cùng, đồng thời sử dụng một tập xác thực (validation set) riêng biệt để tinh chỉnh. Độ tin cậy: 0.95 - Theo dõi mọi lần chạy bằng MLflow hoặc Weights & Biases, ghi nhật ký (log) cấu hình, các chỉ số, phiên bản mã nguồn và phiên bản tập dữ liệu. Độ tin cậy: 0.90 - Lưu cấu hình thử nghiệm trong các tệp YAML được quản lý phiên bản, nạp bằng Hydra hoặc Pydantic, thay vì dùng các hằng số gán cứng (hardcoded). Độ tin cậy: 0.80 - Thiết lập và ghi lại các hạt giống ngẫu nhiên (random seeds) cho Python, NumPy cùng framework đang dùng, đồng thời báo cáo phương sai qua nhiều seed khác nhau. Độ tin cậy: 0.85 - Quản lý phiên bản các tập dữ liệu bằng DVC hoặc các đường dẫn lưu trữ đối tượng bất biến (immutable object storage) để mọi kết quả đều có thể tái lập. Độ tin cậy: 0.80 - Tạo mẫu thử nghiệm (prototype) trong các notebook nhưng hãy chuyển mã nguồn có thể tái sử dụng vào một gói Python đã được kiểm thử ngay khi nó được dùng đến lần thứ hai. Độ tin cậy: 0.85 - Cố định các thư viện phụ thuộc bằng tệp khóa (lock file) và ghi nhận lại phiên bản CUDA cùng trình điều khiển (driver) trong mỗi lần chạy. Độ tin cậy: 0.75 - Kiểm tra thủ công các mẫu dữ liệu thô và các lỗi gán nhãn trước khi tin tưởng vào bất kỳ chỉ số tổng hợp nào. Độ tin cậy: 0.90 - Ưu tiên các mô hình cây tăng cường độ dốc (gradient boosted trees) cho dữ liệu dạng bảng và chỉ tìm đến học sâu (deep learning) khi chúng tỏ ra kém hiệu quả rõ rệt. Độ tin cậy: 0.80 - Sử dụng PyTorch với một vòng lặp huấn luyện gọn nhẹ hoặc dùng Lightning thay vì các framework tùy chỉnh cồng kềnh. Độ tin cậy: 0.70 - Báo cáo các chỉ số theo từng phân khúc (slice) chẳng hạn như khu vực, thiết bị hoặc lớp (class), chứ không chỉ dừng lại ở một con số tổng thể duy nhất. Độ tin cậy: 0.90 - Hiệu chuẩn (calibrate) các xác suất dự đoán khi mã nguồn ở khâu sau đưa ra quyết định dựa trên ngưỡng (threshold). Độ tin cậy: 0.75 - Tinh chỉnh siêu tham số (hyperparameters) bằng Optuna hoặc tìm kiếm ngẫu nhiên (random search) đơn giản trong phạm vi ngân sách tính toán cố định. Độ tin cậy: 0.75 - Viết kiểm thử đơn vị (unit tests) cho các bước biến đổi đặc trưng (feature transforms) và bộ nạp dữ liệu (data loaders), đồng thời kiểm tra xác nhận (assert) kích thước (shapes) cùng kiểu dữ liệu (dtypes) kỳ vọng. Độ tin cậy: 0.85 - Xác thực dữ liệu đầu vào bằng các bước kiểm tra lược đồ (schema) như Pandera hoặc Great Expectations trước khi huấn luyện. Độ tin cậy: 0.80 - Lưu mô hình cùng với luồng tiền xử lý (preprocessing pipeline) của nó thành một tạo phẩm (artifact) duy nhất để quá trình huấn luyện và phục vụ (serving) không bị lệch nhau. Độ tin cậy: 0.90 - So sánh mô hình mới với mô hình đang chạy trên môi trường production ở chế độ ngoại tuyến (offline) và sau đó thông qua triển khai chạy ngầm (shadow) hoặc thử nghiệm A/B. Độ tin cậy: 0.85 - Giám sát sự trôi dạt đặc trưng (feature drift) và phân phối dự đoán sau khi ra mắt, đồng thời cảnh báo khi có sự dịch chuyển lớn. Độ tin cậy: 0.80 - Viết một ghi chú thử nghiệm ngắn cho mỗi lần chạy quan trọng, nêu rõ giả thuyết, kết quả và bước tiếp theo. Độ tin cậy: 0.85 - Xóa hoặc lưu trữ các nhánh công việc thất bại thay vì để lại hàng tá các notebook làm dở dang. Độ tin cậy: 0.65
Giải thích từng dòng của tệp .gitignore, kiểm thử tệp đó với các đường dẫn bắt buộc phải bỏ qua hoặc bắt buộc phải giữ lại để có thể commit, chỉ rõ quy tắc quyết định cho từng đường dẫn, đồng thời cảnh báo các lỗi sai như quy tắc phủ định bị chặn bởi thư mục cha đã bị loại trừ, chú thích nội dòng (inline comment), khoảng trắng ở đầu dòng, dấu gạch chéo ngược (backslash), ký tự ** thừa, thông tin bí mật (secrets) chưa được bao phủ và tệp .env.example bị bỏ qua. Trả về một tệp đã được sửa lỗi. Bao gồm một trình kiểm tra viết bằng thư viện chuẩn Python có hỗ trợ --self-test, đối chiếu khớp với git thực tế.
---
name: gitignore-pattern-tester-and-reviewer
description: Giải thích từng dòng của tệp .gitignore, kiểm thử tệp đó với các đường dẫn bắt buộc phải bỏ qua hoặc bắt buộc phải giữ lại để có thể commit, chỉ rõ quy tắc quyết định cho từng đường dẫn, đồng thời cảnh báo các lỗi sai như quy tắc phủ định bị chặn bởi thư mục cha đã bị loại trừ, chú thích nội dòng (inline comment), khoảng trắng ở đầu dòng, dấu gạch chéo ngược (backslash), ký tự ** thừa, thông tin bí mật (secrets) chưa được bao phủ và tệp .env.example bị bỏ qua. Trả về một tệp đã được sửa lỗi. Bao gồm một trình kiểm tra viết bằng thư viện chuẩn Python có hỗ trợ --self-test, đối chiếu khớp với git thực tế.
---
# Công cụ Kiểm thử và Đánh giá Mẫu Gitignore
Bạn giúp các lập trình viên hiểu và sửa lỗi các tệp `.gitignore`. Bạn giải thích những gì mà mỗi mẫu (pattern) thực sự khớp, chứng minh điều đó bằng các bài kiểm thử đường dẫn, và tìm ra những sai lầm khiến git bỏ qua các tệp mã nguồn hoặc vô tình commit các thông tin bí mật (secrets) cùng kết quả biên dịch (build output). Bạn trả về một tệp đã được hiệu chỉnh kèm theo các bài kiểm thử vượt qua thành công, chứ không chỉ đưa ra lời khuyên suông.
## Các tệp trong kỹ năng này
- `scripts/gitignore_check.py` - giải thích từng quy tắc, chạy các bài kiểm thử "+ path" (bắt buộc phải bỏ qua) và "- path" (bắt buộc phải giữ lại), chỉ rõ dòng quyết định cho từng đường dẫn, và báo cáo các phát hiện ở mức HIGH, MEDIUM, LOW và INFO (chỉ dùng thư viện chuẩn Python 3)
- `references/gitignore-rules.md` - cách git đọc các tệp ignore: các nguồn và thứ tự ưu tiên, cú pháp dòng, neo đường dẫn (anchoring), ký tự đại diện (wildcards), quy tắc thư mục cha, các tệp đã được theo dõi (tracked files) và các lệnh để xác nhận
- `references/common-pitfalls.md` - mọi mã phát hiện kèm cách khắc phục, các bẫy thường gặp mà kịch bản không thể nhìn thấy (tên quá rộng, tệp đã được theo dõi, tệp lồng nhau, phân biệt chữ hoa chữ thường, tệp khóa lock files, sự khác biệt với .dockerignore) và các khối mẫu khởi đầu tốt
- `templates/gitignore-review.md` - bản đánh giá để bàn giao lại cho người dùng
- `examples/webapp.gitignore` - một tệp ignore của ứng dụng web chứa sáu lỗi thực tế
- `examples/webapp-fixed.gitignore` - tệp đã được sửa lỗi (tất cả các bài kiểm thử đều vượt qua)
- `examples/webapp-paths.txt` - 19 bài kiểm thử đường dẫn được sử dụng với cả hai tệp trên
## Quy trình làm việc
### 1. Thu thập dữ liệu đầu vào
Yêu cầu cung cấp tệp `.gitignore` (và bất kỳ tệp `.gitignore` lồng nhau nào), ngăn xếp công nghệ (stack), và các đường dẫn quan trọng: những gì bắt buộc phải bỏ qua (kết quả biên dịch, các gói phụ thuộc, nhật ký log, thông tin bí mật) và những gì bắt buộc phải giữ lại để có thể commit (các thư mục mã nguồn có tên phổ biến như `build/` hoặc `lib/`, các tệp `.gitkeep`, `.env.example`, các tệp khóa lock files). Nếu người dùng chỉ hỏi "tại sao X lại bị bỏ qua?", hãy hỏi đường dẫn và sử dụng kết quả đầu ra của lệnh `git check-ignore -v X` nếu họ có sẵn.
### 2. Chạy trình kiểm tra
```
python3 scripts/gitignore_check.py examples/webapp.gitignore --test examples/webapp-paths.txt
python3 scripts/gitignore_check.py .gitignore --test paths.txt --json
python3 scripts/gitignore_check.py --self-test
```
Tệp đường dẫn (Paths file): `+ path` là bắt buộc phải bỏ qua, `- path` là bắt buộc phải giữ lại, kết thúc tên thư mục bằng dấu `/`. Mã thoát (Exit) 1 có nghĩa là có phát hiện mức HIGH hoặc có bài kiểm thử thất bại, mã thoát 2 có nghĩa là dữ liệu đầu vào không thể đọc được. Trình kiểm tra mô hình hóa một tệp ignore ở thư mục gốc của kho lưu trữ (repo root); đối với tệp lồng bên trong, hãy cung cấp các đường dẫn tương đối tính từ thư mục chứa tệp đó.
### 3. Giải thích và sửa lỗi
- Đi qua bảng Quy tắc (Rules table) bằng ngôn từ dễ hiểu, đặc biệt là quy tắc neo đường dẫn (dấu gạch chéo ở đầu hoặc ở giữa = tương đối so với thư mục chứa `.gitignore`) và quy tắc thư mục cha (xem `references/gitignore-rules.md`).
- Đối với mỗi trường hợp FAIL, hãy trích dẫn dòng quyết định từ cột "Decided by" và đưa ra thay đổi nhỏ nhất để khắc phục nó.
- Sửa mọi phát hiện ở mức HIGH và MEDIUM; giải thích bất kỳ phát hiện mức INFO nào mà bạn giữ nguyên.
- Thêm các bài kiểm thử `- path` cho các thư mục mã nguồn có tên phổ biến để làm lộ diện các quy tắc quá rộng.
- Chạy lại trình kiểm tra trên tệp đã sửa cho đến khi tất cả các bài kiểm thử đều vượt qua, và hiển thị phần khác biệt (diff).
### 4. Bàn giao kết quả
Điền vào mẫu `templates/gitignore-review.md`: tệp hiện tại đang làm gì, bảng kiểm thử, các phát hiện, toàn bộ tệp đã sửa lỗi, bản so sánh khác biệt (diff), và các câu lệnh để xác nhận (`git check-ignore -v`, `git status --ignored`) cũng như để bỏ theo dõi (untrack) các tệp đã lỡ được commit trước đó (`git rm --cached`).
## Kết quả ví dụ
Đối với `examples/webapp.gitignore` chạy cùng `examples/webapp-paths.txt`: 13/19 bài kiểm thử vượt qua, 1 phát hiện mức HIGH, 4 mức MEDIUM, 2 mức LOW, 1 mức INFO. Mức HIGH NEGATION_BLOCKED: `logs/` loại trừ cả thư mục, do đó `!logs/.gitkeep` không bao giờ có thể được áp dụng (cách sửa: `logs/*`). Mức MEDIUM: `config/secrets.yml # never commit` bị hiểu là một mẫu duy nhất, vì vậy tệp chứa thông tin bí mật này sẽ bị commit; ` Thumbs.db` có một khoảng trắng ở đầu dòng; `src\generated` bị đọc thành `srcgenerated`; và các tệp `*.key`, `*.pem` cùng `id_rsa` chưa được bao phủ. Mức LOW: `.env*` làm ẩn mất `.env.example`; `*.log` bị liệt kê hai lần. Mẫu `build` không được neo cũng làm ẩn luôn `src/build/README.md` (cách sửa: `/build/`). Tệp `examples/webapp-fixed.gitignore` vượt qua 19/19 bài kiểm thử mà không có bất kỳ phát hiện nào ở mức HIGH, MEDIUM hay LOW.
## Quy tắc
- Dòng khớp cuối cùng sẽ chiến thắng; một tệp nằm bên trong một thư mục đã bị loại trừ thì không bao giờ có thể được đưa vào lại (re-included). Hãy luôn nhắc lại điều này bất cứ khi nào có liên quan đến một dòng bắt đầu bằng `!`.
- Tuyệt đối không đề xuất bỏ qua các tệp khóa (`package-lock.json`, `poetry.lock`, `go.sum`, `Cargo.lock` của ứng dụng) hoặc cấu hình dùng chung mà nhóm chủ đích commit.
- Nếu một thông tin bí mật (secret) đã lỡ bị commit, hãy nói rõ rằng việc thêm nó vào ignore là chưa đủ: phải xoay vòng (rotate) khóa đó và cân nhắc viết lại lịch sử git.
- Ưu tiên các mẫu được neo ở thư mục gốc (`/dist/`, `/build/`) cho các thư mục đầu ra, và đặt các tệp cá nhân của trình soạn thảo cũng như hệ điều hành vào tệp ignore toàn cục (global ignore file) khi cả nhóm đồng thuận.
- Xác nhận trên kho lưu trữ thực tế bằng lệnh `git check-ignore -v`: kịch bản tuân theo các quy tắc trong tài liệu của git cho một tệp đơn lẻ, nhưng các tệp lồng nhau, `.git/info/exclude`, tệp toàn cục và thiết lập `core.ignorecase` có thể làm thay đổi kết quả.
FILE:references/gitignore-rules.md
# Cách git đọc tệp .gitignore
Nguồn: tài liệu hướng dẫn gitignore(5) (`git help gitignore`). Trình kiểm tra triển khai các quy tắc này và đã được đối chiếu với git 2.47 thực tế trên 6,956 lượt kiểm tra đường dẫn mà không có bất kỳ sự khác biệt nào.
## Nguồn gốc của các quy tắc (ưu tiên từ cao nhất trở xuống)
1. Các tùy chọn `--exclude` trên dòng lệnh (hiếm gặp; được sử dụng bởi một số công cụ).
2. Các tệp `.gitignore`: tệp nằm trong cùng thư mục với đường dẫn đang xét sẽ ưu tiên hơn các tệp nằm ở thư mục cấp trên. Các mẫu (patterns) có tính tương đối so với thư mục chứa tệp `.gitignore` đó.
3. `.git/info/exclude` - cục bộ trên một bản sao (clone) duy nhất, không bao giờ được commit. Rất phù hợp cho các tệp nháp cá nhân.
4. Tệp toàn cục trong `core.excludesFile` (mặc định là `~/.config/git/ignore`) - rất phù hợp cho các tệp của trình soạn thảo và hệ điều hành như `.DS_Store` và `*.swp`.
Bên trong một tệp, dòng khớp CUỐI CÙNG sẽ quyết định kết quả.
## Cú pháp dòng
| Dòng | Ý nghĩa |
|---|---|
| dòng trống | được bỏ qua, dùng làm khoảng phân cách |
| `# text` | chú thích (comment); chỉ có tác dụng khi nằm ngay đầu dòng. `foo # bar` được hiểu là mẫu có tên `foo # bar` |
| `\#file` | một tên tệp bắt đầu bằng ký tự `#` |
| `!pattern` | đưa trở lại (re-include) một đối tượng đã bị loại trừ bởi một dòng trước đó |
| `\!file` | một tên tệp bắt đầu bằng ký tự `!` |
| `name ` | các khoảng trắng ở cuối dòng sẽ bị loại bỏ trừ khi được thoát bằng dấu gạch chéo ngược (`name\ `) |
| ` name` | các khoảng trắng ở đầu dòng ĐƯỢC GIỮ LẠI và trở thành một phần của tên |
| `\` | thoát (escape) ký tự kế tiếp; nó không bao giờ được dùng làm dấu phân cách đường dẫn |
## Dấu gạch chéo (slash) quyết định vị trí mà mẫu sẽ khớp
| Mẫu | Khớp với | Không khớp với |
|---|---|---|
| `build` | `build`, `src/build`, `a/b/build/x.js` (bất kỳ tệp hoặc thư mục nào có tên là build) | `build.sh` |
| `build/` | các thư mục có tên là build ở bất kỳ độ sâu nào | một TỆP (FILE) có tên là build |
| `/build` | chỉ khớp với `build` nằm ngay cạnh tệp .gitignore | `src/build` |
| `doc/frotz` | `doc/frotz` nằm ngay cạnh tệp .gitignore (dấu gạch chéo ở giữa có tác dụng neo giống như dấu gạch chéo ở đầu) | `a/doc/frotz` |
| `**/doc/frotz` | `doc/frotz` ở bất kỳ độ sâu nào | |
Quy tắc nằm lòng: không có dấu gạch chéo (hoặc chỉ có một dấu gạch chéo ở cuối) = khớp ở bất kỳ độ sâu nào; có dấu gạch chéo ở đầu hoặc ở giữa = tương đối so với thư mục chứa tệp .gitignore.
## Ký tự đại diện (Wildcards)
| Ký tự đại diện | Ý nghĩa |
|---|---|
| `*` | bất kỳ chuỗi ký tự nào ngoại trừ `/` (`*.log`, `/a*z` không khớp với `ab/cz`) |
| `?` | chính xác một ký tự bất kỳ ngoại trừ `/` |
| `[abc]`, `[a-z]`, `[!0-9]`, `[^0-9]` | một ký tự nằm trong (hoặc không nằm trong) tập hợp; các lớp ký tự kiểu `[[:digit:]]` đều hoạt động |
| `**/name` | `name` nằm trong bất kỳ thư mục nào, bao gồm cả thư mục gốc trên cùng |
| `dir/**` | mọi thứ nằm bên trong `dir`, nhưng không bao gồm chính bản thân `dir` |
| `a/**/b` | `a/b`, `a/x/b`, `a/x/y/b` |
| `foo**`, `**bar` | `**` không nằm cạnh dấu gạch chéo sẽ hoạt động giống như một dấu `*` đơn lẻ |
## Quy tắc thư mục cha (điều gây bất ngờ phổ biến nhất)
"Không thể đưa một tệp vào lại (re-include) nếu thư mục cha của tệp đó đã bị loại trừ." Git thậm chí còn không liệt kê nội dung bên trong một thư mục đã bị loại trừ, vì vậy một dòng `!` phía sau sẽ không bao giờ có cơ hội được xét tới.
```gitignore
# KHÔNG hoạt động: logs/ đã bị loại trừ dưới dạng cả một thư mục
logs/
!logs/.gitkeep
# hoạt động: loại trừ nội dung bên trong thư mục, giữ cho bản thân thư mục vẫn hiển thị
logs/*
!logs/.gitkeep
# hoạt động đối với các tệp nằm sâu hơn: đưa vào lại (re-include) từng cấp thư mục
/assets/*
!/assets/icons/
/assets/icons/*
!/assets/icons/app.svg
```
## Phong cách danh sách trắng (Whitelist style)
Bỏ qua mọi thứ, sau đó chỉ cho phép những gì bạn muốn:
```gitignore
*
!*/
!*.md
!src/**
```
`!*/` đưa mọi thư mục vào lại để các dòng `!` phía sau có thể chạm tới các tệp nằm bên trong chúng.
## Các tệp đã được theo dõi (Already tracked files)
.gitignore chỉ ảnh hưởng đến các tệp chưa được theo dõi (untracked files). Một tệp đã được commit sẽ vẫn tiếp tục được theo dõi:
```
git rm --cached path/to/file # giữ tệp trên ổ đĩa, ngừng theo dõi nó
git rm -r --cached build/ # tương tự đối với một thư mục
```
Nếu đó là một thông tin bí mật (secret), việc xóa nó khỏi danh sách theo dõi là chưa đủ: hãy xoay vòng (rotate) khóa bí mật đó, bởi vì nó vẫn còn nằm trong lịch sử git.
## Các câu lệnh để xác nhận
```
git check-ignore -v path/to/file # in ra tệp, dòng và mẫu đã bỏ qua đường dẫn đó (không in gì = không bị bỏ qua)
git check-ignore -v --no-index path # dùng được cả cho các tệp đang được theo dõi (tracked)
git status --ignored --short # liệt kê các tệp bị bỏ qua với ký hiệu "!!"
git ls-files --others --ignored --exclude-standard # mọi tệp chưa được theo dõi đang bị bỏ qua
```
FILE:references/common-pitfalls.md
# Các bẫy .gitignore thường gặp và mã lỗi của trình kiểm tra
| Mã | Mức độ nghiêm trọng | Vấn đề xảy ra | Cách khắc phục |
|---|---|---|---|
| NEGATION_BLOCKED | HIGH | `!path/file` nằm sau một quy tắc loại trừ thư mục cha (`logs/`, `dist`, `a/*` đối với `a/b/c`) sẽ không bao giờ có hiệu lực, khiến tệp bạn định commit vẫn bị bỏ qua | Loại trừ nội dung bên trong (`logs/*` hoặc `logs/**`) thay vì cả thư mục, hoặc đưa vào lại từng cấp thư mục (`!a/b/`) |
| SECRETS_NOT_COVERED | MEDIUM | các tệp bí mật điển hình (`.env`, `*.pem`, `*.key`, `id_rsa`) sẽ bị commit khi chạy `git add .` | Thêm `.env`, `.env.*`, `*.pem`, `*.key`, `id_rsa*`; đồng thời sử dụng trình quét thông tin bí mật (secret scanner) trong CI |
| INLINE_COMMENT | MEDIUM | `secrets.yml # never commit` được hiểu là một mẫu duy nhất bao gồm cả phần văn bản chú thích, do đó `secrets.yml` KHÔNG hề bị bỏ qua | Đặt các chú thích trên dòng riêng của chúng |
| LEADING_SPACE | MEDIUM | ` Thumbs.db` chỉ khớp với một cái tên bắt đầu bằng dấu cách | Xóa dấu cách đó đi |
| BACKSLASH_SEPARATOR | MEDIUM | `src\generated` có nghĩa là "src" + ký tự "g" được thoát + "enerated" = `srcgenerated` | Luôn sử dụng `/`, kể cả trên Windows |
| ENV_EXAMPLE_IGNORED | LOW | `.env*` cũng làm ẩn luôn `.env.example`, tệp mẫu mà các lập trình viên khác cần dùng | Thêm `!.env.example` ở dòng phía sau nó |
| DUPLICATE | LOW | cùng một dòng xuất hiện hai lần; vô hại nhưng là dấu hiệu của việc gộp mã (merge) bằng cách sao chép-dán | Xóa bớt một dòng |
| DOUBLE_STAR | LOW | `foo**` thực chất chỉ là `foo*`; rất có thể tác giả định viết là `foo/**` | Chỉ sử dụng `**` dưới dạng `**/x`, `x/**` hoặc `a/**/b` |
| TRAILING_SPACE | LOW | các khoảng trắng ở cuối dòng bị âm thầm lược bỏ; nếu đó là chủ ý thì tệp sẽ không khớp | Xóa hoặc thoát (escape) chúng |
| CRLF | LOW | ký tự xuống dòng kiểu Windows trong tệp .gitignore | Lưu dưới dạng LF; thêm `* text=auto` vào tệp .gitattributes |
| ANCHORED_BY_SLASH | INFO | `src/gen` chỉ khớp ngay cạnh tệp .gitignore, chứ không khớp với `packages/a/src/gen` | Thêm `/` ở đầu để thể hiện rõ chủ ý hoặc thêm `**/` để khớp ở bất kỳ độ sâu nào |
## Các bẫy mà kịch bản không thể nhìn thấy (hãy kiểm tra thủ công)
- **Các tên không được neo nhưng quá rộng.** `build` cũng làm ẩn cả `src/build/` và `docs/build/`; `lib` làm ẩn mọi thư mục `lib` bao gồm cả mã nguồn. Hãy sử dụng `/build/` cho thư mục đầu ra ở cấp gốc. Hãy viết một bài kiểm thử `- path` cho bất kỳ thư mục mã nguồn nào có tên phổ biến.
- **Các tệp đã được theo dõi (Tracked files).** Việc thêm một quy tắc không làm bỏ theo dõi (untrack) các tệp đã được commit trước đó. Hãy chạy `git ls-files -ci --exclude-standard` để liệt kê các tệp đang được theo dõi nhưng khớp với quy tắc ignore, sau đó chạy `git rm --cached`.
- **Các tệp .gitignore lồng nhau.** Một tệp `.gitignore` nằm trong thư mục con sẽ thêm các quy tắc tương đối so với thư mục con đó và có thể đưa vào lại những gì mà tệp ở thư mục gốc đã loại trừ (trừ khi một thư mục cha đã bị loại trừ). Hãy chạy trình kiểm tra trên từng tệp với các đường dẫn tương đối so với thư mục của nó, hoặc xác nhận bằng lệnh `git check-ignore -v`.
- **Phân biệt chữ hoa chữ thường (Case sensitivity).** Trên macOS và Windows, thiết lập `core.ignorecase=true` khiến `*.JPG` khớp với cả `photo.jpg`; nhưng trên môi trường Linux CI thì không. Hãy viết cả hai dạng chữ hoa/thường hoặc sử dụng `*.[jJ][pP][gG]`.
- **Các tệp cá nhân nằm trong tệp dùng chung.** `.idea/`, `.vscode/`, `*.swp`, `.DS_Store` thuộc về tệp ignore toàn cục (`core.excludesFile`), mặc dù nhiều nhóm vẫn giữ chúng trong tệp của kho lưu trữ (repo) để tiện lợi. Đừng bỏ qua `.vscode/settings.json` nếu nhóm chủ đích chia sẻ tệp này.
- **Các tệp khóa (Lock files).** Đừng bỏ qua `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `poetry.lock`, `Cargo.lock` (đối với các ứng dụng) hoặc `go.sum`: chúng giúp quá trình biên dịch (build) có thể tái lập chính xác.
- **Đã thêm vào ignore nhưng vẫn ép commit.** `git add -f` sẽ ghi đè lên các quy tắc ignore; người đánh giá mã (reviewer) nên kiểm tra xem có kết quả biên dịch hoặc thông tin bí mật nào bị ép thêm vào hay không.
- **Các tệp ignore khác sử dụng quy tắc khác.** `.dockerignore` sử dụng `filepath.Match` của Go cộng với `**` và luôn được neo tại thư mục gốc của ngữ cảnh biên dịch (build context root - không có khái niệm "bất kỳ độ sâu nào" cho các tên thông thường). `.npmignore`, `.eslintignore` và `.prettierignore` tuân theo các quy tắc tương tự gitignore nhưng không phải lúc nào cũng giống ở mọi chi tiết. Hãy kiểm thử chúng bằng công cụ riêng của chúng.
## Các khối mẫu khởi đầu tốt
```gitignore
# Node
node_modules/
/dist/
/build/
npm-debug.log*
.npm/
# Python
__pycache__/
*.py[cod]
.venv/
.pytest_cache/
/dist/
*.egg-info/
# Secrets
.env
.env.*
!.env.example
*.pem
*.key
id_rsa*
# Logs that keep their folder
logs/*
!logs/.gitkeep
```
FILE:templates/gitignore-review.md
# Đánh giá .gitignore: <kho lưu trữ hoặc thư mục>
**Ngăn xếp (Stack):** <Node 20 + Vite / Python 3.12 / monorepo với pnpm workspaces>
**Các tệp đã đánh giá:** <.gitignore tại thư mục gốc, packages/api/.gitignore>
**Tóm tắt:** <một câu: vấn đề quan trọng nhất và bản sửa lỗi sẽ thay đổi điều gì>
## Tệp hiện tại đang làm gì
| Dòng | Mẫu (Pattern) | Ý nghĩa |
|---|---|---|
| <9> | <`logs/`> | <bỏ qua mọi thư mục có tên là logs, ở bất kỳ độ sâu nào, và mọi thứ bên trong nó> |
## Kiểm thử đường dẫn (Path tests)
| Kết quả | Đường dẫn | Kỳ vọng | Thực tế | Quyết định bởi |
|---|---|---|---|---|
| <FAIL> | <`logs/.gitkeep`> | <giữ lại (kept)> | <bỏ qua (ignored)> | <thư mục cha logs/ bị loại trừ bởi L9; L10 không thể đưa nó vào lại> |
## Các phát hiện (Findings)
| # | Mức độ nghiêm trọng | Mã | Dòng | Vấn đề | Cách khắc phục |
|---|---|---|---|---|---|
| 1 | <HIGH> | <NEGATION_BLOCKED> | <10> | <...> | <sử dụng logs/* thay vì logs/> |
## Tệp .gitignore đề xuất
```gitignore
<toàn bộ tệp đã sửa lỗi, với các chú thích nằm trên dòng riêng của chúng>
```
## Các thay đổi
```diff
- logs/
+ logs/*
```
## Áp dụng và xác nhận
1. Thay thế tệp, sau đó chạy: `git check-ignore -v <từng đường dẫn từ bài kiểm thử>`
2. Chạy `git status --ignored --short` và kiểm tra xem không có tệp quan trọng nào hiển thị dưới dạng `!!`
3. Bỏ theo dõi (untrack) các tệp hiện đã được bỏ qua nhưng từng bị commit trước đó: `git rm --cached <path>` (xoay vòng bất kỳ khóa bí mật nào đã từng bị commit)
4. Commit: `chore: fix .gitignore (<lý do ngắn gọn>)`
## Các việc cần làm tiếp theo (Follow-ups)
- <chuyển các tệp của trình soạn thảo sang tệp ignore toàn cục>
- <thêm trình quét thông tin bí mật (gitleaks, trufflehog) vào CI>
FILE:examples/webapp.gitignore
# Dependencies
node_modules
/vendor/
# Build output
dist/
build
*.log
logs/
!logs/.gitkeep
# Environment
.env*
config/secrets.yml # never commit
# OS and editor files
.DS_Store
.idea/
*.swp
Thumbs.db
# Test coverage
coverage/**
!coverage/README.md
# Generated code
src\generated
*.log
FILE:examples/webapp-fixed.gitignore
# Dependencies
node_modules/
/vendor/
# Build output (root only, so src/build/ stays tracked)
/dist/
/build/
*.log
# ignore the contents of logs/, not the folder, so the negation can work
logs/*
!logs/.gitkeep
# Environment and secrets
.env
.env.*
!.env.example
*.pem
*.key
id_rsa*
# never commit
config/secrets.yml
# OS and editor files
.DS_Store
Thumbs.db
.idea/
*.swp
# Test coverage
coverage/**
!coverage/README.md
# Generated code
/src/generated/
FILE:examples/webapp-paths.txt
# Paths for examples/webapp.gitignore and webapp-fixed.gitignore
# "+ path" = must be ignored, "- path" = must stay committable
+ node_modules/react/index.js
+ packages/api/node_modules/lodash/lodash.js
+ vendor/autoload.php
- packages/vendor/README.md
+ dist/app.js
+ build/output.js
- src/build/README.md
+ logs/app.log
- logs/.gitkeep
+ .env
+ .env.local
- .env.example
+ config/secrets.yml
+ .DS_Store
+ Thumbs.db
+ coverage/lcov.info
- coverage/README.md
+ src/generated/types.ts
- src/index.ts
FILE:scripts/gitignore_check.py
#!/usr/bin/env python3
"""Gitignore pattern tester and reviewer (Python 3 standard library only).
Explains every rule in a .gitignore file, tests the file against paths that must be
ignored ("+ path") or must stay committable ("- path"), names the rule that decides
each path, and flags common mistakes: negations that cannot work because a parent
directory is excluded, inline comments, leading spaces, Windows backslashes, stray
"**", duplicates, secrets that are not covered and .env.example being ignored.
Matching follows git's documented gitignore rules for a single .gitignore at the
repository root: last matching rule wins, "!" re-includes, a trailing "/" matches
directories only, a "/" at the start or in the middle anchors the pattern to the
.gitignore's directory, "*" "?" "[...]" never match "/", "**" spans directories,
and a file can never be re-included if a parent directory is excluded.
Usage:
python3 gitignore_check.py .gitignore [--test paths.txt] [--json]
python3 gitignore_check.py --self-test
Paths file: one path per line, "+ path" = must be ignored, "- path" = must NOT be
ignored, end a path with "/" if it is a directory, "#" starts a comment.
Exit codes: 0 = no HIGH findings and all tests pass, 1 = HIGH finding or failed test,
2 = unreadable input.
"""
import argparse, json, re, sys
SEV_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "INFO": 3}
SECRET_PROBES = [".env", "config/.env.production", "certs/server.key", "deploy/id_rsa",
"secrets/service-account.pem"]
POSIX_CLASSES = {"alnum": "a-zA-Z0-9", "alpha": "a-zA-Z", "digit": "0-9", "lower": "a-z",
"upper": "A-Z", "space": " \\t\\n\\r\\f\\v", "xdigit": "0-9A-Fa-f",
"punct": re.escape("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"), "blank": " \\t"}
class Rule:
def __init__(self, line_no, raw, negate, dir_only, anchored, body, regex):
self.line_no, self.raw, self.negate, self.dir_only = line_no, raw, negate, dir_only
self.anchored, self.body, self.regex = anchored, body, regex
def matches(self, rel_path, is_dir):
if self.dir_only and not is_dir:
return False
target = rel_path if self.anchored else rel_path.rsplit("/", 1)[-1]
return self.regex.fullmatch(target) is not None
def label(self):
return f"L{self.line_no} `{self.raw}`"
def strip_trailing_spaces(s):
"""git drops trailing spaces unless the last one is escaped with a backslash."""
end = len(s)
while end > 0 and s[end - 1] == " ":
bs, k = 0, end - 2
while k >= 0 and s[k] == "\\":
bs += 1; k -= 1
if bs % 2 == 1:
break
end -= 1
return s[:end]
def class_to_regex(body):
out, m = "", 0
while m < len(body):
ch = body[m]
if ch == "[" and body.startswith("[:", m):
end = body.find(":]", m + 2)
if end != -1 and body[m + 2:end] in POSIX_CLASSES:
out += POSIX_CLASSES[body[m + 2:end]]; m = end + 2; continue
if ch == "\\" and m + 1 < len(body):
out += re.escape(body[m + 1]); m += 2; continue
if ch == "-" and 0 < m < len(body) - 1:
out += "-"; m += 1; continue
out += re.escape(ch); m += 1
return out
def glob_to_regex(p):
out, i, n = "", 0, len(p)
while i < n:
c = p[i]
if c == "\\":
if i + 1 < n:
out += re.escape(p[i + 1]); i += 2
else:
i += 1 # a lone trailing backslash matches nothing extra
continue
if c == "*":
j = i
while j < n and p[j] == "*":
j += 1
if j - i >= 2 and (i == 0 or p[i - 1] == "/") and (j == n or p[j] == "/"):
if j == n:
out += ".*"; i = j
else:
out += "(?:.*/)?"; i = j + 1
continue
out += "[^/]*"; i = j; continue
if c == "?":
out += "[^/]"; i += 1; continue
if c == "[":
j = i + 1
neg = j < n and p[j] in "!^"
if neg:
j += 1
k = j + 1 if j < n and p[j] == "]" else j
while k < n and p[k] != "]":
if p[k] == "[" and p.startswith("[:", k):
end = p.find(":]", k + 2)
if end != -1:
k = end + 2; continue
k += 2 if p[k] == "\\" else 1
if k >= n:
out += re.escape("["); i += 1; continue
cls = class_to_regex(p[j:k])
out += ("[^/" + cls + "]") if neg else ("(?!/)[" + cls + "]")
i = k + 1; continue
out += re.escape(c); i += 1
return out
def parse_rules(text):
rules, findings, seen = [], [], {}
for no, raw in enumerate(text.split("\n"), 1):
if raw.endswith("\r"):
findings.append(("LOW", "CRLF", no, "line ends with CR (Windows line ending)",
"Save the file with LF line endings; set `* text=auto` in .gitattributes."))
raw = raw[:-1]
if not raw.strip() or raw.startswith("#"):
continue
line = strip_trailing_spaces(raw)
if line != raw:
findings.append(("LOW", "TRAILING_SPACE", no, f"`{raw}` has trailing spaces, which git drops",
"Delete them; escape a real trailing space as `\\ `."))
if raw[0] in " \t":
findings.append(("MEDIUM", "LEADING_SPACE", no, f"`{raw}` starts with whitespace, which git keeps as part of the name, so it matches nothing useful",
"Remove the leading whitespace."))
m = re.search(r"\s#", line)
if m:
findings.append(("MEDIUM", "INLINE_COMMENT", no, f"`{line}`: git has no inline comments, so `{line[m.start():].strip()}` becomes part of the pattern",
"Move the comment to its own line starting with #."))
if re.search(r"\\[A-Za-z0-9_]", line):
findings.append(("MEDIUM", "BACKSLASH_SEPARATOR", no, f"`{line}` uses a backslash, which escapes the next character instead of separating directories",
"Use forward slashes: `" + line.replace("\\", "/") + "`."))
key = line.strip()
if key in seen:
findings.append(("LOW", "DUPLICATE", no, f"`{key}` repeats line {seen[key]}", "Delete the duplicate."))
else:
seen[key] = no
negate = line.startswith("!")
body = line[1:] if negate else line
if body.startswith("\\!") or body.startswith("\\#"):
body = body[1:]
if negate and not body:
findings.append(("LOW", "EMPTY_NEGATION", no, "`!` alone does nothing", "Delete the line."))
continue
dir_only = body.endswith("/") and not body.endswith("\\/")
if dir_only:
body = body.rstrip("/")
if not body:
continue
anchored = "/" in body
if body.startswith("/"):
body = body[1:]
if any((m.start() > 0 and body[m.start() - 1] != "/") or (m.end() < len(body) and body[m.end()] != "/")
for m in re.finditer(r"\*{2,}", body)):
findings.append(("LOW", "DOUBLE_STAR", no, f"`{line}`: `**` not between slashes acts like a single `*`",
"Use `**/name`, `dir/**` or `a/**/b`, or a single `*`."))
rules.append(Rule(no, line, negate, dir_only, anchored, body, re.compile(glob_to_regex(body), re.S)))
return rules, findings
def last_match(rules, rel_path, is_dir):
hit = None
for r in rules:
if r.matches(rel_path, is_dir):
hit = r
return hit
def check_path(rules, path):
"""Return (ignored, reason, blocked_rule) following git's parent-directory rule."""
is_dir = path.endswith("/")
parts = [p for p in path.strip("/").split("/") if p]
for k in range(1, len(parts) + 1):
sub = "/".join(parts[:k])
sub_is_dir = k < len(parts) or is_dir
hit = last_match(rules, sub, sub_is_dir)
if k < len(parts):
if hit and not hit.negate:
own = last_match(rules, "/".join(parts), is_dir)
blocked = own if own and own.negate else None
return True, f"parent `{sub}/` excluded by {hit.label()}", blocked
else:
if hit is None:
return False, "no rule matches", None
if hit.negate:
return False, f"re-included by {hit.label()}", None
return True, f"ignored by {hit.label()}", None
return False, "empty path", None
def literal_dirs(body):
parts = body.split("/")[:-1]
out = []
for p in parts:
if any(ch in p for ch in "*?[\\"):
break
out.append(p)
return out
def lint_negations(rules):
findings = []
for idx, r in enumerate(rules):
if not r.negate or not r.anchored:
continue
before = rules[:idx]
dirs = literal_dirs(r.body)
for k in range(1, len(dirs) + 1):
sub = "/".join(dirs[:k])
hit = last_match(before, sub, True)
if hit and not hit.negate:
findings.append(("HIGH", "NEGATION_BLOCKED", r.line_no,
f"`{r.raw.strip()}` can never apply: its parent directory `{sub}/` is excluded by {hit.label()} and git does not look inside excluded directories",
f"Exclude the contents instead of the directory (`{sub}/*` or `{sub}/**`), then keep the `!` rule; or re-include `!{sub}/` first."))
break
return findings
def lint_secrets(rules):
findings = []
missing = [p for p in SECRET_PROBES if not check_path(rules, p)[0]]
if missing:
findings.append(("MEDIUM", "SECRETS_NOT_COVERED", 0, "these typical secret files would NOT be ignored: " + ", ".join(missing),
"Add `.env`, `.env.*` (with `!.env.example`), `*.pem`, `*.key`, `id_rsa*` if the project could ever contain them."))
if check_path(rules, ".env.example")[0]:
findings.append(("LOW", "ENV_EXAMPLE_IGNORED", 0, "`.env.example` is ignored, so the documented template cannot be committed",
"Add `!.env.example` after the .env rule."))
return findings
def explain(rule):
body = rule.body
kind = "directories" if rule.dir_only else "files or directories"
verb = "Re-includes" if rule.negate else "Ignores"
wild = any(ch in body for ch in "*?[")
if rule.anchored and body.endswith("/**") and "*" not in body[:-3]:
text = f"{verb} everything inside `{body[:-3]}/` (but not the folder itself), relative to the .gitignore folder"
elif rule.anchored and body.startswith("**/") and "/" not in body[3:]:
text = f"{verb} {kind} {'matching' if wild else 'named'} `{body[3:]}` at any depth"
elif rule.anchored:
text = f"{verb} {kind} {'matching' if wild else 'at'} `{body}` relative to the .gitignore folder only"
else:
text = f"{verb} {kind} {'matching' if wild else 'named'} `{body}` at any depth"
if not rule.negate and not body.endswith("/**"):
text += "; a matched directory hides everything inside it"
if rule.negate:
text += " (only works if no parent directory is excluded)"
return text + "."
def read_tests(text):
tests = []
for no, line in enumerate(text.splitlines(), 1):
s = line.strip()
if not s or s.startswith("#"):
continue
if s[0] not in "+-" or len(s) < 3:
raise ValueError(f"paths file line {no}: expected '+ path' or '- path', got {line!r}")
tests.append((s[0] == "+", s[1:].strip()))
return tests
def review(gi_text, tests_text=None):
rules, findings = parse_rules(gi_text)
findings += lint_negations(rules)
findings += lint_secrets(rules)
anchored = [r.line_no for r in rules if r.anchored and not r.negate and "/" in r.body
and not r.raw.startswith(("/", "**/")) and not r.body.endswith("/**")]
if anchored:
findings.append(("INFO", "ANCHORED_BY_SLASH", anchored[0],
"lines " + ", ".join(map(str, anchored)) + " contain a slash, so they only match relative to the .gitignore folder, not deeper copies",
"Fine if intended; add a leading `/` to make that explicit, or prefix `**/` to match at any depth."))
results = []
if tests_text:
for want, path in read_tests(tests_text):
got, reason, blocked = check_path(rules, path)
note = reason
if blocked:
note += f"; {blocked.label()} cannot re-include it"
results.append({"path": path, "expect": "ignored" if want else "kept",
"actual": "ignored" if got else "kept", "pass": got == want, "why": note})
findings.sort(key=lambda f: (SEV_ORDER[f[0]], f[2]))
return {"rules": [{"line": r.line_no, "pattern": r.raw, "meaning": explain(r)} for r in rules],
"findings": [dict(zip(("severity", "code", "line", "problem", "fix"), f)) for f in findings],
"tests": results}
def render(name, rep):
out = [f"# .gitignore review: {name}", ""]
counts = {s: sum(1 for f in rep["findings"] if f["severity"] == s) for s in SEV_ORDER}
failed = sum(1 for t in rep["tests"] if not t["pass"])
out.append(f"Findings: {counts['HIGH']} HIGH, {counts['MEDIUM']} MEDIUM, {counts['LOW']} LOW, {counts['INFO']} INFO")
if rep["tests"]:
out.append(f"Tests: {len(rep['tests']) - failed}/{len(rep['tests'])} passed")
out += ["", "## Rules", "", "| Line | Pattern | Meaning |", "|---|---|---|"]
for r in rep["rules"]:
out.append(f"| {r['line']} | `{r['pattern']}` | {r['meaning']} |")
if rep["tests"]:
out += ["", "## Path tests", "", "| Result | Path | Expected | Actual | Decided by |", "|---|---|---|---|---|"]
for t in rep["tests"]:
out.append(f"| {'PASS' if t['pass'] else 'FAIL'} | `{t['path']}` | {t['expect']} | {t['actual']} | {t['why']} |")
out += ["", "## Findings", ""]
if rep["findings"]:
out += ["| Severity | Code | Line | Problem | Fix |", "|---|---|---|---|---|"]
for f in rep["findings"]:
out.append(f"| {f['severity']} | {f['code']} | {f['line'] or '-'} | {f['problem']} | {f['fix']} |")
else:
out.append("No findings.")
out += ["", "Confirm on the real repo with `git check-ignore -v <path>` and `git status --ignored`; files that are already tracked stay tracked until `git rm --cached <path>`."]
return "\n".join(out)
def self_test():
cases = []
def t(name, cond):
cases.append((name, bool(cond)))
R = lambda s: parse_rules(s)[0]
ign = lambda s, p: check_path(R(s), p)[0]
t("*.log matches at any depth", ign("*.log", "a/b/c.log") and ign("*.log", "x.log"))
t("* does not cross /", not ign("/a*z", "ab/cz") and ign("/a*z", "abcz"))
t("name without slash matches basename anywhere", ign("build", "src/build/x.js"))
t("leading slash anchors", ign("/vendor", "vendor/a") and not ign("/vendor", "lib/vendor/a"))
t("middle slash anchors", ign("doc/frotz", "doc/frotz") and not ign("doc/frotz", "a/doc/frotz"))
t("trailing slash = directory only", ign("logs/", "logs/x") and not ign("logs/", "logs"))
t("dir-only rule ignores a dir path", ign("logs/", "logs/"))
t("**/ prefix matches any depth", ign("**/foo", "foo") and ign("**/foo", "a/b/foo"))
t("**/foo/bar", ign("**/foo/bar", "x/foo/bar") and not ign("**/foo/bar", "x/foo/baz"))
t("trailing /** matches contents only", ign("abc/**", "abc/x/y") and not ign("abc/**", "abc"))
t("a/**/b zero or more dirs", ign("a/**/b", "a/b") and ign("a/**/b", "a/x/y/b") and not ign("a/**/b", "ab"))
t("? matches one char not /", ign("/a?c", "abc") and not ign("/a?c", "a/c"))
t("[abc] class", ign("file[12].txt", "file1.txt") and not ign("file[12].txt", "file3.txt"))
t("[!x] negated class", ign("f[!0-9]", "fa") and not ign("f[!0-9]", "f5"))
t("[[:digit:]] posix class", ign("v[[:digit:]]", "v7") and not ign("v[[:digit:]]", "vx"))
t("last match wins with negation", not ign("*.txt\n!keep.txt", "keep.txt") and ign("*.txt\n!keep.txt", "a.txt"))
t("order matters", ign("!keep.txt\n*.txt", "keep.txt"))
t("parent dir excluded blocks negation", ign("logs/\n!logs/.gitkeep", "logs/.gitkeep"))
t("dir/* allows negation", not ign("logs/*\n!logs/.gitkeep", "logs/.gitkeep"))
t("dir/** allows negation", not ign("coverage/**\n!coverage/README.md", "coverage/README.md"))
t("re-include dir then file", not ign("/a/*\n!/a/b/\n/a/b/*\n!/a/b/c", "a/b/c"))
t("escaped # and !", ign("\\#notes", "#notes") and ign("\\!bang", "!bang"))
t("comment lines skipped", not ign("# *.log", "a.log"))
t("trailing spaces stripped", ign("foo ", "foo"))
t("escaped trailing space kept", ign("foo\\ ", "foo ") and not ign("foo\\ ", "foo"))
t("leading space kept as part of name", not ign(" Thumbs.db", "Thumbs.db"))
t("inline comment becomes pattern", not ign("secrets.yml # x", "secrets.yml"))
t("backslash escapes, not a separator", not ign("src\\gen", "src/gen/a.ts"))
def codes(s):
return {f[1] for f in parse_rules(s)[1]} | {f[1] for f in lint_negations(parse_rules(s)[0])} | {f[1] for f in lint_secrets(parse_rules(s)[0])}
t("NEGATION_BLOCKED found", "NEGATION_BLOCKED" in codes("logs/\n!logs/.gitkeep"))
t("NEGATION_BLOCKED not for dir/*", "NEGATION_BLOCKED" not in codes("logs/*\n!logs/.gitkeep"))
t("NEGATION_BLOCKED via dir/* deeper", "NEGATION_BLOCKED" in codes("a/*\n!a/b/c.txt"))
t("INLINE_COMMENT found", "INLINE_COMMENT" in codes("x.yml # c"))
t("LEADING_SPACE found", "LEADING_SPACE" in codes(" x"))
t("BACKSLASH_SEPARATOR found", "BACKSLASH_SEPARATOR" in codes("a\\b"))
t("DUPLICATE found", "DUPLICATE" in codes("*.log\n*.log"))
t("DOUBLE_STAR found", "DOUBLE_STAR" in codes("foo**") and "DOUBLE_STAR" not in codes("**/foo\nfoo/**"))
t("CRLF found", "CRLF" in codes("a\r\nb"))
t("SECRETS_NOT_COVERED found", "SECRETS_NOT_COVERED" in codes("*.log"))
t("secrets covered", "SECRETS_NOT_COVERED" not in codes(".env\n.env.*\n!.env.example\n*.key\n*.pem\nid_rsa*"))
t("ENV_EXAMPLE_IGNORED found", "ENV_EXAMPLE_IGNORED" in codes(".env*"))
acodes = lambda s: {f["code"] for f in review(s)["findings"]}
t("ANCHORED_BY_SLASH info", "ANCHORED_BY_SLASH" in acodes("src/gen") and "ANCHORED_BY_SLASH" not in acodes("/src/gen\nlogs/**\n**/x/y"))
rep = review("logs/\n!logs/.gitkeep\n", "+ logs/a.log\n- logs/.gitkeep\n")
t("test table marks blocked negation FAIL", rep["tests"][0]["pass"] and not rep["tests"][1]["pass"] and "cannot re-include" in rep["tests"][1]["why"])
try:
read_tests("? bad")
t("bad paths line rejected", False)
except ValueError:
t("bad paths line rejected", True)
t("findings sorted HIGH first", review("x # c\nlogs/\n!logs/a\n")["findings"][0]["severity"] == "HIGH")
passed = sum(ok for _, ok in cases)
for name, ok in cases:
print(("PASS " if ok else "FAIL ") + name)
print(f"{passed}/{len(cases)} passed")
return 0 if passed == len(cases) else 1
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("gitignore", nargs="?")
ap.add_argument("--test", help="paths file with '+ path' (must be ignored) and '- path' (must be kept)")
ap.add_argument("--json", action="store_true")
ap.add_argument("--self-test", action="store_true")
a = ap.parse_args()
if a.self_test:
return self_test()
if not a.gitignore:
ap.print_usage(sys.stderr); return 2
try:
gi = open(a.gitignore, encoding="utf-8", newline="").read()
tests = open(a.test, encoding="utf-8").read() if a.test else None
rep = review(gi, tests)
except (OSError, UnicodeDecodeError, ValueError) as e:
print(f"error: {e}", file=sys.stderr); return 2
print(json.dumps(rep, indent=2) if a.json else render(a.gitignore, rep))
bad = any(f["severity"] == "HIGH" for f in rep["findings"]) or any(not t["pass"] for t in rep["tests"])
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())Các quy ước mang tính định hướng rõ ràng để xây dựng hàng đợi tác vụ nền (background job queues) và các worker đáng tin cậy.
# Tác vụ Nền (Background Jobs) taste của một kỹ sư độ tin cậy backend - Thiết kế mọi tác vụ (job) có tính lũy đẳng (idempotent) để việc thử lại (retry) hoặc gửi trùng lặp không bao giờ trừ tiền thẻ hay gửi email hai lần. Confidence: 0.95 - Chỉ truyền các ID trong phần dữ liệu (payload) của tác vụ và tải dữ liệu mới nhất bên trong worker thay vì tuần tự hóa (serialize) toàn bộ bản ghi. Confidence: 0.90 - Đưa các tác vụ vào hàng đợi (enqueue) trong cùng một giao dịch cơ sở dữ liệu (database transaction) với thao tác ghi đã kích hoạt chúng, bằng cách sử dụng bảng hộp thư đi (outbox table) hoặc hàng đợi dựa trên Postgres. Confidence: 0.85 - Ưu tiên sử dụng một hệ thống hàng đợi đơn giản, ổn định và được hỗ trợ tốt như Sidekiq, BullMQ, Celery hoặc hàng đợi Postgres như Graphile Worker thay vì tự xây dựng mới. Confidence: 0.80 - Thử lại với thời gian chờ tăng theo cấp số nhân (exponential backoff) kèm độ lệch ngẫu nhiên (jitter), và giới hạn số lần thử để các tác vụ lỗi độc hại (poison jobs) được chuyển vào hàng đợi thư chết (dead letter queue). Confidence: 0.95 - Phân loại lỗi thành lỗi có thể thử lại (retryable) hoặc lỗi vĩnh viễn (permanent), và cho dừng ngay lập tức các lỗi vĩnh viễn thay vì lãng phí số lần thử lại. Confidence: 0.85 - Thiết lập thời gian chờ tối đa (timeout) rõ ràng cho mọi tác vụ và cho mọi lệnh gọi mạng mà tác vụ đó thực hiện. Confidence: 0.90 - Giữ cho các tác vụ nhỏ gọn và chạy nhanh, đồng thời chia nhỏ các công việc xử lý hàng loạt (batch work) kéo dài thành từng phần (chunks) có thể tiếp tục chạy lại từ điểm đánh dấu (checkpoint). Confidence: 0.85 - Sử dụng các hàng đợi riêng biệt cho công việc nhạy cảm với độ trễ và công việc xử lý số lượng lớn (bulk work) để tác vụ nạp bù dữ liệu (backfill) không bao giờ làm chậm email đặt lại mật khẩu. Confidence: 0.90 - Giới hạn tần suất (rate limit) các tác vụ gọi API bên thứ ba theo từng nhà cung cấp dịch vụ, chứ không phải theo từng tiến trình worker. Confidence: 0.80 - Sử dụng khóa tác vụ duy nhất (unique job keys) hoặc cơ chế khóa (locks) để ngăn cùng một tác vụ định kỳ chạy hai lần cùng lúc. Confidence: 0.85 - Chạy các lịch trình kiểu cron từ một bộ lập lịch (scheduler) duy nhất có cơ chế bầu chọn nút dẫn đầu (leader election), tuyệt đối không chạy từ mọi thực thể (instance) của ứng dụng. Confidence: 0.80 - Đánh phiên bản (version) cho dữ liệu đầu vào (payload) của tác vụ và đảm bảo các worker vẫn đọc được phiên bản cũ trong quá trình triển khai (deploy) bản mới. Confidence: 0.80 - Xử lý tín hiệu SIGTERM một cách êm đẹp (gracefully): ngừng nhận tác vụ mới, hoàn tất hoặc đưa lại tác vụ hiện tại vào hàng đợi, sau đó mới thoát. Confidence: 0.90 - Ghi nhận mã tác vụ (job ID), số thứ tự lần thử (attempt number) và mã tương quan (correlation ID) trên mọi dòng nhật ký (log) phát sinh từ một tác vụ. Confidence: 0.85 - Cảnh báo dựa trên độ sâu của hàng đợi, tuổi của tác vụ cũ nhất và mức tăng trưởng của hàng đợi thư chết (dead letter queue), chứ không chỉ cảnh báo khi worker bị sập. Confidence: 0.90 - Phân công người phụ trách rõ ràng cho hàng đợi thư chết và có tài liệu hướng dẫn lệnh chạy lại (replay command). Confidence: 0.75 - Kiểm thử các tác vụ bằng cách gọi trực tiếp hàm xử lý (handler) trong kiểm thử đơn vị (unit tests) và chạy một bài kiểm thử đầu-cuối (end-to-end) thông qua hàng đợi thật trong hệ thống CI. Confidence: 0.80 - Tránh nối chuỗi quá nhiều tác vụ thành các luồng công việc ngầm phức tạp; hãy sử dụng một công cụ điều phối luồng công việc (workflow engine) như Temporal khi các bước cần được điều phối chặt chẽ. Confidence: 0.70 - Lưu trữ kết quả tác vụ và các dấu hiệu đánh dấu hiệu ứng phụ (side-effect markers) trong cơ sở dữ liệu, chứ không chỉ lưu trong hệ thống lưu trữ của hàng đợi. Confidence: 0.75
Các quy ước mang tính định hướng rõ ràng để xây dựng ứng dụng Vue 3 dễ bảo trì với TypeScript và Composition API.
# Ứng dụng Vue taste của một kỹ sư frontend - Bắt đầu các dự án mới bằng Vite và Vue 3, hoặc Nuxt khi cần kết xuất phía máy chủ (server rendering) hoặc định tuyến dựa trên tệp (file-based routing). Confidence: 0.90 - Viết các component bằng script setup và Composition API, không dùng Options API. Confidence: 0.90 - Sử dụng TypeScript ở mọi nơi và kiểm tra kiểu cho template bằng vue-tsc trong hệ thống CI. Confidence: 0.90 - Định nghĩa kiểu cho props và emits bằng generics của defineProps và defineEmits thay vì chỉ khai báo ở thời điểm chạy (runtime-only). Confidence: 0.85 - Sử dụng Pinia cho trạng thái phía client được chia sẻ chung và giữ cho các store nhỏ gọn, chỉ tập trung vào một miền nghiệp vụ (domain) duy nhất. Confidence: 0.85 - Tải dữ liệu từ máy chủ bằng TanStack Query cho Vue hoặc useFetch của Nuxt thay vì tự viết các cờ trạng thái đang tải (loading flags) thủ công trong store. Confidence: 0.80 - Tách logic có thể tái sử dụng thành các composable có tên theo mẫu useSomething trả về các ref và hàm. Confidence: 0.90 - Tìm đến thư viện VueUse trước khi tự viết một composable tùy chỉnh cho các API trình duyệt phổ biến. Confidence: 0.75 - Ưu tiên dùng computed hơn là watch, và chỉ sử dụng watch cho các tác vụ có hiệu ứng phụ (side effects). Confidence: 0.90 - Tuyệt đối không thay đổi trực tiếp (mutate) giá trị của props; hãy phát sự kiện (emit events) hoặc sử dụng defineModel cho ràng buộc hai chiều (two-way binding). Confidence: 0.95 - Luôn cung cấp cho các phần tử trong v-for một khóa (key) ổn định lấy từ dữ liệu, tuyệt đối không dùng chỉ số mảng (index). Confidence: 0.90 - Không bao giờ kết hợp v-if và v-for trên cùng một phần tử. Confidence: 0.90 - Giữ cho các component có độ dài dưới khoảng 200 dòng và tách nhỏ chúng ra khi phần template trở nên khó đọc lướt. Confidence: 0.70 - Sử dụng tên component gồm nhiều từ viết theo kiểu PascalCase và đặt các component cùng chỗ (colocate) theo tính năng chứ không phân chia theo loại kỹ thuật. Confidence: 0.80 - Định kiểu giao diện bằng scoped CSS hoặc Tailwind, và tránh sử dụng các kiểu toàn cục (global styles) ngoại trừ một phần reset nhỏ và các design token. Confidence: 0.75 - Tải trễ (lazy load) các component của tuyến đường (route) bằng cú pháp import động trong Vue Router. Confidence: 0.85 - Kiểm thử các component bằng Vitest và Vue Testing Library, truy vấn phần tử theo vai trò (role) và văn bản hiển thị. Confidence: 0.85 - Kiểm tra mã nguồn (lint) bằng ESLint và eslint-plugin-vue với bộ quy tắc được khuyến nghị, đồng thời định dạng mã bằng Prettier. Confidence: 0.85 - Tránh sử dụng provide và inject cho trạng thái toàn ứng dụng; chỉ dùng chúng cho phần nội bộ của thư viện component và các cấu hình truyền sâu nhiều tầng. Confidence: 0.70 - Làm sạch (sanitize) bất kỳ mã HTML nào trước khi sử dụng v-html, hoặc tốt hơn hết là tránh dùng v-html hoàn toàn. Confidence: 0.95
Các quy ước mang tính định hướng rõ ràng để xây dựng ứng dụng máy tính để bàn (desktop) đa nền tảng an toàn bằng Tauri và Rust.
# Ứng dụng Desktop Tauri taste của một lập trình viên desktop đa nền tảng - Chọn Tauri thay vì Electron cho các ứng dụng desktop mới khi kích thước gói cài đặt (bundle size) và mức sử dụng bộ nhớ là yếu tố quan trọng. Confidence: 0.80 - Xây dựng phần frontend bằng Vite và TypeScript ở chế độ nghiêm ngặt (strict mode), đồng thời giữ cho nó độc lập với framework nhìn từ phía Rust. Confidence: 0.85 - Giữ logic nghiệp vụ có tương tác với hệ thống tệp, mạng hoặc hệ điều hành ở trong các lệnh Rust (Rust commands), và giữ cho tầng web chỉ tập trung vào giao diện người dùng (UI). Confidence: 0.85 - Chỉ bật các năng lực (capabilities) và quyền hạn (permissions) mà từng cửa sổ thực sự cần, và tuyệt đối không phát hành danh sách cho phép dùng ký tự đại diện (wildcard allowlist). Confidence: 0.95 - Thiết lập Chính sách Bảo mật Nội dung (Content Security Policy - CSP) nghiêm ngặt và không bao giờ tải nội dung từ xa vào một cửa sổ có quyền gọi lệnh hệ thống. Confidence: 0.95 - Tạo các liên kết kiểu (bindings) TypeScript cho các lệnh bằng tauri-specta hoặc ts-rs để các kiểu dữ liệu giữa frontend và Rust không thể bị lệch nhau. Confidence: 0.80 - Trả về các lỗi có cấu trúc từ các lệnh bằng thiserror và serde thay vì trả về chuỗi văn bản thuần túy. Confidence: 0.85 - Chạy các tác vụ nặng hoặc chậm trong các lệnh bất đồng bộ (async commands) hoặc tác vụ nền và báo cáo tiến độ thông qua các sự kiện (events), tuyệt đối không làm chặn luồng chính (main thread). Confidence: 0.90 - Lưu trữ cài đặt của người dùng bằng plugin store chính thức và lưu các thông tin bí mật (secrets) trong trình quản lý khóa của hệ điều hành (OS keychain), không lưu trong các tệp JSON thuần túy. Confidence: 0.90 - Sử dụng SQLite thông qua sqlx hoặc plugin SQL cho dữ liệu cục bộ, với các tệp di trú cơ sở dữ liệu (migrations) được lưu trữ trong kho mã nguồn (repo). Confidence: 0.80 - Phân giải đường dẫn bằng path API cho các thư mục dữ liệu ứng dụng và cấu hình thay vì gán cứng (hardcode) vị trí. Confidence: 0.90 - Phát hành tính năng cập nhật tự động bằng plugin updater và các gói phát hành (release artifacts) đã được ký số. Confidence: 0.85 - Ký số (sign) và chứng thực (notarize) các bản dựng macOS cũng như ký số các trình cài đặt Windows ngay từ hệ thống CI, tuyệt đối không thực hiện từ máy tính cá nhân của lập trình viên. Confidence: 0.90 - Xây dựng bản cài đặt cho mọi nền tảng trong GitHub Actions bằng ma trận tauri-action trên mỗi thẻ phát hành (release tag). Confidence: 0.80 - Kiểm thử giao diện người dùng (UI) trên cả WebKit, WebView2 và WebKitGTK, vì cơ chế hiển thị và các API có sự khác biệt giữa chúng. Confidence: 0.85 - Tôn trọng các quy ước của từng nền tảng đối với thanh menu, phím tắt và nút điều khiển cửa sổ thay vì dùng chung một thanh tiêu đề tùy chỉnh ở mọi nơi. Confidence: 0.75 - Viết kiểm thử đơn vị (unit test) trực tiếp cho các lệnh Rust và bao phủ các luồng chính bằng kiểm thử đầu-cuối (end-to-end) thông qua WebDriver. Confidence: 0.70 - Giữ cây phụ thuộc (dependency tree) nhỏ gọn và chạy cargo audit cùng npm audit trong hệ thống CI. Confidence: 0.80 - Ghi nhật ký (log) vào tệp xoay vòng (rotating file) trong thư mục nhật ký của ứng dụng và cung cấp cho người dùng cách xuất nhật ký chỉ bằng một cú nhấp chuột để gửi báo cáo lỗi. Confidence: 0.75
Rà soát các kịch bản (script) bash và POSIX sh trước khi chúng chạy trong CI, cron hoặc môi trường production. Phát hiện và gắn cờ việc thiếu chế độ nghiêm ngặt (strict mode), lệnh rm -rf trên các biến có thể bị rỗng, truyền kết quả curl qua pipe vào shell, lệnh eval, mở rộng biến không đặt trong dấu ngoặc kép, lệnh cd không kiểm tra lỗi, phân tích cú pháp kết quả lệnh ls, chmod 777, bí mật (secret) gắn cứng trong mã, tệp /tmp có tên dễ đoán trước, lệnh ssh nuốt mất dữ liệu của vòng lặp read, mã thoát (exit code) bị che khuất và các cú pháp riêng của bash (bashism) chạy dưới /bin/sh, sau đó trả về bản script đã được sửa lỗi. Bao gồm một bộ kiểm tra mã nguồn (linter) bằng thư viện chuẩn Python có hỗ trợ --self-test.
--- name: shell-script-safety-reviewer description: Rà soát các kịch bản (script) bash và POSIX sh trước khi chúng chạy trong CI, cron hoặc môi trường production. Phát hiện và gắn cờ việc thiếu chế độ nghiêm ngặt (strict mode), lệnh rm -rf trên các biến có thể bị rỗng, truyền kết quả curl qua pipe vào shell, lệnh eval, mở rộng biến không đặt trong dấu ngoặc kép, lệnh cd không kiểm tra lỗi, phân tích cú pháp kết quả lệnh ls, chmod 777, bí mật (secret) gắn cứng trong mã, tệp /tmp có tên dễ đoán trước, lệnh ssh nuốt mất dữ liệu của vòng lặp read, mã thoát (exit code) bị che khuất và các cú pháp riêng của bash (bashism) chạy dưới /bin/sh, sau đó trả về bản script đã được sửa lỗi. Bao gồm một bộ kiểm tra mã nguồn (linter) bằng thư viện chuẩn Python có hỗ trợ --self-test. --- # Công cụ Rà soát Độ an toàn của Shell Script Bạn rà soát các shell script theo đúng cách một kỹ sư SRE cẩn trọng sẽ làm trước khi cho phép chúng chạy tự động không người giám sát: các script triển khai (deploy), tác vụ định kỳ (cron job), các bước CI, trình cài đặt và các entrypoint của Docker. Bạn tìm ra những dòng lệnh có nguy cơ xóa nhầm dữ liệu, làm lộ bí mật (secret), thực thi mã không đáng tin cậy hoặc gặp lỗi âm thầm mà không báo, giải thích từng vấn đề bằng ngôn ngữ dễ hiểu, và trả lại một bản script đã được sửa lỗi hoàn chỉnh mà vẫn thực hiện đúng công việc ban đầu. ## Các tệp trong kỹ năng này - `scripts/shell_lint.py` - bộ kiểm tra mã nguồn (linter) theo từng dòng dành cho bash và POSIX sh có khả năng hiểu dấu ngoặc kép, chú thích, heredoc, `[[ ]]`, `(( ))`, phép gán và các từ khóa trong cấu trúc case; in ra bảng các phát hiện kèm cách khắc phục, hoặc xuất ra định dạng JSON (chỉ sử dụng thư viện chuẩn Python 3) - `references/shell-pitfalls.md` - mọi quy tắc mà linter kiểm tra: điều gì sẽ xảy ra khi lỗi, ví dụ tối thiểu gây lỗi và cách khắc phục - `references/safe-patterns.md` - phần đầu tệp (header) bật chế độ nghiêm ngặt, cách tạo tệp tạm an toàn và dọn dẹp, vòng lặp duyệt tệp an toàn, phân tích đối số đầu vào, xử lý bí mật, tải tệp và ghi nhật ký (logging) - `templates/shell-review.md` - mẫu báo cáo rà soát để bàn giao lại - `examples/deploy.sh` - một script triển khai thực tế chứa 20 phát hiện lỗi (trong đó có 3 lỗi mức HIGH) - `examples/deploy-fixed.sh` - chính script đó sau khi được rà soát và sửa lỗi; vượt qua hoàn toàn bước kiểm tra của linter và ShellCheck ## Quy trình làm việc ### 1. Thu thập ngữ cảnh Hãy hỏi xem cái gì sẽ chạy script này (CI, cron, systemd, hay con người chạy trực tiếp), chạy trên shell và hệ điều hành nào (bash 5 trên Ubuntu, sh trên Alpine/busybox, hay macOS bash 3.2), ai có thể kiểm soát các dữ liệu đầu vào của nó (đối số dòng lệnh, biến môi trường, tên tệp, dữ liệu mạng) và liệu nó có chạy dưới quyền root hay không. Hãy yêu cầu cung cấp toàn bộ script chứ không chỉ một đoạn cắt rời: chế độ nghiêm ngặt (strict mode) và các bẫy sự kiện (trap) thường nằm ở ngay đầu tệp. ### 2. Chạy bộ kiểm tra (linter) ``` python3 scripts/shell_lint.py examples/deploy.sh python3 scripts/shell_lint.py deploy.sh entrypoint.sh --min-severity MEDIUM python3 scripts/shell_lint.py install.sh --json --ignore BACKTICKS python3 scripts/shell_lint.py --self-test ``` Mã thoát (Exit) 1 có nghĩa là có ít nhất một phát hiện mức HIGH, 2 nghĩa là không thể đọc được tệp. Bỏ qua cảnh báo cho một dòng chủ đích bằng chú thích `# shell-lint: disable=CODE`. Nếu trên máy có sẵn ShellCheck, hãy chạy cả công cụ đó (`shellcheck -f gcc script.sh`) rồi gộp kết quả lại; bộ linter này bổ sung thêm những bước kiểm tra mà ShellCheck không làm (lộ bí mật, `curl | sh`, `chmod 777`, tên tệp `/tmp` cố định, tắt kiểm tra TLS). ### 3. Rà soát thủ công Bộ linter chỉ là bước sàng lọc đầu tiên. Hãy đọc kỹ script từ trên xuống dưới và kiểm tra những điểm mà biểu thức chính quy (regex) không thể phát hiện được (xem `references/shell-pitfalls.md`): - Những đầu vào nào là không đáng tin cậy, và chúng có đi tới lệnh `rm`, `eval`, `ssh`, câu lệnh `sql` hay đường dẫn tệp nào không? - Mọi bước có tính chất xóa/phá hủy dữ liệu có chỉ được chạy sau khi đã thỏa mãn các điều kiện tiên quyết (đúng thư mục, biến không rỗng, đã xác nhận máy chủ đích) hay không? - Script có đảm bảo tính lũy đẳng (idempotent) không: có thể chạy hai lần liên tiếp, hoặc chạy tiếp sau khi bị dừng giữa chừng do lỗi không? - Các lỗi có hiển thị rõ ràng không: khi có lỗi xảy ra trong đường ống (pipe), subshell hoặc hàm, script có dừng lại ngay và để lại thông báo rõ ràng không? - Script có dọn dẹp sạch các tệp tạm, tệp khóa (lock) và các tiến trình chạy ngầm khi thoát bình thường cũng như khi nhấn Ctrl+C không? ### 4. Sửa lỗi và bàn giao Điền đầy đủ vào `templates/shell-review.md`: kết luận tổng quan, bảng danh sách phát hiện (mức độ nghiêm trọng, dòng, vấn đề, cách sửa), toàn bộ mã nguồn script đã sửa, bản so sánh (diff) những thay đổi quan trọng, và hướng dẫn cách kiểm thử an toàn (chạy thử dry run, dùng `bash -n`, chạy trong thư mục hoặc container dùng một lần, bật `set -x`). Giữ nguyên hành vi nghiệp vụ của script; nêu rõ bất kỳ thay đổi hành vi nào. ## Ví dụ kết quả `examples/deploy.sh`: 20 phát hiện, 3 lỗi mức HIGH. Một token Slack bị gắn cứng trong mã (HARDCODED_SECRET, cần xoay vòng/đổi token mới), một trình cài đặt được truyền thẳng từ lệnh curl qua pipe vào sudo bash (CURL_PIPE_SHELL), và lệnh `rm -rf $WEB_ROOT/*` sẽ xóa sạch toàn bộ `/*` nếu biến WEB_ROOT vô tình bị rỗng (RM_EMPTY_VAR). Script không có `set -e`, nên nếu lệnh `cd $BUILD_DIR` thất bại thì các lệnh rm và cp phía sau sẽ chạy ngay tại thư mục không mong muốn; lệnh `ssh` nằm bên trong vòng lặp `while read host` nuốt mất luồng dữ liệu của tệp hosts.txt, khiến chỉ có máy chủ đầu tiên được khởi động lại (SSH_STDIN_LOOP); cú pháp `local count=$(...)` làm che khuất trạng thái lỗi. Tệp `examples/deploy-fixed.sh` đã khắc phục toàn bộ các lỗi trên và vượt qua bước kiểm tra linter không còn cảnh báo nào. ## Quy tắc - Xếp hạng theo mức độ ảnh hưởng: nguy cơ mất dữ liệu, thực thi mã tùy ý và lộ bí mật phải đặt lên hàng đầu, tiếp theo là các lỗi âm thầm (silent failures), cuối cùng mới đến phong cách viết mã (style). - Mặc định luôn đặt biến trong dấu ngoặc kép; giải thích rõ tại vị trí duy nhất nào mà việc tách từ (word splitting) là có chủ đích và hãy sử dụng mảng (array) tại đó. - Không bao giờ chỉ đề xuất mỗi `set -e` làm giải pháp duy nhất: phải kiểm tra cả các đường ống (pipefail), subshell, cú pháp `local x=$(...)` và các ngữ cảnh `if cmd` nơi chế độ tự thoát khi lỗi (errexit) bị vô hiệu hóa. - Yêu cầu xoay vòng (rotate) bất kỳ bí mật nào tìm thấy trong script, ngay cả khi kho mã nguồn (repo) ở chế độ riêng tư, và phải nêu rõ điều đó trong báo cáo. - Ưu tiên sử dụng cú pháp chuẩn POSIX khi dòng shebang là `/bin/sh`; nếu không thì phải đổi dòng shebang thành `bash`. - Không được chạy script đang rà soát trực tiếp trên các hệ thống thật để "kiểm thử"; hãy dùng `bash -n`, chạy trong container hoặc dùng cờ chạy thử (dry-run). FILE:references/shell-pitfalls.md # Các bẫy thường gặp trong Shell được kiểm tra bởi shell_lint.py Mức độ nghiêm trọng: HIGH = có thể xóa nhầm dữ liệu, chạy mã không đáng tin cậy hoặc làm lộ bí mật. MEDIUM = trả về kết quả sai hoặc gặp lỗi âm thầm với dữ liệu đầu vào thực tế. LOW = mã dễ vỡ hoặc dùng cú pháp cũ không còn khuyến khích. ## Mức HIGH ### RM_EMPTY_VAR / RM_ROOT Lệnh `rm -rf $DIR/*` sẽ biến thành `rm -rf /*` khi biến DIR bị rỗng hoặc chưa được khởi tạo (do gõ sai tên biến, thiếu đối số truyền vào, hoặc lệnh thay thế `$(...)` bị lỗi). Trình khách (client) Steam trên Linux từng phát hành bản cập nhật dính chính xác lỗi này vào năm 2015. ```bash rm -rf -- "?DIR is empty"/* # aborts with a message if DIR is empty ``` Chỉ dùng mỗi `set -u` sẽ không giúp ích được gì khi biến đó đã được khởi tạo nhưng mang giá trị là một chuỗi rỗng. ### CURL_PIPE_SHELL Lệnh `curl https://x/install.sh | bash` sẽ thực thi bất cứ thứ gì mà máy chủ (hoặc kẻ tấn công đứng giữa - MITM) gửi về, và nếu kết nối mạng bị đứt giữa chừng thì bash sẽ chạy một nửa script bị cắt cụt. Hãy tải về thành một tệp, kiểm tra mã băm SHA-256 cố định hoặc chữ ký số, đọc nội dung tệp, rồi mới chạy nó. ### EVAL_VAR Lệnh `eval "$cmd"` sẽ thực thi bất kỳ ký tự `;`, `$(...)` hoặc dấu huyền (backtick) nào nằm bên trong dữ liệu. Hãy xây dựng câu lệnh dưới dạng mảng: `cmd=(rsync -a -- "$src" "$dst"); "cmd[@]"`. Để điều hướng lệnh (dispatch), hãy dùng cấu trúc `case`. ### HARDCODED_SECRET Một chuỗi token, mật khẩu hoặc khóa viết trực tiếp trong script sẽ nằm lại trong lịch sử git, nhật ký CI (khi bật `set -x`) và kết quả lệnh `ps`. Hãy đọc bí mật từ biến môi trường hoặc trình quản lý bí mật (secret manager) và cho dừng sớm nếu thiếu: `TOKEN="?"`. Hãy xoay vòng (đổi mới) giá trị đã bị lộ; việc xóa dòng mã đó đi không làm nó biến mất khỏi lịch sử git. ## Mức MEDIUM ### NO_STRICT_MODE / NO_PIPEFAIL Nếu không có `set -e`, một câu lệnh bị lỗi sẽ bị bỏ qua và câu lệnh tiếp theo vẫn cứ chạy bình thường. Nếu không có `pipefail`, câu lệnh `build | tee log` vẫn báo thành công ngay cả khi bước build thất bại. Hãy dùng `set -euo pipefail` trong bash và `set -eu` trong POSIX sh. Lưu ý các lỗ hổng của nó: chế độ tự thoát khi lỗi (errexit) bị bỏ qua bên trong điều kiện `if`, `while`, các chuỗi lệnh nối bằng `&&`/`||` cùng các hàm được gọi từ bên trong chúng, và cả cú pháp `local x=$(cmd)` (xem bên dưới). ### UNQUOTED_VAR Các biểu thức `$var` và `$(cmd)` không đặt trong dấu ngoặc kép sẽ bị tách từ dựa theo biến `IFS` (khoảng trắng, tab, xuống dòng) và mỗi mảnh tách ra lại tiếp tục bị mở rộng theo ký tự đại diện (glob expansion). Lệnh `cp $src $dst` với `src="My File.txt"` sẽ sao chép nhầm hai tệp không tồn tại; lệnh `rm $f` với `f='*'` sẽ xóa sạch mọi thứ. Hãy đặt mọi biểu thức mở rộng vào trong dấu ngoặc kép; dùng `"$@"` cho toàn bộ đối số và `"arr[@]"` cho mảng. Những vị trí an toàn khi không có dấu ngoặc kép: vế phải của phép gán `x=$y`, bên trong `[[ ]]` (ngoại trừ vế phải của toán tử `==`/`=~` vì ở đó nó sẽ trở thành mẫu so khớp pattern), bên trong `(( ))`, và trong cấu trúc `case $x in`. ### CD_UNCHECKED Nếu lệnh `cd build` thất bại, toàn bộ phần còn lại của script sẽ chạy ngay tại thư mục hiện hành: `cd build; rm -rf *`. Hãy dùng `cd -- "$dir" || exit 1` hoặc bật chế độ nghiêm ngặt. Trong các vòng lặp, hãy dùng subshell: `(cd -- "$d" && make)`. ### LS_PARSING Vòng lặp `for f in $(ls *.log)` sẽ bị lỗi khi tên tệp có chứa khoảng trắng hoặc ký tự xuống dòng, đồng thời làm biểu thức đại diện (glob) bị chạy hai lần. Hãy dùng trực tiếp biểu thức glob: `for f in ./*.log; do [ -e "$f" ] || continue; ...; done`, hoặc dùng `find . -name '*.log' -print0 | while IFS= read -r -d '' f; do ...; done`. ### CHMOD_777 Các tệp và thư mục cho phép mọi người dùng đều có quyền ghi (world-writable) sẽ tạo điều kiện cho bất kỳ người dùng cục bộ hoặc dịch vụ bị xâm nhập nào thay thế mã nguồn hoặc dữ liệu. Hãy dùng quyền 755/644, hoặc 700/600 cho dữ liệu riêng tư, hoặc dùng cú pháp `u=rwX,g=rX,o=`. ### PREDICTABLE_TMP / NO_TRAP_CLEANUP (LOW) Một đường dẫn cố định như `/tmp/app.log` có thể bị một người dùng khác tạo sẵn từ trước dưới dạng liên kết tượng trưng (symlink) (khiến bạn vô tình ghi đè lên tệp đích của họ dưới quyền root) và gây xung đột khi có nhiều tiến trình chạy song song. Hãy dùng `mktemp`/`mktemp -d` và xóa tệp tạm khi kết thúc bằng `trap '...' EXIT`. ### LOCAL_MASKS_STATUS Cú pháp `local out=$(cmd)` trả về mã trạng thái của lệnh `local` (luôn là 0), vì vậy `set -e` và biến `$?` không bao giờ phát hiện được việc `cmd` đã thất bại. Điều tương tự cũng xảy ra với `export` và `readonly`. Hãy khai báo biến trước, rồi thực hiện phép gán ở dòng tiếp theo. ### SSH_STDIN_LOOP Vòng lặp `while read host; do ssh "$host" cmd; done < hosts.txt` sẽ dừng lại ngay sau máy chủ đầu tiên bởi vì lệnh `ssh` đã đọc hết phần còn lại của luồng đầu vào chuẩn (stdin). Hãy dùng `ssh -n` hoặc `< /dev/null`. Nguyên tắc tương tự cũng áp dụng cho `ffmpeg`, `docker exec -i` và bất kỳ công cụ nào khác tự động đọc dữ liệu từ stdin. ### TLS_DISABLED Các tùy chọn `curl -k`, `wget --no-check-certificate` và `StrictHostKeyChecking=no` sẽ tắt bỏ bước kiểm tra xác nhận bạn đang giao tiếp với đúng máy chủ đích. Hãy sửa lại gói chứng chỉ CA (CA bundle), ghim cố định khóa máy chủ (host key) vào tệp `known_hosts`, hoặc chỉ dùng `StrictHostKeyChecking=accept-new` cho lần kết nối đầu tiên trên các mạng nội bộ đáng tin cậy. ### BASHISM_IN_SH Khi chạy dưới `#!/bin/sh` (trỏ tới dash trên Debian/Ubuntu, hoặc busybox trên Alpine), những cú pháp sau đây sẽ gây lỗi: `[[ ]]`, toán tử `==` bên trong `[ ]`, từ khóa `function f`, mảng (arrays), lệnh `source`, `<<<`, `&>`, `$'...'`. Hãy chuyển dòng shebang sang `bash` hoặc sử dụng các cú pháp chuẩn POSIX tương ứng là `[ ]`, `=`, `.`, và các tham số vị trí. ## Mức LOW - NO_SHEBANG: shell hiện tại của người gọi sẽ được dùng để chạy script; hành vi sẽ khác nhau giữa bash, zsh và sh. - READ_WITHOUT_R: lệnh `read` không có cờ `-r` sẽ nuốt mất các dấu gạch chéo ngược (backslash); nếu không có `IFS=` thì nó sẽ tự cắt bỏ khoảng trắng ở đầu và cuối dòng. - BACKTICKS: dấu huyền rất khó lồng nhau và khó kết hợp với dấu ngoặc kép; hãy dùng `$(...)`. ## Các vấn đề bộ linter không quét được (cần rà soát thủ công) - Đầu vào không đáng tin cậy đi tới câu lệnh sql, ssh hoặc đường dẫn tệp (nguy cơ tiêm lệnh injection, duyệt thư mục ngược `../`); tên tệp bắt đầu bằng dấu gạch ngang `-` (hãy dùng `--`). - Điều kiện tranh chấp (race condition) trên các tệp khóa (lock files); hãy dùng `flock`. - Thiếu lệnh `wait` cho các tiến trình chạy ngầm, và các tiến trình mồ côi không được dọn dẹp khi script thoát. - Sự khác biệt về thiết lập vùng (locale) và bộ công cụ giữa các hệ điều hành (GNU so với BSD ở các lệnh `sed -i`, `date -d`, `readlink -f` trên macOS). - Lộ bí mật vào nhật ký khi bật `set -x`; hãy tắt chế độ xtrace xung quanh các đoạn mã xử lý bí mật. FILE:references/safe-patterns.md # Các mẫu viết mã an toàn cho bash và sh script ## Phần đầu tệp (Header) ```bash #!/usr/bin/env bash # What it does, how to call it, what it needs (env vars, tools). set -euo pipefail IFS=$'\n\t' # optional: no splitting on spaces readonly SCRIPT_DIR="$(cd -- "$(dirname -- "BASH_SOURCE[0]")" && pwd)" ``` Đối với POSIX sh: dùng `set -eu` (tùy chọn `pipefail` có hỗ trợ trong dash 0.5.11+ và busybox, nhưng không phải mọi môi trường đều có). ## Dừng chương trình kèm thông báo lỗi ```bash die() { printf 'error: %s\n' "$*" >&2; exit 1; } command -v jq >/dev/null 2>&1 || die "jq is required" [ -d "$TARGET" ] || die "no such directory: $TARGET" ``` ## Xử lý đối số đầu vào ```bash usage() { echo "usage: $0 [-n] <release>" >&2; exit 2; } dry_run=0 while getopts ":n" opt; do case $opt in n) dry_run=1 ;; *) usage ;; esac done shift $((OPTIND - 1)) release="-"; [ -n "$release" ] || usage ``` ## Tệp tạm và dọn dẹp ```bash tmpdir="$(mktemp -d)" cleanup() { rm -rf -- "$tmpdir"; } trap cleanup EXIT trap 'exit 130' INT TERM # EXIT trap still runs ``` ## Các bước xóa/phá hủy dữ liệu ```bash target="?WEB_ROOT is empty" case $target in /|/usr|/etc|"$HOME") die "refusing to clean $target" ;; esac run() { if [ "$dry_run" -eq 1 ]; then echo "+ $*"; else "$@"; fi; } run rm -rf -- "$target"/* ``` ## Vòng lặp duyệt qua tệp và dòng văn bản ```bash for f in "$dir"/*.csv; do [ -e "$f" ] || continue; process "$f"; done find "$dir" -type f -name '*.log' -print0 | while IFS= read -r -d '' f; do gzip -- "$f"; done while IFS= read -r line || [ -n "$line" ]; do echo "$line"; done < input.txt # last line without newline ``` ## Biểu diễn câu lệnh dưới dạng dữ liệu (mảng) ```bash args=(--archive --delete -- "$src/" "$dst/") [ "$dry_run" -eq 1 ] && args=(--dry-run "args[@]") rsync "args[@]" ``` ## Xử lý bí mật (Secrets) - Đọc từ biến môi trường hoặc từ tệp được phân quyền 600: `token="$(<"$CRED_FILE")"`. - Truyền vào các công cụ thông qua luồng stdin hoặc biến môi trường, tuyệt đối không truyền qua đối số dòng lệnh (vì sẽ bị lộ khi xem bằng lệnh `ps`): `curl -H @- <<<"Authorization: Bearer $token"`. - Đặt `set +x` bao quanh đoạn mã xử lý bí mật. ## Tải tệp từ mạng ```bash curl -fsSL --retry 3 -o "$tmpdir/tool.tgz" "$URL" echo "$SHA256 $tmpdir/tool.tgz" | sha256sum -c - ``` Cờ `-f` giúp báo lỗi ngay khi gặp mã lỗi HTTP thay vì lưu trang thông báo lỗi thành tệp kết quả. ## Khóa tiến trình (Locking) và tính lũy đẳng (Idempotency) ```bash exec 9>/var/lock/deploy.lock flock -n 9 || die "another deploy is running" mkdir -p -- "$dir" # no error if it exists ln -sfn -- "$release_dir" current # atomic switch of a symlink ``` ## Ghi nhật ký (Logging) ```bash log() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >&2; } ``` Hãy gửi nhật ký ra luồng lỗi chuẩn (stderr) để luồng đầu ra chuẩn (stdout) luôn sạch sẽ dành cho dữ liệu. ## Kiểm thử script một cách an toàn 1. Chạy `bash -n script.sh` (chỉ kiểm tra cú pháp), `shellcheck script.sh`, `python3 scripts/shell_lint.py script.sh`. 2. Chạy với cờ chạy thử (dry-run), sau đó chạy trong một container dùng một lần: `docker run --rm -v "$PWD":/w -w /w bash:5 bash script.sh`. 3. Chạy theo dõi luồng thực thi một lần bằng `bash -x script.sh` (sau khi đã tắt/bỏ các bí mật thật). FILE:templates/shell-review.md # Báo cáo rà soát Shell script: <tên script> **Môi trường chạy:** <cron trên máy chủ web-01 dưới quyền root / GitHub Actions ubuntu-24.04 / Docker entrypoint (Alpine, busybox sh)> **Shell:** <bash 5.2 / sh (dash)> **Đầu vào không đáng tin cậy:** <đối số $1 thẻ release, tệp hosts.txt, tên các tệp trong thư mục uploads/> **Kết luận:** <CHẶN (BLOCK): 3 phát hiện mức HIGH / ĐẠT sau khi sửa lỗi / ĐẠT (OK)> **Tóm tắt:** <một hoặc hai câu: rủi ro nghiêm trọng nhất là gì và bản sửa lỗi thay đổi điều gì> ## Danh sách phát hiện (Findings) | # | Mức độ | Mã lỗi | Dòng | Vấn đề | Cách khắc phục | |---|---|---|---|---|---| | 1 | <HIGH> | <RM_EMPTY_VAR> | <15> | <rm -rf $WEB_ROOT/* sẽ xóa sạch /* nếu WEB_ROOT bị rỗng> | <rm -rf -- "?"/*> | ## Ghi chú rà soát thủ công - <các đầu vào đi tới rm/ssh/sql/đường dẫn, và cách chúng được kiểm tra hợp lệ> - <tính lũy đẳng: điều gì xảy ra nếu script chạy hai lần hoặc bị dừng giữa chừng> - <dọn dẹp tài nguyên khi thoát bình thường và khi nhấn Ctrl+C> ## Bản script đã sửa lỗi ```bash <full corrected script> ``` ## Các thay đổi chính ```diff - rm -rf $WEB_ROOT/* + rm -rf -- "?"/* ``` ## Thay đổi về hành vi - <hiện tại script sẽ thoát ngay tại câu lệnh đầu tiên gặp lỗi; trước đây vẫn tiếp tục chạy> ## Cách kiểm thử an toàn 1. Chạy `bash -n <script>` và `python3 scripts/shell_lint.py <script>` (kỳ vọng 0 lỗi HIGH) 2. Chạy `shellcheck <script>` nếu có sẵn trên máy 3. Chạy thử (dry run) hoặc chạy trong một container dùng một lần: `<câu lệnh>` 4. <xoay vòng/đổi mới bí mật đã bị lộ: đổi ở đâu và ai phụ trách> FILE:examples/deploy.sh #!/bin/bash # Deploy the web app build to /srv/www and restart the service. # Usage: deploy.sh <release-tag> RELEASE=$1 BUILD_DIR=/srv/builds/$RELEASE WEB_ROOT=/srv/www SLACK_TOKEN="xoxb-0000-EXAMPLE-not-real" curl -fsSL https://get.example-tools.dev/install.sh | sudo bash cd $BUILD_DIR echo "Deploying $RELEASE" > /tmp/deploy.log rm -rf $WEB_ROOT/* cp -r dist/* $WEB_ROOT/ for f in $(ls $BUILD_DIR/migrations); do psql -f $BUILD_DIR/migrations/$f app_db done chmod -R 777 $WEB_ROOT/uploads while read host; do ssh -o StrictHostKeyChecking=no deploy@$host "sudo systemctl restart web" done < hosts.txt cleanup() { local count=$(ls /srv/builds | wc -l) echo "Builds on disk: $count" } cleanup curl -s -X POST -H "Authorization: Bearer $SLACK_TOKEN" \ -d "text=Deployed `date`" https://slack.com/api/chat.postMessage FILE:examples/deploy-fixed.sh #!/usr/bin/env bash # Deploy the web app build to /srv/www and restart the service. # Usage: SLACK_TOKEN=... deploy.sh <release-tag> set -euo pipefail RELEASE="?usage: deploy.sh <release-tag>" BUILD_DIR="/srv/builds/RELEASE" WEB_ROOT="/srv/www" SLACK_TOKEN="?set SLACK_TOKEN in the environment" # Installer is pinned and checked instead of curl | bash. installer="$(mktemp)" log="$(mktemp)" trap 'rm -f -- "$installer" "$log"' EXIT curl -fsSL -o "$installer" https://get.example-tools.dev/install-1.4.2.sh echo "3b1f0c9e2d4a5b6c7d8e9f00112233445566778899aabbccddeeff0011223344 $installer" | sha256sum -c - sudo bash "$installer" cd -- "$BUILD_DIR" echo "Deploying RELEASE" > "$log" rm -rf -- "?"/* cp -r dist/. "$WEB_ROOT/" for f in "$BUILD_DIR"/migrations/*.sql; do [ -e "$f" ] || continue psql -v ON_ERROR_STOP=1 -f "$f" app_db done chmod -R u=rwX,g=rX,o= "$WEB_ROOT/uploads" while IFS= read -r host; do ssh -n -o StrictHostKeyChecking=yes "deploy@host" "sudo systemctl restart web" done < hosts.txt report_builds() { local count count="$(find /srv/builds -mindepth 1 -maxdepth 1 -type d | wc -l)" echo "Builds on disk: count" } report_builds curl -fsS -X POST -H "Authorization: Bearer SLACK_TOKEN" \ --data-urlencode "text=Deployed RELEASE at $(date -u +%FT%TZ)" https://slack.com/api/chat.postMessage FILE:scripts/shell_lint.py #!/usr/bin/env python3 """Shell script safety linter (Python 3 standard library only). Reads a bash or POSIX sh script and flags the mistakes that cause data loss, security holes or silent failures: no strict mode, rm -rf on a variable that may be empty, curl | sh, eval on variables, unquoted expansions (word splitting and globbing), unchecked cd, parsing ls, chmod 777, hardcoded secrets, predictable /tmp files, mktemp without a cleanup trap, read without -r, ssh eating a while-read loop's input, local/export masking a failed command substitution, TLS or host-key checks turned off, backticks and bashisms in /bin/sh. It is a fast first pass, not a full parser: heredoc bodies are skipped, quotes, comments, [[ ]], (( )), assignments and case words are understood, and every finding names the line and a concrete fix. Silence a finding on one line with a trailing comment "# shell-lint: disable=CODE[,CODE]". Usage: python3 shell_lint.py script.sh [more.sh ...] [--json] [--min-severity MEDIUM] [--ignore CODE,CODE] python3 shell_lint.py --self-test Exit codes: 0 = no HIGH findings, 1 = at least one HIGH finding, 2 = unreadable input. """ import argparse, json, re, sys SEV_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "INFO": 3} SECRET_NAME = re.compile(r"[A-Z0-9_]*(PASSWORD|PASSWD|TOKEN|SECRET|API_?KEY|ACCESS_KEY|PRIVATE_KEY)[A-Z0-9_]*") FIXES = { "NO_SHEBANG": "Add '#!/usr/bin/env bash' (or '#!/bin/sh' for POSIX) as line 1 so the right shell runs it.", "NO_STRICT_MODE": "Add 'set -euo pipefail' after the shebang (POSIX sh: 'set -eu') so failures stop the script.", "NO_PIPEFAIL": "Add 'set -o pipefail' so 'cmd | tee log' fails when cmd fails.", "RM_EMPTY_VAR": "Guard the variable: rm -rf -- \"?VAR is empty/\"... and quote it; never build rm paths from unchecked input.", "RM_ROOT": "Never rm the filesystem root or /*; point at a specific directory.", "CURL_PIPE_SHELL": "Download to a file, verify a checksum or signature, read it, then run it.", "EVAL_VAR": "Avoid eval; use arrays for commands (cmd=(ls -l \"$dir\"); \"cmd[@]\") or a case statement.", "UNQUOTED_VAR": "Quote expansions: \"$var\", \"arr[@]\", \"$(cmd)\" to stop word splitting and globbing.", "CD_UNCHECKED": "Use 'cd \"$dir\" || exit 1' (or enable set -e) so later commands do not run in the wrong directory.", "LS_PARSING": "Use a glob (for f in ./*.log) or find -print0 | while IFS= read -r -d '' f instead of parsing ls/find output.", "BACKTICKS": "Use $(...) instead of backticks; it nests and quotes predictably.", "CHMOD_777": "Grant only what is needed (755 for dirs/executables, 644 for files, 600 for secrets).", "HARDCODED_SECRET": "Read the secret from the environment or a secret manager (\"?\"), rotate the exposed value.", "PREDICTABLE_TMP": "Use tmp=$(mktemp) or tmpdir=$(mktemp -d) instead of a fixed /tmp name (symlink and race attacks).", "NO_TRAP_CLEANUP": "Add trap 'rm -rf -- \"$tmpdir\"' EXIT right after mktemp.", "READ_WITHOUT_R": "Use 'read -r' (and IFS= read -r line) so backslashes and leading spaces survive.", "LOCAL_MASKS_STATUS": "Split it: 'local out; out=$(cmd)' so set -e and $? see the command's failure.", "TLS_DISABLED": "Remove -k/--insecure/--no-check-certificate/StrictHostKeyChecking=no; fix the CA bundle or pin the host key.", "SSH_STDIN_LOOP": "Use 'ssh -n' (or ssh ... < /dev/null) inside while-read loops; otherwise ssh eats the rest of the input.", "BASHISM_IN_SH": "Either change the shebang to bash or use the POSIX form ([ ], =, . file, no arrays).", } class Finding: def __init__(self, sev, code, line, snippet, problem): self.sev, self.code, self.line, self.snippet, self.problem = sev, code, line, snippet, problem def as_dict(self): return {"severity": self.sev, "code": self.code, "line": self.line, "snippet": self.snippet, "problem": self.problem, "fix": FIXES.get(self.code, "")} def strip_comment(line): """Remove an unquoted comment. '#' starts a comment only at the start of a word.""" state, i = None, 0 while i < len(line): c = line[i] if state is None: if c == "\\": i += 2; continue if c in "'\"`": state = c elif c == "#" and (i == 0 or line[i - 1] in " \t;|&("): return line[:i].rstrip() elif state == "'": if c == "'": state = None else: if c == "\\": i += 2; continue if c == state: state = None i += 1 return line def logical_lines(text): """Yield (line_no, code_without_comment, raw_line, disabled_codes); heredoc bodies skipped.""" lines = text.split("\n") pending = [] # heredoc terminators waiting, in order out = [] for no, raw in enumerate(lines, 1): if pending: delim, dash = pending[0] body = raw.lstrip("\t") if dash else raw if body == delim: pending.pop(0) continue m = re.search(r"#\s*shell-lint:\s*disable=([A-Z_,\s]+)", raw) disabled = {c.strip() for c in m.group(1).split(",")} if m else set() code = strip_comment(raw) for hm in re.finditer(r"<<(-?)\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\2", code): if code[hm.start():hm.start() + 3] == "<<<": continue pending.append((hm.group(3), hm.group(1) == "-")) out.append((no, code, raw, disabled)) return out def match_paren(s, i): """s[i] == '(' ; return index of the matching ')' respecting quotes, or len(s).""" depth, state, j = 0, None, i while j < len(s): c = s[j] if state is None: if c == "\\": j += 2; continue if c in "'\"": state = c elif c == "(": depth += 1 elif c == ")": depth -= 1 if depth == 0: return j elif c == state: state = None elif c == "\\" and state == '"': j += 1 j += 1 return len(s) def unquoted_expansions(code): """Return names of $var / ... / $(...) / $@ expansions that are not inside quotes and not in a context where splitting cannot happen ([[ ]], (( )), assignments, case word).""" found = [] stripped = code.strip() if re.match(r"case\s", stripped): return found state, i, dbl_brackets, arith = None, 0, 0, 0 n = len(code) def word_start(k): while k > 0 and code[k - 1] not in " \t;|&(": k -= 1 return k while i < n: c = code[i] if state == "'": if c == "'": state = None i += 1; continue if c == "\\": i += 2; continue if state is None and c == "'": state = "'"; i += 1; continue if c == '"': state = None if state == '"' else '"' i += 1; continue if state is None and code.startswith("[[", i) and (i == 0 or code[i - 1] in " \t;&|!("): dbl_brackets += 1; i += 2; continue if state is None and code.startswith("]]", i) and dbl_brackets: dbl_brackets -= 1; i += 2; continue if state is None and code.startswith("((", i) and (i == 0 or code[i - 1] in " \t;&|!"): end = match_paren(code, i) i = end + 1; continue if c == "`": end = code.find("`", i + 1) i = (end if end != -1 else n) + 1; continue if c == "$" and i + 1 < n: nxt = code[i + 1] if code.startswith("$((", i): end = match_paren(code, i + 1) i = end + 1; continue if nxt == "(": end = match_paren(code, i + 1) inner = code[i + 2:end] found.extend(unquoted_expansions(inner)) if state is None and not dbl_brackets and not arith: ws = word_start(i) if not re.match(r"[A-Za-z_][A-Za-z0-9_]*(\[[^\]]*\])?\+?=", code[ws:i + 1]): found.append("$(" + inner.strip()[:20] + ")") i = end + 1; continue m = re.match(r"\$(\{[^}]*\}|[A-Za-z_][A-Za-z0-9_]*|[@*]|[0-9])", code[i:]) if m: tok = m.group(0) if state is None and not dbl_brackets: ws = word_start(i) is_assign = re.match(r"[A-Za-z_][A-Za-z0-9_]*(\[[^\]]*\])?\+?=", code[ws:i + 1]) if not is_assign and not tok.startswith("found.append(tok) i += len(tok); continue i += 1 return found def commands(code): """Split a line into simple commands on ; && || | (outside quotes).""" parts, cur, state, i = [], "", None, 0 while i < len(code): c = code[i] if state is None and c in "'\"": state = c elif state and c == state: state = None if state is None and c in ";|&": parts.append(cur); cur = "" while i + 1 < len(code) and code[i + 1] in ";|&": i += 1 i += 1; continue cur += c; i += 1 parts.append(cur) return [p.strip() for p in parts if p.strip()] def lint(text, name="script"): findings = [] lines = logical_lines(text) first = text.split("\n", 1)[0] shebang = first if first.startswith("#!") else "" is_posix_sh = bool(re.search(r"(^#!\s*/bin/sh\b|/env\s+sh\b|/bin/dash\b)", shebang)) all_code = "\n".join(c for _, c, _, _ in lines) strict_e = bool(re.search(r"(^|\n)\s*set\s+(-[a-zA-Z]*e[a-zA-Z]*\b|-o\s+errexit)", all_code)) or bool(re.search(r"\s-[a-z]*e", shebang)) pipefail = "pipefail" in all_code uses_pipes = "|" in all_code.replace("||", "") uses_mktemp = "mktemp" in all_code has_trap = bool(re.search(r"(^|\n)\s*trap\s", all_code)) def add(sev, code, no, snippet, problem, disabled=frozenset()): if code not in disabled: findings.append(Finding(sev, code, no, snippet.strip()[:80], problem)) if not shebang: add("LOW", "NO_SHEBANG", 1, first, "no shebang line; the caller's shell decides how this runs") if not strict_e: add("MEDIUM", "NO_STRICT_MODE", 1, shebang or first, "no 'set -e' (errexit): a failed command does not stop the script") elif uses_pipes and not pipefail and not is_posix_sh: add("LOW", "NO_PIPEFAIL", 1, shebang, "pipes are used but pipefail is off: only the last command's status counts") if uses_mktemp and not has_trap: no = next((n for n, c, _, _ in lines if "mktemp" in c), 1) add("LOW", "NO_TRAP_CLEANUP", no, next(c for n, c, _, _ in lines if n == no), "mktemp is used but nothing removes the temp file on exit or error") read_loop = 0 for no, code, raw, dis in lines: if not code.strip(): continue if re.search(r"(^|[;&|\s])while\b.*\bread\b", code): read_loop += 1 if read_loop and re.search(r"(^|[;&|\s])ssh\s", code) and not re.search(r"\bssh\s+(-\w+\s+)*-\w*n\b|<\s*/dev/null", code): add("MEDIUM", "SSH_STDIN_LOOP", no, code, "ssh inside a while-read loop reads the loop's stdin, so the loop stops after the first item", dis) if read_loop and re.search(r"(^|[;&|\s])done\b", code): read_loop -= 1 # curl | sh if re.search(r"\b(curl|wget)\b[^|]*\|\s*(sudo\s+(-\S+\s+)*)?(ba|z|da|k)?sh\b", code): add("HIGH", "CURL_PIPE_SHELL", no, code, "remote script piped straight into a shell: no review, no integrity check, partial downloads run", dis) if re.search(r"\b(curl\b[^|;]*\s(-k|--insecure)\b|wget\b[^|;]*--no-check-certificate|StrictHostKeyChecking=no)", code): add("MEDIUM", "TLS_DISABLED", no, code, "certificate or host-key verification is turned off (man-in-the-middle risk)", dis) if re.search(r"(^|[;&|(\s])eval\s+.*\$", code): add("HIGH", "EVAL_VAR", no, code, "eval on a string built from variables runs any shell code an attacker or odd filename puts in it", dis) if re.search(r"\bchmod\s+(-[A-Za-z]+\s+)*0?777\b|\bchmod\s+(-[A-Za-z]+\s+)*[augo]*\+rwx\b", code) and re.search(r"777|a\+rwx|o\+rwx", code): add("MEDIUM", "CHMOD_777", no, code, "world-writable permissions let any local user change the files", dis) if "`" in code: add("LOW", "BACKTICKS", no, code, "legacy backtick command substitution", dis) m = re.match(r"\s*(export\s+|readonly\s+|local\s+|declare\s+(-\w+\s+)*)?(" + SECRET_NAME.pattern + r")=(['\"]?)([^$'\"\s][^'\"\s]{3,)", code) if m: val = m.group(6) masked = val[:2] + "*" * max(3, len(val) - 4) + val[-2:] add("HIGH", "HARDCODED_SECRET", no, code.replace(val, masked), f"{m.group(3)} has a literal value in the script (ends up in git, logs and ps output)", dis) if re.match(r"\s*(local|export|declare|readonly)\s+(-\w+\s+)*[A-Za-z_][A-Za-z0-9_]*=\"?\$\(", code): add("MEDIUM", "LOCAL_MASKS_STATUS", no, code, "the builtin's exit status (0) hides a failure of the command substitution, even under set -e", dis) if re.search(r"(^|[^\w.-])/tmp/[A-Za-z0-9_.${}-]+", code) and "mktemp" not in code: add("MEDIUM", "PREDICTABLE_TMP", no, code, "fixed /tmp path: another user can pre-create or symlink it, and parallel runs collide", dis) if re.search(r"\bfor\s+\w+\s+in\s+(\$\(|`)\s*(ls|find)\b", code) or re.search(r"\bls\b[^|]*\|\s*(while|xargs)\b", code): add("MEDIUM", "LS_PARSING", no, code, "parsing ls/find output breaks on spaces, newlines and glob characters in filenames", dis) for cmd in commands(code): words = cmd.split() if not words: continue w0 = words[0] while w0 in ("sudo", "command", "builtin", "exec", "!", "while", "until", "if", "then", "do", "else", "elif", "{", "(") and len(words) > 1: words = words[1:]; w0 = words[0] if w0 == "rm": args = words[1:] rec = any(re.match(r"-[a-zA-Z]*[rR]", a) or a == "--recursive" for a in args) if any(a.strip("'\"") in ("/", "/*") for a in args): add("HIGH", "RM_ROOT", no, cmd, "rm targets / or /*", dis) for a in args: if a.startswith("-") and a != "--": continue if "$" in a and not re.search(r"\$\{[A-Za-z_][A-Za-z0-9_]*:\?", a): a_clean = a.strip("'\"") if re.match(r"\$\{?[A-Za-z_][A-Za-z0-9_]*\}?(/|\"?$)", a_clean) or rec: add("HIGH" if rec else "MEDIUM", "RM_EMPTY_VAR", no, cmd, f"rm{' -r' if rec else ''} on {a}: if the variable is empty or unset this deletes the wrong path (e.g. '/')", dis) break if w0 == "cd" and not strict_e: if not re.search(re.escape(cmd) + r"\s*(\|\||&&)", code) and not re.search(r"(\|\||&&)\s*" + re.escape(cmd), code): add("MEDIUM", "CD_UNCHECKED", no, cmd, "cd may fail and the following commands then run in the wrong directory", dis) if w0 == "read": opts = [a for a in words[1:] if a.startswith("-")] if not any("r" in o for o in opts): add("LOW", "READ_WITHOUT_R", no, cmd, "read without -r mangles backslashes", dis) exps = unquoted_expansions(code) if exps: uniq = list(dict.fromkeys(exps)) shown = ", ".join(uniq[:4]) + (" ..." if len(uniq) > 4 else "") only_echo = re.match(r"\s*(echo|printf)\s+[^|>]*$", code) and not any(e in ("$@", "$*") for e in uniq) if only_echo: add("LOW", "UNQUOTED_VAR", no, code, "unquoted " + shown + " in echo/printf: spacing collapses and * expands", dis) else: add("MEDIUM", "UNQUOTED_VAR", no, code, "unquoted " + shown + ": word splitting and glob expansion", dis) if is_posix_sh: bash = [] if re.search(r"(^|[\s;&|!])\[\[\s", code): bash.append("[[ ]]") if re.search(r"(^|[\s;&|])\[\s[^]]*\s==\s", code): bash.append("== in [ ]") if re.match(r"\s*function\s+\w+", code): bash.append("function keyword") if re.search(r"\b[A-Za-z_]\w*=\(", code): bash.append("array") if re.match(r"\s*source\s", code): bash.append("source") if "<<<" in code: bash.append("here-string") if re.search(r"&>", code): bash.append("&>") if re.search(r"\$'", code): bash.append("$'...'") if bash: add("MEDIUM", "BASHISM_IN_SH", no, code, "bash-only syntax under #!/bin/sh (" + ", ".join(bash) + "); dash and busybox sh will fail", dis) findings.sort(key=lambda f: (SEV_ORDER[f.sev], f.line, f.code)) return findings def render_md(name, findings): counts = {s: sum(1 for f in findings if f.sev == s) for s in SEV_ORDER} out = [f"# Shell lint: {name}", "", f"Findings: {len(findings)} (HIGH {counts['HIGH']}, MEDIUM {counts['MEDIUM']}, LOW {counts['LOW']})", ""] if not findings: out.append("No findings.") return "\n".join(out) out += ["| Severity | Code | Line | Snippet | Problem |", "|---|---|---|---|---|"] for f in findings: snip = f.snippet.replace("|", "\\|").replace("`", "'") out.append(f"| {f.sev} | {f.code} | {f.line} | `{snip}` | {f.problem.replace('|', '/')} |") out += ["", "## Fixes", ""] for code in dict.fromkeys(f.code for f in findings): out.append(f"- {code}: {FIXES[code]}") return "\n".join(out) # ---------------------------------------------------------------- self-test def _codes(src): return [f.code for f in lint(src)] def self_test(): S = "#!/usr/bin/env bash\nset -euo pipefail\n" cases = [ ("clean script has no findings", S + 'dir="$1"\ncp -- "$dir/a" "$dir/b"\n', lambda c: c == []), ("missing shebang", 'set -eu\necho "hi"\n', lambda c: "NO_SHEBANG" in c), ("missing strict mode", "#!/bin/bash\necho ok\n", lambda c: "NO_STRICT_MODE" in c), ("set -e without pipefail and a pipe", "#!/bin/bash\nset -e\nmake | tee log\n", lambda c: "NO_PIPEFAIL" in c), ("set -o errexit counts as strict", "#!/bin/bash\nset -o errexit\nset -o pipefail\necho ok\n", lambda c: c == []), ("rm -rf $VAR/ is HIGH", S + 'rm -rf "$BUILD_DIR"/\n', lambda c: "RM_EMPTY_VAR" in c), ("rm -rf ? is fine", S + 'rm -rf -- "?/"\n', lambda c: "RM_EMPTY_VAR" not in c), ("rm -rf / is RM_ROOT", S + "rm -rf /\n", lambda c: "RM_ROOT" in c), ("curl | bash", S + "curl -fsSL https://x.example/install.sh | sudo bash\n", lambda c: "CURL_PIPE_SHELL" in c), ("curl to file is fine", S + 'curl -fsSLo install.sh "$URL"\n', lambda c: "CURL_PIPE_SHELL" not in c), ("eval with variable", S + 'eval "$cmd"\n', lambda c: "EVAL_VAR" in c), ("unquoted var in command", S + "cp $src $dst\n", lambda c: "UNQUOTED_VAR" in c), ("quoted var is fine", S + 'cp "$src" "$dst"\n', lambda c: "UNQUOTED_VAR" not in c), ("assignment rhs is fine", S + "x=$y\nn=$(date +%s)\n", lambda c: "UNQUOTED_VAR" not in c), ("[[ ]] is fine", S + "if [[ $a == b ]]; then :; fi\n", lambda c: "UNQUOTED_VAR" not in c), ("arith is fine", S + "n=$(( $a + 1 ))\n(( count > $max ))\n", lambda c: "UNQUOTED_VAR" not in c), ("#arr is fine", S + 'printf "%s\\n" #arr[@]\n', lambda c: "UNQUOTED_VAR" not in c), ("unquoted $@", S + "run $@\n", lambda c: "UNQUOTED_VAR" in c), ("unquoted cmd substitution", S + "rm -- $(cat list)\n", lambda c: "UNQUOTED_VAR" in c), ("var inside quoted $( ) still checked", S + 'x="$(basename $path)"\n', lambda c: "UNQUOTED_VAR" in c), ("single quotes protect", S + "echo '$HOME is literal'\n", lambda c: "UNQUOTED_VAR" not in c), ("comment ignored", S + "# rm -rf $DIR/ and curl x | sh\n", lambda c: c == []), ("heredoc body skipped", S + "cat <<'EOF'\nrm -rf $X/\ncurl a | sh\nEOF\n", lambda c: c == []), ("cd unchecked without set -e", "#!/bin/bash\ncd /srv/app\n", lambda c: "CD_UNCHECKED" in c), ("cd || exit is fine", "#!/bin/bash\ncd /srv/app || exit 1\n", lambda c: "CD_UNCHECKED" not in c), ("for in $(ls)", S + "for f in $(ls *.log); do :; done\n", lambda c: "LS_PARSING" in c), ("backticks", S + "d=`date`\n", lambda c: "BACKTICKS" in c), ("chmod 777", S + "chmod -R 777 /srv/app\n", lambda c: "CHMOD_777" in c), ("hardcoded token, value masked", S + 'export API_TOKEN="sk_live_abcdef123456"\n', lambda c: "HARDCODED_SECRET" in c), ("secret from env is fine", S + 'API_TOKEN="?"\n', lambda c: "HARDCODED_SECRET" not in c), ("predictable tmp", S + 'echo "x" > /tmp/deploy.lock\n', lambda c: "PREDICTABLE_TMP" in c), ("mktemp without trap", S + "tmp=$(mktemp)\n", lambda c: "NO_TRAP_CLEANUP" in c), ("mktemp with trap fine", S + "tmp=$(mktemp)\ntrap 'rm -f -- \"$tmp\"' EXIT\n", lambda c: "NO_TRAP_CLEANUP" not in c), ("read without -r", S + 'while read line; do :; done < "$f"\n', lambda c: "READ_WITHOUT_R" in c), ("local masks status", S + "f() {\n local out=$(git rev-parse HEAD)\n}\n", lambda c: "LOCAL_MASKS_STATUS" in c), ("curl -k", S + 'curl -k "$URL" -o x\n', lambda c: "TLS_DISABLED" in c), ("bashisms under /bin/sh", "#!/bin/sh\nset -eu\nif [[ -n \"$x\" ]]; then source ./env; fi\n", lambda c: "BASHISM_IN_SH" in c), ("bash allows [[ ]]", S + 'if [[ -n "$x" ]]; then source ./env; fi\n', lambda c: "BASHISM_IN_SH" not in c), ("disable comment", S + "cp $a $b # shell-lint: disable=UNQUOTED_VAR\n", lambda c: "UNQUOTED_VAR" not in c), ("ssh in while-read loop", S + 'while IFS= read -r h; do\n ssh "$h" uptime\ndone < hosts\n', lambda c: "SSH_STDIN_LOOP" in c), ("ssh -n in loop is fine", S + 'while IFS= read -r h; do\n ssh -n "$h" uptime\ndone < hosts\n', lambda c: "SSH_STDIN_LOOP" not in c), ("case word fine", S + 'case $1 in\n a) echo "a" ;;\nesac\n', lambda c: "UNQUOTED_VAR" not in c), ] passed = 0 for name, src, check in cases: got = _codes(src) ok = check(got) passed += ok print(("PASS " if ok else "FAIL ") + name + ("" if ok else f" -> {got}")) # masking check and exit code check f = [x for x in lint(S + 'export API_TOKEN="sk_live_abcdef123456"\n') if x.code == "HARDCODED_SECRET"][0] ok = "abcdef123456" not in f.snippet passed += ok; print(("PASS " if ok else "FAIL ") + "secret value masked in output") ok = render_md("t", []).endswith("No findings.") passed += ok; print(("PASS " if ok else "FAIL ") + "empty report") total = len(cases) + 2 print(f"{passed}/{total} passed") return 0 if passed == total else 1 def main(): ap = argparse.ArgumentParser(description="Shell script safety linter (stdlib only)") ap.add_argument("files", nargs="*") ap.add_argument("--json", action="store_true") ap.add_argument("--min-severity", default="LOW", choices=list(SEV_ORDER)) ap.add_argument("--ignore", default="", help="comma-separated codes to skip") ap.add_argument("--self-test", action="store_true") a = ap.parse_args() if a.self_test: return self_test() if not a.files: ap.error("give at least one script or --self-test") ignore = {c.strip() for c in a.ignore.split(",") if c.strip()} worst, reports, js = 0, [], {} for path in a.files: try: with open(path, encoding="utf-8") as fh: text = fh.read() except (OSError, UnicodeDecodeError) as e: print(f"error: cannot read {path}: {e}", file=sys.stderr) return 2 fs = [f for f in lint(text, path) if f.code not in ignore and SEV_ORDER[f.sev] <= SEV_ORDER[a.min_severity]] if any(f.sev == "HIGH" for f in fs): worst = 1 js[path] = [f.as_dict() for f in fs] reports.append(render_md(path, fs)) print(json.dumps(js, indent=2) if a.json else "\n\n".join(reports)) return worst if __name__ == "__main__": sys.exit(main())
Các quy ước giàu tính định hướng để xây dựng ứng dụng SvelteKit full-stack với Svelte 5 runes và TypeScript.
# Gu thiết kế (taste) Ứng dụng SvelteKit của một lập trình viên web full-stack - Bắt đầu mọi dự án bằng khuôn mẫu (template) chính thức `sv create`, bật sẵn TypeScript và kích hoạt chế độ nghiêm ngặt (strict mode). Độ tin cậy: 0.90 - Sử dụng Svelte 5 runes (`$state`, `$derived`, `$effect`, `$props`) cho tất cả các component mới thay vì các câu lệnh phản ứng (reactive statements) kiểu cũ. Độ tin cậy: 0.90 - Tìm đến `$derived` trước khi nghĩ tới `$effect`, và chỉ giữ `$effect` cho việc đồng bộ hóa với những thứ nằm ngoài Svelte như DOM hoặc các thư viện bên thứ ba. Độ tin cậy: 0.90 - Tải dữ liệu trong `+page.server.ts` khi cần dùng tới thông tin bí mật (secrets) hoặc cơ sở dữ liệu, và chỉ sử dụng `+page.ts` dùng chung (universal) cho các lệnh gọi lấy dữ liệu công khai. Độ tin cậy: 0.90 - Xử lý các thao tác thay đổi dữ liệu (mutations) bằng form actions và `use:enhance` để biểu mẫu vẫn hoạt động được khi không có JavaScript và tự động hưởng lợi từ khả năng nâng cấp lũy tiến (progressive enhancement). Độ tin cậy: 0.85 - Xác thực dữ liệu đầu vào của biểu mẫu và API ở phía máy chủ bằng Zod hoặc Valibot, đồng thời trả về `fail()` kèm lỗi chi tiết từng trường thay vì ném ngoại lệ (throw). Độ tin cậy: 0.90 - Giữ mã nguồn chỉ chạy trên máy chủ trong thư mục `$lib/server` để quá trình build lập tức báo lỗi nếu mã đó vô tình bị import vào phía máy khách. Độ tin cậy: 0.95 - Đọc các thông tin bí mật từ `$env/static/private` hoặc `$env/dynamic/private`, tuyệt đối không đọc từ các module biến môi trường công khai. Độ tin cậy: 0.95 - Đặt logic xác thực và tra cứu phiên làm việc (session) trong `hooks.server.ts` và cung cấp thông tin người dùng thông qua `event.locals`. Độ tin cậy: 0.90 - Tuyệt đối không lưu trữ dữ liệu người dùng của từng yêu cầu (per-request) vào biến trạng thái ở cấp độ module, vì nó sẽ làm rò rỉ dữ liệu giữa các yêu cầu khác nhau trên máy chủ. Độ tin cậy: 0.95 - Chia sẻ trạng thái phía máy khách thông qua context hoặc các module nhỏ dựa trên rune trong các file `.svelte.ts`, thay vì dùng một store toàn cục cho mọi thứ. Độ tin cậy: 0.80 - Sử dụng các nhóm route như `(app)` và `(marketing)` để áp dụng các bố cục (layouts) khác nhau cho từng phân khu mà không làm thay đổi đường dẫn URL. Độ tin cậy: 0.80 - Kết xuất trước (prerender) các trang tĩnh như trang tài liệu và tiếp thị, đồng thời giữ các trang động được kết xuất từ phía máy chủ (server-rendered). Độ tin cậy: 0.85 - Chọn một bộ chuyển đổi (adapter) tường minh (`adapter-node`, `adapter-vercel`, `adapter-cloudflare`) thay vì phụ thuộc vào `adapter-auto` trên môi trường production. Độ tin cậy: 0.80 - Trả về dữ liệu có kiểu rõ ràng từ các hàm `load` và dựa vào `./$types` được sinh tự động thay vì tự viết kiểu cho prop bằng tay. Độ tin cậy: 0.90 - Sử dụng các trang `+error.svelte` và hàm trợ giúp `error()` cho những lỗi nằm trong dự kiến, và ghi nhật ký các lỗi bất ngờ trong `handleError`. Độ tin cậy: 0.85 - Định kiểu giao diện bằng CSS có phạm vi cục bộ theo component (scoped component CSS) làm mặc định và chỉ thêm Tailwind khi nhóm phát triển đã quen sử dụng nó. Độ tin cậy: 0.70 - Giữ cho các component nhỏ gọn và truyền các `snippets` cho phần nội dung linh hoạt thay vì dùng quá nhiều prop kiểu boolean. Độ tin cậy: 0.80 - Kiểm thử các component bằng Vitest và `@testing-library/svelte`, đồng thời bao phủ các luồng người dùng chính bằng Playwright. Độ tin cậy: 0.85 - Chạy `svelte-check`, ESLint và Prettier kèm plugin Svelte trong quy trình CI trên mọi pull request. Độ tin cậy: 0.85
Các quy ước giàu tính định hướng để xây dựng những hàm serverless đáng tin cậy, tối ưu chi phí trên đám mây.
# Gu thiết kế (taste) Hàm Serverless của một kỹ sư backend đám mây - Giữ cho mỗi hàm nhỏ gọn và tập trung vào một tác nhân kích hoạt (trigger) duy nhất, nhưng hãy gom các route HTTP có liên quan mật thiết vào chung một handler để cắt giảm tình trạng khởi động lạnh (cold starts). Độ tin cậy: 0.80 - Khởi tạo các SDK client và nhóm kết nối cơ sở dữ liệu (database pools) bên ngoài hàm handler để các lần gọi ấm (warm invocations) có thể tái sử dụng chúng. Độ tin cậy: 0.95 - Sử dụng bộ quản lý nhóm kết nối (connection pooler) hoặc trình điều khiển cơ sở dữ liệu dựa trên HTTP, vì hàng nghìn hàm chạy đồng thời sẽ làm cạn kiệt các kết nối Postgres trực tiếp. Độ tin cậy: 0.90 - Thiết lập thời gian chờ (timeout) và dung lượng bộ nhớ tường minh cho từng hàm thay vì chấp nhận các giá trị mặc định của nền tảng. Độ tin cậy: 0.85 - Giữ kích thước gói mã (bundle) nhỏ gọn bằng kỹ thuật loại bỏ mã thừa (tree-shaking) và nạp trễ (lazy imports), đồng thời đo lường thời gian khởi động lạnh sau mỗi thay đổi thư viện phụ thuộc lớn. Độ tin cậy: 0.85 - Đảm bảo mọi hàm xử lý sự kiện đều có tính lũy đẳng (idempotent), bởi vì các hàng đợi và nguồn phát sự kiện luôn phân phối theo cơ chế ít nhất một lần (at-least-once). Độ tin cậy: 0.95 - Chuyển các tác vụ chạy chậm hoặc cần thử lại sang hàng đợi (queue) và trả kết quả về nhanh chóng từ các hàm HTTP. Độ tin cậy: 0.90 - Đẩy các sự kiện bất đồng bộ bị lỗi vào hàng đợi thư chết (dead letter queue - DLQ) kèm theo cảnh báo dựa trên độ sâu (số lượng tồn đọng) của hàng đợi đó. Độ tin cậy: 0.90 - Định nghĩa các hàm, quyền hạn và tác nhân kích hoạt dưới dạng mã (Infrastructure as Code) bằng SST, Serverless Framework, SAM hoặc Terraform, tuyệt đối không bấm chọn thủ công trên giao diện điều khiển (console). Độ tin cậy: 0.90 - Cấp cho mỗi hàm một vai trò (role) riêng theo nguyên tắc đặc quyền tối thiểu, giới hạn phạm vi chính xác vào những tài nguyên mà hàm đó chạm tới. Độ tin cậy: 0.95 - Nạp các thông tin bí mật (secrets) từ trình quản lý bí mật (secrets manager) hoặc biến môi trường được mã hóa của nền tảng, tuyệt đối không lấy từ kho mã nguồn (repo). Độ tin cậy: 0.95 - Ghi nhật ký (logs) định dạng JSON có cấu trúc kèm theo request ID và correlation ID trên mọi dòng. Độ tin cậy: 0.90 - Truy vết (trace) các yêu cầu xuyên qua các hàm và hàng đợi bằng OpenTelemetry hoặc công cụ truy vết có sẵn của nền tảng. Độ tin cậy: 0.80 - Giữ logic nghiệp vụ trong các module thuần túy và chỉ để hàm handler đóng vai trò một lớp chuyển đổi (adapter) mỏng, nhờ đó có thể kiểm thử đơn vị (unit test) mà không cần tới môi trường đám mây. Độ tin cậy: 0.90 - Chạy kiểm thử tích hợp (integration tests) trên một môi trường triển khai (stage) thực tế cho mỗi pull request thay vì đặt niềm tin vào các trình giả lập cục bộ. Độ tin cậy: 0.80 - Thiết lập mức độ đồng thời dành riêng hoặc tối đa (reserved or maximum concurrency) trên các hàm gọi tới những dịch vụ hạ nguồn mỏng manh, dễ quá tải. Độ tin cậy: 0.85 - Chỉ sử dụng mức độ đồng thời được cấp phát sẵn (provisioned concurrency) cho các luồng xử lý nhạy cảm về độ trễ nơi mà quá trình khởi động lạnh đã được đo đạc và chứng minh là gây ảnh hưởng xấu. Độ tin cậy: 0.75 - Ưu tiên môi trường thực thi arm64 khi các thư viện phụ thuộc hỗ trợ để đạt tỷ lệ hiệu năng trên giá thành tốt hơn. Độ tin cậy: 0.70 - Thiết lập cảnh báo hóa đơn thanh toán và bảng theo dõi chi phí theo từng hàm trước khi ra mắt, chứ không phải đợi đến sau tờ hóa đơn gây sốc đầu tiên. Độ tin cậy: 0.85 - Tránh nối chuỗi các hàm gọi nhau một cách đồng bộ; thay vào đó hãy dùng sự kiện hoặc một bộ điều phối luồng công việc (workflow engine) như Step Functions. Độ tin cậy: 0.85 - Lựa chọn container hoặc các dịch vụ chạy liên tục dài hạn cho các khối lượng công việc có thông lượng cao và ổn định, nơi mà mô hình serverless trở nên đắt đỏ hơn. Độ tin cậy: 0.75
Các quy ước giàu tính định hướng để xây dựng những tính năng thời gian thực bảo mật, có khả năng mở rộng bằng WebSockets.
# Gu thiết kế (taste) WebSocket Thời gian thực của một kỹ sư hệ thống thời gian thực - Sử dụng WebSockets thuần hoặc một thư viện mỏng nhẹ như ws cho các trường hợp đơn giản, và chỉ tìm đến Socket.IO khi bạn thực sự cần cơ chế dự phòng (fallbacks) và quản lý phòng (rooms) của nó. Độ tin cậy: 0.75 - Ưu tiên dùng Server-Sent Events (SSE) khi dữ liệu cập nhật chỉ chảy một chiều từ máy chủ xuống máy khách. Độ tin cậy: 0.85 - Xác thực ngay trong quá trình bắt tay (handshake) bằng một token có thời hạn ngắn, tuyệt đối không bao giờ tin tưởng vào user ID được gửi kèm trong một bản tin sau đó. Độ tin cậy: 0.95 - Kiểm tra header Origin trên các yêu cầu nâng cấp kết nối (upgrade requests) để chặn tấn công chiếm quyền điều khiển WebSocket liên trang (cross-site WebSocket hijacking). Độ tin cậy: 0.90 - Định nghĩa mọi bản tin dưới dạng một gói bọc có kiểu tường minh (typed envelope) gồm loại bản tin (type), phiên bản (version) và dữ liệu tải (payload), được xác thực bằng schema ở cả hai đầu. Độ tin cậy: 0.90 - Gửi tín hiệu nhịp tim (heartbeats) bằng ping và pong và đóng các kết nối bỏ lỡ chúng, vì các máy chủ proxy thường âm thầm ngắt những socket nhàn rỗi. Độ tin cậy: 0.90 - Kết nối lại ở phía máy khách theo cơ chế lùi lũy thừa kèm độ lệch ngẫu nhiên (exponential backoff and jitter) để việc khởi động lại máy chủ không gây ra hiệu ứng đàn thú chạy loạn (thundering herd). Độ tin cậy: 0.95 - Gắn cho mỗi bản tin một số thứ tự (sequence number) để máy khách có thể tiếp tục nhận từ bản tin cuối cùng mà chúng nhìn thấy sau khi kết nối lại. Độ tin cậy: 0.85 - Coi socket như một kênh thông báo và giữ cơ sở dữ liệu làm nguồn chân lý chuẩn xác (source of truth) mà máy khách có thể truy vấn lại. Độ tin cậy: 0.85 - Phân phối bản tin (fan out) qua nhiều thực thể máy chủ bằng Redis pub/sub, NATS hoặc một dịch vụ được quản lý thay vì phát quảng bá trong bộ nhớ (in-memory broadcast). Độ tin cậy: 0.90 - Giữ cho các máy chủ kết nối ở trạng thái phi trạng thái (stateless) ngoại trừ các socket đang mở, để bất kỳ thực thể nào cũng có thể được rút cạn kết nối (drain) và thay thế. Độ tin cậy: 0.80 - Rút cạn kết nối một cách êm thấm (drain gracefully) khi triển khai bản mới bằng cách báo cho máy khách chủ động kết nối lại trước khi tắt máy chủ. Độ tin cậy: 0.85 - Áp dụng cơ chế chống tràn ngược (backpressure): theo dõi bộ đệm gửi (send buffer) và loại bỏ hoặc gộp các bản cập nhật đối với những máy khách xử lý chậm thay vì tích trữ vào bộ đệm mãi mãi. Độ tin cậy: 0.85 - Giới hạn tần suất (rate limit) các bản tin gửi đến trên mỗi kết nối và giới hạn kích thước tối đa của bản tin. Độ tin cậy: 0.90 - Phân quyền cho mọi yêu cầu đăng ký (subscription) vào một phòng hoặc kênh ở phía máy chủ, chứ không chỉ kiểm tra ở lần kết nối ban đầu. Độ tin cậy: 0.95 - Gom nhóm (batch) hoặc tiết lưu (throttle) các cập nhật tần suất cao như vị trí con trỏ chuột theo một nhịp xung (tick rate) cố định. Độ tin cậy: 0.80 - Cấu hình thời gian chờ nhàn rỗi (idle timeouts) của bộ cân bằng tải và phiên kết dính (sticky sessions) một cách có chủ đích khi tầng truyền tải cần đến chúng. Độ tin cậy: 0.80 - Theo dõi số lượng kết nối đang mở, số bản tin mỗi giây, tỷ lệ kết nối lại và độ trễ chuyển phát như những chỉ số đo lường (metrics) hạng nhất. Độ tin cậy: 0.85 - Sử dụng các cấu trúc CRDT như Yjs hoặc Automerge cho tính năng chỉnh sửa cộng tác thay vì tự phát minh ra cơ chế giải quyết xung đột của riêng bạn. Độ tin cậy: 0.80 - Kiểm thử tải (load test) với số lượng kết nối sát thực tế bằng k6 hoặc Artillery trước khi ra mắt. Độ tin cậy: 0.80
Các quy ước mang tính định hướng rõ ràng để xây dựng luồng đăng nhập, phiên làm việc, xác thực đa yếu tố (MFA) và OAuth bảo mật trong các ứng dụng web.
# Gu xây dựng hệ thống Xác thực (Authentication) của một kỹ sư backend chú trọng bảo mật - Sử dụng một thư viện hoặc nhà cung cấp dịch vụ xác thực được bảo trì tốt thay vì tự viết thủ công luồng đăng nhập, quản lý phiên và xử lý token. Độ tin cậy: 0.95 - Băm (hash) mật khẩu bằng Argon2id, hoặc bcrypt với hệ số chi phí (cost) tối thiểu là 12, và không bao giờ dùng hàm băm nhanh thông thường như SHA-256. Độ tin cậy: 0.95 - Ưu tiên phiên làm việc phía máy chủ (server-side sessions) lưu trong cookie có cờ `HttpOnly`, `Secure`, `SameSite=Lax` cho các ứng dụng web bên thứ nhất (first-party) thay vì lưu JWT trong `localStorage`. Độ tin cậy: 0.90 - Sử dụng access token có thời hạn ngắn kết hợp với refresh token xoay vòng (rotating refresh tokens) khi bạn thực sự cần token phi trạng thái (stateless) cho các API hoặc ứng dụng di động. Độ tin cậy: 0.85 - Phát hiện hành vi tái sử dụng refresh token và thu hồi (revoke) toàn bộ họ token (token family) ngay khi điều đó xảy ra. Độ tin cậy: 0.85 - Tạo lại (regenerate) ID phiên làm việc khi đăng nhập, đăng xuất và khi thay đổi quyền hạn để ngăn chặn tấn công ấn định phiên (session fixation). Độ tin cậy: 0.95 - Cung cấp khóa truy cập (passkeys / WebAuthn) như một phương thức đăng nhập hạng nhất (first-class), chứ không chỉ làm yếu tố xác thực thứ hai. Độ tin cậy: 0.80 - Hỗ trợ TOTP hoặc passkey cho xác thực đa yếu tố (MFA) và chỉ coi mã SMS là phương án dự phòng cuối cùng. Độ tin cậy: 0.85 - Giới hạn tần suất (rate limit) đăng nhập, đăng ký và đặt lại mật khẩu theo từng tài khoản và từng địa chỉ IP, sử dụng thời gian chờ tăng dần theo cấp số nhân (exponential backoff) thay vì khóa cứng tài khoản vĩnh viễn. Độ tin cậy: 0.90 - Trả về cùng một thông báo chung cho cả trường hợp email không tồn tại và sai mật khẩu để kẻ tấn công không thể dò quét danh sách tài khoản (account enumeration). Độ tin cậy: 0.90 - Đảm bảo các token đặt lại mật khẩu và magic link chỉ sử dụng một lần, sinh ngẫu nhiên, được băm khi lưu trữ (hashed at rest) và hết hạn trong vòng 15 đến 60 phút. Độ tin cậy: 0.95 - Kiểm tra mật khẩu đối chiếu với danh sách các mật khẩu đã bị lộ lọt và loại bỏ các quy tắc đặt mật khẩu tùy tiện như "bắt buộc phải có một ký tự đặc biệt". Độ tin cậy: 0.85 - Sử dụng luồng OAuth 2.1 Authorization Code kết hợp PKCE cho mọi ứng dụng khách (client), và không bao giờ sử dụng luồng implicit (implicit flow). Độ tin cậy: 0.95 - Xác thực tham số `state` và `nonce` trên mọi lệnh gọi lại (callback) của OAuth và OIDC. Độ tin cậy: 0.95 - Chỉ liên kết đăng nhập mạng xã hội (social logins) với các tài khoản hiện có sau khi nhà cung cấp trả về một địa chỉ email đã được xác minh. Độ tin cậy: 0.90 - Tách biệt rõ ràng giữa xác thực (authentication - bạn là ai) và phân quyền (authorization - bạn được làm gì), đồng thời kiểm tra quyền hạn trên máy chủ đối với mọi yêu cầu (request). Độ tin cậy: 0.95 - Yêu cầu nhập lại thông tin đăng nhập hoặc xác thực lại bằng passkey trước khi thực hiện các hành động nhạy cảm như thay đổi email, mật khẩu hoặc thông tin nhận thanh toán. Độ tin cậy: 0.85 - Thông báo cho người dùng qua email khi có lượt đăng nhập từ thiết bị mới, khi thay đổi mật khẩu và khi thay đổi cài đặt MFA. Độ tin cậy: 0.80 - Cho phép người dùng xem và thu hồi các phiên làm việc đang hoạt động của họ ngay trong phần cài đặt tài khoản. Độ tin cậy: 0.80 - Ghi nhật ký (log) các sự kiện xác thực kèm theo ID người dùng, địa chỉ IP và user agent, nhưng không bao giờ ghi log mật khẩu, token hoặc đường dẫn đặt lại mật khẩu. Độ tin cậy: 0.95 - Bảo vệ các yêu cầu làm thay đổi trạng thái dựa trên cookie bằng cookie `SameSite` kết hợp với CSRF token hoặc kiểm tra header `Origin`. Độ tin cậy: 0.90 - Lưu trữ các khóa ký (signing keys) trong trình quản lý bí mật (secrets manager) và hỗ trợ xoay vòng khóa (key rotation) với header `kid` ngay từ ngày đầu tiên. Độ tin cậy: 0.85 - Viết các bài kiểm thử tích hợp (integration tests) cho các kịch bản lỗi (unhappy paths): token hết hạn, phiên bị thu hồi, đường dẫn bị phát lại (replayed links) và sai định danh tổ chức (wrong tenants). Độ tin cậy: 0.85
Các quy ước mang tính định hướng rõ ràng để xây dựng ứng dụng Ruby on Rails dễ bảo trì với Hotwire và các thiết lập mặc định hiện đại của Rails.
# Gu phát triển Ứng dụng Rails của một lập trình viên Ruby on Rails thực dụng - Bắt đầu các ứng dụng mới trên phiên bản Rails mới nhất cùng với PostgreSQL, và gắn bó với các thiết lập mặc định trừ khi có lý do rõ ràng để thay thế một thành phần nào đó. Độ tin cậy: 0.90 - Giữ cho các controller mỏng (thin) và tuân thủ chuẩn RESTful, thêm các tài nguyên mới như `SessionsController` hoặc `ArchivesController` thay vì tạo các action tùy chỉnh. Độ tin cậy: 0.90 - Đặt logic nghiệp vụ (domain logic) trong các model và các đối tượng Ruby thuần túy (PORO) dưới thư mục `app/models`, và chỉ dùng đến các service object khi một luồng công việc trải rộng qua nhiều model. Độ tin cậy: 0.75 - Sử dụng `concerns` một cách tiết chế và chỉ dành cho những hành vi thực sự được chia sẻ chung, chứ không phải để che giấu một model phình to. Độ tin cậy: 0.75 - Ưu tiên dùng Hotwire với Turbo và Stimulus cho các tính năng tương tác trước khi quyết định thêm một ứng dụng JavaScript SPA tách biệt. Độ tin cậy: 0.85 - Xác thực (validate) dữ liệu trong model và củng cố thêm bằng các ràng buộc cơ sở dữ liệu như `NOT NULL`, chỉ mục duy nhất (unique indexes) và khóa ngoại (foreign keys). Độ tin cậy: 0.95 - Viết các bản di chuyển dữ liệu (migrations) có khả năng đảo ngược (reversible), thêm chỉ mục đồng thời (`concurrently`) trên các bảng lớn, và không bao giờ chỉnh sửa một migration đã chạy trên môi trường production. Độ tin cậy: 0.90 - Giữ các callback cho việc chuẩn hóa dữ liệu đơn giản và chuyển các tác vụ phụ (side effects) như gửi email và gọi API vào các công việc chạy nền (jobs) tường minh. Độ tin cậy: 0.85 - Chạy các tác vụ nền bằng Active Job trên Solid Queue hoặc Sidekiq, và giữ cho mọi job có tính lũy đẳng (idempotent) và an toàn khi thử lại (retry). Độ tin cậy: 0.90 - Truyền ID của bản ghi vào các job thay vì truyền toàn bộ đối tượng, và nạp lại (reload) bản ghi đó bên trong job. Độ tin cậy: 0.85 - Phát hiện các truy vấn N+1 bằng `strict_loading` hoặc gem Bullet, và khắc phục chúng bằng `includes` hoặc `preload`. Độ tin cậy: 0.90 - Sử dụng strong parameters ở mọi nơi và không bao giờ truyền trực tiếp `params` vào hàm `update` hoặc `create`. Độ tin cậy: 0.95 - Lưu trữ thông tin bí mật (secrets) trong chứng chỉ mã hóa (encrypted credentials) hoặc biến môi trường, không bao giờ lưu trong các tệp YAML được commit lên kho mã nguồn. Độ tin cậy: 0.95 - Kiểm thử bằng Minitest hoặc RSpec, tập trung vào kiểm thử model và request, kết hợp với một vài kiểm thử hệ thống (system tests) cho các luồng quan trọng. Độ tin cậy: 0.80 - Sử dụng fixtures hoặc các factory gọn nhẹ, tránh tạo các chuỗi factory lồng nhau quá sâu làm khởi tạo tới nửa cơ sở dữ liệu cho mỗi bài kiểm thử. Độ tin cậy: 0.75 - Kiểm tra quy chuẩn mã nguồn (lint) bằng RuboCop sử dụng cấu hình `rubocop-rails-omakase` hoặc một bộ quy tắc nhỏ đã thống nhất, được thực thi bắt buộc trong CI. Độ tin cậy: 0.80 - Quét các lỗ hổng bảo mật bằng Brakeman và `bundler-audit` trên mỗi pull request. Độ tin cậy: 0.90 - Lưu vào bộ nhớ đệm (cache) các giao diện (views) tốn nhiều tài nguyên bằng kỹ thuật fragment caching và khóa cache dựa trên `updated_at`, sử dụng Solid Cache hoặc Redis. Độ tin cậy: 0.80 - Sử dụng các scope và đối tượng truy vấn (query objects) cho các truy vấn tái sử dụng thay vì viết các chuỗi phương thức dài lặp đi lặp lại khắp các controller. Độ tin cậy: 0.80 - Giữ số lượng gem ít và được bảo trì tốt, đồng thời nâng cấp Rails từng phiên bản phụ (minor version) một sau khi đã sửa hết các cảnh báo ngừng hỗ trợ (deprecation warnings). Độ tin cậy: 0.85 - Triển khai bằng Kamal hoặc một nền tảng được quản lý (managed platform) sử dụng Docker image được đóng gói trong CI, và chạy migrations như một bước phát hành (release step) tách biệt. Độ tin cậy: 0.80 - Sử dụng tính năng mã hóa của Active Record (Active Record encryption) cho các cột dữ liệu nhạy cảm như token và dữ liệu cá nhân. Độ tin cậy: 0.80
Các quy ước mang tính định hướng rõ ràng để xây dựng tiện ích mở rộng (extension) đa trình duyệt bảo mật theo chuẩn Manifest V3 với TypeScript.
# Gu phát triển Tiện ích mở rộng trình duyệt (Browser Extension) của một lập trình viên tiện ích mở rộng đa trình duyệt - Xây dựng trên nền tảng Manifest V3 và lên kế hoạch xoay quanh các service worker có thể bị dừng bất cứ lúc nào. Độ tin cậy: 0.95 - Sử dụng một framework như WXT hoặc Plasmo kết hợp với TypeScript và Vite để xử lý việc đóng gói (build), tải lại (reload) và tạo tệp manifest đa trình duyệt. Độ tin cậy: 0.80 - Yêu cầu ít quyền (permissions) nhất có thể, và ưu tiên `activeTab` cùng `optional_permissions` thay vì quyền truy cập host diện rộng. Độ tin cậy: 0.95 - Không bao giờ yêu cầu quyền host `all_urls` trừ khi tính năng cốt lõi thực sự cần hoạt động trên mọi trang web. Độ tin cậy: 0.90 - Không lưu giữ trạng thái quan trọng nào trong biến toàn cục (globals) của service worker mà hãy lưu trữ bền vững bằng `chrome.storage`. Độ tin cậy: 0.95 - Sử dụng `chrome.storage.session` cho các thông tin bí mật tạm thời và `chrome.storage.local` hoặc `sync` cho cài đặt người dùng, đi kèm một lớp bọc có định kiểu (typed wrapper) và giá trị mặc định. Độ tin cậy: 0.85 - Sử dụng `chrome.alarms` cho các tác vụ định kỳ thay vì dùng `setInterval` ở chế độ chạy nền (background). Độ tin cậy: 0.90 - Đăng ký tất cả các trình lắng nghe sự kiện (event listeners) ở cấp cao nhất (top level) của service worker để chúng không bị mất khi khởi động lại. Độ tin cậy: 0.90 - Giữ cho các content script nhỏ gọn và nhúng (inject) chúng bằng mã lệnh thông qua `chrome.scripting` bất cứ khi nào có thể. Độ tin cậy: 0.85 - Cô lập giao diện người dùng (UI) được nhúng bên trong một Shadow DOM để CSS của trang web và CSS của tiện ích mở rộng không thể xung đột với nhau. Độ tin cậy: 0.90 - Coi mọi thông điệp (message) gửi từ content script và các trang web là không đáng tin cậy và phải xác thực (validate) chúng bằng một lược đồ (schema). Độ tin cậy: 0.95 - Định nghĩa các kiểu thông điệp (message types) trong một module dùng chung duy nhất và sử dụng các hàm hỗ trợ request/response có định kiểu cho việc truyền thông điệp lúc chạy (runtime messaging). Độ tin cậy: 0.85 - Không bao giờ nhúng mã nguồn từ xa (remote code) hoặc sử dụng `eval`, đồng thời duy trì Chính sách Bảo mật Nội dung (Content Security Policy - CSP) nghiêm ngặt. Độ tin cậy: 0.95 - Sử dụng `declarativeNetRequest` để chặn hoặc sửa đổi các yêu cầu mạng thay vì cố gắng chặn bắt chúng bằng mã lệnh. Độ tin cậy: 0.85 - Sử dụng `webextension-polyfill` hoặc lớp trừu tượng của framework để cùng một mã nguồn có thể chạy được trên Chrome, Firefox, Edge và Safari. Độ tin cậy: 0.80 - Thu thập ít dữ liệu người dùng nhất có thể và giải thích rõ ràng trên trang thông tin cửa hàng (store listing) về những gì được lưu trữ và lý do lưu trữ. Độ tin cậy: 0.90 - Xử lý các sự kiện cài đặt mới (`install`) và cập nhật (`update`) để di chuyển (migrate) các cài đặt đã lưu giữa các phiên bản. Độ tin cậy: 0.85 - Kiểm thử trang popup và trang tùy chọn (options) như giao diện web thông thường, và chạy kiểm thử đầu-cuối (end-to-end) bằng Playwright với tiện ích mở rộng được nạp ở dạng chưa đóng gói (unpacked). Độ tin cậy: 0.80 - Tự động hóa quá trình build, tăng số phiên bản (version bump) và tải lên cửa hàng trong CI, với một gói tệp zip đầu ra riêng cho từng trình duyệt. Độ tin cậy: 0.80 - Giữ cho cửa sổ popup phản hồi nhanh bằng cách tải trễ (lazy load) các dữ liệu nặng và hiển thị trạng thái đã lưu trong bộ nhớ đệm (cached state) trước. Độ tin cậy: 0.80 - Viết một đoạn giải trình quyền truy cập ngắn gọn cho từng quyền trước khi gửi xét duyệt lên cửa hàng. Độ tin cậy: 0.85
Các quy ước thực tế để nhận và gửi webhook an toàn, đáng tin cậy giữa các dịch vụ.
# Tích hợp Webhook taste của một kỹ sư tích hợp chú trọng độ tin cậy - Xác minh mọi webhook gửi đến bằng chữ ký HMAC trên phần thân request thô (raw request body) trước khi phân tích cú pháp thành JSON. Độ tin cậy: 0.95 - So sánh chữ ký bằng hàm có thời gian thực thi hằng số (constant-time function), không bao giờ dùng phép kiểm tra bằng nhau trên chuỗi thông thường. Độ tin cậy: 0.95 - Từ chối các lượt gửi có dấu thời gian đã ký cũ hơn khoảng năm phút để chặn tấn công phát lại (replay attacks). Độ tin cậy: 0.90 - Phản hồi mã trạng thái 2xx trong vòng vài giây và thực hiện công việc xử lý thực sự trong một tác vụ nền (background job). Độ tin cậy: 0.95 - Lưu trữ bền vững sự kiện thô trước rồi mới xử lý, để không mất dữ liệu nếu worker bị sập giữa chừng. Độ tin cậy: 0.90 - Khử trùng lặp dựa trên event ID của nhà cung cấp bằng chỉ mục duy nhất (unique index), vì mọi nhà cung cấp đều thử gửi lại và phân phối theo cơ chế ít nhất một lần (at-least-once). Độ tin cậy: 0.95 - Không bao giờ giả định các sự kiện đến đúng thứ tự, hãy so sánh trường version hoặc updated_at trước khi ghi đè trạng thái cục bộ. Độ tin cậy: 0.85 - Coi payload như một gợi ý và truy vấn lại đối tượng hiện tại từ API của nhà cung cấp đối với bất kỳ tác vụ nào liên quan đến tiền bạc. Độ tin cậy: 0.80 - Chỉ trả về mã 4xx cho các request lỗi vĩnh viễn và 5xx cho các lỗi tạm thời để cơ chế thử lại của bên gửi hoạt động chính xác. Độ tin cậy: 0.85 - Hỗ trợ đồng thời ít nhất hai khóa bí mật ký (signing secrets) đang hoạt động để có thể xoay vòng khóa mà không gây gián đoạn dịch vụ (downtime). Độ tin cậy: 0.85 - Khi gửi webhook đi, hãy thử lại với thời gian chờ tăng theo hàm mũ kèm độ nhiễu ngẫu nhiên (exponential backoff and jitter) trong tối đa một ngày hoặc lâu hơn, sau đó vô hiệu hóa endpoint và thông báo cho chủ sở hữu. Độ tin cậy: 0.90 - Gửi một payload gọn nhẹ, có đánh phiên bản kèm theo loại sự kiện (event type), event ID, dấu thời gian và ID tài nguyên thay vì đổ toàn bộ dữ liệu dòng trong cơ sở dữ liệu ra. Độ tin cậy: 0.80 - Đặt tên loại sự kiện theo dạng resource.action ở thì quá khứ, chẳng hạn như invoice.paid hoặc user.deleted. Độ tin cậy: 0.80 - Cung cấp cho khách hàng nhật ký gửi (delivery log) kèm theo request, response, trạng thái và nút phát lại (replay) thủ công. Độ tin cậy: 0.85 - Chặn các URL webhook gửi đi phân giải về địa chỉ mạng riêng tư (private), loopback hoặc link-local để ngăn chặn lỗ hổng SSRF. Độ tin cậy: 0.95 - Sử dụng thời gian chờ (timeout) ngắn và tách biệt cho các lượt gửi đi, đồng thời không bao giờ để một endpoint chậm của khách hàng làm nghẽn hàng đợi dùng chung. Độ tin cậy: 0.90 - Bắt buộc sử dụng HTTPS cho tất cả các webhook endpoint ngoài môi trường phát triển cục bộ. Độ tin cậy: 0.90 - Cung cấp API đối soát hoặc cơ chế đồng bộ định kỳ để phía nhận có thể khôi phục lại các sự kiện bị bỏ lỡ. Độ tin cậy: 0.80 - Kiểm thử các bộ xử lý (handlers) ở môi trường cục bộ bằng CLI của nhà cung cấp hoặc đường hầm (tunnel), và lưu lại các payload mẫu đã ghi nhận làm dữ liệu kiểm thử cố định (test fixtures). Độ tin cậy: 0.80 - Cảnh báo khi tỷ lệ lỗi tăng cao và hàng đợi tồn đọng ngày càng lớn trên từng endpoint, chứ không chỉ cảnh báo trên từng lỗi đơn lẻ. Độ tin cậy: 0.80 - Không bao giờ ghi log toàn bộ payload có chứa dữ liệu cá nhân, thông tin bí mật (secrets) hoặc thông tin thẻ thanh toán. Độ tin cậy: 0.90
Các quy ước thực tế để xây dựng ứng dụng di động đa nền tảng nhanh và dễ bảo trì bằng Flutter.
# Ứng dụng Flutter taste của một lập trình viên di động đa nền tảng thực dụng - Bắt đầu trên phiên bản Flutter ổn định mới nhất với null safety, các tùy chọn phân tích nghiêm ngặt (strict analysis options) và bộ quy tắc lint chính thức hoặc very_good_analysis được kiểm tra bắt buộc trong CI. Độ tin cậy: 0.90 - Tổ chức code theo các thư mục tính năng (feature folders), mỗi thư mục có các tầng data, domain và presentation riêng, thay vì gom tất cả vào một thư mục widgets khổng lồ. Độ tin cậy: 0.85 - Sử dụng Riverpod hoặc Bloc để quản lý trạng thái và chọn một giải pháp thống nhất cho toàn bộ ứng dụng thay vì trộn lẫn nhiều thư viện. Độ tin cậy: 0.80 - Giữ các widget nhỏ gọn và có tính kết hợp (composable), tách thành một lớp widget mới thay vì dùng phương thức phụ trợ (helper method) trả về widget. Độ tin cậy: 0.85 - Đánh dấu const cho các widget và constructor ở mọi nơi có thể để giảm thiểu việc vẽ lại (rebuild) không cần thiết. Độ tin cậy: 0.90 - Tách hoàn toàn logic nghiệp vụ ra khỏi phương thức build và ra khỏi các widget. Độ tin cậy: 0.90 - Sử dụng go_router để điều hướng khai báo (declarative navigation) với các route có định kiểu (typed routes) và hỗ trợ deep link hoạt động chuẩn. Độ tin cậy: 0.80 - Mô hình hóa dữ liệu bằng các lớp bất biến (immutable classes) được sinh tự động bởi freezed và json_serializable thay vì tự viết tay copyWith và fromJson. Độ tin cậy: 0.80 - Bọc truy cập API trong các lớp repository và trả về kết quả có kiểu dữ liệu hoặc ngoại lệ rõ ràng, không bao giờ trả trực tiếp map phản hồi thô cho tầng UI. Độ tin cậy: 0.85 - Xây dựng giao diện (theme) một lần duy nhất bằng ThemeData và ColorScheme từ Material 3, và không bao giờ gán cứng (hard-code) màu sắc hoặc kiểu chữ bên trong các widget. Độ tin cậy: 0.85 - Hỗ trợ chế độ tối (dark mode), co giãn kích thước chữ động và trình đọc màn hình (screen readers) ngay từ bản phát hành đầu tiên. Độ tin cậy: 0.85 - Sử dụng flutter_localizations với các file ARB cho mọi chuỗi văn bản hiển thị tới người dùng ngay từ ngày đầu. Độ tin cậy: 0.80 - Chạy các tác vụ phân tích cú pháp nặng hoặc xử lý ảnh trong một isolate riêng bằng compute hoặc Isolate.run để luồng UI luôn mượt mà. Độ tin cậy: 0.85 - Đo đạc hiệu năng bằng DevTools ở chế độ profile trên thiết bị thật cấu hình thấp trước khi kết luận một màn hình là nhanh. Độ tin cậy: 0.85 - Sử dụng ListView.builder hoặc slivers cho các danh sách dài và không bao giờ khởi tạo sẵn hàng trăm phần tử con cùng lúc. Độ tin cậy: 0.90 - Lưu trữ token bằng flutter_secure_storage và giữ các cài đặt đơn giản trong shared_preferences. Độ tin cậy: 0.90 - Viết unit test cho phần logic, widget test cho các màn hình và một số integration test cho các luồng quan trọng. Độ tin cậy: 0.80 - Sử dụng golden test cho các widget thuộc hệ thống thiết kế (design-system) cần đảm bảo độ ổn định từng điểm ảnh. Độ tin cậy: 0.70 - Ghim cố định phiên bản các thư viện phụ thuộc, đánh giá các gói mới về mức độ bảo trì và hỗ trợ nền tảng, đồng thời giữ danh sách phụ thuộc ngắn gọn. Độ tin cậy: 0.80 - Định nghĩa các flavor cho môi trường development, staging và production với app ID và cấu hình tách biệt. Độ tin cậy: 0.85 - Đóng gói và phát hành bản dựng bằng Fastlane hoặc Codemagic từ hệ thống CI, không bao giờ phát hành thủ công từ máy tính cá nhân của lập trình viên. Độ tin cậy: 0.80 - Tích hợp Crashlytics hoặc Sentry cùng bản dựng đã làm rối mã (obfuscated builds) và tải lên các file symbol. Độ tin cậy: 0.80
Các quy ước thực tế để xây dựng ứng dụng Laravel dễ bảo trì với các thiết lập mặc định hiện đại của PHP và Laravel.
# Ứng dụng Laravel taste của một lập trình viên web PHP thực dụng - Bắt đầu trên phiên bản Laravel và PHP mới nhất với khai báo kiểu nghiêm ngặt (strict types) trong mọi file. Độ tin cậy: 0.85 - Giữ controller mỏng và theo chuẩn resourceful, sử dụng single-action invokable controller cho bất kỳ tác vụ nào không phải CRUD. Độ tin cậy: 0.85 - Xác thực dữ liệu đầu vào trong các lớp Form Request và phân quyền bằng Policy thay vì kiểm tra trực tiếp trong controller. Độ tin cậy: 0.90 - Đặt logic nghiệp vụ trong các lớp action nhỏ như CreateInvoice, không đặt trong controller, model hay route closure. Độ tin cậy: 0.75 - Sử dụng Eloquent với các trường $fillable hoặc casts tường minh, và không bao giờ gọi Model::unguard trong code production. Độ tin cậy: 0.85 - Bật Model::preventLazyLoading ngoài môi trường production để phát hiện sớm truy vấn N+1 và khắc phục bằng eager loading. Độ tin cậy: 0.90 - Đảm bảo mọi quan hệ đều có khóa ngoại (foreign key) và chỉ mục duy nhất (unique index) trong migration, chứ không chỉ dựa vào quy tắc của Eloquent. Độ tin cậy: 0.90 - Viết các migration an toàn khi chạy tiến (roll forward) và không bao giờ sửa đổi một migration đã chạy trên production. Độ tin cậy: 0.90 - Định hình phản hồi API bằng API Resources để cấu trúc cơ sở dữ liệu không bao giờ bị lộ trực tiếp ra JSON. Độ tin cậy: 0.85 - Đẩy các tác vụ chậm như gửi email, xuất dữ liệu và gọi dịch vụ bên thứ ba vào các hàng đợi (queued jobs) chạy dưới Horizon, đồng thời đảm bảo mọi job đều có tính idempotent. Độ tin cậy: 0.90 - Chỉ sử dụng event và listener cho các tác dụng phụ (side effects) thực sự, không dùng để che giấu luồng xử lý chính của một request. Độ tin cậy: 0.75 - Đọc cấu hình thông qua config() và chỉ giữ các lệnh gọi env() bên trong các file config để tính năng lưu bộ nhớ đệm cấu hình (config caching) hoạt động. Độ tin cậy: 0.95 - Chạy php artisan optimize, config:cache và route:cache như một phần của mọi quy trình triển khai (deploy). Độ tin cậy: 0.85 - Sử dụng Sanctum cho xác thực SPA chính chủ (first-party) và ứng dụng di động, và chỉ dùng đến Passport khi bạn cần OAuth đầy đủ. Độ tin cậy: 0.80 - Kiểm thử bằng Pest hoặc PHPUnit sử dụng feature test trên cơ sở dữ liệu thực kết hợp với RefreshDatabase. Độ tin cậy: 0.85 - Tạo dữ liệu kiểm thử bằng model factory và state thay vì các file seed dùng chung kích thước lớn. Độ tin cậy: 0.80 - Áp đặt chuẩn định dạng code bằng Laravel Pint và phân tích tĩnh bằng Larastan ở mức 6 trở lên trong CI. Độ tin cậy: 0.85 - Ưu tiên sử dụng Livewire hoặc Inertia cùng các bộ starter kit của Laravel trước khi xây dựng một SPA tách biệt. Độ tin cậy: 0.75 - Giới hạn tần suất (rate limit) cho các route đăng nhập và API công khai bằng các bộ giới hạn tần suất được đặt tên (named rate limiters). Độ tin cậy: 0.90 - Lưu trữ file tải lên thông qua Storage facade trên các ổ đĩa tương thích S3 và phục vụ các file riêng tư qua URL có chữ ký tạm thời (temporary signed URLs). Độ tin cậy: 0.85 - Lên lịch các tác vụ định kỳ trong bộ lập lịch (scheduler) với withoutOverlapping và onOneServer, thay vì viết trực tiếp trong crontab thô. Độ tin cậy: 0.85 - Giữ số lượng gói thư viện (packages) ít và được bảo trì tốt, đồng thời nâng cấp từng phiên bản chính (major version) của Laravel một theo hướng dẫn nâng cấp chính thức. Độ tin cậy: 0.80