建議 model:opus/effort:medium — 跨三個既有子系統接線、背景執行緒租戶脈絡有已知陷阱、DI 漂移是靜默的。
本卡屬 FR-107(母卡 CM-1843)第 .2 棒(子需求卡 CM-1845),子任務 T-2.2:在主專案寫出三個轉接頭,讓套件的插座真的接到系統既有的儲存、任務與框架能力。
T-2.1 在套件定了三個插座(「我需要有人幫我拉檔/找任務/查框架版本」),這張卡在主專案寫出對應的轉接頭(adapter),真的接到系統既有的能力上:
jedi-file-upload 的儲存介面,把整批檔拉到一個本機工作目錄(之後容器就掛這個目錄)。其中「掛證據進任務」那支本卡先留空(NotImplementedError),T-4.1 才實作——因為它要等 job_evidences 表加完欄位。
有一個已知陷阱必須處理:分類是在背景執行緒跑的,背景執行緒沒有 HTTP 請求的脈絡,讀「檔案要存到哪」的設定時會挑錯租戶(這是 memory 記過的坑)。所以拉檔一定要明確帶 tenant_id 進去解析,不能靠 thread-local。
主專案
core/plugins/evidence_classification.py
:97 DriveEvidenceSourceAdapter ← 不動(舊線,已標 deprecated)
:127 LivingSspControlCatalogAdapter ← 不動
:170 build_adapters() ← 多三個參數
:204 _CONTAINER_ENV_KEYS ← 🔴 從九個縮到只剩 ANTHROPIC_API_KEY
(現在是:DB 四個、Drive 四個、ANTHROPIC_API_KEY)
← UploadProviderEvidenceStorageAdapter.stage_to_dir 補實作(T-1.3 留的 NotImplementedError)
← 新增 JobEvidenceTaskSinkAdapter
← 新增 OscalClassificationContextAdapter
infra/evidence_classification/ 對應 adapter 落點(若需要拆檔)
di_containers/evidence_classification/evidence_classification_containers.py ← 同步注入
要接的既有能力(都已存在,不要重寫)
infra/readmodel/oscal/ssp_control_implementation_query.py:74
get_jobs_by_round_id(round_id, locale) → 回 control_id/ao_part_id/job_uid
app/upload_file/service/managed_file_upload_service.py:324
upload_files_for_tenant() ← canonical 的帶租戶脈絡路徑
app/oscal/service/ssp_control_implementation_service.py:237
build_classifier_catalog_by_ssp_id(ssp_id)
UploadProviderEvidenceStorageAdapter.stage_to_dir(tenant_id, file_uids, work_dir):逐檔走 IUploadFileProvider.get_file 拉下來,落在 work_dir/files/<file_uid>.<ext>,回一串 StagedFile(含 file_id/file_uid/original_name/local_path/content_hash)。🔴 provider 的解析一定要用傳進來的 tenant_id 明確做,走 upload_files_for_tenant 同一條解析路徑,絕不能靠 get_user_context() 或 thread-local——背景執行緒沒有那個脈絡,會挑到錯的租戶設定(memory feedback_background_job_storage_config_no_context_trap)。JobEvidenceTaskSinkAdapter.resolve_jobs(round_id):接 ssp_control_implementation_query.py:74 get_jobs_by_round_id,把回傳整理成 {part_id: job_execution_id}。任務側的 key 是 ao_part_id,與 catalog 的 part_id 是同一個東西(形如 AC.L1-3.1.1_obj.2)——首腦已在 DEV 792 筆實查過同一個 part_id 只會對到一個任務,runner 不必重做這個核對,但驗收要對筆數。JobEvidenceTaskSinkAdapter.attach 與 find_same_content 本卡先 raise NotImplementedError,T-4.1 才實作(要等 job_evidences 加欄位)。OscalClassificationContextAdapter.resolve_round(round_uid):查輪次存不存在,回 (round_id, project_id);不存在回 None(不要拋例外,簽名說好回 None)。framework_version_chain(project_id):依「專案指定 → living SSP → catalog」順序回 framework_version_id 清單,去重、保序。_CONTAINER_ENV_KEYS 縮到只剩 ANTHROPIC_API_KEY(:204)。現在九個裡有資料庫四個、Drive 四個——D2 之後容器不再需要它們。這是本案的安全重點之一。build_adapters() 多三個參數,di_containers/evidence_classification/evidence_classification_containers.py 同步。檔頭有一條既有規則「五支 adapter 同時被 DI container 注入」,加到八支時要一起改。Factory 是 lazy 的:__init__ 的參數與 container 註冊對不上時,系統不會在啟動時炸,只在該端點被實際打到時才 500。所以本卡驗收必須實際打過新 route,不能只確認「BE 起得來」。這是本 repo 記過的坑(memory feedback_di_lazy_factory_signature_drift_silent)。