本卡屬 FR-090(母卡 CM-1682)第 6 棒 CM-1696 拆出的第 1 子卡,套件側。決策者 2026-09-13 裁 B 案。動 jedi 5 支(asset 定形+4 支照抄),併進 CM-1559 那個套件版(1.1.0),本卡不發版。第 2 子卡(主專案側)等本卡 commit 後才派。
主專案其他模組要用某支套件的 service(例如 oscal 匯入要用檔案上傳 service),現在的寫法是直接伸手進套件的 DI container(依賴注入容器,就是「誰負責建哪個 service」的組裝表)拿 Provide[upload_file_container.file_upload_service]。這等於主專案知道套件內部怎麼組裝——套件改一個 service 簽名,主專案 48 處要跟著改。
B 案:每支套件在自己的 plugin/ 開一個正式的「宿主取用口」——一支 host_services(app) 函式,回傳套件 register() 時已組好的 service provider 名冊(<Pkg>Services dataclass,欄位是「呼叫就給一個」的 provider)。主專案要用套件的東西,一律從這個口拿,不碰套件的 domain/infra 層。之後套件內部怎麼重構,主專案不受影響。
首腦核對(2026-09-13,jedi HEAD ac9c152 / BE HEAD 3bf476a9):
jedi_asset/plugin/__init__.py:82-121 register() 已把 _RuntimeContext(adapters, config, schema_extensions, services=build_services(adp)) 寫進 app.extensions[EXTENSION_KEY];api/guards.py:17 runtime() 用 current_app 取。取用口只差一支不依賴 request context 的讀法(主專案 DI 是 app 建立期組裝,沒有 current_app)。jedi_asset/plugin/assembly.py build_services():宿主 services 欄位沒給時,套件用 adapters 的 port 自己組一份,回 provider 不回實例。四支照抄對象都有同名函式(file-upload assembly.py 有 build_services;system_core/survey/notification 見各自 plugin/assembly.py)。<Pkg>Services 現有欄位:FileUploadServices{file_upload_service, upload_file_service};SystemCoreServices{system_menu_service, system_config_service};SurveyServices 12 欄(含 survey_domain_service/survey_question_domain_service/task_survey_service);NotificationServices{test_mail_service}。upload_file_domain_service;system_core 要 tenant_storage_config_seeder/storage_config_restore_app_service/security_policy_app_service;survey 要 survey_snapshot_domain_service;notification 要 notification_service(不是 test_mail_service)。這些要加進對應套件的 <Pkg>Services,並在 build_services() 補自組路徑。~/Projects/Jedicogy/module/jedi-python-package/ branch feature/FR-075
jedi-asset/jedi_asset/plugin/{__init__.py,contract.py,assembly.py} 定形:host_services() + 測試
jedi-file-upload/jedi_file_upload/plugin/{contract.py,assembly.py,__init__.py} 照抄+補 upload_file_domain_service
jedi-system-core/jedi_system_core/plugin/... 照抄+補 tenant_storage_config_seeder/storage_config_restore_app_service/security_policy_app_service
jedi-survey/jedi_survey/plugin/... 照抄+補 survey_snapshot_domain_service(已有 survey_domain_service/survey_question_domain_service/task_survey_service)
jedi-notification/jedi_notification/plugin/... 照抄+補 notification_service
主專案 48 處引用(第 2 子卡改,本卡只需知道要開哪些欄位):
upload_file 23:file_upload_service 14/upload_file_domain_service 8/containers.py:178 override 1
consumer:oscal×5 flow_control×2 detection_tools×8 module_frame×2 feedback flow_engine cloud_integration
system_core 14:system_config_service 9/tenant_storage_config_seeder 2/storage_config_restore_app_service 2/security_policy_app_service 1
consumer:notify_config auth setup feedback×2 task_survey flow_engine + api/system_config 兩支 route 六處 Provide[]
survey 4:survey_domain_service 2/survey_question_domain_service 1/survey_snapshot_domain_service 1(task_survey_container 那 7 處是主專案自己的 container,不算)
notification 1:notification_service(feedback)
plugin/__init__.py 加 def host_services(app) -> AssetServices:從 app.extensions[EXTENSION_KEY].services 取,未 register 時拋明確錯誤(不可回 None 或空名冊——安靜降級會讓主專案拿到 None provider、第一個 request 才炸)。__all__ 加入。契約 docstring 寫清楚「這是宿主唯一允許的取用口;主專案不得 import 套件 domain/infra 建 service」。tests/ 加 test_host_services_returns_registered_providers(register 後拿得到、欄位是 callable、呼叫得到 service 實例)、test_host_services_before_register_raises(未 register 拋錯不回 None)、library 模式(mount_api=False)也拿得到。突變:把「未 register 拋錯」改成回空名冊 → 第二條要紅。host_services(app)+三條測試。先查再寫:四支 register() 是否都在 mount_api=False 時也寫 extensions(asset 是;其他三支若不是,要補,否則 library 模式拿不到)。<Pkg>Services(欄位型別照既有 Optional[Callable[[], Any]]),build_services() 補自組路徑(宿主沒給時套件自己用 repo 組)。欄位命名沿用主專案現在 Provide 的名字,第 2 子卡才能機械替換。test/test_module_boundaries.py 加 test_di_containers_do_not_build_package_services:di_containers/** 不得 import jedi_*.infra.repository/jedi_*.app.service/jedi_*.domain.service。現況 26 個 container 檔命中(flow_control 14、auth 14、detection_tools 11 最多),先全數列進白名單常數 _DI_PACKAGE_INTERNAL_IMPORT_ALLOWED,第 2 子卡逐檔清空。守衛紅要印檔名+行號。需決策者裁的已裁:B 案。本卡不需再問。若做到一半發現某支 build_services() 自組某 provider 需要主專案的 port(例如 storage_config_restore_app_service 要讀主專案 storage 設定),那個 provider 維持由宿主 services= 傳入,取用口照樣回它——回寫本卡標記哪幾個是「宿主必填」,第 2 子卡據此保留對應 container provider。