FR-060 檢測基準內容管理 — LOG(append-only,每棒追加一 block)

這是 append-only 檔。既有 block 一律不改。 發現舊 block 有誤 → 在新 block 的「推翻了什麼」欄寫明更正。 現況請看 fr060-STATE.md(living)。


§1

第 1 棒 — 2026-08-03(討論 → 設計定案)

做了什麼

  • big-feature-workflow:Explore 探脈 → HTML 討論稿(1,003 行、18 項 D 項 callout、4 張 mermaid)→ 決策者逐項審 → design.md 定案
  • D1–D19 全數拍板;discussion.md / discussion.html / design.md(1030 行)三份文件產出

commits

  • 7124a473 討論稿補 D19 多工具擴充邊界(決策者當場定調)
  • 7e3e7b97 設計定案 design.md(D1-D19 全數拍板)+ FR 登記表

關鍵決策

  • D5 被決策者當場推翻:原建議「enum 值 FE 寫死」→ 改「存 DB 由 root 維護」(原話「這類最好都要可以維護」)。連帶多出三塊必做工作(顯示名不落庫/刪除保護/維護介面),FR-060.2 範圍因此比原討論稿估的肥
  • D19 是決策者當場定調不是照建議:原話「不能像這次一樣又大幅度更新,上線後很危險」→ 三層擴充結構,擴充點一律「加資料/加實作類別」不是「加欄位/改欄位語意」。連帶改寫 D12 欄位定義(嚴重度欄不叫 impact
  • D20 多語化被決策者砍出範圍:「我不想把需求拉複雜,名稱跟描述之後再補吧」→ 記在 design.md §6.3 免得半年後有人重新發現一次

教訓

  • 原本以為拆表只影響管理面,實 grep orchestration 後發現兩處會壞,其中破口①是無聲的授權漏洞(None is False 為假 → 停用的 profile 照樣派得出去)。「應該不受影響」必須 grep 驗證,不可推估
  • id 32/33 的三欄 JOIN 陷阱是實查 DEV 資料才發現的,而 STG 資料形態測不到它

推翻了什麼

  • 推翻自己原本「拆表對派工鏈零影響」的判斷(§4.3 兩處破口)

§2

第 2 棒 — 2026-08-03(Notion 建卡,未完成)

做了什麼

  • 讀 CM-1054 母卡 + design.md §3/§4/§5/§7/§8,複驗程式碼與 DB 座標
  • 建三張子需求卡:CM-1055(FR-060.1)/CM-1056(FR-060.2)/CM-1057(FR-060.3)
  • 回填 CM-1054 母卡的「子需求卡連結」段(含待辦與座標漂移表)
  • 寫本 arc 的 STATE / LOG 雙檔

commits

  • 無(本棒只動 Notion 與交接文件,零程式碼異動)

關鍵決策

  • 子需求卡的子任務拆法定為每階段四塊,寫進各卡「範圍」表格供下一棒直接建卡:
    • .1 → T-1.1 migration / T-1.2 資料層 / T-1.3 服務與 API 讀取端 / T-1.4 派工鏈破口修補
    • .2 → T-2.1 enum 表+seed+分類補登 / T-2.2 API(taxonomies + PUT 編輯)/ T-2.3 FE 管理頁重構 / T-2.4 FE 下拉分組+fallback 修補
    • .3 → T-3.1 主機依賴 / T-3.2 註冊表+InSpec extractor / T-3.3 非同步管線 / T-3.4 FE 詳細頁

教訓 / 事實更正(給下一棒)

  • 🔴 design.md §4.3 的「三處呼叫點」是錯的,實際只有兩處detection_orchestration_service.py:224_load_profile)與 :794_humanize_profile_param)。建 T-1.4 卡時寫兩處
  • 🔴 design.md 提到的三個 FE 檔案都沒寫路徑,且不在直覺位置:實際在 src/views/detection-profile/src/components/detection-tools/src/views/device/(詳見 STATE §6)
  • DEV 13 筆資料形態已實查列在 STATE §2,下一棒不必重查

未完成(交棒原因)

  • 本 session 反覆撞 Response stalled mid-stream(單次輸出過長導致串流中斷),12 張子任務卡與三份交接 prompt 未建
  • 下一棒接續 STATE §4 的待辦 #1~#3

推翻了什麼

  • 更正第 1 棒 design.md §4.3 的「三處」為「兩處」(見上)

§3

第 3 棒 — 2026-08-03(建子任務卡 + T-1.1 落地)

做了什麼

  • 7 張子任務卡 CM-1060~1066(乙案),回填四處「子任務卡連結」段,七張卡附加共通的「必讀+作業紀律+完成後四步」區塊,CM-1061 額外插入前置狀態段
  • 發出 CM-1060(T-1.1 拆表 migration)並由協調者實查驗收通過
  • 事後補 CM-1062 卡頭的「10 筆 soft-ref」事實更正,更新 STATE §2/§4/§6

commits

  • d7cf9abc T-1.1 拆表 migration(CM-1060)——只新增 scripts/sql/2026-08-03-fr060-1-detection-profile-split.sql(662 行),零 Python 異動

關鍵決策

  • 🔴 子任務卡改乙案 7 張,推翻第 2 棒規劃的 12 張:原 T 之間依賴過緊(T-1.2 與 T-1.3 必定同一 session 做完),卡切得比執行單位細只會讓回寫時出現「一張卡改一半」。改為卡與執行 session 一對一
  • T-3.1(裝 CINC Auditor)未單獨建卡:安裝屬環境動作、由決策者決定時機,程式碼與文件部分併入 CM-1065
  • 共通紀律寫進卡片本身(必讀清單/作業紀律/完成後四步),派工單因此縮成一行。決策者原話:「不要每次都用攏長 prompt,很浪費 token 也會斷掉」
  • DEV 階段不加相容 view:拆表後 BE 會有一段壞掉的過渡期,決策者裁示「開發階段就是開發,直接往下做」

T-1.1 驗收數字(DEV 實測)

  • 主檔 13/從檔 13/current_version_id IS NOT NULL = 12/uid 零遺失 = 0/控制項表 0 筆/extraction_statuspending
  • id 32→tool 8、id 33→tool 4,三欄 JOIN 正確無 cross join;舊表 rename 為 _deprecated_20260803 未 DROP
  • RLS 主從各 4 段共 8 段;cm_app 實測租戶 131 讀到 10 筆全 SYSTEM、對租戶 102 外洩 0 筆
  • STG/POC 均未被動(config.detection_profile% 新表數 0/0)

教訓

  • 🔴 驗收只查 DB 沒起 BE,漏掉「BE 目前壞著」這件事——__tablename__ 仍指已 rename 的舊表,是 T-1.1 runner 自己在回寫裡揭露的,不是驗收查出來的。拆表類任務的驗收要含「服務還起不起得來」,不能只驗資料正確

推翻了什麼

  • ① 推翻第 2 棒規劃的 12 張子任務卡拆法,改乙案 7 張(理由見上)
  • ② 更正 design.md §8.1「既有 10 筆 soft-ref 引用零轉換仍可解析」的描述:實際只有 6 筆指向 profile uidtool_params 4 筆 4/4 全對 + agent_tasks.params 6 筆裡只有 url 型 2 筆),另 4 筆 file 型的 _profile.uid 指向的是 upload_files 的 uid、本來就不指向 profile 表。已寫進 CM-1062 卡頭與 STATE §6

§4

第 4 棒 — 2026-08-03(七張卡執行 + 手測修正 + 收尾)

做了什麼

  • 依 STATE §4 序列逐張發卡執行 CM-1061~1066,六張卡全數落地(CM-1060 於第 3 棒完成)
  • 決策者手測驗收後補三支 FE 修正;本棒收尾產出 arc SUMMARY、更新 STATE、追加本 block

commitsBE/FE 皆未 push

  • BE 01d0909b CM-1061 T-1.2+1.3 資料層/服務層改寫(38 檔,+2925/−1589)
  • BE 40fcc38d CM-1062 T-1.4 派工鏈兩處破口 + 迴歸測試(2 檔,+137/−7)
  • BE a2f2512b CM-1063 T-2.1+2.2 分類 enum 表 + seed + 13 筆補登 + PUT 編輯 + 刪除保護(22 檔,+2028)
  • BE ec4e70b7 CM-1065 T-3.2+3.3 抽取器註冊表 + InSpec extractor + 非同步管線(6 檔,+1170/−6)
  • FE 28f061f 補 8 個 error code i18n/97f9ac8 CM-1064 管理頁重構(1,239→639,拆六支)/bbcb882 CM-1066 控制項詳細頁+摘要區+輪詢
  • FE 手測後三支:07b8a7d 分類欄拆三欄/e4e7967 中繼資料標籤中文化/7274d13 修多列展開只剩最後一筆有資料

Notion:CM-1060~1065 六張 → 修正待驗證;CM-1066 尚未回寫;母卡 CM-1054 仍 In progress(收口待另行下令)

關鍵決策

  • 抽取採非同步管線 + 狀態輪詢extraction_status 落在從檔)。被排除:同步解析——CINC Auditor 解一支 700 條的 profile 是秒級以上,卡在上傳 request 裡會讓使用者以為上傳失敗
  • 控制項讀取端路徑前綴用 -versions 不是 -profiles:控制項屬於某一版而非一支基準(同支 v1/v2 內容可以完全不同),uid 收的是從檔 uid
  • 手動重抽 POST .../extraction 對 create capability(與版更同一組,兩者都是「產生這一版的內容」);同路徑 GET(狀態輪詢)不設 capability,與其他讀取端一致

教訓

  • 🔴 驗收只查 DB 沒起 BE,會漏掉「服務起不起得來」——CM-1060 完成後 BE 其實是壞的(__tablename__ 仍指已 rename 的舊表),是 runner 自己在回寫裡揭露的,協調者的驗收沒抓到。拆表類任務的驗收必須含「服務還活著」這一項,不能只驗資料正確
  • 🔴 401 分不出「端點存在」與「端點不存在」——@jwt_required() 在路由解析前就擋下,兩種情況都回 401。用 401 驗端點是無效驗證。正解是 OPTIONS 探路由(存在回 200 + Allow 標頭)
  • 🔴 把共通紀律寫進卡片本身,派工單就能縮成一行——決策者原話「不要每次都用攏長 prompt,很浪費 token 也會斷掉」。七張卡各附「必讀清單 + 作業紀律 + 完成後四步」後,派工單只剩「請處理 CM-10xx,照卡做」,本棒六張卡全程無一次因 prompt 過長斷流(對比第 2 棒反覆撞 Response stalled mid-stream
  • PrimeVue 3 的展開列會整個銷毀重建BodyRow<component :is="templates['expansion']">,而 templates.expansion 是父層插槽函式本身,父層每次 re-render 都是新實例 → :is 視為新元件型別 → 已展開的列全部銷毀重建。:key 或改具名函式 ref 都擋不住,只能讓子元件自己管載入 + 模組層快取。會再遇到的框架坑

推翻了什麼

  • ① 再次確認第 3 棒已推翻的 12 張子任務卡拆法——乙案 7 張(卡與執行 session 一對一)實跑無「一張卡改一半」情形,拆法定案
  • 推翻協調者對多列展開 bug 的根因推測:原猜是 ref 時序問題(元件 ref 在展開前尚未就緒),實際是 PrimeVue 插槽函式 identity 每次 re-render 都變(見上)。按原推測方向修(調 ref 時序/加 nextTick)不會有效
  • 更正 STATE §2 的 DEV 數字:協調者交付的事實基準寫「控制項 942/已抽取 2 支」,收尾日實查為 控制項 1,641/succeeded 3/failed 1/pending 9——差異來自手測期間又觸發了一支抽取(TWGCB-Windows-2025,708 條),非資料異常。三環境未動的結論不變(STG/POC config.detection_profile% 新表數皆 0)

留給下一棒:全案實作完成,剩 push(BE 14/FE 6,全 branch 的事)與上版(三支 migration 只套 DEV,STG/POC 需決策者放行)。詳見 2026-08-03-fr060-arc-SUMMARY.md


§5

第 5 棒 — 2026-08-04(CM-1072 納入 + 收尾文件更正)

做了什麼

  • 更正 SUMMARY/STATE 兩份收尾文件:CM-1072 納入 commits 清單、新增 §2.4 url 型抽取(含三條不變式)、DEV 數字全面更新、Notion 卡片樹補列、新增「url 型明細會靜默偏離」follow-up
  • 同 arc 另有一支 agent 更新 SPEC(docs/specs/current/system-admin/detection-profile-manage.md)與使用手冊,內容以程式碼為準

commits

  • 本棒零程式碼異動(只改交接文件,由協調者統一 commit)
  • 納入清單的 25045758 feat(FR-060.3): url 型基準支援內容抽取(BE 代下載+SSRF 防護,CM-1072)——4 檔:新增 common/util/safe_http_fetch.py(347 行,SSRF 五道防護)、改 detection_profile_extraction_service.py(+120)/serializers/detection_profile.py(+4)/design.md(+28)。零 SQL migration,STG/POC 未動

關鍵決策

  • 🔴 CM-1072 由決策者裁定納入 FR-060:原本被劃在本 arc 之外(決策者說「那個不管他」),但實作後發現它改的就是 FR-060.3 的抽取服務、程式碼已在裡面、DEV 已生效。決策者原話:「那看起來 1072 必須納入 fr-60 對吧,算是延伸題目…既然要把功能做完整」
  • url 型代下載的三條不變式(改這塊之前必須先讀懂):①掃描路徑完全不動,agent 仍原樣把 URL 交給 cinc-auditor exec,抓的永遠是即時內容不是平台快照 ②下載內容不落儲存source_type 維持 urlfile_idsha256 維持 NULL)——存成 file 型快照會破壞雙軌語意,且掃描與抽取從此可能對不上、對不上時沒人知道該信哪個 ③下載必須在 session_scope() 之外,網路 IO 最多等 60 秒
  • 「url 型明細會靜默偏離」的方向定甲案:開明細頁時對來源 URL 發輕量請求比對 ETag/Last-Modified,一樣直接顯示、不一樣才背景重抽。⚠️ 可行性未驗證(GitHub 動態產生的 tarball 不一定給穩定 ETag)。被排除:乙案(開頁無條件重抽,被開頁頻率放大)、丙案(自動長 v2——url 型會出現同一 URL 有 v1/v2/v3,而掃描永遠拉最新 git 內容,任務綁的版本 uid 指不到掃描實際用的東西,讓「綁版本」既有契約自我矛盾)。已另開 Notion 卡追蹤

教訓

  • 🔴 派工前要查當前 HEAD 有沒有相關異動,不能只憑幾小時前的盤點——協調者把「url 型無法抽取」當既定事實寫進三份派工單,而該事實在派工當下已被 commit 25045758 推翻。SPEC agent 以程式碼為準沒照抄簡報,是對的判斷——這也印證了派工單該寫「去讀什麼」而不是「事實是什麼」
  • 「技術做不到」與「產品範圍不含」要分開講:url 型不抽取原本掛在 FR-059 P5「平台完全不經手檔案」名下,聽起來像技術限制,實測 CINC 原生就吃 URL(cinc-auditor json <tarball 網址> 解得出 59 條)。P5 真正涵蓋的是「掃描路徑不代管私有 repo 憑證」,而抽取是 FR-060 才有的另一個面向

推翻了什麼

  • ① 推翻第 4 棒 SUMMARY/STATE/LOG 寫的「url 型無法抽取、failed 是設計內終態不是故障」——CM-1072 之後已支援,錯誤訊息「平台不代為下載…請改以上傳壓縮檔的方式建立版本」不再出現
  • ② 更正第 4 棒的 DEV 數字:控制項 1,641 → 2,131succeeded:3 failed:1 pending:9succeeded:5 failed:0 pending:8(原本 failed 那支 dev-sec 基準已抽取成功)。主檔 13/版本 13/分類字典 13 不變,三環境未動的結論不變

§6

第 6 棒 — 2026-08-04(主線收尾 + 四張延伸卡)

做了什麼

  • 主線收尾:SPEC(435→839)/使用手冊(163→294)/SUMMARY 新建 316 行/STATE/LOG + SPEC 的 html build 產物,六份一次 commit
  • 新開四張延伸卡:CM-1075(url 型 ETag 比對,已派出實作中)/CM-1076(8 支基準批次抽取,已派出)/CM-1077(版本與基準的修正與刪除)/CM-1078(清單操作按鈕收進 menu)
  • 定 CM-1078 先於 CM-1077 的順序:兩張動到同一個檔(DetectionProfileManageView.vue 操作欄),先收乾淨結構再加新動作

commits

  • BE d064cb61 docs(FR-060): 全案收尾 — SPEC/使用手冊/SUMMARY/STATE/LOG(CM-1054~1066+CM-1072),6 檔 +3,435/-759。未 push
  • 實查未 push 數:BE 16 支、FE 7 支(FE 第 7 支是 9b741c8,CM-1072 的 FE 側)

關鍵決策

  • 🔴 CM-1077 的判準是客觀的「有沒有被使用過」,不是問使用者意圖。從未被使用過 → 可就地修正來源(版號不動、重抽控制項)/可刪除;已被使用過 → 一律凍結,只能建新版本。刪除及於主檔,但主檔判準更嚴——「從未產生過任何掃描結果」,因為任務可能已刪而報告與證據還在。 被排除的方案:協調者原設想「已被使用時跳確認框問『確定要更新嗎?會影響既有資料』讓使用者選」。決策者原話否決:「萬一有被掃過,下次掃同版本結果不一樣,也很奇怪」——uid 是凍結契約、sha256 是對帳基準,同一版本 uid 在不同時間指向不同內容,之後回頭看掃描報告沒人能確定當時掃了什麼。在 GRC 系統裡這是稽核可信度問題,不是 UX 取捨,所以不能交給使用者當次決定。 觸發此題的 DEV 實例:決策者測 SSRF 時產生的「SSRF」基準,v1 http://192.168.50.1/x.tar.gz(被 SSRF 防護擋下)、v2 https://...(為修正而版更),兩版皆 failed 且被任務引用 0 次——版本歷史留下「從來不能用」的孤兒版本,看歷史的人分不出是舊標準還是當初手殘。

教訓

  • 🔴 收尾 commit 前必須先看工作區有什麼不是自己做的。本棒本要統一 commit,git status 才發現 6 個程式碼檔(source_etagsource_last_modified 兩個新欄位 + safe_http_fetch.py +126 + 抽取服務 +206)是另一個 session 的 CM-1075 實作中異動,另有一支未追蹤 migration scripts/sql/2026-08-04-fr060-3-dpv-source-validators.sql 同屬該線。若用 git add -A-am 就會把別人做到一半的東西一起帶走。多 session 併發是本專案常態,收尾的 commit 一律顯式列檔名。

推翻了什麼

  • 更正第 5 棒 STATE 記的未 push 數與 CINC 安裝狀態:BE 15→16 支、FE 6→7 支;CINC Auditor 從「僅開發機已裝、三台部署機未確認」更正為四台皆已安裝(各機 CINC_AUDITOR_CMD 路徑解析實測仍未做)。

留給下一棒:主線只剩 push 與上版(皆需決策者放行);四張延伸卡中 CM-1075/1076 已派出、CM-1078 應先於 CM-1077。


§7

第 7 棒 — 2026-08-04(延伸卡收斂 + CM-1077 拆卡)

做了什麼

  • 接手盤點、驗收 CM-1075/1076、標 CM-1076 為 Done
  • 與決策者討論 CM-1077 的「被使用過」判準,定出軟刪穿透規則
  • 新開 CM-1079(專案軟刪除無法檢視/復原)作為 follow-up 記錄
  • 派三隻 subagent 平行寫 CM-1077 三張子卡的 md(未上 Notion,留給下一棒)

commits

  • BE c3b10b32 docs(FR-060): CM-1077 拆成三張子卡的派工內容(待上 Notion),3 檔 +580。未 push
  • 決策者本人已於本日 push 先前累積的 BE 14 支/FE 8 支

關鍵決策

🔴 CM-1077 的「被使用過」判定要穿透軟刪除的專案——引用只計入「活著的專案」(JOIN project_extensions WHERE deleted_at IS NULL)。

決策者提出的問題:「軟刪除專案 user 看不到,也沒辦法刪除,然後我們要刪除設定檔的時候卻被擋下,這樣 User 會困惑」。這是真的死結——實查確認 grc_project_repo_impl.py 11 處查詢全部帶 deleted_at IS NULL,沒有任何角色(含 admin)看得到已刪除專案,也沒有復原端點。DEV 209 個專案有 204 個已軟刪(97.6%),使用者實際只看得到 5 個。照字面判定,使用者會被一個他看不到、無從查證、也無法處理的專案擋住,且沒有任何自救途徑。

定案原則:擋下的理由必須是使用者看得見的。GRC 語意上也站得住——專案已刪除代表那份稽核工作已被放棄。

被排除的三個方向(決策者主動提出,一併評估後排除):

  • 加專案硬刪除——GRC 系統裡專案硬刪等於銷毀稽核軌跡(AP/AR/POA&M/證據/報告全連著),要動的 FK 與 cascade 範圍極大,風險遠超過它要解的問題
  • archive table 模式——搬 204 筆專案及其關聯樹是獨立 arc 的工程量,且搬完「引用指向的專案查不到」的問題依然存在,只是換個形式,沒解到根本
  • 保守擋下(JOIN 不到專案就擋)——安全但與決策者的原意相牴觸,專案刪掉後那支基準永遠鎖死

主檔層不做軟刪穿透(與版本層的差異):刪主檔會連帶刪掉所有版本與控制項,而任務可能已被刪但報告與證據還在,那時 profile 仍是稽核軌跡的一部分。母卡原話「從未產生過任何掃描結果」,門檻照這句走,寧嚴勿寬。

CM-1077 拆三張的拆點選在「判定」與「動作」之間:判定規則是難點與風險所在(三來源、軟刪穿透、兩種嚴格度),動作本身是常規 CRUD。判定寫錯是資料安全問題,動作寫錯只是 UI 問題,不該綁在同一次驗收裡。

教訓

  • 🔴 _ 在 SQL LIKE 是單字元萬用字元——協調者查 agent_tasks_profile 的筆數時用了 params::text LIKE '%_profile%',回 32 筆;改用 jsonb 存在運算子 params ? '_profile' 才是正確的 7 筆。差了四倍多,而且錯誤方向是高估,若拿它當判定基準會誤擋一堆本來可刪的版本。已寫進 .1 卡的必讀陷阱。
  • 🔴 同一個概念在系統裡有兩套不同步的真相時,要查清楚哪一套是活的——projects.status='archived' 看起來就是「已刪除」,但實查發現:①statusdeleted_at 不同步(8 筆已軟刪但 status 仍 in_progress)②archived 這個 status 現行流程根本沒有寫入端(grep 過 app/ domain/ api/,只有讀取端在比對),那 196 筆是歷史遺留。用直覺上最像的欄位會判錯,唯一可靠的是 project_extensions.deleted_at
  • 🔴 本體一次輸出太長會撞串流中斷——協調者原本要一次建三張 Notion 卡,連續撞 API Error(與第 2 棒的 Response stalled mid-stream 同類)。決策者裁示改成「先寫成 md、下個 session 再上 Notion」+「派 subagent 去跑」。三隻 subagent 平行寫、各約 200 行,全部一次成功。大量結構化輸出要外包,不要本體硬幹。
  • subagent 帶著充分的實查事實派出去,會自己再撈到更多——三隻各自查到一件派工單沒寫、但會直接影響實作正確性的事實:既有 count_referencing_tasks 的 ref-count 實作鏈可照抄(呼應「禁止重複造輪子」)、current_version_id 的 FK 是 ON DELETE SET NULL 故不可拿它當業務規則(會靜默清空指標而非報錯)、該頁已有兩種 disabled 原因機制故本卡的是第三種(四種並存要取聯集)。

推翻了什麼

  • 推翻第 6 棒定的「CM-1078 必須先於 CM-1077」——實查 DetectionProfileManageView.vue 確認 CM-1078 已落地buildActionMenuItems(row) 的 menu 結構已在),CM-1077.3 可直接加 menu item,兩張卡的順序約束解除。
  • 更正第 6 棒 STATE 的「CM-1075 實作中異動卡在工作區、不可 git add -A」警語——CM-1075 已完成並 commit(BE 6964c89f/FE 209bf88),工作區已清乾淨。顯式 git add 的紀律不變(多 session 併發仍是常態)。
  • 更正 DEV 數字:主檔 13→14、版本 13→15、控制項 2,131→4,907、抽取 succ 5/fail 0/pend 8succ 13/fail 2/pend 0。CM-1076 已把原 13 支全數抽完(pending 歸零);2 支 failed 是決策者測 SSRF 防護時建的「SSRF」基準 v1/v2,不是故障而是防護正常運作的證據,且正是 CM-1077 的驗收目標。
  • 修正母卡 CM-1077 寫的「已抽取狀態沒有重抽入口,本卡要順便補」——CM-1075 已部分處理(只在「來源已過期」時給按鈕),一般 succeeded 態仍無入口。已寫進 .2 卡並列為 STATE follow-up 第 10 項。

留給下一棒:三張子卡 md 已寫好待上 Notion(🔴 一次 create-pages 建完保證連號,建完回填母卡 CM-1077);上版 STG/POC 仍需決策者放行(四支 migration 只套 DEV)。


§8

第 8 棒 — 2026-08-04(CM-1078/CM-1080 兩張 UI 卡落地並收尾)

做了什麼

  • CM-1078:掃描設定檔管理頁操作欄 7 顆圖示 → 單一 kebab 選單
  • CM-1080(本棒新開,決策者當場追加):兩頁加「全部展開/收合」+ 展開列視覺降階
  • 收尾:spec + 使用手冊 + frontend-overview.md §3.4 + 兩張 Notion 卡收 Done + memory 一條

commits

  • FE(✅ 決策者已 push)4c6ebd0(操作欄收 menu)e569492(檢視類也收進去)4c41169(全部展開+子表降階)d18ab4c(註解卡號修正)8bf5f64(控制項頁比照+樣式抽全域)
  • BE(❌ 未 push)aa2076a0(spec + 使用手冊)cbc083ea(frontend-overview §3.4 登記新 class)399081b4(STATE/LOG + 重 build 需求站)

關鍵決策

🔴 收合操作時,「沒有這個動作」與「不能做這個動作」必須用不同手法呈現。原本散列按鈕的做法是「can_edit === false → 整組 v-if 不渲染 + 旁邊一顆鎖頭解釋」,收進 menu 後若照抄,使用者只會看到一個少了幾項的選單、無從得知為什麼。定案:語意上沒有的不放(自有列的「複製為自有」、無現行版的「版更」與「檢視控制項」)、存在但不能做的放但 disabled。原因講一次放選單頂端(Menu #start slot),個別缺 capability 才在該項 label 後綴。

🔴 唯讀說明指向的「出路」不可跟著 disabled——說明寫「您可以複製為自有後編輯」,那顆「複製為自有」在公版唯讀時就必須仍可按(它寫的是新的租戶列、不動公版)。跟著灰掉等於把出口堵死。

鎖頭圖示改掛「來源」欄的公用版 Tag 旁:唯讀是「這一列的性質」不是「某顆按鈕的狀態」,掛在來源標籤旁讀得出因果(公版 → 所以唯讀),且操作全進選單後也不會無處可掛。

展開列樣式抽成全域 classrow-expansion-panel / -title / -count / -subtable)放 _theme-overrides.scss,兩頁共用。依 repo 規範「2+ 元件共用 → 全域」;menu-item-danger / dpm-menu-note 則是被迫全域——PrimeVue Menu popup 預設 appendTo="body",元件的 scoped style 到不了。

全部展開刻意不 invalidate 版本快取:展開 N 列=N 個子元件掛載,逐一失效就是打 N 個請求;而快取最舊只到「上次載入列表」那一刻(fetchProfiles 會全清),與主列表欄位的新鮮度同級——多打 N 次換不到實際更新的資訊。控制項頁的全部展開則只作用於當前篩選結果(展開列是純本地渲染沒有請求成本,但 DOM 會膨脹;且使用者按下那一刻想的是「把我現在看到的攤開」)。

教訓

  • 🔴 Notion Case Noauto_increment_id 唯讀欄位,不可在 create-pages 帶入(帶了會 400 type is read-only),而且實際配到的號碼無法事先推測——本棒依「目前最大是 1078」推測新卡是 1079、寫進程式碼註解,實際卻配到 1080(1079 已被第 7 棒的專案軟刪除卡佔走)。建卡完必須 fetch 回來看實際 Case No,再回填程式碼/文件裡的引用(本棒為此多花一支 commit d18ab4c)。
  • 「留最高頻的一兩顆在外面」這個常見建議,在圖示語意相近時不成立——第一版留了「檢視」與「檢視控制項」兩顆在操作欄外,決策者當場回饋「兩顆並排讀起來還是像工具列,眼睛跟清單長太像,user 一定會誤會」。全部進選單、每項都有文字,反而更清楚。
  • Dialog 開關動畫期間的 overlay 會吃掉後續點擊——瀏覽器自動化連續操作時,關 dialog 後要等 2~3 秒再點下一個目標,否則點擊落在 overlay 上(本棒撞到兩次,症狀是「看起來沒反應」)。

推翻了什麼

  • 更正第 7 棒 STATE 的「CM-1078 已落地」——第 7 棒實查看到 buildActionMenuItems(row) 就判定已落地,但那是本棒稍早的中間態(留兩顆在外面)。本棒完成後才是最終形態:操作欄只剩單一 kebab,讀取類也收進去了。STATE 對應列已改為 Done 並註明。

留給下一棒:CM-1077 三張子卡仍待上 Notion(第 7 棒產出的 md);CM-1077.3 加 menu item 時務必照本棒定下的 disabled 規則(見 spec §4 gate 表與 STATE 該段);上版 STG/POC 仍需決策者放行。BE 三支文件 commit 未 push(FE 五支決策者已自行 push)。

本 block 收尾時就地更正過一處:原寫「commits 全部未 push」,實查 git rev-list origin/..HEAD 為 0,FE 五支決策者已 push,僅 BE 未推。STATE 對應表格同步改為逐列標示 push 狀態。


§9

第 9 棒 — 2026-08-04(Notion 建卡收口 + CM-1082 落地)— 協調者

做了什麼

  • CM-1077 三張子卡上 Notion:CM-1082(.1 判定服務)/CM-1083(.2 修正來源+刪除)/CM-1084(.3 前端接 menu),Case No 連號
  • 母卡 CM-1077 回填「已拆成三張子卡」段(連結/依賴順序 .1.2.3/拆卡理由),並修正原寫的「已抽取狀態沒有重抽入口,本卡順便補」為現況(CM-1075 只補了「來源已過期」態,一般 succeeded 態仍無 UI 入口;後端流轉歸 .2、前端入口歸 .3)。母卡狀態維持 Not started
  • CM-1072 收 Done(決策者手測通過,補三條不變式與 SSRF 說明);母卡 CM-1054 收 Done(內容全量重寫五段,含明列的「尚未做」七項)
  • CM-1082 派工並驗收落地.2 卡(CM-1083)md 對齊 .1 實況並同步回 Notion
  • POC/STG 唯讀盤點(純 SELECT,未動任何狀態)

commits

  • BE 17e977eb feat(FR-060.2): 檢測基準「是否被使用過」判定服務(CM-1082 / CM-1077.1),8 檔 +540未 push
    • 新增 infra/detection_tools/repository/detection_profile_usage_query.py 124 行(三來源 UNION ALL,find_refs()
    • app/.../detection_profile_service.py +98(get_version_usage() / get_profile_usage() / _build_usage()
    • domain/.../detection_profile_domain_service.py +11(list_all_versions(),不分頁、判定專用)
    • route +53/serializer +26/api/detection_tools/__init__.py +15(兩支唯讀端點)/DI wiring +9
    • test/test_detection_profile_usage.py 204 行 11 條
  • 工作區未 commit:cards/CM-1077-2-edit-and-delete.md(185→235,+51/−1)
  • 本棒起算 BE 未 push 5 支aa2076a0 cbc083ea 399081b4 7b987e8c 17e977eb(前四支是第 8 棒的)

Notion:CM-1082/1083/1084 新開;CM-1082 → 修正待驗證;CM-1072 → Done;CM-1054 → Done;CM-1077 回填後維持 Not started

關鍵決策

  • Notion 建卡改「兩階段」:先一次 create-pages 建三張薄卡(標題+properties+摘要)鎖住連號,再逐張 update-page 填完整內容。四次呼叫全成功、未撞串流中斷。被排除的:第 7 棒本體一次送三份長內容的做法——已實測會撞 API Error(與第 2 棒 Response stalled mid-stream 同類)。薄卡先建保證了連號這個唯一無法事後補救的性質,內容則可分次補
  • 協調者實跑驗證而非採信 subagent 自報pytest test/test_detection_profile_usage.py11 passed;四條硬性禁令逐條 grep 實查通過(無 LIKE '%_profile%'、無 status='archived' 當軟刪判準、canonical build_profile_ref()extract_profile_uid() 有實際呼叫非自寫 startswith、route 註冊順序正確)

教訓

  • 🔴 STATE 抄 Notion 狀態必然過期,派工前要現查——本棒用 SQL 一次撈 20 張 FR-060 卡,與 STATE 記載對照發現大面積漂移:CM-1055/1056/1057 已全 Done(STATE 只寫「子需求卡」)、CM-1060~1065 已全 Done(STATE 寫「修正待驗證」)、CM-1066 已 Done 且早已完整回寫(STATE 寫「尚未回寫」)、CM-1072 已可收(STATE 寫「狀態待標記」)。協調者照 STATE 寫的派工單交代 subagent「兄弟卡停在修正待驗證,一律不要動」——那份資訊本身就是過期的(subagent 守住紀律沒動兄弟卡狀態,是對的)。Notion 是多方會改的外部真相,交接文件只能當索引不能當狀態來源;STATE 該表已加警語
  • 🔴 fail-open fallback 是守門測試的假性通過來源——_build_usage() 有一條 if self._usage_query is None: return {"in_use": False, ...}(本意是給不組全套依賴的單元測試用)。這條讓「守門有效」與「守門根本沒被呼叫」在測試裡長得一模一樣.2 的 409 測試若 mock 掉 usage_query 或漏組依賴,判定一律回「未使用」,測試會綠但守門沒測到。已寫進 CM-1083 完成定義第 13 條(要求斷言 in_use=true 那條路徑確實走到)。 ⚠️ 附帶:協調者第一次檢查時 DI 尚未 wiring,那時這條 fallback 在真實環境就會生效(判定全面回「沒被使用過」,.2 接上去等於全放行)。後續 commit 已補 DI wiring,退化成僅測試路徑可達
  • 🔴 exclude 式過濾會靜默吃掉你沒想到的東西——查 CM-1082 進度時用 git status --short | grep -vE '\.png|\.docx|...' 濾雜訊,把 app/ 那個已修改的檔一起濾掉了,因而誤判「app service 未實作」。應該用 git status --porcelain 看全量,或至少改 include 式(grep -E '\.py$|\.sql$'

推翻了什麼

  • 推翻第 7 棒 STATE 記的「CM-1066 尚未回寫」——CM-1066 早已是 Done 且早已完整回寫(含四支 commit bbcb88207b8a7de4e79677274d13 與 PrimeVue 展開列根因)
  • 推翻「CM-1077 子卡會接在 1080 後面」的預期——CM-1081 不屬於 FR-060(被別條線佔走),子卡從 1082 起跳。這是第 8 棒教訓「Case No 無法事先推測、建完必須 fetch 回來看」的第二次實例
  • 推翻 STATE Notion 卡片樹整片記載(見上方教訓①),已按實查重寫並加「派工前現查」警語
  • 更正 §5 第 2 項「POC 環境唯讀確認仍未做」——本棒已完成:POC config.detection_tool_profiles 存在、12 筆;STG 10 筆;兩環境 detection_profile% 新表數皆 0。四支 migration 只套 DEV 的結論不變

留給下一棒:CM-1083(.2)前置已就緒、卡已對齊實況,可直接派工;CM-1084(.3)待 .2 完成。CM-1082 與 CM-1075 待決策者手測(CM-1082 卡內三條 DEV 實測斷言需起 BE 打端點,11 條單元測試替代不了)。上版 STG/POC 仍需決策者放行;SPEC/手冊補 CM-1075 行為仍未做。


§10

第 10 棒 — 2026-08-04(CM-1077 三張子卡全數落地 + schema 命名議題)— 協調者

做了什麼

  • CM-1077 三張子卡全數落地:CM-1083(.2 修正來源 + 硬刪,BE 寫入端)/CM-1084(.3 兩動作接進操作 menu,FE);另修 CM-1093(.2 迴歸測試整批跑 6 條 fail)。連同第 9 棒的 CM-1082,四張卡皆 修正待驗證,待決策者手測
  • 分頁預設 10 → 20(決策者口頭交辦、無 Notion 卡)
  • 新開兩張 schema 命名整理卡:CM-1094(母案)/CM-1095(configdetection),現階段只記錄不做

commits(BE/FE 皆未 push

  • BE dddfe7cf CM-1083(.2)修正來源 + 硬刪,9 檔 +790
    • 三支端點 PATCH /detection-tool-profile-versions/<uid>/sourceDELETE /detection-tool-profile-versions/<uid>DELETE /detection-tool-profiles/<uid>
    • 三個 error code DETECTION_TOOLS_409009(版本已被使用)/409010(主檔底下有版本被使用)/409011(current version 不可單獨刪)
    • test/test_detection_profile_edit_delete.py 19 條;無新 migration(CASCADE 在 FR-060.1 拆表時已設好)
  • BE fe615d3e CM-1093 補 autouse patch logger,1 檔 +17
  • FE d6e71e1 CM-1083 的 error code i18n 三語系,3 檔 +9
  • FE 88f2e13 CM-1084(.3),8 檔 +818/-31:新增 components/ProfileSourceDialog.vue 207 行、composables/useProfileUsage.js 122 行;DetectionProfileManageView.vue +170/-10、ProfileVersionTable.vue +185/-21、i18n 中英各 +21、api.js +15、DetectionToolProfileService.js +77
  • FE 247b6f5 分頁預設 10 → 20,1 檔 +1/-1

Notion:CM-1083/CM-1084/CM-1093 → 修正待驗證(連同 CM-1082 共四張待手測);CM-1094/CM-1095 新開,皆 Not started

關鍵決策

🔴 config schema 更名為 detection,表名不動,併進 FR-060 上版執行

觸發點是決策者質疑「為什麼整個功能的資料都被放進 config schema」。實查確認本專案 schema 混用兩種不相容的切法——oscal(58)/compliance(45)/survey(15) 按業務領域切,只有 config(9) 按資料種類切;而決策者原意的「系統設定、選單設定」其實早在 publicsystem_configssystem_menusui_routesrolestenants 等),config 實際裝的是檢測工具這單一功能的全套資料,含 detection_profile_controls 這種數千筆的業務明細表——那不是設定。業界慣例是切「限界上下文」而非資料種類,理由有三:按種類切會無限膨脹成雜物間且單一功能被拆散;schema 是權限與遷移的單位,按領域切未來拆服務可直接搬走;「設定」不是穩定分類(detection_profiles 有版本、有明細、有狀態機,行為完全是業務實體)。

被排除的命名detection_tool——schema 內不只有 tool,還有 profile、taxonomy、job 綁定;detection 才是這個領域的字,程式碼裡也一直是。 被排除的選項:連表名一起改(detection.profiles)——名字重複一點可接受,換來省下改 8 個 model/mapper/所有 raw SQL 的成本,也保住 grep 可搜性。

🔴 時機裁示:併進 FR-060 四支 migration 的上版,不單獨排。理由是這是唯一便宜的時機——分開做等於為了一行 SQL 再排一次上版、再驗一次三環境;而 config 每多長一張表、每多一條 FK 指過來成本就跳一階,現在零跨 schema FK 是最好的狀態。母案 CM-1094(含 public 那 56 張的分流)則照決策者裁示「先不用,之後會再做一次大整理」。

🔴 重要前提:這不是 DEV-only 操作config schema 本身在 STG/POC 都已服役(FR-056 v1.11.0 帶進去的),兩環境各 5 張表(detection_toolstenant_detection_tool_configsdetection_tool_param_schemasdetection_tool_profilesjob_execution_detection_tools),所以改名會動到 POC(等同 production)。成本盤點(實查):跨 schema FK 、DB 端一行 ALTER SCHEMA 即可(RLS/GRANT 跟著物件走)、程式碼 8 個 model 檔、舊 migration 檔不改。必須等決策者當次明示放行。

教訓

  • 🔴 exclude 式過濾會靜默吃掉你沒想到的東西——查 CM-1082 進度時用 git status --short | grep -vE '\.png|\.docx|...' 濾雜訊,app/ 那個已修改的檔一起濾掉,因而誤判「app service 未實作」、對決策者回報了錯誤的半成品結論。應該用 git status --porcelain 看全量,或用 include 式(grep -E '\.py$|\.sql$')。(第 9 棒已記過一次同類,本棒是複發,故再記一次並升級為「盤點類命令一律看全量」。)
  • 🔴 grep 命名法要涵蓋 kebab-case,否則會誤判「這個 prop 不存在」——查 FE 分頁設定時 grep rowsPerPageOptions(駝峰),漏掉實際寫法 :rows-per-page-options(kebab-case),因而在派工單裡寫下「該頁沒有 rowsPerPageOptions、使用者不能自選筆數、不要順手加」——前提整個是錯的,該 prop 一直都在(L711)。subagent 照指示沒動它,結果正確但理由是錯的。Vue template 的 prop 有兩種寫法,grep 要兩種都試。 ⚠️ 附帶:這次僥倖沒事是因為 20 剛好在 [10, 20, 50] 清單內。若改成不在清單裡的值(如 15),PrimeVue 的 paginator 選擇器會顯示空白——改預設分頁時必須同時確認新值在 rows-per-page-options 內。
  • 🔴 多 session 併發時,git add <檔名> 也不夠安全——分頁那個改動下手時,DetectionProfileManageView.vue已有另一 session 約 97 行 .3 的未 commit 內容。單純 git add <檔名> 會把人家做到一半的功能整包 commit 走。正確做法是只抽自己那個 hunk 用 git apply --cached 進 index,commit 後確認對方 6 個檔仍完好留在工作區。→ 紀律升級:同一檔案有他人未 commit 內容時,顯式 git add <檔名> 仍不足,要 hunk 級別暫存。(這是第 6 棒「收尾 commit 前必須先看工作區有什麼不是自己做的」的下一層——看到了還不夠,同檔混雜時要更細的粒度。)
  • 🔴 Notion 的 Cloudflare WAF 會擋含 shell 指令字串的 POST——CM-1093 建卡時,卡片內容含 pytest ... -p no:cacheprovider --continue-on-collection-errors 完整指令行,連續被 WAF 拒。改成散文描述重現方式即可通過。 之後寫 Notion 卡若要放指令,拆開或改描述。
  • .2 加了稽核 log 埋點就會撞本專案已知坑——.1 判定服務唯讀沒有埋點所以測試無事,.2 的刪除/改來源加了 8 處稽核 log,整批跑時前面的測試已初始化 logging、DBLogHandler 會試圖寫 DB 撞 SessionLocal is None六條 fail 全是走到埋點的成功路徑(被擋回 409 的反而沒事,還沒走到 log 就 raise 了)。解法是 autouse fixture + monkeypatch 換成真的 logging.getLogger("test")(比照 test_audit_round_app_service.py:36-39),不是逐條包 context manager(19 條裡 6 條會踩,逐條容易漏且未來新增又會踩)。

推翻了什麼

  • 推翻第 9 棒 STATE 寫的「CM-1081 不屬於 FR-060(被別條線佔走)」的模糊記載——已查明它是**「升級迴歸」那條 arc 的母案**,子卡 CM-1085~1092=T-0~T-6。⚠️ Notion 上現有兩條 arc 交錯編號,看卡號時不可依編號連續性推斷歸屬:1082/1083/1084 是 FR-060,1085~1092 是升級迴歸,1093 又回到 FR-060。
  • BE b393e1d6(CM-1086 migration manifest 對齊機制,4 檔 +541)不屬 FR-060,是升級迴歸那條線的 commit,只是同在未 push 清單裡。STATE Git 節已改為逐列標示「屬本 arc?」。
  • 更正 STATE 的未 push 數:BE 5 → 8 支(其中 7 支屬 FR-060)、FE 0 → 3 支(全屬 FR-060)。實查命令 git log --oneline origin/feature/e2e-env-build..HEAD 於兩 repo 各跑一次。
  • 更正 DEV 數字,並標明它正在變動:第 7 棒記的「主檔 14/版本 15/控制項 4,907/failed 2」已不成立——本棒同一分鐘內連查三次得到 14 → 12 → 11(決策者正在手測 CM-1084 的刪除)。原 2 支 failed(「SSRF」基準 v1/v2)已被 .2 的刪除驗收刪乾淨。STATE 已改為「不要當基準、一律現查」並附命令。
  • 推翻 STATE follow-up 第 11 項的預期「CM-1077.2 會碰到一般 succeeded 態的重抽入口」——.2 只讓「改來源後」自動回落 pending 重抽,沒有補主動重抽入口,該條仍未解。

留給下一棒

  • 四張卡(CM-1082/1083/1084/1093)待決策者手測,手測重點與 DEV 取樣建議在 STATE §4 第 1 項的表格
  • 上版 STG/POC 需決策者放行;🔴 上版時一併執行 CM-1095configdetection),DB 與程式碼必須同批、改完重啟 BE
  • SPEC/手冊未補的行為累積三批:CM-1075(ETag 比對)、CM-1083(三支寫入端點 + 三個 409 守門)、CM-1084(menu 兩動作 +「刪除與停用只出現其中一項」規則)
  • CM-1077 母卡待三張子卡手測通過後才收 Done

§11

第 11 棒 — 2026-08-07(全案結案)— 協調者

做了什麼

  • 決策者明確下令收尾,本棒為純文件與 Notion 更新工作(不改程式碼、不碰 DB 寫入、不 commit 程式碼、不 push)
  • 母卡 CM-1054 追加「全案結案」段insert_content position end,不覆蓋既有五段):三環境 registry 對齊清單、BE/FE 均已 push、POC follow-up 標記、決策者裁示原文
  • 新開 follow-up 卡 CM-1104FR-060 POC 12 支公版尚未觸發控制項抽取——待管理員在 UI 逐支點擊,properties 專案 CM/來源 雷門/作業人員 雷門(此事是管理員自己在 UI 操作,非 Claude 實作)/狀態 Not started/需求編號 FR-060
  • STATE 改寫 §2/§4:一句話改為「全案結案」;補三環境 registry 對齊實查結果;Git 段改為「均已 push、0 未推送」;§4 下一步分組改為「已完成/結案後 follow-up/未確認狀態」

commits:BE <待收尾時填入>(STATE+LOG 兩檔)。不 push。

Notion:CM-1054 追加內容成功;CM-1104 新開,Not started

關鍵決策

  • 無新決策,本棒執行的是上一輪已拍板的收尾動作

教訓

推翻了什麼

  • 推翻第 10 棒 STATE 記的「BE 未 push 8 支/FE 未 push 3 支」——本棒交接派工單提供的實查數字為「均為 0 未 push(已全數 push)」,決策者已於第 10 棒之後完成推送
  • CM-1095(configdetection)執行狀態未經本棒查證——派工單只給了三環境 migration registry 清單(cm1047fr060-1(b)fr060-2fr060-3 五筆),未見 CM-1095 對應記錄;STATE 已標記為「未確認狀態」,不假設已隨上版執行,留待下一手用 \dnschema_migrations 查證

留給下一棒

  • CM-1104:等管理員在 POC UI 上逐支觸發「開始解析」,之後回頭確認 12 支 extraction_status 是否全變 succeeded
  • CM-1095 執行狀態待查證(見上方「推翻什麼」)——若尚未執行,需另外找時機排入
  • CM-1077 母卡、SPEC/手冊 CM-1075/1083/1084 三批行為是否已補寫,本棒未查證