本卡屬 FR-091(母卡 CM-1690),能力點前置三卡的第一張。只動 jedi-asset 與 jedi-common 兩支套件,不動主專案、不動 DB、不動 seed 檔。建議 model:Opus,effort:high——這張在定 19 支要照抄的形狀,判斷比手速重要。
每支套件的 API 用「能力點」(capability,例如 device.create)決定誰能按哪個按鈕。現在套件裡的能力點名字是散裝的:jedi-asset 在 plugin/contract.py 寫了六個 DEFAULT_*_CAPABILITY 常數,survey 與 detection 直接在 route 上寫死字串,read 類的能力點(device.read,前端選單用)套件裡根本沒提到。主專案要知道「這支套件總共需要哪些能力點」,只能打開檔案用眼睛找,然後手抄進 scripts/init/04-seed-core.sql。兩邊靠人對齊,改名或漏抄不會有任何測試變紅,症狀是某個操作永遠 403。
這張卡要做的是:讓 jedi-asset 用程式讀得到的方式,把自己需要的能力點列成一份清單(叫 CAPABILITIES),每一項寫清楚名字、資源、動作、說明、預設該給哪個角色。既有的六個常數與 AssetPluginConfig 的預設值改成從清單取,對外 import 面不變。這份清單之後會被三個地方用:① 主專案守衛測試比對 seed 檔有沒有漏(下一張卡);② 其餘 18 支照抄(回補卡與 1726/1727);③ 未來的 migrate arc 由 init image 讀清單自動補 row(另案,本卡不做)。
class Capability 零筆)。jedi-common 在主專案已是 path dependency(pyproject.toml:228),改動重啟即生效。@capability_required(lambda rt: rt.config.information_system_create_capability)(api/routes/information_system_route.py:82),名字從 config 取,這個不變。jedi_common/plugin/capability.py(目錄 plugin/ 新建,附 __init__.py),一個 frozen dataclass Capability(name: str, resource: str, action: str, description: str, default_roles: tuple[str, ...] = ("Administrator",))。加 __post_init__ 驗 name == f"{resource}.{action}"(asset 八個都符合這規則;不符合就 ValueError,讓填錯當場炸)。放 jedi-common 而不是各套件自己定義,是因為 19 支要同一個型別、主專案守衛才能用同一種方式讀。先查再寫:grep 一次 jedi-common 確認真的沒有同義型別(首腦查過是零,runner 再確認一次)。CAPABILITIES: tuple[Capability, ...] = (...) 八項(六個寫入類+兩個 read),description 照 seed 檔現有文字(device create access 這種)。六個 DEFAULT_*_CAPABILITY 常數保留名字(是凍結的對外符號,版面守衛 FROZEN_PUBLIC_SYMBOLS 有列),值改成從清單取,例如 DEFAULT_DEVICE_CREATE_CAPABILITY = _cap("device", "create").name。AssetPluginConfig 不動。plugin/__init__.py 的 __all__ 加 CAPABILITIES;tests/unittest/test_plugin_layout.py 的 FROZEN_PUBLIC_SYMBOLS 加 CAPABILITIES(這是新增公開符號,不是改既有的,19 處 consumer 不受影響,但 commit message 要寫明)。docs/features/FR-069-2608-jedi-module-extraction/extraction-sop.md §4.3 分檔規格表 contract.py 那列加「CAPABILITIES 清單」,並加一條實作要點:能力點一律進清單、DEFAULT 常數從清單取、read 也要列、route 只透過 config 取名字不寫死字串。§8 收工檢查表加一條。這是 BE repo 的檔,單獨 commit。