Imported from nghyane/staticat (
AGENTS.md). Install upstream withnpx skills add nghyane/staticat. Copyright stays with the author.
AGENTS.md — đọc trước
main= BẢN THIẾT KẾ để agent đọc và GEN dự án static-first. Không phải sản phẩm chạy — sản phẩm là dự án được gen ra. Quy trình gen: xemREADME.md. Data schema chính xác:contract/CONTRACT.md. Vì sao mọi quyết định:DESIGN.md.
Repo = 4 vai, không có gì khác
| Vai | Ở đâu | Là gì |
|---|---|---|
| HIỂU | AGENTS.md · DESIGN.md · README.md |
luật · cơ chế+vì sao · playbook gen |
| HIỆP ƯỚC | contract/CONTRACT.md + contract/fixtures/ |
schema chữ + sự thật thực thi được (fixtures đặt theo cây path thật = đề thi = dev-data = ví dụ sống) |
| SKELETON | app/ |
bộ đọc vanilla TS chạy được trên fixtures — copy khi gen |
| TRỌNG TÀI | npm run check (= tsc + vitest render-test + vite build) |
máy gác edit của agent (types · render headless · bundle). Dev-dep tự do (vitest/happy-dom) — không ship, không đụng kết quả cuối; read-path binary vẫn zero runtime dep |
3 BẤT BIẾN (vi phạm = sai kiến trúc)
- Write ⟂ Read — bộ ghi (job ephemeral, ngôn ngữ tuỳ dự án) và bộ đọc (app tĩnh) gặp nhau CHỈ qua file tĩnh CDN. Không share code, không server trên đường đọc.
- Data ⟂ Blob — data trỏ byte nặng qua token; đổi host không đụng data.
- Mutable ⟂ Immutable — chỉ pointer nhỏ tươi (SWR ngắn); mọi payload
.v{n}immutable cache vĩnh viễn.
LUẬT TẦNG RENDER (app/ — vi phạm = sai hướng; lý do: DESIGN §ADR)
- Không framework JS/CSS. Core tự viết
app/src/core/(router + render + live), trần 340 dòng tổng (ADR 34, nâng ADR 54) — chạm trần là dừng lại bàn. (storechỉ sinh khi có ephemeral state thật.) - One-way flow:
state → render(state) → DOM, re-render nguyên view theo navigation. Cấm patch DOM rải rác. Ngoại lệ duy nhất có tên: live region (clock/countdown). - Render qua clone+fill, KHÔNG HTML-string runtime: component markup dựng từ
<template>bundled trong binary (clone); data vào quatextContent/[data-bind]= escape-by-construction. Leaf 1-2 node/cấu trúc rẽ nhánh → DOM API trực tiếp (createElement/new Option) hợp lệ (ADR 38 — cũng escape-by-construction). CấminnerHTML/HTML-string runtime. JS chỉ chạmdata-*(data-slot/data-bind/data-attr), KHÔNG querySelector style-class. - URL là store chính — bậc thang cư trú state, hỏi từ trên xuống, dừng ở bậc đầu thoả: URL (ảnh hưởng nội dung view: filter/sort/page/query → đổi state = navigation) → DOM (form + ephemeral UI) → localStorage (prefs/history) → HTTP cache (server-data) → store JS (bậc CUỐI, chỉ sinh khi pass test 3 câu: ① sống xuyên view-swap? ② không thuộc URL? ③ ≥2 consumer phải phản ứng khi đổi? — case hợp lệ: auth/bookmark/mini-player/toast, phase D). Store cấm chứa server-data hoặc mirror URL-state (ADR 48). Hình dạng khi sinh:
core/store.tscreateStore<T>trên EventTarget, ~25 dòng, đếm vào trần core → sinh store = mở ADR nâng trần cùng commit. - Không query lib: URL immutable + Cache-Control = state manager cho server-data (browser tự SWR). Router trên Navigation API (fallback History). Chuyển trang = swap tức thì; hiệu ứng (View Transitions) tắt mặc định, dự án tự bật.
- Library ≠ framework — test xoá-thử: xoá nó mà phải viết lại cấu trúc app = framework (cấm); chỉ 1 module chết = library (được, khi native thiếu thật + gói sau 1 adapter trong
app/src/lib/). Ví dụ hợp lệ: hls.js. - Data vào app qua đúng 1 cửa:
app/src/data.ts(types + paths + fetch). Views không tự fetch, không tự viết URL. - Dynamic (auth/sync/comment) ⟂ read path: local-first (IndexedDB), API sau adapter, chết chỉ mất tính năng phụ — không bao giờ chặn đường render.
LUẬT TẦNG ISLAND (lib/ — ADR 49; khuôn sống: region.ts, feed.ts)
- Island = 1
export function start(), document-delegated listener, không lifecycle, sống xuyên view-swap. Chọn việc quaclosest("[data-<tên>]")— không phải việc của mình →returndòng đầu. - Đổi query-state của view (filter/page/tab tại chỗ) = bắt buộc qua khuôn
lib/region.ts(abort +aria-busy+ commit URL SAU render, protocolREGION_COMMIT). Cấm island tựpushState/navigate. - Race guard đúng 2 cơ chế có tên: async theo navigation/region → AbortSignal; async theo input → so lại nguồn sự thật DOM sau await. Cấm chế sequence counter mới.
- Island không cache data, không import island khác — data chỉ qua
data.ts(dedup/freshness ở đó, ADR 48). - Island nhiều trạng thái: derive 1 state object → apply 1 lượt (mẫu:
renderFeedRegion) — không rải toggle DOM theo dòng sự kiện. - Khuôn dùng chung trong
lib/HỢP LỆ khi pass test xoá-thử (luật render #6) — copy-paste plumbing lần 2 = tín hiệu phải rút khuôn.
LUẬT CSS (3 điều)
- Giá trị visual CHỈ từ
var(--token)(styles/tokens.css— từ vựng đóng, nhỏ). Cần giá trị mới → thêm token trước. - Markup component qua
views/ui.ts; style theo prefix vào đúng filestyles/components/{tên}.css— tam giác 1 tên:ui.card()↔.card↔card.css. Xem menu trước khi tạo mới. styles/pages/chỉ layout per-view; cấm định nghĩa lại component; cấm inline style. Cascade khai báo 1 nơi:styles/index.css(@layer tokens, base, components, pages). Ưu tiên phần tử native (<dialog>, popover,<details>) trước khi viết JS.
LUẬT PERF (rút từ cơ chế RenderingNG — DESIGN §Cơ chế browser)
- Render path CẤM đọc geometry (
offsetHeight,getBoundingClientRect,getComputedStylephụ thuộc layout) — đọc xen ghi = forced synchronous layout, thủ phạm thật của "DOM chậm". Cần đo → dời vàorequestAnimationFrameread-phase, đọc trước ghi. - Build off-document, commit 1 lần:
clone→fill→replaceChildren. Browser tự batch → 1 style+layout pass. Cấm chèn từng node vào DOM sống trong vòng lặp. - Animate CHỈ
transform/opacity(compositor-only, skip layout+paint → 60fps off-main-thread). Cấm animatewidth/height/top/left. - List dài =
contain+content-visibility:auto+contain-intrinsic-size(virtualization native theo dirty-bits) — KHÔNG tự chế windowing/VDOM.
LUẬT STYLE CODE (dễ đọc = công cụ phát hiện lỗi, không phải thẩm mỹ)
- Tên đầy đủ kể chuyện; cấm 1 ký tự (trừ
iloop). 2. Một statement một việc; cấm comma-operator, cấm side-effect trong expression. 3. Đặt tên bước trung gian thay vì lồng biểu thức. 4. Early return + guard thay ternary chuỗi (nesting ≤2). 5.export functioncó tên cho public; arrow chỉ cho callback ngắn. 6. Type tường minh ở biên export. 7. Cấm!non-null khi narrow được — check + throw kèm bối cảnh. 8. Comment = VÌ SAO của hiện tại, gói đầu file/hàm; lịch sử/thực nghiệm/vòng thử-sai → DESIGN (ADR/Tripwires), code không kể sử. 9. Một việc một cách toàn repo (fetchJson,throw new Error("bối cảnh: chi tiết"), idt-*).
BẢNG ỔN ĐỊNH — đổi gì, giá bao nhiêu
| Tầng | Đổi thế nào |
|---|---|
| Path templates, primitives, charset, cache, 3 bất biến | ĐÓNG BĂNG — đổi = /v2 |
| Luật render/CSS, trần core, thêm facet | 1 dòng ADR trong DESIGN trước, code sau |
| Field data mới | optional additive + fixture cùng commit |
| views/ · styles/ · slice | tự do theo luật, check gác |
Nghi thức làm việc
- Sau mỗi thay đổi:
npm run check(render-test bắt drift template/slot/bind + path/fixture headless) + liếc dev browser cho visual/router (thứ happy-dom không làm). Thêm view = thêm 1 test render. - Đổi contract: sửa
CONTRACT.md+fixtures/+ code tiêu thụ cùng 1 commit. - Vấp lỗi lặp lại → ghi 1 dòng vào DESIGN §Tripwires. Tối ưu sau khi vấp, không trước.
Trạng thái
Phase A + B xong, D dở (slice manga): core (render clone/fill · router URLPattern+Navigation+load⟂render+client-meta SEO+focus[autofocus]+region-commit ADR 47 · live + canonical whitelist ADR 54, 330/340 dòng) + data.ts + config.ts + views (home/title/browse/reader/history — chrome ADR 34, search = live island + browse?q= ADR 35) + islands lib/ (history · searches · search · rail · feed/region ADR 47 · server/rendition-pref ADR 52) + multi-source rendition (ADR 51/52: contract renditions/ids/alias/source.json + cây nguồn giả fixtures/sources · resolver 4 bậc + rơi bậc · title filter ?src/lang/group + badge lang · reader đổi server cùng URL) + URL ngách manga EN (/manga/{id} · /chapter-{sub} · /genre) + dev-HMR (template hot-swap + fixtures watch) + detail v2 + phân loại curated (ADR 55/56/57: title = hero full-width + facts label-value [author/artist/type/published/sources/official⟂trackers] + related sidebar update-row sticky @1024 guard :has · contract additive type/demographic/rating/chapters/author/artist vào entry, links{label,url,kind} display ⟂ ids identity, rating thiếu = unrated · filter mới CHỈ query param — không facet baked) — npm run check xanh. Tiếp: browse tầng 0 (Type select + Surprise me) rồi tầng 1 (panel demographic/year/length/themes), content-pref rating · C — sitemap + share subdomain · D — reader ngang/settings, PWA, ingest write-side. Nhánh discovery = tiền thân SvelteKit (watchdex) — bài học, không phải chuẩn.