本卡屬 FR-091(母卡 CM-1690)第二批。中量偏重:這支施工日誌最多(94 處),且主專案接線不在 CM-1689 收的 11 支內(走
config/app_modules.py:97的 blueprint 載入迴圈,三張 adapter 在infra/license/,core/scheduler.py:244還從HANDLE_KEY拿 handle),本卡順手收進core/plugins/license.py。授權鎖定是出貨關鍵路徑,本卡零行為變更,手測要走到鎖定與開通。
jedi-license-runtime(授權執法端)plugin.py 443 行 5 dataclass 加 _RuntimeContext、build_services、create_blueprint、register、iter_migrations 全塞一檔;api/ 沒有 __init__.py 守門,route 從 current_app.extensions[EXTENSION_KEY] 直接取(等於自己寫了一份 runtime()),api/routes.py 已經是路由表(形狀接近 routing.py)。94 處施工日誌(CM-1579、CM-1626 等沿革)塞在 docstring 裡。主專案側接線散在 api/license/__init__.py、infra/license/ 三檔、di_containers/license/、core/scheduler.py 四處。兩邊一起整成標準形狀。
首腦核對(2026-09-12):套件側 plugin.py 443 行 5 dataclass;api/ 有 routes.py(路由表+認證等級欄)、license_route.py、license_admin_route.py、serializers.py,無 __init__.py 守門;route 14;entity 檔 3(1 dataclass);表 tenant_licenses、tenant_license_suspensions;migration 3 支;tests 63 支(無 integration);harness 有 compose、無 --smoke;施工日誌 94 處;ctx() 0 處(但 route 直接讀 extensions,要收成 runtime());logger common.jedi_license_runtime ×6/jedi_license_runtime ×1/name ×2;不吐 created_user_name → 不開 identity port。主專案側:api/license/__init__.py、infra/license/{notifier,tenant_admin_directory_adapter,tenant_directory_adapter}.py(守衛白名單三列)、di_containers/license/license_containers.py(import tenant_directory_adapter)、core/scheduler.py:244、config/app_modules.py:97;主專案 14 檔 import;venv 指工作樹。
套件:/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-license-runtime/
範例:jedi-asset/;SOP:extraction-sop.md §4.3/§3.4/§5.1/§5.2;說明:plugin-anatomy.md
設計:docs/features/FR-062-2608-license-management/design.md §4.6(防死鎖——login 級五支不可升 admin)
主專案(本卡允許動的範圍):
api/license/__init__.py → 併進 core/plugins/license.py 後刪
infra/license/*.py(三檔) → 併進同檔 ① 段後刪;tenant_directory_adapter 被 di_containers/license/ 也 import → 改 import 新位置,或依 SOP §5.1 例外留原地並保留白名單(runner 判斷後在回寫說明)
di_containers/license/license_containers.py → 改 import
core/scheduler.py:244 → 從 HANDLE_KEY 拿 handle 的那段不改語意,只確認 import 路徑仍通
config/app_modules.py:97 → 拿掉 "license"
core/plugins/__init__.py → PLUGINS 加一列
test/test_module_boundaries.py → 白名單三列拿掉、已收斂清單加 infra/license/
現成範本:core/plugins/survey.py、core/plugins/integrity.py(CM-1689 產物)
api/routes.py docstring 那兩條紅線(login 五支不可升 admin/註冊順序不可調)是「陷阱」,留一到兩行;CM-1579/CM-1626 的沿革敘述進 commit message,檔內只留「這張 port 也負責租戶樹拓樸」這種一句話事實。plugin.py → plugin/ 五檔(contract 放 LicenseConfig/LicenseAdapters/SchemaExtensions/PluginHandle/EXTENSION_KEY/HANDLE_KEY;runtime 放 _RuntimeContext;assembly 放 build_services/create_blueprint/_guard;migrations 放 iter_migrations);api/ 新建 guards.py(runtime()+三個 lazy decorator,route 改從 runtime() 取不再直讀 extensions)、routes.py 改名 routing.py(內容就是路由表)。register() 簽名與 from jedi_license_runtime.plugin import X 路徑一字不改,HANDLE_KEY 必須仍在 __all__(scheduler 用)。common.jedi_license_runtime;route 直讀 extensions 的地方全改 runtime();命名統一。inspect.signature 逐欄對照(授權狀態的預設值一個都不能漏,漏了執法軸會變)。--smoke(test client,走一次「無照→上傳照→開通」);pyproject.toml 加 addopts = "--import-mode=importlib";README 補新結構與接線路徑。build_adapters(container)+build_config()、③ mount()+PLUGIN。PLUGINS 加一列(位置沿用原 blueprint 載入順序附近);app_modules.py 拿掉;刪 api/license/;infra/license/ 三檔搬走或依例外留一檔(回寫說明判斷);守衛清單更新;core/scheduler.py 的 handle 取法確認仍通。先用 AST 掃全部 import。tests/integration/test_service_behaviour.py(目錄新建):真 postgres 套 001/002/003,走「指派授權→到期推進 advance_expiry_states→停權→解除」一輪,斷言 RLS 下租戶只看得到自己的照。LC 連線用假物件頂替,不打真 License Center。infra/license/(若三檔全搬)。test_plugin_contract.py 凍結集合(14 條 URL、認證等級)整理前後同綠——特別是 login 級五支仍是 login。