---
title: "專案角色併入 RBAC — 需求討論稿 (FR-155)"
brand: "Guidant AI · **FR-155** 專案角色併入 RBAC"
eyebrow: "FR-155 · Project Role → RBAC — 需求討論稿 · 2026-10-21 · 待決策者裁 D1–D10"
h1: "專案角色併入 RBAC"
subtitle: "角色管理分「系統角色」「專案角色」兩個 tab，專案參與者改讀專案角色表"
lede: "專案參與者今天是四個寫死的英文字（manager／reviewer／auditor／viewer），散在三個 repo 約一百多處，連流程圖的 XML 裡都有；系統角色卻是管理員自己建、自己勾能力點，兩邊沒有關係。本案把專案角色也變成「可建、可勾」：角色管理頁分系統／專案兩個 tab，各有各的能力點；專案參與者仍每個專案挑人，但下拉從專案角色表讀；程式裡所有「他是不是 manager」改成「他在這個專案的角色有沒有這個能力點」。同一人 A 專案負責人、B 專案瀏覽者照樣做得到。"
chips: [
  {text: "已拍板：角色頁分兩個 tab、專案角色有自己的能力點、參與者仍每專案指派", kind: ok},
  {text: "D1–D10 待裁", kind: accent},
  {text: "雛形：prototype/index.html（三頁可點）", kind: accent},
  {text: "跨 BE／FE／6 支 jedi 套件、五張參與者表、流程圖 XML", kind: crit}
]
footer: "FR-155 · 專案角色併入 RBAC — 需求討論稿 · 2026-10-21 · 盤點來源：FR-132 inventory/02-project-roles-and-flow.md（專案角色與流程）、本 session 重新 grep BE／FE · 前作：FR-132 存取管理（FR-B 預告）、FR-048 統一授權守門"
---

## 需求背景與目標 {#why nav="背景"}

### 現況：兩套角色，其實是三個地方在寫死

| 層 | 現況 | 寫死在哪 |
|---|---|---|
| **系統角色** | `roles` 表＋`role_capability_patterns`（萬用字元）＋`user_roles`（租戶／部門範圍、起訖日）。管理員在「存取管理」頁自己建角色、勾能力點 | 無，FR-132 已做成資料驅動 |
| **專案角色** | `ParticipantRole` 四值 StrEnum（manager／reviewer／auditor／viewer），存五張參與者表的 `role` 文字欄 | BE 主專案 28 支檔、6 支 jedi 套件、FE 約 30 支檔 |
| **流程管理** | BPMN UserTask 的 `main_role` 擴充屬性寫在 XML 裡；`stage_objects.default_main_roles` 當預設 | 設計器範本 58 處、進行中實例快照約 4,400 處（DEV）、FE 流程編輯器選項寫死四值 |

專案角色寫死的樣態分五類，修法各不同：

| 樣態 | 例子 | 數量（BE 主專案） |
|---|---|---|
| 守門參數 | `allowed_roles=("auditor", "manager")`、`ParticipantRole.MANAGER` | 約 70 處呼叫（大多已走 `common/authz/project.py` 的四支 canonical 函式） |
| 原生 SQL 讀模型 | `pp.role IN ('auditor','manager')` | 8 支 `infra/readmodel/` 查詢（我的稽核待辦、改善計畫儀表板、結案報告清單、到期提醒、偵測通知…） |
| 通知收件人 | `stage_entered_recipients.py` 自訂 `MANAGER = "manager"`；批次完成通知、偵測通知各寫一遍 | 5 處 |
| 序列化與驗證 | API serializer 的 `OneOf([...])`、Excel 匯入驗證、套件 serializer | 4 份清單各自維護 |
| 業務語意 | 建立者自動 manager、manager 可推進任何階段、auditor 才有 `audit_scope`（內稽／外稽） | 散在 service |

另外 enum 有兩份相同複本（主專案 `common/enum/participant_enum.py` 與 jedi_task_platform），FE 的角色標籤與顏色對照是第三份（`ParticipantPicker.vue`、`ProjectAuditorOverview.vue`、`FlowTemplateEditorView.vue` 各一份）。

### 為什麼不能只把四個值搬進 system_menus

專案角色的四個值帶程式語意：流程引擎靠 `auditor` 決定誰能推進稽核階段，通知靠 `reviewer` 決定寄給誰。純粹把值搬進字典表，解決的是標籤與排序，解決不了「兩套角色」——管理員多加一個「資深稽核員」，程式不知道他該收哪種通知、能推哪個階段。

### 決策者已拍板的方向（2026-10-21 對話）

> 專案參與人員的設定還是要有，只是不給角色了，權限就是交給 RBAC 那邊的能力點處理。問完 PM，同一個人在不同專案要能有不同身分，所以角色管理的介面分兩個 tab，一個管系統、一個管專案，專案有專案的能力點設置，系統有系統的能力點設置。
> [— 決策者（2026-10-21）]{.attr}

### 本案要做的三件事

| # | 做什麼 | 白話 |
|---|---|---|
| ① | **角色分兩種** | 角色管理頁分「系統角色」「專案角色」兩個 tab；專案角色勾的是「進了專案之後能做什麼」的能力點（管參與者、審核、稽核判定、推進階段、填答、看報告） |
| ② | **參與者指角色、不指字串** | 專案參與者仍每個專案挑人，但下拉從專案角色表讀；出貨內建四個範例（負責人／審核者／稽核員／瀏覽者）對應今天四個值，升級時舊資料自動對過去 |
| ③ | **判定改查能力點** | 守門、通知收件人、流程推進、待辦清單全部改成「他在這個專案的角色展開後有沒有這個能力點」，不再比對 `'manager'` |

## 分工概述（30 秒版） {#overview nav="分工概述"}

**一句話**：先把散掉的四個字收成一份並定出專案能力點清單（B.1），再改資料表與升級腳本（B.2），改判定與通知（B.3），最後畫面（B.4）與流程管理（B.5）。看得見的畫面排在 B.4 不是因為它不重要，是它要等 B.2 的表才有東西可讀；雛形先給你看，排序以你何時看得到畫面為準。

| 階段 | 做什麼（白話） | 產出 | 完成怎麼判定（決策者可親手檢查） | 依賴 |
|---|---|---|---|---|
| **B.1 收斂與清單** | 四個字只留一份定義，其他地方都去讀它；定出專案能力點清單（每個「專案內按鈕」對到一個能力點）；對照表「今天四個角色各自有哪些能力點」 | 能力點對照表、enum 單一來源、守衛測試擋字面值 | 跑 `pytest test/test_project_role_literal_guard.py` 綠；打開對照表每個專案內的動作都有一列、四個範例角色每個都看得出勾了什麼 | D1、D2 |
| **B.2 資料地基** | `roles` 加種類欄（系統／專案）；專案能力點標 `project` 範圍；五張參與者表 `role` 改指角色；升級腳本替每個租戶建四個範例專案角色、把舊資料對過去 | migration、seed、拿真實舊庫跑過的升級排練紀錄 | 拿一份客戶舊庫升級後，開任一專案的參與者頁，每個人的角色名跟升級前一樣；用舊帳號登入能按的按鈕跟升級前一模一樣 | B.1、D3、D4 |
| **B.3 判定與通知** | 守門四支函式改查專案能力點；8 支 SQL 讀模型、5 處通知改同一個查法；建立者自動指派改讀「預設負責人角色」 | 判定層新版、讀模型改寫 | 建一個「只能看、不能審核」的自訂專案角色指派給某人：他進專案看得到，按審核回 403，批次完成通知不會寄給他；把審核能力點勾回去，立刻能按、會收信 | B.2、D5、D6 |
| **B.4 畫面** | 角色頁兩個 tab；專案角色詳情頁的能力點矩陣只列專案能力點；參與者下拉讀表；專案內按鈕改讀「我在這個專案的能力點」 | FE 四處改動 | 對照雛形三頁；在專案角色 tab 新增「資深稽核員」勾三個能力點，到任一專案參與者下拉看得到它，指派後該人進專案只看得到那三個動作的按鈕 | B.3、D7 |
| **B.5 流程管理** | 流程圖 UserTask 的「主要角色」改成選能力點；`stage_objects.default_main_roles` 改存能力點；舊 XML 讀取時換算 | 流程編輯器改動、換算層 | 開流程編輯器點一個任務節點，「主要角色」下拉列的是能力點不是四個字；進行中的舊專案推進階段行為不變 | B.3、D8 |

B.1→B.2→B.3 必須照順序；B.4 與 B.5 可以平行。

## 已定案事項 {#decided nav="已定案"}

::: {.callout .decided}
**✅ 決策者 2026-10-21 已拍板**

- **角色管理頁分兩個 tab**：系統角色（今天那一套，不動）、專案角色（新）。
- **專案角色有自己的能力點**：管的是「進了專案之後能做什麼」。
- **專案參與者設定保留**：還是每個專案挑人，下拉從專案角色表讀；同一人不同專案可以不同角色（PM 確認需要）。
- **判定改查能力點**：所有「他是不是 manager」改成「他在這個專案的角色有沒有這個能力點」。
:::

## 待決策 D1–D10 {#pending nav="待決策"}

::: {.callout .pending}
**D1 專案能力點的命名與粒度**

今天系統能力點是三段式 `<模組>.<資源>.<動詞>`（例 `grc.project.update`），FR-132 已把 `grc.*` 38 個做出來，但那是「租戶層能不能碰這類東西」。專案內的動作要另一組名字，不能跟 `grc.*` 混用，否則「有 `grc.audit.approve` 的系統角色」會被誤判成「每個專案都能審核」。

選項：
- **(a) 新模組前綴 `proj.*`**：例 `proj.participant.manage`、`proj.review.approve`、`proj.audit.judge`、`proj.stage.advance`、`proj.survey.answer`、`proj.report.read`。與 `grc.*` 一眼分得開，能力點表加 `assignable_scopes=['project']` 就是專案能力點。
- (b) 沿用 `grc.*` 但 `assignable_scopes` 標 project：名字相同、靠範圍欄分，畫面與 grep 都容易混。

[**建議 (a)**。粒度照今天四個角色實際差異切，初版約 12～15 個，清單見第 5 節草案；B.1 盤點後定稿。]{.rec}
:::

::: {.callout .pending}
**D2 出貨範例專案角色與對應**

升級時舊資料的四個字要對到角色，所以每個租戶至少要有四個專案角色存在。

選項：
- **(a) 四個範例角色可改可刪，但有「預設負責人」旗標**：負責人／審核者／稽核員／瀏覽者各一，客戶可改名、可改能力點、可加新角色；其中一個標為「預設負責人」（建專案時建立者自動指派、manager 全域覆寫用），不可刪。
- (b) 四個全鎖不可改：客戶要自訂只能複製。

[**建議 (a)**，與 FR-132 D6 的精神一致（系統管理員鎖、範例可改）。]{.rec}
:::

::: {.callout .pending}
**D3 參與者表怎麼改**

五張參與者表（`project_participants`、`project_group_participants`、`project_control_participants`、`control_group_participants`、`process_participants`）的 `role` 都是 `varchar` 文字欄。

選項：
- **(a) 新增 `project_role_id` FK 指 `roles.id`，舊 `role` 欄並存一版後退役**：升級腳本把 `role='manager'` 對到該租戶的「負責人」角色 id；舊欄留著，回滾不必動資料。與 FR-132 D2 同手法。
- (b) 直接改 `role` 欄語意為角色 uid：回滾要動資料。

[**建議 (a)**。`control_group_participants` 與 `project_group_participants` 同形，B.1 順便確認是否其一已無人用。]{.rec}
:::

::: {.callout .pending}
**D4 系統管理員進專案算什麼**

系統角色 `*.*.*` 是租戶內全權，但專案能力點是另一組名字（D1 a），`*.*.*` 展開後會不會包含 `proj.*`？

選項：
- **(a) 不包含**：系統管理員要在專案裡做事，一樣要被加成參與者、指派專案角色。不是參與者就看不到專案（與今天相同）。通知不會因為他是管理員就每封都收。
- (b) 包含：管理員進任何專案都是全權，每種通知都收，稽核員清單會列他。

[**建議 (a)**。判定時 `proj.*` 只從「這個專案的參與者指派」展開，不從 `user_roles` 展開；程式上就是兩個入口各查各的。]{.rec}
:::

::: {.callout .pending}
**D5 群組層、控制項層的回退規則**

今天 `get_user_role` 由下往上找：控制項層有指派用它，沒有找群組層，再沒有用專案層，回傳第一個找到的角色。

選項：
- **(a) 規則不變，比對對象換成能力點**：由下往上找到第一筆指派，展開它的角色，看有沒有要求的能力點。
- (b) 改成聯集：三層指派的角色全部展開取聯集。

[**建議 (a)**，行為與今天一致，風險最低；(b) 會讓「控制項層降級成只能看」做不到。]{.rec}
:::

::: {.callout .pending}
**D6 稽核範圍（內稽／外稽）跟著誰**

`project_participants.audit_scope`（both／internal／external）今天只對 auditor 有意義：通知與可推進階段會比對稽核性質。

選項：
- **(a) 欄位留在參與者列，判定時只對「有稽核判定能力點」的參與者生效**：不綁角色名，綁能力點。
- (b) 搬到角色上：角色定義「只做內稽」，同一人兩個專案要不同範圍就要兩個角色。

[**建議 (a)**。]{.rec}
:::

::: {.callout .pending}
**D7 專案內按鈕的前端判定**

今天 FE 專案頁用 `p.role === 'manager'` 判按鈕（`ProjectPlanningView.vue` 9 處、`ProjectAuditorOverview.vue` 6 處…）。

選項：
- **(a) 專案詳情 API 回「我在這個專案的能力點清單」（`my_capabilities`），FE 新增 `v-can-project="PROJ.X"` 指令**：與 FR-132 A.6 的 `v-can` 同形，權限名由 BE 產常數檔。
- (b) FE 自己拉角色定義展開。

[**建議 (a)**。回退規則在 BE 算好，FE 不重算。]{.rec}
:::

::: {.callout .pending}
**D8 流程圖 `main_role` 怎麼翻**

BPMN UserTask 的 `main_role` 今天存角色名（`auditor`），進行中實例有約 4,400 份 XML 快照。客戶改了角色名字、或加了新角色，角色名不能再當程式依據。

選項：
- **(a) `main_role` 改存能力點名（例 `proj.audit.judge`），舊值讀取時換算**：`_resolve_main_roles` 遇到舊四值就對到對應能力點；設計器範本與 `stage_objects.default_main_roles` 用 migration 改成能力點；實例 XML 不碰。
- (b) 改存角色 uid：客戶刪角色流程就壞。
- (c) 全部 XML 批次改寫。

[**建議 (a)**。判定變成「使用者在這個專案的能力點是否包含 UserTask 要求的能力點」；manager 全域覆寫改成「有 `proj.stage.advance-any` 能力點」。]{.rec}
:::

::: {.callout .pending}
**D9 jedi 套件的改法**

守門 canonical 在主專案 `common/authz/project.py`，6 支套件透過 adapter 轉發，但套件內 serializer 與 enum 也寫死四值（jedi_task_platform、jedi_compliance_audit、jedi_survey、jedi_evidence_classification、jedi_detection、jedi_flow_engine）。

選項：
- **(a) 參與者表、enum、serializer 的 owner 是 jedi_task_platform，改在套件；其他五支只改呼叫端參數（角色 tuple → 能力點名）**：走 poetry path dependency 開發，完成後一次發版。
- (b) 主專案 monkeypatch：第二套真相。

[**建議 (a)**。套件異動照 CLAUDE.md 規範先列清單等你點頭，B.1 盤點後給。]{.rec}
:::

::: {.callout .pending}
**D10 分版**

整案橫跨三 repo、六支套件、五張表、約 4,400 份 XML。

選項：
- **(a) 兩版**：第一版 B.1＋B.2＋B.3＋B.4（角色頁兩個 tab、參與者讀表、判定改能力點），流程圖 `main_role` 第一版只做讀取換算不改編輯器；第二版 B.5 編輯器改選能力點。
- (b) 一版全做。

[**建議 (a)**。第一版完成客戶就能自訂專案角色；流程編輯器改選項是錦上添花。]{.rec}
:::

## 做出來長什麼樣 {#ui nav="畫面"}

三頁雛形在 [prototype/index.html](prototype/index.html)，純前端假資料，用現有設計系統刻。

| 頁 | 入口 | 新增／改變 |
|---|---|---|
| 1 角色管理（兩個 tab） | 系統設定 ／ 存取管理 ／ 角色 | 頁頂多「系統角色｜專案角色」兩個 tab；專案角色 tab 列四個範例角色＋自訂角色，欄位：名稱、說明、能力點數、使用中專案數、預設負責人標記 |
| 2 專案角色詳情 | 點任一專案角色 | 與系統角色詳情同形（基本資料卡、能力點矩陣、儲存列），但矩陣只列 `proj.*`、沒有「可見選單預覽」、多一個「預設負責人」開關 |
| 3 專案參與者 | 專案 ／ 設定 ／ 參與者 | 角色下拉從專案角色表讀，含自訂角色；稽核員才顯示稽核範圍欄；下拉旁一行「管理專案角色」連去頁 1 |

### 專案能力點清單草案（D1 定稿後 B.1 盤點修正）

| 能力點 | 白話 | 負責人 | 審核者 | 稽核員 | 瀏覽者 |
|---|---|---|---|---|---|
| `proj.project.read` | 看專案 | ✓ | ✓ | ✓ | ✓ |
| `proj.project.update` | 改專案設定、刪專案 | ✓ | | | |
| `proj.participant.manage` | 增刪參與者、指派任務 | ✓ | | | |
| `proj.job.execute` | 執行任務、上傳佐證、填答 | ✓ | | | |
| `proj.job.manage` | 增刪改任務、批次完成不限指派 | ✓ | | | |
| `proj.review.approve` | 標記已審閱、退回 | ✓ | ✓ | | |
| `proj.audit.judge` | 稽核判定、AP 編輯、AO 結果 | ✓ | | ✓ | |
| `proj.stage.advance` | 推進自己負責的階段 | ✓ | ✓ | ✓ | |
| `proj.stage.advance-any` | 推進任何階段、強制推進 | ✓ | | | |
| `proj.poam.manage` | 改善計畫建立與結案 | ✓ | | | |
| `proj.report.read` | 看結案報告、匯出 | ✓ | | ✓ | |
| `proj.report.manage` | 建立與發布結案報告 | ✓ | | | |
| `proj.evidence.manage` | 證據分類批次操作 | ✓ | | | |
| `proj.ssp.update` | SSP 各區塊寫入 | ✓ | | | |

通知收件人規則跟著能力點：「請審閱」寄給有 `proj.review.approve` 的人；「稽核待辦」列有 `proj.audit.judge` 的人；階段進站通知寄給有該站要求能力點的人，加上有 `proj.stage.advance-any` 的人。

## 資料模型草案 {#data nav="資料模型"}

```{.mermaid cap="圖 1 — 專案角色併入後的資料關係（粗線是本案新增）"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  subgraph sys[系統層（今天就有）]
    U[users] --> UR[user_roles<br/>租戶／部門範圍]
    UR --> R[roles<br/>＋ kind: system / project<br/>＋ is_default_owner]
    R --> RP[role_capability_patterns]
    RP --> C[capabilities<br/>grc.* assignable_scopes=tenant,org_unit<br/>proj.* assignable_scopes=project]
  end
  subgraph proj[專案層（本案改）]
    P[projects] --> PP[project_participants<br/>＋ project_role_id → roles<br/>audit_scope 留著]
    PP -. 展開 .-> RP
    PG[project_group_participants<br/>＋ project_role_id] --> R
    PC[project_control_participants<br/>＋ project_role_id] --> R
  end
  PP --> R
```

| 表 | 動作 |
|---|---|
| `roles` | 加 `kind`（`system`／`project`，預設 `system`）、`is_default_owner`（每租戶恰一個專案角色為 true） |
| `capabilities` | 新增 `proj.*` 列，`assignable_scopes=['project']`、`module='proj'` |
| 五張參與者表 | 加 `project_role_id` FK → `roles.id`；`role` 欄並存一版 |
| `stage_objects` | `default_main_roles` 值改能力點名 |
| `flow_templates.bpmn_xml` | migration 把 `main_role` 四值改能力點名；`workflow_templates.xml`（實例快照）不碰，讀取換算 |

## 判定流程 {#flow nav="判定"}

```{.mermaid cap="圖 2 — 一次「按下審核」的判定"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
    participant FE as 前端
    participant API as 後端 route
    participant S as app service
    participant G as 專案能力點守門
    participant DB as 資料庫
    FE->>API: POST /review/mark（帶 control_uid）
    API->>S: mark_reviewed(control_uid)
    S->>DB: resolve 控制項 → project_id, group_id
    S->>G: assert_project_capability("proj.review.approve", project_id, group_id, control_id)
    G->>DB: 由下往上找第一筆參與者指派（控制項→群組→專案）
    DB-->>G: project_role_id
    G->>DB: 展開該角色的 pattern（Redis 快取）
    DB-->>G: {proj.project.read, proj.review.approve, ...}
    G-->>S: 通過／ForbiddenError／非成員 NotFound
    S-->>FE: 200 或 403 或 404
```

守門入口維持 `common/authz/project.py`，四支函式簽名改收能力點名：`assert_project_capability(cap, ...)`（回退版）、`assert_project_capability_exact(cap, ...)`（精確版）、`assert_project_participant` 不變（任一參與者）。舊四支留 deprecated shim 一版，內部把 `allowed_roles` 對到能力點。

## 現況盤點：改哪裡 {#inventory nav="盤點"}

| 元件 | 現況 | 本案動作 | 階段 |
|---|---|---|---|
| `common/enum/participant_enum.py`＋jedi_task_platform 同名 | 兩份相同 enum | 只留套件那份，主專案 re-export；守衛測試擋 app／infra／api 出現 `'manager'` 等字面值 | B.1 |
| `common/authz/project.py` 四支守門 | 比對 `allowed_roles` tuple | 改收能力點名；舊簽名留 shim 一版 | B.3 |
| `participant_role_service.get_user_role`（jedi_task_platform） | 回角色字串 | 回 `project_role_id`，新增 `get_user_project_capabilities` 展開 | B.3 |
| 8 支 `infra/readmodel/` SQL（`pp.role IN (...)`） | 字面值比對 | 改 join 展開表或先查「有此能力點的角色 id 集合」再 `IN` | B.3 |
| `stage_entered_recipients.py`、`job_batch_complete_query.py`、`detection_job_notify_query.py` | 各自比角色 | 統一走「有能力點 X 的參與者」查法 | B.3 |
| `project_start_app_service.py`（建立者自動 manager） | 寫 `'manager'` | 寫該租戶 `is_default_owner` 角色 | B.3 |
| `api/project/serializers/project.py`、`excel_parser/validators.py`、套件 serializer | `OneOf` 四值 | 改驗角色 uid 存在於該租戶專案角色 | B.3 |
| `stage_advance_service._resolve_main_roles` | 讀 XML `main_role` 四值 | 讀取換算：四值→能力點；判定改能力點 | B.3（換算）／B.5（編輯器） |
| `stage_objects.default_main_roles` | 角色名 | migration 改能力點名 | B.2 |
| FE `views/access/RoleList.vue`、`RoleDetail.vue` | 單一列表 | 兩個 tab；專案角色詳情矩陣只列 `proj.*` | B.4 |
| FE `ParticipantPicker.vue`、`ProjectParticipantsPanel.vue` | 四值寫死 | 下拉讀 `GET /project-roles`（租戶的專案角色） | B.4 |
| FE 專案頁約 30 處 `p.role === 'manager'` | 字面值 | `v-can-project`＋專案 API 回 `my_capabilities` | B.4 |
| FE `FlowTemplateEditorView.vue` roleOptions | 四值寫死 | 下拉讀 `proj.*` 能力點 | B.5 |
| 6 支 jedi 套件呼叫端 | `allowed_roles=("manager",)` | 改能力點名參數 | B.3（走 path dependency） |

## 風險與不做 {#risk nav="風險"}

::: {.callout .warn}
**⚠️ 三個最可能出事的點**

- **升級對照**：舊資料四值→角色 id 要每租戶各對一次；租戶沒建範例角色（例如用 SQL 直接灌的測試租戶）升級腳本要先補建。排練用真實舊庫（memory：封包前排練真實舊庫）。
- **SQL 讀模型效能**：原本一個 `role = 'manager'` 變成 join 展開表。做法是先查「這個租戶哪些專案角色有能力點 X」（角色數很少，可快取），再 `project_role_id IN (...)`，查詢形狀不變。
- **流程實例快照**：4,400 份 XML 不改，讀取換算要有測試覆蓋四值與逗號多值；新建流程範本存能力點名後，舊換算層一版後才能拆。
:::

**明確不做**：專案角色的部門範圍、起訖日（參與者指派本來就是專案範圍、無時效）；拒絕規則；把專案角色匯出 JSON（第二版再看）；`process_participants`（子流程參與者）若 B.1 盤點確認無人讀 `role` 欄就不改。
