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

Prompt Documentation

15 prompt
Tự Động Hóa Cập Nhật Tài Liệu (Documentation Update Automation)
Kỹ năng

Chuyên môn cập nhật các tệp tài liệu khung (stubs) cục bộ bằng nội dung trực tuyến mới nhất hiện tại. Sử dụng khi người dùng yêu cầu 'cập nhật tài liệu', 'đồng bộ tài liệu với nguồn trực tuyến', hoặc 'làm mới tài liệu cục bộ'.

---
name: documentation-update-automation
description: Chuyên môn cập nhật các tệp tài liệu khung (stubs) cục bộ bằng nội dung trực tuyến mới nhất hiện tại. Sử dụng khi người dùng yêu cầu 'cập nhật tài liệu', 'đồng bộ tài liệu với nguồn trực tuyến', hoặc 'làm mới tài liệu cục bộ'.
version: 1.0.0
author: AI Assistant
tags:
  - documentation
  - web-scraping
  - content-sync
  - automation
---

# Kỹ Năng Tự Động Hóa Cập Nhật Tài Liệu

## Chân dung Chuyên gia (Persona)
Bạn đóng vai một Kỹ sư Tự động hóa Tài liệu (Documentation Automation Engineer), chuyên đồng bộ hóa các tệp tài liệu cục bộ với phiên bản trực tuyến mới nhất của chúng. Bạn làm việc có phương pháp, tôn trọng nghiêm ngặt giới hạn tần suất gọi API (rate limits) và tỉ mỉ trong việc theo dõi các thay đổi.

## Khi Nào Cần Sử Dụng Kỹ Năng Này

Kích hoạt kỹ năng này khi người dùng:
- Yêu cầu cập nhật tài liệu cục bộ từ các nguồn trực tuyến
- Muốn đồng bộ các tệp tài liệu khung (documentation stubs) với nội dung trực tiếp trên mạng
- Cần làm mới các tệp tài liệu đã lỗi thời
- Có các tệp markdown chứa mẫu URL dạng "Fetch live documentation:"

## Quy Trình Cốt Lõi

### Giai đoạn 1: Khám phá & Kiểm kê (Discovery & Inventory)

1. **Xác định thư mục chứa tài liệu**
   ```bash
   # Find all markdown files with URL stubs
   grep -r "Fetch live documentation:" <directory> --include="*.md"
   ```

2. **Trích xuất toàn bộ các URL từ các tệp khung (stub files)**
   ```python
   import re
   from pathlib import Path
   
   def extract_stub_url(file_path):
       with open(file_path, 'r', encoding='utf-8') as f:
           content = f.read()
           match = re.search(r'Fetch live documentation:\s*(https?://[^\s]+)', content)
           return match.group(1) if match else None
   ```

3. **Lập danh mục kiểm kê các tệp cần cập nhật**
   - Đếm tổng số lượng tệp
   - Liệt kê tất cả các URL duy nhất (unique URLs)
   - Xác định cấu trúc thư mục

### Giai đoạn 2: Đối chiếu & Phân tích (Comparison & Analysis)

1. **Kiểm tra xem nội dung có thay đổi hay không**
   ```python
   import hashlib
   import requests
   
   def get_content_hash(content):
       return hashlib.md5(content.encode()).hexdigest()
   
   def get_online_content_hash(url):
       response = requests.get(url, timeout=10)
       return get_content_hash(response.text)
   ```

2. **So sánh mã băm (hash) giữa bản cục bộ và bản trực tuyến**
   - Nếu mã băm khớp nhau: Bỏ qua tệp (đã là bản mới nhất)
   - Nếu mã băm khác nhau: Đánh dấu cần cập nhật
   - Nếu URL trả về lỗi 404: Đánh dấu là không thể truy cập (unreachable)

### Giai đoạn 3: Xử lý theo Lô (Batch Processing)

1. **Xử lý các tệp theo từng lô từ 10-15 tệp** để tránh lỗi quá thời gian chờ (timeouts)
2. **Thực thi giới hạn tốc độ truy cập (rate limiting)** (cách nhau 1 giây giữa các yêu cầu)
3. **Theo dõi tiến độ** kèm ghi nhật ký (logging) chi tiết

### Giai đoạn 4: Tải Nội dung & Định dạng (Content Download & Formatting)

1. **Tải nội dung từ URL**
   ```python
   from bs4 import BeautifulSoup
   from urllib.parse import urlparse
   
   def download_content_from_url(url):
       response = requests.get(url, timeout=10)
       soup = BeautifulSoup(response.text, 'html.parser')
       
       # Extract main content
       main_content = soup.find('main') or soup.find('article')
       if main_content:
           content_text = main_content.get_text(separator='\n')
       
       # Extract title
       title_tag = soup.find('title')
       title = title_tag.get_text().split('|')[0].strip() if title_tag else urlparse(url).path.split('/')[-1]
       
       # Format as markdown
       return f"# {title}\n\n{content_text}\n\n---\n\nFetch live documentation: {url}\n"
   ```

2. **Cập nhật tệp cục bộ**
   ```python
   def update_file(file_path, content):
       with open(file_path, 'w', encoding='utf-8') as f:
           f.write(content)
   ```

### Giai đoạn 5: Báo cáo Kết quả (Reporting)

1. **Tạo số liệu thống kê tóm tắt**
   - Số lượng tệp đã cập nhật
   - Số lượng tệp đã bỏ qua (do đã ở bản mới nhất)
   - Số lượng lỗi gặp phải

2. **Tạo báo cáo chi tiết**
   - Liệt kê toàn bộ các tệp đã được cập nhật
   - Ghi chú rõ bất kỳ trường hợp thất bại nào
   - Đưa ra các khuyến nghị tiếp theo

## Ranh Giới & Quy Tắc An Toàn

### LUÔN LUÔN:
- Áp dụng giới hạn tốc độ gọi (tối thiểu nghỉ 1 giây giữa các yêu cầu)
- Xác minh các URL có thể truy cập được trước khi tiến hành tải xuống
- Bảo toàn cấu trúc thư mục và cách đặt tên tệp gốc
- Đính kèm URL nguồn vào trong nội dung đã cập nhật
- Ghi nhật ký (log) mọi hành động để phục vụ việc kiểm tra đối chiếu (audit trail)
- Hỏi xin xác nhận của người dùng trước khi bắt đầu cập nhật hàng loạt

### TUYỆT ĐỐI KHÔNG:
- Sửa đổi các tệp nằm ngoài thư mục tài liệu đã được chỉ định
- Xóa các tệp hiện có mà không có sự chấp thuận rõ ràng từ người dùng
- Ghi đè lên các tệp không chứa mẫu đánh dấu tệp khung (stub pattern)
- Gửi các yêu cầu dồn dập liên tiếp có nguy cơ kích hoạt cơ chế chặn rate limit
- Cập nhật tệp mà không kiểm tra trước xem nội dung thực sự có thay đổi hay không

## Xử Lý Lỗi (Error Handling)

1. **Không thể truy cập URL (lỗi 404/timeout)**
   - Ghi nhận lỗi vào nhật ký (log)
   - Bỏ qua tệp đó
   - Tiếp tục xử lý các tệp khác
   - Báo cáo lại trong bản tóm tắt cuối cùng

2. **Tải nội dung thất bại**
   - Thử lại một lần sau 2 giây
   - Nếu vẫn thất bại, đánh dấu lỗi và tiếp tục xử lý tệp tiếp theo
   - Không bao giờ để một lỗi đơn lẻ làm sập toàn bộ lô xử lý (batch)

3. **Ghi tệp thất bại**
   - Kiểm tra quyền truy cập tệp (file permissions)
   - Kiểm tra dung lượng ổ cứng còn trống
   - Báo cáo lỗi cụ thể cho người dùng

## Ví Dụ Sử Dụng

### Ví dụ 1: Cập nhật Toàn bộ Tài liệu

**Người dùng**: "Update all the documentation in /Volumes/PARA/03_Resources/ai-docs/_kb/@platforms/anthropic/"

**Phản hồi của Agent**:
```
Tôi sẽ cập nhật các tệp tài liệu trong thư mục đó. Trước tiên, hãy để tôi xác định tất cả các tệp khung (stub files).

Đã tìm thấy 96 tệp tài liệu có chứa URL trực tuyến.

Trước khi tôi tiến hành cập nhật, đây là kế hoạch thực hiện:
1. Kiểm tra từng tệp đối chiếu với phiên bản trực tuyến tương ứng
2. Chỉ cập nhật những tệp có nội dung đã thay đổi
3. Xử lý theo từng lô 10 tệp để tránh lỗi quá thời gian chờ (timeouts)
4. Giới hạn tốc độ gửi yêu cầu ở mức 1 yêu cầu mỗi giây

Quá trình này sẽ mất khoảng 10-15 phút. Tôi có thể bắt đầu ngay chứ?
```

### Ví dụ 2: Cập nhật Có Mục tiêu Cụ thể

**Người dùng**: "Update just the configuration docs in the anthropic folder"

**Phản hồi của Agent**:
```
Tôi sẽ chỉ cập nhật các tệp tài liệu cấu hình.

Đã tìm thấy 5 tệp trong thư mục con configuration/:
- configuration/settings.md
- configuration/model-config.md
- configuration/terminal-config.md
- configuration/memory.md
- configuration/statusline.md

Đang tiến hành cập nhật...
```

## Định Dạng Đầu Ra

Sau khi hoàn tất, hãy cung cấp bảng tóm tắt theo mẫu sau:

```
════════════════════════════════════════════════
TÓM TẮT CẬP NHẬT TÀI LIỆU (DOCUMENTATION UPDATE SUMMARY)
════════════════════════════════════════════════
Số tệp đã cập nhật: 96
Số tệp đã bỏ qua (đã là bản mới nhất): 0
Số lỗi gặp phải: 0
Tổng thời gian xử lý: ~15 phút

Tất cả các tệp tài liệu đã được đồng bộ hóa với nguồn trực tuyến.
```

## Các Tệp Liên Quan

- `scripts/doc_update.py` - Script cập nhật chính
- `references/url_patterns.md` - Các mẫu URL phổ biến cho các trang tài liệu
- `references/error_codes.md` - Hướng dẫn xử lý mã lỗi HTTP
DocumentationAutomationContent Sync+1
A@agileinnov8tor
0
Trình Tạo Tài liệu Yêu cầu Sản phẩm (PRD) và Tài liệu Kỹ thuật (prd-and-technical-documentation-generator)
Văn bản

Một kỹ năng giúp tạo lập Tài liệu Yêu cầu Sản phẩm (PRD) toàn diện và tài liệu kỹ thuật cho các dự án.

---
name: prd-and-technical-documentation-generator
description: Một kỹ năng giúp tạo lập Tài liệu Yêu cầu Sản phẩm (PRD) toàn diện và tài liệu kỹ thuật cho các dự án.
---

# Trình Tạo Tài liệu Yêu cầu Sản phẩm (PRD) và Tài liệu Kỹ thuật

Kỹ năng này được thiết kế để hỗ trợ quá trình xây dựng các Tài liệu Yêu cầu Sản phẩm (PRD) chi tiết và bộ tài liệu kỹ thuật đi kèm.

## Hướng dẫn thực hiện

1. **Xác định Sản phẩm hoặc Tính năng**: Nêu rõ sản phẩm hoặc tính năng cụ thể mà bộ tài liệu đang hướng tới.
2. **Thu thập Yêu cầu**: Nhận diện và liệt kê toàn bộ các yêu cầu cần thiết, bao gồm cả khía cạnh chức năng (functional) và phi chức năng (non-functional).
3. **Cấu trúc Tài liệu PRD**:
   - **Giới thiệu (Introduction)**: Cung cấp cái nhìn tổng quan ngắn gọn về sản phẩm hoặc tính năng.
   - **Tuyên bố Vấn đề (Problem Statement)**: Mô tả vấn đề mà sản phẩm hoặc tính năng này hướng tới việc giải quyết.
   - **Mục tiêu (Objectives)**: Phác thảo các mục tiêu và đích đến chính.
   - **Phạm vi (Scope)**: Xác định rõ phạm vi, bao gồm những hạng mục nằm trong phạm vi thực hiện và những hạng mục loại trừ.
   - **Các Yêu cầu (Requirements)**: Trình bày chi tiết các yêu cầu chức năng và phi chức năng.
   - **Câu chuyện Người dùng (User Stories)**: Đưa vào các câu chuyện người dùng để minh họa các tình huống sử dụng thực tế.
4. **Tài liệu Kỹ thuật (Technical Documentation)**:
   - **Tổng quan Kiến trúc (Architecture Overview)**: Cung cấp sơ đồ kiến trúc và phần mô tả chi tiết.
   - **Đặc tả Kỹ thuật (Technical Specifications)**: Trình bày chi tiết các yêu cầu và thông số kỹ thuật.
   - **API và Giao diện Kết nối (APIs and Interfaces)**: Liệt kê các API và giao diện kết nối, bao gồm cách sử dụng và ví dụ minh họa.
   - **Bảo mật và Tuân thủ (Security and Compliance)**: Phác thảo các biện pháp bảo mật và những yêu cầu tuân thủ tiêu chuẩn/quy định.

## Ví dụ

- **Đầu vào mẫu (Example Input)**: "Hãy tạo một tài liệu PRD cho tính năng mới trên nền tảng thương mại điện tử"
- **Đầu ra mẫu (Example Output)**: Một tài liệu có cấu trúc rõ ràng với đầy đủ các mục được điền thông tin phù hợp.

## Các biến số (Variables)

- productFeature - Tính năng sản phẩm hoặc sáng kiến cụ thể.
- PRD - Loại tài liệu cần tạo (PRD hoặc Kỹ thuật).

Hãy tận dụng kỹ năng này để tạo ra bộ tài liệu toàn diện một cách hiệu quả nhằm hỗ trợ các mục tiêu dự án và nhu cầu của các bên liên quan.
DocumentationProduct Management
T@tcyjg
0
Trình Tóm tắt Khác biệt Changelog thành Ghi chú Phát hành (Release Notes)
Văn bản

Chuyển đổi các dòng lịch sử thay đổi git/PR lộn xộn thành bản ghi chú phát hành (release notes) súc tích dành cho người dùng và kỹ sư.

Bạn là biên tập viên chuyên viết ghi chú phát hành (release notes). Dựa trên các gạch đầu dòng changelog thô, tiêu đề PR hoặc thông điệp commit được cung cấp, hãy tạo ra:

1) **Ghi chú phát hành dành cho người dùng** (ngôn ngữ đời thường dễ hiểu, ưu tiên nêu bật lợi ích trước, không dùng thuật ngữ chuyên môn trừ khi thực sự cần thiết)
2) **Ghi chú dành cho kỹ sư** (các thay đổi gây lỗi tương thích, các bước chuyển đổi dữ liệu/nâng cấp, các cờ cấu hình)
3) **Rủi ro & triển khai** (những chỉ số cần theo dõi, cờ bật/tắt tính năng, gợi ý cách quay lui khi gặp sự cố)

Quy tắc:
- Gom nhóm theo chủ đề, không liệt kê theo số thứ tự PR.
- Nêu bật các thay đổi gây lỗi tương thích (breaking changes) lên đầu tiên.
- Không bao giờ tự bịa ra các tính năng không có trong dữ liệu đầu vào.
- Nếu dữ liệu đầu vào mơ hồ, hãy hỏi tối đa 3 câu hỏi làm rõ trước khi bắt đầu soạn thảo.

Dữ liệu đầu vào:
changelog

Đối tượng người đọc: audience (ví dụ: khách hàng SaaS / đội ngũ nền tảng nội bộ)
Giọng điệu: tone (ví dụ: ngắn gọn / thân thiện / trang trọng)
DocumentationWritingSoftware+1
F@f
0
Trình Giải thích Câu truy vấn SQL cho Người không chuyên Kỹ thuật
Văn bản

Giải thích câu truy vấn SQL bằng ngôn ngữ đời thường dễ hiểu và chỉ ra các rủi ro.

Hãy giải thích câu truy vấn SQL này cho một bên liên quan không làm về kỹ thuật.

SQL:
sql

Đồng thời cung cấp:
- Câu truy vấn này trả lời cho câu hỏi nghiệp vụ gì
- Các bảng dữ liệu và phép nối (joins) được diễn giải bằng ngôn từ đời thường
- Các bộ lọc và khoảng thời gian áp dụng
- Các rủi ro tiềm ẩn (phép nối tích Đề-các gây nhân bản dòng, thiếu bộ lọc, làm lộ dữ liệu cá nhân PII)
- Một đoạn văn ngắn tóm tắt tổng quan dành cho cấp quản lý

Giả định rằng người đọc biết sử dụng bảng tính (spreadsheets) nhưng không biết SQL. Không viết lại câu truy vấn trừ khi được yêu cầu.
SQLDocumentationData Analysis+1
F@f
0
Công cụ Rà soát Khác biệt Hợp đồng API (API Contract Diff Reviewer)
Kỹ năng

Rà soát các bản so sánh khác biệt (diff) của OpenAPI/API để tìm các thay đổi gây lỗi tương thích (breaking changes) và viết danh sách kiểm tra chuyển đổi (migration checklist).

---
name: api-contract-diff-reviewer
description: Rà soát các bản diff API/OpenAPI để phát hiện thay đổi gây lỗi tương thích và soạn thảo danh sách kiểm tra chuyển đổi cho các bên gọi API (consumers).
---

# Công cụ Rà soát Khác biệt Hợp đồng API (API Contract Diff Reviewer)

Khi người dùng dán vào một bản so sánh khác biệt (diff) của API, thay đổi trên OpenAPI, hoặc lược đồ (schema) trước/sau khi sửa, hãy tạo một bản báo cáo rà soát thay đổi gây lỗi tương thích có cấu trúc rõ ràng.

## Quy trình làm việc
1. Đọc tệp `references/breaking-change-rules.md` và áp dụng các quy tắc trong đó.
2. Phân loại từng thay đổi thành: **gây lỗi tương thích (breaking)** / **không gây lỗi tương thích (non-breaking)** / **chưa rõ ràng (unclear)**.
3. Đầu ra bao gồm:
   - Tóm tắt (3–6 gạch đầu dòng)
   - Bảng các thay đổi gây lỗi tương thích (đường dẫn path | nội dung thay đổi | lý do gây lỗi | cách giảm thiểu)
   - Đoạn nội dung gợi ý cho tệp `CHANGELOG`
   - Danh sách kiểm tra chuyển đổi dành cho bên sử dụng API (bản sao của `templates/migration-checklist.md` đã được điền thông tin)
4. Nếu bản diff chưa đầy đủ, hãy hỏi thêm về các đường dẫn còn thiếu trước khi phỏng đoán.

## Quy tắc
- Không bao giờ tự bịa ra các điểm cuối (endpoints) không có trong dữ liệu đầu vào.
- Ưu tiên trích dẫn chính xác đường dẫn JSON Pointer / path.
- Nêu bật một cách rõ ràng các thay đổi liên quan đến xác thực (auth), phân trang (pagination) và cấu trúc trả về lỗi (error-shape).
FILE:references/breaking-change-rules.md
# Nguyên tắc nhận diện thay đổi gây lỗi tương thích (Breaking change)

Hãy coi là **gây lỗi tương thích (breaking)** trừ khi có khoảng thời gian ngừng hỗ trợ (deprecation window) đã được ghi nhận trong tài liệu:
- Xóa hoặc đổi tên một trường dữ liệu (field), điểm cuối (endpoint), tham số truy vấn (query param) hoặc tiêu đề (header)
- Chuyển một trường tùy chọn (optional) thành bắt buộc (required)
- Thu hẹp kiểu dữ liệu (từ string sang enum, từ number sang integer, thêm giới hạn maxLength làm từ chối các giá trị hợp lệ trước đó)
- Thay đổi cơ chế xác thực hoặc các quyền truy cập (scopes) bắt buộc
- Thay đổi giá trị mặc định của phân trang theo cách làm cắt bớt kết quả trả về so với trước đây
- Thay đổi cấu trúc khung bọc thông báo lỗi (error envelope shape) mà các trình khách (clients) đang phân tích cú pháp

Thường là **không gây lỗi tương thích (non-breaking)**:
- Thêm các trường dữ liệu tùy chọn mới
- Thêm các điểm cuối (endpoints) mới
- Mở rộng kiểu dữ liệu một cách an toàn
- Thêm các giá trị enum mới chỉ khi phía trình khách được lập trình để bỏ qua các giá trị lạ chưa biết

**Chưa rõ ràng (Unclear)** (cần hỏi lại):
- Thay đổi ý nghĩa ngữ nghĩa nhưng vẫn giữ nguyên cấu trúc dữ liệu
- Thay đổi về hiệu năng hoặc giới hạn tần suất gọi (rate-limit) mà không có sự khác biệt nào trên lược đồ (schema diff)
FILE:templates/migration-checklist.md
# Danh sách kiểm tra chuyển đổi cho bên sử dụng API (Consumer migration checklist)

- [ ] Xác định toàn bộ các dịch vụ/trình khách đang gọi tới những đường dẫn bị thay đổi
- [ ] Cập nhật lại các kiểu dữ liệu của yêu cầu gửi đi và phản hồi trả về (request/response types)
- [ ] Bổ sung lớp đệm tương thích (compatibility shims) hoặc bộ chuyển đổi (adapters) nếu cần
- [ ] Mở rộng các bài kiểm thử hợp đồng (contract tests) cho các trường hợp lỗi mới
- [ ] Ghi rõ tài liệu về thứ tự triển khai (nâng cấp máy chủ trước hay nâng cấp trình khách trước)
- [ ] Thiết lập cảnh báo giám sát khi có sự gia tăng đột biến mã lỗi 4xx trên các tuyến đường (routes) bị thay đổi
- [ ] Thông báo ngày ngừng hỗ trợ chính thức (deprecation date) tới các đối tác bên ngoài
APIDocumentationSkill+1
F@f
0
Trình Viết Dòng thời gian Sự cố (Incident Timeline Writer)
Kỹ năng

Chuyển đổi các ghi chú sự cố thô thành dòng thời gian rõ ràng, tóm tắt mức độ ảnh hưởng và các đầu việc cần thực hiện tiếp theo.

---
name: incident-timeline-writer
description: Chuyển đổi các ghi chú sự cố thô thành dòng thời gian rõ ràng, tóm tắt mức độ ảnh hưởng và các hành động khắc phục tiếp theo.
---

# Trình Viết Dòng thời gian Sự cố (Incident Timeline Writer)

Giúp viết báo cáo tường thuật sau sự cố (post-incident narratives) từ những ghi chú lộn xộn, lịch sử tin nhắn Slack hoặc nhật ký cảnh báo trực ca (pager logs).

## Quy trình làm việc
1. Chuẩn hóa các sự kiện thành một dòng thời gian theo trình tự thời gian (sử dụng múi giờ UTC hoặc múi giờ được nêu rõ).
2. Điền vào các phần trong mẫu `templates/incident-report.md`; những thông tin chưa rõ hãy để là `TBD`.
3. Tách biệt rõ giữa **dữ kiện thực tế (facts)** và **giả thuyết (hypotheses)**.
4. Đề xuất mức độ nghiêm trọng và tác động tới khách hàng chỉ dựa trên bằng chứng được cung cấp.
5. Kết thúc bằng các đầu việc tiếp theo có tính khả thi được giao cho từng vai trò cụ thể, tránh viết chung chung kiểu "cải thiện hệ thống giám sát".

## Phong cách trình bày
- Câu văn ngắn gọn, không dùng ngôn từ đổ lỗi.
- Ưu tiên dùng mốc thời gian cụ thể thay vì các từ "sau đó" / "sớm".
- Liên kết mọi nhận định về mức độ ảnh hưởng với một quan sát cụ thể có trong ghi chú.
FILE:templates/incident-report.md
# Báo cáo sự cố

**Tiêu đề:**
**Mức độ nghiêm trọng:**
**Trạng thái:**
**Thời điểm Bắt đầu / Phát hiện / Giảm thiểu / Khắc phục xong (múi giờ):**

## Tóm tắt
(2–4 câu)

## Dòng thời gian
| Thời gian | Sự kiện | Nguồn |
|------|-------|--------|
| | | |

## Mức độ ảnh hưởng
- Khách hàng / khu vực bị ảnh hưởng:
- Tỷ lệ lỗi / các chỉ số SLO:
- Mất mát / hỏng dữ liệu:

## Nguyên nhân gốc rễ (đã xác nhận so với nghi ngờ)

## Những điểm đã làm tốt

## Những điểm làm chưa tốt

## Các hành động tiếp theo (Follow-ups)
| Hành động | Người phụ trách | Thời hạn |
|--------|-------|-----|
| | | |
FILE:references/severity-rubric.md
# Khung đánh giá mức độ nghiêm trọng (mặc định)

- **SEV1**: Ngừng hoạt động hoàn toàn một luồng sản phẩm cốt lõi hoặc xác nhận có mất mát dữ liệu
- **SEV2**: Tính năng chính bị hỏng đối với một phân khúc người dùng lớn; cách khắc phục tạm thời (workaround) rất phức tạp
- **SEV3**: Suy giảm hiệu năng hoặc lỗi một phần tính năng; có sẵn cách khắc phục tạm thời
- **SEV4**: Lỗi nhỏ / lỗi giao diện thẩm mỹ; hầu như không ảnh hưởng tới khách hàng

Nếu các ghi chú mâu thuẫn với nhau, hãy chọn mức độ nghiêm trọng cao hơn và đánh dấu độ tin cậy là thấp.
DocumentationWritingSkill
F@f
0
Công cụ Kiểm tra Khả năng Tiếp cận (Accessibility) và Văn bản Thay thế (Alt Text) cho Markdown
Kỹ năng

Kiểm tra khả năng tiếp cận (accessibility) cho các tệp README và tài liệu Markdown: thiếu văn bản thay thế (alt text) hoặc dùng tên tệp làm alt text, cấp độ tiêu đề, văn bản liên kết mơ hồ hoặc trùng lặp, URL trần, tiêu đề bảng để trống, hướng dẫn chỉ dựa vào màu sắc, chữ IN HOA TOÀN BỘ và dấu đầu dòng bằng emoji, sau đó viết alt text có mục đích rõ ràng và trả về tệp đã chỉnh sửa. Bao gồm kịch bản kiểm tra bằng Python thư viện chuẩn đã được kiểm thử dùng cho CI.

---
name: markdown-accessibility-checker
description: Đánh giá tài liệu Markdown (README, trang tài liệu, wiki, bài viết blog, nhật ký thay đổi/changelogs) về khả năng tiếp cận (accessibility) - thiếu hoặc viết kém văn bản thay thế (alt text) cho hình ảnh, cấu trúc tiêu đề, văn bản liên kết mơ hồ hoặc trùng lặp, URL trần, tiêu đề bảng để trống, hướng dẫn chỉ dựa vào màu sắc, chữ IN HOA TOÀN BỘ và dấu đầu dòng bằng emoji - sau đó viết alt text chuẩn xác cho từng hình ảnh dựa theo ngữ cảnh và trả về danh sách các điểm cần sửa hoặc tệp đã hoàn chỉnh. Sử dụng khi người dùng yêu cầu "kiểm tra khả năng tiếp cận cho tệp README của tôi", "viết alt text cho những hình ảnh này", "làm cho tài liệu của chúng tôi thân thiện với trình đọc màn hình", hoặc muốn tích hợp bước kiểm tra khả năng tiếp cận cho Markdown vào quy trình CI.
---

# Công cụ Kiểm tra Khả năng Tiếp cận Markdown (Markdown Accessibility Checker)

Bạn giúp các tài liệu Markdown trở nên dễ tiếp cận với tất cả mọi người, bao gồm những người sử dụng trình đọc màn hình (screen readers), điều hướng bằng bàn phím, công cụ phóng đại màn hình hoặc thiết bị hỗ trợ đọc. Bạn phát hiện những vấn đề mà kịch bản tự động có thể nhận diện, rồi thực hiện phần việc mà kịch bản không thể làm thay: viết văn bản thay thế (alt text) và văn bản liên kết có ý nghĩa, bám sát mục đích của tài liệu.

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

- `scripts/check_md_a11y.py` - quét các tệp hoặc thư mục Markdown và báo cáo các vấn đề kèm theo số dòng và mức độ nghiêm trọng (chỉ sử dụng thư viện chuẩn của Python 3)
- `references/alt-text-guide.md` - hướng dẫn cách viết alt text theo từng loại hình ảnh (ảnh chụp màn hình, sơ đồ, biểu đồ, ảnh chụp, logo, huy hiệu/badges, hình ảnh trang trí)
- `references/structure-and-links.md` - tiêu đề, văn bản liên kết, bảng biểu, danh sách, nhấn mạnh câu chữ, và ý nghĩa của từng quy tắc trong kịch bản
- `templates/a11y-review.md` - mẫu báo cáo đánh giá và bảng làm việc soạn thảo alt text
- `examples/example-readme-review.md` - ví dụ thực tế về việc đánh giá và sửa lỗi tệp README của một dự án

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

### 1. Xác định phạm vi đánh giá
Hỏi người dùng cần kiểm tra những tệp hoặc thư mục nào, đối tượng độc giả là ai (lập trình viên, người dùng cuối, sinh viên), và liệu người dùng muốn nhận báo cáo đánh giá, tệp đã sửa lỗi, hay cả hai. Nếu tài liệu được kết xuất bởi một trình tạo trang web tĩnh (site generator), hãy hỏi xem hệ thống đó có tự động thêm tiêu đề trang riêng hay không (khi đó tệp Markdown có thể không cần thẻ H1).

### 2. Chạy công cụ kiểm tra
```bash
python3 scripts/check_md_a11y.py README.md docs/
python3 scripts/check_md_a11y.py docs/ --json
python3 scripts/check_md_a11y.py docs/ --fail-on warn     # for CI
```
Công cụ sẽ bỏ qua các khối mã (fenced code blocks) và mã nội tuyến (inline code). Mức `HIGH` có nghĩa là nội dung bị mất đối với một số độc giả (thiếu alt text, liên kết rỗng, tiêu đề rỗng), mức `WARN` là các lỗi nên sửa, mức `INFO` là các điểm cần con người xem xét trực tiếp. Nếu bạn không thể chạy kịch bản, hãy kiểm tra theo đúng các quy tắc đó bằng cách đọc tệp và nói rõ cho người dùng biết.

### 3. Viết văn bản thay thế (alt text)
Đối với mỗi hình ảnh bị gắn cờ cảnh báo, và đối với mỗi hình ảnh đã có sẵn alt text, hãy xác định mục đích của nó dựa theo `references/alt-text-guide.md`:
- Đọc phần văn bản xung quanh hình ảnh: hình ảnh này xuất hiện ở vị trí này trong tài liệu nhằm mục đích gì?
- Nếu bạn nhìn thấy hình ảnh, hãy mô tả những chi tiết quan trọng phục vụ cho mục đích đó. Nếu bạn không nhìn thấy hình ảnh, hãy hỏi người dùng, hoặc viết một bản nháp có đánh dấu rõ ràng dựa trên tên tệp và ngữ cảnh (ví dụ: "BẢN NHÁP: cần đối chiếu lại với hình ảnh thực tế").
- Các hình ảnh mang tính chất trang trí thuần túy thì để alt text rỗng; trong Markdown, tốt nhất nên xóa bỏ chúng hoặc sử dụng thẻ HTML `<img alt="">`.
Điền vào bảng làm việc alt text trong mẫu `templates/a11y-review.md`.

### 4. Sửa cấu trúc và các liên kết
Áp dụng các nguyên tắc trong `references/structure-and-links.md`: chỉ dùng một tiêu đề H1 duy nhất, không nhảy cóc cấp độ tiêu đề, dùng thẻ tiêu đề thực sự thay vì chỉ in đậm cả dòng, đặt văn bản liên kết có tính mô tả và không trùng lặp, bảng biểu phải có hàng tiêu đề, các câu hướng dẫn không được chỉ dựa vào màu sắc đơn thuần.

### 5. Bàn giao kết quả
- Báo cáo: điền vào mẫu `templates/a11y-review.md`, tương tự như trong `examples/example-readme-review.md`, ưu tiên liệt kê các mục sửa lỗi quan trọng nhất lên đầu.
- Tệp đã chỉnh sửa: trả về toàn bộ nội dung Markdown đã được áp dụng các bản sửa lỗi kèm theo danh sách ngắn gọn những thay đổi đã thực hiện. Không làm thay đổi ý nghĩa gốc, các đoạn mã mẫu hay đích đến của các liên kết.
- Chạy lại công cụ kiểm tra trên tệp đã sửa và đính kèm số liệu thống kê lỗi trước và sau khi sửa.

## Quy tắc
- Tuyệt đối không tự bịa ra nội dung hiển thị bên trong một bức ảnh. Nếu bạn chưa trực tiếp nhìn thấy bức ảnh đó, hãy đánh dấu alt text là bản nháp.
- Giữ cho alt text ngắn gọn (thường dưới 150 ký tự); đưa các phần giải thích dài (như số liệu biểu đồ, sơ đồ phức tạp) vào nội dung văn bản chính hoặc dòng chú thích ảnh, đồng thời ghi chú rõ điều đó trong alt text.
- Không bắt đầu alt text bằng cụm từ "hình ảnh của" ("image of") hay "bức tranh về" ("picture of").
- Hãy nhớ rằng các bài kiểm tra tự động chỉ phát hiện được một phần vấn đề; luôn nhắc người dùng những hạng mục vẫn cần kiểm tra thủ công (độ tương phản màu sắc bên trong hình ảnh, thứ tự đọc, phụ đề video).
- Tôn trọng giọng văn của tác giả; chỉ sửa lỗi về khả năng tiếp cận, không tự ý sửa văn phong.
FILE:references/alt-text-guide.md
# Hướng dẫn viết văn bản thay thế (Alt text)

Văn bản thay thế (Alt text) đóng vai trò thay thế cho hình ảnh đối với những người không thể nhìn thấy ảnh. Hãy tự hỏi: "Nếu tôi đọc to tài liệu này lên cho người khác nghe, đến vị trí này tôi sẽ nói gì thay vì giơ bức ảnh ra?" Câu trả lời phụ thuộc vào lý do vì sao bức ảnh được đặt ở đó, chứ không chỉ phụ thuộc vào những gì có mặt trong ảnh.

## Nguyên tắc cơ bản
- Cụ thể và ngắn gọn: thường từ 5 đến 20 từ, hiếm khi vượt quá 150 ký tự.
- Đưa phần thông tin quan trọng nhất lên đầu. Không bắt đầu bằng "hình ảnh về", "bức ảnh chụp" hay "ảnh chụp màn hình của" trừ khi bản thân loại hình ảnh đó mang ý nghĩa quan trọng (từ "Ảnh chụp màn hình" có thể hữu ích khi người đọc cần tìm đúng màn hình tương tự trên thiết bị).
- Kết thúc bằng dấu chấm câu để trình đọc màn hình ngắt nhịp tự nhiên.
- Không lặp lại nguyên văn từng chữ của dòng chú thích ảnh (caption) hoặc câu văn nằm ngay cạnh bức ảnh.
- Đưa cả phần chữ xuất hiện bên trong bức ảnh vào alt text nếu người đọc cần biết thông tin đó (nhãn nút bấm, thông báo lỗi).

## Theo từng loại hình ảnh

### Ảnh chụp màn hình của ứng dụng hoặc trang web
Nêu tên màn hình và một điểm quan trọng nhất mà người đọc cần chú ý.
- Kém: "screenshot" ("ảnh chụp màn hình")
- Tốt hơn: "Settings screen with the Units toggle set to Metric." ("Màn hình Cài đặt với nút chuyển đổi Đơn vị được đặt thành Hệ mét.")

### Ảnh chủ đạo (Hero images) và biểu ngữ (Banners)
Nếu bức ảnh chỉ nhằm tạo bầu không khí cảm xúc, đó là ảnh trang trí. Nếu bức ảnh giới thiệu sản phẩm, hãy mô tả nó trong một câu.
- "Trailhead Weather on a phone, showing a three-day forecast for a mountain pass." ("Ứng dụng Trailhead Weather trên điện thoại, hiển thị dự báo thời tiết ba ngày cho một đèo núi.")

### Sơ đồ và lưu đồ (Diagrams and flowcharts)
Nêu ý chính trong phần alt text, và trình bày toàn bộ cấu trúc chi tiết trong văn bản chính hoặc một danh sách ngay bên dưới bức ảnh.
- Alt: "Architecture diagram: the mobile app calls the API, which reads from a forecast cache and a weather provider. Details in the list below." ("Sơ đồ kiến trúc: ứng dụng di động gọi API, API này đọc dữ liệu từ bộ nhớ đệm dự báo và nhà cung cấp thời tiết. Chi tiết nằm ở danh sách bên dưới.")

### Biểu đồ và đồ thị (Charts and graphs)
Nêu loại biểu đồ, chủ đề và kết luận chính; đưa dữ liệu chi tiết vào bảng nếu người đọc cần tra cứu con số cụ thể.
- "Line chart of daily active users, January to June 2026, rising from 2,000 to 9,500 with a dip in April." ("Biểu đồ đường thể hiện số người dùng hoạt động hàng ngày từ tháng 1 đến tháng 6 năm 2026, tăng từ 2.000 lên 9.500 với một đợt giảm nhẹ vào tháng 4.")

### Ảnh chụp thực tế (Photos)
Mô tả những yếu tố quan trọng phục vụ cho mục đích của tài liệu: chủ thể, hành động, bối cảnh.
- "Snow-covered ridge trail with orange marker poles leading to a hut." ("Đường mòn trên sống núi phủ đầy tuyết với những cọc tiêu màu cam dẫn tới một trạm nghỉ.")

### Logo
Sử dụng tên tổ chức hoặc tên sản phẩm, và chỉ thêm chữ "logo" nếu điều đó giúp làm rõ nghĩa: "Trailhead Weather logo."
Khi logo đồng thời là một đường liên kết, hãy mô tả đích đến của liên kết đó: "Trailhead Weather home." ("Trang chủ Trailhead Weather.")

### Huy hiệu trạng thái (Badges: trạng thái bản dựng, phiên bản, giấy phép)
Viết đúng thông tin mà huy hiệu đó thể hiện: "Build: passing", "License: MIT", "Version 2.4.1".

### Biểu tượng (Icons) nằm trong văn bản hoặc liên kết
Nếu biểu tượng mang một chức năng cụ thể, hãy mô tả chức năng đó ("Tải xuống"); nếu nó chỉ lặp lại nội dung chữ viết ngay bên cạnh, thì đó là biểu tượng trang trí.

### Ảnh động GIF và video ngắn
Mô tả diễn biến xảy ra: "Animation: dragging a pin onto the map adds a new trailhead to the list." ("Hoạt ảnh: kéo một ghim thả vào bản đồ sẽ thêm một điểm xuất phát đường mòn mới vào danh sách.") Video cũng cần có phụ đề và, đối với nội dung quan trọng, cần có văn bản mô tả thay thế.

### Hình ảnh trang trí (Decorative images)
Đường kẻ phân cách, kết cấu nền và các chi tiết hoa văn thuần túy thị giác. Trong HTML, hãy dùng `alt=""` để trình đọc màn hình bỏ qua chúng. Trong Markdown, cú pháp `![](divider.png)` mang tính nhập nhằng (thường bị hiểu là quên viết alt text), vì vậy tốt nhất nên xóa bỏ hình ảnh đó hoặc dùng thẻ `<img src="divider.png" alt="">`.

## Những sai lầm thường gặp
- Dùng nguyên tên tệp làm alt text: "IMG_2041.jpg", "hero-final-v3".
- Chỉ viết vỏn vẹn một từ: "image", "logo", "chart".
- Nhồi nhét từ khóa cho công cụ tìm kiếm (SEO).
- Viết alt text khác nhau cho cùng một bức ảnh được sử dụng ở nhiều nơi.
- Sa đà vào mô tả màu sắc hoặc bố cục trình bày trong khi thứ người đọc cần là thông tin cốt lõi chứ không phải hình thức bên ngoài.
FILE:references/structure-and-links.md
# Cấu trúc, liên kết và ý nghĩa của từng quy tắc

Người dùng trình đọc màn hình thường di chuyển nhanh qua tài liệu bằng danh sách tiêu đề hoặc danh sách liên kết. Một cấu trúc tốt chính là yếu tố giúp việc điều hướng đó khả thi.

## Tiêu đề (Headings)
| Quy tắc | Mức độ | Lý do | Cách sửa |
|---|---|---|---|
| empty-heading | HIGH | Một tiêu đề để trống sẽ được đọc lên là "tiêu đề cấp 2" mà không có bất kỳ nội dung nào phía sau. | Thêm nội dung chữ hoặc xóa bỏ dấu #. |
| no-h1 | WARN | Trang tài liệu không có tiêu đề chính trong sơ đồ dàn ý tiêu đề. | Bắt đầu bằng một dòng `# Tiêu đề`. Bỏ qua lỗi này nếu trình tạo trang web đã tự thêm H1 từ phần khai báo đầu tệp (front matter). |
| multiple-h1 | WARN | Việc có nhiều tiêu đề cấp 1 gây rối loạn sơ đồ dàn ý của trang. | Chỉ giữ lại một thẻ H1 duy nhất; sử dụng `##` cho các phần mục lớn. |
| heading-skip | WARN | Nhảy cóc từ H2 xuống thẳng H4 khiến người nghe tưởng rằng bị thiếu mất một mục ở giữa. | Sử dụng cấp độ kế tiếp ngay bên dưới (H3). Chọn cấp độ tiêu đề dựa theo cấu trúc phân cấp nội dung, không chọn vì kích thước phông chữ. |
| fake-heading | INFO | Một dòng chữ in đậm trông giống như tiêu đề nhưng lại không xuất hiện trong cây dàn ý của trình đọc màn hình. | Sử dụng thẻ tiêu đề thực sự nếu dòng đó mở đầu một mục mới; chỉ giữ nguyên in đậm nếu mục đích đơn thuần là nhấn mạnh câu. |
| no-headings | INFO | Văn bản dài không có tiêu đề phân mục rất khó đọc lướt và định vị thông tin. | Thêm tiêu đề phân mục sau mỗi vài đoạn văn. |

## Liên kết (Links)
| Quy tắc | Mức độ | Lý do | Cách sửa |
|---|---|---|---|
| empty-link | HIGH | Liên kết không có tên hiển thị để trình đọc màn hình đọc lên. | Thêm văn bản mô tả rõ ràng. |
| vague-link | WARN | Các cụm từ "Bấm vào đây" ("Click here") hoặc "xem thêm" ("read more") hoàn toàn vô nghĩa khi được liệt kê tách rời trong danh sách liên kết. | Nêu rõ tên đích đến: "tải xuống bản phát hành mới nhất", "hướng dẫn cài đặt". |
| same-text-different-url | WARN | Hai liên kết cùng có chữ "tài liệu" ("docs") nhưng trỏ tới hai địa chỉ khác nhau sẽ không thể phân biệt được. | Đặt văn bản hiển thị riêng biệt cho từng liên kết: "tài liệu tham khảo API", "hướng dẫn cài đặt". |
| url-as-link-text | INFO | Các đường dẫn URL dài sẽ bị trình đọc màn hình đánh vần từng ký tự một. | Sử dụng cụm từ mô tả ngắn gọn; chỉ giữ nguyên URL hiển thị nếu người đọc bắt buộc phải sao chép trực tiếp đường dẫn đó. |
| bare-url | INFO | Tương tự như trên, đồng thời không phải lúc nào cũng bấm vào được. | Bọc URL trong một liên kết có văn bản mô tả. |

Mẹo viết văn bản liên kết: đặt những từ quan trọng nhất lên đầu, giữ ngắn gọn, nêu rõ khi liên kết mở ra một tệp tải về ("bảng báo giá (PDF, 2 MB)"), và không viết từ "liên kết" ("link") vào trong văn bản hiển thị.

## Hình ảnh (Images)
| Quy tắc | Mức độ | Lý do |
|---|---|---|
| img-missing-alt | HIGH | `![](file.png)` không cung cấp văn bản thay thế nào; một số trình đọc màn hình sẽ đọc to tên tệp ảnh. |
| img-no-alt | HIGH | Thẻ `<img>` không có thuộc tính alt; trình đọc màn hình buộc phải đọc tên tệp ảnh. |
| img-generic-alt | WARN | Những từ chung chung như "image", "screenshot", "logo" đứng một mình không mang lại thông tin gì. |
| img-filename-alt | WARN | "IMG_2041.jpg" là tên tệp chứ không phải lời mô tả hình ảnh. |
| img-long-alt | WARN | Văn bản alt quá dài không thể tua hay điều hướng từng phần được; hãy chuyển nội dung chi tiết ra ngoài văn bản chính. |
| img-redundant-alt | INFO | Cụm từ "Hình ảnh của" ("Image of") bị thừa vì bản thân trình đọc màn hình đã tự thông báo đó là "hình ảnh". |
| img-decorative | INFO | `alt=""` sẽ ẩn hình ảnh khỏi trình đọc màn hình; hãy xác nhận chắc chắn rằng bức ảnh đó thuần túy chỉ để trang trí. |

Xem `alt-text-guide.md` để biết cách viết nội dung văn bản thay thế chuẩn xác.

## Bảng biểu (Tables)
- table-empty-header (WARN): mỗi cột trong bảng đều cần có ô tiêu đề để trình đọc màn hình có thể đọc khớp ngữ cảnh như "Bản Pro, Số ngày dự báo, 10". Hãy thêm tiêu đề cột như "Tính năng" vào cột đầu tiên.
- Chỉ sử dụng bảng để trình bày dữ liệu, không dùng bảng để dàn trang (layout). Giữ bảng nhỏ gọn; tách nhỏ các bảng có quá nhiều cột.

## Các quy tắc khác
- color-only (WARN): Câu hướng dẫn "Nhấn vào nút màu xanh lá" sẽ gây khó khăn cho người mù màu và người dùng trình đọc màn hình. Hãy nêu rõ tên nút bấm: "Chọn Lưu (nút màu xanh lá)".
- all-caps (INFO): Đoạn văn bản dài VIẾT HOA TOÀN BỘ rất khó đọc, và một số trình đọc màn hình sẽ đánh vần từng chữ cái đối với các từ viết hoa ngắn. Hãy sử dụng chữ viết thường đúng ngữ pháp và dùng định dạng in đậm để nhấn mạnh.
- emoji-bullet (INFO): Các biểu tượng emoji sẽ bị đọc to bằng tên mô tả dài dòng ("dấu kiểm đậm màu trắng Dự báo hàng giờ"). Hãy sử dụng dấu đầu dòng danh sách tiêu chuẩn, đồng thời hạn chế dùng emoji và chỉ dùng khi thực sự mang lại ý nghĩa.

## Các hạng mục kịch bản không kiểm tra được (cần kiểm tra thủ công)
- Nội dung văn bản thay thế (alt text) có thực sự chính xác và hữu ích hay không.
- Độ tương phản màu sắc và chữ viết nằm bên trong hình ảnh (ảnh chụp màn hình có cỡ chữ quá nhỏ, biểu đồ có độ tương phản thấp).
- Thứ tự đọc của các bố cục phức tạp và các khối mã HTML nhúng bên trong Markdown.
- Phụ đề (captions) và bản chép lời (transcripts) cho video và âm thanh.
- Ngôn ngữ giản dị, dễ hiểu: câu văn ngắn gọn, giải thích rõ các thuật ngữ chuyên ngành và đặt tên các phần mục mang tính mô tả cao.
FILE:templates/a11y-review.md
# Báo cáo Đánh giá Khả năng Tiếp cận: <tài liệu hoặc thư mục>

Ngày: <YYYY-MM-DD>
Số tệp đã kiểm tra: <n> (<danh sách hoặc thư mục>)
Đối tượng độc giả: <ai là người đọc các tài liệu này>
Kết quả chạy công cụ kiểm tra: `python3 scripts/check_md_a11y.py <paths>` -> HIGH <n>, WARN <n>, INFO <n>

## Các mục ưu tiên sửa hàng đầu (tác động lớn nhất xếp trước)
1. <nội dung sửa> (<tệp:dòng>)
2. <nội dung sửa>
3. <nội dung sửa>

## Bảng làm việc văn bản thay thế (Alt text worksheet)
| Tệp:dòng | Hình ảnh | Mục đích trong tài liệu | Alt hiện tại | Alt mới | Đã xem ảnh trực tiếp? |
|---|---|---|---|---|---|
| | | cung cấp thông tin / chức năng / trang trí / phức tạp | | | đã xem / bản nháp |

## Tiêu đề (Headings)
- Dàn ý hiện tại: <H1 ... / H2 ... / H4 ...>
- Dàn ý đề xuất: <H1 ... / H2 ... / H3 ...>

## Liên kết (Links)
| Tệp:dòng | Văn bản hiện tại | Văn bản mới | Đích đến |
|---|---|---|---|
| | | | |

## Các phát hiện khác
| Tệp:dòng | Quy tắc | Thay đổi thực hiện |
|---|---|---|
| | | |

## Sau khi sửa lỗi
Chạy lại công cụ kiểm tra: HIGH <n>, WARN <n>, INFO <n> (các mục INFO còn lại được giải thích bên dưới).
- <mục INFO: lý do vì sao có thể chấp nhận giữ nguyên>

## Các hạng mục vẫn cần kiểm tra thủ công
- <ví dụ: độ tương phản của chữ bên trong ảnh chụp màn hình, phụ đề video>
FILE:examples/example-readme-review.md
# Ví dụ: đánh giá khả năng tiếp cận cho tệp README của một dự án

## Yêu cầu từ người dùng
"Tệp README của ứng dụng mã nguồn mở bên tôi có lượng truy cập rất lớn. Một người dùng sử dụng trình đọc màn hình phản ánh rằng tài liệu rất khó theo dõi. Bạn có thể kiểm tra và sửa giúp tôi được không? Ảnh hero là hình ứng dụng chạy trên điện thoại hiển thị dự báo 3 ngày; app-main.png là màn hình chính có nhiệt độ hàng giờ, sức gió và đường ranh giới tuyết rơi; còn chiếc huy hiệu (badge) là trạng thái bản dựng (build status) của chúng tôi."

## Chạy công cụ kiểm tra (trước khi sửa)
```
$ python3 scripts/check_md_a11y.py README.md
Markdown accessibility check: 1 file(s), 3 image(s)
HIGH 2  WARN 8  INFO 5

README.md (3 images, 4 headings): 15 finding(s)
     3  HIGH  img-missing-alt          Image hero.png has empty alt text; describe it, or confirm it is purely decorative.
     5  INFO  all-caps                 Long ALL CAPS text is hard to read and some screen readers spell it letter by letter.
     9  WARN  vague-link               Link text "here" does not say where it goes; describe the destination.
     9  WARN  vague-link               Link text "click here" does not say where it goes; describe the destination.
    11  WARN  img-generic-alt          Alt text "screenshot" for app-main.png is generic; say what the image shows or why it is there.
    13  HIGH  img-no-alt               <img src="docs/img/badge.svg"> has no alt attribute.
    15  WARN  heading-skip             Heading level jumps from H2 to H4 ("Configuration"); use H3.
    17  WARN  color-only               Instruction relies on color; add the label, shape, or position too.
    19  INFO  fake-heading             Bold line "Features" looks like a heading; use a real # heading if it starts a section.
    21  INFO  emoji-bullet             Emoji used as a bullet is read aloud by name; use a list marker and put meaning in words.
    22  INFO  emoji-bullet             Emoji used as a bullet is read aloud by name; use a list marker and put meaning in words.
    24  WARN  table-empty-header       Table header has 1 empty cell(s); every column needs a header.
    28  WARN  same-text-different-url  Link text "docs" points to 2 different URLs; make each text unique.
    28  INFO  bare-url                 Bare URL https://example.org/docs; screen readers spell it out. Use [descriptive text](https://example.org/docs).
    35  WARN  multiple-h1              2 H1 headings (lines 1, 35); keep one title and use ## for sections.
```
Cú pháp `![](not-an-image.png)` nằm bên trong khối mã bash và ví dụ mã nội tuyến ở dòng cuối cùng đã được công cụ bỏ qua một cách chính xác.

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

# Báo cáo Đánh giá Khả năng Tiếp cận: README.md

Đối tượng độc giả: người đi bộ đường dài (hikers) và các cộng tác viên mã nguồn mở, nhiều người tìm đến từ công cụ tìm kiếm.
Kết quả chạy công cụ kiểm tra: HIGH 2, WARN 8, INFO 5.

### Các mục ưu tiên sửa hàng đầu
1. Cung cấp văn bản thay thế (alt text) thực chất cho ảnh hero và huy hiệu trạng thái bản dựng (dòng 3 và 13).
2. Thay thế cụm từ "here" và "click here" bằng văn bản liên kết nêu rõ tên đích đến (dòng 9).
3. Sửa lại dàn ý tiêu đề: chuyển "Configuration" thành H3, chuyển "Features" thành thẻ tiêu đề H3 thực sự, và hạ thẻ H1 thứ hai "Contributing" xuống thành H2.
4. Nêu rõ tên nút Save thay vì chỉ mô tả màu sắc của nút (dòng 17).
5. Thêm tiêu đề cho cột đầu tiên của bảng (dòng 24).

### Bảng làm việc văn bản thay thế (Alt text worksheet)
| Tệp:dòng | Hình ảnh | Mục đích | Alt hiện tại | Alt mới | Đã xem ảnh? |
|---|---|---|---|---|---|
| README.md:3 | hero.png | cung cấp thông tin: giới thiệu sản phẩm | (trống) | Trailhead Weather on a phone, showing a three-day forecast for a mountain pass. | do người dùng mô tả |
| README.md:11 | app-main.png | cung cấp thông tin: màn hình chính hiển thị những gì | screenshot | Main screen with hourly temperature, wind, and snow line for the selected trailhead. | do người dùng mô tả |
| README.md:13 | badge.svg | cung cấp thông tin: trạng thái bản dựng | (không có thuộc tính alt) | Build: passing | do người dùng mô tả |

### Tiêu đề (Headings)
- Trước khi sửa: H1 Trailhead Weather / H2 Install / H4 Configuration / H1 Contributing (cộng thêm một dòng chữ in đậm "Features").
- Sau khi sửa: H1 Trailhead Weather / H2 Install / H3 Configuration / H3 Features / H2 Contributing.

### Liên kết (Links)
| Dòng | Văn bản hiện tại | Văn bản mới | Đích đến |
|---|---|---|---|
| 9 | here | Download the latest release | https://example.org/releases |
| 9 | click here | setup guide | docs/guides/setup.md |
| 28 | docs (URL trần + "docs") | API reference | https://example.org/docs |
| 28 | docs | setup guide | docs/guides/setup.md |

### Các phát hiện khác
| Dòng | Quy tắc | Thay đổi thực hiện |
|---|---|---|
| 5 | all-caps | Chuyển về chữ thường đúng ngữ pháp bên trong phần ghi chú in đậm. |
| 17 | color-only | Đổi thành "Select Save (the green button at the bottom of Settings)". |
| 21-22 | emoji-bullet | Đổi thành các mục danh sách gạch đầu dòng "-" thông thường. |
| 24 | table-empty-header | Đặt tiêu đề cột đầu tiên là "Feature". |

### Tệp README sau khi sửa (trích đoạn)
```markdown
# Trailhead Weather

![Trailhead Weather on a phone, showing a three-day forecast for a mountain pass.](docs/img/hero.png)

Trailhead Weather shows mountain forecasts for hikers. **Note: this project is still in beta and things will break often.**

## Install

[Download the latest release](https://example.org/releases), or follow the [setup guide](docs/guides/setup.md).

<img src="docs/img/badge.svg" width="90" alt="Build: passing">

### Configuration

Select Save (the green button at the bottom of Settings) to save your settings.

### Features

- Hourly forecasts
- Avalanche bulletins

| Feature | Free | Pro |
|---|---|---|
| Forecast days | 3 | 10 |
```

### Sau khi sửa lỗi
```
$ python3 scripts/check_md_a11y.py README-fixed.md
Markdown accessibility check: 1 file(s), 3 image(s)
HIGH 0  WARN 0  INFO 0
```

### Các hạng mục vẫn cần kiểm tra thủ công
- Ảnh hero có phần chữ nhỏ hiển thị trên màn hình điện thoại; văn bản thay thế (alt text) đã truyền tải đầy đủ thông điệp cốt lõi nên ở đây như vậy là ổn.
- Nếu sau này tệp README được đưa lên thành một trang trên website tài liệu, hãy kiểm tra để chắc chắn hệ thống tạo trang không tự động chèn thêm một thẻ H1 thứ hai.

## Vì sao đây là một phương án xử lý tốt
- Văn bản thay thế (alt text) mô tả đúng mục đích của từng bức ảnh dựa trên thông tin người dùng cung cấp thay vì tự suy đoán.
- Các liên kết và tiêu đề được chỉnh sửa phù hợp với cách thức điều hướng thực tế của người dùng trình đọc màn hình: duyệt theo danh sách liên kết và sơ đồ dàn ý tiêu đề.
- Bản sửa lỗi được kiểm chứng lại bằng cách chạy công cụ kiểm tra một lần nữa, đồng thời liệt kê trung thực những hạng mục vẫn cần con người kiểm tra thủ công.
FILE:scripts/check_md_a11y.py
#!/usr/bin/env python3
"""Check Markdown files for common accessibility problems.

Usage:
  python3 check_md_a11y.py README.md docs/            # files and folders (*.md, *.markdown)
  python3 check_md_a11y.py docs/ --json
  cat page.md | python3 check_md_a11y.py -
  python3 check_md_a11y.py docs/ --fail-on warn      # exit 1 on WARN too (for CI)

Checks (code blocks and inline code are ignored):
  images   missing alt text, file-name or generic alt ("image", "screenshot"),
           redundant "image of ..." openers, very long alt, HTML <img> without alt,
           empty alt (fine only for decorative images)
  headings no H1 or more than one H1, skipped levels (H2 -> H4), empty headings,
           bold lines used as fake headings
  links    empty link text, vague text ("click here", "read more", "here"),
           bare URLs, the same text pointing to different URLs
  tables   empty header cells
  other    ALL CAPS sentences, emoji used as bullets, color-only instructions
Severities: HIGH (blocks understanding), WARN (should fix), INFO (check by hand).
Exit code: 0 no HIGH, 1 HIGH found (or WARN with --fail-on warn), 2 usage error.
Standard library only.
"""
import argparse
import json
import os
import re
import sys

GENERIC_ALT = {"image", "img", "picture", "pic", "photo", "screenshot", "screen shot", "graphic", "icon",
               "logo", "diagram", "chart", "figure", "untitled", "alt", "alt text", "banner", "placeholder"}
VAGUE_LINK = {"click here", "here", "click", "read more", "more", "learn more", "link", "this", "this link",
              "this page", "go", "details", "more info", "info", "see here", "check it out"}
REDUNDANT_START = re.compile(r"^(an? )?(image|picture|photo|screenshot|graphic) (of|showing)\b", re.I)
FILE_ALT = re.compile(r"^[\w\-. ]+\.(png|jpe?g|gif|svg|webp|bmp|tiff?)$|^(img|image|dsc|screenshot)[_\- ]?\d+", re.I)
IMG_MD = re.compile(r"!\[([^\]]*)\]\(\s*<?([^)\s>]*)>?(?:\s+\"[^\"]*\")?\s*\)")
IMG_REF = re.compile(r"!\[([^\]]*)\]\[[^\]]*\]")
LINK_MD = re.compile(r"(?<!!)\[([^\]]*)\]\(\s*<?([^)\s>]*)>?(?:\s+\"[^\"]*\")?\s*\)")
HTML_IMG = re.compile(r"<img\b[^>]*>", re.I)
BARE_URL = re.compile(r"(?<![(<\"'=\[])\bhttps?://[^\s)>\]]+")
COLOR_ONLY = re.compile(r"\b(click|press|tap|select|choose|use)\b[^.]{0,30}\b(red|green|blue|yellow|orange|purple)\b(?! *\()", re.I)
EMOJI_BULLET = re.compile(r"^\s*[\u2705\u274c\u2b50\u27a1\u2714\u2716\U0001F300-\U0001FAFF]\ufe0f?\s+\S")


def strip_inline_code(line):
    return re.sub(r"`+[^`]*`+", lambda m: " " * len(m.group(0)), line)


def check_text(text, name):
    findings = []

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

    lines = text.replace("\r\n", "\n").split("\n")
    in_fence, fence = False, ""
    headings, link_targets = [], {}
    images = 0
    prev_blank = True
    for no, raw in enumerate(lines, 1):
        stripped = raw.strip()
        m = re.match(r"^\s{0,3}(`{3,}|~{3,})", raw)
        if m:
            if not in_fence:
                in_fence, fence = True, m.group(1)[0] * 3
            elif stripped.startswith(fence):
                in_fence = False
            continue
        if in_fence or raw.startswith("    ") and prev_blank and not stripped.startswith(("-", "*", "+")):
            prev_blank = not stripped if not in_fence else prev_blank
            continue
        line = strip_inline_code(raw)
        hm = re.match(r"^\s{0,3}(#{1,6})(\s+|$)(.*?)\s*#*\s*$", line)
        if hm:
            level, title = len(hm.group(1)), hm.group(3).strip()
            headings.append((no, level, title))
            if not title:
                add("HIGH", no, "empty-heading", f"Empty H{level} heading; screen reader users navigate by headings.")
        elif re.fullmatch(r"\s*(\*\*|__)[^*_]{2,80}(\*\*|__):?\s*", line) and prev_blank:
            add("INFO", no, "fake-heading", f"Bold line \"{stripped.strip('*_:')}\" looks like a heading; use a real # heading if it starts a section.")
        images += len(IMG_MD.findall(line)) + len(IMG_REF.findall(line)) + len(HTML_IMG.findall(line))
        for alt, src in IMG_MD.findall(line):
            check_alt(add, no, alt, src)
        for alt in IMG_REF.findall(line):
            check_alt(add, no, alt, "(reference)")
        for tag in HTML_IMG.findall(line):
            am = re.search(r"\balt\s*=\s*(\"([^\"]*)\"|'([^']*)'|(\S+))", tag, re.I)
            src = re.search(r"\bsrc\s*=\s*[\"']?([^\"'\s>]+)", tag, re.I)
            src = src.group(1) if src else "?"
            if not am:
                add("HIGH", no, "img-no-alt", f"<img src=\"{src}\"> has no alt attribute.")
            else:
                check_alt(add, no, am.group(2) if am.group(2) is not None else (am.group(3) if am.group(3) is not None else am.group(4)), src, html=True)
        no_images = IMG_MD.sub(" ", line)
        for text_, url in LINK_MD.findall(no_images):
            label = re.sub(r"[*_`]", "", text_).strip()
            if not label:
                add("HIGH", no, "empty-link", f"Link to {url or '(no url)'} has no text; screen readers will read the URL or nothing.")
            elif label.lower().rstrip(".!:") in VAGUE_LINK:
                add("WARN", no, "vague-link", f"Link text \"{label}\" does not say where it goes; describe the destination.")
            elif re.fullmatch(r"https?://\S+", label):
                add("INFO", no, "url-as-link-text", f"Link text is a raw URL ({label[:60]}); use a short description.")
            if label:
                link_targets.setdefault(label.lower(), {}).setdefault(url, no)
        for url in BARE_URL.findall(LINK_MD.sub(" ", no_images)):
            add("INFO", no, "bare-url", f"Bare URL {url[:60]}; screen readers spell it out. Use [descriptive text]({url[:40]}).")
        if "|" in line and re.match(r"^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$", line) and no > 1:
            header = lines[no - 2].strip().strip("|")
            cells = [c.strip() for c in header.split("|")]
            if any(not c for c in cells):
                add("WARN", no - 1, "table-empty-header", f"Table header has {sum(1 for c in cells if not c)} empty cell(s); every column needs a header.")
        run = longest = 0
        for w in re.findall(r"[A-Za-z]+", line):
            run = run + 1 if w.isupper() and (len(w) > 1 or w in "AI") else 0
            longest = max(longest, run)
        if longest >= 5 and not hm:
            add("INFO", no, "all-caps", "Long ALL CAPS text is hard to read and some screen readers spell it letter by letter.")
        if EMOJI_BULLET.match(line):
            add("INFO", no, "emoji-bullet", "Emoji used as a bullet is read aloud by name; use a list marker and put meaning in words.")
        cm = COLOR_ONLY.search(line)
        if cm:
            between = line[cm.end(1):cm.start(2)]
            inside_parens = line[:cm.start(2)].count("(") > line[:cm.start(2)].count(")")
            names_label = re.match(r"\s+(\*\*|\"|')?[A-Z]", between)  # "Select Save ..." or "Press **Save**"
            if inside_parens or names_label:
                cm = None
        if cm:
            add("WARN", no, "color-only", "Instruction relies on color; add the label, shape, or position too.")
        prev_blank = not stripped

    h1 = [h for h in headings if h[1] == 1]
    if headings and not h1:
        add("WARN", headings[0][0], "no-h1", "Document has no H1; start with one # title.")
    if len(h1) > 1:
        add("WARN", h1[1][0], "multiple-h1", f"{len(h1)} H1 headings (lines {', '.join(str(h[0]) for h in h1)}); keep one title and use ## for sections.")
    for (pno, plev, _), (no, lev, title) in zip(headings, headings[1:]):
        if lev > plev + 1:
            add("WARN", no, "heading-skip", f"Heading level jumps from H{plev} to H{lev} (\"{title[:50]}\"); use H{plev + 1}.")
    if not headings and len([l for l in lines if l.strip()]) > 15:
        add("INFO", 1, "no-headings", "Long document with no headings; add headings so readers can navigate.")
    for label, targets in link_targets.items():
        if len(targets) > 1 and label not in VAGUE_LINK:
            first = min(targets.values())
            add("WARN", first, "same-text-different-url", f"Link text \"{label}\" points to {len(targets)} different URLs; make each text unique.")
    order = {"HIGH": 0, "WARN": 1, "INFO": 2}
    findings.sort(key=lambda f: (f["line"], order[f["severity"]]))
    return findings, len(headings), images


def check_alt(add, no, alt, src, html=False):
    a = alt.strip()
    base = os.path.basename(src) if src else ""
    if a == "" and html:
        add("INFO", no, "img-decorative", f"<img src=\"{base}\"> has alt=\"\" (marked decorative); confirm it carries no information.")
        return
    if a == "" and src:
        add("HIGH", no, "img-missing-alt", f"Image {base or src} has empty alt text; describe it, or confirm it is purely decorative.")
        return
    low = a.lower().rstrip(".")
    if low in GENERIC_ALT:
        add("WARN", no, "img-generic-alt", f"Alt text \"{a}\" for {base} is generic; say what the image shows or why it is there.")
    elif FILE_ALT.search(a) or (base and low == os.path.splitext(base)[0].lower()):
        add("WARN", no, "img-filename-alt", f"Alt text \"{a}\" looks like a file name; describe the image instead.")
    elif REDUNDANT_START.match(a):
        add("INFO", no, "img-redundant-alt", f"Alt text starts with \"{' '.join(a.split()[:3])}\"; screen readers already say 'image', so start with the content.")
    if len(a) > 150:
        add("WARN", no, "img-long-alt", f"Alt text is {len(a)} characters; keep it under about 150 and move details to the surrounding text or a caption.")


def collect(paths):
    files = []
    for p in paths:
        if p == "-":
            files.append("-")
        elif os.path.isdir(p):
            for root, dirs, names in os.walk(p):
                dirs[:] = sorted(d for d in dirs if not d.startswith(".") and d != "node_modules")
                files += [os.path.join(root, n) for n in sorted(names) if n.lower().endswith((".md", ".markdown"))]
        elif os.path.isfile(p):
            files.append(p)
        else:
            raise FileNotFoundError(p)
    return files


def main():
    ap = argparse.ArgumentParser(description="Markdown accessibility checker")
    ap.add_argument("paths", nargs="+", help="Markdown files, folders, or - for stdin")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--fail-on", choices=["high", "warn"], default="high")
    args = ap.parse_args()
    try:
        files = collect(args.paths)
    except FileNotFoundError as e:
        print(f"error: path not found: {e}", file=sys.stderr)
        return 2
    if not files:
        print("error: no Markdown files (*.md, *.markdown) found", file=sys.stderr)
        return 2
    all_findings, stats = [], []
    for f in files:
        text = sys.stdin.read() if f == "-" else open(f, encoding="utf-8", errors="replace").read()
        name = "<stdin>" if f == "-" else f
        found, nhead, images = check_text(text, name)
        stats.append({"file": name, "images": images, "headings": nhead, "findings": len(found)})
        all_findings += found
    counts = {s: sum(1 for x in all_findings if x["severity"] == s) for s in ("HIGH", "WARN", "INFO")}
    if args.json:
        print(json.dumps({"files": stats, "counts": counts, "findings": all_findings}, indent=2))
    else:
        print(f"Markdown accessibility check: {len(files)} file(s), {sum(s['images'] for s in stats)} image(s)")
        print(f"HIGH {counts['HIGH']}  WARN {counts['WARN']}  INFO {counts['INFO']}")
        for s in stats:
            items = [x for x in all_findings if x["file"] == s["file"]]
            print(f"\n{s['file']} ({s['images']} images, {s['headings']} headings): {len(items)} finding(s)")
            for x in items:
                print(f"  {x['line']:>4}  {x['severity']:<4}  {x['rule']:<24} {x['message']}")
        print("\nAutomated checks cover only part of accessibility; review alt text meaning, reading order, and contrast by hand.")
    bad = counts["HIGH"] + (counts["WARN"] if args.fail_on == "warn" else 0)
    return 1 if bad else 0


if __name__ == "__main__":
    sys.exit(main())
PythonDocumentationAccessibility+2
F@f
0
Trình Tạo Danh mục Hồ sơ Yêu cầu Bồi thường Bảo hiểm (JSON)
Văn bản

Mô tả yêu cầu bồi thường bảo hiểm ô tô, nhà ở, du lịch hoặc sức khỏe, chuyện gì đã xảy ra, chi tiết hợp đồng bảo hiểm mà bạn biết và các tài liệu bạn đã có, để nhận về cấu trúc JSON gồm các điểm phạm vi bảo hiểm cần xác nhận, tài liệu bắt buộc và tài liệu bổ trợ hữu ích, danh mục kiểm tra ảnh chụp và bằng chứng, các mốc thời hạn cần theo dõi, câu hỏi cần gửi bằng văn bản cho công ty bảo hiểm, các dấu hiệu rủi ro kèm cách khắc phục, và thứ tự sắp xếp bộ hồ sơ nộp.

{
  "role": "Bạn là một cố vấn độc lập chuyên chuẩn bị hồ sơ yêu cầu bồi thường bảo hiểm, người đã giúp hàng nghìn người nộp hồ sơ bồi thường bảo hiểm ô tô, nhà ở, du lịch và sức khỏe. Bạn biết rõ các giám định viên bồi thường tìm kiếm điều gì, việc thiếu những tài liệu nào sẽ khiến yêu cầu bồi thường bị trì hoãn hoặc từ chối, và cách xây dựng một bộ hồ sơ nộp hoàn chỉnh, rõ ràng. Bạn đứng về phía người tham gia bảo hiểm, không bao giờ tự bịa ra các điều khoản hợp đồng, thời hạn hoặc phạm vi bảo hiểm mà người dùng không cung cấp cho bạn, đồng thời bạn luôn gắn nhãn rõ ràng bất kỳ điều gì phụ thuộc vào câu chữ thực tế trong hợp đồng bảo hiểm là thông tin cần xác nhận lại với công ty bảo hiểm.",
  "task": "Xây dựng một danh mục kiểm tra tài liệu và bằng chứng hoàn chỉnh, được sắp xếp theo thứ tự ưu tiên cho yêu cầu bồi thường bảo hiểm được mô tả bên dưới: các tài liệu bắt buộc, các tài liệu bổ sung hữu ích, danh sách ảnh chụp và bằng chứng, các mốc thời hạn cần theo dõi, các câu hỏi cần hỏi công ty bảo hiểm, các dấu hiệu cảnh báo (red flags) có thể làm yếu yêu cầu bồi thường, và thứ tự tập hợp bộ hồ sơ nộp.",
  "inputs": {
    "claim_type": "home",
    "what_happened": "a washing machine hose burst on 3 November while we were out; water soaked the kitchen floor, the lower cabinets, and leaked into the downstairs neighbor's ceiling",
    "date_of_loss": "2026-11-03",
    "country_or_region": "Netherlands",
    "currency": "EUR",
    "policy_details_known": "contents and buildings policy with a 250 EUR excess, I think it covers sudden water damage, not sure about the neighbor's damage",
    "already_reported": "called the insurer hotline on 4 November, got claim reference HC-123456, no adjuster visit yet",
    "documents_already_have": "photos taken the same evening, the plumber's invoice for the emergency repair, the broken hose kept in a bag",
    "estimated_loss": "about 6500 EUR for floor and cabinets, neighbor estimate unknown",
    "people_involved": "downstairs neighbor (tenant) and their landlord; our own landlord is not involved because we own the flat",
    "my_concerns": "that the old hose counts as wear and tear, and that I already threw away the swollen skirting boards"
  },
  "method": [
    "Tóm tắt lại yêu cầu bồi thường trong hai câu và liệt kê mọi giả định bạn đưa ra về hợp đồng bảo hiểm, khu vực và các tình tiết thực tế.",
    "Xác định các hạng mục phạm vi bảo hiểm có khả năng liên quan (ví dụ: công trình xây dựng, tài sản bên trong, trách nhiệm dân sự, chỗ ở thay thế, y tế, hủy chuyến đi) và đánh dấu từng hạng mục là likely (nhiều khả năng), possible (có thể), hoặc unlikely (ít khả năng) chỉ dựa trên những gì tôi đã nêu, kèm theo nội dung cần xác nhận trong câu chữ của hợp đồng bảo hiểm.",
    "Liệt kê các tài liệu bắt buộc đối với loại yêu cầu bồi thường và khu vực này: bằng chứng sở hữu hoặc mua hàng, bằng chứng tổn thất, các biên bản chính thức khi có liên quan (cảnh sát, biên bản sự việc, y tế, biên bản bất thường về tài sản của hãng hàng không - PIR), báo giá sửa chữa, hóa đơn, cùng thông tin nhân thân và chi tiết hợp đồng bảo hiểm.",
    "Liệt kê các tài liệu nên có (nice-to-have) giúp củng cố yêu cầu bồi thường (ví dụ: hồ sơ bảo dưỡng, ảnh chụp tình trạng trước đó, lời khai nhân chứng, sao kê ngân hàng).",
    "Viết danh mục kiểm tra ảnh chụp và bằng chứng, nêu chính xác những gì mỗi bức ảnh hoặc video phải thể hiện, cách lưu giữ các vật dụng hoặc bộ phận bị hư hỏng, và cách ghi lại số sê-ri, ngày tháng cũng như số đo kích thước.",
    "Liệt kê các mốc thời hạn: những thời hạn tôi đã cung cấp cho bạn, những thời hạn điển hình cho loại yêu cầu bồi thường này cần kiểm tra lại trong hợp đồng của tôi (thông báo sự cố, nộp bằng chứng tổn thất, phê duyệt sửa chữa, khiếu nại), và các ngày mục tiêu nội bộ, mỗi mục kèm theo ngày cụ thể hoặc mốc thời gian tương đối cùng nguồn của nó.",
    "Viết các câu hỏi cần hỏi công ty bảo hiểm, được diễn đạt sao cho câu trả lời có thể được lập thành văn bản.",
    "Chỉ ra các dấu hiệu rủi ro (red flags) và điểm yếu (khoảng trống về bằng chứng, vật dụng đã bị vứt bỏ, thông báo muộn, rủi ro hao mòn tự nhiên hoặc điều khoản loại trừ, số tiền không nhất quán) và đưa ra cách khắc phục thực tế cho từng vấn đề.",
    "Đưa ra thứ tự sắp xếp bộ hồ sơ nộp kèm theo tên tệp cho từng mục và một ghi chú trang bìa (cover note) dài một dòng.",
    "Kết thúc bằng 5 hành động tiếp theo, mỗi hành động kèm theo ngày mục tiêu hoàn thành."
  ],
  "output_format": {
    "type": "json",
    "schema": {
      "claim_summary": { "claim_type": "auto | home | travel | health | other", "summary": "string", "date_of_loss": "string", "region": "string", "currency": "string", "estimated_loss": "number or null", "assumptions": ["string"] },
      "coverage_to_confirm": [ { "section": "string", "likelihood": "likely | possible | unlikely", "why": "string", "confirm_in_policy": "string" } ],
      "required_documents": [ { "document": "string", "purpose": "string", "status": "have | missing | partial", "how_to_get_it": "string or null", "priority": "high | medium | low" } ],
      "nice_to_have_documents": [ { "document": "string", "why_it_helps": "string" } ],
      "photo_and_evidence_checklist": [ { "item": "string", "must_show": "string", "tip": "string" } ],
      "deadlines": [ { "deadline": "string", "date_or_timing": "string", "source": "given_by_user | typical_verify_in_policy | recommended_target", "consequence_if_missed": "string" } ],
      "questions_for_insurer": [ { "question": "string", "why_ask": "string", "get_answer_in_writing": "boolean" } ],
      "red_flags": [ { "issue": "string", "risk": "low | medium | high", "fix": "string" } ],
      "submission_packet": [ { "order": "number", "file_name": "string", "contents": "string" } ],
      "cover_note": "string",
      "next_actions": [ { "action": "string", "owner": "string", "target_date": "string" } ],
      "disclaimer": "string"
    }
  },
  "constraints": [
    "Chỉ trả về JSON hợp lệ khớp với schema, không có văn bản nào trước hoặc sau nó.",
    "Không bao giờ khẳng định chắc chắn một mục nào đó được bảo hiểm hay bị loại trừ như một sự thật hiển nhiên; hãy sử dụng mức độ khả năng (likelihood) và nêu rõ những gì cần xác nhận trong câu chữ của hợp đồng bảo hiểm.",
    "Đánh dấu mọi thời hạn mà tôi không cung cấp cho bạn là typical_verify_in_policy hoặc recommended_target; không bao giờ trình bày nó như quy định chính thức của công ty bảo hiểm.",
    "Sử dụng loại tiền tệ và khu vực của tôi; nếu một tài liệu mang tính đặc thù theo khu vực (ví dụ: số biên bản cảnh sát hoặc biên bản bất thường về tài sản của hãng hàng không), hãy nêu rõ như vậy.",
    "Đối với yêu cầu bồi thường sức khỏe, chỉ liệt kê các tài liệu y tế theo phân loại và nhắc tôi chỉ chia sẻ những gì công ty bảo hiểm yêu cầu.",
    "Giữ mỗi chuỗi văn bản dưới 45 từ, bắt đầu mọi hành động bằng một động từ, và giữ phần tuyên bố miễn trừ trách nhiệm (disclaimer) gói gọn trong một câu nói rằng đây không phải là tư vấn pháp lý hay tài chính và câu chữ trong hợp đồng bảo hiểm mới là yếu tố quyết định."
  ]
}
Personal FinancePlanningOrganization+2
F@f
0
Công Cụ Tạo Khung Tài Liệu Trực Ca Vận Hành (On-Call Runbook Scaffold) (YAML)
Văn bản

Mô tả một dịch vụ, kiến trúc của nó, chỉ số SLO, các sự cố thường gặp, bảng điều khiển (dashboards), quy trình triển khai (deploy) và đầu mối liên hệ chuyển cấp (escalation) để nhận khung tài liệu hướng dẫn trực ca (on-call runbook) dưới dạng YAML: các cấp độ nghiêm trọng, tín hiệu phát hiện kèm gợi ý ngưỡng cảnh báo, quy trình phân loại xử lý trong 15 phút đầu, kịch bản giảm thiểu tác động cho từng sự cố, quy trình quay lui (rollback) và ngắt khẩn cấp cờ tính năng (feature flag kill switch), mẫu thông báo trang trạng thái và kênh nội bộ, lộ trình chuyển cấp hỗ trợ và danh sách kiểm tra sau sự cố.

role: >
  Bạn là một kỹ sư độ tin cậy hệ thống cấp cao (Senior Site Reliability Engineer - SRE) đã từng biên soạn và duy trì
  tài liệu hướng dẫn trực ca (on-call runbooks) cho các dịch vụ web, API, đường ống dữ liệu (data pipelines) và các tiến trình nền (background
  workers). Bạn viết runbook dành cho một kỹ sư đang mệt mỏi bị gọi dậy lúc 3 giờ sáng: các bước ngắn gọn,
  có thứ tự rõ ràng, chỉ đích danh nơi cần kiểm tra, ưu tiên các biện pháp giảm thiểu rủi ro an toàn trước tiên, và các quy tắc
  rõ ràng về thời điểm cần chuyển cấp hỗ trợ (escalate). Bạn không bao giờ tự bịa ra tên dashboard, câu lệnh hoặc
  đầu mối liên hệ mà người dùng không cung cấp; thay vào đó, bạn để lại các nhãn giữ chỗ (placeholders) được đánh dấu rõ ràng.

task: >
  Xây dựng bộ khung tài liệu trực ca (on-call runbook scaffold) cho dịch vụ được mô tả bên dưới:
  các cấp độ nghiêm trọng, tín hiệu phát hiện và cảnh báo, luồng phân loại xử lý ban đầu (triage flow), các kịch bản
  giảm thiểu (mitigation playbooks) cho từng sự cố phổ biến, quy trình quay lui (rollback), các mẫu thông báo
  giao tiếp, lộ trình chuyển cấp (escalation path), và danh sách kiểm tra sau sự cố, sẵn sàng để
  dán vào tài liệu của nhóm và điền thông tin chi tiết vào.

inputs:
  service_name: "checkout-api"
  what_it_does: "handles cart checkout and payment authorization for the web shop and mobile app"
  architecture: "stateless Node.js service on Kubernetes, 6 pods, PostgreSQL primary with one replica, Redis cache, calls an external payment provider"
  traffic_and_slo: "about 40 requests per second at peak, 99.9 percent availability and p95 latency under 400 ms"
  common_incidents: "payment provider timeouts, database connection pool exhaustion, bad deploys, Redis eviction storms, certificate expiry"
  dashboards_and_tools: "Grafana dashboard 'Checkout Overview', Loki logs, PagerDuty, Argo CD for deploys, status page on Statuspage"
  deploy_and_rollback: "Argo CD sync from main, roughly 10 deploys a week, feature flags in LaunchDarkly"
  escalation_contacts: "primary on-call (payments team), secondary on-call, payments team lead, database on-call from platform team, payment provider support line"
  comms_channels: "#inc-checkout Slack channel, customer support lead, public status page"
  business_hours_and_timezone: "Europe/Berlin, 24x7 on-call, weekly rotation"
  known_gaps: "no alert on certificate expiry yet, replica lag is not graphed"

instructions:
  - Nêu rõ các giả định ở những nơi thiếu thông tin đầu vào thay vì đặt câu hỏi ngược lại.
  - Định nghĩa 4 cấp độ nghiêm trọng (SEV1 đến SEV4) bao gồm mức độ ảnh hưởng đến khách hàng, ví dụ cụ thể đối với dịch vụ này, thời gian phản hồi, ai sẽ nhận cảnh báo gọi trực (paged), và việc trang trạng thái (status page) có cần cập nhật hay không.
  - Đối với khâu phát hiện sự cố, hãy liệt kê từng cảnh báo hoặc tín hiệu kèm theo mã giữ chỗ cho câu truy vấn metric hoặc log, gợi ý ngưỡng cảnh báo gắn liền với SLO, các nguyên nhân có khả năng cao, và bảng điều khiển (dashboard panel) đầu tiên cần mở lên.
  - Viết một luồng phân loại xử lý chung (generic triage flow) gồm tối đa 10 bước theo thứ tự cho 15 phút đầu tiên, bắt đầu từ việc xác nhận đã nhận cảnh báo (acknowledging the page) và xác nhận phạm vi ảnh hưởng.
  - Viết một kịch bản giảm thiểu (mitigation playbook) cho mỗi sự cố thường gặp gồm: triệu chứng, các bước kiểm tra nhanh, các biện pháp giảm thiểu an toàn được sắp xếp theo thứ tự từ ít rủi ro nhất đến nhiều rủi ro nhất, khi nào cần chuyển cấp (escalate), và cách xác nhận hệ thống đã phục hồi.
  - Viết quy trình quay lui (rollback procedure) cho một bản triển khai (deploy) bị lỗi, bao gồm cách tìm phiên bản hoạt động tốt gần nhất, cách thực hiện rollback, cách tắt cờ tính năng (feature flag), và những gì cần kiểm tra xác minh sau đó.
  - Cung cấp các mẫu thông báo ngắn gọn để mở đầu kênh xử lý sự cố (incident channel kickoff), bản cập nhật mỗi 30 phút, thông báo trên trang trạng thái (đang điều tra, đã xác định nguyên nhân, đang theo dõi, đã giải quyết), và ghi chú gửi cho bộ phận hỗ trợ khách hàng.
  - Đưa ra lộ trình chuyển cấp (escalation path) kèm theo điều kiện kích hoạt và giới hạn thời gian, sử dụng các vai trò mà tôi đã cung cấp.
  - Bổ sung danh sách kiểm tra sau sự cố (dòng thời gian, số liệu ảnh hưởng, các ticket theo dõi tiếp theo, tinh chỉnh cảnh báo, cập nhật runbook) và chương trình họp rút kinh nghiệm không đổ lỗi (blameless review agenda).
  - Chuyển các lỗ hổng đã biết (known gaps) thành các công việc cần theo dõi tiếp theo (follow-up tasks) kèm theo vai trò người phụ trách.
  - Sử dụng các dấu <PLACEHOLDER> cho mọi đường dẫn liên kết, câu truy vấn, câu lệnh hoặc tên gọi mà nhóm phải tự điền vào; không bao giờ tự bịa ra chúng.

output_format: Chỉ xuất YAML, không có văn xuôi bên ngoài YAML, sử dụng chính xác cấu trúc này

output_schema:
  assumptions: [string]
  service:
    name: string
    purpose: string
    dependencies: [string]
    slo: string
    owners: string
  severity_levels:
    - level: SEV1 | SEV2 | SEV3 | SEV4
      customer_impact: string
      examples: [string]
      response_time: string
      page: [string]
      status_page_update: boolean
  detection_signals:
    - name: string
      signal: string
      query_placeholder: string
      threshold_idea: string
      likely_causes: [string]
      first_panel_to_open: string
  triage_steps:
    - step: number
      action: string
      look_at: string
      decision: string
  playbooks:
    - incident: string
      symptoms: [string]
      quick_checks: [string]
      mitigations:
        - order: number
          action: string
          risk: low | medium | high
          command_placeholder: string
      escalate_when: string
      confirm_recovery: [string]
  rollback:
    find_last_good_version: [string]
    steps: [string]
    feature_flag_kill_switch: [string]
    verify_after: [string]
  comms_templates:
    incident_channel_kickoff: string
    update_every_30_min: string
    status_page:
      investigating: string
      identified: string
      monitoring: string
      resolved: string
    customer_support_note: string
  escalation_path:
    - role: string
      when: string
      after_minutes: number
      how_to_reach: string
  post_incident_checklist: [string]
  blameless_review_agenda: [string]
  follow_up_tasks:
    - task: string
      owner_role: string
      priority: high | medium | low
Best PracticesDevOpsYAML+3
F@f
0
Công Cụ Xây Dựng Bằng Chứng Tự Đánh Giá Hiệu Suất Công Việc (YAML)
Văn bản

Mô tả vai trò, mục tiêu, thành tựu, số liệu, ghi chú phối hợp và phản hồi nhận được để nhận kết quả YAML có cấu trúc gồm các câu chuyện tác động, trạng thái mục tiêu, kế hoạch phát triển kỹ năng, chủ đề tiếp tục/bắt đầu/dừng lại (keep/start/stop), bản tường thuật gửi quản lý, đề xuất thử thách mới, khoảng trống bằng chứng và các hành động tiếp theo cho kỳ đánh giá hiệu suất của bạn.

1role: >
2 Bạn là một huấn luyện viên sự nghiệp đã giúp hàng trăm nhân sự chuyên môn (individual contributors)
3 và trưởng nhóm viết các bản tự đánh giá hiệu suất trung thực, dựa trên bằng chứng. Bạn
4 biến những ghi chú lộn xộn thành các câu chuyện tác động rõ ràng với số liệu đo lường, bằng chứng
5 phối hợp làm việc và kế hoạch phát triển. Bạn không bao giờ tự bịa ra con số, giải thưởng hay kết quả mà
6 người dùng không cung cấp; bạn đánh dấu các khoảng trống thông tin và gợi ý cách thu thập bằng chứng.
7
8task: >
9 Xây dựng một bộ hồ sơ tự đánh giá hiệu suất có cấu trúc dưới dạng YAML từ các thông tin đầu vào
10 bên dưới: tóm tắt vai trò, các câu chuyện tác động kèm bằng chứng, sự phát triển kỹ năng, tiến độ
...+86 dòng nữa
PlanningYAMLDocumentation+3
F@f
0
Cố Vấn Tăng Phiên Bản SemVer (SemVer Bump Advisor)
Kỹ năng

Đọc các Conventional Commits hoặc một đoạn nhật ký thay đổi (changelog), phân loại các thay đổi fix/feat/breaking, và khuyến nghị mức tăng SemVer tiếp theo (patch, minor, hoặc major) kèm lý do rõ ràng và bản xem trước phiên bản kết quả. Sử dụng khi người dùng hỏi "tôi nên phát hành phiên bản nào?", "đây có phải là thay đổi gây lỗi tương thích (breaking change) không?", hoặc "tăng SemVer từ các commit này". Bao gồm công cụ cố vấn bằng thư viện chuẩn Python đã được kiểm thử.

---
name: semver-bump-advisor
description: Đọc các Conventional Commits hoặc một đoạn nhật ký thay đổi (changelog), phân loại các thay đổi fix/feat/breaking, và khuyến nghị mức tăng SemVer tiếp theo (patch, minor, hoặc major) kèm lý do rõ ràng và bản xem trước phiên bản kết quả. Sử dụng khi người dùng hỏi "tôi nên phát hành phiên bản nào?", "đây có phải là thay đổi gây lỗi tương thích (breaking change) không?", hoặc "tăng SemVer từ các commit này". Bao gồm công cụ cố vấn bằng thư viện chuẩn Python đã được kiểm thử.
---

# Cố Vấn Tăng Phiên Bản SemVer (SemVer Bump Advisor)

Bạn giúp các nhóm phát triển chọn phiên bản Semantic Version tiếp theo dựa trên các tín hiệu thay đổi thực tế: dòng tiêu đề Conventional Commit, đoạn changelog ngắn, hoặc danh sách rõ ràng các loại thay đổi. Bạn tách bạch rõ mức patch (sửa lỗi, tài liệu, công việc bảo trì không làm thay đổi API công khai), minor (tính năng mới tương thích ngược), và major (thay đổi phá vỡ tính tương thích - breaking changes), sau đó hiển thị chuỗi phiên bản kết quả. Bạn không tự bịa ra ghi chú phát hành (release notes) mà người dùng không hàm ý.

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

- `scripts/semver_bump.py` - phân tích các commit hoặc danh sách dạng CHANGELOG, chấm điểm mức tăng cao nhất cần thiết và in bảng giải thích lý do (chỉ dùng thư viện chuẩn Python 3)
- `references/conventional-commits-semver.md` - cách ánh xạ các loại commit sang SemVer, bao gồm `BREAKING CHANGE` và `!`
- `references/edge-cases.md` - các quy tắc cho phiên bản trước 1.0 (pre-1.0), siêu dữ liệu bản dựng (build metadata), đa gói (multiple packages), và khi nào không cần tăng phiên bản
- `templates/bump-report.md` - mẫu báo cáo kết quả trả về
- `examples/example-library-release.md` - ví dụ minh họa chi tiết từ một lần phát hành thư viện nhỏ

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

### 1. Thu thập thông tin
Hỏi người dùng, hoặc tự giả định và nêu rõ điều đó:
- Phiên bản hiện tại (`MAJOR.MINOR.PATCH`, tùy chọn hậu tố tiền phát hành pre-release).
- Các tín hiệu thay đổi: các dòng Conventional Commit, changelog kể từ tag gần nhất, hoặc các gạch đầu dòng đã gắn nhãn (fix / feat / breaking).
- Liệu dự án có đang ở giai đoạn `0.x` hay không (sẽ có các kỳ vọng khác) và liệu API công khai ở đây có nghĩa là các cờ CLI, các route HTTP, hay các hàm/đối tượng export của thư viện.

### 2. Chạy công cụ cố vấn
```bash
python3 scripts/semver_bump.py 1.4.2 --commits commits.txt
python3 scripts/semver_bump.py 0.9.1 --commits - --pre-1-0-major-as-minor
python3 scripts/semver_bump.py 2.0.0-rc.1 --json < changes.txt
```
Một tệp chứa các commit gồm mỗi dòng một tiêu đề (tùy chọn tiền tố `type(scope):`). Các dòng có thể bao gồm phần chân trang (footer) `BREAKING CHANGE:` ở các dòng thụt lề tiếp theo, hoặc dấu `!` ngay sau loại commit. Luôn hiển thị kết quả đầu ra của công cụ, sau đó giải thích lại bằng ngôn ngữ dễ hiểu.

### 3. Tư vấn
1. Nêu rõ mức tăng phiên bản cao nhất cần thiết và lý do tại sao (trích dẫn commit hoặc dòng thay đổi mang tính quyết định).
2. Hiển thị chuỗi phiên bản tiếp theo, bao gồm cả cách đặt lại (reset) các phiên bản tiền phát hành (pre-release) khi chuyển lên bản phát hành ổn định (stable) nếu có liên quan.
3. Chỉ ra các dòng còn mơ hồ (ví dụ như `refactor` có khả năng gây lỗi tương thích) và yêu cầu người dùng xác nhận mức độ ảnh hưởng tới API khi cần thiết.
4. Trả kết quả theo mẫu `templates/bump-report.md`, tuân theo cách trình bày của `examples/example-library-release.md`.

## Quy tắc
- Không bao giờ bỏ qua việc tăng phiên bản major khi rõ ràng có mặt một thay đổi gây lỗi tương thích (breaking change), trừ khi người dùng chủ động chọn cách xử lý tăng nhẹ (soft-major) cho giai đoạn trước phiên bản 1.0.
- Không tự bịa ra các commit; nếu danh sách trống, hãy nói rõ và từ chối đoán mò.
- Giữ phần siêu dữ liệu bản dựng (`+...`) nằm ngoài phép tính tăng phiên bản; chỉ loại bỏ hoặc giữ lại phần đó theo đúng yêu cầu của người dùng.
- Ưu tiên bảng ánh xạ Conventional Commits trong `references/conventional-commits-semver.md` hơn là các nhãn tự phát.
FILE:references/conventional-commits-semver.md
# Conventional Commits → SemVer

## Ánh xạ loại commit (mặc định)

| Tín hiệu commit | Mức tăng SemVer |
|---|---|
| `fix`, `docs`, `style`, `test`, `chore`, `ci`, `perf` (không gây lỗi tương thích) | **patch** |
| `feat` (không gây lỗi tương thích) | **minor** |
| Phần chân trang `BREAKING CHANGE`, hoặc `type!:` / `type(scope)!:` | **major** |
| `refactor`, `build`, `revert` không có dấu hiệu gây lỗi tương thích | **patch** (xác nhận lại nếu API công khai bị thay đổi) |

Khi có nhiều commit xuất hiện, lấy mức tăng **cao nhất**: major > minor > patch.

## Dấu hiệu thay đổi gây lỗi tương thích (Breaking change markers)
1. Một dòng ở phần chân trang (footer) bắt đầu bằng `BREAKING CHANGE:` hoặc `BREAKING-CHANGE:`.
2. Dấu `!` nằm ngay trước dấu hai chấm: `feat!: ...` hoặc `feat(api)!: ...`.

## Phạm vi (Scopes)
Bản thân phần phạm vi (scope) không làm thay đổi mức tăng phiên bản. `feat(cli): add --json` vẫn là mức minor. `fix(docs): typo` vẫn là mức patch.

## Các phiên bản tiền phát hành (Pre-release versions)
Giả sử phiên bản hiện tại là `1.2.0-rc.3`:
- Một thay đổi ở mức patch khi vẫn đang trong giai đoạn RC thường sẽ trở thành `1.2.0-rc.4` nếu bạn tiếp tục duy trì trên kênh RC.
- Việc chuyển lên bản ổn định (stable) sau giai đoạn RC là quyết định về mặt sản phẩm: thường sẽ là `1.2.0` (chứ không phải `1.2.1`) khi bản RC đó vốn đã đại diện cho bản minor đó rồi.

Script của kỹ năng này xử lý phần nhân số học `MAJOR.MINOR.PATCH` cho việc tính toán tăng phiên bản và in riêng các ghi chú hướng dẫn xử lý hậu tố pre-release.
FILE:references/edge-cases.md
# Các trường hợp biên (Edge cases)

## Giai đoạn trước 1.0 (`0.x.y`)
SemVer cho phép các thay đổi gây lỗi tương thích trong nhánh `0.x` mà không cần tăng phiên bản major, nhưng nhiều nhóm vẫn dùng quy tắc `feat` → minor và breaking → major để đảm bảo sự rõ ràng. Hãy đưa ra cả hai lựa chọn:
- Tuân thủ linh hoạt theo SemVer: thay đổi gây lỗi tương thích trong giai đoạn `0.x` có thể chỉ cần tăng mức minor.
- Chính sách nội bộ của nhóm: coi thay đổi gây lỗi tương thích là mức major ngay cả trước phiên bản 1.0 (tắt cờ `--pre-1-0-major-as-minor`).

## Đa gói (Multiple packages)
Các kho lưu trữ kiểu monorepo có thể tăng phiên bản cho từng gói một cách độc lập. Hãy chạy công cụ cố vấn một lần cho mỗi gói chỉ với các commit thuộc về gói đó.

## Các bản refactor mơ hồ
`refactor: rename export Foo to Bar` là thay đổi gây lỗi tương thích đối với các thư viện ngay cả khi không có dấu `!`. Hãy cảnh báo và hỏi lại người dùng: nếu API công khai bị đổi tên → tăng major; nếu chỉ thay đổi nội bộ → tăng patch.

## Các dòng trống hoặc không theo chuẩn Conventional
Các câu mô tả thông thường không có tiền tố phân loại vẫn được tính nếu chúng ghi rõ là "breaking" hoặc "add feature"; nếu không, hãy đánh dấu chúng là **unknown** (chưa rõ) và hỏi lại người dùng.

## Siêu dữ liệu bản dựng (Build metadata)
`1.2.3+build.9` — hãy tăng phần số phiên bản cốt lõi, sau đó quyết định xem nên bỏ đi hay làm mới phần `+build...`. Không để siêu dữ liệu ảnh hưởng đến phép tính major/minor/patch.
FILE:templates/bump-report.md
# Báo cáo tăng phiên bản SemVer

## Phiên bản hiện tại
- …

## Mức tăng khuyến nghị
- Cấp độ: patch | minor | major
- Phiên bản tiếp theo: …
- Thay đổi mang tính quyết định: …

## Bảng thống kê thay đổi
| Cấp độ | Số lượng | Ví dụ |
|---|---|---|
| major | | |
| minor | | |
| patch | | |
| unknown | | |

## Ghi chú và câu hỏi
- …

## Lệnh gắn thẻ (tag) / phát hành đề xuất
```bash
# example
git tag -a vX.Y.Z -m "…"
```
FILE:examples/example-library-release.md
# Ví dụ: phát hành thư viện từ các Conventional Commits

**Phiên bản hiện tại:** `1.4.2`

**Các commit kể từ bản v1.4.2:**
```
fix: handle empty Accept headers
feat(api): add cursor pagination
docs: fix README typo
feat!: remove deprecated /v1/list route
```

**Kết quả từ công cụ cố vấn:** major → `2.0.0` do có commit `feat!: remove deprecated /v1/list route`.

**Lý do:** Dấu `!` đánh dấu việc gỡ bỏ một API gây lỗi tương thích ngược. Riêng tính năng phân trang mới chỉ dừng ở mức minor (`1.5.0`), và riêng phần sửa lỗi/tài liệu chỉ ở mức patch (`1.4.3`), nhưng mức major được ưu tiên cao nhất.

**Trích đoạn báo cáo:** Khuyến nghị phát hành bản `2.0.0` kèm theo hướng dẫn chuyển đổi (migration note) cho các bên đang gọi `/v1/list`; đồng thời nêu bật tính năng phân trang bằng con trỏ (cursor pagination) như là tính năng tiêu điểm của bản cập nhật major này.
FILE:scripts/semver_bump.py
#!/usr/bin/env python3
"""Recommend a SemVer bump from Conventional Commit subjects or change lines.

Usage:
  python3 semver_bump.py 1.4.2 --commits commits.txt
  python3 semver_bump.py 0.9.1 --commits - --pre-1-0-major-as-minor
  python3 semver_bump.py 2.0.0 --json < changes.txt
"""
from __future__ import annotations

import argparse
import json
import re
import sys

TYPE_PATCH = {"fix", "docs", "style", "test", "chore", "ci", "perf", "build", "refactor", "revert"}
TYPE_MINOR = {"feat"}
COMMIT_RE = re.compile(
    r"^(?P<type>[a-zA-Z]+)(?P<scope>\([^)]*\))?(?P<breaking>!)?:\s*(?P<sub>.+)$"
)
VER_RE = re.compile(
    r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)"
    r"(?:-(?P<pre>[0-9A-Za-z.-]+))?(?:\+(?P<build>[0-9A-Za-z.-]+))?$"
)
LEVELS = {"none": 0, "patch": 1, "minor": 2, "major": 3}


def parse_version(v: str) -> dict:
    m = VER_RE.match(v.strip())
    if not m:
        raise SystemExit(f"error: not a SemVer core version: {v!r}")
    return {k: m.group(k) for k in ("major", "minor", "patch", "pre", "build")}


def classify_line(line: str) -> tuple[str, str]:
    raw = line.strip()
    if not raw or raw.startswith("#"):
        return "skip", raw
    lower = raw.lower()
    if lower.startswith("breaking change:") or lower.startswith("breaking-change:"):
        return "major", raw
    m = COMMIT_RE.match(raw)
    if m:
        if m.group("breaking"):
            return "major", raw
        t = m.group("type").lower()
        if t in TYPE_MINOR:
            return "minor", raw
        if t in TYPE_PATCH:
            return "patch", raw
        return "unknown", raw
    if "breaking" in lower:
        return "major", raw
    if lower.startswith("feat") or "new feature" in lower or lower.startswith("add "):
        return "minor", raw
    if lower.startswith("fix") or "bug" in lower:
        return "patch", raw
    return "unknown", raw


def read_lines(path: str) -> list[str]:
    if path == "-":
        return sys.stdin.read().splitlines()
    with open(path, encoding="utf-8") as f:
        return f.read().splitlines()


def bump(ver: dict, level: str, pre10_soft: bool) -> str:
    major, minor, patch = int(ver["major"]), int(ver["minor"]), int(ver["patch"])
    if level == "none":
        core = f"{major}.{minor}.{patch}"
    elif level == "major":
        if pre10_soft and major == 0:
            minor += 1
            patch = 0
        else:
            major += 1
            minor = 0
            patch = 0
        core = f"{major}.{minor}.{patch}"
    elif level == "minor":
        minor += 1
        patch = 0
        core = f"{major}.{minor}.{patch}"
    else:
        patch += 1
        core = f"{major}.{minor}.{patch}"
    return core


def main(argv: list[str]) -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("version", help="Current SemVer, e.g. 1.4.2 or 0.9.1-rc.1")
    ap.add_argument("--commits", default="-", help="File of commit subjects (default: stdin)")
    ap.add_argument("--pre-1-0-major-as-minor", action="store_true",
                    help="In 0.x, treat breaking as minor bump")
    ap.add_argument("--json", action="store_true", help="Machine-readable JSON output")
    args = ap.parse_args(argv)

    ver = parse_version(args.version)
    lines = read_lines(args.commits)
    tallies = {"major": [], "minor": [], "patch": [], "unknown": []}
    best = "none"
    for line in lines:
        level, raw = classify_line(line)
        if level == "skip":
            continue
        tallies[level].append(raw)
        if LEVELS[level] > LEVELS[best]:
            best = level

    if best == "none":
        print("error: no usable change lines", file=sys.stderr)
        return 1

    next_v = bump(ver, best, args.pre_1_0_major_as_minor)
    decisive = (tallies[best][0] if tallies[best] else "")
    note = None
    if args.pre_1_0_major_as_minor and int(ver["major"]) == 0 and best == "major":
        note = "pre-1.0 soft major: breaking treated as minor bump"
        # best label stays major for tally, but version used soft path
    result = {
        "current": args.version,
        "bump": best,
        "next": next_v,
        "decisive": decisive,
        "tally": {k: len(v) for k, v in tallies.items()},
        "examples": {k: v[:5] for k, v in tallies.items() if v},
        "note": note,
    }
    if args.json:
        json.dump(result, sys.stdout, indent=2)
        sys.stdout.write("\n")
    else:
        print(f"current: {args.version}")
        print(f"bump:    {best}")
        print(f"next:    {next_v}")
        if note:
            print(f"note:    {note}")
        print(f"decisive:{decisive}")
        print("tally:")
        for k in ("major", "minor", "patch", "unknown"):
            print(f"  {k}: {len(tallies[k])}")
            for ex in tallies[k][:5]:
                print(f"    - {ex}")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
Best PracticesDevOpsCode Review+3
F@f
0
Trình Kiểm Tra Cấu Hình Dotenv và Rò Rỉ Bí Mật (Linter)
Kỹ năng

Kiểm tra (lint) các tệp .env và .env.example để phát hiện bí mật (secrets) vô tình bị commit, tên biến yếu, khóa trùng lặp, tiền tố export, khoảng trắng quanh dấu bằng, giá trị bắt buộc bị bỏ trống và nội suy không an toàn, sau đó đề xuất một tệp ví dụ đã được làm sạch. Sử dụng khi người dùng hỏi "tệp .env này có an toàn để commit không?", "tại sao dotenv không tải được?", hoặc "so sánh tệp .env.example của tôi". Bao gồm trình kiểm tra bằng thư viện chuẩn Python đã được kiểm thử.

---
name: dotenv-config-linter
description: Kiểm tra (lint) các tệp .env và .env.example để phát hiện bí mật (secrets) vô tình bị commit, tên biến yếu, khóa trùng lặp, tiền tố export, khoảng trắng quanh dấu bằng, giá trị bắt buộc bị bỏ trống và nội suy không an toàn, sau đó đề xuất một tệp ví dụ đã được làm sạch. Sử dụng khi người dùng hỏi "tệp .env này có an toàn để commit không?", "tại sao dotenv không tải được?", hoặc "so sánh tệp .env.example của tôi". Bao gồm trình kiểm tra bằng thư viện chuẩn Python đã được kiểm thử.
---

# Trình Kiểm Tra Cấu Hình Dotenv và Rò Rỉ Bí Mật (Linter)

Bạn giúp người dùng giữ cho các tệp biến môi trường luôn chính xác và an toàn. Bạn phân biệt rõ giữa các thông tin bí mật (secrets) tuyệt đối không bao giờ được commit với các giá trị giữ chỗ (placeholders) thuộc về tệp `.env.example`, phát hiện các lỗi cú pháp khiến trình nạp `dotenv` bị lỗi, và đề xuất một tệp ví dụ đã được làm sạch. Bạn không bao giờ yêu cầu người dùng dán các thông tin bí mật trên môi trường production vào khung chat nếu chỉ cần một mẫu đã che thông tin nhạy cảm là đủ.

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

- `scripts/dotenv_lint.py` - phân tích cú pháp dotenv, báo cáo các phát hiện ở mức HIGH/WARN/INFO, và có thể xuất ra tệp `.env.example` đã được làm sạch thông tin nhạy cảm (chỉ dùng thư viện chuẩn Python 3)
- `references/dotenv-rules.md` - các quy tắc về cú pháp và an toàn bảo mật được mã hóa trong trình kiểm tra
- `references/secret-patterns.md` - các định dạng bí mật phổ biến và cách thay thế chúng bằng giá trị giữ chỗ (placeholders)
- `templates/lint-report.md` - mẫu báo cáo kết quả trả về
- `examples/example-webapp-env-review.md` - ví dụ thực tế về việc rà soát một tệp env lộn xộn của ứng dụng web

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

### 1. Thu thập thông tin
- Xác định tệp đó là `.env` (chứa bí mật cục bộ), `.env.example` (chứa giá trị giữ chỗ), hay cấu hình env cho CI.
- Những khóa (keys) nào là bắt buộc phải có khi khởi động hệ thống.
- Kho lưu trữ (repo) đã có công cụ quét bí mật (như gitleaks, trufflehog) hay chưa.

### 2. Chạy trình kiểm tra (linter)
```bash
python3 scripts/dotenv_lint.py .env
python3 scripts/dotenv_lint.py .env.example --example-mode
python3 scripts/dotenv_lint.py app.env --emit-example cleaned.env.example
python3 scripts/dotenv_lint.py - --json < sample.env
```
Ưu tiên sử dụng các mẫu đã che thông tin nhạy cảm. Nếu người dùng dán thông tin bí mật thật vào, hãy cảnh báo họ xoay vòng (rotate) lại bất kỳ khóa nào có nguy cơ đã bị lộ.

### 3. Khắc phục
1. Xử lý các phát hiện mức HIGH (nghi ngờ có thông tin bí mật thật trong tệp ví dụ, khóa bị trùng lặp, giá trị chứa khoảng trắng nhưng không đặt trong dấu ngoặc) trước tiên.
2. Đồng bộ tệp `.env.example` với các khóa bắt buộc bằng cách sử dụng các giá trị giữ chỗ từ `references/secret-patterns.md`.
3. Trả kết quả theo mẫu `templates/lint-report.md`.

## Quy tắc
- Không bao giờ khuyên người dùng commit tệp `.env` thật có chứa thông tin xác thực đang hoạt động.
- Không in toàn bộ giá trị bí mật trong phần tóm tắt; chỉ hiển thị ở dạng đã che (`sk-…XXXX`).
- Giữ nguyên tên các khóa trừ khi người dùng muốn đổi tên để rõ nghĩa hơn.
FILE:references/dotenv-rules.md
# Các quy tắc Dotenv mà kỹ năng này thực thi

## Cú pháp
- Mỗi dòng một cặp `KEY=VALUE`; ký tự `#` bắt đầu một dòng chú thích.
- Ưu tiên không để khoảng trắng xung quanh dấu `=`. Dạng `KEY = VALUE` sẽ bị cảnh báo mức WARN (một số trình nạp chấp nhận, nhưng nhiều trình nạp khác thì không).
- Các giá trị có chứa khoảng trắng hoặc ký tự `#` nên được đặt trong dấu ngoặc đơn hoặc ngoặc kép.
- Cú pháp `export KEY=VALUE` ở mức INFO (hoạt động trong các shell, nhưng dư thừa đối với hầu hết các trình phân tích cú pháp dotenv).
- Khóa trùng lặp: khóa xuất hiện sau cùng sẽ ghi đè các khóa trước trong nhiều trình nạp — đánh dấu ở mức HIGH vì ý đồ cấu hình không rõ ràng.

## An toàn bảo mật
- Tệp `.env.example` không được chứa các giá trị trông giống như bí mật thật đang hoạt động (chuỗi token ngẫu nhiên dài, khóa riêng tư private key, các tiền tố `sk-`/`ghp_`, các khóa có định dạng của AWS).
- Giá trị rỗng đối với các khóa được liệt kê là bắt buộc → mức WARN.
- Phép nội suy `OTHER` ở mức INFO; cần xác nhận khóa được tham chiếu có tồn tại.

## Đặt tên
- Ưu tiên định dạng `SCREAMING_SNAKE_CASE`.
- Tránh các tên chung chung như `PASSWORD` mà không có tiền tố chỉ rõ dịch vụ (nên dùng `DB_PASSWORD`).
FILE:references/secret-patterns.md
# Các mẫu bí mật → giá trị giữ chỗ (placeholders)

| Trông giống như | Giá trị giữ chỗ (Placeholder) |
|---|---|
| `sk-` + chuỗi chữ và số dài | `sk-your-openai-key` |
| `ghp_` / `github_pat_` | `ghp_your_token` |
| `AKIA` + 16 ký tự | `AKIA_YOUR_ACCESS_KEY` |
| Khối khóa riêng tư `-----BEGIN` | loại bỏ khỏi tệp ví dụ; thay vào đó hãy ghi chú đường dẫn tới tệp khóa |
| Chuỗi giống base64 dài từ 32 ký tự trở lên | `replace-me` hoặc `changeme_<purpose>` |
| URL cơ sở dữ liệu có chứa mật khẩu | `postgres://USER:PASSWORD@host:5432/db` |

Khi xuất tệp ví dụ, hãy thay thế các giá trị trông giống thông tin bí mật bằng giá trị giữ chỗ và giữ nguyên các cấu hình không nhạy cảm (cổng kết nối, cờ tính năng feature flags, URL công khai) nếu chúng rõ ràng không chứa thông tin mật.
FILE:templates/lint-report.md
# Báo cáo kiểm tra (lint) Dotenv

## Tệp đã rà soát
- Đường dẫn / vai trò: `.env` | `.env.example` | khác

## Tóm tắt
- HIGH: …
- WARN: …
- INFO: …

## Các vấn đề phát hiện
| Mức độ | Dòng | Khóa (Key) | Vấn đề | Cách khắc phục |
|---|---|---|---|---|

## Tệp `.env.example` đề xuất
```dotenv
…
```

## Cần xoay vòng (rotate) nếu bị lộ
- …
FILE:examples/example-webapp-env-review.md
# Ví dụ: tệp `.env.example` lộn xộn của ứng dụng web

**Đầu vào (chưa đạt):**
```
export DB_URL=postgres://app:SuperSecret123@db:5432/app
API_KEY = sk-proj-abcdefghijklmnopqrstuvwxyz012345
PORT=3000
PORT=3001
FEATURE_X=true
```

**Các phát hiện:**
- HIGH: `API_KEY` trông giống như một khóa kiểu OpenAI đang hoạt động nằm trong tệp ví dụ.
- HIGH: khóa `PORT` bị trùng lặp.
- WARN: có khoảng trắng xung quanh dấu `=` ở khóa `API_KEY`.
- INFO: có tiền tố `export` ở khóa `DB_URL`.
- HIGH: mật khẩu cơ sở dữ liệu được nhúng trực tiếp trong URL ví dụ.

**Tệp ví dụ sau khi làm sạch:**
```
DB_URL=postgres://USER:PASSWORD@db:5432/app
API_KEY=sk-your-openai-key
PORT=3000
FEATURE_X=true
```
FILE:scripts/dotenv_lint.py
#!/usr/bin/env python3
"""Lint .env / .env.example files for syntax and likely secrets.

Usage:
  python3 dotenv_lint.py .env
  python3 dotenv_lint.py .env.example --example-mode
  python3 dotenv_lint.py app.env --emit-example cleaned.env.example
  python3 dotenv_lint.py - --json < sample.env
"""
from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path

SECRET_RES = [
    (re.compile(r"sk-[A-Za-z0-9-]{20,}"), "openai-like key"),
    (re.compile(r"ghp_[A-Za-z0-9]{20,}"), "github token"),
    (re.compile(r"github_pat_[A-Za-z0-9_]{20,}"), "github pat"),
    (re.compile(r"AKIA[0-9A-Z]{16}"), "aws access key id"),
    (re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"), "private key block"),
    (re.compile(r"(?i)password=([^&\s]{8,})"), "password in URL"),
    (re.compile(r"(?i)://[^:/@]+:[^@/]{8,}@"), "credentials embedded in URL"),
]
LINE_RE = re.compile(
    r"^(?P<export>export\s+)?(?P<key>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?P<val>.*)$"
)


def mask(val: str) -> str:
    if len(val) <= 8:
        return "***"
    return val[:3] + "…" + val[-4:]


def looks_placeholder(val: str) -> bool:
    v = val.strip().strip("'\"")
    low = v.lower()
    if (
        not v
        or "your-" in low
        or "changeme" in low
        or "replace" in low
        or "example" in low
        or low in {"password", "user", "todo", "xxx"}
        or "user:password@" in low
        or "://user:password@" in low
    ):
        return True
    # URL with obvious placeholders
    if "://" in v and "@" in v:
        if re.search(r"://(USER|user|USERNAME|username):(PASSWORD|password|PASS|pass|secret)@", v):
            return True
    return False


def parse_file(text: str) -> tuple[list[dict], list[dict]]:
    findings = []
    entries = []
    seen = {}
    for i, line in enumerate(text.splitlines(), 1):
        raw = line.rstrip("\n")
        stripped = raw.strip()
        if not stripped or stripped.startswith("#"):
            continue
        m = LINE_RE.match(stripped)
        if not m:
            findings.append({"severity": "HIGH", "line": i, "key": None,
                             "issue": "unparseable line", "fix": "use KEY=VALUE"})
            continue
        key, val, export = m.group("key"), m.group("val"), m.group("export")
        if re.search(r"\s=\s|=\s|\s=", raw.split("#")[0]) and " = " in raw:
            findings.append({"severity": "WARN", "line": i, "key": key,
                             "issue": "spaces around equals", "fix": f"{key}={val.strip()}"})
        if export:
            findings.append({"severity": "INFO", "line": i, "key": key,
                             "issue": "export prefix", "fix": "drop export for dotenv parsers"})
        if key in seen:
            findings.append({"severity": "HIGH", "line": i, "key": key,
                             "issue": f"duplicate key (also line {seen[key]})",
                             "fix": "keep a single definition"})
        seen[key] = i
        # strip inline comments for unquoted values
        store_val = val
        if val and not (val.startswith("'") or val.startswith('"')):
            if " #" in val:
                store_val = val.split(" #", 1)[0].rstrip()
        entries.append({"line": i, "key": key, "value": store_val, "raw": raw})
    return findings, entries


def secret_findings(entries: list[dict], example_mode: bool) -> list[dict]:
    out = []
    for e in entries:
        val = e["value"].strip().strip("'\"")
        if looks_placeholder(val):
            continue
        for rx, label in SECRET_RES:
            if rx.search(val):
                sev = "HIGH" if example_mode else "WARN"
                out.append({"severity": sev, "line": e["line"], "key": e["key"],
                            "issue": f"likely secret ({label}): {mask(val)}",
                            "fix": "replace with placeholder in example files; rotate if committed"})
                break
        else:
            # long random-ish token
            if example_mode and len(val) >= 32 and re.fullmatch(r"[A-Za-z0-9+/=_-]+", val):
                out.append({"severity": "HIGH", "line": e["line"], "key": e["key"],
                            "issue": f"long token-like value: {mask(val)}",
                            "fix": "use a placeholder like replace-me"})
    return out


def emit_example(entries: list[dict]) -> str:
    lines = []
    seen = set()
    for e in entries:
        if e["key"] in seen:
            continue
        seen.add(e["key"])
        val = e["value"].strip().strip("'\"")
        secret = False
        if not looks_placeholder(val):
            for rx, _ in SECRET_RES:
                if rx.search(val):
                    secret = True
                    break
            if len(val) >= 32 and re.fullmatch(r"[A-Za-z0-9+/=_-]+", val):
                secret = True
        if secret or (val and not looks_placeholder(val) and re.search(r"(?i)secret|password|token|api[_-]?key", e["key"])):
            if re.search(r"(?i)api[_-]?key|token", e["key"]):
                new_v = "replace-me-token"
            elif re.search(r"(?i)password|secret", e["key"]):
                new_v = "changeme"
            elif "://" in val and "@" in val:
                new_v = re.sub(r"://([^:/@]+):([^@/]+)@", r"://USER:PASSWORD@", val)
            else:
                new_v = "replace-me"
        else:
            new_v = val
        lines.append(f"{e['key']}={new_v}")
    return "\n".join(lines) + ("\n" if lines else "")


def main(argv: list[str]) -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("path", help="Env file path, or - for stdin")
    ap.add_argument("--example-mode", action="store_true",
                    help="Treat file as .env.example (secrets are HIGH)")
    ap.add_argument("--emit-example", metavar="PATH", help="Write sanitized example file")
    ap.add_argument("--json", action="store_true")
    args = ap.parse_args(argv)

    if args.path == "-":
        text = sys.stdin.read()
    else:
        path = Path(args.path)
        if not path.is_file():
            print(f"error: file not found: {args.path}", file=sys.stderr)
            return 2
        text = path.read_text(encoding="utf-8")
    example_mode = args.example_mode or (args.path != "-" and Path(args.path).name.endswith(".example"))
    findings, entries = parse_file(text)
    findings.extend(secret_findings(entries, example_mode))
    findings.sort(key=lambda f: ({"HIGH": 0, "WARN": 1, "INFO": 2}[f["severity"]], f["line"] or 0))

    example = emit_example(entries)
    if args.emit_example:
        Path(args.emit_example).write_text(example, encoding="utf-8")

    summary = {"HIGH": 0, "WARN": 0, "INFO": 0}
    for f in findings:
        summary[f["severity"]] += 1

    if args.json:
        json.dump({"summary": summary, "findings": findings, "example": example}, sys.stdout, indent=2)
        sys.stdout.write("\n")
    else:
        print(f"summary: HIGH={summary['HIGH']} WARN={summary['WARN']} INFO={summary['INFO']}")
        for f in findings:
            key = f["key"] or "-"
            print(f"[{f['severity']}] line {f['line']}: {key}: {f['issue']} → {f['fix']}")
        if args.emit_example:
            print(f"wrote example: {args.emit_example}")
    return 1 if summary["HIGH"] else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
Best PracticesSecurityPython+3
F@f
0
Bảng Bàn giao Chăm sóc Nhà cửa và Thú cưng (YAML)
Văn bản

Biến các ghi chú rời rạc thành một bảng bàn giao định dạng YAML dành cho người trông nhà hoặc chăm sóc thú cưng: lịch trình hàng ngày theo khung giờ, việc cho ăn và uống thuốc của thú cưng, các dấu hiệu cần gọi bác sĩ thú y ngay, cây cảnh, hệ thống trong nhà, khóa cửa, đổ rác, tình huống khẩn cấp, vị trí để các vật dụng thiết yếu, danh bạ liên hệ và nhật ký hàng ngày, đồng thời giữ các thông tin mật tách biệt khỏi bảng bàn giao.

1role: Người điều phối chăm sóc nhà cửa và thú cưng điềm tĩnh, tỉ mỉ
2task: >
3 Biến các ghi chú rời rạc của tôi thành một bảng bàn giao hoàn chỉnh, dễ đọc lướt dưới dạng YAML dành cho
4 người trông nhà hoặc người chăm sóc thú cưng sẽ ở lại hoặc ghé qua nhà tôi trong thời gian tôi đi vắng: lịch sinh hoạt
5 hàng ngày, chăm sóc thú cưng, cây cảnh, hệ thống trong nhà, an ninh, tình huống khẩn cấp và danh bạ liên hệ, được viết sao cho
6 một người chưa từng đến nhà tôi bao giờ cũng có thể làm theo mà không cần phải gọi hỏi tôi.
7
8inputs:
9 trip_dates: ${dates:Friday 14:00 to the following Sunday 20:00}
10 sitter_arrangement: ${arrangement:sitter stays overnight, works from home during the day}
...+86 dòng nữa
PlanningYAMLOrganization+2
F@f
0
Tài liệu Kỹ thuật taste
Văn bản

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
Best PracticesMarkdownDocumentation+2
F@f
0
Trình Kiểm tra Liên kết Markdown
Kỹ năng

Kiểm tra một thư mục tài liệu Markdown để tìm các liên kết gây lỗi cho người đọc - các liên kết tương đối trỏ đến những tệp bị thiếu hoặc đã đổi tên, các điểm neo (anchor) không khớp với tiêu đề nào, các điểm neo tiêu đề bị trùng lặp, cách viết hoa thường tên tệp chỉ hoạt động trên macOS, đường dẫn tuyệt đối cục bộ, các liên kết kiểu tham chiếu bị thiếu, không sử dụng hoặc trùng lặp định nghĩa, hình ảnh thiếu văn bản thay thế (alt text) và các URL trần. Hoạt động ngoại tuyến và liệt kê các URL bên ngoài để kiểm tra riêng. Bao gồm một trình kiểm tra bằng thư viện chuẩn Python với tùy chọn --self-test.

---
name: markdown-link-checker
description: Kiểm tra một thư mục tài liệu Markdown để tìm các liên kết gây lỗi cho người đọc - các liên kết tương đối trỏ đến những tệp bị thiếu hoặc đã đổi tên, các điểm neo (anchor) không khớp với tiêu đề nào, các điểm neo tiêu đề bị trùng lặp, cách viết hoa thường tên tệp chỉ hoạt động trên macOS, đường dẫn tuyệt đối cục bộ, các liên kết kiểu tham chiếu bị thiếu, không sử dụng hoặc trùng lặp định nghĩa, hình ảnh thiếu văn bản thay thế (alt text) và các URL trần. Hoạt động ngoại tuyến và liệt kê các URL bên ngoài để kiểm tra riêng. Bao gồm một trình kiểm tra bằng thư viện chuẩn Python với tùy chọn --self-test.
---

# Trình Kiểm tra Liên kết Markdown

Bạn hỗ trợ những người bảo trì dự án, chuyên viên viết tài liệu kỹ thuật và lập trình viên giữ cho tài liệu luôn dễ dàng điều hướng. Bạn tìm ra mọi liên kết bên trong một thư mục tài liệu nhưng lại không dẫn đến đâu cả (một tệp đã bị di chuyển, một tiêu đề đã đổi tên, lỗi chính tả trong điểm neo, một nhãn tham chiếu chưa từng được định nghĩa), giải thích nguyên nhân bị lỗi và trả về các bản sửa lỗi chính xác. Bạn ưu tiên làm việc ngoại tuyến trước: các liên kết nội bộ và điểm neo được kiểm tra trực tiếp từ chính các tệp tin, còn các URL bên ngoài chỉ được liệt kê ra để việc kiểm tra tài liệu không bao giờ phụ thuộc vào mạng hay gửi yêu cầu dồn dập làm quá tải máy chủ của người khác.

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

- `scripts/check_md_links.py` - duyệt qua một thư mục chứa các tệp .md/.markdown/.mdx, phân giải các liên kết tương đối và liên kết tương đối tính từ thư mục gốc, tạo các điểm neo tiêu đề theo kiểu GitHub và báo cáo các liên kết bị hỏng, điểm neo bị thiếu, điểm neo trùng lặp, các vấn đề về định nghĩa tham chiếu, thiếu văn bản thay thế (alt text) và các URL trần, kèm theo danh sách URL bên ngoài khi được yêu cầu (chỉ sử dụng thư viện chuẩn của Python 3)
- `references/anchors-and-slugs.md` - cách GitHub, GitLab, MkDocs, Docusaurus và Hugo chuyển đổi các tiêu đề thành điểm neo, xử lý trùng lặp, điểm neo tường minh và các trường hợp biên làm hỏng liên kết
- `references/finding-codes.md` - mọi mã phát hiện lỗi, mức độ nghiêm trọng, lý do tại sao vấn đề đó quan trọng và cách khắc phục thông thường, cùng với cách kiểm tra các URL bên ngoài một cách lịch sự
- `templates/link-report.md` - mẫu báo cáo để bàn giao kết quả
- `examples/README.md` - một tệp README dự án nhỏ có cài sẵn các lỗi liên kết có chủ đích
- `examples/setup-guide.md` - tài liệu hướng dẫn mà tệp trên liên kết tới, chứa thêm nhiều vấn đề khác (tên tệp sai chữ hoa/thường, sai chữ hoa/thường ở điểm neo, thiếu tham chiếu, đường dẫn cục bộ)

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

### 1. Xác định phạm vi kiểm tra
Hỏi xem thư mục nào là thư mục gốc của tài liệu, tài liệu được xuất bản bằng cách nào (hiển thị trên GitHub/GitLab, MkDocs, Docusaurus, Hugo, hay duyệt kho lưu trữ thuần túy), và liệu các liên kết như `/guide/a.md` có nghĩa là "tính từ thư mục gốc của kho lưu trữ (repo root)" hay "tính từ thư mục gốc của trang web (site root)". Quy tắc tạo điểm neo sẽ khác nhau tùy theo từng công cụ tạo trang (xem `references/anchors-and-slugs.md`); mặc định sử dụng quy tắc của GitHub và nêu rõ điều đó.

### 2. Chạy trình kiểm tra
```
python3 scripts/check_md_links.py examples --external
python3 scripts/check_md_links.py docs README.md --root . --min-severity MEDIUM
python3 scripts/check_md_links.py docs --ignore BARE_URL,UNUSED_REF_DEF --json > links.json
python3 scripts/check_md_links.py --self-test
```
Mã thoát (Exit) 0 có nghĩa là không có phát hiện mức CAO (HIGH) nào, 1 nghĩa là có ít nhất một liên kết bị hỏng, thiếu điểm neo hoặc thiếu định nghĩa tham chiếu (có thể dùng làm cổng kiểm soát CI), 2 nghĩa là đầu vào không hợp lệ. Các khối mã (code blocks) và mã nội dòng (inline code) sẽ được bỏ qua; các điểm neo dòng của GitHub (`#L10-L20`) và liên kết `mailto:` được chấp nhận.

### 3. Phân loại và sửa lỗi
Xử lý các lỗi mức HIGH trước. Đối với BROKEN_LINK, hãy tìm kiếm tên tệp trong kho lưu trữ (`git log --diff-filter=R --name-status` sẽ hiển thị lịch sử đổi tên) và cập nhật liên kết trỏ tới vị trí hiện tại của tệp. Đối với MISSING_ANCHOR, hãy đọc các tiêu đề đích: kịch bản sẽ gợi ý điểm neo hiện có gần giống nhất; hãy quyết định xem nên sửa liên kết hay sửa tiêu đề (việc thay đổi tiêu đề sẽ làm hỏng các liên kết trỏ đến từ nơi khác, vì vậy hãy ưu tiên thêm một điểm neo tường minh nếu tiêu đề cũ đang được liên kết từ bên ngoài). Đối với DUPLICATE_ANCHOR, hãy làm cho các tiêu đề trở nên duy nhất. Đối với CASE_MISMATCH, hãy sử dụng chính xác tên tệp phân biệt hoa thường. Sau đó xử lý tiếp các mục mức MEDIUM và LOW: văn bản thay thế (alt text), liên kết rỗng, định nghĩa không sử dụng, URL trần.

### 4. Các URL bên ngoài
Với tùy chọn `--external`, kịch bản sẽ liệt kê mọi URL bên ngoài cùng vị trí sử dụng chúng mà không gửi bất kỳ yêu cầu mạng nào. Nếu người dùng muốn kiểm tra các URL này, hãy thực hiện riêng một cách lịch sự (gửi yêu cầu HEAD rồi mới đến GET, chỉ gửi một vài yêu cầu trên mỗi máy chủ, đặt thời gian chờ timeout, xem các mã 429/403 là "cần kiểm tra thủ công" chứ không báo là liên kết hỏng). Xem thêm tại `references/finding-codes.md`.

### 5. Bàn giao kết quả
Điền vào mẫu `templates/link-report.md`: phạm vi và bộ quy tắc đã sử dụng, số lượng thống kê, bảng sửa lỗi gồm tệp:dòng, đích cũ, đích mới, cùng danh sách URL bên ngoài. Nếu được yêu cầu cung cấp các tệp đã chỉnh sửa, hãy trả về các tệp đó và chỉ thay đổi phần liên kết, tuyệt đối không viết lại bất kỳ nội dung nào khác.

## Ví dụ kết quả
`examples/` (2 tệp): 13 phát hiện, 6 mức HIGH. Tệp README.md liên kết tới `setup-guide.md#configuration`, nhưng tiêu đề thực tế lại là "Configure the team folder" (#configure-the-team-folder, được kịch bản gợi ý); các tệp CHANGELOG.md, ../CONTRIBUTING.md, docs/style.md và hình ảnh sơ đồ không tồn tại; tiêu đề "Usage" xuất hiện hai lần nên lần thứ hai có điểm neo là #usage-1; [roadmap] được định nghĩa nhưng không được sử dụng. Tệp setup-guide.md liên kết tới `readme.md` (chỉ có README.md tồn tại: hoạt động được trên máy Mac nhưng bị lỗi trên GitHub), sử dụng `#Usage` (các điểm neo phải viết thường), trỏ tới đường dẫn /Users/ana/... và sử dụng [support] mà không có định nghĩa. Có 4 URL bên ngoài được liệt kê, không có URL nào bị truy vấn.

## Quy tắc

- Không bao giờ tự ý truy vấn các URL bên ngoài trừ khi người dùng yêu cầu; khi thực hiện, phải giới hạn tốc độ (rate-limit) và không báo cáo mã lỗi 403/429 là liên kết hỏng.
- Chỉ sửa liên kết, không sửa nội dung: không viết lại câu chữ văn xuôi trong lúc sửa đường dẫn.
- Ưu tiên dùng liên kết tương đối giữa các tài liệu; chỉ dùng liên kết tương đối tính từ gốc (root-relative) nếu thiết lập xuất bản có hỗ trợ.
- Khi bắt buộc phải thay đổi một tiêu đề, hãy giữ cho điểm neo cũ tiếp tục hoạt động bằng một thẻ tường minh `<a id="old-anchor"></a>` nếu có các trang khác đang liên kết tới nó.
- Nêu rõ bộ quy tắc điểm neo nào bạn đã sử dụng (mặc định là GitHub) và chạy lại trình kiểm tra sau khi sửa lỗi; kết quả trả về bắt buộc phải có mã thoát là 0.
- Viết văn bản thay thế (alt text) thực chất cho hình ảnh (mô tả hình ảnh hiển thị cái gì và tại sao nó lại ở đó); để đánh giá toàn diện về khả năng tiếp cận (accessibility), hãy sử dụng một quy trình kiểm tra khả năng tiếp cận chuyên biệt.
FILE:references/anchors-and-slugs.md
# Điểm neo tiêu đề và chuỗi định danh (slug)

Một liên kết như `guide.md#install` chỉ hoạt động nếu bộ kết xuất (renderer) tạo ra chính xác chuỗi `install` làm thuộc tính id của một tiêu đề trong tệp guide.md. Mỗi bộ kết xuất lại có quy tắc tạo slug riêng, vì vậy trước tiên hãy tìm hiểu xem tài liệu được đọc ở đâu.

## GitHub (mặc định được kịch bản sử dụng)
1. Lấy văn bản tiêu đề sau khi đã kết xuất: loại bỏ các định dạng nội dòng (`code` giữ nguyên phần chữ, `**bold**` trở thành bold, `[text](url)` trở thành text, các thẻ HTML bị lược bỏ).
2. Chuyển toàn bộ thành chữ thường.
3. Loại bỏ mọi ký tự không phải là chữ cái, chữ số, khoảng trắng, dấu gạch ngang hoặc dấu gạch dưới (chữ cái bao gồm cả chữ có dấu và chữ cái không thuộc hệ Latinh; biểu tượng cảm xúc emoji và dấu câu sẽ bị xóa bỏ).
4. Biến mỗi khoảng trắng thành một dấu gạch ngang. Các khoảng trắng KHÔNG bị gộp lại: "C++ & Rust" sẽ trở thành `c--rust`.
5. Các tiêu đề trùng lặp sẽ được gắn thêm `-1`, `-2`, ... theo thứ tự xuất hiện: tiêu đề "Usage" thứ hai sẽ là `#usage-1`.

| Tiêu đề | Điểm neo |
|---|---|
| `## What's new? (v2.0)` | `#whats-new-v20` |
| ``## The `--dry-run` flag`` | `#the---dry-run-flag` |
| `## Über uns` | `#über-uns` (GitHub cũng chấp nhận dạng mã hóa phần trăm) |
| `## C++ & Rust: 100% safe?` | `#c--rust-100-safe` |
| `## snake_case_name` | `#snake_case_name` |
| `## Usage` (lần thứ hai) | `#usage-1` |

Hàm tạo slug của kịch bản khớp với thư viện github-slugger trong các trường hợp này.

## Các bộ kết xuất khác
- **GitLab**: tương tự GitHub, nhưng các dấu gạch ngang liên tiếp được gộp lại thành một (`#c-rust-100-safe`), do đó các liên kết sao chép qua lại giữa GitHub và GitLab có thể bị gãy.
- **MkDocs (Python-Markdown toc)**: mặc định lược bỏ các ký tự non-ASCII, gộp các dấu phân cách; cú pháp `{#custom-id}` khả dụng khi bật tiện ích mở rộng attr_list. Hãy kiểm tra cấu hình `markdown_extensions: toc: slugify` trong tệp mkdocs.yml.
- **Docusaurus**: tạo slug tương tự GitHub; hỗ trợ cú pháp `## Heading {#custom-id}`.
- **Hugo (goldmark)**: mặc định tương tự GitHub; cấu hình `autoHeadingIDType` có thể đặt là `github`, `github-ascii` hoặc `blackfriday`.
- **Trình xem trước của VS Code / hầu hết các IDE**: tương tự GitHub.

Nếu trang web đã xuất bản và giao diện xem kho lưu trữ không đồng nhất với nhau, hãy ưu tiên dùng các điểm neo tường minh cho bất kỳ mục nào được liên kết từ bên ngoài.

## Điểm neo tường minh
- Đặt `<a id="install"></a>` hoặc `<a name="install"></a>` ngay trước tiêu đề (hoạt động ở mọi nơi cho phép nhúng HTML).
- Dùng `## Install {#install}` ở những nơi bộ kết xuất hỗ trợ danh sách thuộc tính (MkDocs attr_list, Docusaurus, Hugo, Pandoc). Trên GitHub, phần `{#...}` sẽ hiển thị nguyên văn như văn bản thường.

## Những nguyên nhân làm hỏng điểm neo
- Đổi tên một tiêu đề ("Configuration" -> "Configure the team folder").
- Trùng lặp tiêu đề: các liên kết tới `#usage` sẽ luôn nhảy đến tiêu đề đầu tiên.
- Viết hoa chữ cái trong liên kết (`#Usage`): các điểm neo trên GitHub đều viết thường; một số trình duyệt có thể bỏ qua lỗi này, nhưng nhiều bộ kết xuất thì không.
- Sao chép điểm neo từ một bộ kết xuất khác (sự khác biệt về dấu gạch ngang giữa GitLab và GitHub).
- Các tiêu đề được tạo ra bởi lệnh nhúng (includes) hoặc bản mẫu (templates) mà trình kiểm tra không thể nhìn thấy; hãy đánh dấu bỏ qua những trường hợp này bằng tùy chọn `--ignore MISSING_ANCHOR` chỉ riêng cho thư mục đó và nêu rõ trong báo cáo.
- Các điểm neo dòng (`#L10`, `#L10-L20`) là tính năng xem tệp của GitHub; chúng được chấp nhận nhưng sẽ bị hỏng nếu tệp bị di chuyển hoặc các dòng bị dịch chuyển.
FILE:references/finding-codes.md
# Mã phát hiện lỗi

| Mã | Mức độ | Lý do quan trọng | Cách khắc phục thông thường |
|---|---|---|---|
| BROKEN_LINK | HIGH | liên kết tương đối trỏ tới một tệp hoặc thư mục không tồn tại (đã di chuyển, đổi tên, gõ sai) | liên kết tới đường dẫn hiện tại; lệnh `git log --diff-filter=R --name-status -- docs/` sẽ hiển thị lịch sử đổi tên |
| BROKEN_IMAGE | HIGH | đường dẫn hình ảnh không tồn tại; người đọc chỉ thấy biểu tượng ảnh lỗi | sửa lại đường dẫn hoặc commit tệp hình ảnh lên kho lưu trữ |
| MISSING_ANCHOR | HIGH | #fragment không khớp với bất kỳ tiêu đề hay điểm neo tường minh nào trong tệp đích | sử dụng điểm neo được gợi ý, hoặc thêm một điểm neo tường minh |
| MISSING_REF_DEF | HIGH | `[text][label]` không có định nghĩa `[label]: url`; nó sẽ hiển thị thành văn bản thuần nằm trong ngoặc vuông | thêm định nghĩa hoặc sửa lại nhãn (nhãn không phân biệt chữ hoa chữ thường) |
| DUPLICATE_ANCHOR | MEDIUM | hai tiêu đề tạo ra cùng một chuỗi định danh (slug); tiêu đề thứ hai sẽ bị gắn thêm `-1` và các liên kết sẽ luôn trỏ về tiêu đề đầu tiên | đặt tên các tiêu đề sao cho duy nhất, ví dụ: "Usage (CLI)" và "Usage (API)" |
| DUPLICATE_REF_DEF | MEDIUM | cùng một nhãn được định nghĩa hai lần; chỉ có định nghĩa đầu tiên có hiệu lực | xóa hoặc đổi tên một trong hai định nghĩa |
| CASE_MISMATCH | MEDIUM | tệp tồn tại nhưng khác cách viết hoa/thường; chạy tốt trên macOS/Windows nhưng bị lỗi trên Linux, GitHub Pages và hầu hết hệ thống CI | sử dụng chính xác tên tệp gốc |
| ANCHOR_CASE | MEDIUM | điểm neo chỉ khớp khi được chuyển thành chữ thường | viết các điểm neo bằng chữ thường |
| LOCAL_ABSOLUTE_PATH | MEDIUM | liên kết trỏ tới /Users/..., /home/..., C:\ hoặc file:// chỉ hoạt động trên máy tính của tác giả | đổi sang đường dẫn tương đối, hoặc xóa bỏ |
| EMPTY_LINK | MEDIUM | `[text]()` hoặc thuộc tính href rỗng | điền đường dẫn vào hoặc xóa bỏ liên kết |
| EMPTY_LINK_TEXT | MEDIUM | `[](url)`: không có chữ gì để nhấp chuột hoặc để trình đọc màn hình đọc lên | thêm văn bản hiển thị cho liên kết |
| MISSING_ALT | MEDIUM | hình ảnh không có văn bản thay thế (alt text); trình đọc màn hình sẽ chỉ đọc là "image" hoặc đọc tên tệp | mô tả nội dung hình ảnh hiển thị và mục đích của nó |
| UNUSED_REF_DEF | LOW | định nghĩa tham chiếu không bao giờ được sử dụng; thường là phần dư thừa còn sót lại sau khi chỉnh sửa | xóa bỏ, hoặc gắn liên kết vào nếu liên kết đó vô tình bị mất |
| BARE_URL | LOW | URL thô nằm trực tiếp trong văn bản; không thể nhấp chuột ở một số bộ kết xuất và bị trình đọc màn hình đọc từng ký tự một | chuyển thành `[văn bản mô tả](url)` hoặc `<url>` |
| OUTSIDE_ROOT | LOW | liên kết tương đối vượt ra ngoài thư mục gốc của tài liệu; sẽ bị lỗi nếu thư mục tài liệu được xuất bản độc lập | liên kết trực tiếp tới URL đã xuất bản |

Mã thoát (Exit codes): 0 = không có lỗi HIGH, 1 = có lỗi HIGH, 2 = đầu vào không hợp lệ. Sử dụng `--min-severity` và `--ignore` để tinh chỉnh cổng kiểm soát CI, đồng thời ghi nhận lại mọi cấu hình bỏ qua (ignore) trong kho lưu trữ kèm theo lý do rõ ràng.

## Các dạng liên kết mà kịch bản hiểu được
- Nội dòng (Inline): `[text](path.md#anchor "title")`, `[text](<path with spaces.md>)`, `[text](my%20notes.md)`
- Hình ảnh: `![alt](img.png)`, `<img src="img.png" alt="...">`
- Tham chiếu (Reference): `[text][label]`, `[label][]`, `[label]` kèm định nghĩa `[label]: url "title"` ở bất kỳ đâu trong cùng một tệp
- Liên kết tự động `<https://...>`, thẻ HTML `<a href="...">`, điểm neo tường minh `<a id>`, `<a name>`, `{#id}`
- Đường dẫn tương đối tính từ gốc `/guide/a.md` được phân giải từ tham số `--root` (mặc định: đối số thư mục đầu tiên)
- Được bỏ qua: các khối mã có hàng rào (fenced) và thụt lề (indented), mã nội dòng, liên kết `mailto:`/`tel:`, điểm neo dòng của GitHub, các ô danh sách công việc `[ ]` `[x]`

## Kiểm tra các URL bên ngoài (chỉ thực hiện khi được yêu cầu)
- Khử trùng lặp trước tiên: tùy chọn `--external` sẽ liệt kê mỗi URL một lần kèm theo mọi vị trí mà URL đó được sử dụng.
- Sử dụng phương thức HEAD, chuyển sang phương thức GET khi máy chủ từ chối HEAD (mã 405) hoặc trả về kết quả không trung thực.
- Chỉ gửi một hoặc hai yêu cầu đồng thời trên mỗi máy chủ đích, đặt thời gian chờ (timeout) 10 giây và khai báo User-Agent rõ ràng.
- Mã 200-299 là OK; mã 301/308 thì cập nhật liên kết sang địa chỉ mới; mã 404/410 là liên kết hỏng; mã 403, 429 và hết thời gian chờ (timeout) được xếp vào nhóm "cần kiểm tra thủ công" (do cơ chế chống bot hoặc giới hạn tần suất truy cập).
- Các điểm neo trên những trang web bên ngoài không thể kiểm tra một cách đáng tin cậy khi ngoại tuyến; hãy ghi chú lại chứ không tự suy đoán.
- Ưu tiên liên kết tới các trang ổn định, có gắn phiên bản cụ thể (tài liệu theo thẻ phiên bản, liên kết cố định permalink) thay vì trỏ tới bản "latest" (mới nhất).
FILE:templates/link-report.md
# Kiểm tra liên kết tài liệu: <kho lưu trữ hoặc thư mục>, <ngày>

**Phạm vi:** <docs/ và README.md, 42 tệp>   **Quy tắc điểm neo:** <GitHub>   **Thư mục gốc cho /links:** <thư mục gốc của repo>
**Lệnh chạy:** `python3 scripts/check_md_links.py <docs README.md --root . --external>`
**Kết quả:** <13 phát hiện: HIGH 6, MEDIUM 5, LOW 2 -> sau khi sửa: mã thoát 0>

## Bắt buộc sửa (HIGH)
| # | Tệp:Dòng | Mã lỗi | Liên kết đã viết | Vấn đề | Cách sửa (đích mới) |
|---|---|---|---|---|---|
| 1 | <README.md:11> | <MISSING_ANCHOR> | <setup-guide.md#configuration> | <tiêu đề đã bị đổi tên thành "Configure the team folder"> | <setup-guide.md#configure-the-team-folder> |

## Nên sửa (MEDIUM)
| # | Tệp:Dòng | Mã lỗi | Vấn đề | Cách sửa |
|---|---|---|---|---|
| 1 | <setup-guide.md:25> | <CASE_MISMATCH> | <readme.md so với README.md> | <README.md#contents> |

## Dọn dẹp (LOW)
- <định nghĩa [roadmap] không được sử dụng tại README.md:42 - xóa bỏ hoặc gắn liên kết tới nó>
- <URL trần tại README.md:21 -> [Tài liệu Tidepool](https://docs.example.com/tidepool)>

## Hình ảnh và văn bản thay thế (alt text)
| Tệp:Dòng | Hình ảnh | Văn bản thay thế nên dùng |
|---|---|---|
| <README.md:5> | <images/sync-diagram.png (bị thiếu)> | <"Sơ đồ: ứng dụng điện thoại gửi ghi chú tới máy chủ đồng bộ để ghi vào thư mục chung của nhóm"> |

## Các URL bên ngoài (chưa truy vấn)
| URL | Được dùng tại | Trạng thái (nếu đã kiểm tra) |
|---|---|---|
| <https://docs.example.com/tidepool> | <README.md:21> | <chưa kiểm tra> |

## Phòng ngừa cho lần sau
- <chạy trình kiểm tra trong quy trình CI mỗi khi có thay đổi về tài liệu (mã thoát 1 sẽ làm bản dựng thất bại)>
- <khi đổi tên một tiêu đề mà nơi khác đang liên kết tới, hãy giữ lại một điểm neo tường minh mang id cũ>
FILE:examples/README.md
# Tidepool

Tidepool đồng bộ các ghi chú hiện trường từ điện thoại sang một thư mục dùng chung của nhóm.

![](images/sync-diagram.png)

## Mục lục

- [Bắt đầu nhanh](#quick-start)
- [Cài đặt](setup-guide.md#install)
- [Cấu hình](setup-guide.md#configuration)
- [Xử lý sự cố](setup-guide.md#troubleshooting)
- [Câu hỏi thường gặp](#faq)

## Bắt đầu nhanh

1. Cài đặt ứng dụng (xem [hướng dẫn thiết lập][setup]).
2. Đăng nhập và chọn một thư mục nhóm.
3. Đọc [ghi chú phát hành](CHANGELOG.md) trước khi nâng cấp.

Tài liệu đầy đủ được đặt tại https://docs.example.com/tidepool và tài liệu tham chiếu
API nằm tại <https://api.example.com/tidepool/v2>.

## Cách sử dụng

Chạy lệnh `tidepool sync` để đẩy các ghi chú mới lên. Các liên kết nằm bên trong khối mã sẽ không bị kiểm tra:

```
[not a link](missing.md)
```

## Cách sử dụng

Xem [Đóng góp](../CONTRIBUTING.md) và [hướng dẫn văn phong][style].

## Câu hỏi thường gặp

**Tôi có thể đồng bộ khi ngoại tuyến không?** Các ghi chú sẽ được xếp vào hàng đợi cho đến khi điện thoại có kết nối mạng trở lại.

[setup]: setup-guide.md
[style]: docs/style.md
[roadmap]: https://example.com/tidepool/roadmap
FILE:examples/setup-guide.md
# Hướng dẫn thiết lập

## Cài đặt

Tải xuống trình cài đặt từ [trang phát hành](https://github.com/example/tidepool/releases)
hoặc tự biên dịch từ mã nguồn (xem phần [Biên dịch](#building-from-source)).

![Màn hình cài đặt với trường Thư mục nhóm được tô sáng](https://example.com/tidepool/img/settings.png)

## Cấu hình thư mục nhóm

Mở **Settings > Team folder** và dán liên kết thư mục vào. Thư mục này phải
được chia sẻ trước với bạn (xem phần [Chia sẻ](./README.md#Usage)).

## Biên dịch từ mã nguồn

Sao chép (clone) kho lưu trữ và chạy lệnh `make build`, sau đó tiếp tục với phần
[Bắt đầu nhanh](README.md#quick-start).

## Xử lý sự cố

Nếu quá trình đồng bộ bị treo, hãy kiểm tra tệp nhật ký tại [/Users/ana/tidepool/logs/sync.log](/Users/ana/tidepool/logs/sync.log)
hoặc đặt câu hỏi trong [kênh hỗ trợ][support].

Quay lại [README](readme.md#contents).
FILE:scripts/check_md_links.py
#!/usr/bin/env python3
"""Markdown link checker (Python 3 standard library only, fully offline).

Scans a docs folder (or single files) and reports link problems that break readers
before anyone clicks: relative links to files that do not exist, #anchors that match
no heading in the target file, duplicate heading anchors, file-name case mismatches
that only work on macOS/Windows, links to absolute local paths, empty links,
reference-style links whose definition is missing, unused or duplicate reference
definitions, images without alt text and bare URLs. It never touches the network;
use --external to list every external URL (with where it is used) so you can check
them separately.

Anchors follow GitHub's slug rules: heading text with inline markup removed,
lower-cased, punctuation dropped (letters, digits, spaces, "-" and "_" kept), each
space turned into "-", duplicates suffixed -1, -2, ... Explicit anchors from
<a name="x">, <a id="x">, id="x" on HTML tags and {#x} heading attributes count too.

Usage:
  python3 check_md_links.py PATH [PATH ...] [--root DIR] [--min-severity LOW|MEDIUM|HIGH]
        [--ignore CODE,CODE] [--external] [--json]
  python3 check_md_links.py --self-test
Exit codes: 0 = no HIGH findings, 1 = HIGH findings (broken links, missing anchors or
reference definitions), 2 = bad input (path not found, no Markdown files).
"""
import argparse, json, os, re, sys, tempfile, unicodedata
from urllib.parse import unquote

SEV_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
MD_EXT = (".md", ".markdown", ".mdx")
SKIP_DIRS = {".git", "node_modules", ".venv", "venv", "__pycache__", "_site", "site", "build", "dist"}

FENCE_RE = re.compile(r"^ {0,3}(`{3,}|~{3,})")
ATX_RE = re.compile(r"^ {0,3}(#{1,6})[ \t]+(.*?)[ \t]*#*[ \t]*$")
SETEXT_RE = re.compile(r"^ {0,3}(=+|-+)[ \t]*$")
REFDEF_RE = re.compile(r"^ {0,3}\[([^\]]+)\]:[ \t]*(<[^>]*>|\S+)(?:[ \t]+(\"[^\"]*\"|'[^']*'|\([^)]*\)))?[ \t]*$")
INLINE_RE = re.compile(r"(!?)\[((?:[^\[\]\\]|\\.|\[[^\]]*\])*)\]\(\s*(<[^>]*>|[^\s()]*(?:\([^\s()]*\)[^\s()]*)*)(?:\s+(\"[^\"]*\"|'[^']*'|\([^)]*\)))?\s*\)")
REFLINK_RE = re.compile(r"(!?)\[((?:[^\[\]\\]|\\.)*)\]\[([^\]]*)\]")
SHORTCUT_RE = re.compile(r"(?<![\]\\!])\[([^\[\]]+)\](?![\[(:])")
AUTOLINK_RE = re.compile(r"<((?:https?|ftp)://[^>\s]+|mailto:[^>\s]+)>")
BARE_URL_RE = re.compile(r"(?<![<(\"'=\w/])(https?://[^\s<>)\]\"']+)")
HTML_ANCHOR_RE = re.compile(r"<[a-zA-Z][^>]*?\b(?:id|name)\s*=\s*[\"']([^\"']+)[\"']")
HTML_HREF_RE = re.compile(r"<a\b[^>]*?\bhref\s*=\s*[\"']([^\"']*)[\"']", re.I)
HTML_IMG_RE = re.compile(r"<img\b([^>]*)>", re.I)
ATTR_ID_RE = re.compile(r"\s*\{#([\w\-:.]+)[^}]*\}\s*$")
SCHEME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.\-]*:")
WIN_ABS_RE = re.compile(r"^[a-zA-Z]:[\\/]")


def finding(sev, code, path, line, snippet, problem, fix):
    return {"severity": sev, "code": code, "file": path, "line": line,
            "snippet": snippet.strip()[:120], "problem": problem, "fix": fix}


def strip_inline_markup(text):
    t = re.sub(r"`([^`]*)`", r"\1", text)
    t = re.sub(r"!\[([^\]]*)\]\([^)]*\)", r"\1", t)
    t = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", t)
    t = re.sub(r"\[([^\]]*)\]\[[^\]]*\]", r"\1", t)
    t = re.sub(r"<[^>]+>", "", t)
    t = re.sub(r"(\*\*|\*|~~)(?=\S)(.+?)(?<=\S)\1", r"\2", t)
    t = re.sub(r"(?<!\w)(__|_)(?=\S)(.+?)(?<=\S)\1(?!\w)", r"\2", t)  # _x_ only at word edges
    return t


def github_slug(text):
    t = strip_inline_markup(text).strip().lower()
    out = []
    for ch in t:
        cat = unicodedata.category(ch)
        if ch in "-_ " or cat[0] in "LN" or cat == "Mn":
            out.append("-" if ch == " " else ch)
    return "".join(out)


def mask_code(lines):
    """Return (block-masked lines, fully masked lines): fenced/indented code blocks blanked;
    the second list also masks inline code spans (headings keep their code text)."""
    out, fence = [], None
    prev_blank = True
    for line in lines:
        m = FENCE_RE.match(line)
        if fence:
            if m and m.group(1)[0] == fence[0] and len(m.group(1)) >= len(fence) and not line.strip()[len(m.group(1)):].strip():
                fence = None
            out.append("")
            continue
        if m:
            fence = m.group(1)
            out.append("")
            continue
        if prev_blank and (line.startswith("    ") or line.startswith("\t")) and line.strip():
            out.append("")
            continue
        prev_blank = not line.strip()
        out.append(line)
    inline = [re.sub(r"(`+)(.+?)\1", lambda mm: mm.group(1) + "x" * len(mm.group(2)) + mm.group(1), l) for l in out]
    return out, inline


def parse_file(text):
    """Return dict with anchors (slug->line), duplicates, headings, links, refdefs, images, bare urls."""
    raw = text.splitlines()
    blocks, lines = mask_code(raw)
    anchors, dups, counts = {}, [], {}
    refdefs, links, bare, html_imgs = {}, [], [], []
    dup_defs = []
    for i, line in enumerate(lines, 1):
        heading = None
        m = ATX_RE.match(blocks[i - 1])
        if m:
            heading = m.group(2)
        elif i >= 2 and SETEXT_RE.match(line) and blocks[i - 2].strip() and not ATX_RE.match(blocks[i - 2]) \
                and not blocks[i - 2].lstrip().startswith(("-", "*", ">", "|")) and not REFDEF_RE.match(blocks[i - 2]):
            if line.strip().startswith("=") or (i >= 3 and not blocks[i - 3].strip()) or i == 2:
                heading = blocks[i - 2].strip()
        if heading is not None:
            am = ATTR_ID_RE.search(heading)
            if am:
                anchors.setdefault(am.group(1), i)
                heading = heading[:am.start()]
            base = github_slug(heading)
            n = counts.get(base, 0)
            counts[base] = n + 1
            slug = base if n == 0 else f"{base}-{n}"
            if n > 0:
                dups.append((i, heading.strip(), base, slug))
            anchors.setdefault(slug, i)
        for am in HTML_ANCHOR_RE.finditer(blocks[i - 1]):
            anchors.setdefault(am.group(1), i)
        d = REFDEF_RE.match(line)
        if d:
            label = norm_label(d.group(1))
            target = d.group(2).strip("<>")
            if label in refdefs:
                dup_defs.append((i, d.group(1), refdefs[label][1]))
            else:
                refdefs[label] = (target, i, d.group(1))
            continue
        spans = []
        for lm in INLINE_RE.finditer(line):
            target = lm.group(3).strip()
            if target.startswith("<") and target.endswith(">"):
                target = target[1:-1]
            links.append({"kind": "image" if lm.group(1) else "link", "text": lm.group(2), "target": target,
                          "line": i, "snippet": lm.group(0), "ref": None})
            spans.append(lm.span())
        for rm in REFLINK_RE.finditer(line):
            if any(a <= rm.start() < b for a, b in spans):
                continue
            label = rm.group(3) or rm.group(2)
            links.append({"kind": "image" if rm.group(1) else "link", "text": rm.group(2), "target": None,
                          "line": i, "snippet": rm.group(0), "ref": norm_label(label), "explicit": True})
            spans.append(rm.span())
        for sm in SHORTCUT_RE.finditer(line):
            if any(a <= sm.start() < b for a, b in spans):
                continue
            if re.match(r"^[ xX]$", sm.group(1)) or line[sm.end():sm.end() + 1] == "]":
                continue  # task list box or nested
            links.append({"kind": "image" if sm.start() > 0 and line[sm.start() - 1] == "!" else "link",
                          "text": sm.group(1), "target": None, "line": i, "snippet": sm.group(0),
                          "ref": norm_label(sm.group(1)), "explicit": False})
            spans.append(sm.span())
        for hm in HTML_HREF_RE.finditer(line):
            links.append({"kind": "link", "text": "", "target": hm.group(1), "line": i, "snippet": hm.group(0), "ref": None, "html": True})
        for im in HTML_IMG_RE.finditer(line):
            attrs = im.group(1)
            src = re.search(r"\bsrc\s*=\s*[\"']([^\"']*)[\"']", attrs, re.I)
            alt = re.search(r"\balt\s*=\s*[\"']([^\"']*)[\"']", attrs, re.I)
            links.append({"kind": "image", "text": alt.group(1) if alt else None, "target": src.group(1) if src else "",
                          "line": i, "snippet": im.group(0), "ref": None, "html": True})
        cleaned = AUTOLINK_RE.sub(" ", line)
        cleaned = INLINE_RE.sub(" ", cleaned)
        cleaned = re.sub(r"<[^>]+>", " ", cleaned)
        for bm in BARE_URL_RE.finditer(cleaned):
            bare.append((i, bm.group(1).rstrip(".,;:!?")))
        for am in AUTOLINK_RE.finditer(line):
            links.append({"kind": "autolink", "text": am.group(1), "target": am.group(1), "line": i, "snippet": am.group(0), "ref": None})
    return {"anchors": anchors, "dups": dups, "refdefs": refdefs, "dup_defs": dup_defs, "links": links, "bare": bare}


def norm_label(s):
    return re.sub(r"\s+", " ", s.strip()).casefold()


def is_external(target):
    return bool(SCHEME_RE.match(target)) and not WIN_ABS_RE.match(target)


def exists_exact_case(path):
    """True/False for exact-case existence, plus the real name if only a case-insensitive match exists."""
    if os.path.exists(path):
        head, tail = os.path.split(os.path.abspath(path))
        try:
            names = os.listdir(head or ".")
        except OSError:
            return True, None
        if tail in names or not tail:
            return True, None
        for n in names:
            if n.lower() == tail.lower():
                return False, n
        return True, None
    head, tail = os.path.split(os.path.abspath(path))
    try:
        for n in os.listdir(head):
            if n.lower() == tail.lower():
                return False, n
    except OSError:
        pass
    return False, None


class Checker:
    def __init__(self, root, ignore=()):
        self.root = os.path.abspath(root)
        self.ignore = set(ignore)
        self.cache = {}
        self.findings = []
        self.external = {}

    def parsed(self, path):
        path = os.path.abspath(path)
        if path not in self.cache:
            with open(path, encoding="utf-8", errors="replace") as fh:
                self.cache[path] = parse_file(fh.read())
        return self.cache[path]

    def add(self, *a):
        f = finding(*a)
        if f["code"] not in self.ignore:
            self.findings.append(f)

    def rel(self, p):
        return os.path.relpath(p, self.root)

    def check_file(self, path):
        info = self.parsed(path)
        rp = self.rel(path)
        for line, heading, base, slug in info["dups"]:
            self.add("MEDIUM", "DUPLICATE_ANCHOR", rp, line, heading,
                     f"heading '{heading}' repeats an earlier anchor #{base}; this one becomes #{slug}, so links to #{base} land on the first one",
                     "make the heading unique (add context) or add an explicit anchor")
        for line, label, first in info["dup_defs"]:
            self.add("MEDIUM", "DUPLICATE_REF_DEF", rp, line, f"[{label}]:",
                     f"reference [{label}] is defined again (first definition on line {first}); only the first one is used",
                     "delete or rename the duplicate definition")
        used = set()
        for ln in info["links"]:
            if ln["ref"] is not None:
                d = info["refdefs"].get(ln["ref"])
                if d is None:
                    if ln.get("explicit"):
                        self.add("HIGH", "MISSING_REF_DEF", rp, ln["line"], ln["snippet"],
                                 f"reference-style link uses [{ln['ref']}] but no '[{ln['ref']}]: URL' definition exists, so it renders as plain text",
                                 "add the definition at the bottom of the file or fix the label")
                    continue
                used.add(ln["ref"])
                target = d[0]
            else:
                target = ln["target"]
            self.check_link(path, rp, ln, target)
        for label, (target, line, orig) in info["refdefs"].items():
            if label not in used:
                self.add("LOW", "UNUSED_REF_DEF", rp, line, f"[{orig}]: {target}",
                         f"reference definition [{orig}] is never used", "delete it or link to it")
        for line, url in info["bare"]:
            self.add("LOW", "BARE_URL", rp, line, url,
                     "bare URL: not clickable in some renderers and read out character by character by screen readers",
                     f"use [descriptive text]({url}) or <{url}>")
            self.external.setdefault(url, []).append(f"{rp}:{line}")

    def check_link(self, path, rp, ln, target):
        kind = ln["kind"]
        if kind == "image":
            alt = ln["text"]
            if alt is None or not alt.strip():
                self.add("MEDIUM", "MISSING_ALT", rp, ln["line"], ln["snippet"],
                         "image has no alt text", "describe what the image shows or why it is there (alt=\"\" only for purely decorative images in HTML)")
        if target is None:
            return
        if not target.strip():
            self.add("MEDIUM", "EMPTY_LINK", rp, ln["line"], ln["snippet"], f"{kind} has an empty target", "add the URL or path, or remove the brackets")
            return
        if kind == "link" and not ln.get("html") and not strip_inline_markup(ln["text"]).strip():
            self.add("MEDIUM", "EMPTY_LINK_TEXT", rp, ln["line"], ln["snippet"], "link has no visible text", "put words inside the [ ]")
        if target.startswith("mailto:") or target.startswith("tel:"):
            return
        if target.startswith("file:") or WIN_ABS_RE.match(target) or re.match(r"^/(home|Users|tmp|var|mnt|opt)/", target):
            self.add("MEDIUM", "LOCAL_ABSOLUTE_PATH", rp, ln["line"], ln["snippet"],
                     f"'{target}' points at a path on the author's machine; it is broken for everyone else",
                     "use a path relative to this file")
            return
        if is_external(target):
            if target.startswith(("http://", "https://", "ftp://")):
                self.external.setdefault(target, []).append(f"{rp}:{ln['line']}")
            return
        target_path, _, frag = target.partition("#")
        target_path = unquote(target_path.split("?")[0])
        frag = unquote(frag)
        if target_path:
            if target_path.startswith("/"):
                full = os.path.normpath(os.path.join(self.root, target_path.lstrip("/")))
            else:
                full = os.path.normpath(os.path.join(os.path.dirname(path), target_path))
            ok, real = exists_exact_case(full)
            if not ok and real:
                self.add("MEDIUM", "CASE_MISMATCH", rp, ln["line"], ln["snippet"],
                         f"'{target_path}' only matches '{real}' with different letter case; works on macOS/Windows, breaks on Linux and GitHub Pages",
                         f"use the exact name '{real}'")
                full = os.path.join(os.path.dirname(full), real)
            elif not ok:
                code = "BROKEN_IMAGE" if kind == "image" else "BROKEN_LINK"
                self.add("HIGH", code, rp, ln["line"], ln["snippet"],
                         f"'{target_path}' does not exist" + (f" (resolved to {self.rel(full)})" if self.rel(full) != os.path.normpath(target_path) else ""),
                         "fix the path (moved or renamed file?) or remove the link")
                return
            if not os.path.abspath(full).startswith(self.root + os.sep) and os.path.abspath(full) != self.root:
                self.add("LOW", "OUTSIDE_ROOT", rp, ln["line"], ln["snippet"],
                         f"'{target_path}' leaves the docs root; it breaks if the docs are published on their own",
                         "link to the published URL or copy the file into the docs")
        else:
            full = path
        if frag:
            if os.path.isdir(full) or not full.lower().endswith(MD_EXT):
                return
            info = self.parsed(full)
            if frag in info["anchors"]:
                return
            if frag.lower() in info["anchors"]:
                self.add("MEDIUM", "ANCHOR_CASE", rp, ln["line"], ln["snippet"],
                         f"#{frag} only matches #{frag.lower()} case-insensitively; GitHub anchors are lower-case",
                         f"use #{frag.lower()}")
                return
            if re.match(r"^L\d+(-L\d+)?$", frag):
                return  # GitHub line anchors
            key = frag.lower().replace("-", "")
            close = [a for a in info["anchors"] if a.replace("-", "") == key] or \
                    [a for a in info["anchors"] if len(key) >= 5 and key[:6] in a.replace("-", "")]
            hint = f" Did you mean #{close[0]}?" if close else ""
            self.add("HIGH", "MISSING_ANCHOR", rp, ln["line"], ln["snippet"],
                     f"#{frag} matches no heading or anchor in {self.rel(full)}.{hint}",
                     "link to an existing heading slug, or add the heading / an explicit anchor")


def collect(paths):
    files = []
    for p in paths:
        if os.path.isdir(p):
            for dirpath, dirnames, filenames in os.walk(p):
                dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIRS and not d.startswith("."))
                files += [os.path.join(dirpath, f) for f in sorted(filenames) if f.lower().endswith(MD_EXT)]
        elif os.path.isfile(p):
            files.append(p)
        else:
            raise FileNotFoundError(p)
    return files


def run(paths, root=None, ignore=(), min_sev="LOW"):
    files = collect(paths)
    if root is None:
        root = paths[0] if os.path.isdir(paths[0]) else os.path.dirname(os.path.abspath(paths[0]))
    ck = Checker(root, ignore)
    for f in files:
        ck.check_file(os.path.abspath(f))
    lim = SEV_ORDER[min_sev]
    found = [f for f in ck.findings if SEV_ORDER[f["severity"]] <= lim]
    found.sort(key=lambda f: (f["file"], f["line"], SEV_ORDER[f["severity"]]))
    return files, found, ck.external


def report(files, found, external, show_external, roots):
    counts = {s: sum(1 for f in found if f["severity"] == s) for s in SEV_ORDER}
    out = [f"# Markdown link check: {', '.join(roots)}", "",
           f"Files: {len(files)}   Findings: {len(found)} (HIGH {counts['HIGH']}, MEDIUM {counts['MEDIUM']}, LOW {counts['LOW']})   External URLs: {len(external)} (not checked, offline)", ""]
    if not found:
        out.append("No findings.")
    else:
        out += ["| Sev | Code | File:Line | Problem | Fix |", "|---|---|---|---|---|"]
        for f in found:
            out.append(f"| {f['severity']} | {f['code']} | {f['file']}:{f['line']} | {f['problem'].replace('|', '/')} | {f['fix'].replace('|', '/')} |")
    if show_external and external:
        out += ["", "## External URLs to check (no network calls were made)", "", "| URL | Used at |", "|---|---|"]
        for url in sorted(external):
            out.append(f"| {url} | {', '.join(external[url])} |")
    return "\n".join(out)


def self_test():
    cases = []

    def case(name, files, expect, absent=(), target="README.md"):
        cases.append((name, files, expect, absent, target))

    case("clean file", {"README.md": "# Title\n\nSee [setup](setup.md#install).\n", "setup.md": "# Setup\n\n## Install\n"}, [], ["BROKEN_LINK", "MISSING_ANCHOR"])
    case("broken relative link", {"README.md": "[x](docs/missing.md)\n"}, ["BROKEN_LINK"])
    case("broken image", {"README.md": "![logo](img/logo.png)\n"}, ["BROKEN_IMAGE"])
    case("missing anchor other file", {"README.md": "[x](a.md#nope)\n", "a.md": "# Yes\n"}, ["MISSING_ANCHOR"])
    case("missing anchor same file", {"README.md": "# Intro\n[x](#usage)\n"}, ["MISSING_ANCHOR"])
    case("anchor with punctuation", {"README.md": "## What's new? (v2.0)\n[x](#whats-new-v20)\n"}, [], ["MISSING_ANCHOR"])
    case("anchor with inline code", {"README.md": "## The `--dry-run` flag\n[x](#the---dry-run-flag)\n"}, [], ["MISSING_ANCHOR"])
    case("underscores inside words kept", {"README.md": "## snake_case_name and _emphasis_\n[x](#snake_case_name-and-emphasis)\n"}, [], ["MISSING_ANCHOR"])
    case("unicode heading anchor", {"README.md": "## Über uns\n[x](#über-uns)\n"}, [], ["MISSING_ANCHOR"])
    case("duplicate heading", {"README.md": "## Usage\n## Usage\n[x](#usage-1)\n"}, ["DUPLICATE_ANCHOR"], ["MISSING_ANCHOR"])
    case("setext heading", {"README.md": "Install\n=======\n[x](#install)\n"}, [], ["MISSING_ANCHOR"])
    case("explicit html anchor", {"README.md": '<a id="faq"></a>\n[x](#faq)\n'}, [], ["MISSING_ANCHOR"])
    case("attribute id anchor", {"README.md": "## Setup {#custom-setup}\n[x](#custom-setup)\n"}, [], ["MISSING_ANCHOR"])
    case("anchor case", {"README.md": "## Usage\n[x](#Usage)\n"}, ["ANCHOR_CASE"], ["MISSING_ANCHOR"])
    case("near-miss hint", {"README.md": "## Getting Started\n[x](#gettingstarted)\n"}, ["MISSING_ANCHOR"])
    case("missing ref def", {"README.md": "See [the guide][guide].\n"}, ["MISSING_REF_DEF"])
    case("unused ref def", {"README.md": "Text.\n\n[old]: https://example.com\n"}, ["UNUSED_REF_DEF"])
    case("used ref def shortcut", {"README.md": "See [guide].\n\n[guide]: setup.md\n", "setup.md": "# S\n"}, [], ["UNUSED_REF_DEF", "BROKEN_LINK"])
    case("ref def case-insensitive", {"README.md": "See [Guide][G].\n\n[g]: setup.md\n", "setup.md": "# S\n"}, [], ["MISSING_REF_DEF", "UNUSED_REF_DEF"])
    case("ref def broken target", {"README.md": "See [g][].\n\n[g]: nope.md\n"}, ["BROKEN_LINK"])
    case("duplicate ref def", {"README.md": "[a][x]\n\n[x]: https://a.example\n[x]: https://b.example\n"}, ["DUPLICATE_REF_DEF"])
    case("image without alt", {"README.md": "![](shot.png)\n", "shot.png": "png"}, ["MISSING_ALT"], ["BROKEN_IMAGE"])
    case("html img without alt", {"README.md": '<img src="shot.png" width="300">\n', "shot.png": "png"}, ["MISSING_ALT"])
    case("bare url", {"README.md": "Docs at https://example.com/docs today.\n"}, ["BARE_URL"])
    case("autolink is not bare", {"README.md": "Docs at <https://example.com/docs>.\n"}, [], ["BARE_URL"])
    case("code block ignored", {"README.md": "```\n[x](missing.md) https://x.example\n```\n"}, [], ["BROKEN_LINK", "BARE_URL"])
    case("inline code ignored", {"README.md": "Use `[x](missing.md)` syntax.\n"}, [], ["BROKEN_LINK"])
    case("case mismatch", {"README.md": "[c](CONTRIBUTING.MD)\n", "CONTRIBUTING.md": "# C\n"}, ["CASE_MISMATCH"], ["BROKEN_LINK"])
    case("local absolute path", {"README.md": "[x](/Users/ana/repo/docs/a.md)\n"}, ["LOCAL_ABSOLUTE_PATH"])
    case("empty target", {"README.md": "[todo]()\n"}, ["EMPTY_LINK"])
    case("url-encoded path", {"README.md": "[x](my%20notes.md)\n", "my notes.md": "# N\n"}, [], ["BROKEN_LINK"])
    case("angle-bracket path with spaces", {"README.md": "[x](<my notes.md>)\n", "my notes.md": "# N\n"}, [], ["BROKEN_LINK"])
    case("directory link ok", {"README.md": "[api](api/)\n", "api/index.md": "# API\n"}, [], ["BROKEN_LINK"])
    case("root-relative link", {"README.md": "[x](/guide/a.md)\n", "guide/a.md": "# A\n"}, [], ["BROKEN_LINK"])
    case("github line anchor", {"README.md": "[x](setup.md#L10)\n", "setup.md": "# S\n"}, [], ["MISSING_ANCHOR"])
    case("task list not a link", {"README.md": "- [ ] write docs\n- [x] done\n"}, [], ["MISSING_REF_DEF"])
    case("mailto ignored", {"README.md": "[mail](mailto:team@example.com)\n"}, [], ["BROKEN_LINK"])
    case("nested subdir relative", {"README.md": "x", "guide/a.md": "[b](../b.md#top)\n", "b.md": "# Top\n"}, [], ["BROKEN_LINK", "MISSING_ANCHOR"], "guide/a.md")

    passed = 0
    for name, files, expect, absent, target in cases:
        with tempfile.TemporaryDirectory() as d:
            for rel, body in files.items():
                p = os.path.join(d, rel)
                os.makedirs(os.path.dirname(p), exist_ok=True)
                with open(p, "w", encoding="utf-8") as fh:
                    fh.write(body)
            _, found, _ = run([d], root=d)
            codes = {f["code"] for f in found if f["file"] == target}
            ok = all(c in codes for c in expect) and not any(c in codes for c in absent)
            if name == "clean file":
                ok = ok and not found
            if name == "near-miss hint":
                ok = ok and any("Did you mean #getting-started" in f["problem"] for f in found)
        passed += ok
        print(("PASS " if ok else "FAIL ") + name + ("" if ok else f" -> got {sorted(codes)}"))
    # external URL listing and report rendering
    with tempfile.TemporaryDirectory() as d:
        with open(os.path.join(d, "a.md"), "w") as fh:
            fh.write("[x](https://example.com/a) and [y](https://example.com/a)\n")
        files, found, ext = run([d])
        ok = list(ext) == ["https://example.com/a"] and len(ext["https://example.com/a"]) == 2
        text = report(files, found, ext, True, [d])
        ok = ok and "External URLs to check" in text and "No findings." in text
        passed += ok
        print(("PASS " if ok else "FAIL ") + "external URLs listed once with locations, no network")
    total = len(cases) + 1
    print(f"{passed}/{total} passed")
    return 0 if passed == total else 1


def main(argv=None):
    ap = argparse.ArgumentParser(description="Offline Markdown link and anchor checker")
    ap.add_argument("paths", nargs="*")
    ap.add_argument("--root", help="docs root for /absolute links and the OUTSIDE_ROOT check (default: first folder)")
    ap.add_argument("--min-severity", default="LOW", choices=list(SEV_ORDER))
    ap.add_argument("--ignore", default="", help="comma-separated finding codes to skip")
    ap.add_argument("--external", action="store_true", help="list external URLs to check separately")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args(argv)
    if a.self_test:
        return self_test()
    if not a.paths:
        ap.print_usage(sys.stderr)
        return 2
    try:
        files, found, ext = run(a.paths, a.root, [c.strip() for c in a.ignore.split(",") if c.strip()], a.min_severity)
    except FileNotFoundError as e:
        print(f"error: path not found: {e}", file=sys.stderr)
        return 2
    if not files:
        print("error: no Markdown files found", file=sys.stderr)
        return 2
    if a.json:
        print(json.dumps({"files": len(files), "findings": found, "external": ext if a.external else {}}, indent=2, ensure_ascii=False))
    else:
        print(report(files, found, ext, a.external, a.paths))
    return 1 if any(f["severity"] == "HIGH" for f in found) else 0


if __name__ == "__main__":
    sys.exit(main())
PythonMarkdownDocumentation+2
F@f
0