本卡屬 FR-114 資安修正(母卡 CM-2019),b4 總測回報收集卡 CM-2231 #34。b4 回歸(CHECK 是 CM-2247 加的),高:任何客戶只要一版抽取失敗過一次,之後永遠抽不成功、沒有自動復原路徑。修 jedi-detection 套件。
檢測基準的某一版抽取失敗後(190 上是 b3 時期「找不到 cinc-auditor」),錯誤訊息留在 extraction_error 欄。b4 補了 CINC 後重抽,CINC 有跑成功(log「成功 controls=59」),但寫回資料庫時只把狀態改成 succeeded、舊的錯誤訊息沒清掉,撞到 CM-2247 加的 CHECK chk_dpv_succeeded_no_error(succeeded 時 error 必須為空),整個 UPDATE 回滾,接著失敗路徑又把這次的約束錯誤寫進 error 欄。下次再重抽還是同樣結果,死循環。
為什麼清不掉:成功落庫走共用的 BaseRepositoryImpl.update(),它的欄位迴圈帶 value is not None——設成 None 的欄位會被靜默跳過(jedi-common base_repository_impl.py:424)。同一支 repo 的 replace_source() 早就知道這件事(repo_impl.py:164 註解「為什麼不能走 update()」,:190 直接對 model 清),成功落庫這條路徑漏了同樣處理。
首腦已核對的證據:190 config.detection_profile_versions id=1(url 型,dev-sec linux-baseline)在升級後重抽,api log 20:49:25 先「inspec._run:243 成功 controls=59」,35ms 後「_extract_in_thread:472 worker 未預期失敗」,SQL 是 UPDATE ... SET sha256, source_etag, extraction_status='succeeded'——參數裡沒有 extraction_error。190 該列已由首腦手動 SET extraction_error=NULL 解鎖(決策者核准);DEV 11 版從沒失敗過所以驗不到。
/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/.claude/worktrees/jedi-wt-fix-security/jedi-detection(branch fix/security-b1,HEAD d94a472d,jedi-detection 版本 1.2.2)。只動 jedi_detection/ 子目錄、只 git add 該子目錄下的檔。/Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/.claude/worktrees/wt-fix-security(branch fix/security-b1)。pyproject.toml:73 pin jedi-detection==1.2.2、:124 有註解掉的 path 行——把 path 行改成上面的工作區路徑並取消註解,poetry update jedi-detection;這個 path 改動不 commit。8005 已有一支舊 BE 在跑(1.21.0b2),手測時另起 PORT=8006。jedi_detection/app/service/detection_profile_extraction_service.py:611-635 _persist() 成功段:612 設 extraction_error=None,635 呼叫 update_version → 走共用 update() 被跳過
jedi_detection/app/service/detection_profile_extraction_service.py:649-656 _write_status():失敗路徑,同一支 update_version(error 寫 None 時同樣清不掉,例如 STATUS_RUNNING 進場)
jedi_detection/domain/detection_tools/service/detection_profile_domain_service.py:138-141 update_version() 直接 self._versions.update(entity)
jedi_detection/infra/detection_tools/repository/detection_profile_version_repo_impl.py:153-190 replace_source():既有「不走 update()、直接對 model 清欄」的範例
jedi_detection/domain/detection_tools/repository/detection_profile_version.py version repo port(沒有 clear_fields;profile 那張表的 port 在 detection_profile.py:41 有)
主線 migration(BE 工作區)scripts/sql/2026-09-27-fr114-detection-profile-succeeded-no-error.sql CM-2247 加的 CHECK,只清了 succeeded 帶 error 的列
detection_profile.py:41,實作 detection_profile_repo_impl.py:130)。建議在 version repo port+impl 比照加一支 clear_fields(uid, field_names)(同 docstring 理由),或更直接:加一支 mark_succeeded(version) 在同一個 flush 內同時 extraction_status='succeeded' 與 extraction_error=None(照 replace_source :164-190 的寫法對 model 逐欄 set)。兩欄必須同一句 UPDATE——先 update() 再 clear_fields() 兩步,第一步就會撞 CHECK(repo_impl.py:164 的註解講的就是這件事,對 chk_dpv_source_exclusive)。_persist() :611-635 成功段改呼叫新方法;_write_status() :649-656 若 error 傳 None(進 running 態)同樣要真的清掉,否則 running→succeeded 之間仍可能殘留。succeeded AND error IS NOT NULL;既有客戶 failed 帶 error 的列在程式修好後重抽就會過,不必再開 migration。但本卡回寫時明講「190 id=1 已手動清、其他客戶不必清」。_no_error/<> 'succeeded' 型 CHECK(git grep -n "OR .* IS NULL" -- '*.sql'),看還有沒有同型「成功路徑靠 update() 清 None」的漏洞;有就記回寫,不擴 scope。屬核心共用邏輯(改 repo 寫入路徑),要寫。tests/integration/test_service_behaviour.py(需 Docker 起真 postgres,conftest 在同目錄)加一條:建一版 failed+extraction_error='x' → 跑成功落庫 → 斷言 status=succeeded 且 error IS NULL 且沒拋 IntegrityError。突變:把新方法改回走 update() 要紅(CHECK 會擋)。跑法 cd /Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/.claude/worktrees/jedi-wt-fix-security/jedi-detection && PYTHONPATH=. python -m pytest tests/integration -q -k succeeded;整合測試起不來就跑 tests -q 契約測試並在回寫寫明打折。
guidant_ai_dev,先確認 chk_dpv_succeeded_no_error 在:select conname from pg_constraint where conname like 'chk_dpv%'):挑一版 url 型(id=2 或 3),手動 UPDATE config.detection_profile_versions SET extraction_status='failed', extraction_error='模擬' WHERE id=<n>,畫面「檢測基準管理」按重新抽取 → 期待 succeeded、error 欄空、控制項數不變。