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 Backend

47 prompt
Công cụ Kiểm tra Lệch chuẩn Đặc tả OpenAPI
Kỹ năng

So sánh đặc tả OpenAPI YAML/JSON với danh sách các route đã được triển khai và gắn cờ cảnh báo sự lệch chuẩn (drift): thiếu thao tác (operation), thừa route, lệch phương thức (method), và khác biệt về cấu trúc tham số đường dẫn (path parameter). Sử dụng khi đánh giá PR của API, trước khi phát hành, hoặc khi hỏi "mã nguồn của chúng ta có còn khớp với OpenAPI không?". Bao gồm công cụ kiểm tra bằng thư viện chuẩn Python có hỗ trợ --self-test.

---
name: openapi-spec-drift-checker
description: So sánh đặc tả OpenAPI YAML/JSON với danh sách các route đã được triển khai và gắn cờ cảnh báo sự lệch chuẩn (drift): thiếu thao tác (operation), thừa route, lệch phương thức (method), và khác biệt về cấu trúc tham số đường dẫn (path parameter). Sử dụng khi đánh giá PR của API, trước khi phát hành, hoặc khi hỏi "mã nguồn của chúng ta có còn khớp với OpenAPI không?". Bao gồm công cụ kiểm tra bằng thư viện chuẩn Python có hỗ trợ --self-test.
---

# Công cụ Kiểm tra Lệch chuẩn Đặc tả OpenAPI

Bạn giúp các đội ngũ phát triển API tìm ra sự lệch chuẩn (drift) giữa tài liệu OpenAPI 3.x và các route mà mã nguồn của họ thực sự phục vụ. Bạn so sánh các đường dẫn (path) và phương thức (method), gắn cờ các thao tác bị thiếu hoặc thừa, đồng thời chỉ ra những điểm không khớp về tham số hoặc phản hồi làm ảnh hưởng đến phía client. Bạn không tự bịa ra các endpoint; bạn chỉ báo cáo những gì dữ liệu đầu vào thể hiện.

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

- `scripts/check_drift.py` - so sánh một đoạn đặc tả OpenAPI nhỏ với danh sách route và in ra báo cáo lệch chuẩn (chỉ dùng thư viện chuẩn Python 3)
- `references/openapi-checklist.md` - những điều cần xác minh trong đặc tả trước khi so sánh
- `references/common-drift.md` - các nguyên nhân gây lệch chuẩn thường gặp và cách khắc phục
- `templates/drift-report.md` - cấu trúc báo cáo dùng cho các PR và ghi chú phát hành (release notes)
- `examples/openapi-snippet.yaml` - đoạn mẫu OpenAPI dùng cho bản chạy thử (demo)

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

### 1. Thu thập dữ liệu đầu vào
Yêu cầu cung cấp, hoặc suy luận từ ngữ cảnh:
- Tệp OpenAPI (YAML hoặc JSON) hoặc phần paths được dán trực tiếp
- Các route đã triển khai: danh sách phương thức + đường dẫn từ bộ định tuyến (router) của framework, được trích xuất từ mã nguồn, hoặc từ cấu hình gateway
- Liệu tên tham số đường dẫn có bắt buộc phải khớp chính xác từng chữ hay không (`{id}` so với `{userId}`)

### 2. Chuẩn hóa dữ liệu
Chuyển các phương thức về chữ thường. Cắt bỏ dấu gạch chéo ở cuối đường dẫn (trailing slash). Coi `/users/{id}` và `/users/{userId}` có cùng hình thái đường dẫn trừ khi nhóm phát triển yêu cầu khớp chính xác tên tham số.

### 3. Chạy công cụ kiểm tra
```
python3 scripts/check_drift.py --spec examples/openapi-snippet.yaml --routes examples/routes.txt
python3 scripts/check_drift.py --self-test
```
Nếu danh sách các route chưa có sẵn trong tệp, hãy giúp người dùng trích xuất chúng từ mã nguồn trước.

### 4. Phân loại các phát hiện
- **missing_in_code**: có trong OpenAPI, nhưng chưa được triển khai trong mã nguồn
- **missing_in_spec**: đã triển khai trong mã nguồn, nhưng không có trong OpenAPI
- **method_mismatch**: đường dẫn có tồn tại nhưng khác phương thức HTTP (verb)
- **param_shape_diff**: cùng mẫu đường dẫn, nhưng khác tên tham số (chế độ nghiêm ngặt tùy chọn)

### 5. Trả lời
Trả về bảng tóm tắt số lượng ngắn gọn, sau đó liệt kê từng phát hiện kèm mức độ nghiêm trọng (high/medium/low) và gợi ý cách khắc phục. Sử dụng `templates/drift-report.md` để viết bình luận vào PR.

## Quy tắc

- Ưu tiên cập nhật lại tài liệu theo thực tế khi mã nguồn được viết có chủ đích và bản đặc tả đã lỗi thời; ưu tiên viết bổ sung mã nguồn khi bản đặc tả là hợp đồng giao tiếp đã được công bố cho các client.
- Không bao giờ tuyên bố rằng đã thực hiện đánh giá bảo mật; chỉ báo cáo sự lệch chuẩn về mặt cấu trúc.
- Nếu không thể phân tích cú pháp (parse) tệp OpenAPI, hãy dừng lại và hiển thị lỗi phân tích cú pháp đó.
FILE:references/openapi-checklist.md
# Danh sách kiểm tra OpenAPI trước khi đối chiếu

Trước khi chạy kiểm tra lệch chuẩn, hãy xác nhận bản đặc tả có tính nhất quán:

1. Trường **openapi** là `3.0.x` hoặc `3.1.x`.
2. Phần **paths** có tồn tại và bắt đầu bằng dấu `/`.
3. Mọi thao tác (operation) đều có phương thức HTTP (`get`, `post`, `put`, `patch`, `delete`, `head`, `options`).
4. Các tham số đường dẫn được khai báo trong đường dẫn (`{id}`) cũng xuất hiện dưới mục `parameters` với `in: path` và `required: true`.
5. Các đích tham chiếu `$ref` đều phân giải được (hoặc bạn đang so sánh một tài liệu đã được phân giải/đóng gói hoàn chỉnh).
6. Đã nắm rõ đường dẫn cơ sở (base path) trong URL của servers: nếu `servers[].url` bao gồm `/api/v1`, các route có thể được liệt kê kèm hoặc không kèm tiền tố đó—hãy chọn một quy ước thống nhất và giữ nguyên trong suốt quá trình so sánh.
7. Các thao tác không còn khuyên dùng (deprecated) vẫn phải được liệt kê cho đến khi bị xóa hoàn toàn khỏi mã nguồn; hãy đánh dấu chúng trong báo cáo thay vì bỏ qua.

## Cấu trúc paths tối thiểu

```yaml
paths:
  /pets:
    get:
      summary: List pets
    post:
      summary: Create pet
  /pets/{petId}:
    get:
      parameters:
        - name: petId
          in: path
          required: true
          schema: { type: string }
```
FILE:references/common-drift.md
# Các trường hợp lệch chuẩn OpenAPI thường gặp

| Triệu chứng | Nguyên nhân có thể | Cách khắc phục |
| --- | --- | --- |
| Đặc tả có `GET /users`, mã nguồn không có | Đặc tả được viết trước khi triển khai mã nguồn | Triển khai mã nguồn hoặc xóa khỏi bản đặc tả đã công bố |
| Mã nguồn có `POST /users/{id}/reset`, thiếu trong đặc tả | Bản vá nóng (hotfix) được đưa lên mà không cập nhật tài liệu | Thêm thao tác vào đặc tả; tăng phiên bản phụ (minor) nếu là API công khai |
| Đặc tả là `PUT`, mã nguồn là `PATCH` | Ngữ nghĩa cập nhật một phần đã thay đổi | Đồng bộ lại phương thức; lưu ý thay đổi gây lỗi tương thích (breaking change) nếu client đang gọi PUT |
| `{userId}` so với `{id}` | Framework tự động đặt tên so với đặc tả viết tay | Chuẩn hóa khi so sánh; ghi tài liệu quy tắc đặt tên |
| Chỉ có dấu gạch chéo cuối trong mã nguồn | Bộ định tuyến (router) chấp nhận linh hoạt dấu gạch chéo | Cắt bỏ khi so sánh; chọn dạng chuẩn tắc (canonical) trong đặc tả |
| Đường dẫn cơ sở `/api` chỉ có ở gateway | Mục servers trong đặc tả bỏ qua tiền tố của gateway | Cắt bỏ hoặc thêm cùng một tiền tố ở cả hai phía |
| Webhooks / callbacks chỉ có trong đặc tả | Không phải là các route HTTP thông thường | Loại trừ khỏi việc so sánh route |
| Route tải tệp lên chưa được ghi tài liệu | Bổ sung multipart vào giai đoạn muộn | Ghi tài liệu `multipart/form-data` vào requestBody |

## Hướng dẫn đánh giá mức độ nghiêm trọng

- **High**: các client công khai đã và đang gọi thao tác này; thiếu phương thức hoặc đường dẫn ở một trong hai phía.
- **Medium**: route chỉ dùng nội bộ bị thiếu trong đặc tả; đổi tên tham số nhưng có bí danh (alias).
- **Low**: dấu gạch chéo ở cuối, nhiễu từ các phương thức HEAD/OPTIONS tùy chọn, đã đánh dấu deprecated nhưng vẫn còn tồn tại.
FILE:templates/drift-report.md
# Báo cáo lệch chuẩn OpenAPI

- **Đặc tả (Spec)**: <path or URL>
- **Nguồn trích xuất route**: <router dump / gateway / manual>
- **Thời điểm so sánh**: <ISO timestamp>
- **Tóm tắt**: missing_in_code=<n>, missing_in_spec=<n>, method_mismatch=<n>, param_shape_diff=<n>

## Các phát hiện

| Mức độ nghiêm trọng | Loại | Đặc tả (Spec) | Mã nguồn (Code) | Ghi chú |
| --- | --- | --- | --- | --- |
| high | missing_in_code | `GET /pets/{petId}` | — | Triển khai hoặc gỡ khỏi tài liệu công bố |
| medium | missing_in_spec | — | `POST /pets/{petId}/adopt` | Bổ sung vào tài liệu trước khi phát hành |

## Quyết định

- [ ] Cập nhật OpenAPI
- [ ] Cập nhật mã nguồn
- [ ] Chấp nhận như một ngoại lệ đã biết (nêu rõ lý do)

## Các việc cần theo dõi tiếp

- [ ]
FILE:examples/openapi-snippet.yaml
openapi: 3.0.3
info:
  title: Pets demo
  version: 0.1.0
paths:
  /pets:
    get:
      summary: List pets
      operationId: listPets
    post:
      summary: Create pet
      operationId: createPet
  /pets/{petId}:
    get:
      summary: Get pet by id
      operationId: getPet
      parameters:
        - name: petId
          in: path
          required: true
          schema:
            type: string
    delete:
      summary: Delete pet
      operationId: deletePet
      parameters:
        - name: petId
          in: path
          required: true
          schema:
            type: string
  /health:
    get:
      summary: Health check
      operationId: health
FILE:scripts/check_drift.py
#!/usr/bin/env python3
"""Compare OpenAPI paths to an implemented route list.

Usage:
    python3 check_drift.py --spec openapi.yaml --routes routes.txt
    python3 check_drift.py --spec openapi.yaml --route 'GET /pets' --route 'POST /pets'
    python3 check_drift.py --self-test

Route file format: one "METHOD /path" per line. Comments (#) and blank lines ignored.
Python 3 standard library only.
"""
from __future__ import annotations

import argparse
import json
import re
import sys
from typing import Dict, List, Set, Tuple

try:
    import yaml  # type: ignore
except ImportError:
    yaml = None

PathKey = str  # normalized path template
Method = str


def normalize_path(path: str) -> str:
    path = path.strip()
    if not path.startswith("/"):
        path = "/" + path
    if len(path) > 1 and path.endswith("/"):
        path = path.rstrip("/")
    # unify parameter names to {}
    return re.sub(r"\{[^}]+\}", "{}", path)


def normalize_method(m: str) -> str:
    return m.strip().upper()


def parse_routes(lines: List[str]) -> Dict[PathKey, Set[Method]]:
    out: Dict[PathKey, Set[Method]] = {}
    for raw in lines:
        line = raw.split("#", 1)[0].strip()
        if not line:
            continue
        parts = line.split()
        if len(parts) < 2:
            raise ValueError(f"Bad route line: {raw!r}")
        method, path = normalize_method(parts[0]), normalize_path(parts[1])
        out.setdefault(path, set()).add(method)
    return out


def _parse_openapi_json(text: str) -> dict:
    return json.loads(text)


def _parse_openapi_yaml_simple(text: str) -> dict:
    """Minimal YAML subset parser for simple OpenAPI path maps when PyYAML is absent.

    Supports the examples shipped with this skill (no anchors, no multiline).
    Prefer PyYAML when installed.
    """
    # Very small fallback: extract path keys and methods via regex for our demos.
    paths: Dict[str, dict] = {}
    current_path = None
    for raw in text.splitlines():
        if raw.startswith("  /") and raw.rstrip().endswith(":"):
            current_path = raw.strip()[:-1]
            paths[current_path] = {}
            continue
        if current_path and re.match(r"^    (get|post|put|patch|delete|head|options):", raw):
            method = raw.strip().split(":", 1)[0]
            paths[current_path][method] = {}
    if not paths:
        raise ValueError("Could not parse OpenAPI YAML without PyYAML; install pyyaml or pass JSON.")
    return {"paths": paths}


def load_spec(path: str) -> Dict[PathKey, Set[Method]]:
    text = open(path, encoding="utf-8").read()
    if path.endswith(".json"):
        data = _parse_openapi_json(text)
    else:
        if yaml is not None:
            data = yaml.safe_load(text)
        else:
            # try JSON first, then simple YAML
            try:
                data = _parse_openapi_json(text)
            except json.JSONDecodeError:
                data = _parse_openapi_yaml_simple(text)
    paths = data.get("paths") or {}
    out: Dict[PathKey, Set[Method]] = {}
    http = {"get", "post", "put", "patch", "delete", "head", "options", "trace"}
    for p, item in paths.items():
        if not isinstance(item, dict):
            continue
        methods = {normalize_method(m) for m in item if m.lower() in http}
        if methods:
            out[normalize_path(p)] = methods
    return out


def compare(spec: Dict[PathKey, Set[Method]], code: Dict[PathKey, Set[Method]]) -> List[dict]:
    findings = []
    all_paths = set(spec) | set(code)
    for p in sorted(all_paths):
        s = spec.get(p, set())
        c = code.get(p, set())
        if s and not c:
            for m in sorted(s):
                findings.append({"kind": "missing_in_code", "severity": "high", "method": m, "path": p})
        elif c and not s:
            for m in sorted(c):
                findings.append({"kind": "missing_in_spec", "severity": "medium", "method": m, "path": p})
        else:
            only_spec = s - c
            only_code = c - s
            for m in sorted(only_spec):
                findings.append({"kind": "method_mismatch", "severity": "high", "method": m, "path": p, "side": "spec_only"})
            for m in sorted(only_code):
                findings.append({"kind": "method_mismatch", "severity": "high", "method": m, "path": p, "side": "code_only"})
    return findings


def format_report(findings: List[dict]) -> str:
    counts: Dict[str, int] = {}
    for f in findings:
        counts[f["kind"]] = counts.get(f["kind"], 0) + 1
    lines = ["Drift summary: " + ", ".join(f"{k}={v}" for k, v in sorted(counts.items()))]
    if not findings:
        lines.append("No drift found.")
        return "\n".join(lines)
    for f in findings:
        side = f.get("side", "")
        extra = f" ({side})" if side else ""
        lines.append(f"- [{f['severity']}] {f['kind']}: {f['method']} {f['path']}{extra}")
    return "\n".join(lines)


def self_test() -> int:
    spec = {
        normalize_path("/pets"): {"GET", "POST"},
        normalize_path("/pets/{petId}"): {"GET", "DELETE"},
        normalize_path("/health"): {"GET"},
    }
    code = {
        normalize_path("/pets"): {"GET", "POST"},
        normalize_path("/pets/{id}"): {"GET"},  # same shape, missing DELETE
        normalize_path("/pets/{id}/adopt"): {"POST"},  # extra
    }
    findings = compare(spec, code)
    kinds = sorted(f["kind"] + ":" + f["method"] + ":" + f["path"] for f in findings)

    cases = [
        ("missing delete on pet", any(f["kind"] == "method_mismatch" and f["method"] == "DELETE" for f in findings), True),
        ("extra adopt route", any(f["kind"] == "missing_in_spec" and f["path"].endswith("/adopt") for f in findings), True),
        ("health missing in code", any(f["kind"] == "missing_in_code" and f["path"] == "/health" for f in findings), True),
        ("list pets matched", not any(f["path"] == "/pets" and f["kind"] == "missing_in_code" for f in findings), True),
        ("normalize params", normalize_path("/pets/{petId}") == normalize_path("/pets/{id}"), True),
        ("normalize slash", normalize_path("/pets/") == "/pets", True),
        ("parse routes", parse_routes(["GET /pets", "POST /pets # create"]) == {"/pets": {"GET", "POST"}}, True),
        ("empty compare", compare({"/x": {"GET"}}, {"/x": {"GET"}}) == [], True),
    ]

    # parse simple yaml fallback
    sample = """openapi: 3.0.3\npaths:\n  /pets:\n    get:\n      summary: x\n    post:\n      summary: y\n"""
    parsed = _parse_openapi_yaml_simple(sample)
    cases.append(("yaml fallback paths", "get" in parsed["paths"]["/pets"] and "post" in parsed["paths"]["/pets"], True))

    failed = 0
    for name, got, expected in cases:
        ok = got == expected
        failed += not ok
        print(f"{'PASS' if ok else 'FAIL'}  {name}: got={got!r} expected={expected!r}")
    # also assert finding count sanity
    ok = len(findings) >= 3
    failed += not ok
    print(f"{'PASS' if ok else 'FAIL'}  findings_count>={3}: got={len(findings)}")
    print(f"\n{len(cases)+1 - failed}/{len(cases)+1} passed")
    return 1 if failed else 0


def main(argv=None) -> int:
    p = argparse.ArgumentParser(description="Compare OpenAPI paths to implemented routes.")
    p.add_argument("--spec", help="OpenAPI YAML or JSON file")
    p.add_argument("--routes", help="File with METHOD /path lines")
    p.add_argument("--route", action="append", default=[], help="Single METHOD /path (repeatable)")
    p.add_argument("--json", action="store_true", help="Print findings as JSON")
    p.add_argument("--self-test", action="store_true")
    a = p.parse_args(argv)
    if a.self_test:
        return self_test()
    if not a.spec:
        p.error("--spec is required (or use --self-test)")
    route_lines: List[str] = list(a.route)
    if a.routes:
        route_lines.extend(open(a.routes, encoding="utf-8").read().splitlines())
    if not route_lines:
        p.error("provide --routes and/or --route")
    spec = load_spec(a.spec)
    code = parse_routes(route_lines)
    findings = compare(spec, code)
    if a.json:
        print(json.dumps(findings, indent=2))
    else:
        print(format_report(findings))
    return 0 if not findings else 2


if __name__ == "__main__":
    sys.exit(main())
BackendWeb DevelopmentBest Practices+3
F@f
0
Thiết Kế API taste
Văn bản

Những ưu tiên mang tính định hướng rõ ràng để thiết kế các HTTP API rõ ràng và ổn định.

# Thiết Kế API taste
- Ưu tiên các URL hướng tài nguyên sử dụng danh từ và dùng các phương thức HTTP (HTTP verbs) để biểu thị hành động, không đưa các động từ kiểu RPC vào đường dẫn. Độ tin cậy: 0.95
- Đánh số phiên bản API ngay trong đường dẫn URL (`/v1/...`) và không đưa các thay đổi gây tương thích ngược (breaking changes) vào một phiên bản chính (major version) đang tồn tại. Độ tin cậy: 0.90
- Sử dụng danh từ số nhiều cho tên các tập hợp (`/users`, `/orders`) và chỉ lồng sâu tối đa một cấp khi tài nguyên con không có ý nghĩa nếu đứng độc lập. Độ tin cậy: 0.85
- Trả về dữ liệu JSON với tên trường viết theo quy tắc camelCase nhất quán và không bao giờ trộn lẫn kiểu snake_case trong cùng một API. Độ tin cậy: 0.85
- Sử dụng các mã trạng thái HTTP tiêu chuẩn: 201 khi tạo mới thành công, 204 khi xóa thành công không kèm nội dung trả về, 404 khi không tìm thấy tài nguyên, 409 khi xảy ra xung đột. Độ tin cậy: 0.90
- Đặt các tham số phân trang, sắp xếp và lọc vào tham số truy vấn (query params); trả về siêu dữ liệu phân trang trong một cấu trúc vỏ bọc ổn định (`data`, `nextCursor`). Độ tin cậy: 0.85
- Ưu tiên phân trang dựa trên con trỏ (cursor-based pagination) hơn là dùng độ lệch (offset) đối với bất kỳ danh sách nào có thể phình to. Độ tin cậy: 0.90
- Xác thực mọi phần thân yêu cầu (request body) bằng một lược đồ (zod, OpenAPI, JSON Schema) và từ chối các trường không xác định. Độ tin cậy: 0.90
- Định hình cấu trúc phản hồi lỗi giống hệt nhau ở mọi nơi: gồm `code`, `message` và trường tùy chọn `details`. Độ tin cậy: 0.90
- Ưu tiên sử dụng khóa bất biến (idempotency keys) trên các điểm cuối POST có tạo giao dịch tiền tệ, đơn hàng hoặc gây ra hiệu ứng phụ. Độ tin cậy: 0.85
- Sử dụng ETags hoặc tiêu đề `If-Match` cho các thao tác cập nhật đồng thời trên những tài nguyên có sự tranh chấp. Độ tin cậy: 0.75
- Viết tài liệu API bằng chuẩn OpenAPI 3 và sinh mã trình khách (clients) tự động từ bản hợp đồng đó, thay vì tự viết tay các bộ SDK. Độ tin cậy: 0.85
- Không bao giờ đặt thông tin bí mật (secrets) hoặc thông tin định danh cá nhân (PII) vào URL hay chuỗi truy vấn (query strings); hãy đặt chúng trong phần tiêu đề (headers) hoặc phần thân yêu cầu (body). Độ tin cậy: 0.95
- Ưu tiên cơ chế mở rộng tường minh (`?include=`) hơn là lúc nào cũng nhúng sẵn toàn bộ các thực thể liên quan vào phản hồi. Độ tin cậy: 0.80
- Giữ cho các điểm cuối ghi dữ liệu (write endpoints) nhỏ gọn: mỗi đường dẫn chỉ thực hiện một thao tác thay đổi rõ ràng thay vì gom đủ thứ cập nhật vào một chỗ. Độ tin cậy: 0.80
- Giới hạn tần suất gọi (rate-limit) theo khóa API hoặc theo người dùng và trả về tiêu đề `Retry-After` khi chạm ngưỡng giới hạn. Độ tin cậy: 0.85
- Ưu tiên định dạng dấu thời gian chuẩn ISO-8601 theo múi giờ UTC và không bao giờ tự chế ra các định dạng chuỗi ngày tháng tùy chỉnh. Độ tin cậy: 0.95
Backend
F@f
0
Di Chuyển Cơ Sở Dữ Liệu (Database Migrations) taste
Văn bản

Những ưu tiên mang tính định hướng rõ ràng để viết các kịch bản di chuyển lược đồ cơ sở dữ liệu (database schema migrations) an toàn, không gây gián đoạn dịch vụ (zero-downtime).

# Di Chuyển Cơ Sở Dữ Liệu (Database Migrations) taste
- Giữ mọi thay đổi lược đồ trong một tệp migration được quản lý phiên bản và commit ngay cạnh đoạn mã nguồn cần đến nó. Độ tin cậy: 0.95
- Viết các tệp migration bằng ngôn ngữ SQL thuần túy thay vì dùng các đoạn mã so sánh (diffs) do ORM tự sinh ra mà bạn chưa đọc kỹ. Độ tin cậy: 0.80
- Không bao giờ chỉnh sửa một tệp migration đã được chạy trên môi trường dùng chung; thay vào đó hãy thêm một tệp migration mới. Độ tin cậy: 0.95
- Sử dụng mô hình mở rộng và thu hẹp (expand and contract): thêm cột mới, điền bù dữ liệu cũ (backfill), chuyển luồng đọc sang cột mới, sau đó mới xóa cột cũ ở một bản phát hành sau. Độ tin cậy: 0.90
- Đảm bảo mọi tệp migration đều an toàn để chạy trong khi phiên bản ứng dụng trước đó vẫn đang phục vụ lưu lượng truy cập. Độ tin cậy: 0.90
- Thêm các cột mới ở dạng cho phép giá trị rỗng (nullable) hoặc có giá trị mặc định là hằng số để tránh việc phải ghi lại toàn bộ bảng kéo dài. Độ tin cậy: 0.85
- Tạo chỉ mục (index) trên các bảng Postgres lớn bằng lệnh CREATE INDEX CONCURRENTLY nằm ngoài giao dịch (transaction). Độ tin cậy: 0.90
- Thiết lập giá trị lock_timeout và statement_timeout ngắn ngay ở đầu mỗi tệp migration. Độ tin cậy: 0.85
- Chạy các tác vụ điền bù dữ liệu lớn (large backfills) dưới dạng các tiến trình chạy ngầm theo lô (batched background jobs), không chạy ngay bên trong tệp migration. Độ tin cậy: 0.90
- Chỉ giữ một thay đổi hợp lý duy nhất trong mỗi tệp migration để khi xảy ra lỗi sẽ dễ dàng đọc hiểu và chạy thử lại. Độ tin cậy: 0.80
- Đặt tên cho các tệp migration bằng tiền tố dấu thời gian UTC kèm theo một cụm động từ ngắn gọn như 20261017_add_orders_status. Độ tin cậy: 0.75
- Ưu tiên các tệp migration chỉ tiến về phía trước (forward-only) và sửa sai bằng một tệp migration mới thay vì phụ thuộc vào các kịch bản hoàn tác (down scripts). Độ tin cậy: 0.70
- Chạy các tệp migration như một bước triển khai (deploy) tách biệt trước khi mã ứng dụng mới bắt đầu khởi chạy. Độ tin cậy: 0.85
- Kiểm thử mọi tệp migration trong quy trình CI trên một cơ sở dữ liệu hoàn toàn mới và trên một bản sao dữ liệu có quy mô tương đương môi trường thực tế (production). Độ tin cậy: 0.80
- Thêm các khóa ngoại (foreign keys) và ràng buộc kiểm tra (check constraints) với tùy chọn NOT VALID trước, sau đó mới xác thực chúng ở một bước riêng biệt. Độ tin cậy: 0.80
- Chỉ đổi tên cột và bảng thông qua một khung nhìn (view) hoặc giai đoạn ghi kép (dual-write), không bao giờ đổi tên ngay trong một bước duy nhất. Độ tin cậy: 0.75
- Lưu trữ lịch sử các bản migration đã áp dụng trong cơ sở dữ liệu và cho dừng lỗi quá trình triển khai nếu phát hiện sai lệch (drift). Độ tin cậy: 0.85
- Duyệt (review) các tệp migration cẩn thận như duyệt mã nguồn ứng dụng, với câu lệnh SQL được tạo ra hiển thị rõ ràng trong pull request. Độ tin cậy: 0.90
- Tách biệt dữ liệu khởi tạo (seed) và dữ liệu mẫu kiểm thử (fixture) ra khỏi các tệp migration lược đồ. Độ tin cậy: 0.80
BackendSQLDatabase+2
F@f
0
Dịch vụ Go taste
Văn bản

Các ưu tiên mang tính định hướng rõ ràng để xây dựng và vận hành các dịch vụ backend Go nhỏ gọn, dễ bảo trì.

# Gu thiết kế dịch vụ Go của một kỹ sư backend
- Sử dụng bộ định tuyến net/http trong thư viện chuẩn từ Go 1.22 trước khi tìm đến một framework. Độ tin cậy: 0.85
- Bố trí dự án theo cấu trúc cmd/<service>/main.go cùng với các gói trong internal/, và bỏ qua thư mục pkg/. Độ tin cậy: 0.80
- Giữ cho tệp main.go thật gọn mỏng: đọc cấu hình, kết nối các thành phần phụ thuộc và khởi động máy chủ. Độ tin cậy: 0.90
- Truyền context.Context làm đối số đầu tiên cho mọi hàm thực hiện thao tác vào/ra (I/O). Độ tin cậy: 0.95
- Bọc lỗi bằng fmt.Errorf và %w, đồng thời kiểm tra lỗi bằng errors.Is và errors.As. Độ tin cậy: 0.95
- Ghi nhật ký bằng log/slog ở định dạng JSON và đính kèm mã định danh yêu cầu (request ID) trên mọi dòng. Độ tin cậy: 0.90
- Đọc cấu hình từ các biến môi trường vào một struct có kiểu dữ liệu rõ ràng ngay khi khởi động. Độ tin cậy: 0.85
- Dừng chương trình ngay lập tức khi khởi động (fail fast) nếu thiếu cấu hình bắt buộc. Độ tin cậy: 0.90
- Tự viết câu lệnh SQL và sinh mã có kiểu dữ liệu rõ ràng bằng sqlc thay vì sử dụng ORM. Độ tin cậy: 0.80
- Sử dụng pgx làm trình điều khiển (driver) Postgres kèm theo bộ quản lý kết nối (connection pool). Độ tin cậy: 0.85
- Định nghĩa các interface nhỏ ngay trong gói sử dụng chúng, chứ không phải trong gói triển khai chúng. Độ tin cậy: 0.85
- Sử dụng kiểm thử hướng bảng (table-driven tests) với các bài kiểm thử con t.Run. Độ tin cậy: 0.90
- Chạy go test với cờ -race trong CI trên mọi pull request. Độ tin cậy: 0.90
- Kiểm tra mã nguồn bằng golangci-lint với một danh sách ngắn các bộ kiểm tra (linters) đã được chọn lọc kỹ. Độ tin cậy: 0.80
- Tắt dịch vụ một cách êm đẹp (graceful shutdown) khi nhận tín hiệu SIGTERM bằng signal.NotifyContext và server.Shutdown. Độ tin cậy: 0.90
- Thiết lập thời gian chờ (timeout) đọc, ghi và nhàn rỗi rõ ràng trên mọi http.Server và http.Client. Độ tin cậy: 0.90
- Cung cấp điểm cuối /healthz để kiểm tra trạng thái hoạt động (liveness) và /readyz để kiểm tra trạng thái sẵn sàng (readiness). Độ tin cậy: 0.85
- Tránh trạng thái toàn cục (global state) và các hàm init cấp gói có tác dụng phụ (side effects). Độ tin cậy: 0.85
- Chỉ khởi chạy các goroutine khi có chủ sở hữu và điều kiện dừng rõ ràng, sử dụng errgroup cho các nhóm goroutine. Độ tin cậy: 0.85
- Biên dịch một tệp thực thi tĩnh (static binary) đưa vào ảnh container distroless bằng Dockerfile đa tầng (multi-stage). Độ tin cậy: 0.80
- Chỉ thêm generics khi chúng giúp loại bỏ sự trùng lặp mã nguồn thực sự. Độ tin cậy: 0.70
BackendArchitectureGo+1
F@f
0
Công cụ Kiểm tra JWT Token
Kỹ năng

Giải mã một chuỗi JWT mà không mặc định tin tưởng nó, đồng thời rà soát lỗi (lint) phần header và các thông tin xác nhận (claims): thuật toán alg none, khóa jku/x5u từ xa, thiếu hoặc hết hạn exp, lệch đồng hồ nbf/iat, dấu thời gian tính bằng mili-giây, thời hạn sống quá dài, sai lệch iss/aud, dữ liệu nhạy cảm nằm trong payload, và kiểm tra chữ ký HMAC bằng khóa bí mật lấy từ biến môi trường. Sử dụng khi thao tác đăng nhập hoặc lệnh gọi API thất bại với mã lỗi 401, hoặc khi rà soát cấu hình token. Bao gồm công cụ kiểm tra bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.

---
name: jwt-token-inspector
description: Giải mã một chuỗi JWT mà không mặc định tin tưởng nó, đồng thời rà soát lỗi (lint) phần header và các thông tin xác nhận (claims): thuật toán alg none, khóa jku/x5u từ xa, thiếu hoặc hết hạn exp, lệch đồng hồ nbf/iat, dấu thời gian tính bằng mili-giây, thời hạn sống quá dài, sai lệch iss/aud, dữ liệu nhạy cảm nằm trong payload, và kiểm tra chữ ký HMAC bằng khóa bí mật lấy từ biến môi trường. Sử dụng khi thao tác đăng nhập hoặc lệnh gọi API thất bại với mã lỗi 401, hoặc khi rà soát cấu hình token. Bao gồm công cụ kiểm tra bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.
---

# Công cụ Kiểm tra JWT Token

Bạn giúp các lập trình viên tìm hiểu và gỡ lỗi JSON Web Tokens (JWT) một cách an toàn. Bạn giải mã phần header và payload, chuyển đổi các dấu thời gian sang định dạng giờ UTC dễ đọc, giải thích ý nghĩa của từng claim, và gắn cờ cảnh báo những thiết lập khiến token trở nên mất an toàn, không thể sử dụng hoặc dễ gây lỗi trên môi trường thực tế (production). Bạn luôn coi mọi token là không đáng tin cậy: giải mã (decoding) không phải là xác minh chữ ký (verification), và bạn luôn nêu rõ điều đó.

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

- `scripts/jwt_inspect.py` - giải mã một JWS compact token, rà soát lỗi trong header và claims, tùy chọn xác minh chữ ký HS256/HS384/HS512 bằng khóa bí mật, xuất kết quả ra markdown hoặc JSON (chỉ sử dụng thư viện chuẩn Python 3)
- `references/claims-and-validation.md` - các claim đã đăng ký chuẩn, những gì bộ xác minh (verifier) bắt buộc phải kiểm tra và theo thứ tự nào
- `references/common-jwt-pitfalls.md` - những sai lầm đứng sau hầu hết các lỗi và sự cố bảo mật liên quan đến JWT, kèm theo cách khắc phục
- `templates/jwt-review.md` - mẫu báo cáo ngắn gọn cho phiếu báo lỗi (bug ticket), đánh giá PR hoặc rà soát bảo mật
- `examples/sample-tokens.md` - ba token mẫu minh họa (thời hạn quá dài, alg none, khóa jku từ xa kèm thời gian hết hạn exp tính bằng mili-giây) và các phát hiện kỳ vọng

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

### 1. Xử lý token an toàn
Yêu cầu người dùng cung cấp token từ môi trường thử nghiệm (test) hoặc tiền triển khai (staging). Nếu người dùng dán một token đang hoạt động trên môi trường thực tế (production), hãy cảnh báo họ rằng token đó vẫn cấp quyền truy cập cho đến khi hết hạn và đề nghị họ xoay vòng (rotate)/thu hồi nó sau phiên làm việc. Không bao giờ lặp lại toàn bộ chuỗi token hoặc phần chữ ký trong câu trả lời của bạn.

### 2. Chạy công cụ kiểm tra
```
python3 scripts/jwt_inspect.py "<token>"
python3 scripts/jwt_inspect.py "<token>" --now 2026-10-19T09:00:00Z --leeway 60
python3 scripts/jwt_inspect.py "<token>" --expect-iss https://auth.example.com --expect-aud api://orders
JWT_SECRET=... python3 scripts/jwt_inspect.py "<token>" --secret-env JWT_SECRET
python3 scripts/jwt_inspect.py --self-test
```
Tiền tố `Bearer ` sẽ tự động được lược bỏ. Sử dụng `-` để đọc token từ luồng đầu vào chuẩn (stdin). Mã thoát (exit code) bằng 1 có nghĩa là phát hiện ít nhất một vấn đề mức độ cao (`--strict`: bất kỳ phát hiện nào); mã thoát bằng 2 có nghĩa là không thể giải mã được token.

### 3. Giải thích kết quả
- Hiển thị phần header và các claim đã giải mã cùng thời gian exp/nbf/iat theo cả giờ UTC và múi giờ của người dùng.
- Nhóm các phát hiện theo mức độ nghiêm trọng: **cao (high)** (mất an toàn hoặc bị từ chối: alg none, chữ ký trống, đã hết hạn, chưa đến thời điểm có hiệu lực, sai lệch iss/aud, khóa nhúng trực tiếp hoặc tải từ xa, chữ ký sai, dấu thời gian tính bằng mili-giây), **trung bình (medium)** (không có exp, thời hạn vượt quá chính sách, chứa claim nhạy cảm, khóa bí mật HMAC yếu, iat nằm ở tương lai), **thấp (low)** (thiếu iss, aud, sub hoặc kid).
- Đối với lỗi 401, hãy đối chiếu phát hiện với triệu chứng thực tế bằng cách tham khảo `references/claims-and-validation.md`: lệch đồng hồ hệ thống, sai đối tượng thụ hưởng (audience), sai bên phát hành (issuer) hoặc sai khóa xác minh là những nguyên nhân phổ biến nhất.

### 4. Đề xuất biện pháp khắc phục
Sử dụng `references/common-jwt-pitfalls.md`: ghim cố định danh sách thuật toán cho phép trên bộ xác minh, giữ thời hạn của access token ngắn (5-15 phút) kết hợp với refresh token, chuyển dữ liệu cá nhân ra khỏi payload, cho phép khoảng dung sai thời gian (leeway) từ 30-60 giây, và chỉ tải khóa từ một URL JWKS đã được cấu hình cố định.

### 5. Báo cáo
Điền vào mẫu `templates/jwt-review.md` với dấu vân tay (fingerprint) của token (tuyệt đối không ghi nguyên chuỗi token), các phát hiện, nguyên nhân gốc rễ và biện pháp khắc phục đã thống nhất.

## Quy tắc

- Giải mã không phải là xác minh chữ ký. Không bao giờ khẳng định một token là hợp lệ trừ khi chữ ký của nó đã được kiểm tra bằng đúng khóa tương ứng.
- Không bao giờ in ra hoặc lưu trữ toàn bộ chuỗi token, chữ ký hoặc khóa bí mật. Hãy truyền khóa bí mật thông qua biến môi trường chứ không truyền trực tiếp trên dòng lệnh.
- Không hỗ trợ làm giả (forge), phát lại (replay) hoặc chỉnh sửa trái phép (tamper) các token cho những hệ thống mà người dùng không sở hữu.
- Các chữ ký RS/ES/PS/EdDSA cần có khóa công khai (public key) và thư viện JOSE chuyên dụng; kịch bản đi kèm chỉ xác minh các thuật toán họ HMAC.
FILE:references/claims-and-validation.md
# Các Claim và Thứ tự Xác thực

## Các claim chuẩn đã đăng ký (RFC 7519)
| Claim | Ý nghĩa | Kiểu dữ liệu | Ghi chú |
|---|---|---|---|
| iss | Bên phát hành (Issuer) | chuỗi/URL | Ghim cố định trên bộ xác minh; mỗi miền tin cậy một bên phát hành |
| sub | Chủ thể (ID người dùng hoặc dịch vụ) | chuỗi | ID ổn định, không dùng địa chỉ email vốn có thể thay đổi |
| aud | Đối tượng đích (Audience) | chuỗi hoặc mảng | Dịch vụ API phải tìm thấy ID của chính mình trong trường này |
| exp | Thời điểm hết hạn | NumericDate (giây) | Bắt buộc phải có đối với access token |
| nbf | Không có hiệu lực trước thời điểm | NumericDate | Token sẽ bị từ chối cho đến khi chạm mốc thời gian này |
| iat | Thời điểm phát hành | NumericDate | Dùng để kiểm tra vòng đời và độ mới của token |
| jti | Định danh token (Token ID) | chuỗi | Hỗ trợ danh sách thu hồi (revocation lists) và phát hiện tấn công phát lại (replay) |

NumericDate là số giây tính từ mốc 1970-01-01T00:00:00Z. Một giá trị gồm 13 chữ số là mili-giây và sẽ bị hiểu nhầm thành một ngày cách hiện tại hàng chục nghìn năm trong tương lai.

## Các tham số trong Header
- `alg` - thuật toán ký. Bộ xác minh mới là bên quyết định những giá trị nào được phép sử dụng; bản thân token không bao giờ được quyền quyết định.
- `kid` - định danh khóa (key ID), được dùng để chọn đúng khóa từ tập JWKS của bên phát hành trong quá trình xoay vòng khóa.
- `typ` - thường là `JWT` hoặc `at+jwt` (hồ sơ access token theo chuẩn RFC 9068).
- `jku`, `x5u`, `jwk` - trỏ tới hoặc nhúng trực tiếp các khóa. Hãy bỏ qua chúng trừ khi chúng khớp hoàn toàn với một nguồn đáng tin cậy đã được ghim cố định.

## Những gì bộ xác minh bắt buộc phải kiểm tra, theo đúng thứ tự
1. Token có đúng ba phân đoạn và giải mã ra các đối tượng JSON hợp lệ.
2. `alg` nằm trong danh sách cho phép (allow-list) của bộ xác minh (ví dụ: chỉ chấp nhận RS256). Từ chối thẳng `none`.
3. Chữ ký được xác minh thành công bằng khóa được chọn qua `kid` từ một nguồn JWKS đáng tin cậy, tuyệt đối không lấy khóa trực tiếp từ bên trong token.
4. `exp` nằm ở tương lai và `nbf` nằm ở quá khứ, cả hai đều được áp dụng một khoảng dung sai nhỏ (30-60 giây).
5. `iss` bằng đúng định danh của bên phát hành kỳ vọng.
6. `aud` có chứa ID của dịch vụ hiện tại.
7. Các quy tắc nghiệp vụ của ứng dụng: phạm vi quyền (scopes), vai trò (roles), định danh thuê bao (tenant), và `jti` chưa bị thu hồi.

## Đối chiếu triệu chứng lỗi 401 với nguyên nhân
| Triệu chứng | Nguyên nhân khả dĩ | Điểm cần kiểm tra |
|---|---|---|
| Chạy được ở máy cục bộ (local), lỗi trên prod | Lệch đồng hồ hệ thống hoặc URL của bên phát hành khác nhau | So sánh iat/nbf với giờ máy chủ, kiểm tra trường iss |
| Lỗi ngay lập tức sau khi vừa đăng nhập | nbf hoặc iat nằm ở tương lai | Đồng bộ thời gian trên các máy chủ (NTP) |
| Chỉ bị lỗi ở một API duy nhất | Sai đối tượng đích (audience) | Danh sách aud |
| Bị lỗi sau khi xoay vòng khóa (key rotation) | Không nhận diện được kid mới hoặc JWKS bị lưu đệm (cache) quá lâu | Đối chiếu kid với JWKS, kiểm tra thời gian sống (TTL) của bộ nhớ đệm |
| Không bao giờ hết hạn | Thiếu trường exp hoặc exp đang tính bằng mili-giây | Giá trị của trường exp |
FILE:references/common-jwt-pitfalls.md
# Những Sai lầm Phổ biến khi Sử dụng JWT

1. **Chấp nhận `alg: none`.** Các token không có chữ ký có thể bị làm giả dễ dàng. Cách khắc phục: truyền một danh sách thuật toán cho phép (allow-list) tường minh vào hàm xác minh.
2. **Nhầm lẫn thuật toán (RS256 so với HS256).** Nếu bộ xác minh chấp nhận cả hai, kẻ tấn công có thể ký bằng thuật toán HMAC với việc dùng chính khóa công khai (public key) làm khóa bí mật (secret). Cách khắc phục: mỗi khóa chỉ gắn với một họ thuật toán duy nhất.
3. **Tin tưởng khóa lấy từ bên trong token.** Các trường `jku`, `x5u` hoặc `jwk` nhúng sẵn cho phép người gửi tự ý chọn khóa xác minh. Cách khắc phục: chỉ tải khóa từ một URL JWKS đã được cấu hình cố định từ trước.
4. **Khóa bí mật HMAC yếu.** Các khóa bí mật ngắn hoặc nằm trong từ điển có thể bị dò quét vét cạn (brute-force) ngoại tuyến chỉ từ một token thu được. Cách khắc phục: dùng ít nhất 32 byte ngẫu nhiên cho HS256 (48 byte cho HS384, 64 byte cho HS512) và lưu trữ trong trình quản lý bí mật (secret manager).
5. **Access token có thời hạn quá dài.** Một token bị lộ sẽ tiếp tục có hiệu lực cho đến tận thời điểm exp. Cách khắc phục: đặt thời hạn access token từ 5-15 phút kết hợp với refresh token có xoay vòng; thu hồi theo `jti` nếu cần.
6. **Chứa dữ liệu cá nhân trong payload.** Base64url chỉ là mã hóa định dạng (encoding) chứ không phải mật mã hóa (encryption); bất kỳ ai cầm token đều đọc được nội dung. Cách khắc phục: chỉ lưu các ID và phạm vi quyền (scopes), hoặc sử dụng JWE.
7. **Dùng mili-giây thay vì giây.** Rất hay gặp khi dùng hàm `Date.now()` trong JavaScript. Cách khắc phục: sử dụng `Math.floor(Date.now() / 1000)`.
8. **Không cấu hình khoảng dung sai thời gian (leeway).** Sự chênh lệch nhỏ giữa đồng hồ các máy chủ sẽ gây ra lỗi 401 ngẫu nhiên. Cách khắc phục: cấu hình dung sai 30-60 giây và bật đồng bộ NTP trên tất cả các máy chủ.
9. **Bỏ qua bước kiểm tra aud/iss.** Token cấp cho dịch vụ này có thể bị đem phát lại (replay) để truy cập trái phép vào dịch vụ khác. Cách khắc phục: luôn xác thực cả hai trường này.
10. **Đặt token trên URL hoặc trong nhật ký (logs).** Chúng sẽ bị rò rỉ qua trường Referer, máy chủ proxy và hệ thống tìm kiếm nhật ký. Cách khắc phục: gửi token trong trường header Authorization và che (redact) token khi ghi nhật ký.
FILE:templates/jwt-review.md
# Báo cáo Rà soát JWT - <tên dịch vụ hoặc mã phiếu>

**Dấu vân tay token:** sha256 <12 ký tự hex đầu tiên>  **Môi trường:** <dev / staging / prod>  **Thời điểm kiểm tra:** <giờ UTC>
**Bên phát hành (Issuer):** <iss>  **Đối tượng đích (Audience):** <aud>  **Thuật toán:** <alg>  **Thời hạn sống:** <exp - iat>

## Các phát hiện
| Mức độ nghiêm trọng | Mã phát hiện | Chi tiết | Cách khắc phục |
|---|---|---|---|
| | | | |

## Nguyên nhân gốc rễ
<một hoặc hai câu liên kết phát hiện trên với triệu chứng lỗi được báo cáo>

## Các thay đổi đã thống nhất
- [ ] Bộ xác minh (Verifier): <danh sách cho phép allow-list, dung sai thời gian leeway, kiểm tra iss/aud>
- [ ] Bên phát hành (Issuer): <thời hạn sống, các claim, kích thước khóa, kid>
- [ ] Xoay vòng token hoặc khóa bí mật bị lộ: <có/không, người phụ trách>

## Ghi chú
Được giải mã bằng scripts/jwt_inspect.py. Bản thân chuỗi token không được lưu lại trong báo cáo này.
FILE:examples/sample-tokens.md
# Các Token Mẫu (chỉ dùng để minh họa, không thuộc hệ thống thực tế)

Chạy từng mẫu bằng lệnh:
```
python3 scripts/jwt_inspect.py "<token>" --now 2026-10-19T09:00:00Z --expect-aud api://orders
```

## 1. Token khách hàng có thời hạn sống dài (HS256, khóa bí mật minh họa "dev-secret")
```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguc2hvcGZyb250LmV4YW1wbGUiLCJzdWIiOiJjdXN0Xzg4NDEiLCJhdWQiOlsiYXBpOi8vb3JkZXJzIiwiYXBpOi8vcHJvZmlsZSJdLCJpYXQiOjE3OTIzOTUwMDAsImV4cCI6MTc5Mjk5OTgwMCwiZW1haWwiOiJtaW5hQGV4YW1wbGUuY29tIiwicm9sZXMiOlsiY3VzdG9tZXIiXSwiY2FyZF9sYXN0NCI6IjQyNDIifQ.xupeoN2LYDYe-FKD_4qOh7KCw6G68Zi1pCms7dtUGh0
```
Kỳ vọng: mã thoát 0; mức trung bình `lifetime_long` (7.0 ngày vượt quá chính sách 1.0 ngày), mức trung bình `sensitive_claim` (card_last4). Khi đặt biến môi trường cho `--secret-env` là "dev-secret": xác minh chữ ký thành công, kèm theo cảnh báo mức trung bình `weak_secret` (10 byte).

## 2. Token quản trị viên không có chữ ký (alg none)
```
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2F1dGguc2hvcGZyb250LmV4YW1wbGUiLCJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc5MjM5NTAwMH0.
```
Kỳ vọng: mã thoát 1; mức cao `alg_none`, mức cao `aud_mismatch` (không có aud trong khi yêu cầu `api://orders`), mức trung bình `exp_missing`.

## 3. URL khóa từ xa và thời gian hết hạn tính bằng mili-giây (RS256)
```
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImprdSI6Imh0dHBzOi8va2V5cy5hdHRhY2tlci5leGFtcGxlL2p3a3MuanNvbiJ9.eyJpc3MiOiJodHRwczovL2F1dGguc2hvcGZyb250LmV4YW1wbGUiLCJzdWIiOiJzdmMtcmVwb3J0cyIsImF1ZCI6ImFwaTovL29yZGVycyIsImlhdCI6MTc5MjM5NTAwMCwiZXhwIjoxNzkyMzk4NjAwMDAwfQ.bm90LWEtcmVhbC1zaWduYXR1cmUtanVzdC1hLWRlbW8
```
Kỳ vọng: mã thoát 1; mức cao `exp_milliseconds`, mức cao `header_jku`, mức thấp `kid_missing`.
FILE:scripts/jwt_inspect.py
#!/usr/bin/env python3
"""JWT token inspector and claim linter (Python 3 standard library only).

Decodes a JSON Web Token (JWS compact form: header.payload.signature) WITHOUT trusting it,
prints the header and claims with human-readable times, and flags risky or broken settings.
Optionally verifies HS256/HS384/HS512 signatures when you pass the shared secret.

Usage:
  python3 jwt_inspect.py <token>                     # or "-" to read the token from stdin
  python3 jwt_inspect.py <token> --now 2026-10-19T09:00:00Z --leeway 60
  python3 jwt_inspect.py <token> --expect-iss https://auth.example.com --expect-aud api://orders
  python3 jwt_inspect.py <token> --secret-env JWT_SECRET   # verify HS* with a secret from an env var
  python3 jwt_inspect.py <token> --max-lifetime 3600 --json
  python3 jwt_inspect.py --self-test

Severity: high (token unsafe or unusable), medium (likely bug or weak policy), low (hygiene).
Exit code: 0 no high findings, 1 at least one high finding (or --strict and any finding), 2 bad input.
The signature itself is never printed; only its length and a short SHA-256 fingerprint of the token.
"""
import argparse
import base64
import datetime as dt
import hashlib
import hmac
import json
import os
import re
import sys

HS_ALGS = {"HS256": hashlib.sha256, "HS384": hashlib.sha384, "HS512": hashlib.sha512}
MIN_HS_SECRET = {"HS256": 32, "HS384": 48, "HS512": 64}
KNOWN_ALGS = set(HS_ALGS) | {"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
                             "ES256", "ES384", "ES512", "EdDSA"}
SENSITIVE = re.compile(r"(pass(word)?|pwd|secret|ssn|social|credit|card|cvv|iban|private|api_?key|refresh_?token)", re.I)
TIME_CLAIMS = ("exp", "nbf", "iat")


class TokenError(ValueError):
    pass


def b64url_decode(part, what):
    if not re.fullmatch(r"[A-Za-z0-9_-]*", part):
        raise TokenError(f"{what} is not base64url (found characters outside A-Z a-z 0-9 - _ or '=' padding)")
    try:
        return base64.urlsafe_b64decode(part + "=" * (-len(part) % 4))
    except (ValueError, TypeError) as e:
        raise TokenError(f"{what} could not be base64url-decoded: {e}")


def b64url_encode(raw):
    return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")


def decode(token):
    token = token.strip()
    if token.lower().startswith("bearer "):
        token = token[7:].strip()
    parts = token.split(".")
    if len(parts) == 5:
        raise TokenError("5 segments: this is an encrypted JWE, not a signed JWS; it cannot be inspected without the key")
    if len(parts) != 3:
        raise TokenError(f"expected 3 dot-separated segments, found {len(parts)}")
    out = {}
    for name, part in (("header", parts[0]), ("payload", parts[1])):
        raw = b64url_decode(part, name)
        try:
            obj = json.loads(raw.decode("utf-8"))
        except (UnicodeDecodeError, ValueError):
            raise TokenError(f"{name} is not valid UTF-8 JSON")
        if not isinstance(obj, dict):
            raise TokenError(f"{name} must be a JSON object")
        out[name] = obj
    out["signature"] = b64url_decode(parts[2], "signature")
    out["signing_input"] = (parts[0] + "." + parts[1]).encode("ascii")
    out["fingerprint"] = hashlib.sha256(token.encode("utf-8")).hexdigest()[:12]
    return out


def parse_now(value):
    if value is None:
        return int(dt.datetime.now(dt.timezone.utc).timestamp())
    if re.fullmatch(r"\d+", value):
        return int(value)
    t = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
    if t.tzinfo is None:
        t = t.replace(tzinfo=dt.timezone.utc)
    return int(t.timestamp())


def iso(ts):
    try:
        return dt.datetime.fromtimestamp(ts, dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    except (OverflowError, OSError, ValueError, TypeError):
        return "invalid"


def human_delta(seconds):
    s = abs(int(seconds))
    for unit, size in (("d", 86400), ("h", 3600), ("m", 60)):
        if s >= size:
            return f"{s / size:.1f}{unit}"
    return f"{s}s"


def verify_hs(tok, secret):
    alg = tok["header"].get("alg")
    fn = HS_ALGS.get(alg)
    if fn is None:
        return None
    mac = hmac.new(secret.encode("utf-8"), tok["signing_input"], fn).digest()
    return hmac.compare_digest(mac, tok["signature"])


def inspect(token, now=None, leeway=0, expect_iss=None, expect_aud=None, max_lifetime=86400, secret=None):
    tok = decode(token)
    h, p = tok["header"], tok["payload"]
    findings = []

    def add(sev, code, msg):
        findings.append({"severity": sev, "code": code, "message": msg})

    alg = h.get("alg")
    if alg is None:
        add("high", "alg_missing", "header has no 'alg'; libraries may reject it or guess")
    elif str(alg).lower() == "none":
        add("high", "alg_none", "alg is 'none': the token is unsigned and anyone can forge it")
        if tok["signature"]:
            add("medium", "none_with_signature", "alg 'none' but a signature segment is present")
    elif alg not in KNOWN_ALGS:
        add("medium", "alg_unknown", f"unusual alg '{alg}'")
    if alg and str(alg).lower() != "none" and not tok["signature"]:
        add("high", "signature_empty", "signature segment is empty although alg requires one")
    for key in ("jku", "x5u"):
        if key in h:
            add("high", f"header_{key}", f"header '{key}' points to a remote key URL ({h[key]}); never fetch keys from a URL inside the token")
    if "jwk" in h:
        add("high", "header_jwk", "header embeds its own 'jwk'; a verifier that trusts it accepts attacker keys")
    if "crit" in h:
        add("medium", "header_crit", f"'crit' lists extensions {h['crit']}; verifiers must understand them or reject the token")
    if alg in KNOWN_ALGS and alg not in HS_ALGS and "kid" not in h:
        add("low", "kid_missing", "asymmetric alg without 'kid'; key rotation will be painful")

    now = parse_now(now) if not isinstance(now, int) else now
    times = {}
    for c in TIME_CLAIMS:
        if c in p:
            v = p[c]
            if isinstance(v, bool) or not isinstance(v, (int, float)):
                add("high", f"{c}_not_numeric", f"'{c}' must be NumericDate seconds, got {v!r}")
            elif v > 10**11:
                add("high", f"{c}_milliseconds", f"'{c}'={v} looks like milliseconds; JWT uses seconds")
            else:
                times[c] = int(v)
    if "exp" not in p:
        add("medium", "exp_missing", "no 'exp': the token never expires")
    elif "exp" in times and times["exp"] + leeway < now:
        add("high", "expired", f"expired at {iso(times['exp'])} ({human_delta(now - times['exp'])} ago)")
    if "nbf" in times and times["nbf"] - leeway > now:
        add("high", "not_yet_valid", f"not valid before {iso(times['nbf'])} ({human_delta(times['nbf'] - now)} from now)")
    if "iat" in times and times["iat"] - leeway > now:
        add("medium", "iat_future", f"issued in the future ({iso(times['iat'])}); check clock skew")
    if "exp" in times and "iat" in times:
        life = times["exp"] - times["iat"]
        if life <= 0:
            add("high", "exp_before_iat", "exp is not after iat")
        elif max_lifetime and life > max_lifetime:
            add("medium", "lifetime_long", f"lifetime {human_delta(life)} exceeds policy {human_delta(max_lifetime)}")
    if "iss" not in p:
        add("low", "iss_missing", "no 'iss'; verifiers cannot pin the issuer")
    elif expect_iss and p["iss"] != expect_iss:
        add("high", "iss_mismatch", f"iss '{p['iss']}' != expected '{expect_iss}'")
    aud = p.get("aud")
    auds = aud if isinstance(aud, list) else ([aud] if aud is not None else [])
    if aud is None and expect_aud:
        add("high", "aud_mismatch", f"no 'aud' claim but '{expect_aud}' is required")
    elif aud is None:
        add("low", "aud_missing", "no 'aud'; the token could be replayed against another service")
    elif expect_aud and expect_aud not in auds:
        add("high", "aud_mismatch", f"aud {auds} does not contain expected '{expect_aud}'")
    if "sub" not in p:
        add("low", "sub_missing", "no 'sub'; who is this token about?")
    for k in p:
        if SENSITIVE.search(k):
            add("medium", "sensitive_claim", f"claim '{k}' looks sensitive; JWT payloads are only encoded, not encrypted")

    verified = None
    if secret is not None:
        if alg not in HS_ALGS:
            add("low", "verify_skipped", f"--secret only verifies HS256/384/512; alg is {alg}")
        else:
            verified = verify_hs(tok, secret)
            if not verified:
                add("high", "bad_signature", "HMAC signature does NOT match the given secret")
            if len(secret.encode("utf-8")) < MIN_HS_SECRET[alg]:
                add("medium", "weak_secret", f"{alg} secret is {len(secret.encode('utf-8'))} bytes; use at least {MIN_HS_SECRET[alg]} random bytes")

    order = {"high": 0, "medium": 1, "low": 2}
    findings.sort(key=lambda f: (order[f["severity"]], f["code"]))
    return {
        "fingerprint": tok["fingerprint"],
        "header": h,
        "payload": p,
        "times": {c: iso(v) for c, v in times.items()},
        "now": iso(now),
        "signature_bytes": len(tok["signature"]),
        "signature_verified": verified,
        "findings": findings,
    }


def to_markdown(r):
    lines = [f"# JWT inspection (sha256 {r['fingerprint']})", "",
             f"Checked at {r['now']}. Signature: {r['signature_bytes']} bytes"
             + ("" if r["signature_verified"] is None else (", verified OK" if r["signature_verified"] else ", NOT verified")), "",
             "## Header", "```json", json.dumps(r["header"], indent=2, ensure_ascii=False), "```", "",
             "## Claims", "```json", json.dumps(r["payload"], indent=2, ensure_ascii=False), "```", ""]
    if r["times"]:
        lines += ["| Claim | UTC |", "|---|---|"] + [f"| {k} | {v} |" for k, v in r["times"].items()] + [""]
    lines += ["## Findings"]
    if not r["findings"]:
        lines.append("No findings.")
    else:
        lines += ["| Severity | Code | Detail |", "|---|---|---|"]
        lines += [f"| {f['severity']} | {f['code']} | {f['message']} |" for f in r["findings"]]
    return "\n".join(lines)


def make_token(header, payload, secret=None, signature=b""):
    si = b64url_encode(json.dumps(header, separators=(",", ":")).encode()) + "." + \
         b64url_encode(json.dumps(payload, separators=(",", ":")).encode())
    if secret is not None:
        signature = hmac.new(secret.encode(), si.encode(), HS_ALGS[header["alg"]]).digest()
    return si + "." + b64url_encode(signature)


def self_test():
    now = 1760860800  # 2025-10-19T08:00:00Z
    strong = "k" * 40
    good_claims = {"iss": "https://auth.example.com", "sub": "user-42", "aud": "api://orders",
                   "iat": now - 60, "exp": now + 840}
    good = make_token({"alg": "HS256", "typ": "JWT"}, good_claims, strong)
    codes = lambda r: {f["code"] for f in r["findings"]}
    cases = []

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

    r = inspect(good, now=now, secret=strong, expect_iss="https://auth.example.com", expect_aud="api://orders")
    check("clean token has no findings", r["findings"] == [])
    check("clean token signature verified", r["signature_verified"] is True)
    check("bearer prefix accepted", inspect("Bearer " + good, now=now)["payload"]["sub"] == "user-42")
    check("wrong secret flagged", "bad_signature" in codes(inspect(good, now=now, secret="x" * 40)))
    check("weak secret flagged", "weak_secret" in codes(inspect(make_token({"alg": "HS256"}, good_claims, "short"), now=now, secret="short")))
    check("alg none flagged high", "alg_none" in codes(inspect(make_token({"alg": "none"}, good_claims), now=now)))
    check("expired flagged", "expired" in codes(inspect(good, now=now + 3600)))
    check("leeway tolerates small skew", "expired" not in codes(inspect(good, now=now + 850, leeway=30)))
    check("nbf in future flagged", "not_yet_valid" in codes(inspect(make_token({"alg": "HS256"}, dict(good_claims, nbf=now + 600), strong), now=now)))
    check("exp missing flagged", "exp_missing" in codes(inspect(make_token({"alg": "HS256"}, {"sub": "a", "iat": now}, strong), now=now)))
    check("milliseconds exp flagged", "exp_milliseconds" in codes(inspect(make_token({"alg": "HS256"}, dict(good_claims, exp=(now + 600) * 1000), strong), now=now)))
    check("long lifetime flagged", "lifetime_long" in codes(inspect(make_token({"alg": "HS256"}, dict(good_claims, exp=now + 30 * 86400), strong), now=now)))
    check("iss mismatch flagged", "iss_mismatch" in codes(inspect(good, now=now, expect_iss="https://evil.example")))
    check("aud list accepted", "aud_mismatch" not in codes(inspect(make_token({"alg": "HS256"}, dict(good_claims, aud=["a", "api://orders"]), strong), now=now, expect_aud="api://orders")))
    check("aud mismatch flagged", "aud_mismatch" in codes(inspect(good, now=now, expect_aud="api://billing")))
    check("missing aud with expected aud is high", "aud_mismatch" in codes(inspect(make_token({"alg": "HS256"}, {k: v for k, v in good_claims.items() if k != "aud"}, strong), now=now, expect_aud="api://orders")))
    check("jku header flagged", "header_jku" in codes(inspect(make_token({"alg": "RS256", "kid": "1", "jku": "https://x/keys"}, good_claims, signature=b"sig"), now=now)))
    check("rs256 without kid is low", "kid_missing" in codes(inspect(make_token({"alg": "RS256"}, good_claims, signature=b"sig"), now=now)))
    check("sensitive claim flagged", "sensitive_claim" in codes(inspect(make_token({"alg": "HS256"}, dict(good_claims, password="hunter2"), strong), now=now)))
    check("empty signature flagged", "signature_empty" in codes(inspect(make_token({"alg": "HS256"}, good_claims), now=now)))
    for bad, label in (("abc.def", "two segments rejected"), ("a.b.c.d.e", "JWE rejected"), ("!!.e30.", "bad base64 rejected")):
        try:
            inspect(bad, now=now)
            check(label, False)
        except TokenError:
            check(label, True)
    check("markdown never prints signature", make_token({"alg": "HS256"}, good_claims, strong).split(".")[2] not in to_markdown(r))
    passed = sum(ok for _, ok in cases)
    for name, ok in cases:
        print(("PASS " if ok else "FAIL ") + name)
    print(f"{passed}/{len(cases)} passed")
    return 0 if passed == len(cases) else 1


def main(argv=None):
    ap = argparse.ArgumentParser(description="Decode and lint a JWT without trusting it.")
    ap.add_argument("token", nargs="?", help="JWT string, or '-' to read from stdin")
    ap.add_argument("--now", help="reference time: ISO 8601 (Z or offset) or epoch seconds; default: current UTC time")
    ap.add_argument("--leeway", type=int, default=0, help="allowed clock skew in seconds (default 0)")
    ap.add_argument("--expect-iss")
    ap.add_argument("--expect-aud")
    ap.add_argument("--max-lifetime", type=int, default=86400, help="max exp-iat seconds before warning (0 = off)")
    ap.add_argument("--secret-env", help="name of an environment variable holding the HS* secret (avoids shell history)")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--strict", action="store_true", help="exit 1 on any finding")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args(argv)
    if a.self_test:
        return self_test()
    if not a.token:
        ap.error("token required (or --self-test)")
    token = sys.stdin.read() if a.token == "-" else a.token
    secret = None
    if a.secret_env:
        secret = os.environ.get(a.secret_env)
        if secret is None:
            print(f"error: environment variable {a.secret_env} is not set", file=sys.stderr)
            return 2
    try:
        r = inspect(token, now=a.now, leeway=a.leeway, expect_iss=a.expect_iss, expect_aud=a.expect_aud,
                    max_lifetime=a.max_lifetime, secret=secret)
    except (TokenError, ValueError) as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    print(json.dumps(r, indent=2, ensure_ascii=False) if a.json else to_markdown(r))
    if any(f["severity"] == "high" for f in r["findings"]) or (a.strict and r["findings"]):
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
BackendAPISecurity+3
F@f
0
Xử lý Lỗi taste
Văn bản

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

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

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

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

Kiểm tra các thiết lập retry, backoff và timeout trên toàn bộ chuỗi lệnh gọi để ngăn chặn bão retry (retry storm). Tính toán lịch trình độ trễ hàm mũ với jitter dạng full, equal hoặc decorrelated, độ trễ trường hợp xấu nhất cho mỗi tầng, số lần thử kỳ vọng và mức độ khuếch đại yêu cầu. Cảnh báo các timeout bên trong kéo dài vượt quá thời gian của bên gọi, việc retry các lệnh gọi không bất biến (non-idempotent), retry trên mã lỗi 4xx, bỏ qua Retry-After cũng như thiếu giới hạn trần (cap) hoặc hạn chót (deadline). Bao gồm một công cụ tính toán dùng thư viện chuẩn Python có hỗ trợ --self-test.

---
name: retry-backoff-policy-calculator
description: Kiểm tra các thiết lập retry, backoff và timeout trên toàn bộ chuỗi lệnh gọi để ngăn chặn bão retry (retry storm). Tính toán lịch trình độ trễ hàm mũ với jitter dạng full, equal hoặc decorrelated, độ trễ trường hợp xấu nhất cho mỗi tầng, số lần thử kỳ vọng và mức độ khuếch đại yêu cầu. Cảnh báo các timeout bên trong kéo dài vượt quá thời gian của bên gọi, việc retry các lệnh gọi không bất biến (non-idempotent), retry trên mã lỗi 4xx, bỏ qua Retry-After cũng như thiếu giới hạn trần (cap) hoặc hạn chót (deadline). Bao gồm một công cụ tính toán dùng thư viện chuẩn Python có hỗ trợ --self-test.
---

# Công cụ tính toán chính sách Retry và Backoff

Bạn giúp các kỹ sư thiết lập chính sách retry, backoff và timeout giúp các dịch vụ hoạt động đáng tin cậy hơn thay vì biến một sự cố nhỏ thành một sự cố sập hệ thống diện rộng. Bạn thu thập các thiết lập của từng chặng (hop) trong chuỗi lệnh gọi (trình duyệt, gateway, các dịch vụ, cơ sở dữ liệu, API bên thứ ba), tính toán những gì thực sự diễn ra khi gặp lỗi, và đề xuất một chính sách an toàn hơn kèm theo các con số cụ thể.

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

- `scripts/retry_policy.py` - đọc JSON chuỗi lệnh gọi và in ra lịch trình độ trễ, độ trễ trường hợp xấu nhất, số lần thử kỳ vọng, mức độ khuếch đại và các cờ cảnh báo (chỉ sử dụng thư viện chuẩn Python 3)
- `references/backoff-math.md` - các công thức tính backoff, jitter, thời gian trường hợp xấu nhất, số lần thử kỳ vọng, mức độ khuếch đại và việc lồng ghép timeout
- `references/retry-safety-guide.md` - những gì an toàn để retry, khóa bất biến (idempotency key), ngắt mạch (circuit breaker), ngân sách retry (retry budget), các thiết lập thư viện phổ biến
- `templates/retry-policy-review.md` - báo cáo đánh giá gồm chuỗi hiện tại, các phát hiện và chính sách đề xuất
- `examples/checkout-chain.json` - một chuỗi thanh toán ba tầng với những sai lầm điển hình

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

### 1. Lập bản đồ chuỗi lệnh gọi
Hỏi thông tin cho từng chặng, bắt đầu từ tầng ngoài cùng trước: số lần thử (attempts - tổng số lần thực hiện), độ trễ cơ sở (base delay), hệ số nhân (multiplier), độ trễ tối đa (max delay), loại jitter, timeout cho mỗi lần thử (per-try timeout), hạn chót tổng thể (overall deadline), điều kiện kích hoạt retry (mã trạng thái, "timeout", "network"), liệu lệnh gọi có tính bất biến (idempotent) hay không, có gửi kèm khóa bất biến (idempotency key) hay không, và có tuân thủ Retry-After hay không. Đọc các thông số này từ mã nguồn hoặc tệp cấu hình (SDK client, bộ chuyển đổi retry của HTTP client, các route Envoy/Istio, cấu hình dịch vụ gRPC, hàng đợi công việc). Với các giá trị chưa biết: sử dụng giá trị mặc định của thư viện và đánh dấu chúng là giả định.

### 2. Chạy công cụ tính toán
```
python3 scripts/retry_policy.py examples/checkout-chain.json
python3 scripts/retry_policy.py chain.json --failure-rate 0.5 --max-amplification 8
python3 scripts/retry_policy.py chain.json --json > retry-report.json
python3 scripts/retry_policy.py --self-test
```
Mã thoát (exit code) 1 có nghĩa là có các phát hiện mức độ nghiêm trọng cao, 2 có nghĩa là dữ liệu đầu vào không hợp lệ.

### 3. Giải thích các phát hiện
Thứ tự ưu tiên: RETRY_UNSAFE và AMPLIFICATION trước tiên (chúng gây ra các tác dụng phụ trùng lặp và làm sập hệ thống), tiếp theo là INNER_OUTLIVES_CALLER, HOT_LOOP, NO_PER_TRY_TIMEOUT, sau đó đến mức trung bình (medium) và thấp (low). Giải thích từng vấn đề bằng các con số trích từ báo cáo, ví dụ: "một lần thanh toán có thể biến thành 36 lệnh gọi tới nhà cung cấp dịch vụ thanh toán".

### 4. Đề xuất chính sách đã khắc phục
Sử dụng `references/backoff-math.md` và `references/retry-safety-guide.md`: chỉ retry tại một tầng duy nhất, dùng full jitter có giới hạn trần (cap), thời gian trường hợp xấu nhất của tầng bên trong phải nhỏ hơn per-try timeout của bên gọi, truyền hạn chót (deadline) xuống các tầng dưới, dùng khóa bất biến cho các thao tác ghi, không retry trên các lỗi 4xx ngoại trừ 408/429, tuân thủ Retry-After, áp dụng ngân sách retry hoặc bộ ngắt mạch. Chạy lại kịch bản trên tệp JSON đề xuất và hiển thị kết quả so sánh trước và sau khi sửa.

### 5. Viết báo cáo tổng hợp
Điền vào biểu mẫu `templates/retry-policy-review.md` và cung cấp các đoạn mã cấu hình (config snippets) cho thư viện mà người dùng đang sử dụng.

## Ví dụ kết quả
Đối với `examples/checkout-chain.json` ở tỷ lệ lỗi 30%: mức độ khuếch đại trường hợp xấu nhất là 36 lệnh gọi cho mỗi lần thanh toán (kỳ vọng là 2.74); thời gian trường hợp xấu nhất 9.3 giây của tầng gateway kéo dài vượt quá per-try timeout 8 giây của ứng dụng web; tầng thanh toán retry một lệnh trừ tiền không bất biến 4 lần mà không có khóa bất biến và retry cả lỗi 409; không có jitter tại tầng gateway; bỏ qua Retry-After trên lỗi 429/503.

## Quy tắc

- Không bao giờ khuyến nghị retry một lệnh gọi không bất biến mà không có khóa bất biến (idempotency key).
- Giữ tính trung thực của các con số: gắn nhãn rõ ràng các giá trị mặc định của thư viện và các phỏng đoán là giả định.
- Ưu tiên số lần retry ít hơn kèm theo thiết lập timeout hợp lý hơn là retry quá nhiều lần.
- Khuyến nghị kiểm thử tải (load test) hoặc tiêm lỗi (fault injection) để xác nhận chính sách trong thực tế, chứ không chỉ dựa vào tính toán lý thuyết.
FILE:references/backoff-math.md
# Toán học về Backoff và Timeout

## Thuật ngữ
- **attempts**: tổng số lần thử bao gồm cả lần đầu tiên. attempts 3 = 1 lần thử ban đầu + 2 lần retry.
- **base**: độ trễ của lần retry đầu tiên. **multiplier**: hệ số tăng trưởng (thường là 2). **cap**: độ trễ tối đa cho một lần chờ.
- **per-try timeout**: khoảng thời gian tối đa mà một lần thử được phép diễn ra trước khi bị hủy bỏ.
- **deadline**: tổng ngân sách thời gian cho toàn bộ lệnh gọi bao gồm tất cả các lần thử và thời gian chờ.

## Độ trễ trước lần retry thứ k (k = 1 .. attempts-1)
d(k) = min(cap, base x multiplier^(k-1))

| Jitter | Độ trễ | Phạm vi | Giá trị trung bình |
|---|---|---|---|
| none | d | d | d |
| full | random(0, d) | 0 .. d | d/2 |
| equal | d/2 + random(0, d/2) | d/2 .. d | 0.75 d |
| decorrelated | min(cap, random(base, 3 x previous)) | base .. min(cap, base x 3^k) | mô phỏng |

Full jitter phân tán tải tốt nhất và là khuyến nghị mặc định. Equal jitter giữ lại một khoảng thời gian chờ tối thiểu. Decorrelated jitter thích ứng với độ trễ liền trước đó và hoạt động tốt cho các client chạy dài hạn.

## Thời lượng trường hợp xấu nhất của một lệnh gọi
worst = attempts x per_try_timeout + tổng các độ trễ tối đa, sau đó cắt tại ngưỡng deadline nếu có thiết lập deadline.
Ví dụ: 3 lần thử x 1 s + 100 ms + 200 ms = 3.3 s.

## Số lần thử kỳ vọng
Với xác suất lỗi độc lập p cho mỗi lần thử và tổng số n lần thử (attempts):
E[tries] = 1 + p + p^2 + ... + p^(n-1)
p = 0.3, n = 3 -> 1.39. Trong trường hợp sập toàn bộ hệ thống, p = 1 và E = n.

## Sự khuếch đại qua các tầng
Nếu tầng A retry 3 lần và gọi B, B lại retry 3 lần và gọi C, C lại retry 4 lần, thì một yêu cầu từ người dùng có thể biến thành 3 x 3 x 4 = 36 lệnh gọi tới C khi xảy ra sự cố. Đây chính là bão retry (retry storm): các lệnh retry giữ cho dịch vụ đang gặp sự cố luôn trong tình trạng quá tải, khiến nó không thể phục hồi.

Quy tắc kinh nghiệm:
- Chỉ retry tại một tầng duy nhất, thường là tầng gần người dùng nhất hoặc tầng sở hữu khóa bất biến (idempotency key).
- Các tầng bên trong: đặt attempts là 1 hoặc 2 với hạn chót (deadline) ngắn.
- Giữ mức độ khuếch đại trong trường hợp xấu nhất ở mức bằng hoặc dưới khoảng 10.
- Thêm **ngân sách retry (retry budget)**: số lượng retry chỉ được chiếm tối đa 10-20% tổng số yêu cầu trong một cửa sổ trượt (sliding window); vượt quá ngưỡng đó, hãy báo lỗi ngay lập tức (fail fast).

## Lồng ghép Timeout (Timeout nesting)
Trường hợp xấu nhất của mỗi tầng phải nằm gọn bên trong per-try timeout của bên gọi nó, trừ đi một khoảng dự phòng (margin) cho độ trễ mạng và xử lý:
inner_worst <= outer_per_try_timeout - margin
Nếu không, bên gọi sẽ hủy bỏ lần thử đó và thực hiện retry trong khi tầng bên trong vẫn đang tiếp tục xử lý, làm tăng gấp đôi tải hệ thống. Hãy truyền một hạn chót (deadline) xuống dọc theo chuỗi lệnh gọi (ví dụ qua header `grpc-timeout` hoặc `X-Request-Deadline`) và dừng xử lý ngay khi thời gian này cạn kiệt.

## Retry-After
Đối với mã lỗi 429 và 503, máy chủ có thể gửi kèm `Retry-After: <seconds>` hoặc một ngày giờ theo chuẩn HTTP. Hãy chờ ít nhất bằng khoảng thời gian đó (sử dụng max(Retry-After, backoff tính toán được)), và từ bỏ ngay nếu khoảng thời gian chờ này dài hơn thời hạn (deadline) còn lại.
FILE:references/retry-safety-guide.md
# Hướng dẫn an toàn khi Retry

## Những trường hợp nên retry
| Kết quả | Có Retry không? | Ghi chú |
|---|---|---|
| Từ chối kết nối (Connection refused) / ngắt kết nối (reset) trước khi yêu cầu được gửi đi | Có | An toàn cho bất kỳ phương thức nào |
| Hết thời gian chờ (Timeout) sau khi yêu cầu đã được gửi đi | Chỉ khi có tính bất biến hoặc có khóa bất biến (idempotency key) | Máy chủ có thể đã thực hiện xong công việc đó |
| 408 Request Timeout | Có, kèm theo backoff | |
| 429 Too Many Requests | Có, sau khoảng thời gian Retry-After | Không bao giờ gọi lại nhanh hơn thời gian máy chủ yêu cầu |
| 500 Internal Server Error | Đôi khi | Chỉ khi biết chắc lỗi đó là lỗi tạm thời (transient) |
| 502 / 503 / 504 | Có, kèm theo backoff và jitter | Tuân thủ Retry-After trên lỗi 503 |
| 400 / 401 / 403 / 404 / 405 / 410 / 422 | Không | Cùng một yêu cầu sẽ thất bại theo cùng một cách; hãy sửa lại yêu cầu, hoặc làm mới xác thực (auth) tối đa một lần |
| 409 Conflict | Thường là không | Ngoại lệ: một khóa bất biến vẫn đang trong quá trình xử lý, hãy retry sau một khoảng chờ ngắn |

## Tính bất biến (Idempotency)
- GET, HEAD, OPTIONS, PUT và DELETE có tính bất biến theo ngữ nghĩa HTTP, nếu máy chủ triển khai chúng đúng chuẩn.
- POST và PATCH không có tính bất biến. Để retry chúng, hãy gửi kèm một **khóa bất biến (idempotency key)** (một mã UUID cho mỗi thao tác logic, ví dụ qua header `Idempotency-Key`) mà máy chủ sẽ lưu trữ cùng với kết quả trong vòng 24 giờ hoặc lâu hơn. Khởi tạo khóa này một lần duy nhất trước lần thử đầu tiên, và tái sử dụng lại nó cho mọi lần retry.
- Các tác vụ thanh toán, gửi email, SMS, tạo đơn hàng và giữ chỗ tồn kho tuyệt đối không bao giờ được retry nếu không có khóa bất biến.

## Danh sách kiểm tra cho Client (Client checklist)
1. Đã thiết lập per-try timeout và ngắn hơn timeout của bên gọi.
2. Đã thiết lập hạn chót tổng thể (overall deadline) và truyền xuống dọc theo chuỗi lệnh gọi.
3. Sử dụng exponential backoff có giới hạn trần (cap) và full jitter.
4. Số lần thử (attempts) <= 3 ở tầng biên (edge), 1-2 ở các tầng bên trong.
5. Chỉ retry trên các hàng có giá trị "Có" trong bảng hướng dẫn.
6. Đính kèm khóa bất biến (idempotency key) trên mọi lệnh gọi không bất biến có thực hiện retry.
7. Có ngân sách retry (retry budget) hoặc bộ ngắt mạch (circuit breaker) để dừng retry khi xảy ra sự cố diện rộng.
8. Các chỉ số đo lường (metrics): số lần thử trên mỗi yêu cầu, tỷ lệ retry, số lần từ bỏ (give-up count), phân loại theo từng dịch vụ phụ thuộc (dependency).
9. Chỉ ghi log lỗi cuối cùng kèm theo tổng số lần đã thử, không ghi log ở mức error cho từng lần thử thất bại riêng lẻ.

## Kiến thức cơ bản về Bộ ngắt mạch (Circuit breaker)
- Đóng (Closed): các lệnh gọi diễn ra bình thường; đếm số lỗi trong một cửa sổ trượt (rolling window).
- Mở (Open): sau khi vượt ngưỡng ví dụ như 50% lỗi trên ít nhất 20 lệnh gọi, báo lỗi ngay lập tức (fail fast) trong vòng 30 giây.
- Nửa mở (Half-open): cho phép một vài lệnh gọi thăm dò đi qua; chuyển về trạng thái Đóng nếu chúng thành công.
Sự kết hợp giữa bộ ngắt mạch và ngân sách retry sẽ ngăn chặn bão retry làm trầm trọng thêm sự cố sập hệ thống.

## Thiết lập của các thư viện phổ biến
- AWS SDKs: chế độ tiêu chuẩn (standard mode) = 3 lần thử (attempts), full jitter, độ trễ cơ sở (base) 1 giây khi bị giới hạn tốc độ (throttling); chế độ thích ứng (adaptive mode) bổ sung thêm tính năng giới hạn tốc độ phía client.
- Chính sách retry của gRPC: maxAttempts, initialBackoff, maxBackoff, backoffMultiplier, retryableStatusCodes (UNAVAILABLE là mã phổ biến nhất); việc retry cần tuân thủ các quy tắc hedging/transparent retry đối với các phương thức không bất biến.
- Envoy / Istio: retry_on: 5xx,reset,connect-failure; num_retries; per_try_timeout; thiết lập route timeout >= per_try_timeout x (num_retries + 1).
- Python `urllib3.Retry(total=3, backoff_factor=0.5, status_forcelist=[502,503,504], respect_retry_after_header=True, allowed_methods=[...])`.
Hãy kiểm tra tài liệu của thư viện tương ứng với phiên bản bạn đang sử dụng; các giá trị mặc định có thể thay đổi theo phiên bản.
FILE:templates/retry-policy-review.md
# Đánh giá Chính sách Retry - <dịch vụ / chuỗi lệnh gọi> - <ngày>

**Người đánh giá:** <tên>  **Đầu vào:** <JSON chuỗi / các tệp cấu hình>  **Tỷ lệ lỗi giả định:** <30%>

## Chuỗi lệnh gọi
| # | Bên gọi -> Bên được gọi | Số lần thử (Attempts) | Backoff (base / x / cap) | Jitter | Per-try timeout | Deadline | Trường hợp xấu nhất |
|---|---|---|---|---|---|---|---|
| 1 | | | | | | | |

**Mức độ khuếch đại trường hợp xấu nhất:** <36> lệnh gọi trên mỗi yêu cầu (mục tiêu <= 10)
**Mức độ khuếch đại kỳ vọng tại <p>:** <2.7>

## Các phát hiện
| Mức độ nghiêm trọng | Tầng | Mã lỗi | Vấn đề | Cách khắc phục |
|---|---|---|---|---|
| high | | | | |

## Chính sách đề xuất
| # | Bên gọi -> Bên được gọi | Số lần thử (Attempts) | Backoff | Jitter | Per-try timeout | Deadline | Retry khi gặp |
|---|---|---|---|---|---|---|---|
| 1 | | | | full | | | |

**Mức độ khuếch đại trường hợp xấu nhất mới:** <n>  **Độ trễ trường hợp xấu nhất mới:** <s>

## Công việc tiếp theo
- [ ] Bổ sung khóa bất biến (idempotency keys) cho: <các lệnh gọi>
- [ ] Thiết lập ngân sách retry / bộ ngắt mạch cho: <dịch vụ phụ thuộc>
- [ ] Bảng điều khiển (Dashboards): số lần thử trên mỗi yêu cầu, tỷ lệ retry, số lần từ bỏ
FILE:examples/checkout-chain.json
{
  "name": "checkout: browser -> api gateway -> orders service -> payments provider",
  "failure_rate": 0.3,
  "max_amplification": 10,
  "layers": [
    {
      "name": "web app -> api gateway",
      "attempts": 3,
      "base_delay_ms": 200,
      "multiplier": 2,
      "max_delay_ms": 2000,
      "jitter": "full",
      "per_try_timeout_ms": 8000,
      "deadline_ms": 20000,
      "retry_on": ["network", 502, 503, 504],
      "idempotent": true,
      "respect_retry_after": true
    },
    {
      "name": "api gateway -> orders service",
      "attempts": 3,
      "base_delay_ms": 100,
      "multiplier": 2,
      "max_delay_ms": 1000,
      "jitter": "none",
      "per_try_timeout_ms": 3000,
      "retry_on": [500, 502, 503, 504, "timeout"],
      "idempotent": true
    },
    {
      "name": "orders service -> payments provider (create charge)",
      "attempts": 4,
      "base_delay_ms": 250,
      "multiplier": 2,
      "jitter": "equal",
      "per_try_timeout_ms": 2500,
      "deadline_ms": 9000,
      "retry_on": [409, 429, 500, 503, "timeout"],
      "idempotent": false,
      "idempotency_key": false,
      "respect_retry_after": false
    }
  ]
}
FILE:scripts/retry_policy.py
#!/usr/bin/env python3
"""Retry and backoff policy calculator (Python 3 standard library only).

Input: a JSON file describing a call chain, outermost caller first:
  {"name": "checkout", "failure_rate": 0.3, "max_amplification": 10,
   "layers": [
     {"name": "web -> api", "attempts": 3, "base_delay_ms": 100, "multiplier": 2,
      "max_delay_ms": 2000, "jitter": "full", "per_try_timeout_ms": 5000,
      "deadline_ms": 12000, "retry_on": [502, 503, 504, "timeout"],
      "idempotent": true, "respect_retry_after": true}, ...]}
attempts = total tries including the first one (attempts 1 = no retries).
jitter: none | full | equal | decorrelated.  Delay before retry k (k = 1..attempts-1):
  none  d = min(cap, base * multiplier^(k-1))
  full  U(0, d)              equal  d/2 + U(0, d/2)
  decorrelated  min(cap, U(base, 3 * previous))  (AWS style; expected value simulated, seed 7)

Reports per layer: delay schedule (min / expected / max), worst-case time for one call
(attempts x per-try timeout + max delays, cut at deadline), expected tries at the given
failure rate, and for the chain: worst-case and expected request amplification at the
innermost dependency. Flags:
  NO_JITTER, NO_CAP, HOT_LOOP (base 0), TOO_MANY_ATTEMPTS (> 5), NO_PER_TRY_TIMEOUT,
  NO_DEADLINE, DEADLINE_TOO_SHORT (deadline < one per-try timeout), INNER_OUTLIVES_CALLER
  (worst case > the outer layer's per-try timeout), RETRY_UNSAFE (non-idempotent with
  retries and no idempotency key), RETRY_ON_4XX (retrying 400/401/403/404/409/422),
  IGNORES_RETRY_AFTER (retries 429/503 without honoring Retry-After), AMPLIFICATION.
Usage:
  python3 retry_policy.py chain.json
  python3 retry_policy.py chain.json --failure-rate 0.5 --json
  python3 retry_policy.py --self-test
Exit code: 0 OK, 1 high-severity flags found, 2 bad input.
"""
import argparse
import json
import random
import sys

JITTERS = ("none", "full", "equal", "decorrelated")
BAD_4XX = {400, 401, 403, 404, 405, 409, 410, 422}
SEVERITY = {"INNER_OUTLIVES_CALLER": "high", "RETRY_UNSAFE": "high", "AMPLIFICATION": "high",
            "HOT_LOOP": "high", "NO_PER_TRY_TIMEOUT": "high", "RETRY_ON_4XX": "medium",
            "NO_JITTER": "medium", "NO_CAP": "medium", "IGNORES_RETRY_AFTER": "medium",
            "NO_DEADLINE": "medium", "DEADLINE_TOO_SHORT": "medium", "TOO_MANY_ATTEMPTS": "low"}


class PolicyError(ValueError):
    pass


def num(layer, key, default=None, minimum=0):
    v = layer.get(key, default)
    if v is None:
        return None
    if isinstance(v, bool) or not isinstance(v, (int, float)):
        raise PolicyError(f"layer {layer.get('name', '?')!r}: {key} must be a number")
    if v < minimum:
        raise PolicyError(f"layer {layer.get('name', '?')!r}: {key} must be >= {minimum}")
    return v


def delays(layer):
    """Return [(min, expected, max)] in ms for each retry."""
    n = int(layer["attempts"])
    base, mult, cap = layer["base_delay_ms"], layer["multiplier"], layer["max_delay_ms"]
    capf = (lambda x: x) if cap is None else (lambda x: min(cap, x))
    j = layer["jitter"]
    out = []
    if j == "decorrelated":
        rng = random.Random(7)
        runs = 20000
        sums = [0.0] * (n - 1)
        for _ in range(runs):
            prev = base
            for k in range(n - 1):
                prev = capf(rng.uniform(base, prev * 3)) if base > 0 else 0.0
                sums[k] += prev
        hi = base
        for k in range(n - 1):
            hi = capf(hi * 3)
            out.append((capf(base), sums[k] / runs, hi))
        return out
    for k in range(n - 1):
        d = capf(base * mult ** k)
        if j == "none":
            out.append((d, d, d))
        elif j == "full":
            out.append((0.0, d / 2, d))
        else:
            out.append((d / 2, d * 0.75, d))
    return out


def normalize(raw):
    layers = raw.get("layers") if isinstance(raw, dict) else None
    if not layers or not isinstance(layers, list):
        raise PolicyError("JSON must have a non-empty 'layers' list (outermost caller first)")
    out = []
    for i, l in enumerate(layers):
        if not isinstance(l, dict):
            raise PolicyError(f"layer {i} must be an object")
        l = dict(l)
        l.setdefault("name", f"layer {i + 1}")
        a = num(l, "attempts", 1, 1)
        if int(a) != a:
            raise PolicyError(f"layer {l['name']!r}: attempts must be a whole number")
        l["attempts"] = int(a)
        l["base_delay_ms"] = num(l, "base_delay_ms", 0)
        l["multiplier"] = num(l, "multiplier", 2, 1)
        l["max_delay_ms"] = num(l, "max_delay_ms", None)
        l["per_try_timeout_ms"] = num(l, "per_try_timeout_ms", None, 1)
        l["deadline_ms"] = num(l, "deadline_ms", None, 1)
        j = str(l.get("jitter", "none")).lower()
        if j not in JITTERS:
            raise PolicyError(f"layer {l['name']!r}: jitter must be one of {', '.join(JITTERS)}")
        l["jitter"] = j
        l["retry_on"] = list(l.get("retry_on", []))
        l["idempotent"] = bool(l.get("idempotent", True))
        l["idempotency_key"] = bool(l.get("idempotency_key", False))
        l["respect_retry_after"] = bool(l.get("respect_retry_after", False))
        out.append(l)
    return out


def expected_tries(n, p):
    if p >= 1:
        return float(n)
    return sum(p ** k for k in range(n))


def analyze(raw, failure_rate=None, max_amp=None):
    layers = normalize(raw)
    p = failure_rate if failure_rate is not None else raw.get("failure_rate", 0.2)
    if not 0 <= p <= 1:
        raise PolicyError("failure rate must be between 0 and 1")
    max_amp = max_amp if max_amp is not None else raw.get("max_amplification", 10)
    res = {"name": raw.get("name", "retry policy"), "failure_rate": p, "max_amplification": max_amp, "layers": []}
    worst_amp, exp_amp = 1, 1.0
    for i, l in enumerate(layers):
        n = l["attempts"]
        ds = delays(l)
        flags = []
        retries = n > 1
        if retries and l["jitter"] == "none":
            flags.append(("NO_JITTER", "all clients retry at the same moments; use full or decorrelated jitter"))
        if retries and l["max_delay_ms"] is None and n > 3:
            flags.append(("NO_CAP", "no max_delay_ms; exponential delays grow without bound"))
        if retries and l["base_delay_ms"] == 0:
            flags.append(("HOT_LOOP", "base_delay_ms is 0; retries fire immediately and hammer the dependency"))
        if n > 5:
            flags.append(("TOO_MANY_ATTEMPTS", f"{n} attempts; 3 (2 retries) is usually enough"))
        if l["per_try_timeout_ms"] is None:
            flags.append(("NO_PER_TRY_TIMEOUT", "no per-try timeout; one hung call can block every retry"))
        if l["deadline_ms"] is None and retries:
            flags.append(("NO_DEADLINE", "no overall deadline across attempts"))
        if l["deadline_ms"] is not None and l["per_try_timeout_ms"] and l["deadline_ms"] < l["per_try_timeout_ms"]:
            flags.append(("DEADLINE_TOO_SHORT", "deadline is shorter than a single try"))
        if retries and not l["idempotent"] and not l["idempotency_key"]:
            flags.append(("RETRY_UNSAFE", "retries a non-idempotent call without an idempotency key (double charge / duplicate write risk)"))
        bad = sorted(c for c in l["retry_on"] if isinstance(c, int) and c in BAD_4XX)
        if retries and bad:
            flags.append(("RETRY_ON_4XX", f"retries client errors {bad}; they will fail the same way again"))
        if retries and any(c in (429, 503) for c in l["retry_on"]) and not l["respect_retry_after"]:
            flags.append(("IGNORES_RETRY_AFTER", "retries 429/503 but ignores the Retry-After header"))
        max_delay_total = sum(d[2] for d in ds)
        exp_delay_total = sum(d[1] for d in ds)
        worst = None
        if l["per_try_timeout_ms"] is not None:
            worst = n * l["per_try_timeout_ms"] + max_delay_total
            if l["deadline_ms"] is not None:
                worst = min(worst, l["deadline_ms"])
        elif l["deadline_ms"] is not None:
            worst = l["deadline_ms"]
        if i > 0:
            parent = layers[i - 1]
            pt = parent["per_try_timeout_ms"]
            if pt is not None and (worst is None or worst > pt):
                flags.append(("INNER_OUTLIVES_CALLER", f"worst case {fmt_ms(worst)} > caller per-try timeout {fmt_ms(pt)}; the caller gives up and retries while this layer is still working"))
        et = expected_tries(n, p)
        worst_amp *= n
        exp_amp *= et
        res["layers"].append({
            "name": l["name"], "attempts": n, "jitter": l["jitter"],
            "delays_ms": [[round(a, 1), round(b, 1), round(c, 1)] for a, b, c in ds],
            "max_delay_total_ms": round(max_delay_total, 1), "expected_delay_total_ms": round(exp_delay_total, 1),
            "per_try_timeout_ms": l["per_try_timeout_ms"], "deadline_ms": l["deadline_ms"],
            "worst_case_ms": None if worst is None else round(worst, 1),
            "expected_tries": round(et, 3), "cumulative_worst_amplification": worst_amp,
            "cumulative_expected_amplification": round(exp_amp, 3),
            "flags": [{"code": c, "severity": SEVERITY[c], "detail": d} for c, d in flags]})
    if worst_amp > max_amp:
        res["layers"][-1]["flags"].append({"code": "AMPLIFICATION", "severity": "high",
            "detail": f"one top-level request can become {worst_amp} calls to the innermost dependency during an outage (limit {max_amp}); retry at one layer only or add a retry budget"})
    res["worst_amplification"] = worst_amp
    res["expected_amplification"] = round(exp_amp, 3)
    res["high_flags"] = sum(1 for l in res["layers"] for f in l["flags"] if f["severity"] == "high")
    return res


def fmt_ms(v):
    if v is None:
        return "unbounded"
    if v >= 1000:
        s = f"{v / 1000:.2f}".rstrip("0").rstrip(".")
        return s + " s"
    return f"{v:g} ms"


def to_markdown(r):
    out = [f"# Retry policy report: {r['name']}", "",
           f"Failure rate used for expected values: {r['failure_rate']:.0%}", "",
           "| Layer | Attempts | Jitter | Max total delay | Per-try timeout | Deadline | Worst case | Expected tries |",
           "|---|---|---|---|---|---|---|---|"]
    for l in r["layers"]:
        out.append(f"| {l['name']} | {l['attempts']} | {l['jitter']} | {fmt_ms(l['max_delay_total_ms'])} | "
                   f"{fmt_ms(l['per_try_timeout_ms'])} | {fmt_ms(l['deadline_ms'])} | {fmt_ms(l['worst_case_ms'])} | {l['expected_tries']:g} |")
    out += ["", "## Delay schedule (min / expected / max)"]
    for l in r["layers"]:
        if not l["delays_ms"]:
            out.append(f"- {l['name']}: no retries")
            continue
        parts = [f"retry {k + 1}: {fmt_ms(a)} / {fmt_ms(b)} / {fmt_ms(c)}" for k, (a, b, c) in enumerate(l["delays_ms"])]
        out.append(f"- {l['name']}: " + "; ".join(parts))
    out += ["", "## Amplification at the innermost dependency",
            f"- Worst case (every try fails): {r['worst_amplification']} calls per top-level request",
            f"- Expected at {r['failure_rate']:.0%} failure rate: {r['expected_amplification']:g} calls per top-level request", "",
            "## Findings"]
    n = 0
    for sev in ("high", "medium", "low"):
        for l in r["layers"]:
            for f in l["flags"]:
                if f["severity"] == sev:
                    n += 1
                    out.append(f"- **{sev.upper()} {f['code']}** ({l['name']}): {f['detail']}")
    if not n:
        out.append("- No findings.")
    return "\n".join(out)


def self_test():
    cases = []

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

    def one(**kw):
        base = {"name": "x", "attempts": 3, "base_delay_ms": 100, "multiplier": 2, "max_delay_ms": 1000,
                "jitter": "full", "per_try_timeout_ms": 1000, "deadline_ms": 5000, "retry_on": [503],
                "respect_retry_after": True}
        base.update(kw)
        return base

    def codes(r, i=0):
        return {f["code"] for f in r["layers"][i]["flags"]}

    d = delays(normalize({"layers": [one(jitter="none", attempts=5)]})[0])
    ok("exponential no jitter 100,200,400,800", [x[2] for x in d] == [100, 200, 400, 800])
    d = delays(normalize({"layers": [one(jitter="none", attempts=6)]})[0])
    ok("cap applied at 1000", d[-1][2] == 1000 and d[-2][2] == 800)
    d = delays(normalize({"layers": [one(jitter="full")]})[0])
    ok("full jitter mean half", d[0] == (0.0, 50.0, 100))
    d = delays(normalize({"layers": [one(jitter="equal")]})[0])
    ok("equal jitter 50..100 mean 75", d[0] == (50.0, 75.0, 100))
    d = delays(normalize({"layers": [one(jitter="decorrelated", attempts=4)]})[0])
    ok("decorrelated within bounds", all(a <= b <= c for a, b, c in d) and d[0][2] == 300 and d[2][2] == 1000)
    r = analyze({"layers": [one()]})
    ok("clean policy has no flags", not codes(r))
    ok("worst case = 3x1000 + 100 + 200", r["layers"][0]["worst_case_ms"] == 3300)
    r = analyze({"layers": [one(deadline_ms=2000)]})
    ok("deadline caps worst case", r["layers"][0]["worst_case_ms"] == 2000)
    ok("expected tries p=0.5 n=3 is 1.75", expected_tries(3, 0.5) == 1.75 and expected_tries(3, 1) == 3)
    ok("no jitter flagged", "NO_JITTER" in codes(analyze({"layers": [one(jitter="none")]})))
    ok("hot loop flagged", "HOT_LOOP" in codes(analyze({"layers": [one(base_delay_ms=0)]})))
    ok("no cap flagged with 4+ attempts", "NO_CAP" in codes(analyze({"layers": [one(max_delay_ms=None, attempts=4)]})))
    ok("unsafe retry flagged", "RETRY_UNSAFE" in codes(analyze({"layers": [one(idempotent=False)]})))
    ok("idempotency key makes it safe", "RETRY_UNSAFE" not in codes(analyze({"layers": [one(idempotent=False, idempotency_key=True)]})))
    ok("4xx retry flagged", "RETRY_ON_4XX" in codes(analyze({"layers": [one(retry_on=[400, 503])]})))
    ok("Retry-After ignored flagged", "IGNORES_RETRY_AFTER" in codes(analyze({"layers": [one(retry_on=[429], respect_retry_after=False)]})))
    ok("single attempt is not a retry policy", not codes(analyze({"layers": [one(attempts=1, jitter="none", deadline_ms=None)]})))
    r = analyze({"layers": [one(per_try_timeout_ms=2000, deadline_ms=None), one(per_try_timeout_ms=1000, deadline_ms=None)]})
    ok("inner outlives caller flagged", "INNER_OUTLIVES_CALLER" in codes(r, 1))
    r = analyze({"layers": [one(per_try_timeout_ms=4000), one(per_try_timeout_ms=1000, deadline_ms=3500)]})
    ok("inner within caller timeout ok", "INNER_OUTLIVES_CALLER" not in codes(r, 1))
    r = analyze({"layers": [one(), one(), one()], "max_amplification": 10})
    ok("3x3x3 amplification 27 flagged", r["worst_amplification"] == 27 and "AMPLIFICATION" in codes(r, 2))
    r = analyze({"layers": [one(), one()]}, failure_rate=0.5)
    ok("expected amplification 1.75^2", r["expected_amplification"] == 3.062)
    for bad, label in ((({"layers": []}), "empty layers rejected"), (({"layers": [one(jitter="random")]}), "bad jitter rejected"),
                       (({"layers": [one(attempts=0)]}), "zero attempts rejected"), (({"layers": [one(attempts=2.5)]}), "fractional attempts rejected")):
        try:
            analyze(bad)
            ok(label, False)
        except PolicyError:
            ok(label, True)
    ok("fmt_ms", fmt_ms(1500) == "1.5 s" and fmt_ms(250) == "250 ms" and fmt_ms(None) == "unbounded")
    passed = sum(c for _, c in cases)
    for name, c in cases:
        print(("PASS " if c else "FAIL ") + name)
    print(f"{passed}/{len(cases)} passed")
    return 0 if passed == len(cases) else 1


def main(argv=None):
    ap = argparse.ArgumentParser(description="Check retry/backoff/timeout policies across a call chain.")
    ap.add_argument("file", nargs="?", help="chain JSON ('-' for stdin)")
    ap.add_argument("--failure-rate", type=float, help="probability that one try fails (default from file or 0.2)")
    ap.add_argument("--max-amplification", type=float, help="flag chains above this worst-case amplification (default 10)")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args(argv)
    if a.self_test:
        return self_test()
    if not a.file:
        ap.error("chain JSON required (or --self-test)")
    try:
        raw = json.load(sys.stdin if a.file == "-" else open(a.file, encoding="utf-8"))
        r = analyze(raw, a.failure_rate, a.max_amplification)
    except (OSError, PolicyError, ValueError) as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    print(json.dumps(r, indent=2) if a.json else to_markdown(r))
    return 1 if r["high_flags"] else 0


if __name__ == "__main__":
    sys.exit(main())
BackendAPIPython+2
F@f
0
Trình Đánh Giá Tiêu Đề Bộ Nhớ Đệm HTTP (HTTP Cache Header Reviewer)
Kỹ năng

Đánh giá các tiêu đề bộ nhớ đệm HTTP (Cache-Control, Expires, ETag, Last-Modified, Vary, Set-Cookie) cho các trang web, tài sản tĩnh (assets) và API. Tính toán thời gian còn hiệu lực (freshness) trên trình duyệt và CDN, gắn cờ cảnh báo các phản hồi chứa cookie hoặc dữ liệu cá nhân mà CDN có nguy cơ lưu chung, trang HTML được lưu đệm quá lâu, tài sản đã gắn mã băm (hashed) lại lưu đệm quá ngắn, thuộc tính immutable gắn trên các URL có nội dung thay đổi, phân mảnh bộ nhớ đệm do Vary, thiếu bộ xác thực (validator), lưu đệm theo suy đoán (heuristic caching) và lưu đệm cả các phản hồi lỗi. Đề xuất tiêu đề phù hợp cho từng loại tài nguyên. Bao gồm công cụ kiểm tra viết bằng thư viện chuẩn Python có hỗ trợ cờ `--self-test`.

---
name: http-cache-header-reviewer
description: Đánh giá các tiêu đề bộ nhớ đệm HTTP (Cache-Control, Expires, ETag, Last-Modified, Vary, Set-Cookie) cho các trang web, tài sản tĩnh (assets) và API. Tính toán thời gian còn hiệu lực (freshness) trên trình duyệt và CDN, gắn cờ cảnh báo các phản hồi chứa cookie hoặc dữ liệu cá nhân mà CDN có nguy cơ lưu chung, trang HTML được lưu đệm quá lâu, tài sản đã gắn mã băm (hashed) lại lưu đệm quá ngắn, thuộc tính immutable gắn trên các URL có nội dung thay đổi, phân mảnh bộ nhớ đệm do Vary, thiếu bộ xác thực (validator), lưu đệm theo suy đoán (heuristic caching) và lưu đệm cả các phản hồi lỗi. Đề xuất tiêu đề phù hợp cho từng loại tài nguyên. Bao gồm công cụ kiểm tra viết bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.
---

# Trình Đánh Giá Tiêu Đề Bộ Nhớ Đệm HTTP (HTTP Cache Header Reviewer)

Bạn giúp các lập trình viên web cấu hình bộ nhớ đệm (caching) chuẩn xác: tăng tốc độ cho các lần truy cập lặp lại và giảm hóa đơn chi phí CDN mà không làm trang web bị cũ sau khi triển khai (deploy) hoặc tệ hơn là trả nhầm dữ liệu của người dùng này cho người dùng khác. Bạn thu thập các tiêu đề phản hồi (response headers) thực tế, giải thích cách từng tầng bộ nhớ đệm sẽ thực sự xử lý chúng, và đề xuất tiêu đề chuẩn cho từng loại tài nguyên kèm theo danh sách khắc phục ngắn gọn.

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

- `scripts/cache_audit.py` - đọc các khối phản hồi (đầu ra từ `curl -sI` hoặc DevTools) và in ra loại tài nguyên, thời gian sống (TTL) trên trình duyệt và CDN, cơ chế xác thực lại (revalidation), các cờ cảnh báo và tiêu đề `Cache-Control` được đề xuất (chỉ dùng thư viện chuẩn của Python 3)
- `references/cache-control-directives.md` - giải thích mọi chỉ thị (directive), quy tắc tính thời gian hiệu lực (freshness) và lưu đệm suy đoán (heuristic), bộ xác thực (validators), tiêu đề `Vary`, các mã trạng thái có thể lưu đệm
- `references/caching-strategy-playbook.md` - các tiêu đề khuyến nghị theo từng loại tài nguyên, đảm bảo an toàn khi triển khai (deploy), các lưu ý về CDN, cách thu thập và kiểm chứng tiêu đề
- `templates/cache-review-report.md` - mẫu báo cáo đối chiếu tiêu đề hiện tại với tiêu đề khuyến nghị cùng danh sách các mục cần sửa
- `examples/shop-responses.txt` - tám phản hồi từ một trang thương mại điện tử mẫu chứa các lỗi cấu hình phổ biến

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

### 1. Thu thập các tiêu đề
Yêu cầu danh sách các URL đại diện: trang chủ, một trang sau khi đăng nhập, một tệp JS/CSS có gắn mã băm (hashed), một tệp kịch bản không gắn mã băm, một hình ảnh, một phông chữ, một lệnh gọi API công khai, một lệnh gọi API cá nhân, và một phản hồi báo lỗi. Thu thập bằng lệnh `curl -sI <url>` (thêm `-H 'Cookie: ...'` hoặc `-H 'Authorization: ...'` đối với các yêu cầu đã đăng nhập, tuyệt đối không dán token thật vào báo cáo) hoặc dùng DevTools. Đặt từng phản hồi theo định dạng khối của tệp `examples/shop-responses.txt`: một dòng `### METHOD URL`, các dòng tùy chọn `> Request-Header: value`, dòng trạng thái (status line), rồi đến các tiêu đề phản hồi.

### 2. Chạy công cụ kiểm tra
```
python3 scripts/cache_audit.py examples/shop-responses.txt
python3 scripts/cache_audit.py headers.txt --json > cache-report.json
python3 scripts/cache_audit.py --self-test
```
Mã thoát (exit code) bằng 1 có nghĩa là phát hiện vấn đề mức độ nghiêm trọng cao, bằng 2 có nghĩa là không thể phân tích cú pháp dữ liệu đầu vào.

### 3. Giải thích các phát hiện
Thứ tự ưu tiên: quyền riêng tư là trên hết (AUTH_SHARED_CACHE, PUBLIC_WITH_COOKIE, PERSONAL_NOT_PRIVATE), tiếp theo là tính đúng đắn sau khi triển khai bản mới (HTML_LONG_TTL, IMMUTABLE_UNHASHED, ERROR_CACHED), sau đó đến hiệu năng (HASHED_SHORT_TTL, ASSET_NO_CACHING, VARY_FRAGMENTS, NO_VALIDATOR, HEURISTIC_ONLY) và tính chuẩn mực trong cấu hình (INVALID_DIRECTIVE, NO_STORE_CONFLICT, EXPIRES_ONLY, PRAGMA_ONLY). Sử dụng tệp `references/cache-control-directives.md` để giải thích lý do.

### 4. Đề xuất tiêu đề chuẩn
Sử dụng tệp `references/caching-strategy-playbook.md`. Đưa ra tiêu đề chính xác cho từng loại tài nguyên và chỉ rõ vị trí cần thiết lập trong hệ thống công nghệ (stack) của người dùng (Nginx `add_header`, Express `res.set`, Next.js `headers()`, siêu dữ liệu S3/CloudFront, quy tắc Cloudflare). Nhắc tới nguyên tắc triển khai: lưu đệm tài sản có mã băm trong một năm, còn tệp HTML thì luôn xác thực lại (revalidate).

### 5. Xác minh và lập báo cáo
Điền thông tin vào mẫu `templates/cache-review-report.md`. Sau khi thay đổi cấu hình, hãy thu thập lại các tiêu đề và chạy lại công cụ kiểm tra; đồng thời kiểm tra các tiêu đề HIT/MISS của CDN.

## Ví dụ kết quả
Đối với tệp `examples/shop-responses.txt`: 5 phát hiện mức độ cao (trang HTML trang chủ lưu đệm 1 ngày, tệp `widget.js` đặt `immutable` nhưng không có mã băm, API giỏ hàng có cookie phiên làm việc lại bị CDN lưu đệm trong 2 phút, trang đăng nhập có `Set-Cookie` nhưng bị đánh dấu `public` và lưu đệm 10 phút), 4 phát hiện mức trung bình (`app.3f9a1c7b.js` chỉ lưu đệm 1 giờ, `hero.jpg` lưu đệm theo suy đoán 8,1 ngày, dùng `Vary: Cookie`, lỗi 503 bị lưu đệm trong 1 giờ) và 1 phát hiện mức thấp (API giỏ hàng không có bộ xác thực); chỉ có duy nhất tệp `vendor.81d2e0aa.css` là đã được cấu hình lý tưởng.

## Quy tắc

- Luôn coi bất kỳ nguy cơ nào khiến bộ nhớ đệm dùng chung (shared cache) lưu trữ dữ liệu cá nhân là vấn đề ưu tiên hàng đầu.
- Không yêu cầu hoặc lưu giữ cookie hay token thật; chỉ cần dùng các giá trị giữ chỗ (placeholders) là đủ.
- Khuyến nghị xóa bộ nhớ đệm (purge) hoặc đánh phiên bản URL (URL versioning) khi thay đổi thời gian sống (TTL) của các tài nguyên đang chạy thực tế.
- Nêu rõ khi nào hành vi phụ thuộc vào cấu hình riêng của nhà cung cấp CDN và đề xuất người dùng kiểm tra cấu hình đó.
FILE:references/cache-control-directives.md
# Các Chỉ Thị Cache-Control và Quy Tắc Tính Thời Gian Hiệu Lực

## Các chỉ thị trong phản hồi (Response directives)
| Chỉ thị | Ý nghĩa |
|---|---|
| max-age=N | Còn hiệu lực (fresh) trong N giây ở bất kỳ bộ nhớ đệm nào (cả trình duyệt lẫn bộ nhớ đệm dùng chung) |
| s-maxage=N | Ghi đè giá trị max-age chỉ dành riêng cho các bộ nhớ đệm dùng chung (CDN, proxy) |
| no-cache | Có thể được lưu trữ, nhưng bắt buộc phải xác thực lại (revalidate) với máy chủ gốc trước mỗi lần sử dụng |
| no-store | Tuyệt đối không được lưu trữ ở bất cứ đâu; ghi đè lên mọi chỉ thị khác |
| private | Chỉ trình duyệt của người dùng mới được phép lưu trữ; các CDN và proxy không được phép lưu |
| public | Bộ nhớ đệm dùng chung được phép lưu trữ ngay cả khi thông thường phản hồi đó không được lưu đệm (ví dụ khi có tiêu đề Authorization) |
| must-revalidate | Một khi đã hết hạn (stale), tuyệt đối không được phục vụ nếu chưa xác thực lại thành công với máy chủ gốc |
| proxy-revalidate | Tương tự như must-revalidate, nhưng chỉ áp dụng cho các bộ nhớ đệm dùng chung |
| immutable | Sẽ không bao giờ thay đổi trong suốt thời gian còn hiệu lực; trình duyệt bỏ qua bước xác thực lại khi người dùng tải lại trang |
| stale-while-revalidate=N | Cho phép phục vụ bản cũ đã hết hạn trong N giây trong khi âm thầm làm mới dưới nền |
| stale-if-error=N | Cho phép phục vụ bản cũ đã hết hạn trong N giây nếu máy chủ gốc trả về lỗi 5xx hoặc không thể kết nối |
| no-transform | Các trạm trung gian không được phép sửa đổi phần thân phản hồi (như nén lại hình ảnh, v.v.) |

Những nhầm lẫn phổ biến: `no-cache` KHÔNG có nghĩa là "không lưu vào bộ nhớ đệm" (đó là chức năng của `no-store`); giá trị của `max-age` bắt buộc phải là số nguyên giây (`max-age=10m` là không hợp lệ và sẽ bị bỏ qua); `Pragma: no-cache` chỉ có tác dụng trên chiều yêu cầu gửi đi (request).

## Thời gian sống còn hiệu lực (Freshness lifetime - RFC 9111)
1. Bộ nhớ đệm dùng chung (Shared cache): ưu tiên `s-maxage`, nếu không có thì lấy `max-age`, nếu không có nữa thì lấy `Expires - Date`.
2. Trình duyệt (Browser): ưu tiên `max-age`, nếu không có thì lấy `Expires - Date`.
3. Khi không có cả hai tiêu đề trên nhưng có `Last-Modified`: các bộ nhớ đệm có thể tính theo suy đoán (heuristic), thường lấy bằng 10% của `(Date - Last-Modified)`. Một tệp được sửa đổi lần cuối cách đây 80 ngày có thể được tái sử dụng suốt 8 ngày mà không cần kiểm tra lại.
4. Chỉ thị `private` đặt thời gian sống trên bộ nhớ đệm dùng chung về 0. Chỉ thị `no-store` đặt cả hai về 0.

## Bộ xác thực (Validators) và cơ chế xác thực lại (Revalidation)
- ETag (mạnh `"abc"` hoặc yếu `W/"abc"`) -> phía máy khách gửi kèm tiêu đề `If-None-Match`.
- Last-Modified -> phía máy khách gửi kèm tiêu đề `If-Modified-Since`.
- Nếu khớp, máy chủ trả về mã `304 Not Modified` không kèm phần thân (body), rất nhẹ và tiết kiệm băng thông. Nếu không có bộ xác thực, mỗi lần xác thực lại đều phải tải về toàn bộ phần thân phản hồi.

## Tiêu đề Vary
- `Vary: Accept-Encoding` là bình thường và an toàn.
- `Vary: Cookie`, `User-Agent` hoặc `Referer` sẽ tạo ra một bản ghi bộ nhớ đệm riêng cho mỗi giá trị khác nhau, đồng nghĩa với việc hầu như không bao giờ trúng bộ nhớ đệm (cache hit) trên CDN.
- `Vary: *` có nghĩa là phản hồi này không bao giờ có thể tái sử dụng.

## Các mã trạng thái (Status codes)
Được phép lưu đệm theo mặc định khi còn hiệu lực: 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501. Các phản hồi lỗi (4xx/5xx) chỉ nên lưu đệm rất ngắn hoặc không lưu đệm, nếu không CDN có thể hiển thị trang báo sập hệ thống suốt nhiều giờ liền. Hãy sử dụng `stale-if-error` để phục vụ bản sao hợp lệ gần nhất thay thế.
FILE:references/caching-strategy-playbook.md
# Cẩm Nang Chiến Lược Bộ Nhớ Đệm

## Các tiêu đề khuyến nghị theo từng loại tài nguyên
| Tài nguyên | Cache-Control | Ghi chú |
|---|---|---|
| Trang HTML, khung ứng dụng (app shell) | `no-cache` + ETag | Luôn mới sau mỗi lần triển khai (deploy); phản hồi 304 giúp tiết kiệm băng thông |
| Trang HTML tiếp thị (Marketing) trên CDN | `public, max-age=0, s-maxage=300, stale-while-revalidate=60` | Xóa bộ nhớ đệm CDN (purge) khi xuất bản nội dung mới |
| Tệp JS/CSS có mã băm nội dung (`app.3f9a1c7b.js`) | `public, max-age=31536000, immutable` | Bản dựng (build) mới = URL mới |
| Tệp JS/CSS không có mã băm | `public, max-age=300, stale-while-revalidate=86400` | Hoặc bổ sung cơ chế gắn mã băm vào quy trình build |
| Hình ảnh, tệp người dùng tải lên có URL cố định | `public, max-age=86400, stale-while-revalidate=604800` | Đánh phiên bản cho URL khi hình ảnh thay đổi |
| Phông chữ (URL có đánh phiên bản) | `public, max-age=31536000, immutable` | Đồng thời gửi kèm các tiêu đề CORS |
| Dữ liệu API công khai (giá cả, danh mục sản phẩm) | `public, max-age=30, s-maxage=300, stale-if-error=600` | Giữ TTL ngắn; xóa bộ nhớ đệm khi dữ liệu thay đổi |
| Dữ liệu API cá nhân (giỏ hàng, hồ sơ cá nhân) | `private, no-cache` + ETag | Tuyệt đối không đặt là public |
| Thông tin bí mật, ngân hàng, hồ sơ y tế, token dùng một lần | `no-store` | Áp dụng cả trên trang đăng xuất và trang quản lý tài khoản |
| Chuyển hướng 301 (Redirect 301) | Bắt đầu với `max-age=86400` | Trình duyệt có thể lưu đệm vĩnh viễn một mã 301 không kèm giới hạn thời gian |
| Các lỗi 5xx | `no-store` hoặc `max-age=0`, kết hợp dùng `stale-if-error` trên các phản hồi thành công | |

## Đảm bảo an toàn khi triển khai (Deploy safety)
1. Gắn mã băm (hash) vào tên của mọi tệp tài sản được tạo ra khi build, sau đó lưu đệm chúng trong thời hạn một năm.
2. Cho tệp HTML xác thực lại (revalidate) để nó luôn trỏ tới các tệp có mã băm mới nhất hiện tại.
3. Giữ các tệp tài sản gắn mã băm của bản cũ tiếp tục trực tuyến ít nhất qua một đợt phát hành để các tab trình duyệt đang mở sẵn vẫn hoạt động bình thường.
4. Tuyệt đối không gắn chỉ thị `immutable` lên một URL mà nội dung của nó có thể thay đổi.

## Các lưu ý về CDN
- Hầu hết các CDN chỉ bỏ qua `private`/`no-store` khi bị cấu hình sai; hãy kiểm tra tùy chọn "tôn trọng tiêu đề từ máy chủ gốc" (respect origin headers).
- Nhiều CDN mặc định không lưu đệm các phản hồi có chứa `Set-Cookie`; nhưng một số CDN vẫn lưu nếu có đặt chỉ thị `public`. Hãy loại bỏ `Set-Cookie` khỏi các phản hồi tài nguyên tĩnh.
- Sử dụng tiêu đề `Surrogate-Control` hoặc `CDN-Cache-Control` nếu CDN có hỗ trợ và bạn muốn thiết lập TTL trên CDN tách biệt với TTL trên trình duyệt.
- Chuẩn hóa khóa bộ nhớ đệm (cache key) (bỏ qua các chuỗi truy vấn theo dõi quảng cáo như `utm_*`).

## Cách thu thập các tiêu đề
```
curl -sI https://example.com/ ; curl -sI -H 'Cookie: session=...' https://example.com/api/me
curl -s -o /dev/null -D - https://example.com/assets/app.js   # Dùng GET thay vì HEAD
```
Một số máy chủ trả lời phương thức HEAD khác với phương thức GET; khi nghi ngờ, hãy sử dụng cú pháp GET. Trong Chrome DevTools: mở tab Network, chọn một yêu cầu, vào mục Headers, tìm phần Response Headers (sao chép dưới dạng văn bản).

## Kiểm tra kết quả
- Tải lại trang hai lần trong DevTools: các tài sản có gắn mã băm phải hiển thị "(memory cache)" hoặc "(disk cache)", còn trang HTML phải hiển thị mã 304 hoặc 200.
- Đối với CDN: quan sát các tiêu đề `Age`, `X-Cache` hoặc `CF-Cache-Status` (HIT / MISS / BYPASS).
FILE:templates/cache-review-report.md
# Báo Cáo Đánh Giá Bộ Nhớ Đệm HTTP - <site / service> - <date>

**Phạm vi:** <URLs or HAR file>  **CDN:** <name or none>  **Thu thập bằng:** <curl -sI / DevTools / HAR>

## Tóm tắt
- Số phản hồi đã đánh giá: <n>  Mức Cao: <n>  Mức Trung bình: <n>  Mức Thấp: <n>
- Rủi ro lớn nhất: <ví dụ: API cá nhân bị lưu đệm trên CDN>
- Cải thiện mang lại hiệu quả lớn nhất: <ví dụ: lưu đệm một năm cho các tài sản tĩnh có gắn mã băm>

## Đối chiếu hiện tại và khuyến nghị
| Tài nguyên | Loại | Cache-Control hiện tại | TTL Trình duyệt / CDN | Cờ cảnh báo | Khuyến nghị |
|---|---|---|---|---|---|
| | | | | | |

## Danh sách cần khắc phục
| Ưu tiên | Thay đổi cần làm | Vị trí sửa (máy chủ, framework, quy tắc CDN) | Người phụ trách | Hoàn thành |
|---|---|---|---|---|
| 1 | | | | [ ] |

## Xác minh
- [ ] Chạy lại lệnh `python3 scripts/cache_audit.py` trên các tiêu đề mới thu thập: không còn phát hiện mức độ cao
- [ ] CDN hiển thị HIT đối với các tệp tĩnh, BYPASS/MISS đối với API dữ liệu cá nhân
- [ ] Kiểm thử triển khai (deploy test): trang HTML mới tải đúng các tài sản mới mà không cần ép tải lại trang (hard refresh)
FILE:examples/shop-responses.txt
# Responses captured with: curl -sI <url>  (request headers sent are listed with "> ")
### GET https://shop.example/
HTTP/2 200
content-type: text/html; charset=utf-8
cache-control: public, max-age=86400
date: Wed, 21 Oct 2026 07:00:00 GMT

### GET https://shop.example/assets/app.3f9a1c7b.js
HTTP/2 200
content-type: application/javascript
cache-control: public, max-age=3600
etag: "3f9a1c7b"

### GET https://shop.example/assets/vendor.81d2e0aa.css
HTTP/2 200
content-type: text/css
cache-control: public, max-age=31536000, immutable
etag: "81d2e0aa"

### GET https://shop.example/js/widget.js
HTTP/2 200
content-type: application/javascript
cache-control: public, max-age=31536000, immutable

### GET https://shop.example/img/hero.jpg
HTTP/2 200
content-type: image/jpeg
date: Wed, 21 Oct 2026 07:00:00 GMT
last-modified: Sat, 01 Aug 2026 07:00:00 GMT

### GET https://shop.example/api/cart
> Cookie: session=abc123
HTTP/2 200
content-type: application/json
cache-control: public, s-maxage=120
vary: Cookie

### POST https://shop.example/login
HTTP/2 200
content-type: text/html; charset=utf-8
cache-control: public, max-age=600
set-cookie: session=xyz789; HttpOnly; Secure; SameSite=Lax

### GET https://shop.example/api/products?page=1
HTTP/2 503
content-type: application/json
cache-control: public, max-age=3600
FILE:scripts/cache_audit.py
#!/usr/bin/env python3
"""HTTP caching header reviewer (Python 3 standard library only).

Input: a text file with one block per response, as copied from `curl -sI`, browser
devtools or a HAR export:
  ### GET https://shop.example/assets/app.3f9a1c7b.js
  > Authorization: Bearer ...          (optional request headers start with "> ")
  HTTP/2 200
  content-type: application/javascript
  cache-control: public, max-age=31536000, immutable
  etag: "3f9a1c7b"
Blank lines and lines starting with "#" (but not "###") are ignored.

For each response it works out the kind (html, hashed-asset, asset, api, image, font, other),
browser and shared-cache (CDN) freshness in seconds (s-maxage > max-age > Expires - Date >
10% heuristic from Last-Modified), whether it can be revalidated, and flags:
  NO_STORE_CONFLICT, PUBLIC_WITH_COOKIE, AUTH_SHARED_CACHE, PERSONAL_NOT_PRIVATE, VARY_STAR,
  VARY_FRAGMENTS, HTML_LONG_TTL, HASHED_SHORT_TTL, IMMUTABLE_UNHASHED, ASSET_NO_CACHING,
  NO_VALIDATOR, HEURISTIC_ONLY, EXPIRES_ONLY, PRAGMA_ONLY, ERROR_CACHED, INVALID_DIRECTIVE.
It suggests a Cache-Control value per kind.
Usage:
  python3 cache_audit.py responses.txt
  python3 cache_audit.py responses.txt --json
  python3 cache_audit.py --self-test
Exit code: 0 OK, 1 high-severity flags found, 2 bad input.
"""
import argparse
import email.utils
import json
import re
import sys

KNOWN = {"max-age", "s-maxage", "no-cache", "no-store", "no-transform", "must-revalidate",
         "proxy-revalidate", "must-understand", "private", "public", "immutable",
         "stale-while-revalidate", "stale-if-error"}
NEEDS_INT = {"max-age", "s-maxage", "stale-while-revalidate", "stale-if-error"}
HASH_RE = re.compile(r"[.\-_]([0-9a-f]{8,}|[0-9A-Za-z_-]{8,}(?=\.[a-z0-9]+$))\.[a-z0-9]+$")
SEVERITY = {"NO_STORE_CONFLICT": "low", "PUBLIC_WITH_COOKIE": "high", "AUTH_SHARED_CACHE": "high",
            "PERSONAL_NOT_PRIVATE": "medium", "VARY_STAR": "medium", "VARY_FRAGMENTS": "medium",
            "HTML_LONG_TTL": "high", "HASHED_SHORT_TTL": "medium", "IMMUTABLE_UNHASHED": "high",
            "ASSET_NO_CACHING": "medium", "NO_VALIDATOR": "low", "HEURISTIC_ONLY": "medium",
            "EXPIRES_ONLY": "low", "PRAGMA_ONLY": "low", "ERROR_CACHED": "medium",
            "INVALID_DIRECTIVE": "medium"}
RECOMMEND = {
    "html": "no-cache (plus ETag or Last-Modified), or max-age=0, must-revalidate",
    "hashed-asset": "public, max-age=31536000, immutable",
    "asset": "public, max-age=3600, stale-while-revalidate=86400 (or add a content hash to the file name)",
    "image": "public, max-age=86400, stale-while-revalidate=604800",
    "font": "public, max-age=31536000, immutable (if the URL is versioned)",
    "api": "private, no-cache for user data; public, max-age=60, s-maxage=300 only for truly public data",
    "personal": "private, no-cache (or no-store for sensitive data)",
    "other": "set an explicit Cache-Control",
}


class CacheError(ValueError):
    pass


def parse_cc(value):
    """Return (directives dict, problems list)."""
    d, problems = {}, []
    for part in [p.strip() for p in value.split(",") if p.strip()]:
        k, eq, v = part.partition("=")
        k = k.strip().lower()
        v = v.strip().strip('"')
        if k not in KNOWN:
            problems.append(f"unknown directive {k!r}")
            continue
        if k in NEEDS_INT:
            if not re.fullmatch(r"\d+", v or ""):
                problems.append(f"{k} needs a whole number of seconds, got {v!r}")
                continue
            d[k] = int(v)
        else:
            d[k] = True if not eq else v
    return d, problems


def parse_blocks(text):
    blocks, cur = [], None
    for raw in text.splitlines():
        line = raw.rstrip("\r")
        if line.startswith("###"):
            cur = {"label": line[3:].strip(), "status": None, "headers": {}, "request": {}}
            blocks.append(cur)
            continue
        if not line.strip() or line.lstrip().startswith("#"):
            continue
        if cur is None:
            raise CacheError("file must start with a '### METHOD URL' line")
        if line.startswith(">"):
            k, _, v = line[1:].strip().partition(":")
            cur["request"][k.strip().lower()] = v.strip()
            continue
        m = re.match(r"HTTP/[\d.]+\s+(\d{3})", line)
        if m:
            cur["status"] = int(m.group(1))
            continue
        k, colon, v = line.partition(":")
        if not colon:
            raise CacheError(f"cannot parse header line {line!r} in block {cur['label']!r}")
        k = k.strip().lower()
        if k in cur["headers"] and k in ("cache-control", "vary"):
            cur["headers"][k] += ", " + v.strip()
        elif k == "set-cookie":
            cur["headers"].setdefault(k, []).append(v.strip())
        else:
            cur["headers"][k] = v.strip()
    if not blocks:
        raise CacheError("no response blocks found")
    return blocks


def url_of(label):
    parts = label.split()
    return parts[-1] if parts else ""


def kind_of(url, ctype):
    path = re.sub(r"[?#].*$", "", url)
    name = path.rsplit("/", 1)[-1].lower()
    ctype = ctype.lower()
    hashed = bool(HASH_RE.search(name)) and bool(re.search(r"\d", name))
    if "text/html" in ctype or name.endswith(".html") or (not ctype and "." not in name):
        return "html", False
    if "json" in ctype or "/api/" in path or "graphql" in path:
        return "api", False
    if name.endswith((".woff2", ".woff", ".ttf", ".otf")) or "font" in ctype:
        return "font", hashed
    if ctype.startswith("image/") or name.endswith((".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico")):
        return ("hashed-asset" if hashed else "image"), hashed
    if name.endswith((".js", ".mjs", ".css", ".map", ".wasm")) or "javascript" in ctype or "text/css" in ctype:
        return ("hashed-asset" if hashed else "asset"), hashed
    return "other", hashed


def http_date(v):
    try:
        return email.utils.parsedate_to_datetime(v)
    except (TypeError, ValueError, IndexError):
        return None


def review(block):
    h, req = block["headers"], block["request"]
    status = block["status"] or 200
    url = url_of(block["label"])
    kind, hashed = kind_of(url, h.get("content-type", ""))
    cc, problems = parse_cc(h.get("cache-control", ""))
    flags = []

    def flag(code, detail):
        flags.append({"code": code, "severity": SEVERITY[code], "detail": detail})

    for p in problems:
        flag("INVALID_DIRECTIVE", p)
    cookies = h.get("set-cookie", [])
    auth = "authorization" in req or "cookie" in req
    date, expires, lastmod = http_date(h.get("date")), h.get("expires"), http_date(h.get("last-modified"))
    no_store = "no-store" in cc
    heuristic = False
    if no_store:
        browser = shared = 0
    else:
        browser = cc.get("max-age")
        if browser is None and expires is not None:
            ex = http_date(expires)
            browser = max(0, int((ex - date).total_seconds())) if ex and date else 0
        if browser is None and "no-cache" not in cc and lastmod and date and status in (200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501):
            browser = max(0, int((date - lastmod).total_seconds() / 10))
            heuristic = True
        browser = browser or 0
        shared = 0 if "private" in cc else cc.get("s-maxage", browser)
    if "no-cache" in cc:
        browser_eff, shared_eff = 0, 0
    else:
        browser_eff, shared_eff = browser, shared
    validator = "etag" in h or "last-modified" in h
    personal = bool(cookies) or auth

    if no_store and any(k in cc for k in ("max-age", "s-maxage", "public", "immutable")):
        flag("NO_STORE_CONFLICT", "no-store overrides the other directives; remove the extras so intent is clear")
    if cookies and ("public" in cc or "s-maxage" in cc) and not no_store:
        flag("PUBLIC_WITH_COOKIE", "response sets a cookie but allows shared caches; a CDN may serve one user's cookie to others")
    if auth and shared > 0 and ("public" in cc or "s-maxage" in cc) and kind in ("api", "html", "other"):
        flag("AUTH_SHARED_CACHE", "authenticated request with a response that shared caches may store; personal data can leak across users")
    elif personal and kind in ("api", "html", "other") and not no_store and "private" not in cc \
            and not any(f["code"] == "PUBLIC_WITH_COOKIE" for f in flags):
        flag("PERSONAL_NOT_PRIVATE", "personalized response without private or no-store")
    vary = [v.strip().lower() for v in h.get("vary", "").split(",") if v.strip()]
    if "*" in vary:
        flag("VARY_STAR", "Vary: * makes the response effectively uncacheable")
    frag = [v for v in vary if v in ("user-agent", "cookie", "referer")]
    if frag and shared > 0:
        flag("VARY_FRAGMENTS", f"Vary on {', '.join(frag)} splits the CDN cache into many copies; vary on a normalized header instead")
    if kind == "html" and browser_eff > 300:
        flag("HTML_LONG_TTL", f"HTML cached {fmt_s(browser_eff)} without revalidation; users keep old pages that point to deleted assets after a deploy")
    if kind == "hashed-asset" and browser_eff < 30 * 86400 and not no_store:
        flag("HASHED_SHORT_TTL", f"content-hashed file cached only {fmt_s(browser_eff)}; it can be cached for a year with immutable")
    if "immutable" in cc and not hashed:
        flag("IMMUTABLE_UNHASHED", "immutable on a URL without a content hash; browsers will not pick up changes until max-age expires")
    if kind in ("asset", "image", "font") and (no_store or "no-cache" in cc) and not personal:
        flag("ASSET_NO_CACHING", "static file is never reused from cache; every page view downloads or revalidates it")
    if ("no-cache" in cc or browser_eff == 0) and not no_store and not validator and status == 200:
        flag("NO_VALIDATOR", "must revalidate but has no ETag or Last-Modified, so every check downloads the full body")
    if heuristic:
        flag("HEURISTIC_ONLY", f"no Cache-Control or Expires; caches guess {fmt_s(browser)} from Last-Modified")
    if "cache-control" not in h and expires is not None:
        flag("EXPIRES_ONLY", "only the legacy Expires header; add Cache-Control max-age")
    if "cache-control" not in h and "pragma" in h:
        flag("PRAGMA_ONLY", "Pragma: no-cache is a request header and has no effect on responses; use Cache-Control")
    if status >= 400 and max(browser_eff, shared_eff) > 300:
        flag("ERROR_CACHED", f"status {status} cached for {fmt_s(max(browser_eff, shared_eff))}; a brief error becomes a long outage")
    rec_kind = "personal" if personal and kind in ("api", "html", "other") else kind
    return {"label": block["label"], "status": status, "kind": kind, "hashed": hashed,
            "cache_control": h.get("cache-control"), "browser_ttl_s": browser_eff, "shared_ttl_s": shared_eff,
            "revalidate": "no-cache" in cc or "must-revalidate" in cc, "validator": validator,
            "stale_while_revalidate_s": cc.get("stale-while-revalidate"), "flags": flags,
            "recommended": RECOMMEND[rec_kind]}


def fmt_s(s):
    if s is None:
        return "-"
    for unit, n in (("d", 86400), ("h", 3600), ("m", 60)):
        if s >= n:
            v = s / n
            return f"{v:.0f}{unit}" if v == int(v) else f"{v:.1f}{unit}"
    return f"{s}s"


def audit(text):
    items = [review(b) for b in parse_blocks(text)]
    return {"items": items, "high_flags": sum(1 for i in items for f in i["flags"] if f["severity"] == "high")}


def to_markdown(r):
    out = ["# HTTP cache header review", "",
           "| Response | Status | Kind | Browser TTL | CDN TTL | Revalidate | Validator | Flags |",
           "|---|---|---|---|---|---|---|---|"]
    for i in r["items"]:
        out.append(f"| {i['label']} | {i['status']} | {i['kind']} | {fmt_s(i['browser_ttl_s'])} | {fmt_s(i['shared_ttl_s'])} | "
                   f"{'yes' if i['revalidate'] else 'no'} | {'yes' if i['validator'] else 'no'} | {', '.join(f['code'] for f in i['flags']) or '-'} |")
    out += ["", "## Findings"]
    n = 0
    for sev in ("high", "medium", "low"):
        for i in r["items"]:
            for f in i["flags"]:
                if f["severity"] == sev:
                    n += 1
                    out.append(f"- **{sev.upper()} {f['code']}** {i['label']}: {f['detail']}")
    if not n:
        out.append("- No findings.")
    out += ["", "## Suggested Cache-Control"]
    for i in r["items"]:
        if i["flags"]:
            out.append(f"- {i['label']}: `{i['recommended']}` (now: `{i['cache_control'] or 'none'}`)")
    return "\n".join(out)


def self_test():
    cases = []

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

    def one(url, *lines):
        return review(parse_blocks("### GET " + url + "\nHTTP/1.1 200 OK\n" + "\n".join(lines))[0])

    def codes(item):
        return {f["code"] for f in item["flags"]}

    ok("parse max-age", parse_cc("public, max-age=600")[0] == {"public": True, "max-age": 600})
    ok("bad max-age reported", parse_cc("max-age=10m")[1])
    ok("unknown directive reported", parse_cc("maxage=60")[1])
    ok("hashed js detected", kind_of("https://x/app.3f9a1c7b.js", "") == ("hashed-asset", True))
    ok("unhashed css", kind_of("https://x/styles.css", "text/css") == ("asset", False))
    ok("html by content type", kind_of("https://x/", "text/html; charset=utf-8")[0] == "html")
    ok("api by json", kind_of("https://x/api/cart", "application/json")[0] == "api")
    i = one("https://x/app.3f9a1c7b.js", "Cache-Control: public, max-age=31536000, immutable", "ETag: \"a\"")
    ok("ideal hashed asset clean", not codes(i) and i["browser_ttl_s"] == 31536000)
    i = one("https://x/app.3f9a1c7b.js", "Cache-Control: public, max-age=3600")
    ok("hashed short ttl", "HASHED_SHORT_TTL" in codes(i))
    i = one("https://x/main.js", "Cache-Control: public, max-age=31536000, immutable")
    ok("immutable unhashed", "IMMUTABLE_UNHASHED" in codes(i))
    i = one("https://x/", "Content-Type: text/html", "Cache-Control: max-age=86400")
    ok("html long ttl", "HTML_LONG_TTL" in codes(i))
    i = one("https://x/", "Content-Type: text/html", "Cache-Control: no-cache", "ETag: \"1\"")
    ok("html no-cache with etag clean", not codes(i) and i["browser_ttl_s"] == 0)
    i = one("https://x/", "Content-Type: text/html", "Cache-Control: no-cache")
    ok("no validator", "NO_VALIDATOR" in codes(i))
    i = one("https://x/login", "Content-Type: text/html", "Cache-Control: public, max-age=600", "Set-Cookie: sid=1")
    ok("public with cookie", "PUBLIC_WITH_COOKIE" in codes(i))
    i = review(parse_blocks("### GET https://x/api/me\n> Authorization: Bearer t\nHTTP/2 200\ncontent-type: application/json\ncache-control: public, s-maxage=300")[0])
    ok("auth shared cache", "AUTH_SHARED_CACHE" in codes(i) and i["shared_ttl_s"] == 300)
    i = review(parse_blocks("### GET https://x/api/me\n> Cookie: sid=1\nHTTP/2 200\ncontent-type: application/json\ncache-control: max-age=60")[0])
    ok("personal not private", "PERSONAL_NOT_PRIVATE" in codes(i))
    i = one("https://x/api/me", "Content-Type: application/json", "Cache-Control: private, max-age=60")
    ok("private hides from cdn", i["shared_ttl_s"] == 0 and i["browser_ttl_s"] == 60)
    i = one("https://x/a.css", "Cache-Control: no-store, max-age=60")
    ok("no-store conflict and asset no caching", {"NO_STORE_CONFLICT", "ASSET_NO_CACHING"} <= codes(i))
    i = one("https://x/a.css", "Cache-Control: public, max-age=600", "Vary: *")
    ok("vary star", "VARY_STAR" in codes(i))
    i = one("https://x/a.css", "Cache-Control: public, max-age=600", "Vary: Accept-Encoding, User-Agent")
    ok("vary user-agent fragments", "VARY_FRAGMENTS" in codes(i))
    i = one("https://x/logo.png", "Date: Wed, 21 Oct 2026 10:00:00 GMT", "Last-Modified: Mon, 11 Oct 2026 10:00:00 GMT")
    ok("heuristic 10% of 10 days = 1 day", "HEURISTIC_ONLY" in codes(i) and i["browser_ttl_s"] == 86400)
    i = one("https://x/logo.png", "Date: Wed, 21 Oct 2026 10:00:00 GMT", "Expires: Wed, 21 Oct 2026 11:00:00 GMT")
    ok("expires only gives 1h", "EXPIRES_ONLY" in codes(i) and i["browser_ttl_s"] == 3600)
    i = one("https://x/", "Content-Type: text/html", "Pragma: no-cache")
    ok("pragma only", "PRAGMA_ONLY" in codes(i))
    i = review(parse_blocks("### GET https://x/a.css\nHTTP/1.1 503 Service Unavailable\ncache-control: public, max-age=3600")[0])
    ok("error cached", "ERROR_CACHED" in codes(i))
    ok("repeated cache-control merged", parse_blocks("### GET https://x/a\nHTTP/1.1 200\ncache-control: public\ncache-control: max-age=5")[0]["headers"]["cache-control"] == "public, max-age=5")
    try:
        parse_blocks("HTTP/1.1 200 OK\n")
        ok("missing ### rejected", False)
    except CacheError:
        ok("missing ### rejected", True)
    ok("fmt_s", fmt_s(31536000) == "365d" and fmt_s(600) == "10m" and fmt_s(45) == "45s" and fmt_s(699840) == "8.1d")
    passed = sum(c for _, c in cases)
    for name, c in cases:
        print(("PASS " if c else "FAIL ") + name)
    print(f"{passed}/{len(cases)} passed")
    return 0 if passed == len(cases) else 1


def main(argv=None):
    ap = argparse.ArgumentParser(description="Review HTTP caching headers.")
    ap.add_argument("file", nargs="?", help="responses text file ('-' for stdin)")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args(argv)
    if a.self_test:
        return self_test()
    if not a.file:
        ap.error("responses file required (or --self-test)")
    try:
        text = sys.stdin.read() if a.file == "-" else open(a.file, encoding="utf-8").read()
        r = audit(text)
    except (OSError, CacheError) as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    print(json.dumps(r, indent=2) if a.json else to_markdown(r))
    return 1 if r["high_flags"] else 0


if __name__ == "__main__":
    sys.exit(main())
BackendWeb DevelopmentSecurity+2
F@f
0
PostgreSQL taste
Văn bản

Các quy ước định kiến cho việc thiết kế lược đồ (schema), đánh chỉ mục (indexing), truy vấn và vận hành trong PostgreSQL.

# Phong cách (taste) sử dụng PostgreSQL của một kỹ sư Backend
- Sử dụng cột định danh kiểu bigint (GENERATED ALWAYS AS IDENTITY) cho khóa chính (primary key); chỉ dùng đến UUIDv7 khi định danh (id) bắt buộc phải được tạo bên ngoài cơ sở dữ liệu. Độ tin cậy: 0.80
- Lưu trữ dấu thời gian dưới dạng timestamptz theo múi giờ UTC, tuyệt đối không dùng timestamp without time zone. Độ tin cậy: 0.95
- Sử dụng kiểu text kết hợp với ràng buộc CHECK thay vì varchar(n) để giới hạn độ dài chuỗi. Độ tin cậy: 0.80
- Lưu trữ tiền tệ dưới dạng kiểu numeric hoặc kiểu số nguyên tính theo đơn vị tiền tệ nhỏ nhất (minor units), tuyệt đối không dùng kiểu số thực dấu phẩy động (float). Độ tin cậy: 0.95
- Đặt tên bảng ở dạng số nhiều theo quy tắc snake_case và đặt tên khóa ngoại (foreign key) theo định dạng <singular>_id. Độ tin cậy: 0.70
- Khai báo NOT NULL theo mặc định và chỉ cho phép NULL khi "chưa xác định" (unknown) là một trạng thái thực tế có thật. Độ tin cậy: 0.85
- Thực thi các quy tắc bất biến (invariants) ngay trong cơ sở dữ liệu bằng khóa ngoại (foreign keys), ràng buộc UNIQUE và CHECK, chứ không chỉ kiểm tra trong mã nguồn ứng dụng. Độ tin cậy: 0.90
- Đánh chỉ mục (index) cho mọi cột khóa ngoại được sử dụng trong các phép nối (join) hoặc xóa dây chuyền (cascading delete). Độ tin cậy: 0.85
- Đọc kết quả EXPLAIN (ANALYZE, BUFFERS) trước khi thêm một chỉ mục, và đọc lại lần nữa sau khi thêm để chứng minh rằng chỉ mục đó thực sự mang lại hiệu quả. Độ tin cậy: 0.90
- Ưu tiên chỉ mục từng phần (partial index) và chỉ mục biểu thức (expression index) hơn là các chỉ mục phức hợp (composite index) trải rộng đối với các bộ lọc có phân phối dữ liệu lệch (skewed filters). Độ tin cậy: 0.70
- Tạo chỉ mục trên các bảng lớn bằng câu lệnh CREATE INDEX CONCURRENTLY. Độ tin cậy: 0.90
- Sử dụng kiểu jsonb cho các thuộc tính thực sự không có lược đồ cố định (schemaless), nhưng hãy tách các trường mà bạn thường xuyên lọc ra thành những cột thực thụ. Độ tin cậy: 0.80
- Phân trang bằng phương pháp tập khóa keyset (WHERE id > $last ORDER BY id LIMIT n) thay vì dùng giá trị OFFSET lớn. Độ tin cậy: 0.85
- Giữ các giao dịch (transactions) diễn ra ngắn gọn và tuyệt đối không giữ một giao dịch mở trong suốt quá trình gọi mạng (network call). Độ tin cậy: 0.90
- Thiết lập tham số statement_timeout và idle_in_transaction_session_timeout cho từng vai trò (role) của ứng dụng. Độ tin cậy: 0.85
- Đặt một bộ điều phối kết nối (connection pooler) chẳng hạn như PgBouncer ở chế độ giao dịch (transaction mode) đứng trước cơ sở dữ liệu. Độ tin cậy: 0.80
- Sử dụng cú pháp SELECT ... FOR UPDATE SKIP LOCKED cho các hàng đợi công việc (job queues) đơn giản trước khi quyết định thêm một hệ thống message broker riêng biệt. Độ tin cậy: 0.70
- Sử dụng cú pháp INSERT ... ON CONFLICT cho các thao tác thêm mới hoặc cập nhật (upsert) có tính lũy đẳng (idempotent). Độ tin cậy: 0.85
- Cấp cho mỗi dịch vụ một vai trò (role) riêng với đặc quyền tối thiểu (least privilege); ứng dụng tuyệt đối không bao giờ kết nối dưới quyền siêu người dùng (superuser). Độ tin cậy: 0.90
- Bật tiện ích mở rộng pg_stat_statements và rà soát các truy vấn chậm nhất mỗi tuần. Độ tin cậy: 0.85
- Viết các biểu thức bảng chung (CTEs) để mã dễ đọc hơn, với lưu ý rằng Postgres 12+ sẽ tự động nội tuyến (inline) chúng trừ khi bạn thêm từ khóa MATERIALIZED. Độ tin cậy: 0.65
- Kiểm thử khả năng khôi phục tại một thời điểm (point-in-time recovery) từ các bản sao lưu một cách định kỳ; một bản sao lưu chưa từng được kiểm thử coi như không có giá trị. Độ tin cậy: 0.95
BackendBest PracticesSQL+2
F@f
0
API GraphQL taste
Văn bản

Các quy ước mang tính định hướng rõ rệt để thiết kế, phát triển mở rộng và vận hành các API GraphQL.

# Gu thiết kế API GraphQL của một kỹ sư nền tảng (platform engineer)
- Thiết kế lược đồ (schema) xoay quanh các ca sử dụng (use cases) của phía client và ngôn ngữ nghiệp vụ (domain language), chứ không xoay quanh các bảng cơ sở dữ liệu. Độ tin cậy: 0.90
- Tự động sinh kiểu dữ liệu (types) cho cả phía server và client từ schema (sử dụng GraphQL Code Generator hoặc công cụ tương đương); không bao giờ tự viết tay các kiểu dữ liệu phản hồi. Độ tin cậy: 0.90
- Chỉ đặt các trường (fields) là non-null theo mặc định khi server luôn có thể phân giải (resolve) được chúng; giữ cho các trường phụ thuộc vào dịch vụ khác ở trạng thái có thể null (nullable) để một lỗi đơn lẻ không làm mất trắng toàn bộ phản hồi. Độ tin cậy: 0.80
- Sử dụng một đối tượng đầu vào duy nhất cho mỗi thao tác biến đổi dữ liệu (mutation) (ví dụ: createOrder(input: CreateOrderInput!)) và trả về một kiểu payload chuyên biệt, không trả về trực tiếp thực thể gốc (bare entity). Độ tin cậy: 0.85
- Trả về các lỗi nghiệp vụ dự kiến dưới dạng các thành phần kiểu hợp (typed union members) trong payload (ví dụ: OrderCreated | InsufficientStock); dành riêng mảng errors ở cấp cao nhất cho các lỗi hệ thống bất ngờ. Độ tin cậy: 0.75
- Phân trang danh sách bằng kết nối con trỏ theo phong cách Relay (edges, node, pageInfo) và giới hạn tham số first/last ở một mức tối đa hợp lý. Độ tin cậy: 0.80
- Gom nhóm (batch) và lưu đệm việc tải dữ liệu theo từng yêu cầu bằng DataLoader để triệt tiêu hoàn toàn vấn đề truy vấn N+1. Độ tin cậy: 0.95
- Áp dụng giới hạn độ sâu (depth) và độ phức tạp (complexity) của truy vấn ngay tại cổng giao tiếp (gateway). Độ tin cậy: 0.90
- Sử dụng truy vấn cố định (persisted queries - danh sách cho phép các mã băm thao tác) cho các ứng dụng client nội bộ (first-party) trên môi trường production. Độ tin cậy: 0.80
- Tắt tính năng tự tra cứu lược đồ (introspection) trên các điểm cuối (endpoints) production công khai trừ khi API đó thực sự được thiết kế để công khai cho bên thứ ba. Độ tin cậy: 0.70
- Phát triển mở rộng schema theo hướng bổ sung thêm (additively) và đánh dấu các trường cũ bằng chỉ thị @deprecated(reason: ...); không bao giờ phân tách phiên bản endpoint bằng đường dẫn như /v2. Độ tin cậy: 0.90
- Chỉ xóa bỏ một trường đã đánh dấu @deprecated sau khi số liệu đo lường mức sử dụng trường đó cho thấy không còn lưu lượng truy cập nào trong suốt một chu kỳ phát hành đầy đủ. Độ tin cậy: 0.85
- Chạy kiểm tra so sánh sự khác biệt của schema (schema diff) trong quy trình CI và cho dừng quá trình build nếu phát hiện các thay đổi gây phá vỡ tương thích (breaking changes). Độ tin cậy: 0.90
- Sử dụng các định danh ẩn (opaque IDs) duy nhất trên toàn cục và triển khai giao diện Node để hỗ trợ truy xuất lại dữ liệu (refetching). Độ tin cậy: 0.70
- Giữ cho các hàm phân giải (resolvers) thật mỏng gọn: chúng chỉ gọi các dịch vụ miền nghiệp vụ (domain services), và việc phân quyền (authorization) phải nằm ở tầng dịch vụ đó chứ không nằm trong các câu lệnh if của resolver. Độ tin cậy: 0.85
- Đặt tên rõ ràng cho mọi thao tác (operations) trong từng truy vấn của client để dữ liệu truy vết (traces), nhật ký (logs) và số liệu đo lường (metrics) dễ đọc hiểu. Độ tin cậy: 0.90
- Đặt các mảnh truy vấn (fragments) nằm cùng chỗ với chính các thành phần giao diện (UI components) sử dụng chúng. Độ tin cậy: 0.80
- Ưu tiên các kiểu vô hướng tùy chỉnh (custom scalars như DateTime, URL, Email) có kiểm tra tính hợp lệ hơn là dùng kiểu String thuần túy. Độ tin cậy: 0.75
- Chỉ sử dụng cơ chế đăng ký nhận dữ liệu (subscriptions) cho dữ liệu thời gian thực thực sự; cơ chế hỏi vòng định kỳ (polling) là hoàn toàn ổn đối với bất kỳ dữ liệu nào chỉ thay đổi sau mỗi vài phút. Độ tin cậy: 0.70
- Chỉ sử dụng kiến trúc liên hợp (federation) khi có nhiều nhóm khác nhau cùng sở hữu các đồ thị con (subgraphs) riêng biệt; một nhóm duy nhất thì chỉ nên dùng một schema duy nhất. Độ tin cậy: 0.75
- Truy vết mọi resolver bằng OpenTelemetry và thiết lập cảnh báo cho các trường xử lý chậm, chứ không chỉ cảnh báo các yêu cầu (requests) chậm nói chung. Độ tin cậy: 0.80
- Viết tài liệu cho mọi kiểu dữ liệu và mọi trường bằng phần mô tả trực tiếp trong schema; bản thân schema chính là tài liệu tham chiếu API. Độ tin cậy: 0.85
BackendWeb DevelopmentBest Practices+2
F@f
0
Bộ Nhớ Đệm Redis taste
Văn bản

Các quy ước mang tính định hướng rõ rệt khi sử dụng Redis làm bộ nhớ đệm (cache): khóa (keys), thời gian sống (TTLs), cơ chế vô hiệu hóa (invalidation) và vận hành.

# Gu thiết kế Bộ nhớ đệm Redis (Redis Caching) của một kỹ sư backend
- Mặc định sử dụng mô hình cache-aside: đọc từ Redis, truy xuất lại từ nguồn dữ liệu gốc (source of truth) khi không tìm thấy trong cache (cache miss), sau đó nạp dữ liệu vào cache. Độ tin cậy: 0.90
- Coi bộ nhớ đệm là dữ liệu có thể loại bỏ bất cứ lúc nào; hệ thống vẫn phải hoạt động chính xác (chỉ chậm hơn) nếu toàn bộ dữ liệu Redis bị xóa sạch (flushed). Độ tin cậy: 0.95
- Thiết lập thời gian hết hạn (TTL) trên mọi khóa cache; một khóa không có thời hạn sử dụng chính là mầm mống gây rò rỉ bộ nhớ (memory leak). Độ tin cậy: 0.90
- Thêm độ lệch ngẫu nhiên (random jitter) vào các giá trị TTL để các khóa được ghi cùng lúc không bị hết hạn đồng loạt. Độ tin cậy: 0.85
- Đặt không gian tên (namespace) cho khóa theo định dạng service:entity:id:version (ví dụ: catalog:product:42:v3) và tăng số phiên bản khi có thay đổi định dạng gây phá vỡ tương thích. Độ tin cậy: 0.85
- Vô hiệu hóa cache bằng cách xóa khóa sau khi ghi dữ liệu thành công, thay vì cập nhật trực tiếp giá trị đang lưu trong cache. Độ tin cậy: 0.80
- Bảo vệ các khóa có tần suất truy cập cao (hot keys) khỏi hiện tượng quá tải đột ngột (cache stampede) bằng khóa ngắn hạn (SET key value NX PX) hoặc gộp nhóm yêu cầu (request coalescing). Độ tin cậy: 0.80
- Lưu vào cache cả các kết quả rỗng (không tìm thấy) với TTL ngắn để giảm tải các truy vấn lặp lại cho những ID không tồn tại. Độ tin cậy: 0.70
- Lưu trữ các giá trị đã tuần tự hóa (serialized) nhỏ gọn (dùng JSON để dễ gỡ lỗi, dùng MessagePack khi kích thước là yếu tố quan trọng) và giữ kích thước giá trị dưới khoảng 100 KB. Độ tin cậy: 0.75
- Sử dụng kiểu dữ liệu hash cho các đối tượng có từng trường (fields) được đọc riêng lẻ, và dùng chuỗi đơn thuần (plain strings) cho các giá trị được đọc nguyên khối. Độ tin cậy: 0.70
- Không bao giờ chạy lệnh KEYS trên môi trường thực tế (production); hãy sử dụng lệnh SCAN với con trỏ (cursor). Độ tin cậy: 0.95
- Tránh tạo các khóa quá lớn (big keys) và các tập hợp khổng lồ; hãy tách nhỏ bất kỳ dữ liệu nào có thể khiến một lệnh đơn lẻ bị chặn (block) tới vài mili-giây. Độ tin cậy: 0.85
- Sử dụng kỹ thuật pipelining hoặc lệnh MGET để đọc theo lô (batch reads) thay vì gửi từng yêu cầu mạng riêng biệt cho mỗi khóa. Độ tin cậy: 0.85
- Đưa các logic nguyên tử (atomic) gồm nhiều bước vào một kịch bản Lua (Lua script) hoặc một hàm, chứ không thực hiện kiểm tra rồi mới gán giá trị (check-then-set) ở phía client. Độ tin cậy: 0.80
- Thiết lập maxmemory kèm theo chính sách trục xuất dữ liệu (eviction policy) rõ ràng (allkeys-lru cho bộ nhớ đệm thuần túy, noeviction cho dữ liệu tuyệt đối không được mất). Độ tin cậy: 0.90
- Tách biệt bộ nhớ đệm và dữ liệu bền vững (hàng đợi, giới hạn tốc độ, phiên đăng nhập) vào các phiên bản (instances) Redis riêng với các chính sách riêng. Độ tin cậy: 0.80
- Sử dụng Redis Streams hoặc một hệ thống message broker thực thụ cho các tác vụ nền (jobs); kiểu danh sách (lists) đơn thuần sẽ làm mất tác vụ khi tiến trình xử lý (consumer) bị sập. Độ tin cậy: 0.70
- Triển khai giới hạn tần suất (rate limiting) bằng kịch bản cửa sổ trượt (sliding window) hoặc xô thẻ bài (token bucket), không dùng cách kết hợp lệnh INCR và EXPIRE tạm bợ dễ gây lỗi tranh chấp (race conditions). Độ tin cậy: 0.75
- Thiết lập thời gian chờ (timeouts) ở phía client và bộ ngắt mạch (circuit breaker) để khi Redis bị chậm thì hệ thống chỉ chuyển sang trạng thái cache miss chứ không làm sập toàn bộ dịch vụ. Độ tin cậy: 0.90
- Giám sát tỷ lệ trúng cache (hit ratio), số lượng khóa bị trục xuất (evictions), độ phân mảnh bộ nhớ và độ trễ, đồng thời cảnh báo khi tỷ lệ trúng cache sụt giảm. Độ tin cậy: 0.85
- Bắt buộc xác thực (auth) và mã hóa TLS, vô hiệu hóa hoặc đổi tên các lệnh nguy hiểm (FLUSHALL, CONFIG), và tuyệt đối không phơi bày Redis trực tiếp ra ngoài internet. Độ tin cậy: 0.95
- Đừng đưa vào cache những gì vốn đã nhanh; hãy đo lường độ trễ của nguồn dữ liệu gốc trước khi thêm một lớp bộ nhớ đệm. Độ tin cậy: 0.80
BackendDatabasePerformance+2
F@f
0
Kiến trúc Hướng sự kiện taste
Văn bản

Các quy ước mang tính định hướng rõ ràng để thiết kế các hệ thống hướng sự kiện và dựa trên thông điệp (message-based) đáng tin cậy.

# Kiến trúc Hướng sự kiện taste của một kỹ sư hệ thống phân tán
- Sử dụng sự kiện (events) để thông báo những sự việc đã xảy ra (OrderPlaced), và chỉ dùng lệnh (commands) cho các yêu cầu gửi tới một chủ sở hữu duy nhất (ChargeCard). Độ tin cậy: 0.90
- Đặt tên sự kiện ở thì quá khứ bằng ngôn ngữ nghiệp vụ (domain language), tuyệt đối không đặt theo tên bảng hoặc dịch vụ phát sinh ra nó. Độ tin cậy: 0.85
- Định nghĩa mọi sự kiện bằng một lược đồ có đánh phiên bản (Avro, Protobuf hoặc JSON Schema) trong một kho đăng ký (registry) và kiểm tra tính tương thích trong CI. Độ tin cậy: 0.90
- Chỉ phát triển lược đồ bằng các thay đổi tương thích ngược (backward-compatible); phát hành một loại sự kiện mới cho các thay đổi gây phá vỡ (breaking changes). Độ tin cậy: 0.85
- Giả định cơ chế chuyển phát ít nhất một lần (at-least-once delivery) và đảm bảo mọi consumer đều có tính lũy đẳng (idempotent) bằng một khóa khử trùng lặp (deduplication key). Độ tin cậy: 0.95
- Phát hành sự kiện thông qua mẫu hộp thư đi có giao dịch (transactional outbox pattern) để các thao tác ghi cơ sở dữ liệu và thông điệp không bao giờ bị lệch nhau. Độ tin cậy: 0.90
- Đưa đầy đủ event id, type, version, timestamp, correlation id và causation id vào trong mọi vỏ bọc thông điệp (envelope). Độ tin cậy: 0.85
- Giữ cho phần dữ liệu (payload) của sự kiện đủ độc lập cho các consumer phổ biến, nhưng không đổ toàn bộ các khối tổng hợp (aggregates) vào đó. Độ tin cậy: 0.75
- Phân vùng (partition) theo aggregate id khi thứ tự xử lý là quan trọng, và không bao giờ phụ thuộc vào thứ tự toàn cục (global ordering). Độ tin cậy: 0.85
- Định tuyến các thông điệp lỗi (poison messages) tới hàng đợi thư chết (dead-letter queue) sau một số lần thử lại có giới hạn kèm theo thời gian chờ tăng dần theo cấp số nhân (exponential backoff) và độ nhiễu ngẫu nhiên (jitter). Độ tin cậy: 0.90
- Cảnh báo dựa trên độ trễ của consumer (consumer lag) và độ sâu của hàng đợi thư chết, chứ không chỉ dựa vào tình trạng hoạt động của broker. Độ tin cậy: 0.90
- Lan truyền ngữ cảnh truy vết (trace context) thông qua các tiêu đề thông điệp (message headers) để có thể theo dõi một yêu cầu xuyên suốt qua nhiều dịch vụ. Độ tin cậy: 0.85
- Sử dụng mẫu saga với các hành động bù trừ (compensating actions) tường minh cho các quy trình đa dịch vụ thay vì dùng giao dịch phân tán (distributed transactions). Độ tin cậy: 0.80
- Ưu tiên cơ chế tự phối hợp (choreography) cho các luồng đơn giản và chỉ dùng bộ điều phối chuyên biệt (orchestrator) khi một quy trình có nhiều hơn một vài bước. Độ tin cậy: 0.70
- Chỉ sử dụng Event Sourcing ở những nơi mà việc lưu vết kiểm toán (audit trail) hoặc truy vấn theo thời gian (temporal queries) là yêu cầu thực sự. Độ tin cậy: 0.75
- Đảm bảo việc phát lại (replay) luôn an toàn: các consumer phải chịu được việc xử lý lại lịch sử sự kiện từ bất kỳ vị trí offset nào. Độ tin cậy: 0.80
- Thiết lập thời gian lưu trữ (retention) khớp với khoảng thời gian mà các consumer có thể cần để xây dựng lại trạng thái, và ghi tài liệu rõ ràng cho từng topic. Độ tin cậy: 0.70
- Giao mỗi topic cho một nhóm sở hữu duy nhất chịu trách nhiệm kiểm soát lược đồ và vòng đời của nó. Độ tin cậy: 0.85
- Giữ lại các lời gọi đồng bộ (synchronous calls) cho những truy vấn cần câu trả lời ngay lập tức; không ép buộc mô hình yêu cầu-phản hồi (request-response) phải đi qua hàng đợi. Độ tin cậy: 0.80
- Kiểm thử các consumer bằng kiểm thử hợp đồng (contract tests) đối chiếu với lược đồ đã đăng ký và kiểm thử các producer bằng kiểm thử tích hợp hộp thư đi (outbox integration tests). Độ tin cậy: 0.80
- Duy trì một danh mục sống (AsyncAPI hoặc tương tự) ghi nhận đầy đủ các topic, producer và consumer. Độ tin cậy: 0.75
BackendBest PracticesDevOps+2
F@f
0
Ứng dụng Web Django taste
Văn bản

Các quy ước định hướng rõ ràng để xây dựng và vận hành ứng dụng web Django trên môi trường thực tế (production).

# Ứng dụng Web Django taste của một kỹ sư backend Python
- Bắt đầu mọi dự án với một mô hình người dùng tùy chỉnh (AbstractUser) ngay cả khi chưa cần thêm trường dữ liệu mới nào. Độ tin cậy: 0.95
- Tách biệt cấu hình (settings) theo từng môi trường và đọc các khóa bí mật cũng như URL từ biến môi trường bằng django-environ hoặc thư viện tương tự. Độ tin cậy: 0.90
- Giữ cho các app nhỏ gọn và tập trung vào miền nghiệp vụ; một app phải import tới một nửa dự án là dấu hiệu cho thấy nó nên được tách nhỏ hoặc gộp lại. Độ tin cậy: 0.75
- Đặt logic nghiệp vụ trong các hàm dịch vụ (service functions) thuần túy hoặc phương thức của model, không đặt trong views, serializers hay signals. Độ tin cậy: 0.85
- Tránh sử dụng signals cho các luồng công việc cốt lõi; hãy gọi tường minh tác vụ phụ (side effect) để luồng xử lý dễ đọc và dễ kiểm thử. Độ tin cậy: 0.85
- Chủ động sử dụng select_related và prefetch_related, đồng thời kiểm tra số lượng truy vấn bằng django-debug-toolbar hoặc assertNumQueries. Độ tin cậy: 0.90
- Thêm các ràng buộc cơ sở dữ liệu (UniqueConstraint, CheckConstraint) thay vì chỉ dựa vào việc xác thực trên form hoặc serializer. Độ tin cậy: 0.90
- Xem xét kỹ mọi tệp migration được tạo ra trước khi commit và tuyệt đối không chỉnh sửa một migration đã chạy trên môi trường production. Độ tin cậy: 0.90
- Viết các data migration bằng RunPython sử dụng apps.get_model, tuyệt đối không import trực tiếp các model. Độ tin cậy: 0.85
- Ưu tiên sử dụng class-based generic views cho các thao tác CRUD và function views cho bất kỳ xử lý nào có luồng điều khiển đặc biệt. Độ tin cậy: 0.70
- Sử dụng Django REST Framework hoặc django-ninja cho các API, với serializers hoặc schemas được khai báo tường minh thay vì dùng fields = "__all__". Độ tin cậy: 0.85
- Bọc các thao tác ghi nhiều bước trong transaction.atomic và đưa các tác vụ chạy nền vào hàng đợi bằng transaction.on_commit. Độ tin cậy: 0.90
- Chạy các tác vụ chậm hoặc gọi dịch vụ bên ngoài trong Celery hoặc hàng đợi tác vụ khác, với các tác vụ có tính lũy đẳng (idempotent) và cơ chế thử lại (retry) rõ ràng. Độ tin cậy: 0.85
- Viết kiểm thử bằng pytest-django và factory_boy thay vì lưu các tệp fixture dưới dạng JSON trong kho mã nguồn. Độ tin cậy: 0.85
- Giữ trang quản trị (admin) cho các thao tác vận hành nội bộ; tùy chỉnh list_display, search_fields và list_select_related để trang luôn phản hồi nhanh. Độ tin cậy: 0.80
- Phục vụ các tệp tĩnh bằng WhiteNoise hoặc CDN và lưu trữ tệp do người dùng tải lên trên dịch vụ lưu trữ đối tượng (object storage), tuyệt đối không lưu trên ổ đĩa của máy chủ ứng dụng. Độ tin cậy: 0.85
- Chạy ứng dụng phía sau gunicorn hoặc uvicorn với DEBUG=False, thiết lập ALLOWED_HOSTS và đảm bảo lệnh python manage.py check --deploy vượt qua kiểm tra trong CI. Độ tin cậy: 0.95
- Sử dụng HTMX kết hợp với các template kết xuất phía máy chủ (server-rendered) trước khi nghĩ đến việc tách riêng một ứng dụng SPA. Độ tin cậy: 0.65
- Cố định phiên bản các thư viện phụ thuộc bằng tệp lock và nâng cấp các bản phát hành Django LTS theo lịch trình định kỳ. Độ tin cậy: 0.80
- Kiểm tra lỗi cú pháp (lint) và định dạng mã bằng Ruff, đồng thời kiểm tra kiểu dữ liệu bằng mypy kết hợp với django-stubs. Độ tin cậy: 0.75
BackendWeb DevelopmentBest Practices+1
F@f
0
Dịch Vụ FastAPI taste
Văn bản

Các quy ước mang tính định hướng rõ ràng để xây dựng và vận hành các dịch vụ FastAPI trên môi trường production bằng Python.

# Dịch Vụ FastAPI taste của một kỹ sư API Python
- Định nghĩa phần thân của yêu cầu (request) và phản hồi (response) bằng các mô hình Pydantic v2 và luôn thiết lập response_model để không vô tình làm rò rỉ dữ liệu ngoài ý muốn. Độ tin cậy: 0.95
- Tách riêng các lược đồ (schemas) cho các thao tác tạo mới (create), cập nhật (update) và đọc (read) thay vì tái sử dụng mô hình ORM làm cấu trúc dữ liệu trả về của API. Độ tin cậy: 0.90
- Chỉ sử dụng các endpoint khai báo với async def khi mọi lệnh gọi bên trong đều thực sự là bất đồng bộ (async); nếu không, hãy viết bằng def thông thường và để threadpool tự xử lý. Độ tin cậy: 0.85
- Tiêm (inject) các phiên kết nối cơ sở dữ liệu (database sessions), cài đặt cấu hình và người dùng hiện tại thông qua Depends, tuyệt đối không dùng các biến toàn cục ở cấp độ module. Độ tin cậy: 0.90
- Tải cấu hình bằng pydantic-settings từ các biến môi trường và cho dừng ứng dụng báo lỗi ngay lúc khởi động (fail fast) nếu thiếu bất kỳ thông tin cấu hình nào. Độ tin cậy: 0.90
- Chia nhỏ các tuyến đường (routes) thành các module APIRouter theo từng miền nghiệp vụ (domain) và giữ cho tệp main.py chỉ giới hạn ở việc khởi tạo ứng dụng và kết nối các thành phần lại với nhau. Độ tin cậy: 0.85
- Đặt logic nghiệp vụ vào trong các hàm dịch vụ (service functions) hoàn toàn tách biệt và không biết gì về giao thức HTTP, để có thể tái sử dụng chúng từ các tiến trình chạy ngầm (workers) và trong kiểm thử (tests). Độ tin cậy: 0.85
- Sử dụng cú pháp SQLAlchemy 2.0 với các phiên bất đồng bộ (async sessions) và quản lý lược đồ cơ sở dữ liệu bằng các bản di chuyển (migrations) của Alembic. Độ tin cậy: 0.80
- Sử dụng trình quản lý ngữ cảnh lifespan cho quá trình khởi động (startup) và tắt ứng dụng (shutdown) thay vì các hook on_event đã lỗi thời. Độ tin cậy: 0.85
- Chỉ phát sinh ngoại lệ HTTPException ở tầng giao tiếp ngoài cùng và ánh xạ các lỗi nghiệp vụ (domain errors) sang mã trạng thái HTTP bằng các trình xử lý ngoại lệ (exception handlers) đã đăng ký. Độ tin cậy: 0.80
- Trả về cấu trúc lỗi nhất quán gồm mã lỗi máy có thể đọc được (machine-readable code), thông báo lỗi và thông tin chi tiết tùy chọn. Độ tin cậy: 0.85
- Đánh phiên bản cho API ngay trên đường dẫn (/v1) và coi lược đồ OpenAPI được tạo ra như một bản hợp đồng giao tiếp được kiểm tra tự động trong quy trình CI. Độ tin cậy: 0.75
- Sử dụng phân trang dựa trên con trỏ (cursor-based pagination) cho các danh sách lớn và giới hạn kích thước trang tối đa ở phía máy chủ. Độ tin cậy: 0.80
- Kiểm thử bằng pytest và httpx.AsyncClient, ghi đè các phụ thuộc (overriding dependencies) thay vì dùng kỹ thuật monkey-patch vào các thành phần nội bộ. Độ tin cậy: 0.90
- Chạy các tiến trình con (workers) của uvicorn phía sau gunicorn hoặc một trình quản lý tiến trình trên môi trường production, và chỉ dùng cờ --reload trong môi trường phát triển (development). Độ tin cậy: 0.80
- Chuyển các tác vụ xử lý chậm sang một hàng đợi tác vụ (task queue) thực thụ như Celery, Arq hoặc Dramatiq; BackgroundTasks chỉ dành cho các công việc nhỏ chạy một lần rồi thôi (fire-and-forget). Độ tin cậy: 0.85
- Thêm ID yêu cầu (request IDs), nhật ký định dạng JSON có cấu trúc và middleware truy vết OpenTelemetry ngay từ ngày đầu tiên. Độ tin cậy: 0.80
- Bảo vệ các endpoint bằng OAuth2 bearer token hoặc khóa API thông qua các phụ thuộc bảo mật (security dependencies) để trang tài liệu tự động hiển thị cơ chế xác thực. Độ tin cậy: 0.80
- Cung cấp riêng biệt hai endpoint /healthz và /readyz, trong đó bước kiểm tra mức độ sẵn sàng (readiness) sẽ kiểm tra kết nối cơ sở dữ liệu và các phụ thuộc cứng khác. Độ tin cậy: 0.85
- Quản lý các thư viện phụ thuộc bằng uv kèm theo tệp khóa (lock file), đồng thời kiểm tra lỗi cú pháp (lint) và định dạng mã nguồn bằng Ruff. Độ tin cậy: 0.75
- Kiểm tra kiểu dữ liệu (type check) bằng mypy hoặc pyright ở chế độ nghiêm ngặt (strict mode) trên gói dịch vụ. Độ tin cậy: 0.70
BackendBest PracticesAPI+2
F@f
0
Tác vụ Nền (Background Jobs) taste
Văn bản

Các quy ước mang tính định hướng rõ ràng để xây dựng hàng đợi tác vụ nền (background job queues) và các worker đáng tin cậy.

# Tác vụ Nền (Background Jobs) taste của một kỹ sư độ tin cậy backend
- Thiết kế mọi tác vụ (job) có tính lũy đẳng (idempotent) để việc thử lại (retry) hoặc gửi trùng lặp không bao giờ trừ tiền thẻ hay gửi email hai lần. Confidence: 0.95
- Chỉ truyền các ID trong phần dữ liệu (payload) của tác vụ và tải dữ liệu mới nhất bên trong worker thay vì tuần tự hóa (serialize) toàn bộ bản ghi. Confidence: 0.90
- Đưa các tác vụ vào hàng đợi (enqueue) trong cùng một giao dịch cơ sở dữ liệu (database transaction) với thao tác ghi đã kích hoạt chúng, bằng cách sử dụng bảng hộp thư đi (outbox table) hoặc hàng đợi dựa trên Postgres. Confidence: 0.85
- Ưu tiên sử dụng một hệ thống hàng đợi đơn giản, ổn định và được hỗ trợ tốt như Sidekiq, BullMQ, Celery hoặc hàng đợi Postgres như Graphile Worker thay vì tự xây dựng mới. Confidence: 0.80
- Thử lại với thời gian chờ tăng theo cấp số nhân (exponential backoff) kèm độ lệch ngẫu nhiên (jitter), và giới hạn số lần thử để các tác vụ lỗi độc hại (poison jobs) được chuyển vào hàng đợi thư chết (dead letter queue). Confidence: 0.95
- Phân loại lỗi thành lỗi có thể thử lại (retryable) hoặc lỗi vĩnh viễn (permanent), và cho dừng ngay lập tức các lỗi vĩnh viễn thay vì lãng phí số lần thử lại. Confidence: 0.85
- Thiết lập thời gian chờ tối đa (timeout) rõ ràng cho mọi tác vụ và cho mọi lệnh gọi mạng mà tác vụ đó thực hiện. Confidence: 0.90
- Giữ cho các tác vụ nhỏ gọn và chạy nhanh, đồng thời chia nhỏ các công việc xử lý hàng loạt (batch work) kéo dài thành từng phần (chunks) có thể tiếp tục chạy lại từ điểm đánh dấu (checkpoint). Confidence: 0.85
- Sử dụng các hàng đợi riêng biệt cho công việc nhạy cảm với độ trễ và công việc xử lý số lượng lớn (bulk work) để tác vụ nạp bù dữ liệu (backfill) không bao giờ làm chậm email đặt lại mật khẩu. Confidence: 0.90
- Giới hạn tần suất (rate limit) các tác vụ gọi API bên thứ ba theo từng nhà cung cấp dịch vụ, chứ không phải theo từng tiến trình worker. Confidence: 0.80
- Sử dụng khóa tác vụ duy nhất (unique job keys) hoặc cơ chế khóa (locks) để ngăn cùng một tác vụ định kỳ chạy hai lần cùng lúc. Confidence: 0.85
- Chạy các lịch trình kiểu cron từ một bộ lập lịch (scheduler) duy nhất có cơ chế bầu chọn nút dẫn đầu (leader election), tuyệt đối không chạy từ mọi thực thể (instance) của ứng dụng. Confidence: 0.80
- Đánh phiên bản (version) cho dữ liệu đầu vào (payload) của tác vụ và đảm bảo các worker vẫn đọc được phiên bản cũ trong quá trình triển khai (deploy) bản mới. Confidence: 0.80
- Xử lý tín hiệu SIGTERM một cách êm đẹp (gracefully): ngừng nhận tác vụ mới, hoàn tất hoặc đưa lại tác vụ hiện tại vào hàng đợi, sau đó mới thoát. Confidence: 0.90
- Ghi nhận mã tác vụ (job ID), số thứ tự lần thử (attempt number) và mã tương quan (correlation ID) trên mọi dòng nhật ký (log) phát sinh từ một tác vụ. Confidence: 0.85
- Cảnh báo dựa trên độ sâu của hàng đợi, tuổi của tác vụ cũ nhất và mức tăng trưởng của hàng đợi thư chết (dead letter queue), chứ không chỉ cảnh báo khi worker bị sập. Confidence: 0.90
- Phân công người phụ trách rõ ràng cho hàng đợi thư chết và có tài liệu hướng dẫn lệnh chạy lại (replay command). Confidence: 0.75
- Kiểm thử các tác vụ bằng cách gọi trực tiếp hàm xử lý (handler) trong kiểm thử đơn vị (unit tests) và chạy một bài kiểm thử đầu-cuối (end-to-end) thông qua hàng đợi thật trong hệ thống CI. Confidence: 0.80
- Tránh nối chuỗi quá nhiều tác vụ thành các luồng công việc ngầm phức tạp; hãy sử dụng một công cụ điều phối luồng công việc (workflow engine) như Temporal khi các bước cần được điều phối chặt chẽ. Confidence: 0.70
- Lưu trữ kết quả tác vụ và các dấu hiệu đánh dấu hiệu ứng phụ (side-effect markers) trong cơ sở dữ liệu, chứ không chỉ lưu trong hệ thống lưu trữ của hàng đợi. Confidence: 0.75
BackendBest PracticesDevOps+2
F@f
0
Hàm Serverless taste
Văn bản

Các quy ước giàu tính định hướng để xây dựng những hàm serverless đáng tin cậy, tối ưu chi phí trên đám mây.

# Gu thiết kế (taste) Hàm Serverless của một kỹ sư backend đám mây
- Giữ cho mỗi hàm nhỏ gọn và tập trung vào một tác nhân kích hoạt (trigger) duy nhất, nhưng hãy gom các route HTTP có liên quan mật thiết vào chung một handler để cắt giảm tình trạng khởi động lạnh (cold starts). Độ tin cậy: 0.80
- Khởi tạo các SDK client và nhóm kết nối cơ sở dữ liệu (database pools) bên ngoài hàm handler để các lần gọi ấm (warm invocations) có thể tái sử dụng chúng. Độ tin cậy: 0.95
- Sử dụng bộ quản lý nhóm kết nối (connection pooler) hoặc trình điều khiển cơ sở dữ liệu dựa trên HTTP, vì hàng nghìn hàm chạy đồng thời sẽ làm cạn kiệt các kết nối Postgres trực tiếp. Độ tin cậy: 0.90
- Thiết lập thời gian chờ (timeout) và dung lượng bộ nhớ tường minh cho từng hàm thay vì chấp nhận các giá trị mặc định của nền tảng. Độ tin cậy: 0.85
- Giữ kích thước gói mã (bundle) nhỏ gọn bằng kỹ thuật loại bỏ mã thừa (tree-shaking) và nạp trễ (lazy imports), đồng thời đo lường thời gian khởi động lạnh sau mỗi thay đổi thư viện phụ thuộc lớn. Độ tin cậy: 0.85
- Đảm bảo mọi hàm xử lý sự kiện đều có tính lũy đẳng (idempotent), bởi vì các hàng đợi và nguồn phát sự kiện luôn phân phối theo cơ chế ít nhất một lần (at-least-once). Độ tin cậy: 0.95
- Chuyển các tác vụ chạy chậm hoặc cần thử lại sang hàng đợi (queue) và trả kết quả về nhanh chóng từ các hàm HTTP. Độ tin cậy: 0.90
- Đẩy các sự kiện bất đồng bộ bị lỗi vào hàng đợi thư chết (dead letter queue - DLQ) kèm theo cảnh báo dựa trên độ sâu (số lượng tồn đọng) của hàng đợi đó. Độ tin cậy: 0.90
- Định nghĩa các hàm, quyền hạn và tác nhân kích hoạt dưới dạng mã (Infrastructure as Code) bằng SST, Serverless Framework, SAM hoặc Terraform, tuyệt đối không bấm chọn thủ công trên giao diện điều khiển (console). Độ tin cậy: 0.90
- Cấp cho mỗi hàm một vai trò (role) riêng theo nguyên tắc đặc quyền tối thiểu, giới hạn phạm vi chính xác vào những tài nguyên mà hàm đó chạm tới. Độ tin cậy: 0.95
- Nạp các thông tin bí mật (secrets) từ trình quản lý bí mật (secrets manager) hoặc biến môi trường được mã hóa của nền tảng, tuyệt đối không lấy từ kho mã nguồn (repo). Độ tin cậy: 0.95
- Ghi nhật ký (logs) định dạng JSON có cấu trúc kèm theo request ID và correlation ID trên mọi dòng. Độ tin cậy: 0.90
- Truy vết (trace) các yêu cầu xuyên qua các hàm và hàng đợi bằng OpenTelemetry hoặc công cụ truy vết có sẵn của nền tảng. Độ tin cậy: 0.80
- Giữ logic nghiệp vụ trong các module thuần túy và chỉ để hàm handler đóng vai trò một lớp chuyển đổi (adapter) mỏng, nhờ đó có thể kiểm thử đơn vị (unit test) mà không cần tới môi trường đám mây. Độ tin cậy: 0.90
- Chạy kiểm thử tích hợp (integration tests) trên một môi trường triển khai (stage) thực tế cho mỗi pull request thay vì đặt niềm tin vào các trình giả lập cục bộ. Độ tin cậy: 0.80
- Thiết lập mức độ đồng thời dành riêng hoặc tối đa (reserved or maximum concurrency) trên các hàm gọi tới những dịch vụ hạ nguồn mỏng manh, dễ quá tải. Độ tin cậy: 0.85
- Chỉ sử dụng mức độ đồng thời được cấp phát sẵn (provisioned concurrency) cho các luồng xử lý nhạy cảm về độ trễ nơi mà quá trình khởi động lạnh đã được đo đạc và chứng minh là gây ảnh hưởng xấu. Độ tin cậy: 0.75
- Ưu tiên môi trường thực thi arm64 khi các thư viện phụ thuộc hỗ trợ để đạt tỷ lệ hiệu năng trên giá thành tốt hơn. Độ tin cậy: 0.70
- Thiết lập cảnh báo hóa đơn thanh toán và bảng theo dõi chi phí theo từng hàm trước khi ra mắt, chứ không phải đợi đến sau tờ hóa đơn gây sốc đầu tiên. Độ tin cậy: 0.85
- Tránh nối chuỗi các hàm gọi nhau một cách đồng bộ; thay vào đó hãy dùng sự kiện hoặc một bộ điều phối luồng công việc (workflow engine) như Step Functions. Độ tin cậy: 0.85
- Lựa chọn container hoặc các dịch vụ chạy liên tục dài hạn cho các khối lượng công việc có thông lượng cao và ổn định, nơi mà mô hình serverless trở nên đắt đỏ hơn. Độ tin cậy: 0.75
BackendBest PracticesDevOps+1
F@f
0
WebSocket Thời gian thực taste
Văn bản

Các quy ước giàu tính định hướng để xây dựng những tính năng thời gian thực bảo mật, có khả năng mở rộng bằng WebSockets.

# Gu thiết kế (taste) WebSocket Thời gian thực của một kỹ sư hệ thống thời gian thực
- Sử dụng WebSockets thuần hoặc một thư viện mỏng nhẹ như ws cho các trường hợp đơn giản, và chỉ tìm đến Socket.IO khi bạn thực sự cần cơ chế dự phòng (fallbacks) và quản lý phòng (rooms) của nó. Độ tin cậy: 0.75
- Ưu tiên dùng Server-Sent Events (SSE) khi dữ liệu cập nhật chỉ chảy một chiều từ máy chủ xuống máy khách. Độ tin cậy: 0.85
- Xác thực ngay trong quá trình bắt tay (handshake) bằng một token có thời hạn ngắn, tuyệt đối không bao giờ tin tưởng vào user ID được gửi kèm trong một bản tin sau đó. Độ tin cậy: 0.95
- Kiểm tra header Origin trên các yêu cầu nâng cấp kết nối (upgrade requests) để chặn tấn công chiếm quyền điều khiển WebSocket liên trang (cross-site WebSocket hijacking). Độ tin cậy: 0.90
- Định nghĩa mọi bản tin dưới dạng một gói bọc có kiểu tường minh (typed envelope) gồm loại bản tin (type), phiên bản (version) và dữ liệu tải (payload), được xác thực bằng schema ở cả hai đầu. Độ tin cậy: 0.90
- Gửi tín hiệu nhịp tim (heartbeats) bằng ping và pong và đóng các kết nối bỏ lỡ chúng, vì các máy chủ proxy thường âm thầm ngắt những socket nhàn rỗi. Độ tin cậy: 0.90
- Kết nối lại ở phía máy khách theo cơ chế lùi lũy thừa kèm độ lệch ngẫu nhiên (exponential backoff and jitter) để việc khởi động lại máy chủ không gây ra hiệu ứng đàn thú chạy loạn (thundering herd). Độ tin cậy: 0.95
- Gắn cho mỗi bản tin một số thứ tự (sequence number) để máy khách có thể tiếp tục nhận từ bản tin cuối cùng mà chúng nhìn thấy sau khi kết nối lại. Độ tin cậy: 0.85
- Coi socket như một kênh thông báo và giữ cơ sở dữ liệu làm nguồn chân lý chuẩn xác (source of truth) mà máy khách có thể truy vấn lại. Độ tin cậy: 0.85
- Phân phối bản tin (fan out) qua nhiều thực thể máy chủ bằng Redis pub/sub, NATS hoặc một dịch vụ được quản lý thay vì phát quảng bá trong bộ nhớ (in-memory broadcast). Độ tin cậy: 0.90
- Giữ cho các máy chủ kết nối ở trạng thái phi trạng thái (stateless) ngoại trừ các socket đang mở, để bất kỳ thực thể nào cũng có thể được rút cạn kết nối (drain) và thay thế. Độ tin cậy: 0.80
- Rút cạn kết nối một cách êm thấm (drain gracefully) khi triển khai bản mới bằng cách báo cho máy khách chủ động kết nối lại trước khi tắt máy chủ. Độ tin cậy: 0.85
- Áp dụng cơ chế chống tràn ngược (backpressure): theo dõi bộ đệm gửi (send buffer) và loại bỏ hoặc gộp các bản cập nhật đối với những máy khách xử lý chậm thay vì tích trữ vào bộ đệm mãi mãi. Độ tin cậy: 0.85
- Giới hạn tần suất (rate limit) các bản tin gửi đến trên mỗi kết nối và giới hạn kích thước tối đa của bản tin. Độ tin cậy: 0.90
- Phân quyền cho mọi yêu cầu đăng ký (subscription) vào một phòng hoặc kênh ở phía máy chủ, chứ không chỉ kiểm tra ở lần kết nối ban đầu. Độ tin cậy: 0.95
- Gom nhóm (batch) hoặc tiết lưu (throttle) các cập nhật tần suất cao như vị trí con trỏ chuột theo một nhịp xung (tick rate) cố định. Độ tin cậy: 0.80
- Cấu hình thời gian chờ nhàn rỗi (idle timeouts) của bộ cân bằng tải và phiên kết dính (sticky sessions) một cách có chủ đích khi tầng truyền tải cần đến chúng. Độ tin cậy: 0.80
- Theo dõi số lượng kết nối đang mở, số bản tin mỗi giây, tỷ lệ kết nối lại và độ trễ chuyển phát như những chỉ số đo lường (metrics) hạng nhất. Độ tin cậy: 0.85
- Sử dụng các cấu trúc CRDT như Yjs hoặc Automerge cho tính năng chỉnh sửa cộng tác thay vì tự phát minh ra cơ chế giải quyết xung đột của riêng bạn. Độ tin cậy: 0.80
- Kiểm thử tải (load test) với số lượng kết nối sát thực tế bằng k6 hoặc Artillery trước khi ra mắt. Độ tin cậy: 0.80
BackendBest PracticesWebSocket+2
F@f
0
Trình Đánh Giá Thiết Kế Lược Đồ SQL (SQL Schema Design Reviewer)
Kỹ năng

Đánh giá các bảng PostgreSQL và MySQL (tệp schema.sql, bản dump hoặc lệnh CREATE TABLE trong pull request) để tìm ra các lỗi thiết kế có thể sửa dễ dàng và ít tốn kém trước khi có dữ liệu thực tế - thiếu khóa chính, lưu tiền tệ bằng cột số thực (float), khóa ngoại lệch kiểu dữ liệu hoặc thiếu chỉ mục (index), ID 32-bit, dấu thời gian (timestamp) không kèm múi giờ, các cờ (flags) cho phép NULL, trạng thái dạng văn bản tự do, đặt tên trùng từ khóa dành riêng và email duy nhất (unique) nhưng phân biệt chữ hoa chữ thường. Trả về mã DDL đã được hiệu chỉnh. Bao gồm một công cụ linter viết bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.

---
name: sql-schema-design-reviewer
description: Đánh giá các bảng PostgreSQL và MySQL (tệp schema.sql, bản dump hoặc lệnh CREATE TABLE trong pull request) để tìm ra các lỗi thiết kế có thể sửa dễ dàng và ít tốn kém trước khi có dữ liệu thực tế - thiếu khóa chính, lưu tiền tệ bằng cột số thực (float), khóa ngoại lệch kiểu dữ liệu hoặc thiếu chỉ mục (index), ID 32-bit, dấu thời gian (timestamp) không kèm múi giờ, các cờ (flags) cho phép NULL, trạng thái dạng văn bản tự do, đặt tên trùng từ khóa dành riêng và email duy nhất (unique) nhưng phân biệt chữ hoa chữ thường. Trả về mã DDL đã được hiệu chỉnh. Bao gồm một công cụ linter viết bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.
---

# Trình Đánh Giá Thiết Kế Lược Đồ SQL (SQL Schema Design Reviewer)

Bạn đánh giá các lược đồ cơ sở dữ liệu (database schemas) theo đúng cách mà một chuyên gia mô hình hóa dữ liệu giàu kinh nghiệm sẽ làm ở giai đoạn thiết kế: ngay trước khi dòng dữ liệu đầu tiên được ghi vào, khi việc thay đổi một kiểu dữ liệu hay một khóa chỉ là sửa một dòng mã thay vì phải chạy migration vất vả suốt cả dịp cuối tuần. Bạn xem xét cấu trúc hình thái của dữ liệu (khóa, kiểu dữ liệu, ràng buộc, cách đặt tên, chỉ mục), giải thích rõ sự cố thực tế mà từng vấn đề sẽ gây ra, và trả lại mã DDL đã được sửa chuẩn chỉnh.

Kỹ năng này tập trung đánh giá thiết kế của các bảng. Nó không bàn về việc triển khai thay đổi lên một cơ sở dữ liệu đang chạy thực tế (khóa bảng, điền dữ liệu ngược - backfill, triển khai mở rộng/thu hẹp - expand/contract); đối với nhu cầu đó, hãy sử dụng quy trình đánh giá độ an toàn của migration.

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

- `scripts/schema_lint.py` - phân tích cú pháp các câu lệnh `CREATE TABLE`, `CREATE INDEX` và `ALTER TABLE ... ADD`, đồng thời báo cáo các vấn đề về khóa, kiểu dữ liệu, ràng buộc, cách đặt tên và chỉ mục kèm theo mức độ nghiêm trọng và cách khắc phục; tự động nhận diện PostgreSQL hoặc MySQL (chỉ sử dụng thư viện chuẩn Python 3, không cần kết nối cơ sở dữ liệu)
- `references/design-rules.md` - toàn bộ mã lỗi phát hiện (finding codes) kèm theo sự cố mà nó gây ra, trường hợp nào có thể chấp nhận giữ nguyên và cách khắc phục
- `references/type-choices.md` - hướng dẫn chọn kiểu dữ liệu nào cho ID, tiền tệ, thời gian, văn bản, cờ (flags), trạng thái và JSON trong PostgreSQL và MySQL
- `templates/schema-review.md` - mẫu báo cáo đánh giá để bàn giao lại
- `examples/shop-schema.sql` - một lược đồ cửa hàng trực tuyến nhỏ chứa các lỗi thiết kế được cài cắm có chủ đích
- `examples/shop-schema-fixed.sql` - cùng lược đồ đó sau khi được đánh giá và sửa lỗi; vượt qua kiểm tra lint hoàn toàn sạch sẽ

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

### 1. Thu thập lược đồ và ngữ cảnh
Yêu cầu cung cấp mã DDL (`schema.sql`, `pg_dump --schema-only`, `mysqldump --no-data`, hoặc câu lệnh `CREATE TABLE` từ pull request), hệ quản trị cơ sở dữ liệu (engine) và phiên bản, số lượng dòng dự kiến của mỗi bảng trong một năm, và cách dữ liệu được sử dụng (ứng dụng OLTP, báo cáo, đa tổ chức - multi-tenant, sao chép dữ liệu - replicated, hay truy xuất thông qua ORM). Hãy hỏi xem nhóm đã có sẵn những quy tắc nội bộ nào chưa (dùng UUID hay `bigint` cho ID, xóa mềm - soft deletes, quy tắc đặt tên).

### 2. Chạy công cụ linter
```
python3 scripts/schema_lint.py examples/shop-schema.sql
python3 scripts/schema_lint.py schema.sql --dialect mysql --min-severity MEDIUM
python3 scripts/schema_lint.py db/*.sql --ignore NO_CREATED_AT --json > schema-review.json
python3 scripts/schema_lint.py --self-test
```
Mã thoát (exit code) 0 có nghĩa là không có phát hiện mức HIGH nào, 1 có nghĩa là có ít nhất một lỗi mức HIGH (dùng làm cổng chặn CI trên các tệp schema), 2 có nghĩa là dữ liệu đầu vào không hợp lệ. Công cụ linter hoạt động theo hướng suy nghiệm (heuristic): nó đọc tên cột (ví dụ cột `price` dùng kiểu float, cột `order_id` không có khóa ngoại), vì vậy hãy đối chiếu xác nhận từng phát hiện với ý đồ thực sự của người dùng.

### 3. Đánh giá những điểm mà script không thể nhìn thấy
Sau đó, hãy tự đọc kỹ lược đồ (xem thêm `references/design-rules.md`):
- Mỗi bảng có mang một ý nghĩa rõ ràng duy nhất và có một khóa tự nhiên duy nhất (natural unique key) bên cạnh ID tự tăng (surrogate id) hay không (ví dụ: `sku`, `email`, `provider + provider_id`)?
- Các mối quan hệ đã chuẩn xác chưa: quan hệ một-nhiều (one-to-many) so với nhiều-nhiều (many-to-many), hành vi `ON DELETE` (`RESTRICT` làm mặc định; chỉ dùng `CASCADE` cho các bảng con phụ thuộc hoàn toàn), khóa ngoại chỉ cho phép `NULL` ở những nơi mối quan hệ thực sự là tùy chọn (optional)?
- Các bất biến dữ liệu (invariants) có được đặt trong ràng buộc `CHECK` (`quantity > 0`, `ends_at > starts_at`) thay vì chỉ kiểm tra trong mã nguồn ứng dụng hay không?
- Đa tổ chức (Multi-tenant): mọi bảng thuộc sở hữu của tenant có mang cột `tenant_id` không, cột đó có nằm trong các khóa duy nhất (unique keys) và đứng ở vị trí cột đầu tiên của chỉ mục (leading index column) không?
- Xóa mềm (Soft deletes): các ràng buộc duy nhất có phải là ràng buộc cục bộ có điều kiện (`WHERE deleted_at IS NULL`) không?
- Dữ liệu cá nhân: những cột nào chứa thông tin định danh cá nhân (PII), và chúng có cần quy tắc kiểm soát truy cập hoặc thời hạn lưu trữ riêng biệt không?

### 4. Bàn giao kết quả
Điền vào mẫu `templates/schema-review.md`: kết luận tổng quan (verdict), các phát hiện được xếp hạng theo mức độ ảnh hưởng kèm theo sự cố thực tế mà từng lỗi gây ra, tiếp theo là mã DDL đã được hiệu chỉnh (xem `examples/shop-schema-fixed.sql`) và danh sách các quyết định dành quyền tự chọn cho nhóm. Nếu các bảng đã chứa dữ liệu thực tế, hãy chỉ rõ những sửa đổi nào hiện cần viết kịch bản di chuyển dữ liệu (migration) và gắn cờ chuyển sang bước đánh giá an toàn migration thay vì viết lại trực tiếp các bảng đang chạy.

## Ví dụ kết quả
`examples/shop-schema.sql` (PostgreSQL, 5 bảng): 20 phát hiện, 4 lỗi mức HIGH. Bảng `order_items` không có khóa chính (có thể bị trùng lặp dòng); `products.price` dùng kiểu `double precision` và `payments.amount` dùng kiểu `real` (gây sai lệch từng xu khi tính toán); `payments.order_id` là `integer` nhưng lại tham chiếu tới `"order".id` (`bigserial`). Mức MEDIUM: dùng ID kiểu `serial` trên bảng `customers` và `products`, dùng `timestamp without time zone` trên `customers.created_at` và `placed_at`, đặt tên bảng là `"order"` (trùng từ khóa dành riêng của SQL), khóa ngoại trên `"order".customer_id` và `order_items.order_id` không có chỉ mục (index). Mức LOW: email duy nhất (unique) nhưng phân biệt chữ hoa chữ thường, thói quen dùng `varchar(255)`, cột cờ `is_verified` cho phép NULL, cột `status` lưu văn bản tự do, `product_id` không có ràng buộc khóa ngoại, dư thừa một chỉ mục trên bảng `payments`, lưu dữ liệu JSON trong cột kiểu `text`. Tệp `examples/shop-schema-fixed.sql` khắc phục toàn bộ các lỗi trên (đổi tên thành `orders`, dùng `bigint identity`, `numeric(12,2)`, `timestamptz`, `citext`, ràng buộc `CHECK` trên cột `status`, khóa chính tổ hợp trên `order_items`) và vượt qua kiểm tra lint hoàn toàn sạch sẽ.

## Quy tắc

- Giải thích từng phát hiện dưới góc độ sự cố thực tế mà nó gây ra (sai lệch tổng tiền, bản ghi mồ côi, đăng ký trùng lặp tài khoản, sập hệ thống khi chạm ngưỡng 2,1 tỷ ID), chứ không phải như một quy tắc bắt bẻ về mặt hình thức.
- Giữ nguyên các quy ước của người dùng (dùng UUID làm ID, quy tắc đặt tên) trừ khi chúng gây ra vấn đề thực sự; hãy nói rõ khi nào một phát hiện chỉ mang tính chất cân nhắc đánh đổi (judgement call).
- Ưu tiên đặt các ràng buộc (constraints) ngay trong cơ sở dữ liệu hơn là chỉ kiểm tra trong tầng ứng dụng; cơ sở dữ liệu là tuyến phòng thủ cuối cùng.
- Tiền tệ phải chính xác tuyệt đối: dùng `numeric`/`DECIMAL` hoặc số nguyên biểu thị đơn vị tiền tệ nhỏ nhất (như xu/cents), không bao giờ dùng `float`, và phải lưu kèm loại tiền tệ (currency).
- Thời gian là một thời điểm xác định (instant): dùng `timestamptz` trong PostgreSQL, và lưu theo giờ chuẩn UTC ở mọi nơi trong MySQL.
- Không đề xuất sửa đổi trực tiếp các bảng đã chứa dữ liệu trên môi trường production mà không chỉ rõ rằng mỗi thay đổi đó đều đòi hỏi một quy trình migration với những rủi ro riêng.
FILE:references/design-rules.md
# Quy tắc thiết kế và mã lỗi phát hiện (finding codes)

| Mã lỗi | Mức độ | Sự cố xảy ra là gì | Chấp nhận được khi nào | Cách khắc phục |
|---|---|---|---|---|
| NO_PRIMARY_KEY | HIGH | không thể phân biệt các dòng trùng lặp hoặc xóa từng dòng một; sao chép logic (logical replication) và CDC cần có khóa; các ORM hoạt động sai lệch | không bao giờ chấp nhận cho các bảng OLTP (có thể tạm chấp nhận cho bảng tạm staging chỉ nạp vào rồi xóa ngay trong một job) | thêm khóa đại diện `id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY` hoặc khóa tự nhiên tổ hợp (`order_id`, `product_id`) |
| FLOAT_MONEY | HIGH | `0.1 + 0.2 = 0.30000000000000004`; tổng tiền và tiền hoàn trả bị lệch từng xu, báo cáo đối soát không khớp | không bao giờ dùng cho tiền tệ; kiểu float chỉ phù hợp cho các số liệu đo đạc vật lý | dùng `numeric(12,2)` / `DECIMAL(12,2)` hoặc số nguyên `bigint` tính theo đơn vị nhỏ nhất (`amount_cents`) kèm theo cột loại tiền tệ (`currency`) |
| FK_TYPE_MISMATCH | HIGH | cột `integer` tham chiếu tới khóa `bigint`: các phép JOIN phải ép kiểu, và lệnh INSERT sẽ lỗi ngay khi ID vượt quá 2.147.483.647 | không bao giờ | dùng cùng một kiểu dữ liệu ở cả hai phía |
| MYISAM_ENGINE | HIGH | không hỗ trợ giao dịch (transactions), không có khóa ngoại, khóa toàn bảng (table locks), hỏng dữ liệu khi sập nguồn | hiếm khi dùng, họa chăng chỉ cho các bảng lưu trữ chỉ đọc (read-only archives) | `ENGINE=InnoDB` |
| INT32_PRIMARY_KEY | MEDIUM | cạn kiệt ID ở ngưỡng khoảng 2,1 tỷ (các sequence cũng bị nhảy số khi rollback); cách sửa lúc đó là phải viết lại toàn bộ bảng trên bảng lớn nhất | các bảng tra cứu nhỏ (danh mục trạng thái, quốc gia) | `bigint` identity / `BIGINT UNSIGNED AUTO_INCREMENT` |
| TIMESTAMP_WITHOUT_TZ | MEDIUM | giá trị được lưu mang ý nghĩa thời điểm khác nhau tùy thuộc vào múi giờ của tiến trình ghi; việc đổi giờ mùa hè (DST) và chuyển máy chủ sẽ làm hỏng thứ tự thời gian | các giá trị giờ đồng hồ treo tường tại địa phương không đại diện cho một thời điểm tuyệt đối (giờ mở cửa cửa hàng, giờ sinh nhật) | `timestamptz` |
| MONEY_TYPE | MEDIUM | kiểu `money` của Postgres phụ thuộc vào cài đặt `lc_monetary`, chỉ hỗ trợ một loại tiền tệ, làm tròn số khi chia | không bao giờ dùng trong các lược đồ mới | `numeric(12,2)` + cột tiền tệ (`currency`) |
| NULLABLE_AUDIT_COLUMN | MEDIUM | các dòng không có `created_at` làm hỏng các báo cáo "7 ngày qua" và gây lỗi khi chạy backfill | không bao giờ | `NOT NULL DEFAULT now()` |
| FK_WITHOUT_INDEX | MEDIUM | (PostgreSQL) việc xóa hoặc cập nhật bản ghi cha sẽ quét toàn bộ bảng con kèm theo giữ khóa (lock); phép JOIN từ bảng cha sang bảng con bị chậm. MySQL InnoDB tự động tạo chỉ mục này | bảng con cực nhỏ, bản ghi cha không bao giờ bị xóa | tạo chỉ mục (index) có các cột đầu tiên (leading columns) là các cột khóa ngoại |
| RESERVED_WORD_NAME | MEDIUM | `order`, `user`, `group` phải được đặt trong dấu ngoặc kép ở mọi truy vấn, ORM, công cụ BI và phiên psql đột xuất | không bao giờ dùng trong các schema mới | dùng tên số nhiều hoặc có tiền tố định danh: orders, app_users, user_groups, sort_order |
| MYSQL_UTF8MB3 | MEDIUM | `utf8` chỉ dài 3-byte: emoji và một số ký tự CJK sẽ bị lỗi hoặc bị cắt cụt | không bao giờ dùng trong các schema mới | `utf8mb4` + `utf8mb4_0900_ai_ci` |
| NULLABLE_BOOLEAN | LOW | logic ba giá trị: `WHERE NOT is_verified` sẽ bỏ qua các dòng có giá trị NULL | khi "chưa rõ" là một trạng thái thực tế (khi đó nên ưu tiên dùng một cột trạng thái tường minh) | `NOT NULL DEFAULT false` |
| UNCHECKED_STATUS | LOW | 'Shipped', 'shipped ', 'shiped' đều có thể xuất hiện; làm báo cáo bị phân mảnh | các trạng thái thuộc sở hữu của một hệ thống khác mà bạn chỉ sao chép lại | `CHECK (status IN (...))`, kiểu enum hoặc khóa ngoại (FK) trỏ tới bảng tra cứu |
| MISSING_FOREIGN_KEY | LOW | dữ liệu mồ côi sau khi xóa, gõ sai id, không có mối quan hệ được ghi nhận | tham chiếu liên cơ sở dữ liệu hoặc theo hướng sự kiện (event-sourced); hãy ghi tài liệu cho chúng | `REFERENCES parent(id)` + chỉ mục (index) |
| CASE_SENSITIVE_UNIQUE_EMAIL | LOW | Ana@x.com và ana@x.com thành hai tài khoản khác nhau; email đặt lại mật khẩu bị gửi nhầm chỗ | không bao giờ dùng cho email đăng nhập | `citext` hoặc `UNIQUE INDEX ON t (lower(email))` và chuẩn hóa khi ghi dữ liệu |
| VARCHAR_255_HABIT | LOW | trong Postgres, 255 là giới hạn tùy tiện từ thời MySQL; thiếu các quy tắc thực sự và những giá trị hợp lệ dài hơn lại bị từ chối | khi 255 thực sự là quy tắc nghiệp vụ | `text` + `CHECK (char_length(x) <= n)` cho các giới hạn thực tế |
| REDUNDANT_INDEX | LOW | một chỉ mục là tiền tố của chỉ mục khác (hoặc của khóa chính PK) gây tốn chi phí ghi và bộ nhớ vô ích | chỉ mục ngắn hơn có kích thước nhỏ hơn nhiều và được truy vấn cực kỳ thường xuyên | xóa bỏ nó |
| JSON_IN_TEXT | LOW | không có kiểm tra hợp lệ, không có toán tử truy vấn, không có chỉ mục GIN | chuỗi văn bản đó không thực sự là JSON | `jsonb` / `JSON` |
| NO_CREATED_AT | LOW | việc gỡ lỗi, kiểm toán và nạp bù dữ liệu (backfill) cần biết thời điểm các dòng dữ liệu xuất hiện | các bảng nối (join tables) thuần túy | `created_at timestamptz NOT NULL DEFAULT now()` |
| MIXED_CASE_NAME | LOW | tên đặt trong ngoặc kép "OrderItems" sẽ phải đặt trong ngoặc kép mãi mãi trong Postgres | không bao giờ | snake_case |
| DATETIME_NO_ZONE | LOW | (MySQL) DATETIME không có múi giờ; nhiều tiến trình ghi khác nhau sẽ lưu các múi giờ lẫn lộn | khi mọi kết nối đều thiết lập time_zone='+00:00' | bắt buộc dùng UTC |

## Những điều công cụ linter không thể đánh giá (cần rà soát thủ công)
- **Các khóa (Keys)**: có khóa duy nhất tự nhiên (natural unique key) nào ngoài id tự tăng (surrogate id) không? Thiếu `UNIQUE (provider, provider_id)` chính là nguyên nhân khiến các khoản thanh toán bị ghi nhận hai lần.
- **ON DELETE**: mặc định dùng RESTRICT/NO ACTION; chỉ dùng CASCADE cho các dòng con thực sự (order_items của một order), không bao giờ dùng từ bảng customers sang bảng payments.
- **Ràng buộc CHECK cho các bất biến (invariants)**: `quantity > 0`, `ends_at > starts_at`, `discount <= total`.
- **Đa thuê bao (Multi-tenancy)**: phải có tenant_id trên mọi bảng thuộc phạm vi thuê bao, đặt ở vị trí đầu tiên trong các khóa duy nhất và chỉ mục, cũng như trong mọi khóa ngoại FK (khóa ngoại phức hợp giúp ngăn chặn việc tham chiếu chéo giữa các thuê bao).
- **Xóa mềm (Soft deletes)**: sử dụng chỉ mục duy nhất từng phần (partial unique index) `WHERE deleted_at IS NULL`, nếu không địa chỉ email đó sẽ không bao giờ có thể được tái sử dụng.
- **Tổng giá trị phi chuẩn hóa (Denormalized totals)**: nếu lưu trữ cột orders.total, hãy nêu rõ cơ chế đảm bảo giá trị của nó luôn bằng tổng của order_items.
- **Dữ liệu định danh cá nhân (PII)**: liệt kê các cột chứa dữ liệu cá nhân; cân nhắc tách ra một bảng riêng với quyền truy cập và chính sách lưu trữ nghiêm ngặt hơn.
FILE:references/type-choices.md
# Lựa chọn kiểu dữ liệu (PostgreSQL / MySQL 8)

| Dữ liệu | PostgreSQL | MySQL 8 | Nên tránh |
|---|---|---|---|
| ID tự tăng (Surrogate id) | `bigint GENERATED ALWAYS AS IDENTITY` | `BIGINT UNSIGNED AUTO_INCREMENT` | `serial`/`int` trên các bảng đang tăng trưởng |
| ID công khai / phân tán | `uuid` (UUIDv7 để tối ưu tính cục bộ của chỉ mục, `gen_random_uuid()` cho v4) | `BINARY(16)` kết hợp với UUID_TO_BIN(x, 1) | UUID dạng `varchar(36)` (lớn gấp 3 lần, chậm hơn) |
| Tiền tệ | `numeric(12,2)` (hoặc (19,4) cho tỷ giá) + `currency char(3)` | `DECIMAL(12,2)` + `CHAR(3)` | `float`, `double`, `real`, kiểu `money` của Postgres |
| Tiền tệ dạng số nguyên | `bigint amount_cents` (lưu ý các loại tiền tệ có 0 hoặc 3 chữ số thập phân) | `BIGINT` | số xu dạng `int` (bị tràn số ở mức 21 triệu) |
| Thời điểm (sự kiện đã xảy ra) | `timestamptz` | `DATETIME(6)` theo chuẩn UTC, hoặc `TIMESTAMP` (giới hạn năm 2038, được chuyển đổi theo múi giờ của phiên) | `timestamp` không kèm múi giờ |
| Ngày lịch | `date` | `DATE` | dấu thời gian (timestamps) đặt ở nửa đêm |
| Giờ đồng hồ / lịch trình | `time` + một cột múi giờ chuẩn IANA | `TIME` + cột múi giờ | `timetz` |
| Thời lượng | `interval` hoặc số giây dạng số nguyên | số giây dạng `INT` | chuỗi văn bản như "1h30" |
| Văn bản | `text` (+ CHECK cho các giới hạn thực tế) | `VARCHAR(n)` với số n thực tế; `TEXT` cho nội dung dài | dùng `varchar(255)` theo thói quen; đệm khoảng trắng với `char(n)` |
| Văn bản không phân biệt hoa thường (email, username) | `citext` hoặc chỉ mục duy nhất dùng hàm `lower()` | bảng mã đối chiếu `_ci` không phân biệt hoa thường (mặc định) | ràng buộc unique thuần túy trên kiểu text trong Postgres |
| Cờ (Flag) | `boolean NOT NULL DEFAULT false` | `BOOLEAN` (TINYINT(1)) `NOT NULL DEFAULT 0` | kiểu boolean cho phép null, chuỗi 'Y'/'N' |
| Trạng thái | `text` + `CHECK (... IN (...))`, kiểu enum, hoặc bảng tra cứu | `ENUM(...)` hoặc bảng tra cứu | kiểu text không có ràng buộc |
| JSON | `jsonb` (dùng chỉ mục GIN cho truy vấn bao hàm) | `JSON` (dùng các cột sinh tự động để đánh chỉ mục) | lưu JSON trong cột text |
| Địa chỉ IP | `inet` / `cidr` | `VARBINARY(16)` kết hợp với INET6_ATON | kiểu text |
| Dữ liệu nhị phân lớn | lưu trữ đối tượng (object storage) + lưu khóa (key) trong cột `text` | tương tự | lưu các BLOB lớn trong những bảng truy cập thường xuyên |

## Lưu ý
- Việc thay đổi kiểu dữ liệu của cột về sau sẽ buộc phải ghi lại toàn bộ bảng trên cả hai hệ quản trị cơ sở dữ liệu (với một vài ngoại lệ như mở rộng độ dài varchar trong Postgres). Hãy thiết kế chuẩn xác các cột id và tiền tệ ngay từ đầu.
- Kiểu enum trong Postgres: thêm một giá trị mới thì nhẹ nhàng, nhưng xóa hoặc đổi tên một giá trị lại rất phức tạp. Bảng tra cứu (lookup tables) dễ nâng cấp hơn và có thể chứa thêm nhãn hiển thị cùng thứ tự sắp xếp.
- Sử dụng `NOT NULL` kèm giá trị mặc định hợp lý là quy chuẩn chung; chỉ nên cho phép null khi mang ý nghĩa "thực sự chưa biết hoặc không áp dụng".
- Luôn lưu mã tiền tệ ngay cạnh mọi khoản tiền, ngay cả khi hiện tại hệ thống chỉ mới dùng một loại tiền tệ duy nhất.
- Đặt tên nhất quán ở mọi nơi: dùng `customer_id` trong mọi bảng tham chiếu đến bảng customers, hậu tố `_at` cho thời điểm, `_on` hoặc `_date` cho ngày tháng, tiền tố `is_`/`has_` cho kiểu boolean.
FILE:templates/schema-review.md
# Đánh giá schema: <tệp dự án / schema>, <ngày>

**Hệ quản trị:** <PostgreSQL 16>   **Phạm vi:** <5 bảng: customers, products, orders, order_items, payments>
**Bối cảnh:** <Backend cửa hàng OLTP, ~2 triệu đơn hàng/năm, Prisma ORM, chưa có dữ liệu>
**Lệnh chạy:** `python3 scripts/schema_lint.py <shop-schema.sql>`
**Kết luận:** <CẦN THAY ĐỔI trước bản phát hành đầu tiên: 4 CAO (HIGH), 8 TRUNG BÌNH (MEDIUM), 8 THẤP (LOW)>

## Bắt buộc sửa (HIGH)
| # | Bảng.cột | Phát hiện | Vấn đề xảy ra | Cách khắc phục |
|---|---|---|---|---|
| 1 | <products.price> | <FLOAT_MONEY> | <0.1 + 0.2 != 0.3; tổng tiền đơn hàng bị lệch từng xu> | <numeric(12,2) NOT NULL CHECK (price >= 0)> |

## Nên sửa (MEDIUM)
| # | Bảng.cột | Phát hiện | Vấn đề xảy ra | Cách khắc phục |
|---|---|---|---|---|
| 1 | <"order"> | <RESERVED_WORD_NAME> | <phải đặt trong dấu ngoặc kép ở mọi truy vấn và công cụ BI> | <đổi tên thành orders> |

## Cân nhắc (LOW / tùy thuộc đánh giá)
- <customers.email: dùng citext để Ana@x.com và ana@x.com được tính là cùng một tài khoản>

## Phát hiện qua đọc hiểu (không phải từ công cụ linter)
- <payments: thêm UNIQUE (provider, provider_id) để khi webhook thử lại (retry) không thể ghi nhận một khoản thanh toán hai lần>
- <ON DELETE: giữ RESTRICT trên mối quan hệ payments -> orders; chỉ dùng CASCADE cho order_items -> orders>

## DDL đã hiệu chỉnh
```sql
<full corrected schema, lints clean>
```

## Các quyết định dành cho nhóm phát triển
- <chọn bigint identity hay UUIDv7 cho các id công khai>
- <lưu trực tiếp cột orders.total hay tính toán từ bảng order_items?>

## Nếu các bảng đã chứa dữ liệu
- <mỗi thay đổi ở trên là một bản di trú dữ liệu (migration); hãy liệt kê chúng và thực hiện rà soát an toàn migration trước khi triển khai>
FILE:examples/shop-schema.sql
-- Small online shop schema (PostgreSQL), first draft for review.
-- The problems are deliberate: this is what the skill is meant to catch.

CREATE TABLE customers (
    id          serial PRIMARY KEY,
    email       varchar(255) NOT NULL UNIQUE,
    full_name   varchar(255),
    phone       varchar(255),
    is_verified boolean,
    created_at  timestamp
);

CREATE TABLE products (
    id         serial PRIMARY KEY,
    sku        varchar(64) NOT NULL UNIQUE,
    title      text NOT NULL,
    price      double precision NOT NULL,
    attributes text,
    created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE "order" (
    id          bigserial PRIMARY KEY,
    customer_id integer NOT NULL REFERENCES customers (id),
    status      varchar(20) NOT NULL DEFAULT 'pending',
    total       numeric(12,2) NOT NULL,
    placed_at   timestamp NOT NULL DEFAULT now(),
    created_at  timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE order_items (
    order_id   bigint NOT NULL REFERENCES "order" (id),
    product_id bigint NOT NULL,
    quantity   integer NOT NULL,
    unit_price numeric(12,2) NOT NULL
);

CREATE TABLE payments (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    order_id    integer NOT NULL REFERENCES "order" (id),
    amount      real NOT NULL,
    provider    text NOT NULL,
    provider_id text,
    created_at  timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX payments_order_idx ON payments (order_id);
CREATE INDEX payments_order_created_idx ON payments (order_id, created_at);
FILE:examples/shop-schema-fixed.sql
-- Small online shop schema (PostgreSQL), after review.
-- Every change is explained in templates/schema-review.md style in the review.

CREATE EXTENSION IF NOT EXISTS citext;

CREATE TABLE customers (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email       citext NOT NULL UNIQUE,
    full_name   text,
    phone       text CHECK (char_length(phone) <= 32),
    is_verified boolean NOT NULL DEFAULT false,
    created_at  timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE products (
    id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    sku        text NOT NULL UNIQUE,
    title      text NOT NULL,
    price      numeric(12,2) NOT NULL CHECK (price >= 0),
    attributes jsonb NOT NULL DEFAULT '{}',
    created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE orders (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    customer_id bigint NOT NULL REFERENCES customers (id),
    status      text NOT NULL DEFAULT 'pending'
                CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled', 'refunded')),
    total       numeric(12,2) NOT NULL CHECK (total >= 0),
    placed_at   timestamptz NOT NULL DEFAULT now(),
    created_at  timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX orders_customer_idx ON orders (customer_id);

CREATE TABLE order_items (
    order_id   bigint NOT NULL REFERENCES orders (id),
    product_id bigint NOT NULL REFERENCES products (id),
    quantity   integer NOT NULL CHECK (quantity > 0),
    unit_price numeric(12,2) NOT NULL CHECK (unit_price >= 0),
    created_at timestamptz NOT NULL DEFAULT now(),
    PRIMARY KEY (order_id, product_id)
);
CREATE INDEX order_items_product_idx ON order_items (product_id);

CREATE TABLE payments (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    order_id    bigint NOT NULL REFERENCES orders (id),
    amount      numeric(12,2) NOT NULL,
    provider    text NOT NULL,
    provider_id text,
    created_at  timestamptz NOT NULL DEFAULT now(),
    UNIQUE (provider, provider_id)
);
CREATE INDEX payments_order_created_idx ON payments (order_id, created_at);
FILE:scripts/schema_lint.py
#!/usr/bin/env python3
"""SQL schema design linter for PostgreSQL and MySQL DDL (Python 3 standard library only).

Reads CREATE TABLE / CREATE INDEX / ALTER TABLE ... ADD statements (a schema dump,
schema.sql, or the DDL of new tables in a pull request) and reports design problems
that are cheap to fix before data exists and expensive after: tables without a
primary key, money stored in floating point, foreign keys whose type differs from the
referenced key or that have no supporting index (PostgreSQL), *_id columns without a
foreign key, 32-bit primary keys, timestamps without time zone, nullable audit
columns and booleans, unchecked status columns, reserved-word and mixed-case names,
case-sensitive unique emails, redundant indexes, JSON kept in text columns, and MySQL
tables using MyISAM or 3-byte utf8.

It does not connect to a database and does not judge migration safety (locks,
backfills); it reviews the shape of the schema itself.

Usage:
  python3 schema_lint.py FILE.sql [FILE.sql ...] [--dialect auto|postgres|mysql]
        [--min-severity LOW|MEDIUM|HIGH] [--ignore CODE,CODE] [--json]
  python3 schema_lint.py --self-test
Exit codes: 0 = no HIGH findings, 1 = HIGH findings, 2 = bad input (unreadable file,
no CREATE TABLE found).
"""
import argparse, json, re, sys

SEV_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
MONEY_RE = re.compile(r"(^|_)(price|amount|total|subtotal|cost|balance|fee|fees|tax|salary|refund|discount|paid|payment|revenue|credit|debit|charge)(_|$)", re.I)
RESERVED = {"user", "order", "group", "select", "table", "column", "desc", "asc", "check", "default", "from",
            "where", "limit", "offset", "references", "primary", "key", "range", "rank", "window", "all", "both",
            "end", "grant", "to", "type", "values", "constraint", "index", "match", "condition", "interval", "read", "row"}
RESERVED_PG_ONLY_SAFE = {"key", "range", "rank", "type", "values", "match", "condition", "interval", "read", "row", "index"}  # fine unquoted in Postgres
RESERVED_MY_ONLY_SAFE = {"offset", "user", "end", "type", "values", "row"}  # fine unquoted in MySQL 8
STATUS_RE = re.compile(r"^(status|state|kind|role|type|.*_status|.*_state|.*_type)$", re.I)
JSONISH_RE = re.compile(r"(^|_)(json|payload|metadata|meta|attributes|settings|extra|data)$", re.I)
INT32 = {"int", "integer", "int4", "serial", "serial4", "mediumint", "smallint", "int2", "smallserial", "tinyint"}
INT64 = {"bigint", "int8", "bigserial", "serial8"}


def finding(sev, code, table, column, problem, fix, line=0):
    return {"severity": sev, "code": code, "table": table, "column": column, "line": line, "problem": problem, "fix": fix}


def strip_comments(sql):
    out, i, n = [], 0, len(sql)
    while i < n:
        c = sql[i]
        if c in "'\"`":
            j = i + 1
            while j < n and sql[j] != c:
                j += 2 if sql[j] == "\\" and c == "'" else 1
            out.append(sql[i:j + 1]); i = j + 1
        elif sql.startswith("--", i) or c == "#" and (i == 0 or sql[i - 1] in "\n \t"):
            j = sql.find("\n", i); j = n if j < 0 else j
            out.append(" " * (j - i)); i = j
        elif sql.startswith("/*", i):
            j = sql.find("*/", i + 2); j = n if j < 0 else j + 2
            out.append(re.sub(r"[^\n]", " ", sql[i:j])); i = j
        else:
            out.append(c); i += 1
    return "".join(out)


def split_top(s, sep=","):
    parts, depth, cur, q = [], 0, [], None
    for ch in s:
        if q:
            cur.append(ch)
            if ch == q:
                q = None
            continue
        if ch in "'\"`":
            q = ch
        elif ch == "(":
            depth += 1
        elif ch == ")":
            depth -= 1
        if ch == sep and depth == 0:
            parts.append("".join(cur)); cur = []
        else:
            cur.append(ch)
    if "".join(cur).strip():
        parts.append("".join(cur))
    return parts


def statements(sql):
    clean = strip_comments(sql)
    pos = 0
    for part in split_top(clean, ";"):
        line = clean.count("\n", 0, pos + len(part) - len(part.lstrip())) + 1
        pos += len(part) + 1
        if part.strip():
            yield line, part.strip()


def ident(s):
    s = s.strip()
    quoted = s[:1] in "\"`"
    name = s.split(".")[-1].strip().strip("\"`")
    return name, quoted


def cols_list(s):
    return [ident(re.sub(r"\s+(ASC|DESC|NULLS\s+(FIRST|LAST)).*$", "", c.strip(), flags=re.I))[0].lower() if not c.strip().startswith("(") and "(" not in c else c.strip().lower()
            for c in split_top(s)]


class Schema:
    def __init__(self):
        self.tables, self.indexes, self.fks, self.enums = {}, [], [], set()
        self.dialect_hints = {"mysql": 0, "postgres": 0}


def parse(sql):
    sc = Schema()
    if re.search(r"AUTO_INCREMENT|ENGINE\s*=|`|UNSIGNED|DATETIME|TINYINT\(", sql, re.I):
        sc.dialect_hints["mysql"] += 1
    if re.search(r"\bSERIAL\b|BIGSERIAL|TIMESTAMPTZ|JSONB|GENERATED\s+(ALWAYS|BY DEFAULT)\s+AS\s+IDENTITY|::|CREATE\s+TYPE|CITEXT|CONCURRENTLY|\bTEXT\[\]", sql, re.I):
        sc.dialect_hints["postgres"] += 1
    for line, st in statements(sql):
        m = re.match(r"CREATE\s+(?:TEMP(?:ORARY)?\s+|UNLOGGED\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([\w\".`]+)\s*\((.*)\)\s*(.*)$", st, re.I | re.S)
        if m:
            name, quoted = ident(m.group(1))
            t = {"name": name, "quoted": quoted, "line": line, "cols": {}, "order": [], "pk": [], "uniques": [],
                 "checks": " ".join([]), "options": m.group(3), "raw": st}
            checks = []
            for item in split_top(m.group(2)):
                parse_item(item.strip(), t, sc, checks)
            t["checks"] = " ".join(checks).lower()
            sc.tables[name.lower()] = t
            continue
        m = re.match(r"CREATE\s+(UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?([\w\".`]*)\s*ON\s+(?:ONLY\s+)?([\w\".`]+)\s*(?:USING\s+\w+\s*)?\((.*)\)\s*(.*)$", st, re.I | re.S)
        if m:
            sc.indexes.append({"table": ident(m.group(3))[0].lower(), "cols": cols_list(m.group(4)), "unique": bool(m.group(1)),
                               "name": ident(m.group(2))[0] if m.group(2) else "", "partial": "where" in m.group(5).lower(), "line": line})
            continue
        m = re.match(r"ALTER\s+TABLE\s+(?:ONLY\s+)?(?:IF\s+EXISTS\s+)?([\w\".`]+)\s+(.*)$", st, re.I | re.S)
        if m:
            tname = ident(m.group(1))[0].lower()
            for action in split_top(m.group(2)):
                a = action.strip()
                am = re.match(r"ADD\s+(?:COLUMN\s+)(?:IF\s+NOT\s+EXISTS\s+)?(.*)$", a, re.I | re.S)
                if am and tname in sc.tables:
                    parse_item(am.group(1), sc.tables[tname], sc, [])
                    continue
                am = re.match(r"ADD\s+(.*)$", a, re.I | re.S)
                if am and tname in sc.tables:
                    chk = []
                    parse_item(am.group(1), sc.tables[tname], sc, chk)
                    sc.tables[tname]["checks"] += " " + " ".join(chk).lower()
            continue
        m = re.match(r"CREATE\s+TYPE\s+([\w\".]+)\s+AS\s+ENUM", st, re.I)
        if m:
            sc.enums.add(ident(m.group(1))[0].lower())
        m = re.match(r"CREATE\s+(?:DOMAIN|EXTENSION)\s+(?:IF\s+NOT\s+EXISTS\s+)?([\w\".]+)", st, re.I)
        if m:
            sc.enums.add(ident(m.group(1))[0].lower())
    return sc


def parse_item(item, t, sc, checks):
    u = item.upper()
    m = re.match(r"CONSTRAINT\s+[\w\"`]+\s+(.*)$", item, re.I | re.S)
    if m:
        item, u = m.group(1).strip(), m.group(1).strip().upper()
    if u.startswith("PRIMARY KEY"):
        t["pk"] = cols_list(re.search(r"\((.*)\)", item, re.S).group(1)); return
    if re.match(r"UNIQUE\b", u):
        cm = re.search(r"\((.*?)\)", item, re.S)
        if cm:
            cols = cols_list(cm.group(1))
            t["uniques"].append(cols)
            sc.indexes.append({"table": t["name"].lower(), "cols": cols, "unique": True, "name": "", "partial": False, "line": t["line"]})
        return
    if u.startswith("FOREIGN KEY"):
        fm = re.match(r"FOREIGN\s+KEY\s*\((.*?)\)\s*REFERENCES\s+([\w\".`]+)\s*(?:\((.*?)\))?", item, re.I | re.S)
        if fm:
            sc.fks.append({"table": t["name"].lower(), "cols": cols_list(fm.group(1)), "ref": ident(fm.group(2))[0].lower(),
                           "ref_cols": cols_list(fm.group(3)) if fm.group(3) else None})
        return
    if u.startswith("CHECK"):
        checks.append(item); return
    if re.match(r"(UNIQUE\s+)?(KEY|INDEX)\b", u) or re.match(r"(FULLTEXT|SPATIAL)\b", u):
        cm = re.search(r"\((.*)\)", item, re.S)
        if cm:
            sc.indexes.append({"table": t["name"].lower(), "cols": cols_list(cm.group(1)), "unique": u.startswith("UNIQUE"),
                               "name": "", "partial": False, "line": t["line"]})
        return
    if u.startswith(("EXCLUDE", "LIKE", "PERIOD")):
        return
    cm = re.match(r"([\"`][^\"`]+[\"`]|[\w$]+)\s+(.*)$", item, re.S)
    if not cm:
        return
    name, quoted = ident(cm.group(1))
    rest = cm.group(2)
    tm = re.match(r"((?:double\s+precision|character\s+varying|bit\s+varying|timestamp(?:\s*\(\d+\))?(?:\s+with(?:out)?\s+time\s+zone)?(?!\w)|time(?:\s+with(?:out)?\s+time\s+zone)?(?!\w)|[\w.]+)(?:\s*\([^)]*\))?(?:\s+unsigned)?(?:\s*\[\])?)", rest, re.I)
    ctype = re.sub(r"\s+", " ", tm.group(1).lower()) if tm else ""
    ru = rest.upper()
    col = {"name": name, "quoted": quoted, "type": ctype, "not_null": "NOT NULL" in ru or "PRIMARY KEY" in ru,
           "default": "DEFAULT" in ru or "IDENTITY" in ru or "AUTO_INCREMENT" in ru or ctype.endswith("serial"),
           "check": "CHECK" in ru}
    t["cols"][name.lower()] = col
    t["order"].append(name.lower())
    if "PRIMARY KEY" in ru:
        t["pk"] = [name.lower()]
    if re.search(r"\bUNIQUE\b", ru):
        t["uniques"].append([name.lower()])
        sc.indexes.append({"table": t["name"].lower(), "cols": [name.lower()], "unique": True, "name": "", "partial": False, "line": t["line"]})
    rm = re.search(r"REFERENCES\s+([\w\".`]+)\s*(?:\((.*?)\))?", rest, re.I)
    if rm:
        sc.fks.append({"table": t["name"].lower(), "cols": [name.lower()], "ref": ident(rm.group(1))[0].lower(),
                       "ref_cols": cols_list(rm.group(2)) if rm.group(2) else None})
    if col["check"]:
        checks.append(f"check ({name})")


def qi(name):
    return f'"{name}"' if name.lower() in RESERVED or name != name.lower() else name


def base_type(ctype):
    t = re.sub(r"\(.*?\)|\s+unsigned|\[\]", "", ctype).strip()
    return {"int4": "integer", "int": "integer", "serial": "integer", "serial4": "integer", "int8": "bigint",
            "bigserial": "bigint", "serial8": "bigint", "int2": "smallint", "smallserial": "smallint"}.get(t, t)


def lint(sql, dialect="auto", ignore=()):
    sc = parse(sql)
    if dialect == "auto":
        dialect = "mysql" if sc.dialect_hints["mysql"] > sc.dialect_hints["postgres"] else "postgres"
    out = []

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

    table_names = set(sc.tables)
    for key, t in sc.tables.items():
        tn = t["name"]
        ln = t["line"]
        if not t["pk"]:
            add("HIGH", "NO_PRIMARY_KEY", tn, "", "table has no primary key: duplicate rows cannot be told apart, updates and replication (logical replication, MySQL row-based, CDC) suffer",
                "add a bigint identity (or UUID) primary key, or a composite key on the natural unique columns", line=ln)
        elif len(t["pk"]) == 1 and t["pk"][0] in t["cols"]:
            c = t["cols"][t["pk"][0]]
            bt = base_type(c["type"])
            if bt in {"integer", "smallint", "mediumint", "tinyint"} and not re.search(r"(^|_)(type|status|kind|country|currency|lookup)s?$", tn, re.I):
                add("MEDIUM", "INT32_PRIMARY_KEY", tn, c["name"], f"primary key {c['name']} is {c['type']} (max about 2.1 billion, less for smaller types); running out means an emergency type change on the biggest table",
                    "use bigint GENERATED ALWAYS AS IDENTITY (Postgres) or BIGINT UNSIGNED AUTO_INCREMENT (MySQL)", line=ln)
        if dialect == "postgres" and t["quoted"] and t["name"] != t["name"].lower():
            add("LOW", "MIXED_CASE_NAME", tn, "", f'table "{tn}" is quoted mixed case: every query must quote it forever', "use snake_case lower-case names", line=ln)
        if tn.lower() in RESERVED and not (dialect == "postgres" and tn.lower() in RESERVED_PG_ONLY_SAFE) and not (dialect == "mysql" and tn.lower() in RESERVED_MY_ONLY_SAFE):
            add("MEDIUM", "RESERVED_WORD_NAME", tn, "", f"table name '{tn}' is a reserved word and must be quoted in every query and ORM", "rename (e.g. users, orders, user_groups)", line=ln)
        if dialect == "mysql":
            opts = t["options"].lower()
            if re.search(r"engine\s*=\s*myisam", opts):
                add("HIGH", "MYISAM_ENGINE", tn, "", "ENGINE=MyISAM has no transactions, no foreign keys and table-level locks; crashes can corrupt it", "use ENGINE=InnoDB", line=ln)
            if re.search(r"charset\s*=\s*utf8(mb3)?\b(?!mb4)", opts):
                add("MEDIUM", "MYSQL_UTF8MB3", tn, "", "utf8 in MySQL is 3-byte utf8mb3: emoji and some CJK characters fail to insert or get mangled", "use DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci", line=ln)
        if not any(re.match(r"^(created_at|created|created_on|inserted_at|date_created|creation_date)$", c) for c in t["cols"]):
            add("LOW", "NO_CREATED_AT", tn, "", "no created_at column: hard to debug, audit or backfill later", "add created_at timestamptz NOT NULL DEFAULT now()", line=ln)
        v255 = [c["name"] for c in t["cols"].values() if re.match(r"(varchar|character varying)\s*\(255\)", c["type"])]
        if dialect == "postgres" and len(v255) >= 3:
            add("LOW", "VARCHAR_255_HABIT", tn, ", ".join(v255), f"{len(v255)} columns are varchar(255): in Postgres the limit costs nothing to drop but 255 is rarely the real rule",
                "use text, with a CHECK (char_length(x) <= n) where a real limit exists", line=ln)
        for cname in t["order"]:
            c = t["cols"][cname]
            ct, bt = c["type"], base_type(c["type"])
            cn = c["name"]
            if MONEY_RE.search(cn) and bt in {"float", "real", "double", "double precision", "float4", "float8"}:
                add("HIGH", "FLOAT_MONEY", tn, cn, f"{cn} is {ct}: binary floating point cannot store 0.10 exactly, sums drift by cents",
                    "use numeric(12,2) / DECIMAL(12,2), or bigint minor units (amount_cents)", line=ln)
            if bt == "money" and dialect == "postgres":
                add("MEDIUM", "MONEY_TYPE", tn, cn, "Postgres money type depends on lc_monetary, has one fixed currency and rounds", "use numeric(12,2) plus a currency column", line=ln)
            if dialect == "postgres" and (re.match(r"^timestamp(\s*\(\d+\))?$", ct) or "without time zone" in ct):
                add("MEDIUM", "TIMESTAMP_WITHOUT_TZ", tn, cn, f"{cn} is timestamp without time zone: the instant depends on whatever zone the writer assumed", "use timestamptz (stores an absolute instant, displays in the session zone)", line=ln)
            if dialect == "mysql" and bt == "datetime" and re.search(r"_at$", cn):
                add("LOW", "DATETIME_NO_ZONE", tn, cn, f"{cn} is DATETIME (no zone): make sure every writer stores UTC", "document and enforce UTC (time_zone='+00:00' on connections) or use TIMESTAMP within its 2038 range", line=ln)
            if re.match(r"^(created_at|updated_at|inserted_at)$", cn, re.I) and not c["not_null"]:
                add("MEDIUM", "NULLABLE_AUDIT_COLUMN", tn, cn, f"{cn} is nullable{'' if c['default'] else ' and has no default'}: rows without it will appear", f"{cn} timestamptz NOT NULL DEFAULT now()", line=ln)
            if bt in {"boolean", "bool"} and not c["not_null"]:
                add("LOW", "NULLABLE_BOOLEAN", tn, cn, f"{cn} is a nullable boolean: three states (true, false, unknown) and NULL-unsafe WHERE clauses", f"{cn} boolean NOT NULL DEFAULT false", line=ln)
            if STATUS_RE.match(cn) and re.match(r"^(text|varchar|character varying|char|character)", bt) and not c["check"] \
                    and cn.lower() not in t["checks"] and not any(f["table"] == key and cname in f["cols"] for f in sc.fks):
                add("LOW", "UNCHECKED_STATUS", tn, cn, f"{cn} is free text: typos like 'Shipped ' and 'shiped' will end up in the data", "add CHECK (status IN (...)), an enum type, or a FK to a lookup table", line=ln)
            if bt in {"text", "longtext", "mediumtext"} and JSONISH_RE.search(cn):
                add("LOW", "JSON_IN_TEXT", tn, cn, f"{cn} looks like JSON stored as {ct}: no validation, no indexing", "use jsonb (Postgres) or JSON (MySQL) if it really is JSON", line=ln)
            if cn.lower() in RESERVED and not (dialect == "postgres" and cn.lower() in RESERVED_PG_ONLY_SAFE) and not (dialect == "mysql" and cn.lower() in RESERVED_MY_ONLY_SAFE) and cn.lower() not in {"type"}:
                add("MEDIUM", "RESERVED_WORD_NAME", tn, cn, f"column name '{cn}' is a reserved word and needs quoting everywhere", "rename it (e.g. sort_order, user_id, group_name, ends_at)", line=ln)
            if dialect == "postgres" and c["quoted"] and cn != cn.lower():
                add("LOW", "MIXED_CASE_NAME", tn, cn, f'column "{cn}" is quoted mixed case: every query must quote it', "use snake_case", line=ln)
            if cn.lower().endswith("_id") and cname not in t["pk"] and not any(f["table"] == key and cname in f["cols"] for f in sc.fks):
                stem = cn.lower()[:-3]
                cands = [x for x in (stem, stem + "s", stem + "es", stem[:-1] + "ies" if stem.endswith("y") else None) if x and x in table_names]
                if cands:
                    add("LOW", "MISSING_FOREIGN_KEY", tn, cn, f"{cn} looks like a reference to {cands[0]} but has no foreign key: orphans and typos are possible", f"add REFERENCES {cands[0]}(id) (with an index)", line=ln)
            if re.search(r"(^|_)email$", cn, re.I) and [cname] in t["uniques"] and dialect == "postgres" and bt != "citext":
                if not any(i["table"] == key and any("lower(" in c2 for c2 in i["cols"]) for i in sc.indexes):
                    add("LOW", "CASE_SENSITIVE_UNIQUE_EMAIL", tn, cn, "UNIQUE on email is case-sensitive in Postgres: Ana@x.com and ana@x.com can both sign up", "CREATE UNIQUE INDEX ... ON t (lower(email)) or use citext", line=ln)
    # foreign keys: types and indexes
    for f in sc.fks:
        t = sc.tables.get(f["table"])
        r = sc.tables.get(f["ref"])
        if not t:
            continue
        ref_cols = f["ref_cols"] or (r["pk"] if r else None)
        if r and ref_cols and len(ref_cols) == len(f["cols"]):
            for c, rc in zip(f["cols"], ref_cols):
                a, b = t["cols"].get(c), r["cols"].get(rc)
                if a and b and base_type(a["type"]) != base_type(b["type"]) and not ({base_type(a["type"]), base_type(b["type"])} <= {"uuid"}):
                    add("HIGH", "FK_TYPE_MISMATCH", t["name"], a["name"], f"{a['name']} is {a['type']} but references {r['name']}.{b['name']} ({b['type']}): joins need casts and the FK fails once ids pass the smaller range",
                        f"make {a['name']} the same type as {r['name']}.{b['name']}", line=t["line"])
        if dialect == "postgres":
            covered = any(idx["table"] == f["table"] and not idx["partial"] and idx["cols"][:len(f["cols"])] == f["cols"] for idx in sc.indexes) \
                or t["pk"][:len(f["cols"])] == f["cols"] or any(u[:len(f["cols"])] == f["cols"] for u in t["uniques"])
            if not covered:
                add("MEDIUM", "FK_WITHOUT_INDEX", t["name"], ", ".join(f["cols"]), f"foreign key ({', '.join(f['cols'])}) -> {f['ref']} has no index starting with it: deletes/updates on {f['ref']} scan {t['name']}, and joins are slow",
                    f"CREATE INDEX CONCURRENTLY ON {qi(t['name'])} ({', '.join(f['cols'])})", line=t["line"])
    # redundant indexes
    for i, a in enumerate(sc.indexes):
        t = sc.tables.get(a["table"])
        if not t or a["partial"] or any("(" in c for c in a["cols"]):
            continue
        others = [b for j, b in enumerate(sc.indexes) if j != i and b["table"] == a["table"] and not b["partial"]]
        if t["pk"] and a["cols"] == t["pk"][:len(a["cols"])] and not a["unique"]:
            add("LOW", "REDUNDANT_INDEX", t["name"], ", ".join(a["cols"]), f"index on ({', '.join(a['cols'])}) duplicates the primary key's leading columns", "drop it", line=a["line"])
            continue
        for b in others:
            if len(b["cols"]) > len(a["cols"]) and b["cols"][:len(a["cols"])] == a["cols"] and not a["unique"]:
                add("LOW", "REDUNDANT_INDEX", t["name"], ", ".join(a["cols"]), f"index on ({', '.join(a['cols'])}) is a prefix of the index on ({', '.join(b['cols'])})", "drop the shorter one unless it is much smaller and hot", line=a["line"])
                break
            if b["cols"] == a["cols"] and j_before(sc.indexes, b, a) and a["unique"] == b["unique"]:
                add("LOW", "REDUNDANT_INDEX", t["name"], ", ".join(a["cols"]), f"two indexes on ({', '.join(a['cols'])})", "drop one", line=a["line"])
                break
    out.sort(key=lambda f: (SEV_ORDER[f["severity"]], f["table"], f["column"], f["code"]))
    return dialect, sc, out


def j_before(lst, b, a):
    return lst.index(b) < lst.index(a)


def report(path, dialect, sc, found):
    counts = {s: sum(1 for f in found if f["severity"] == s) for s in SEV_ORDER}
    out = [f"# Schema review: {path} ({dialect})", "",
           f"Tables: {len(sc.tables)}   Indexes: {len(sc.indexes)}   Foreign keys: {len(sc.fks)}   Findings: {len(found)} (HIGH {counts['HIGH']}, MEDIUM {counts['MEDIUM']}, LOW {counts['LOW']})", ""]
    if not found:
        out.append("No findings.")
        return "\n".join(out)
    out += ["| Sev | Code | Table.Column | Problem | Fix |", "|---|---|---|---|---|"]
    for f in found:
        loc = f["table"] + ("." + f["column"] if f["column"] else "")
        out.append(f"| {f['severity']} | {f['code']} | {loc} | {f['problem'].replace('|', '/')} | {f['fix'].replace('|', '/')} |")
    return "\n".join(out)


def self_test():
    ok_pk = "CREATE TABLE t (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, created_at timestamptz NOT NULL DEFAULT now()"
    cases = [
        ("clean postgres table", ok_pk + ", name text NOT NULL);", "postgres", [], ["NO_PRIMARY_KEY", "INT32_PRIMARY_KEY", "NO_CREATED_AT"]),
        ("no primary key", "CREATE TABLE logs (msg text, created_at timestamptz NOT NULL);", "auto", ["NO_PRIMARY_KEY"], []),
        ("table-level composite pk", "CREATE TABLE m (a bigint, b bigint, created_at timestamptz NOT NULL, PRIMARY KEY (a, b));", "auto", [], ["NO_PRIMARY_KEY"]),
        ("named pk constraint", "CREATE TABLE m (id bigint, created_at timestamptz NOT NULL, CONSTRAINT m_pk PRIMARY KEY (id));", "auto", [], ["NO_PRIMARY_KEY"]),
        ("pk added by alter", "CREATE TABLE m (id bigint NOT NULL, created_at timestamptz NOT NULL); ALTER TABLE m ADD CONSTRAINT m_pkey PRIMARY KEY (id);", "auto", [], ["NO_PRIMARY_KEY"]),
        ("serial pk is int32", "CREATE TABLE orders (id serial PRIMARY KEY, created_at timestamptz NOT NULL);", "auto", ["INT32_PRIMARY_KEY"], []),
        ("small lookup table int pk ok", "CREATE TABLE order_status (id smallint PRIMARY KEY, created_at timestamptz NOT NULL);", "auto", [], ["INT32_PRIMARY_KEY"]),
        ("float money", ok_pk + ", total_price double precision);", "postgres", ["FLOAT_MONEY"], []),
        ("numeric money ok", ok_pk + ", amount numeric(12,2) NOT NULL);", "postgres", [], ["FLOAT_MONEY"]),
        ("float non-money ok", ok_pk + ", latitude double precision);", "postgres", [], ["FLOAT_MONEY"]),
        ("money type", ok_pk + ", balance money);", "postgres", ["MONEY_TYPE"], []),
        ("timestamp without tz", ok_pk + ", shipped_at timestamp);", "postgres", ["TIMESTAMP_WITHOUT_TZ"], []),
        ("timestamp(3) without time zone", ok_pk + ", shipped_at timestamp(3) without time zone);", "postgres", ["TIMESTAMP_WITHOUT_TZ"], []),
        ("timestamptz ok", ok_pk + ", shipped_at timestamp with time zone);", "postgres", [], ["TIMESTAMP_WITHOUT_TZ"]),
        ("nullable created_at", "CREATE TABLE t (id bigint PRIMARY KEY, created_at timestamptz);", "postgres", ["NULLABLE_AUDIT_COLUMN"], []),
        ("no created_at", "CREATE TABLE t (id bigint PRIMARY KEY, name text);", "postgres", ["NO_CREATED_AT"], []),
        ("nullable boolean", ok_pk + ", is_active boolean);", "postgres", ["NULLABLE_BOOLEAN"], []),
        ("boolean not null ok", ok_pk + ", is_active boolean NOT NULL DEFAULT true);", "postgres", [], ["NULLABLE_BOOLEAN"]),
        ("unchecked status", ok_pk + ", status varchar(20) NOT NULL);", "postgres", ["UNCHECKED_STATUS"], []),
        ("status with check ok", ok_pk + ", status text NOT NULL CHECK (status IN ('a','b')));", "postgres", [], ["UNCHECKED_STATUS"]),
        ("status table check ok", ok_pk + ", status text NOT NULL, CONSTRAINT st CHECK (status IN ('a')));", "postgres", [], ["UNCHECKED_STATUS"]),
        ("reserved table name", "CREATE TABLE \"user\" (id bigint PRIMARY KEY, created_at timestamptz NOT NULL);", "postgres", ["RESERVED_WORD_NAME"], []),
        ("reserved column order", ok_pk + ", \"order\" int);", "postgres", ["RESERVED_WORD_NAME"], []),
        ("mixed case quoted", "CREATE TABLE \"OrderItems\" (id bigint PRIMARY KEY, created_at timestamptz NOT NULL);", "postgres", ["MIXED_CASE_NAME"], []),
        ("fk without index", "CREATE TABLE a (id bigint PRIMARY KEY, created_at timestamptz NOT NULL); CREATE TABLE b (id bigint PRIMARY KEY, created_at timestamptz NOT NULL, a_id bigint NOT NULL REFERENCES a(id));", "postgres", ["FK_WITHOUT_INDEX"], ["FK_TYPE_MISMATCH"]),
        ("fk with index ok", "CREATE TABLE a (id bigint PRIMARY KEY, created_at timestamptz NOT NULL); CREATE TABLE b (id bigint PRIMARY KEY, created_at timestamptz NOT NULL, a_id bigint REFERENCES a); CREATE INDEX ON b (a_id, id);", "postgres", [], ["FK_WITHOUT_INDEX"]),
        ("fk covered by composite pk", "CREATE TABLE a (id bigint PRIMARY KEY, created_at timestamptz NOT NULL); CREATE TABLE ab (a_id bigint REFERENCES a(id), x bigint, created_at timestamptz NOT NULL, PRIMARY KEY (a_id, x));", "postgres", [], ["FK_WITHOUT_INDEX"]),
        ("fk type mismatch", "CREATE TABLE a (id bigint PRIMARY KEY, created_at timestamptz NOT NULL); CREATE TABLE b (id bigint PRIMARY KEY, created_at timestamptz NOT NULL, a_id integer, FOREIGN KEY (a_id) REFERENCES a (id));", "postgres", ["FK_TYPE_MISMATCH"], []),
        ("missing fk on _id", "CREATE TABLE customers (id bigint PRIMARY KEY, created_at timestamptz NOT NULL); CREATE TABLE orders (id bigint PRIMARY KEY, created_at timestamptz NOT NULL, customer_id bigint NOT NULL);", "postgres", ["MISSING_FOREIGN_KEY"], []),
        ("external _id without table ok", ok_pk + ", stripe_id text);", "postgres", [], ["MISSING_FOREIGN_KEY"]),
        ("case-sensitive unique email", ok_pk + ", email text NOT NULL UNIQUE);", "postgres", ["CASE_SENSITIVE_UNIQUE_EMAIL"], []),
        ("lower(email) index ok", ok_pk + ", email text NOT NULL UNIQUE); CREATE UNIQUE INDEX t_email ON t (lower(email));", "postgres", [], ["CASE_SENSITIVE_UNIQUE_EMAIL"]),
        ("redundant prefix index", ok_pk + ", a bigint, b bigint); CREATE INDEX i1 ON t (a); CREATE INDEX i2 ON t (a, b);", "postgres", ["REDUNDANT_INDEX"], []),
        ("json in text", ok_pk + ", payload text);", "postgres", ["JSON_IN_TEXT"], []),
        ("varchar 255 habit", ok_pk + ", a varchar(255), b varchar(255), c varchar(255));", "postgres", ["VARCHAR_255_HABIT"], []),
        ("mysql myisam and utf8", "CREATE TABLE `t` (`id` int unsigned NOT NULL AUTO_INCREMENT, `created_at` datetime NOT NULL, PRIMARY KEY (`id`)) ENGINE=MyISAM DEFAULT CHARSET=utf8;", "auto", ["MYISAM_ENGINE", "MYSQL_UTF8MB3", "INT32_PRIMARY_KEY", "DATETIME_NO_ZONE"], ["TIMESTAMP_WITHOUT_TZ"]),
        ("mysql utf8mb4 innodb ok", "CREATE TABLE `t` (`id` bigint unsigned NOT NULL AUTO_INCREMENT, `created_at` datetime NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;", "auto", [], ["MYISAM_ENGINE", "MYSQL_UTF8MB3", "INT32_PRIMARY_KEY"]),
        ("mysql fk needs no extra index", "CREATE TABLE a (id bigint PRIMARY KEY, created_at datetime NOT NULL) ENGINE=InnoDB; CREATE TABLE b (id bigint PRIMARY KEY, created_at datetime NOT NULL, a_id bigint, FOREIGN KEY (a_id) REFERENCES a(id)) ENGINE=InnoDB;", "mysql", [], ["FK_WITHOUT_INDEX"]),
        ("comments and strings ignored", "-- CREATE TABLE x (a int);\n/* CREATE TABLE y (b int); */\n" + ok_pk + ", note text DEFAULT 'a;b -- c');", "postgres", [], ["NO_PRIMARY_KEY"]),
        ("alter add column parsed", ok_pk + "); ALTER TABLE t ADD COLUMN total_amount float;", "postgres", ["FLOAT_MONEY"], []),
    ]
    passed = 0
    for name, sql, dialect, expect, absent in cases:
        d, sc, found = lint(sql, dialect)
        codes = {f["code"] for f in found}
        ok = all(c in codes for c in expect) and not any(c in codes for c in absent)
        if name == "clean postgres table":
            ok = ok and not found
        if name == "comments and strings ignored":
            ok = ok and len(sc.tables) == 1
        if name.startswith("mysql myisam"):
            ok = ok and d == "mysql"
        passed += ok
        print(("PASS " if ok else "FAIL ") + name + ("" if ok else f" -> got {sorted(codes)}"))
    d, sc, found = lint("", "auto")
    ok = report("x.sql", d, sc, found).endswith("No findings.")
    passed += ok
    print(("PASS " if ok else "FAIL ") + "empty report")
    total = len(cases) + 1
    print(f"{passed}/{total} passed")
    return 0 if passed == total else 1


def main(argv=None):
    ap = argparse.ArgumentParser(description="SQL schema design linter (Postgres/MySQL DDL)")
    ap.add_argument("files", nargs="*")
    ap.add_argument("--dialect", default="auto", choices=["auto", "postgres", "mysql"])
    ap.add_argument("--min-severity", default="LOW", choices=list(SEV_ORDER))
    ap.add_argument("--ignore", default="")
    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.files:
        ap.print_usage(sys.stderr)
        return 2
    ignore = {c.strip() for c in a.ignore.split(",") if c.strip()}
    sql_all = []
    for p in a.files:
        try:
            with open(p, encoding="utf-8") as fh:
                sql_all.append(fh.read())
        except OSError as e:
            print(f"error: cannot read {p}: {e}", file=sys.stderr)
            return 2
    dialect, sc, found = lint("\n;\n".join(sql_all), a.dialect, ignore)
    if not sc.tables:
        print("error: no CREATE TABLE statements found", file=sys.stderr)
        return 2
    found = [f for f in found if SEV_ORDER[f["severity"]] <= SEV_ORDER[a.min_severity]]
    if a.json:
        print(json.dumps({"dialect": dialect, "tables": len(sc.tables), "findings": found}, indent=2))
    else:
        print(report(", ".join(a.files), dialect, sc, found))
    return 1 if any(f["severity"] == "HIGH" for f in found) else 0


if __name__ == "__main__":
    sys.exit(main())
BackendPythonSQL+2
F@f
0
Xác thực người dùng taste
Văn bản

Các quy ước mang tính định hướng rõ ràng để xây dựng luồng đăng nhập, phiên làm việc, xác thực đa yếu tố (MFA) và OAuth bảo mật trong các ứng dụng web.

# Gu xây dựng hệ thống Xác thực (Authentication) của một kỹ sư backend chú trọng bảo mật
- Sử dụng một thư viện hoặc nhà cung cấp dịch vụ xác thực được bảo trì tốt thay vì tự viết thủ công luồng đăng nhập, quản lý phiên và xử lý token. Độ tin cậy: 0.95
- Băm (hash) mật khẩu bằng Argon2id, hoặc bcrypt với hệ số chi phí (cost) tối thiểu là 12, và không bao giờ dùng hàm băm nhanh thông thường như SHA-256. Độ tin cậy: 0.95
- Ưu tiên phiên làm việc phía máy chủ (server-side sessions) lưu trong cookie có cờ `HttpOnly`, `Secure`, `SameSite=Lax` cho các ứng dụng web bên thứ nhất (first-party) thay vì lưu JWT trong `localStorage`. Độ tin cậy: 0.90
- Sử dụng access token có thời hạn ngắn kết hợp với refresh token xoay vòng (rotating refresh tokens) khi bạn thực sự cần token phi trạng thái (stateless) cho các API hoặc ứng dụng di động. Độ tin cậy: 0.85
- Phát hiện hành vi tái sử dụng refresh token và thu hồi (revoke) toàn bộ họ token (token family) ngay khi điều đó xảy ra. Độ tin cậy: 0.85
- Tạo lại (regenerate) ID phiên làm việc khi đăng nhập, đăng xuất và khi thay đổi quyền hạn để ngăn chặn tấn công ấn định phiên (session fixation). Độ tin cậy: 0.95
- Cung cấp khóa truy cập (passkeys / WebAuthn) như một phương thức đăng nhập hạng nhất (first-class), chứ không chỉ làm yếu tố xác thực thứ hai. Độ tin cậy: 0.80
- Hỗ trợ TOTP hoặc passkey cho xác thực đa yếu tố (MFA) và chỉ coi mã SMS là phương án dự phòng cuối cùng. Độ tin cậy: 0.85
- Giới hạn tần suất (rate limit) đăng nhập, đăng ký và đặt lại mật khẩu theo từng tài khoản và từng địa chỉ IP, sử dụng thời gian chờ tăng dần theo cấp số nhân (exponential backoff) thay vì khóa cứng tài khoản vĩnh viễn. Độ tin cậy: 0.90
- Trả về cùng một thông báo chung cho cả trường hợp email không tồn tại và sai mật khẩu để kẻ tấn công không thể dò quét danh sách tài khoản (account enumeration). Độ tin cậy: 0.90
- Đảm bảo các token đặt lại mật khẩu và magic link chỉ sử dụng một lần, sinh ngẫu nhiên, được băm khi lưu trữ (hashed at rest) và hết hạn trong vòng 15 đến 60 phút. Độ tin cậy: 0.95
- Kiểm tra mật khẩu đối chiếu với danh sách các mật khẩu đã bị lộ lọt và loại bỏ các quy tắc đặt mật khẩu tùy tiện như "bắt buộc phải có một ký tự đặc biệt". Độ tin cậy: 0.85
- Sử dụng luồng OAuth 2.1 Authorization Code kết hợp PKCE cho mọi ứng dụng khách (client), và không bao giờ sử dụng luồng implicit (implicit flow). Độ tin cậy: 0.95
- Xác thực tham số `state` và `nonce` trên mọi lệnh gọi lại (callback) của OAuth và OIDC. Độ tin cậy: 0.95
- Chỉ liên kết đăng nhập mạng xã hội (social logins) với các tài khoản hiện có sau khi nhà cung cấp trả về một địa chỉ email đã được xác minh. Độ tin cậy: 0.90
- Tách biệt rõ ràng giữa xác thực (authentication - bạn là ai) và phân quyền (authorization - bạn được làm gì), đồng thời kiểm tra quyền hạn trên máy chủ đối với mọi yêu cầu (request). Độ tin cậy: 0.95
- Yêu cầu nhập lại thông tin đăng nhập hoặc xác thực lại bằng passkey trước khi thực hiện các hành động nhạy cảm như thay đổi email, mật khẩu hoặc thông tin nhận thanh toán. Độ tin cậy: 0.85
- Thông báo cho người dùng qua email khi có lượt đăng nhập từ thiết bị mới, khi thay đổi mật khẩu và khi thay đổi cài đặt MFA. Độ tin cậy: 0.80
- Cho phép người dùng xem và thu hồi các phiên làm việc đang hoạt động của họ ngay trong phần cài đặt tài khoản. Độ tin cậy: 0.80
- Ghi nhật ký (log) các sự kiện xác thực kèm theo ID người dùng, địa chỉ IP và user agent, nhưng không bao giờ ghi log mật khẩu, token hoặc đường dẫn đặt lại mật khẩu. Độ tin cậy: 0.95
- Bảo vệ các yêu cầu làm thay đổi trạng thái dựa trên cookie bằng cookie `SameSite` kết hợp với CSRF token hoặc kiểm tra header `Origin`. Độ tin cậy: 0.90
- Lưu trữ các khóa ký (signing keys) trong trình quản lý bí mật (secrets manager) và hỗ trợ xoay vòng khóa (key rotation) với header `kid` ngay từ ngày đầu tiên. Độ tin cậy: 0.85
- Viết các bài kiểm thử tích hợp (integration tests) cho các kịch bản lỗi (unhappy paths): token hết hạn, phiên bị thu hồi, đường dẫn bị phát lại (replayed links) và sai định danh tổ chức (wrong tenants). Độ tin cậy: 0.85
BackendWeb DevelopmentBest Practices+1
F@f
0
Ứng dụng Rails taste
Văn bản

Các quy ước mang tính định hướng rõ ràng để xây dựng ứng dụng Ruby on Rails dễ bảo trì với Hotwire và các thiết lập mặc định hiện đại của Rails.

# Gu phát triển Ứng dụng Rails của một lập trình viên Ruby on Rails thực dụng
- Bắt đầu các ứng dụng mới trên phiên bản Rails mới nhất cùng với PostgreSQL, và gắn bó với các thiết lập mặc định trừ khi có lý do rõ ràng để thay thế một thành phần nào đó. Độ tin cậy: 0.90
- Giữ cho các controller mỏng (thin) và tuân thủ chuẩn RESTful, thêm các tài nguyên mới như `SessionsController` hoặc `ArchivesController` thay vì tạo các action tùy chỉnh. Độ tin cậy: 0.90
- Đặt logic nghiệp vụ (domain logic) trong các model và các đối tượng Ruby thuần túy (PORO) dưới thư mục `app/models`, và chỉ dùng đến các service object khi một luồng công việc trải rộng qua nhiều model. Độ tin cậy: 0.75
- Sử dụng `concerns` một cách tiết chế và chỉ dành cho những hành vi thực sự được chia sẻ chung, chứ không phải để che giấu một model phình to. Độ tin cậy: 0.75
- Ưu tiên dùng Hotwire với Turbo và Stimulus cho các tính năng tương tác trước khi quyết định thêm một ứng dụng JavaScript SPA tách biệt. Độ tin cậy: 0.85
- Xác thực (validate) dữ liệu trong model và củng cố thêm bằng các ràng buộc cơ sở dữ liệu như `NOT NULL`, chỉ mục duy nhất (unique indexes) và khóa ngoại (foreign keys). Độ tin cậy: 0.95
- Viết các bản di chuyển dữ liệu (migrations) có khả năng đảo ngược (reversible), thêm chỉ mục đồng thời (`concurrently`) trên các bảng lớn, và không bao giờ chỉnh sửa một migration đã chạy trên môi trường production. Độ tin cậy: 0.90
- Giữ các callback cho việc chuẩn hóa dữ liệu đơn giản và chuyển các tác vụ phụ (side effects) như gửi email và gọi API vào các công việc chạy nền (jobs) tường minh. Độ tin cậy: 0.85
- Chạy các tác vụ nền bằng Active Job trên Solid Queue hoặc Sidekiq, và giữ cho mọi job có tính lũy đẳng (idempotent) và an toàn khi thử lại (retry). Độ tin cậy: 0.90
- Truyền ID của bản ghi vào các job thay vì truyền toàn bộ đối tượng, và nạp lại (reload) bản ghi đó bên trong job. Độ tin cậy: 0.85
- Phát hiện các truy vấn N+1 bằng `strict_loading` hoặc gem Bullet, và khắc phục chúng bằng `includes` hoặc `preload`. Độ tin cậy: 0.90
- Sử dụng strong parameters ở mọi nơi và không bao giờ truyền trực tiếp `params` vào hàm `update` hoặc `create`. Độ tin cậy: 0.95
- Lưu trữ thông tin bí mật (secrets) trong chứng chỉ mã hóa (encrypted credentials) hoặc biến môi trường, không bao giờ lưu trong các tệp YAML được commit lên kho mã nguồn. Độ tin cậy: 0.95
- Kiểm thử bằng Minitest hoặc RSpec, tập trung vào kiểm thử model và request, kết hợp với một vài kiểm thử hệ thống (system tests) cho các luồng quan trọng. Độ tin cậy: 0.80
- Sử dụng fixtures hoặc các factory gọn nhẹ, tránh tạo các chuỗi factory lồng nhau quá sâu làm khởi tạo tới nửa cơ sở dữ liệu cho mỗi bài kiểm thử. Độ tin cậy: 0.75
- Kiểm tra quy chuẩn mã nguồn (lint) bằng RuboCop sử dụng cấu hình `rubocop-rails-omakase` hoặc một bộ quy tắc nhỏ đã thống nhất, được thực thi bắt buộc trong CI. Độ tin cậy: 0.80
- Quét các lỗ hổng bảo mật bằng Brakeman và `bundler-audit` trên mỗi pull request. Độ tin cậy: 0.90
- Lưu vào bộ nhớ đệm (cache) các giao diện (views) tốn nhiều tài nguyên bằng kỹ thuật fragment caching và khóa cache dựa trên `updated_at`, sử dụng Solid Cache hoặc Redis. Độ tin cậy: 0.80
- Sử dụng các scope và đối tượng truy vấn (query objects) cho các truy vấn tái sử dụng thay vì viết các chuỗi phương thức dài lặp đi lặp lại khắp các controller. Độ tin cậy: 0.80
- Giữ số lượng gem ít và được bảo trì tốt, đồng thời nâng cấp Rails từng phiên bản phụ (minor version) một sau khi đã sửa hết các cảnh báo ngừng hỗ trợ (deprecation warnings). Độ tin cậy: 0.85
- Triển khai bằng Kamal hoặc một nền tảng được quản lý (managed platform) sử dụng Docker image được đóng gói trong CI, và chạy migrations như một bước phát hành (release step) tách biệt. Độ tin cậy: 0.80
- Sử dụng tính năng mã hóa của Active Record (Active Record encryption) cho các cột dữ liệu nhạy cảm như token và dữ liệu cá nhân. Độ tin cậy: 0.80
BackendWeb DevelopmentBest Practices
F@f
0
Tích hợp Webhook taste
Văn bản

Các quy ước thực tế để nhận và gửi webhook an toàn, đáng tin cậy giữa các dịch vụ.

# Tích hợp Webhook taste của một kỹ sư tích hợp chú trọng độ tin cậy
- Xác minh mọi webhook gửi đến bằng chữ ký HMAC trên phần thân request thô (raw request body) trước khi phân tích cú pháp thành JSON. Độ tin cậy: 0.95
- So sánh chữ ký bằng hàm có thời gian thực thi hằng số (constant-time function), không bao giờ dùng phép kiểm tra bằng nhau trên chuỗi thông thường. Độ tin cậy: 0.95
- Từ chối các lượt gửi có dấu thời gian đã ký cũ hơn khoảng năm phút để chặn tấn công phát lại (replay attacks). Độ tin cậy: 0.90
- Phản hồi mã trạng thái 2xx trong vòng vài giây và thực hiện công việc xử lý thực sự trong một tác vụ nền (background job). Độ tin cậy: 0.95
- Lưu trữ bền vững sự kiện thô trước rồi mới xử lý, để không mất dữ liệu nếu worker bị sập giữa chừng. Độ tin cậy: 0.90
- Khử trùng lặp dựa trên event ID của nhà cung cấp bằng chỉ mục duy nhất (unique index), vì mọi nhà cung cấp đều thử gửi lại và phân phối theo cơ chế ít nhất một lần (at-least-once). Độ tin cậy: 0.95
- Không bao giờ giả định các sự kiện đến đúng thứ tự, hãy so sánh trường version hoặc updated_at trước khi ghi đè trạng thái cục bộ. Độ tin cậy: 0.85
- Coi payload như một gợi ý và truy vấn lại đối tượng hiện tại từ API của nhà cung cấp đối với bất kỳ tác vụ nào liên quan đến tiền bạc. Độ tin cậy: 0.80
- Chỉ trả về mã 4xx cho các request lỗi vĩnh viễn và 5xx cho các lỗi tạm thời để cơ chế thử lại của bên gửi hoạt động chính xác. Độ tin cậy: 0.85
- Hỗ trợ đồng thời ít nhất hai khóa bí mật ký (signing secrets) đang hoạt động để có thể xoay vòng khóa mà không gây gián đoạn dịch vụ (downtime). Độ tin cậy: 0.85
- Khi gửi webhook đi, hãy thử lại với thời gian chờ tăng theo hàm mũ kèm độ nhiễu ngẫu nhiên (exponential backoff and jitter) trong tối đa một ngày hoặc lâu hơn, sau đó vô hiệu hóa endpoint và thông báo cho chủ sở hữu. Độ tin cậy: 0.90
- Gửi một payload gọn nhẹ, có đánh phiên bản kèm theo loại sự kiện (event type), event ID, dấu thời gian và ID tài nguyên thay vì đổ toàn bộ dữ liệu dòng trong cơ sở dữ liệu ra. Độ tin cậy: 0.80
- Đặt tên loại sự kiện theo dạng resource.action ở thì quá khứ, chẳng hạn như invoice.paid hoặc user.deleted. Độ tin cậy: 0.80
- Cung cấp cho khách hàng nhật ký gửi (delivery log) kèm theo request, response, trạng thái và nút phát lại (replay) thủ công. Độ tin cậy: 0.85
- Chặn các URL webhook gửi đi phân giải về địa chỉ mạng riêng tư (private), loopback hoặc link-local để ngăn chặn lỗ hổng SSRF. Độ tin cậy: 0.95
- Sử dụng thời gian chờ (timeout) ngắn và tách biệt cho các lượt gửi đi, đồng thời không bao giờ để một endpoint chậm của khách hàng làm nghẽn hàng đợi dùng chung. Độ tin cậy: 0.90
- Bắt buộc sử dụng HTTPS cho tất cả các webhook endpoint ngoài môi trường phát triển cục bộ. Độ tin cậy: 0.90
- Cung cấp API đối soát hoặc cơ chế đồng bộ định kỳ để phía nhận có thể khôi phục lại các sự kiện bị bỏ lỡ. Độ tin cậy: 0.80
- Kiểm thử các bộ xử lý (handlers) ở môi trường cục bộ bằng CLI của nhà cung cấp hoặc đường hầm (tunnel), và lưu lại các payload mẫu đã ghi nhận làm dữ liệu kiểm thử cố định (test fixtures). Độ tin cậy: 0.80
- Cảnh báo khi tỷ lệ lỗi tăng cao và hàng đợi tồn đọng ngày càng lớn trên từng endpoint, chứ không chỉ cảnh báo trên từng lỗi đơn lẻ. Độ tin cậy: 0.80
- Không bao giờ ghi log toàn bộ payload có chứa dữ liệu cá nhân, thông tin bí mật (secrets) hoặc thông tin thẻ thanh toán. Độ tin cậy: 0.90
BackendBest PracticesAPI+2
F@f
0
Ứng dụng Laravel taste
Văn bản

Các quy ước thực tế để xây dựng ứng dụng Laravel dễ bảo trì với các thiết lập mặc định hiện đại của PHP và Laravel.

# Ứng dụng Laravel taste của một lập trình viên web PHP thực dụng
- Bắt đầu trên phiên bản Laravel và PHP mới nhất với khai báo kiểu nghiêm ngặt (strict types) trong mọi file. Độ tin cậy: 0.85
- Giữ controller mỏng và theo chuẩn resourceful, sử dụng single-action invokable controller cho bất kỳ tác vụ nào không phải CRUD. Độ tin cậy: 0.85
- Xác thực dữ liệu đầu vào trong các lớp Form Request và phân quyền bằng Policy thay vì kiểm tra trực tiếp trong controller. Độ tin cậy: 0.90
- Đặt logic nghiệp vụ trong các lớp action nhỏ như CreateInvoice, không đặt trong controller, model hay route closure. Độ tin cậy: 0.75
- Sử dụng Eloquent với các trường $fillable hoặc casts tường minh, và không bao giờ gọi Model::unguard trong code production. Độ tin cậy: 0.85
- Bật Model::preventLazyLoading ngoài môi trường production để phát hiện sớm truy vấn N+1 và khắc phục bằng eager loading. Độ tin cậy: 0.90
- Đảm bảo mọi quan hệ đều có khóa ngoại (foreign key) và chỉ mục duy nhất (unique index) trong migration, chứ không chỉ dựa vào quy tắc của Eloquent. Độ tin cậy: 0.90
- Viết các migration an toàn khi chạy tiến (roll forward) và không bao giờ sửa đổi một migration đã chạy trên production. Độ tin cậy: 0.90
- Định hình phản hồi API bằng API Resources để cấu trúc cơ sở dữ liệu không bao giờ bị lộ trực tiếp ra JSON. Độ tin cậy: 0.85
- Đẩy các tác vụ chậm như gửi email, xuất dữ liệu và gọi dịch vụ bên thứ ba vào các hàng đợi (queued jobs) chạy dưới Horizon, đồng thời đảm bảo mọi job đều có tính idempotent. Độ tin cậy: 0.90
- Chỉ sử dụng event và listener cho các tác dụng phụ (side effects) thực sự, không dùng để che giấu luồng xử lý chính của một request. Độ tin cậy: 0.75
- Đọc cấu hình thông qua config() và chỉ giữ các lệnh gọi env() bên trong các file config để tính năng lưu bộ nhớ đệm cấu hình (config caching) hoạt động. Độ tin cậy: 0.95
- Chạy php artisan optimize, config:cache và route:cache như một phần của mọi quy trình triển khai (deploy). Độ tin cậy: 0.85
- Sử dụng Sanctum cho xác thực SPA chính chủ (first-party) và ứng dụng di động, và chỉ dùng đến Passport khi bạn cần OAuth đầy đủ. Độ tin cậy: 0.80
- Kiểm thử bằng Pest hoặc PHPUnit sử dụng feature test trên cơ sở dữ liệu thực kết hợp với RefreshDatabase. Độ tin cậy: 0.85
- Tạo dữ liệu kiểm thử bằng model factory và state thay vì các file seed dùng chung kích thước lớn. Độ tin cậy: 0.80
- Áp đặt chuẩn định dạng code bằng Laravel Pint và phân tích tĩnh bằng Larastan ở mức 6 trở lên trong CI. Độ tin cậy: 0.85
- Ưu tiên sử dụng Livewire hoặc Inertia cùng các bộ starter kit của Laravel trước khi xây dựng một SPA tách biệt. Độ tin cậy: 0.75
- Giới hạn tần suất (rate limit) cho các route đăng nhập và API công khai bằng các bộ giới hạn tần suất được đặt tên (named rate limiters). Độ tin cậy: 0.90
- Lưu trữ file tải lên thông qua Storage facade trên các ổ đĩa tương thích S3 và phục vụ các file riêng tư qua URL có chữ ký tạm thời (temporary signed URLs). Độ tin cậy: 0.85
- Lên lịch các tác vụ định kỳ trong bộ lập lịch (scheduler) với withoutOverlapping và onOneServer, thay vì viết trực tiếp trong crontab thô. Độ tin cậy: 0.85
- Giữ số lượng gói thư viện (packages) ít và được bảo trì tốt, đồng thời nâng cấp từng phiên bản chính (major version) của Laravel một theo hướng dẫn nâng cấp chính thức. Độ tin cậy: 0.80
PHPBackendWeb Development+1
F@f
0
Trước2 / 2