---
title: "專案角色併入 RBAC — 設計文件 (FR-155)"
brand: "Guidant AI · **FR-155** 專案角色併入 RBAC"
eyebrow: "FR-155 · Project Role → RBAC — 設計文件 · 2026-10-21"
h1: "專案角色併入 RBAC：設計定案"
subtitle: "角色管理分「系統角色」「專案角色」兩個 tab；參與者指角色不指字串；判定查能力點"
lede: "專案參與者今天是四個寫死的英文字（manager／reviewer／auditor／viewer），散在三個 repo 約一百多處與流程圖 XML 裡。本案把專案角色做成跟系統角色一樣「可建、可勾」：角色管理頁分兩個 tab，專案角色有自己的一組能力點（`proj.*`）；專案參與者仍每個專案指派，但指的是角色；守門、通知、流程推進、待辦清單全部改成「他在這個專案的角色展開後有沒有這個能力點」。D1–D10 已定案；一版做完，流程編輯器併入第一版。"
chips: [
  {text: "D1–D10 定案；實作完成待進版", kind: ok},
  {text: "拆分 4 批 × 11 子卡，一版做完", kind: accent},
  {text: "母案 CM-2977（子卡 CM-2978～2988）", kind: ok},
  {text: "跨 BE／FE／jedi_task_platform＋5 套件呼叫端／五張表／流程圖 XML", kind: crit}
]
footer: "FR-155 · 專案角色併入 RBAC — 設計文件 · 2026-10-21 · 討論稿 discussion.md · 雛形 prototype/ · 前作 FR-132 存取管理（FR-B 預告）、FR-048 統一授權守門"
---

> 狀態：**實作完成、待進版**（D1–D10 已拍板，D5／D7 依實作改寫）｜日期：2026-10-22｜討論稿（含圖與現況細節）：[`discussion.html`](./discussion.html)｜雛形：[`prototype/index.html`](./prototype/index.html)｜母案：[CM-2977](https://app.notion.com/p/FR-155-RBAC-tab-4-11-400346da4cd081c1b7ffce63ec77390d)（子卡 CM-2978～CM-3014）｜收口審查：[`docs/review/2026-10-22-fr155-review.md`](../../review/2026-10-22-fr155-review.md)
> 前作：FR-132 存取管理（`capabilities.assignable_scopes` 預留 `project`）、FR-048 統一授權守門（`common/authz/`）、FR-069 jedi-iam

## 變更紀錄 {#changelog nav="變更紀錄"}

| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-21 | 初版設計定案：D1–D10 全數照討論稿建議拍板；雛形三頁審過；流程編輯器（原第二版）併入第一版，與畫面同批 | 本檔 |
| 2026-10-21 | Notion 母卡 CM-2977 ＋ 11 子卡 CM-2978～2988 建卡完成，工作坊四欄已填；等決策者在工作坊派第 1 批 | 母案 CM-2977 |
| 2026-10-22 | 依實作與決策者裁示改成現況：D5 指派只在專案層；D7 `my_capabilities` 是專案層精確版；§6.1 能力點 13 個（去 `proj.stage.advance`、`proj.job.execute`，加 `proj.round.manage`）；§6.4 設計器主責只收三個；§6.5 補 `GET /project-roles?project_uid=`、錯誤碼實際編號與 `GRC_400185` | CM-3014 |

## 需求背景與端到端流程 {#why nav="背景"}

### 2.1 要解決什麼

| 缺口 | 白話 | 證據 |
|---|---|---|
| 專案角色寫死 | 四個英文字散在 BE 28 支檔、6 支 jedi 套件、FE 約 30 支；enum 有兩份相同複本 | FR-132 `inventory/02-project-roles-and-flow.md` §1–2；本案 grep |
| 管理員不能自訂專案角色 | 客戶要「資深稽核員」（稽核＋審閱）做不到；角色的能力綁在程式裡 | `common/enum/participant_enum.py`、`ParticipantPicker.vue:102-107` |
| 流程圖綁角色名 | BPMN UserTask `main_role` 存四值；進行中實例約 4,400 份 XML 快照 | `stage_advance_service.py:672-679`；inventory §3、§5 |
| 通知與待辦各自比字串 | 8 支 SQL 讀模型 `pp.role IN (...)`、5 處通知收件人各寫一遍 | `infra/readmodel/audit/*.py`、`stage_entered_recipients.py` |
| 兩套角色沒有關係 | 系統角色是資料驅動（FR-132），專案角色不是 | `roles`／`role_capability_patterns` vs 五張參與者表 `role varchar` |

### 2.2 端到端流程

1. 管理員在「存取管理 ／ 角色 ／ 專案角色」tab 建角色「資深稽核員」，勾 `proj.audit.judge`、`proj.review.approve`、`proj.report.read`。
2. 專案負責人在「專案 ／ 設定 ／ 參與者」把陳美玲加進來，角色下拉選「資深稽核員」，稽核範圍選外稽。
3. 陳美玲進專案：專案詳情 API 回她的 `my_capabilities`，前端只顯示那三個能力點對應的按鈕。
4. 她按「標記已審閱」：後端 `assert_project_capability("proj.review.approve", ...)` 由下往上找她的參與者指派、展開角色、比對通過。
5. 批次完成通知：查「本專案有 `proj.review.approve` 的參與者」，她會收到；「稽核待辦」清單列有 `proj.audit.judge` 的人，她也在。
6. 流程走到「稽核」站：該站要求 `proj.audit.judge`，她可以推進；負責人有 `proj.stage.advance-any` 也可以。

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

**一句話**：先收斂與定清單（B.1），再改表與升級（B.2），改判定與通知與 API（B.3），最後畫面與流程編輯器（B.4）。

| 批 | 做什麼（白話） | 產出 | 完成怎麼判定（決策者可親手檢查） | 依賴 |
|---|---|---|---|---|
| **B.1 收斂與清單** | 四個字只留一份；守衛擋字面值；專案能力點清單與四個範例角色對照定稿 | enum 單一來源＋守衛測試；`inventory/b1-project-capabilities.md` | 守衛測試綠，故意加一行 `'manager'` 會紅；對照表每個專案內動作都有一列 | — |
| **B.2 資料地基** | `roles` 加種類與預設負責人旗標；`capabilities` 加 `proj.*`；五張參與者表加 `project_role_id`；升級腳本每租戶建四範例角色並回填；新裝 seed | migration、seed、排練紀錄 | 真實舊庫升級後，任一專案參與者頁每個人角色名與升級前一樣；舊帳號能按的按鈕一樣 | B.1 |
| **B.3 判定、通知、API** | 守門四支改收能力點；`get_user_role` 回角色並能展開；8 支 SQL＋5 處通知改同一查法；流程推進讀取換算；專案角色 CRUD／`GET /project-roles`／專案詳情回 `my_capabilities`／FE 常數檔 | 判定層、讀模型、API | 建「只能看」的自訂角色指派給某人：他進得了專案、按審核 403、不收審閱通知；勾回審閱立刻能按、會收信 | B.2 |
| **B.4 畫面與流程編輯器** | 角色頁兩個 tab＋專案角色詳情；參與者下拉讀表；專案內按鈕改讀 `my_capabilities`；流程編輯器「主要角色」改選能力點 | FE 改動＋BE 存檔驗證 | 對照雛形三頁；新增「資深稽核員」指派後該人只看得到三個動作；流程編輯器下拉列的是能力點 | B.3 |

B.1→B.2→B.3→B.4 照順序；B.4 內三張可依卡號串行。

## 決策定案 D1–D10 {#decisions nav="決策"}

| # | 題目 | 定案 | 理由 | 被排除方案 |
|---|---|---|---|---|
| **D1** | 專案能力點命名 | 新模組前綴 `proj.<資源>.<動詞>`，`assignable_scopes=['project']`、`module='proj'`；13 個（§6.1） | 與 `grc.*`（租戶層「能不能碰這類東西」）一眼分得開；有 `grc.audit.approve` 的系統角色不會被誤判成每個專案都能審核 | 沿用 `grc.*` 靠範圍欄分——畫面與 grep 都會混 |
| **D2** | 出貨範例專案角色 | 每租戶四個範例（負責人／審核者／稽核員／瀏覽者），可改名、改能力點、可刪；其中一個 `is_default_owner=true`（建專案建立者自動指派、不可刪、每租戶恰一個）；使用中專案數 >0 不可刪 | 與 FR-132 D6 同精神：保證至少一個角色能管專案，其餘是起點 | 四個全鎖——客戶要自訂只能複製 |
| **D3** | 參與者表怎麼改 | 五張表加 `project_role_id` FK → `roles.id`（nullable 一版）；舊 `role` 欄並存一版後退役；寫入路徑雙寫（新欄為準、舊欄回填對應範例角色名） | 回滾不必動資料，與 FR-132 D2 同手法 | 直接改 `role` 欄語意——回滾要動資料 |
| **D4** | 系統管理員進專案 | `proj.*` 只從「這個專案的參與者指派」展開，不從 `user_roles` 展開；系統 `*.*.*` 不包含 `proj.*`；不是參與者就看不到專案（與今天相同） | 管理員不會每封通知都收、稽核員清單不會列他；程式上兩個入口各查各的 | 包含——每種通知都收 |
| **D5** | 群組／控制項層回退 | 指派只在專案層。子層參與者表（群組、控制項、控制群組、流程）保留 `project_role_id` 欄，但沒有寫入路徑；回退版守門（控制項→群組→專案找第一筆）仍在，實際一律落到專案層那一列 | 子層的寫入端點早已退役、DEV 0 筆、前端控制項樹也不送；為它補畫面與回填沒有使用者 | 補子層指派畫面——沒有需求；拿掉子層欄——回滾要動表 |
| **D6** | 稽核範圍（內稽／外稽） | `audit_scope` 留在參與者列；判定時只對「有 `proj.audit.judge`」的參與者生效；前端只對有該能力點的角色顯示欄位 | 不綁角色名、綁能力點 | 搬到角色——同一人兩專案不同範圍要兩個角色 |
| **D7** | 專案內按鈕前端判定 | 專案詳情與專案列表回 `my_capabilities`，是**專案層精確版**（只看專案層那一列，與後端精確版守門同判法；D5 下子層沒有指派，兩版結果相同）；FE 新增 `v-can-project="PROJ.X"` 指令；權限名由 BE 產常數檔（比照 `gen_permission_matrix.py`） | 回退規則 BE 算一次，FE 不重算 | FE 自己展開角色 |
| **D8** | 流程圖 `main_role` | 改存能力點名（例 `proj.audit.judge`，逗號多值照舊）；`_resolve_main_roles` 讀到舊四值即換算；`flow_templates.bpmn_xml` 與 `stage_objects.default_main_roles` 用 migration 改成能力點名；`workflow_templates.xml`（實例快照）不碰；manager 全域覆寫改成「有 `proj.stage.advance-any`」；設計器存範本的 `main_role` 只收 `proj.review.approve`、`proj.audit.judge`、`proj.stage.advance-any` 三個（§6.4） | 客戶會改角色名、加角色，角色名不能當程式依據；4,400 份快照不改最安全 | 存角色 uid（刪角色流程壞）；批次改寫 XML |
| **D9** | jedi 套件改法 | 參與者表、enum、serializer 的 owner 是 jedi_task_platform，改在套件；jedi_compliance_audit／jedi_survey／jedi_evidence_classification／jedi_detection／jedi_flow_engine 只改呼叫端參數（角色 tuple → 能力點名）；開發走 poetry path dependency，發版等決策者明示 | 不造第二套真相 | 主專案 monkeypatch |
| **D10** | 分版 | 一版做完（原「流程編輯器放第二版」改判：客戶第一版就能自建角色，編輯器若還只能選四個舊字，自建角色在流程裡沒用） | 決策者 2026-10-21 改判 | 兩版 |

**明確不做**：專案角色的部門範圍與起訖日（參與者指派本來就是專案範圍、無時效）；拒絕規則；專案角色匯出 JSON；專案角色的萬用字元（只有十幾個能力點，逐個勾比較清楚）。

## 現況接入點盤點 {#inventory nav="現況盤點"}

| 元件 | 現況 | 本案動作 | 批 |
|---|---|---|---|
| `common/enum/participant_enum.py`＋`jedi_task_platform/participant/common/participant_enum.py` | 兩份相同 StrEnum | 只留套件那份；主專案改 re-export；守衛測試 `test/test_project_role_literal_guard.py` 掃 `api/ app/ infra/ core/` 的 `'manager'|'reviewer'|'auditor'|'viewer'` 字面值（白名單：migration、i18n、本 enum） | B.1 |
| `common/authz/project.py:28-137` 四支守門 | `allowed_roles` tuple 比對 | 新增 `assert_project_capability(cap, role_service, project_id, group_id, control_id)`（回退版）與 `assert_project_capability_exact(cap, participant_ds, project_id, user_id)`（精確版）；舊四支留 shim 一版，內部把 `allowed_roles` 對到能力點（manager→`proj.participant.manage`、auditor→`proj.audit.judge`、reviewer→`proj.review.approve`） | B.3 |
| jedi_task_platform `participant_role_service.py:34-85` `get_user_role` | 回角色字串 | 新增 `get_user_project_role_id`（同回退規則，回 `project_role_id`）與 `get_user_project_capabilities`（展開，走 jedi-iam pattern 展開服務＋Redis 快取鍵 `authz:proj:{tenant}:{catalog_version}:{role_id}`）；`get_user_role` 保留一版回舊欄 | B.3 |
| 8 支 `infra/readmodel/` SQL（`audit_plan_list_query.py:47,53,196,310`、`closing_report_list_query.py:27,32`、`auditor_dashboard_query.py:60,157`、`poam_dashboard_query.py:61`、`job_due_reminder_query.py:55`、`job_batch_complete_query.py:239`、`detection_job_notify_query.py:156`、`support_end_query.py:52`） | 字面值 | 先查「租戶內有能力點 X 的專案角色 id 集合」（新 helper `project_role_ids_with_capability(tenant_id, cap)`，可快取），再 `project_role_id = ANY(:ids)`；查詢形狀不變 | B.3 |
| `stage_entered_recipients.py`、`job_batch_complete_query.py:232`、`detection_job_notify_query.py:150`、`auditor_dashboard_query.py:58` | 各自比角色 | 統一改為「有能力點 X 的參與者」 | B.3 |
| `project_start_app_service.py:135-146` | 建立者寫 `'manager'` | 寫該租戶 `is_default_owner` 角色 | B.3 |
| `api/project/serializers/project.py:27-29`、`excel_parser/validators.py:9`、jedi_task_platform serializer ×2、jedi_compliance_audit `serializers/project.py:77` | `OneOf` 四值 | 收 `project_role_uid`，驗存在於該租戶 `kind='project'` 且啟用；Excel 匯入收角色名、對不到報錯列出 | B.3 |
| `stage_advance_service.py:148-156,344-355,672-679` | `main_role` 四值 vs `user_role` | `_resolve_main_roles` 回能力點名（舊四值換算表）；判定改 `cap in user_caps or "proj.stage.advance-any" in user_caps`；`force` 改要求 `proj.stage.advance-any` | B.3 |
| `stage_objects.default_main_roles`、`flow_templates.bpmn_xml` | 角色名 | migration 改能力點名（XML 用 `regexp_replace` 只動 `name="main_role"` 的 value） | B.2 |
| jedi-iam `api/routing.py` roles CRUD | 無 kind | list 加 `kind` 篩選；create 收 `kind`；`kind='project'` 的角色 pattern 只准 `proj.*`、必含 `proj.project.read`；`is_default_owner` 切換時同租戶其他角色清掉；刪除前查使用中專案數 | B.3 |
| 專案詳情 API（`api/project/`） | 回 participants | 加 `my_capabilities: [str]`；`GET /project-roles`（租戶內啟用的專案角色，含能力點清單） | B.3 |
| `scripts/deliverables/gen_permission_matrix.py` | 產 `CAP` 常數 | 加產 `PROJ` 常數（FE `src/config/projectCapabilities.js`） | B.3 |
| FE `views/access/RoleList.vue`、`RoleDetail.vue`、`PermissionResourceMatrix.vue` | 單一列表 | 兩個 tab（`?kind=`）；專案角色詳情矩陣只列 `proj.*`、拿掉可見選單預覽、加預設負責人開關 | B.4 |
| FE `ParticipantPicker.vue:102-115`、`ProjectParticipantsPanel.vue`、`ProjectCreateView.vue` | 四值寫死 | 下拉讀 `GET /project-roles`；`audit_scope` 欄只對含 `proj.audit.judge` 的角色顯示 | B.4 |
| FE 專案頁約 30 處 `p.role === 'manager'`（`ProjectPlanningView.vue` 9、`ProjectAuditorOverview.vue` 6、`MyTasksView.vue` 2、`FlowPhaseBanner.vue` 1、`ApPlanOverviewTab.vue` 2…） | 字面值 | `v-can-project="PROJ.X"`＋`my_capabilities` | B.4 |
| FE `FlowTemplateEditorView.vue:129-134,376-389,594`、`FlowTemplatePreviewDialog.vue` | 四值寫死 | 下拉讀 `proj.*` 能力點；存檔驗證改能力點名 | B.4 |
| 5 支套件呼叫端（`review_service.py`、`task_survey_guard.py`、`evidence_batch_service.py:1476`、`detection_orchestration_service.py` ×8、`core/plugins/compliance_audit.py:73-93`） | `allowed_roles=("manager",)` | 改能力點名參數，透過 adapter 轉發新函式 | B.3 |

## 詳細設計 {#detail nav="詳細設計"}

### 6.1 專案能力點清單（D1）

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

共 13 個。四個範例角色的勾選以 `resources/roles/project-roles.json` 為準（與 seed migration 由 `test/test_role_templates.py` 鎖成一致）。「執行任務、上傳佐證、填答」不設能力點：這類動作走「被指派人 ∪ `proj.job.manage`」（任務被指派人本身就有操作權，不看角色）。「推進自己負責的階段」也不設：可不可以推進由站的 `main_role` 決定（§6.4）。每個接入點對到哪一個能力點見 [`inventory/b1-project-capabilities.md`](inventory/b1-project-capabilities.md)。

**舊角色→能力點換算表**（守門 shim、`main_role` 讀取換算、`get_user_role` 相容用同一張）：manager→`proj.participant.manage`（守門語意「是 manager」）／`proj.stage.advance-any`（流程語意）；reviewer→`proj.review.approve`；auditor→`proj.audit.judge`；viewer→`proj.project.read`。

### 6.2 資料模型（D2、D3）

| 表 | 動作 |
|---|---|
| `public.roles` | `kind varchar(10) NOT NULL DEFAULT 'system'`（CHECK in system/project）；`is_default_owner boolean NOT NULL DEFAULT false`；部分唯一索引 `(tenant_id) WHERE kind='project' AND is_default_owner AND is_delete=0` |
| `public.capabilities` | `proj.*` 13 列：`module='proj'`、`assignable_scopes=['project']`、`is_platform=false`、`license_module` 空（不受授權照管）、`display_name='capability.proj.<資源>.<動詞>'` |
| `compliance.project_participants`、`project_group_participants`、`project_control_participants`、`control_group_participants`、`process_participants` | 加 `project_role_id integer NULL REFERENCES public.roles(id) ON DELETE RESTRICT`＋索引；`role` 欄保留一版 |
| `compliance.stage_objects.default_main_roles` | 值改能力點名 |
| `compliance.flow_templates.bpmn_xml` | `main_role` 值改能力點名（設計器範本）；`workflow_templates.xml` 不碰 |

**升級腳本**（T-2.2）：對每個 `tenants` 列建四個 `kind='project'` 角色（名稱走 i18n key `project_role.owner` 等，DB 存中文名「負責人／審核者／稽核員／瀏覽者」）＋ `role_capability_patterns` 照 6.1 表；負責人 `is_default_owner=true`；五張表 `UPDATE ... SET project_role_id = (該租戶對應角色) WHERE role = '<舊值>'`；結尾斷言五張表 `project_role_id IS NULL` 為 0 列，否則 rollback。**新裝**：`scripts/init/` seed 與 `tenant_provisioning_service.py` 同樣建四個；回寫時明講「出貨基線待重產」。

### 6.3 判定層（D4、D5）

- **兩支展開、兩支守門**：回退版 `get_user_project_capabilities(project_id, user_id, group_id=None, control_id=None)`＋`assert_project_capability`（由下往上找第一筆指派）；精確版 `get_participant_capabilities`＋`assert_project_capability_exact`（只看專案層那一列）。兩版都是 `project_role_id` → jedi-iam `CapabilityExpansionService.expand_project_roles` → Redis 快取（鍵 `authz:proj:{tenant}:{catalog_version}:{role_id}`，角色寫入時 `invalidate_all` 一併清）。非參與者回 `None`／空集合，守門轉 404；是成員但缺能力點 403。
- **🔴 正式環境 Redis ACL**：`docker/production/docker-compose.yml` 的 guidant-api／socketio／worker 三個帳號都要開 `~authz:proj:*`（與 `~authz:exp:*` 同權限）。漏開時 Redis 回 NOPERM，adapter 只把連線錯誤當「快取不可用、改查 DB」，NOPERM 會直接變 500。
- **舊守門 shim**：`assert_project_manager`／`assert_project_role_fallback`／`assert_project_role` 保留一版，內部用換算表把舊角色字對到能力點（manager→`proj.participant.manage`、reviewer→`proj.review.approve`、auditor→`proj.audit.judge`、viewer→`proj.project.read`）。新程式一律用能力點版。
- **列表上的「負責人」欄**（稽核計畫列表、結案報告列表）看的是「預設負責人」角色，不是看能力點：顯示的是「誰掛名負責」，自建的副負責人有管理能力點但不算掛名。「可不可以指派」仍看 `proj.participant.manage`。
- **不從 `user_roles` 展開 `proj.*`**（D4）：`CapabilityGuard` 的展開服務遇到 `assignable_scopes=['project']` 的能力點一律不納入租戶層集合；守衛測試驗 `*.*.*` 展開結果不含 `proj.*`。
- **守門入口**維持 `common/authz/project.py`；決策表（`common/authz/__init__.py`）軸③改寫為能力點版。
- **`audit_scope`**（D6）：`resolve_stage_recipients` 與 `_get_user_role` 的稽核性質比對只對 `proj.audit.judge in caps` 的參與者做。

### 6.4 流程（D8）

- `_resolve_main_roles` → `_resolve_main_caps`：讀 `main_role`，每個值若在舊四值換算表就換，否則視為能力點名；回 `list[str]`。
- 可推進：`caps & main_caps` 非空，或 `proj.stage.advance-any in caps`。`force` 要 `proj.stage.advance-any`。
- `stage_entered_notify_service`：收件人＝有 `main_caps` 任一的參與者（套 D6）＋有 `proj.stage.advance-any` 的參與者。
- 進站通知：`main_role` 空值時退回站預設 `stage_objects.default_main_roles`，與推進判定共用 `stage_entered_recipients.resolve_main_caps`。
- 規劃準備度檢查（「有站沒人負責」軟提醒）用同一套換算；持 `proj.stage.advance-any` 的人整個不算該站負責人（預設負責人什麼都有，算進去就永遠不提醒）。
- **設計器只收三個主責**：存範本時每個 UserTask `main_role` 只能是 `proj.review.approve`、`proj.audit.judge`、`proj.stage.advance-any`（`app/flow_engine/service/main_role_check.py` `MAIN_ROLE_ALLOWED`），否則 400 `GRC_400185` 並列出允許值。其他能力點不准選：推進判定只看「持有任一主責」，選了 `proj.ssp.update` 這類會讓守門失真。
- **專案流程設定存實例 XML**：先把舊四值換算成能力點寫回，再驗「是專案能力點」即可（舊實例可能存了三個以外的值，收嚴會讓它存不回去）。
- FE 流程編輯器：下拉只列三個，顯示 `lang.capability.*`；載入舊範本時整張換算；舊範本存了三個以外的值，臨時併入選項並標「不在建議清單」。

### 6.5 API（D7）

| 端點 | 說明 |
|---|---|
| `POST /roles`（jedi-iam 列表，`filters.kind`） | 預設 `system`；`kind=project` 列專案角色，每列回 `kind`／`is_default_owner`／`in_use_project_count` |
| `POST /role`、`PUT /role/<uid>`（既有，收 `kind`） | `kind='project'` 時只准 `proj.*` 完整名、自動補 `proj.project.read`；`is_default_owner=true` 時同交易先清同租戶其他角色的旗標；專案角色不過授權照守門（`proj.*` 不是售賣模組） |
| `GET /project-roles` | 登入者租戶內啟用的專案角色（uid／name／capabilities／is_default_owner），建專案的參與者下拉用；任何登入者可讀 |
| `GET /project-roles?project_uid=<uid>` | 專案**所在租戶**的專案角色（總部帳號改子租戶專案時用）；限專案成員，非成員 404 |
| `GET /grc/project/<uid>`（既有） | 加 `my_capabilities`；participants 每筆補 `project_role_uid`／`project_role_name` |
| 專案列表（既有） | 每列加 `my_capabilities`（整頁一次展開） |
| `PUT /grc/project/<uid>`（既有） | 改設定要 `proj.project.update`；送 `participants` 另要 `proj.participant.manage`，且每個 `project_role_uid` 須屬專案所在租戶（否則 404 `GRC_404057`） |
| `GET /users/me/capabilities`（既有） | 不含 `proj.*` |

錯誤碼（`common/code/flow_control_error_code.py`，FE i18n 已同步）：

| 碼 | 常數 | 情境 |
|---|---|---|
| `GRC_403071` | `GRC_PROJECT_CAPABILITY_REQUIRED` | 是專案成員但角色缺該能力點（呼叫端沒給專屬碼時用；原本有專屬 403 碼的守門保留原碼） |
| `GRC_404057` | `GRC_PROJECT_ROLE_NOT_FOUND` | 專案角色不存在、停用，或不屬專案所在租戶 |
| `GRC_412078` | `GRC_DEFAULT_OWNER_REQUIRED` | 租戶沒有預設負責人（建專案時）；預設負責人不可停用 |
| `GRC_400183` | `GRC_PROJECT_ROLE_AUDIT_SCOPE_NOT_ALLOWED` | 給沒有 `proj.audit.judge` 的角色設內稽／外稽範圍 |
| `GRC_409050` | `GRC_PROJECT_ROLE_IN_USE` | 刪除仍有專案在用的專案角色（優先於 412） |
| `GRC_400184` | `GRC_PROJECT_ROLE_INVALID_PATTERN` | 專案角色勾了非 `proj.*` 或萬用字元 |
| `GRC_400185` | `GRC_INVALID_MAIN_ROLE` | 流程圖 `main_role` 不在允許清單 |

### 6.6 前端（D7）

照雛形三頁：`prototype/roles.html`（兩個 tab）、`prototype/role-detail.html`（專案角色詳情）、`prototype/participants.html`（參與者）。`v-can-project` 指令實作比照 `v-can`（`src/directives/`），資料源是當前專案 store 的 `my_capabilities`；專案 store 載入專案詳情時一併存。

## 拆分（4 批 × 11 子卡＋收尾 2 批） {#split nav="拆分"}

下表是原始拆分；驗收後加開第 5 批（CM-3010 合回與套件 bump、CM-3011 BE 裁示收尾、CM-3012 FE 裁示收尾）與第 6 批（CM-3013 出貨基線重產、CM-3014 收口審查與文件）。

依賴鏈：**B.1 → B.2 → B.3 → B.4**；同批依卡號串行。jedi 套件開發走 poetry path dependency（`pyproject.toml:118-124` 的註解行），發版等決策者明示；migration 只套 DEV。

| 編號 | 卡號 | 批 | 做什麼（白話） | 改哪些檔 | 驗收條件 | 建議 model／effort |
|---|---|---|---|---|---|---|
| T-1.1 | CM-2978 | 1 | 四個字只留一份：主專案 enum 改 re-export jedi_task_platform；守衛測試擋字面值 | `common/enum/participant_enum.py`、`test/test_project_role_literal_guard.py`、jedi_task_platform enum | 守衛綠；突變（加一行 `'manager'`）會紅 | sonnet／medium |
| T-1.2 | CM-2979 | 1 | 專案能力點清單定稿：逐一對照 70 處守門、8 支 SQL、5 處通知、6 個流程站、FE 30 處按鈕各對哪個 `proj.*`；缺的補進清單 | `inventory/b1-project-capabilities.md` | 每個接入點都有一列且有能力點；四個範例角色勾選表定稿 | opus／high |
| T-2.1 | CM-2980 | 2 | 資料表：`roles` 加欄、`capabilities` 加 `proj.*`、五張參與者表加 FK；jedi-iam／jedi_task_platform ORM 同步 | `scripts/sql/2026-10-xx-fr155-*.sql` ×1、jedi-iam `infra/models/role.py`、jedi_task_platform 五個 model | DEV 套用後 `\d` 看得到欄；`capabilities` 多 14 列 | opus／medium |
| T-2.2 | CM-2981 | 2 | 升級腳本與新裝 seed：每租戶建四範例角色、回填 `project_role_id`、`stage_objects`／`flow_templates` 改能力點名；拿真實舊庫排練；`tenant_provisioning_service` 建四個 | migration ×2、`scripts/init/` seed、`tenant_provisioning_service.py`、`inventory/b2-upgrade-rehearsal.md` | 排練後五張表 `project_role_id IS NULL` 0 列；任一專案參與者頁角色名不變；回報「出貨基線待重產」 | opus／high |
| T-3.1 | CM-2982 | 3 | 守門改能力點：新兩支守門函式＋舊四支 shim；`get_user_project_capabilities` 展開＋快取；D4 守衛；6 套件呼叫端改參數；serializer 收角色 uid；建立者寫預設負責人 | `common/authz/project.py`、`__init__.py` 決策表、jedi_task_platform `participant_role_service.py`、5 套件呼叫端、`core/plugins/compliance_audit.py`、`project_start_app_service.py`、serializer ×4、error code | 「只能看」自訂角色按審核 403；非成員 404；`*.*.*` 展開不含 `proj.*`（守衛） | opus[1m]／high |
| T-3.2 | CM-2983 | 3 | 通知與 SQL 讀模型：`project_role_ids_with_capability` helper；8 支 SQL 改 `project_role_id = ANY`；5 處收件人改能力點 | `infra/readmodel/` 8 支、`stage_entered_recipients.py`、`job_batch_complete_query.py`、`detection_job_notify_query.py` | 勾掉審閱能力點批次完成通知不寄；勾回會寄；我的稽核待辦列有 `proj.audit.judge` 的人 | opus／high |
| T-3.3 | CM-2984 | 3 | 流程推進讀取換算：`_resolve_main_caps`、可推進判定、`force`、進站通知收件人；舊四值與逗號多值測試 | `stage_advance_service.py`、`stage_entered_notify_service.py`、測試 | 進行中舊專案推進行為不變；新範本存能力點名可推進 | opus／high |
| T-3.4 | CM-2985 | 3 | 專案角色 API：roles CRUD 加 `kind`、pattern 限制、`is_default_owner` 互斥、刪除前查使用中；`GET /project-roles`；專案詳情回 `my_capabilities`；`gen_permission_matrix.py` 產 `PROJ` 常數 | jedi-iam `api/`＋`app/`、`api/project/`、`scripts/deliverables/gen_permission_matrix.py`、FE `src/config/projectCapabilities.js` | Swagger 打得通；`GET /projects/<uid>` 有 `my_capabilities`；常數檔與 DB 一致 | opus／medium |
| T-4.1 | CM-2986 | 4 | FE 角色頁兩個 tab＋專案角色詳情（照雛形 1、2） | FE `views/access/RoleList.vue`、`RoleDetail.vue`、`components/access/PermissionResourceMatrix.vue`、i18n | 新增「資深稽核員」勾三個能力點存得起來；預設負責人開關互斥；使用中不可刪 | opus／medium |
| T-4.2 | CM-2987 | 4 | FE 參與者下拉讀表＋專案內按鈕改 `v-can-project`（約 30 處）＋`audit_scope` 只對稽核判定角色顯示（照雛形 3） | FE `ParticipantPicker.vue`、`ProjectParticipantsPanel.vue`、`ProjectCreateView.vue`、`directives/vCanProject.js`、專案頁約 10 支 | 指派「資深稽核員」後進專案只看得到三個動作；`grep "role === 'manager'"` 專案頁為零 | opus[1m]／high |
| T-4.3 | CM-2988 | 4 | FE 流程編輯器「主要角色」改選能力點＋BE 存檔驗證 | FE `FlowTemplateEditorView.vue`、`FlowTemplatePreviewDialog.vue`、BE `workflow_xml_sync_service.py` 驗證 | 編輯器下拉列能力點；存舊四值被拒；預覽顯示能力點 i18n | sonnet／medium |

收口：審查、SPEC 三頁、使用手冊、release note 草稿由 CM-3014 做（報告 `docs/review/2026-10-22-fr155-review.md`）；套件發版與還原 pin、e2e 到 test repo 等決策者令。

**上版綁定**：① 參與者回填 migration（T-2.2）與判定改查能力點（T-3.x）必須同一版上，只上判定，參與者沒有 `project_role_id` 會一律 404；② jedi-iam 1.7.0、jedi-task-platform 1.4.0、jedi-compliance-audit 1.4.0、jedi-survey 1.3.0 四支新版必須一起進 image（task-platform 的 adapter 新增必填參數，新套件配舊宿主會 TypeError）；③ 正式 compose 的 Redis ACL 要含 `~authz:proj:*`。

## 端到端驗收 {#acceptance nav="端到端驗收"}

1. 真實舊庫升級：五張表零 NULL；任一專案參與者頁角色名不變；舊帳號按鈕不變；進行中流程推進不變。
2. 新裝：每租戶四個範例專案角色，負責人為預設；建專案建立者自動負責人。
3. 自訂角色「資深稽核員」（稽核判定＋審閱＋看報告）：指派後只看得到三個動作；按審核通過、按刪專案 403；收審閱通知與稽核待辦；流程稽核站可推進。
4. 「只能看」角色：進得了專案、所有寫入 403、不收任何待辦通知。
5. 系統管理員未加入專案：看不到專案（404）；`GET /users/me/capabilities` 不含 `proj.*`。
6. 控制項層指派成「只能看」：該控制項寫入 403，專案層其他控制項照常。
7. 流程編輯器新建範本 `main_role` 存能力點名；舊範本開啟顯示換算後的能力點。
8. 守衛：字面值守衛、`*.*.*` 不含 `proj.*`、`test/test_module_boundaries.py`、RLS 形狀測試全綠。
