本卡屬 FR-091(母卡 CM-1690),版面殘留回補第二張。動三支已 Done 的套件,主專案不改。建議 model:Sonnet 5,effort:medium。可與 CM-1731 平行。
SOP 說 port(套件要宿主提供的介面)放 domain/ports.py。首腦 2026-09-13 掃 21 支發現有五支開了頂層 ports/ 目錄:file-upload、issue、notification、oscal-v2(無 plugin,不在 FR-091 範圍)、iam(兩邊都有,CM-1725 在跑,收回時要求收斂)。兩處並存,讀者分不清哪個才是 port。另外 jedi-issue 把 tests/ 放在 jedi_issue/tests/ 套件內,靠 pyproject.toml 一條 exclude 才不進 wheel,其他 20 支都在套件根。這些都是 FR-069 早期抽取時各自就近做的選擇,FR-091 第一批~第三批整理時沒列入檢查項。今天已把三條規則補進 SOP §4.3(頂層不平鋪、port 只放 domain/ports.py、tests 在套件根),這張卡把已 Done 的三支收乾淨。
jedi_file_upload/ports/:__init__.py、dto、upload_file_provider(後兩個是子目錄或模組,runner 開看)。主專案 7 處 from jedi_file_upload.ports import ...(api/app/core/di_containers/infra 內),是三支裡唯一被宿主直接 import 的——搬完 ports/__init__.py 保留為 re-export 薄殼,主專案不改。jedi_issue/ports/:__init__.py、attachment_provider、issue_provider、project_member_provider。主專案零 import。jedi_notification/ports/:__init__.py、notification_provider。主專案零 import。notification 沒有 domain/ 目錄(dirs: api app common infra plugin ports)——要新建 domain/ports.py。jedi_issue/tests/:16 檔在 git 內(__init__.py、conftest、initdb、issue_attachment、issues、label、member、project_member);pyproject.toml :58-61 有一條標紅的 exclude 註解「這條 exclude 不可拿掉」。搬到套件根 tests/ 後那條 exclude 與註解就可以刪。套件根是否已有 tests/(CM-1712 F 組補了 testcontainers 整合測試)runner 先看,有就合併目錄。ports/ 內的 ABC/Protocol/型別 git mv 進 domain/ports.py(單檔;若原本拆多檔且合起來超過 300 行,改成 domain/ports/ 子套件,domain/ports/__init__.py re-export 全部)。套件內 import 全改新路徑。ports/__init__.py 留下,內容只剩 re-export(7 處主專案 import 的符號全部要在,runner 先 grep 主專案列出符號清單貼回寫);ports/ 內其他檔刪。加一條測試:from jedi_file_upload.ports import <7 處用到的每個符號> 都 getattr 得到。ports/ 整個刪(主專案零引用,runner AST 掃 BE repo 再確認一次含 scripts/ 與 test/)。git mv jedi_issue/tests/* tests/,合併 conftest(兩支 conftest 若都存在,合成一支,fixture 名字不衝突才合);刪 pyproject 的 exclude 與那段註解;pytest tests/ --import-mode=importlib 數字與搬前一致。ls -d */ 不含 ports」(file-upload 例外:允許 ports/ 但只能有 __init__.py 一檔)與「jedi_<pkg>/ 內不含 tests/」。ports/ 加回一個空檔 → 紅。file-upload 的 7 符號 getattr 測試,突變:從 re-export 拿掉一個 → 紅。__file__ 指工作樹。