FR-147 · 設計定案 v1 · 2026-10-17
顧問在資源庫替每條評估目標的預設任務寫名稱、描述、指南;啟動帶進任務;一份 Excel 兩邊共用
合規資源庫的「控制項與評估目標」頁,每條評估目標(AO)底下多一段「任務說明」。專案還沒啟動時每條 AO 固定一個範本預設任務,顧問填它的名稱、描述、指南,三欄存在範本流程圖的方塊上;專案啟動時原封帶進任務;啟動後這一段改讀專案目前輪次的任務,PM 改過流程就看到多個,顧問改的三欄直接寫進任務。顧問不改流程,資源庫頁的流程編輯器入口收掉。一份 Excel 一列一任務、AO 欄合併儲存格,資源庫頁與專案規劃頁同一份格式。不開新表、不動 jedi 套件、不套 migration。本文件寫定案規則、為什麼這樣定、踩到會怎樣。
🔴 這份是設計決策,不是現況。 內文停在 2026-10-17 定稿當時,實作改動不回頭改寫這裡。
狀態:設計定案,待開卡派工|討論稿:
discussion.md|雛形:prototype/index.html
檔案路徑與行號以 2026-10-17 為準,查不到時以函式名重新定位。路徑慣例:BE 路徑相對
compliance-manager-be/;FE 路徑相對compliance-manager-fe/;pkg/<套件>/代表~/Projects/Jedicogy/module/jedi-python-package/<套件>/。
名詞:資源庫=合規資源庫(compliance.module_frames),一本只給一個專案用;AO=評估目標,每條控制項底下的檢查項目,鍵是 catalog_control_parts.part_id;範本流程圖=每條 AO 配的一張 BPMN 圖(compliance.workflow_templates,uid 寫在 AO props 的 workflow-template-uid),預設一個方塊;範本預設任務=那個方塊,資源庫階段「任務」指的就是它;任務=compliance.job_executions 一列,有 name/description/guide;目前輪次=專案最新一輪(第 1 輪或最新覆核輪)。
| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-17 | v1:D1~D4、D6 定案;資料落點、讀寫規則、Excel 契約、三棒七張卡、端到端驗收 | — |
| 2026-10-17 | v2:決策者審雛形 v3 後加 D7 資源庫編輯頁版型比照稽核計畫頁(左樹到 AO、右欄一次一條),新增卡 3c、3b 改依賴 3c;雛形 v4 | CM-2872 |
整案一句話:資源庫頁每條 AO 底下多一段「任務說明」,顧問填預設任務的名稱、描述、指南;啟動帶進任務、啟動後直接改任務;一份 Excel 兩邊共用;顧問不改流程。
| 卡 | 做什麼 | 產出 | 完成怎麼判定(決策者親手做什麼、看到什麼) |
|---|---|---|---|
| 2a | 專案啟動時三欄從範本方塊帶進任務 | 方塊屬性讀寫 helper;任務產生改三欄來源 | DEV 把一本資源庫某條 AO 三欄用 API 填好,用它啟動專案,規劃頁那條 AO 的任務名稱、描述、指南就是填的;沒填的資源庫啟動後任務名稱仍是「控制碼 / AO 收證據」 |
| 2b | 覆核輪新任務抄上一輪三欄 | 覆核輪承襲加三欄 | DEV 在第 1 輪任務填三欄後啟動覆核輪,新輪同一條 AO 的任務三欄一樣 |
| 3a | 資源庫 AO 任務說明讀寫 API | 新 service、route、readmodel | 沒綁專案的資源庫:讀回一個任務、寫三欄再讀回一致;綁了專案的:讀回專案目前輪次任務、寫三欄後規劃頁任務也變 |
| 3c | 資源庫編輯頁版型比照稽核計畫頁(D7) | 左樹到 AO、右欄單一 AO 表單 | 對照雛形 v4:左樹點控制項右邊只有控制項欄位,點 AO 右邊只有那條 AO;上一條/下一條;15 條 AO 的控制項不再整頁堆 |
| 3b | 右欄 AO 表單底下加「任務說明」區塊、左樹圓點、收掉流程編輯器入口 | FE 新子元件掛進 AO 表單、拿掉一顆鈕 | 對照雛形畫面 1:AO 展開看到任務小卡三格、填存重整還在;已啟動的資源庫看到專案任務(多個時有流程帶、狀態與指派人唯讀);AO 右邊沒有流程編輯器鈕 |
| 4a | 任務說明 Excel 匯出匯入(資源庫端) | 共用 workbook builder、解析、預覽、確認 | 資源庫頁匯出開 Excel:一列一任務、多任務的 AO 前幾欄合併、三欄黃底;改兩格匯回預覽只標那兩格、確認後畫面更新 |
| 4b | 專案規劃頁既有任務配置匯出匯入對齊 | 加描述指南兩欄、AO 合併、匯入收三欄 | 專案規劃頁匯出長相與資源庫一致(多指派、部門、設備欄);改三欄匯回寫進任務 |
| 4c | 資源庫頁匯出匯入對話框與預覽 | FE | 對照雛形匯入預覽 |
依賴:2a 先建 helper,3a 的「寫圖」用同一支;3c(版型)不依賴後端可與 2a 同時派;3b 等 3a 與 3c;其餘可平行。
%%{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 B2[棒 2 啟動與覆核輪]
A2a[2a helper+啟動帶三欄] --> A2b[2b 覆核輪抄三欄]
end
subgraph B3[棒 3 資源庫頁]
A3a[3a 讀寫 API] --> A3b[3b FE 區塊+收入口]
end
subgraph B4[棒 4 Excel]
A4a[4a 資源庫端匯出匯入] --> A4c[4c FE 對話框]
A4a --> A4b[4b 專案端對齊]
end
A2a -.方塊屬性 helper.-> A3a
A3a -.讀取 readmodel.-> A4a
| 編號 | 定案 | 理由 | 被排除 |
|---|---|---|---|
| D1 | 啟動前三欄存範本流程圖的方塊上:名稱=方塊 name,描述與指南=方塊的 camunda 屬性 task_description/task_guide |
與既有掛任務編號(job_execution_uid)同一招;不開新表、不套 migration;複製資源庫連圖一起 clone 免費帶走;沒有第二份真相 |
另開「AO 任務說明」表:顧問刪方塊時表上殘留、複製要多抄、要 RLS 與 migration |
| D2 | 啟動後顧問在資源庫頁改三欄,守門=資源庫編輯權(能力點 oscal.resource-library.update + verify_module_frame_exists_by_uid(writable=True) 歸屬守門 + 稽核鎖 assert_template_not_audit_locked),不要求專案 manager;端點只開三欄。已綁專案時這三欄直接寫進專案目前輪次的任務,非專案成員的顧問也能改——這是本意,決策者 2026-10-22 再次確認(arc-review 重要級 3 曾列為疑慮) |
決策者裁「顧問只改三欄文字,流程歸 PM」;顧問通常不是專案成員;三欄是說明文字不是稽核範圖 | 要求同時是專案 manager:顧問根本進不來 |
| D3 | 啟動後資源庫頁讀專案目前輪次的任務(wecm 按 round_id,與任務配置匯出同一條查法) | 覆核輪啟動後顧問改的應是新輪;舊輪已進稽核不該再動 | 固定第 1 輪 |
| D4 | Excel 匯入對不到的列跳過、其餘照寫,預覽列出跳過數 | 與既有任務配置匯入一致;PM 改過任務後整份退回會永遠匯不進去 | 一列對不到整份退回 |
| D6 | 資源庫頁每條 AO 的「流程編輯器」按鈕拿掉;資源庫階段每條 AO 固定一個範本預設任務,顧問只填三欄;流程一律啟動後 PM 在專案規劃頁改 | 決策者裁「不想讓顧問有這麼大權限」;現況那顆鈕設了也白設(啟動只認第一個方塊);拿掉後資源庫永遠一 AO 一任務,畫面、Excel、啟動邏輯都不必處理多方塊 | 留著鈕並把啟動改成整張圖都長任務:權限方向相反,且動啟動與覆核輪共用核心 |
| D7 | 資源庫編輯頁「適用控制項」頁籤版型改成與稽核計畫頁(FR-141 ApObjectiveTree+ApObjectiveAoForm)相同:左樹群組 → 控制項 → AO(每條 AO 一節點,圓點標任務說明填了沒),右欄一次只顯示一個:點控制項顯示控制項欄位、底下列這條控制項的 AO 清單可點跳過去(圓點、編號、文字、任務數與有說明數),點 AO 顯示那條 AO 的全部欄位+任務說明;上一條/下一條。兩個匯入預覽不動;專案規劃頁另案 |
決策者原話「有的 AO 一多這邊就爆掉了」「比照稽核計畫、稽核作業那樣」;現行頁右欄把一條控制項的全部 AO(DEV 最多 15 條)直向堆,加任務說明後更長 | 右欄上段固定控制項欄位、下段 AO(兩段都長);三欄(多一欄,與稽核計畫頁不一致) |
| 撤銷 | PM 在專案規劃頁改方塊名稱與任務列名稱的關係是現況既有行為(兩邊各自、不同步),與本案無關 | — |
| 接入點 | 現況 | 本案動作 | 卡 |
|---|---|---|---|
建資源庫初始化 AO 流程圖 app/oscal/service/resource_library_app_service.py:_init_ao_workflows()(L269) |
每條 AO 一張圖、一個方塊「Evidence upload」,uid 寫進 AO props | 不動;那個方塊就是範本預設任務 | — |
專案啟動長任務 app/flow_control/service/prep_job_generation_service.py:generate()(L122) |
只認第一個 userTask;label = f"{cid} / {ao_part_id} 收證據"(L241)寫死名稱;描述指南不帶 |
名稱改取方塊 name(空則退回原 label);描述指南取方塊屬性 | 2a |
覆核輪長任務 同一支 reclone_from_props=True |
同上 | 名稱、描述、指南從上一輪同 AO 任務列抄;配對用 reverify_inheritance_service 的 get_job_pairs |
2b |
覆核輪承襲 app/flow_control/service/reverify_inheritance_service.py:inherit_from_parent()(L40) |
抄指派、部門、設備、證據、問卷,不抄三欄 | 配對結果多抄三欄 | 2b |
編輯器存檔三向比對 app/flow_control/service/workflow_xml_sync_service.py:sync()(L64) |
圖上沒編號的方塊建任務、沒在圖上的任務刪 | 不動;資源庫頁的寫圖不經它(只改屬性不增刪方塊) | — |
資源庫一對一 infra/readmodel/oscal/resource_library_binding_query.py(ever_bound/has_live_project) |
已有 | 3a 用它判「讀圖還是讀任務」;要再補一支「資源庫 → 專案 id 與目前輪次 id」 | 3a |
資源庫寫入守門 domain/module_frame/service/module_frame_domain_service.py:verify_module_frame_exists_by_uid(writable=True)(L79)、assert_template_not_audit_locked(L98) |
已有 | 3a 兩支寫入端點照走 | 3a |
任務三欄更新 app/flow_control/service/job_service.py:update_job()(L331) |
收三欄,守專案 manager | 不動;3a 啟動後寫三欄走 domain service update_job 只帶三欄、守門換 D2 |
3a |
圖 XML 雙寫 infra/flow_control/repository/flow_control_job_repo_impl.py:write_template_xml() |
主表與翻譯列一起寫 | 3a 寫圖用它 | 3a |
FE 資源庫頁 src/views/module_frame/ModuleFrameTemplateEditView.vue(1,954 行) |
AO 展開區 L1496~;流程編輯器鈕 L1479~1487 | 新區塊開子元件,原檔只加一行掛載、刪那顆鈕 | 3b |
FE 任務表單 src/components/grc/planning/AoTaskForm.vue(L395~420) |
三格已存在 | 3b 照這三格的樣子刻 | 3b |
任務配置匯出匯入 app/flow_control/service/job_import_service.py(546 行) |
十欄、一任務一列、AO 欄不合併、匯入不收三欄 | 4b 加兩欄、合併、收三欄;加欄時把表頭常數與列組裝抽到共用 builder,不往裡塞 | 4a、4b |
匯出查詢 infra/readmodel/tasks/job_export_query.py:get_export_data()(L85) |
回 job_name,沒 description/guide |
加兩欄 | 4a |
改善計畫匯出 app/flow_control/service/poam_workbook_export.py:_plan_sheet()(L131~205) |
AO 多列合併、列高平分、灰黃著色 | 合併與著色寫法借用 | 4a |
複製資源庫 app/module_frame/service/resource_library_copy_service.py:_copy_ao_workflows()(L102) |
連圖 clone | 不動,三欄在圖上自動帶走 | — |
name 是方塊的 name 屬性;描述與指南是 <camunda:properties> 底下兩個 <camunda:property name="task_description" value="…"/>、name="task_guide"。job_executions.name/description/guide。新檔 app/flow_control/service/task_node_props.py,純函式、不碰 DB:
read_first_task(xml) -> {node_id, name, description, guide} | None:用 BpmnUtils(xml).get_jobs() 取第一個 userTask(與 _first_user_task_id 同一個「第一個」),從 properties 取兩個屬性。write_first_task(xml, name, description, guide) -> xml:只改第一個 userTask 的 name 與那兩個屬性;不動方塊 id、線、其他屬性(含 job_execution_uid);屬性值為空就把該屬性拿掉,<camunda:properties> 清到空要整段拿掉(空節點會讓 BpmnUtils 解析炸,見 flow_apply_to_tasks_service.py:_strip_uid 註解)。flow_apply_to_tasks_service.py 用 xmltodict parse/unparse。prep_job_generation_service.py:generate() 專案啟動模式(reclone_from_props=False):clone 完範本後 read_first_task(cloned.xml),name 有值就用它當 label,否則維持 f"{cid} / {ao_part_id} 收證據";description/guide 直接寫進 JobExecutionEntity。workflow_execution 的 name 維持原 label(它是流程實例名稱,不是任務名稱)。
generate() 覆核輪模式不讀圖(上一輪的圖方塊名稱是上一輪 clone 當下的,PM 之後改的任務名稱不在圖上)。改在 reverify_inheritance_service.py:inherit_from_parent() 的 get_job_pairs 迴圈裡,每對新舊任務把舊任務的三欄 UPDATE 到新任務(infra 層加一支 clone_task_text_fields(pair),與 clone_assignees 同形)。
新 app/module_frame/service/ao_task_guidance_service.py(@transaction,守門見 D2):
| 端點 | 做什麼 |
|---|---|
GET /module-frame/<uid>/control/<control_code>/task-guidance |
回這條控制項所有 AO 的任務清單:`{source: "template" |
PUT /module-frame/<uid>/task-guidance |
body [{key, name, description, guide}],批次寫。template 模式:解析 key 找 AO 的圖 uid,write_first_task 後走 write_template_xml 雙寫;project 模式:走 flow_control domain update_job 只帶三欄,不經 job_service.update_job(那支守專案 manager) |
左樹計數(已填說明/任務數)由同一支 GET 的聚合版 GET /module-frame/<uid>/task-guidance/summary 回每條控制項 {code, filled, total},一次 SQL/一次解析,不逐條打。
新 readmodel infra/readmodel/module_frame/ao_task_guidance_query.py:綁定資源庫 → 專案 id、目前輪次 id;按輪次撈 AO 任務(與 job_export_query.py 同一條 wecm 查法)。
新子元件 src/components/grc/module-frame/AoTaskGuidanceSection.vue,掛在 ModuleFrameTemplateEditView.vue AO 展開區「參考程序書」之後,一行 <AoTaskGuidanceSection :module-frame-uid :control-code :readonly="isVersionView || writeDisabled()" />。元件自己打 GET、三格 @change 累積、頁面儲存時 emit 批次 PUT。版本檢視(isVersionView)唯讀顯示三欄文字。
長相照雛形畫面 1:template 一張小卡;project 多張小卡+上方流程帶,小卡右上角狀態徽章與指派人(唯讀)。刪掉 L1479~1487 的流程編輯器 <button> 與 openBpmnEditor(D6)。
共用 builder app/flow_control/service/task_guidance_workbook.py:
| 欄 | 內容 | 匯入 |
|---|---|---|
| A(隱藏) | 比對鍵:template 模式 `<part_id> | <node_id>`,project 模式 job_uid |
| B 控制項群組、C 控制項識別碼、D 控制項名稱、E 評估目標 | 灰底唯讀;同一 AO 多列時 B~E merge_cells |
不讀 |
| F 順序 | 流程順序 1..n | 不讀 |
| G 任務名稱、H 描述、I 指南 | 黃底可改 | 讀、只寫這三欄 |
| (專案端加)J 任務類型、K 指派人員、L 參與部門、M 設備 | 既有欄位保留 | 既有邏輯 |
另一頁「填寫說明」(照 prototype/sample/make_sample_xlsx.py 的六條)。合併、列高、著色借 poam_workbook_export.py:_plan_sheet() 的寫法。
匯入三段:parse(回每列的新舊值與變更旗標、跳過清單)→ FE 預覽只標變更格 → confirm(只寫有變更且 key 對得到的列;D4 跳過不報錯)。資源庫端端點 POST /module-frame/<uid>/task-guidance/export、.../import/parse、.../import/confirm;守門同 3a。
專案端 job_import_service.py 改用共用 builder 產前九欄,_EXPORT_COLUMNS 加 description/guide,confirm_import 多把三欄傳進 update_job。job_export_query.py 回 description/guide。
@require_capability("oscal.resource-library.update") + service 內 verify_module_frame_exists_by_uid(uid, writable=True) + assert_template_not_audit_locked。| 卡 | 做什麼 | 碰哪裡 | 依賴 | 驗收 |
|---|---|---|---|---|
| 2a | 方塊屬性 helper+專案啟動帶三欄 | task_node_props.py 新檔;prep_job_generation_service.py L229~252 |
— | 驗收 4 |
| 2b | 覆核輪抄三欄 | reverify_inheritance_service.py、reverify_clone_query 加 clone_task_text_fields |
2a | 驗收 7 |
| 3a | 任務說明讀寫 API+summary+readmodel | ao_task_guidance_service.py 新、route、ao_task_guidance_query.py 新 |
2a helper | 驗收 1、2、5 的 API 面 |
| 3c | 版型比照稽核計畫頁:左樹到 AO、右欄單一控制項或 AO 表單(D7) | 新 MfControlTree.vue、MfControlForm.vue、MfAoForm.vue(從 ModuleFrameTemplateEditView.vue L1173~1560 抽出);原檔變薄 |
— | 左樹點控制項/AO 右欄各只顯示一個;既有欄位與存檔行為不變 |
| 3b | AO 表單底下任務說明子元件、左樹圓點、收流程編輯器入口 | AoTaskGuidanceSection.vue 新,掛進 MfAoForm.vue |
3a、3c | 驗收 1、2、5、8 |
| 4a | Excel 共用 builder、資源庫端匯出/parse/confirm | task_guidance_workbook.py 新、route、job_export_query.py 加兩欄 |
3a readmodel | 驗收 3、6 前半 |
| 4b | 專案端任務配置匯出匯入對齊 | job_import_service.py(抽共用、不往裡塞) |
4a | 驗收 6 後半 |
| 4c | FE 匯出匯入對話框與預覽 | FE | 3b、4a | 驗收 3 |
| 坑 | 在哪 | 怎麼避 |
|---|---|---|
| 圖的 XML 是可翻譯欄位,只寫主表編輯器看不到 | prep_job_generation_service.py L262 註解 |
寫圖一律 write_template_xml 雙寫 |
資源庫頁寫方塊屬性時動到 job_execution_uid 或方塊 id |
三向比對 workflow_xml_sync_service.py L94 會判成新任務或查不到 |
helper 只改 name 與兩個自家屬性,其餘原樣 |
空的 <camunda:properties/> 讓解析炸 |
flow_apply_to_tasks_service.py:_strip_uid |
清到空整段拿掉 |
啟動後走 job_service.update_job 會擋顧問 |
它守專案 manager | 3a 走 domain update_job 只帶三欄,守門換 D2 |
| 資源庫側行為要用資源庫入口驗 | FR-139 T-24 教訓 | 驗收用 /module-frame/... 端點,不拿專案端點頂 |
ModuleFrameTemplateEditView.vue 超標 |
1,954 行 | 新區塊必開子元件,原檔只加一行、刪一顆鈕 |
job_import_service.py 超標邊緣 |
546 行 | 4b 把表頭與列組裝抽到共用 builder,淨行數不增 |
| 改 service 後共用 BE 跑舊程式 | 2026-10-15 教訓 | 驗收前先重啟 |