FR-140 · 設計定案 · 2026-10-15
D1~D6、P1~P6 全部定案;六塊設計、十二張卡
一個稽核輪次走完整條流程:專案規劃與執行 → 內部稽核 → 有缺失就改善、把不合格的原任務卡退回原指派人重做、再內稽一次 → 內部沒缺失才進外部稽核 → 外部有缺失走既有的覆核輪。每一次稽核(第 1 次內稽、第 2 次內稽、外稽)各留一組 SSP 快照、評估計畫、評估結果、改善計畫,互不覆蓋。做法是把輪次上四個單值欄位改成一輪多筆的「稽核次」,引擎在流程第二次走到同一站時開新任務卡,任務卡加完成期限與到期提醒。本文件寫定案規則、為什麼這樣定、踩到會怎樣。
🔴 這份是設計決策,不是現況。 內文停在 2026-10-15 定稿當時,實作改動不回頭改寫這裡。
狀態:設計定案(2026-10-15),待開卡|母卡 CM-2765|討論稿:
discussion.md
檔案路徑與行號以 2026-10-15 為準,查不到時以函式名重新定位。路徑慣例:BE 路徑相對
compliance-manager-be/;pkg/代表~/Projects/Jedicogy/module/jedi-python-package/;FE 路徑相對compliance-manager-fe/。
名詞:輪次=稽核輪次(compliance.project_audit_rounds),一輪對應一次專案稽核流程;稽核次=一輪內每做一次稽核(內部或外部)就是一筆(compliance.audit_round_passes);AP=評估計畫(assessment plan);AR=評估結果(assessment results);POA&M=缺失改善計畫(plan of action and milestones);job=流程引擎依範本站產生的任務卡執行紀錄(compliance.job_executions);站=範本(BPMN 流程圖)上的一個使用者任務節點;reverse 連線=範本上標記為「往回走」的連線;wecm=任務卡對控制項的對應表(public.workflow_execution_control_mapping)。
| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-15 | v1:D1~D6、P1~P6 設計定案;稽核次資料模型、輪次狀態機改綁、範本稽核性質、引擎回流開新卡、退卡與期限、覆核輪期限承襲六塊設計;十二張卡拆分與端到端驗收 | 母卡 CM-2765 |
| 2026-10-16 | 實作與複測後三條規則被推翻,正文不改寫、只在對應處標註:P6 進度條改「七站全畫、顏色只講這一次」;退回改沿實際軌跡、可跨稽核次;回規劃後到下一次計畫站前,三處顯示用推算補上這一次。理由見 docs/analysis/2026-10-16-fr140-progress-and-rollback-redesign.md |
母卡 CM-2765 |
整案一句話:一輪就是一條完整流程,內稽不合格就把那幾張原任務卡退回重做、再稽核一次,內稽過了才進外稽;每一次稽核各留一組 SSP 快照+AP+AR+POA&M,看得出是內部或外部、第幾次。
| 設計塊 | 做什麼 | 產出 | 完成怎麼判定(決策者親手做什麼、看到什麼) |
|---|---|---|---|
| 一、稽核次表 | 把輪次上四個單值槽位(SSP、AP、AR、POA&M)改成一輪多筆 | compliance.audit_round_passes 表、輪次加 current_pass_id、既有輪次搬成第 1 次的 migration |
在 DEV 查一個既有輪次,看到它有一筆第 1 次稽核次、四個值與輪次上的一致;用既有範本跑一輪,畫面行為與現在相同 |
| 二、輪次狀態機改綁稽核次 | 四個站方法的斷言與寫入改到「目前稽核次」;每次走到稽核計畫填寫站開一筆新的稽核次 | audit_round_app_service.py 改寫、finalize_audit 加 terminal 參數、輪次 status 與目前稽核次同步 |
跑完第 1 次內稽有缺失、退回、再跑第 2 次內稽;查 DB 看到兩筆稽核次、各自的 AP/AR/POA&M 不同,第 1 次的資料沒被蓋掉 |
| 三、範本 | 站多一個「稽核性質」(內部/外部)下拉;新公版「內外稽核流程」範本 | system_menus 的 audit_nature 選項、範本編輯器下拉、新公版 BPMN seed |
打開範本編輯器,點稽核計畫填寫站看到「稽核性質」下拉可選內部/外部;公版清單多一個「內外稽核流程」,打開看到兩段稽核與一條往回走的虛線 |
| 四、引擎回流開新卡 | 流程第二次走到同一站時新建一筆 job,不再重用第一次那筆 | 兩份 complete_*_job 改寫、attempt_no 欄、依站 element id 找 job |
退回後重做的卡在任務歷程看得到兩筆,第一次的完成時間還在 |
| 五、退卡與期限 | 內稽不合格的控制項找到原任務卡退回原指派人、壓上 POA&M 里程碑日期當期限、留重做紀錄;到期提醒 | job_executions.due_at 全鏈、退卡 handler、job_execution_redo_log 表、job_due 提醒 |
內稽標 3 條不合格並填好改善計畫里程碑,完成改善站後,原指派人的「我的任務」出現 3 張退回卡、期限=里程碑日期、收到通知;離到期 3 天時收到提醒信 |
| 六、覆核輪期限承襲 | 外稽不合格開覆核輪時,新卡帶期限 | _generate_reverify_prep_jobs 寫 due_at |
外稽標不合格並填里程碑、啟動覆核輪,覆核輪的新卡期限=外稽改善計畫的里程碑日期 |
依賴:一→二→(四、五)是主線;三與五的期限欄(BE+FE)可平行先做;最先看得到畫面的是第三塊的範本編輯器下拉(卡 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'}}}%%
flowchart LR
subgraph G1[地基]
K1[卡1 稽核次表<br/>+migration]
K2[卡2 稽核性質選單<br/>+公版範本+編輯器下拉]
K6[卡6 期限欄<br/>BE 全鏈]
end
subgraph G2[主線]
K3[卡3 開新稽核次<br/>launch_audit 改綁]
K4[卡4 其餘三站改綁<br/>+terminal+回規劃]
K5[卡5 引擎回流開新 job]
K8[卡8 回流退卡 handler]
K7[卡7 FE 期限欄]
end
subgraph G3[收尾]
K9[卡9 到期提醒]
K10[卡10 總覽稽核次切換<br/>+標題帶第 N 次]
K11[卡11 覆核輪期限承襲]
K12[卡12 流程進度條]
end
K1 --> K3 --> K4
K4 --> K5
K4 --> K8
K5 --> K8
K6 --> K7
K6 --> K8
K2 --> K3
K8 --> K9
K4 --> K10
K6 --> K11
K5 --> K12
%%{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 TD
plan["專案規劃+執行"] --> iap["內部稽核計畫"]
iap --> iaudit["內部稽核"]
iaudit --> g1{"有缺失?"}
g1 -- 有 --> ipoam["內部缺失改善(POA&M)"]
ipoam -. 退回 .-> plan
g1 -- 無 --> eap["外部稽核計畫(全範圍)"]
eap --> eaudit["外部稽核"]
eaudit --> g2{"有缺失?"}
g2 -- 無 --> done(["結案"])
g2 -- 有 --> epoam["外部 POA&M"]
epoam --> endr(["本輪結束(待覆核)"])
endr -. 發起覆核 .-> rev["覆核輪(現有機制,新的一輪)"]
逐句需求(決策者 2026-10-15 確認):
內稽回流與覆核輪看起來都是「不合格再做一次」,但是兩件事:
| 比較 | 內稽回流(本案新做) | 覆核輪(既有機制,不動) |
|---|---|---|
| 發生在哪 | 同一輪內,流程退回規劃站 | 新開一輪 |
| 任務卡 | 原卡退回原指派人 | 新卡,承襲母輪資料 |
| PM 看到的範圍 | 全部控制項,其中幾張卡變回待處理 | 只有缺失項 |
| 下一次的 AP | 從上一次內稽 AP 縮到不合格項 | 只建缺失項 |
決策者 2026-10-15 全部裁定。
| 項 | 定案 | 理由 | 被排除的方案 |
|---|---|---|---|
| D1 不合格怎麼重做 | 內稽不合格的任務卡原卡退回:status 回「進行中」(沿用既有退回語意,指派人不必再按開始)、end_time 清空,指派人、部門、設備、證據都不動;另寫一筆重做紀錄保留歷次完成時間。外稽不合格走覆核輪、開新卡 | 內稽回流是同一輪內的修正,PM 要在同一張全控制項的圖上看到哪幾張卡退回;原卡退回讓指派人在原地補做,證據與指派關係不必重建。外稽是對外的正式結果,維持既有覆核輪 | 內稽也開新卡(證據與指派要重建,PM 頁會出現重複卡);外稽也原卡退回(決策者明示外稽不過維持開新輪) |
| D2 第 N 次的 AP 範圍 | 第 N 次(N≥2)內稽的 AP 從上一次內稽 AP 複製、縮到 not_met 控制項,沿用覆核輪的 clone 邏輯、來源改成同一輪上一次稽核次;外稽 AP 一律全範圍新建 | 第 2 次內稽只是確認上次不合格的項目改好了,不必重查全部;外稽是正式稽核,必須涵蓋全範圍;clone 邏輯已在覆核輪驗證過,換來源即可重用 | 每次內稽都全範圍(重查已合格項目,徒增工作);另寫一套縮範圍邏輯(與覆核輪重複) |
| D3 怎麼留下每一次稽核 | 新增稽核次表,一輪多筆,每筆帶 SSP 快照、AP、AR、POA&M;概念同 living SSP:活本是任務卡與證據,每次走到「稽核計畫填寫」站凍一組 | 輪次上四個槽位是單值,第二次稽核必然覆寫第一次;需求要求每一次都留得下來且分得出內外與次數,只能改成多筆 | 維持四個單值槽位(第二次蓋掉第一次);每次稽核開一個新輪(違反「一輪=一條流程」,PM 頁會拆成多輪) |
| D4 引擎改多少 | 引擎 token 走法與範本驗證規則都不動;只改「回到同一站時新開一筆 job」與站 handler 的綁定 | 範本已畫得出退回再稽核的圖(同站碼可重複、reverse 與閘道都跳過檢查),問題只在 job 重用與輪次單向,改動收在這兩處最小 | 改引擎走法或驗證規則(動到所有既有範本的行為) |
| D5 覆核輪 | 照現有方式開新輪、新卡承襲、只建缺失項;唯一新增是新卡帶上期限 | 外稽不過維持開新輪是決策者明示;期限是本案新加的欄位,覆核輪的卡也要有才一致 | 把覆核輪改成輪內回流(決策者明示不做) |
| D6 舊資料與舊範本 | migration 把既有輪次的四槽位搬成第 1 次稽核次;舊範本一輪一次照常跑 | 既有輪次與範本不能因本案失效;搬成第 1 次後,所有讀取者統一讀「目前稽核次」,不必分新舊兩條路 | 新舊兩條讀取路徑並存(每個讀取者都要判斷新舊,漏一處就壞) |
| P1 記不記重做來源 | 記:稽核次帶 prev_pass_id,第 2 次內稽指向第 1 次 |
畫面串得出「第 1 次 → 第 2 次」的軌跡,不必用序號推算 | 不記,靠 audit_nature+nature_seq 推算 |
| P2 輪次自己的 status | 保留欄位與七態,跟著目前稽核次同步寫;斷言改綁稽核次 | 讀輪次 status 的地方很多,保留七態舊畫面不會壞 | 收成粗狀態(進行中/待覆核/結案)——所有讀者都要改 |
| P3 第 2 次內稽無缺失時輪次怎麼不被結案 | finalize_audit 加參數 terminal,由 handler 依「這站之後是否接結束事件」決定;terminal=true 才把輪次設 closed |
舊範本(稽核站後直接接結束)行為不變;新範本內稽後還有外稽時不會被提早結案 | 一律不設 closed、改由範本結束事件結案(舊範本行為跟著變) |
| P4 到期提醒 | 到期前 3 天、到期當天兩個時點,寄指派人+PM;沿用 task_alert_notifications,object_type 新增 job_due,stage 為 due_3d/due_today |
兩個時點足以提醒又不擾人;沿用既有提醒表與排程,不另立機制 | 前 7 天、前 1 天、逾期當天三個時點;只在逾期當天寄指派人 |
| P5 期限可否手改 | PM 可手改,改了標「手動調整」,不回寫 POA&M | 兩邊責任分清楚:POA&M 是改善計畫,任務卡期限是執行安排 | 不可改、只能去改 POA&M(PM 調排程要繞一圈) |
| P6 進度怎麼顯示(⚠ 進度條規則已推翻,見變更紀錄 2026-10-16) | 總覽頁輪次下拉底下多一層「第 N 次內部/外部」切換,另畫一條照範本站排列的進度條;進度條直接讀引擎目前所在的站,不推算 | 一輪內有多次稽核,沒有切換與進度條看不出現在走到哪;讀引擎目前站才不會與實際狀態漂移 | 只加切換、不畫進度條;進度條依輪次 status 推算(回流時會推錯) |
稽核次的想法與既有的 living SSP 相同:有一份一直在變的「活本」,每到某個時點就凍一份下來。
專案有一份持續編輯的 SSP(活本)。啟動稽核時把它凍成一份快照,稽核只看快照;活本繼續改,不影響正在進行的稽核。
活本是任務卡與證據:指派人一直在上面做事。每次流程走到「稽核計畫填寫」站,就凍一組 SSP 快照+AP+AR+POA&M 成為一筆稽核次。第 2 次內稽、外稽各自再凍一組,前面的不會被覆蓋。
| 比較 | Living SSP | 稽核次 |
|---|---|---|
| 活本 | 專案的 SSP | 任務卡與證據 |
| 凍結時機 | 啟動稽核 | 每次走到「稽核計畫填寫」站 |
| 凍結內容 | 一份 SSP | SSP 快照+AP+AR+POA&M 一組 |
| 掛在哪 | 輪次的 ssp_id |
稽核次表,一輪多筆 |
| 第 N 次怎麼來 | — | 內稽第 N 次(N≥2)的 AP 從上一次內稽複製、縮到不合格項;外稽 AP 一律全範圍新建 |
每個接入點列現況與本案動作,座標以 2026-10-15 為準。
| 接入點 | 現況 | 本案動作 |
|---|---|---|
| 建 job | pkg/jedi-flow-engine/app/service/workflow_execution_service.py:308-335;主專案版 app/flow_engine/service/workflow_execution_service.py:612-640(complete_main_workflow_job 從 :512 起)。查重 key=(workflow_execution_id, template_id, template_job_id),查到就重用並改回 PROCESSING(主專案版 :625-629 覆寫 start_time、不清 end_time)。同一站第二次走到不會建新筆,第一次的紀錄被蓋掉 |
遇到同站已 COMPLETED 時新建一筆、加 attempt_no(第四塊) |
| COMPLETED 判定 | 套件 :285-292;主專案 :590-609(多排除 CANCEL) | 兩份一致改成「看最新一筆」 |
| 退回 job | 主專案 workflow_execution_service.py:813 revert_job 已有::832 輪次凍結檢查;:882-884 目標改 PROCESSING、清 end_time;:887-889 notify_user_todo_job 通知;:910-912 目前 job 回 TODO、後續 job 全回 TODO。套件版 :339-422 沒有通知 |
不整段呼叫(它會把目標之後所有 job 回 TODO,只退幾張時會波及其他卡);把「單卡回 PROCESSING、清 end_time、通知」抽成單卡函式(新檔),回流 handler 逐張呼叫(第五塊) |
| job 對控制項 | public.workflow_execution_control_mapping(wecm):control_id varchar(50) 見 scripts/sql/2026-06-15-fr038-q1-workflow-execution-control-mapping.sql:18-21、ao_part_id 見同檔 :35;round_id 見 2026-07-22-fr053-wecm-round-id.sql:16 |
退卡時用 (control_id, ao_part_id, round_id) 找卡 |
| 接入點 | 現況 | 本案動作 |
|---|---|---|
| 同一站碼出現兩次 | pkg/.../common/utils/bpmn_topology_validator.py:82-102、:195-212 只檢查站碼存在,不擋重複 |
不改;選配加「同站碼多次必帶 audit_nature」 |
| allowed_successors 檢查 | 同檔 :232 只查使用者任務直連使用者任務;目標是閘道或結束事件就跳過(:225-228) | 不改 |
| reverse 連線 | sequenceFlow 帶 camunda:property name="reverse" value="true"(:138-154,判斷在 :144) |
新公版範本的「內部缺失改善 → 規劃」必須標 reverse |
| 站之間允許的下一站 | scripts/sql/2026-09-14-fr093-4d-flow-templates-stage-objects.sql:194/201/222/229:planning→ap_authoring、ap_authoring→audit、audit→poam、poam→[] |
不改(閘道與 reverse 都會被跳過檢查) |
| 閘道條件 | 字串 ${has_findings == true}(scripts/sql/seeds/bpmn/builtin-full-audit-with-review.bpmn:91);組字串 bpmn_uilts.py:414-437;比對是字串全等(:322-327) |
新範本照抄同一字串 |
| 站屬性讀取 | 現有 stage_object_code、main_role;parser 把所有 camunda:property 讀進 JobDTO.properties(bpmn_uilts.py:70-76),加 audit_nature 不用改 parser |
新增 audit_nature 屬性 |
| 站屬性消費端 | stage_advance_service.py:115,251,497,586-587,648;stage_rollback_service.py:181-187 的 _find_job_by_stage_code 只回第一筆 |
同站碼出現兩次會找錯站,改用 element id |
| 範本編輯器 | FE src/views/flow-template/FlowTemplateEditorView.vue:userTaskForm :244、讀屬性 :278-280(helper :311-319)、寫屬性 :322-360、面板 :1093-1126、驗證 :592-605 |
面板加「稽核性質」下拉 |
站 handler(app/flow_control/service/oscal_stage_handlers.py:8-11,42-106)呼叫 pkg/jedi-compliance-audit/app/service/audit_round_app_service.py:
| 站 | 方法 | 斷言 | 推進後 |
|---|---|---|---|
| planning | launch_audit |
:413 status ≠ planning 就擋 | 轉 audit_planning(:438);:435 寫 assessment_plan_id、:437 寫 ssp_id,直接覆寫 |
| ap_authoring | start_auditing |
:474 ≠ audit_planning 就擋;:476 沒有 AP 擋 | 轉 auditing(:487);:481 只在 not ar_result_id 時建 AR,第二次會沿用舊 AR |
| audit | finalize_audit |
:506 ≠ auditing 就擋 | 沒有不合格→closed(:513);有→remediation(:520),:521 寫 poam_id |
| poam | close_round |
:582 ≠ remediation 就擋;:569 POA&M 全關才可 | 母輪轉 pending_reverify(:584) |
閘道判斷 has_findings:stage_advance_service.py:614-628,等於 handler_result.status == "remediation";finalize_audit 讀 assessment_result_app_service.py:823-845(:830 讀 round.ar_result_id)。
四槽位(ssp_id、assessment_plan_id、ar_result_id、poam_id)的讀取者,全部要改讀「目前稽核次」:
| repo | 位置 |
|---|---|
| 主專案 | oscal_stage_preconditions.py:53,57;assessment_plan_app_service.py:204,222,526,528;app/oscal/service/ssp_control_round_scope.py:10,49-66,76,91,93;ssp_control_implementation_service.py:285,290,298,304;stage_advance_service.py:558;oscal_stage_rollback_handlers.py:39 |
| 套件 | audit_round_app_service.py:189-203,634-637;assessment_result_app_service.py:9,208,360-379,748,750,830;poam_app_service.py:422,424,443,450;infra/repository/flow_control_task_setup_repo_impl.py:33;project_audit_round_repo_impl.py:50 |
覆核輪:launch_reverify(audit_round_app_service.py:596)clone AP 並縮到 not_met、_generate_reverify_prep_jobs(:732)產卡、ReverifyInheritanceService 承襲;承襲查詢 infra/flow_control/repository/reverify_clone_query.py:31;DEV 範例 project 1321。
| 接入點 | 現況 | 本案動作 |
|---|---|---|
| job 欄位 | compliance.job_executions 只有 start_time/end_time/next_due_at |
加 due_at |
| ORM/entity/mapper/repo | pkg/jedi-flow-engine/infra/models/job_execution.py:16-47;domain/entity/job_execution_entity.py:34;mapper :34;repo update 只寫 status 與時間 |
全鏈補 due_at |
| task-platform entity | pkg/jedi-task-platform/.../flow_control_job_entity.py:126 |
補 due_at |
| 詳情 API | api/flow_control/routes/job_route.py:75-94 → job_service.py:178 → job_dto.py:226-265 → serializers/job.py:166-183 |
回傳 due_at 與「手動調整」標記 |
| 我的任務 | view scripts/sql/view/vw_user_job_queue.sql:62-63 → my_grc_jobs_query.py:96 |
view 加欄(重建要 security_invoker) |
| 提醒排程 | core/scheduler.py:622-646;task_alert_notifications 的 object_type CHECK(2026-10-13-fr137-task-alert-notifications-object.sql:30-36) |
新增 job_due,CHECK 放寬 |
| POA&M 里程碑 | oscal.poam_milestones.target_date;套件 poam_app_service.py _item_control_id(:183)、_deadline(:212) |
期限取該控制項最早的 target_date |
| FE 頁面 | MyTasksView.vue(751 行)、JobExecutionDrawer.vue(2393 行,已超標) |
新邏輯開 composable/子元件新檔 |
🔴 真正的牆在輪次層,不在引擎
範本不擋:同一站碼可以出現兩次、reverse 連線與閘道都會被跳過檢查,畫得出「退回再稽核一次」的圖。
引擎會重用 job:同一站第二次走到時拿第一次那筆改回進行中,第一次的完成紀錄被蓋掉——這是第四塊要改的。
輪次是單向的:每個站方法都斷言「目前狀態必須是上一階段」,而且 SSP、AP、AR、POA&M 只有四個單值槽位,第二次稽核會直接覆寫第一次。這是本案的核心,也是第一、二塊要改的。
compliance.audit_round_passes| 欄位 | 型別 | 說明 |
|---|---|---|
id |
bigserial PK | |
uid |
uuid | 對外識別 |
round_id |
FK → project_audit_rounds.id |
屬於哪一輪 |
pass_no |
int | 輪內序號,1 起算 |
audit_nature |
varchar | internal/external,值來自 system_menus group audit_nature;舊資料可為 NULL(畫面顯示「未標示」) |
nature_seq |
int | 同性質第幾次(第 2 次內部、第 1 次外部) |
prev_pass_id |
FK → 本表,可 NULL | 這次是哪一次的重做(P1):第 N 次內稽(N≥2)指向同一輪上一次內稽,其餘為 NULL |
status |
varchar | planning/audit_planning/auditing/remediation/closed,沿用 RoundStatus 子集 |
ssp_id |
這次凍結的 SSP 快照 | |
assessment_plan_id |
這次的 AP | |
ar_result_id |
這次的 AR result | |
poam_id |
這次的 POA&M | |
template_job_id |
對應範本上的 BPMN element | |
created_user/created_time/updated_user/updated_time |
審計欄 |
GRANT ... TO cm_app(含 sequence 權限)。
| 表 | 欄位 | 說明 |
|---|---|---|
compliance.project_audit_rounds |
current_pass_id |
指向目前稽核次;輪次上四個槽位與 status 保留,跟著目前稽核次同步寫(P2) |
compliance.job_executions |
due_at timestamptz |
任務卡完成期限 |
compliance.job_executions |
attempt_no |
同一站第幾次走到(第四塊) |
compliance.job_execution_redo_log| 欄位 | 說明 |
|---|---|
job_execution_id |
被退回的卡 |
pass_id |
因哪一次稽核不合格而退回 |
prev_end_time |
退回前的完成時間(退回會清 end_time,靠這欄保留歷次完成時間) |
revert_at |
退回時間 |
既有輪次的搬移規則見「一、稽核次表」。
%%{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'}}}%%
erDiagram
project_audit_rounds ||--o{ audit_round_passes : "一輪多次"
project_audit_rounds |o--o| audit_round_passes : "current_pass_id"
audit_round_passes |o--o| audit_round_passes : "prev_pass_id"
audit_round_passes }o--|| ssp_snapshot : "ssp_id"
audit_round_passes }o--o| assessment_plan : "assessment_plan_id"
audit_round_passes }o--o| ar_result : "ar_result_id"
audit_round_passes }o--o| poam : "poam_id"
job_executions ||--o{ job_execution_redo_log : "歷次退回"
audit_round_passes ||--o{ job_execution_redo_log : "因這次退回"
以「第 1 次內稽有 3 條不合格 → 退卡重做 → 第 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 PM as PM
participant U as 指派人
participant A as 稽核員
participant E as 流程引擎
participant R as 輪次服務
participant P as 稽核次
PM->>E: 啟動專案,產出規劃+執行任務卡
U->>E: 完成任務卡
E->>R: 走到內部稽核計畫填寫站
R->>P: 開新稽核次,拍 SSP 快照、建 AP
Note over P: 開新稽核次 #1 內部
A->>E: 填完 AP 送出
E->>R: start_auditing
R->>P: 在 #1 建新 AR result
A->>E: 內部稽核完成,3 條不合格
E->>R: finalize_audit
R->>P: #1 轉 remediation,寫 poam_id
PM->>E: 內部缺失改善完成,POA&M 全關
E->>R: close_round,下一條是 reverse 回規劃
R->>P: #1 closed,輪次回 planning
R->>E: revert_job 退回不合格卡,寫 due_at 與重做紀錄
Note over U,E: 退 3 張卡,期限=里程碑
E->>U: 通知重做
U->>E: 重做完成
E->>R: 再走到內部稽核計畫填寫站
R->>P: 開新稽核次,複製 #1 AP 縮到不合格項
Note over P: 開稽核次 #2 內部,AP 縮到不合格項
A->>E: 第 2 次內稽完成,無缺失
E->>R: finalize_audit,terminal=false
R->>P: #2 closed,輪次不結案
E->>R: 走到外部稽核計畫填寫站
R->>P: 開新稽核次,AP 全範圍新建
Note over P: 開稽核次 #3 外部,AP 全範圍
A->>E: 外部稽核完成,有缺失
E->>R: finalize_audit
R->>P: #3 轉 remediation
PM->>E: 外部 POA&M 完成
E->>R: close_round,下一條接結束
R->>R: 母輪轉 pending_reverify
PM->>R: 啟動覆核輪(既有機制)
R->>E: 新輪新卡,due_at=外稽里程碑
每站 handler 做什麼:
| 站 | 觸發的輪次方法 | 對稽核次做什麼 | 對任務卡做什麼 |
|---|---|---|---|
| 規劃+執行完成,進到稽核計畫填寫站 | 先開新稽核次,再 launch_audit |
新建一筆(讀站屬性 audit_nature);拍 SSP 快照;內稽第 N 次(N≥2)複製上一次 AP 縮到 not_met,其他新建 AP;狀態 planning → audit_planning |
無 |
| 稽核計畫填寫完成(內/外) | start_auditing |
每次都新建 AR result(同一個 AR 根底下追加,標題帶「第 N 次內部/外部」);狀態 → auditing | 無 |
| 稽核完成、有缺失 | finalize_audit |
狀態 → remediation,寫 poam_id;閘道走「有」 | 無 |
| 稽核完成、無缺失、下一站是外部稽核計畫 | finalize_audit(terminal=false),接著開新稽核次+launch_audit |
這次 closed、輪次不結案;新開一筆外部稽核次,AP 全範圍 | 無 |
| 稽核完成、無缺失、下一個是結束事件 | finalize_audit(terminal=true) |
這次 closed、輪次 closed | 無 |
| 內部缺失改善完成(下一條是 reverse 回規劃) | close_round |
這次 closed,輪次 status 回 planning(下一筆要到計畫站才建;這段期間畫面用推算補上,見變更紀錄 2026-10-16) | 篩 not_met 控制項 → 找原卡 → 退回、寫期限、寫重做紀錄、通知 |
| 外部缺失改善完成(下一個是結束事件) | close_round(維持現有) |
這次 closed,母輪 → pending_reverify | 無(覆核輪產卡時再寫期限) |
| 啟動覆核輪 | launch_reverify(不動) |
新的一輪 | 新卡承襲,due_at=外稽 POA&M 里程碑 |
每塊寫改哪些檔(座標)、規則、陷阱。
改哪些檔:新 migration(scripts/sql/,建 compliance.audit_round_passes、project_audit_rounds 加 current_pass_id、搬舊資料);pkg/jedi-compliance-audit 的 entity、ORM、repo(project_audit_round_repo_impl.py 旁新增 pass repo)。
規則:
audit_nature 值來自 system_menus group audit_nature,不寫死在程式。pass_no=1,audit_nature 取範本站屬性,沒有就 NULL(畫面顯示「未標示」);四槽位複製;current_pass_id 指向它。GRANT ... TO cm_app、sequence 權限;收尾 INSERT public.schema_migrations。陷阱:本 migration 是主線 schema 變更,寫完要回報出貨基線待重產;升級既有客戶資料時第 1 次稽核次必須補齊,否則所有改讀目前稽核次的地方都會讀到空值。 狀態對應:輪次 pending_reverify → 稽核次 closed、not_started → planning、其餘同名;輪次自己的 status 不動。
改哪些檔:pkg/jedi-compliance-audit/app/service/audit_round_app_service.py(launch_audit、start_auditing、finalize_audit、close_round);app/flow_control/service/oscal_stage_handlers.py;四槽位讀取者全部(見「現況接入點」③的清單)。
規則:
audit_nature;內稽第 N 次(N≥2)複製上一次內稽 AP 縮到 not_met;外稽全新 AP;每次重拍 SSP 快照;重做的稽核次一律寫 prev_pass_id 指向同一輪上一次內稽(P1);nature_seq 依同性質已有筆數加一;寫完把輪次 current_pass_id 指向新筆。start_auditing 每次新建 AR result,在同一個 engagement 的 AR 根底下追加,標題帶「第 N 次內部/外部」。finalize_audit 加 terminal 參數(P3):handler 看範本,這站之後經閘道「無缺失」那條接的是結束事件就傳 terminal=true(稽核次與輪次都 closed),接的是下一個稽核計畫填寫站就傳 false(只有稽核次 closed,輪次不結案、接著開新稽核次)。舊範本稽核站後直接接結束,行為不變。close_round:下一條是 reverse 回規劃時,這次設 closed、輪次 status 回 planning、觸發退卡;下一個是結束事件時維持現有(母輪 → pending_reverify 供覆核)。陷阱:
start_auditing 的 not ar_result_id 判斷(:481)會讓第二次沿用第一次的 AR,必須改成每次新建。launch_audit 直接覆寫 ssp_id/assessment_plan_id(:435、:437),改綁稽核次後才不會蓋掉第一次。改哪些檔:system_menus seed migration(group audit_nature:internal、external);FE FlowTemplateEditorView.vue(面板 :1093-1126 加下拉、讀寫屬性 :278-280、:322-360);新公版 BPMN seed(scripts/sql/seeds/bpmn/);選配 bpmn_topology_validator.py。
規則:
audit_nature 用下拉,選項讀 system_menus。stage_objects.allowed_successors 不用改(閘道與 reverse 都會被跳過檢查)。audit_nature。陷阱:
${has_findings == true},比對是字串全等。改哪些檔:pkg/jedi-flow-engine/app/service/workflow_execution_service.py:308-335 與主專案 app/flow_engine/service/workflow_execution_service.py:612-640;stage_rollback_service.py:181-187;job_executions ORM/entity(attempt_no)。
規則:
complete_*_job 遇到同 element 已 COMPLETED 時新建一筆,attempt_no 遞增。get_job_execution 取最新一筆。_find_job_by_stage_code 改用 element id 找。陷阱:
_find_job_by_stage_code 只回第一筆,新公版範本同一站碼出現兩次(內部與外部稽核計畫填寫),不改會找錯站。改哪些檔:job_executions.due_at 全鏈——pkg/jedi-flow-engine ORM(infra/models/job_execution.py)、entity、mapper、repo update;pkg/jedi-task-platform flow_control_job_entity.py;主專案 job_dto.py、serializers/job.py、vw_user_job_queue.sql、my_grc_jobs_query.py;FE MyTasksView.vue、JobExecutionDrawer.vue(新邏輯開新檔)。退卡 handler 新檔;compliance.job_execution_redo_log migration;core/scheduler.py 提醒;task_alert_notifications CHECK migration。
規則:
revert_job 抽出的單卡退回函式(略過輪次凍結檢查、保留通知;不呼叫整段 revert_job,否則後續所有 job 會被一起退回)→ due_at 寫該控制項 POA&M 最早的 target_date(經 job_due_date_service.due_at_from_milestone_date 換成台北當天 23:59:59,與覆核輪同一把尺)→ 寫一筆重做紀錄(job_execution_id、pass_id、prev_end_time、revert_at)。job_due 兩個時點:到期前 3 天、到期當天,寄指派人+PM;寫入 task_alert_notifications,object_type job_due,stage due_3d/due_today(P4)。due_at 與手動調整標記。陷阱:
due_at 要補進 update 欄位,否則寫了不會存。security_invoker,否則繞過 RLS。task_alert_notifications 的 object_type CHECK 要放寬加 job_due,否則寫入直接被擋。control_id 格式與 AR finding 的 target_id 能不能直接對上尚未驗證,實作前要先拿 DEV 資料比對。MyTasksView.vue、JobExecutionDrawer.vue 只准新開 composable/子元件,不准往原檔加邏輯(JobExecutionDrawer.vue 2393 行已超標)。改哪些檔:pkg/jedi-compliance-audit audit_round_app_service.py 的 _generate_reverify_prep_jobs(:732)。
規則:產卡時寫 due_at=母輪最後一筆稽核次(外稽)的 POA&M 里程碑日期。其他覆核輪行為不動。
陷阱:母輪的 POA&M 要從最後一筆稽核次取,不能讀輪次上的 poam_id(改綁後語意是「目前」那一次,剛好也是外稽,但讀取來源要統一走稽核次)。
規則(P6;⚠ 進度條部分已推翻,見變更紀錄 2026-10-16):
audit_nature 為 NULL,切換顯示「未標示」。陷阱:進度條若改由輪次 status 推算,內稽與外稽兩段共用同一組狀態值、回流時 status 又回到 planning,推算結果會與引擎實際所在的站不一致——一律讀引擎目前站。
依「決策者何時看得到畫面」排序。e2e 屬母卡收口,不在單卡做。
| 卡 | 名稱 | 範圍 | 套件 | 依賴 | 驗收條件(決策者親手做什麼、看到什麼) |
|---|---|---|---|---|---|
| 1 | 建稽核次表並把既有輪次搬成第 1 次 | migration、輪次加 current_pass_id、pass entity/repo |
jedi-compliance-audit | — | DEV 任一既有輪次查得到 pass_no=1 一筆,四個值與輪次一致 |
| 2 | 範本站加「稽核性質」下拉並出新公版範本 | audit_nature 選單 seed、新公版 BPMN seed、FE 範本編輯器下拉 |
— | — | 範本編輯器點稽核計畫填寫站看到「稽核性質」可選內部/外部;公版清單多「內外稽核流程」 |
| 3 | 走到稽核計畫站時開新稽核次 | launch_audit 改綁 pass、AP 複製或新建分流、SSP 重拍 |
jedi-compliance-audit | 1、2 | 用新公版範本走到內部稽核計畫填寫,DB 多一筆內部稽核次、帶 SSP 快照與 AP |
| 4 | 其餘三站改綁稽核次並支援回到規劃 | start_auditing/finalize_audit/close_round 改綁、terminal、回 planning |
jedi-compliance-audit | 3 | 第 1 次內稽有缺失、改善完成後輪次回 planning;第 2 次內稽產生新的 AR,第 1 次的 AR 還在 |
| 5 | 流程第二次走到同一站時開新任務卡 | 兩份推進邏輯、attempt_no、依 element id 找 job |
jedi-flow-engine | 4 | 退回重做後,任務歷程看到同站兩筆,第一次的完成時間還在 |
| 6 | 任務卡加期限欄(後端) | due_at ORM/entity/mapper/repo/DTO/serializer/view/API |
jedi-flow-engine、jedi-task-platform | — | 用 API 寫入期限,詳情 API 與我的任務查詢都回得到 |
| 7 | 我的任務與任務抽屜顯示期限 | FE 兩頁,新邏輯開 composable/子元件 | — | 6 | 我的任務列表與抽屜都看到期限;PM 改期限後標「手動調整」 |
| 8 | 內稽不合格自動退回原卡 | 篩 not_met → 找卡 → 單卡退回函式(自 revert_job 抽出) → 寫期限 → 寫重做紀錄 |
— | 4、5、6 | 內稽標 3 條不合格並填里程碑,改善完成後原指派人看到 3 張退回卡、期限=里程碑、收到通知 |
| 9 | 任務卡到期提醒 | job_due 兩時點(前 3 天、當天)、CHECK 放寬、排程 |
— | 8 | 把一張卡期限設在 3 天後,排程跑完指派人與 PM 各收到一封提醒信、task_alert_notifications 多一筆 stage due_3d;把期限改成今天再跑一次,再收一封、多一筆 due_today;同一 stage 重跑排程不重寄 |
| 10 | 總覽頁切換第幾次稽核並讓文件標題帶次數 | 稽核次切換、AP/AR/POA&M 標題帶「第 N 次內部/外部」、OSCAL props | — | 4 | 總覽頁輪次下拉底下切換「第 1 次內部/第 2 次內部/第 1 次外部」,各自看到自己的 AP/AR/POA&M |
| 11 | 覆核輪新卡帶期限 | _generate_reverify_prep_jobs 寫 due_at |
jedi-compliance-audit | 6 | 外稽有缺失並填里程碑、啟動覆核輪,新卡期限=外稽里程碑 |
| 12 | 流程進度條 | 純 FE,讀引擎目前站(P6) | — | 5 | 總覽頁看到照範本站排列的進度條,目前站高亮;退回後高亮回到規劃站 |
順序:1 → 3 → 4 是主線;2、6 → 7 平行;5、8 接在 4 後;9、10、11、12 收尾。
套件:jedi-compliance-audit(卡 1、3、4、11)、jedi-flow-engine(卡 5、6)、jedi-task-platform(卡 6)。開發期走 poetry path dependency,不發版;發版等決策者明示。
含 migration 的卡(1、5、6、8、9 與卡 2 的 system_menus seed)照 SQL migration 鐵則:日期註解、新表 GRANT ... TO cm_app 與 sequence 權限、收尾 INSERT public.schema_migrations、只套 DEV、寫完回報出貨基線待重產。
在 DEV 用新公版「內外稽核流程」範本走一輪完整劇本,批次走完再一次回報。每步寫「在哪個畫面/按什麼/看到什麼/DB 查什麼」。
compliance.audit_round_passes 此輪尚無資料。pass_no=1、audit_nature=internal、nature_seq=1、prev_pass_id 為 NULL、帶 ssp_id 與 assessment_plan_id;project_audit_rounds.current_pass_id 指向它、輪次 status=audit_planning。ar_result_id、poam_id,status=remediation;輪次 status 同為 remediation。closed;輪次 status 回 planning;這 3 筆 job_executions 的 end_time 為 NULL、due_at 有值;compliance.job_execution_redo_log 多 3 筆,pass_id=#1、prev_end_time=退回前的完成時間。pass_no=2、audit_nature=internal、nature_seq=2、prev_pass_id=#1;這次 AP 只列那 3 條控制項;稽核次 #1 的 AP/AR/POA&M 不變。稽核員稽核後無缺失 → 稽核次 #2 status=closed,輪次沒有結案、流程推進到外部稽核計畫填寫。pass_no=3、audit_nature=external、nature_seq=1、prev_pass_id 為 NULL,AP 涵蓋全部控制項。稽核員標 2 條不合格並填里程碑 → 外部 POA&M 建立。總覽頁輪次下拉底下切換「第 1 次內部/第 2 次內部/第 1 次外部」,各自看到自己的 AP/AR/POA&M;進度條高亮在外部缺失改善站。pending_reverify;稽核次 #3 status=closed。job_executions.due_at 有值;母輪三筆稽核次都還在。task_alert_notifications 多一筆 object_type job_due、stage due_3d;改成今天再跑 → 多一筆 due_today。pass_no=1 一筆,四個值與輪次一致、audit_nature 為 NULL、總覽頁顯示「未標示」;用既有公版範本跑一輪,行為與上線前相同(稽核無缺失直接結案)。audit_nature(舊輪次顯示「未標示」)。開工前先掃這張表,每列都對應一張卡。
| # | 坑 | 後果 | 對應卡 |
|---|---|---|---|
| 1 | 推進邏輯有兩份(套件與主專案) | 只改一份,兩條路行為分歧 | 5 |
| 2 | _find_job_by_stage_code 只回第一筆 |
同站碼出現兩次時找錯站 | 5 |
| 3 | 閘道條件字串是全等比對 | 多一個空白閘道就判斷失敗 | 2 |
| 4 | 內部缺失改善 → 規劃沒標 reverse | allowed_successors 檢查擋下範本 | 2 |
| 5 | start_auditing 的 not ar_result_id |
第二次稽核沿用第一次的 AR | 4 |
| 6 | launch_audit 直接覆寫 ssp_id/assessment_plan_id |
第二次稽核蓋掉第一次 | 3 |
| 7 | 四槽位讀取者分散在主專案與套件十餘處 | 漏改一處就靜默讀到舊的一次 | 3、4 |
| 8 | MyTasksView.vue、JobExecutionDrawer.vue 超標 |
往原檔加邏輯違反體積上限 | 7 |
| 9 | view 重建沒帶 security_invoker |
繞過 RLS | 6 |
| 10 | task_alert_notifications 的 object_type CHECK |
新增 job_due 寫入被擋 |
9 |
| 11 | wecm control_id 格式與 AR finding target_id 的對應未驗證 |
退卡找不到卡或找錯卡,實作前先用 DEV 資料驗 | 8 |
| 12 | job repo update 只寫 status 與時間 | due_at 寫了不存 |
6 |