FR-080 唯讀盤點:六支「有殼無肉」jedi 插件

盤點日期 2026-09-09。宿主 HEAD 92412948(branch feature/FR-075),套件 monorepo HEAD 為當下工作區。 所有行號皆為當下實查,未讀 .venv。

§1

0. 一句話結論(先講重點,證據在後)

六支裡只有五支是「有殼無肉」,jedi-system-menu 不是——它在 P3.5(CM-1471)已經做完 route 上移, 是七支補殼套件裡三支「完整體」之一,宿主 api/system_menu/ 目錄已刪、core/app_factory.py:388-391 真的呼叫 register(..., mount_api=True)。把它算進「有殼無肉」是誤判,本報告予以更正並保留其為對照組。

其餘五支(bulletin / device / information-system / flow-engine / system-config)確實是 零 route 的空 blueprint + 宿主零呼叫 register(),套件在生產上只被當 library import。


§2

A. 殼的現況(create_blueprint() 掛幾條 route/宿主呼叫 register 幾次)

A-1 五支空殼:create_blueprint() 掛 0 條,宿主 零呼叫

五支的 create_blueprint() 實作逐字相同(只有型別名不同):

bp = Blueprint(cfg.blueprint_name, __name__, url_prefix=cfg.url_prefix)
bp.record_once(lambda state: state.app.extensions.__setitem__(EXTENSION_KEY, context))
return bp
  • jedi-bulletin/jedi_bulletin/plugin.py:171-193(add_resource 出現 0 次)
  • jedi-device/jedi_device/plugin.py:174-196(0 次)
  • jedi-information-system/jedi_information_system/plugin.py:175-197(0 次)
  • jedi-flow-engine/jedi_flow_engine/plugin.py:207-229(0 次)
  • jedi-system-config/jedi_system_config/plugin.py:171-193(0 次)

五支的 plugin docstring 都寫死同一句(例:jedi-bulletin/jedi_bulletin/plugin.py:41-42):

故本套件目前沒有 api 層,mount_api=True 掛出來的 blueprint 沒有任何 resource ——這是刻意的,且有測試焊死(tests/unittest/test_plugin_contract.py)。

焊死的測試是 tests/unittest/test_plugin_contract.py:113 test_blueprint_has_no_resources_by_design()。

register() 的 docstring 更把現況講明白(jedi-bulletin/.../plugin.py:213-215):

⚠️ 本套件現階段兩種模式的對外行為完全相同(blueprint 是空的), 差別只在 app.extensions 有沒有那個 key。

宿主呼叫次數:零。 全 repo grep jedi_*.plugin(排除 .venv)只命中兩處,且都屬 system-menu:

infra/system_menu/system_menu_plugin_wiring.py:35: from jedi_system_menu.plugin import SystemMenuAdapters, SystemMenuServices
core/app_factory.py:388:                                from jedi_system_menu.plugin import register as register_system_menu

即 jedi_bulletin.plugin / jedi_device.plugin / jedi_information_system.plugin / jedi_flow_engine.plugin / jedi_system_config.plugin 在宿主是 0 處 import。

🔴 flow-engine 是唯一有例外的:它的 plugin.py 除了空 blueprint 之外,還在 import 期做真事—— jedi-flow-engine/jedi_flow_engine/plugin.py:84-98 呼叫 set_repo_providers(RepoProviders(...)) 把四支 repo 實作注入 app/wiring。套件內 WorkflowExecutionService 有 22 處 get_repos() (jedi-flow-engine/jedi_flow_engine/app/service/workflow_execution_service.py),沒接線就 RuntimeError。 但因為宿主完全不用套件那支 WES(見 C-4),這條接線在生產上也沒有任何效果。

A-2 對照組 jedi-system-menu:掛 5 條,宿主真的呼叫 register

jedi-system-menu/jedi_system_menu/api/__init__.py:151-160 mount_routes():

/api/1.0/system/menu/<string:group>     GET   僅登入(CM-779 明文例外,消費端點不加 platform 守門)
/api/1.0/system/menus                    POST  登入+platform-admin
/api/1.0/system/category-menu            GET   登入+platform-admin
/api/1.0/system-menu                     POST  登入+system-menu.create+platform-admin
/api/1.0/system-menu/<int:id>            PUT/DELETE  登入+能力點+platform-admin

凍結清單在 jedi_system_menu/api/__init__.py:166-172 FROZEN_URLS(測試做集合比對,不是比數量)。

宿主呼叫點 core/app_factory.py:388-391:

from infra.system_menu.system_menu_plugin_wiring import build_system_menu_adapters
from jedi_system_menu.plugin import register as register_system_menu
register_system_menu(app, build_system_menu_adapters(container), mount_api=True)

config/app_modules.py:21-26 已把 "system_menu" 註解掉、api/system_menu/ 目錄已刪。

A-3 harness 起來後有哪些端點可打

五支空殼:一條都沒有。 harness 是 harness/dev_app.py + docker-compose.yml(各自佔一個 port 避免互撞:bulletin 5491、system-config 5492、device 5493、information-system 5494、flow-engine 5498), 做的事是「建表 → CRUD 走 model↔︎mapper↔︎entity → 斷言 → 非零退出」,不起 HTTP server 打端點。 harness 檔頭自陳(jedi-bulletin/harness/dev_app.py:21-22):

故本 harness 做到「掛得上 + CRUD 會動」,做不到 jedi-iam 那種「實打一支端點走通」。 如實記錄,不用假端點充數。

flow-engine 的 harness 改成餵最小 BPMN 進 BpmnUtils 走一遍解析並斷言,同樣沒有端點。

system-menu 沒有 harness 目錄(ls jedi-system-menu/ 無 harness/)——它的「拔掉測試」 是在宿主端做的(註解掉 app_factory.py:391 那行 → 五條 URL 全 404)。


§3

B. 肉在哪裡(宿主的 route / service / repo)

B-1 jedi-bulletin

項目 事實
route 掛載 api/bulletin/__init__.py:20-24,3 個 resource(/bulletins、/bulletin、/bulletin/<uid>),5 個 HTTP verb
route 檔 api/bulletin/routes/bulletin_route.py(含 serializer 共 184 行)
app service app/bulletin/service/bulletin_service.py 181 行(app/bulletin/ 全 231 行)
domain domain/bulletin/ 154 行(bulletin_domain_service.py 47、bulletin_org_unit_domain_service.py 15、entity/repo 介面)
infra infra/bulletin/ 268 行(models/bulletin.py 101、bulletin_repo_impl.py 97、bulletin_org_unit_repo_impl.py 36)
DI di_containers/bulletin/bulletin_containers.py 41 行
import 套件的什麼 只有三個「繼承 base」:domain/bulletin/entities/bulletin_entity.py:1 繼承 BaseBulletinEntity、infra/bulletin/models/bulletin.py:4 繼承 BaseBulletin、app/bulletin/dto/bulletin.py:4 繼承 BaseBulletinDTO。零呼叫套件 service(grep jedi_bulletin.app.service = 0 命中)

宿主自己寫的業務:

  1. 公告發給哪些部門——bulletin_org_units 橋表(infra/bulletin/models/bulletin.py:13-27),寫入在 app/bulletin/service/bulletin_service.py:136-145(新增)與 :161-172(更新先刪後建)。
  2. 時間窗驗證——_assert_bulletin_time_window()(bulletin_service.py:158-166),expire_time <= release_time 丟 ErrorCode.BULLETIN_INVALID_TIME_WINDOW。
  3. 部門可見性過濾——auth=True 且使用者有 org_unit_id 時,把清單縮成該部門(bulletin_service.py:76-82)。
  4. 「僅限本人建立」的搜尋不失效寫法——用 owner_created_user 而非 created_user,避免與搜尋關鍵字落進同一 OR 群組讓搜尋框失效(bulletin_service.py:83-90 註解)。
  5. 審計 nickname——建構子注入 jedi_iam 的 UserDomainService(bulletin_service.py:65)。
  6. org_unit uid → id 解析+不存在丟 404(bulletin_service.py:141-144)。

B-2 jedi-device

項目 事實
route 掛載 api/device/__init__.py:22-27,5 個 resource,7 個 verb
route 檔 api/device/routes/device_route.py(模組共 265 行)
app service app/device/service/device_app_service.py 98 行(純 wrapper)
domain / infra 宿主沒有 domain/device/、infra/device/——全在套件
DI di_containers/device/device_containers.py 42 行
import 套件的什麼 呼叫套件 service:device_app_service.py:11 import jedi_device.app.service.device_service.DeviceService,六個 method 逐一委派

宿主自己寫的業務:

  1. 審計欄位 nickname enrich——_fill_user_names()(device_app_service.py:30-54),批次查 jedi_iam UserQueryEntity(_in_login_name=...) 把 login_name 換 nickname。
  2. 刪除前引用計數——get_device_reference_count()(device_app_service.py:89-98),查三張稽核關聯表:compliance.job_execution_devices、compliance.job_execution_device_mapping、compliance.project_job_execution_device_mapping(實作已於 FR-069 搬進 jedi_task_platform/task/infra/repository/device_reference_query.py:26-36,raw SQL 三表 UNION count)。 ⚠️ 套件 README 寫「DeviceReferenceQuery(infra/flow_control/)」,該路徑已過時——現在在 jedi-task-platform 套件內。
  3. 設備不存在丟 404(device_app_service.py:96-97,用套件的 DeviceErrorCode.DEVICE_NOT_FOUND)。

B-3 jedi-information-system

項目 事實
route 掛載 api/information_system/__init__.py:23-26,4 個 resource(不是 README 寫的 6 條——README 說的是 HTTP verb 數,實查 verb 為 6),6 個 verb
route 檔 api/information_system/routes/information_system_route.py(模組共 305 行)
app service app/information_system/service/information_system_app_service.py 24 行(只 wrap 列表一支)
infra infra/information_system/jedi_auth_user_lookup.py 36 行(IUserLookup 的宿主實作)
DI di_containers/information_system/information_system_container.py 40 行
import 套件的什麼 route 直接吃套件 service:information_system_route.py:22 import InformationSystemService,4 個 resource 裡有 3 個直接注入套件 service,只有 InformationSystemListResource(:62)走宿主 wrapper

宿主自己寫的業務:

  1. 列表的審計 nickname enrich——information_system_app_service.py:23 enrich_audit_nickname_objs(...)(唯一的 wrapper 理由)。
  2. IUserLookup 實作——infra/information_system/jedi_auth_user_lookup.py,套件以 dependency inversion 依賴抽象、由宿主注入 jedi_iam 的 UserDomainService(FR-032 G 的先例)。

⚠️ 這支是五支裡最接近可以整組上移的:只差一支 24 行的 nickname wrapper。

B-4 jedi-flow-engine(規模差距最大的一支)

項目 事實
route 掛載 api/flow_engine/__init__.py,19 條 live(add_resource 20 次,其中 :49 是已退役註解掉的 UpdateJobExecutionLinkRoute),26 個 verb
route 檔 6 支(flow_engine_route.py / flow_template_route.py / job_evidence_route.py / stage_advance_route.py / stage_object_route.py / stage_rollback_route.py),api/flow_engine/ 共 1,334 行
app service app/flow_engine/ 3,263 行(workflow_execution_service.py 1,559、stage_advance_service.py 702、flow_template_app_service.py 264、stage_rollback_service.py 189、job_evidence_service.py 181、workflow_template_snapshot_service.py 85)
domain domain/flow_engine/ 875 行(stage_completion_registry.py 186、job_evidence 三層、ext_workflow_execution_*)
infra infra/flow_engine/ 1,109 行(ext_workflow_execution.py 164 是繼承套件 model 的擴充)
DI di_containers/flow_engine/ 430 行(五支 container)
宿主總計 7,011 行(另有 app/flow_control/ 4,070 行也吃 flow-engine 的 enum / model)
import 套件的什麼 三種都有:① 繼承 model(infra/flow_engine/models/ext_workflow_execution.py:4 繼承 BaseWorkflowExecution)② 呼叫套件 service(WorkflowTemplateService / ElementVariableService / JobExecutionService)③ 自己 fork 了一份 WES(見 E-2)

⚠️ 套件 README 說的「6,375 行」已過時——那是 2026-08-31(a1332c8d)當時的數字, 當時 app/flow_engine/ 有 9,183 行含 camunda_service.py 2,852 行與 bpmn_generator.py 2,428 行; 前者已於 CM-1486 退役(561922d4),後者已於 CM-1492 歸建套件(48e7923)。現值 3,263 行。

宿主自己寫的業務(每項一句):

  1. 階段推進 / 回退——stage_advance_service.py(702 行)+ stage_rollback_service.py(189 行), 靠 BPMN UserTask 的 stage_object_code / main_role extension property 識別階段, handler 由 grc 模組啟動時註冊進 stage_completion_registry(domain/flow_engine/service/stage_completion_registry.py,186 行)。
  2. FE 流程編輯器範本管理——flow_template_app_service.py(264 行),builtin 範本=全域共用、 一般租戶唯讀、平台管理員可維護(CM-906);表是 compliance.flow_templates(14 筆),不是套件的 workflow_templates。
  3. 任務證明(job evidence)——job_evidence_service.py(181 行)+ domain / infra 三層。
  4. 專案參與者守門——assert_project_participant()(workflow_execution_service.py:122)走 common.authz.workflow。
  5. 輪次凍結 gate——_assert_round_not_frozen_for_workflow()(:155)。
  6. 問卷關聯——process_survey_property()(:1466),actionType == 'SURVEY' 時把 job 綁 jedi_survey 的 TaskSurvey。
  7. 設備 × 部門任務組合展開——build_task_combinations()(:1497,itertools.product)。
  8. 四類通知——notify_users_batch_assigned / notify_user_todo_job / notify_control_reviewers_on_task_complete(:1265 / :1310 / :1376),走 NotificationService + flask_babel i18n。
  9. 控制項對應——workflow_execution_control_mapping(public.workflow_execution_control_mapping 表 + domain service 32 行 + repo 93 行)。
  10. 擴充的執行體查詢——ExtWorkflowExecution(infra/flow_engine/models/ext_workflow_execution.py,164 行) 對 jedi_participant 的三張參與者表做 relationship / association_proxy / hybrid_property,是套件 model 沒有的。

B-5 jedi-system-config

項目 事實
route 掛載 api/system_config/__init__.py:26-36,6 個 resource,12 個 verb
route 檔 system_config_route.py + security_policy_route.py(模組共 611 行)
app service app/system_config/ 586 行(security_policy_app_service.py 207、tenant_storage_config_seeder.py 118、shared_config_app_service.py 88、guarded_system_config_service.py 85、storage_config_restore_app_service.py 81)
infra infra/system_config/ 489 行(system_config_root_reader.py 321、tenant_config_seed_writer.py 86、runtime_config.py 82)
DI 54 行
import 套件的什麼 route 直接吃套件 service + 宿主 service 並列:system_config_route.py:6 import 套件 SystemConfigService,同一個 handler 同時注入宿主 SharedConfigAppService(:211 / :249 / :268 / :374)。另 guarded_system_config_service.py:21 繼承套件 SystemConfigService 加守門過濾

宿主自己寫的業務:

  1. 共用設定寫入釘死 ROOT——shared_config_app_service.py(88 行):SMTP / LDAP 兩組設定讀取端一直讀 ROOT 那列, 但寫入端是 tenant-scoped,客戶存了不生效且無錯誤訊息;白名單 SHARED_ROOT_CONFIGS 把寫入拉回 ROOT(CM-1283 ③)。
  2. 安全政策 group 粒度讀寫——security_policy_app_service.py(207 行,CM-1423)。
  3. 出廠儲存快照還原——storage_config_restore_app_service.py(81 行,CM-1333)。
  4. 租戶儲存設定 seeding——tenant_storage_config_seeder.py(118 行)+ tenant_config_seed_writer.py(86 行)。
  5. ROOT 設定直讀——system_config_root_reader.py(321 行,繞 tenant scope 的 raw 讀寫)。
  6. 可見性守門——guarded_system_config_service.py:41-42 覆寫 get_system_configs() 過濾。

B-6 jedi-system-menu(對照組)

宿主只剩 80 行:infra/system_menu/system_menu_plugin_wiring.py(58 行,只組 adapters) + di_containers/system_menu/system_menu_containers.py(22 行)。 沒有 api/ / app/ / domain/ 目錄。這就是「肉搬進套件之後」宿主該長的樣子。


§4

C. 套件裡的東西宿主用了嗎(app service method 逐支點名)

C-1 jedi-bulletin BulletinService(7 個 public method)→ 100% 死碼

method 宿主呼叫
get_bulletins_and_pager ❌
get_bulletins ❌
get_bulletin ❌
add_bulletin ❌
update_bulletin ❌
delete_bulletin ❌

證據:grep -rn "jedi_bulletin.app.service" --include='*.py'(排除 .venv)在宿主 0 命中。 宿主用的是同名不同物的 app/bulletin/service/bulletin_service.py。 domain/bulletin/service/bulletin_domain_service.py(宿主,47 行)也是另寫一份, 與套件 jedi_bulletin/domain/service/bulletin_domain_service.py(43 行)方法名幾乎一樣但多了 locale 參數與 get_bulletin_by_uid / get_bulletin_by_id。 → 套件的 app 層 + domain 層 在生產上完全沒被執行,實際活著的只有三支 base class(entity / model / DTO)。

C-2 jedi-device DeviceService(8 個 public method)→ 2/8 死碼(25%)

method 宿主呼叫
get_devices_and_pager ✅ device_app_service.py:58
get_devices ❌(宿主改走 domain service:ssp_import_template_app_service.py:757/855、common/util/system_asset_snapshot.py:171)
get_device ✅ :64 / :95
get_device_by_id ❌ 死碼
add_device ✅ :71
update_device ✅ :77
delete_device ✅ :83
get_device_menu ✅ :87

C-3 jedi-information-system InformationSystemService(8 個 public method)→ 1/8 死碼(12.5%)

method 宿主呼叫
get_information_system_menu ✅ route :40
get_information_systems_and_pager ✅ wrapper information_system_app_service.py:19
get_information_systems ❌(app service 那支沒人叫;宿主用的是 domain service 同名 method:ssp_resources_context_service.py:156、ssp_import_template_app_service.py:783/868/1421、system_asset_snapshot.py:114)
get_information_system ✅ route :121
add_information_system ✅ route :94
update_information_system ✅ route :143
delete_information_system ✅ route :168
upsert_by_name ❌ 死碼

C-4 jedi-flow-engine(4 支 app service)

service 行數 宿主用嗎
WorkflowExecutionService 549 ❌ 整支死碼。grep -rn "jedi_flow_engine.app.service.workflow_execution_service" 宿主 0 命中;宿主用的是 fork 出去的 app/flow_engine/service/workflow_execution_service.py(1,559 行)
WorkflowTemplateService 237 ✅ 5 處 import(di_containers/flow_engine/workflow_excution_containers.py:4、app/module_frame/service/module_frame_service.py:8、module_frame_item_service.py:8、app/flow_engine/service/workflow_template_snapshot_service.py:20、api/flow_engine/routes/flow_engine_route.py:11)。16 個 method 裡 get_workflow_templates 零呼叫
ElementVariableService 84 ✅ di_containers/.../workflow_excution_containers.py:2、api/flow_engine/routes/flow_engine_route.py:10。get_element_variables / add_element_variable / delete_element_variable* 零呼叫
JobExecutionService 50 ✅ DI 有註冊(workflow_excution_containers.py:3 + :114);但 grep 不到 route/service 端的實際呼叫點 —— 疑似只掛在 container 沒人取用(未完全查證,見 §未查證)

app service 死碼比例(按行):549 / 920 ≈ 60%。

⚠️ 這代表 plugin.py:84-98 的 set_repo_providers() 接線(CM-1514 做的 app/infra 解耦) 在生產上不會被執行到——那 22 處 get_repos() 全在死掉的 WES 裡。

C-5 jedi-system-config SystemConfigService(10 個 public method)→ 1/10 死碼(10%)

method 宿主呼叫
get_system_configs ✅(被 guarded_system_config_service.py:42 以 super() 覆寫呼叫)
get_system_config ✅ 6 處
get_system_config_by_key ✅ 6 處
get_system_config_by_group ✅ 4 處
create_system_config ✅
update_system_config ✅
delete_system_config ✅
update_smtp_config ❌ 死碼
update_config_by_group_key ✅ 3 處
delete_config_by_group_key ✅

C-6 jedi-system-menu SystemMenuService(9 個 public method)→ 3/9 死碼(33%),且呼叫者是套件自己的 route

method 呼叫者
get_system_menus ❌ 死碼
get_system_menu ✅ 套件 route api/routes/system_menu_route.py:56
get_system_manus_and_pager ✅ 套件 route :79(method 名有 typo manus,未改)
get_system_all_menu ❌ 死碼
get_system_menu_category ✅ 套件 route :95
get_system_menu_by_id ❌ 死碼
add_system_menu ✅ 套件 route :117
update_system_menu ✅ 套件 route :137
delete_system_menu ✅ 套件 route :148

宿主對這 9 支 0 直接呼叫——這正是「完整體插件」該有的樣子(宿主只給 provider,不碰 service)。


§5

D. 搬進套件會撞到什麼

D-1 搬不進去的「產品知識」逐項

套件 產品知識項 為什麼搬不進去
bulletin ① bulletin_org_units 橋表 → jedi_iam.OrgUnit(infra/bulletin/models/bulletin.py:22 FK 指 OrgUnit.id) 「公告發給哪些部門」是產品規則;套件要吃就得 import jedi-iam
② 審計 nickname enrich(jedi-iam UserDomainService) 全站 API 回傳規範,不是公告知識
③ ErrorCode.BULLETIN_INVALID_TIME_WINDOW / BULLETIN_ORG_UNIT_NOT_FOUND(common/code/error_code.py) 業務規則碼在宿主
④ 部門可見性過濾 + owner_created_user 搜尋不失效寫法 產品的權限視角
可直接搬:時間窗驗證(_parse_bulletin_time / _assert_bulletin_time_window,純函式無外依賴,只差 error code 常數)
device ① DeviceReferenceQuery 查三張稽核表(compliance.job_execution_devices / job_execution_device_mapping / project_job_execution_device_mapping) 稽核任務是主專案疆界,設備套件不該知道它存在
② 審計 nickname enrich 同上
可直接搬:整個 DeviceAppService 的委派骨架(6/8 method 純 pass-through)
information-system ① 列表的審計 nickname enrich(唯一一項,24 行) 待 D8「身分脈絡名冊」port 定案
可直接搬:其餘全部(3/4 resource 已經直接吃套件 service;IUserLookup port 已就位)
flow-engine ① 階段推進/回退 + stage_completion_registry(grc 模組註冊 handler) 稽核輪次語意
② FE 流程編輯器範本(compliance.flow_templates,與套件 workflow_templates 同名不同物) 產品的 BPMN 編輯器資料
③ 專案參與者守門(common.authz.workflow)+輪次凍結 gate 產品授權模型
④ 問卷關聯(jedi-survey)、任務證明、四類通知(jedi-notification + babel i18n) 跨套件業務編排
⑤ ExtWorkflowExecution 對 jedi-participant 三張表的 relationship 參與者模型是另一支套件
⑥ 控制項對應表 workflow_execution_control_mapping 合規控制項語意
可直接搬:BPMN 產生器/驗證器(已搬,CM-1492 48e7923)
system-config ① 共用設定 ROOT 合併規則(SMTP/LDAP 白名單,SHARED_ROOT_CONFIGS) 多租戶產品規則
② 安全政策(207 行)、出廠儲存快照還原(81 行)、租戶 seeding(118+86 行) 產品功能,非「設定表 CRUD」
③ system_config_root_reader.py(321 行繞 tenant scope) RLS 多租戶模型
可直接搬:無明顯可直接搬項(宿主 app/system_config/ 586 行全是產品知識,README 的說法實查成立)
system-menu 已搬完。留在宿主的只有守門注入(JWT / capability / platform-admin,infra/system_menu/system_menu_plugin_wiring.py:49-58)——那正是應該留在宿主的東西

D-2 資料面的產品知識(DB 實查,DEV guidant_ai_dev @ 188:25432)

表 schema 筆數 RLS tenant_id 欄
bulletins public 1 on 有
bulletin_org_units public 1 — —
devices public 3 on 有
information_systems compliance 81 off 有
system_configs public 20 on 有
system_menus public 74 off 無
workflow_executions compliance 11,012 off 有
workflow_templates compliance 17,540 off 有
flow_templates compliance 14 — —

system_menus 的資料本身含產品知識(16 個 group):

DEVICE_TYPE 11 | ssp_party_role 9 | ANS_TYPE 7 | AUDIT_METHOD 6 | TASK_TYPE 5
USER_STATUS 4 | PDF_PARSER 4 | STORAGE_TYPE 4 | FEEDBACK_TYPE 4
PROJECT_SELECT_RULE 4 | BULLETIN_CATEGORY 4 | SYSTEM_TYPE 3 | PERMISSION_OPTION 3
COMPLIANCE_FRAMEWORK_PUBLISH_STATUS 3 | ENABLE_STATUS 2 | AGENT_TYPE 1

AUDIT_METHOD(檢視 / 訪談 / 測試 / 文件檢查 / 電腦設定比對 / 門禁系統設定比對)、 ssp_party_role、COMPLIANCE_FRAMEWORK_PUBLISH_STATUS 全是稽核產品的字典值, seed 在宿主 scripts/init/04-seed-core.sql:138-143 與 scripts/sql/2026-06-20-audit-method-menu.sql。 套件搬走了 route 與 service,資料(seed)仍留宿主——這是 system-menu 現況的疆界形狀, 也是「合併成 jedi-system-dict」時要注意的:表結構可以是套件的,字典內容不可能是。

⚠️ system_menus 無 tenant_id、無 RLS、(group,key) 全域唯一——套件 plugin 的三個守門 刻意無預設值就是為了這件事(jedi-system-menu/.../plugin.py:220-224:缺接線直接拒絕掛載, 不是靜默跳過;預設放行=任何人都能改到全站 UI 下拉選項,服務照常起、健康檢查照樣綠燈)。

D-3 搬家時要凍結的契約(宿主 route URL + FE 常數)

bulletin(api/bulletin/__init__.py:20-24) | URL | FE 常數(compliance-manager-fe/src/config/api/api.js) | FE 引用數 | |---|---|---| | GET/POST /api/1.0/bulletins | BULLETINS(:88) | 1 | | POST /api/1.0/bulletin | BULLETIN(:89) | 5 | | GET/PUT/DELETE /api/1.0/bulletin/<uid> | BULLETIN + uid | (同上) |

device(api/device/__init__.py:22-27) | URL | FE 常數 | FE 引用數 | |---|---|---| | POST /api/1.0/devices | DEVICES(:327) | 3 | | GET /api/1.0/devices/menu | DEVICES_MENU(:328) | 2 | | POST /api/1.0/device | DEVICE(:329) | 4 | | GET/PUT/DELETE /api/1.0/device/<uid> | DEVICE + uid | (同上) | | GET /api/1.0/device/<uid>/references | DEVICE_REFERENCES(:330,函式型) | 1 |

information-system(api/information_system/__init__.py:23-26) | URL | FE 常數 | FE 引用數 | |---|---|---| | GET /api/1.0/information-systems/menu | INFORMATION_SYSTEMS_MENU(:197) | 4 | | POST /api/1.0/information-systems/list | INFORMATION_SYSTEMS_LIST(:198) | 1 | | POST /api/1.0/information-systems | INFORMATION_SYSTEMS(:199) | 1 | | GET/PUT/DELETE /api/1.0/information-system/<uid> | INFORMATION_SYSTEM(:200) | 3 |

system-config(api/system_config/__init__.py:26-36) | URL | FE 常數 | FE 引用數 | |---|---|---| | POST /api/1.0/system/config | SYSTEM_CONFIG(:334) | 16 | | GET/PUT/DELETE /api/1.0/system/config/<uid> | SYSTEM_CONFIG + uid | (同上) | | GET /api/1.0/system/configs/<group> | SYSTEM_CONFIGS(:335) | 1 | | GET/POST /api/1.0/system/config/storage/restore-default | FE api.js 無對應常數(未查證是否有硬編碼) | — | | GET/PUT /api/1.0/system/security-policy | SECURITY_POLICY(:449) | 2 | | GET/PUT /api/1.0/system/config/<group>/<key> | SYSTEM_CONFIG 拼接 | (同上) |

⚠️ api/system_config/__init__.py:29-36 有兩處路徑順序陷阱(註解已寫明): storage/restore-default 與 security-policy 都必須避開 <group>/<key> 動態規則, 搬家時順序不可調換。

flow-engine(19 條 live,api/flow_engine/__init__.py:38-93) | URL | FE 常數 | |---|---| | /flow-engine/process/comments/<id> | PROCESS_COMMENTS(:101) | | /flow-engine/task/complete/<id> | TASK_COMPLETE(:102) | | /flow-engine/task/revert/<id> | TASK_REVERT(:103) | | /flow-engine/process-definition/<uid> | PROCESS_DEFINITION(:100)← 唯一吃套件 service 的那條 | | /flow-engine/task/queue | TASK_QUEUE(:104) | | /job-evidences、/job-evidence、/job-evidence/<uid> | JOB_EVIDENCES(:106)/ JOB_EVIDENCE(:105) | | /flow-engine/stage-objects | FLOW_ENGINE_STAGE_OBJECTS(:109) | | /flow-engine/flow-templates(+ /validate、/<uid>、/<uid>/duplicate、/<uid>/publish、/<uid>/unpublish,共 6 條) | FLOW_ENGINE_FLOW_TEMPLATES(:110) | | /project/<pid>/audit-round/<rid>/stage/{info,advance,rollback,transitions}(4 條) | STAGE_API(:113) |

⚠️ /flow-engine/flow-templates/validate 必須註冊在 /<uid> 之前(__init__.py:68-70 註解), 否則 validate 會被 uid converter 吃掉。

system-menu(已在套件,FROZEN_URLS 5 條) | URL | FE 常數 | FE 引用數 | |---|---|---| | /system/menu/<group> | SYSTEM_MENU(:25) | 16 | | /system-menu(POST) | SYSTEM_MENU_OBJECT(:26) | 4 | | /system/menus | SYSTEM_MENUS(:27) | 1 | | /system/category-menu | SYSTEM_MENU_CATEGORY(:28) | 1 | | /system-menu/<int:id>(PUT/DELETE) | SYSTEM_MENU_OBJECT + id | (同上) |


§6

E. 歷史(補殼那次 commit 與「為什麼沒把 route 搬進來」原話)

E-1 補殼 commit 一覽(monorepo git log)

套件 commit 日期 標題
jedi-flow-engine 738f13d 2026-08-31 feat(CM-1470): FR-069 P3.4 jedi-flow-engine 輕量升級——register 四插槽+harness+README+依賴衛生
jedi-bulletin f996740 2026-08-31 refactor(CM-1471): FR-069 P3.5 輕量批補殼 ①/7——jedi-bulletin
system-config / device / information-system 104df8c 2026-08-31 refactor(CM-1471): FR-069 P3.5 輕量批補殼 ②-④/7
jedi-system-menu f59be9c 2026-08-31 refactor(CM-1471): FR-069 P3.5 補殼 ⑤/7——jedi-system-menu 升格完整體插件(route 上移)
(後續) e467466 2026-09-01 fix(CM-1497): D6 契約統一——七支插件 mount_api=False 也寫 runtime context
(後續) 3502e3e 2026-09-02 refactor(CM-1514): jedi-flow-engine 架構 18 紅歸零——…app/infra 解耦

E-2 「為什麼當時沒把 route 搬進來」——原話逐支抄錄

jedi-flow-engine(738f13d commit message,同文亦在 jedi-flow-engine/README.md:173-186 與 plugin.py:20-45):

原卡(P3.4)要求照 3.1 jedi-iam 範本把主專案 api/flow_engine/ 的 route 上移進本 套件。實查後證明做不到,已停手回報、決策者 2026-08-31 裁示降級((a)+(c)):

  • 主專案 api/flow_engine/ 共 19 條 live route,只有 1 條 (/flow-engine/process-definition/<uid>)吃本套件的 service;
  • 其餘 18 條吃主專案 app/flow_engine/(6,375 行)與 app/flow_control/ ——流程範本管理、階段推進/回退、任務證明。搬進來=套件反向 import 主專案。
  • ⚠️ 系統內有兩套同名不同物的「流程範本」:主專案 flow_templates(FE 編輯器 畫的 BPMN,6 條 API)vs 本套件 workflow_templates(引擎執行期快照,1 條)。 原卡指的是前者,它整組不在套件裡。

route 歸屬已排入第四階段「流程疆界」設計(flow-engine/flow_control/project 三方同席,與 P12/P13 同場)。

plugin.py:37-43 另補一句紅字:

在那之前,不要因為「順手」把主專案的 service 用 provider 名冊接進來——那不是 補殼,是把 flow_control 的疆界問題偷渡進本套件。

jedi-bulletin(README.md:26-36、plugin.py:23-38):

主專案 api/bulletin/ 的 5 條 route 吃的是主專案的 app/bulletin/service/bulletin_service.py(181 行),不是本套件的 BulletinService。 那支主專案 service 的相依是產品知識、不是公告知識:

  • BulletinOrgUnitDomainService——公告↔︎部門對象關聯(domain/bulletin/ 在主專案)
  • jedi_iam 的 UserDomainService / OrgUnitDomainService——審計欄位 nickname、部門樹
  • 主專案 common.code.ErrorCode——BULLETIN_INVALID_TIME_WINDOW 等業務規則碼

把它們搬進來等於套件反向 import 主專案(正是抽套件要消滅的東西);把它們宣告成套件的 provider 名冊,則是把「公告要發給哪些部門」這個產品規則偷渡進通用套件。

jedi-device(README.md:26-40、plugin.py:23-41):

主專案 api/device/ 的 4 條 route 吃的是主專案的 app/device/service/device_app_service.py(98 行),不是本套件的 DeviceService。 那支 wrapper 的存在理由正是產品規則:

  • 審計欄位 nickname enrich……這是本產品的 API 回傳規範……不是設備知識。
  • DeviceReferenceQuery……刪除前查「這台設備被幾個稽核任務引用」。稽核任務是主專案的疆界,設備套件不該知道它存在。

⚠️ DeviceReferenceRoute 這種「被誰引用」的查詢天生跨疆界(要問稽核那邊), 故留宿主是正確的分工,不是遷就。

jedi-information-system(README.md:26-37、plugin.py:23-36):

主專案 api/information_system/ 的 6 條 route 是混合的:

  • 4 條(menu / create / detail get・put・delete)直接吃本套件的 InformationSystemService ✅
  • 1 條(InformationSystemListResource)吃主專案的 InformationSystemAppService…… 它做的事是審計欄位 nickname enrich……那是本產品的 API 回傳規範,不是資訊系統知識。

只搬 4 條、留 1 條在主專案,會讓同一個資源的端點散在兩個 blueprint 裡——URL 前綴 相同、真相卻有兩處,比現狀更難維護。故整組留宿主,等 D8「身分脈絡名冊」定案 (把 nickname enrich 變成套件可用的 port)之後再整組上移。

jedi-system-config(README.md:26-42、plugin.py:23-38):

主專案 api/system_config/ 的 route 分兩群,都不能上移:

  • SystemConfigRoute / SystemConfigsRoute / SystemConfigGroupRoute(混合): 同一個 handler 同時吃本套件的 SystemConfigService 與主專案的 SharedConfigAppService(app/system_config/,88 行)——後者處理「共用設定 (ROOT tenant 那份)怎麼與租戶自己的那份合併」,那是多租戶的產品規則。
  • StorageConfigRestoreRoute / SecurityPolicyRoute(純主專案): 分別吃 StorageConfigRestoreAppService(81 行)與 SecurityPolicyAppService(207 行) ——出廠儲存快照與安全政策,兩者都是產品功能,不是「設定表 CRUD」。

主專案 app/system_config/ 合計 585 行,全部是產品知識。

jedi-system-menu(README.md:12-20,反面說明為什麼它搬得動):

P3.5 補殼七支裡,只有三支(本支/jedi-issue/jedi-log)做了 route 上移, 判準是service 在哪:

本套件 同批的 bulletin/device/system-config/information-system
service 全在套件內 在主專案(wrapper app service,含產品規則)
route 已上移進套件 ✅ 留宿主(搬進來=套件反向 import 主專案)
D6 五件套 全做到(含拔掉測試) 只做到三項(無 api 層=無拔掉測試可驗)

四支空殼共用的「驗收邊界誠實聲明」表(例 jedi-bulletin/README.md:13-21):

項目 狀態
register 四插槽 ✅
harness ✅(建表+CRUD 斷言+register 掛載;但沒有端點可打)
接入 README ✅
自帶 api 層 ❌ 未做
拔掉測試 ❌ 做不到——沒有 route 就沒有「拔掉註冊行 → API 消失」可驗。沒有拿假端點充數。

E-3 flow-engine 特別調查:宿主 6,375 行 vs 套件 workflow_execution_service.py 的關係

結論:宿主是 2025-07-14 從套件 fork 出去的,之後兩邊各自演化 14 個月,套件那支從此在生產上零使用。

證據鏈:

  1. fork 時間點——套件那支的第一版出現在 c57b16b(2025-07-13,「更新Table 名稱 process_definition → workflow_template…」); 宿主那支的第一次出現是 65fedaa7(2025-07-14,「flow-engine refactor V1.4」,+525 行)。間隔一天。

  2. fork 時兩檔幾乎逐字相同——把兩個歷史版本抓出來對比:

    • 套件 c57b16b 版:485 行
    • 宿主 65fedaa7 版:524 行
    • diff 輸出僅 63 行,且差異全部集中在四個點:
      • +import json
      • +from jedi_flow_engine.domain.entity.hi_workflow_template_query_Entity import HistoryWorkflowTemplateQueryEntity
      • +from app.task_survey.service.task_survey_service import task_survey_service ← 這就是 fork 的動機
      • 一段 35 行的新邏輯:把 job properties 的 devices 拆成 element variable + actionType == 'SURVEY' 時呼叫 task_survey_service.link_task_survey(...)

    也就是說:fork 的直接原因是「要在建 job 時關聯問卷」,而 task_survey_service 在主專案。 (套件後來為此開了 on_job_created callback hook,見 jedi-flow-engine/.../workflow_execution_service.py:31-37 ——但宿主沒有回頭改用那個 hook,fork 已經走遠。)

  3. 演化落差(現況):

    套件 jedi_flow_engine/app/service/workflow_execution_service.py 宿主 app/flow_engine/service/workflow_execution_service.py
    行數 549 1,559
    method 數 13 29
    建構子 (workflow_template_service, on_job_created=None) 15+ 個注入參數(di_containers/flow_engine/workflow_excution_containers.py:128-150)
    repo 取得方式 get_repos()(CM-1514 的 wiring 縫隙,22 處) 建構子注入的 domain service
    宿主獨有 method — assert_project_participant、_assert_round_not_frozen_for_workflow、complete_main_workflow_job、add_job_comment、get_ext_main_workflow_executions_and_pager、notify_*×3、process_survey_property、build_task_combinations、get_ext_workflow_execution_by_uid…
    共同 method 也已分歧 complete_job(…, user_nickname) complete_job(…, user_nickname, suppress_notify=False)
    revert_job(…, user_nickname) revert_job(…, suppress_notify, bypass_round_frozen_gate, known_project_id)
    update_job_comment 同名但宿主另有 add_job_comment
    start_workflow_execution(…) start_workflow_execution(…, locale=None)
  4. 套件那支現在有沒有人用?零。 grep -rn "jedi_flow_engine.app.service.workflow_execution_service" --include='*.py'(排除 .venv)在宿主 0 命中; 在套件內部除了自己與 tests 也沒有 caller。 → 套件的 WorkflowExecutionService(549 行)是純死碼, 而 CM-1514(3502e3e)為它做的 app/infra 解耦(app/wiring/ + plugin 的 set_repo_providers) 是對死碼做的架構修正——測試會綠,生產不走這條路徑。

  5. 另一半是真的在用的:WorkflowTemplateService(237 行)被宿主 5 處 import、 ElementVariableService(84 行)被 2 處 import——它們沒被 fork,宿主直接吃套件版。 也就是說 flow-engine 不是「整包沒被用」,而是四支 app service 裡最大的那支被 fork 掉了。


§7

F. 六支總表

套件 套件行數(<mod>/,不含 tests/harness) 宿主對應模組行數(api+app+domain+infra+di) 宿主 route 數(resource / verb) 套件 route 數 宿主 import 套件的形式 套件 app service 死碼比例 搬進去的產品知識阻礙
jedi-bulletin 647(plugin 226) 878 3 / 5 0 繼承 model + entity + DTO 三支 base class,零呼叫套件 service **6/6 method = 100%(整個 app + domain 層死碼) 4 項**(org_units 橋表指 jedi-iam / 審計 nickname / 業務 error code / 部門可見性+搜尋寫法)
jedi-device 770(plugin 229) 412 5 / 7 0 呼叫套件 service(wrapper 委派 6 支) **2/8 = 25%(get_devices、get_device_by_id) 2 項**(三張稽核關聯表引用計數 / 審計 nickname)
jedi-information-system 1,146(plugin 230) 405 4 / 6 0 route 直接吃套件 service(3/4 resource)+ 1 支 24 行 wrapper;另有宿主實作的 IUserLookup port **1/8 = 12.5%(upsert_by_name;get_information_systems 有 domain 層同名替代) 1 項**(列表的審計 nickname enrich,24 行)
jedi-flow-engine 6,728(plugin 261) 7,011(另 app/flow_control/ 4,070 也吃它) 19 / 26(其中僅 1 條吃套件 service) 0 三種都有:繼承 model(ExtWorkflowExecution)+呼叫套件 service(Template/ElementVariable)+fork 了 WES **549/920 ≈ 60%(WorkflowExecutionService 整支死碼;JobExecutionService 疑似也是) 6 項**(階段推進/回退+registry / FE 流程範本(同名不同物)/ 參與者守門+輪次凍結 / 問卷+通知+證明 / participant 三表 relationship / 控制項對應)
jedi-system-config 921(plugin 226) 1,740 6 / 12 0 route 直接吃套件 service + 宿主 service 並列;另 GuardedSystemConfigService 繼承套件 service **1/10 = 10%(update_smtp_config) 3 項**(ROOT 共用設定合併 / 安全政策+出廠還原+租戶 seeding / RLS 繞行 root reader 321 行)
jedi-system-menu(對照組,不是空殼) 1,199(plugin 304,另有 api/ 層) 80(只剩 wiring 58 + DI 22) 0(已刪 api/system_menu/) 5(FROZEN_URLS) **register(mount_api=True)(core/app_factory.py:391),宿主只給 provider 與三道守門 3/9 = 33%(get_system_menus、get_system_all_menu、get_system_menu_by_id;有用的 6 支全由套件自己的 route 呼叫) 0 項程式碼**,但資料是產品知識:74 筆字典含 AUDIT_METHOD(6) / ssp_party_role(9) / COMPLIANCE_FRAMEWORK_PUBLISH_STATUS(3),seed 仍在宿主 scripts/init/04-seed-core.sql

F-補充:一個容易誤讀的比例

「套件行數 vs 宿主行數」不是「該不該併」的直接指標,因為兩邊的內容性質不同:

  • bulletin:套件 647 行幾乎不執行,宿主 878 行是全部真相 → 套件是影子。
  • device / information-system:套件是真核心(770 / 1,146 行都在跑),宿主 412 / 405 行是薄殼 → 這兩支離完整體最近。
  • system-config:套件 921 行在跑,宿主 1,740 行也在跑,兩邊都是真肉 → 疆界確實混。
  • flow-engine:套件 6,728 行裡 549 行是死的、bpmn_generator.py 2,428 行是 CM-1492 才搬進去的真資產;宿主 7,011 行是產品流程 → 兩邊都大,且有一塊重複。

§8

未查證事項(誠實列出)

  1. jedi_flow_engine.app.service.JobExecutionService 是否真的零使用——它在 DI container 有註冊(di_containers/flow_engine/workflow_excution_containers.py:111-114),但我 grep 不到取用 job_execution_service provider 的呼叫端。可能有 Provide[...] 的間接取法我漏掉。若確認零使用,flow-engine 死碼比例會從 60% 升到 65%。
  2. /system/config/storage/restore-default 的 FE 呼叫端——compliance-manager-fe/src/config/api/api.js 沒有對應常數,未查證是否有頁面硬編碼字串或走 SYSTEM_CONFIG 拼接。搬家時這條契約沒有 FE 常數當錨點。
  3. STG / POC 的 system_menus 內容是否與 DEV 一致——本次只查 DEV(guidant_ai_dev)。若三環境字典內容有落差,「搬字典」的成本會比看起來高。
  4. 各套件 tests/ 內是否還有第二個 service 使用者——我只查了 <mod>/ 本體與宿主,套件自己的 test 也算「有人用」但不算生產使用;死碼比例是以生產呼叫為準統計的。
  5. jedi-bulletin 宿主 domain service 與套件 domain service 的差異是否只有 locale 與兩支 by_uid/by_id——我比對了方法簽名,沒有逐行 diff 內部實作。若內部邏輯也已分歧,就是第二個 fork(性質同 flow-engine WES)。
  6. information-system 的 get_information_systems(app 層)與 domain 層同名 method 是否語意等價——宿主全部走 domain 層那支,app 層那支標為死碼;若兩者行為不同(例如 app 層多做 DTO 轉換或 user_lookup enrich),這個「死碼」標籤可能是「該用而沒用」而非「不需要」。