FR-140 · 討論稿 · 2026-10-15

一輪一條流程,內稽回流、外稽覆核

D1~D6、P1~P6 全部已裁;六塊設計、十二張卡

一個稽核輪次要能走完整條流程:專案規劃與執行 → 內部稽核 → 有缺失就改善並退回原任務卡重做、再稽核一次 → 內部沒缺失才進外部稽核 → 外部有缺失走既有的覆核輪。每一次稽核(第 1 次內稽、第 2 次內稽、外稽)各自留一組 SSP 快照、評估計畫、評估結果、改善計畫,不互相覆蓋。這份把決策者 2026-10-15 確認的流程、已裁的 D1~D6 與 P1~P6 展開成可以開工的規格;每項現況都附程式碼座標。

已裁 D1~D6 P1~P6 已裁 十二張卡,範本編輯器先出畫面 動三支套件

全部已裁,下一步寫 design.md,本稿不再改寫。

狀態:討論稿(D1~D6、P1~P6 全部已裁,下一步寫 design.md、開 Notion 卡) | 日期:2026-10-15 | 母卡:CM-2765

路徑慣例:BE 路徑相對 compliance-manager-be/;pkg/ 代表 ~/Projects/Jedicogy/module/jedi-python-package/;FE 路徑相對 compliance-manager-fe/。

名詞:輪次=稽核輪次(compliance.project_audit_rounds),一輪對應一次專案稽核流程;稽核次=本案新增的概念,一輪內每做一次稽核(內部或外部)就是一筆;AP=評估計畫(assessment plan);AR=評估結果(assessment results);POA&M=缺失改善計畫(plan of action and milestones);job=流程引擎依範本站產生的任務卡執行紀錄(compliance.job_executions);站=範本(BPMN 流程圖)上的一個使用者任務節點;reverse 連線=範本上標記為「往回走」的連線。

§1

變更紀錄

日期 變更 對應
2026-10-15 初稿:需求流程圖、現況盤點、稽核次概念與資料模型草案、端到端流程、已裁 D1~D6、待裁 P1~P6、六塊詳細設計、十二張卡拆卡草案 —
2026-10-15 決策者裁 P1~P6:P4 改為到期前 3 天與到期當天兩個時點,其餘照建議 P1~P6

§2

分工概述(30 秒版)

整案一句話:一輪就是一條完整流程,內稽不合格就把那幾張原任務卡退回重做、再稽核一次,內稽過了才進外稽;每一次稽核各留一組 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
圖 1 — 十二張卡的依賴(箭頭=必須先做)

§3

需求

%%{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["覆核輪(現有機制,新的一輪)"]
圖 2 — 決策者確認的一輪流程(虛線=退回)

逐句需求(決策者 2026-10-15 確認):

  1. 一輪=一條完整流程,內稽與外稽都在同一輪內(PM 裁決)。
  2. 內部稽核有缺失 → 做內部缺失改善(POA&M)→ 退回專案規劃+執行(圖上虛線)。
  3. 退回時,不合格的任務卡原卡退回起點,原指派人收到通知重做;卡有完成期限=該控制項 POA&M 的里程碑日期。
  4. 重做完再走一次內部稽核計畫 → 內部稽核(第 2 次),計畫只列上次不合格的控制項。
  5. 內部稽核無缺失 → 外部稽核計畫(全範圍)→ 外部稽核。
  6. 外部稽核有缺失 → 外部 POA&M → 啟動覆核輪(現有機制:新的一輪、新卡承襲、只建缺失項);覆核輪任務卡的期限=外稽 POA&M 的里程碑日期。外部稽核無缺失 → 結案。
  7. 每一次稽核各留一組 SSP 快照+AP+AR+POA&M,看得出是內部或外部、第幾次,不被下一次覆蓋。
  8. 專案管理頁永遠看全部控制項;內稽回流只是其中幾張卡退回。覆核輪才是只有缺失項的小圖。

內稽回流與覆核輪看起來都是「不合格再做一次」,但是兩件事:

比較 內稽回流(本案新做) 覆核輪(既有機制,不動)
發生在哪 同一輪內,流程退回規劃站 新開一輪
任務卡 原卡退回原指派人 新卡,承襲母輪資料
PM 看到的範圍 全部控制項,其中幾張卡變回待處理 只有缺失項
下一次的 AP 從上一次內稽 AP 縮到不合格項 只建缺失項

§4

現況盤點:接入點 × 現況 × 本案動作

① 流程引擎(兩份推進邏輯,都要改)

接入點 現況 本案動作
建 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 沒有通知 退卡 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 只有四個單值槽位,第二次稽核會直接覆寫第一次。這是本案的核心,也是第一、二塊要改的。


§5

核心概念:稽核次

稽核次的想法與既有的 living SSP 相同:有一份一直在變的「活本」,每到某個時點就凍一份下來。

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 一律全範圍新建

§6

資料模型草案

新表:稽核次 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)
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 指向目前稽核次;輪次上四個槽位保留,跟著目前稽核次同步寫(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 退回時間

migration

既有輪次各建一筆 pass_no=1:audit_nature 取範本站屬性,沒有就 NULL;四槽位複製過去;current_pass_id 指向它。舊範本一輪一次照常跑(D6)。

%%{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 : "因這次退回"
圖 3 — 稽核次與相關表

§7

端到端流程

以「第 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=外稽里程碑
圖 4 — 一輪走完內稽回流與外稽

每站 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 篩 not_met 控制項 → 找原卡 → 退回、寫期限、寫重做紀錄、通知
外部缺失改善完成(下一個是結束事件) close_round(維持現有) 這次 closed,母輪 → pending_reverify 無(覆核輪產卡時再寫期限)
啟動覆核輪 launch_reverify(不動) 新的一輪 新卡承襲,due_at=外稽 POA&M 里程碑

§8

已裁決策

✅ D1 — 內稽不合格原卡退回,外稽不合格走覆核輪

定案:內稽不合格的任務卡原卡退回:status 回待處理、end_time 清空,指派人、部門、設備、證據都不動;另寫一筆重做紀錄,保留歷次完成時間。外稽不合格走覆核輪,開新卡。

理由:內稽回流是同一輪內的修正,PM 要在同一張全控制項的圖上看到哪幾張卡退回;原卡退回讓指派人在原地補做,證據與指派關係不必重建。外稽是對外的正式結果,維持既有覆核輪機制。

✅ D2 — 第 N 次內稽 AP 縮到不合格項,外稽 AP 全範圍

定案:第 N 次(N≥2)內稽的 AP 從上一次內稽的 AP 複製,縮到 not_met 控制項,沿用覆核輪的 clone 邏輯,只是來源改成同一輪上一次稽核次。外稽 AP 一律全範圍新建。

理由:第 2 次內稽只是確認上次不合格的項目改好了,不必重查全部;外稽是正式稽核,必須涵蓋全範圍。clone 邏輯已在覆核輪驗證過,換來源即可重用。

✅ D3 — 稽核次表=把輪次四個單值槽位改成一輪多筆

定案:新增稽核次表,一輪多筆,每筆帶 SSP 快照、AP、AR、POA&M。概念同 living SSP:活本是任務卡與證據,每次走到「稽核計畫填寫」站凍一組。

理由:四個槽位是單值,第二次稽核必然覆寫第一次;需求要求每一次都留得下來且分得出內外與次數,只能改成多筆。

✅ D4 — 不改引擎的走法與範本驗證規則

定案:引擎 token 怎麼走、範本驗證規則都不動;只改「回到同一站時新開一筆 job」與站 handler 的綁定。

理由:盤點顯示範本已畫得出退回再稽核的圖(同站碼可重複、reverse 與閘道都跳過檢查),問題只在 job 重用與輪次單向,改動範圍收在這兩處最小。

✅ D5 — 覆核輪機制保留不動,只補期限承襲

定案:覆核輪照現有方式開新輪、新卡承襲、只建缺失項;唯一新增是新卡帶上期限。

理由:外稽不過維持開新輪是決策者明示;期限是本案新加的欄位,覆核輪的卡也要有才一致。

✅ D6 — 舊資料與舊範本照常跑

定案:migration 把既有輪次的四槽位搬成第 1 次稽核次;舊範本一輪一次照常跑。

理由:既有輪次與範本不能因本案失效;搬成第 1 次後,所有讀取者統一讀「目前稽核次」即可,不必分新舊兩條路。


§9

補裁 P1~P6

決策者 2026-10-15 已全部裁定:P4 改為到期前 3 天與到期當天,其餘照建議。

✅ P1 — 稽核次要不要記「是哪一次的重做」(prev_pass_id)

問題:第 2 次內稽是第 1 次的重做,要不要在稽核次上直接記這個關係。

選項:A. 記 prev_pass_id;B. 不記,靠 audit_nature+nature_seq 推。

定案(照建議):A,要記。畫面才串得出「第 1 次 → 第 2 次」的軌跡,不必用序號推算。

✅ P2 — 輪次自己的 status 保留七態還是收成粗狀態

問題:狀態機改綁稽核次後,輪次的 status 要怎麼處理。

選項:A. 保留欄位與七態,跟著目前稽核次同步寫;B. 收成粗狀態(進行中/待覆核/結案)。

定案(照建議):A。讀輪次 status 的地方很多,保留欄位與七態不動,斷言改綁稽核次,輪次 status 跟著目前稽核次同步寫,舊畫面不會壞。

✅ P3 — 第 2 次內稽無缺失時輪次怎麼不被結案

問題:finalize_audit 遇到沒有不合格就把輪次設 closed(audit_round_app_service.py:513),但第 2 次內稽之後還有外稽。

選項:A. finalize_audit 加參數 terminal,由 handler 依「這站之後是否接結束事件」決定;B. 一律不設 closed,改由範本結束事件結案。

定案(照建議):A。terminal 由 handler 看範本決定,舊範本(稽核站後直接接結束)行為不變。

✅ P4 — 到期提醒的時點與對象

問題:任務卡有了期限,什麼時候提醒、提醒誰。

選項:A. 前 7 天、前 1 天、逾期當天,寄指派人+PM;B. 只在逾期當天寄指派人。

定案:到期前 3 天、到期當天兩個時點,寄指派人+PM;沿用 task_alert_notifications,object_type 新增 job_due,stage 為 due_3d/due_today。

✅ P5 — 期限可不可以由 PM 手改

問題:期限是從 POA&M 里程碑帶過來的,PM 要不要能改。

選項:A. 可改,改了標「手動調整」,不回寫 POA&M;B. 不可改,只能去改 POA&M。

定案(照建議):A。改了標「手動調整」,不回寫 POA&M,兩邊的責任分清楚。

✅ P6 — 進度怎麼顯示

問題:一輪內有多次稽核,總覽頁要怎麼讓人看出現在走到哪。

選項:A. 輪次下拉底下多一層「第 N 次內部/外部」切換,另畫一條照範本站排列的進度條;B. 只加切換,不畫進度條。

定案(照建議):A,進度條直接讀引擎目前所在的站,不推算。


§10

詳細設計

一、稽核次表

改哪些檔:新 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 次稽核次必須補齊,否則所有改讀目前稽核次的地方都會讀到空值。

二、輪次狀態機改綁稽核次

改哪些檔: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;四槽位讀取者全部(見現況盤點③的清單)。

規則:

  • 四個方法的斷言與寫入改到目前稽核次;輪次 status 跟著目前稽核次同步寫(P2)。
  • 走到稽核計畫填寫站時,handler 先「開新稽核次」:讀站屬性 audit_nature;內稽第 N 次(N≥2)複製上一次內稽 AP 縮到 not_met;外稽全新 AP;每次重拍 SSP 快照。
  • start_auditing 每次新建 AR result,在同一個 engagement 的 AR 根底下追加,標題帶「第 N 次內部/外部」。
  • finalize_audit 加 terminal 參數(P3)。
  • 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。
  • 新公版「內外稽核流程」:規劃 → 內部稽核計畫填寫 → 內部稽核 → 閘道 → 內部缺失改善 →(reverse)回規劃;閘道無缺失 → 外部稽核計畫填寫 → 外部稽核 → 閘道 → 外部缺失改善 → 結束;閘道無缺失 → 結束。
  • stage_objects.allowed_successors 不用改(閘道與 reverse 都會被跳過檢查)。
  • validator 選配規則:同一站碼出現多次時,每一站都必須帶 audit_nature。

陷阱:

  • 內部缺失改善 → 規劃那條連線必須標 reverse,否則 allowed_successors 檢查會擋(poam 的下一站清單是空的)。
  • 閘道條件字串必須與既有一字不差:${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。

規則:

  • 回流 handler:取目前稽核次 AR 的 not_met 控制項 → 經 wecm(control_id、ao_part_id、round_id)找卡 → 呼叫主專案 revert_job(略過輪次凍結檢查、保留通知)→ due_at 寫該控制項 POA&M 最早的 target_date → 寫一筆重做紀錄(job_execution_id、pass_id、prev_end_time、revert_at)。
  • 提醒 job_due 兩個時點:到期前 3 天、到期當天(P4 定案)。
  • PM 可手改期限,標「手動調整」(P5)。

陷阱:

  • repo update 只寫 status 與時間,due_at 要補進 update 欄位,否則寫了不會存。
  • view 重建要帶 security_invoker,否則繞過 RLS。
  • task_alert_notifications 的 object_type CHECK 要放寬加 job_due,否則寫入直接被擋。
  • wecm 的 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(改綁後語意是「目前」那一次,剛好也是外稽,但讀取來源要統一走稽核次)。


§11

拆卡草案(依「決策者何時看得到畫面」排序)

卡 名稱 範圍 套件 依賴 驗收
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 天後,排程跑完收到提醒信;到期當天再收一封
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,不發版。


§12

不做清單

  • 角色重構、小組長兩級審核、外稽員只看自己那次(角色題另案處理)。
  • 專案層級範本(一張圖自動開輪)。
  • 舊資料回填 audit_nature(舊輪次顯示「未標示」)。
  • 同範本內把覆核輪也改成輪內回流:外稽不過維持開新輪(決策者明示)。
  • 本稿不附雛形;期限兩頁與總覽切換在 design 階段再刻。

§13

坑與陷阱總表

開工前先掃這張表,每列都對應一張卡。

# 坑 後果 對應卡
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