skills/cgv-script/SKILL.md
Sinh code clientScript cho Layout Rule của form builder Cogover. Dùng khi user mô tả nghiệp vụ tiếng Việt và muốn code paste-ready để dán vào ô "Cấu hình script" của layout. Bắt buộc kích hoạt decision tree 3 câu hỏi khi nghiệp vụ có thao tác SET giá trị (tránh re-render loop + wipe user edit).
npx skillsauth add stringeecom/cogover-fe-skills cgv-scriptInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
User mô tả nghiệp vụ bằng tiếng Việt → output code JavaScript paste-ready cho pageSettings.script của layout. Đối tượng user không biết code → mọi rủi ro (re-render loop, wipe data, idempotency) phải được skill chặn bằng guard + hỏi tự nhiên.
API runtime: xem reference/api-cheatsheet.md.
return (CRITICAL)Đây là rule QUAN TRỌNG NHẤT của skill. Vi phạm = script lỗi.
Mọi script Layout Rule được runtime gom vào CÙNG 1 hàm async duy nhất. Nếu script này có return → các script khác user paste bên dưới (gộp từ nhiều nguồn / nghiệp vụ) sẽ BỊ CHẶN không chạy.
// ❌ SAI — chặn mọi logic phía dưới
if (!customerId) return;
const item = screen.get("address", "FORM_ITEM");
item.value = ...;
// ❌ SAI — early return dù có lý do
if (formType === "view") return;
// ❌ SAI — return trong if-else
if (type === "A") {
// ...
} else {
return;
}
if (condition) { ... } block:// ✅ ĐÚNG — bao bằng if-block, logic phía dưới vẫn chạy
if (customerId) {
const item = screen.get("address", "FORM_ITEM");
item.value = ...;
}
// ✅ ĐÚNG — đảo điều kiện
if (formType !== "view") {
// ... logic chỉ chạy ở create mode
}
// ✅ ĐÚNG — chỉ vào nhánh khi điều kiện thoả
if (type === "A") {
// ...
} else if (type === "B") {
// ...
}
// (không có else { return; } — script tự kết thúc)
User có thể paste nhiều script từ nhiều nguồn vào cùng 1 ô "Cấu hình script" của layout (skill này sinh + các script user copy từ task trước + script đồng nghiệp viết). Runtime gom hết → 1 hàm. return ở script đầu = các script sau không chạy → user debug khổ.
continue trong loop// ✅ OK — continue chỉ skip iteration, không thoát script
for (const c of changedRowCells || []) {
if (c.relatedListSlug !== "order_lines") continue;
// ...
}
Mọi guard cũ (if (formType === "view") return;, if (!changedFields["X"]) return;, if (rows.length > 0) return;) PHẢI đảo thành if (condition_đúng) { ... } block. Xem safety-rules.md đã refactor.
Đây là rule QUAN TRỌNG THỨ NHÌ. Vi phạm = browser freeze / crash, mất data, user mất niềm tin.
Script Layout Rule tự re-run khi 1 trong các điều xảy ra:
changedFields[slug] = true)changedRowCells populate)screen.changeLayout(...))→ Nghĩa là: script tự set value rồi tự bị trigger lại bởi chính value đó = INFINITE LOOP.
// ❌ SAI — vòng lặp vô tận
const totalItem = screen.get("total", "FORM_ITEM");
totalItem.value = sum;
// → value đổi → script re-run → set lại → re-run → ... → freeze
// ❌ SAI — set cell trong loop không guard
for (const row of rows) {
const priceCell = row.get("price");
priceCell.value = computePrice(row);
// → cell đổi → script re-run → loop tiếp
}
if (current !== new) { ... } TRƯỚC KHI SETMỌI lệnh set .value (field hoặc cell) PHẢI có so sánh trước:
// ✅ ĐÚNG — field cha
const totalItem = screen.get("total", "FORM_ITEM");
if (totalItem.value !== sum) {
totalItem.value = sum;
}
// ✅ ĐÚNG — cell trong related list
const priceCell = row.get("price");
const newPrice = computePrice(row);
if (priceCell.value !== newPrice) {
priceCell.value = newPrice;
}
// ❌ SAI — re-set null mỗi lần script chạy (vẫn loop ngầm nếu trigger khác)
applyTypeItem.value = null;
// ✅ ĐÚNG — chỉ reset khi đang có value
if (applyTypeItem.value) {
applyTypeItem.value = null;
}
| API | Trigger re-run? | Cần guard? |
|---|---|---|
| field.value = / cell.value = | ✅ CÓ | 🚨 BẮT BUỘC so sánh |
| field.limitedOptions = | ❌ KHÔNG | Optional (so sánh để tránh re-render thừa) |
| field.display = | ❌ KHÔNG | Optional |
| field.readOnly = | ❌ KHÔNG | Optional |
| field.required = | ❌ KHÔNG | Optional |
| cell.displayHtml = | ❌ KHÔNG | Optional (cosmetic) |
| rl.setData(...) | ⚠️ Có thể | Guard rows.length === 0 + idempotent payload |
Built-in safety net: runtime có
markParentScriptStart/End+ 200ms debounce cho cell value channel — đỡ 1 phần, nhưng KHÔNG đủ để dựa. Luôn tự guard.
// ✅ ĐÚNG — mỗi field 1 so sánh riêng
if (totalItem.value !== sum) {
totalItem.value = sum;
}
if (qtyItem.value !== qty) {
qtyItem.value = qty;
}
// ✅ ĐÚNG — array of string IDs, so sánh shallow
const ids = records.map((r) => r.id);
const current = item.limitedOptions || [];
const same = current.length === ids.length && current.every((v, i) => v === ids[i]);
if (!same) {
item.limitedOptions = ids;
}
→ Phòng từ đầu bằng if (current !== new) rẻ hơn debug nhiều lần.
User là non-coder, sau này sẽ phải tự đọc lại / sửa script. Mọi dòng code phải readable ngay từ lần đọc đầu tiên — KHÔNG được trade clarity lấy "elegance" hay "smart code".
Tên biến nói tiếng người, không viết tắt tối nghĩa:
const orderId = $record.order;const oid = $record.order; / const x = $record.order;Mỗi dòng làm 1 việc, không gom nhiều bước thành 1 expression dài:
Tránh ternary lồng nhau / optional chaining sâu. Chia thành if-block rõ ràng:
KHÔNG dùng functional combinators (reduce/flatMap/curry/pipe) khi for...of đơn giản đủ dùng. User đọc for (const row of rows) { ... } dễ hơn rows.reduce((acc, row) => ...).
KHÔNG abstract sớm. 1 script chỉ chạy 1 nghiệp vụ — đừng tạo helper function applyOverride(), withGuard(), factory… trừ khi có lý do bắt buộc (vd dùng lại 3+ lần trong cùng script).
Comment giải thích WHY khi guard / pass-through không hiển nhiên:
if (existingRows.length === 0) { /* bảng còn rỗng → mới seed, tránh ghi đè data user */ }rl.setData(records); // records từ filterRecords đã ở API shape, pass thẳng — converter xử// set value vào field (thừa)Comment header ⚠️ đầu script là BẮT BUỘC khi có set value — liệt kê 3 dòng: chạy khi nào / KHÔNG chạy khi / KHÔNG ghi đè. Đây là thứ user xem trước tiên khi mở lại script.
Hardcode rõ ràng > config thông minh. Slug field, slug bảng, threshold → viết thẳng làm string literal, đừng tạo const CONFIG = { ... } cho 1-2 hằng.
Đừng "phòng thủ" cho case không tồn tại. Vd nếu đã if (!orderId) return; rồi thì không cần customerId?.toString() ?? "" ở dòng tiếp.
Output ngắn tốt hơn output đầy đủ-nhưng-rườm-rà. Nếu cùng nghiệp vụ làm được trong 15 dòng → đừng viết 40 dòng.
Test "dễ đọc": trước khi output, đọc lại script với mindset của user non-coder. Nếu có 1 dòng phải đọc lần 2 mới hiểu → viết lại.
Luôn Read file reference/api-cheatsheet.md đầu tiên. Đây là knowledge base bắt buộc — không sinh code mà chưa đọc.
Trích 4 entity:
Code script cần đúng slug và fieldType kỹ thuật. Nếu user mô tả nghiệp vụ chung (vd "khi đổi khách hàng thì fill địa chỉ") mà chưa cho biết slug/type → bắt buộc hỏi trước khi đi tiếp.
Các info phải biết để sinh code đúng:
| Info | Khi nào cần | Ví dụ |
|---|---|---|
| Object slug (đang config layout) | Luôn — để biết script chạy trên object nào | return_order, order, customer |
| Field slug + fieldType | Mỗi field nhắc trong mô tả (target SET, trigger field, field đọc data) | customer (lookup), address (short_text), quantity (number), delivery_date (date) |
| Related list slug TRONG LAYOUT | Khi nghiệp vụ đụng tới bảng (related list) | ⚠️ Xem cảnh báo dưới |
Slug của related list trong layout KHÁC slug của object liên kết.
return_order có related list trỏ tới object order_line, designer đặt slug bảng đó là return_order_lines (HOẶC lines, HOẶC bất kỳ tên gì designer chọn).order_line).screen.get("order_line", "RELATED_LIST") trả null → script silent fail.Khi hỏi user, luôn nhấn mạnh:
"Slug của related list trong layout là gì? Đây là slug designer đặt khi kéo bảng vào layout. Vào trang Sửa giao diện → click vào bảng → check field 'slug'. KHÔNG phải slug của object kia."
Dùng AskUserQuestion với 1-3 câu hỏi (mỗi câu cover 1 cluster info), HOẶC hỏi text trực tiếp với checklist:
Để sinh code chính xác, cho mình biết các slug và type:
1. **Object** đang config layout: slug là gì? (vd: `return_order`)
2. **Field "khách hàng"**: slug? fieldType? (vd: slug=`customer`, type=`lookup`)
3. **Field "địa chỉ"**: slug? fieldType? (vd: slug=`address`, type=`short_text`)
4. **Bảng "chi tiết đơn"**: slug **TRONG LAYOUT** là gì? ⚠️ KHÔNG phải slug của object kia.
(Vào Sửa giao diện → click vào bảng → xem field 'slug')
Nếu user trả lời thiếu → hỏi lại đúng item còn thiếu. KHÔNG đoán slug. KHÔNG dùng placeholder /* SLUG */ nữa — phải có slug thực tế hoặc dừng workflow.
Mô tả có 1 trong các keyword sau → đi vào DANGER MODE:
→ Mở reference/safety-rules.md + đi qua Decision tree bên dưới. Không skip dù mô tả có vẻ đầy đủ.
Mô tả KHÔNG có set value (chỉ show/hide/readOnly/highlight) → skip decision tree, đi thẳng Bước 5.
Dùng AskUserQuestion lần lượt 3 câu sau. Format option phải có ví dụ cụ thể tiếng Việt, không dùng thuật ngữ kỹ thuật:
Header: "Khi nào chạy" Question: "Script này nên chạy khi nào?" Options:
Header: "Mở form đã lưu" Question: "Khi user mở lại form đã lưu trước đó để xem/sửa, script có cần chạy lại không?" Options:
Header: "Ghi đè data" Question: "Trước khi script đổi giá trị, target (ô hoặc bảng) có thể đã có sẵn data user nhập không?" Options:
Hỏi text trực tiếp (không AskUserQuestion):
order, customer)order_lines, ô product)Đọc reference/recipes.md để chọn recipe. Áp guard theo bảng (từ safety-rules.md).
⚠️ NHẮC LẠI RULE §0: TUYỆT ĐỐI KHÔNG dùng
return. Mọi guard dưới đây dùngif (positive_condition) { ... }block bao quanh logic, KHÔNG được dùngif (negative_condition) return;.
| Q1 | Q2 | Q3 | Guard wrapper (if-block) |
|---|---|---|---|
| 1 mọi lần mở | 1 không re-seed | 1 không overwrite | if (formType !== "view") { if (rows.length === 0) { ... } } + (optional) $ref.seeded |
| 1 mọi lần mở | 1 không re-seed | 2 chắc chắn rỗng | if (formType !== "view") { ... } |
| 1 mọi lần mở | 2 có re-seed | 3 luôn overwrite | banner ⚠️ — chạy thẳng, không cần guard |
| 2 chỉ tạo mới | n/a | 1 không overwrite | if (formType !== "view") { if (rows.length === 0) { ... } } |
| 2 chỉ tạo mới | n/a | 2 chắc chắn rỗng | if (formType !== "view") { ... } |
| 3 user đổi field X | n/a | 1 không overwrite | if (changedFields["X"]) { if (!target.value) { ... } } |
| 3 user đổi field X | n/a | 2 chắc chắn rỗng | if (changedFields["X"]) { ... } |
| 3 user đổi field X | n/a | 3 luôn overwrite | if (changedFields["X"]) { ... } + ⚠️ banner |
| 4 user gõ cell | n/a | bất kỳ | for (const c of changedRowCells \|\| []) { if (c.relatedListSlug === "X" && c.fieldSlug === "Y") { ... } } — KHÔNG dùng continue trừ khi loop body có nhiều bước |
⚠️ NHẮC LẠI RULE §0: TUYỆT ĐỐI KHÔNG dùng
return. Bao toàn bộ logic bằngif (positive_condition) { ... }block.
Mọi set value field cha bắt buộc kèm so sánh:
const item = screen.get("X", "FORM_ITEM");
if (item.value !== newValue) {
item.value = newValue; // tránh re-render loop
}
Mọi rl.setData(...) bắt buộc check existingRows.length === 0 (bằng if-block) trừ khi Q3=3.
Header comment tự động (auto-prepend khi có DANGER MODE):
/**
* ⚠️ SCRIPT NÀY THAY ĐỔI DỮ LIỆU FORM TỰ ĐỘNG
*
* Chạy khi: <Q1 answer>
* KHÔNG chạy khi: <Q2 logic>
* KHÔNG ghi đè: <Q3 logic>
*
* Nếu sửa script này, GIỮ NGUYÊN các điều kiện `if (...)` bao quanh logic.
* TUYỆT ĐỐI KHÔNG thêm `return` — script này nằm chung 1 hàm với
* các script khác, `return` sẽ chặn chúng không chạy.
*/
Tự kiểm 10 điểm:
return ở bất kỳ đâu trong script (kể cả trong if-else, kể cả "có lý do chính đáng"). Script chạy chung 1 hàm với scripts khác — return chặn hết. Grep code vừa sinh xem có return không. CÓ → fix bằng cách bao if-block. Ngoại lệ duy nhất: continue trong for loop..value (field hoặc cell) PHẢI có if (current !== new) { current = new; } — RULE §1. Re-trigger script là tự động → set value không guard = infinite loop. Grep code: tìm mọi dòng có .value = (cả field.value, cell.value, item.value, row.get(...).value) → check ngay phía trên có if (... !== ...) không. Set null cũng phải so sánh if (item.value) { item.value = null; }.rl.row/rows/findRow/submit/filterRecords có await chưa?row.get(slug).value / field.value có khớp §7 SCRIPT shape không? (date → Date, time → ms, range → { gte, lte }, lookup → string ID — KHÔNG phải { id, ... })rl.setData(records) có dùng §7B API record shape không? (date → epoch ms number — KHÔNG Date object; lookup → chấp RecordItem | string | array). KHÔNG nhầm 2 shape.if (rows.length === 0) { rl.setData(...) } (trừ khi user chọn overwrite)? Payload setData có idempotent (cùng input → cùng output) không?if (...) return;)if ($ref.xxx) ...; / $ref.cached = await filterRecords(...). filterRecords đã có React Query cache built-in — wrap thừa = code rác + data stale.changeLayout / triggerButton có guard chống loop? (vd if ($ref.lastLayout !== id) { ... } cho changeLayout)Lỗi → fix in-place. Không output code chưa pass 10/10. Điểm 0 hoặc điểm 1 sai = REJECT toàn bộ code.
Format BẮT BUỘC (4 phần):
## Code
[code block JavaScript đầy đủ — paste vào ô Cấu hình script]
## Script này làm gì
[3-5 dòng tiếng Việt thân thiện: chạy khi nào, kiểm tra gì, đổi gì, có gì để bảo vệ data user]
## 🧪 Bắt buộc test trước khi đưa lên production
1. [test case 1 cụ thể với business]
2. [test case 2]
3. [test case 3]
4. [test case 4 — chú ý edge case Q2/Q3 user đã chọn]
## 📖 Thuật ngữ trong code
[Glossary cá nhân hoá: CHỈ in những thuật ngữ thực sự xuất hiện trong code lần này. Tra từ Glossary Dictionary bên dưới.]
Khi output, scan code vừa sinh, chỉ in entries match những từ xuất hiện:
| Trong code | Giải thích tiếng Việt |
|---|---|
| changedFields["X"] | "user vừa đổi field tên X" (true/false) |
| changedRowCells | "danh sách các ô bảng user vừa gõ" (mảng) |
| await | "đợi" — đứng trước hàm cần đợi xong mới chạy tiếp |
| $record | "bản ghi đang sửa" — chứa giá trị các field |
| $record.id | "ID của bản ghi" — nếu có nghĩa là form đã lưu rồi |
| formType | "loại form đang mở" — "create" = Tạo mới, "view" = đã lưu (xem/sửa) |
| $parentRecord | "bản ghi cha" — khi form con (modal/related list) |
| $currentPersonnel | "nhân viên đang đăng nhập" |
| $ref | "bộ nhớ tạm của script" — giữ qua các lần script chạy lại trong 1 session |
| screen.get(slug, "FORM_ITEM") | "lấy field có slug = X" |
| screen.get(slug, "RELATED_LIST") | "lấy bảng (related list) có slug = X" |
| screen.changeLayout(id) | "chuyển sang layout khác" |
| screen.triggerButton(slug) | "tự động bấm button có slug = X" |
| field.value | "giá trị của field" — đọc và ghi được |
| field.display = false | "ẩn field đi, không hiển thị" |
| field.readOnly = true | "khoá field, không cho sửa" |
| field.required = true | "bắt buộc nhập field này" |
| field.limitedOptions | "giới hạn option chọn (cho select)" |
| rl.setData([...]) | "ghi đè toàn bộ dòng trong bảng bằng mảng record mới (theo API record shape — xem §7B cheatsheet, KHÁC SCRIPT shape của row.get().value)" |
| rl.rows() | "lấy danh sách dòng hiện có trong bảng" |
| rl.row(i) | "lấy dòng thứ i (0 là dòng đầu)" |
| rl.findRow(predicate) | "tìm 1 dòng theo điều kiện" |
| rl.submit() | "tự động lưu bảng — không cần user bấm nút Lưu" |
| row.get(slug) | "lấy ô có slug = X trong dòng" |
| cell.displayHtml | "đổi cách hiển thị ô (chỉ visual, không lưu data)" |
| filterRecords(slug, params) | "query danh sách bản ghi từ object khác" |
| isDirtyForm | "form có thay đổi chưa lưu hay không" |
| new Date() | "ngày giờ hiện tại" |
| File | Khi nào đọc |
|---|---|
| reference/api-cheatsheet.md | LUÔN — Bước 1 |
| reference/safety-rules.md | Khi vào DANGER MODE — Bước 3+5 |
| reference/recipes.md | Bước 5 — chọn template |
| reference/case-studies.md | Khi user mô tả khớp 1 case study đã có (vd "set order_line từ order") — đọc để clone pattern |
return TRONG SCRIPT — CỰC KỲ NGHIÊM TRỌNG. Script Layout Rule chạy chung 1 hàm async với các script khác user paste vào. return ở script này → chặn mọi logic phía dưới (kể cả script đồng nghiệp viết, script copy từ task khác). LUÔN dùng if (positive_condition) { ... } block. Ngoại lệ duy nhất: continue trong for loop. Xem RULE §0 đầu file.
0.5. 🚨 SET .value không có if (current !== new) guard — TẠO VÒNG LẶP VÔ TẬN. Re-trigger script là cơ chế built-in: mỗi lần value field/cell thay đổi → script chạy lại. Set value mà không so sánh → infinite loop → browser freeze. Áp dụng cho mọi field.value =, item.value =, row.get(...).value =, cell.value =. Xem RULE §1 đầu file.⚠️ khi script có set value. User maintain sau sẽ không biết guard tại sao có.if (item.value !== ...). Trigger re-render loop.rows.length === 0 trừ khi user chọn overwrite explicit.filterRecords vào $ref (vd if ($ref.products) return;, $ref.cached = await filterRecords(...)). filterRecords đã có cache built-in của React Query — gọi lại cùng params = cache hit. Self-cache = code thừa + data stale. $ref chỉ dùng cho state script thật (seeded, submitted, counter, last-layout-id chống loop swap).rl.setData(records) với SCRIPT shape của row.get().value. setData nhận API record shape (§7B): date = epoch ms number, lookup = RecordItem | string | array. row.get().value nhận SCRIPT shape (§7): date = Date, lookup = string ID. Đừng truyền new Date(...) cho field date trong setData → blank. Mặc định: nếu records lấy từ filterRecords → pass thẳng, KHÔNG map thủ công.reduce/flatMap/curry khi for...of đủ dùng, tạo helper function chỉ dùng 1 lần, abstract sớm (factory, hooks-style trong script), tên biến viết tắt tối nghĩa (oid, c, x). User non-coder sẽ không hiểu, không sửa được, hỏng business. Xem "Nguyên tắc tối thượng" đầu file.development
Cogover Frontend UI Kit (components, forms, hooks, design tokens), commit convention, i18n, unit test. Use when writing/reviewing UI code, creating commits, translating text, or writing unit tests.
tools
Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layers like Lobster, ACPX, plugins, or plain code. Keep conditional logic in the caller; use TaskFlow for flow identity, child-task linkage, waiting state, revision-checked mutations, and user-facing emergence.
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------