docs(agent): bổ sung role fix-dispatcher và siết lại bộ tài liệu agent
- Thêm agent/roles/0_fix_dispatcher.md: phân tier/lane cho từng defect trước khi các agent khác chạy, kèm agent/commands/fix.md và hợp đồng đầu ra agent/output/dispatch_plan.md. - Cập nhật system/guardrail, response_policy, security và các checklist ui/ux/pr_readiness cho khớp luồng mới. - Mở rộng knowledge: i18n_rules, screen_map, theme_tokens, secrets_and_config; cập nhật workflow intake_to_fix và handoff_contract. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
thanhnv
co-authored by
Claude Opus 5
parent
9459dbe197
commit
3c3ec748f9
+62
-8
@@ -21,6 +21,7 @@ Anti-pattern mà bộ này cố tình tránh (mục 10 của tài liệu trainin
|
||||
| Không có Output Contract | Mọi output đi qua template trong `output/` |
|
||||
| Không có Quality Gate | Mỗi role có Quality Gate riêng + `checklist/` dùng chung |
|
||||
| Không có example | `examples/good_fix.md` và `examples/bad_fix.md` |
|
||||
| Effort cố định bất kể lỗi to nhỏ | `roles/0_fix_dispatcher.md` chấm tier trước, lỗi 4px chạy 0 agent |
|
||||
|
||||
Sáu role **không** bị tách thành 7 file nhỏ mỗi role (role/task/process/...). Lý do:
|
||||
phần bị lặp giữa các role chính là guardrail, knowledge và checklist — chúng đã được
|
||||
@@ -47,7 +48,8 @@ agent/
|
||||
│ ├─ qt_pitfalls.md ← 20 nguyên nhân gốc hay gặp của bug UI PySide6
|
||||
│ ├─ secrets_and_config.md ← SecretStore, schema migration, bẫy .get() trên config merge
|
||||
│ └─ quality_gates.md ← CASAN gate, lệnh chạy, test headless
|
||||
├─ roles/ ← 7 agent chuyên biệt
|
||||
├─ roles/ ← 1 hub + 7 agent chuyên biệt
|
||||
│ ├─ 0_fix_dispatcher.md ← HUB: chấm tier T0/T1/T2/T3, chọn lane, tách defect
|
||||
│ ├─ 1_ui_bug_triage.md
|
||||
│ ├─ 2_ui_visual_fixer.md
|
||||
│ ├─ 3_ux_flow_fixer.md
|
||||
@@ -55,14 +57,17 @@ agent/
|
||||
│ ├─ 5_fix_implementer.md
|
||||
│ ├─ 6_regression_reviewer.md
|
||||
│ └─ 7_security_defect_fixer.md
|
||||
├─ commands/
|
||||
│ └─ fix.md ← nguồn của slash command /fix (điểm vào của hub)
|
||||
├─ workflow/
|
||||
│ ├─ intake_to_fix.md ← pipeline end-to-end, ai làm gì ở bước nào
|
||||
│ ├─ intake_to_fix.md ← pipeline end-to-end, 4 lane theo tier
|
||||
│ └─ handoff_contract.md ← envelope truyền giữa các agent
|
||||
├─ checklist/
|
||||
│ ├─ ui_review.md
|
||||
│ ├─ ux_review.md
|
||||
│ └─ pr_readiness.md
|
||||
├─ output/
|
||||
│ ├─ dispatch_plan.md ← template điều phối (output của Hub)
|
||||
│ ├─ defect_record.md ← template hồ sơ lỗi (output của Triage)
|
||||
│ ├─ fix_plan.md ← template phương án sửa (output của Fixer)
|
||||
│ ├─ fix_report.md ← template báo cáo sau khi sửa (output của Implementer)
|
||||
@@ -74,10 +79,11 @@ agent/
|
||||
|
||||
---
|
||||
|
||||
## 3. Bảy agent và khi nào dùng
|
||||
## 3. Một hub + bảy agent, và khi nào dùng
|
||||
|
||||
| # | Agent | Pattern | Nhận vào | Trả ra |
|
||||
|---|---|---|---|---|
|
||||
| **0** | **Fix Dispatcher** (hub) | Router | Phản ánh thô của người dùng | `dispatch_plan.md` — tier + lane + tách defect |
|
||||
| 1 | **UI Bug Triage** | Reviewer | Lời kể lộn xộn của user, ảnh chụp màn hình, log | `defect_record.md` + phân loại + route |
|
||||
| 2 | **UI Visual Fixer** | Generator | defect_record (loại `visual`) | `fix_plan.md` — layout/QSS/theme/icon/DPI |
|
||||
| 3 | **UX Flow Fixer** | Generator | defect_record (loại `flow`) | `fix_plan.md` — luồng, trạng thái, phản hồi |
|
||||
@@ -86,19 +92,36 @@ agent/
|
||||
| 6 | **Regression Reviewer** | Reviewer | Patch + fix_report | Verdict PASS/FAIL + `pr_body.md` |
|
||||
| 7 | **Security Defect Fixer** | Generator | defect_record (loại `security`) | `fix_plan.md` — credential, secret, migration |
|
||||
|
||||
Đây là **Multi-Agent Pattern**: `Triage (Planner) → Specialist → Implementer (Executor)
|
||||
→ Reviewer`. Không bỏ bước. Đặc biệt không bỏ bước 1: 80% bug UI báo lên là mô tả
|
||||
triệu chứng, không phải nguyên nhân.
|
||||
Đây là **Multi-Agent Pattern**: `Dispatcher (Router) → Triage (Planner) → Specialist →
|
||||
Implementer (Executor) → Reviewer`.
|
||||
|
||||
**Số bước thực chạy do agent 0 quyết định, không phải mặc định 5.** Bộ v1.2 chạy đủ pipeline
|
||||
cho mọi lỗi, kể cả đổi một giá trị 4px — đó là lý do agent 0 ra đời. Bốn lane:
|
||||
|
||||
| Tier | Lỗi kiểu gì | Lane | Gọi agent |
|
||||
|---|---|---|---|
|
||||
| **T0** | Đổi số đo hiển thị, sai chính tả chuỗi có key sẵn, đổi token màu có sẵn | DIRECT | **0 lần** — hub sửa luôn + 4 cổng máy |
|
||||
| **T1** | Nguyên nhân gốc đã rõ kèm `file:line`, 1 màn, ≤ 3 file, ≤ 40 LOC | SOLO | 1 lần |
|
||||
| **T2** | Nguyên nhân chưa rõ nhưng đã khoanh 1 màn; chạm QSS/token/i18n dùng chung | PAIR | 3 lần |
|
||||
| **T3** | Mô tả thuần triệu chứng, không tái hiện được, nhiều category, > 150 LOC | FULL | 4–5 lần |
|
||||
|
||||
Bước 1 vẫn **không** được bỏ ở T3 — 80% bug UI báo lên là mô tả triệu chứng, không phải
|
||||
nguyên nhân. Ở T1/T2, phần triage do hub tự làm trong `dispatch_plan`, và chỉ hợp lệ khi
|
||||
phản ánh đã tự chỉ ra màn hình + triệu chứng cụ thể. Bước 6 chỉ được bỏ ở T0/T1, và phải
|
||||
nêu rõ cổng nào thay thế.
|
||||
|
||||
Agent 7 là specialist thứ tư, ngang hàng 2/3/4 trong pipeline, nhưng khác ở hai điểm: nó
|
||||
được phép chạm `config.py`, `infrastructure/`, `core/` (ba role kia bị chặn ở tầng
|
||||
presentation), và nó **không được tự quyết chính sách bảo mật** — bốn câu hỏi bắt buộc trả
|
||||
về cho Cowork Team.
|
||||
|
||||
### Routing rule (Triage quyết định)
|
||||
### Routing rule (Hub chấm tier → Triage chọn specialist)
|
||||
|
||||
```text
|
||||
Người dùng báo lỗi
|
||||
│
|
||||
├─ agent 0 tách thành N defect_id, chấm tier từng cái
|
||||
│ (≤ 5 lệnh đọc/grep, 0 subagent; hết mà chưa chấm được → T2)
|
||||
│
|
||||
├─ "nhìn sai / lệch / mất chữ / màu lạ / bị che" → 2. UI Visual Fixer
|
||||
├─ "bấm không ăn / không biết đang chạy / mất dữ liệu" → 3. UX Flow Fixer
|
||||
@@ -108,12 +131,36 @@ Người dùng báo lỗi
|
||||
Trả về, mở issue type:bug thường.
|
||||
|
||||
Nhóm `security` THẮNG mọi nhóm khác: lỗi vừa lệch layout vừa lộ credential thì đi 7 trước.
|
||||
Tín hiệu bảo mật cũng ép tier lên **T3-SEC** bất kể diff nhỏ cỡ nào — một dòng `==` so
|
||||
mật khẩu không bao giờ là T0.
|
||||
```
|
||||
|
||||
Tier chỉ đi **lên**. FAIL ở bước 6 → tier +1 rồi chạy lại, không sửa lại ở nguyên tier cũ.
|
||||
|
||||
---
|
||||
|
||||
## 4. Cách dùng
|
||||
|
||||
### 4.0 Điểm vào (khuyến nghị)
|
||||
|
||||
Cài một lần cho mỗi máy — `.claude/` nằm trong `.gitignore`, nên nó **không** theo
|
||||
clone; `agent/` mới là bản gốc được version:
|
||||
|
||||
```bash
|
||||
mkdir -p .claude/agents .claude/commands
|
||||
cp agent/roles/[1-7]_*.md .claude/agents/
|
||||
cp agent/commands/fix.md .claude/commands/
|
||||
```
|
||||
|
||||
Rồi:
|
||||
|
||||
```text
|
||||
/fix màn Folder kéo to ra thì mất cây thư mục bên trái
|
||||
```
|
||||
|
||||
Hub sẽ chấm tier, in `dispatch_plan`, rồi tự chạy đúng lane. Chỉ gọi trực tiếp role 1–7
|
||||
khi đã biết chắc tier.
|
||||
|
||||
### 4.1 Dùng thủ công (mọi trợ lý AI)
|
||||
|
||||
Nạp theo đúng thứ tự này rồi dán bug report của user vào:
|
||||
@@ -122,6 +169,7 @@ Nạp theo đúng thứ tự này rồi dán bug report của user vào:
|
||||
agent/system/guardrail.md
|
||||
agent/system/security.md
|
||||
agent/system/response_policy.md
|
||||
agent/roles/0_fix_dispatcher.md ← luôn nạp trước, để biết cần chạy tới đâu
|
||||
agent/roles/<role đang dùng>.md
|
||||
+ các file knowledge/ mà role đó liệt kê ở mục "KNOWLEDGE"
|
||||
```
|
||||
@@ -133,9 +181,13 @@ subagent, copy sang `.claude/agents/`:
|
||||
|
||||
```bash
|
||||
mkdir -p .claude/agents
|
||||
cp agent/roles/*.md .claude/agents/
|
||||
cp agent/roles/[1-7]_*.md .claude/agents/
|
||||
```
|
||||
|
||||
`0_fix_dispatcher.md` **không** copy vào `.claude/agents/`: hub cần quyền gọi agent khác,
|
||||
mà subagent trong Claude Code không gọi được subagent. Hub chạy ở session chính, qua
|
||||
`/fix` (`.claude/commands/fix.md`).
|
||||
|
||||
Sau đó gọi bằng tên: `ui-bug-triage`, `ui-visual-fixer`, `ux-flow-fixer`,
|
||||
`i18n-a11y-fixer`, `fix-implementer`, `regression-reviewer`, `security-defect-fixer`.
|
||||
|
||||
@@ -154,4 +206,6 @@ trong commit message — instruction cũng là code.
|
||||
|---|---|---|
|
||||
| 1.0 | 2026-09-07 | Bản đầu: 6 role, 6 knowledge module, 4 output contract |
|
||||
| 1.1 | 2026-09-07 | Thêm role 7 `security-defect-fixer` + `knowledge/secrets_and_config.md`. Lý do: bộ v1.0 chỉ phủ UI/UX, nên credential hardcode phát hiện qua màn Settings bị rơi vào `not-ui` và không ai nhận |
|
||||
| 1.4 | 2026-09-08 | Nạp bài học từ lượt audit i18n toàn app. `knowledge/i18n_rules.md` §2.0 (`bind_*` là cách mặc định cho chuỗi tĩnh, `bind_dynamic` cho chữ theo trạng thái, không bind dữ liệu), §"Cách TÌM ra hết các chỗ bị lỗi" (grep chuỗi tiếng Việt ra 962 dòng mà **không** dòng nào là lỗi thật; phép đo đúng là thay `tr()` bằng chuỗi mốc trên `MainWindow` thật), và 3 mục checklist mới. Lý do: bộ v1.3 không có cách nào phát hiện lỗi "chữ không được áp lại" — nó không để lại dấu vết nào trong source |
|
||||
| 1.3 | 2026-09-08 | Thêm hub `0_fix_dispatcher` + `output/dispatch_plan.md` + `/fix`. Lý do: bộ v1.2 không có tầng điều phối, nên **mọi** lỗi đều kéo cả pipeline 4–5 agent — kể cả nới một `setMinimumWidth` lên 232px. Bổ sung 4 lane theo tier, danh sách đóng T0 (6 loại + 9 disqualifier), 4 cổng máy thay reviewer ở T0, luật escalate một chiều, và luật tách một phản ánh thành nhiều `defect_id` chấm tier riêng |
|
||||
| 1.2 | 2026-09-07 | Nạp bài học từ lần chạy thật đầu tiên (`SEC-20260907-01`). Bản vá của bước 5 mang một blocker mà **không mục nào trong bộ v1.1 bắt được** — reviewer tìm ra bằng tay. Bổ sung: `secrets_and_config.md` §9 (chặn rỗng, `compare_digest` + ASCII, và luật "API an toàn hơn thường có miền đầu vào hẹp hơn"); `6_regression_reviewer.md` Bước 2.1 (ràng buộc miền đầu vào) và 4.1 (test rỗng ruột); `5_fix_implementer.md` + `quality_gates.md` (baseline bằng `comm -13` trên tên test, guard `git add`, và thực tế suite vốn đã đỏ 11+66); `bad_fix.md` ca 11-12 — hai ví dụ **có thật** đầu tiên trong file |
|
||||
|
||||
+144
-39
@@ -1,53 +1,158 @@
|
||||
# Checklist sẵn sàng tạo PR
|
||||
|
||||
Dùng bởi `fix-implementer` (bước 9) và `regression-reviewer` (bước 8).
|
||||
Bám theo `.gitea/PULL_REQUEST_TEMPLATE.md` và `docs/governance/definition-of-done.md`.
|
||||
Checklist này được sử dụng bởi:
|
||||
|
||||
## A. Cổng chất lượng
|
||||
* `fix-implementer` — kiểm tra ở bước 9.
|
||||
* `regression-reviewer` — kiểm tra ở bước 8.
|
||||
|
||||
- [ ] `python scripts/run_quality_gate.py` — xanh cả 5 cổng, **có dán output thật**.
|
||||
- [ ] Gate C: `domain/`/`application/` không import PySide6/PyQt/`ui`/`app`.
|
||||
- [ ] Gate A: không secret/plaintext mới.
|
||||
- [ ] Gate S: không file nào > 400 LOC.
|
||||
- [ ] Gate O: không module mồ côi (file mới đã được import trong cùng commit).
|
||||
- [ ] Gate A/N: pytest xanh; test vốn đỏ từ trước được ghi riêng.
|
||||
Tham chiếu:
|
||||
|
||||
## B. Kiểm chứng
|
||||
* `.gitea/PULL_REQUEST_TEMPLATE.md`
|
||||
* `docs/governance/definition-of-done.md`
|
||||
|
||||
- [ ] Test regression tồn tại và **đỏ trước / xanh sau**.
|
||||
- [ ] Test chạy được headless (`QT_QPA_PLATFORM=offscreen`).
|
||||
- [ ] Đã kiểm bằng mắt ở dark + light — hoặc ghi rõ "chưa kiểm chứng bằng mắt" kèm lý do.
|
||||
- [ ] Đã kiểm ở các ngôn ngữ liên quan.
|
||||
---
|
||||
|
||||
## C. Phạm vi & lịch sử
|
||||
## A. Kiểm tra chất lượng
|
||||
|
||||
- [ ] Một PR = một thay đổi logic. Không refactor lẫn vào.
|
||||
- [ ] Không đổi format/indent toàn file; diff đọc được.
|
||||
- [ ] Nhánh riêng, không commit thẳng `main`.
|
||||
- [ ] Commit message nêu nguyên nhân gốc + `file:line` + issue.
|
||||
- [ ] Không commit `.env`, `config.json` local, dữ liệu dưới `.cowork_local/`, `.venv`.
|
||||
* [ ] Chạy `python scripts/run_quality_gate.py`.
|
||||
Cả **5 quality gate đều phải PASS** và phải ghi lại **output thực tế**.
|
||||
|
||||
## D. Bảo mật
|
||||
* [ ] **Gate C:** Các thư mục `domain/` và `application/` không được import:
|
||||
- `PySide6`
|
||||
- `PyQt`
|
||||
- `ui`
|
||||
- `app`
|
||||
|
||||
- [ ] Không secret/PII/đường dẫn cá nhân trong code, test fixture, commit message, PR body.
|
||||
- [ ] Ảnh chụp màn hình đính kèm đã được redact.
|
||||
- [ ] Nếu chạm permission / credential / MCP write-exec / sandbox / network / TLS /
|
||||
isolation / model routing / xoá dữ liệu → đánh dấu `security-review: required` và ghi
|
||||
rõ trong PR rằng **CI xanh không đủ để merge**.
|
||||
* [ ] **Gate A:** Không tạo thêm secret hoặc thông tin nhạy cảm dạng plaintext.
|
||||
|
||||
## E. Nội dung PR
|
||||
* [ ] **Gate S:** Không có file nào vượt quá **400 dòng code (LOC)**.
|
||||
|
||||
- [ ] Summary nói **tại sao**, không chỉ **cái gì**.
|
||||
- [ ] Change Type đã tick.
|
||||
- [ ] Scope: nêu rõ cả phần **cố ý không** làm.
|
||||
- [ ] Validation: có lệnh và output thật.
|
||||
- [ ] Security Impact: đã điền, kể cả khi là "không có".
|
||||
- [ ] Compatibility: đã tick.
|
||||
- [ ] Reviewer Notes: chỉ ra chỗ cần soi kỹ nhất.
|
||||
- [ ] Tài liệu (`docs/`, ảnh `docs/screens/`) đã cập nhật nếu cần.
|
||||
* [ ] **Gate O:** Không có file/module mới bị bỏ quên.
|
||||
File Python mới phải được sử dụng/import trong cùng thay đổi.
|
||||
|
||||
## F. Ranh giới
|
||||
* [ ] **Gate A/N:** Test phải PASS.
|
||||
Nếu đã có test FAIL từ trước thì phải ghi rõ đó là **lỗi có sẵn**, không phải lỗi do bản sửa này gây ra.
|
||||
|
||||
- [ ] Agent **không** tự merge, **không** tự đóng issue.
|
||||
- [ ] Nếu là đóng góp của FSG AI Core: hiểu rằng chỉ "Done" khi PR đã merge vào Cowork Local,
|
||||
kèm đủ core issue reference, PR, evidence, reviewer phía Cowork, merge reference.
|
||||
---
|
||||
|
||||
## B. Kiểm tra bản sửa
|
||||
|
||||
* [ ] Có **regression test** cho lỗi đã sửa.
|
||||
|
||||
* [ ] Regression test phải chứng minh được:
|
||||
- **Trước khi sửa:** test FAIL.
|
||||
- **Sau khi sửa:** test PASS.
|
||||
|
||||
* [ ] Test chạy được ở chế độ headless:
|
||||
`QT_QPA_PLATFORM=offscreen`
|
||||
|
||||
* [ ] Nếu thay đổi liên quan đến UI:
|
||||
- Đã kiểm tra giao diện ở **Dark Mode**.
|
||||
- Đã kiểm tra giao diện ở **Light Mode**.
|
||||
- Nếu chưa thể kiểm tra bằng mắt, phải ghi rõ:
|
||||
**"Chưa kiểm chứng bằng mắt"** và nêu lý do.
|
||||
|
||||
* [ ] Nếu thay đổi liên quan đến ngôn ngữ:
|
||||
đã kiểm tra các ngôn ngữ bị ảnh hưởng.
|
||||
|
||||
---
|
||||
|
||||
## C. Kiểm tra phạm vi thay đổi và Git
|
||||
|
||||
* [ ] Một PR chỉ giải quyết **một thay đổi logic chính**.
|
||||
Không đưa refactor không liên quan vào cùng PR.
|
||||
|
||||
* [ ] Không tự ý format hoặc thay đổi indent của toàn bộ file.
|
||||
Diff phải rõ ràng và dễ review.
|
||||
|
||||
* [ ] Làm việc trên **branch riêng**.
|
||||
Không commit trực tiếp vào `main`.
|
||||
|
||||
* [ ] Commit message phải nêu:
|
||||
- Nguyên nhân gốc của lỗi.
|
||||
- Vị trí code liên quan (`file:line`).
|
||||
- Issue liên quan.
|
||||
|
||||
* [ ] Không commit các file/dữ liệu sau:
|
||||
- `.env`
|
||||
- `config.json` local
|
||||
- `.cowork_local/`
|
||||
- `.venv/`
|
||||
|
||||
---
|
||||
|
||||
## D. Kiểm tra bảo mật
|
||||
|
||||
* [ ] Không có các thông tin sau trong code, test fixture, commit message hoặc PR body:
|
||||
- Secret
|
||||
- PII/thông tin cá nhân
|
||||
- Đường dẫn chứa thông tin cá nhân trên máy local
|
||||
|
||||
* [ ] Nếu có ảnh chụp màn hình trong PR:
|
||||
đã che (redact) toàn bộ thông tin nhạy cảm trước khi đính kèm.
|
||||
|
||||
* [ ] Nếu thay đổi liên quan đến một trong các nội dung sau:
|
||||
|
||||
```
|
||||
- Permission/quyền truy cập
|
||||
- Credential/thông tin xác thực
|
||||
- MCP write/exec
|
||||
- Sandbox
|
||||
- Network
|
||||
- TLS
|
||||
- Isolation
|
||||
- Model routing
|
||||
- Xóa dữ liệu
|
||||
|
||||
thì phải:
|
||||
|
||||
1. Đặt `security-review: required`.
|
||||
2. Ghi rõ trong PR rằng:
|
||||
**"CI xanh không có nghĩa là có thể merge ngay."**
|
||||
3. Chờ security review theo quy trình trước khi merge.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E. Kiểm tra nội dung PR
|
||||
|
||||
* [ ] **Summary** phải giải thích **tại sao cần sửa**, không chỉ mô tả đã sửa cái gì.
|
||||
|
||||
* [ ] Đã chọn **Change Type** phù hợp.
|
||||
|
||||
* [ ] **Scope** phải ghi rõ:
|
||||
- Những gì đã thay đổi.
|
||||
- Những gì **cố ý không thay đổi**.
|
||||
|
||||
* [ ] **Validation** phải ghi:
|
||||
- Lệnh đã chạy.
|
||||
- Kết quả thực tế/output.
|
||||
|
||||
* [ ] **Security Impact** phải được điền.
|
||||
Nếu không ảnh hưởng bảo mật, ghi rõ **"Không có"**.
|
||||
|
||||
* [ ] Đã chọn **Compatibility** phù hợp.
|
||||
|
||||
* [ ] **Reviewer Notes** phải chỉ ra những phần reviewer cần kiểm tra kỹ nhất.
|
||||
|
||||
* [ ] Đã cập nhật tài liệu nếu cần:
|
||||
- `docs/`
|
||||
- Ảnh màn hình trong `docs/screens/`
|
||||
|
||||
---
|
||||
|
||||
## F. Giới hạn quyền của Agent
|
||||
|
||||
* [ ] Agent **không được tự merge PR**.
|
||||
|
||||
* [ ] Agent **không được tự đóng issue**.
|
||||
|
||||
* [ ] Nếu đây là đóng góp từ **FSG AI Core**, cần hiểu rằng trạng thái **"Done"** chỉ được xác nhận khi PR đã thực sự được merge vào Cowork Local và có đầy đủ:
|
||||
|
||||
```
|
||||
- Core issue reference
|
||||
- PR reference
|
||||
- Evidence
|
||||
- Reviewer phía Cowork
|
||||
- Merge reference
|
||||
```
|
||||
|
||||
+190
-36
@@ -1,49 +1,203 @@
|
||||
# Checklist review bản vá UI (visual)
|
||||
# Checklist review bản vá UI (Visual)
|
||||
|
||||
Dùng bởi `ui-visual-fixer` (bước 7) và `regression-reviewer` (bước 5).
|
||||
Checklist này được sử dụng bởi:
|
||||
|
||||
## A. Đúng file
|
||||
* `ui-visual-fixer` — kiểm tra ở bước 7.
|
||||
* `regression-reviewer` — kiểm tra ở bước 5.
|
||||
|
||||
- [ ] Đã `grep` cả `ui/` và `presentation/`; file được sửa là file thực sự import vào runtime.
|
||||
- [ ] Widget này không có bản trùng tên ở thư mục còn lại.
|
||||
Mục tiêu: đảm bảo bản vá UI sửa đúng nguyên nhân, không phá theme, layout, icon hoặc vòng đời của giao diện.
|
||||
|
||||
## B. Màu & theme
|
||||
---
|
||||
|
||||
- [ ] Không hex literal (`#rrggbb`), không tên màu (`"red"`) ngoài `theme/`.
|
||||
- [ ] Không `setStyleSheet` cục bộ mới; style đi qua `objectName` + `theme/qss.py`.
|
||||
- [ ] Token mới có ở **cả** `DARK` và `LIGHT`.
|
||||
- [ ] Chữ trên nền đặc dùng `accent_solid`, không dùng `accent`.
|
||||
- [ ] Bậc bề mặt đúng ngữ nghĩa: `bg` / `surface` / `surface_raised` / `overlay` / `sunken`.
|
||||
- [ ] Contrast ≥ 4.5:1 cho body text và chữ trên nút đặc, ở cả hai theme.
|
||||
- [ ] Không thêm gradient/glow (trái ràng buộc thiết kế).
|
||||
- [ ] Nav rail vẫn tối hơn vùng nội dung.
|
||||
- [ ] Không trả bốn giá trị đã nhích lên WCAG AA về giá trị VS Code gốc.
|
||||
- [ ] Nếu chạm `_TEMPLATE`: đã liệt kê phạm vi ảnh hưởng toàn app.
|
||||
## A. Kiểm tra đúng file
|
||||
|
||||
## C. Layout & kích thước
|
||||
* [ ] Đã tìm kiếm trong **cả `ui/` và `presentation/`** để xác định file thực sự được ứng dụng sử dụng khi chạy.
|
||||
|
||||
- [ ] Không thêm `setFixedWidth` / `setFixedSize` / `setFixedHeight` mới.
|
||||
- [ ] Stretch factor / size policy được đặt tường minh.
|
||||
- [ ] `QScrollArea` có `setWidgetResizable(True)`.
|
||||
- [ ] Margin/spacing của layout lồng nhau không cộng dồn ngoài ý muốn.
|
||||
- [ ] Còn đúng ở cửa sổ nhỏ nhất **và** maximize.
|
||||
- [ ] Còn đúng ở scale 125% / 150% nếu bản vá chạm kích thước.
|
||||
* [ ] Đã kiểm tra xem widget có file/bản triển khai trùng tên ở thư mục còn lại hay không.
|
||||
|
||||
## D. Icon & vẽ tay
|
||||
* [ ] Nếu có nhiều file cùng chức năng, đã xác định rõ **file nào thực sự được import và chạy**.
|
||||
|
||||
- [ ] Icon lấy qua `ui/icons.py::icon`, không load file trực tiếp.
|
||||
- [ ] `paintEvent` đọc màu qua `current_palette()`, không đọc lại config.
|
||||
- [ ] Dùng `update()`, không `repaint()` trong vòng lặp.
|
||||
- [ ] `QPainter` có `end()`; nền được xoá đúng cách.
|
||||
---
|
||||
|
||||
## E. Vòng đời
|
||||
## B. Kiểm tra màu sắc và Theme
|
||||
|
||||
- [ ] Bản vá còn đúng khi đổi theme **trước** rồi mới mở màn dựng lười (P07).
|
||||
- [ ] `setProperty` để đổi style động có kèm `unpolish`/`polish`.
|
||||
- [ ] Không `connect()` lặp lại trong hàm được gọi nhiều lần.
|
||||
* [ ] Không thêm mã màu trực tiếp như `#rrggbb` hoặc tên màu như `"red"` bên ngoài thư mục `theme/`.
|
||||
|
||||
## F. Bằng chứng
|
||||
* [ ] Không thêm `setStyleSheet()` trực tiếp vào widget.
|
||||
Style phải được quản lý thông qua:
|
||||
|
||||
- [ ] Đã đối chiếu `docs/screens/<slug>-dark.png` và `<slug>-light.png`.
|
||||
- [ ] Ảnh trong `docs/screens/` cần cập nhật thì đã nêu.
|
||||
- [ ] Có test regression chạy headless, đỏ-trước-xanh-sau.
|
||||
```
|
||||
`objectName` → `theme/qss.py`
|
||||
```
|
||||
|
||||
* [ ] Nếu thêm token màu mới, token đó phải được khai báo cho **cả `DARK` và `LIGHT`**.
|
||||
|
||||
* [ ] Khi đặt chữ trên nền màu đặc, dùng `accent_solid`.
|
||||
Không dùng `accent` cho trường hợp này.
|
||||
|
||||
* [ ] Dùng đúng loại màu nền theo mục đích:
|
||||
|
||||
```
|
||||
- `bg` — nền chính.
|
||||
- `surface` — bề mặt thông thường.
|
||||
- `surface_raised` — bề mặt nổi.
|
||||
- `overlay` — lớp phủ.
|
||||
- `sunken` — khu vực chìm.
|
||||
```
|
||||
|
||||
* [ ] Contrast của chữ đạt tối thiểu **4.5:1** đối với:
|
||||
- Body text.
|
||||
- Chữ trên nút có nền đặc.
|
||||
- Cả Dark Mode và Light Mode.
|
||||
|
||||
* [ ] Không thêm:
|
||||
- Gradient.
|
||||
- Glow.
|
||||
|
||||
```
|
||||
Đây là các kiểu không phù hợp với design constraint hiện tại.
|
||||
```
|
||||
|
||||
* [ ] `Nav rail` vẫn **tối hơn khu vực nội dung**.
|
||||
Đây là thiết kế có chủ ý, không tự ý làm sáng lên.
|
||||
|
||||
* [ ] Không khôi phục các giá trị màu cũ theo VS Code nếu các giá trị hiện tại đã được điều chỉnh để đạt WCAG AA.
|
||||
|
||||
* [ ] Nếu thay đổi `_TEMPLATE`:
|
||||
đã đánh giá và ghi rõ **phạm vi ảnh hưởng trên toàn ứng dụng** vì `_TEMPLATE` có thể ảnh hưởng nhiều màn hình.
|
||||
|
||||
---
|
||||
|
||||
## C. Kiểm tra Layout và kích thước
|
||||
|
||||
* [ ] Không thêm mới:
|
||||
|
||||
```
|
||||
- `setFixedWidth()`
|
||||
- `setFixedHeight()`
|
||||
- `setFixedSize()`
|
||||
|
||||
để che hoặc né lỗi layout.
|
||||
```
|
||||
|
||||
* [ ] `stretch factor` và `size policy` được thiết lập rõ ràng khi cần.
|
||||
|
||||
* [ ] Nếu sử dụng `QScrollArea`, phải có:
|
||||
|
||||
```
|
||||
`setWidgetResizable(True)`
|
||||
```
|
||||
|
||||
* [ ] Kiểm tra margin và spacing của các layout lồng nhau.
|
||||
Không được để chúng cộng dồn khiến UI bị lệch hoặc quá rộng.
|
||||
|
||||
* [ ] UI vẫn hiển thị đúng ở:
|
||||
- Kích thước cửa sổ nhỏ nhất.
|
||||
- Cửa sổ maximize.
|
||||
|
||||
* [ ] Nếu bản vá liên quan đến kích thước, phải kiểm tra thêm ở:
|
||||
- Scale 125%.
|
||||
- Scale 150%.
|
||||
|
||||
---
|
||||
|
||||
## D. Kiểm tra Icon và Custom Painting
|
||||
|
||||
* [ ] Icon phải được lấy thông qua:
|
||||
|
||||
```
|
||||
`ui/icons.py::icon`
|
||||
|
||||
Không tự load file icon trực tiếp.
|
||||
```
|
||||
|
||||
* [ ] Trong `paintEvent()`, màu sắc phải lấy từ:
|
||||
|
||||
```
|
||||
`current_palette()`
|
||||
|
||||
Không đọc lại màu trực tiếp từ config.
|
||||
```
|
||||
|
||||
* [ ] Trong các vòng lặp hoặc thao tác cập nhật UI, dùng:
|
||||
|
||||
```
|
||||
`update()`
|
||||
|
||||
Không dùng `repaint()` nếu không thực sự cần thiết.
|
||||
```
|
||||
|
||||
* [ ] `QPainter` được kết thúc đúng cách bằng `end()` khi sử dụng thủ công.
|
||||
|
||||
* [ ] Nền của khu vực custom painting được xử lý/xóa đúng cách, không để lại hình ảnh hoặc pixel cũ.
|
||||
|
||||
---
|
||||
|
||||
## E. Kiểm tra vòng đời UI
|
||||
|
||||
* [ ] UI vẫn hoạt động đúng nếu người dùng:
|
||||
|
||||
```
|
||||
1. Đổi theme trước.
|
||||
2. Sau đó mới mở màn hình được tạo theo kiểu lazy.
|
||||
|
||||
Đặc biệt kiểm tra lỗi **P07**.
|
||||
```
|
||||
|
||||
* [ ] Nếu dùng `setProperty()` để thay đổi style động:
|
||||
phải gọi `unpolish()` và `polish()` khi cần để QSS được áp dụng lại.
|
||||
|
||||
* [ ] Không gọi `connect()` nhiều lần trong một hàm có thể được gọi nhiều lần.
|
||||
|
||||
* [ ] Không tạo signal/slot bị kết nối lặp, gây ra:
|
||||
- Event chạy nhiều lần.
|
||||
- UI cập nhật nhiều lần.
|
||||
- Memory leak hoặc hành vi bất thường.
|
||||
|
||||
---
|
||||
|
||||
## F. Kiểm tra bằng chứng
|
||||
|
||||
* [ ] Đã đối chiếu với screenshot trong:
|
||||
|
||||
```
|
||||
`docs/screens/<slug>-dark.png`
|
||||
|
||||
và
|
||||
|
||||
`docs/screens/<slug>-light.png`
|
||||
```
|
||||
|
||||
* [ ] Nếu bản vá làm thay đổi giao diện, đã xác định screenshot nào cần cập nhật.
|
||||
|
||||
* [ ] Nếu cần cập nhật screenshot trong `docs/screens/`, phải ghi rõ trong phạm vi thay đổi.
|
||||
|
||||
* [ ] Có regression test cho lỗi đã sửa.
|
||||
|
||||
* [ ] Regression test chạy được ở chế độ headless:
|
||||
|
||||
```
|
||||
`QT_QPA_PLATFORM=offscreen`
|
||||
```
|
||||
|
||||
* [ ] Regression test chứng minh được:
|
||||
|
||||
```
|
||||
**Trước khi sửa → FAIL**
|
||||
|
||||
**Sau khi sửa → PASS**
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Kết luận
|
||||
|
||||
Chỉ đánh giá bản vá là **PASS** khi:
|
||||
|
||||
1. Sửa đúng file thực sự chạy.
|
||||
2. Không phá theme hoặc layout hiện có.
|
||||
3. Không dùng workaround để che lỗi.
|
||||
4. Không tạo regression.
|
||||
5. Có regression test phù hợp.
|
||||
6. Có đủ bằng chứng kiểm chứng.
|
||||
7. Các vấn đề liên quan đến security hoặc product decision đã được route đúng agent/người phụ trách.
|
||||
|
||||
+198
-34
@@ -1,48 +1,212 @@
|
||||
# Checklist review bản vá UX (flow)
|
||||
# Checklist review bản vá UX (Flow)
|
||||
|
||||
Dùng bởi `ux-flow-fixer` (bước 8) và `regression-reviewer`.
|
||||
Checklist này được sử dụng bởi:
|
||||
|
||||
## A. Bốn trạng thái
|
||||
* `ux-flow-fixer` — kiểm tra ở bước 8.
|
||||
* `regression-reviewer` — kiểm tra trong quá trình review bản vá.
|
||||
|
||||
Cho mỗi view có dữ liệu bất đồng bộ:
|
||||
Mục tiêu: đảm bảo người dùng luôn biết **hệ thống đang làm gì, chuyện gì xảy ra và cần làm gì tiếp theo**, đồng thời không bị mất dữ liệu.
|
||||
|
||||
- [ ] **Rỗng** — hiện thông điệp có nghĩa, nói được bước tiếp theo (không phải màn trắng).
|
||||
- [ ] **Đang tải** — có dấu hiệu chuyển động; nút bị vô hiệu hoá để chống bấm đúp.
|
||||
- [ ] **Lỗi** — nói *cái gì hỏng* và *làm gì tiếp*; có đường thử lại; không in nguyên exception.
|
||||
- [ ] **Thành công** — có xác nhận rõ; có undo nếu hành động khó đảo ngược.
|
||||
---
|
||||
|
||||
## B. An toàn dữ liệu
|
||||
## A. Kiểm tra 4 trạng thái chính
|
||||
|
||||
- [ ] Ô nhập dài (instruction, composer, node property, AI Edit) không mất nội dung khi
|
||||
chuyển tab / đóng dialog / đổi project.
|
||||
- [ ] Có dirty-state; `closeEvent` chặn khi còn thay đổi chưa lưu.
|
||||
- [ ] Hành động phá huỷ (xoá project/task, ghi đè file) có xác nhận.
|
||||
- [ ] Xác nhận nêu rõ **cái gì** sẽ mất, không phải "Bạn có chắc không?".
|
||||
- [ ] Nút phá huỷ **không** phải default button, **không** nhận Enter.
|
||||
Đối với mỗi màn hình có dữ liệu hoặc thao tác chạy bất đồng bộ, phải kiểm tra đủ 4 trạng thái:
|
||||
|
||||
## C. Phản hồi theo thời gian
|
||||
### 1. Trạng thái Rỗng (Empty)
|
||||
|
||||
- [ ] 100ms-1s: đổi con trỏ hoặc vô hiệu hoá nút.
|
||||
- [ ] 1s-10s: chỉ báo tiến trình rõ ràng.
|
||||
- [ ] \>10s: có tiến trình, **huỷ được**, không chặn phần còn lại của UI.
|
||||
- [ ] Việc nặng chạy ở service `application/`, không ở GUI thread.
|
||||
- [ ] Bấm hai lần không chạy hai lần (kiểm `connect()` trùng — P10).
|
||||
* [ ] Khi chưa có dữ liệu, màn hình phải hiển thị thông báo có ý nghĩa.
|
||||
|
||||
## D. Khám phá được
|
||||
* [ ] Thông báo phải cho người dùng biết **cần làm gì tiếp theo**.
|
||||
|
||||
- [ ] Mọi nút icon-only có tooltip (nav rail thu gọn, toolbar Co4E, top bar).
|
||||
- [ ] Nút bị vô hiệu hoá nói được **lý do** (mẫu đúng: `app.nav.needs_project`).
|
||||
- [ ] Chức năng chính không bị chôn sau menu chuột phải mà không có lối vào khác.
|
||||
- [ ] Thứ tự control khớp thứ tự người dùng thực hiện.
|
||||
* [ ] Không để màn hình trắng khiến người dùng không biết chuyện gì đang xảy ra.
|
||||
|
||||
## E. Nhất quán
|
||||
### 2. Trạng thái Đang tải (Loading)
|
||||
|
||||
- [ ] Cùng một hành động dùng cùng một từ trên mọi màn (không chỗ "Lưu" chỗ "Cập nhật").
|
||||
- [ ] Vị trí nút chính/phụ giống các dialog khác.
|
||||
- [ ] Chuỗi mới đi qua `tr()` với đủ `en`/`ja`/`vi`.
|
||||
* [ ] Có dấu hiệu rõ ràng cho biết hệ thống đang xử lý, ví dụ loading indicator.
|
||||
|
||||
## F. Phạm vi
|
||||
* [ ] Các nút có thể gây chạy lại cùng một thao tác được vô hiệu hóa trong lúc đang xử lý.
|
||||
|
||||
- [ ] Bản vá chọn mức can thiệp thấp nhất (thêm thông tin trước, đổi luồng sau).
|
||||
- [ ] Thay đổi luồng được đánh dấu là **đề xuất** cần Cowork Team duyệt.
|
||||
- [ ] Có test regression cho signal/state, chạy headless.
|
||||
* [ ] Bấm liên tục hoặc bấm đúp không được tạo ra nhiều request/thao tác giống nhau.
|
||||
|
||||
### 3. Trạng thái Lỗi (Error)
|
||||
|
||||
* [ ] Thông báo lỗi phải cho biết:
|
||||
- **Chuyện gì đã xảy ra.**
|
||||
- **Người dùng cần làm gì tiếp theo.**
|
||||
|
||||
* [ ] Có cách để người dùng **thử lại** khi phù hợp.
|
||||
|
||||
* [ ] Không hiển thị nguyên exception, stack trace hoặc thông tin kỹ thuật khó hiểu cho người dùng.
|
||||
|
||||
### 4. Trạng thái Thành công (Success)
|
||||
|
||||
* [ ] Sau khi thao tác thành công, phải có thông báo/xác nhận rõ ràng.
|
||||
|
||||
* [ ] Với thao tác khó hoặc không thể hoàn tác, phải có cơ chế **Undo** nếu phù hợp.
|
||||
|
||||
---
|
||||
|
||||
## B. Kiểm tra an toàn dữ liệu
|
||||
|
||||
* [ ] Các ô nhập nội dung dài, ví dụ:
|
||||
- Instruction
|
||||
- Composer
|
||||
- Node property
|
||||
- AI Edit
|
||||
|
||||
```
|
||||
không được mất nội dung khi:
|
||||
|
||||
- Chuyển tab.
|
||||
- Đóng/mở dialog.
|
||||
- Đổi project.
|
||||
```
|
||||
|
||||
* [ ] Có cơ chế xác định **dirty-state** khi dữ liệu đã thay đổi nhưng chưa lưu.
|
||||
|
||||
* [ ] `closeEvent` phải cảnh báo hoặc chặn việc đóng màn hình khi vẫn còn thay đổi chưa lưu.
|
||||
|
||||
* [ ] Các thao tác có thể làm mất dữ liệu phải có bước xác nhận, ví dụ:
|
||||
- Xóa project.
|
||||
- Xóa task.
|
||||
- Ghi đè file.
|
||||
|
||||
* [ ] Nội dung xác nhận phải nói rõ **dữ liệu nào sẽ bị mất**.
|
||||
|
||||
```
|
||||
Không dùng thông báo quá chung chung như:
|
||||
|
||||
`"Bạn có chắc không?"`
|
||||
```
|
||||
|
||||
* [ ] Nút thực hiện thao tác phá hủy dữ liệu:
|
||||
- Không được đặt làm **default button**.
|
||||
- Không được thực hiện khi người dùng chỉ nhấn `Enter`.
|
||||
|
||||
---
|
||||
|
||||
## C. Kiểm tra phản hồi theo thời gian
|
||||
|
||||
Phản hồi của UI phải phù hợp với thời gian xử lý:
|
||||
|
||||
* [ ] **100ms – 1s:**
|
||||
Có thể thay đổi con trỏ hoặc vô hiệu hóa nút để người dùng biết thao tác đã được nhận.
|
||||
|
||||
* [ ] **1s – 10s:**
|
||||
Hiển thị chỉ báo tiến trình rõ ràng.
|
||||
|
||||
* [ ] **Trên 10s:**
|
||||
- Có chỉ báo tiến trình.
|
||||
- Người dùng có thể **hủy thao tác** khi phù hợp.
|
||||
- Không khóa toàn bộ UI nếu không cần thiết.
|
||||
|
||||
* [ ] Các tác vụ xử lý nặng không được chạy trực tiếp trên GUI thread.
|
||||
Phải chuyển phần xử lý nặng sang service trong `application/`.
|
||||
|
||||
* [ ] Một thao tác không được chạy hai lần khi người dùng bấm liên tục hoặc bấm đúp.
|
||||
|
||||
* [ ] Kiểm tra các `connect()` có bị đăng ký nhiều lần hay không, đặc biệt với lỗi **P10**.
|
||||
|
||||
---
|
||||
|
||||
## D. Kiểm tra khả năng khám phá chức năng
|
||||
|
||||
Người dùng phải dễ dàng biết **nút này làm gì và tìm chức năng ở đâu**.
|
||||
|
||||
* [ ] Tất cả các nút chỉ có icon (`icon-only`) đều có tooltip.
|
||||
|
||||
```
|
||||
Đặc biệt kiểm tra:
|
||||
- Nav rail khi thu gọn.
|
||||
- Toolbar Co4E.
|
||||
- Top bar.
|
||||
```
|
||||
|
||||
* [ ] Nút đang bị vô hiệu hóa phải cho người dùng biết **tại sao không thể bấm**.
|
||||
|
||||
```
|
||||
Ví dụ sử dụng key:
|
||||
|
||||
`app.nav.needs_project`
|
||||
```
|
||||
|
||||
* [ ] Chức năng chính không được chỉ nằm trong menu chuột phải nếu không có cách truy cập khác.
|
||||
|
||||
* [ ] Thứ tự các control trên màn hình phải phù hợp với **thứ tự người dùng thực hiện công việc**.
|
||||
|
||||
---
|
||||
|
||||
## E. Kiểm tra tính nhất quán
|
||||
|
||||
* [ ] Một hành động phải sử dụng **cùng một thuật ngữ** trên toàn bộ ứng dụng.
|
||||
|
||||
```
|
||||
Ví dụ:
|
||||
|
||||
Nếu dùng `"Lưu"` ở một màn hình thì không nên dùng `"Cập nhật"` ở màn hình khác cho cùng một hành động.
|
||||
```
|
||||
|
||||
* [ ] Vị trí của nút chính và nút phụ phải nhất quán với các dialog khác.
|
||||
|
||||
* [ ] Chuỗi text mới phải sử dụng `tr()`.
|
||||
|
||||
* [ ] Chuỗi mới phải có bản dịch đầy đủ cho:
|
||||
|
||||
```
|
||||
- `en`
|
||||
- `ja`
|
||||
- `vi`
|
||||
```
|
||||
|
||||
* [ ] Không hardcode text mới trực tiếp trong UI code nếu text đó cần hỗ trợ đa ngôn ngữ.
|
||||
|
||||
---
|
||||
|
||||
## F. Kiểm tra phạm vi thay đổi
|
||||
|
||||
* [ ] Bản vá sử dụng **cách can thiệp nhỏ nhất có thể**.
|
||||
|
||||
```
|
||||
Ưu tiên:
|
||||
|
||||
**Bổ sung thông tin → cải thiện feedback → điều chỉnh control → thay đổi flow**
|
||||
|
||||
Không thay đổi cả luồng khi chỉ cần bổ sung thông tin.
|
||||
```
|
||||
|
||||
* [ ] Nếu cần thay đổi flow của người dùng, thay đổi đó phải được ghi rõ là:
|
||||
|
||||
```
|
||||
**ĐỀ XUẤT**
|
||||
```
|
||||
|
||||
* [ ] Agent không tự quyết định thay đổi product/UX quan trọng.
|
||||
|
||||
* [ ] Các thay đổi flow cần được **Cowork Team xem xét và phê duyệt**.
|
||||
|
||||
* [ ] Có regression test kiểm tra:
|
||||
- Signal.
|
||||
- State.
|
||||
- Chuyển trạng thái.
|
||||
- Hành vi của user flow liên quan.
|
||||
|
||||
* [ ] Regression test chạy được ở chế độ headless:
|
||||
|
||||
```
|
||||
`QT_QPA_PLATFORM=offscreen`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Kết luận
|
||||
|
||||
Bản vá UX chỉ nên được đánh giá là đạt khi:
|
||||
|
||||
1. Người dùng biết rõ trạng thái hiện tại của hệ thống.
|
||||
2. Không có nguy cơ mất dữ liệu ngoài ý muốn.
|
||||
3. UI phản hồi phù hợp với thời gian xử lý.
|
||||
4. Chức năng dễ tìm và dễ hiểu.
|
||||
5. Cách gọi tên và cách bố trí control nhất quán.
|
||||
6. Thay đổi flow lớn đã được đánh dấu để Cowork Team phê duyệt.
|
||||
7. Có regression test chứng minh flow vẫn hoạt động đúng.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
description: Điều phối fix bug UI/UX — chấm tier T0/T1/T2/T3 rồi chạy đúng số agent cần thiết
|
||||
argument-hint: <phản ánh của người dùng, dán nguyên văn>
|
||||
---
|
||||
|
||||
Bạn đang chạy với vai **`fix-dispatcher`** — agent hub điều phối của bộ agent trong `agent/`.
|
||||
|
||||
Nạp theo đúng thứ tự rồi làm theo:
|
||||
|
||||
1. @agent/system/guardrail.md
|
||||
2. @agent/system/security.md
|
||||
3. @agent/system/response_policy.md
|
||||
4. @agent/roles/0_fix_dispatcher.md
|
||||
5. @agent/output/dispatch_plan.md
|
||||
|
||||
Phản ánh cần xử lý:
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
Trình tự bắt buộc:
|
||||
|
||||
- Tách defect (Bước 1) → xét override bảo mật (Bước 2) → chấm tier (Bước 3).
|
||||
- Trần chấm điểm: **≤ 5 lệnh đọc/grep, 0 subagent**. Hết mà chưa chấm được → T2.
|
||||
- In `dispatch_plan` (≤ 30 dòng phần người đọc) **trước** khi chạy bất kỳ agent nào.
|
||||
- Rồi chạy đúng lane ở bảng Bước 4:
|
||||
- **T0** → tự sửa, sau đó chạy đủ 4 cổng máy ở §4.1 và dán output thật.
|
||||
- **T1** → gọi `fix-implementer`, rồi tự review bằng @agent/checklist/ui_review.md.
|
||||
- **T2** → specialist → `fix-implementer` → `regression-reviewer`.
|
||||
- **T3** → `ui-bug-triage` → specialist → `fix-implementer` → `regression-reviewer`.
|
||||
- **T3-SEC** → `security-defect-fixer`, dừng chờ Cowork Team trả 4 câu chính sách.
|
||||
- Các `defect_id` độc lập gọi song song trong **một** message. Các bước trong cùng một
|
||||
`defect_id` chạy tuần tự.
|
||||
- Escalate theo Bước 5. Tier chỉ đi lên. Không tự merge (`guardrail.md` G9).
|
||||
+354
-52
@@ -1,71 +1,373 @@
|
||||
# i18n — luật chuỗi hiển thị
|
||||
# i18n — Quy tắc xử lý chuỗi hiển thị
|
||||
|
||||
Nguồn: docstring `i18n/__init__.py`.
|
||||
**Nguồn:** docstring `i18n/__init__.py`
|
||||
|
||||
---
|
||||
|
||||
## 1. Ba ngôn ngữ, mặc định tiếng Việt
|
||||
## 1. Ngôn ngữ được hỗ trợ
|
||||
|
||||
Cowork Local hỗ trợ 3 ngôn ngữ:
|
||||
|
||||
```python
|
||||
LANGUAGES = {"en": "English", "ja": "日本語", "vi": "Tiếng Việt"}
|
||||
LANGUAGE_SHORT = {"en": "EN", "ja": "JP", "vi": "VN"} # switcher gọn ở top bar
|
||||
LANGUAGES = {
|
||||
"en": "English",
|
||||
"ja": "日本語",
|
||||
"vi": "Tiếng Việt",
|
||||
}
|
||||
|
||||
LANGUAGE_SHORT = {
|
||||
"en": "EN",
|
||||
"ja": "JP",
|
||||
"vi": "VN",
|
||||
}
|
||||
|
||||
DEFAULT_LANGUAGE = "vi"
|
||||
```
|
||||
|
||||
`tr(key, **kwargs)` trả chuỗi theo ngôn ngữ hiện tại, fallback lần lượt:
|
||||
**ngôn ngữ hiện tại → `en` → chính cái key**. Nghĩa là thiếu entry thì UI hiện ra
|
||||
`workspace.tab_folder` chứ không crash — nếu người dùng chụp màn hình có chuỗi dạng
|
||||
`a.b_c` thì đó chính là triệu chứng thiếu key.
|
||||
Ngôn ngữ mặc định là **Tiếng Việt (`vi`)**.
|
||||
|
||||
`.format(**kwargs)` được áp dụng khi có placeholder: `tr("composer.attachments", n=3)`.
|
||||
### Hàm `tr()`
|
||||
|
||||
## 2. Widget nào phải đăng ký callback
|
||||
Sử dụng:
|
||||
|
||||
| Loại widget | Cách xử lý |
|
||||
|---|---|
|
||||
| **Sống lâu** — chrome cửa sổ chính, tab, sidebar, composer | Đăng ký `on_language_changed(cb)`; `cb` áp lại `tr()` cho chính widget đó. Callback chạy **ngay một lần** và mỗi lần đổi ngôn ngữ |
|
||||
| **Tạm thời** — Settings, Skills, Flow, Permission dialog | Dựng lại từ đầu mỗi lần mở, nên chỉ cần gọi `tr()` lúc construct, **không** đăng ký |
|
||||
|
||||
Quy ước đặt tên hàm callback trong repo: `_retranslate()` / `_apply_i18n()` — xem
|
||||
`ui/workspace_tab.py:484` trở đi làm mẫu chuẩn.
|
||||
|
||||
**Bug điển hình:** "Đổi ngôn ngữ nhưng nhãn X không đổi" → widget sống lâu mà quên đăng ký,
|
||||
hoặc có đăng ký nhưng callback bỏ sót đúng nhãn đó. Không sửa bằng cách gọi `tr()` lại ở
|
||||
chỗ khác — sửa trong callback.
|
||||
|
||||
## 3. File từ điển
|
||||
|
||||
`i18n/` chia theo màn hình, không phải một file khổng lồ:
|
||||
|
||||
```text
|
||||
i18n/login_dialog.py i18n/sidebar.py i18n/composer.py
|
||||
i18n/cowork_tab.py i18n/settings_dialog.py i18n/skills_dialog.py
|
||||
i18n/libreoffice_view.py i18n/agents_admin_tab.py i18n/monitoring_overview.py
|
||||
i18n/hint.py
|
||||
```python
|
||||
tr(key, **kwargs)
|
||||
```
|
||||
|
||||
Mỗi file export dict `key -> {"en":..., "ja":..., "vi":...}`, được `i18n/__init__.py`
|
||||
import và gộp lại. Thêm key mới:
|
||||
để lấy chuỗi hiển thị theo ngôn ngữ hiện tại.
|
||||
|
||||
1. Chọn đúng file theo màn hình (không nhét đại vào `login_dialog.py` chỉ vì nó lớn nhất).
|
||||
2. Điền **đủ 3 ngôn ngữ**. Thiếu `ja` là lỗi hay gặp nhất và chỉ lộ ra khi khách Nhật dùng.
|
||||
3. Đặt key theo `<màn>.<thành_phần>` — `workspace.tab_folder`, `app.nav.recents`.
|
||||
Thứ tự fallback:
|
||||
|
||||
## 4. Rủi ro riêng của tiếng Nhật và tiếng Việt
|
||||
```text
|
||||
Ngôn ngữ hiện tại → English (en) → chính key
|
||||
```
|
||||
|
||||
| Rủi ro | Triệu chứng | Cách xử lý |
|
||||
|---|---|---|
|
||||
| Tiếng Nhật ngắn hơn, tiếng Việt dài hơn tiếng Anh | Nút vừa với `EN`, tràn với `VI`; label bị `...` với `JA` | Không `setFixedWidth` theo chuỗi tiếng Anh. Dùng `sizeHint` + `minimumWidth`, hoặc cho phép wrap |
|
||||
| Dấu tiếng Việt bị cắt phần trên/dưới | `Ắ`, `ộ` mất dấu ở nhãn cao cố định | Không đặt `setFixedHeight` cho label theo pixel; để layout tự tính |
|
||||
| Font mặc định thiếu glyph Nhật | Ô vuông tofu `□□□` trên máy chưa cài font | Kiểm tra `_FONT` trong `theme/palettes.py`, khai báo fallback |
|
||||
| Sắp xếp / so sánh chuỗi | Danh sách project sắp sai với tên có dấu | Dùng `locale`-aware sort, không `sorted()` thô |
|
||||
| Chiều dài chuỗi tính bằng ký tự ≠ chiều rộng hiển thị | Elide sai với chữ Nhật | Đo bằng `QFontMetrics.horizontalAdvance`, không `len()` |
|
||||
Ví dụ, nếu đang dùng tiếng Nhật nhưng key `workspace.tab_folder` chưa có bản dịch tiếng Nhật:
|
||||
|
||||
## 5. Checklist sửa bug i18n
|
||||
```text
|
||||
JA → EN → workspace.tab_folder
|
||||
```
|
||||
|
||||
- [ ] Key mới có đủ `en` / `ja` / `vi`?
|
||||
- [ ] Đã thử đổi qua cả 3 ngôn ngữ **trong lúc app đang chạy** (không phải restart)?
|
||||
- [ ] Widget sống lâu đã đăng ký `on_language_changed`?
|
||||
- [ ] Không còn chuỗi hardcode nào trong bản vá?
|
||||
- [ ] Layout còn đúng với chuỗi dài nhất trong 3 ngôn ngữ?
|
||||
- [ ] Không dùng `len()` để đo bề rộng chữ?
|
||||
Ứng dụng **không được crash** chỉ vì thiếu bản dịch.
|
||||
|
||||
Nếu UI hiển thị một chuỗi dạng:
|
||||
|
||||
```text
|
||||
workspace.tab_folder
|
||||
```
|
||||
|
||||
thì đây là dấu hiệu cho thấy **đang thiếu translation key**.
|
||||
|
||||
### Placeholder
|
||||
|
||||
Nếu chuỗi có placeholder, truyền giá trị thông qua `kwargs`:
|
||||
|
||||
```python
|
||||
tr("composer.attachments", n=3)
|
||||
```
|
||||
|
||||
Việc `.format(**kwargs)` được thực hiện sau khi lấy chuỗi dịch.
|
||||
|
||||
---
|
||||
|
||||
## 2. Widget nào phải cập nhật khi đổi ngôn ngữ?
|
||||
|
||||
Có 2 loại widget:
|
||||
|
||||
| Loại widget | Cách xử lý |
|
||||
| ------------------- | ----------------------------------------------------- |
|
||||
| **Widget sống lâu** | `bind_*` cho chuỗi tĩnh; `on_language_changed(cb)` cho phần còn lại |
|
||||
| **Widget tạm thời** | Không cần đăng ký callback; gọi `tr()` khi tạo widget |
|
||||
|
||||
### 2.0. `bind_*` — cách mặc định cho chuỗi tĩnh
|
||||
|
||||
`w.setToolTip(tr("k"))` chỉ đúng ở đúng thời điểm chạy dòng đó. `bind_*` gộp "gán ngay"
|
||||
và "gán lại sau mỗi lần đổi ngôn ngữ" vào một lời gọi, dùng `weakref` nên không giữ widget
|
||||
sống thêm và tự dọn khi widget bị xoá:
|
||||
|
||||
```python
|
||||
from ...i18n import bind_dynamic, bind_items, bind_placeholder, bind_text, bind_tip
|
||||
|
||||
self.save_btn = bind_text(QPushButton(), "co4e.save") # thay QPushButton(tr(...))
|
||||
bind_tip(self.save_btn, "co4e.tt_save") # thay .setToolTip(tr(...))
|
||||
bind_placeholder(self.chat_input, "co4e.chat_placeholder")
|
||||
bind_items(self.perm_combo, [f"co4e.perm.{p}" for p in PERMISSION_PRESETS])
|
||||
form.addRow(bind_text(QLabel(), "co4e.f_label"), self.label_edit) # KHÔNG addRow(tr(...))
|
||||
```
|
||||
|
||||
Ba luật:
|
||||
|
||||
1. **Chuỗi tĩnh → `bind_*`.** Đổi tại chỗ, **không thêm dòng** — quan trọng với file đã
|
||||
sát trần Gate S hoặc đang bị bánh cóc `LEGACY_ALLOWANCE` chốt (`quality_gates.md` §4).
|
||||
2. **Chữ phụ thuộc trạng thái → `bind_dynamic(w, setter, fn)`**, với `fn` đọc trạng thái:
|
||||
nút Chạy ⇄ Dừng, tooltip Thu gọn ⇄ Mở rộng, nhãn có số đếm. Các nhánh xử lý trạng thái
|
||||
**vẫn** gọi setter trực tiếp như cũ để phản hồi ngay khi bấm; `bind_dynamic` chỉ lo lúc
|
||||
đổi ngôn ngữ. Bind cứng một nhãn động sẽ **xoá** trạng thái khi người dùng đổi ngôn ngữ
|
||||
giữa lúc đang chạy.
|
||||
3. **Chữ là DỮ LIỆU thì không bind.** Tên agent, tên project, tên nhà cung cấp trong
|
||||
`config.PROVIDER_LABELS` — dịch danh tính là sai.
|
||||
|
||||
`QFormLayout.addRow(tr(...), w)` và `_add_section(outer, tr(...))` là hai bẫy hay gặp:
|
||||
chúng tự dựng `QLabel` bên trong, không giữ tham chiếu nào để áp lại. Truyền
|
||||
`bind_text(QLabel(), key)` hoặc truyền **khoá** thay vì chuỗi đã dịch.
|
||||
|
||||
### 2.1. Widget sống lâu
|
||||
|
||||
Ví dụ:
|
||||
|
||||
* Chrome của cửa sổ chính.
|
||||
* Tab.
|
||||
* Sidebar.
|
||||
* Composer.
|
||||
|
||||
Các widget này vẫn tồn tại khi người dùng đổi ngôn ngữ.
|
||||
|
||||
Vì vậy phải:
|
||||
|
||||
1. Đăng ký `on_language_changed(cb)`.
|
||||
2. Trong callback, gọi lại `tr()` cho các text của chính widget.
|
||||
3. Callback phải chạy:
|
||||
|
||||
* Một lần ngay khi đăng ký.
|
||||
* Mỗi lần người dùng đổi ngôn ngữ.
|
||||
|
||||
Tên callback được sử dụng trong repo:
|
||||
|
||||
```text
|
||||
_retranslate()
|
||||
_apply_i18n()
|
||||
```
|
||||
|
||||
Có thể tham khảo implementation chuẩn từ:
|
||||
|
||||
```text
|
||||
ui/workspace_tab.py:484
|
||||
```
|
||||
|
||||
### 2.2. Widget tạm thời
|
||||
|
||||
Ví dụ:
|
||||
|
||||
* Settings dialog.
|
||||
* Skills dialog.
|
||||
* Flow dialog.
|
||||
* Permission dialog.
|
||||
|
||||
Các dialog này được tạo lại từ đầu mỗi lần mở.
|
||||
|
||||
Vì vậy chỉ cần gọi `tr()` khi construct widget.
|
||||
|
||||
**Không cần đăng ký `on_language_changed()`**.
|
||||
|
||||
### Bug thường gặp
|
||||
|
||||
Triệu chứng:
|
||||
|
||||
> Đổi ngôn ngữ nhưng một label/nút vẫn giữ ngôn ngữ cũ.
|
||||
|
||||
Nguyên nhân thường là:
|
||||
|
||||
* Widget sống lâu nhưng chưa đăng ký `on_language_changed()`.
|
||||
* Callback có đăng ký nhưng quên cập nhật label đó.
|
||||
|
||||
**Cách sửa đúng:**
|
||||
|
||||
`bind_*` tại chính dòng đang gán (mục 2.0), hoặc — nếu chữ phụ thuộc trạng thái/dữ liệu —
|
||||
sửa trong `_retranslate()` / `_apply_i18n()` của chính widget.
|
||||
|
||||
**Không** giải quyết bằng cách gọi `tr()` ở một nơi khác chỉ để ép label thay đổi.
|
||||
|
||||
### Cách TÌM ra hết các chỗ bị lỗi
|
||||
|
||||
Đừng grep chuỗi tiếng Việt trong source: lượt audit tháng 9/2026 grep ra 962 dòng mà
|
||||
**không dòng nào** là lỗi thật (toàn docstring), trong khi 84 lỗi thật lại không xuất hiện
|
||||
— vì chúng đi qua `tr()` đúng cách, chỉ thiếu người áp lại.
|
||||
|
||||
Phép đo đúng nằm ở `tests/ui/test_i18n_khong_con_chu_cu.py`: dựng `MainWindow` thật, thay
|
||||
`tr()` bằng chuỗi **mốc**, gọi `set_language()`, rồi tìm chỗ **không** mang mốc. Hai chi
|
||||
tiết mà bản kiểm ngây thơ sẽ sai:
|
||||
|
||||
* `from ...i18n import tr` copy tham chiếu vào namespace từng module → phải thay `tr` ở
|
||||
**mọi** module đã import, không chỉ `i18n.tr`;
|
||||
* lưới vẽ lại bằng `deleteLater()` để lại widget cũ còn sống → không
|
||||
`sendPostedEvents(DeferredDelete)` thì báo oan hàng chục widget bóng ma (lượt audit đầu
|
||||
báo 84 lỗi, trong đó 65 là bóng ma và widget bị `id()` cấp lại làm cắt vòng quét).
|
||||
|
||||
Chạy: `QT_QPA_PLATFORM=offscreen pytest tests/ui/test_i18n_khong_con_chu_cu.py -q`
|
||||
|
||||
---
|
||||
|
||||
## 3. Tổ chức file translation
|
||||
|
||||
Thư mục `i18n/` được chia theo **màn hình/chức năng**, không gom tất cả translation vào một file lớn.
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
i18n/
|
||||
├── login_dialog.py
|
||||
├── sidebar.py
|
||||
├── composer.py
|
||||
├── cowork_tab.py
|
||||
├── settings_dialog.py
|
||||
├── skills_dialog.py
|
||||
├── libreoffice_view.py
|
||||
├── agents_admin_tab.py
|
||||
├── monitoring_overview.py
|
||||
└── hint.py
|
||||
```
|
||||
|
||||
Mỗi file export một dictionary có dạng:
|
||||
|
||||
```text
|
||||
key → {
|
||||
"en": "...",
|
||||
"ja": "...",
|
||||
"vi": "..."
|
||||
}
|
||||
```
|
||||
|
||||
`i18n/__init__.py` sẽ import và gộp các dictionary này.
|
||||
|
||||
### Khi thêm key mới
|
||||
|
||||
Thực hiện theo 3 bước:
|
||||
|
||||
#### Bước 1 — Chọn đúng file
|
||||
|
||||
Đưa key vào file tương ứng với màn hình/chức năng.
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
workspace.* → file liên quan đến workspace
|
||||
composer.* → composer.py
|
||||
settings.* → settings_dialog.py
|
||||
```
|
||||
|
||||
**Không** đưa key vào `login_dialog.py` chỉ vì file đó đang có nhiều key nhất.
|
||||
|
||||
#### Bước 2 — Điền đủ 3 ngôn ngữ
|
||||
|
||||
Mỗi key mới phải có:
|
||||
|
||||
```text
|
||||
en
|
||||
ja
|
||||
vi
|
||||
```
|
||||
|
||||
Thiếu `ja` là lỗi đặc biệt cần chú ý vì có thể chỉ được phát hiện khi khách hàng Nhật sử dụng.
|
||||
|
||||
#### Bước 3 — Đặt tên key nhất quán
|
||||
|
||||
Format khuyến nghị:
|
||||
|
||||
```text
|
||||
<màn hình>.<thành phần>
|
||||
```
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
workspace.tab_folder
|
||||
app.nav.recents
|
||||
```
|
||||
|
||||
Tên key phải mô tả rõ nó được dùng ở đâu và cho thành phần nào.
|
||||
|
||||
---
|
||||
|
||||
## 4. Các rủi ro thường gặp với tiếng Nhật và tiếng Việt
|
||||
|
||||
| Vấn đề | Triệu chứng | Cách xử lý |
|
||||
| ---------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| Độ dài chuỗi khác nhau | EN vừa nút nhưng VI bị tràn hoặc JA bị `...` | Không đặt width cố định dựa trên tiếng Anh. Dùng `sizeHint()`, `minimumWidth` hoặc cho phép wrap |
|
||||
| Dấu tiếng Việt bị cắt | Các chữ như `Ắ`, `ộ` bị mất dấu | Không dùng `setFixedHeight()` cho label. Để layout tự tính chiều cao |
|
||||
| Thiếu font/glyph tiếng Nhật | Xuất hiện `□□□` | Kiểm tra `_FONT` trong `theme/palettes.py` và khai báo font fallback |
|
||||
| Sắp xếp chuỗi | Project có dấu được sắp xếp không đúng | Dùng locale-aware sorting, không dùng `sorted()` một cách máy móc |
|
||||
| Số ký tự không phản ánh chiều rộng | Text bị elide sai, đặc biệt với tiếng Nhật | Dùng `QFontMetrics.horizontalAdvance()`, không dùng `len()` để đo chiều rộng |
|
||||
|
||||
### Đặc biệt lưu ý về độ dài text
|
||||
|
||||
Không được giả định:
|
||||
|
||||
```text
|
||||
số ký tự = chiều rộng hiển thị
|
||||
```
|
||||
|
||||
Ví dụ hai chuỗi có cùng số ký tự nhưng có thể có chiều rộng hiển thị khác nhau.
|
||||
|
||||
Khi cần đo text trên UI, dùng:
|
||||
|
||||
```python
|
||||
QFontMetrics.horizontalAdvance(...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Checklist khi sửa lỗi i18n
|
||||
|
||||
Trước khi hoàn thành bản vá i18n, phải kiểm tra:
|
||||
|
||||
* [ ] Key mới có đủ **`en` / `ja` / `vi`**?
|
||||
|
||||
* [ ] Đã chuyển qua cả 3 ngôn ngữ **ngay trong lúc app đang chạy** chưa?
|
||||
|
||||
```
|
||||
Không chỉ restart app rồi kiểm tra.
|
||||
```
|
||||
|
||||
* [ ] `ja` có **khác** `en` không? Bằng nhau nghĩa là chưa dịch — trừ tên thương hiệu /
|
||||
ký hiệu, và khi đó phải khai vào `KHOA_KHONG_CAN_DICH` kèm lý do.
|
||||
|
||||
* [ ] Chuỗi tĩnh đã dùng `bind_text` / `bind_tip` / `bind_placeholder` / `bind_items`
|
||||
thay cho `setX(tr(...))` một lần?
|
||||
|
||||
* [ ] Chữ phụ thuộc trạng thái đã dùng `bind_dynamic` (không bind cứng, kẻo mất trạng thái)?
|
||||
|
||||
* [ ] Nếu widget sống lâu và còn phần không bind được, đã đăng ký:
|
||||
|
||||
```
|
||||
`on_language_changed(...)`
|
||||
```
|
||||
|
||||
* [ ] Callback `_retranslate()` hoặc `_apply_i18n()` đã cập nhật **tất cả text liên quan**?
|
||||
|
||||
* [ ] Đã chạy `pytest tests/ui/test_i18n_khong_con_chu_cu.py -q` và nó **xanh**?
|
||||
|
||||
* [ ] Không còn chuỗi hardcode mới trong bản vá?
|
||||
|
||||
* [ ] Layout vẫn đúng với **chuỗi dài nhất** trong 3 ngôn ngữ?
|
||||
|
||||
* [ ] Không dùng `len()` để tính chiều rộng text?
|
||||
|
||||
* [ ] Nếu có thay đổi UI, đã kiểm tra cả Dark Mode và Light Mode?
|
||||
|
||||
---
|
||||
|
||||
## 6. Nguyên tắc quan trọng
|
||||
|
||||
Khi sửa lỗi i18n, **không sửa triệu chứng ở nơi khác**.
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
Đổi ngôn ngữ
|
||||
↓
|
||||
Label X không thay đổi
|
||||
↓
|
||||
Kiểm tra widget X
|
||||
↓
|
||||
Widget sống lâu?
|
||||
↓
|
||||
Có on_language_changed()?
|
||||
↓
|
||||
_retranslate() có cập nhật Label X?
|
||||
```
|
||||
|
||||
Nếu thiếu callback hoặc callback bỏ sót label, hãy sửa **đúng callback của widget đó**.
|
||||
|
||||
Không thêm các lệnh `tr()` rải rác ở nơi khác chỉ để làm cho UI thay đổi.
|
||||
|
||||
Mục tiêu là đảm bảo cơ chế i18n hoạt động đúng và nhất quán cho toàn bộ ứng dụng.
|
||||
|
||||
+439
-54
@@ -1,95 +1,480 @@
|
||||
# Screen Map — dịch lời người dùng thành file:line
|
||||
# Screen Map — Tra mô tả của người dùng về đúng file:line
|
||||
|
||||
Người dùng báo lỗi bằng lời ("cái bảng bên phải màn thống kê"). File này để agent
|
||||
Triage quy nó về đúng widget.
|
||||
Người dùng thường mô tả lỗi bằng ngôn ngữ tự nhiên, ví dụ:
|
||||
|
||||
> "Cái bảng bên phải của màn thống kê bị lệch."
|
||||
|
||||
Agent phải dùng file này để chuyển mô tả đó thành:
|
||||
|
||||
```text
|
||||
Màn hình → Tab/View → Widget → File → Line → Control
|
||||
```
|
||||
|
||||
Mục tiêu là tìm được **đúng widget và đúng vị trí code**, thay vì đoán file dựa trên tên.
|
||||
|
||||
---
|
||||
|
||||
## 1. Nav rail — bốn màn chính
|
||||
## 1. Bốn màn hình chính trong Nav Rail
|
||||
|
||||
Định nghĩa tại `presentation/shell/main_window.py:151` (`_nav_defs`), thứ tự = page index:
|
||||
Các màn hình chính được định nghĩa tại:
|
||||
|
||||
| Row | i18n key | Icon | Dựng | Widget |
|
||||
|---|---|---|---|---|
|
||||
| 0 | `app.tab.dashboard` | `dashboard` | lười | `presentation/dashboard/dashboard_tab.py::DashboardTab` |
|
||||
| 1 | `app.tab.schedule` | `schedule` | lười | `presentation/scheduling/schedule_task_tab.py::ScheduleTaskTab` |
|
||||
| 2 | `app.tab.workspace` | `workspaces` | **ngay** (màn HOME) | `ui/workspace_tab.py::WorkspaceTab` |
|
||||
| 3 | `app.tab.monitoring` | `monitoring` | lười | `ui/monitoring_tab.py::MonitoringTab` |
|
||||
```text
|
||||
presentation/shell/main_window.py:151
|
||||
```
|
||||
|
||||
App mở lên là ở **Workspace ▸ Project**.
|
||||
Danh sách nằm trong `_nav_defs`.
|
||||
|
||||
## 2. Sub-tab của Workspace
|
||||
**Thứ tự trong bảng chính là page index.**
|
||||
|
||||
`ui/workspace_tab.py:214-245`:
|
||||
| Row | i18n key | Icon | Cách tạo | Widget |
|
||||
| --: | -------------------- | ------------ | --------------- | --------------------------------------------------------------- |
|
||||
| 0 | `app.tab.dashboard` | `dashboard` | Lazy | `presentation/dashboard/dashboard_tab.py::DashboardTab` |
|
||||
| 1 | `app.tab.schedule` | `schedule` | Lazy | `presentation/scheduling/schedule_task_tab.py::ScheduleTaskTab` |
|
||||
| 2 | `app.tab.workspace` | `workspaces` | Ngay khi mở app | `ui/workspace_tab.py::WorkspaceTab` |
|
||||
| 3 | `app.tab.monitoring` | `monitoring` | Lazy | `ui/monitoring_tab.py::MonitoringTab` |
|
||||
|
||||
| Tab | i18n key | Widget |
|
||||
|---|---|---|
|
||||
| Project | `workspace.tab_project` | `_build_project_tab()` trong chính file đó |
|
||||
### Màn hình mặc định
|
||||
|
||||
Khi mở app, người dùng bắt đầu tại:
|
||||
|
||||
```text
|
||||
Workspace → Project
|
||||
```
|
||||
|
||||
### Lưu ý về Lazy
|
||||
|
||||
`Dashboard`, `Schedule` và `Monitoring` được tạo **lazy** — chỉ được dựng khi người dùng mở màn hình.
|
||||
|
||||
Vì vậy, khi điều tra lỗi liên quan đến các màn hình này, phải kiểm tra cả **thời điểm widget được tạo** và **vòng đời của widget**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Các tab bên trong Workspace
|
||||
|
||||
Các tab được định nghĩa trong:
|
||||
|
||||
```text
|
||||
ui/workspace_tab.py:214-245
|
||||
```
|
||||
|
||||
| Tab | i18n key | Widget/File |
|
||||
| -------- | ------------------------ | -------------------------------------------------- |
|
||||
| Project | `workspace.tab_project` | `_build_project_tab()` trong `ui/workspace_tab.py` |
|
||||
| Cowork | `workspace.tab_cowork` | `ui/cowork_tab.py` |
|
||||
| Co4E | `workspace.tab_co4e` | `ui/co4e_tab.py` → `presentation/co4e/` |
|
||||
| Folder | `workspace.tab_folder` | `presentation/folder/folder_tab.py` |
|
||||
| GraphRAG | `workspace.tab_graphrag` | `presentation/graph/structure_graph_view.py` |
|
||||
|
||||
Monitoring **giữ tab strip riêng** với 8 sub-view (tổng quan, trạng thái agent, công cụ,
|
||||
nhật ký hành động, lịch sử gọi MCP, sự kiện bảo mật, agents admin, icon). Workspace là màn
|
||||
duy nhất giấu tab strip đi.
|
||||
### Monitoring có cấu trúc khác
|
||||
|
||||
## 3. Thành phần luôn nổi trên mọi màn
|
||||
Monitoring có **tab strip riêng**, gồm 8 sub-view:
|
||||
|
||||
| Thành phần | File | Triệu chứng người dùng hay mô tả |
|
||||
|---|---|---|
|
||||
| Nav rail trái, nút thu gọn | `presentation/shell/nav_rail.py` | "menu bị co lại", "không thấy tên project" |
|
||||
| Top bar (theme, ngôn ngữ) | `presentation/shell/top_bar.py` | "đổi giao diện không ăn" |
|
||||
| Toast góc trên trái | `presentation/shell/toast.py` | "thông báo xong việc che mất nút" |
|
||||
| Help agent nổi góc dưới phải | `ui/help_agent_widget.py` | "con robot che nút gửi" |
|
||||
| Status bar dưới cùng | `main_window.statusBar()` | "dòng chữ dưới đáy không đổi" |
|
||||
1. Tổng quan.
|
||||
2. Trạng thái Agent.
|
||||
3. Công cụ.
|
||||
4. Nhật ký hành động.
|
||||
5. Lịch sử gọi MCP.
|
||||
6. Sự kiện bảo mật.
|
||||
7. Agents Admin.
|
||||
8. Icon.
|
||||
|
||||
## 4. Dialog
|
||||
**Workspace là màn hình duy nhất không hiển thị tab strip theo cách này.**
|
||||
|
||||
`ui/`: `login_dialog.py`, `permission_dialog.py`, `settings_dialog.py`, `skills_dialog.py`,
|
||||
`task_editor_dialog.py`, `file_edit_dialog.py`, `flow_dialog.py`, `mcp_servers_dialog.py`,
|
||||
`co4e_agent_dialog.py`, `ext_connector_dialog.py`.
|
||||
Nếu người dùng nói:
|
||||
|
||||
## 5. 🔎 Hai file tra cứu bắt buộc dùng
|
||||
> "Tab trạng thái agent trong màn Monitoring"
|
||||
|
||||
### `docs/screens/manifest.json`
|
||||
thì không được nhầm nó với một tab của Workspace.
|
||||
|
||||
Mỗi màn đã chụp ảnh có một entry: `slug`, `title`, `theme`, `note` (**đúng `file.py:line`
|
||||
nơi màn đó được dựng**), `file` (ảnh), `nav`.
|
||||
---
|
||||
|
||||
## 3. Các thành phần luôn xuất hiện trên mọi màn hình
|
||||
|
||||
Một số thành phần nằm ngoài nội dung của từng màn hình.
|
||||
|
||||
| Thành phần | File | Cách người dùng thường mô tả |
|
||||
| ------------------------------- | -------------------------------- | -------------------------------------------------- |
|
||||
| Nav rail bên trái / nút thu gọn | `presentation/shell/nav_rail.py` | "Menu bị co lại", "Không thấy tên project" |
|
||||
| Top bar / theme / ngôn ngữ | `presentation/shell/top_bar.py` | "Đổi giao diện không ăn", "Đổi ngôn ngữ không đổi" |
|
||||
| Toast góc trên trái | `presentation/shell/toast.py` | "Thông báo xong việc che mất nút" |
|
||||
| Help Agent góc dưới phải | `ui/help_agent_widget.py` | "Con robot che nút gửi" |
|
||||
| Status bar phía dưới | `main_window.statusBar()` | "Dòng chữ dưới đáy không đổi" |
|
||||
|
||||
### Quy tắc
|
||||
|
||||
Nếu người dùng mô tả một thành phần thuộc nhóm trên, **không cần tìm sub-tab trước**.
|
||||
|
||||
Hãy kiểm tra trực tiếp file tương ứng.
|
||||
|
||||
---
|
||||
|
||||
## 4. Các Dialog
|
||||
|
||||
Các dialog chính nằm trong `ui/`:
|
||||
|
||||
```text
|
||||
ui/
|
||||
├── login_dialog.py
|
||||
├── permission_dialog.py
|
||||
├── settings_dialog.py
|
||||
├── skills_dialog.py
|
||||
├── task_editor_dialog.py
|
||||
├── file_edit_dialog.py
|
||||
├── flow_dialog.py
|
||||
├── mcp_servers_dialog.py
|
||||
├── co4e_agent_dialog.py
|
||||
└── ext_connector_dialog.py
|
||||
```
|
||||
|
||||
Ví dụ:
|
||||
|
||||
> "Khi mở Permission thì nút Allow bị..."
|
||||
|
||||
→ kiểm tra trước:
|
||||
|
||||
```text
|
||||
ui/permission_dialog.py
|
||||
```
|
||||
|
||||
Không tự động tìm trong `presentation/` chỉ vì lỗi xảy ra trên UI.
|
||||
|
||||
---
|
||||
|
||||
# 5. Hai file tra cứu bắt buộc
|
||||
|
||||
Khi cần chuyển mô tả của người dùng thành `file:line`, phải ưu tiên sử dụng:
|
||||
|
||||
```text
|
||||
docs/screens/manifest.json
|
||||
docs/screens/controls.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.1. `docs/screens/manifest.json`
|
||||
|
||||
File này chứa thông tin về các màn hình đã được chụp screenshot.
|
||||
|
||||
Mỗi màn hình có các thông tin chính:
|
||||
|
||||
```text
|
||||
slug
|
||||
title
|
||||
theme
|
||||
note
|
||||
file
|
||||
nav
|
||||
```
|
||||
|
||||
Trong đó:
|
||||
|
||||
* `slug` — tên định danh của màn hình.
|
||||
* `title` — tên hiển thị.
|
||||
* `theme` — Dark hoặc Light.
|
||||
* `note` — **vị trí code dựng màn hình (`file.py:line`)**.
|
||||
* `file` — đường dẫn đến screenshot.
|
||||
* `nav` — màn hình thuộc nav nào.
|
||||
|
||||
### Ví dụ
|
||||
|
||||
Người dùng nói:
|
||||
|
||||
> "Màn Kanban lịch trình bị lỗi."
|
||||
|
||||
Có thể tìm màn hình liên quan bằng:
|
||||
|
||||
```bash
|
||||
# Người dùng nói "màn Kanban lịch trình"
|
||||
python -c "import json;print([e for e in json.load(open('docs/screens/manifest.json')) if 'schedule' in e['slug']])"
|
||||
```
|
||||
|
||||
Ảnh có **cả bản dark và light** (`*-dark.png` / `*-light.png`) — dùng để đối chiếu trước/sau
|
||||
và để kiểm tra bug chỉ xảy ra ở một theme.
|
||||
Sau đó lấy `note` để biết:
|
||||
|
||||
### `docs/screens/controls.json`
|
||||
```text
|
||||
file.py:line
|
||||
```
|
||||
|
||||
Danh mục **mọi control** đã trích tự động từ source: `file`, `var`, `type` (`QLineEdit`...),
|
||||
`kind` (mô tả tiếng Việt: "ô nhập", "nút"...), `label`, `line`, `signals`, `object_name`.
|
||||
### Screenshot Dark và Light
|
||||
|
||||
Mỗi màn hình thường có hai ảnh:
|
||||
|
||||
```text
|
||||
<slug>-dark.png
|
||||
<slug>-light.png
|
||||
```
|
||||
|
||||
Dùng hai ảnh này để:
|
||||
|
||||
* So sánh trước/sau.
|
||||
* Kiểm tra lỗi chỉ xảy ra ở một theme.
|
||||
* Kiểm tra sự khác biệt giữa Dark Mode và Light Mode.
|
||||
|
||||
---
|
||||
|
||||
## 5.2. `docs/screens/controls.json`
|
||||
|
||||
Đây là danh sách các control được trích tự động từ source code.
|
||||
|
||||
Mỗi control có thông tin như:
|
||||
|
||||
```text
|
||||
file
|
||||
var
|
||||
type
|
||||
kind
|
||||
label
|
||||
line
|
||||
signals
|
||||
object_name
|
||||
```
|
||||
|
||||
Trong đó:
|
||||
|
||||
* `file` — file chứa control.
|
||||
* `var` — tên biến.
|
||||
* `type` — loại widget, ví dụ `QLineEdit`.
|
||||
* `kind` — mô tả dễ hiểu, ví dụ `"ô nhập"`, `"nút"`.
|
||||
* `label` — text/label liên quan.
|
||||
* `line` — dòng code.
|
||||
* `signals` — signal liên quan.
|
||||
* `object_name` — `objectName` của widget.
|
||||
|
||||
### Ví dụ
|
||||
|
||||
Người dùng nói:
|
||||
|
||||
> "Ô nhập email trong màn tài khoản bị lỗi."
|
||||
|
||||
Có thể tìm control bằng:
|
||||
|
||||
```bash
|
||||
# Người dùng nói "ô nhập email trong màn tài khoản"
|
||||
python - <<'PY'
|
||||
import json
|
||||
|
||||
for f in json.load(open('docs/screens/controls.json')):
|
||||
for c in f['controls']:
|
||||
if 'email' in (c['var'] + c['label']).lower():
|
||||
print(f["file"], c["line"], c["var"], c["type"], c["object_name"])
|
||||
text = (c['var'] + c['label']).lower()
|
||||
if 'email' in text:
|
||||
print(
|
||||
f["file"],
|
||||
c["line"],
|
||||
c["var"],
|
||||
c["type"],
|
||||
c["object_name"]
|
||||
)
|
||||
PY
|
||||
```
|
||||
|
||||
Cột `object_name` đặc biệt quan trọng khi sửa bug màu/style: rỗng nghĩa là widget **chưa**
|
||||
được style qua `_TEMPLATE`, nên nó đang ăn style mặc định của class — thường chính là
|
||||
nguyên nhân của "chỗ này nhìn khác chỗ kia".
|
||||
Từ kết quả có thể xác định:
|
||||
|
||||
## 6. Quy trình tra 4 bước cho Triage
|
||||
```text
|
||||
file
|
||||
line
|
||||
variable
|
||||
widget type
|
||||
objectName
|
||||
```
|
||||
|
||||
1. Xác định **nav row** (Dashboard / Schedule / Workspace / Monitoring) từ mô tả hoặc ảnh.
|
||||
2. Xác định **sub-tab / dialog**.
|
||||
3. Tra `manifest.json` → lấy `note` = `file.py:line`.
|
||||
4. Tra `controls.json` → lấy đúng `var` + `line` + `object_name` của control bị lỗi.
|
||||
---
|
||||
|
||||
Không qua đủ 4 bước thì `confidence` tối đa là `low`.
|
||||
## 6. `object_name` đặc biệt quan trọng khi điều tra UI
|
||||
|
||||
Khi sửa lỗi màu hoặc style, phải chú ý đến:
|
||||
|
||||
```text
|
||||
object_name
|
||||
```
|
||||
|
||||
Nếu `object_name` đang rỗng, có nghĩa widget đó **chưa được gắn `objectName` để áp style theo cơ chế template/QSS**.
|
||||
|
||||
Khi đó widget có thể đang sử dụng style mặc định của class.
|
||||
|
||||
Đây thường là nguyên nhân khiến người dùng thấy:
|
||||
|
||||
> "Chỗ này nhìn khác chỗ kia."
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
Widget A → objectName = "project_title"
|
||||
↓
|
||||
QSS áp style riêng
|
||||
|
||||
Widget B → objectName = ""
|
||||
↓
|
||||
dùng style mặc định
|
||||
```
|
||||
|
||||
Vì vậy, khi gặp lỗi visual liên quan đến màu/style, hãy kiểm tra `object_name` trước khi tự thêm màu hoặc `setStyleSheet()`.
|
||||
|
||||
---
|
||||
|
||||
# 7. Quy trình 4 bước dành cho Triage
|
||||
|
||||
Khi người dùng báo lỗi bằng ngôn ngữ tự nhiên, thực hiện theo thứ tự sau:
|
||||
|
||||
### Bước 1 — Xác định màn hình chính
|
||||
|
||||
Xác định lỗi thuộc:
|
||||
|
||||
```text
|
||||
Dashboard
|
||||
Schedule
|
||||
Workspace
|
||||
Monitoring
|
||||
```
|
||||
|
||||
Dựa trên mô tả của người dùng hoặc screenshot.
|
||||
|
||||
---
|
||||
|
||||
### Bước 2 — Xác định tab/view/dialog
|
||||
|
||||
Tiếp tục xác định:
|
||||
|
||||
```text
|
||||
Sub-tab
|
||||
→ View
|
||||
→ Dialog
|
||||
```
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
Workspace
|
||||
→ Co4E
|
||||
→ Agent Dialog
|
||||
```
|
||||
|
||||
hoặc:
|
||||
|
||||
```text
|
||||
Monitoring
|
||||
→ Security Events
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bước 3 — Tra `manifest.json`
|
||||
|
||||
Mở:
|
||||
|
||||
```text
|
||||
docs/screens/manifest.json
|
||||
```
|
||||
|
||||
Tìm màn hình tương ứng và lấy:
|
||||
|
||||
```text
|
||||
note → file.py:line
|
||||
```
|
||||
|
||||
Đây là điểm bắt đầu để tìm code dựng màn hình.
|
||||
|
||||
---
|
||||
|
||||
### Bước 4 — Tra `controls.json`
|
||||
|
||||
Nếu lỗi liên quan đến một control cụ thể, tiếp tục tìm trong:
|
||||
|
||||
```text
|
||||
docs/screens/controls.json
|
||||
```
|
||||
|
||||
Lấy:
|
||||
|
||||
```text
|
||||
var
|
||||
line
|
||||
type
|
||||
object_name
|
||||
```
|
||||
|
||||
Sau đó xác định chính xác widget bị lỗi.
|
||||
|
||||
---
|
||||
|
||||
# 8. Quy tắc về Confidence
|
||||
|
||||
Triage phải phản ánh đúng mức độ chắc chắn của kết quả.
|
||||
|
||||
Nếu chưa hoàn thành đủ 4 bước:
|
||||
|
||||
```text
|
||||
1. Nav
|
||||
2. Tab/View/Dialog
|
||||
3. manifest.json
|
||||
4. controls.json
|
||||
```
|
||||
|
||||
thì:
|
||||
|
||||
```yaml
|
||||
confidence: low
|
||||
```
|
||||
|
||||
Không được tự nâng lên `medium` hoặc `high` chỉ vì file nhìn có vẻ đúng.
|
||||
|
||||
### Khi nào có thể tăng Confidence?
|
||||
|
||||
Chỉ tăng khi có bằng chứng cụ thể, ví dụ:
|
||||
|
||||
```text
|
||||
User description
|
||||
↓
|
||||
Dashboard
|
||||
↓
|
||||
Statistics view
|
||||
↓
|
||||
manifest.json
|
||||
↓
|
||||
presentation/dashboard/dashboard_tab.py:123
|
||||
↓
|
||||
controls.json
|
||||
↓
|
||||
QTableView
|
||||
↓
|
||||
line 245
|
||||
```
|
||||
|
||||
Khi đó mới có đủ cơ sở để ghi nhận `file:line` và đánh giá confidence cao hơn.
|
||||
|
||||
---
|
||||
|
||||
# 9. Nguyên tắc quan trọng
|
||||
|
||||
**Không đoán file từ tên.**
|
||||
|
||||
Không nên suy luận kiểu:
|
||||
|
||||
> "Lỗi ở Workspace nên chắc chắn nằm trong `workspace_tab.py`."
|
||||
|
||||
Thay vào đó:
|
||||
|
||||
```text
|
||||
Mô tả của user
|
||||
↓
|
||||
Xác định màn hình
|
||||
↓
|
||||
Xác định tab/view/dialog
|
||||
↓
|
||||
Tra manifest.json
|
||||
↓
|
||||
Xác định file:line
|
||||
↓
|
||||
Tra controls.json
|
||||
↓
|
||||
Xác định widget/control
|
||||
↓
|
||||
Đánh giá confidence
|
||||
```
|
||||
|
||||
Mục tiêu cuối cùng của Screen Map là biến một mô tả mơ hồ của người dùng thành một đầu vào có thể sử dụng được cho `defect_record`, đặc biệt là:
|
||||
|
||||
```text
|
||||
screen
|
||||
widget
|
||||
file
|
||||
line
|
||||
object_name
|
||||
confidence
|
||||
```
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+635
-71
@@ -1,101 +1,665 @@
|
||||
# Theme & Design Tokens — luật màu sắc của Cowork Local
|
||||
# Theme & Design Tokens — Luật màu sắc của Cowork Local
|
||||
|
||||
Nguồn: docstring đầu `theme/__init__.py`, `theme/palettes.py`, `theme/qss.py`,
|
||||
`theme/qss_controls.py`.
|
||||
> Knowledge module dành cho các agent xử lý **UI Visual / Theme / QSS** của Cowork Local.
|
||||
|
||||
## Nguồn chính
|
||||
|
||||
* `theme/__init__.py` — docstring và API theme
|
||||
* `theme/palettes.py` — định nghĩa Palette/token
|
||||
* `theme/qss.py` — `_TEMPLATE` và stylesheet
|
||||
* `theme/qss_controls.py` — style cho các Qt controls
|
||||
|
||||
---
|
||||
|
||||
## 1. Luật gốc
|
||||
# 1. Luật quan trọng nhất
|
||||
|
||||
> **Không file nào ngoài `theme/` được đặt tên một màu.**
|
||||
> **Ngoài thư mục `theme/`, không file nào được tự định nghĩa màu.**
|
||||
|
||||
Cơ chế duy nhất:
|
||||
Luồng màu chuẩn của Cowork Local:
|
||||
|
||||
```text
|
||||
Palette (token ngữ nghĩa) → _TEMPLATE (một QSS duy nhất) → stylesheet(theme)
|
||||
Palette
|
||||
↓
|
||||
token ngữ nghĩa
|
||||
↓
|
||||
_TEMPLATE
|
||||
↓
|
||||
stylesheet(theme)
|
||||
↓
|
||||
QApplication.setStyleSheet(...)
|
||||
```
|
||||
|
||||
Hai cách hợp lệ để một widget có màu:
|
||||
Nói đơn giản:
|
||||
|
||||
1. **Khai báo** — gán `objectName` cho widget, style nó trong `_TEMPLATE`
|
||||
(`theme/qss.py`). Đây là cách mặc định.
|
||||
2. **Vẽ tay** — widget vẽ bằng `QPainter` (chart, canvas, syntax highlighter) thì gọi
|
||||
`current_palette()` rồi đọc token.
|
||||
> **Widget không tự chọn màu. Theme quyết định màu.**
|
||||
|
||||
Cách **không** hợp lệ, bị reject review:
|
||||
---
|
||||
|
||||
# 2. Hai cách hợp lệ để widget có màu
|
||||
|
||||
## Cách 1 — Style bằng QSS
|
||||
|
||||
Đây là cách mặc định.
|
||||
|
||||
Widget đặt `objectName`, sau đó style được định nghĩa trong:
|
||||
|
||||
```text
|
||||
theme/qss.py
|
||||
```
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```python
|
||||
self.label.setStyleSheet("color: #dc2626;") # ❌ hex ngoài theme/
|
||||
pen.setColor(QColor("red")) # ❌ tên màu literal
|
||||
self.card.setStyleSheet("background: rgba(0,0,0,.1)") # ❌
|
||||
widget.setObjectName("my_widget")
|
||||
```
|
||||
|
||||
## 2. API cần nhớ
|
||||
và style tương ứng nằm trong `_TEMPLATE`.
|
||||
|
||||
| Hàm | Dùng khi |
|
||||
|---|---|
|
||||
| `theme.stylesheet(theme)` | Sinh QSS toàn app, truyền vào `QApplication.setStyleSheet` |
|
||||
| `theme.set_active_theme(theme)` | **Phải** gọi ngay cạnh mỗi `setStyleSheet(stylesheet(...))` |
|
||||
| `theme.current_theme()` | `'dark'` / `'light'` đang hiển thị |
|
||||
| `theme.current_palette()` | Token của theme đang hiển thị — dùng trong `paintEvent` |
|
||||
| `theme.palette(theme)` | Token của một theme cụ thể |
|
||||
| `theme.resolve_theme('system')` | Suy ra dark/light từ color scheme của OS |
|
||||
| `theme.role_colors(theme)` | Màu theo vai trò hội thoại: user/assistant/tool/result/error |
|
||||
---
|
||||
|
||||
`current_palette()` tồn tại để code vẽ **không** phải đọc lại `config.json` mỗi lần
|
||||
repaint — đó từng là bug hiệu năng thật. Không thay bằng đọc config.
|
||||
## Cách 2 — Widget tự vẽ bằng `QPainter`
|
||||
|
||||
## 3. Nhóm token
|
||||
Dùng cho các thành phần như:
|
||||
|
||||
Palette là `@dataclass(frozen=True)`. Các nhóm chính:
|
||||
* chart;
|
||||
* canvas;
|
||||
* syntax highlighter;
|
||||
* custom painting.
|
||||
|
||||
| Nhóm | Token | Ý nghĩa |
|
||||
|---|---|---|
|
||||
| Bề mặt (thang 4 bậc) | `bg` | nền cửa sổ / canvas |
|
||||
| | `surface` | panel, card, group box (**không** phải nav rail) |
|
||||
| | `surface_raised` | input, list, tree — thứ người dùng gõ/chọn |
|
||||
| | `overlay` | menu, tooltip, popup |
|
||||
| | `sunken` | log, code, terminal — thứ để đọc vào |
|
||||
| | `hover` / `active` | trạng thái hover / đang bấm |
|
||||
| Chữ | `text`, `text_muted`, ... | |
|
||||
| Nhấn | `accent`, `accent_solid` | **Hai token khác nhau có chủ đích**: màu đọc được *dạng chữ* trên nền tối thì quá nhạt để làm *nền* cho chữ trắng |
|
||||
| Trạng thái | `danger`, ... | |
|
||||
| Vai trò hội thoại | `role_user`, `role_assistant`, `role_tool`, `role_result`, `role_error` | |
|
||||
| Code | `code_string`, ... | syntax highlighting |
|
||||
Code phải lấy màu từ:
|
||||
|
||||
Token là **ngữ nghĩa**, không phải literal: `danger` / `text_muted` — không bao giờ
|
||||
`blue` / `grey2`. Thêm một theme = thêm một `Palette`, không phải sửa stylesheet.
|
||||
```python
|
||||
current_palette()
|
||||
```
|
||||
|
||||
## 4. Ràng buộc thiết kế (đừng "sửa" nhầm thành bug)
|
||||
Ví dụ:
|
||||
|
||||
- **Không gradient, không glow.** Bảng màu lấy từ VS Code "Dark Modern" / "Light Modern".
|
||||
Bề mặt phẳng, góc gần vuông, một màu accent chỉ dành cho thứ người dùng thao tác.
|
||||
- **Chiều sâu đến từ thang bề mặt và viền mảnh**, không từ màu.
|
||||
- **Silhouette VS Code:** nav rail **tối hơn** vùng nội dung, không sáng hơn.
|
||||
Người dùng báo "menu trái tối quá" — đó là thiết kế, không phải bug. Xem `examples/bad_fix.md`.
|
||||
- **Contrast giữ ở WCAG AA (4.5:1)** cho body text và cho chữ trên nút đặc.
|
||||
- Bốn giá trị của VS Code không đạt AA đã được nhích lên vừa đủ (số dòng dark 3.59:1,
|
||||
chữ mờ trên sidebar sáng 4.28:1, xanh lá sáng 4.33:1, hổ phách sáng 3.12:1). Mỗi chỗ có
|
||||
comment ghi giá trị gốc — **không** trả chúng về giá trị VS Code.
|
||||
```python
|
||||
palette = current_palette()
|
||||
```
|
||||
|
||||
## 5. Mũi tên combo box (`_chevron_asset`)
|
||||
Sau đó dùng token từ palette.
|
||||
|
||||
QSS `image:` chỉ nhận đường dẫn file/resource, không nhận `QPixmap`. Và một khi
|
||||
`::drop-down` / `::up-button` / `::down-button` bị style, Qt **ngừng vẽ mũi tên mặc định**.
|
||||
Vì vậy `theme/palettes.py::_chevron_asset` render sẵn PNG chevron ra thư mục tạm và cache
|
||||
theo hash `(direction, color)`.
|
||||
---
|
||||
|
||||
Hệ quả khi debug:
|
||||
# 3. Những cách KHÔNG được phép
|
||||
|
||||
- "Combo box mất mũi tên" → gần như luôn do một stylesheet cục bộ đè lên `::drop-down`.
|
||||
- File cache nằm ở `%TEMP%/cowork_local_theme/chevron_*.png`. Xoá nó để buộc render lại
|
||||
khi test màu mới.
|
||||
Không được tự đặt màu trong UI code.
|
||||
|
||||
## 6. Checklist sửa bug liên quan màu sắc
|
||||
### ❌ Hardcode HEX
|
||||
|
||||
- [ ] Đã kiểm tra bug xuất hiện ở **cả** dark và light chưa? (`docs/screens/*-dark.png` / `*-light.png`)
|
||||
- [ ] Bản sửa dùng token, không dùng hex?
|
||||
- [ ] Nếu thêm token mới: đã thêm cho **cả** `DARK` và `LIGHT`?
|
||||
- [ ] Nếu là chữ trên nền đặc: đã dùng `accent_solid` thay vì `accent`?
|
||||
- [ ] Contrast còn ≥ 4.5:1?
|
||||
- [ ] Widget dựng sau khi đổi theme có nhận đúng stylesheet? (xem `qt_pitfalls.md` P07)
|
||||
```python
|
||||
self.label.setStyleSheet("color: #dc2626;")
|
||||
```
|
||||
|
||||
### ❌ Hardcode tên màu
|
||||
|
||||
```python
|
||||
pen.setColor(QColor("red"))
|
||||
```
|
||||
|
||||
### ❌ Hardcode RGBA
|
||||
|
||||
```python
|
||||
self.card.setStyleSheet(
|
||||
"background: rgba(0,0,0,.1)"
|
||||
)
|
||||
```
|
||||
|
||||
Các trường hợp này phải bị reject khi review.
|
||||
|
||||
### Rule ngắn gọn
|
||||
|
||||
```text
|
||||
Không có màu literal ngoài theme/
|
||||
```
|
||||
|
||||
Không chỉ tránh `#hex`, mà cả:
|
||||
|
||||
* tên màu;
|
||||
* RGB;
|
||||
* RGBA;
|
||||
* stylesheet cục bộ chứa màu.
|
||||
|
||||
---
|
||||
|
||||
# 4. API Theme cần nhớ
|
||||
|
||||
| API | Dùng để |
|
||||
| ------------------------------- | --------------------------------------------------- |
|
||||
| `theme.stylesheet(theme)` | Tạo QSS cho toàn app |
|
||||
| `theme.set_active_theme(theme)` | Ghi nhận theme hiện đang active |
|
||||
| `theme.current_theme()` | Lấy theme hiện tại: `dark` / `light` |
|
||||
| `theme.current_palette()` | Lấy Palette của theme hiện tại |
|
||||
| `theme.palette(theme)` | Lấy Palette của một theme cụ thể |
|
||||
| `theme.resolve_theme("system")` | Xác định dark/light theo OS |
|
||||
| `theme.role_colors(theme)` | Lấy màu theo role: user/assistant/tool/result/error |
|
||||
|
||||
---
|
||||
|
||||
## Khi đổi theme
|
||||
|
||||
Hai lệnh này phải đi cùng nhau:
|
||||
|
||||
```python
|
||||
theme.set_active_theme(theme)
|
||||
app.setStyleSheet(theme.stylesheet(theme))
|
||||
```
|
||||
|
||||
Không được chỉ gọi `setStyleSheet()` mà quên cập nhật active theme.
|
||||
|
||||
---
|
||||
|
||||
# 5. `current_palette()` dùng để làm gì?
|
||||
|
||||
Code vẽ bằng `QPainter` phải dùng:
|
||||
|
||||
```python
|
||||
current_palette()
|
||||
```
|
||||
|
||||
Không được mỗi lần `paintEvent()` lại đọc:
|
||||
|
||||
```text
|
||||
config.json
|
||||
```
|
||||
|
||||
Lý do:
|
||||
|
||||
```text
|
||||
paintEvent()
|
||||
↓
|
||||
repaint
|
||||
↓
|
||||
đọc config
|
||||
↓
|
||||
lặp lại rất nhiều lần
|
||||
```
|
||||
|
||||
Điều này từng gây vấn đề hiệu năng thực tế.
|
||||
|
||||
Vì vậy:
|
||||
|
||||
> `current_palette()` tồn tại để custom painting lấy màu nhanh từ theme hiện tại.
|
||||
|
||||
---
|
||||
|
||||
# 6. Palette và Design Token
|
||||
|
||||
`Palette` là:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
```
|
||||
|
||||
Token phải mang **ý nghĩa**, không phải tên màu.
|
||||
|
||||
### ❌ Không đặt token kiểu:
|
||||
|
||||
```text
|
||||
blue
|
||||
grey2
|
||||
dark_blue
|
||||
light_grey
|
||||
```
|
||||
|
||||
### ✅ Đặt theo vai trò:
|
||||
|
||||
```text
|
||||
accent
|
||||
danger
|
||||
text
|
||||
text_muted
|
||||
surface
|
||||
surface_raised
|
||||
```
|
||||
|
||||
Lợi ích:
|
||||
|
||||
> Thêm theme mới = thêm một `Palette`, không phải viết lại stylesheet.
|
||||
|
||||
---
|
||||
|
||||
# 7. Các nhóm token chính
|
||||
|
||||
## 7.1. Surface — các mức bề mặt
|
||||
|
||||
| Token | Dùng cho |
|
||||
| ---------------- | -------------------------------------------- |
|
||||
| `bg` | Nền chính của cửa sổ/canvas |
|
||||
| `surface` | Panel, card, group box |
|
||||
| `surface_raised` | Input, list, tree — nơi người dùng nhập/chọn |
|
||||
| `overlay` | Menu, tooltip, popup |
|
||||
| `sunken` | Log, code, terminal — vùng chủ yếu để đọc |
|
||||
| `hover` | Trạng thái hover |
|
||||
| `active` | Trạng thái đang active/pressed |
|
||||
|
||||
### Lưu ý
|
||||
|
||||
`surface` **không có nghĩa là nav rail**.
|
||||
|
||||
Nav rail có chủ đích riêng về độ sáng/tối.
|
||||
|
||||
---
|
||||
|
||||
## 7.2. Text
|
||||
|
||||
Các token chính:
|
||||
|
||||
```text
|
||||
text
|
||||
text_muted
|
||||
...
|
||||
```
|
||||
|
||||
Dùng token theo vai trò thay vì tự chọn màu.
|
||||
|
||||
---
|
||||
|
||||
## 7.3. Accent
|
||||
|
||||
Có hai token:
|
||||
|
||||
```text
|
||||
accent
|
||||
accent_solid
|
||||
```
|
||||
|
||||
**Hai token này khác nhau có chủ đích.**
|
||||
|
||||
### `accent`
|
||||
|
||||
Dùng cho accent thông thường, ví dụ:
|
||||
|
||||
* trạng thái;
|
||||
* thành phần UI;
|
||||
* điểm nhấn.
|
||||
|
||||
### `accent_solid`
|
||||
|
||||
Dùng khi accent trở thành **nền đặc và bên trên có chữ**.
|
||||
|
||||
Lý do:
|
||||
|
||||
> Một màu accent có thể đủ sáng để đọc khi dùng như chữ trên nền tối, nhưng lại quá sáng khi dùng làm nền cho chữ trắng.
|
||||
|
||||
Vì vậy:
|
||||
|
||||
```text
|
||||
Chữ trên nền accent đặc
|
||||
↓
|
||||
accent_solid
|
||||
```
|
||||
|
||||
Không tự lấy `accent` chỉ vì nó có vẻ "cùng màu".
|
||||
|
||||
---
|
||||
|
||||
## 7.4. State
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
danger
|
||||
...
|
||||
```
|
||||
|
||||
Các state token cũng phải mang ý nghĩa, không đặt theo tên màu.
|
||||
|
||||
---
|
||||
|
||||
## 7.5. Conversation roles
|
||||
|
||||
Có các token:
|
||||
|
||||
```text
|
||||
role_user
|
||||
role_assistant
|
||||
role_tool
|
||||
role_result
|
||||
role_error
|
||||
```
|
||||
|
||||
Dùng để phân biệt các role trong giao diện hội thoại.
|
||||
|
||||
---
|
||||
|
||||
## 7.6. Code / Syntax
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
code_string
|
||||
...
|
||||
```
|
||||
|
||||
Dùng cho syntax highlighting.
|
||||
|
||||
---
|
||||
|
||||
# 8. Các nguyên tắc thiết kế — đừng nhầm thành bug
|
||||
|
||||
Một số đặc điểm nhìn "khác mắt" nhưng **có chủ đích**.
|
||||
|
||||
Không được tự ý sửa chỉ vì người dùng nói "trông hơi tối" hoặc "không giống app hiện đại".
|
||||
|
||||
---
|
||||
|
||||
## 8.1. Không gradient, không glow
|
||||
|
||||
Thiết kế lấy cảm hứng từ:
|
||||
|
||||
```text
|
||||
VS Code Dark Modern
|
||||
VS Code Light Modern
|
||||
```
|
||||
|
||||
Phong cách chính:
|
||||
|
||||
* surface phẳng;
|
||||
* góc gần vuông;
|
||||
* không gradient;
|
||||
* không glow;
|
||||
* một accent chính;
|
||||
* accent dành cho thứ người dùng tương tác.
|
||||
|
||||
---
|
||||
|
||||
## 8.2. Độ sâu đến từ surface và border
|
||||
|
||||
Không tạo chiều sâu bằng cách:
|
||||
|
||||
```text
|
||||
đổi màu quá mạnh
|
||||
```
|
||||
|
||||
Thay vào đó dùng:
|
||||
|
||||
```text
|
||||
surface hierarchy
|
||||
+
|
||||
border mảnh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 9. Nav rail tối hơn là thiết kế có chủ đích
|
||||
|
||||
Silhouette của Cowork Local lấy theo VS Code:
|
||||
|
||||
```text
|
||||
NAV RAIL
|
||||
↓
|
||||
tối hơn
|
||||
↓
|
||||
CONTENT AREA
|
||||
```
|
||||
|
||||
Không phải:
|
||||
|
||||
```text
|
||||
nav rail sáng hơn content
|
||||
```
|
||||
|
||||
Vì vậy nếu user báo:
|
||||
|
||||
> "Menu bên trái tối quá."
|
||||
|
||||
thì **chưa được kết luận ngay là visual bug**.
|
||||
|
||||
Đây có thể là design intent.
|
||||
|
||||
Xem thêm:
|
||||
|
||||
```text
|
||||
examples/bad_fix.md
|
||||
```
|
||||
|
||||
để tránh sửa nhầm.
|
||||
|
||||
---
|
||||
|
||||
# 10. Contrast — WCAG AA
|
||||
|
||||
Body text và chữ trên button nền đặc phải đạt:
|
||||
|
||||
```text
|
||||
Contrast ratio ≥ 4.5:1
|
||||
```
|
||||
|
||||
Đây là yêu cầu tối thiểu.
|
||||
|
||||
Khi thay token/màu:
|
||||
|
||||
```text
|
||||
Dark theme
|
||||
+
|
||||
Light theme
|
||||
+
|
||||
text/background
|
||||
```
|
||||
|
||||
đều phải được kiểm tra.
|
||||
|
||||
---
|
||||
|
||||
## Không khôi phục màu VS Code cũ nếu màu đó không đạt AA
|
||||
|
||||
Một số màu gốc của VS Code không đạt yêu cầu AA.
|
||||
|
||||
Các giá trị đã được Cowork Local điều chỉnh vừa đủ, ví dụ:
|
||||
|
||||
| Trường hợp | Contrast cũ |
|
||||
| ------------------------ | ----------: |
|
||||
| Dark line | 3.59:1 |
|
||||
| Chữ mờ trên sidebar sáng | 4.28:1 |
|
||||
| Xanh lá sáng | 4.33:1 |
|
||||
| Hổ phách sáng | 3.12:1 |
|
||||
|
||||
Các chỗ này có comment ghi lại giá trị gốc.
|
||||
|
||||
### Rule
|
||||
|
||||
**Không đưa chúng trở lại giá trị VS Code ban đầu.**
|
||||
|
||||
Mục tiêu của Cowork Local là:
|
||||
|
||||
```text
|
||||
VS Code silhouette
|
||||
+
|
||||
WCAG AA
|
||||
```
|
||||
|
||||
không phải copy nguyên xi mọi giá trị màu của VS Code.
|
||||
|
||||
---
|
||||
|
||||
# 11. ⚠️ Combo Box và `_chevron_asset`
|
||||
|
||||
Một lỗi dễ gặp:
|
||||
|
||||
> Combo box mất mũi tên.
|
||||
|
||||
Nguyên nhân liên quan đến cách Qt xử lý QSS.
|
||||
|
||||
---
|
||||
|
||||
## 11.1. `image:` trong QSS không nhận `QPixmap`
|
||||
|
||||
QSS:
|
||||
|
||||
```text
|
||||
image:
|
||||
```
|
||||
|
||||
chỉ nhận đường dẫn tới:
|
||||
|
||||
* file;
|
||||
* resource.
|
||||
|
||||
Không nhận trực tiếp:
|
||||
|
||||
```text
|
||||
QPixmap
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11.2. Style `::drop-down` sẽ làm Qt ngừng vẽ arrow mặc định
|
||||
|
||||
Khi style các selector như:
|
||||
|
||||
```text
|
||||
::drop-down
|
||||
::up-button
|
||||
::down-button
|
||||
```
|
||||
|
||||
Qt có thể ngừng vẽ mũi tên mặc định.
|
||||
|
||||
---
|
||||
|
||||
## 11.3. Cowork Local dùng `_chevron_asset`
|
||||
|
||||
Trong:
|
||||
|
||||
```text
|
||||
theme/palettes.py
|
||||
```
|
||||
|
||||
`_chevron_asset`:
|
||||
|
||||
1. render chevron thành PNG;
|
||||
2. lưu vào thư mục tạm;
|
||||
3. cache theo:
|
||||
|
||||
```text
|
||||
(direction, color)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Khi debug combo box
|
||||
|
||||
Nếu thấy:
|
||||
|
||||
> Combo box mất mũi tên.
|
||||
|
||||
Hãy kiểm tra trước:
|
||||
|
||||
```text
|
||||
stylesheet cục bộ
|
||||
↓
|
||||
::drop-down
|
||||
```
|
||||
|
||||
Đây thường là nguyên nhân.
|
||||
|
||||
Cache nằm tại:
|
||||
|
||||
```text
|
||||
%TEMP%/cowork_local_theme/chevron_*.png
|
||||
```
|
||||
|
||||
Nếu đang test màu mới, có thể xóa cache để buộc render lại.
|
||||
|
||||
---
|
||||
|
||||
# 12. Checklist sửa bug màu sắc/theme
|
||||
|
||||
Trước khi hoàn thành visual fix, kiểm tra:
|
||||
|
||||
### Theme coverage
|
||||
|
||||
* [ ] Bug đã được kiểm tra trên **Dark** chưa?
|
||||
* [ ] Bug đã được kiểm tra trên **Light** chưa?
|
||||
* [ ] Có thể dùng screenshot:
|
||||
|
||||
* `docs/screens/*-dark.png`
|
||||
* `docs/screens/*-light.png`
|
||||
|
||||
### Token
|
||||
|
||||
* [ ] Patch dùng semantic token thay vì hex literal?
|
||||
* [ ] Không có `setStyleSheet()` cục bộ để thay màu?
|
||||
* [ ] Không có `QColor("red")`, `QColor("blue")`, v.v.?
|
||||
* [ ] Nếu thêm token mới, đã thêm cho **cả `DARK` và `LIGHT`**?
|
||||
* [ ] Token mới có tên theo **ý nghĩa**, không theo màu?
|
||||
|
||||
### Accent
|
||||
|
||||
* [ ] Chữ trên nền accent đặc đã dùng `accent_solid`?
|
||||
* [ ] Không dùng `accent` chỉ vì hai token có vẻ giống nhau?
|
||||
|
||||
### Accessibility
|
||||
|
||||
* [ ] Contrast đạt **≥ 4.5:1**?
|
||||
* [ ] Đã kiểm tra cả text và button có nền đặc?
|
||||
|
||||
### Theme lifecycle
|
||||
|
||||
* [ ] Widget tạo sau khi đổi theme có nhận đúng stylesheet?
|
||||
* [ ] Đã kiểm tra vấn đề lazy screen theo `qt_pitfalls.md` **P07**?
|
||||
|
||||
### Design intent
|
||||
|
||||
* [ ] Không vô tình thêm gradient?
|
||||
* [ ] Không thêm glow?
|
||||
* [ ] Không làm nav rail sáng hơn content?
|
||||
* [ ] Không khôi phục các màu VS Code cũ đã bị loại vì không đạt WCAG AA?
|
||||
|
||||
---
|
||||
|
||||
# 13. Quy tắc review nhanh
|
||||
|
||||
Khi gặp một defect liên quan màu sắc, đi theo thứ tự:
|
||||
|
||||
```text
|
||||
1. Xác định widget
|
||||
↓
|
||||
2. Kiểm tra objectName
|
||||
↓
|
||||
3. Tìm rule trong theme/qss.py
|
||||
↓
|
||||
4. Kiểm tra token trong palettes.py
|
||||
↓
|
||||
5. Kiểm tra DARK + LIGHT
|
||||
↓
|
||||
6. Kiểm tra contrast
|
||||
↓
|
||||
7. Kiểm tra local setStyleSheet()
|
||||
↓
|
||||
8. Kiểm tra lazy theme lifecycle (P07)
|
||||
↓
|
||||
9. Xác định đây là bug thật hay design intent
|
||||
↓
|
||||
10. Chỉ sau đó mới tạo fix_plan
|
||||
```
|
||||
|
||||
## Nguyên tắc cuối
|
||||
|
||||
```text
|
||||
UI code
|
||||
↓
|
||||
không tự chọn màu
|
||||
↓
|
||||
semantic token
|
||||
↓
|
||||
Palette
|
||||
↓
|
||||
_TEMPLATE / current_palette()
|
||||
↓
|
||||
theme
|
||||
```
|
||||
|
||||
**Nếu một màu mới cần xuất hiện, trước tiên hỏi:**
|
||||
|
||||
> "Màu này đang đại diện cho vai trò gì?"
|
||||
|
||||
Sau đó tạo hoặc dùng **semantic token** phù hợp.
|
||||
|
||||
Không hỏi:
|
||||
|
||||
> "Mình muốn màu xanh nào?"
|
||||
|
||||
Vì trong Cowork Local, **ý nghĩa của màu quan trọng hơn bản thân màu**.
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Output Contract — `dispatch_plan`
|
||||
|
||||
Do `fix-dispatcher` sinh ra, trước khi bất kỳ agent nào khác chạy.
|
||||
Đây là thứ quyết định **effort** của cả lượt xử lý, nên nó phải chứng minh được lựa chọn
|
||||
của mình — nhưng phải ngắn. Trần: **30 dòng** cho phần người đọc.
|
||||
|
||||
---
|
||||
|
||||
```yaml
|
||||
---
|
||||
report_id: RPT-<YYYYMMDD>-<NN> # một phản ánh của người dùng = một report_id
|
||||
defects:
|
||||
- defect_id: UI-<YYYYMMDD>-<NN>
|
||||
tier: <T0 | T1 | T2 | T3 | T3-SEC>
|
||||
lane: <DIRECT | SOLO | PAIR | FULL | FULL-SEC>
|
||||
category: <visual | flow | i18n-a11y | security | not-ui>
|
||||
severity: <S1 | S2 | S3 | S4>
|
||||
confidence: <low | medium | high>
|
||||
reproducible: <yes | no | intermittent>
|
||||
security_review: <required | not-required>
|
||||
entry_agent: <fix-implementer | ui-visual-fixer | ux-flow-fixer | i18n-a11y-fixer | security-defect-fixer | ui-bug-triage | SELF | RETURN_TO_REPORTER>
|
||||
affected_files: [path/to/file.py:123]
|
||||
tier_evidence: "<dòng nào của roles/0_fix_dispatcher.md Bước 3 đã trúng>"
|
||||
budget_calls: <số lần gọi agent dự kiến>
|
||||
execution:
|
||||
parallel: [[UI-...-01, UI-...-02]] # các defect_id độc lập, chạy cùng lúc
|
||||
sequential: [UI-...-03] # phụ thuộc, hoặc T3 cần triage trước
|
||||
blocked_on: []
|
||||
---
|
||||
```
|
||||
|
||||
# 1. Phản ánh gốc
|
||||
|
||||
Nguyên văn của người báo lỗi, **đã redact** (`system/security.md`). Không diễn giải lại.
|
||||
|
||||
# 2. Tách defect
|
||||
|
||||
| defect_id | Triệu chứng người dùng thấy | Category | Tier |
|
||||
|---|---|---|---|
|
||||
| | | | |
|
||||
|
||||
Một dòng = một nguyên nhân gốc. Chỉ có một defect thì bảng có một dòng — không xoá bảng.
|
||||
|
||||
# 3. Bằng chứng chấm tier
|
||||
|
||||
Mỗi defect **một dòng**, trích đúng tiêu chí đã trúng. Không được viết "trông đơn giản".
|
||||
|
||||
| defect_id | Tier | Trúng tiêu chí | Lệnh đã dùng để xác nhận |
|
||||
|---|---|---|---|
|
||||
| | T0 | loại 1 (số đo hiển thị), 0 disqualifier | `check_loc.py`, `grep -rn` blast radius |
|
||||
| | T2 | "chạm QSS/token dùng chung" | `grep -rn "<objectName>"` |
|
||||
|
||||
Với **T0** bắt buộc có cột lệnh — Gate S và blast radius phải đo, không được ước lượng.
|
||||
|
||||
# 4. Kế hoạch chạy
|
||||
|
||||
```text
|
||||
UI-...-01 T0 DIRECT → hub sửa luôn, cổng máy §4.1
|
||||
UI-...-02 T2 PAIR → ui-visual-fixer → fix-implementer → regression-reviewer
|
||||
UI-...-03 T3 FULL → ui-bug-triage → ... (chờ triage mới biết specialist nào)
|
||||
```
|
||||
|
||||
Ngân sách tổng: `___` lần gọi agent (bảng §4 của role 0 cho phép `___`).
|
||||
|
||||
# 5. Điều đã cố ý KHÔNG làm
|
||||
|
||||
- Không gọi `ui-bug-triage` cho defect nào? Vì sao được phép bỏ (phản ánh đã tự chỉ ra
|
||||
màn hình + triệu chứng cụ thể).
|
||||
- Không gọi `regression-reviewer` cho defect nào? Chỉ hợp lệ ở T0/T1 — nêu rõ cổng nào
|
||||
thay thế.
|
||||
|
||||
# 6. Open question
|
||||
|
||||
Tối đa 3, mỗi câu kèm phương án mặc định nếu người dùng không trả lời
|
||||
(`response_policy.md` R3). Câu hỏi **chặn** thì đưa vào `blocked_on`.
|
||||
File diff suppressed because it is too large
Load Diff
+429
-43
@@ -1,81 +1,467 @@
|
||||
# Guardrail — luật bất biến cho mọi agent trong `agent/`
|
||||
# Guardrail — Luật bất biến cho mọi agent trong `agent/`
|
||||
|
||||
Áp dụng cho cả 6 role. Role nào mâu thuẫn với file này thì **file này thắng**.
|
||||
> **PRECEDENCE:** File này áp dụng cho **tất cả 6 role** trong `agent/`.
|
||||
>
|
||||
> Nếu role-specific instruction mâu thuẫn với bất kỳ quy tắc nào dưới đây, **Guardrail này thắng**.
|
||||
|
||||
---
|
||||
|
||||
## G1. Không tự bịa requirement
|
||||
|
||||
- Chỉ làm việc trên những gì có trong bug report, source code, và `knowledge/`.
|
||||
- Thiếu thông tin → ghi vào mục **Assumption** hoặc **Open Question**, KHÔNG tự suy diễn
|
||||
rồi sửa theo suy diễn đó.
|
||||
- Không tự ý "tiện tay cải thiện UX" ngoài phạm vi lỗi được báo. Phát hiện vấn đề khác →
|
||||
ghi vào mục **Out of scope (đề xuất issue riêng)**.
|
||||
* Chỉ làm việc dựa trên:
|
||||
|
||||
* bug report;
|
||||
* source code thực tế;
|
||||
* các tài liệu trong `knowledge/`;
|
||||
* governance và security policy liên quan.
|
||||
* Nếu thiếu thông tin:
|
||||
|
||||
* ghi vào `Assumption`; hoặc
|
||||
* ghi vào `Open Question`.
|
||||
* **Không được tự suy diễn requirement rồi sửa theo suy diễn đó.**
|
||||
* Không tự ý "tiện tay cải thiện UX", refactor hoặc đổi behavior ngoài phạm vi bug.
|
||||
* Nếu phát hiện vấn đề khác:
|
||||
|
||||
* ghi vào `Out of scope (đề xuất issue riêng)`;
|
||||
* không sửa trong cùng patch.
|
||||
|
||||
---
|
||||
|
||||
## G2. Không đoán vị trí code
|
||||
|
||||
- Mọi khẳng định về code phải kèm `path/file.py:line`. Chưa đọc file thì chưa được kết luận.
|
||||
- Người dùng mô tả bằng tiếng Việt/Nhật → tra `knowledge/screen_map.md` và
|
||||
`docs/screens/controls.json` để tìm đúng widget, không đoán theo tên gọi.
|
||||
* Không được kết luận về code khi chưa đọc code thực tế.
|
||||
* Mọi khẳng định cụ thể về implementation phải kèm:
|
||||
|
||||
```text
|
||||
path/file.py:line
|
||||
```
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
Root cause nằm tại presentation/shell/nav_rail.py:242
|
||||
```
|
||||
|
||||
* Khi người dùng mô tả bằng tiếng Việt hoặc tiếng Nhật:
|
||||
|
||||
1. tra `knowledge/screen_map.md`;
|
||||
2. tra `docs/screens/manifest.json`;
|
||||
3. tra `docs/screens/controls.json`;
|
||||
4. xác nhận `screen → view → widget → file → line`.
|
||||
* **Không đoán file chỉ dựa vào tên widget hoặc tên màn hình.**
|
||||
* Nếu chưa đủ bằng chứng để xác định vị trí:
|
||||
|
||||
* `confidence: low`;
|
||||
* ghi rõ thông tin còn thiếu.
|
||||
|
||||
---
|
||||
|
||||
## G3. Sửa đúng tầng
|
||||
|
||||
Cowork Local là Clean Architecture 4 tầng, phụ thuộc chỉ hướng vào trong:
|
||||
Cowork Local sử dụng Clean Architecture 4 tầng:
|
||||
|
||||
```text
|
||||
presentation/ → application/ → domain/ ← infrastructure/
|
||||
```
|
||||
|
||||
- Bug UI/UX được sửa ở `presentation/`, `ui/`, `theme/`, `i18n/`. Đó là mặc định.
|
||||
- Nếu buộc phải đụng `application/` hoặc `domain/`, phải nêu rõ **lý do tại sao không
|
||||
sửa được ở tầng trên** trong `fix_plan.md`, và coi đó là thay đổi cần reviewer chú ý.
|
||||
- `domain/` và `application/` là **100% Pure Python**. Tuyệt đối không thêm import
|
||||
`PySide6`/`PyQt` vào hai tầng này — Gate C sẽ chặn.
|
||||
- Widget chỉ gọi xuống service của `application/`. Không query SQLite/JSON trực tiếp,
|
||||
không gọi LLM trực tiếp trong GUI thread.
|
||||
### Quy tắc
|
||||
|
||||
* Bug UI/UX mặc định được xử lý tại:
|
||||
|
||||
* `presentation/`
|
||||
* `ui/`
|
||||
* `theme/`
|
||||
* `i18n/`
|
||||
|
||||
* Nếu buộc phải sửa `application/` hoặc `domain/`:
|
||||
|
||||
* phải giải thích trong `fix_plan.md` **tại sao không thể giải quyết ở tầng trên**;
|
||||
* phải đánh dấu đây là thay đổi cần reviewer chú ý.
|
||||
|
||||
### Pure Python boundary
|
||||
|
||||
`domain/` và `application/` phải là **100% Pure Python**.
|
||||
|
||||
**Tuyệt đối không thêm:**
|
||||
|
||||
```python
|
||||
from PySide6 ...
|
||||
from PyQt...
|
||||
```
|
||||
|
||||
vào hai tầng này.
|
||||
|
||||
Gate C sẽ chặn vi phạm này.
|
||||
|
||||
### GUI boundary
|
||||
|
||||
Widget:
|
||||
|
||||
* chỉ gọi service/use case của `application/`;
|
||||
* không query SQLite trực tiếp;
|
||||
* không đọc/ghi JSON repository trực tiếp;
|
||||
* không gọi LLM trực tiếp trong GUI thread.
|
||||
|
||||
---
|
||||
|
||||
## G4. Không đặt tên màu ngoài `theme/`
|
||||
|
||||
- Không hex literal (`#1f6fb2`), không `QColor("red")`, không `setStyleSheet("color: blue")`
|
||||
trong bất kỳ file nào ngoài `theme/`.
|
||||
- Sửa màu = sửa/đọc token trong `theme/palettes.py`, hoặc gán `objectName` rồi style trong
|
||||
`theme/qss.py`. Chi tiết: `knowledge/theme_tokens.md`.
|
||||
- Đây là lỗi bị từ chối review thường xuyên nhất khi sửa bug UI.
|
||||
Ngoài `theme/`, tuyệt đối không định nghĩa màu trực tiếp.
|
||||
|
||||
### Không được dùng
|
||||
|
||||
```python
|
||||
"#1f6fb2"
|
||||
QColor("red")
|
||||
setStyleSheet("color: blue")
|
||||
```
|
||||
|
||||
Cũng không được tạo màu bằng:
|
||||
|
||||
* hex literal;
|
||||
* color name;
|
||||
* RGB/RGBA literal;
|
||||
* stylesheet màu viết trực tiếp.
|
||||
|
||||
### Cách đúng
|
||||
|
||||
Màu phải đi qua theme system:
|
||||
|
||||
```text
|
||||
Palette
|
||||
↓
|
||||
semantic token
|
||||
↓
|
||||
QSS template / current_palette()
|
||||
↓
|
||||
widget
|
||||
```
|
||||
|
||||
Có hai cách hợp lệ:
|
||||
|
||||
1. Widget có `objectName` và được style trong `theme/qss.py`.
|
||||
2. Custom painting dùng `current_palette()`.
|
||||
|
||||
Chi tiết xem:
|
||||
|
||||
```text
|
||||
knowledge/theme_tokens.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## G5. Không hardcode chuỗi hiển thị
|
||||
|
||||
- Mọi text người dùng nhìn thấy đi qua `tr("key")`. Chi tiết: `knowledge/i18n_rules.md`.
|
||||
- Sửa một nhãn = sửa cả 3 ngôn ngữ `en` / `ja` / `vi`, không sửa mỗi tiếng Việt.
|
||||
Mọi text người dùng nhìn thấy phải đi qua:
|
||||
|
||||
```python
|
||||
tr("key")
|
||||
```
|
||||
|
||||
Chi tiết xem:
|
||||
|
||||
```text
|
||||
knowledge/i18n_rules.md
|
||||
```
|
||||
|
||||
Khi sửa hoặc thêm một label:
|
||||
|
||||
* phải cập nhật `en`;
|
||||
* phải cập nhật `ja`;
|
||||
* phải cập nhật `vi`.
|
||||
|
||||
**Không chỉ sửa tiếng Việt.**
|
||||
|
||||
Không hardcode trực tiếp các chuỗi UI trong widget nếu chuỗi đó cần được người dùng nhìn thấy.
|
||||
|
||||
---
|
||||
|
||||
## G6. Giữ Single Responsibility
|
||||
|
||||
- Mọi module production `<= 400 LOC` (Gate S). Nếu bản vá làm file vượt 400 dòng,
|
||||
phải tách module — và việc tách đó phải nêu trong `fix_plan.md` trước khi làm.
|
||||
- Không "sửa bug" bằng cách nhét thêm 150 dòng vào một file đã 380 dòng.
|
||||
Mọi production module phải:
|
||||
|
||||
```text
|
||||
<= 400 LOC
|
||||
```
|
||||
|
||||
Đây là giới hạn của Gate S.
|
||||
|
||||
### Nếu patch làm file vượt 400 dòng
|
||||
|
||||
Không được tiếp tục nhồi code vào file.
|
||||
|
||||
Phải:
|
||||
|
||||
1. xác định phần cần tách;
|
||||
2. ghi kế hoạch tách trong `fix_plan.md`;
|
||||
3. thực hiện việc tách như một phần rõ ràng của patch;
|
||||
4. đảm bảo dependency direction không bị phá vỡ.
|
||||
|
||||
### Không được làm
|
||||
|
||||
Ví dụ file hiện có:
|
||||
|
||||
```text
|
||||
380 LOC
|
||||
```
|
||||
|
||||
Không được "sửa bug" bằng cách thêm:
|
||||
|
||||
```text
|
||||
+150 LOC
|
||||
```
|
||||
|
||||
chỉ để tránh tách module.
|
||||
|
||||
---
|
||||
|
||||
## G7. Không làm suy yếu kiểm thử
|
||||
|
||||
- Không xoá test, không `@pytest.mark.skip`, không nới assert để pass gate.
|
||||
- Test đang đỏ vì lý do khác → báo trong report, không sửa lén.
|
||||
- Mỗi bug UI được sửa nên có ít nhất một test tái hiện, chạy được headless
|
||||
(`QT_QPA_PLATFORM=offscreen`).
|
||||
Tuyệt đối không:
|
||||
|
||||
* xoá test;
|
||||
* disable test;
|
||||
* dùng `@pytest.mark.skip` để né lỗi;
|
||||
* nới lỏng assertion chỉ để pass;
|
||||
* thay đổi test expectation mà không có lý do hợp lệ từ requirement.
|
||||
|
||||
Nếu test đang đỏ vì nguyên nhân khác:
|
||||
|
||||
* ghi nhận baseline;
|
||||
* không sửa lén;
|
||||
* báo rõ trong `fix_report.md`.
|
||||
|
||||
### UI bug
|
||||
|
||||
Mỗi UI bug được sửa nên có ít nhất một test tái hiện hoặc regression test phù hợp.
|
||||
|
||||
Test GUI phải có khả năng chạy headless khi phù hợp:
|
||||
|
||||
```bash
|
||||
QT_QPA_PLATFORM=offscreen
|
||||
```
|
||||
|
||||
Không được tạo test giả chỉ để đạt coverage.
|
||||
|
||||
---
|
||||
|
||||
## G8. Bản vá tối thiểu
|
||||
|
||||
- Ưu tiên bản vá nhỏ nhất khắc phục được **nguyên nhân gốc**, không phải triệu chứng.
|
||||
- Không refactor kèm trong PR fix bug. Một PR = một thay đổi logic (Definition of Done).
|
||||
- Không đổi format/indent toàn file — diff phải đọc được.
|
||||
Mục tiêu là:
|
||||
|
||||
> **Bản vá nhỏ nhất có thể sửa đúng nguyên nhân gốc.**
|
||||
|
||||
Không chỉ sửa triệu chứng.
|
||||
|
||||
### Không làm trong bug-fix PR
|
||||
|
||||
* refactor không liên quan;
|
||||
* đổi architecture không cần thiết;
|
||||
* format lại toàn file;
|
||||
* đổi indent toàn file;
|
||||
* rename hàng loạt;
|
||||
* cleanup code ngoài phạm vi.
|
||||
|
||||
Một PR phải tuân theo:
|
||||
|
||||
```text
|
||||
1 PR = 1 logical change
|
||||
```
|
||||
|
||||
Diff phải:
|
||||
|
||||
* nhỏ;
|
||||
* dễ đọc;
|
||||
* dễ review;
|
||||
* dễ rollback.
|
||||
|
||||
---
|
||||
|
||||
## G9. Không tự merge, không tự đóng issue
|
||||
|
||||
- Agent chỉ đề xuất. Quyết định merge thuộc Cowork Team (`docs/governance/ownership.md`).
|
||||
- Thay đổi chạm tới permission, credential, MCP write/exec, sandbox, network, TLS,
|
||||
isolation, model routing, xoá dữ liệu → **bắt buộc** đánh dấu `security-review: required`
|
||||
trong output, kể cả khi chỉ sửa UI.
|
||||
Agent chỉ:
|
||||
|
||||
* phân tích;
|
||||
* đề xuất;
|
||||
* tạo `fix_plan`;
|
||||
* implement khi đúng role;
|
||||
* kiểm chứng;
|
||||
* tạo report;
|
||||
* handoff.
|
||||
|
||||
Agent **không tự quyết định merge**.
|
||||
|
||||
Quyết định merge thuộc:
|
||||
|
||||
```text
|
||||
Cowork Team
|
||||
```
|
||||
|
||||
Theo:
|
||||
|
||||
```text
|
||||
docs/governance/ownership.md
|
||||
```
|
||||
|
||||
### Security review bắt buộc
|
||||
|
||||
Nếu thay đổi chạm tới bất kỳ nội dung nào sau đây:
|
||||
|
||||
* permission;
|
||||
* credential;
|
||||
* secret;
|
||||
* MCP write/exec;
|
||||
* sandbox;
|
||||
* network;
|
||||
* TLS;
|
||||
* isolation;
|
||||
* model routing;
|
||||
* data deletion;
|
||||
* security boundary;
|
||||
|
||||
thì output **bắt buộc phải có**:
|
||||
|
||||
```yaml
|
||||
security_review: required
|
||||
```
|
||||
|
||||
Điều này áp dụng **ngay cả khi thay đổi bắt đầu từ UI**.
|
||||
|
||||
`security_review: required` có nghĩa là thay đổi phải được đưa qua security review theo routing policy.
|
||||
|
||||
Không được tự kết luận:
|
||||
|
||||
> "Chỉ sửa UI nên không cần security review."
|
||||
|
||||
---
|
||||
|
||||
## G10. Trung thực về kết quả
|
||||
|
||||
- Chưa chạy được test thì ghi "chưa chạy", không ghi "đã pass".
|
||||
- Sửa được 2/3 vấn đề trong report thì nói rõ phần còn lại và lý do.
|
||||
- Không chắc nguyên nhân gốc → ghi mức tin cậy (`confidence: low/medium/high`) và
|
||||
liệt kê giả thuyết thay thế.
|
||||
Agent phải báo cáo đúng những gì thực sự đã làm.
|
||||
|
||||
### Chưa chạy test
|
||||
|
||||
Không được viết:
|
||||
|
||||
```text
|
||||
Tests passed
|
||||
```
|
||||
|
||||
Phải viết:
|
||||
|
||||
```text
|
||||
Tests: not run
|
||||
```
|
||||
|
||||
hoặc:
|
||||
|
||||
```text
|
||||
Chưa chạy test do <lý do>.
|
||||
```
|
||||
|
||||
### Chỉ sửa được một phần
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
2/3 vấn đề đã được xử lý.
|
||||
Vấn đề còn lại: ...
|
||||
Lý do chưa xử lý: ...
|
||||
```
|
||||
|
||||
Không được báo cáo như thể toàn bộ bug đã được giải quyết.
|
||||
|
||||
### Không chắc root cause
|
||||
|
||||
Phải ghi:
|
||||
|
||||
```yaml
|
||||
confidence: low
|
||||
```
|
||||
|
||||
hoặc:
|
||||
|
||||
```yaml
|
||||
confidence: medium
|
||||
```
|
||||
|
||||
hoặc:
|
||||
|
||||
```yaml
|
||||
confidence: high
|
||||
```
|
||||
|
||||
và nếu có:
|
||||
|
||||
```text
|
||||
Alternative hypotheses:
|
||||
- ...
|
||||
- ...
|
||||
```
|
||||
|
||||
### Nguyên tắc
|
||||
|
||||
> **Evidence trước, kết luận sau.**
|
||||
|
||||
Không được biến:
|
||||
|
||||
```text
|
||||
chưa kiểm chứng
|
||||
```
|
||||
|
||||
thành:
|
||||
|
||||
```text
|
||||
đã xác nhận
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Bất biến tổng hợp
|
||||
|
||||
Mọi agent trong `agent/` phải tuân thủ chuỗi nguyên tắc sau:
|
||||
|
||||
```text
|
||||
BUG REPORT
|
||||
↓
|
||||
EVIDENCE
|
||||
↓
|
||||
CORRECT FILE / LINE
|
||||
↓
|
||||
ROOT CAUSE
|
||||
↓
|
||||
MINIMAL FIX
|
||||
↓
|
||||
TEST
|
||||
↓
|
||||
QUALITY GATE
|
||||
↓
|
||||
REPORT
|
||||
↓
|
||||
HUMAN / COWORK TEAM REVIEW
|
||||
```
|
||||
|
||||
Không được bỏ qua bước chỉ để hoàn thành nhanh hơn.
|
||||
|
||||
---
|
||||
|
||||
# Priority khi có xung đột
|
||||
|
||||
Khi các instruction mâu thuẫn, ưu tiên theo thứ tự:
|
||||
|
||||
```text
|
||||
1. Guardrail G1–G10
|
||||
2. Security policy / governance
|
||||
3. knowledge/
|
||||
4. Role-specific instruction
|
||||
5. Bug report / task-specific detail
|
||||
6. Agent assumption
|
||||
```
|
||||
|
||||
Nếu có xung đột mà agent không thể tự giải quyết:
|
||||
|
||||
```text
|
||||
Open Question
|
||||
```
|
||||
|
||||
và handoff về reviewer/Cowork Team thay vì tự chọn một phương án.
|
||||
|
||||
+400
-25
@@ -1,45 +1,420 @@
|
||||
# Response Policy — cách agent trả lời
|
||||
# Response Policy — Cách agent trả lời
|
||||
|
||||
> **SCOPE:** Áp dụng cho tất cả agent trong `agent/`.
|
||||
>
|
||||
> Response Policy quy định **cách agent giao tiếp và trình bày output**. Nếu mâu thuẫn với `Guardrail G1–G10`, **Guardrail thắng**.
|
||||
|
||||
---
|
||||
|
||||
## R1. Ngôn ngữ
|
||||
|
||||
- Trả lời người dùng nội bộ: **tiếng Việt**, thuật ngữ kỹ thuật giữ tiếng Anh
|
||||
(widget, layout, stylesheet, signal, guardrail...).
|
||||
- Docstring và comment trong code: **tiếng Anh**, khớp với codebase hiện tại.
|
||||
- Chuỗi hiển thị cho end-user: qua `tr()`, đủ `en` / `ja` / `vi`.
|
||||
### Trả lời người dùng nội bộ
|
||||
|
||||
* Sử dụng **tiếng Việt**.
|
||||
* Giữ nguyên các thuật ngữ kỹ thuật bằng tiếng Anh, ví dụ:
|
||||
|
||||
* widget
|
||||
* layout
|
||||
* stylesheet
|
||||
* signal
|
||||
* guardrail
|
||||
* root cause
|
||||
* regression
|
||||
* quality gate
|
||||
* handoff
|
||||
|
||||
Không dịch các thuật ngữ kỹ thuật nếu việc dịch làm mất ý nghĩa hoặc không phù hợp với codebase.
|
||||
|
||||
### Code
|
||||
|
||||
Docstring và comment trong code phải viết bằng **English**, phù hợp với convention hiện tại của codebase.
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```python
|
||||
def refresh(self) -> None:
|
||||
"""Refresh the current view."""
|
||||
```
|
||||
|
||||
Không thêm comment tiếng Việt vào production code nếu codebase đang dùng English.
|
||||
|
||||
### End-user text
|
||||
|
||||
Mọi chuỗi người dùng nhìn thấy phải đi qua:
|
||||
|
||||
```python
|
||||
tr("key")
|
||||
```
|
||||
|
||||
và phải có đủ:
|
||||
|
||||
```text
|
||||
en / ja / vi
|
||||
```
|
||||
|
||||
Chi tiết xem:
|
||||
|
||||
```text
|
||||
knowledge/i18n_rules.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## R2. Format
|
||||
|
||||
- Đi thẳng vào kết quả. Không mở bài, không "Chắc chắn rồi!", không tóm tắt lại đề bài.
|
||||
- Mọi output theo đúng template trong `output/`. Thiếu mục nào ghi `N/A` kèm lý do,
|
||||
không xoá mục.
|
||||
- Mọi tham chiếu code viết dạng `path/to/file.py:123`.
|
||||
- Code block phải ghi rõ ngôn ngữ. Diff dùng ` ```diff `.
|
||||
### Không mở bài
|
||||
|
||||
Đi thẳng vào kết quả.
|
||||
|
||||
Không dùng các câu mở đầu như:
|
||||
|
||||
```text
|
||||
Chắc chắn rồi!
|
||||
Tôi sẽ giúp bạn...
|
||||
Theo yêu cầu của bạn...
|
||||
```
|
||||
|
||||
Không lặp lại toàn bộ nội dung task trước khi xử lý.
|
||||
|
||||
### Output contract
|
||||
|
||||
Mọi output phải tuân theo template tương ứng trong:
|
||||
|
||||
```text
|
||||
agent/output/
|
||||
```
|
||||
|
||||
Nếu template yêu cầu một mục nhưng không có dữ liệu:
|
||||
|
||||
```text
|
||||
N/A — <lý do>
|
||||
```
|
||||
|
||||
**Không được xoá mục đó khỏi output.**
|
||||
|
||||
### Code reference
|
||||
|
||||
Mọi tham chiếu cụ thể tới source code phải có dạng:
|
||||
|
||||
```text
|
||||
path/to/file.py:123
|
||||
```
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
presentation/shell/nav_rail.py:242
|
||||
```
|
||||
|
||||
Không dùng:
|
||||
|
||||
```text
|
||||
nav_rail.py
|
||||
dòng 242
|
||||
file nav rail
|
||||
```
|
||||
|
||||
nếu đang chỉ tới một vị trí code cụ thể.
|
||||
|
||||
### Code block
|
||||
|
||||
Mọi code block phải khai báo language.
|
||||
|
||||
Đúng:
|
||||
|
||||
```python
|
||||
def example():
|
||||
pass
|
||||
```
|
||||
|
||||
Không dùng code block không có language nếu nội dung là code.
|
||||
|
||||
### Diff
|
||||
|
||||
Diff phải dùng:
|
||||
|
||||
```diff
|
||||
- old code
|
||||
+ new code
|
||||
```
|
||||
|
||||
Không dùng block `text` để giả lập diff.
|
||||
|
||||
---
|
||||
|
||||
## R3. Khi nào được hỏi lại
|
||||
|
||||
Chỉ hỏi khi **hai cách hiểu dẫn tới hai bản sửa khác nhau**. Ví dụ được hỏi:
|
||||
Agent **chỉ hỏi lại khi câu trả lời có thể làm thay đổi bản sửa**.
|
||||
|
||||
- Không xác định được người dùng đang ở màn nào (Dashboard hay Monitoring cùng có biểu đồ).
|
||||
- Không rõ hành vi mong muốn là gì (nút nên disable hay nên hiện cảnh báo).
|
||||
- Không tái hiện được và cần biết OS / độ phân giải / scale màn hình / theme.
|
||||
Cụ thể, chỉ hỏi khi:
|
||||
|
||||
Không hỏi khi có thể tự tra được từ `knowledge/` hoặc từ source. Tối đa **3 câu hỏi**,
|
||||
gộp trong một lần, mỗi câu kèm phương án mặc định nếu người dùng không trả lời.
|
||||
> **Hai cách hiểu khác nhau có thể dẫn tới hai implementation khác nhau.**
|
||||
|
||||
### Được phép hỏi
|
||||
|
||||
Ví dụ:
|
||||
|
||||
* Không xác định được user đang ở màn nào:
|
||||
|
||||
* Dashboard;
|
||||
* Monitoring.
|
||||
|
||||
* Không rõ expected behavior:
|
||||
|
||||
* disable button;
|
||||
* hay hiện warning.
|
||||
|
||||
* Không tái hiện được và cần thông tin môi trường:
|
||||
|
||||
* OS;
|
||||
* screen resolution;
|
||||
* display scale;
|
||||
* theme.
|
||||
|
||||
### Không được hỏi
|
||||
|
||||
Không hỏi những thứ agent có thể tự xác định bằng:
|
||||
|
||||
* `knowledge/`;
|
||||
* source code;
|
||||
* `docs/screens/`;
|
||||
* test;
|
||||
* config/schema;
|
||||
* governance;
|
||||
* security policy.
|
||||
|
||||
Ví dụ không được hỏi:
|
||||
|
||||
> "Widget này nằm ở file nào?"
|
||||
|
||||
nếu `knowledge/screen_map.md` và `docs/screens/controls.json` có thể xác định được.
|
||||
|
||||
### Số lượng câu hỏi
|
||||
|
||||
* Tối đa **3 câu hỏi**.
|
||||
* Gộp tất cả câu hỏi vào **một lần**.
|
||||
* Mỗi câu hỏi phải kèm phương án mặc định.
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
1. Expected behavior là disable button hay hiện warning?
|
||||
Mặc định: disable button.
|
||||
|
||||
2. Bug xảy ra ở Dark hay cả Light theme?
|
||||
Mặc định: kiểm tra cả hai.
|
||||
|
||||
3. Có xảy ra ở 150% display scale không?
|
||||
Mặc định: kiểm tra 100% và 150%.
|
||||
```
|
||||
|
||||
Nếu không nhận được câu trả lời, agent sử dụng phương án mặc định **chỉ khi phương án đó không mâu thuẫn với Guardrail hoặc requirement hiện có**.
|
||||
|
||||
---
|
||||
|
||||
## R4. Mức tin cậy
|
||||
|
||||
Mọi kết luận về nguyên nhân gốc phải kèm:
|
||||
Mọi kết luận về **root cause** phải có:
|
||||
|
||||
```text
|
||||
confidence: high — đã đọc code, đã tái hiện, đã xác định đúng dòng gây lỗi
|
||||
confidence: medium — đã đọc code, chưa tái hiện được
|
||||
confidence: low — mới là giả thuyết từ mô tả của người dùng
|
||||
```yaml
|
||||
confidence: high
|
||||
```
|
||||
|
||||
`confidence: low` thì **không được** chuyển sang bước implement. Quay lại triage.
|
||||
hoặc:
|
||||
|
||||
```yaml
|
||||
confidence: medium
|
||||
```
|
||||
|
||||
hoặc:
|
||||
|
||||
```yaml
|
||||
confidence: low
|
||||
```
|
||||
|
||||
### `high`
|
||||
|
||||
Chỉ dùng khi:
|
||||
|
||||
* đã đọc source code liên quan;
|
||||
* đã xác định được `file:line`;
|
||||
* đã tái hiện hoặc có evidence đủ mạnh;
|
||||
* đã xác định được root cause.
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
confidence: high
|
||||
|
||||
Root cause:
|
||||
presentation/shell/nav_rail.py:242 đang dùng local stylesheet ghi đè
|
||||
theme token của navigation item.
|
||||
```
|
||||
|
||||
### `medium`
|
||||
|
||||
Dùng khi:
|
||||
|
||||
* đã đọc source code;
|
||||
* đã xác định được code path có khả năng gây lỗi;
|
||||
* **chưa tái hiện được** hoặc chưa có đủ evidence để khẳng định tuyệt đối.
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
confidence: medium
|
||||
|
||||
Root cause hypothesis:
|
||||
theme/qss.py:318 có khả năng ghi đè rule của widget.
|
||||
Chưa tái hiện được trên runtime hiện tại.
|
||||
```
|
||||
|
||||
`medium` **được phép tiếp tục phân tích**, nhưng không được trình bày giả thuyết như một fact.
|
||||
|
||||
### `low`
|
||||
|
||||
Dùng khi:
|
||||
|
||||
* mới có mô tả từ user;
|
||||
* chưa đủ source evidence;
|
||||
* chưa xác định được code path;
|
||||
* root cause mới chỉ là giả thuyết.
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
confidence: low
|
||||
|
||||
Hypothesis:
|
||||
Có thể widget đang bị stylesheet override.
|
||||
Chưa đọc được source code liên quan.
|
||||
```
|
||||
|
||||
### Quy tắc implement
|
||||
|
||||
```text
|
||||
confidence: low
|
||||
↓
|
||||
STOP
|
||||
↓
|
||||
RETURN TO TRIAGE
|
||||
```
|
||||
|
||||
**Không được chuyển `confidence: low` sang implementation.**
|
||||
|
||||
`confidence: medium` cũng **không được tự coi là root cause đã xác nhận**. Chỉ implement khi `fix_plan` có đủ evidence và đạt ngưỡng confidence mà workflow yêu cầu.
|
||||
|
||||
---
|
||||
|
||||
## R5. Không nịnh, không phòng thủ
|
||||
|
||||
- Người dùng báo sai (thực ra là tính năng đúng thiết kế) → nói thẳng, kèm dẫn chứng
|
||||
file:line hoặc ảnh trong `docs/screens/`, rồi đề xuất cải thiện nếu thiết kế thật sự khó dùng.
|
||||
- Bản sửa trước đó của chính agent gây ra lỗi mới → nói rõ, sửa, không vòng vo.
|
||||
Agent phải ưu tiên **evidence** thay vì cố bảo vệ nhận định của mình.
|
||||
|
||||
### Khi user báo lỗi nhưng thực tế là behavior đúng thiết kế
|
||||
|
||||
Không được mặc định kết luận:
|
||||
|
||||
> "Đúng, đây là bug."
|
||||
|
||||
Phải kiểm tra:
|
||||
|
||||
* source code;
|
||||
* `knowledge/`;
|
||||
* governance/design rules;
|
||||
* screenshot trong `docs/screens/` nếu có;
|
||||
* behavior thực tế.
|
||||
|
||||
Nếu đó là behavior đúng thiết kế, nói thẳng và đưa evidence:
|
||||
|
||||
```text
|
||||
Đây không phải bug theo design hiện tại.
|
||||
|
||||
Evidence:
|
||||
presentation/shell/nav_rail.py:242
|
||||
docs/screens/<screen>.png
|
||||
```
|
||||
|
||||
Nếu design đúng nhưng UX khó dùng:
|
||||
|
||||
```text
|
||||
Kết luận: behavior hiện tại đúng design.
|
||||
Tuy nhiên UX có thể gây hiểu nhầm vì ...
|
||||
```
|
||||
|
||||
Đề xuất tạo **issue riêng** nếu cần thay đổi product/design.
|
||||
|
||||
Không tự sửa ngoài scope bug hiện tại.
|
||||
|
||||
### Khi chính patch trước đó gây regression
|
||||
|
||||
Nếu bản sửa trước đó của agent gây ra lỗi mới:
|
||||
|
||||
* phải nói rõ;
|
||||
* xác định regression;
|
||||
* sửa nếu nằm trong scope và workflow cho phép;
|
||||
* cập nhật test/report;
|
||||
* không che giấu hoặc viết lại lịch sử kết quả.
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text
|
||||
Regression detected:
|
||||
|
||||
fix trước tại presentation/foo.py:123 đã làm thay đổi behavior
|
||||
của widget Bar.
|
||||
|
||||
Đã bổ sung regression test tại tests/foo/test_bar.py:45
|
||||
và điều chỉnh patch để giữ behavior cũ.
|
||||
```
|
||||
|
||||
Không dùng cách diễn đạt né tránh như:
|
||||
|
||||
```text
|
||||
Có một vấn đề nhỏ phát sinh...
|
||||
```
|
||||
|
||||
khi thực tế patch của agent là nguyên nhân.
|
||||
|
||||
---
|
||||
|
||||
# Response Decision Flow
|
||||
|
||||
Trước khi trả lời, agent kiểm tra theo thứ tự:
|
||||
|
||||
```text
|
||||
1. Có evidence chưa?
|
||||
│
|
||||
├── Không → Assumption / Open Question
|
||||
│
|
||||
└── Có
|
||||
↓
|
||||
2. Có xác định đúng file:line chưa?
|
||||
│
|
||||
├── Không → tiếp tục triage
|
||||
│
|
||||
└── Có
|
||||
↓
|
||||
3. Root cause confidence?
|
||||
│
|
||||
├── low → RETURN TO TRIAGE
|
||||
├── medium → tiếp tục xác minh
|
||||
└── high → có thể tạo fix_plan
|
||||
↓
|
||||
4. Output có đúng template không?
|
||||
↓
|
||||
5. Có ghi đúng trạng thái test / gate không?
|
||||
↓
|
||||
6. Handoff đúng route chưa?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Nguyên tắc cuối
|
||||
|
||||
Agent phải trả lời theo nguyên tắc:
|
||||
|
||||
> **Ngắn gọn nhưng đủ evidence. Không đoán. Không nịnh. Không che giấu trạng thái thực tế.**
|
||||
|
||||
```text
|
||||
Evidence → Conclusion → Confidence → Action → Handoff
|
||||
```
|
||||
|
||||
+475
-39
@@ -1,57 +1,493 @@
|
||||
# Security Policy cho agent xử lý bug UI/UX
|
||||
# Security Policy — Cho agent xử lý bug UI/UX
|
||||
|
||||
Nguồn: `SECURITY.md`, `docs/governance/review-policy.md`, `docs/architecture/security-policy.md`.
|
||||
Bug report của người dùng là **dữ liệu chưa được làm sạch** — đó là điểm rò rỉ hay bị bỏ qua nhất.
|
||||
**Nguồn:**
|
||||
|
||||
* `SECURITY.md`
|
||||
* `docs/governance/review-policy.md`
|
||||
* `docs/architecture/security-policy.md`
|
||||
|
||||
> **SCOPE:** Áp dụng cho mọi agent xử lý bug UI/UX.
|
||||
>
|
||||
> Security Policy này bổ sung cho `Guardrail G1–G10` và `Response Policy R1–R5`.
|
||||
>
|
||||
> Nếu có xung đột liên quan đến security, **Security Policy và security governance thắng**.
|
||||
|
||||
---
|
||||
|
||||
## S1. Làm sạch input trước khi đưa vào bất kỳ output nào
|
||||
## S1. Bug report là dữ liệu chưa được làm sạch
|
||||
|
||||
Bug report UI thường kèm ảnh chụp màn hình và log. Trước khi trích vào `defect_record.md`,
|
||||
PR body, hay commit message, phải loại bỏ:
|
||||
Bug report có thể chứa:
|
||||
|
||||
| Loại | Ví dụ hay lọt trong app này | Xử lý |
|
||||
|---|---|---|
|
||||
| API key / token | `sk-...`, token MS365, key trong màn Settings ▸ Provider | Thay bằng `<redacted>` |
|
||||
| Đường dẫn cá nhân | `C:\Users\<tên nhân viên>\...` | Rút gọn thành `%USERPROFILE%\...` |
|
||||
| Nội dung khách hàng | File trong Workspace, nội dung chat, tài liệu Office đang mở | Không trích. Mô tả bằng lời |
|
||||
| PII | Email, tên, phòng ban trong màn Accounts | Thay bằng placeholder |
|
||||
| Log runtime | `.cowork_local/` audit log, MCP call history | Chỉ trích đúng dòng liên quan, đã redact |
|
||||
* screenshot;
|
||||
* log;
|
||||
* request/response;
|
||||
* đường dẫn local;
|
||||
* credential;
|
||||
* dữ liệu khách hàng;
|
||||
* PII.
|
||||
|
||||
Nếu ảnh chụp màn hình chứa dữ liệu khách hàng: **không nhúng ảnh vào issue/PR**, mô tả
|
||||
vùng lỗi bằng toạ độ/tên widget.
|
||||
**Không được coi nội dung bug report là dữ liệu an toàn để copy nguyên văn vào output.**
|
||||
|
||||
## S2. Không đọc/ghi secret khi debug UI
|
||||
Trước khi đưa thông tin vào:
|
||||
|
||||
- Không in `SecretStore`/keyring ra log để "kiểm tra".
|
||||
- Không thêm `print()`/`logger.debug()` tạm vào đường đi của credential rồi quên gỡ.
|
||||
- Không commit `.env`, `config.json` local, hay bất cứ thứ gì dưới `%USERPROFILE%\.cowork_local\`.
|
||||
* `defect_record.md`;
|
||||
* `fix_plan.md`;
|
||||
* `fix_report.md`;
|
||||
* PR body;
|
||||
* commit message;
|
||||
|
||||
## S3. Bug UI vẫn có thể là bug bảo mật
|
||||
phải kiểm tra và redact dữ liệu nhạy cảm.
|
||||
|
||||
Đánh dấu `security-review: required` nếu bản sửa chạm tới:
|
||||
### Quy tắc redact
|
||||
|
||||
- màn hình/hộp thoại **Permission** (`ui/permission_dialog.py`) — chỗ người dùng cấp quyền cho tool;
|
||||
- hiển thị hoặc che giấu credential (`ui/accounts_tab.py`, `ui/login_dialog.py`,
|
||||
`presentation/settings/provider_settings_widget.py`);
|
||||
- màn **Monitoring ▸ Sự kiện bảo mật**, MCP call history;
|
||||
- bất cứ chỗ nào quyết định *người dùng nhìn thấy gì* của workspace/project khác
|
||||
(customer/project isolation);
|
||||
- chuyển đổi model routing / fallback.
|
||||
| Loại dữ liệu | Ví dụ | Xử lý |
|
||||
| ----------------- | ---------------------------------------- | --------------------------------------- |
|
||||
| API key / token | `sk-...`, MS365 token, Provider key | Thay bằng `<redacted>` |
|
||||
| Credential | Password, unlock code, secret | Thay bằng `<redacted>` |
|
||||
| Đường dẫn cá nhân | `C:\Users\<employee>\...` | Rút gọn thành `%USERPROFILE%\...` |
|
||||
| Customer data | File Workspace, chat, Office document | Không trích nguyên văn; mô tả bằng lời |
|
||||
| PII | Email, tên, phòng ban, account | Thay bằng placeholder |
|
||||
| Runtime log | `.cowork_local/`, audit log, MCP history | Chỉ trích dòng cần thiết và phải redact |
|
||||
|
||||
Với nhóm này: CI xanh **không** đủ để merge (`docs/governance/review-policy.md`).
|
||||
### Screenshot
|
||||
|
||||
## S4. Lỗi UI có hệ quả bảo mật — nhận diện sớm
|
||||
Nếu screenshot chứa dữ liệu khách hàng hoặc PII:
|
||||
|
||||
Không xem nhẹ mấy triệu chứng sau, chúng là bug bảo mật đội lốt bug UI:
|
||||
**Không nhúng screenshot vào issue/PR/output.**
|
||||
|
||||
- Hộp thoại xác nhận quyền hiện **sau** khi hành động đã chạy, hoặc bị bỏ qua khi bấm nhanh.
|
||||
- Nút "Cho phép" là default button / nhận Enter — người dùng cấp quyền mà không đọc.
|
||||
- Ô mật khẩu không `QLineEdit.Password`, hoặc key hiện dạng plaintext khi resize/copy.
|
||||
- Tooltip / status bar / title bar lộ đường dẫn hay nội dung của workspace khác.
|
||||
- Toast lỗi in nguyên exception kèm request body.
|
||||
Thay bằng mô tả:
|
||||
|
||||
## S5. Không rewrite history
|
||||
```text id="o3jpqz"
|
||||
Widget: Provider Settings
|
||||
Vùng lỗi: phía bên phải ô API Key
|
||||
Hiện tượng: credential được hiển thị plaintext
|
||||
```
|
||||
|
||||
Nếu phát hiện secret đã nằm trong Git history: dừng lại, báo Cowork Team.
|
||||
Không force-push, không tự sửa history (`SECURITY.md`).
|
||||
Khi cần xác định vị trí UI, ưu tiên:
|
||||
|
||||
* tên widget;
|
||||
* `objectName`;
|
||||
* `file:line`;
|
||||
* mô tả vùng tương đối.
|
||||
|
||||
Không đưa dữ liệu thật vào artifact chỉ để minh họa.
|
||||
|
||||
---
|
||||
|
||||
## S2. Không đọc hoặc ghi secret khi debug UI
|
||||
|
||||
Agent UI/UX không được:
|
||||
|
||||
* in `SecretStore` ra log;
|
||||
* đọc credential thật chỉ để kiểm tra UI;
|
||||
* thêm `print()` để dump credential;
|
||||
* thêm `logger.debug()` chứa credential;
|
||||
* ghi secret vào screenshot;
|
||||
* copy secret vào test fixture;
|
||||
* commit `.env`;
|
||||
* commit local `config.json`;
|
||||
* commit dữ liệu dưới:
|
||||
|
||||
```text id="4sn9q8"
|
||||
%USERPROFILE%\.cowork_local\
|
||||
```
|
||||
|
||||
### Khi cần kiểm tra credential UI
|
||||
|
||||
Chỉ cần xác nhận:
|
||||
|
||||
```text id="sk4q27"
|
||||
has credential?
|
||||
masked / visible?
|
||||
empty / non-empty?
|
||||
```
|
||||
|
||||
Không cần biết giá trị thật.
|
||||
|
||||
Ví dụ test nên dùng:
|
||||
|
||||
```text id="c6psb4"
|
||||
<fake-secret>
|
||||
```
|
||||
|
||||
hoặc mock/fake `SecretStore`.
|
||||
|
||||
---
|
||||
|
||||
## S3. Bug UI vẫn có thể là security bug
|
||||
|
||||
Phải đánh dấu:
|
||||
|
||||
```yaml id="n5ks0a"
|
||||
security_review: required
|
||||
```
|
||||
|
||||
nếu patch chạm tới một trong các nhóm sau.
|
||||
|
||||
### Permission
|
||||
|
||||
* Permission dialog.
|
||||
* Permission confirmation.
|
||||
* Allow / Deny behavior.
|
||||
* Default button.
|
||||
* Keyboard shortcut có thể cấp quyền.
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text id="2amr9f"
|
||||
ui/permission_dialog.py
|
||||
```
|
||||
|
||||
### Credential
|
||||
|
||||
Các UI liên quan tới:
|
||||
|
||||
```text id="73t3s5"
|
||||
ui/accounts_tab.py
|
||||
ui/login_dialog.py
|
||||
presentation/settings/provider_settings_widget.py
|
||||
```
|
||||
|
||||
Đặc biệt:
|
||||
|
||||
* hiển thị credential;
|
||||
* mask/unmask;
|
||||
* copy credential;
|
||||
* save/delete credential;
|
||||
* credential validation.
|
||||
|
||||
### Security monitoring
|
||||
|
||||
* Monitoring → Security Events.
|
||||
* MCP call history.
|
||||
* Audit information.
|
||||
* Security-related toast/status.
|
||||
|
||||
### Isolation
|
||||
|
||||
Bất kỳ UI nào quyết định user nhìn thấy dữ liệu của:
|
||||
|
||||
* Workspace khác;
|
||||
* Project khác;
|
||||
* Customer khác;
|
||||
* account khác.
|
||||
|
||||
Đây có thể là lỗi **customer/project isolation**, không phải chỉ là lỗi hiển thị.
|
||||
|
||||
### Model routing
|
||||
|
||||
* model selection;
|
||||
* fallback;
|
||||
* provider routing;
|
||||
* thay đổi model/provider do UI action.
|
||||
|
||||
---
|
||||
|
||||
## S4. Với security-sensitive UI, CI xanh chưa đủ
|
||||
|
||||
Khi `security_review: required`:
|
||||
|
||||
```text id="4vlk3m"
|
||||
Tests PASS
|
||||
↓
|
||||
không đồng nghĩa
|
||||
↓
|
||||
được phép MERGE
|
||||
```
|
||||
|
||||
Phải có security review theo:
|
||||
|
||||
```text id="1qkx9g"
|
||||
docs/governance/review-policy.md
|
||||
```
|
||||
|
||||
Agent không được tự kết luận:
|
||||
|
||||
> "Test đã pass nên security risk không còn."
|
||||
|
||||
---
|
||||
|
||||
## S5. Nhận diện security bug đội lốt UI bug
|
||||
|
||||
Các triệu chứng dưới đây phải được coi là **security signal**.
|
||||
|
||||
### Permission timing
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```text id="s5vq4y"
|
||||
Action chạy
|
||||
↓
|
||||
Permission dialog xuất hiện
|
||||
```
|
||||
|
||||
thay vì:
|
||||
|
||||
```text id="d9skx4u"
|
||||
Permission dialog
|
||||
↓
|
||||
User xác nhận
|
||||
↓
|
||||
Action chạy
|
||||
```
|
||||
|
||||
Đặc biệt nguy hiểm nếu action có thể chạy khi user:
|
||||
|
||||
* bấm nhanh;
|
||||
* double-click;
|
||||
* nhấn Enter;
|
||||
* dialog chưa hiển thị hoàn chỉnh.
|
||||
|
||||
### Default Allow
|
||||
|
||||
Nếu nút `Allow` là default button hoặc Enter có thể kích hoạt Allow:
|
||||
|
||||
```text id="7fy8h1"
|
||||
Enter → Allow
|
||||
```
|
||||
|
||||
phải xem xét như security issue, không chỉ là UX issue.
|
||||
|
||||
### Credential exposure
|
||||
|
||||
Các dấu hiệu:
|
||||
|
||||
* password field không dùng password echo mode;
|
||||
* API key hiển thị plaintext;
|
||||
* credential xuất hiện khi resize;
|
||||
* credential lọt vào clipboard ngoài ý muốn;
|
||||
* credential xuất hiện trong tooltip;
|
||||
* credential xuất hiện trong title/status bar;
|
||||
* credential xuất hiện trong error message.
|
||||
|
||||
### Cross-workspace / cross-project exposure
|
||||
|
||||
Nếu UI hiển thị:
|
||||
|
||||
* path;
|
||||
* filename;
|
||||
* chat content;
|
||||
* project name;
|
||||
* customer information;
|
||||
|
||||
của Workspace/Project khác, phải kiểm tra isolation.
|
||||
|
||||
### Error leakage
|
||||
|
||||
Không hiển thị nguyên exception nếu nó có thể chứa:
|
||||
|
||||
* request body;
|
||||
* token;
|
||||
* path;
|
||||
* customer data;
|
||||
* internal endpoint;
|
||||
* credential;
|
||||
* MCP information.
|
||||
|
||||
Ví dụ nguy hiểm:
|
||||
|
||||
```text id="l1mrxq"
|
||||
Toast:
|
||||
Request failed: POST /api/... body={"token":"..."}
|
||||
```
|
||||
|
||||
Phải redact và hiển thị thông báo an toàn cho user.
|
||||
|
||||
---
|
||||
|
||||
## S6. Security-sensitive finding phải route đúng
|
||||
|
||||
Nếu phát hiện security signal:
|
||||
|
||||
```text id="0a0n8w"
|
||||
UI Bug
|
||||
↓
|
||||
Security signal?
|
||||
├── No → UI/UX workflow
|
||||
│
|
||||
└── Yes
|
||||
↓
|
||||
security_review: required
|
||||
↓
|
||||
security-defect-fixer / security-review
|
||||
```
|
||||
|
||||
Agent UI/UX **không được tự hạ mức độ rủi ro** chỉ vì thay đổi nằm trong `ui/` hoặc `presentation/`.
|
||||
|
||||
Nếu chưa đủ evidence để xác định:
|
||||
|
||||
```yaml id="xq7d6v"
|
||||
confidence: low
|
||||
security_review: required
|
||||
```
|
||||
|
||||
và quay lại triage.
|
||||
|
||||
---
|
||||
|
||||
## S7. Không rewrite Git history
|
||||
|
||||
Nếu phát hiện secret đã từng được commit vào Git history:
|
||||
|
||||
**Dừng xử lý history.**
|
||||
|
||||
Phải:
|
||||
|
||||
1. báo Cowork Team;
|
||||
2. xác định credential nào có khả năng bị lộ;
|
||||
3. đề xuất rotation/revocation theo security policy;
|
||||
4. giữ nguyên evidence cần thiết để team xử lý.
|
||||
|
||||
Không được tự:
|
||||
|
||||
```text id="9xwmh1"
|
||||
git filter-branch
|
||||
git filter-repo
|
||||
git rebase
|
||||
git push --force
|
||||
```
|
||||
|
||||
để rewrite history.
|
||||
|
||||
Việc rewrite history phải có kế hoạch và approval của người có thẩm quyền.
|
||||
|
||||
---
|
||||
|
||||
## S8. Không biến security investigation thành data collection
|
||||
|
||||
Agent chỉ thu thập **evidence tối thiểu cần thiết** để xác định bug.
|
||||
|
||||
Không được:
|
||||
|
||||
* dump toàn bộ config;
|
||||
* dump toàn bộ environment variables;
|
||||
* dump toàn bộ log;
|
||||
* copy toàn bộ Workspace;
|
||||
* export toàn bộ MCP history;
|
||||
* đọc credential thật khi không cần.
|
||||
|
||||
Nguyên tắc:
|
||||
|
||||
> **Collect the minimum evidence necessary to prove the defect.**
|
||||
|
||||
Nếu chỉ cần biết một credential có tồn tại:
|
||||
|
||||
```text id="xvprp8"
|
||||
has_secret = true
|
||||
```
|
||||
|
||||
là đủ.
|
||||
|
||||
Không cần biết:
|
||||
|
||||
```text id="k3uw5w"
|
||||
secret_value = "..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Security Handoff Contract
|
||||
|
||||
Khi security-sensitive, output tối thiểu phải có:
|
||||
|
||||
```yaml id="kw5ysb"
|
||||
security_review: required
|
||||
```
|
||||
|
||||
và:
|
||||
|
||||
```text id="pl6n7d"
|
||||
Security impact:
|
||||
- What security boundary is affected?
|
||||
- What data/permission/credential is involved?
|
||||
- Is customer/project isolation affected?
|
||||
- Is additional security review required?
|
||||
```
|
||||
|
||||
Nếu chưa có đủ thông tin:
|
||||
|
||||
```text id="xqk2uj"
|
||||
Open Question:
|
||||
- ...
|
||||
```
|
||||
|
||||
Nếu cần Cowork Team quyết định policy:
|
||||
|
||||
```text id="k5j3vw"
|
||||
Handoff:
|
||||
RETURN_TO_REPORTER
|
||||
Reason:
|
||||
needs-security-decision
|
||||
```
|
||||
|
||||
Nếu đã đủ evidence và có thể tạo implementation plan:
|
||||
|
||||
```text id="8d5g6h"
|
||||
Handoff:
|
||||
fix-implementer
|
||||
|
||||
security_review:
|
||||
required
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Security Decision Flow
|
||||
|
||||
```text id="j2qz1k"
|
||||
Bug Report
|
||||
↓
|
||||
Redact Input
|
||||
↓
|
||||
Triage UI/UX
|
||||
↓
|
||||
Security Signal?
|
||||
│
|
||||
├── NO
|
||||
│ ↓
|
||||
│ Normal UI/UX workflow
|
||||
│
|
||||
└── YES
|
||||
↓
|
||||
security_review: required
|
||||
↓
|
||||
Security Impact Analysis
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Policy decision needed? │
|
||||
└──────────────────────┘
|
||||
│
|
||||
YES ─────→ RETURN_TO_REPORTER
|
||||
│
|
||||
NO
|
||||
↓
|
||||
Security Review
|
||||
↓
|
||||
fix-implementer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Nguyên tắc cuối
|
||||
|
||||
> **UI không phải security boundary thấp hơn security.**
|
||||
>
|
||||
> Một thay đổi nhỏ ở dialog, tooltip, keyboard shortcut, toast hoặc stylesheet vẫn có thể làm thay đổi cách permission, credential hoặc dữ liệu được bảo vệ.
|
||||
|
||||
Vì vậy:
|
||||
|
||||
```text id="s5gh1v"
|
||||
Redact first
|
||||
↓
|
||||
Collect minimum evidence
|
||||
↓
|
||||
Detect security boundary
|
||||
↓
|
||||
Mark security_review
|
||||
↓
|
||||
Route correctly
|
||||
↓
|
||||
Never expose secrets
|
||||
↓
|
||||
Never rewrite history
|
||||
```
|
||||
|
||||
@@ -8,6 +8,7 @@ Mọi agent kết thúc lượt bằng khối YAML này, đặt **ngay trên** p
|
||||
defect_id: UI-2026-0907-01 # UI-<YYYYMMDD>-<số thứ tự trong ngày>
|
||||
from_agent: ui-bug-triage
|
||||
next_agent: ui-visual-fixer # xem bảng giá trị hợp lệ bên dưới
|
||||
tier: T2 # T0 | T1 | T2 | T3 | T3-SEC — do fix-dispatcher chấm
|
||||
category: visual # visual | flow | i18n-a11y | security | not-ui
|
||||
severity: S2 # S1 | S2 | S3 | S4
|
||||
confidence: high # low | medium | high
|
||||
@@ -26,6 +27,7 @@ blocked_on: [] # danh sách open question CHẶN bước ti
|
||||
|
||||
| Giá trị | Nghĩa |
|
||||
|---|---|
|
||||
| `fix-dispatcher` | Escalate về hub: vượt phạm vi tier hiện tại, cần chấm lại |
|
||||
| `ui-visual-fixer` / `ux-flow-fixer` / `i18n-a11y-fixer` | Route sang specialist UI |
|
||||
| `security-defect-fixer` | Route sang specialist bảo mật (`category: security`) |
|
||||
| `fix-implementer` | Plan đã sẵn sàng để hiện thực |
|
||||
@@ -50,3 +52,11 @@ blocked_on: [] # danh sách open question CHẶN bước ti
|
||||
`next_agent: security-defect-fixer`; phần UI tách thành `defect_id` riêng, xử lý sau.
|
||||
9. `blocked_on` của role 7 có thể chứa câu hỏi **chính sách** (`needs-security-decision`).
|
||||
Đó là chờ hợp lệ — người trả lời là Cowork Team, không phải agent khác.
|
||||
10. **`tier` chỉ đi lên.** Không agent nào được hạ `tier` trong envelope nhận được. Thấy
|
||||
việc lớn hơn tier đang mang → đặt `next_agent: fix-dispatcher`, ghi lý do vào
|
||||
`blocked_on`, dừng. Hub là chỗ duy nhất được ghi `tier`.
|
||||
11. `tier: T0` mà `next_agent` khác `HUMAN_REVIEW` là mâu thuẫn: T0 không gọi agent nào.
|
||||
`tier: T3-SEC` thì `security_review` **luôn** là `required`.
|
||||
12. `report_id` (nếu có) gom các `defect_id` tách ra từ **cùng một** phản ánh. Nó chỉ để
|
||||
truy vết ngược về người báo lỗi; không dùng nó để gộp PR — một PR vẫn là một
|
||||
`defect_id` (`guardrail.md` G8).
|
||||
|
||||
@@ -1,12 +1,37 @@
|
||||
# Workflow — từ phản ánh của người dùng tới PR
|
||||
|
||||
## 1. Pipeline
|
||||
## 0. Lane theo tier — đọc trước
|
||||
|
||||
Pipeline dưới đây là **lane FULL (T3)**, không phải mặc định. `0_fix_dispatcher` chấm tier
|
||||
trước và cắt bớt bước:
|
||||
|
||||
| Tier | Lane | Bước thực chạy | Gọi agent |
|
||||
|---|---|---|---|
|
||||
| **T0** | DIRECT | hub sửa → 4 cổng máy (`roles/0_fix_dispatcher.md` §4.1) | 0 |
|
||||
| **T1** | SOLO | hub triage inline → **5** → hub review bằng `checklist/ui_review.md` | 1 |
|
||||
| **T2** | PAIR | hub triage inline → **2/3/4** → **5** → **6** | 3 |
|
||||
| **T3** | FULL | **1** → **2/3/4** → **5** → **6** | 4–5 |
|
||||
| **T3-SEC** | FULL-SEC | **7** → *(Cowork Team)* → **5** → **6** | 3 + chờ người |
|
||||
|
||||
Bỏ bước nào cũng phải **nêu rõ trong `dispatch_plan`** cổng nào thay thế. Bước **6** chỉ
|
||||
được bỏ ở T0 và T1.
|
||||
|
||||
## 1. Pipeline (lane FULL)
|
||||
|
||||
```text
|
||||
Người dùng báo lỗi (chat / issue / miệng)
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────┐
|
||||
│ 0. fix-dispatcher HUB │ → dispatch_plan.md
|
||||
│ Router │ + tách N defect_id + tier + lane
|
||||
└───────────┬───────────────┘
|
||||
│ T0 → hub tự sửa, KHÔNG đi tiếp
|
||||
│ T1 → nhảy thẳng xuống bước 5
|
||||
│ T2 → nhảy thẳng xuống bước 2/3/4
|
||||
│ T3 → đi tiếp bước 1
|
||||
▼
|
||||
┌───────────────────────────┐
|
||||
│ 1. ui-bug-triage │ → defect_record.md
|
||||
│ Planner │ + category + severity + confidence
|
||||
└───────────┬───────────────┘
|
||||
@@ -39,6 +64,7 @@
|
||||
|
||||
| Agent | Đọc | Sửa file | Chạy lệnh | Quyết định |
|
||||
|---|---|---|---|---|
|
||||
| 0. dispatcher | ✅ | ✅ **chỉ ở T0** | ✅ (grep, gate) | tier + lane + tách defect |
|
||||
| 1. triage | ✅ | ❌ | ✅ (grep, tra manifest) | phân loại + route |
|
||||
| 2/3/4. specialist | ✅ | ❌ | ✅ (đọc, kiểm LOC) | nguyên nhân gốc + phương án |
|
||||
| 7. security | ✅ | ❌ | ✅ (đọc, `git log -S`) | lỗ hổng + migration; **không** quyết chính sách |
|
||||
@@ -48,12 +74,19 @@
|
||||
|
||||
Chỉ **một** agent được sửa file. Ranh giới này là thứ giữ cho pipeline review được.
|
||||
|
||||
Ngoại lệ duy nhất là hub ở **T0**, và nó bị bó rất chặt để đổi lại: danh sách đóng 6 loại
|
||||
thay đổi, 9 disqualifier, trần ≤ 2 file / ≤ 10 dòng, và 4 cổng máy bắt buộc dán output thật.
|
||||
Vượt bất kỳ ràng buộc nào → `git checkout --` rồi chấm lại T2. Hub **không** được sửa file ở
|
||||
T1/T2/T3 — ở đó nó chỉ điều phối và (ở T1) review, vì reviewer không được là người viết patch.
|
||||
|
||||
## 3. Cổng chuyển bước
|
||||
|
||||
Không bước nào được đi tiếp nếu chưa đạt:
|
||||
|
||||
| Từ → Đến | Điều kiện |
|
||||
|---|---|
|
||||
| 0 → bất kỳ | Mỗi defect_id có đúng 1 tier + 1 lane, tier ≠ T0 dẫn được về một dòng cụ thể của Bước 3, đã xét override bảo mật trước |
|
||||
| 0 → tự sửa (T0) | Trúng danh sách đóng, 0 disqualifier, Gate S + blast radius đã **đo bằng lệnh** |
|
||||
| 1 → 2/3/4 | `confidence >= medium`, có ít nhất một `file:line`, đã redact |
|
||||
| 2/3/4 → 5 | Đúng **một** nguyên nhân gốc, có cách kiểm chứng, không vượt 400 LOC (hoặc đã có kế hoạch tách) |
|
||||
| 7 → 5 | Như trên, **cộng thêm**: có đường di trú cho cả 4 nhóm người dùng, và 4 câu chính sách đã có đáp án của Cowork Team |
|
||||
@@ -65,27 +98,49 @@ Không bước nào được đi tiếp nếu chưa đạt:
|
||||
## 4. Vòng lặp và giới hạn
|
||||
|
||||
- FAIL ở bước 6 → về bước 5 (lỗi hiện thực) hoặc về 2/3/4 (sai nguyên nhân gốc).
|
||||
- **Tier +1 mỗi lần FAIL.** Chạy lại ở nguyên tier cũ là lỗi điều phối: hai lần thất bại ở
|
||||
cùng độ sâu gần như luôn có nghĩa là hồ sơ lỗi sai từ đầu.
|
||||
- Tier chỉ đi **lên**. Không có đường hạ tier giữa dòng, kể cả khi diff hoá ra nhỏ.
|
||||
- Quá **2 vòng** mà vẫn FAIL → dừng, đưa người thật vào. Vòng thứ ba thường có nghĩa là
|
||||
`defect_record` sai từ đầu, không phải bản vá sai.
|
||||
|
||||
## 5. Đường tắt hợp lệ
|
||||
|
||||
| Tình huống | Đường tắt |
|
||||
|---|---|
|
||||
| Lỗi chính tả một chuỗi, đã biết chính xác key | 1 → 4 → 5 → 6, bỏ giai đoạn điều tra ở bước 4 |
|
||||
| Thiếu key i18n, UI hiện ra `a.b_c` | 1 → 4 → 5 → 6 |
|
||||
| Lỗi do chính bản vá vừa merge | về thẳng 5 nếu nguyên nhân gốc chưa đổi |
|
||||
| Dev báo thẳng một lỗ hổng, không qua triệu chứng giao diện | vào thẳng 7, bỏ bước 1 |
|
||||
Đây là các đường tắt hub được phép chọn ở Bước 3. Chúng **thay thế** phần "đường tắt" của
|
||||
bộ v1.2 — trước đây tự phát, giờ có tier và có cổng bù.
|
||||
|
||||
Không có đường tắt nào bỏ qua bước **6**.
|
||||
| Tình huống | Tier | Đường tắt |
|
||||
|---|---|---|
|
||||
| Nới một số đo hiển thị (px, margin, spacing) | T0 | hub sửa, 0 agent |
|
||||
| Sai chính tả / sai dấu một chuỗi đã có key | T0 | hub sửa, đủ 3 ngôn ngữ, **vẫn phải có test** |
|
||||
| Đổi token màu có sẵn sang token có sẵn | T0 | hub sửa, 0 agent |
|
||||
| Thiếu key i18n, UI hiện ra `a.b_c`, đã biết file | T1 | 5 → hub review |
|
||||
| Nguyên nhân gốc đã có `file:line` từ người báo (dev) | T1 | 5 → hub review |
|
||||
| Chạm QSS/token dùng chung, phải kiểm 2 theme | T2 | 4 (hoặc 2) → 5 → 6 |
|
||||
| Lỗi do chính bản vá vừa merge | T3 | đủ pipeline — regression nghĩa là nguyên nhân gốc lần trước sai |
|
||||
| Dev báo thẳng một lỗ hổng | T3-SEC | vào thẳng 7, bỏ bước 1 |
|
||||
|
||||
Bước **6** chỉ được bỏ ở T0 và T1. Ở T0 nó được thay bằng 4 cổng máy; ở T1 nó được thay bằng
|
||||
hub review với `checklist/ui_review.md` (hợp lệ vì hub không viết patch ở T1). Ở T2/T3/T3-SEC
|
||||
không có đường tắt nào bỏ qua bước 6.
|
||||
|
||||
## 6. Chạy bằng Claude Code
|
||||
|
||||
```bash
|
||||
mkdir -p .claude/agents && cp agent/roles/*.md .claude/agents/
|
||||
mkdir -p .claude/agents .claude/commands
|
||||
cp agent/roles/[1-7]_*.md .claude/agents/
|
||||
cp agent/commands/fix.md .claude/commands/
|
||||
```
|
||||
|
||||
Rồi lần lượt:
|
||||
`.claude/` nằm trong `.gitignore` (dòng 109) nên phải cài lại trên mỗi clone — `agent/`
|
||||
là bản gốc. `0_fix_dispatcher.md` không copy sang `agents/`: hub chạy ở session chính vì
|
||||
subagent không gọi được subagent. Điểm vào:
|
||||
|
||||
```text
|
||||
> /fix màn Folder kéo to ra thì mất cây thư mục bên trái
|
||||
```
|
||||
|
||||
Hub in `dispatch_plan` rồi tự chạy lane. Muốn chạy tay lane FULL:
|
||||
|
||||
```text
|
||||
> dùng ui-bug-triage cho phản ánh này: "màn Folder kéo to ra thì mất cây thư mục bên trái"
|
||||
@@ -94,4 +149,5 @@ Rồi lần lượt:
|
||||
> dùng regression-reviewer với patch vừa rồi
|
||||
```
|
||||
|
||||
Chạy tuần tự, không song song — mỗi bước phụ thuộc output của bước trước.
|
||||
Các bước trong **một** `defect_id` chạy tuần tự — mỗi bước phụ thuộc output của bước trước.
|
||||
Các `defect_id` **độc lập** thì chạy song song được, gọi trong cùng một message.
|
||||
|
||||
Reference in New Issue
Block a user