FR-135 · 第二期設計文件(平台層 P2-A)· 2026-10-11

沒有評估目標的框架,也看得到條文、開得出工作

規劃頁控制項多帶條文與建議證據;收證據範本改系統層級,沒有 AO 的控制項每條各開一筆準備期工作,覆核輪照樣開

第〇步露出兩個任何沒有評估目標(AO)的框架都會踩到的底層缺口:規劃頁只看得到控制項標題,看不到條文與建議證據;建專案時收證據的流程範本來源全空,準備期一個工作都開不出來,而且沒有任何錯誤訊息。本文件把決策者已裁的 D4、D8、D12 落成可直接施工的設計:控制項樹多帶兩欄、「收證據」範本改系統層級隨安裝帶上、沒有 AO 的控制項每條各開一份流程、覆核輪從上一輪取範本。拆成 A1/B1/B2/B3 四卡,CMMC 這類有 AO 的框架行為不變。

D4/D8/D12 已裁+實作層決定 P1~P9 P2-A 四卡:A1/B1/B2/B3 跨 3 repo:BE/FE/jedi-compliance-audit 陷阱:只補範本不改 clone 粒度=31 筆工作互相覆蓋 陷阱:驗收必須用非 102 租戶

狀態:設計定案|日期:2026-10-11|範圍:第二期 P2-A(平台層 A/B,四卡 A1/B1/B2/B3)|CRA 層 P2-B~D 見 design-cra.md 討論稿(現況盤點、方案比較、D1~D14 原文):phase2-discussion.md

🔴 這份是設計決策,不是現況。 內文行號停在 2026-10-11(BE 89aadc59b、FE 9d50d2d1),實作改動後不回頭改寫;上線後的現況看 SPEC 頁。

變更紀錄

日期 變更 對應
2026-10-11 初版設計定案(D4、D8、D12 落地方式+實作層決定 P1~P9),拆 A1/B1/B2/B3 四卡 FR-135 母卡 CM-2675

1. 需求背景與端到端流程

第〇步拿 CRA 31 條在 DEV 走完一輪,露出兩個跟 CRA 無關、任何框架只要控制項底下沒有評估目標(AO,Assessment Objective,一條控制項要查的幾件具體事)就會踩到的缺口:

  1. 規劃頁看不到條文與建議證據:顧問在規劃頁只看得到控制項標題。CMMC 每條都有 AO 文字補上「要查什麼」,所以沒人發現;CRA 沒有 AO,畫面就只剩一個標題。後端其實已把每條控制項的全部段落撈出來,只是只留 AO、把條文(statement)與建議證據(guidance)丟掉。
  2. 沒有 AO 時準備期一個工作都開不出來:程式已寫好「沒有 AO 就在控制項層開一筆」,但收證據的流程範本來源全空,於是靜默略過,建專案回 job_count: 0,只在 info log 記一筆 skipped。

為什麼不限 CRA 做:之後的 SEMI E187、ISO 系列、客戶自訂框架多半也不會一條條寫 AO。這兩處修在平台層,CRA 只是第一個用到的框架;CMMC 這類有 AO 的框架,所有行為維持不變。

端到端:啟動稽核時每條控制項怎麼開出準備期工作

%%{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 API as 建專案/發起覆核
  participant ST as 啟動服務
  participant GEN as 準備期產生器
  participant WT as 流程範本表
  API->>ST: start_project(或 launch_reverify)
  ST->>ST: 讀資源庫 template_uid
  Note over ST: 空值 → 退回系統「收證據」範本(新)
  ST->>GEN: generate(fallback_template_uid)
  loop 每條在範圍內的控制項
    alt 有 AO
      loop 每個 AO
        GEN->>GEN: 範本來源=AO props 的 workflow-template-uid,沒有才用 fallback
      end
    else 沒有 AO
      GEN->>GEN: 控制項層一筆(AO 編號=控制項編號),範本來源=fallback
    end
    alt 範本來源仍是空,或讀不到
      Note over GEN: 略過並記 warning,寫出控制項編號(新)
    else 讀得到(SYSTEM 範本所有租戶可讀)
      GEN->>WT: 每條各 clone 一份專案副本(控制項層不再共用,新)
      GEN->>GEN: 建工作、注入工作 uid、寫控制項對應紀錄
    end
  end
圖 1 — 修好後的判斷順序(藍底=本期新增或改動)

覆核輪(launch_reverify)走同一支產生器,差別只在範本來源:有 AO 讀 AO props 現值;控制項層改為從上一輪同一控制項的對應紀錄取上一輪的專案副本再 clone(本期新)。


2. 分工概述(30 秒版)

整案一句話:任何沒有 AO 的框架,規劃頁也看得到條文與建議證據;建專案時每條控制項各開一筆收證據的工作,覆核輪照樣開;有 AO 的框架畫面與行為一點都不變。

卡 做什麼 產出 完成怎麼判定(決策者檢查法)
A1 規劃頁條文與建議證據 控制項樹多帶「條文」「建議證據」兩欄,規劃頁控制項面板顯示兩塊 BE 樹多兩欄、FE 新元件一支 開 CRA 專案規劃頁點任一條,看得到「條文」「建議證據」兩塊、段落換行正常;開 CMMC 專案同一頁,AO 面板與現在一樣
B1 收證據範本系統化 「收證據」範本改系統層級並隨安裝帶上;資源庫沒設範本時退回這份 migration 一支、啟動服務一處 換一個非 102 的租戶,用 CRA 公版建專案,log 不再出現 skipped=31;新裝機查得到這份範本且 scope=SYSTEM
B2 每條各開一份流程 控制項層每條 clone 自己的流程副本;略過時記 warning;規劃頁控制項層那筆帶得到流程 產生器、控制項樹兩處 CRA 新專案準備期 31 筆工作;任開一條的流程編輯器加一個任務存檔,另一條不受影響;規劃頁每條都能打開流程編輯器
B3 覆核輪也開得出 覆核輪控制項層從上一輪取範本,不再傳空 jedi-compliance-audit 一處+發版 CRA 專案首輪判一條「不符合」後發起覆核,覆核輪那條有工作,且承襲上一輪指派人

依賴:A1 與 B1 平行;B2 等 B1;B3 等 B2(要用到 B2 寫的對應紀錄形狀)。A1 不碰 B 系列任何檔。

3. 決策定案

決策者已裁(2026-10-11,討論稿 D4/D8/D12)

# 決策 定案 理由 被排除的方案
D4 「收證據」範本怎麼讓所有租戶讀得到 改 scope=SYSTEM,uid 固定 f4038001-0000-4000-8000-000000000001,隨安裝帶上;出貨基線只放行這一份 流程範本表的 RLS(資料庫依租戶隔離的規則)讀取政策放行 scope='SYSTEM';不改的話除了 DEV 租戶 102,所有租戶都讀不到、照樣 0 工作;新裝客戶更是根本沒有這份 每個租戶建立時各複製一份(新租戶流程多一步、範本升版要逐租戶改);放任 TENANT(等於沒修)
D8 覆核輪控制項層傳空值 本期修,改 jedi-compliance-audit,與 B 系列一起發版 平台層 B 修好後首輪有工作、覆核輪卻沒有,使用者會以為是新 bug;改法小 留到 GRC(首輪與覆核輪行為不一致的期間會被當成退步)
D12 收證據範本來源 啟動時讀到空值退回系統預設;建資源庫的 INSERT 不改 既有資源庫不必回頭補值,所有租戶自動生效;資源庫有設的照舊優先 建資源庫時寫入 template_uid(要回頭補 DEV 既有 15 庫與客戶既有資源庫,且公版 seed 也要跟著改)

本文件實作層決定

# 決定 理由
P1 收證據範本走 phase=seed 的主專案 migration(冪等 UPSERT,新裝與升級都套),不 dump 進 04-seed-core.sql;04 的「workflow_templates 必須為空」檢查維持不動 照現行出貨規範(scripts/init/README.md:31-48):主專案自己的預設資料走 phase=seed migration,先例是公版稽核流程 2026-09-14-fr093-4d-flow-templates-stage-objects.sql。新裝順序是 init(04 seed 的檢查此時表仍為空)→ init_apply_package_content 跑 migrate(install.sh:1859-1862)才插入,所以 04 檢查不衝突。dump 進 04 的話既有客戶升級拿不到——那正是 FR-093.4d 收掉的病
P2 系統預設 uid 寫成模組常數,放 app/flow_control/service/prep_job_generation_service.py(與 _DEFAULT_PREP_TASK_ID 並列),啟動服務 import 使用 「收證據」BPMN 的任務 id collect_evidence 已在此檔當預設;uid 與它是同一份範本的兩個契約,放一起
P3 退回系統預設寫在產生器(generate() 內 fallback_template_uid or 系統預設),覆核輪與首輪共用;啟動服務不另寫 一處判斷兩條路都生效;寫在 project_start_app_service.py:146 只修首輪
P4 控制項層(part is None)每條各 clone,不進共用快取;有 AO 的路徑維持現行快取 有 AO 時每個 AO 的範本 uid 本來就各不同(建資源庫時 _init_ao_workflows 每 AO 一份),快取實質 1:1,不動以免影響 CMMC
P5 略過時 logger.warning,訊息帶控制項編號與原因(「無範本來源」/「範本讀不到」),彙總行照舊 info 現在 0 工作只有一行 info,除錯時看不出是哪條、為什麼
P6 規劃頁控制項層那筆補 workflow_template_uid/workflow_execution_uid,只做有輪次的路徑;無輪次的舊路徑(依 AO props 找工作)不改 專案啟動必建首輪;DEV 控制項對應紀錄 64,063 筆 round_id 全非空(2026-10-11 查),無輪次路徑實際只剩資源庫範本 SSP,那裡本來就沒有工作
P7 控制項樹新欄位叫 statement/guidance,不沿用 description;AO 物件不加欄 框架管理頁的 description 是條文、規劃頁的 description 是空佔位,同字兩義
P8 規劃頁兩塊做成新元件 ControlReferenceText.vue(放 src/components/grc/planning/),ProjectPlanningView.vue 只加一行引用 該檔 2,638 行已超過 800 行上限,新增邏輯只准新開檔
P9 稽核執行頁 RoundAuditReviewView.vue:337-338 寫死空字串本期不接 不在第〇步缺口內;那頁讀的是 AR 樹不是規劃樹,接起來要另查資料來源。列入 §10 後續

4. 現況接入點盤點

座標以 BE 89aadc59b、FE 9d50d2d1、jedi-compliance-audit 工作樹為準。

元件 座標 現況 本案動作 卡
控制項段落解析 app/oscal/service/ssp_control_implementation_service.py:165,224-228 每條控制項 get_by_control 撈全部段落,只留 assessment-objective 同一次撈取多回 statement/guidance A1
分類器目錄 同檔 :261,266-272 為了拿 statement 又對每條重打一次 get_by_control 改吃上一列多回的 statement,刪重複查詢 A1
控制項物件 同檔 :757 _build_control、:806-817 回傳 無 statement/guidance 加兩欄 A1
無 AO 假節點 同檔 :792-804 塞一個與控制項同名的 AO;有輪次時帶工作,但沒有範本與流程 uid 補 workflow_template_uid/workflow_execution_uid(從 wf_uid_by_key/we_uid_by_key 取 (cid,cid)) B2
輪次對位 同檔 :530-538 有輪次時以 (control_id, ao_part_id) 收工作、範本 uid、流程 uid 不改,B2 直接取用 —
挑段落先例 app/oscal/service/framework_version_edit_service.py:266-276 _statement_prose/_guidance_prose 照同樣挑法(取第一筆 name 相符的 prose) A1
規劃頁組樹 FE src/views/project/ProjectPlanningView.vue:647 控制項 description: '' 寫死 接 statement/guidance A1
規劃頁控制項面板 同檔 :2201 只渲染 description(永遠空) 下方引用新元件 A1
換行樣式先例 同檔 :2226 white-space: pre-wrap 新元件照抄 A1
流程編輯器入口 同檔 :1406-1410 workflow_template_uid 空就提示「沒有流程」 不改,B2 補資料後自然可開 —
範本來源讀取 app/project/service/project_start_app_service.py:123,146 讀資源庫 template_uid,DEV 全部資源庫皆空 不改(P3:退回寫在產生器) —
準備期產生器 app/flow_control/service/prep_job_generation_service.py:142-156 _clone_for 同來源只 clone 一次 控制項層每條各 clone B2
同上 同檔 :168-175 範本來源空或讀不到 → skipped+=1 靜默略過 退回系統預設(B1);略過改 warning(B2) B1、B2
同上 同檔 :220-225 info 彙總行 不改 —
收證據範本 DEV compliance.workflow_templates uid f4038001-…0001 scope=TENANT、tenant_id=102;衍生副本 0 份 migration 改 SYSTEM、新裝帶上 B1
範本讀取政策 scripts/init/02-schema.sql:26101 SELECT 放行 scope='SYSTEM' 不改,靠它 —
出貨基線檢查 scripts/init/04-seed-core.sql:824,867-869(產生器 gen_seed_sql.sh:289,333) workflow_templates 必須為 0 不改(P1) —
覆核輪 jedi-compliance-audit jedi_compliance_audit/app/service/audit_round_app_service.py:774-779 fallback_template_uid=None 改傳上一輪範本對照(見 §5.4) B3
承襲配對 infra/flow_control/repository/reverify_clone_query.py:31-53 兩輪以 (control_id, ao_part_id) 配對,IS NOT DISTINCT FROM 不改,控制項層 (cid,cid) 已對得上 —

5. 詳細設計

5.1 A1:控制項樹帶條文與建議證據

後端(app/oscal/service/ssp_control_implementation_service.py,1,058 行,已超標——本卡只改既有函式內幾行,不新增函式;挑段落的小工具放新檔):

  1. 新檔 app/oscal/service/control_part_prose.py:兩個純函式 statement_prose(parts)、guidance_prose(parts),挑法照 framework_version_edit_service.py:266-276(取第一筆 name 相符段落的 prose,沒有回 None)。不把 framework_version_edit_service 那兩個私有方法改成共用——它們是 instance method,改動會牽連框架編輯頁;兩處各自五行,留在 domain-capabilities.md 登記「段落挑法 canonical 在 control_part_prose.py」。
  2. _resolve_inscope_catalog(:224-228):迴圈內 get_by_control 的結果先存變數,AO 照舊進 parts_by_cc,另建 prose_by_cc[cc.id] = (statement, guidance)。回傳從三元組改四元組 (all_groups, controls, parts_by_cc, prose_by_cc)。
  3. 兩個呼叫端一起改:build_control_tree_by_ssp_id(:474-477)與 build_classifier_catalog_by_ssp_id(:261-272)。後者刪掉自己重打的那段,改從 prose_by_cc 取 statement;stmt.prose or stmt.title or "" 的兜底保留(statement_prose 回 None 時用空字串)。分類器輸出契約不變(docstring 明訂只可加欄)。
  4. _build_control 回傳(:806-817)加 "statement": stmt or None, "guidance": guid or None。
  5. 不增 SQL:段落原本就逐條全撈。CMMC L2 110 條合計約 50KB、CRA 31 條約 25KB(探脈量測),直接併進樹不做延遲載入。逐條查詢(N+1)是既有現象,不在本卡處理。

前端(ProjectPlanningView.vue 2,638 行,已超標——只准加引用,邏輯進新元件):

  1. :647 控制項組樹多帶 statement: c.statement ?? ''、guidance: c.guidance ?? '';description: '' 保留(其他程式可能讀它,待驗後再收)。
  2. 新元件 src/components/grc/planning/ControlReferenceText.vue:props statement、guidance;兩塊各自 v-if 有值才渲染,標題用 i18n lang.project_planning.control_statement/control_guidance(zh_Hant_TW/en 兩檔都補);內文 white-space: pre-wrap,樣式照同頁 :2226 的 text-sm+var(--text-secondary)。
  3. :2201 下方一行 <ControlReferenceText :statement="selectedControl.statement" :guidance="selectedControl.guidance" />。
  4. 無 AO 的假節點(後端 :792-804)不另外塞建議證據:控制項面板已經顯示兩塊,點假 AO 節點時面板仍是同一個控制項,重複顯示無意義。這點與討論稿「無 AO 退回分支顯示控制項建議證據」不同,理由是面板層級已涵蓋。

影響面:control-tree 另有 6 頁在用(RoundAuditReview、RoundApAuthoring、ProjectAuditorOverview、EvidenceClassificationReview、AutoClassifyBatch 等),只加欄位、不動既有欄位,不會壞。CMMC 控制項同樣會顯示兩塊(CMMC 條文與建議證據本來就有資料)——這是刻意的:顧問在 CMMC 也需要看到條文;AO 面板不受影響。

5.2 B1:收證據範本系統化+空值退回系統預設

migration scripts/sql/2026-10-xx-fr135-collect-evidence-template-system.sql,manifest 登記 phase=seed、envs=*:

  1. 冪等 UPSERT 這一份範本:INSERT ... ON CONFLICT (uid) DO UPDATE SET scope='SYSTEM', tenant_id=1, updated_at=now(), updated_user='system'。BPMN 內容照 scripts/sql/2026-06-15-fr038-q1-seed-collect-evidence-template.sql 原文(start → userTask collect_evidence → end)。不寫顯式 id(既有客戶庫的 id 已被佔,先例同 fr093-4d)。
  2. 翻譯列:workflow_templates_trans 依 workflow_template_id 補一列(language_code 與 xml),冪等用 NOT EXISTS。DEV 現況這份範本沒有翻譯列(2026-10-11 查),_clone_for 走 create_workflow_template 會自己建副本的翻譯列,本份是否必須有翻譯列待驗——runner 先在 DEV 實測 clone 一次確認讀得到 xml,讀得到就不補。
  3. 檔頭寫清楚:scope 必須本檔自己給(停在預設 TENANT 時,非 1 號租戶讀不到且 RLS 靜默無錯);只套 DEV。
  4. 收尾 INSERT public.schema_migrations。

程式:

  1. prep_job_generation_service.py 加模組常數 SYSTEM_COLLECT_EVIDENCE_TEMPLATE_UID = "f4038001-0000-4000-8000-000000000001"(與 _DEFAULT_PREP_TASK_ID 並列,註解一行:「對外契約,migration 與此同值,不可改」)。
  2. generate() 開頭:fallback_template_uid = fallback_template_uid or SYSTEM_COLLECT_EVIDENCE_TEMPLATE_UID——只在首輪模式(reclone_from_props=False)。覆核輪的來源由 B3 處理,B1 不改覆核輪行為。
  3. docstring 的 fallback_template_uid 說明改一行:「空值退回系統收證據範本」。

DEV 套用:只套 DEV(環境異動鐵律)。套完確認:SELECT scope, tenant_id FROM compliance.workflow_templates WHERE uid='f4038001-…0001' → SYSTEM|1。

出貨基線:本支是 phase=seed,不進 99-stamp 蓋章,不需重產 02-schema/04-seed;新裝由 init_apply_package_content 套上。runner 仍照 sql-migration skill 回報「已確認不需重產基線」一句。

5.3 B2:控制項層每條各 clone+可觀測+規劃頁帶得到流程

  1. clone 粒度(prep_job_generation_service.py:142-175):_clone_for(src_uid, shared=True) 加參數;呼叫處 cloned = _clone_for(src_uid, shared=part is not None)。shared=False 時不讀不寫快取、每次新建副本。來源範本只讀一次:get_workflow_template_by_uid 的結果另用 src_cache 存,避免 31 條各查一次。
  2. 為什麼要拆:同來源共用一份副本時,inject_job_execution_uid(:204-210)每條都改寫同一份 xml,最後只留最後一條的工作 uid;流程編輯器開其他 30 條會把它們當「新任務」重複建工作(:139-141 註解已點名這個不變量)。
  3. 略過可觀測(:170-175):兩處 skipped += 1 前加 logger.warning("generate_prep_jobs skip control=%s ao=%s reason=%s src=%s", cid, ao_part_id, "no-template-source"|"template-not-readable", src_uid)。
  4. 規劃頁帶得到流程(ssp_control_implementation_service.py:792-804):有輪次時 key=(cid,cid),補 "workflow_template_uid": wf_uid_by_key.get(key)、"workflow_execution_uid": we_uid_by_key.get(key);無輪次時兩欄給 None(P6)。FE 不用改,:647 附近 AO 組裝已讀這兩欄、:1406-1410 有值就能開流程編輯器。

5.4 B3:覆核輪控制項層從上一輪取範本

現況:覆核輪呼叫 generate(..., reclone_from_props=True, fallback_template_uid=None)(套件 audit_round_app_service.py:774-779)。有 AO 時來源是 AO props 現值(首輪回填過的專案副本),控制項層沒有 props 可讀,於是一定略過。

設計:

  1. 套件(jedi-compliance-audit):generate() 多傳 parent_round_id=round_entity.parent_round_id。只改這一處呼叫;round_entity 已在手上(launch_reverify 建輪時寫入 parent_round_id=parent.id,:655)。
  2. 主專案產生器:generate() 加 parent_round_id: Optional[int] = None 參數。覆核輪模式且 part is None 時,來源改為「上一輪同一 (cid, cid) 對應紀錄的流程執行所用的範本 uid」,查不到才略過(照 B2 記 warning)。
  3. 查詢放哪:新方法 get_round_control_template_uids(round_id) -> {control_id: template_uid},加在 infra/flow_control/repository/reverify_clone_query.py(176 行,與承襲配對同主題)。一次查整輪,SQL:workflow_execution_control_mapping 以 round_id 與 ao_part_id = control_id 過濾 → workflow_executions.template_id → workflow_templates.uid。flow_control_job_repo_impl.py 1,215 行已超標,不加在那裡。產生器透過 DI 注入(di_containers/flow_control/flow_control_containers.py:289 已有 reverify_clone_query provider 可接)。
  4. 承襲不用改:新輪工作建好後,inherit_from_parent 以 (control_id, ao_part_id) IS NOT DISTINCT FROM 配對(reverify_clone_query.py:48),控制項層兩輪都是 (cid,cid),指派人、部門設備、證據照常承襲。
  5. 發版:jedi-compliance-audit 進一版,走套件規範(dev 期 poetry path dependency、發版與推 Nexus 等決策者明示)。舊版套件不傳 parent_round_id 時,產生器參數預設 None,行為與現在相同(控制項層略過)——主專案可以先上、套件後上。

6. 守衛與測試

依本專案測試政策(碎片卡預設不寫新 unit test),本期四卡的測試要求如下:

卡 要跑的既有測試 新寫測試 理由
A1 test/test_ssp_control_tree_round_scope.py、test/test_control_tree_file_evidences.py;守衛 test/test_module_boundaries.py 不寫 純加欄位,既有樹測試確認形狀沒壞即可
B1 test/test_project_start_creates_first_round.py;守衛 test/test_module_boundaries.py 不寫;migration 以 DEV 實套+重跑冪等驗 屬 scripts/sql/ migration,當次要跑 ddd-compliance-reviewer(CLAUDE.md 例外條款)
B2 同 B1 寫一支 test/test_prep_job_control_level_clone.py:mock 範本服務,給兩條無 AO 控制項,斷言 create_workflow_template 被呼叫 2 次、兩筆工作的 template_id 不同;再給兩個 AO 共用同一來源,斷言只 clone 1 次(CMMC 行為不變)。做突變測試:把 shared=part is not None 改成 True,第一個斷言要紅 改的是建專案核心共用邏輯,錯了會靜默讓 31 筆工作互相覆蓋
B3 test/test_audit_round_app_service.py(主專案)+套件既有覆核輪測試 不寫;以 DEV 實跑覆核輪驗 套件改一行呼叫;主專案查詢走 DEV 實測

全卡共同:commit 前跑 pytest test/test_module_boundaries.py test/security/code/(幾秒)。


7. 拆分

每節可直接轉成 Notion 卡內文。四卡共同鐵則:不切 branch、不 push;只動 DEV;顯式 git add 自己的檔;超過 800 行的檔只准抽出、不准往裡塞;檔內註解只寫為什麼與陷阱,經過寫進 commit message;做完立刻 commit+Notion 子卡回寫+狀態改「修正待驗證」。

A1 規劃頁顯示條文與建議證據(BE+FE 同卡)

  • 做什麼:控制項樹多帶 statement/guidance,規劃頁控制項面板顯示「條文」「建議證據」兩塊;分類器順手刪掉重複查詢。設計見 §5.1。
  • 改哪些檔:BE 新檔 app/oscal/service/control_part_prose.py;app/oscal/service/ssp_control_implementation_service.py:224-228(解析)、:261-272(分類器刪重複查詢)、:474-477(接四元組)、:806-817(回傳加兩欄);docs/claude/domain-capabilities.md 補一列。FE 新檔 src/components/grc/planning/ControlReferenceText.vue;src/views/project/ProjectPlanningView.vue:647(組樹)、:2201(引用一行);i18n 兩語系各兩個 key。
  • 驗收(可親手做):① 開 CRA 第〇步專案規劃頁,點 I.1 → 面板出現「條文」「建議證據」,建議證據的條列分行顯示 ② 開任一 CMMC 專案規劃頁點 AC.L1-3.1.1 → 兩塊出現且 AO 列表與改前一樣 ③ 稽核執行頁、證據分類審閱頁開得起來、控制項樹正常 ④ 證據自動分類跑一批,build_classifier_catalog_by_ssp_id 輸出的 statement 與改前一致(拿同一 SSP 比對改前後 JSON)。
  • 依賴:無。可與 B1 平行。
  • 規模:S。
  • 要帶的鐵則:體積上限(兩支都超標,新邏輯一律新檔);新增 method 前先查既有能力(挑段落沿用既有挑法,不另發明);分類器輸出契約只可加欄。

B1 收證據範本系統化+空值退回系統預設

  • 做什麼:一支 phase=seed migration 把「收證據」範本改 SYSTEM、新裝帶上;產生器首輪模式讀到空值退回這份。設計見 §5.2。
  • 改哪些檔:新檔 scripts/sql/2026-10-xx-fr135-collect-evidence-template-system.sql;scripts/sql/manifest.tsv 加一列(seed/*);app/flow_control/service/prep_job_generation_service.py 常數+generate() 開頭一行+docstring 一行。不動 scripts/init/04-seed-core.sql 與 gen_seed_sql.sh。
  • 驗收(可親手做):① DEV 套 migration 兩次,第二次無錯無變化 ② SELECT scope, tenant_id FROM compliance.workflow_templates WHERE uid='f4038001-0000-4000-8000-000000000001' 回 SYSTEM|1 ③ 以非 102 租戶帳號用 CRA 公版建專案,log/app.log 的 generate_prep_jobs 彙總行 skipped=0(B2 未做前 jobs 可能是 31 但共用副本,屬預期) ④ 用 CMMC 資源庫建專案,工作數與改前相同。
  • 依賴:無。
  • 規模:S~M。
  • 要帶的鐵則:SQL Migration 鐵則(日期註解、檔頭 -- Date:、收尾登 schema_migrations、--single-transaction -v ON_ERROR_STOP=1、用 cmmgr);環境異動鐵律(只套 DEV);改 scripts/sql/ 當次跑 ddd-compliance-reviewer;回報「本支 phase=seed,不需重產出貨基線」。

B2 控制項層每條各 clone+略過可觀測+規劃頁帶得到流程

  • 做什麼:沒有 AO 的控制項每條 clone 自己的流程副本;略過記 warning;規劃頁控制項層那筆補範本與流程 uid。設計見 §5.3。
  • 改哪些檔:app/flow_control/service/prep_job_generation_service.py:142-175;app/oscal/service/ssp_control_implementation_service.py:792-804;新測試 test/test_prep_job_control_level_clone.py。
  • 驗收(可親手做):① 以非 102 租戶用 CRA 公版新建專案 → 準備期 31 筆工作 ② SELECT count(DISTINCT we.template_id) FROM public.workflow_execution_control_mapping m JOIN compliance.workflow_executions we ON we.id=m.workflow_execution_id WHERE m.round_id=<新專案首輪 id> 回 31 ③ 規劃頁任開兩條的流程編輯器,第一條加一個任務存檔,第二條任務數不變 ④ 每條控制項都能開流程編輯器、不再提示「沒有流程」 ⑤ 把資源庫範本暫時指到一個不存在的 uid 再建專案,log/app.log 出現 31 行 generate_prep_jobs skip ... reason=template-not-readable(驗完改回) ⑥ CMMC 專案建立結果與改前一致。
  • 依賴:B1。
  • 規模:M。
  • 要帶的鐵則:體積上限(ssp_control_implementation_service.py 只改既有分支內兩行);新測試要做突變驗證;DB 寫入只在 DEV。

B3 覆核輪控制項層從上一輪取範本(含 jedi-compliance-audit 發版)

  • 做什麼:套件傳 parent_round_id;主專案產生器覆核輪模式下控制項層從上一輪對應紀錄取範本。設計見 §5.4。
  • 改哪些檔:套件 jedi-compliance-audit/jedi_compliance_audit/app/service/audit_round_app_service.py:774-779;主專案 infra/flow_control/repository/reverify_clone_query.py(新方法)、app/flow_control/service/prep_job_generation_service.py(參數與分支)、di_containers/flow_control/flow_control_containers.py(注入)。
  • 驗收(可親手做):① CRA 新專案首輪對一條(例 I.1)指派人員、判「不符合」,走到發起覆核 ② 覆核輪規劃頁 I.1 底下有一筆工作,指派人與首輪相同 ③ 該筆工作的流程編輯器開得起來,改它不影響首輪那份 ④ CMMC 專案跑一次覆核輪,工作數與改前相同。
  • 依賴:B2。
  • 規模:S(套件一行)+S(主專案查詢)。
  • 要帶的鐵則:jedi-* 套件異動規範(開工前在卡上列「動哪支套件、範圍、其他 consumer 風險」等決策者點頭;dev 期走 poetry path dependency,path 改動不 commit;發版與推 Nexus 等決策者明示);主專案向下相容(舊套件不傳參數時行為不變)。
%%{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
  A1["A1 規劃頁條文"]
  B1["B1 範本系統化"] --> B2["B2 每條各 clone"] --> B3["B3 覆核輪"]
  A1 --> V["P2-A 驗收"]
  B3 --> V
圖 2 — 四卡依賴

8. 端到端驗收

四卡全部「修正待驗證」後,決策者在 DEV 一次跑完(先全部跑完、收集失敗,再一起修):

  1. 新租戶建 CRA 專案:以非 102 的租戶登入,用 CRA 公版(SYSTEM 451,或 D5 重建後的新公版)建專案 → 準備期 31 筆工作;log/app.log 該次 generate_prep_jobs 行 jobs=31 skipped=0。
  2. 規劃頁:點任一條看得到「條文」「建議證據」兩塊、段落換行正常;每條都能開流程編輯器。
  3. 流程互不干擾:任選兩條,第一條流程加一個任務存檔,第二條不變;重整後第一條仍是新流程。
  4. 覆核輪:首輪判一條不符合後發起覆核 → 覆核輪那條有工作、承襲首輪指派人。
  5. CMMC 不變:用 CMMC 資源庫建專案,工作數、規劃頁 AO 面板、覆核輪工作數與改前一致(改前數字由 A1/B1 runner 在動工前先記下,寫在 Notion 卡上)。
  6. 新裝:等下一次封安裝包時,新裝機查 compliance.workflow_templates 有且僅有這一份 scope=SYSTEM 的「收證據」(本項跟著下次出包驗,不為此單獨出包——驗收打折處見下)。

驗收打折處

第 6 項「新裝帶上」在 P2-A 期間只能用 DEV 套 migration 模擬,真正的新裝路要等下一次 build_bundle.sh 出包時在乾淨機驗。回報時須註明「新裝路未實測」。


9. 風險與陷阱

🔴 只補範本來源、不改 clone 粒度:從「0 筆工作」變成「31 筆工作互相覆蓋」

_clone_for 快取讓控制項層全部工作共用一份流程副本,inject_job_execution_uid 只留最後一筆,流程編輯器存檔會把其他 30 條當新任務重複建工作(prep_job_generation_service.py:139-141,194-210)。B1 上線、B2 未上的空窗,不可對外宣稱 CRA 準備期可用。

🔴 範本跨租戶讀不到也是靜默略過

DEV 那份是租戶 102 的 TENANT 範本,102 自己測永遠看不出問題。B1/B2 驗收必須換一個非 102 的租戶建專案。

🔴 scope 必須由 migration 自己給

表預設 scope='TENANT'。若 migration 只 INSERT ... ON CONFLICT DO NOTHING,DEV 既有那筆維持 TENANT 不會被改,新裝會插成 SYSTEM——兩邊不一致而且都不報錯。一律 DO UPDATE SET scope='SYSTEM'。

🔴 _resolve_inscope_catalog 有兩個使用者

控制項樹與分類器共用,回傳改四元組時兩個呼叫端都要改;分類器輸出會整份存成 run 的 catalog_snapshot 給報表讀,只可加欄、不可改名或刪欄(ssp_control_implementation_service.py:248-251)。

CMMC 規劃頁也會多出兩塊

A1 是平台層改動,CMMC 控制項的條文與建議證據本來就有資料,所以 CMMC 規劃頁同樣會顯示。這是刻意的;若決策者不要,改成「控制項沒有 AO 才顯示」只需改元件一個條件。

系統範本被某租戶改壞

SYSTEM 範本的寫入政策(workflow_templates_update)只放行超級管理員或範本所屬租戶(tenant 1),一般租戶改不到;但 tenant 1 的管理員可以從流程編輯器改它。clone 是在建專案當下複製,改壞只影響之後建的專案。本期不另加保護,列入 §10 後續。


10. 與其他需求的關係

需求 關係
**design-cra.md(P2-B~D) 互不碰檔。CRA 層 C1(7 條拆 AO)只解 7 條,另 24 條仍靠本期 B 系列開工;C4 重建 CRA 公版要等 B1~B3 全部完成**,否則重建出的公版驗不出準備期
D5 重建 CRA 公版 本期驗收可先用現有 SYSTEM 451;重建後再跑一次 §8 第 1~3 項
FR-038 Q1 「收證據」範本與 template_uid 退路是 FR-038 Q1 建的;本期把它從 DEV 專屬資料升級成出貨內容
FR-053 輪次隔離 覆核輪以 round_id 為接線依據、不回填 AO props 的設計不變;B3 只補「控制項層從上一輪取範本」這一塊
D11 刪舊 clone_module_frame 屬 design-cra.md 範圍。本期不寫 template_uid,該端點行為不受影響
後續(不在 FR-135) 稽核執行頁 RoundAuditReviewView.vue:337-338 接條文與建議證據(P9);系統範本的寫入保護;規劃頁 description 佔位欄確認無人讀後移除

沿革見 handoff/FR-135-LOG.md 與 git log。