稽核輪次流程引擎再整合 — BE Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 把 FR-026 BPMN 流程引擎從舊 AP(ap_uid + assessment_plan_extensions)retarget 到 v2 first-class project_audit_rounds,新增 ap_authoring 階段,round.status 改由 BPMN 推導。

Architecture: flow_template master → 建輪次時 clone snapshot → 綁 project_audit_rounds.workflow_execution_uid;stage_advance 以 round_uid 解 workflow context;既有 stage handler 改 wrap audit_round_app_service;round.status 由當前 BPMN UserTask 的 stage_object_code 經對照表推導(§4.1b)。

Tech Stack: Flask + SQLAlchemy + PostgreSQL(DDD),jedi-flow-engine(BPMN),jedi-oscal-v2(AP/AR),pytest。

設計依據: round-flow-engine-reintegration-design.md(§3 決策含 D5 stage 模型 / §4.1 canonical 4 階段鏈 + handler 對應 / §4.1b status 推導)

canonical stage 鏈(D5): planning → ap_authoring → audit → poam → End(task_execution 收進 planning;review 暫不納)。


§1

前置驗證(開工第一步)


§2

任務總覽(依賴順序)

# 任務 依賴 風險
1 schema:3 新欄位 + entity/mapper + _to_dto
2 error code + i18n key
3 stage_object seed(加 ap_authoring + 改 planning handler/precondition) 1,2
4 builtin flow_templates BPMN 重 seed(canonical 4 階段) 3
5 registry interface + stage_advance retarget(ap_uid→round_uid + status 推導) 1
6 handler 全改 wrap audit_round + 新 submit_ap + 拆 _safe_register 5
7 precondition retarget + 新 2 個 + count_reviewed_controls 5
8 create_round 綁 flow snapshot 1,4
8b 建專案順帶建第一輪(D6)+ serializer 8
9 round-scoped stage route + 退役舊 ap-scoped route 6,7
10 DI wiring 6,7,8,9
11 整合測試 + boot 驗證 all

建專案順帶建第一輪(D6,新規格):舊專案資料已清空、不做向後相容;serializer 清掉舊 Meta.unknown=EXCLUDE / 舊欄位 + 帶 resource_library_uid/flow_template_uidstart_project 建 round 1 = Task 8b。FE 照新規格建畫面在 FE plan。


Task 1: Schema — 3 新欄位 + entity/mapper + _to_dto

Files: Create scripts/sql/2026-06-16-fr038-round-flow-binding.sql;Modify infra/module_frame/models/module_frame.pyinfra/grc/model/project_audit_round.pyinfra/grc/mapper/project_audit_round_mapper.pydomain/grc/entities/project_audit_round_entity.pyapp/grc/service/audit_round_app_service.py_to_dto

-- Date: 2026-06-16  FR-038 round flow binding
ALTER TABLE compliance.module_frames
  ADD COLUMN IF NOT EXISTS audit_flow_template_uid VARCHAR(36);
COMMENT ON COLUMN compliance.module_frames.audit_flow_template_uid IS '預設稽核流程範本 uid(→ compliance.flow_templates.uid,soft ref)';
ALTER TABLE compliance.project_audit_rounds
  ADD COLUMN IF NOT EXISTS flow_template_snapshot_uid VARCHAR(36),
  ADD COLUMN IF NOT EXISTS workflow_execution_uid     VARCHAR(36);
COMMENT ON COLUMN compliance.project_audit_rounds.flow_template_snapshot_uid IS '本輪凍結流程範本 snapshot uid(soft ref)';
COMMENT ON COLUMN compliance.project_audit_rounds.workflow_execution_uid IS '本輪 main workflow_execution uid(BPMN 驅動 round 狀態)';
INSERT INTO public.schema_migrations (version) VALUES ('2026-06-16-fr038-round-flow-binding') ON CONFLICT DO NOTHING;
git add scripts/sql/2026-06-16-fr038-round-flow-binding.sql infra/grc/model/project_audit_round.py infra/grc/mapper/project_audit_round_mapper.py domain/grc/entities/project_audit_round_entity.py infra/module_frame/models/module_frame.py app/grc/service/audit_round_app_service.py
git commit -m "feat(FR-038): round flow binding 欄位 + 資源庫預設流程範本欄 + _to_dto"

Task 2: Error code + i18n key

Files: Modify common/code/grc_error_code.pyconfig/translations/zh_Hant_TW/*.poconfig/translations/en/*.po(或對應 json,依專案 i18n 載入機制)

GRC_ROUND_NO_WORKFLOW_BINDING = ("GRC_404xxx", "稽核輪次尚未綁定流程")
GRC_AP_REVIEWED_CONTROLS_REQUIRED = ("GRC_412xxx", "請先選定本輪查核控制項")
GRC_ROUND_PREP_TASKS_NOT_DONE = ("GRC_412xxx", "本輪準備任務尚未全部完成")
lang.flow_engine.precondition.round_prep_tasks_done = 本輪準備任務尚未全部完成({count} 項待完成)
lang.flow_engine.precondition.ap_reviewed_controls_required = 請先選定本輪查核控制項
git add common/code/grc_error_code.py config/translations/
git commit -m "feat(FR-038): round flow error codes + precondition i18n"

Task 3: stage_object seed(加 ap_authoring + 收斂 planning)

Files: Append scripts/sql/2026-06-16-fr038-round-flow-binding.sql

stage_object UPDATE 矩陣(D5 收斂後的最終狀態):

code complete_handler_key precondition_key allowed_predecessors allowed_successors 動作
planning launch_audit(原 activate_project) round_prep_tasks_done(原 all_tasks_*) [] ["ap_authoring"] UPDATE
ap_authoring submit_ap ap_reviewed_controls_set ["planning"] ["audit"] INSERT(新)
audit confirm_audit ar_all_verdicts_filled ["ap_authoring"] ["poam"] UPDATE predecessors
poam close_round poam_all_closed ["audit"] ["audit"] 不動
task_execution / review 保留 table(builtin 不用;未來變體可復用)
-- Date: 2026-06-16  FR-038 ap_authoring 階段 + planning 收斂
INSERT INTO compliance.stage_objects
  (uid, code, name_i18n, kind, route_pattern, complete_button_label_i18n,
   complete_handler_key, precondition_key, default_main_roles, can_start, can_end,
   allowed_predecessors, allowed_successors, is_builtin, sort)
VALUES
  (gen_random_uuid(), 'ap_authoring',
   '{"zh_Hant_TW":"稽核計畫填寫","en":"Assessment Plan Authoring"}', 'routed',
   '/project/projects/:projectUID/rounds/:roundUID/ap',
   '{"zh_Hant_TW":"完成填寫,開始稽核","en":"Submit & Start Auditing"}',
   'submit_ap', 'ap_reviewed_controls_set', '["auditor"]', false, false,
   '["planning"]', '["audit"]', true, 15)
ON CONFLICT (code) DO NOTHING;
-- planning:handler activate_project→launch_audit;precondition→round_prep_tasks_done;successor→ap_authoring
UPDATE compliance.stage_objects
   SET complete_handler_key='launch_audit', precondition_key='round_prep_tasks_done',
       allowed_successors='["ap_authoring"]'
 WHERE code='planning';
-- audit:predecessor 改 ap_authoring(不再從 task_execution/review 進)
UPDATE compliance.stage_objects
   SET allowed_predecessors='["ap_authoring","poam"]'
 WHERE code='audit';
git add scripts/sql/2026-06-16-fr038-round-flow-binding.sql
git commit -m "feat(FR-038): ap_authoring stage seed + planning 收斂(task_execution 收進 planning)"

Task 4: builtin flow_templates BPMN 重 seed(canonical 4 階段)

Files: Append scripts/sql/2026-06-16-fr038-round-flow-binding.sql

目標串: 4 個 builtin(或本期至少 builtin-full-audit)BPMN 改成 Start → planning → ap_authoring → audit → (Gateway: has_findings) → poam → audit / End。每個 UserTask extension 帶 stage_object_code + main_roleSELECT bpmn_xml ... WHERE name='builtin-full-audit' 取既有當底,在 planning 與 audit 間插 ap_authoring UserTask,移除 task_execution/review 節點。

git add scripts/sql/2026-06-16-fr038-round-flow-binding.sql
git commit -m "feat(FR-038): builtin flow templates 改 canonical 4 階段(planning→ap_authoring→audit→poam)"

Task 5: registry interface + stage_advance retarget ⚠️ 高風險

Files: Modify domain/flow_engine/service/stage_completion_registry.pyapp/flow_engine/service/stage_advance_service.py;Test test/test_stage_advance_round_context.py

def test_resolve_workflow_context_by_round(stage_advance_service, seeded_round_with_workflow):
    ctx = stage_advance_service._resolve_workflow_context(seeded_round_with_workflow.uid)
    assert ctx["workflow_execution_uid"] == seeded_round_with_workflow.workflow_execution_uid
    assert "workflow_template_xml" in ctx

def test_round_status_derived_from_stage(stage_advance_service, seeded_round_at_ap_authoring):
    # 推進 ap_authoring 後 round.status 應為 audit_planning(§4.1b 對照)
    ...
def _resolve_workflow_context(self, round_uid: str) -> dict:
    rnd = self._audit_round_domain_service.get_by_uid(round_uid)
    if rnd is None or not rnd.workflow_execution_uid:
        raise NotFound(GrcErrorCode.GRC_ROUND_NO_WORKFLOW_BINDING)
    wf = self._workflow_execution_domain_service.get_workflow_execution(
        WorkflowExecutionQueryEntity(uid=str(rnd.workflow_execution_uid)))
    if wf is None:
        raise NotFound(GrcErrorCode.GRC_ROUND_NO_WORKFLOW_BINDING)
    return {"round": rnd, "assessment_plan_id": rnd.assessment_plan_id,
            "workflow_execution_id": wf.id, "workflow_execution_uid": str(rnd.workflow_execution_uid),
            "workflow_status": str(wf.status) if wf.status is not None else None,
            "workflow_template_xml": wf.workflow_template_xml, "template_id": wf.template_id}
    • stage_advance 推進成功後,依「新的當前 stage_object_code」查 §4.1b map → audit_round_domain_service.update(status=...)
    • audit_round_app_servicelaunch_audit/start_auditing/finalize_audit/close_round移除 e.status = STATUS_* 寫死(audit_round_app_service.py:201/229/252/254/289/301),保留 status guard(讀推進前值,仍正確)+ 保留業務寫入ssp_id/assessment_plan_id/ar_result_id/poam_id)。
    • finalize_audit:回傳 gateway 變數 has_findingsres["not_met_count"]>0)供 BPMN 路由(has_findings→poam=remediation / →End=closed);不自己寫 status。
git add domain/flow_engine/service/stage_completion_registry.py app/flow_engine/service/stage_advance_service.py test/test_stage_advance_round_context.py
git commit -m "refactor(FR-038): stage 引擎 retarget round_uid + round.status BPMN 推導"

Task 6: handler 全改 wrap audit_round + 新 submit_ap ⚠️ 高風險

Files: Modify app/grc/service/oscal_stage_handlers.py;Test test/test_round_stage_handlers.py

既有 6 handler 原 wrap app/project/service/oscal_audit_service.py(2A dark no-op)。本任務全部建構子改注入 audit_round_app_serviceexecute(round_uid, …)activate_project handler 退役。

def test_submit_ap_handler_calls_start_auditing(submit_ap_handler, mock_round_service, seeded_round):
    submit_ap_handler.execute(round_uid=seeded_round.uid, project_uid="p", user_id=1, curr_user="u", ctx={})
    mock_round_service.start_auditing.assert_called_once()

(autouse logger patch fixture,mirror test_ssp_excel_import_app_service.py:46

class SubmitApOnCompleteHandler(IStageCompletionHandler):
    def __init__(self, audit_round_app_service): self._rounds = audit_round_app_service
    @property
    def key(self): return "submit_ap"
    def execute(self, round_uid, project_uid, user_id, curr_user, ctx):
        return self._rounds.start_auditing(round_uid=round_uid, curr_user=curr_user, curr_user_id=user_id)

其餘:launch_auditself._rounds.launch_auditconfirm_auditfinalize_auditclose_roundclose_roundterminal_close→round 標 closed、review_decision 保留(不 wire)。全部 __init__(self, audit_round_app_service)execute(round_uid, ...)

git add app/grc/service/oscal_stage_handlers.py test/test_round_stage_handlers.py
git commit -m "feat(FR-038): stage handler 全改 wrap audit_round + 新 submit_ap"

Task 7: precondition retarget + 新 2 個 + count_reviewed_controls

Files: Modify app/grc/service/oscal_stage_preconditions.pyapp/grc/service/assessment_plan_app_service.py;Test test/test_round_stage_preconditions.py

def count_reviewed_controls(self, ap_id: int) -> int:
    return len(self._reviewed_repo.get_by_ap(ap_id) or [])
class ApReviewedControlsSetCheck(IStagePreconditionCheck):
    def __init__(self, assessment_plan_app_service, audit_round_domain_service):
        self._ap, self._rounds = assessment_plan_app_service, audit_round_domain_service
    @property
    def key(self): return "ap_reviewed_controls_set"
    def check(self, round_uid, project_uid, ctx):
        rnd = self._rounds.get_by_uid(round_uid)
        if not rnd or not rnd.assessment_plan_id:
            return PreconditionResult.fail("lang.flow_engine.precondition.ap_reviewed_controls_required")
        n = self._ap.count_reviewed_controls(rnd.assessment_plan_id)
        return PreconditionResult.ok() if n > 0 else \
            PreconditionResult.fail("lang.flow_engine.precondition.ap_reviewed_controls_required", count=n)
# RoundPrepTasksDoneCheck:per-AO 收證據 job 全 JobStatus.COMPLETED 才放行
#   linkage:JobExecution 直接以 workflow_execution_id 過濾(workflow_execution_control_mapping
#   無 job_execution_id FK,不可 join 它)。⚠️ 收證據 job 由 _generate_prep_jobs 在「專案成立」建、
#   為專案層級(非 round 綁定)→ scoping「哪些 job 算本輪 prep」於實作時確認:
#   暫定查專案所有 prep WE 的 job 全 COMPLETED;無 prep job(template_uid 未設)→ ok 放行(不硬擋)。
git add app/grc/service/oscal_stage_preconditions.py app/grc/service/assessment_plan_app_service.py test/test_round_stage_preconditions.py
git commit -m "feat(FR-038): precondition retarget round + reviewed_controls/prep_tasks 守門 + count_reviewed_controls"

Task 8: create_round 綁 flow_template snapshot

Files: Modify app/grc/service/audit_round_app_service.py;Test test/test_create_round_flow_binding.py

範本來源(O1 a+b): 入參 flow_template_uid(選填)> 前一輪 flow_template_snapshot_uid 對應 master > 資源庫 module_frames.audit_flow_template_uid

def test_create_round_binds_flow_snapshot(round_service, seeded_project_with_resource_library):
    r = round_service.create_round(project_uid=..., name="R1", round_type="initial", curr_user="u", curr_user_id=1)
    assert r["flow_template_snapshot_uid"] is not None
    assert r["workflow_execution_uid"] is not None
git add app/grc/service/audit_round_app_service.py test/test_create_round_flow_binding.py
git commit -m "feat(FR-038): create_round clone flow snapshot + 綁 round(O1 a+b 來源)"

Task 8b: 建專案順帶建第一輪(D6)+ serializer

Files: Modify api/project/serializers/project.pyOscalProjectStartRequest 加回 flow_template_uid)、app/project/service/project_start_app_service.pystart_project 建 round 1);Test test/test_project_start_creates_first_round.py

def test_start_project_creates_initial_round_with_flow(project_start_service, seeded_resource_library):
    res = project_start_service.start_project(name="P", resource_library_uid=..., flow_template_uid=<uid>, curr_user="u", curr_user_uid=..., curr_user_id=1)
    rounds = audit_round_domain_service.list_by_project(res_project_id)
    assert len(rounds) == 1 and rounds[0].round_type == "initial"
    assert rounds[0].flow_template_snapshot_uid is not None
git add api/project/serializers/project.py app/project/service/project_start_app_service.py test/test_project_start_creates_first_round.py
git commit -m "feat(FR-038): 建專案順帶建第一輪 initial round + 綁所選流程範本(D6)"

Task 9: round-scoped stage route + 退役舊 route(ap-scoped + 4 直接轉換)

Files: Modify api/project/routes/audit_round_route.pyapi/project/__init__.pyapi/flow_engine/__init__.py;Remove api/flow_engine/routes/stage_advance_route.py;Test test/test_audit_round_stage_route.py

git add api/project/routes/audit_round_route.py api/project/__init__.py api/flow_engine/__init__.py test/test_audit_round_stage_route.py
git rm api/flow_engine/routes/stage_advance_route.py
git commit -m "feat(FR-038): round stage route + 退役舊 ap-scoped + 4 直接轉換 route(O2 收斂 stage/advance)"

Task 10: DI wiring

Files: Modify di_containers/grc/grc_containers.pydi_containers/flow_engine/*

git add di_containers/
git commit -m "feat(FR-038): DI wiring round stage handlers/preconditions + 拆 2A dark-skip"

Task 11: 整合測試 + boot 驗證

    • 建專案(選資源庫)→ 建輪次(範本帶資源庫預設)→ Banner 顯示 planning
    • planning:per-AO 收證據 job 未全完成 → 推進按鈕 disable(round_prep_tasks_done tooltip)
    • 全完成 → 推進 → snapshot SSP + AP 草稿 → 進 ap_authoring(round.status=audit_planning)
    • ap_authoring:未選 reviewed-controls → disable;選了 → 推進 → 建 AR 矩陣 → 進 audit(auditing)
    • audit → poam → close → round closed;public.system_logs 有 605x 推進紀錄

§3

Plan Review

寫完 dispatch plan-document-reviewer(spec=round-flow-engine-reintegration-design.md);❌ 修正重審;✅ handoff 執行。