盤點日期 2026-09-09。宿主 HEAD 92412948(branch feature/FR-075),套件 monorepo HEAD 為當下工作區。 所有行號皆為當下實查,未讀 .venv。
六支裡只有五支是「有殼無肉」,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。
create_blueprint() 掛幾條 route/宿主呼叫 register 幾次)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 bpjedi-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),這條接線在生產上也沒有任何效果。
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/ 目錄已刪。
五支空殼:一條都沒有。 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)。
| 項目 | 事實 |
|---|---|
| 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 命中) |
宿主自己寫的業務:
bulletin_org_units 橋表(infra/bulletin/models/bulletin.py:13-27),寫入在 app/bulletin/service/bulletin_service.py:136-145(新增)與 :161-172(更新先刪後建)。_assert_bulletin_time_window()(bulletin_service.py:158-166),expire_time <= release_time 丟 ErrorCode.BULLETIN_INVALID_TIME_WINDOW。auth=True 且使用者有 org_unit_id 時,把清單縮成該部門(bulletin_service.py:76-82)。owner_created_user 而非 created_user,避免與搜尋關鍵字落進同一 OR 群組讓搜尋框失效(bulletin_service.py:83-90 註解)。jedi_iam 的 UserDomainService(bulletin_service.py:65)。bulletin_service.py:141-144)。| 項目 | 事實 |
|---|---|
| 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 逐一委派 |
宿主自己寫的業務:
_fill_user_names()(device_app_service.py:30-54),批次查 jedi_iam UserQueryEntity(_in_login_name=...) 把 login_name 換 nickname。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 套件內。device_app_service.py:96-97,用套件的 DeviceErrorCode.DEVICE_NOT_FOUND)。| 項目 | 事實 |
|---|---|
| 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 |
宿主自己寫的業務:
information_system_app_service.py:23 enrich_audit_nickname_objs(...)(唯一的 wrapper 理由)。IUserLookup 實作——infra/information_system/jedi_auth_user_lookup.py,套件以 dependency inversion 依賴抽象、由宿主注入 jedi_iam 的 UserDomainService(FR-032 G 的先例)。⚠️ 這支是五支裡最接近可以整組上移的:只差一支 24 行的 nickname wrapper。
| 項目 | 事實 |
|---|---|
| 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 行。
宿主自己寫的業務(每項一句):
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 行)。flow_template_app_service.py(264 行),builtin 範本=全域共用、 一般租戶唯讀、平台管理員可維護(CM-906);表是 compliance.flow_templates(14 筆),不是套件的 workflow_templates。job_evidence_service.py(181 行)+ domain / infra 三層。assert_project_participant()(workflow_execution_service.py:122)走 common.authz.workflow。_assert_round_not_frozen_for_workflow()(:155)。process_survey_property()(:1466),actionType == 'SURVEY' 時把 job 綁 jedi_survey 的 TaskSurvey。build_task_combinations()(:1497,itertools.product)。notify_users_batch_assigned / notify_user_todo_job / notify_control_reviewers_on_task_complete(:1265 / :1310 / :1376),走 NotificationService + flask_babel i18n。workflow_execution_control_mapping(public.workflow_execution_control_mapping 表 + domain service 32 行 + repo 93 行)。ExtWorkflowExecution(infra/flow_engine/models/ext_workflow_execution.py,164 行) 對 jedi_participant 的三張參與者表做 relationship / association_proxy / hybrid_property,是套件 model 沒有的。| 項目 | 事實 |
|---|---|
| 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 加守門過濾 |
宿主自己寫的業務:
shared_config_app_service.py(88 行):SMTP / LDAP 兩組設定讀取端一直讀 ROOT 那列, 但寫入端是 tenant-scoped,客戶存了不生效且無錯誤訊息;白名單 SHARED_ROOT_CONFIGS 把寫入拉回 ROOT(CM-1283 ③)。security_policy_app_service.py(207 行,CM-1423)。storage_config_restore_app_service.py(81 行,CM-1333)。tenant_storage_config_seeder.py(118 行)+ tenant_config_seed_writer.py(86 行)。system_config_root_reader.py(321 行,繞 tenant scope 的 raw 讀寫)。guarded_system_config_service.py:41-42 覆寫 get_system_configs() 過濾。宿主只剩 80 行:infra/system_menu/system_menu_plugin_wiring.py(58 行,只組 adapters) + di_containers/system_menu/system_menu_containers.py(22 行)。 沒有 api/ / app/ / domain/ 目錄。這就是「肉搬進套件之後」宿主該長的樣子。
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)。
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 |
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 |
❌ 死碼 |
| 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 裡。
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 |
✅ |
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)。
| 套件 | 產品知識項 | 為什麼搬不進去 |
|---|---|---|
| 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)——那正是應該留在宿主的東西 |
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 下拉選項,服務照常起、健康檢查照樣綠燈)。
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 | (同上) |
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 解耦 |
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 消失」可驗。沒有拿假端點充數。 |
workflow_execution_service.py 的關係結論:宿主是 2025-07-14 從套件 fork 出去的,之後兩邊各自演化 14 個月,套件那支從此在生產上零使用。
證據鏈:
fork 時間點——套件那支的第一版出現在 c57b16b(2025-07-13,「更新Table 名稱 process_definition → workflow_template…」); 宿主那支的第一次出現是 65fedaa7(2025-07-14,「flow-engine refactor V1.4」,+525 行)。間隔一天。
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 的動機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 已經走遠。)
演化落差(現況):
套件 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) |
套件那支現在有沒有人用?零。 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) 是對死碼做的架構修正——測試會綠,生產不走這條路徑。
另一半是真的在用的:WorkflowTemplateService(237 行)被宿主 5 處 import、 ElementVariableService(84 行)被 2 處 import——它們沒被 fork,宿主直接吃套件版。 也就是說 flow-engine 不是「整包沒被用」,而是四支 app service 裡最大的那支被 fork 掉了。
| 套件 | 套件行數(<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 |
「套件行數 vs 宿主行數」不是「該不該併」的直接指標,因為兩邊的內容性質不同:
bpmn_generator.py 2,428 行是 CM-1492 才搬進去的真資產;宿主 7,011 行是產品流程 → 兩邊都大,且有一塊重複。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%。/system/config/storage/restore-default 的 FE 呼叫端——compliance-manager-fe/src/config/api/api.js 沒有對應常數,未查證是否有頁面硬編碼字串或走 SYSTEM_CONFIG 拼接。搬家時這條契約沒有 FE 常數當錨點。system_menus 內容是否與 DEV 一致——本次只查 DEV(guidant_ai_dev)。若三環境字典內容有落差,「搬字典」的成本會比看起來高。tests/ 內是否還有第二個 service 使用者——我只查了 <mod>/ 本體與宿主,套件自己的 test 也算「有人用」但不算生產使用;死碼比例是以生產呼叫為準統計的。locale 與兩支 by_uid/by_id——我比對了方法簽名,沒有逐行 diff 內部實作。若內部邏輯也已分歧,就是第二個 fork(性質同 flow-engine WES)。information-system 的 get_information_systems(app 層)與 domain 層同名 method 是否語意等價——宿主全部走 domain 層那支,app 層那支標為死碼;若兩者行為不同(例如 app 層多做 DTO 轉換或 user_lookup enrich),這個「死碼」標籤可能是「該用而沒用」而非「不需要」。