merge: hoàn tất merge origin/feature/teamhoa/r05-r06 vào feature/delta-team/epic-R04

Resolve 3 file conflict:
- docs/refactor/Refactoring_Checklist.md: giữ nội dung incoming (phía HEAD
  trống ở đoạn conflict).
- tests/integration/test_routing_surfaces.py: khôi phục từ incoming (bị mất
  ở merge trước đó), điều chỉnh lại cho khớp API hiện tại của
  RoutingApplicationService (resolve()/RouteEvaluation/mode_resolver thay vì
  route_turn()/mode_reader cũ), bỏ 2 test pin một lớp RoutingDecision không
  còn tồn tại trên nhánh này.
- ui/folder_tab.py: chấp nhận xoá (deleted by them) — đã được thay thế hoàn
  toàn bởi presentation/folder/* (R08-T12), không còn nơi nào import module
  cũ.

Sửa thêm 2 chỗ lệch API bị auto-merge không báo conflict (phát hiện khi chạy
lại test):
- presentation/folder/ai_edit_model_resolver.py + ai_file_editor_dialog.py:
  AiEditModelResolver.apply_routing() gọi route_turn() đã bị xoá khỏi
  RoutingApplicationService — chuyển sang build_routing_application_service()
  .resolve(RoutingRequest(...)) giống chat_panel.py/co4e_chat.py; sửa luôn
  chữ ký _confirm_routing_switch nhận thêm timeout cho khớp contract confirm
  mới.
- config.py: import JsonConfigRepository ở đầu file gây circular import với
  core/tasks.py (cần CONFIG_DIR) qua chuỗi mới
  infrastructure/persistence/json/task_repository_impl.py (R07). Dời import
  xuống ngay trước chỗ dùng đầu tiên.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 11:40:44 +09:00
co-authored by Claude Sonnet 5
73 changed files with 7674 additions and 4174 deletions
+4 -1
View File
@@ -1,8 +1,10 @@
"""JSON-file persistence adapters: crash-safe writes, AtomicJsonFile and repositories."""
"""JSON-file persistence adapters: crash-safe writes and the workspace/
conversation/task repositories built on them (EPIC R06, R07)."""
from .atomic_json_file import AtomicJsonFile
from .atomic_write import write_json
from .conversation_repository_impl import ConversationRepository
from .task_repository_impl import TaskRepository
from .workspace_repository_impl import WorkspaceRepository
__all__ = [
@@ -10,4 +12,5 @@ __all__ = [
"write_json",
"WorkspaceRepository",
"ConversationRepository",
"TaskRepository",
]
@@ -0,0 +1,63 @@
"""TaskRepository - an object-shaped, atomic-write-backed facade over
``core/tasks.py`` (R07-T01).
``core/tasks.py``'s module-level functions (``list_tasks``, ``load_task``,
``save_task``, ``delete_task``, ``new_task``, ``duplicate_task``) are still
what every existing call site (``core/task_scheduler.py``,
``core/task_executors.py``, ``ui/schedule_task_tab.py``) uses, and stay that
way - ``save_task`` now writes through :func:`atomic_write.write_json`
itself (R07-T01, same class of durability fix already applied to
``core/projects.py``/``core/history.py`` at R06-T02), so the fix applies
whether or not a caller ever touches this class.
This repository exists for the application layer
(``application/scheduling``, R07-T04) to depend on an interface instead of
reaching into ``core/`` directly. It is a thin pass-through today, not a
re-implementation: same on-disk format, same directory, same functions
underneath.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List, Optional
from cowork_local.core.tasks import (
TASKS_DIR,
delete_task,
duplicate_task,
list_tasks,
load_task,
new_task,
save_task,
)
class TaskRepository:
"""CRUD over task dicts (see ``core/tasks.py::DEFAULT_TASK`` for shape),
scoped to one ``directory`` (defaults to the app's real ``TASKS_DIR``;
tests pass a ``tmp_path`` so nothing touches the user's real config
folder)."""
def __init__(self, directory: Optional[Path] = None) -> None:
self._directory = directory or TASKS_DIR
def list(self) -> List[Dict[str, Any]]:
return list_tasks(self._directory)
def get(self, task_id: str) -> Optional[Dict[str, Any]]:
return load_task(task_id, self._directory)
def save(self, task: Dict[str, Any]) -> Path:
return save_task(task, self._directory)
def create(self, title: str = "", **overrides: Any) -> Dict[str, Any]:
return new_task(title, **overrides)
def duplicate(self, task: Dict[str, Any]) -> Dict[str, Any]:
return duplicate_task(task)
def delete(self, task_id: str) -> None:
delete_task(task_id, self._directory)
__all__ = ["TaskRepository"]