Spec 3:AP 套用流程

狀態:v2 — brainstorm 收斂後完整版,待 review 整體脈絡../README.md 依賴:Spec 1(已 ship)/ Spec 2(已 ship)


§1

一、目標

建立新 AP 時 user 可挑選流程範本;AP 建立同時 snapshot clone 該範本 BPMN 到該 AP 專屬的 workflow_execution。後續範本被改 / 刪不影響進行中 AP;多 AP 之間流程獨立運作;未來可開放「中途調整 AP 自己的流程」也不會污染兄弟 AP 或 master。


§2

二、Brainstorm 結論(v1 → v2 變更)

Q 主題 決議
Q1 dropdown 是否按合規資源庫過濾 純開放 — 全部 active 範本都列;YAGNI,不加 compatible_frameworks 欄位
Q2 預覽 Dialog 呈現 跟 Spec 1 對齊 — reuse 既有 FlowTemplatePreviewDialog.vue(唯讀 BPMN + 階段清單並列)
Q3 launch_new_round 預設值 Derived from latest AP — 不加 project 層級欄位,dropdown default 取「最新一輪 AP 的 master template uid」
Q4 Snapshot 儲存方式 Reuse Spec 2 — 直接用 WorkflowTemplateSnapshotService.clone_master_as_snapshot,零 migration
Q5 第一輪 AP 入口 保留雙入口 — ProjectCreateView 跟 ProjectApListView 兩處都加 dropdown;Spec 2 lifecycle 假設不動
API flow_template_uid 必填 vs optional 必填 — 一次拔乾淨 Spec 2 hardcode default 邏輯,不留技術債

§3

三、Spec 2 補洞:launch_new_round 沒做 main workflow snapshot

審核 Spec 2 落地時發現的重要事實:

  • start_oscal_project:487 已呼叫 _bind_main_workflow_to_ap → 第一輪 AP 有 snapshot + assessment_plan_extensions + main workflow_execution
  • launch_new_round 從未呼叫該 helper → 後續輪 AP 綁 main workflow,Banner 找不到 stage → 推進按鈕永遠 disabled

→ Spec 3 不只是「加 dropdown + 把 hardcode 換成 parameter」,還要把 main workflow snapshot 整段 helper 邏輯接上 launch_new_round

這同時是 Phase E launchProjectViaUi / launchAuditViaUi 系列 fixture 撞 disabled button 的潛在根因(fixture 是新一輪 flow 上跑的)。


§4

四、Snapshot 三層獨立性保證

場景 保證機制
兩個 AP 選同一個 master 各自 clone 一份新的 snapshot row(不同 uid);engine template_id 用各自 snapshot.id
Admin 改 master XML engine 讀的是 snapshot row,現有 AP 的 snapshot.xml 不動
Admin 軟刪 master 現有 AP snapshot row 仍存在;engine 仍能跑;只影響後續新 AP 不能再選此 master
未來「中途調整 AP 流程」 snapshot row 是 per-AP 私有資料 → 直接 mutate 該 AP 的 snapshot.xml 不污染 master 也不污染兄弟 AP

§5

五、需求摘要

來源:requirement.md §「想法(粗略)」§3

  1. 建專案時 user 選流程範本,帶到自動建立的第一輪 AP
  2. 稽核計畫頁建新一輪 AP 時 user 選流程範本(default 帶最新一輪 master)
  3. AP 建立同時 snapshot clone 範本 BPMN 到該 AP 專屬 workflow_execution
  4. 後續範本被修改 / 刪除不影響進行中 AP(snapshot 不可變)
  5. 多 AP 之間流程互不影響(snapshot per-AP)

§6

六、範圍

In scope

  • BE start_oscal_project 簽章加 flow_template_uid: str (required)
  • BE launch_new_round 簽章加 flow_template_uid: str (required) + 補做 main workflow snapshot
  • BE _bind_main_workflow_to_ap 改為接 flow_template_uid 必填(拔 default lookup)
  • BE 拔除 FlowTemplateDomainService.DEFAULT_MAIN_WORKFLOW_TEMPLATE_NAME / get_default_main_workflow_template()
  • BE AP 詳情 API 補回 flow_template enrichment(snapshot uid / master uid / master name)
  • BE error code 整理:刪 GRC_FLOW_MAIN_TEMPLATE_NOT_FOUND(412022);reuse 既有 GRC_FLOW_TEMPLATE_NOT_FOUND(404027)
  • FE ProjectCreateView 加流程範本 dropdown + 預覽 Dialog
  • FE ProjectApListView「新增稽核計畫」Dialog 加流程範本 dropdown + default 帶值邏輯
  • FE i18n(zh-tw / en)

Out of scope

  • 範本管理 UI(→ Spec 1,已 ship)
  • Stage 推進 runtime(→ Spec 2,已 ship)
  • 中途調整 AP 流程 UI(未來 spec)
  • Overview shell 化重構(Banner 已在 Spec 2 掛好)
  • Stage progress bar 互動跳轉(v1 純預覽,Spec 2 已處理)
  • 範本按合規資源庫過濾(Q1 YAGNI)
  • 跨 tenant 範本共享(v1 純 tenant 隔離;RLS 自動處理)

§7

七、AP 建立入口(雙入口)

入口 URL / View 動作 API
建專案(自動帶第一輪 AP) /project/projects/newProjectCreateView 建立專案 + 自動建第一輪 AP POST /oscal/projects/startOscalProjectService.start_oscal_project
建後續輪 AP /project/projects/:idProjectApListView「新增稽核計畫」Dialog 建新 AP POST /grc/project/<uid>/launch-new-roundOscalProjectService.launch_new_round

兩處共用同一個「流程範本 dropdown + 📋 詳細選擇」UX,預覽元件 reuse Spec 1 FlowTemplatePreviewDialog.vue


§8

八、架構:data flow

建專案 / 建後續輪 AP(任一入口)
  ↓ (使用者選範本 uid)
flow_template_uid (UUID, required) ──┐
                                     ▼
start_oscal_project / launch_new_round
                                     │
                                     ▼
_bind_main_workflow_to_ap(ap, project_uid, flow_template_uid, curr_user)
                                     │
                ┌────────────────────┴────────────────────┐
                ▼                                         ▼
  FlowTemplateDomainService                  GRC_FLOW_TEMPLATE_NOT_FOUND
    .get_by_uid(flow_template_uid)           (uid 不存在 / is_active=false)
                │
                ▼
  master (FlowTemplateEntity)
                │
                ▼
  WorkflowTemplateSnapshotService.clone_master_as_snapshot(
    master_uid=master.uid,
    master_name=master.name,
    xml=master.bpmn_xml,
    description=master.description,
    curr_user=curr_user,
  )
                │
                ▼
  snapshot (engine `public.workflow_templates`,per-AP immutable)
                │
                ▼
  workflow_execution_service.start_workflow_execution(template_id=snapshot.id, ...)
                │
                ▼
  main_wf (engine `workflow_executions`)
                │
                ▼
  ap_extension_domain_service.upsert(
    assessment_plan_id=ap.id,
    workflow_execution_uid=main_wf.uid,
    flow_template_snapshot_uid=snapshot.uid,
    curr_user=curr_user,
  )
                │
                ▼
  assessment_plan_extensions row written
                │
                ▼
  AP 詳情 API enrichment ◀── 從 ext 反查 snapshot → 反查 master → 三段組合回傳

§9

九、BE 改動細節

9.1 OscalProjectService.start_oscal_project

@transaction
def start_oscal_project(
    self,
    project_name,
    ...既有參數...,
    flow_template_uid: str,         # 新增,必填
    curr_user: str = None,
    locale: str = None,
) -> Project:
    ...既有 21 個 step 不動...

    # Step 22 改造:把 flow_template_uid 顯式傳入
    self._bind_main_workflow_to_ap(
        ap=ap,
        project_uid=str(project.uid),
        flow_template_uid=flow_template_uid,
        curr_user=curr_user,
    )
    return project

9.2 OscalProjectService.launch_new_round

@transaction
def launch_new_round(
    self,
    project_uid: str,
    user_id: int,
    title: str,
    flow_template_uid: str,         # 新增,必填
    start_date=None, end_date=None,
    curr_user: str = None,
    locale: str = None,
) -> dict:
    ...既有 step 19 不動(含 sub-workflow per-task clone,跟 main 不衝突)...

    # 新增 step 10:補做 main workflow snapshot(Spec 2 的 gap)
    self._bind_main_workflow_to_ap(
        ap=ap,
        project_uid=str(project_entity.uid),
        flow_template_uid=flow_template_uid,
        curr_user=curr_user,
    )

    return {...既有...}

9.3 _bind_main_workflow_to_ap 改造

def _bind_main_workflow_to_ap(
    self,
    ap,
    project_uid: str,
    flow_template_uid: str,         # 新增,必填
    curr_user: str,
) -> None:
    """Spec 3 — 接 caller 指定的 flow_template_uid,clone 一份 per-AP 凍結 snapshot
    到 engine 表,啟動 workflow_execution,寫 assessment_plan_extensions。

    Caller 已開 @transaction scope,本 helper 不再加。
    """
    if (self._flow_template_domain_service is None
            or self._ap_extension_domain_service is None
            or self._workflow_template_snapshot_service is None):
        raise PreconditionFailedError(GrcErrorCode.GRC_FLOW_TEMPLATE_NOT_FOUND)

    master = self._flow_template_domain_service.get_by_uid(flow_template_uid)
    if master is None or not master.is_active:
        raise NotFound(GrcErrorCode.GRC_FLOW_TEMPLATE_NOT_FOUND)

    snapshot = self._workflow_template_snapshot_service.clone_master_as_snapshot(
        master_uid=str(master.uid),
        master_name=master.name,
        xml=master.bpmn_xml,
        description=master.description,
        curr_user=curr_user,
    )
    main_wf = self.workflow_execution_service.start_workflow_execution(
        template_id=snapshot.id,
        name=f"AP-{ap.uid}-main",
        params={"ap_uid": str(ap.uid), "project_uid": project_uid},
        created_user=curr_user,
        description=f"AP main workflow for {ap.title}",
    )
    self._ap_extension_domain_service.upsert(
        assessment_plan_id=ap.id,
        workflow_execution_uid=str(main_wf.uid),
        flow_template_snapshot_uid=str(snapshot.uid),
        curr_user=curr_user,
    )

9.4 拔除 Spec 2 過渡 hardcode

domain/flow_engine/service/flow_template_domain_service.py

# 全部移除
# DEFAULT_MAIN_WORKFLOW_TEMPLATE_NAME = "完整稽核流程"
# def get_default_main_workflow_template(self) -> Optional[FlowTemplateEntity]: ...

IFlowTemplateRepo.get_builtin_by_name() — grep 確認無其他 caller 即刪。

9.5 Route schema 改動

Endpoint Request schema 改動
POST /oscal/projects/start flow_template_uid: fields.UUID(required=True)
POST /grc/project/<uid>/launch-new-round 同上

9.6 AP enrichment(list + detail)

兩個 endpoint 都要回 flow_template enrichment

  • GET /grc/project/<uid>/aps(列表)— FE default 帶值靠這個,必須有
  • GET /grc/project/<uid>/aps/<apUid>(詳情)— FE 顯示「目前用的範本」靠這個

Response 加欄位:

{
  ...既有欄位...,
  "flow_template": {
    "snapshot_uid":     "engine-snapshot-uid",
    "master_uid":       "compliance-master-uid",
    "master_name":      "完整稽核流程",
    "is_master_active": true
  }
}

Enrichment path(app service 層):

  1. assessment_plan_extensions by assessment_plan_idflow_template_snapshot_uid
  2. 用 snapshot uid 取 engine workflow_templates row → source_template_uid
  3. 用 master uid 取 compliance.flow_templates row → name + is_active

Enrichment 行為(lenient):若任一段缺失(舊資料 ext 不存在 / master 已硬刪等 edge case),flow_templatenull不擋 API 回應is_master_active=false 代表 master 已軟刪但 snapshot 仍正常運作(這 AP 還能跑完現有 lifecycle)。

對比 — 建立路徑(strict,見 §十一):建 AP 時 master.is_active=False 直接 404 拒絕。語意:「進行中 AP 容忍 master 被刪、但新 AP 不能選已刪的 master」。

列表 enrichment 效能考量:列表 enrichment 會 N+1 — N AP × 3 段 query。實作要用 batch query:一次取所有 ext rows → 一次 batch 取 snapshot rows → 一次 batch 取 master rows,三段 dict join 完拼 response。

9.6.1 取代既有 workflow_template_uid 欄位(注意:清掉 §906)

launch_new_round 既有程式碼(line 906–924)對 task 動態 setattr task.workflow_template_uid — 那是 sub-workflow per-task 的 template uid,跟本 spec 加的 main workflow flow_template 是不同層級。保留,不衝突。但 §9.6 enrichment 必須清楚標 flow_template 是 main workflow(AP 級別),跟 task-level sub-workflow 分開不混淆。

9.7 Error code

Code 動作
GRC_FLOW_TEMPLATE_NOT_FOUND (404027) reuse,原本 Spec 1 已有
GRC_FLOW_MAIN_TEMPLATE_NOT_FOUND (412022) 刪除(default lookup 拔了就用不到)
GRC_AR_DATA_MISSING (500001) 順手 fix:改 412024(Spec 2 carry-forward M1)

§10

十、FE 改動細節

10.1 ProjectCreateView.vue

加在「基本資訊」step(建議 Step 0 末尾):

基本資訊
├── 專案名稱 *
├── 描述
├── 合規資源庫 *
├── 第一輪稽核計畫流程範本 *    ← 新增
│   ├── <Dropdown
│   │     v-model="form.flowTemplateUid"
│   │     :options="flowTemplates"
│   │     optionLabel="name"
│   │     optionValue="uid"
│   │     :loading="loading"
│   │     :class="{'p-invalid': v$.flowTemplateUid.$error}" />
│   └── <Button text size="small" @click="openPreview">📋 詳細選擇</Button>
└── ...

10.2 ProjectApListView.vue 新增稽核計畫 Dialog

新增稽核計畫
├── 計畫名稱 *
├── 起訖日期
├── 流程範本 *                  ← 新增
│   ├── <Dropdown ... v-model="form.flowTemplateUid" />
│   └── <Button text>📋 詳細選擇</Button>
└── ...

Default 帶值(依賴 §9.6 — AP 列表 API 必須含 flow_template enrichment):

const defaultFlowTemplateUid = computed(() => {
  // apList 排序:最新一輪在 [0](既有排序慣例,新 spec 不改)
  // 依賴 GET /grc/project/<uid>/aps 回 flow_template enrichment
  const latest = props.apList?.[0]
  return latest?.flow_template?.master_uid ?? null
})

watch(() => props.visible, (v) => {
  if (v) form.value.flowTemplateUid = defaultFlowTemplateUid.value
})

注意: 若 §9.6 列表 enrichment 未實作,dropdown default 會永遠 null,user 每次都要重選。列表 enrichment 是 dropdown UX 的硬依賴,不能延後到 v2 補。

10.3 預覽 Dialog reuse

<FlowTemplatePreviewDialog
  v-model:visible="previewVisible"
  :template-uid="form.flowTemplateUid"
/>

Spec 1 已 ship src/views/flow-template/components/FlowTemplatePreviewDialog.vue — input 是 master uid,內部自己拉 detail + render BPMN + 列階段清單。

10.4 Service / API client

src/service/FlowTemplateService.js(Spec 1 已有 list / detail),新增方法可直接 reuse。

10.5 i18n

zh-tw:

project:
  flowTemplate:
    label: 流程範本
    firstRoundLabel: 第一輪稽核計畫流程範本
    placeholder: 請選擇流程範本
    detailBtn: 詳細選擇
    requiredError: 請選擇流程範本
    inheritedHint: 預設沿用上一輪稽核計畫的流程範本

en 對應 key 一併補。


§11

十一、驗證與 Error handling

情境 行為
flow_template_uid 400 by webargs schema validation
flow_template_uid 非 UUID 格式 400 by webargs schema validation
UUID 形式正確但查無此 master(不存在 / 軟刪) 404 GRC_FLOW_TEMPLATE_NOT_FOUND
master.is_active = False 404 同上
Cross-tenant master RLS 自動 filter → 404
Engine snapshot 建立失敗 / start_workflow_execution 失敗 整個 @transaction rollback;不留半成品 AP

§12

十二、測試策略

12.1 BE pytest

案例 範圍 對應
start_oscal_project 帶 valid uid → 第一輪 AP 有 snapshot + ext integration §9.1
start_oscal_project 帶 invalid uid → 404 unit §11
start_oscal_project 缺 uid → 400 unit §11
launch_new_round 帶 valid uid → 新輪 AP 有獨立 snapshot + ext(與第一輪不同 snapshot uid) integration §9.2 + §三
Snapshot 隔離:建 AP 後改 master XML → AP snapshot.xml 不動 integration §四
AP 詳情 API 回 flow_template enrichment integration §9.6
AP 詳情 enrichment 在 master 已軟刪時 is_master_active=false integration §9.6

順手補 Spec 2 carry-forward test gap(同 commit 或鄰近 commit):

  • WorkflowTemplateSnapshotService — clone 行為 unit test
  • StageAdvanceService — stage 推進 path unit test
  • oscal_stage_preconditions — precondition function 個別 unit test
  • oscal_stage_handlers — handler 個別 unit test
  • AssessmentPlanExtensionRepoImpl — upsert / 查詢 unit test

12.2 FE E2E(compliance-manager-test)

留到 Spec 3 BE+FE 都 ship 後 — 跟 Phase E 4 fixture bug 一起重做:

  • ProjectCreatePage.addFourParticipantsWithRoles(適配 wizard step 數變化 + 新增範本 dropdown)
  • launchProjectViaUi / launchAuditViaUi / launchRemediationViaUi
  • 重寫 Spec 2 Banner 3 個 scenario(adapt 新範本選擇 UI)

12.3 Manual smoke(必過項)

□ 建專案選範本 A「完整稽核流程」→ 第一輪 AP1
  └ Banner 顯示 stage = 規劃 → 推進按鈕作用 → status preparing→active
  └ 連續推完 4 個 stage 到 closed
□ 同專案 launch_new_round 選範本 B「自我評估稽核」(少 poam)
  └ AP2 snapshot uid 跟 AP1 不同
  └ Banner stage 組合對應 B 的 BPMN(規劃→執行→稽核→closed,跳 poam)
  └ 推到 audit 完成後直接 closed
□ DB 驗:
  - assessment_plan_extensions 兩列,flow_template_snapshot_uid 不同
  - public.workflow_templates 兩列 snapshot row,source_template_uid 對應 A/B master
  - 兩個 workflow_execution 互不影響
□ Smoke 通不過 → 根因落 Spec 2 stage_advance_service / oscal_stage_handlers
  → 此時補 Spec 2 carry-forward 5 service test gap 是必做(不再延後)

§13

十三、落地順序

里程碑 範圍 提交策略
M0 Pre-flight verify:grep get_default_main_workflow_template / get_builtin_by_name / DEFAULT_MAIN_WORKFLOW_TEMPLATE_NAME 所有 caller;確認除了 _bind_main_workflow_to_ap 外無其他 reference 無 commit,純檢查
M1 BE error code 整理(刪 412022、fix 500001→412024) 獨立 commit
M2 BE AP 列表 + 詳情 API 補 flow_template enrichment(純加欄位,不破 caller) 獨立 commit
M3 BE Spec 2 carry-forward 5 service test gap 補測試(move ahead — 防 M4 改動踩到沒測過的 service) 獨立 commit
M4 BE _bind_main_workflow_to_apflow_template_uidstart_oscal_project + launch_new_round 加參數;拔 default lookup;加 launch_new_round 呼叫 helper 獨立 commit
M5 FE ProjectCreateView dropdown + 預覽 Dialog + i18n 獨立 commit
M6 FE ProjectApListView Dialog dropdown + default 帶值 + i18n 獨立 commit
M7 Manual smoke(必過項,§12.3)+ 收尾 changelog 必過
M8 (事後)Phase E 4 fixture bug 修補 + 3 個 Banner BDD scenario 重寫 跨 repo

M3/M4 順序理由:M4 改 _bind_main_workflow_to_ap + 拔 get_default_main_workflow_template 會影響 StageAdvanceService / WorkflowTemplateSnapshotService 等 carry-forward 服務。先 M3 補測試,再做 M4 改動,M4 出 regression 時 test 立刻 fail,不會拖到 M7 smoke 才發現。


§14

十四、對外依賴

依賴 提供方 狀態
GET /flow-engine/flow-templates(列表) Spec 1 ✅ 已 ship
GET /flow-engine/flow-templates/<uid>(detail + BPMN XML) Spec 1 ✅ 已 ship
FlowTemplatePreviewDialog.vue Spec 1 FE ✅ 已 ship
WorkflowTemplateSnapshotService.clone_master_as_snapshot Spec 2 ✅ 已 ship
_bind_main_workflow_to_ap helper(既有 backbone) Spec 2 ✅ 已 ship(Spec 3 改造參數)
assessment_plan_extensions table + repo Spec 2 ✅ 已 ship

§15

十五、開放問題(v2 已 resolved)

v1 列的 5 個 open questions 全部 resolved(見 §二)。

v2 沒有新增 open question — 進入 Phase 3 implementation-plan.md 階段。


§16

十六、Implementation Reality / Reconciliation(v3 加,summary 收尾時)

Phase 5 實作 + M7 smoke + Phase 6 review 後,下列為實作偏離 v2.1 plan 的條目。記錄目的:給未來讀者保留決策軌跡,不是補救。

16.1 Plan 偏差

# Plan 寫的 實作的 原因
1 M2 plan 寫「修改 jedi-flow-engine 加 get_many_by_uids 不動套件,用 enricher 內 _engine_get_many_by_uids adapter 把既有 get_workflow_templates_by_uids list 結果轉 dict jedi-flow-engine 0.0.27 已有此 method(返回 list),M2 implementer 評估後 reuse
2 M2.4 plan 範例 AP detail GET endpoint AssessmentPlanDetailResource 只有 PUT/DELETE 沒 GET grep 後發現 FE 也未呼叫 detail GET;enricher 改在 PUT response 補;FE 未來要 detail GET 時再加
3 M2.4 plan 寫 integration test 用 app_client + DB fixture test/ 目錄無 DB fixture infra 改 service-level unit test 用 mock enricher 驗 service 把 enrichment 塞到 dto;integration coverage deferred
4 M4.2 plan 寫的 schema class 名 StartProjectRequestSchema 實際是 OscalProjectStartRequestStartProjectRequest 是 module_frame 另一 route 用) Plan 寫錯 class 名
5 M4.3 plan 寫 launch_new_round 簽章 flow_template_uid: str(無 default required positional) 實際 flow_template_uid: str = "" + 內部 raise BadRequestError(GRC_FLOW_TEMPLATE_UID_REQUIRED) 與既有 schema integration test pattern 對齊;schema 已 enforce required,service 層的 default 是 defensive guard(後續 PR-TD2 計畫改回必填)
6 M0 Step 6 poetry show jedi-flow-engine 預期輸出 location path 實際只輸出 version 0.0.27 grep pyproject.toml 直接驗 path mode 取代
7 Plan 假設 dropdown 落點在 ProjectBasicInfoForm.vue 實際加在 ProjectCreateView.vue 內 inline 跟既有 profile dropdown 同 slot 層 M0 Step 8 grep 後決定(ProjectBasicInfoForm 是 generic component)
8 Plan 範例 FE service method FlowTemplateService.getList({ is_active: true }) 實際既有 service listFlowTemplates({page, size, name, is_builtin}){items, total, page, size} M5 implementer 對齊既有 API;BE service 已固定 is_active=True filter,FE 不用傳
9 Plan 範例 FlowTemplatePreviewDialog:template-uid 實際 prop 是 :template Object(含 bpmn_xml 既有 component 設計;M5 加 openFlowTemplatePreview helper 串接 list summary + detail fetch

16.2 M7 smoke 發現的 Spec 2 carry-forward bug(M9–M13)

# 問題 修補 commit
M9 自建「規劃→執行→End」流程:執行任務 stage advance 後 AP 卡 auditing 出不來,因為 task_execution stage 的 hardcode handler 是 launch_audit(假設下一步是 audit stage) BE a365d71 — 加 _is_terminal_user_task 偵測 + TerminalCloseHandler + OscalAuditService.terminal_close_ap 寧包子 close
M10 自建流程沒含 audit/poam stage 時 FE「稽核結果」「改善計劃」按鈕仍依 status 顯示但點進去無資料 BE d0e6465 — enricher 加 stage_codes from BPMN;FE 152d1a1 — 兩個 view button v-if 加 stage gate
M11 「規劃→End」流程:AP 已 closed 但 project.status 卡 PENDING(M9 漏寫 project.status sync),列表顯示「未開始」跟詳情「已結案」不一致 BE 02512b9terminal_close_ap 加 project_uid 參數 + project.status → COMPLETED;dev DB 既有資料用 SQL 手動修
M12 自建流程沒掛 OSCAL task 時 completion_rate 永遠 0%;AP closed 後 user 預期 100% 但仍 0% BE 728add2 + b979a71(hotfix String import)— 4 處 completion_rate 計算路徑加「ap_status='closed' → 100%」分支
M13 ApListView row buttons 只判斷 status+stage_codes,缺角色維度;ProjectAuditorOverview 右上同名 button 已對齊角色但 ApListView 沒 FE 695baad — 加 isAuditor computed + v-else-if 三維度判斷對齊

16.3 §11 strict-vs-lenient 行為對比實際落地

Path 設計 實作
建立 AP (_bind_main_workflow_to_ap) strict — master 不存在 / is_active=False 拒絕 404 raise NotFound(GRC_FLOW_TEMPLATE_NOT_FOUND)
列表 / 詳情 enrichment (FlowTemplateEnricher) lenient — master 軟刪 is_master_active=False 不擋 API ✅ 三段 batch query lenient fallback,缺 ext / 缺 master 都回 None 或 partial
BPMN parse 失敗 (_extract_stage_codes) M10 新加 — lenient(不擋 enrichment) try/except[] 加 log warning
terminal stage detection (_is_terminal_user_task) M9 新加 — get_next_elements 空 → terminal ✅ 含 Gateway 視為 non-terminal(user 確認的 rule)

16.4 v3 已知 follow-up(不影響 ship)

整理自 Phase 6 review docs/issues/pending/2026-05-13-spec3-phase-6-followups.md

項目 優先級
_sync_project_status_to_completed helper 統一三處不一致 High
terminal_close_ap 加角色檢查 High
launch_audit dead code 54 行刪除 Medium
M12 completion_rate 補 unit test Medium
FE default flow_template guard(master 軟刪 UX) Medium
Type design 重構(payload dataclass / StartProjectDto 必填 / StageInfoDto) 中期

16.5 Spec plugin 化討論

User Phase 5 + smoke 收尾後提出「stage 物件能否 plugin 化」討論,當前不做,紀錄在 docs/analysis/2026-05-13-stage-plugin-architecture-thoughts.md。3 個 phase(FE button config / StagePlugin 整合 / auto-discovery)+ 觸發條件已記錄。


§17

十七、文件版本

版本 日期 變更
v1 2026-05-13 初版大方向,5 條 open questions 待 brainstorm
v2 2026-05-13 brainstorm 收斂完整版:Q1–Q5 全部 resolved;補 §三 launch_new_round Spec 2 gap 認知;補 §四 snapshot 三層獨立性;BE/FE 改動細節到位;落地順序拆 M1–M8
v2.1 2026-05-13 spec reviewer 反饋整合:§9.6 列表 + 詳情兩個 endpoint 都要 enrichment(strict vs lenient 行為對比);§10.2 標明 default 帶值靠 list enrichment(硬依賴);§13 加 M0 pre-flight verify、M3/M4 順序對調防 regression
v3 2026-05-13 summary 收尾加 §16 Implementation Reality / Reconciliation:plan 偏差 9 條 + M7 smoke 補 M9–M13 5 條 + strict/lenient 落地對照 + Phase 6 follow-up 引用 + plugin 化討論 reference