# FR-048 Phase 2c 交接 prompt（B 類收編：7 份私有 _check_* 委派 canonical）

> 下面整段是給執行 session 的 prompt。**前置：Phase 2b 與 2b-2 都已收完**（2c 與它們動同一批 `app/grc/service/` 檔案，必須序列執行；2b-2 見同目錄 `2026-07-07-phase2b2-remainder-prompt.md`）。未收完就停下回報。

---

【接手主題】FR-048 統一授權守門 Phase 2c — `common/authz/project.py` 泛化 `assert_project_role`，grc 7 份私有 `_check_*` 複製品改為委派（**行為保真、零行為變更**）

必讀：
1. `docs/features/FR-048-2607-unified-auth-guard/endpoint-authz-matrix.md` §4 B 類（本波 scope 定義）
2. `docs/analysis/2026-07-07-unified-auth-guard-design.md` §2 軸模型
3. `common/authz/project.py` 現況

【branch】fix/v1.8.0-bugs，禁止切 branch。不動 jedi-* 套件。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【本波鐵則】

- **行為保真是唯一目的**：這是重複消除，不是行為修正。任何 403/404 行為、error code、判定順序都必須跟收編前一模一樣。
- 發現語意不一致（見下方差異表的 audit_service 案）→ **標註回報，不修**。

【7 份複製品與語意差異表（2026-07-07 實查，執行前先驗證仍準確——2b 可能又動過這些檔）】

| # | 檔:行 | method | 入口 | 判定 | 特殊點 |
|---|------|--------|------|------|--------|
| 1 | `app/grc/service/audit_service.py:27` | `_check_role(project_uid, user_id, required_role)` | uid→resolve+404 | **exact match，無 manager override** | error_map 按 required_role 選碼 |
| 2 | `app/grc/service/poam_app_service.py:91` | `_check_manager(project_id, user_id)` | id | manager-only | |
| 3 | `app/grc/service/audit_round_app_service.py:99` | `_check_role(project_id, user_id, required_role)` | id | required_role **含 manager override** | 錯誤碼按 required_role 選 NOT_MANAGER/NOT_AUDITOR |
| 4 | `app/grc/service/job_import_service.py:81` | `_check_manager_role(project_uid, user_id)` | uid→resolve+404 | manager-only | |
| 5 | `app/grc/service/task_execution_service.py:47` | `_check_manager(project_uid, user_id)` | uid→resolve+404 | manager-only | **回傳 project entity** |
| 6 | `app/grc/service/assessment_plan_app_service.py:230` | `_check_auditor(project_id, user_id)` | id | auditor+manager override | |
| 7 | `app/grc/service/assessment_result_app_service.py:111` | `_check_auditor(project_id, user_id)` | id | auditor+manager override | |

**⚠️ 查法差異（不可混）**：這 7 份都是「project 層 participant 精確查詢」（`participant_domain_service.get_one(ProjectParticipantQueryEntity(project_id, user_id))`）。既有 canonical `assert_project_manager` 走的是 `role_service.get_user_role`（control→group→project **由下往上 fallback**）——兩者語意不同，**不能把 7 份改成走 assert_project_manager**。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【要做的事】

### 1. `common/authz/project.py` 新增泛化 canonical

```python
def assert_project_role(
    participant_domain_service,
    project_id: int,
    user_id: int,
    allowed_roles: tuple = ("manager",),
    error_code=None,
) -> None:
    """Project 層 participant 精確查詢守門（不含 group/control fallback——那是
    assert_project_manager 的軸）。participant 為 None 或 role 不在 allowed_roles → ForbiddenError。

    error_code 未給時按 allowed_roles 選預設：含 "auditor" → GRC_NOT_AUDITOR，否則 GRC_NOT_MANAGER。
    manager override 由 caller 用 allowed_roles=("auditor", "manager") 表達，本函式不隱含 override。
    caller 必須在 @transaction scope 內。
    """
```

- docstring 必須寫明與 `assert_project_manager`（fallback 查法）的分工，兩支並列在 `__init__.py` 決策表。
- **uid→project resolve＋404 不收進 authz**（那是業務前置條件不是授權），caller 保留自己那兩行；task_execution 本來就要 project entity，維持原樣。

### 2. 7 份逐一改為薄殼委派（保留原方法名，call site 全不動）

```python
def _check_manager(self, project_id: int, user_id: int) -> None:
    # FR-048 2c：委派 canonical，語意同前（project 層精確查詢、manager-only）
    assert_project_role(self._participant_domain_service, project_id, user_id)
```

- 各檔對映：#2/#4/#5 → `allowed_roles=("manager",)`；#6/#7 → `("auditor", "manager")`；#3 → required_role=="manager" 時 `("manager",)`、=="auditor" 時 `("auditor", "manager")`（保住 override 語意與錯誤碼選擇）；#1 見下。
- 改完每檔跑該檔相關測試，再全套。

### 3. audit_service（#1）特殊處理——標註不改行為

#1 的 exact match **沒有 manager override**，與 #3/#6/#7 不同調（同是 required_role 語意）。這可能是 bug 也可能刻意：
- 收編時用 `allowed_roles=(required_role,)` **保住無 override 的現行為**；
- 在該處加註解標明「與 audit_round 的 override 語意不一致，待 user 定調是否對齊」，並在收尾回報清單裡列出（含受影響端點）。**不要自作主張加 override**。

### 4. 驗證（做完必跑）

1. `pytest test/`（用 `python -m pytest`）全綠比對：**baseline worktree 集合 diff**（P3 review 用過的方法）——在 2c 前 commit 開 worktree 跑同套件，`comm` 比對 FAILED/ERROR 清單，新增失敗必須為 0。
2. grep 確認 7 份方法體都只剩委派一行＋註解，無殘留複製邏輯。
3. 矩陣 §4 B 類狀態改「已收編」＋ commit hash。

【commit】一個 commit 即可（收編性質單一）：`refactor(FR-048): grc 7 份私有 _check_* 收編為 common.authz assert_project_role 委派（行為保真）`。顯式 git add、禁 -am、不 push。

【收尾】commit 完停下，回報：① 一句話 status；② audit_service override 不一致案（等 user 定調）；③ 若 2b 期間又長出新的私有 check helper，列出來。收尾動作等 user 下令。
