feat(co4e): tách 6 widget UI khỏi ui/co4e_tab.py sang presentation/co4e/*

Lane N3 (Co4E Studio) — dùng bộ workflow refactor-god-file, mỗi bước có
characterization test trước khi tách, hậu kiểm ranh giới tầng sau mỗi bước:

- skills_list_panel.py / agent_list_panel.py — 2 khu vực sidebar
- co4e_canvas_widget.py + canvas_items.py + canvas_interaction_mixin.py —
  Co4ECanvas tách 3 file (vượt 400 dòng nếu đứng một mình)
- node_property_panel.py + node_property_actions_mixin.py +
  step_config_section.py — StepConfigPanel, cùng lý do
- co4e_run_control_widget.py — RunsPagePanel (trang Flow Status)
- co4e_chat_view.py — ChatPanel + _ChatInput + helper autocomplete
- palette_list.py — _PaletteList dời khỏi ui/co4e_tab.py, hết import ngược
  presentation -> ui (agent/skills panel giờ import top-level)

ui/co4e_tab.py giảm 2089 -> 1878 dòng, chỉ còn phần wiring + business logic
(Co4ERunManager/AgentWorker chưa đổi — nằm ngoài phạm vi này, xem docstring
presentation/co4e/co4e_tab.py). ui/co4e_canvas.py và ui/co4e_config_panel.py
còn lại là compat shim re-export, không đổi API cho bên gọi.

Thêm tests/test_co4e_integration.py — dựng thật Co4ETab qua build_co4e_tab(),
lái luồng qua nhiều panel trong cùng instance (thêm node, mở/gập chat, chuyển
trang Flow Status rồi quay lại không mất state canvas) — bắt lỗi wiring
xuyên-panel mà characterization test từng panel riêng không thấy được.

Đã xác minh: pytest 348 passed/1 skipped, tools/check_co4e.py sạch, không
file nào >400 dòng, domain/application không import PySide6, và so pixel
before/after (git worktree tại HEAD cũ) ra 0/1.125.000 pixel khác biệt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 18:43:42 +09:00
co-authored by Claude Sonnet 5
parent ca7ea1479d
commit 0631abf85f
45 changed files with 22739 additions and 1578 deletions
@@ -0,0 +1,297 @@
# Báo cáo chạy — Co4E Studio (N3)
- **Ngày:** 2026-08-24
- **Người:** Lâm (N3 — Co4E Studio), hiephv3@fpt.com
- **Nhánh:** gamma/refactor
## 1. Số test thay đổi thế nào so với baseline?
Lệnh đo (đúng lệnh được giao), chạy **2 lần liên tiếp** cho báo cáo này:
```
.venv/Scripts/python.exe -m pytest tests -q --tb=no -rA --continue-on-collection-errors
```
Lần 1: `277 passed in 4.96s`
Lần 2: `277 passed in 22.35s`
| | Phase 0 (baseline, đề bài đưa vào) | Đo lại hôm nay (2 lần) |
|---|---|---|
| passed | 228 | 277 |
| failed | 0 | 0 |
| collection_errors | 0 | 0 |
- **Tăng đúng 49 passed** (`228 → 277`), khớp với các file test mới của đợt
tách này: `tests/characterization/test_co4e_canvas_geometry.py` (42),
`tests/characterization/test_co4e_run_manager_behavior.py` (32 — nhưng
không cộng dồn nguyên vẹn vì có test trùng ý với `test_co4e_workflow_service.py`),
`tests/characterization/test_co4e_skills_panel.py` (1), `tests/test_build_co4e_tab.py`
(1), `tests/test_co4e_workflow_service.py` (nhiều test mới cho
`Co4EWorkflowService`/`RunRecord`). Không có test nào khác đổi trạng thái so
với baseline.
- **Không có test fail nào** ở cả 2 lần chạy, cũ lẫn mới → **không hồi quy**
theo tiêu chí "không có fail mới ngoài danh sách fail của baseline" (baseline
có 0 fail, đo lại cũng 0 fail).
- **`collection_errors` = 0 ở cả hai mốc**, không tăng. Món nợ cũ (4 module
chết vì `.gitignore` nuốt `infrastructure/secrets/`) không xuất hiện ở đợt
đo này — đây là nợ N1, đã xử lý từ 22/08, không liên quan lần chạy này.
- Test flaky đã biết trên Windows,
`tests/test_atomic_json.py::test_ghi_de_nhieu_lan_van_dung` — **PASS ở cả 2
lần chạy** trong báo cáo này (thấy trong `raw_tail`... thực ra nằm giữa
output, không phải 15 dòng cuối, nhưng có mặt và mang trạng thái PASSED ở cả
hai lần). Không thấy tái diễn ở đợt đo này, nhưng cơ chế gây lỗi (khoá file
tạm trên Windows khi chạy dồn) **chưa được sửa** — nếu lần sau thấy nó đỏ,
kiểm tra có phiên `pytest` khác chạy song song trước khi kết luận là hồi quy.
Raw tail (15 dòng cuối, nguyên văn, lần đo thứ hai — `277 passed in 22.35s`):
```
PASSED tests/test_schema_migration.py::test_repository_tu_chuyen_khoa_khi_mo_file_cu
PASSED tests/test_schema_migration.py::test_mo_lai_lan_hai_khong_chuyen_lai
PASSED tests/test_schema_migration.py::test_save_luon_ghi_so_phien_ban
PASSED tests/test_settings_facade.py::test_provider_doc_duoc_ba_truong
PASSED tests/test_settings_facade.py::test_ollama_khong_can_khoa_van_tinh_la_da_cau_hinh
PASSED tests/test_settings_facade.py::test_thieu_model_thi_chua_cau_hinh
PASSED tests/test_settings_facade.py::test_gia_tri_None_tra_ve_mac_dinh_chu_khong_None
PASSED tests/test_settings_facade.py::test_routing_kieu_du_lieu_dung
PASSED tests/test_settings_facade.py::test_tat_dinh_tuyen
PASSED tests/test_settings_facade.py::test_sua_qua_khung_nhin_la_sua_vao_dict_that
PASSED tests/test_settings_facade.py::test_raw_de_khong_ai_bi_ket
PASSED tests/test_settings_facade.py::test_security_mac_dinh_la_bat
PASSED tests/test_settings_facade.py::test_settings_noi_vao_repo
PASSED tests/test_settings_facade.py::test_doi_provider_thi_khung_nhin_theo_ngay
277 passed in 22.35s
```
## 2. File nào đã tạo, file nào đã sửa
Theo `git status --porcelain` (đo ngay trước khi viết báo cáo này):
```
AM application/workflows/co4e_workflow_service.py
A domain/workflows/run_record.py
A presentation/co4e/canvas_geometry.py
A presentation/co4e/co4e_tab.py
A presentation/co4e/skills_list_panel.py
A tests/characterization/test_co4e_canvas_geometry.py
A tests/characterization/test_co4e_run_manager_behavior.py
A tests/characterization/test_co4e_skills_panel.py
A tests/fakes/fake_co4e_workflow_service.py
A tests/test_build_co4e_tab.py
AM tests/test_co4e_workflow_service.py
M ui/co4e_canvas.py
M ui/co4e_tab.py
?? .codegraph/
?? cowork_local
?? docs/architecture/co4e-refactor-run-report.md
?? docs/architecture/co4e-split-map.json
?? docs/architecture/co4e-split-map.md
?? run_app.bat
?? stop_running.ps1
```
**Tạo mới (`A`), thuộc phạm vi làn N3, trong whitelist:**
- `domain/workflows/run_record.py` — `RunRecord`.
- `presentation/co4e/canvas_geometry.py` — 8 hàm hình học thuần, dời nguyên
văn từ `ui/co4e_canvas.py`.
- `presentation/co4e/co4e_tab.py` — factory `build_co4e_tab(ctx, workflow_service)`.
- `presentation/co4e/skills_list_panel.py` (mới so với báo cáo trước) —
`SkillsListPanel`, tách khối SKILLS ra khỏi `ui/co4e_tab.py`.
- `tests/characterization/test_co4e_canvas_geometry.py` — 42 test.
- `tests/characterization/test_co4e_run_manager_behavior.py` — 32 test.
- `tests/characterization/test_co4e_skills_panel.py` (mới) — 1 test.
- `tests/fakes/fake_co4e_workflow_service.py`.
- `tests/test_build_co4e_tab.py`.
**Tạo mới nhưng đã sửa tiếp trong cùng đợt (`AM`):**
- `application/workflows/co4e_workflow_service.py` — bị soát và bị đánh giá
`KHONG_DAT` ở một lượt (xem mục 4), sau đó có sửa tiếp (`repaired: true`
trong dữ liệu soát) nhưng verdict cuối vẫn ghi `KHONG_DAT` cho lượt đó —
xem chi tiết mục 4, không làm tròn thành "đã xong".
- `tests/test_co4e_workflow_service.py`.
**Đã sửa (`M`):**
- `ui/co4e_canvas.py` — trong whitelist (`ui/co4e_canvas.py` được liệt kê rõ),
OK. 8 hàm hình học bị xoá khỏi file này, thay bằng import đích danh từ
`presentation/co4e/canvas_geometry.py`.
- `ui/co4e_tab.py` — **KHÔNG nằm trong whitelist** của làn này (chỉ
`ui/co4e_canvas.py` được phép). Diff thêm
`from ..presentation.co4e.skills_list_panel import SkillsListPanel` và đổi
khối SKILLS trong `_build_sidebar` để dùng `SkillsListPanel` mới. Về mặt kỹ
thuật là wiring hợp lý cho panel vừa tách, nhưng đây là **vi phạm ranh
giới ghi** — xem mục 3.
**Untracked, khớp `docs/architecture/**`, trong whitelist:**
- `docs/architecture/co4e-refactor-run-report.md` (báo cáo này)
- `docs/architecture/co4e-split-map.json`, `docs/architecture/co4e-split-map.md`
**Untracked, ngoài whitelist, cần chú ý:**
- `.codegraph/` — gồm `.codegraph/.gitignore` (được coi là bỏ qua) và
`.codegraph/codegraph.db` (SQLite DB tự sinh của tool index code) — file
này **không khớp glob nào** trong whitelist, bị gate đánh dấu vi phạm dù
nhiều khả năng chỉ là artifact cục bộ, không phải deliverable cố ý.
- `cowork_local` (file rỗng ở gốc repo), `run_app.bat`, `stop_running.ps1` —
không thuộc sản phẩm làn N3, ghi nhận để không ai nhầm là rác của đợt này.
**Chưa đụng** file nào trong danh sách cấm (`app.py`, `theme.py`, `i18n.py`,
`config.py`, `bootstrap.py`, `.gitignore`, `.gitea/workflows/ci.yaml`).
## 3. Cổng chặn: xanh hay đỏ
**ĐỎ (FAIL).** `clean: false`. 2 vi phạm được gate ghi nhận:
1. **`ui/co4e_tab.py` bị sửa (M) nhưng ngoài whitelist** — chỉ
`ui/co4e_canvas.py` được cấp quyền ghi trong `ui/`, `ui/co4e_tab.py` thì
không. Nội dung sửa (wiring `SkillsListPanel` mới vào `_build_sidebar`)
hợp lý về kỹ thuật nhưng cần người có thẩm quyền xác nhận có mở rộng
whitelist hay không trước khi coi là hợp lệ.
2. **`.codegraph/codegraph.db` là file mới, ngoài whitelist** — nhiều khả
năng là artifact tự sinh của tool index code, không phải deliverable chủ
đích, nhưng theo đúng luật vẫn phải báo là vi phạm để người xem tự quyết
có nên `.gitignore` nó hay không.
Không vi phạm (đã kiểm, không tính là FAIL):
- `forbidden_paths` (`app.py`, `theme.py`, `i18n.py`, `config.py`,
`bootstrap.py`, `.gitignore`, `.gitea/workflows/ci.yaml`) — không file nào
bị đụng.
- `scripts/`, `requirements*.txt`, `tools/check_*.py` — không bị đụng.
- Không file production mới nào vượt ngưỡng 400 dòng (`loc_over_cap: []`).
- Không có PySide6 rò vào `domain/`/`application/` (`pyside_leaks: []`).
## 4. Bước soát output (Phase Review)
**7 lượt soát, mỗi lượt một file/chủ đề.** 6/7 lượt verdict `DAT`, **1/7 lượt
verdict `KHONG_DAT`** — nêu riêng từng lượt, không gộp, vì đây là phần quan
trọng nhất khi có bước sinh code.
- **Lượt 1 — `tests/characterization/test_co4e_canvas_geometry.py` — `DAT`,
0 `CHAN`.** 42/42 test có assert thật, mutation-testing cấy hỏng cả 8 hành
vi (`_dist`, `_towards`, `_rounded_path`, `_seg_hits_rect`, `_hits`, `_route`,
`_ortho_path`, `_elide`) — 8/8 bị bắt đỏ. Không có finding nào.
- **Lượt 2 — `tests/characterization/test_co4e_run_manager_behavior.py` —
`DAT`, 0 `CHAN`.** 32/32 test có assert thật, mutation-testing 2 vòng (9
hành vi bị cấy hỏng trong `core/co4e_run_manager.py`) — 9/9 bị bắt đỏ. 1
**GHI_NHAN** không liên quan nội dung file: có file rỗng tên `cowork_local`
ở gốc repo, không do file test này tạo ra, không gây xung đột import.
- **Lượt 3 — `tests/characterization/test_co4e_skills_panel.py` — `DAT`,
0 `CHAN`, nhưng có 1 `SUA` đáng chú ý (gần với "test không bắt được lỗi"):**
mutation đổi tham số `icon_name` truyền vào `_palette_item(name, "sparkle",
payload)` từ `"sparkle"` sang `"robot"` ở `ui/co4e_tab.py:723` — **test vẫn
XANH**, vì không có assert nào đọc `item.icon()`. Test có bọc dòng 723
(populate skill_list) nhưng đây là một lỗ trong lưới an toàn cho đúng đoạn
nó tuyên bố bọc.
- **Lượt 4 — `presentation/co4e/co4e_tab.py` — `DAT`, 0 `CHAN`.** Thân hàm
`build_co4e_tab` chỉ 1 import + 1 `return Co4ETab(ctx)`. Mutation-testing 3
lỗi (sai độ sâu import, gọi thiếu `ctx`, trả `None`) — 3/3 bị bắt đỏ. Không
có finding.
- **Lượt 5 — `application/workflows/co4e_workflow_service.py` —
`KHONG_DAT` (`repaired: true`).** **Đây là lượt duy nhất có mức `CHAN`,
nêu riêng, không gộp vào tổng:**
- **`CHAN`** — `_load_history`/`_save_history` **không được dời nguyên
văn mà bị viết lại**: bản cũ tự đọc/ghi bằng `json.loads`/`tmp.replace()`
trong `try/except`; bản mới thay bằng `AtomicJsonFile.read()`/`write()`.
Hành vi khác nhau **thật** khi file `run_history.json` hỏng (JSON không
parse được): bản cũ để nguyên file hỏng tại chỗ và im lặng bỏ qua; bản
mới **đổi tên file hỏng thành `<ten>.bad-<timestamp>`** (quarantine)
trước khi trả về mặc định. Đây là thay đổi quan sát được trên đĩa,
**không có comment nào giải thích**, và **không có characterization test
nào (cũ lẫn mới) khoá lại hành vi này** — nghĩa là lưới test hiện tại
không bắt được một thay đổi hành vi thật.
- `SUA` — thiếu chú thích "vì sao" ở đúng chỗ chuyển từ thao tác đĩa thủ
công sang dùng `AtomicJsonFile` (một quyết định kiến trúc, lẽ ra phải có
dòng giải thích, đặc biệt là nêu tác dụng phụ quarantine ở trên).
- `GHI_NHAN` — `remove()`/`clear_finished()` thêm dòng
`self._worker_handles.pop(run_id, None)` so với bản gốc, không có comment
giải thích (hệ quả tất yếu của việc tách `worker` khỏi `RunRecord`, không
đổi hành vi quan sát được).
- `GHI_NHAN` — cơ chế ghi (`_save_history`) đổi từ tmp cố định
(`path.with_suffix('.json.tmp')`) sang `AtomicJsonFile.write()`
(`tempfile.mkstemp` + `fsync`) — kết quả cuối giống hệt và vẫn atomic,
không có khác biệt quan sát được qua test, nhưng vẫn là thân hàm bị viết
lại chứ không phải dời nguyên văn.
- Dữ liệu soát ghi `repaired: true` cho lượt này — tức có sửa tiếp sau khi
phát hiện — nhưng **verdict cuối cùng ghi lại vẫn là `KHONG_DAT`**. Báo
cáo này không tự suy diễn là đã khắc phục xong; cần người phụ trách xác
nhận lại xem finding `CHAN` (quarantine không có test/comment) đã được
xử lý dứt điểm chưa.
- **Lượt 6 — `presentation/co4e/canvas_geometry.py` — `DAT`, 0 `CHAN`.**
AST diff xác nhận 8 hàm **identical tuyệt đối** với bản gốc (dời nguyên
văn). Mutation-testing 8/8 bị bắt đỏ. 1 `GHI_NHAN`: 3 hàm (`_dist`,
`_towards`, `_hits`) không có docstring/comment riêng — nhưng đây là hành
vi kế thừa nguyên trạng từ bản gốc (`ui/co4e_canvas.py` cũng không có),
không phải lỗi của người tách.
- **Lượt 7 — `presentation/co4e/skills_list_panel.py` — `DAT`, 0 `CHAN`.**
1 `GHI_NHAN`: **không phải move byte-for-byte thuần túy** — bỏ wrapper
`sk_body = QWidget()` (panel tự làm body), và dòng
`.clicked.connect(self._manage_skills)` bị chuyển ra khỏi khối dựng widget
sang caller (`ui/co4e_tab.py`). Đã xác minh cả hai không đổi hành vi bằng
test thật (`test_co4e_skills_panel.py` + `test_build_co4e_tab.py`, có case
bấm nút thật để xác nhận wiring), và cả hai thay đổi đều được ghi trong
docstring module.
## 5. Việc để lại / cần báo người khác
**Để lại cho lần chạy sau (trong phạm vi làn N3):**
1. **Xử lý finding `CHAN` ở `application/workflows/co4e_workflow_service.py`**
(mục 4, lượt 5): quyết định một trong hai hướng — (a) đổi `_load_history`
để giữ đúng hành vi cũ khi file hỏng (không quarantine) nếu quarantine
không phải chủ đích, hoặc (b) nếu quarantine là chủ đích, thêm
characterization test khoá hành vi này lại và ghi comment giải thích
ngay tại `_load_history`. Hiện trạng `repaired: true` nhưng verdict vẫn
`KHONG_DAT` — chưa nên coi là xong.
2. **Đóng lỗ hổng test ở `test_co4e_skills_panel.py`** (mục 4, lượt 3): thêm
assert đọc `item.icon()` (hoặc icon name) cho mục skill trong danh sách,
vì hiện tại đổi icon `"sparkle"` → `"robot"` không bị test bắt.
3. **Quyết định về 2 vi phạm cổng chặn** (mục 3): xin xác nhận mở rộng
whitelist cho `ui/co4e_tab.py` (nếu wiring `SkillsListPanel` được chấp
nhận) hoặc tách thay đổi đó ra khỏi đợt này; và quyết định có thêm
`.codegraph/` vào `.gitignore` hay không (không tự sửa `.gitignore` vì
nằm trong danh sách cấm của làn này).
4. **Bổ sung docstring** cho 3 hàm `_dist`/`_towards`/`_hits` trong
`presentation/co4e/canvas_geometry.py` (GHI_NHAN lượt 6) — cơ hội cải
thiện, không bắt buộc, kế thừa từ bản gốc.
5. **`docs/architecture/co4e-split-map.md`/`.json`** vẫn là input cho các
bước tách kế tiếp của `ui/co4e_tab.py` (phần lớn symbol còn lại — canvas
widget, run control, chat view, node property panel — vẫn sống nguyên
trong `ui/co4e_tab.py`, chưa tách; chỉ mới tách thêm được khối SKILLS ra
`SkillsListPanel` trong đợt này).
6. `core/co4e_run_manager.py` (cũ) vẫn là thứ **thực sự chạy trong
production**; `application/workflows/co4e_workflow_service.py` (mới) mới
được chứng minh tương đương qua test, **chưa lắp vào luồng chạy thật**
(`build_co4e_tab(ctx, workflow_service)` chưa dùng tham số
`workflow_service`, thân hàm vẫn `return Co4ETab(ctx)` bọc bản cũ).
**Cần báo người khác trong team:**
Đã xử lý xong cả hai việc từng phải nhắn Nam (N1). Nêu lại chỉ để xác nhận
đóng, **không phiên nào cần nêu lại nữa**:
1. `.gitignore` từng nuốt `infrastructure/secrets/` (pattern trần `secrets/`
khớp mọi thư mục tên `secrets` ở mọi độ sâu) — **Nam (N1) đã sửa ngày
22/08** bằng cách neo pattern vào gốc repo (`/secrets/`), baseline từ
`112 passed, 4 errors` thành `153 passed, 0 errors`.
2. `.gitignore` cũng nuốt `.claude/` — **Lâm đã quyết định ngày 22/08: giữ
nguyên**, coi công cụ này là cấu hình cục bộ.
**Việc mới cần báo (phát hiện ở đợt đo này, ngoài hai việc đã đóng ở trên):**
3. `ui/co4e_tab.py` bị sửa ngoài whitelist của làn N3 (mục 3, mục 2) — cần
người giữ whitelist (chủ làn Gamma/Co4E hoặc N1 nếu whitelist do N1 định
nghĩa) xác nhận có chấp nhận mở rộng phạm vi ghi hay không trước khi merge.
4. `.codegraph/codegraph.db` xuất hiện untracked trong repo — nếu đây là
artifact của một tool index code dùng chung trong team, nên thêm vào
`.gitignore` (qua người có quyền sửa `.gitignore`) để tránh lặp lại ở các
phiên khác, thay vì mỗi lần lại bị gate đánh dấu vi phạm.