FR-140 · 討論稿 · 2026-10-15
D1~D6、P1~P6 全部已裁;六塊設計、十二張卡
一個稽核輪次要能走完整條流程:專案規劃與執行 → 內部稽核 → 有缺失就改善並退回原任務卡重做、再稽核一次 → 內部沒缺失才進外部稽核 → 外部有缺失走既有的覆核輪。每一次稽核(第 1 次內稽、第 2 次內稽、外稽)各自留一組 SSP 快照、評估計畫、評估結果、改善計畫,不互相覆蓋。這份把決策者 2026-10-15 確認的流程、已裁的 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 連線=範本上標記為「往回走」的連線。
| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-15 | 初稿:需求流程圖、現況盤點、稽核次概念與資料模型草案、端到端流程、已裁 D1~D6、待裁 P1~P6、六塊詳細設計、十二張卡拆卡草案 | — |
| 2026-10-15 | 決策者裁 P1~P6:P4 改為到期前 3 天與到期當天兩個時點,其餘照建議 | P1~P6 |
整案一句話:一輪就是一條完整流程,內稽不合格就把那幾張原任務卡退回重做、再稽核一次,內稽過了才進外稽;每一次稽核各留一組 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 縮到不合格項 | 只建缺失項 |
| 接入點 | 現況 | 本案動作 |
|---|---|---|
| 建 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 只有四個單值槽位,第二次稽核會直接覆寫第一次。這是本案的核心,也是第一、二塊要改的。
稽核次的想法與既有的 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 一律全範圍新建 |
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 |
退回時間 |
既有輪次各建一筆 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 : "因這次退回"
以「第 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 | 篩 not_met 控制項 → 找原卡 → 退回、寫期限、寫重做紀錄、通知 |
| 外部缺失改善完成(下一個是結束事件) | close_round(維持現有) |
這次 closed,母輪 → pending_reverify | 無(覆核輪產卡時再寫期限) |
| 啟動覆核輪 | launch_reverify(不動) |
新的一輪 | 新卡承襲,due_at=外稽 POA&M 里程碑 |
✅ 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 次後,所有讀取者統一讀「目前稽核次」即可,不必分新舊兩條路。
決策者 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,進度條直接讀引擎目前所在的站,不推算。
改哪些檔:新 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;四槽位讀取者全部(見現況盤點③的清單)。
規則:
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。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(略過輪次凍結檢查、保留通知)→ due_at 寫該控制項 POA&M 最早的 target_date → 寫一筆重做紀錄(job_execution_id、pass_id、prev_end_time、revert_at)。job_due 兩個時點:到期前 3 天、到期當天(P4 定案)。陷阱:
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(改綁後語意是「目前」那一次,剛好也是外稽,但讀取來源要統一走稽核次)。
| 卡 | 名稱 | 範圍 | 套件 | 依賴 | 驗收 |
|---|---|---|---|---|---|
| 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,不發版。
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 |