---
title: "CRA 第二期平台層 — 設計文件 (FR-135)"
brand: "Guidant AI · **FR-135** CRA 合規框架"
eyebrow: "FR-135 · 第二期設計文件（平台層 P2-A）· 2026-10-11"
h1: "沒有評估目標的框架，也看得到條文、開得出工作"
subtitle: "規劃頁控制項多帶條文與建議證據；收證據範本改系統層級，沒有 AO 的控制項每條各開一筆準備期工作，覆核輪照樣開"
lede: "第〇步露出兩個**任何沒有評估目標（AO）的框架都會踩到**的底層缺口：規劃頁只看得到控制項標題，看不到條文與建議證據；建專案時收證據的流程範本來源全空，準備期一個工作都開不出來，而且沒有任何錯誤訊息。本文件把決策者已裁的 D4、D8、D12 落成可直接施工的設計：控制項樹多帶兩欄、「收證據」範本改系統層級隨安裝帶上、沒有 AO 的控制項每條各開一份流程、覆核輪從上一輪取範本。拆成 A1／B1／B2／B3 四卡，CMMC 這類有 AO 的框架行為不變。"
chips: [
  {text: "D4／D8／D12 已裁＋實作層決定 P1～P9", kind: ok},
  {text: "P2-A 四卡：A1／B1／B2／B3", kind: accent},
  {text: "跨 3 repo：BE／FE／jedi-compliance-audit", kind: accent},
  {text: "陷阱：只補範本不改 clone 粒度＝31 筆工作互相覆蓋", kind: crit},
  {text: "陷阱：驗收必須用非 102 租戶", kind: crit}
]
footer: "FR-135 · CRA 第二期平台層設計文件 · 2026-10-11 · 討論稿：[phase2-discussion.md](phase2-discussion.md) · CRA 層另見 [design-cra.md](design-cra.md) · 沿革見 LOG 與 git log"
---

> 狀態：**設計定案**｜日期：2026-10-11｜範圍：第二期 P2-A（平台層 A／B，四卡 A1／B1／B2／B3）｜CRA 層 P2-B～D 見 [`design-cra.md`](design-cra.md)
> 討論稿（現況盤點、方案比較、D1～D14 原文）：[`phase2-discussion.md`](phase2-discussion.md)

> 🔴 **這份是設計決策，不是現況。** 內文行號停在 2026-10-11（BE `89aadc59b`、FE `9d50d2d1`），實作改動後不回頭改寫；上線後的現況看 SPEC 頁。

## 變更紀錄 {#changelog nav="變更紀錄"}

| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-11 | 初版設計定案（D4、D8、D12 落地方式＋實作層決定 P1～P9），拆 A1／B1／B2／B3 四卡 | FR-135 母卡 CM-2675 |

---

## 1. 需求背景與端到端流程 {#background nav="背景"}

第〇步拿 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 的框架，所有行為維持不變。

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

```{.mermaid cap="圖 1 — 修好後的判斷順序（藍底＝本期新增或改動）"}
%%{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
```

覆核輪（`launch_reverify`）走同一支產生器，差別只在範本來源：有 AO 讀 AO props 現值；控制項層改為**從上一輪同一控制項的對應紀錄取上一輪的專案副本**再 clone（本期新）。

---

## 2. 分工概述（30 秒版） {#overview nav="概述"}

整案一句話：**任何沒有 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. 決策定案 {#decisions nav="決策定案"}

### 決策者已裁（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. 現況接入點盤點 {#touchpoints nav="接入點"}

座標以 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. 詳細設計 {#design nav="設計"}

### 5.1 A1：控制項樹帶條文與建議證據 {#design-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：收證據範本系統化＋空值退回系統預設 {#design-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＋可觀測＋規劃頁帶得到流程 {#design-b2}

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：覆核輪控制項層從上一輪取範本 {#design-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. 守衛與測試 {#tests nav="測試"}

依本專案測試政策（碎片卡預設不寫新 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. 拆分 {#breakdown nav="拆分"}

每節可直接轉成 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 等決策者明示）；主專案向下相容（舊套件不傳參數時行為不變）。

```{.mermaid cap="圖 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
  A1["A1 規劃頁條文"]
  B1["B1 範本系統化"] --> B2["B2 每條各 clone"] --> B3["B3 覆核輪"]
  A1 --> V["P2-A 驗收"]
  B3 --> V
```

## 8. 端到端驗收 {#acceptance nav="驗收"}

四卡全部「修正待驗證」後，決策者在 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` 的「收證據」（本項跟著下次出包驗，不為此單獨出包——驗收打折處見下）。

::: {.callout .warn}
**驗收打折處**

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

---

## 9. 風險與陷阱 {#risks nav="風險"}

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

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

::: {.callout .crit}
**🔴 範本跨租戶讀不到也是靜默略過**

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

::: {.callout .crit}
**🔴 scope 必須由 migration 自己給**

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

::: {.callout .crit}
**🔴 `_resolve_inscope_catalog` 有兩個使用者**

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

::: {.callout .warn}
**CMMC 規劃頁也會多出兩塊**

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

::: {.callout .warn}
**系統範本被某租戶改壞**

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

---

## 10. 與其他需求的關係 {#relations nav="關係"}

| 需求 | 關係 |
|---|---|
| **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`](handoff/FR-135-LOG.md) 與 git log。
