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

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

## 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。

---

## A. 殼的現況（`create_blueprint()` 掛幾條 route／宿主呼叫 register 幾次）

### A-1 五支空殼：`create_blueprint()` 掛 **0** 條，宿主 **零呼叫**

五支的 `create_blueprint()` 實作**逐字相同**（只有型別名不同）：

```python
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`：

```python
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）。

---

## 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/` 目錄**。這就是「肉搬進套件之後」宿主該長的樣子。

---

## 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）。

---

## 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 | （同上） |

---

## 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 掉了**。

---

## 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 行是產品流程 → **兩邊都大，且有一塊重複**。

---

## 未查證事項（誠實列出）

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），這個「死碼」標籤可能是「該用而沒用」而非「不需要」。
