Compare commits

..
Author SHA1 Message Date
huongltt35 10739f19aa breakdown folder tree for epic R01 2026-08-21 18:46:46 +09:00
gitea-admin 86c27e2e79 Merge pull request 'Feature/deltateam/refactor plan' (#5) from feature/deltateam/refactor-plan into main
CI / test (push) Canceled after 0s
Reviewed-on: #5
2026-08-21 00:46:40 +00:00
gitea-admin f1fc5bd7e7 Merge pull request 'feat(mcp): scaffold three project context tools' (#4) from codex/project-context-mcp-template into main
CI / test (push) Canceled after 0s
Reviewed-on: #4
2026-08-20 14:33:46 +00:00
thanhnv 202925e6ed feat(mcp): scaffold three project context tools
CI / test (pull_request) Canceled after 0s
2026-08-20 20:50:37 +07:00
anhtnm1andClaude Opus 5 d633dffae6 docs(refactor): add plan.md with roadmap sections VI-IX
CI / test (pull_request) Canceled after 0s
Copy of sections VI-IX from Feature_Architecture_Proposal.md
(roadmap, team assignment/KPI, anti-patterns, function migration map).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 22:22:16 +09:00
thanhnv 3827552909 fix(security): remove shared unlock defaults 2026-08-20 20:20:04 +07:00
anhtnm1andClaude Opus 5 73c9e4344c rename prompt.md to DeltaTeam_prompt.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 22:14:49 +09:00
huongltt35 2331b86db9 move file to docs folder 2026-08-20 22:05:52 +09:00
huongltt35 34626546b4 refactor plan 2026-08-20 22:00:53 +09:00
1419587401 Feature/fsg gamma team ui fix (#3)
CI / test (push) Canceled after 0s
## Summary

What changed and why?

## Change Type

- [ ] Cowork feature
- [ ] Bug fix
- [x] Core AI contribution
- [ ] Test / hardening
- [ ] Performance
- [ ] Documentation

## Related Work

Cowork Task:

Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets

Core AI Issue:

Core Task:

Related PR:

## Scope

What is intentionally included?

What is intentionally NOT included?

## Validation

- [ ] Unit tests
- [ ] Integration tests
- [ ] Manual verification
- [ ] Regression check

Commands / evidence:

## Security Impact

Permission / credential / network / customer data impact:

## Compatibility

- [ ] No breaking change
- [ ] Breaking change documented

## Reviewer Notes

Anything Cowork reviewers should pay attention to.

---------

Co-authored-by: Hiep Ha Van <hiephv3@fpt.com>
Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com>
Co-authored-by: Lam Hoang Van <lamhv7@fpt.com>
Co-authored-by: NamPDT <minhanhpkpro@gmail.com>
Reviewed-on: #3
2026-08-20 12:12:56 +00:00
70 changed files with 4145 additions and 11 deletions
+1
View File
@@ -0,0 +1 @@
"""Application Layer: Pure Python use cases and application services."""
+1
View File
@@ -0,0 +1 @@
"""Application conversations package: turn lifecycle orchestration and agent execution."""
+1
View File
@@ -0,0 +1 @@
"""Application model routing package: model route decisions and multi-provider balancing."""
+1
View File
@@ -0,0 +1 @@
"""Application monitoring package: Monitoring query service for audit and metrics."""
+1
View File
@@ -0,0 +1 @@
"""Application scheduling package: TaskApplicationService and AI task planning."""
+1
View File
@@ -0,0 +1 @@
"""Application settings package: Settings application service."""
+1
View File
@@ -0,0 +1 @@
"""Application workflows package: Co4E graph execution orchestration."""
+1
View File
@@ -0,0 +1 @@
"""Application workspaces package: File workspace and AI file editor services."""
+6 -2
View File
@@ -105,7 +105,7 @@ DEFAULT_CONFIG: Dict[str, Any] = {
# sandboxes agent-run shell commands) — reading a URL for info is safe and
# useful, so this defaults ON. Toggle in Settings → Security.
"allow_url_fetch": True,
"sandbox_pw": "quandh14", # default password to unlock sandbox settings
"sandbox_pw": "", # set through COWORK_SANDBOX_PASSWORD
"rulebase_path": "", # custom RULEBASE.md — attached to every agent execution
},
# Legacy generic-MCP-server list. MERGED into ext_connectors["other"] as of
@@ -173,7 +173,7 @@ DEFAULT_CONFIG: Dict[str, Any] = {
# Microsoft. Real Outlook/Teams/OneDrive/SharePoint access still requires a
# proper OAuth sign-in (not implemented yet) using tenant_id/client_id below.
"ms365": {
"unlock_code": "quandh14",
"unlock_code": "", # set through COWORK_MS365_UNLOCK_CODE
"unlocked": False, # runtime-only — never persisted as True, see save()
# Auto-connect MS365/OneDrive/SharePoint: the built-in MS365 MCP server
# launches automatically once the user is signed in (OAuth tenant/client
@@ -294,6 +294,10 @@ def _apply_env_overrides(data: Dict[str, Any]) -> Dict[str, Any]:
data["active_provider"] = os.environ["COWORK_ACTIVE_PROVIDER"]
if os.getenv("COWORK_CA_BUNDLE"):
data["tls_ca_bundle"] = os.environ["COWORK_CA_BUNDLE"]
if os.getenv("COWORK_SANDBOX_PASSWORD"):
data["agent_security"]["sandbox_pw"] = os.environ["COWORK_SANDBOX_PASSWORD"]
if os.getenv("COWORK_MS365_UNLOCK_CODE"):
data["ms365"]["unlock_code"] = os.environ["COWORK_MS365_UNLOCK_CODE"]
return data
@@ -0,0 +1,103 @@
# ADR-001: 4-Tier Clean Architecture for Desktop Local Application
* **Status**: ACCEPTED / ENFORCED
* **Date**: 2026-08-21
* **Deciders**: Team Duy (Tech Lead & AI Runtime), Team Nam (Governance & Automation), Team Hoa (Workspace & Scheduling)
* **Target Project**: Cowork Local (Cowork-Local BamBOO)
---
## 1. Context and Problem Statement
Cowork Local is a desktop application written in Python using PySide6 (Qt) and designed for local-first execution.
Historically, the codebase suffered from architectural coupling across layers:
1. **God-Widget Problem**: Monolithic UI widgets (e.g., `ui/chat_panel.py` >1,800 LOC, `ui/co4e_tab.py` >1,400 LOC) mixed UI rendering, network I/O, business rules, filesystem operations, and background worker lifecycle.
2. **Untestable Business Logic**: Core algorithms (model routing, conversation turn management, schedule calculation) were tightly coupled to `PySide6` widgets or `QTimer`, making unit testing in headless CI environments impossible without a graphical display server.
3. **Circular Dependencies & Global State Leaks**: Uncontrolled module imports (`model_pricing.py` ↔ `usage_tracker.py`, `agent_security.py` ↔ `agent_security_alert.py`) and mutable global state (`state.py::AppContext.active_project_id`) caused race conditions in background task runs.
---
## 2. Decision: 4-Tier Clean Architecture
We enforce a strict **4-Tier Clean Architecture** based on the Dependency Inversion Principle:
```text
┌─────────────────────────────────────────────────────────────┐
│ PRESENTATION │
│ (PySide6 Widgets, Dialogs, Qt Signals/Slots, View Models) │
└──────────────────────────────┬──────────────────────────────┘
│ depends on
▼
┌─────────────────────────────────────────────────────────────┐
│ APPLICATION │
│ (Use Case Services, Turn Orchestrators, Route Dispatchers) │
│ *** STRICTLY PURE PYTHON (0 Qt) *** │
└──────────────────────────────┬──────────────────────────────┘
│ depends on
▼
┌─────────────────────────────────────────────────────────────┐
│ DOMAIN & RUNTIME CORE │
│ (Entities, Value Objects, Domain Events, Tool Descriptors) │
│ *** STRICTLY PURE PYTHON (0 Qt) *** │
└──────────────────────────────▲──────────────────────────────┘
│ implemented by
┌──────────────────────────────┴──────────────────────────────┐
│ INFRASTRUCTURE │
│ (LLM Providers, Keyring Secrets, Atomic Persistence, MCP) │
└─────────────────────────────────────────────────────────────┘
```
---
## 3. Layer Definitions and Responsibilities
### Tier 1: Presentation Layer (`presentation/`)
* **Responsibilities**: UI component layout, user event capture, progress display, visual animations, confirmation dialog triggers.
* **Allowed Imports**: `PySide6.*`, `application.*`, `domain.*`.
* **Forbidden**: Direct database queries, raw LLM API calls, disk writes outside UI cache, executing tool commands directly.
* **Constraints**: Every widget file must strictly be **under 400 lines of code (LOC)**.
### Tier 2: Application Layer (`application/`)
* **Responsibilities**: Orchestrate single use cases (e.g. `ConversationApplicationService`, `RoutingApplicationService`, `TaskApplicationService`). Convert UI requests into domain requests, coordinate domain services with infrastructure adapters.
* **Allowed Imports**: `domain.*`, `infrastructure.*` interfaces/contracts, standard Python libraries.
* **Forbidden**: `PySide6`, `PyQt5`, `PyQt6`, `ui.*`, `app.*`.
* **Nature**: **100% Pure Python**. Must be executable and testable in headless CI environments without a display driver.
### Tier 3: Domain Layer (`domain/`)
* **Responsibilities**: Core domain models, frozen DTO snapshots (`ConversationExecutionRequest`), typed event streams (`AgentEvent`), descriptors (`ToolDescriptor`, `ProviderDescriptor`), deterministic calculation algorithms (`ScheduleCalculator`).
* **Allowed Imports**: Standard Python library only (`dataclasses`, `typing`, `enum`, `datetime`, `pathlib`, `abc`).
* **Forbidden**: `PySide6`, `PyQt*`, `requests`, `sqlalchemy`, filesystem mutations, OS network calls.
* **Nature**: Completely isolated and zero-dependency core.
### Tier 4: Infrastructure Layer (`infrastructure/`)
* **Responsibilities**: Adapters for external systems (OpenAI/Anthropic/Ollama/FPT providers, OS Keyring via `SecretStore`, `AtomicJsonFile` persistence, MCP child processes, filesystem tools).
* **Allowed Imports**: Third-party SDKs, OS libraries, `domain.*`.
* **Forbidden**: `presentation.*`, `PySide6.QtWidgets`.
---
## 4. Architectural Rules and Non-Negotiable Invariants
1. **Zero Qt in Business Logic**:
- `domain/` and `application/` must never import `PySide6` or `PyQt*`.
- Verified via AST parser script `scripts/check_imports.py`.
2. **Immutable Request Snapshots**:
- Turns are initiated using immutable frozen dataclasses (`ConversationExecutionRequest`) to decouple runtime state from mutable UI state.
3. **Thread Safety and Signal Decoupling**:
- AI generation and tool calls run asynchronously in worker threads.
- UI updates occur strictly on the Qt main thread by consuming `AgentEvent` streams through Qt Signal bridges.
4. **Single Responsibility and Modularity**:
- Production files must stay within **400 LOC**.
5. **English In-Code Comments**:
- Every modified or created line/block must include concise English comments explaining design decisions and processing logic.
---
## 5. Consequences and Compliance
* **Positive**:
- Full testability: Unit tests run in milliseconds without GUI or network mocks.
- Zero circular dependencies: Clear top-down data flow.
- Resilience: UI crashes do not corrupt background tasks or files.
* **Verification**:
- Automated CI gate: `python scripts/check_imports.py` and `python scripts/check_loc.py`.
+39
View File
@@ -0,0 +1,39 @@
# Danh Mục & Kế Hoạch Cô Lập Mã Nguồn Dormant / Dead Code (Dormant Code Catalog)
* **Tài liệu**: `docs/architecture/dormant-code.md`
* **Thuộc EPIC**: `R01: Architecture Foundation & Characterization`
* **Team phụ trách**: 🔵 **Team Duy (Tech Lead)**
---
## 1. Mục Đích & Nguyên Tắc Quản Trị
Trong quá trình phát triển nhanh, một số module, hàm hoặc script đã trở thành mã nguồn không hoạt động (**dormant**), mã nguồn thử nghiệm cũ (**legacy prototypes**), hoặc mã nguồn không còn được sử dụng (**dead code**).
> [!IMPORTANT]
> ### 🛡️ NGUYÊN TẮC CÔ LẬP MÃ NGUỒN CŨ:
> 1. **Tuyệt đối không import vào các tầng mới**: Các tầng `domain/`, `application/`, `infrastructure/` mới được xây dựng **cấm tuyệt đối import bất kỳ module dormant nào**.
> 2. **Không xóa vội vàng khi chưa có test bảo vệ**: Giữ nguyên mã nguồn cũ trong giai đoạn tái cấu trúc R01–R08; chỉ dọn dẹp hoặc xóa sau khi bộ kiểm thử khói E2E (EPIC R10) chạy pass 100%.
> 3. **Phân loại rõ ràng trạng thái**: Mỗi module dormant phải được gắn nhãn (DEPRECATED / ISOLATED / PENDING_DELETION).
---
## 2. Bảng Danh Mục Mã Nguồn Dormant / Dead Code Đã Rà Soát
| STT | File / Module / Ký Hiệu | Trạng Thái Hiện Tại | Lý Do Phân Loại & Phân Tích Kỹ Thuật | Kế Hoạch Xử Lý & Thời Điểm Gỡ Bỏ |
| :---: | :--- | :---: | :--- | :--- |
| **1** | `requirements (cloud copy).txt` | `PENDING_DELETION` | File sao chép dự phòng tạm thời trong quá khứ, không được tham chiếu bởi bất kỳ quy trình setup nào. | Gỡ bỏ trong EPIC R10 (Packaging & Clean-up). |
| **2** | `preview-desktop` | `ISOLATED` | Script shell rỗng/phác thảo cho môi trường dev container cũ. | Cô lập, không liên kết vào build workflow. |
| **3** | `scripts/bootstrap_gitea_repo.py` | `ISOLATED` | Script tiện ích bootstrap kho lưu trữ Gitea nội bộ; không thuộc runtime ứng dụng chính. | Di chuyển vào `docs/gitea/` làm tài liệu tham khảo ops. |
| **4** | Hàm routing sao chép tại `ui/chat_panel.py#L638` | `DEPRECATED` | Đoạn code logic chọn model lặp lại từ `core/routing/` nằm trực tiếp trong UI widget. | Thay thế hoàn toàn bằng `RoutingApplicationService` trong EPIC R03. |
| **5** | Biến toàn cục `state.py::active_project_id` | `DEPRECATED` | Biến global mutable gây race condition khi chạy background task song song. | Thay thế bằng `WorkspaceSession` trong EPIC R06. |
| **6** | Các hàm xử lý UI đồng bộ trong `core/tools.py` | `DEPRECATED` | `core/tools.py` chứa mã monolithic vừa xử lý file vừa gọi dialog xác thực trực tiếp. | Phân rã thành `file_tools.py`, `command_tools.py` và `ToolPolicyGateway` trong EPIC R05. |
---
## 3. Quy Trình Cô Lập & Kiểm Soát
1. **Kiểm tra tự động qua AST Guard**:
- Bộ script `scripts/check_imports.py` tự động quét để đảm bảo không có bất kỳ import mới nào trỏ tới các thành phần đã đánh dấu deprecated.
2. **Kế hoạch dọn dẹp cuối cùng (Release Phase - 31/08/2026)**:
- Sau khi hoàn thành EPIC R10 và pass toàn bộ bài test E2E (`tests/e2e/test_smoke.py`), các file đánh dấu `PENDING_DELETION` sẽ được gỡ bỏ khỏi nhánh `main`.
+58
View File
@@ -0,0 +1,58 @@
# Project Context MCP — hướng dẫn làm song song
Mục tiêu: hoàn thiện ba tool trên **cùng một server** `project_context`. Không tạo server, registry,
policy hay error envelope mới. Shared skeleton đã khóa sẵn thứ tự an toàn:
```text
validate input → policy ALLOW → resolve provider → gọi upstream → validate output
```
## Chia việc
| Người | Tool | Chỉ sửa | Branch đề xuất |
|---|---|---|---|
| Member A | `get_project_issue_context` | `tools/issue_context.py`, `providers/issue.py`, test riêng | `feat/mcp-issue-context` |
| Member B | `search_project_knowledge` | `tools/knowledge_search.py`, `providers/knowledge.py`, test riêng | `feat/mcp-knowledge-search` |
| Member C | `get_project_change_context` | `tools/change_context.py`, `providers/change.py`, test riêng | `feat/mcp-change-context` |
Trước khi gửi task, thay `Member A/B/C` bằng username thật trên ba issue. Mỗi người **không sửa**
`foundation.py`, `registry.py`, `runtime.py`, `server.py` hoặc file của người khác. Nếu shared contract
cần đổi, mở một PR nhỏ riêng và để cả ba người rebase sau khi PR đó merge.
## Bắt đầu trong 5 phút
1. Chạy `python --version` và xác nhận Python 3.11+ như baseline trong `requirements.txt`.
2. Tạo branch từ commit template chứa tài liệu này sau khi PR template merge.
3. Đọc input/output model trong module tool được giao; không thêm field riêng của Gitea/Jira/Redmine.
4. Implement provider read-only trong module `providers/<tool>.py`; credential chỉ lấy sau policy ALLOW.
5. Thêm test happy, invalid, not-found, timeout, DENIED với `resolver.calls == 0`, output sai schema,
truncation/cursor và source mở được có `revision`.
6. Chạy:
```bash
python -m pytest tests/test_project_context_mcp_template.py tests/test_project_context_<tool>.py -q
```
Lệnh trên chạy trực tiếp từ root repo `cowork_local`; `tests/conftest.py` đã thiết lập import path.
## Definition of Done của từng người
- Tool trả đúng schema, có `project_id` và source gồm `system`, `url`, `revision`, `retrieved_at`.
- Provider-neutral: đổi Gitea sang GitHub/Jira/Redmine không đổi schema hay tool name.
- Sai project bị `DENIED` trước khi resolve credential và trước mọi upstream call.
- Không log/return token; lỗi ngoài dự kiến không lộ exception; read không có side effect.
- Output lớn có `truncated`, `returned`, `remaining`, `next_cursor`; không cắt im lặng.
- Test riêng pass, test shared pass, PR chỉ chạm đúng vùng sở hữu trong bảng trên.
## Chạy server sau khi provider đã cấu hình
```bash
COWORK_MCP_ACTOR_ID=<actor> \
COWORK_MCP_ORG_UNIT=<org> \
COWORK_MCP_CUSTOMER=<customer> \
COWORK_MCP_PROJECT=<project> \
python -m cowork_local.mcp_servers.project_context_server
```
Không commit giá trị môi trường hoặc credential. Cowork kết nối bằng stdio với command Python và
args `-m cowork_local.mcp_servers.project_context_server`.
+115
View File
@@ -0,0 +1,115 @@
# HỆ THỐNG PROMPT KỸ SƯ TRƯỞNG PYTHON & KIẾN TRÚC SƯ TÁI CẤU TRÚC (TEAM DUY)
Bạn là một **Kỹ sư phần mềm Python Cao cấp (Senior / Staff Python Engineer) & Chuyên gia Kiến trúc Ứng dụng Desktop Local-First**, giữ vai trò Tech Lead thực thi kỹ thuật cho **🔵 Team Duy** trong dự án **Cowork Local (Cowork-Local BamBOO)**.
---
## 🎯 NHIỆM VỤ CỐT LÕI & PHẠM VI SỞ HỮU CỦA TEAM DUY
Nhiệm vụ của bạn là trực tiếp chỉ đạo và thực thi kế hoạch tái cấu trúc mã nguồn theo đúng tài liệu thiết kế kiến trúc `Feature_Architecture_Proposal.md` và cập nhật tiến độ vào file `Refactoring_Checklist.md`.
### 📦 Các Phân Hệ Thư Mục Do Team Duy Quản Lý:
- **Tầng Giao Diện (Presentation)**: `presentation/chat/` (Bóc tách từ `ui/chat_panel.py` và `ui/help_agent_widget.py`).
- **Tầng Nghiệp Vụ (Application)**: `application/conversations/`, `application/model_routing/`.
- **Tầng Miền Dữ Liệu (Domain)**: `domain/agents/`, `domain/models/`.
- **Tầng Hạ Tầng (Infrastructure)**: `infrastructure/providers/`, `infrastructure/telemetry/`.
- **Kiểm Thử & Quản Trị Hệ Thống (Testing & Governance)**: `tests/` (Unit, Contract, Integration, E2E Smoke), `scripts/` (Bộ công cụ kiểm duyệt CASAN Gate), `docs/governance/`.
- **Các EPIC Trọng Tâm**: **R01, R03, R04, R08 (Phân hệ Chat UI: R08-T01 ➔ R08-T06), R10 (Chủ trì chính Testing Pyramid & Phát hành)**.
---
## ⚖️ CÁC QUY TẮC KIẾN TRÚC & NGUYÊN TẮC BẤT BIẾN
1. **Kiến Trúc 4 Tầng Sạch (4-Tier Clean Architecture)**:
```text
presentation/chat/ (PySide6 UI Widgets & Qt Signals)
│
▼
application/conversations/ & application/model_routing/ (Pure Python Orchestration)
│
▼
domain/agents/ & domain/models/ (Pure Python Entities, Events, Descriptors)
▲
│
infrastructure/providers/ & infrastructure/telemetry/ (Adapters, Keyring, Network, Disk)
```
- **QUY TẮC CỐT TỬ**: Tầng `domain/` và `application/` phải là **100% Pure Python**. TUYỆT ĐỐI KHÔNG import `PySide6`, `PyQt*` hay bất kỳ UI widget nào trong 2 tầng này.
2. **Tuân Thủ Tuyệt Đối Cổng Kiểm Duyệt CASAN (CASAN Verification Gate)**:
- **C (Clean Arch)**: Chạy `python scripts/check_imports.py` phải đạt `0 Qt imports in domain and application`.
- **A (Atomic & Secret)**: 0 plaintext API Key/Token trong file cấu hình; 100% keys quản lý qua `SecretStore` (Keyring); ghi tệp an toàn qua `AtomicJsonFile`.
- **S (Single Responsibility)**: **GIỚI HẠN CỨNG: Không có file production nào vượt quá 400 dòng code (LOC)**.
- **A (Automated Tests)**: Bộ test chạy offline hoàn toàn, tốc độ siêu nhanh (< 1 giây cho unit tests), không phụ thuộc mạng hay Qt loop.
- **N (No Regression)**: 100% test pass khi chạy lệnh `pytest tests/`.
3. **Bắt Buộc Comment Code Bằng Tiếng Anh (Mandatory English Comments)**:
- Ở **mỗi dòng hoặc khối code được chỉnh sửa/tạo mới**, bạn **BẮT BUỘC phải viết comment bằng Tiếng Anh** giải thích rõ logic xử lý, cách xử lý ngoại lệ và lý do kỹ thuật/kiến trúc (rationale).
- *Ví dụ mẫu*:
```python
# Extract an immutable execution snapshot to decouple turn lifecycle from PySide6 UI state
request = ConversationExecutionRequest.from_ui_state(session_id=session_id, prompt=prompt)
```
4. **Ghi Nhận Mốc Thời Gian Thực Hiện (Start/End Timestamps)**:
- Trước khi bắt đầu code task nào, phải ghi nhận: `Start: YYYY-MM-DD HH:mm`.
- Sau khi code xong và unit test pass 100%, phải ghi nhận: `End: YYYY-MM-DD HH:mm` và đánh dấu `[x]` vào `Refactoring_Checklist.md`.
5. **An Toàn Đa Luồng (Thread-Safety) & Snapshot Bất Biến**:
- Mọi tiến trình gọi AI và thực thi Tool phải chạy bất đồng bộ trong background thread, không bao giờ làm đơ Main Thread của PySide6.
- Giao diện UI chỉ được cập nhật thông qua Qt Signals/Slots lắng nghe luồng sự kiện `AgentEvent`.
- Luôn đóng gói trạng thái đầu vào thành `ConversationExecutionRequest` bất biến trước khi gửi vào Application Service.
---
## 🛠️ LỘ TRÌNH THỰC THI TỪNG BƯỚC (TEAM DUY)
Khi thực hiện nhiệm vụ, tuân thủ đúng thứ tự 5 giai đoạn sau:
### 📍 Giai Đoạn 1: Thiết Lập Nền Móng Kiến Trúc & Test Bảo Vệ (EPIC R01)
1. `R01-T01`: Soạn thảo `docs/architecture/ADR-001-layered-architecture.md` định nghĩa ranh giới 4 tầng.
2. `R01-T02`: Xây dựng `tests/fakes/fake_provider.py` & `fake_tool_executor.py` phục vụ test offline.
3. `R01-T03`: Viết script phân tích cú pháp AST `scripts/check_imports.py` chặn import Qt trái phép.
4. `R01-T04`: Viết Characterization Tests tại `tests/characterization/test_run_cowork.py` chụp snapshot hàm `core/chat_agent.py::run_cowork`.
5. `R01-T05`: Phân loại và cô lập mã nguồn cũ trong `docs/architecture/dormant-code.md`.
### 📍 Giai Đoạn 2: Chuẩn Hóa Provider & Hợp Nhất Bộ Định Tuyến (EPIC R03)
1. `R03-T01`: Xây dựng bộ Contract Tests chuẩn hóa cho các Provider trong `tests/contracts/test_providers.py`.
2. `R03-T02`: Tạo `domain/models/provider_descriptor.py` và `infrastructure/providers/provider_registry.py`.
3. `R03-T03`: Xây dựng `application/model_routing/routing_application_service.py` (Pure Python) hỗ trợ 4 chế độ: Off, Auto, Manual, Fallback.
4. `R03-T04` & `R03-T05`: Hợp nhất logic routing bị phân tán tại `ui/chat_panel.py#L638`, `ui/co4e_tab.py`, `ui/folder_tab.py` về gọi chung `RoutingApplicationService`.
5. `R03-T06`: Tách bộ ghi nhận token usage thành `infrastructure/telemetry/usage_sink.py`.
### 📍 Giai Đoạn 3: Động Cơ Hội Thoại & Vòng Đời Turn Chat (EPIC R04)
1. `R04-T01`: Định nghĩa frozen dataclass snapshot `domain/agents/conversation_execution_request.py`.
2. `R04-T02`: Định nghĩa các sự kiện có kiểu dữ liệu mạnh trong `domain/agents/agent_event.py` (`TextChunkEvent`, `ToolCallStartedEvent`, `ToolCallFinishedEvent`, `TurnCompletedEvent`, `ErrorEvent`).
3. `R04-T03`: Cài đặt `application/conversations/conversation_application_service.py` điều phối toàn bộ vòng đời turn.
4. `R04-T04` & `R04-T05`: Chuyển đổi `ui/cowork_tab.py` và `core/task_executors.py` sang dùng chung `ConversationApplicationService`.
### 📍 Giai Đoạn 4: Phân Rã God-Widget Màn Hình Chat (EPIC R08 - Phân Hệ Chat)
Bóc tách file khổng lồ `ui/chat_panel.py` (>1.800 dòng) thành 6 widget con chuyên biệt (< 400 dòng/file):
1. `R08-T01`: `presentation/chat/chat_history_widget.py` (Render bong bóng chat, markdown stream, tool cards).
2. `R08-T02`: `presentation/chat/composer_widget.py` (Ô nhập liệu text auto-resize, phím tắt Ctrl+Enter).
3. `R08-T03`: `presentation/chat/attachment_picker.py` (Bộ chọn file, folder, ảnh đính kèm).
4. `R08-T04`: `presentation/chat/audio_recorder_widget.py` (Ghi âm giọng nói & nhận diện văn bản).
5. `R08-T05`: `presentation/chat/chat_output_panel.py` (Panel hiển thị và theo dõi file output trong turn).
6. `R08-T06`: `presentation/chat/chat_panel.py` (Shell container điều phối các widget con và `Floating HelpAgent`).
### 📍 Giai Đoạn 5: Tháp Kiểm Thử, Cổng CI Quality Gate & Smoke Test (EPIC R10 - Chủ Trì Chính)
1. `R10-T01`: Cấu trúc lại thư mục test phân tầng (`tests/unit/`, `tests/contracts/`, `tests/integration/`, `tests/fakes/`).
2. `R10-T02`: Xây dựng bộ script kiểm thử tự động (`scripts/check_imports.py`, `scripts/check_loc.py`, `scripts/audit_security.py`, `scripts/run_quality_gate.py`).
3. `R10-T03`: Cập nhật tài liệu `README.md` và `START_CONTRIBUTING.md` với sơ đồ 4 tầng và hướng dẫn cấu hình Git hook.
4. `R10-T04`: Soạn thảo `docs/governance/contributor-recipes.md` (3 công thức: Thêm Provider mới, Thêm Tool/MCP mới, Thêm Màn hình UI mới).
5. `R10-T05`: Xây dựng bộ kiểm thử khói phát hành `tests/e2e/test_smoke.py` chạy qua headless Qt kiểm tra tự động 5 luồng nghiệp vụ cốt lõi.
---
## 📋 CHECKLIST TIÊU CHUẨN HOÀN THÀNH (DEFINITION OF DONE - DOD)
Trước khi đóng bất kỳ task nào hoặc gửi PR, bạn phải tự kiểm tra 7 tiêu chí sau:
- [ ] 1. **Kích thước file (LOC)**: Mọi file sửa đổi hoặc tạo mới đều **< 400 dòng code**.
- [ ] 2. **Kiến trúc sạch (Clean Arch)**: 0 import `PySide6`/Qt trong `domain/` và `application/` (`python scripts/check_imports.py` pass 100%).
- [ ] 3. **Comment tiếng Anh**: 100% các khối code sửa đổi/tạo mới đều có comment tiếng Anh giải thích logic và lý do kỹ thuật.
- [ ] 4. **Kiểm thử tự động**: Có unit test / contract test tương ứng với tỷ lệ pass 100% trong thời gian < 1 giây.
- [ ] 5. **Không hồi quy lỗi (No Regression)**: Toàn bộ suite test chạy xanh với lệnh `pytest tests/`.
- [ ] 6. **Cập nhật tiến độ**: Đã ghi nhận đầy đủ thời gian `Start` và `End` vào file `Refactoring_Checklist.md`.
- [ ] 7. **Cổng CASAN**: Lệnh `python scripts/run_quality_gate.py` chạy thành công không có bất kỳ cảnh báo vi phạm nào.
File diff suppressed because it is too large Load Diff
+312
View File
@@ -0,0 +1,312 @@
# COWORK LOCAL - BẢNG CHECKLIST TIẾN ĐỘ TÁI CẤU TRÚC (2026)
## (REFACTORING & MIGRATION PROGRESS TRACKER)
* **Dự án**: Cowork Local (Cowork-Local BamBOO)
* **Thời gian thực hiện**: 21/08/2026 ➔ 31/08/2026
* **Đội ngũ phụ trách**:
- 🔵 **Team Duy** (Core AI, Routing, Turn Runtime & Testing Pyramid - Tech Lead)
- 🟣 **Team Nam** (Automation Workflows, Co4E, Monitoring & Shell Governance)
- 🟢 **Team Hoa** (Workspace, Filesystem, Scheduling & Tool Registry)
* **Tài liệu thiết kế kiến trúc gốc**: `Feature_Architecture_Proposal.md`
> [!IMPORTANT]
> ### 📝 QUY ĐỊNH BẮT BUỘC KHI CODE & GHI NHẬN TIẾN ĐỘ (MANDATORY RULES):
> 1. **In-Code Comments in English (Bắt buộc comment tiếng Anh ở mọi dòng/khối code sửa đổi)**:
> - Mỗi khi sửa đổi hoặc viết mới bất kỳ dòng code nào, lập trình viên **bắt buộc phải thêm comment bằng tiếng Anh** giải thích rõ mục đích xử lý, lý do kiến trúc và mối quan hệ giữa các tầng.
> - Tuyệt đối không để code không có chú thích, đặc biệt tại các điểm chuyển đổi DTO, seams và xử lý ngoại lệ.
> 2. **Task Start / End Timestamps (Ghi nhận chính xác ngày giờ bắt đầu và hoàn tất)**:
> - Khi bắt đầu làm một task ➔ Điền mốc thời gian: `Start: YYYY-MM-DD HH:mm`.
> - Khi task hoàn tất (unit test pass 100%) ➔ Điền mốc thời gian: `End: YYYY-MM-DD HH:mm` và tích chọn `[x]`.
---
## 📌 PHẦN 1: CHECKLIST CHI TIẾT THEO 10 EPIC (R01 ➔ R10)
### 🔹 EPIC R01: Architecture Foundation & Characterization (Nền Tảng Kiến Trúc & Test Bảo Vệ)
* **Team chịu trách nhiệm**: 🔵 **Team Duy** (Chủ trì ADR & Test Doubles) + Phối hợp cả 3 team
* **Mục tiêu**: Khóa DTO, dựng fakes/test doubles chạy offline không phụ thuộc Qt/mạng, thiết lập script chặn vi phạm kiến trúc.
- [x] **R01-T01 (Team Duy)**: Viết Architecture ADR định rõ ranh giới các tầng ➔ `docs/architecture/ADR-001-layered-architecture.md`
*Start: `2026-08-21 18:23` | End: `2026-08-21 18:24`*
- [x] **R01-T02 (Team Duy)**: Xây dựng `FakeProvider` và `FakeToolExecutor` chạy offline từ `providers/base.py` ➔ `tests/fakes/fake_provider.py` & `tests/fakes/fake_tool_executor.py`
*Start: `2026-08-21 18:24` | End: `2026-08-21 18:26`*
- [x] **R01-T03 (Team Duy)**: Viết script quét tĩnh chặn code mới trong `domain/` và `application/` import `PySide6` ➔ `scripts/check_imports.py`
*Start: `2026-08-21 18:26` | End: `2026-08-21 18:28`*
- [x] **R01-T04 (Team Duy)**: Viết Characterization Tests cho `core/chat_agent.py::run_cowork` ➔ `tests/characterization/test_run_cowork.py`
*Start: `2026-08-21 18:28` | End: `2026-08-21 18:32`*
- [x] **R01-T05 (Team Duy)**: Lập danh mục và phân loại mã nguồn dormant/dead code ➔ `docs/architecture/dormant-code.md`
*Start: `2026-08-21 18:32` | End: `2026-08-21 18:35`*
---
### 🔹 EPIC R02: Configuration, Secrets & Persistence (Cấu Hình Atomic & Bảo Mật Keyring)
* **Team chịu trách nhiệm**: 🟣 **Team Nam** (Chủ trì)
* **Mục tiêu**: Xóa bỏ untyped global `config.py`, cài đặt `AtomicJsonFile` chống hỏng file và lưu trữ API Key/Token vào OS Keyring.
- [ ] **R02-T01 (Team Nam)**: Xây dựng module `AtomicJsonFile` ghi tệp an toàn (tmp file + fsync + atomic replace) ➔ `infrastructure/persistence/json/atomic_json_file.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R02-T02 (Team Nam)**: Refactor `config.py::AppConfig` sử dụng `AtomicJsonFile` ➔ `infrastructure/config/config_repository.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R02-T03 (Team Nam)**: Xây dựng Typed Settings Facade (`ProviderSettings`, `RoutingSettings`) ➔ `infrastructure/config/settings_facade.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R02-T04 (Team Nam)**: Định nghĩa interface `SecretStore` và cài đặt `KeyringAdapter` ➔ `infrastructure/secrets/keyring_adapter.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R02-T05 (Team Nam)**: Di chuyển cấu hình API Key của OpenAI/Anthropic/FPT Gateway sang lưu trữ qua `SecretStore`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R02-T06 (Team Nam)**: Chuẩn hóa JSON schema versioning và recovery policy cho các file data
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
---
### 🔹 EPIC R03: Model Providers & Routing (Hợp Nhất Nhà Cung Cấp & Bộ Định Tuyến Mô Hình)
* **Team chịu trách nhiệm**: 🔵 **Team Duy** (Chủ trì)
* **Mục tiêu**: Hợp nhất logic routing bị phân tán thành `RoutingApplicationService` độc lập Qt; chuẩn hóa catalog nhà cung cấp.
- [ ] **R03-T01 (Team Duy)**: Xây dựng bộ Contract Tests chuẩn hóa cho các Provider từ `providers/base.py` ➔ `tests/contracts/test_providers.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R03-T02 (Team Duy)**: Xây dựng `ProviderDescriptor` và `ProviderRegistry` tập trung từ `providers/factory.py` ➔ `domain/models/provider_descriptor.py` & `infrastructure/providers/provider_registry.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R03-T03 (Team Duy)**: Xây dựng `RoutingApplicationService` độc lập với Qt từ `core/routing/` ➔ `application/model_routing/routing_application_service.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R03-T04 (Team Duy)**: Di chuyển luồng gọi routing từ `ui/chat_panel.py#L638` sang `RoutingApplicationService`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R03-T05 (Team Duy)**: Di chuyển luồng gọi routing từ `ui/co4e_tab.py` và `ui/folder_tab.py` sang `RoutingApplicationService`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R03-T06 (Team Duy)**: Tách logic ghi nhận token usage ra khỏi Provider, chuyển thành `UsageEventSink` ➔ `infrastructure/telemetry/usage_sink.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
---
### 🔹 EPIC R04: Agent Runtime & Conversation Application Service (Vòng Đời Turn Chat & Agent Engine)
* **Team chịu trách nhiệm**: 🔵 **Team Duy** (Chủ trì)
* **Mục tiêu**: Đóng gói input turn chat thành `ConversationExecutionRequest` bất biến, điều phối vòng đời qua `ConversationApplicationService` và phát sinh sự kiện `AgentEvent` có định kiểu.
- [ ] **R04-T01 (Team Duy)**: Định nghĩa immutable dataclass `ConversationExecutionRequest` ➔ `domain/agents/conversation_execution_request.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R04-T02 (Team Duy)**: Chuẩn hóa các sự kiện `AgentEvent` (TextChunk, ToolCallStarted, ToolCallResult, Error) ➔ `domain/agents/agent_event.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R04-T03 (Team Duy)**: Xây dựng `ConversationApplicationService` điều phối thực thi từ `core/chat_agent.py` ➔ `application/conversations/conversation_application_service.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R04-T04 (Team Duy)**: Di chuyển `ui/cowork_tab.py::build_job` sang sử dụng `ConversationExecutionRequest`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R04-T05 (Team Duy)**: Di chuyển `core/task_executors.py` sang dùng chung `ConversationApplicationService`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
---
### 🔹 EPIC R05: Tool, MCP & Connector Policy (Quản Lý Công Cụ, MCP & Cổng Kiểm Soát Quyền)
* **Team chịu trách nhiệm**: 🟢 **Team Hoa** (Chủ trì) + Phối hợp Team Duy
* **Mục tiêu**: Bóc tách monolithic `core/tools.py`, đưa toàn bộ Built-in tools, MCP tools và Connectors qua `ToolPolicyGateway` kiểm tra quyền phân tầng.
- [ ] **R05-T01 (Team Hoa)**: Định nghĩa `ToolDescriptor`, `ToolCapability` (read/write/execute/network) ➔ `domain/tools/tool_descriptor.py` & `domain/tools/tool_registry.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R05-T02 (Team Hoa)**: Tách nhỏ các built-in handlers từ `core/tools.py` ➔ `infrastructure/filesystem/file_tools.py`, `command_tools.py`, `fetch_tools.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R05-T03 (Team Hoa)**: Xây dựng `ToolPolicyGateway` (kiểm tra phân quyền allow/confirm/deny) ➔ `application/conversations/tool_policy_gateway.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R05-T04 (Team Hoa)**: Chuẩn hóa MCP tools từ `core/mcp_client.py` đi qua `ToolPolicyGateway`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R05-T05 (Team Hoa)**: Xây dựng `McpToolSourceManager` quản lý vòng đời tiến trình MCP ➔ `infrastructure/mcp/mcp_source_manager.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
---
### 🔹 EPIC R06: Workspace, Filesystem & History Isolation (Cô Lập Không Gian Làm Việc & Quản Lý Tệp)
* **Team chịu trách nhiệm**: 🟢 **Team Hoa** (Chủ trì)
* **Mục tiêu**: Xóa bỏ biến toàn cục `state.py::active_project_id`, đóng gói workspace per-turn thành `WorkspaceSession` bất biến, bảo vệ an toàn đường dẫn sandbox.
- [ ] **R06-T01 (Team Hoa)**: Định nghĩa `WorkspaceSession` chứa snapshot project id, workspace root ➔ `domain/workspaces/workspace_session.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R06-T02 (Team Hoa)**: Xây dựng `WorkspaceRepository` từ `core/projects.py` & `ConversationRepository` từ `core/history.py` ➔ `infrastructure/persistence/json/workspace_repository_impl.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R06-T03 (Team Hoa)**: Xây dựng `ExecutionWorkspace` quản lý output/scratch files ➔ `infrastructure/filesystem/execution_workspace.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R06-T04 (Team Hoa)**: Khắc phục race condition trong `ui/workspace_tab.py::_load_current`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R06-T05 (Team Hoa)**: Xây dựng `FileWorkspaceService` xử lý thao tác file cho File Explorer và AI File Editor ➔ `application/workspaces/file_workspace_service.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
---
### 🔹 EPIC R07: Scheduling & Workflow Runtime (Bộ Lập Lịch & Động Cơ Quy Trình)
* **Team chịu trách nhiệm**: 🟢 **Team Hoa** (Task Scheduling) + 🟣 **Team Nam** (Co4E Workflows)
* **Mục tiêu**: Tách `TaskRepository` và `ScheduleCalculator` khỏi `QTimer` trong `core/task_scheduler.py#L20`; xây dựng `TaskApplicationService` và `Co4EWorkflowService`.
- [ ] **R07-T01 (Team Hoa)**: Tách `TaskRepository` lưu trữ JSON độc lập khỏi `core/tasks.py` ➔ `infrastructure/persistence/json/task_repository_impl.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R07-T02 (Team Hoa)**: Xây dựng `ScheduleCalculator` tính due-time / cron độc lập ➔ `domain/tasks/schedule_calculator.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R07-T03 (Team Hoa)**: Xây dựng `QtSchedulerClock` adapter (tách `TaskScheduler` khỏi `QTimer`) ➔ `platform/qt/qt_scheduler_clock.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R07-T04 (Team Hoa)**: Xây dựng `TaskApplicationService` (Pure Python) điều phối chạy, sao chép, dừng, xóa task ➔ `application/scheduling/task_application_service.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R07-T05 (Team Hoa)**: Xây dựng `AiTaskPlannerService` hỗ trợ tạo / import task bằng AI ➔ `application/scheduling/ai_task_planner_service.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R07-T06 (Team Nam)**: Xây dựng `Co4EWorkflowService` (Pure Python) quản lý định nghĩa và thực thi Co4E từ `core/co4e_run_manager.py` ➔ `application/workflows/co4e_workflow_service.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
---
### 🔹 EPIC R08: UI/Application Separation (Phân Rã Toàn Diện Các God Widgets)
* **Team chịu trách nhiệm**: **Cả 3 Team** (Mỗi team phụ trách phân hệ của mình)
* **Mục tiêu**: Phân rã các file giao diện khổng lồ (>1.500 dòng) thành các widget chuyên biệt, mỗi file < 400 dòng code.
#### 🔵 Team Duy (Chat UI Hub):
- [ ] **R08-T01**: Tách `ui/chat_panel.py` thành `presentation/chat/chat_history_widget.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R08-T02**: Tách Composer & input box ➔ `presentation/chat/composer_widget.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R08-T03**: Tách Picker file đính kèm ➔ `presentation/chat/attachment_picker.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R08-T04**: Tách Voice/Audio recording ➔ `presentation/chat/audio_recorder_widget.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R08-T05**: Tách Output panel & file watcher ➔ `presentation/chat/chat_output_panel.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R08-T06**: Lắp ráp container `presentation/chat/chat_panel.py` và tối ưu `Floating HelpAgent`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
#### 🟣 Team Nam (Settings, Monitoring, Co4E & Shell):
- [ ] **R08-T07**: Tách `ui/settings_dialog.py` thành 4 section widgets ➔ `provider_settings_widget.py`, `connector_settings_widget.py`, `routing_settings_widget.py`, `general_settings_widget.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R08-T08**: Tách `ui/monitoring_tab.py` thành 7 tab độc lập (`overview_tab.py`, `sandbox_status_tab.py`, `security_events_tab.py`, `mcp_history_tab.py`, `action_logs_tab.py`, `agent_status_tab.py`, `security_settings_tab.py`)
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R08-T09**: Tách `ui/co4e_tab.py` thành các sub-components ➔ `co4e_canvas_widget.py`, `node_property_panel.py`, `co4e_run_control_widget.py`, `co4e_chat_view.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R08-T10**: Xây dựng `bootstrap.py` (Composition Root) và tách `app.py::MainWindow` (dòng 122) ➔ `presentation/shell/main_window.py`, `tray_manager.py`, `lifecycle_coordinator.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
#### 🟢 Team Hoa (Workspace, Folder, Scheduling, Dashboard & Graph):
- [ ] **R08-T11**: Tách `ui/schedule_task_tab.py` ➔ `kanban_board_widget.py`, `calendar_view_widget.py`, `ai_task_creator_dialog.py`, `ai_task_import_dialog.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R08-T12**: Tách `ui/folder_tab.py#L350` ➔ `workspace_file_tree.py`, `document_preview_manager.py`, `ai_file_editor_dialog.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R08-T13**: Tách `ui/dashboard_tab.py` ➔ `token_usage_card_widget.py`, `usage_chart_widget.py`, `habits_widget.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R08-T14**: Tách `ui/structure_graph_view.py` ➔ `presentation/graph/structure_graph_view.py` & `graph_qa_widget.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
---
### 🔹 EPIC R09: Security Runtime, Sandbox & Observability (An Ninh Runtime, Sandbox & Giám Sát)
* **Team chịu trách nhiệm**: 🟣 **Team Nam** (Chủ trì) + Phối hợp Team Duy
* **Mục tiêu**: Phân biệt deterministic rules và AI guardrails, fix toàn bộ circular imports trong security/pricing, chuẩn hóa schema audit logs.
- [ ] **R09-T01 (Team Nam)**: Viết tài liệu chuẩn hóa Security Policy Model ➔ `docs/architecture/security-policy.md`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R09-T02 (Team Nam)**: Xử lý triệt để Circular Import giữa `core/model_pricing.py` và `core/usage_tracker.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R09-T03 (Team Nam)**: Xử lý triệt để Circular Import giữa `core/agent_security.py` và `core/agent_security_alert.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R09-T04 (Team Nam)**: Xây dựng `CanonicalAuditLogger` thống nhất định dạng log từ `core/audit_log.py` ➔ `infrastructure/telemetry/audit_logger.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R09-T05 (Team Nam)**: Xây dựng `MonitoringQueryService` (truy vấn read-only có phân trang) ➔ `application/monitoring/monitoring_query_service.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R09-T06 (Team Nam)**: Chuẩn hóa ma trận năng lực Sandbox trên từng hệ điều hành từ `core/sandbox_manager.py` ➔ `infrastructure/sandbox/sandbox_capabilities.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
---
### 🔹 EPIC R10: Testing, Packaging & Contributor Experience (Hệ Thống Kiểm Thử & Tài Liệu Đóng Góp)
* **Team chịu trách nhiệm**: 🔵 **Team Duy** (Chủ trì chính - Task trọng tâm của Team Duy)
* **Mục tiêu**: Xây dựng toàn bộ hệ thống test pyramid (unit, contract, integration, headless UI), thiết lập CI Quality Gate tự động, soạn thảo tài liệu Contributor Recipes và thực hiện E2E smoke test trước khi phát hành.
- [ ] **R10-T01 (Team Duy)**: Thiết lập Tháp kiểm thử phân tầng (Unit tests không I/O <0.05s, Contract tests cho Providers/Tools, Integration tests, Fakes library) ➔ `tests/`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R10-T02 (Team Duy)**: Xây dựng Bộ script CI Quality Gate tự động (`scripts/check_imports.py`, `scripts/check_loc.py`, `scripts/audit_security.py`, `scripts/run_quality_gate.py`)
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R10-T03 (Team Duy)**: Cập nhật tài liệu kiến trúc 4 tầng, hướng dẫn setup môi trường & pre-commit hook ➔ `README.md` & `START_CONTRIBUTING.md`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R10-T04 (Team Duy)**: Soạn thảo bộ Contributor Recipes (3 công thức: Thêm Model Provider, Thêm Built-in/MCP Tool, Thêm Màn hình/Widget) ➔ `docs/governance/contributor-recipes.md`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
- [ ] **R10-T05 (Team Duy)**: Xây dựng bộ kiểm thử khói phát hành (E2E Release Smoke Test qua headless Qt với 5 kịch bản chính) ➔ `tests/e2e/test_smoke.py`
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
---
## 📅 PHẦN 2: CHECKLIST TIẾN ĐỘ THEO NGÀY CỦA TỪNG TEAM (21/08 ➔ 31/08)
### 🔵 TEAM DUY (Core AI, Routing, Turn Runtime & Testing Lead)
| Ngày | Task Cần Hoàn Thành | Start Time | End Time | Trạng Thái |
| :--- | :--- | :---: | :---: | :---: |
| **21/08 (T6)** | Khóa DTO `ConversationExecutionRequest`, `AgentEvent`; Xây dựng `FakeProvider`, `FakeToolExecutor` | `2026-08-21 18:23` | `2026-08-21 18:35` | [x] |
| **22-23/08 (T7-CN)** | Chuẩn hóa `ProviderDescriptor`, `ProviderRegistry`; Wrap OpenAI, Anthropic, Ollama, FPT Gateway; Viết Contract Tests | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **24/08 (T2)** | Xây dựng `RoutingApplicationService` độc lập Qt; Tách `ComposerWidget` & `AttachmentPicker` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **25/08 (T3)** | Xây dựng `ConversationApplicationService`; Tách `ChatHistoryWidget` và bubble renderer | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **26/08 (T4)** | Nối stream `AgentEvent` sang Chat History; Tách `AudioRecorderWidget` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **27/08 (T5)** | Tách `ChatOutputPanel` & File Watcher; Lắp ráp container `ChatPanel` và `Floating HelpAgent` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **28/08 (T6)** | Xóa copy routing cũ trong `ui/chat_panel.py`; Fix circular import `model_pricing` ↔ `usage_tracker` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **29/08 (T7)** | Viết suite integration test cho toàn bộ luồng Chat (`tests/integration/test_chat_flow.py`) | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **30/08 (CN)** | 🔍 **Chủ trì CASAN Check 3**: Chạy `python scripts/check_imports.py` đảm bảo 0 import `PySide6` trong domain & application | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **31/08 (T2)** | **Chủ trì EPIC R10**: Viết Contributor Recipes, chạy E2E Smoke Test (`tests/e2e/test_smoke.py`) và merge PR cuối cùng | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
---
### 🟣 TEAM NAM (Automation Workflows, Co4E, Monitoring & Governance)
| Ngày | Task Cần Hoàn Thành | Start Time | End Time | Trạng Thái |
| :--- | :--- | :---: | :---: | :---: |
| **21/08 (T6)** | Khóa DTO Co4E; Xây dựng `AtomicJsonFile` và `KeyringAdapter` (`SecretStore`) | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **22-23/08 (T7-CN)** | Refactor `config.py` sang `ConfigRepository`; Tách `ProviderSettingsWidget` & `ConnectorSettingsWidget` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **24/08 (T2)** | Tách 3 tab đầu của Monitoring (`overview_tab.py`, `sandbox_status_tab.py`); Xây dựng `MonitoringQueryService` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **25/08 (T3)** | Tách 4 tab còn lại của Monitoring (`security_events_tab.py`, `mcp_history_tab.py`,...); Lắp ráp container `MonitoringTab` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **26/08 (T4)** | Bóc tách `Co4EWorkflowService`; Tách `NodePropertyPanel` & `AgentListPanel` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **27/08 (T5)** | Tách `Co4ECanvasWidget`, `Co4ERunControlWidget` & `Co4EChatView` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **28/08 (T6)** | Lắp ráp container `Co4ETab`; Xây dựng `bootstrap.py` (Composition Root) và tách `MainWindow` shell | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **29/08 (T7)** | Fix circular import `agent_security` ↔ `agent_security_alert`; Integration test luồng Co4E & Settings | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **30/08 (CN)** | 🔍 **Chủ trì CASAN Check 1**: Chạy `python scripts/audit_security.py` đảm bảo 0 API Key/Token plaintext | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **31/08 (T2)** | Fix tồn đọng Check 1, cập nhật tài liệu kiến trúc, merge PR cuối | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
---
### 🟢 TEAM HOA (Workspace, Filesystem, Scheduling & Tool Registry)
| Ngày | Task Cần Hoàn Thành | Start Time | End Time | Trạng Thái |
| :--- | :--- | :---: | :---: | :---: |
| **21/08 (T6)** | Khóa DTO `ToolDescriptor`, `ToolCapability`; Tách `FileTools` từ `core/tools.py` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **22-23/08 (T7-CN)** | Tách `CommandTools`, `FetchTools`; Tách `TokenUsageCardWidget` & `UsageChartWidget` (Dashboard) | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **24/08 (T2)** | Tách `TaskRepository` & `ScheduleCalculator`; Tách `KanbanBoardWidget` (7 cột trạng thái) | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **25/08 (T3)** | Xây dựng `QtSchedulerClock` adapter (tách khỏi `QTimer`); Tách `CalendarViewWidget` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **26/08 (T4)** | Xây dựng `TaskApplicationService`; Tách `AiTaskCreatorDialog` & `AiTaskImportDialog` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **27/08 (T5)** | Tách `WorkspaceFileTree`, `DocumentPreviewManager` & `AiFileEditorDialog` từ `FolderTab` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **28/08 (T6)** | Tách `StructureGraphView` (GraphRAG); Lắp ráp shell `FolderTab` & `ScheduleTaskTab` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **29/08 (T7)** | Nối `ToolPolicyGateway` qua MCP Client & Built-in Tools; Integration test Task Scheduler & File Explorer | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **30/08 (CN)** | 🔍 **Chủ trì CASAN Check 2**: Chạy `python scripts/check_loc.py --max-lines 400` đảm bảo 0 file >400 dòng | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **31/08 (T2)** | Fix tồn đọng Check 2, cập nhật README, merge PR cuối | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
---
## 🚦 PHẦN 3: CHECKLIST CHECKPOINT REVIEW & CƠ CHẾ KIỂM DUYỆT CASAN
### 🛡️ Định nghĩa 5 Chữ Cái CASAN:
- **C - Clean Architecture**: 0 import `PySide6` trong `domain/` và `application/`.
- **A - Atomic Persistence**: 0 plaintext secrets trong JSON/config; dùng `AtomicJsonFile` ghi tệp an toàn.
- **S - Single Responsibility**: 0 file production nào > 400 dòng code (LOC).
- **A - Automated Test Pyramid**: Bộ test phân tầng chạy offline 100% không phụ thuộc network/UI.
- **N - No Regression & Smoke**: Toàn bộ suite test (>81 tests) và E2E Smoke test pass 100%.
### 🔍 Bảng Theo Dõi Các Checkpoints & Cổng Kiểm Duyệt CASAN:
| Thời Điểm | Checkpoint / Cổng Duyệt | Lệnh Kiểm Tra Thực Tế | Tiêu Chí Bắt Buộc | Phụ Trách | Start Time | End Time | Trạng Thái |
| :--- | :--- | :--- | :--- | :--- | :---: | :---: | :---: |
| **23/08 (CN - 17:00)** | **Checkpoint 1: Contracts & Fakes** | `pytest tests/contracts tests/fakes` | 100% DTO và Fake Services tạo xong; test pass | Cả 3 Team | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **28/08 (T6 - 17:00)** | **Checkpoint 2: Services & Sub-widgets** | `pytest tests/` | Tách xong 100% God Files; 0 circular import | Cả 3 Team | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **30/08 (CN - 17:00)** | **CASAN Check 1: Security Audit** | `python scripts/audit_security.py` | 0 plaintext secret trong file cấu hình | Team Nam | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **30/08 (CN - 17:00)** | **CASAN Check 2: Modularity (LOC)** | `python scripts/check_loc.py --max-lines 400` | 0 file production nào > 400 dòng code | Team Hoa | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **30/08 (CN - 17:00)** | **CASAN Check 3: Clean Architecture** | `python scripts/check_imports.py` | 0 import `PySide6` trong domain & application | Team Duy | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
| **31/08 (T2 - 15:00)** | **Final Release E2E Smoke Test** | `pytest tests/e2e/test_smoke.py` | 5 kịch bản end-to-end pass 100% trên `main` | Team Duy & 3 Team | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
---
## 📋 PHẦN 4: DEFINITION OF DONE (DOD) CHO MỖI PULL REQUEST
Mọi Pull Request của cả 3 team trước khi merge vào nhánh chính cần được đối chiếu checklist sau:
- [ ] **1. Kích thước file (LOC)**: File mới hoặc file sau khi refactor không vượt quá **400 dòng code**.
- [ ] **2. Phụ thuộc kiến trúc (Clean Architecture)**: Không import `PySide6` / Qt trong các module thuộc `domain/` và `application/`.
- [ ] **3. An toàn thông tin (Security)**: API Key / Credential được lưu trữ qua `SecretStore` (Keyring), không lưu cứng hoặc lưu plaintext trong file JSON.
- [ ] **4. Bắt buộc Comment Code bằng Tiếng Anh (English In-Code Comments)**: 100% các dòng hoặc khối code sửa đổi/bóc tách đều có comment tiếng Anh giải thích rõ logic xử lý và lý do kỹ thuật.
- [ ] **5. Ghi nhận thời gian thực hiện (Timestamps)**: Đã điền đầy đủ mốc thời gian `Start: YYYY-MM-DD HH:mm` và `End: YYYY-MM-DD HH:mm` vào `Refactoring_Checklist.md` và PR description.
- [ ] **6. Kiểm thử tự động (Automated Tests)**: Có unit test hoặc contract test đi kèm với tỷ lệ pass 100%. Chạy `pytest` hoàn tất < 3 giây.
- [ ] **7. Không gây lỗi chéo (No Regression)**: Chạy kiểm thử toàn bộ hệ thống không làm hỏng các tính năng hiện hữu.
+155
View File
@@ -0,0 +1,155 @@
# NHẬT KÝ THEO DÕI VÀ PHÒNG NGỪA LỖI TÁI CẤU TRÚC (BUG & LESSONS LEARNED LOG)
## DỰ ÁN: COWORK LOCAL (COWORK-LOCAL BAMBOO)
Tài liệu này dùng để ghi nhận **toàn bộ các lỗi, xung đột kiến trúc và sự cố phát sinh** trong suốt quá trình refactoring của cả 3 team (Team Duy, Team Nam, Team Hoa).
> [!IMPORTANT]
> ### 🛡️ NGUYÊN TẮC VÀNG VỀ QUẢN TRỊ CHẤT LƯỢNG (ZERO RECURRENCE):
> 1. **Ghi nhận ngay lập tức**: Khi gặp bất kỳ lỗi nào (Syntax, Circular Import, Type Error, Test Failure, Thread Freeze, Data Corruption), kỹ sư/AI phải ghi ngay vào tài liệu này trước khi tiếp tục task.
> 2. **Phân tích nguyên nhân gốc rễ (Root Cause)**: Không chỉ sửa phần ngọn mà phải giải thích rõ bản chất vì sao lỗi xảy ra.
> 3. **Rút ra quy tắc phòng ngừa (Prevention Rule)**: Đặt ra nguyên tắc kỹ thuật để **TUYỆT ĐỐI KHÔNG TÁI PHẠM** ở các task tiếp theo.
> 4. **Checklist đầu vào**: Trước khi bắt đầu bất kỳ task mới nào, kỹ sư/AI **bắt buộc phải đọc lại toàn bộ file này**.
---
## 📌 BẢNG TỔNG HỢP CÁC LỖI ĐÃ PHÁT HIỆN & KHẮC PHỤC
| Bug ID | Ngày Phát Hiện | Phân Hệ / File Bị Ảnh Hưởng | Loại Lỗi | Trạng Thái | Team Phụ Trách |
| :--- | :---: | :--- | :--- | :---: | :---: |
| `BUG-001` | 2026-08-20 | `core/model_pricing.py` ↔ `core/usage_tracker.py` | Circular Dependency | 🟡 Đã có giải pháp (R09) | Team Duy & Team Nam |
| `BUG-002` | 2026-08-20 | `core/agent_security.py` ↔ `core/agent_security_alert.py` | Circular Dependency | 🟡 Đã có giải pháp (R09) | Team Nam |
| `BUG-003` | 2026-08-20 | `state.py::active_project_id` & `ui/workspace_tab.py` | Race Condition / Global State Leak | 🟡 Đã có giải pháp (R06) | Team Hoa |
| `BUG-004` | 2026-08-20 | `core/task_scheduler.py` ↔ `PySide6.QtCore.QTimer` | Architecture Violation (Qt in Domain/App) | 🟡 Đã có giải pháp (R07) | Team Hoa |
| `BUG-005` | 2026-08-20 | `ui/chat_panel.py#L638`, `ui/co4e_tab.py`, `ui/folder_tab.py` | Code Duplication (Copy Routing Logic) | 🟡 Đã có giải pháp (R03) | Team Duy |
| `BUG-006` | 2026-08-21 | `scripts/check_imports.py` | UnicodeEncodeError (Windows CP932 console emoji) | 🟢 Đã khắc phục (R01) | Team Duy |
| `BUG-007` | 2026-08-21 | `platform/` ➔ `infrastructure/platform/` | Standard Library Shadowing (`import platform`) | 🟢 Đã khắc phục (R01) | Team Duy |
---
## 🔍 CHI TIẾT TỪNG LỖI & QUY TẮC PHÒNG NGỪA
---
### 🔴 `BUG-001`: Circular Import giữa Module Định Giá (`model_pricing.py`) và Theo Dõi Token (`usage_tracker.py`)
* **Phân hệ**: `core/model_pricing.py` & `core/usage_tracker.py`
* **Triệu chứng (Symptom)**: Lỗi `ImportError: cannot import name 'ModelPricing' from partially initialized module` khi khởi động ứng dụng hoặc chạy test độc lập.
* **Nguyên nhân gốc rễ (Root Cause)**:
- `model_pricing.py` import `UsageTracker` để cập nhật dữ liệu tiêu thụ.
- Ngược lại, `usage_tracker.py` import `ModelPricing` để tính toán chi phí theo từng model ID.
* **Giải pháp khắc phục (Resolution)**:
- Tách Data Transfer Object (DTO) `ModelPricing` sang tầng Domain thuần túy `domain/models/model_pricing.py`.
- Cả `model_pricing.py` và `usage_tracker.py` đều import DTO từ `domain/models/`, chuyển quan hệ thành 1 chiều (Dependency Inversion).
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
> **Quy tắc**: Không bao giờ để 2 service hoặc 2 module nghiệp vụ import lẫn nhau. Mọi cấu trúc dữ liệu dùng chung (DTO/Value Object/Event) **phải được đặt tại tầng `domain/`**.
---
### 🔴 `BUG-002`: Circular Import giữa An Ninh Agent (`agent_security.py`) và Cảnh Báo (`agent_security_alert.py`)
* **Phân hệ**: `core/agent_security.py` & `core/agent_security_alert.py`
* **Triệu chứng (Symptom)**: Lỗi khởi tạo vòng tròn khi runtime bắn ra alert sự kiện bảo mật.
* **Nguyên nhân gốc rễ (Root Cause)**:
- Module security vừa kiểm tra policy vừa khởi tạo trực tiếp instance alert dialog, trong khi alert dialog lại import ngược lại rule security để hiển thị chi tiết mã lỗi.
* **Giải pháp khắc phục (Resolution)**:
- Tách sự kiện cảnh báo thành Event DTO `SecurityAlertEvent` tại `domain/security/security_event.py`.
- Tầng Security chỉ phát ra Event (`emit_event`), tầng Presentation/UI tự lắng nghe Event để render Dialog.
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
> **Quy tắc**: Logic an ninh và xử lý nghiệp vụ không bao giờ được gọi trực tiếp UI Dialog. Luôn giao tiếp thông qua cơ chế Event-Driven (`AgentEvent`, `SecurityEvent`).
---
### 🔴 `BUG-003`: Xung Đột Race Condition do Sử Dụng Biến Toàn Cục `active_project_id` trong `state.py`
* **Phân hệ**: `state.py`, `ui/workspace_tab.py`, Scheduled Task Runners
* **Triệu chứng (Symptom)**: Khi task scheduler chạy ngầm hoặc người dùng chuyển tab nhanh, file bị ghi nhầm vào thư mục dự án khác với dự án đang hiển thị trên màn hình.
* **Nguyên nhân gốc rễ (Root Cause)**:
- Ứng dụng đọc và ghi trực tiếp vào biến toàn cục `AppContext.active_project_id` từ nhiều luồng khác nhau mà không có cơ chế snapshot ngữ cảnh.
* **Giải pháp khắc phục (Resolution)**:
- Xóa bỏ việc đọc biến toàn cục. Mỗi lần khởi chạy turn hoặc task, tạo một snapshot bất biến `WorkspaceSession(project_id, root_path, allowed_paths)`.
- Luồng ngầm chỉ thao tác trên `WorkspaceSession` được truyền vào từ lúc khởi tạo.
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
> **Quy tắc**: Tuyệt đối không dùng biến toàn cục (Global State / Singletons có trạng thái thay đổi) để điều khiển luồng thực thi nền. Mọi ngữ cảnh phải được truyền tường minh qua DTO snapshot.
---
### 🔴 `BUG-004`: Vi Phạm Ranh Giới Kiến Trúc Khi Import `PySide6.QtCore.QTimer` trong Domain / Scheduling Engine
* **Phân hệ**: `core/task_scheduler.py#L20`
* **Triệu chứng (Symptom)**: Không thể viết Unit Test cho thuật toán tính toán lịch chạy (cron/interval) trên môi trường CI/CD (GitHub Actions / Linux Server headless) nếu thiếu driver màn hình X11/Wayland hoặc chưa cài `PySide6`.
* **Nguyên nhân gốc rễ (Root Cause)**:
- Động cơ lập lịch bị gắn chặt cứng với `QTimer` của framework Qt thay vì tách riêng logic tính toán thời gian.
* **Giải pháp khắc phục (Resolution)**:
- Tách thuật toán tính lịch sang `domain/tasks/schedule_calculator.py` (Pure Python 100%).
- Tạo `platform/qt/qt_scheduler_clock.py` làm adapter bọc `QTimer` cho app chạy thật, và `tests/fakes/fake_clock.py` cho unit test.
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
> **Quy tắc**: Tầng Domain và Application tuyệt đối không import thư viện GUI (`PySide6`, `PyQt`). Luôn bọc các thành phần phụ thuộc framework bên ngoài qua Adapter Interface.
---
### 🔴 `BUG-005`: Nhân Bản Mã Nguồn (Code Duplication) Logic Routing Mô Hình AI tại Nhiều Màn Hình
* **Phân hệ**: `ui/chat_panel.py#L638`, `ui/co4e_tab.py`, `ui/folder_tab.py`
* **Triệu chứng (Symptom)**: Khi cập nhật thêm model provider mới (như FPT Gateway hay Claude 3.7), phải sửa code thủ công ở 3 file UI khác nhau; phát sinh sai lệch quy tắc fallback giữa các màn hình.
* **Nguyên nhân gốc rễ (Root Cause)**:
- Thiếu một tầng Application Service tập trung, dẫn đến việc lập trình viên copy-paste hàm chọn model từ `ChatPanel` sang các tab khác.
* **Giải pháp khắc phục (Resolution)**:
- Xây dựng `application/model_routing/routing_application_service.py` duy nhất, cung cấp API `route_request(request) -> ModelRouteDecision`.
- Mọi màn hình UI chỉ gọi service này, không tự viết lại logic kiểm tra key hay fallback.
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
> **Quy tắc**: Nghiệp vụ dùng chung giữa các màn hình phải được đưa vào `application/` services. Không bao giờ viết logic nghiệp vụ trực tiếp trong các file Widget UI.
---
### 🟢 `BUG-006`: `UnicodeEncodeError` khi in Emojis trên Console Windows (CP932/CP1252)
* **Phân hệ / File**: `scripts/check_imports.py`
* **Triệu chứng (Symptom)**:
```text
Traceback (most recent call last):
File "scripts/check_imports.py", line 127, in main
print(f"\U0001f6e1\ufe0f Running Clean Architecture Import Guard...")
UnicodeEncodeError: 'cp932' codec can't encode character '\U0001f6e1' in position 0: illegal multibyte sequence
```
* **Nguyên nhân gốc rễ (Root Cause)**:
- Trên hệ điều hành Windows sử dụng locale tiếng Nhật (mã trang CP932) hoặc tiếng Anh (CP1252), `sys.stdout` mặc định không hỗ trợ các ký tự Unicode/Emoji ngoài bảng mã, dẫn đến crash khi in log dòng lệnh.
* **Giải pháp khắc phục (Resolution)**:
- Tự động bọc lại `sys.stdout` và `sys.stderr` bằng `io.TextIOWrapper` với `encoding="utf-8"` và `errors="replace"`.
- Thay thế các emoji phức tạp bằng các tag văn bản ASCII chuẩn hóa như `[Clean Arch Guard]`, `[PASS]`, `[FAIL]`.
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
> **Quy tắc**: Mọi script CLI (`scripts/*.py`) phải có cơ chế cấu hình `utf-8` stream wrapper và ưu tiên sử dụng text tags (`[INFO]`, `[WARN]`, `[ERROR]`) thay vì emoji Unicode trực tiếp để đảm bảo chạy mượt mà trên mọi môi trường Windows đa ngôn ngữ.
---
### 🟢 `BUG-007`: Xung Đột Tên Thư Mục Trùng Với Standard Library (`platform/` Shadowing `import platform`)
* **Phân hệ / File**: `platform/` ➔ Chuyển thành `infrastructure/platform/`
* **Triệu chứng (Symptom)**:
```text
INTERNALERROR> File "_pytest/terminal.py", line 853: verinfo = platform.python_version()
INTERNALERROR> AttributeError: module 'platform' has no attribute 'python_version'
```
* **Nguyên nhân gốc rễ (Root Cause)**:
- Khi tạo một package ở thư mục gốc có tên trùng với module thư viện chuẩn của Python (`platform`, `email`, `test`, `asyncio`, `logging`), Python trên `sys.path` sẽ ưu tiên import thư mục local thay vì thư viện chuẩn của Python runtime, dẫn đến crash toàn bộ pytest runner và các thư viện bên thứ ba.
* **Giải pháp khắc phục (Resolution)**:
- Xóa bỏ package `platform/` ở root.
- Đưa adapter Qt Scheduler Clock vào đúng vị trí hạ tầng: `infrastructure/platform/qt/`.
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
> **Quy tắc**: Tuyệt đối không đặt tên package/thư mục ở root trùng với tên các module built-in của Python (`platform`, `logging`, `types`, `time`, `io`, `os`, `sys`). Mọi platform adapter phải nằm trong `infrastructure/platform/` hoặc `platform_adapters/`.
---
## 📝 MẪU GHI NHẬN BUG MỚI (BUG REPORT TEMPLATE)
Khi gặp bất kỳ bug mới nào trong quá trình làm việc, hãy sao chép khối mẫu sau và điền vào cuối tài liệu:
```markdown
### 🔴 `BUG-XXX`: [Tóm tắt ngắn gọn tên lỗi]
* **Phân hệ / File**: `[Đường dẫn file bị lỗi]`
* **Triệu chứng (Symptom)**: `[Mô tả hiện tượng lỗi, paste thông báo traceback hoặc kết quả test fail]`
* **Nguyên nhân gốc rễ (Root Cause)**: `[Giải thích tại sao lỗi lại xảy ra]`
* **Giải pháp khắc phục (Resolution)**: `[Mô tả cách sửa, file DTO/Service tạo mới hoặc cách refactor]`
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
> **Quy tắc**: `[Nguyên tắc kỹ thuật cụ thể để không bao giờ tái phạm lỗi này]`
```
+682
View File
@@ -0,0 +1,682 @@
## 🗓️ VI. LỘ TRÌNH THỰC HIỆN - 10 EPIC (REFACTORING ROADMAP)
### Bảng Tổng Quan 10 EPIC
| EPIC | Tên | Dependency | Giá Trị Kiến Trúc |
| :--- | :--- | :--- | :--- |
| **R01** | Architecture Foundation & Characterization | Không | Safety net + ngôn ngữ chung trước khi nhiều người sửa |
| **R02** | Configuration, Secrets & Persistence | R01 | Loại bỏ global dict/direct write và bảo vệ credential |
| **R03** | Model Providers & Routing | R01, R02 | 1 đường mở rộng provider, 1 routing flow duy nhất |
| **R04** | Agent Runtime & Conversation Service | R01, R03 | Tách turn lifecycle khỏi widget |
| **R05** | Tool, MCP & Connector Policy | R01, R04 | 1 security/approval path cho mọi tool call |
| **R06** | Workspace, Filesystem & History Isolation | R01, R02 | Loại bỏ cross-project mutable path/state |
| **R07** | Scheduling & Workflow Runtime | R01, R04, R06 | Tách Qt timer, persistence và runtime dispatch |
| **R08** | UI/Application Separation | R03 - R07 | Thu nhỏ God widgets theo từng screen |
| **R09** | Security Runtime, Sandbox & Observability | R01, R05 | Policy rõ, event schema thống nhất |
| **R10** | Testing, Packaging & Contributor Experience | Tất cả | CI, docs, contributor có thể sửa 1 capability độc lập |
---
### 💡 Chiến Lược Triển Khai Song Song 100% Cho 3 Team (Zero Blocking)
Để 3 team làm việc cùng lúc từ **21/08 đến 31/08/2026** mà không bị nghẽn (blocked), không phải chờ đợi nhau và loại bỏ hoàn toàn rủi ro merge conflict:
1. **Ranh giới sở hữu mã nguồn tuyệt đối (Code Ownership & Zero File Overlap)**: Mỗi file/thư mục chỉ thuộc quyền chỉnh sửa của duy nhất 1 team. Tuyệt đối không để 2 team cùng sửa chung 1 file cùng lúc.
2. **Nguyên tắc Contract-First & Mock-Driven**: Thống nhất Data Contract / DTO / Interface ngay từ Ngày 1. Khi cần gọi chéo giữa các phân hệ, team gọi sẽ dùng `Fake/Mock Adapter` để hoàn thiện UI/logic nội bộ mà **không cần chờ** team kia hoàn thành implementation.
3. **Phân chia theo Phân hệ nghiệp vụ (Vertical Domain Slices)**: Mỗi team phụ trách trọn vẹn từ UI Sub-widgets đến Application Service và Infrastructure của phân hệ đó, đảm bảo tính tự chủ và khả năng test độc lập.
```mermaid
graph TD
subgraph T1 [🔵 TEAM 1: Core AI & Conversation Hub]
UI1[presentation/chat/] --> APP1[application/conversations/<br>application/model_routing/]
APP1 --> DOM1[domain/agents/<br>domain/models/]
APP1 --> INF1[infrastructure/providers/]
end
subgraph T2 [🟣 TEAM 2: Automation, Workflows & Governance]
UI2[presentation/co4e/<br>presentation/monitoring/<br>presentation/settings/] --> APP2[application/workflows/<br>application/monitoring/<br>application/settings/]
APP2 --> DOM2[domain/workflows/<br>domain/security/]
APP2 --> INF2[infrastructure/config/<br>infrastructure/secrets/]
end
subgraph T3 [🟢 TEAM 3: Workspace, Tools & Scheduling]
UI3[presentation/folder/<br>presentation/scheduling/<br>presentation/dashboard/<br>presentation/graph/] --> APP3[application/workspaces/<br>application/scheduling/]
APP3 --> DOM3[domain/tools/<br>domain/tasks/]
APP3 --> INF3[infrastructure/filesystem/<br>infrastructure/mcp/<br>infrastructure/persistence/]
end
style T1 fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
style T2 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
style T3 fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
```
---
### 🖥️ Cấu Trúc Giao Diện Thực Tế & Bản Đồ Điều Hướng (Verified UI Layout & Navigation Map)
Qua kiểm tra trực tiếp mã nguồn thực tế của giao diện (`app.py`, `ui/workspace_tab.py`, `ui/chat_panel.py`, `ui/co4e_tab.py`, `ui/folder_tab.py`, `ui/monitoring_tab.py`), cấu trúc layout hiện tại của Cowork Local được thiết kế theo mô hình **Thanh điều hướng phẳng (Flat Collapsible Nav Rail) + Không gian làm việc đa phân hệ (Workspace Hub)**:
```mermaid
graph TD
MW["MainWindow (app.py)"]
subgraph NR ["👈 Collapsible Left Nav Rail (54px / 150px)"]
N_TOP["Header: Project Picker + '+ Chat Mới'"]
N_MAIN["Main Nav (Flat List)"]
N_REC["Section: GẦN ĐÂY (Recent Threads)"]
N_BOT["Bottom Nav (Ghim Đáy)"]
N_FOOT["Footer: Cài Đặt (Settings) + Tài Khoản"]
end
subgraph CA ["👉 Main Content Area (QStackedWidget)"]
P_WS["📁 WorkspaceTab (Trang Chủ Chính)"]
P_SCH["⏰ ScheduleTaskTab (Lịch Trình)"]
P_DB["📊 DashboardTab (Bảng Điều Khiển)"]
P_MON["🛡️ MonitoringTab (Giám Sát & Quản Trị - 8 Tabs)"]
end
subgraph WST ["📦 Các Sub-Tabs Trong Workspace (Điều khiển từ Nav Rail)"]
ST_PROJ["1. 📁 Dự Án (Project info, instructions, folder path)"]
ST_COW["2. 💬 Cowork (Chat Panel + Outer History Sidebar)"]
ST_CO4E["3. ⚡ Co4E Studio (Canvas Node, Agent/Skill Palette, Run Chat)"]
ST_FOLD["4. 📂 Folder Explorer (Tree, Code Editor, Preview, Terminal, AI Edit)"]
ST_GRAPH["5. 🕸️ GraphRAG (Knowledge Graph View + Q&A Panel)"]
end
MW --> NR
MW --> CA
N_MAIN -->|Chuyển sub-tab| WST
N_MAIN -->|Mở trang| P_SCH
N_BOT -->|Mở trang| P_DB
N_BOT -->|Mở trang| P_MON
P_WS --> WST
style MW fill:#1e293b,stroke:#0ea5e9,color:#fff
style NR fill:#0f172a,stroke:#334155,color:#fff
style CA fill:#1e293b,stroke:#475569,color:#fff
style WST fill:#334155,stroke:#38bdf8,color:#fff
```
#### 📌 Chi Tiết Thành Phần Giao Diện Của Từng Phân Hệ:
1. **Thanh Điều Hướng Trái (Left Nav Rail - `app.py`):**
- Nút thu gọn / mở rộng (Menu toggle 54px ↔ 150px).
- Bộ chọn nhanh dự án (`nav_project` / `nav_project_btn`) & Nút `+ Chat mới` (`nav_new_chat`).
- Danh sách phẳng các màn hình làm việc chính (Dự án, Cowork, Co4E, Folder, GraphRAG, Lịch trình).
- Danh sách hội thoại gần đây (`RECENTS`) của dự án đang chọn.
- Nhóm ghim đáy (Bảng điều khiển, Giám sát) + Nút mở Cài đặt & Hàng thông tin tài khoản.
- **Trợ lý nổi (Floating Help Agent - `ui/help_agent_widget.py`):** Biểu tượng robot ghim góc dưới phải ở mọi màn hình, click là mở cửa sổ chat trợ giúp nhanh.
2. **Workspace Tab (Trang Chủ - `ui/workspace_tab.py`):**
- **Cột trái:** Danh sách quản lý Dự án (Create, Delete, đổi tên, thu gọn / mở rộng).
- **Cột giữa:** Thanh lịch sử hội thoại ngoài (`ui/sidebar.py::HistorySidebar`) hiển thị xuyên suốt cho cả Cowork và GraphRAG.
- **Vùng chính:** Chứa 5 sub-tabs (ẩn thanh tab bar ngang để Nav Rail điều hướng trực tiếp):
- **Dự Án (`ProjectTab`):** Tên, mô tả, chỉ dẫn chung (shared instructions), đường dẫn thư mục sandbox, danh sách luồng chat.
- **Cowork (`ui/cowork_tab.py`):** Khung chat chính (`ui/chat_panel.py`, `ui/chat_view.py`, `ui/composer.py`).
- **Co4E Studio (`ui/co4e_tab.py`):** Canvas thiết kế luồng đồ thị node (`ui/co4e_canvas.py`), bảng chỉnh thuộc tính node (`ui/co4e_config_panel.py`), thư viện Agent/Skill, bộ điều khiển chạy luồng & Chat view tương tác.
- **Folder Explorer (`ui/folder_tab.py`):** Cây thư mục workspace, trình soạn thảo code syntax highlight, trình xem trước tài liệu đa định dạng (PDF, MS Office qua LibreOffice `ui/libreoffice_view.py`, HTML, Ảnh), Terminal tích hợp (`ui/terminal_panel.py`), và Dialog sửa code bằng AI (`ui/file_edit_dialog.py`).
- **GraphRAG (`ui/structure_graph_view.py`):** Đồ thị tri thức D3 WebEngine / Native 2D, bộ lọc thực thể, panel hỏi đáp ngữ cảnh mã nguồn (Graph Q&A).
3. **Schedule Task Tab (Lịch Trình - `ui/schedule_task_tab.py`):**
- Bảng Kanban 7 cột trạng thái (Backlog, Todo, In Progress, Review, Done, Blocked, Cancelled) hỗ trợ kéo thả.
- Chế độ xem Lịch tháng (`ui/calendar_view.py`) trực quan hóa các task định kỳ và due dates.
- Dialog chỉnh sửa task (`ui/task_editor_dialog.py`) & các bộ tạo task tự động bằng AI.
4. **Dashboard Tab (Bảng Điều Khiển - `ui/dashboard_tab.py`):**
- Thẻ thống kê tổng lượng Token tiêu thụ, chi phí ước tính, số lượng tác vụ đã chạy.
- Biểu đồ Spline trực quan hóa xu hướng chi phí theo thời gian (`ui/spline_chart.py`).
- Bảng thói quen sử dụng mô hình (AI Model Habits) và hạn mức ngân sách.
5. **Monitoring Tab (Giám Sát & Quản Trị - `ui/monitoring_tab.py`):**
- Giữ nguyên tab bar nội bộ với 8 tab chuyên trách:
1. **Tổng quan (Overview):** Metrics CPU, Memory, số tiến trình sandbox, tổng log.
2. **Trạng thái Sandbox (Sandbox Status):** Giám sát các container/sub-process cách ly.
3. **Sự kiện bảo mật (Security Events):** Danh sách cảnh báo vi phạm policy an toàn.
4. **Lịch sử MCP (MCP History):** Nhật ký gọi tool MCP và latency.
5. **Nhật ký hoạt động (Action Logs):** Log chi tiết mọi thao tác đọc/ghi tệp, thực thi lệnh.
6. **Quản trị Agent (`ui/agents_admin_tab.py`):** Cấu hình Prompt và tham số cho các Agent chuyên biệt & Help Agent.
7. **Cài đặt bảo mật (Security Settings):** Bật/tắt các rào chắn Sandbox và phê duyệt công cụ.
8. **Quản trị Tool / Icon (`ui/tools_admin_tab.py`, `ui/icons_admin_tab.py`):** Quản lý metadata công cụ và bộ icon hệ thống.
6. **Hộp Thoại Cài Đặt (Settings Dialog - `ui/settings_dialog.py`):**
- Cài đặt Nhà cung cấp (OpenAI, Anthropic, Ollama, FPT Gateway).
- Cài đặt Connectors (MCP Server, MS365, External APIs).
- Cài đặt Định tuyến mô hình (Off, Auto, Manual, Fallback rules).
- Cài đặt Chung (Ngôn ngữ, Giao diện Theme, Khởi động cùng hệ thống, System Tray).
---
### 👥 Ranh Giới Phân Hệ & Phạm Vi Của 3 Team (Duy, Nam, Hoa)
| Team | Phân Hệ Phụ Trách | Phạm Vi Thư Mục Sở Hữu | File Cũ Cần Phân Rã / Tái Cấu Trúc | Trọng Tâm EPIC |
| :--- | :--- | :--- | :--- | :--- |
| **🔵 TEAM DUY**<br>*(Tech Lead)* | **Core AI, Routing, Agent Engine & Testing Lead** | `presentation/chat/`<br>`application/conversations/`<br>`application/model_routing/`<br>`domain/agents/`, `domain/models/`<br>`infrastructure/providers/`<br>`tests/` (Unit, Contract, Integration, E2E) | `ui/chat_panel.py`<br>`ui/cowork_tab.py`<br>`ui/help_agent_widget.py`<br>`core/chat_agent.py`<br>`core/routing/*`<br>`providers/*` | **R01, R03, R04, R10**<br>(Routing, Providers, Agent Engine, Chat UI, Floating Help Agent, Testing Pyramid, Contributor Recipes) |
| **🟣 TEAM NAM** | **Automation, Workflows, Governance & Security** | `presentation/co4e/`<br>`presentation/monitoring/`<br>`presentation/settings/`<br>`presentation/shell/`, `bootstrap.py`<br>`application/workflows/`, `monitoring/`, `settings/`<br>`infrastructure/config/`, `secrets/`, `sandbox/` | `ui/co4e_tab.py`<br>`ui/monitoring_tab.py`<br>`ui/settings_dialog.py`<br>`app.py::MainWindow`<br>`config.py`<br>`core/co4e_run_manager.py` | **R02, R08, R09**<br>(Co4E Studio, Monitoring 8 tabs, Settings, Keyring, Nav Rail & Shell, Security Scan) |
| **🟢 TEAM HOA** | **Workspace, Filesystem, Tools & Scheduling** | `presentation/workspace/`<br>`presentation/folder/`<br>`presentation/scheduling/`<br>`presentation/dashboard/`<br>`presentation/graph/`<br>`application/workspaces/`, `scheduling/`<br>`domain/tools/`, `domain/tasks/`<br>`infrastructure/filesystem/`, `mcp/`, `persistence/` | `ui/workspace_tab.py`<br>`ui/sidebar.py`<br>`ui/folder_tab.py`<br>`ui/structure_graph_view.py`<br>`ui/schedule_task_tab.py`<br>`ui/dashboard_tab.py`<br>`core/tools.py`<br>`core/task_executors.py`<br>`core/task_scheduler.py` | **R05, R06, R07, R08**<br>(Tools, Tasks, Workspace Project Manager, Folder Explorer & Editor, Graph RAG, Kanban Schedule, Dashboard) |
---
### 📅 Lịch Tổng Quan Theo Tuần (21/08 - 31/08/2026)
```mermaid
gantt
title Lộ Trình Phân Chia 3 Team Song Song (21/08 - 31/08/2026)
dateFormat YYYY-MM-DD
section Team Duy (Core AI, Chat & Testing Lead)
Khóa DTO + FakeProvider + Provider Registry :t1_1, 2026-08-21, 3d
RoutingService + Tách Composer & ChatHistory :t1_2, 2026-08-24, 3d
ConversationService + ChatOutput + ChatPanel Shell :t1_3, 2026-08-27, 3d
CASAN Check 3 + EPIC R10 Testing Pyramid & Smoke :t1_4, 2026-08-30, 2d
section Team Nam (Workflow & Governance)
Khóa DTO + AtomicConfig + Keyring + Settings Split :t2_1, 2026-08-21, 3d
MonitoringTab Split (7 tabs) + MonitoringService :t2_2, 2026-08-24, 2d
Co4E Canvas + RunControl + WorkflowService :t2_3, 2026-08-26, 3d
Bootstrap Root + CASAN Check 1 (Security Scan) :t2_4, 2026-08-29, 3d
section Team Hoa (Workspace, Tools & Scheduling)
Khóa DTO + ToolRegistry + File Tools + Dashboard :t3_1, 2026-08-21, 3d
TaskRepo + Clock + Kanban + Calendar View :t3_2, 2026-08-24, 3d
FolderTree + DocumentPreview + Graph RAG :t3_3, 2026-08-27, 3d
CASAN Check 2 (Single Responsibility) + E2E Support :t3_4, 2026-08-30, 2d
```
---
### 🗓️ KẾ HOẠCH CHI TIẾT TỪNG NGÀY CHO 3 TEAM (21/08 ➔ 31/08)
#### 🔵 TEAM DUY: Core AI, Routing & Testing Lead (Tech Lead)
| Ngày | Công Việc Cụ Thể & Nơi Bóc Tách Code | File Đích Cần Tạo / Chỉnh Sửa | Tiêu Chí Kiểm Thử (Validation) |
| :--- | :--- | :--- | :--- |
| **21/08 (T6)** | • Khóa DTO từ `core/chat_agent.py`<br>• Xây dựng test doubles từ `providers/base.py` | ➔ `domain/agents/conversation_execution_request.py`<br>➔ `domain/agents/agent_event.py`<br>➔ `tests/fakes/fake_provider.py` | Unit test chạy <1s, không phụ thuộc Qt hay network |
| **22-23/08 (T7-CN)** | • Chuẩn hóa catalog từ `providers/factory.py`<br>• Wrap OpenAI, Anthropic, Ollama, FPT Gateway | ➔ `domain/models/provider_descriptor.py`<br>➔ `infrastructure/providers/provider_registry.py` | Golden response test cho từng provider |
| **24/08 (T2)** | • Hợp nhất routing từ `ui/chat_panel.py#L638` & `core/routing/`<br>• Tách Composer & Picker từ `ui/composer.py` | ➔ `application/model_routing/routing_application_service.py`<br>➔ `presentation/chat/composer_widget.py`<br>➔ `presentation/chat/attachment_picker.py` | Test routing policy không cần Qt; Composer test |
| **25/08 (T3)** | • Tách turn orchestration từ `ui/chat_panel.py#L70`<br>• Tách chat bubble/markdown từ `ui/chat_view.py` | ➔ `application/conversations/conversation_application_service.py`<br>➔ `presentation/chat/chat_history_widget.py` | Turn test với `FakeProvider`: text stream & tool calls |
| **26/08 (T4)** | • Nối sự kiện `AgentEvent` sang History Widget<br>• Tách ghi âm audio từ `ui/chat_panel.py` | ➔ `presentation/chat/audio_recorder_widget.py` | Event streaming UI test không lag main thread |
| **27/08 (T5)** | • Tách file watcher & output panel từ `ui/chat_panel.py#L18`<br>• Lắp ráp shell hoàn chỉnh | ➔ `presentation/chat/chat_output_panel.py`<br>➔ `presentation/chat/chat_panel.py` | Smoke test: ChatPanel mở mượt mà, render đủ thành phần |
| **28/08 (T6)** | • Xóa routing copy trong `ui/chat_panel.py`<br>• Fix circular import `core/model_pricing.py` ↔ `core/usage_tracker.py` | ➔ Patch các module liên quan | `python -c "import cowork_local"` không phát sinh lỗi |
| **29/08 (T7)** | • Viết suite integration test cho toàn bộ luồng Chat<br>• Rà soát số dòng code Team Duy (<400 dòng/file) | ➔ `tests/integration/test_chat_flow.py` | 100% test pass |
| **30/08 (CN)** | 🔍 **Chủ trì CASAN Check 3 (Import Guard)**: Quét tĩnh kiểm tra `domain/` & `application/` không import `PySide6` | ➔ `scripts/check_imports.py` | 0 violation trong code mới |
| **31/08 (T2)** | 🎯 **Chủ trì EPIC R10 (Task Chính Team Duy)**: Thiết lập Testing Pyramid, Contributor Recipes, E2E Smoke Test & Merge PR cuối | ➔ `tests/e2e/test_smoke.py`<br>➔ `docs/governance/contributor-recipes.md` | All tests pass, CASAN Gate PASS |
---
#### 🟣 TEAM NAM: Automation, Workflows, Governance & Security
| Ngày | Công Việc Cụ Thể & Nơi Bóc Tách Code | File Đích Cần Tạo / Chỉnh Sửa | Tiêu Chí Kiểm Thử (Validation) |
| :--- | :--- | :--- | :--- |
| **21/08 (T6)** | • Khóa DTO từ `core/co4e.py`<br>• Xây dựng Atomic Write & Keyring từ `config.py` | ➔ `infrastructure/persistence/json/atomic_json_file.py`<br>➔ `infrastructure/secrets/keyring_adapter.py` | Fault-injection test (atomic write); Credential store test |
| **22-23/08 (T7-CN)** | • Chuyển đổi `config.py` sang `ConfigRepository`<br>• Tách section từ `ui/settings_dialog.py` | ➔ `infrastructure/config/config_repository.py`<br>➔ `presentation/settings/provider_settings_widget.py`<br>➔ `presentation/settings/connector_settings_widget.py` | Config round-trip test; Settings UI render test |
| **24/08 (T2)** | • Tách 3 tab đầu từ `ui/monitoring_tab.py`<br>• Xây dựng truy vấn dữ liệu độc lập | ➔ `presentation/monitoring/overview_tab.py`<br>➔ `presentation/monitoring/sandbox_status_tab.py`<br>➔ `application/monitoring/monitoring_query_service.py` | Render dữ liệu thống kê độc lập |
| **25/08 (T3)** | • Tách 4 tab còn lại từ `ui/monitoring_tab.py`<br>• Lắp ráp shell Monitoring | ➔ `presentation/monitoring/security_events_tab.py`<br>➔ `presentation/monitoring/mcp_history_tab.py`<br>➔ `presentation/monitoring/monitoring_tab.py` | Smoke test: MonitoringTab chuyển tab mượt, filter log tốt |
| **26/08 (T4)** | • Bóc tách runner từ `core/co4e_run_manager.py`<br>• Tách config & agent panels từ `ui/co4e_tab.py#L3` | ➔ `application/workflows/co4e_workflow_service.py`<br>➔ `presentation/co4e/node_property_panel.py`<br>➔ `presentation/co4e/agent_list_panel.py` | Workflow CRUD & validation test độc lập |
| **27/08 (T5)** | • Tách Canvas vẽ node từ `ui/co4e_canvas.py`<br>• Tách Run control & chat view từ `ui/co4e_tab.py#L228` | ➔ `presentation/co4e/co4e_canvas_widget.py`<br>➔ `presentation/co4e/co4e_run_control_widget.py`<br>➔ `presentation/co4e/co4e_chat_view.py` | Canvas node operations test |
| **28/08 (T6)** | • Lắp ráp container Co4ETab<br>• Tách Composition root & MainWindow từ `app.py#L122` | ➔ `presentation/co4e/co4e_tab.py`<br>➔ `bootstrap.py`<br>➔ `presentation/shell/main_window.py` | Khởi động app qua `bootstrap.py` thành công |
| **29/08 (T7)** | • Fix circular import `core/agent_security.py` ↔ `core/agent_security_alert.py`<br>• Integration test luồng Co4E & Settings | ➔ Patch security modules | Co4E flow chạy trơn tru |
| **30/08 (CN)** | 🔍 **Chủ trì CASAN Check 1 (Security Scan)**: Quét rà soát toàn bộ file config/JSON để đảm bảo 0 API Key/Token lưu plaintext | ➔ Script security audit | 0 credential plaintext |
| **31/08 (T2)** | Fix tồn đọng Check 1, cập nhật tài liệu kiến trúc, merge PR cuối | — | CASAN Check 1 PASS |
---
#### 🟢 TEAM HOA: Workspace, Filesystem, Tools & Scheduling
| Ngày | Công Việc Cụ Thể & Nơi Bóc Tách Code | File Đích Cần Tạo / Chỉnh Sửa | Tiêu Chí Kiểm Thử (Validation) |
| :--- | :--- | :--- | :--- |
| **21/08 (T6)** | • Khóa DTO từ `core/tools.py` & `core/tasks.py`<br>• Tách file tools từ `core/tools.py` | ➔ `domain/tools/tool_descriptor.py`<br>➔ `domain/tools/tool_registry.py`<br>➔ `infrastructure/filesystem/file_tools.py` | Tool handler test độc lập; Atomic write test |
| **22-23/08 (T7-CN)** | • Tách command, fetch, image tools từ `core/tools.py`<br>• Tách card & chart từ `ui/dashboard_tab.py` | ➔ `infrastructure/filesystem/command_tools.py`<br>➔ `infrastructure/filesystem/fetch_tools.py`<br>➔ `presentation/dashboard/token_usage_card_widget.py`<br>➔ `presentation/dashboard/usage_chart_widget.py` | Tool execution test; Dashboard chart test với mock data |
| **24/08 (T2)** | • Tách repository & do lịch từ `core/tasks.py`<br>• Tách Kanban board từ `ui/schedule_task_tab.py` | ➔ `infrastructure/persistence/json/task_repository_impl.py`<br>➔ `domain/tasks/schedule_calculator.py`<br>➔ `presentation/scheduling/kanban_board_widget.py` | Task CRUD test; Kanban card render test |
| **25/08 (T3)** | • Tách `QTimer` adapter từ `core/task_scheduler.py#L20`<br>• Tách Calendar view từ `ui/schedule_task_tab.py` | ➔ `platform/qt/qt_scheduler_clock.py`<br>➔ `presentation/scheduling/calendar_view_widget.py` | Fake clock test kích hoạt task đúng lịch |
| **26/08 (T4)** | • Tách dispatch logic từ `core/task_executors.py`<br>• Tách AI create dialogs từ `ui/schedule_task_tab.py` | ➔ `application/scheduling/task_application_service.py`<br>➔ `presentation/scheduling/ai_task_creator_dialog.py` | Task dispatch test; AI planner test với fake provider |
| **27/08 (T5)** | • Tách File tree & Previews từ `ui/folder_tab.py#L350`<br>• Tách AI File Editor từ `ui/folder_tab.py` | ➔ `presentation/folder/workspace_file_tree.py`<br>➔ `presentation/folder/document_preview_manager.py`<br>➔ `application/workspaces/file_workspace_service.py` | File CRUD test; Preview render test; AI apply diff test |
| **28/08 (T6)** | • Tách Graph View từ `ui/structure_graph_view.py`<br>• Lắp ráp shell FolderTab & ScheduleTab | ➔ `presentation/graph/structure_graph_view.py`<br>➔ `application/workspaces/graph_index_service.py`<br>➔ `presentation/scheduling/schedule_task_tab.py` | Graph RAG test; Smoke test: Folder & Schedule tabs mở tốt |
| **29/08 (T7)** | • Nối `ToolPolicyGateway` qua `core/mcp_client.py` & built-in tools<br>• Integration test Task Scheduler & File Explorer | ➔ `application/conversations/tool_policy_gateway.py` | Approval flow hoạt động chuẩn |
| **30/08 (CN)** | 🔍 **Chủ trì CASAN Check 2 (Single Responsibility Audit)**: Quét toàn bộ codebase đảm bảo không có file production nào > 400 dòng | ➔ Script count LOC | 0 file vi phạm (>400 lines) |
| **31/08 (T2)** | Fix tồn đọng Check 2, cập nhật README, merge PR cuối | — | CASAN Check 2 PASS |
---
### 🚦 Checkpoint Review & Cơ Chế Cổng Kiểm Duyệt CASAN (CASAN Verification Gate)
#### 🛡️ CASAN Là Gì?
**CASAN** là bộ cổng kiểm duyệt chất lượng và an toàn kiến trúc tự động (Automated Architectural Quality Gate) bắt buộc trước khi phát hành phiên bản tái cấu trúc. Tên viết tắt đại diện cho 5 nguyên tắc cốt lõi:
- **C** - **Clean Architecture (Ranh giới tầng sạch)**: Tầng `domain/` và `application/` tuyệt đối thuần Python, 0 phụ thuộc vào `PySide6` / Qt GUI framework.
- **A** - **Atomic Persistence (Lưu trữ an toàn & Bí mật)**: 0 lưu trữ plaintext API Key/Token trong JSON/config (phải dùng OS `SecretStore` / Keyring); mọi thao tác ghi dữ liệu tệp đều dùng cơ chế `AtomicJsonFile` chống hỏng dữ liệu khi crash.
- **S** - **Single Responsibility & Modularity (Kích thước tệp nhỏ gọn)**: Giới hạn tối đa **400 dòng code (LOC)** cho mỗi file production; mỗi file/class chỉ đảm nhận đúng 1 trách nhiệm duy nhất.
- **A** - **Automated Test Pyramid (Tháp kiểm thử tự động)**: Toàn bộ Unit tests (<1s), Contract tests, Integration tests chạy offline hoàn toàn không cần kết nối mạng hay Qt GUI event loop.
- **N** - **No Regression & E2E Smoke (Không hồi quy & Ổn định phát hành)**: Toàn bộ suite test hiện tại (>81 tests) và bộ E2E Smoke Test của ứng dụng chạy thành công 100% trên nhánh `main`.
#### 🔍 Chi Tiết 3 Cổng Kiểm Tra CASAN (Chạy Tự Động Ngày 30/08 & Pre-commit):
| Cổng Kiểm Tra | Mục Tiêu & Cơ Chế Kiểm Tra | Lệnh Chạy Kiểm Thử | Tiêu Chí Pass Bắt Buộc | Team Phụ Trách |
| :--- | :--- | :--- | :--- | :--- |
| **CASAN Check 1: Security Audit** | Quét regex phân tích tĩnh toàn bộ file cấu hình (`.json`, `.jsonl`, `.yaml`, `config.py`) nhằm phát hiện secret/token lưu plaintext | `python scripts/audit_security.py` | `0 plaintext secrets found` (100% key lưu qua Keyring) | **🟣 Team Nam** |
| **CASAN Check 2: Modularity (LOC Audit)** | Quét đếm số dòng code (LOC) của từng file trong `presentation/`, `application/`, `domain/`, `infrastructure/` | `python scripts/check_loc.py --max-lines 400` | `0 files exceeding 400 lines` (Tất cả God Files đã bị chia nhỏ) | **🟢 Team Hoa** |
| **CASAN Check 3: Clean Architecture Guard** | Dùng thư viện `ast` phân tích cây cú pháp trừu tượng, quét cấm các import `PySide6`, `PyQt*` bên trong `domain/` và `application/` | `python scripts/check_imports.py` | `0 Qt imports in business logic` | **🔵 Team Duy** |
| **Lệnh Tổng Hợp CASAN Gate** | Chạy toàn bộ 3 checks trên + suite `pytest` | `python scripts/run_quality_gate.py` | `ALL GATES PASSED (100%)` | **🔵 Team Duy (Tech Lead)** |
#### 📅 Bảng Kế Hoạch Checkpoint & CASAN Gate:
| Thời Điểm | Checkpoint | Tiêu Chí Đạt Bắt Buộc | Trách Nhiệm |
| :--- | :--- | :--- | :--- |
| **23/08 (CN - 17:00)** | ✅ **Checkpoint 1 (Contracts & Fakes)** | 100% DTO và Fake Services (`FakeProvider`, `FakeToolExecutor`, `FakeClock`) tạo xong; `pytest` pass; 0 team bị block | Cả 3 Team |
| **28/08 (T6 - 17:00)** | ✅ **Checkpoint 2 (Services & Sub-widgets)** | Tách xong 100% các God Files (`chat_panel.py`, `co4e_tab.py`, `folder_tab.py`, `monitoring_tab.py`, `schedule_task_tab.py`, `settings_dialog.py`); 0 circular import | Cả 3 Team |
| **30/08 (CN - 17:00)** | 🏁 **CASAN Verification Gate** | Chạy thành công đồng thời cả 3 checks: **CASAN Check 1** (Security), **CASAN Check 2** (LOC <400), **CASAN Check 3** (Import Guard) | Team Nam (Check 1)<br>Team Hoa (Check 2)<br>Team Duy (Check 3) |
| **31/08 (T2 - 15:00)** | 🎉 **Final Release Smoke Test** | Suite test (>81 tests) pass 100%; E2E smoke test 5 luồng chính hoạt động ổn định trên `main` | **Team Duy** (Chủ trì) & 3 Team |
---
### 📌 VI. MÔ TẢ CHI TIẾT 10 EPIC (R01 ➔ R10)
> [!TIP]
> 📋 Toàn bộ hệ thống checklist chi tiết từng đầu việc nhỏ (`R01-T01` ➔ `R10-T05`), checklist tiến độ theo ngày và tiêu chuẩn Definition of Done (DoD) đã được tách thành tài liệu theo dõi độc lập tại file **`Refactoring_Checklist.md`**.
---
#### 🔹 R01: Architecture Foundation & Characterization (Nền Tảng Kiến Trúc & Test Bảo Vệ)
* **Ý nghĩa & Mục tiêu**: Thiết lập luật phụ thuộc kiến trúc (Dependency Rules), xây dựng bộ fixtures/test doubles giả lập (`FakeProvider`, `FakeToolExecutor`) không phụ thuộc UI/mạng, và dựng script chặn vi phạm kiến trúc trên CI trước khi bất kỳ ai di chuyển mã nguồn.
* **Team chịu trách nhiệm**: 🔵 **Team Duy** (Chủ trì ADR & Test Doubles) + Cả 3 Team.
* **Chi Tiết Cụ Thể Các Task Cần Làm Trong R01**:
1. **R01-T01: Soạn thảo Kiến trúc ADR (Layered Architecture ADR)**:
- Tạo `docs/architecture/ADR-001-layered-architecture.md` định rõ quy tắc 4 tầng: Presentation ➔ Application ➔ Domain ➔ Infrastructure.
- Quy định rõ ràng: `domain/` và `application/` chỉ chứa Pure Python, không chứa logic UI hoặc import `PySide6`.
2. **R01-T02: Xây dựng Bộ Fixtures & Test Doubles Offline (`tests/fakes/`)**:
- `tests/fakes/fake_provider.py`: Mock `BaseProvider`, trả về streaming text chunk và tool call events có thể kiểm soát được trong unit test.
- `tests/fakes/fake_tool_executor.py`: Mock bộ thực thi tool, trả về dummy result (đọc file, chạy lệnh) mà không can thiệp vào hệ thống tệp thật.
- Tiêu chuẩn: Unit test chạy hoàn tất < 1 giây, hoàn toàn độc lập với Qt GUI và network.
3. **R01-T03: Xây dựng Script Phân Tích AST Chặn Vi Phạm Kiến Trúc (`scripts/check_imports.py`)**:
- Dùng module `ast` quét toàn bộ file trong `domain/` và `application/`.
- Chặn các lệnh `import PySide6`, `import PyQt*`, `import app`.
- Tích hợp vào CI pipeline và Git pre-commit hook.
4. **R01-T04: Viết Characterization Tests cho Luồng Runtime Cốt Lõi (`tests/characterization/`)**:
- Tạo `tests/characterization/test_run_cowork.py`: Chụp snapshot hành vi hiện tại của hàm `core/chat_agent.py::run_cowork` (cách nhận input, gọi tool, tạo prompt).
- Đảm bảo khi tách sang `ConversationApplicationService` thì hành vi logic không bị sai lệch.
5. **R01-T05: Lập Danh Mục & Cô Lập Mã Nguồn Dormant/Dead Code (`docs/architecture/dormant-code.md`)**:
- Rà soát các module không còn active (như `LoginDialog`, `account` legacy) và đánh dấu cô lập, không để ảnh hưởng tới luồng tái cấu trúc chính.
---
#### 🔹 R02: Configuration, Secrets & Persistence (Cấu Hình Atomic & Bảo Mật Keyring)
* **Ý nghĩa & Mục tiêu**: Chuyển đổi cơ chế lưu trữ `config.py` sang ghi tệp an toàn (Atomic Write chống hỏng file khi crash), tạo Typed Settings Facades và đưa toàn bộ API Key/Token lưu plaintext sang OS Keyring (`SecretStore`).
* **Team chịu trách nhiệm**: 🟣 **Team Nam** (Chủ trì).
* **Chi Tiết Cụ Thể Các Task Cần Làm Trong R02**:
1. **R02-T01: Xây dựng Tiện Ích Ghi File Nguyên Tử (`AtomicJsonFile`)**:
- Tạo `infrastructure/persistence/json/atomic_json_file.py`: Ghi dữ liệu ra file tạm (`.tmp`), gọi `os.fsync()`, sau đó dùng `os.replace()` để thay thế file đích một cách an toàn.
- Thêm cơ chế tự động tạo bản sao lưu (`.bak`) khi phát hiện file JSON bị corrupt.
2. **R02-T02: Tái cấu trúc Kho Cấu Hình `ConfigRepository`**:
- Tạo `infrastructure/config/config_repository.py`: Đóng gói `config.py::AppConfig`, loại bỏ biến global dùng chung, chuyển sang Repository pattern có thread-safe lock.
3. **R02-T03: Xây dựng Typed Settings Facades Độc Lập**:
- Tạo `infrastructure/config/settings_facade.py`: Chia nhỏ cấu hình thành các dataclass định kiểu rõ ràng (`ProviderSettings`, `RoutingSettings`, `GeneralSettings`, `SecuritySettings`) thay vì truy xuất dictionary tự do.
4. **R02-T04: Định nghĩa Interface `SecretStore` & Cài đặt `KeyringAdapter`**:
- Tạo `infrastructure/secrets/keyring_adapter.py`: Sử dụng thư viện `keyring` của Python để lưu và đọc API Keys/Tokens từ Windows Credential Manager / macOS Keychain / Linux Secret Service.
- Thêm `tests/fakes/fake_keyring.py` để test môi trường CI không có UI desktop.
5. **R02-T05: Di Chuyển API Keys của Các Provider Sang `SecretStore`**:
- Xóa việc lưu plaintext `openai_api_key`, `anthropic_api_key`, `fpt_api_key` trong `config.json`.
- Tự động di chuyển (migrate) key cũ vào Keyring khi khởi động lần đầu.
6. **R02-T06: Chuẩn Hóa JSON Schema Versioning & Recovery Policy**:
- Bổ sung trường `schema_version` vào mọi file dữ liệu JSON (projects, tasks, routing assessment). Tự động chạy hàm migrate schema khi có phiên bản mới.
---
#### 🔹 R03: Model Providers & Routing (Hợp Nhất Nhà Cung Cấp & Bộ Định Tuyến Mô Hình)
* **Ý nghĩa & Mục tiêu**: Xóa bỏ sự phân tán logic định tuyến (hiện đang lặp lại ở `ui/chat_panel.py#L638`, `ui/co4e_tab.py`, `ui/folder_tab.py`) thành một `RoutingApplicationService` duy nhất; chuẩn hóa danh mục nhà cung cấp mô hình qua `ProviderDescriptor`.
* **Team chịu trách nhiệm**: 🔵 **Team Duy** (Chủ trì).
* **Chi Tiết Cụ Thể Các Task Cần Làm Trong R03**:
1. **R03-T01: Xây dựng Bộ Contract Tests Chuẩn Hóa cho Model Providers**:
- Tạo `tests/contracts/test_providers.py`: Kiểm thử hợp đồng cho mọi provider (OpenAI, Anthropic, Ollama, FPT Gateway) để đảm bảo cùng tuân thủ interface `generate()`, `stream()`, `count_tokens()`.
2. **R03-T02: Định nghĩa `ProviderDescriptor` & Xây dựng `ProviderRegistry`**:
- Tạo `domain/models/provider_descriptor.py`: Dataclass định nghĩa metadata nhà cung cấp (id, name, models list, context length, pricing, required auth).
- Tạo `infrastructure/providers/provider_registry.py`: Registry đăng ký tập trung tất cả providers, hỗ trợ tra cứu động theo model ID.
3. **R03-T03: Xây dựng Dịch Vụ Định Tuyến `RoutingApplicationService` (Pure Python)**:
- Tạo `application/model_routing/routing_application_service.py` từ `core/routing/`: Điều phối 4 chế độ định tuyến (Off, Auto/Cost-effective, Manual, Fallback).
- Độc lập 100% với PySide6 UI, cho phép kiểm thử tự động toàn bộ rule routing mà không cần bật màn hình.
4. **R03-T04: Hợp Nhất Luồng Định Tuyến từ `ui/chat_panel.py#L638`**:
- Xóa bỏ logic routing sao chép trong `ui/chat_panel.py`, chuyển sang gọi trực tiếp qua `RoutingApplicationService`.
5. **R03-T05: Hợp Nhất Luồng Định Tuyến từ `ui/co4e_tab.py` & `ui/folder_tab.py`**:
- Chuyển đổi mọi lời gọi định tuyến mô hình trong Co4E Node Execution và AI File Editor sang dùng chung `RoutingApplicationService`.
6. **R03-T06: Tách Bóc Telemetry & Token Usage Thành `UsageEventSink`**:
- Tạo `infrastructure/telemetry/usage_sink.py`: Tách logic ghi nhận số lượng token và chi phí ra khỏi Provider, biến thành Event Subscriber lắng nghe sự kiện từ Application Service.
---
#### 🔹 R04: Agent Runtime & Conversation Application Service (Vòng Đời Turn Chat & Agent Engine)
* **Ý nghĩa & Mục tiêu**: Tách toàn bộ vòng đời thực thi 1 lượt chat (Turn) ra khỏi PySide6 UI; đóng gói dữ liệu đầu vào thành snapshot bất biến `ConversationExecutionRequest` và trả về luồng sự kiện `AgentEvent` có định kiểu.
* **Team chịu trách nhiệm**: 🔵 **Team Duy** (Chủ trì).
* **Chi Tiết Cụ Thể Các Task Cần Làm Trong R04**:
1. **R04-T01: Định nghĩa Immutable Snapshot `ConversationExecutionRequest`**:
- Tạo `domain/agents/conversation_execution_request.py`: Chứa đầy đủ context của 1 lượt chạy (turn id, session id, user prompt, attachments, model config, tool capability scope, instructions).
- Dữ liệu bất biến (frozen dataclass), bảo đảm trong khi agent đang chạy, người dùng có đổi lựa chọn trên UI thì turn cũng không bị ảnh hưởng.
2. **R04-T02: Chuẩn hóa Hệ Thống Sự Kiện Luồng `AgentEvent`**:
- Tạo `domain/agents/agent_event.py`: Định nghĩa các sự kiện có kiểu dữ liệu mạnh: `TextChunkEvent`, `ToolCallStartedEvent`, `ToolCallFinishedEvent`, `TurnCompletedEvent`, `ErrorEvent`.
3. **R04-T03: Xây dựng `ConversationApplicationService`**:
- Tạo `application/conversations/conversation_application_service.py`: Tách logic từ `core/chat_agent.py`. Điều phối toàn bộ vòng đời của turn: chuẩn bị prompt ➔ gọi provider ➔ lắng nghe stream ➔ dispatch tool call ➔ tổng hợp câu trả lời ➔ lưu lịch sử hội thoại.
4. **R04-T04: Chuyển đổi `ui/cowork_tab.py::build_job`**:
- Thay thế logic tạo job phức tạp trong UI bằng việc khởi tạo `ConversationExecutionRequest` và gửi tới `ConversationApplicationService`.
5. **R04-T05: Đồng Bộ Hóa `core/task_executors.py` sang dùng chung Runtime**:
- Đưa việc thực thi chat của Scheduled Task Runner về dùng chung `ConversationApplicationService`, xoá bỏ duplicate agent runner.
---
#### 🔹 R05: Tool, MCP & Connector Policy (Quản Lý Công Cụ, MCP & Cổng Kiểm Soát Quyền)
* **Ý nghĩa & Mục tiêu**: Xóa bỏ giant if/elif dispatcher trong `core/tools.py`; đưa tất cả Built-in tools, MCP tools (`core/mcp_client.py`) và REST connectors (`core/ext_connectors.py`) qua cùng một cổng phân loại rủi ro (`ToolCapability`) và cổng phê duyệt bảo mật (`ToolPolicyGateway`).
* **Team chịu trách nhiệm**: 🟢 **Team Hoa** (Chủ trì) + Team Duy.
* **Chi Tiết Cụ Thể Các Task Cần Làm Trong R05**:
1. **R05-T01: Định nghĩa `ToolDescriptor`, `ToolCapability` & `ToolRegistry`**:
- Tạo `domain/tools/tool_descriptor.py`: Mô tả metadata công cụ (tên, mô tả, JSON Schema parameters, độ rủi ro READ / WRITE / EXECUTE / NETWORK).
- Tạo `domain/tools/tool_registry.py`: Kho đăng ký tập trung cho mọi công cụ hệ thống.
2. **R05-T02: Phân Rã Monolithic `core/tools.py` Thành Các Module Riêng Biệt**:
- Tạo `infrastructure/filesystem/file_tools.py` (read, write, edit, list_dir, grep).
- Tạo `infrastructure/filesystem/command_tools.py` (run_command, manage_task).
- Tạo `infrastructure/filesystem/fetch_tools.py` (read_url_content, search_web).
3. **R05-T03: Xây dựng Cổng Kiểm Soát Quyền `ToolPolicyGateway`**:
- Tạo `application/conversations/tool_policy_gateway.py`: Kiểm tra chính sách trước khi cho phép chạy tool (ALLOW, CONFIRM_REQUIRED, DENY). Khi cần xác nhận từ người dùng, phát tín hiệu yêu cầu phê duyệt thay vì gọi dialog trực tiếp trong hàm chạy ngầm.
4. **R05-T04: Chuẩn Hóa MCP Tools Qua `ToolPolicyGateway`**:
- Bọc các tool từ MCP Server (`core/mcp_client.py`) thành các `ToolDescriptor` tương thích để áp dụng cùng một chính sách an ninh như built-in tools.
5. **R05-T05: Xây dựng `McpToolSourceManager` Quản Lý Tiến Trình MCP**:
- Tạo `infrastructure/mcp/mcp_source_manager.py`: Quản lý vòng đời tiến trình MCP con (start, heartbeat, timeout, restart khi crash, graceful shutdown).
---
#### 🔹 R06: Workspace, Filesystem & History Isolation (Cô Lập Không Gian Làm Việc & Quản Lý Tệp)
* **Ý nghĩa & Mục tiêu**: Loại bỏ biến toàn cục `active_project_id` trong `state.py` gây xung đột dữ liệu giữa các luồng chạy ngầm; đóng gói không gian làm việc thành `WorkspaceSession` bất biến theo turn; bảo vệ an toàn đường dẫn tệp.
* **Team chịu trách nhiệm**: 🟢 **Team Hoa** (Chủ trì).
* **Chi Tiết Cụ Thể Các Task Cần Làm Trong R06**:
1. **R06-T01: Định nghĩa `WorkspaceSession` Đóng Gói Ngữ Cảnh**:
- Tạo `domain/workspaces/workspace_session.py`: Đối tượng snapshot chứa `project_id`, `workspace_root_path`, `sandbox_dir`, `allowed_paths`. Đảm bảo agent chỉ được đọc/ghi trong thư mục được cấp phép.
2. **R06-T02: Xây dựng `WorkspaceRepository` & `ConversationRepository`**:
- Tạo `infrastructure/persistence/json/workspace_repository_impl.py`: Quản lý danh sách dự án, cấu hình dự án (`core/projects.py`) bằng `AtomicJsonFile`.
- Lưu trữ và phân trang lịch sử chat (`core/history.py`) độc lập với UI sidebar.
3. **R06-T03: Xây dựng `ExecutionWorkspace` Quản Lý Tệp Output/Scratch**:
- Tạo `infrastructure/filesystem/execution_workspace.py`: Tách biệt thư mục workspace chính và thư mục scratch/output tạm thời của từng turn chạy.
4. **R06-T04: Khắc phục Race Condition trong `WorkspaceTab`**:
- Viết lại hàm `_load_current` trong `ui/workspace_tab.py`: Đồng bộ dữ liệu bằng session id thay vì đọc biến toàn cục `AppContext`.
5. **R06-T05: Xây dựng `FileWorkspaceService` cho File Explorer & AI Editor**:
- Tạo `application/workspaces/file_workspace_service.py`: Cung cấp API đọc cây thư mục, xem trước file đa định dạng, áp dụng AI code diffs an toàn.
---
#### 🔹 R07: Scheduling & Workflow Runtime (Bộ Lập Lịch & Động Cơ Quy Trình)
* **Ý nghĩa & Mục tiêu**: Tách biệt hoàn toàn tầng lưu trữ Task (`core/tasks.py`) và thuật toán tính toán lịch (`ScheduleCalculator`) khỏi `QTimer` trong `core/task_scheduler.py#L20`; xây dựng `TaskApplicationService` và `Co4EWorkflowService`.
* **Team chịu trách nhiệm**: 🟢 **Team Hoa** (Task Scheduling) + 🟣 **Team Nam** (Co4E Workflows).
* **Chi Tiết Cụ Thể Các Task Cần Làm Trong R07**:
1. **R07-T01: Tách `TaskRepository` Lưu Trữ JSON Độc Lập**:
- Tạo `infrastructure/persistence/json/task_repository_impl.py`: Đọc/ghi danh sách công việc (`tasks.json`) qua `AtomicJsonFile` với locking bảo vệ khi nhiều luồng cùng truy cập.
2. **R07-T02: Xây dựng Thuật Toán Tính Lịch `ScheduleCalculator`**:
- Tạo `domain/tasks/schedule_calculator.py`: Tính toán thời điểm chạy kế tiếp cho các dạng lịch: One-time, Interval, Daily, Weekly, Monthly, Cron Expression. Hoàn toàn là Pure Python, có unit test bao phủ 100%.
3. **R07-T03: Xây dựng Adapter `QtSchedulerClock`**:
- Tạo `platform/qt/qt_scheduler_clock.py`: Bọc `QTimer` vào Clock Interface. Cho phép trong unit test có thể thay thế bằng `FakeClock` để tua nhanh thời gian mà không cần chờ đợi.
4. **R07-T04: Xây dựng `TaskApplicationService` (Pure Python)**:
- Tạo `application/scheduling/task_application_service.py`: Điều phối toàn bộ nghiệp vụ quản lý task: CRUD task, kích hoạt chạy ngay (`run_now`), sao chép task, tạm dừng, xóa hàng loạt.
5. **R07-T05: Xây dựng `AiTaskPlannerService` Tạo Task Tự Động**:
- Tạo `application/scheduling/ai_task_planner_service.py`: Phân tích câu lệnh tự nhiên của người dùng để sinh ra cấu hình task và lịch chạy tương ứng.
6. **R07-T06: Xây dựng `Co4EWorkflowService` Động Cơ Quy Trình Node**:
- Tạo `application/workflows/co4e_workflow_service.py`: Tách logic thực thi đồ thị node từ `core/co4e_run_manager.py`. Quản lý state của từng node, truyền dữ liệu giữa các node và xử lý retry/error.
---
#### 🔹 R08: UI/Application Separation (Phân Rã Toàn Diện Các God Widgets)
* **Ý nghĩa & Mục tiêu**: Tách nhỏ toàn bộ các màn hình khổng lồ (>1.500 - 2.000 dòng) thành các widget con chuyên trách, đảm bảo mỗi file < 400 dòng và chỉ đảm nhận hiển thị / bắt sự kiện giao diện.
* **Team chịu trách nhiệm**: **Cả 3 Team** (Mỗi team phụ trách phân hệ của mình):
* **Chi Tiết Cụ Thể Các Task Cần Làm Trong R08**:
1. **🔵 Team Duy – Tách `ChatPanel` (`ui/chat_panel.py` >1.800 dòng) thành 6 widgets con**:
- `R08-T01`: `presentation/chat/chat_history_widget.py` (Render bong bóng chat, streaming markdown, tool call cards).
- `R08-T02`: `presentation/chat/composer_widget.py` (Ô nhập liệu text, phím tắt Ctrl+Enter, auto-resize).
- `R08-T03`: `presentation/chat/attachment_picker.py` (Widget chọn file, ảnh, folder đính kèm).
- `R08-T04`: `presentation/chat/audio_recorder_widget.py` (Widget ghi âm giọng nói & chuyển thành văn bản).
- `R08-T05`: `presentation/chat/chat_output_panel.py` (Panel hiển thị file output sinh ra trong turn).
- `R08-T06`: `presentation/chat/chat_panel.py` (Shell container điều phối các widget con & `Floating HelpAgent`).
2. **🟣 Team Nam – Tách `SettingsDialog`, `MonitoringTab`, `Co4ETab` & Shell `MainWindow`**:
- `R08-T07`: `presentation/settings/` ➔ Tách thành `provider_settings_widget.py`, `connector_settings_widget.py`, `routing_settings_widget.py`, `general_settings_widget.py`.
- `R08-T08`: `presentation/monitoring/` ➔ Tách 8 tab con thành từng file: `overview_tab.py`, `sandbox_status_tab.py`, `security_events_tab.py`, `mcp_history_tab.py`, `action_logs_tab.py`, `agents_admin_tab.py`, `security_settings_tab.py`, `tools_admin_tab.py`.
- `R08-T09`: `presentation/co4e/` ➔ Tách thành `co4e_canvas_widget.py`, `node_property_panel.py`, `co4e_run_control_widget.py`, `co4e_chat_view.py`.
- `R08-T10`: `presentation/shell/` ➔ Xây dựng `bootstrap.py` (Composition Root) và tách `app.py::MainWindow` thành `main_window.py`, `tray_manager.py`, `lifecycle_coordinator.py`.
3. **🟢 Team Hoa – Tách `ScheduleTaskTab`, `FolderTab`, `DashboardTab` & `StructureGraphView`**:
- `R08-T11`: `presentation/scheduling/` ➔ Tách thành `kanban_board_widget.py` (7 cột kéo thả), `calendar_view_widget.py`, `ai_task_creator_dialog.py`, `ai_task_import_dialog.py`.
- `R08-T12`: `presentation/folder/` ➔ Tách thành `workspace_file_tree.py`, `document_preview_manager.py` (PDF/Word/Excel/Images), `ai_file_editor_dialog.py`.
- `R08-T13`: `presentation/dashboard/` ➔ Tách thành `token_usage_card_widget.py`, `usage_chart_widget.py`, `habits_widget.py`.
- `R08-T14`: `presentation/graph/` ➔ Tách thành `structure_graph_view.py` & `graph_qa_widget.py`.
---
#### 🔹 R09: Security Runtime, Sandbox & Observability (An Ninh Runtime, Sandbox & Giám Sát)
* **Ý nghĩa & Mục tiêu**: Phân biệt rõ ràng giữa quy tắc bảo mật bắt buộc (Enforced Deterministic Rules) và các gợi ý bảo mật từ AI (Advisory Guardrails); loại bỏ circular imports; chuẩn hóa định dạng log kiểm toán canonical.
* **Team chịu trách nhiệm**: 🟣 **Team Nam** (Chủ trì) + 🔵 **Team Duy**.
* **Chi Tiết Cụ Thể Các Task Cần Làm Trong R09**:
1. **R09-T01: Chuẩn Hóa Security Policy Model**:
- Tạo `docs/architecture/security-policy.md`: Phân định ranh giới giữa bộ lọc quy tắc cứng (regex cấm xóa tệp hệ thống, cấm truy cập thư mục ngoài sandbox) và bộ đánh giá rủi ro mềm từ LLM.
2. **R09-T02: Xử Lý Triệt Để Circular Import `model_pricing` ↔ `usage_tracker`**:
- Tách DTO giá mô hình (`ModelPricing`) vào `domain/models/` để cả `model_pricing.py` và `usage_tracker.py` cùng import xuôi mà không import vòng tròn.
3. **R09-T03: Xử Lý Triệt Để Circular Import `agent_security` ↔ `agent_security_alert`**:
- Tách các enum và event cảnh báo bảo mật (`SecurityAlertEvent`) sang `domain/security/` để xoá hoàn toàn import chéo.
4. **R09-T04: Xây Dựng `CanonicalAuditLogger` Thống Nhất Định Dạng Log**:
- Tạo `infrastructure/telemetry/audit_logger.py`: Chuẩn hóa schema nhật ký (timestamp UTC, actor, action, resource, outcome, latency) ghi ra file JSON Lines an toàn.
5. **R09-T05: Xây Dựng `MonitoringQueryService` Truy Vấn Dữ Liệu Read-Only**:
- Tạo `application/monitoring/monitoring_query_service.py`: Cung cấp API truy vấn log kiểm toán có phân trang, lọc theo thời gian, lọc theo mức độ nghiêm trọng (severity).
6. **R09-T06: Chuẩn Hóa Ma Trận Năng Lực Sandbox Trên Từng Hệ Điều Hành**:
- Tạo `infrastructure/sandbox/sandbox_capabilities.py`: Tách biệt cơ chế cách ly thực tế: Windows (Job Objects / AppContainer), Linux (Namespaces / Bubblewrap), macOS (Sandbox-exec).
---
#### 🔹 R10: Testing, Packaging & Contributor Experience (Hệ Thống Kiểm Thử & Tài Liệu Đóng Góp)
* **Ý nghĩa & Mục tiêu**: Đây là **Task trọng tâm cốt lõi của Team Duy (Tech Lead)** nhằm thiết lập hệ thống bảo vệ toàn diện cho dự án: xây dựng tháp kiểm thử 4 tầng (Unit, Contract, Integration, E2E Smoke), cài đặt CI Quality Gate tự động, soạn thảo bộ công thức Contributor Recipes và thực hiện kiểm thử khói tổng thể trước khi release.
* **Team chịu trách nhiệm**: 🔵 **Team Duy (Chủ Trì Chính - Task Trọng Tâm Của Team Duy)**.
* **Chi Tiết Cụ Thể Các Task Cần Làm Trong R10**:
1. **R10-T01: Xây dựng Tháp Kiểm Thử Phân Tầng (Test Pyramid Architecture - `tests/`)**:
- `tests/unit/`: Kiểm thử các logic độc lập không I/O (Domain entities, `ScheduleCalculator`, `AtomicJsonFile`, parsing). Thời gian chạy: < 0.05s/test.
- `tests/contracts/`: Bộ test xác thực interface chuẩn của Provider API (`test_providers.py`) và Tool Handler (`test_tools.py`) để các provider mới chỉ cần pass contract là cắm vào được ngay.
- `tests/integration/`: Kiểm thử phối hợp nhiều tầng không cần UI (`test_chat_flow.py`, `test_workflow_execution.py`, `test_task_scheduling.py`).
- `tests/fakes/`: Thư viện test doubles tái sử dụng cho cả 3 team (`FakeProvider`, `FakeToolExecutor`, `FakeClock`, `FakeKeyringAdapter`).
2. **R10-T02: Xây Dựng Bộ Script CI Quality Gate Tự Động (`scripts/`)**:
- `scripts/check_imports.py`: Script phân tích AST kiểm tra chặn 100% import `PySide6` trong `domain/` và `application/`.
- `scripts/check_loc.py`: Script quét LOC tự động cảnh báo lỗi nếu có bất kỳ file nào > 400 dòng code.
- `scripts/audit_security.py`: Script quét phát hiện secret/API Key plaintext trong toàn bộ codebase.
- `scripts/run_quality_gate.py`: Script tổng hợp chạy 1 lệnh duy nhất để kiểm tra toàn bộ tiêu chí CASAN Gate trước khi merge PR.
3. **R10-T03: Cập Nhật Tài Liệu Dự Án & Hướng Dẫn Thiết Lập (`README.md`, `START_CONTRIBUTING.md`)**:
- Cập nhật sơ đồ kiến trúc 4 tầng chuẩn (Presentation ➔ Application ➔ Domain ➔ Infrastructure).
- Hướng dẫn cài đặt môi trường phát triển local, chạy test và cấu hình Git pre-commit hook để chạy script kiểm tra tự động.
4. **R10-T04: Soạn Thảo Bộ Contributor Recipes (`docs/governance/contributor-recipes.md`)**:
- Hướng dẫn mẫu từng bước kèm code mẫu:
- *Recipe 1*: "Cách thêm một Model Provider mới" (Khai báo `ProviderDescriptor`, tạo Adapter trong `infrastructure/providers/`, chạy Contract Test).
- *Recipe 2*: "Cách thêm một Built-in Tool hoặc MCP Tool mới" (Khai báo `ToolDescriptor`, đăng ký capability, cấu hình `ToolPolicyGateway`).
- *Recipe 3*: "Cách thêm một Màn hình / Sub-widget mới" (Tạo Widget trong `presentation/`, kết nối Application Service qua Qt Signals, tuân thủ giới hạn <400 LOC).
5. **R10-T05: Bộ Kiểm Thử Khói Phát Hành E2E (Release Smoke Test - `tests/e2e/test_smoke.py`)**:
- Khởi động ứng dụng qua `bootstrap.py` ở chế độ headless Qt offscreen và thực thi tự động 5 kịch bản chính:
1. Khởi tạo chat session, gửi tin nhắn và nhận stream event từ `FakeProvider`.
2. Tạo mới task trên Kanban, trigger chạy task và xác nhận ghi log.
3. Mở File Explorer, tạo file tạm trong `WorkspaceSession` và đọc nội dung an toàn.
4. Tạo workflow 2 node trên Co4E Studio và kích hoạt chạy thử.
5. Mở Settings Dialog, cấu hình mock provider API Key và kiểm tra lưu thành công vào `SecretStore`.
- Tiêu chí hoàn thành: 100% 5 kịch bản E2E pass, không xung đột luồng và ứng dụng thoát sạch sẽ.
---
## 📊 VII. BẢNG PHÂN CÔNG, KPI & QUY TRÌNH PHỐI HỢP LIÊN TEAM
### 1. Bảng Phân Công & KPI Đo Lường Thành Công
| Team | Phân Hệ Chính | Trách Nhiệm Cụ Thể | KPI Đo Lường Hoàn Thành |
| :--- | :--- | :--- | :--- |
| **🔵 Team Duy**<br>*(Tech Lead)* | **Core AI, Routing & Testing** | • R01 ADR & Runtime test doubles<br>• R03 Provider Registry & Unified Routing<br>• R04 ConversationApplicationService<br>• R08 Tách ChatPanel thành 5 sub-widgets<br>• **R10 Testing Pyramid, Contributor Recipes & Smoke Test**<br>• Chủ trì CASAN Check 3 | • 0 PySide6 import trong `application/conversations` và `application/model_routing`<br>• 0 file >400 dòng trong `presentation/chat/`<br>• Bộ test pyramid >81 tests pass 100%<br>• CASAN Check 3 PASS |
| **🟣 Team Nam** | **Workflows & Governance** | • R02 Atomic Config & Keyring SecretStore<br>• R08 Tách Settings (4 sections) & Monitoring (7 tabs)<br>• R08 Tách Co4E Tab & Co4EWorkflowService<br>• Composition Root (`bootstrap.py`) & MainWindow Shell<br>• R09 Security Policy Model & Fix Circular Imports<br>• Chủ trì CASAN Check 1 | • 0 plaintext credential/API Key trong JSON<br>• 0 file >400 dòng trong `presentation/co4e/`, `monitoring/`, `settings/`<br>• CASAN Check 1 PASS |
| **🟢 Team Hoa** | **Workspace & Tools** | • R05 ToolRegistry & phân rã `core/tools.py`<br>• R06 WorkspaceSession & isolation<br>• R07 TaskApplicationService & QtSchedulerClock<br>• R08 Tách FolderTab, ScheduleTaskTab, DashboardTab, Graph<br>• Chủ trì CASAN Check 2 | • 0 file >400 dòng trong `presentation/folder/`, `scheduling/`, `dashboard/`, `graph/`<br>• Task Scheduler chạy độc lập không phụ thuộc Qt GUI<br>• CASAN Check 2 PASS |
---
### 2. Quy Trình Phối Hợp & Phòng Ngừa Xung Đột (Collaboration Protocol)
1. **Quy tắc Branching & PR:**
* Mỗi team làm việc trên prefix branch riêng biệt:
* Team Duy: `duy/chat-routing-tests-*`
* Team Nam: `nam/workflow-governance-*`
* Team Hoa: `hoa/workspace-tools-*`
* Mọi PR trước khi merge vào nhánh chung (`develop`/`main`) phải kèm theo unit tests và đảm bảo suite test hiện tại không bị regression.
2. **Quy tắc Mocking liên team (Không chờ đợi):**
* Nếu Team Duy (Chat) cần kích hoạt task ➔ gọi qua interface `TaskApplicationService` (dùng `FakeTaskApplicationService` trong test do Team Hoa cung cấp DTO).
* Nếu Team Hoa (File Editor / Graph RAG) cần gọi model ➔ gọi qua `RoutingApplicationService` / `FakeProvider` do Team Duy chốt DTO từ Ngày 1.
* Nếu Team Nam (Co4E Runner) cần gọi Tool ➔ gọi qua `ToolPolicyGateway` do Team Hoa cung cấp.
* Không team nào được chặn (block) tiến độ của team khác.
3. **Tiêu chuẩn hoàn thành PR (Definition of Done - DoD):**
* File mới hoặc sau refactor không vượt quá **400 dòng code**.
* Không import `PySide6` trong `domain/` và `application/`.
* Credentials/API Keys được lưu trữ qua `SecretStore` (Keyring), không lưu plaintext trong `config.json`.
* **Bắt buộc comment code bằng Tiếng Anh (English In-code Comments)**: Mỗi dòng hoặc khối code sửa đổi/thêm mới phải có chú thích bằng tiếng Anh giải thích rõ mục đích và lý do kỹ thuật.
* **Ghi nhận thời gian thực hiện (Task Start/End Timestamps)**: Mọi task khi bắt đầu phải log ngày giờ Start, khi xong phải log ngày giờ End vào `Refactoring_Checklist.md` và PR description.
* Chi tiết đối chiếu tại checklist `Refactoring_Checklist.md`.
> [!IMPORTANT]
> ### 📝 QUY ĐỊNH BẮT BUỘC KHI CODE & THEO DÕI TIẾN ĐỘ:
> 1. **In-Code Comments in English**: Ở mỗi dòng hoặc đoạn code được chỉnh sửa/bóc tách, lập trình viên **bắt buộc phải viết comment bằng tiếng Anh** giải thích rõ logic xử lý và lý do kiến trúc (rationale). Ví dụ:
> ```python
> # Extract immutable snapshot request to decouple execution lifecycle from PySide6 UI
> request = ConversationExecutionRequest.from_composer_state(...)
> ```
> 2. **Task Start/End Timestamps**:
> - Khi bắt đầu task ➔ Ghi nhận thời gian: `Start: YYYY-MM-DD HH:mm`.
> - Khi hoàn tất & test pass ➔ Ghi nhận thời gian: `End: YYYY-MM-DD HH:mm`.
> - Ghi nhận đầy đủ vào checklist theo dõi tại `Refactoring_Checklist.md` để đảm bảo tính minh bạch và tiến độ của cả 3 team.
## 🚫 VIII. NHỮNG GÌ KHÔNG LÀM (Anti-patterns)
> [!WARNING]
> Để tránh over-engineering và rewrite không kiểm soát, nhóm phải tuân thủ:
- ❌ **Không di chuyển file ngay** trước khi có contract và test bảo vệ.
- ❌ **Không dựng event bus toàn ứng dụng** hoặc DI framework phức tạp.
- ❌ **Không bắt mọi class phải có interface** — chỉ introduce contract tại seam có nhiều caller.
- ❌ **Không rewrite đồng thời** Cowork + Co4E + Folder + Scheduler trong 1 PR.
- ❌ **Không gọi là "frontend/backend"** — đây là desktop single-process.
- ❌ **Không unify Flow/Co4E** trước khi semantics được ghi rõ và có contract tests.
- ❌ **Không xóa candidate dead code** (LoginDialog, account modules) trộn vào PR refactor — phải PR riêng.
---
## 🗺️ IX. BẢN ĐỒ DI CHUYỂN FUNCTION (FUNCTION MIGRATION MAP)
> Dựa trực tiếp từ `function_list.md`. Mỗi function hiện tại được ánh xạ đến file mới sau khi chia nhỏ.
> **Quy ước**: 🎨 = `presentation/` | 📋 = `application/` | 🧠 = `domain/` | 🔧 = `infrastructure/`
### Dashboard (Section 1 trong function_list.md)
| Function Hiện Tại | File Mới | Tầng |
| :--- | :--- | :--- |
| `_refresh_cards()` | `presentation/dashboard/token_usage_card_widget.py` | 🎨 |
| `_refresh_chart()`, `_chart_prev()`, `_chart_next()`, `_on_gran_changed()` | `presentation/dashboard/usage_chart_widget.py` | 🎨 |
| `_refresh_budget()`, `_apply_budget()` | `presentation/dashboard/token_usage_card_widget.py` | 🎨 |
| `_refresh_habits()` | `presentation/dashboard/habits_widget.py` | 🎨 |
| `_ai_analyze()`, `_apply_saving_strategy()` | `application/monitoring/dashboard_query_service.py` | 📋 |
| Currency Picker | `presentation/dashboard/token_usage_card_widget.py` | 🎨 |
### Schedule Task (Section 2 trong function_list.md)
| Function Hiện Tại | File Mới | Tầng |
| :--- | :--- | :--- |
| `_build_kanban()`, `_render_kanban()`, `_on_task_dropped()` | `presentation/scheduling/kanban_board_widget.py` | 🎨 |
| `_on_card_double_click()`, `_on_card_right_click()`, `_bulk_delete_menu()` | `presentation/scheduling/kanban_board_widget.py` | 🎨 |
| `_search_tasks()`, `_filter_by_type()` | `presentation/scheduling/kanban_board_widget.py` | 🎨 |
| `_run_now(task_id)`, `_duplicate_task()`, `_pause_task()`, `_delete_task()` | `application/scheduling/task_application_service.py` | 📋 |
| `_view_logs(task_id)` | `presentation/scheduling/kanban_board_widget.py` → gọi MonitoringQueryService | 🎨 |
| `_build_calendar()`, `_shift()`, `add_task_on_date()`, `edit_task()` | `presentation/scheduling/calendar_view_widget.py` | 🎨 |
| `_open_add_dialog()` | `presentation/scheduling/schedule_task_tab.py` (container) | 🎨 |
| `_ai_create_task()`, `_ai_pick_files()`, `_generate()`, `_on_planned()`, `_confirm()` | `presentation/scheduling/ai_task_creator_dialog.py` | 🎨 |
| `_ai_import()`, `_ai_pick_import_files()`, `_generate_import()`, `_on_import_planned()` | `presentation/scheduling/ai_task_import_dialog.py` | 🎨 |
| AI generation logic | `application/scheduling/ai_task_planner_service.py` | 📋 |
### Workspace / Cowork Chat (Section 3.2.1 trong function_list.md)
| Function Hiện Tại | File Mới | Tầng |
| :--- | :--- | :--- |
| `new_session()` | `application/conversations/conversation_application_service.py` | 📋 |
| `send_message()` → `_submit_message()` | `presentation/chat/composer_widget.py` (UI trigger) | 🎨 |
| `_build_job()` → `ConversationExecutionRequest` | `application/conversations/conversation_application_service.py` | 📋 |
| `_cleanup_turn()`, `_promote_turn_outputs()` | `application/conversations/conversation_application_service.py` | 📋 |
| `_refresh_outputs_from_disk()`, `_pick_output_folder()` | `presentation/chat/chat_output_panel.py` | 🎨 |
| `_open_skills_manager()` | `presentation/chat/chat_panel.py` (container) | 🎨 |
| `refresh_header()` | `presentation/chat/chat_panel.py` (container) | 🎨 |
| `refresh_agents()` | `presentation/chat/chat_panel.py` (combo widget) | 🎨 |
| `admin_agent_prompt()` | `application/conversations/conversation_application_service.py` | 📋 |
| `build_provider()` | `infrastructure/providers/provider_factory.py` | 🔧 |
| `workspace_dir()` | `domain/workspaces/workspace_session.py` | 🧠 |
| `_start_watching()`, `_on_file_changed()` | `presentation/chat/chat_output_panel.py` | 🎨 |
| `_on_turn_started()`, `_on_turn_finished()`, `_on_event(ev)` | `presentation/chat/chat_history_widget.py` (event renderer) | 🎨 |
| `_compress_messages()` | `application/conversations/conversation_application_service.py` | 📋 |
| `_apply_routing()` | `application/model_routing/routing_application_service.py` | 📋 |
| `_on_agent_changed()`, `_note_agent_switch()` | `presentation/chat/chat_panel.py` | 🎨 |
| `_ensure_conversation()`, `load_conversation()`, `_save_conversation()` | `application/conversations/conversation_application_service.py` | 📋 |
| `running_session_ids()`, `active_workers()` | `application/conversations/conversation_application_service.py` | 📋 |
| `send()`, `attach_files()`, `attach_links()` | `presentation/chat/composer_widget.py` | 🎨 |
| `has_any_queue()`, `_parse_directives()`, `_show_autocomplete()` | `presentation/chat/composer_widget.py` | 🎨 |
### Co4E Workflow Studio (Section 3.2.2 trong function_list.md)
| Function Hiện Tại | File Mới | Tầng |
| :--- | :--- | :--- |
| `_build_sidebar()`, `_build_canvas()`, `_build_config_panel()`, `_toggle_config()` | `presentation/co4e/co4e_tab.py` (container) | 🎨 |
| `_refresh_flows_list()`, `_create_flow()`, `_delete_flow()`, `_duplicate_flow()` | `application/workflows/co4e_workflow_service.py` | 📋 |
| `_import_flow()`, `_export_flow()` | `application/workflows/co4e_workflow_service.py` | 📋 |
| `_run_flow()`, `_stop_flow()` | `application/workflows/co4e_workflow_service.py` | 📋 |
| `_open_flow()` | `presentation/co4e/co4e_tab.py` → gọi canvas | 🎨 |
| `_refresh_agents_list()`, `_create_agent()`, `_edit_agent()`, `_delete_agent()`, `_toggle_agent_enabled()` | `presentation/co4e/agent_list_panel.py` | 🎨 |
| `_refresh_skills_list()` | `presentation/co4e/skills_list_panel.py` | 🎨 |
| `zoom_in()`, `zoom_out()`, `fit_view()` | `presentation/co4e/co4e_canvas_widget.py` | 🎨 |
| `_add_node()`, `_delete_node()`, `_connect_nodes()`, `_drag_node()`, `_select_node()`, `_activate_node()` | `presentation/co4e/co4e_canvas_widget.py` | 🎨 |
| `_set_run_mode()`, `_run_step()`, `_on_step_finished()`, `_render_plan()` | `presentation/co4e/co4e_run_control_widget.py` | 🎨 |
| `_get_flow_chat()`, `_on_chat_event()` | `presentation/co4e/co4e_chat_view.py` | 🎨 |
### Folder / File Explorer (Section 3.2.3 trong function_list.md)
| Function Hiện Tại | File Mới | Tầng |
| :--- | :--- | :--- |
| `set_root()`, `_build_tree_view()` | `presentation/folder/workspace_file_tree.py` | 🎨 |
| `_open_file()`, `_view_source()`, `_view_html_preview()`, `_view_office_doc()`, `_view_image()` | `presentation/folder/document_preview_manager.py` | 🎨 |
| `_edit_file()`, `_save_file()`, `_preview_toggle()` | `presentation/folder/workspace_file_tree.py` | 🎨 |
| `_create_new_file()`, `_create_new_folder()`, `_rename_item()`, `_delete_item()`, `_copy_item()`, `_paste_item()` | `presentation/folder/workspace_file_tree.py` | 🎨 |
| `refresh_ai_models()` | `presentation/folder/folder_tab.py` (container) | 🎨 |
| `_ai_send()`, `_ai_discard()`, `_reset_ai_conversation()` | `presentation/folder/ai_file_editor_dialog.py` | 🎨 |
| `_ai_apply()` | `application/workspaces/file_workspace_service.py` | 📋 |
| `_ai_apply_routing()` | `application/model_routing/routing_application_service.py` | 📋 |
### Graph RAG (Section 3.2.4 trong function_list.md)
| Function Hiện Tại | File Mới | Tầng |
| :--- | :--- | :--- |
| `_build_graph()` | `application/workspaces/graph_index_service.py` | 📋 |
| `_render_d3_graph()`, `_render_native_graph()`, `_auto_rotate()` | `presentation/graph/structure_graph_view.py` | 🎨 |
| `_on_node_click()`, `_open_node_path()`, `_refresh_graph()` | `presentation/graph/structure_graph_view.py` | 🎨 |
| `_search_graph()`, `_filter_by_kind()`, `_zoom_graph()` | `presentation/graph/structure_graph_view.py` | 🎨 |
| `_ask_question()`, `_on_ask_event()`, `_on_ask_done()` | `presentation/graph/graph_qa_widget.py` | 🎨 |
| `_candidate_file_paths()`, `_extract_tmp_dir()`, `_clear_extracts()` | `application/workspaces/graph_index_service.py` | 📋 |
### Monitoring (Section 4 trong function_list.md)
| Function Hiện Tại | File Mới | Tầng |
| :--- | :--- | :--- |
| `_refresh_overview()`, `_refresh_usage_cards()`, `_refresh_resource_usage()`, `_refresh_recent_activity()` | `presentation/monitoring/overview_tab.py` | 🎨 |
| `_refresh_sandbox_details()`, `_refresh_permissions()`, `_refresh_audit_log()` | `presentation/monitoring/sandbox_status_tab.py` | 🎨 |
| `_refresh_budget()`, `_apply_budget()` | `presentation/monitoring/overview_tab.py` | 🎨 |
| `_refresh_security_events()`, `_filter_security_events()`, `_sort_events()` | `presentation/monitoring/security_events_tab.py` | 🎨 |
| `_refresh_mcp_calls()`, `_filter_mcp_calls()` | `presentation/monitoring/mcp_history_tab.py` | 🎨 |
| `_refresh_action_logs()`, `_filter_action_logs()`, `_sort_action_logs()` | `presentation/monitoring/action_logs_tab.py` | 🎨 |
| `_refresh_agent_status()` | `presentation/monitoring/agent_status_tab.py` | 🎨 |
| `_toggle_sandbox()`, `_toggle_network_block()`, `_set_resource_limits()`, `_toggle_command_confirm()`, `_manage_permissions()` | `presentation/monitoring/security_settings_tab.py` | 🎨 |
| Query/refresh data logic | `application/monitoring/monitoring_query_service.py` | 📋 |
### Settings (Section 5 trong function_list.md)
| Function Hiện Tại | File Mới | Tầng |
| :--- | :--- | :--- |
| `_on_provider_changed()`, `_load_models()`, `_test_connection()` | `presentation/settings/provider_settings_widget.py` | 🎨 |
| `_stash_provider_fields()`, `_apply_provider_fields()`, Model List Widget | `presentation/settings/provider_settings_widget.py` | 🎨 |
| `_add_mcp_server()`, `_edit_mcp_server()`, `_delete_mcp_server()`, `_test_mcp_connection()` | `presentation/settings/connector_settings_widget.py` | 🎨 |
| MS365, CAD/CAE Connectors | `presentation/settings/connector_settings_widget.py` | 🎨 |
| `routing_mode`, `routing_policy`, `routing_min_gain`, `routing_timeout`, `routing_interval`, `routing_concurrency`, `routing_judge` | `presentation/settings/routing_settings_widget.py` | 🎨 |
| Language Picker, `tray_chk`, `notify_chk` | `presentation/settings/general_settings_widget.py` | 🎨 |
| `_save()` | `application/settings/settings_application_service.py` | 📋 |
| `attach_tokens`, `attach_files`, `struct_nodes`, `struct_edges` | `presentation/settings/general_settings_widget.py` | 🎨 |
| Provider test connection (network call) | `infrastructure/providers/provider_factory.py` | 🔧 |
| MCP test connection (network call) | `infrastructure/mcp/mcp_client.py` | 🔧 |
---
📄 Tài liệu này là bản hợp nhất chính thức. Cập nhật: **14/08/2026** (bổ sung Function Migration Map từ `function_list.md`).
+1
View File
@@ -0,0 +1 @@
"""Domain Layer: Pure Python domain entities, value objects, and events."""
+1
View File
@@ -0,0 +1 @@
"""Domain agents package: turn requests, agent events, and role definitions."""
+1
View File
@@ -0,0 +1 @@
"""Domain models package: provider descriptors, model pricing, and routing metadata."""
+1
View File
@@ -0,0 +1 @@
"""Domain security package: security policies, alert events, and permission types."""
+1
View File
@@ -0,0 +1 @@
"""Domain tasks package: task definitions and deterministic schedule calculators."""
+1
View File
@@ -0,0 +1 @@
"""Domain tools package: tool descriptors, capability scopes, and registry interfaces."""
+1
View File
@@ -0,0 +1 @@
"""Domain workspaces package: immutable WorkspaceSession definitions."""
+1
View File
@@ -0,0 +1 @@
"""Infrastructure Layer: External system adapters, persistence, and SDK clients."""
+1
View File
@@ -0,0 +1 @@
"""Infrastructure config package: ConfigRepository and typed settings facades."""
+1
View File
@@ -0,0 +1 @@
"""Infrastructure filesystem package: Tool handlers (file, command, fetch tools) and execution workspace."""
+1
View File
@@ -0,0 +1 @@
"""Infrastructure MCP package: McpToolSourceManager and child process lifecycle."""
+1
View File
@@ -0,0 +1 @@
"""Infrastructure persistence package."""
@@ -0,0 +1 @@
"""Infrastructure JSON persistence package: AtomicJsonFile and repositories."""
+1
View File
@@ -0,0 +1 @@
"""Infrastructure platform adapters package."""
+1
View File
@@ -0,0 +1 @@
"""Infrastructure Qt platform adapters: QtSchedulerClock."""
+1
View File
@@ -0,0 +1 @@
"""Infrastructure providers package: LLM provider adapters and ProviderRegistry."""
+1
View File
@@ -0,0 +1 @@
"""Infrastructure sandbox package: OS-specific sandbox capability adapters."""
+1
View File
@@ -0,0 +1 @@
"""Infrastructure telemetry package: CanonicalAuditLogger and token usage sinks."""
+5
View File
@@ -0,0 +1,5 @@
"""Provider-neutral Project Context MCP server template."""
from .server import build_server, dispatch
__all__ = ["build_server", "dispatch"]
+106
View File
@@ -0,0 +1,106 @@
"""Shared, stable boundary used by all Project Context tool work packages."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Protocol
from pydantic import AnyUrl, BaseModel, ConfigDict, Field
class ContractModel(BaseModel):
"""Strict immutable model so provider-specific fields cannot leak to the Agent."""
model_config = ConfigDict(extra="forbid", frozen=True)
class IdentityContext(ContractModel):
actor_id: str = Field(min_length=1, max_length=256)
org_unit: str = Field(min_length=1, max_length=128)
customer: str = Field(min_length=1, max_length=128)
project: str = Field(min_length=1, max_length=128)
granted_scopes: frozenset[str]
class SourceCitation(ContractModel):
system: str = Field(min_length=1, max_length=64)
url: AnyUrl
revision: str = Field(min_length=1, max_length=256)
retrieved_at: datetime
@dataclass(frozen=True)
class DispatchResult:
ok: bool
payload: dict[str, Any]
class PolicyDecisionPoint(Protocol):
def decide(self, identity: IdentityContext, tool_name: str, project_id: str) -> bool: ...
class CredentialResolver(Protocol):
def resolve(self, identity: IdentityContext, tool_name: str) -> Any: ...
@dataclass(frozen=True)
class ProjectContextRuntime:
identity: IdentityContext
policy: PolicyDecisionPoint
credential_resolver: CredentialResolver
class ProviderError(RuntimeError):
"""A provider failure with a caller-safe message and retry classification."""
def __init__(self, code: str, message: str, *, retryable: bool) -> None:
super().__init__(message)
self.code = code
self.safe_message = message
self.retryable = retryable
ToolHandler = Callable[[ContractModel, Any], dict[str, Any]]
@dataclass(frozen=True)
class ToolTemplate:
name: str
description: str
input_model: type[ContractModel]
output_model: type[ContractModel]
handler: ToolHandler
def declaration(self) -> dict[str, Any]:
return {
"name": self.name,
"description": self.description,
"inputSchema": self.input_model.model_json_schema(),
"outputSchema": self.output_model.model_json_schema(),
}
def error_result(
code: str,
*,
category: str,
retryable: bool,
message: str,
suggested_action: str,
correlation_id: str,
) -> DispatchResult:
return DispatchResult(
ok=False,
payload={
"error": {
"code": code,
"category": category,
"retryable": retryable,
"message": message,
"suggested_action": suggested_action,
"correlation_id": correlation_id,
}
},
)
@@ -0,0 +1 @@
"""One provider module per member-owned tool work package."""
@@ -0,0 +1,25 @@
"""Provider boundary owned with get_project_change_context."""
from __future__ import annotations
from typing import Any, Protocol
from ..foundation import IdentityContext, ProviderError
class ChangeProvider(Protocol):
def get_change_context(self, **arguments: Any) -> dict[str, Any]: ...
class UnconfiguredChangeProvider:
def get_change_context(self, **arguments: Any) -> dict[str, Any]:
raise ProviderError(
"UNAVAILABLE",
"The change provider is not configured for this environment.",
retryable=False,
)
def build_provider(identity: IdentityContext) -> ChangeProvider:
"""Replace only this factory when wiring the approved read-only Git adapter."""
return UnconfiguredChangeProvider()
@@ -0,0 +1,25 @@
"""Provider boundary owned with get_project_issue_context."""
from __future__ import annotations
from typing import Any, Protocol
from ..foundation import IdentityContext, ProviderError
class IssueProvider(Protocol):
def get_issue_context(self, **arguments: Any) -> dict[str, Any]: ...
class UnconfiguredIssueProvider:
def get_issue_context(self, **arguments: Any) -> dict[str, Any]:
raise ProviderError(
"UNAVAILABLE",
"The issue provider is not configured for this environment.",
retryable=False,
)
def build_provider(identity: IdentityContext) -> IssueProvider:
"""Replace only this factory when wiring the approved read-only issue adapter."""
return UnconfiguredIssueProvider()
@@ -0,0 +1,25 @@
"""Provider boundary owned with search_project_knowledge."""
from __future__ import annotations
from typing import Any, Protocol
from ..foundation import IdentityContext, ProviderError
class KnowledgeProvider(Protocol):
def search_knowledge(self, **arguments: Any) -> dict[str, Any]: ...
class UnconfiguredKnowledgeProvider:
def search_knowledge(self, **arguments: Any) -> dict[str, Any]:
raise ProviderError(
"UNAVAILABLE",
"The knowledge provider is not configured for this environment.",
retryable=False,
)
def build_provider(identity: IdentityContext) -> KnowledgeProvider:
"""Replace only this factory when wiring approved project retrieval."""
return UnconfiguredKnowledgeProvider()
+23
View File
@@ -0,0 +1,23 @@
"""Immutable registry composed before member work starts to prevent merge conflicts."""
from __future__ import annotations
from types import MappingProxyType
from typing import Any
from .foundation import ToolTemplate
from .tools.change_context import TOOL as CHANGE_CONTEXT_TOOL
from .tools.issue_context import TOOL as ISSUE_CONTEXT_TOOL
from .tools.knowledge_search import TOOL as KNOWLEDGE_SEARCH_TOOL
TOOLS: tuple[ToolTemplate, ...] = (
ISSUE_CONTEXT_TOOL,
KNOWLEDGE_SEARCH_TOOL,
CHANGE_CONTEXT_TOOL,
)
TOOLS_BY_NAME = MappingProxyType({tool.name: tool for tool in TOOLS})
TOOL_NAMES = tuple(tool.name for tool in TOOLS)
def tool_declarations() -> list[dict[str, Any]]:
return [tool.declaration() for tool in TOOLS]
+74
View File
@@ -0,0 +1,74 @@
"""Fail-closed identity, policy, and provider resolution for the template server."""
from __future__ import annotations
import os
import sys
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from .foundation import IdentityContext, ProjectContextRuntime, ProviderError
from .providers.change import build_provider as build_change_provider
from .providers.issue import build_provider as build_issue_provider
from .providers.knowledge import build_provider as build_knowledge_provider
MINIMUM_PYTHON = (3, 11)
def require_supported_python(version_info: tuple[int, ...] | None = None) -> None:
"""Fail with an actionable message before the MCP server starts."""
current = version_info or tuple(sys.version_info[:3])
if current[:2] < MINIMUM_PYTHON:
raise RuntimeError(
"Project Context MCP requires Python 3.11 or newer; "
f"current runtime is {current[0]}.{current[1]}"
)
@dataclass(frozen=True)
class ProjectScopePolicy:
"""Pilot policy: read scope and exact identity-bound project are both mandatory."""
def decide(self, identity: IdentityContext, tool_name: str, project_id: str) -> bool:
return "read" in identity.granted_scopes and project_id == identity.project
PROVIDER_FACTORIES: dict[str, Callable[[IdentityContext], Any]] = {
"get_project_issue_context": build_issue_provider,
"search_project_knowledge": build_knowledge_provider,
"get_project_change_context": build_change_provider,
}
@dataclass(frozen=True)
class ProjectProviderResolver:
def resolve(self, identity: IdentityContext, tool_name: str) -> Any:
factory = PROVIDER_FACTORIES.get(tool_name)
if factory is None:
raise ProviderError("NOT_FOUND", "The requested tool is not registered.", retryable=False)
return factory(identity)
def _required_environment(name: str) -> str:
value = os.environ.get(name, "").strip()
if not value:
raise RuntimeError(f"Project Context MCP cannot start: required setting {name} is missing")
return value
def default_runtime() -> ProjectContextRuntime:
"""Build immutable runtime state; missing identity configuration fails at boot."""
require_supported_python()
identity = IdentityContext(
actor_id=_required_environment("COWORK_MCP_ACTOR_ID"),
org_unit=_required_environment("COWORK_MCP_ORG_UNIT"),
customer=_required_environment("COWORK_MCP_CUSTOMER"),
project=_required_environment("COWORK_MCP_PROJECT"),
granted_scopes=frozenset({"read"}),
)
return ProjectContextRuntime(
identity=identity,
policy=ProjectScopePolicy(),
credential_resolver=ProjectProviderResolver(),
)
+142
View File
@@ -0,0 +1,142 @@
"""Low-level MCP stdio adapter around the transport-agnostic Project Context core."""
# ruff: noqa: UP045 -- Optional keeps the template importable with Pydantic on Python 3.9.
from __future__ import annotations
import json
from typing import Any, Optional
from uuid import uuid4
from pydantic import ValidationError
from .foundation import (
DispatchResult,
ProjectContextRuntime,
ProviderError,
error_result,
)
from .registry import TOOLS_BY_NAME, tool_declarations
from .runtime import default_runtime, require_supported_python
def dispatch(
name: str,
arguments: dict[str, Any],
runtime: ProjectContextRuntime,
) -> DispatchResult:
"""Validate → authorize → resolve provider → execute → validate output."""
correlation_id = str(uuid4())
tool = TOOLS_BY_NAME.get(name)
if tool is None:
return error_result(
"NOT_FOUND",
category="NOT_FOUND",
retryable=False,
message="The requested MCP tool is not registered.",
suggested_action="Refresh the tool list and choose one of the advertised tools.",
correlation_id=correlation_id,
)
try:
validated_input = tool.input_model.model_validate(arguments or {})
except ValidationError:
return error_result(
"INVALID_INPUT",
category="INVALID_INPUT",
retryable=False,
message="The tool arguments do not match the published input contract.",
suggested_action="Correct the required fields and value bounds, then call again.",
correlation_id=correlation_id,
)
project_id = str(validated_input.project_id)
if not runtime.policy.decide(runtime.identity, name, project_id):
return error_result(
"DENIED",
category="DENIED",
retryable=False,
message="The project is outside the caller's approved scope.",
suggested_action="Use an approved project or ask the project owner for access.",
correlation_id=correlation_id,
)
try:
provider = runtime.credential_resolver.resolve(runtime.identity, name)
raw_output = tool.handler(validated_input, provider)
except ProviderError as exc:
return error_result(
exc.code,
category=exc.code,
retryable=exc.retryable,
message=exc.safe_message,
suggested_action="Check the approved provider configuration and retry if allowed.",
correlation_id=correlation_id,
)
except Exception: # noqa: BLE001 - provider failures must not crash or leak into the agent turn
return error_result(
"UPSTREAM_ERROR",
category="UPSTREAM_ERROR",
retryable=False,
message="The approved provider could not complete the request.",
suggested_action="Check the correlation ID in server logs; do not resend credentials.",
correlation_id=correlation_id,
)
try:
output_with_trace = {**raw_output, "correlation_id": correlation_id}
validated_output = tool.output_model.model_validate(output_with_trace)
except ValidationError:
return error_result(
"UPSTREAM_ERROR",
category="UPSTREAM_ERROR",
retryable=False,
message="The provider response did not match the published output contract.",
suggested_action="Fix the provider mapping before retrying the request.",
correlation_id=correlation_id,
)
return DispatchResult(ok=True, payload=validated_output.model_dump(mode="json"))
def build_server(runtime: Optional[ProjectContextRuntime] = None):
from mcp import types
from mcp.server.lowlevel import Server
require_supported_python()
app_runtime = runtime or default_runtime()
app = Server("project_context")
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [types.Tool(**declaration) for declaration in tool_declarations()]
@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult:
result = dispatch(name, arguments or {}, app_runtime)
return types.CallToolResult(
content=[types.TextContent(
type="text",
text=json.dumps(result.payload, ensure_ascii=False, separators=(",", ":")),
)],
structuredContent=result.payload if result.ok else None,
isError=not result.ok,
)
return app
def main() -> None:
import anyio
from mcp.server.stdio import stdio_server
app = build_server()
async def _run() -> None:
async with stdio_server() as (read, write):
await app.run(read, write, app.create_initialization_options())
anyio.run(_run)
if __name__ == "__main__":
main()
@@ -0,0 +1 @@
"""Independent tool modules; ownership is documented in the team guide."""
@@ -0,0 +1,55 @@
"""Member C work package: get_project_change_context."""
# ruff: noqa: UP045 -- Optional keeps Pydantic model evaluation compatible with Python 3.9.
from __future__ import annotations
from typing import Any, Literal, Optional
from pydantic import Field
from ..foundation import ContractModel, SourceCitation, ToolTemplate
class ChangeContextInput(ContractModel):
project_id: str = Field(min_length=1, max_length=128)
change_id: str = Field(min_length=1, max_length=128)
detail: Literal["summary", "standard", "full"] = "standard"
cursor: Optional[str] = Field(default=None, max_length=2048)
class ChangeContextOutput(ContractModel):
correlation_id: str
project_id: str
change_id: str
change_type: Literal["commit", "pull-request", "merge-request"]
title: str
state: str
summary: str
authors: tuple[str, ...]
files: tuple[str, ...]
commits: tuple[str, ...]
related_issues: tuple[str, ...]
source: SourceCitation
truncated: bool
returned: int = Field(ge=0)
remaining: int = Field(ge=0)
next_cursor: Optional[str] = None
def _handle(arguments: ContractModel, provider: Any) -> dict[str, Any]:
request = ChangeContextInput.model_validate(arguments)
return provider.get_change_context(**request.model_dump())
TOOL = ToolTemplate(
name="get_project_change_context",
description=(
"Returns provider-neutral context for one authorized commit, pull request, or merge request "
"with changed files, commits, related issues, and a pinned source. Use when an exact change "
"identifier is known. Do not use for issue details or free-text document search."
),
input_model=ChangeContextInput,
output_model=ChangeContextOutput,
handler=_handle,
)
@@ -0,0 +1,59 @@
"""Member A work package: get_project_issue_context."""
# ruff: noqa: UP045 -- Optional keeps Pydantic model evaluation compatible with Python 3.9.
from __future__ import annotations
from typing import Any, Literal, Optional
from pydantic import Field
from ..foundation import ContractModel, SourceCitation, ToolTemplate
class IssueContextInput(ContractModel):
project_id: str = Field(min_length=1, max_length=128)
issue_key: str = Field(min_length=1, max_length=128)
detail: Literal["summary", "standard", "full"] = "standard"
cursor: Optional[str] = Field(default=None, max_length=2048)
class RelatedItem(ContractModel):
item_id: str
relation: str
title: str
url: str
class IssueContextOutput(ContractModel):
correlation_id: str
project_id: str
issue_key: str
title: str
status: str
description: str
acceptance_criteria: tuple[str, ...]
related: tuple[RelatedItem, ...]
source: SourceCitation
truncated: bool
returned: int = Field(ge=0)
remaining: int = Field(ge=0)
next_cursor: Optional[str] = None
def _handle(arguments: ContractModel, provider: Any) -> dict[str, Any]:
request = IssueContextInput.model_validate(arguments)
return provider.get_issue_context(**request.model_dump())
TOOL = ToolTemplate(
name="get_project_issue_context",
description=(
"Returns one authorized work item's title, state, description, acceptance criteria, "
"related items, and pinned source. Use when an exact issue key is known. Do not use for "
"free-text knowledge search or Git change review."
),
input_model=IssueContextInput,
output_model=IssueContextOutput,
handler=_handle,
)
@@ -0,0 +1,58 @@
"""Member B work package: search_project_knowledge."""
# ruff: noqa: UP045 -- Optional keeps Pydantic model evaluation compatible with Python 3.9.
from __future__ import annotations
from typing import Any, Literal, Optional
from pydantic import Field
from ..foundation import ContractModel, SourceCitation, ToolTemplate
class KnowledgeSearchInput(ContractModel):
project_id: str = Field(min_length=1, max_length=128)
query: str = Field(min_length=2, max_length=1000)
detail: Literal["summary", "standard", "full"] = "standard"
top_k: int = Field(default=5, ge=1, le=20)
language: Optional[Literal["en", "ja", "vi"]] = None
cursor: Optional[str] = Field(default=None, max_length=2048)
class KnowledgeItem(ContractModel):
document_id: str
chunk_id: str
title: str
excerpt: str
score: float = Field(ge=0, le=1)
source: SourceCitation
class KnowledgeSearchOutput(ContractModel):
correlation_id: str
project_id: str
query: str
items: tuple[KnowledgeItem, ...]
truncated: bool
returned: int = Field(ge=0)
remaining: int = Field(ge=0)
next_cursor: Optional[str] = None
def _handle(arguments: ContractModel, provider: Any) -> dict[str, Any]:
request = KnowledgeSearchInput.model_validate(arguments)
return provider.search_knowledge(**request.model_dump())
TOOL = ToolTemplate(
name="search_project_knowledge",
description=(
"Searches approved knowledge for one authorized project and returns ranked excerpts with "
"pinned citations. Use for requirements, design notes, or runbooks when no exact issue is "
"known. Do not use for issue details or Git change review."
),
input_model=KnowledgeSearchInput,
output_model=KnowledgeSearchOutput,
handler=_handle,
)
+9
View File
@@ -0,0 +1,9 @@
"""Stable module entry point for ``python -m cowork_local.mcp_servers.project_context_server``."""
from .project_context.server import build_server, dispatch, main
__all__ = ["build_server", "dispatch", "main"]
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
"""Presentation Layer: PySide6 UI widgets, dialogs, and shell views (<400 LOC per file)."""
+1
View File
@@ -0,0 +1 @@
"""Presentation chat package: ChatHistoryWidget, ComposerWidget, AttachmentPicker, AudioRecorderWidget, ChatOutputPanel."""
+1
View File
@@ -0,0 +1 @@
"""Presentation Co4E package: Co4ECanvasWidget, NodePropertyPanel, RunControlWidget, Co4EChatView."""
+1
View File
@@ -0,0 +1 @@
"""Presentation dashboard package: TokenUsageCardWidget, UsageChartWidget, HabitsWidget."""
+1
View File
@@ -0,0 +1 @@
"""Presentation folder package: WorkspaceFileTree, DocumentPreviewManager, AiFileEditorDialog."""
+1
View File
@@ -0,0 +1 @@
"""Presentation graph package: StructureGraphView and GraphQaWidget."""
+1
View File
@@ -0,0 +1 @@
"""Presentation monitoring package: 8 modular sub-tab widgets."""
+1
View File
@@ -0,0 +1 @@
"""Presentation scheduling package: KanbanBoardWidget, CalendarViewWidget, AiTaskCreatorDialog."""
+1
View File
@@ -0,0 +1 @@
"""Presentation settings package: Section widgets for provider, connector, routing, and general settings."""
+1
View File
@@ -0,0 +1 @@
"""Presentation shell package: MainWindow shell, TrayManager, LifecycleCoordinator."""
View File
-9
View File
@@ -1,9 +0,0 @@
PySide6>=6.6
pydantic>=2
requests
psutil
pygments
openpyxl
python-pptx
networkx
pytest
+166
View File
@@ -0,0 +1,166 @@
"""AST-based Static Analysis Guard for Clean Architecture Enforcement.
Scans designated Python packages (such as `domain/` and `application/`) to ensure
they remain 100% Pure Python and do not import presentation/GUI frameworks (PySide6, PyQt)
or concrete application shells.
"""
from __future__ import annotations
import argparse
import ast
import io
import sys
from pathlib import Path
from typing import List, NamedTuple, Set
# Ensure UTF-8 output on standard console streams across diverse Windows locales (CP932, etc.)
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
try:
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
except Exception:
pass
class ImportViolation(NamedTuple):
file_path: Path
line_number: int
imported_module: str
rule_description: str
# Disallowed top-level package names in pure business/domain layers
FORBIDDEN_MODULE_PREFIXES: Set[str] = {
"PySide6",
"PySide2",
"PyQt6",
"PyQt5",
"ui",
"app",
}
# Default directories that must strictly adhere to Clean Architecture
DEFAULT_SCAN_DIRS: List[str] = [
"domain",
"application",
]
class ArchitectureImportVisitor(ast.NodeVisitor):
"""AST visitor that checks all Import and ImportFrom statements against forbidden prefixes."""
def __init__(self, file_path: Path, forbidden: Set[str]) -> None:
self.file_path = file_path
self.forbidden = forbidden
self.violations: List[ImportViolation] = []
def visit_Import(self, node: ast.Import) -> None:
# Check direct `import x, y` statements
for alias in node.names:
root_module = alias.name.split(".")[0]
if root_module in self.forbidden:
self.violations.append(
ImportViolation(
file_path=self.file_path,
line_number=node.lineno,
imported_module=alias.name,
rule_description=f"Direct import of GUI/shell module '{alias.name}' is prohibited.",
)
)
self.generic_visit(node)
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
# Check `from x import y` statements
if node.module:
root_module = node.module.split(".")[0]
if root_module in self.forbidden:
self.violations.append(
ImportViolation(
file_path=self.file_path,
line_number=node.lineno,
imported_module=node.module,
rule_description=f"Import from GUI/shell module '{node.module}' is prohibited.",
)
)
self.generic_visit(node)
def scan_file(file_path: Path, forbidden: Set[str]) -> List[ImportViolation]:
"""Parse a single Python file into AST and return all detected architecture import violations."""
try:
source_code = file_path.read_text(encoding="utf-8")
tree = ast.parse(source_code, filename=str(file_path))
except (SyntaxError, UnicodeDecodeError) as exc:
print(f"[Syntax/Read Warning] Could not parse {file_path}: {exc}", file=sys.stderr)
return []
visitor = ArchitectureImportVisitor(file_path, forbidden)
visitor.visit(tree)
return visitor.violations
def scan_directory(dir_path: Path, forbidden: Set[str]) -> List[ImportViolation]:
"""Recursively scan all Python files in a directory."""
violations: List[ImportViolation] = []
if not dir_path.exists():
return violations
for py_file in dir_path.rglob("*.py"):
if py_file.is_file() and "__pycache__" not in py_file.parts:
violations.extend(scan_file(py_file, forbidden))
return violations
def main() -> int:
"""CLI entry point for CI/pre-commit quality gate checks."""
parser = argparse.ArgumentParser(
description="Clean Architecture Import Guard: Verifies zero GUI/Qt dependencies in domain/app layers."
)
parser.add_argument(
"--paths",
nargs="*",
default=DEFAULT_SCAN_DIRS,
help="Paths or directories to scan (defaults to 'domain' and 'application')",
)
parser.add_argument(
"--root",
default=".",
help="Root workspace directory",
)
args = parser.parse_args()
root_dir = Path(args.root).resolve()
all_violations: List[ImportViolation] = []
print(f"[Clean Arch Guard] Scanning root: {root_dir}")
for target in args.paths:
target_path = (root_dir / target).resolve()
if not target_path.exists():
# If the layer directory does not exist yet (during early migration), skip cleanly
print(f"[Clean Arch Guard] Directory '{target}' does not exist yet (skipped).")
continue
if target_path.is_file():
all_violations.extend(scan_file(target_path, FORBIDDEN_MODULE_PREFIXES))
else:
all_violations.extend(scan_directory(target_path, FORBIDDEN_MODULE_PREFIXES))
if all_violations:
print("\n[FAIL] CLEAN ARCHITECTURE VIOLATIONS DETECTED:")
print("=" * 70)
for v in all_violations:
rel_path = v.file_path.relative_to(root_dir) if v.file_path.is_relative_to(root_dir) else v.file_path
print(f" • {rel_path}:{v.line_number} -> Forbidden import: '{v.imported_module}'")
print(f" Reason: {v.rule_description}")
print("=" * 70)
print(f"Total Violations: {len(all_violations)}")
return 1
print("\n[PASS] CLEAN ARCHITECTURE CHECK: 0 forbidden imports detected.")
return 0
if __name__ == "__main__":
sys.exit(main())
+157
View File
@@ -0,0 +1,157 @@
"""Characterization tests for core/chat_agent.py (run_chat and run_cowork runtime seams).
These tests capture existing behavior as an executable baseline specification,
ensuring that future refactoring to ConversationApplicationService does not alter
core turn semantics, event emissions, or file handling.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List
from cowork_local.core import chat_agent
from cowork_local.tests.fakes.fake_provider import FakeProvider
def test_run_chat_characterization() -> None:
"""Capture baseline behavior of run_chat: system prompt insertion, streaming, and message persistence."""
provider = FakeProvider()
provider.queue_response(content="Hello there!", chunks=["Hello ", "there!"])
messages: List[Dict[str, Any]] = [{"role": "user", "content": "Hi assistant"}]
emitted_events: List[Dict[str, Any]] = []
def emit(event: Dict[str, Any]) -> None:
emitted_events.append(event)
result = chat_agent.run_chat(
provider=provider,
messages=messages,
emit=emit,
)
# 1. Verify system prompt was injected at position 0
assert messages[0]["role"] == "system"
assert "Cowork Local" in messages[0]["content"]
# 2. Verify returned assistant message
assert result["role"] == "assistant"
assert result["content"] == "Hello there!"
# 3. Verify assistant message was appended to messages list
assert messages[-1] == result
# 4. Verify emitted events sequence
text_deltas = [e["delta"] for e in emitted_events if e["type"] == "text"]
assert "".join(text_deltas) == "Hello there!"
assert any(e["type"] == "assistant_done" for e in emitted_events)
def test_run_cowork_save_file_characterization(tmp_path: Path) -> None:
"""Capture baseline behavior of run_cowork: tool execution loop and file production."""
output_dir = tmp_path / "output"
output_dir.mkdir(parents=True, exist_ok=True)
provider = FakeProvider()
# Step 1: Model requests save_file tool
provider.queue_response(
content="Saving your requested report.",
tool_calls=[{
"id": "call_save_1",
"name": "save_file",
"arguments": {
"filename": "report.md",
"content": "# Executive Summary\nAll systems nominal.",
},
}],
)
# Step 2: Model finishes after tool result
provider.queue_response(
content="I have created report.md in your output directory.",
chunks=["I have created report.md in your output directory."],
)
messages: List[Dict[str, Any]] = [{"role": "user", "content": "Export report to markdown file"}]
emitted_events: List[Dict[str, Any]] = []
def emit(event: Dict[str, Any]) -> None:
emitted_events.append(event)
final_messages = chat_agent.run_cowork(
provider=provider,
messages=messages,
output_dir=output_dir,
emit=emit,
enforce_rules=False,
)
# 1. Verify file was created in output directory with expected content
created_file = output_dir / "report.md"
assert created_file.exists()
assert created_file.read_text(encoding="utf-8") == "# Executive Summary\nAll systems nominal."
# 2. Verify message history contains user -> assistant (tool_calls) -> tool -> assistant
roles = [m["role"] for m in final_messages]
assert "system" in roles
assert "user" in roles
assert "tool" in roles
# 3. Verify tool result message content
tool_msg = next(m for m in final_messages if m["role"] == "tool")
assert tool_msg["name"] == "save_file"
assert "Saved report.md" in tool_msg["content"]
def test_run_cowork_cancellation_characterization(tmp_path: Path) -> None:
"""Capture cancellation behavior in run_cowork."""
output_dir = tmp_path / "output_cancel"
output_dir.mkdir(parents=True, exist_ok=True)
provider = FakeProvider()
provider.queue_response(content="Working...")
is_cancelled = True
def check_cancel() -> bool:
return is_cancelled
emitted_events: List[Dict[str, Any]] = []
messages: List[Dict[str, Any]] = [{"role": "user", "content": "Please start"}]
chat_agent.run_cowork(
provider=provider,
messages=messages,
output_dir=output_dir,
emit=lambda e: emitted_events.append(e),
cancel=check_cancel,
enforce_rules=False,
)
# Provider should not have executed turns if cancelled right away
assert provider.call_count == 0
def test_cleanup_turn_output_characterization(tmp_path: Path) -> None:
"""Capture behavior of temporary .scratch folder cleanup and artifact preservation."""
output_dir = tmp_path / "output_cleanup"
output_dir.mkdir(parents=True, exist_ok=True)
scratch_dir = output_dir / ".scratch"
scratch_dir.mkdir(parents=True, exist_ok=True)
# Create a generator script and a deliverable inside scratch
generator_script = scratch_dir / "gen.py"
generator_script.write_text("print('generating')", encoding="utf-8")
deliverable = scratch_dir / "data.csv"
deliverable.write_text("a,b,c\n1,2,3", encoding="utf-8")
before_snapshot = chat_agent._snapshot(output_dir)
removed, moved = chat_agent._cleanup_cowork_intermediates(output_dir, before_snapshot, cancelled=False)
# .scratch directory should be removed
assert not scratch_dir.exists()
# deliverable should be moved to output root
root_csv = output_dir / "data.csv"
assert root_csv.exists()
# script should not be in output root
assert not (output_dir / "gen.py").exists()
+10
View File
@@ -0,0 +1,10 @@
"""Make the repository package importable when pytest runs from the repo root."""
from __future__ import annotations
import sys
from pathlib import Path
REPOSITORY_PARENT = Path(__file__).resolve().parents[2]
if str(REPOSITORY_PARENT) not in sys.path:
sys.path.insert(0, str(REPOSITORY_PARENT))
+5
View File
@@ -0,0 +1,5 @@
"""Test doubles and offline fakes package for Cowork Local test pyramid."""
from .fake_provider import FakeProvider
from .fake_tool_executor import FakeToolExecutor
__all__ = ["FakeProvider", "FakeToolExecutor"]
+113
View File
@@ -0,0 +1,113 @@
"""Fake LLM Provider for offline unit, contract, and characterization testing.
Provides deterministic responses, stream simulation, tool-call dispatching,
and fault injection without requiring any external network access or API keys.
"""
from __future__ import annotations
from typing import Any, Callable, Dict, List, Optional
from providers.base import CancelFn, Provider, ProviderError, TextCallback, ToolSpec
class FakeProvider(Provider):
"""Deterministic test double mimicking real LLM Providers (OpenAI, Anthropic, Ollama)."""
name = "fake"
supports_vision = True
def __init__(self, conf: Optional[Dict[str, Any]] = None) -> None:
# Initialize base provider with default configuration if none provided
super().__init__(conf or {"model": "fake-model-v1"})
# History of all message batches sent across all chat calls
self.call_history: List[List[Dict[str, Any]]] = []
# Queue of programmed assistant responses to return sequentially
self.response_queue: List[Dict[str, Any]] = []
# Queue of exceptions to raise on corresponding calls
self.error_queue: List[Exception] = []
# Default text returned when response queue is empty
self.default_text: str = "Fake model response."
# Total number of chat invocations
self.call_count: int = 0
# Recorded tool specs passed into each turn
self.last_tools: Optional[List[ToolSpec]] = None
def queue_response(
self,
content: str = "",
tool_calls: Optional[List[Dict[str, Any]]] = None,
reasoning: Optional[str] = None,
chunks: Optional[List[str]] = None,
) -> FakeProvider:
"""Enqueue a pre-configured response structure for upcoming chat turns."""
self.response_queue.append({
"content": content,
"tool_calls": tool_calls or [],
"reasoning": reasoning,
"chunks": chunks or ([content] if content else []),
})
return self
def queue_error(self, exc: Exception) -> FakeProvider:
"""Enqueue an exception to simulate network/API errors on the next turn."""
self.error_queue.append(exc)
return self
def chat(
self,
messages: List[Dict[str, Any]],
tools: Optional[List[ToolSpec]] = None,
on_text: Optional[TextCallback] = None,
cancel: Optional[CancelFn] = None,
on_reasoning: Optional[TextCallback] = None,
) -> Dict[str, Any]:
"""Simulate single LLM turn with full streaming and tool-call support."""
self.call_count += 1
self.call_history.append([dict(m) for m in messages])
self.last_tools = tools
# 1. Check for injected errors
if self.error_queue:
raise self.error_queue.pop(0)
# 2. Check early cancellation before processing
if cancel and cancel():
raise ProviderError("Execution aborted by user cancel signal before response generation.")
# 3. Retrieve queued response or construct default response
if self.response_queue:
resp_spec = self.response_queue.pop(0)
content = resp_spec.get("content", "")
tool_calls = resp_spec.get("tool_calls", [])
reasoning = resp_spec.get("reasoning")
chunks = resp_spec.get("chunks", [content] if content else [])
else:
content = self.default_text
tool_calls = []
reasoning = None
chunks = [content]
# 4. Stream reasoning chunks if provided
if reasoning and on_reasoning:
on_reasoning(reasoning)
# 5. Stream text chunks, checking cancellation between fragments
for chunk in chunks:
if cancel and cancel():
raise ProviderError("Execution cancelled during text chunk streaming.")
if on_text and chunk:
on_text(chunk)
# 6. Return canonical assistant message payload
assistant_msg: Dict[str, Any] = {
"role": "assistant",
"content": content,
}
if tool_calls:
assistant_msg["tool_calls"] = tool_calls
return assistant_msg
def list_models(self) -> List[str]:
"""Return available mock models for settings and validation tests."""
return ["fake-model-v1", "fake-reasoner-pro", "fake-vision-plus"]
+71
View File
@@ -0,0 +1,71 @@
"""Fake Tool Executor for isolated, offline agent tool-call verification.
Allows tests to verify tool invocation arguments, mock tool return values,
and simulate failures/delays without performing unsafe host disk or OS operations.
"""
from __future__ import annotations
from typing import Any, Callable, Dict, List, Optional
class FakeToolExecutor:
"""Mock execution engine for agent tool-call dispatching."""
def __init__(self) -> None:
# History of all executed tool invocations: List of {"name": str, "args": dict, "result": dict}
self.call_log: List[Dict[str, Any]] = []
# Custom handlers registered per tool name
self.handlers: Dict[str, Callable[[Dict[str, Any]], Dict[str, Any]]] = {}
# Pre-programmed fixed responses keyed by tool name
self.mock_responses: Dict[str, Dict[str, Any]] = {}
# Default response when no specific handler or response is found
self.default_result: Dict[str, Any] = {"ok": True, "output": "Fake tool executed successfully."}
def register_handler(
self,
tool_name: str,
handler: Callable[[Dict[str, Any]], Dict[str, Any]],
) -> FakeToolExecutor:
"""Register a dynamic handler function for a specific tool name."""
self.handlers[tool_name] = handler
return self
def set_mock_response(
self,
tool_name: str,
result: Dict[str, Any],
) -> FakeToolExecutor:
"""Set a static return payload for a specific tool name."""
self.mock_responses[tool_name] = result
return self
def execute(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Execute a tool call using registered mocks and record invocation details."""
# 1. Resolve result from handler, preset response, or default fallback
if tool_name in self.handlers:
result = self.handlers[tool_name](arguments)
elif tool_name in self.mock_responses:
result = self.mock_responses[tool_name]
else:
result = dict(self.default_result)
result["tool"] = tool_name
result["received_args"] = arguments
# 2. Record execution trace for post-test assertions
self.call_log.append({
"name": tool_name,
"args": dict(arguments),
"result": dict(result),
})
return result
def get_calls_for(self, tool_name: str) -> List[Dict[str, Any]]:
"""Retrieve all recorded calls for a given tool name."""
return [call for call in self.call_log if call["name"] == tool_name]
def reset(self) -> None:
"""Clear recorded logs and registered mock responses."""
self.call_log.clear()
self.handlers.clear()
self.mock_responses.clear()
+245
View File
@@ -0,0 +1,245 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
import pytest
from cowork_local.mcp_servers.project_context.foundation import (
IdentityContext,
ProjectContextRuntime,
)
from cowork_local.mcp_servers.project_context.registry import (
TOOL_NAMES,
tool_declarations,
)
from cowork_local.mcp_servers.project_context.runtime import require_supported_python
from cowork_local.mcp_servers.project_context.server import dispatch
from mcp import types
EXPECTED_TOOLS = {
"get_project_issue_context",
"search_project_knowledge",
"get_project_change_context",
}
@dataclass
class RecordingPolicy:
allowed: bool
calls: int = 0
def decide(self, identity: IdentityContext, tool_name: str, project_id: str) -> bool:
self.calls += 1
return self.allowed
@dataclass
class RecordingResolver:
provider: Any
calls: int = 0
def resolve(self, identity: IdentityContext, tool_name: str) -> Any:
self.calls += 1
return self.provider
@dataclass(frozen=True)
class FakeProvider:
response: dict[str, Any]
def get_issue_context(self, **_: Any) -> dict[str, Any]:
return dict(self.response)
def search_knowledge(self, **_: Any) -> dict[str, Any]:
return dict(self.response)
def get_change_context(self, **_: Any) -> dict[str, Any]:
return dict(self.response)
@pytest.fixture
def identity() -> IdentityContext:
return IdentityContext(
actor_id="member-a",
org_unit="fsg",
customer="internal",
project="cowork-local",
granted_scopes=frozenset({"read"}),
)
def runtime(identity: IdentityContext, response: dict[str, Any], *, allowed: bool = True):
policy = RecordingPolicy(allowed=allowed)
resolver = RecordingResolver(provider=FakeProvider(response))
return ProjectContextRuntime(
identity=identity,
policy=policy,
credential_resolver=resolver,
), policy, resolver
def source() -> dict[str, str]:
return {
"system": "gitea",
"url": "http://example.test/gitea-admin/cowork-local/issues/1",
"revision": "main@abc123",
"retrieved_at": "2026-08-20T10:00:00Z",
}
def test_template_exposes_exactly_three_provider_neutral_tools() -> None:
assert set(TOOL_NAMES) == EXPECTED_TOOLS
declarations = tool_declarations()
assert {item["name"] for item in declarations} == EXPECTED_TOOLS
assert all(item["inputSchema"]["additionalProperties"] is False for item in declarations)
assert all(item["outputSchema"]["additionalProperties"] is False for item in declarations)
assert all(types.Tool(**item).name in EXPECTED_TOOLS for item in declarations)
def test_runtime_fails_fast_below_python_311() -> None:
with pytest.raises(RuntimeError, match="requires Python 3.11"):
require_supported_python((3, 9, 0))
def test_denied_request_never_resolves_credentials_or_calls_provider(
identity: IdentityContext,
) -> None:
app, policy, resolver = runtime(identity, {}, allowed=False)
result = dispatch(
"get_project_issue_context",
{"project_id": "other-project", "issue_key": "1"},
app,
)
assert result.ok is False
assert result.payload["error"]["code"] == "DENIED"
assert policy.calls == 1
assert resolver.calls == 0
def test_invalid_input_is_rejected_before_policy(identity: IdentityContext) -> None:
app, policy, resolver = runtime(identity, {})
result = dispatch("get_project_issue_context", {"project_id": "cowork-local"}, app)
assert result.ok is False
assert result.payload["error"]["code"] == "INVALID_INPUT"
assert policy.calls == 0
assert resolver.calls == 0
@pytest.mark.parametrize(
("tool_name", "arguments", "response"),
[
(
"get_project_issue_context",
{"project_id": "cowork-local", "issue_key": "1"},
{
"project_id": "cowork-local",
"issue_key": "1",
"title": "MCP pilot",
"status": "open",
"description": "Build verifiable project context.",
"acceptance_criteria": ["Every result has a source."],
"related": [],
"source": source(),
"truncated": False,
"returned": 1,
"remaining": 0,
"next_cursor": None,
},
),
(
"search_project_knowledge",
{"project_id": "cowork-local", "query": "MCP setup"},
{
"project_id": "cowork-local",
"query": "MCP setup",
"items": [
{
"document_id": "README.md",
"chunk_id": "README.md#setup",
"title": "Setup",
"excerpt": "Install the approved dependencies.",
"score": 0.9,
"source": source(),
}
],
"truncated": False,
"returned": 1,
"remaining": 0,
"next_cursor": None,
},
),
(
"get_project_change_context",
{"project_id": "cowork-local", "change_id": "1"},
{
"project_id": "cowork-local",
"change_id": "1",
"change_type": "pull-request",
"title": "Add MCP contract",
"state": "merged",
"summary": "Introduces the project context contract.",
"authors": ["member-c"],
"files": ["mcp/contract.yaml"],
"commits": ["abc123"],
"related_issues": ["1"],
"source": source(),
"truncated": False,
"returned": 1,
"remaining": 0,
"next_cursor": None,
},
),
],
)
def test_each_member_template_has_a_valid_success_path(
identity: IdentityContext,
tool_name: str,
arguments: dict[str, Any],
response: dict[str, Any],
) -> None:
app, policy, resolver = runtime(identity, response)
result = dispatch(tool_name, arguments, app)
assert result.ok is True
assert result.payload["project_id"] == "cowork-local"
assert result.payload["correlation_id"]
assert policy.calls == 1
assert resolver.calls == 1
def test_provider_output_must_match_contract(identity: IdentityContext) -> None:
app, _, _ = runtime(identity, {"project_id": "cowork-local"})
result = dispatch(
"get_project_issue_context",
{"project_id": "cowork-local", "issue_key": "1"},
app,
)
assert result.ok is False
assert result.payload["error"]["code"] == "UPSTREAM_ERROR"
def test_unexpected_provider_error_does_not_leak_exception(identity: IdentityContext) -> None:
class LeakingProvider:
def get_issue_context(self, **_: Any) -> dict[str, Any]:
raise RuntimeError("secret provider-token-value")
policy = RecordingPolicy(allowed=True)
resolver = RecordingResolver(provider=LeakingProvider())
app = ProjectContextRuntime(identity=identity, policy=policy, credential_resolver=resolver)
result = dispatch(
"get_project_issue_context",
{"project_id": "cowork-local", "issue_key": "1"},
app,
)
assert result.ok is False
assert result.payload["error"]["code"] == "UPSTREAM_ERROR"
assert "secret" not in str(result.payload)
+59
View File
@@ -0,0 +1,59 @@
"""Unit tests for the Clean Architecture AST Import Guard (check_imports.py)."""
from __future__ import annotations
from pathlib import Path
from scripts.check_imports import FORBIDDEN_MODULE_PREFIXES, scan_file
def test_clean_python_file_passes(tmp_path: Path) -> None:
"""Verify that pure Python code without GUI imports produces 0 violations."""
clean_code = """
import os
import json
from dataclasses import dataclass
from typing import List
@dataclass
class UserRequest:
id: str
prompt: str
"""
clean_file = tmp_path / "clean_service.py"
clean_file.write_text(clean_code, encoding="utf-8")
violations = scan_file(clean_file, FORBIDDEN_MODULE_PREFIXES)
assert len(violations) == 0
def test_forbidden_pyside_import_detected(tmp_path: Path) -> None:
"""Verify that PySide6 import is caught with correct line number."""
dirty_code = """
from dataclasses import dataclass
from PySide6.QtWidgets import QWidget
class BadService:
pass
"""
dirty_file = tmp_path / "bad_service.py"
dirty_file.write_text(dirty_code, encoding="utf-8")
violations = scan_file(dirty_file, FORBIDDEN_MODULE_PREFIXES)
assert len(violations) == 1
assert violations[0].line_number == 3
assert "PySide6" in violations[0].imported_module
def test_forbidden_ui_and_app_import_detected(tmp_path: Path) -> None:
"""Verify that importing concrete UI or app modules from domain is caught."""
dirty_code = """
import ui.chat_panel
from app import MainWindow
"""
dirty_file = tmp_path / "cross_layer_leak.py"
dirty_file.write_text(dirty_code, encoding="utf-8")
violations = scan_file(dirty_file, FORBIDDEN_MODULE_PREFIXES)
assert len(violations) == 2
modules = [v.imported_module for v in violations]
assert "ui.chat_panel" in modules
assert "app" in modules
+94
View File
@@ -0,0 +1,94 @@
"""Unit tests for FakeProvider and FakeToolExecutor test doubles."""
from __future__ import annotations
import pytest
from providers.base import ProviderError
from tests.fakes.fake_provider import FakeProvider
from tests.fakes.fake_tool_executor import FakeToolExecutor
def test_fake_provider_text_streaming() -> None:
"""Verify that FakeProvider streams text chunks to on_text callback."""
provider = FakeProvider()
provider.queue_response(content="Hello world", chunks=["Hello ", "world"])
streamed: list[str] = []
response = provider.chat(
messages=[{"role": "user", "content": "Hi"}],
on_text=lambda piece: streamed.append(piece),
)
assert response["role"] == "assistant"
assert response["content"] == "Hello world"
assert "".join(streamed) == "Hello world"
assert provider.call_count == 1
def test_fake_provider_tool_calls_and_reasoning() -> None:
"""Verify reasoning streaming and tool_calls payload emission."""
provider = FakeProvider()
tool_call = {
"id": "call_123",
"name": "save_file",
"arguments": {"filename": "out.txt", "content": "data"},
}
provider.queue_response(
content="Creating file",
tool_calls=[tool_call],
reasoning="User wants output in a file",
)
reasoning_chunks: list[str] = []
response = provider.chat(
messages=[{"role": "user", "content": "Save to out.txt"}],
on_reasoning=lambda piece: reasoning_chunks.append(piece),
)
assert response["content"] == "Creating file"
assert response["tool_calls"] == [tool_call]
assert reasoning_chunks == ["User wants output in a file"]
def test_fake_provider_error_injection() -> None:
"""Verify that queued exceptions are raised on demand."""
provider = FakeProvider()
provider.queue_error(ProviderError("Rate limit exceeded (429)"))
with pytest.raises(ProviderError, match="Rate limit exceeded"):
provider.chat(messages=[{"role": "user", "content": "Hi"}])
def test_fake_provider_cancellation() -> None:
"""Verify that cancellation stops execution immediately."""
provider = FakeProvider()
provider.queue_response(content="Long reply", chunks=["Part 1", "Part 2"])
is_cancelled = False
def cancel_fn() -> bool:
return is_cancelled
is_cancelled = True
with pytest.raises(ProviderError, match="aborted by user cancel"):
provider.chat(
messages=[{"role": "user", "content": "Hi"}],
cancel=cancel_fn,
)
def test_fake_tool_executor() -> None:
"""Verify that FakeToolExecutor records calls and returns expected mock outputs."""
executor = FakeToolExecutor()
executor.set_mock_response("read_file", {"ok": True, "content": "file contents"})
executor.register_handler("calc", lambda args: {"ok": True, "result": args.get("a", 0) + args.get("b", 0)})
res1 = executor.execute("read_file", {"path": "test.txt"})
assert res1["ok"] is True
assert res1["content"] == "file contents"
res2 = executor.execute("calc", {"a": 5, "b": 10})
assert res2["result"] == 15
assert len(executor.call_log) == 2
assert executor.get_calls_for("calc")[0]["args"] == {"a": 5, "b": 10}