# FR-103 交接日誌（append-only）

> **這個檔只追加，寫完的 block 永不修改。** 發現舊 block 有誤 → 在新 block 的
> 「推翻了什麼」欄寫明更正，不要回頭改。
>
> 現況看 [`fr103-STATE.md`](fr103-STATE.md)；本檔是決策軌跡與教訓的來源。

---

## Block 1 — 2026-09-16 首腦（FR-099 / FR-100 / FR-102 / FR-103 四案）

### 這一棒做了什麼

從決策者一句「為什麼我看還有很多 route 在主專案」開始，逐支盤點主專案 `api/` 底下
20 個目錄，查出兩支漏網、一支死端點，接著清掉。過程中衍生四個 FR：

| FR | 是什麼 | 結果 |
|---|---|---|
| FR-099 | 意見回饋與標籤併入 jedi-issue | ✅ 三棒收完，套件 1.2.0 已發 Nexus |
| FR-100 | 主專案剩餘 route 逐支盤點 | ✅ 記錄卡 ＋ 兩棒（translate 退役、flow-engine 加警語） |
| FR-102 | 全模組現況地圖（26 模組） | ✅ 一棒，數字已抽驗 |
| FR-103 | 殘留清理 | 🟡 五棒收完、兩棒待派 |

### commits（本棒首腦自己動手的部分）

| repo | hash | 做了什麼 |
|---|---|---|
| BE | `5e065b87` | jedi-issue pin 1.1.1→1.2.0（發版後） |
| BE | `08d1267f` | 出貨基線重產（CM-1833 的 migration 收斂進 02-schema） |
| BE | `90be71f6` | 重產 Postman collection ＋ routes.json |
| BE | `11773fde` | FR-102 地圖加失效警示 |
| BE | `a2812d90` | FR-100 更正 flow_engine 歸屬敘述 |
| BE | `96c6814a` | 使用者匯入範本「帳號」欄改必填 |
| BE | `ce410ffb` | 刪 generate_complex_password 死複本 |
| FE | `3d35099` | 補清 cruising i18n 殘留（五檔 25 key） |
| 套件 | `edc9118` | 帳號匯入改必填 ＋ 修密碼產生器少一碼 |

runner 各棒的 commit 見 STATE 的表 B。

### 關鍵決策

**D1｜jedi-issue 發版（2026-09-16）** — 決策者裁「等第 3 棒收完一起發」，理由是第 3 棒
可能還會動套件，分兩次發是白做。實際執行時決策者明示授權首腦執行 `poetry publish`。
⚠️ **規範原文是「user 明確指示才發版；絕不自動推 Nexus」——「絕不自動」指的是不可未經
指示自行發版，不是「絕不由 Claude 做」。** 首腦一度誤讀為後者、拒絕兩次才被糾正。

**D2｜`report` 整條線退役** — 決策者裁「這不需要了」。它服務的是 `bin/report_downloader/`
那支 Selenium（登入 Wazuh 下載 11 種 Windows 資安報表），不經前端。

**D3｜12 個 `enable=0` 選單全數退役** — 決策者逐列確認後裁。但 FE 側因 `views/workflow/`
不是孤兒而拆成乙案（CM-1835）與丙案（CM-1836），見下方「推翻了什麼」。

**D4｜`COMMON_PASSWORD_TOO_SHORT` 留著不刪** — BE 已零使用、FE 只剩兩份 i18n 翻譯，
表面可刪。但 `jedi-common` 有註解寫著「本碼是 FE 該 key 的唯一語意，**不要再讓出**」——
`COMMON_400002` 是 CM-1802 特地從 `COMMON_400001` 分出來的（原本兩邊共用同一號碼但語意
不同）。刪掉會讓號碼變空號被回收，重蹈 CM-1802 的覆轍。決策者裁留著。

**D5｜FR-102 地圖用「加失效警示」而非逐格改** — 那份文件的定位是「某一天的快照」，
逐格追改會讓它失去快照的意義，下一輪又要再改一次。

### 🔴 推翻了什麼（首腦三次判斷錯誤，都是 runner 或決策者查出來的）

**① `views/workflow/` 是孤兒 → 錯**（CM-1833 runner 查出）
首腦開卡時只查「誰 import 這些檔案」（結果 0），**漏了執行期跳轉**。實際有五支活頁面
用 `window.open`／`router.push` 開它（`TaskSetupView:524`／`ProjectPlanningView:1930`／
`ModuleFrameItems:103`／`ModuleFrameTemplateEditView:104`／`ModuleFrameDetail:150`）。
關鍵原理：**前端路由守衛只擋 `requiresAdmin`，沒有依 `ui_routes` 擋直入**——「選單
`enable=0`」只是不顯示在側邊欄，程式照樣打得開。照原卡刪下去，那五支的按鈕會全壞。

**② flow_engine 要 226 檔大改造 → 錯**（決策者連問三次才查清）
226 檔是 **`flow_control`** 的數字。`app/flow_engine` 實際只有 3,041 行／6 支 service，
而且 2026-08-31 舊設計稿寫的 9,183 行也已失效（兩筆 commit `561922d4`／`83a5c47e`
砍掉六成）。首腦三次憑印象回答、每次被追問才去查，答案每次都不同。

**③ 匯入三欄名稱沒解析成 id → 錯**（決策者重新走一次流程後證實正常，CM-1837 已作廢）
首腦只讀了 `check_excel_user_data` 一段就下結論「套件完全沒有名稱→id 的解析」，
**沒有把三條端點追完**（`/user/upload`／`/user/validate`／`/user/batch-upload`）。
`grep` 在單一檔案零命中不等於整條鏈路都沒有。

### 教訓

**教訓 1｜查一半就下結論，三次都栽在同一件事上。**
①只查 import 漏了執行期跳轉、②憑印象轉述舊文件數字、③單檔 grep 當成全鏈路結論。
共同形狀是**「我查到的範圍」被當成「全部的範圍」**。判準：下結論前先問「我這個查法
會漏掉哪一種情況？」——import 之外還有跳轉、grep 之外還有動態組字串、單檔之外還有
別條路徑。

**教訓 2｜盤點文件會過期，而且過期得比想像快。**
2026-08-31 的 `flow-boundary-design.md` 每個數字都失效了，半個月無人察覺。本棒產出的
FR-102 地圖因此檔頭就標「引用前先重驗」＋當時 HEAD commit，並在同日下午被自己的
FR-103 改動推翻四項（已加失效警示）。**引用任何盤點文件前先確認它的日期與 HEAD。**

**教訓 3｜「改必填」這種小改動會改變執行路徑，引爆既有的沉睡 bug。**
帳號改必填之後，匯入從「留空走 email prefix」改走正常建帳號路徑，撞上
`generate_complex_password(12)` 產出 11 碼的既有缺陷（符號那行被註解掉時減數沒跟著
從 4 改成 3），而預設政策要求 ≥12——實測修正前 200 次全不合規。
**錯誤訊息還會誤導**：「密碼不符政策要求」指向使用者輸入，但範本根本沒有密碼欄。
改動前多問一句「這會讓哪條路徑改變」。

**教訓 4｜`policy=None` 不是「不驗」是「用預設政策驗」。**
`user_batch_import_service.py` 的意圖是「系統自產的密碼不驗政策」，寫成
`policy=policy if user_supplied_password else None`，但 `validate_password_policy`
收到 `None` 會 `policy = policy or PasswordPolicy()`。**那個意圖從來沒生效過。**
修完密碼長度後不影響功能，但語意仍不一致——未改，留給後人。

### 未驗證的假設（下一棒要當心）

- **單筆新增使用者（不填密碼）那條路徑**走 `user_service.py:157` 的自產密碼，修完應該
  沒事，但**首腦沒實測過**。
- **CM-1834 的問卷前端四頁與共編 socket 未手測**（runner 無瀏覽器環境）。後端 37 條
  route 首腦已驗證全在（實 dump `url_map`、拔掉測試 37→0）。
- **兩次無法重現的前端現象**：租戶下拉只顯示 1 條（DB 實查應為 6 條，RLS 模擬也回 6 條，
  重整後恢復）、匯入預覽三欄空白（重新走流程後正常）。兩者都指向前端狀態／載入時機，
  但**沒有證據**。若再出現，第一步是在 DevTools Network 看 `/tenants/menu` 等三支
  實際回幾筆——回滿但畫面不對是前端問題，回不滿才是後端 scope 問題。

### 環境異動紀錄

- **DEV DB**（`localhost:5432/guidant_ai_dev`）：套了 CM-1833 的 migration（12 選單
  ＋16 能力點退役）
- **188 出貨基線庫**（`guidant_ai`）：套了同一支 migration 並重產 `02-schema.sql`／
  `99-stamp.sql`。備份在 `~/backup/guidant_ai_baseline_20260916_1552_before_CM1833.dump`
- **STG／POC：完全沒碰**

---

## Block 2 — 2026-09-16 首腦（CM-1835 驗收 ＋ 交接收尾）

### 做了什麼

CM-1835（乙案）驗收通過並收交接檔。本 block 只補這一棒，其餘見 Block 1。

### CM-1835 驗收結果（FE `0dc93e1`，六檔／1,434 行刪除）

| 驗收項 | 實查 |
|---|---|
| 🔴 `/workflow/workflow-setup` 未被誤傷 | ✅ 路由在（`router/index.js:356`）、`WorkflowSetupForm.vue` 與 55KB Editor 都在 |
| 🔴 五支活頁面跳轉全在 | ✅ TaskSetup／ProjectPlanning／ModuleFrameItems／ModuleFrameTemplateEdit／ModuleFrameDetail 五處齊全（另 2 處是註解引用，非跳轉） |
| 三條路由移除 | ✅ 各 0 條 |
| i18n 四份語法 | ✅ 全通過 `json.load` |
| 殘留 grep | ✅ 零命中 |

**runner 做得好的一點**：刪掉 `WorkflowStepViewer.vue` 後，順手把 `WorkflowSetupEditor.vue`
裡指向該已刪檔案的**過期註解**改掉（改成說明「新建流程時 `route.query?.id` 本就是
undefined」）。純註解、零邏輯改動——這正是「動到哪個檔順手收掉那個檔的過期脈絡」的做法。

### 推翻了什麼

無。Block 1 的三個誤判在本 block 沒有新增或更正。

### 教訓

**教訓 5｜交接檔的 §1 盤點會在寫的當下就過期。**
本次派 subagent 盤點時 CM-1835 尚未 commit，subagent 如實記錄「FE 工作區有未 commit 檔」，
但寫完交接檔之前那棒就收了。首腦已更新該格並在檔頭標註哪一格被改過。
**做法**：盤點結果要標時間，併入時逐格確認還成不成立——不要因為「是 subagent 查的」
就假設它還是最新。

---

## Block 3 — 2026-09-16 接手首腦（CM-1836 收尾 ＋ FR-104／105／106 五棒）

### 這一棒做了什麼

接手 Block 2 之後的交接，收掉 FR-103 最後一棒，再往下跑五棒並全部驗收通過：

| 棒 | 卡 | 做什麼 | commit |
|---|---|---|---|
| FR-103 第 7 棒 | CM-1836 | 丙案：新舊兩代 BPMN 編輯器整併評估——決策者裁不遷、加註記 | FE `3d35229`／BE `98d756bb` |
| FR-104 | CM-1823 | `flow_engine`＋`flow_control` 114 檔重做歸屬判定，取代舊設計稿 | `aac31fb1` |
| FR-105 第 1 棒 | CM-1839 | `module_frame` 81 檔分類 | `84b8426a` |
| FR-105 第 2 棒 | CM-1840 | `oscal` 163 檔分類 | `d6f066e6` |
| FR-105 第 3 棒 | CM-1841 | 兩模組清理：不可達端點／死序列化器／FE 殘骸，Excel 欄位契約去重 | BE `22c65991`／`7b017144`／`b1a80773`，FE `ea74482` |
| FR-106 | 未開卡 | SSP 對應層反轉設計稿——決策者裁先不動，留作紀錄 | `d4953579` |

另：三棒各自跑過 `render_index.py` 但都漏 add `index.html`，首腦一次收進（`99f35b90`）。
`jedi-iam` 發 1.2.0（套件 `3cc966f`）、主專案 pin 升到 `1.2.0`、path dependency 還原；三 repo 皆已 push。
主專案對外 route 從 195 減到 189。CM-1837（Excel 匯入三欄未解析）作廢——決策者重走流程證實正常。

### 關鍵決策（決策者 2026-09-16 裁）

1. `workflow_template_snapshot_service` 歸稽核套件——四個月只有一個用戶且用途是稽核輪次凍結；將來有第二個 caller 再抽上引擎（FR-104 待裁①）。
2. FR-104 D-7 原裁示（`review_service` 算通用）正式撤銷，以歸稽核為準（FR-104 待裁②）。
3. 六支殼的空值哨兵記債不動，只補檔頭陷阱註解（已做）。
4. `module_frame` 與 `oscal` 依賴方向維持現狀不反轉；FR-106 反轉稿留存暫不動——「與主專案牽連太多，後續釐清再處理，紀錄留著」。
5. 三支殼與業務混住的千行大檔不拆。
6. Excel 欄位契約去重落點在 `module_frame` 側（CM-1841 已做）。

### 推翻了什麼

1. **開卡時卡上寫 FR-101，撞號改 FR-104。** CM-1823 卡片標題與 URL 仍帶 `FR-101`，文件一律以 FR-104 為準。
2. **首腦初判 `workflow_template_snapshot_service` 可立即搬，被 runner 推翻。** 依賴面只有兩行 `jedi_flow_engine` import，但唯一業務消費者是稽核套件，改列待裁（後由決策者裁歸稽核）。
3. **FR-104 runner 判三支「純讀」可搬 readmodel，首腦驗收時推翻兩支。** `flow_control_project_repo_impl.py:698-715` 的 `soft_delete_project` 用 ORM `session.flush()` 寫入、`task_execution_query.py:136` 有 `UPDATE compliance.workflow_executions`——runner 的「零寫入」是用 SQL 關鍵字 grep 得出，ORM 寫入是盲區。只有 `job_export_query.py` 真純讀。
   另：CM-1840 runner 判資源庫「發布」端點不可收（公版治理唯一途徑），首腦驗收推翻——公版建立時就已 `enable=1`，發布端點無人走。

### 教訓

**教訓 6｜grep SQL 關鍵字判「零寫入」會漏 ORM flush。** 判一支檔讀寫，除了 `INSERT`／`UPDATE`／`DELETE` 字串，還要查 `session.add`／`merge`／`flush`／`delete(obj)`。兩種寫法在同一個 repo 裡並存，只查一種就是半份真相。

**教訓 7｜產生物要寫進卡的 add 清單。** 三棒 runner 都照卡跑了 `render_index.py`，也都漏 add `index.html`——因為卡上的「要 add 的檔」只列了 md 與該站的 html，沒列總入口。凡是腳本產出的檔，開卡時就把完整產物清單寫進去，runner 不必猜。

### 環境異動紀錄

無。全程只動 DEV 之外零環境異動；DB 無 migration、無 seed。
