# FR-080 批次 C 盤點回報（task-platform / participant / flow-engine / survey / compliance-audit）

> 唯讀盤點。證據路徑相對 monorepo 根 `~/Projects/Jedicogy/module/jedi-python-package/`
> 或主專案根 `~/Projects/Billows/Audit-Manager/compliance-manager-be/`（標「主專案」）。
> DB 事實查自 DEV `192.168.50.188:25432 / guidant_ai_dev`（唯讀 SELECT）。

---

## jedi-task-platform

**Q1 它是什麼**

「一個專案裡有哪些任務、任務被誰留言、綁了什麼東西、進度怎麼樣」——專案主檔 CRUD ＋ 通用任務層（留言／任務綁設備組織問卷／匯入匯出／批次完成／執行進度／儀表板聚合），業務插件（稽核、問卷、檢測）插在它上面。

README 與程式碼**大致一致，但有兩處不符**：

1. README `jedi-task-platform/README.md:35` 宣稱機械判準是「平台層 grep 不到 `poam` / `audit_round` / `assessment_plan` / `ssp`」，且說掃全部內容不只 import。**實際 grep 到兩處 `ssp`**：`jedi_task_platform/infra/model/project.py:85-88`（`living_ssp_id` 欄位＋ comment 明寫 `oscal.system_security_plans.id`）與 `jedi_task_platform/domain/entity/project_query_entity.py:18`。守衛測試只掃 `jedi_task_platform/task/` 子樹（`tests/test_task_plugin_contract.py`），**`jedi_task_platform/`（project 那半）不在守衛範圍內**——所以 README 說的「平台層」其實只指 `task/` 子目錄，project 主檔那半帶著 OSCAL 錨點欄位而守衛掃不到。
2. `task/` 子樹本身也**不是零產品詞**：`task/app/service/task_execution_service.py` 有 17 處 `detection`（`detection_orchestration_service`、`_auto_dispatch_detection_scans`、`get_detection_jobs_without_execution`、回傳鍵 `detection_dispatched_count`），守衛的黑名單只含 poam/audit_round/assessment_plan/ssp，`detection` 不在其中。

**Q2 資料**

自己擁有的表（5 張）：

| 表 | 存什麼 | 關鍵欄位 |
|---|---|---|
| `compliance.projects` | 專案主檔（`infra/model/project.py:16`） | `uid` / `name` / `status` / `module_frame_id` / **`living_ssp_id`** / `owner_id` / `deleted_at` |
| `compliance.job_execution_comments` | 任務留言（`task/infra/model/job_execution_comment.py:32`） | `job_execution_id`（軟參照）/ `author_id`（FK users）/ `author_nickname` 快照 / `content` |
| `compliance.job_execution_devices` | 任務↔設備綁定（`job_execution_device.py:29`） | `job_execution_id`（軟參照）/ `device_id`（FK devices CASCADE） |
| `compliance.job_execution_org_units` | 任務↔組織綁定（`job_execution_org_unit.py:29`） | `job_execution_id`（軟參照）/ `org_unit_id`（FK org_units CASCADE） |
| `compliance.job_execution_surveys` | 任務↔問卷綁定（`job_execution_survey.py:29`） | `job_execution_id`（軟參照）/ `survey_id`（FK `survey.surveys` CASCADE） |

**`projects` 表有 `living_ssp_id`**（產品欄位）：`infra/model/project.py:85-89`，型別 BigInteger，comment 逐字寫「專案 living SSP（oscal.system_security_plans.id, soft-ref）— 專案唯一 OSCAL 錨點」。DEV 實查 `compliance.projects` 確有此欄（`information_schema.columns` 回：`module_frame_id / living_ssp_id / owner_id / deleted_at` 四欄都在主表）。另外 `compliance.project_extensions` 表在 DEV **已不存在**（`to_regclass` 回空），四欄已收回主表（CM-1449/CM-1474）。同檔 line 29 還有 `Index("ix_projects_living_ssp_id", ...)` ——不只是欄位，還為 SSP 反查建了索引。

對別人的表的參照：

(a) **真 FK**（DEV `pg_constraint` 實查確認，非只是 ORM 宣告）：
- `job_execution_comments.author_id` → `public.users`（ON DELETE SET NULL）
- `job_execution_devices.device_id` → `public.devices`（CASCADE）
- `job_execution_org_units.org_unit_id` → `public.org_units`（CASCADE）
- `job_execution_surveys.survey_id` → `survey.surveys`（CASCADE）

(b) **ORM relationship 跨包**：**目前 0 條**。四張綁定表的 relationship 全被拔（CM-1493 拔 survey/device、CM-1512 拔 users/org_units），檔內留紅字禁止加回（`job_execution_comment.py:84-92`、`job_execution_org_unit.py:55-62`）。

(c) **軟參照**：四張綁定表的 `job_execution_id` → `compliance.job_executions.id`（flow-engine 的表）。DEV 實查四張表確實**只有 1 條 FK 各自**（devices/org_units/users/surveys 那條），**指向 job_executions 的 FK 已不存在**——CM-1490 的 migration 確實生效。另 `projects.living_ssp_id` → `oscal.system_security_plans.id` 亦為軟參照（DEV 實查 `compliance.projects` 零 FK 出邊）。

(d) **raw SQL 直打別人的表**：無（套件內 `text(` 零處；危險的跨 schema SQL 留在主專案，經 `ITaskExecutionQuery` port 回問）。

表名產品詞彙：表名本身乾淨（projects / job_execution_*），**欄位名不乾淨**（`living_ssp_id`）。

**Q3 Port**

需要宿主提供（`task/domain/ports.py`，5 張，全部 ABC）：

| Port | 簽名 | 用在哪 |
|---|---|---|
| `ILicenseGuard` (line 21) | `require(resource_type) -> Callable` / `viewer_licensed_modules() -> Optional[frozenset]` | route decorator、任務型別過濾 |
| `IProjectRoleGuard` (line 40) | `assert_role(participant_domain_service, project_id, user_id, allowed_roles, error_code)` / `assert_role_fallback(role_service, project_id, allowed_roles, group_id, control_id, error_code)` | `task/common/guard.py` |
| `IIdentityGuard` (line 62) | `viewer_is_super_admin() -> bool` | 超管判定 |
| `ITaskExecutionQuery` (line 70) | 4 支：`get_assigned_todo_prep_jobs` / `activate_jobs` / `get_detection_jobs_without_execution` / `get_prep_job_progress` | `task/app/service/task_execution_service.py:34` |
| `ITaskExistenceQuery` (line 102) | `get_workflow_execution_id(job_uid)` / `get_internal_id(job_uid)` | `task/app/service/job_comment_service.py:18`、`task/infra/repository/flow_control_job_comment_repo_impl.py:35` |

提供給別人的入口：`jedi_task_platform.task.plugin.{TaskAdapters, TaskServices, attach, register}`；`jedi_task_platform.domain.service.project_domain_service.ProjectDomainService`；`jedi_task_platform.infra.model.project.Project`（ORM，被 compliance-audit 直接 import 6 處）；`jedi_task_platform.task.domain.task_type_registry.{TaskTypeSpec, register_job_type}`（申報制入口，survey 用）。

宣告但零使用的 port：**無**（五張都有消費端）。

**Q4 消費者**

(a) 主專案（`from jedi_task_platform` 共 **104 處**，含 test）——集中在：`app/flow_control/service`（15）、`infra/flow_control/repository`（14）、`di_containers/flow_control/flow_control_containers.py`（7）、`app/project/service`（5）、`infra|domain|app/associations/*`（16）、`infra/readmodel/audit`（3）、`di_containers/project|associations`（6）。import 內容以 `infra.model.project.Project`、`domain.service.project_domain_service`、`task.plugin`、`task.domain.code.flow_control_job_type_enum` 為主。

(b) 其他 jedi 套件：
- **jedi-compliance-audit → task-platform：13 處**（6 處 import ORM `Project`：`infra/repository/flow_control_control_group_repo_impl.py:20`、`flow_control_task_setup_repo_impl.py:17`、`flow_control_control_repo_impl.py:23`、`project_extension_repo_impl.py:4`、`infra/mapper/flow_control_project_mapper.py:2`、`infra/mapper/project_extension_mapper.py:2`；7 處 import service/entity/enum）
- **jedi-participant → task-platform：2 處**（`app/service/task_assignee_service.py:10`、`app/service/project_participant_service.py:6`，皆 `ProjectDomainService`）
- **jedi-survey → task-platform：1 處**（`app/task_type_declaration.py:35`，`TaskTypeSpec`；且有守衛測試 `jedi-survey/tests/unittest/test_plugin_contract.py:119` 焊死「核心路徑對 task_platform import 必須 0，唯一白名單是這支」）

**注意方向**：task-platform 反向 import jedi-participant **3 處**（見 Q8）。

**Q5 執行期形狀**

- route **10 條**，掛在宿主建的 blueprint `grc_projects` / prefix `/api/1.0/grc`（`task/api/__init__.py:39-40, 70-79`）：`/job-executions/<uid>/comments`、`/job/comment/<uid>`、`/dashboard/summary`、`/project/<uid>/ap/<ap_uid>/jobs/{export,import,import/validate,import/confirm}`、`/project/<uid>/jobs/batch-complete`、`/project/<uid>/start-task-execution`、`/project/<uid>/task-execution/progress`。
- 背景排程／worker／長駐 thread：**無**（套件內 `threading.Thread` / scheduler / APScheduler 零處）。註：README 提到的「每天 03:10 UTC 孤兒清理」實作在**主專案**（`core/scheduler.py:374-431` ＋ `app/flow_control/service/job_binding_orphan_cleanup_service.py:64`），不在套件內。
- 對外 I/O：只有 `flask.send_file`（`task/api/routes/job_import_route.py:9,37`）。無 SMTP / HTTP / S3 / subprocess / socket。
- 快取／Redis：無（`os.getenv` / `os.environ` 全套件 0 處）。
- 單獨起 process：技術上可（route 齊、無背景工），但需要宿主注入 5 張 port 且需要 flow-engine 的 `job_executions` 表在同一個庫（軟參照跨表讀）。

**Q6 通用性**

(a) 不能直接用。(b) 寫死的 Guidant 產品知識：
- `infra/model/project.py:85-89` `living_ssp_id` 欄位 ＋ `:29` 對它建索引（OSCAL 概念）
- `task/app/service/task_execution_service.py:43,51,102-129` `detection_orchestration_service` / `_auto_dispatch_detection_scans`（檢測工具業務）
- `task/api/__init__.py:73-76` URL 路徑硬帶 `/ap/<ap_uid>`（AP＝assessment plan，稽核詞；檔頭 line 16-20 自承「路徑是 D5 凍結的對外契約，平台層當不透明識別子」）
- `task/domain/code/flow_control_job_type_enum.py:41-42` 枚舉成員 `SURVEY` / `DETECTION_TOOL` 硬編碼（檔頭自承是歷史落庫值，新型別走申報制）
- 錯誤碼前綴 `GRC_`（`task/common/error_code.py:41-51`，5 個成員，值凍結為 FE i18n key）
- `common/code/status_code.py:5` `ProjectStatus` docstring 寫「稽核專案執行狀態」
- class 名 `FlowControlJobType` / `FlowControlJobCommentRepoImpl`（flow_control ＝ 分家前的稽核套件名）

**Q7 插件完整度**

| 項 | 有/無 | 證據 |
|---|---|---|
| ① api 層隨包 | ✅ 10 條 | `task/api/__init__.py:54-79` |
| ② `register(app, adapters, config, schema_extensions, mount_api)` ＋ `attach(bp, adapters)` | ✅ | `task/plugin.py`；簽名有測試焊死 `tests/test_task_plugin_contract.py:45-49` |
| ③ migrations 隨包 | ❌ 0 支 | `find` 無 `migrations/` |
| ④ DI container 預設 | ❌ | 套件內 0 個 container 檔（刻意：存 provider 名冊由宿主注入） |
| ⑤ 獨立 harness | ✅ | `harness/task_dev_app.py`（README:89-94 宣稱實打 `/dashboard/summary` 回 401） |
| ⑥ 接入 README | ✅ | `README.md:56-94` |

**Q8 糾纏對象**

**與 jedi-participant（最強）**：
- **反向 import 3 處**：`task/infra/repository/flow_control_job_comment_repo_impl.py:18`（`from jedi_participant.common import user_directory`——留言者身分批次換人）、`task/app/service/task_execution_service.py:18`（`ProjectParticipantDomainService`）、`task/common/guard.py`（`assert_project_role` 委派）。**這是「平台包 import 業務／人員包」的方向**，與 `pyproject.toml:50` 宣告的 `jedi-participant>=0.0.1` 一致，且 README:119 只說「已不再依賴任何業務套件」——participant 被歸類為非業務。
- **正向：participant → task-platform 2 處**（`ProjectDomainService`）。**兩包互相 import**（雙向循環依賴，pyproject 也是雙向宣告：`jedi-task-platform/pyproject.toml:50` 依賴 participant，`jedi-participant/pyproject.toml:31` 依賴 task-platform）。
- 資料層：participant 六張表的 `project_id` **DEV 實查 100% 對得上 `compliance.projects.id`**（`project_participants` 752 列中 716 列 join 得到 projects，0 列 join 得到 workflow_executions；`task_assignees` 12479 列**全部**對得上 projects），亦即「participant 的表在語意上是 projects 的子表，只是沒建 FK」。
- 同一交易寫兩邊：`task_execution_service._check_manager()`（`task/app/service/task_execution_service.py:53-60`）在同一 `@transaction` 內先查 task-platform 的 `Project`、再查 participant 的 `ProjectParticipant` 判角色——**每一支寫入端點都走這條**。

**與 jedi-flow-engine（次強，但已用 port 隔開）**：四張綁定表的 `job_execution_id` 全指向 flow-engine 的 `compliance.job_executions`，原本是 CASCADE FK（CM-1490 拆成軟參照）。README:121-126 自承「四張綁定表全部對 flow-engine 的 job_executions 建了 CASCADE 外鍵——依 D17 律①『想建 FK ＝ 該合併疆界』，這是合體的最強理由」。拆 FK 後責任轉給應用層：`FlowControlJobRepoImpl.delete_job()` 顯式刪四張表 ＋ 主專案的每日孤兒清理排程。**程式碼層 import 已歸零**（`ITaskExistenceQuery` port，`task/domain/ports.py:102-147`）。

**與 jedi-compliance-audit**：**單向、方向正確**（CA → TP 13 處，TP → CA 0 處，三處守衛焊死）。但 CA 有 6 處直接 import TP 的 **ORM model `Project`** 而非走 service——那是「稽核插件的 repo 直接查平台的表」，`project_extension_repo_impl.py:14-43` 甚至用 `Project` 當自己的 `BaseRepositoryImpl` model 並 **UPDATE `compliance.projects`**（`add()` 與 `update_owner()`，line 90-125）——**稽核插件在寫平台的主檔表**。

**看起來像但其實不是一件事**：
- **jedi-survey**：只有申報書一支 import（`TaskTypeSpec`），且 survey 側有守衛測試證明核心路徑零依賴。TP 側 `job_execution_surveys` 只留 `survey_id` 軟參照。這條是「任務可以綁問卷」的最鬆耦合形態。
- **jedi-device / jedi-iam**：綁定表對 `devices` / `org_units` / `users` 有真 FK，但那是「宿主的身分／資產表」不是套件疆界；program 層 import 已零（CM-1512）。

---

## jedi-participant

**Q1 它是什麼**

「誰被指派到哪一層」——專案／控制群組／控制項／任務四層的人員指派紀錄與角色（manager/reviewer/auditor/viewer）繼承。

README 與程式碼**有一處明顯過時**：`harness/dev_app.py:11` 仍寫「README §5 已知限制①：六支 ORM model 直接 relationship 到 `jedi_iam` 的…」，但實際六支 model 的 jedi_iam relationship **已全部拔除**（CM-1484，見 `infra/model/*.py` 各檔的紅字註解），套件對 `jedi_iam` 的 import **實查 0 處**。反之 `pyproject.toml:26-31` 誠實記載仍待償的兩條（flow-engine 的 `JobExecution`、task-platform 的 `ProjectDomainService`），與程式碼相符。

**Q2 資料**

自己擁有的表（6 張，全在 `compliance` schema，migration `jedi_participant/migrations/001-participant-tables.sql`）：

| 表 | 存什麼 | 關鍵欄位（皆為複合主鍵） |
|---|---|---|
| `project_participants` | 專案層參與者 | `(project_id, user_id)` ＋ `role` |
| `process_participants` | 流程層參與者 | `(project_id, process_id, user_id)` ＋ `role` |
| `project_group_participants` | 專案×群組 | `(project_id, group_id, user_id)` ＋ `role` |
| `project_control_participants` | 專案×群組×控制項 | `(project_id, group_id, control_id, user_id)` ＋ `role` |
| `control_group_participants` | 控制項群組 | `(project_id, group_id, user_id)` ＋ `role` |
| `task_assignees` | 任務指派 | `(project_id, control_id, task_id, user_id)` ＋ `task_uid` / `task_template_id` / `is_approver` / `role` |

對別人的表的參照：

(a) **真 FK：六張表全部 0 條**。DEV `pg_constraint` 實查逐表確認 `fk_count = 0`（六張全 0）。migration 檔頭 line 16-19 亦明記「本套件的表沒有 RLS、也沒有 tenant_id / org_unit_id」。

→ **回答派工單的問題：participant 六張表「全部沒有」FK 掛 task-platform 的表**（不是「全部有」）。它們的 `project_id` 在**資料上**確實指 `compliance.projects.id`（見下），但**約束層是空的**。

(b) **ORM relationship 跨包：1 條**——`infra/model/project_participant.py:6,23-30`：
```
from jedi_flow_engine.infra.models.workflow_execution import WorkflowExecution
project = relationship("WorkflowExecution", primaryjoin=WorkflowExecution.id == ProjectParticipant.project_id, viewonly=True)
```
這條 relationship 把 `project_id` 對到 **flow-engine 的 `workflow_executions.id`**。**DEV 實查證明這條是壞的**：752 列 `project_participants` 中，join `compliance.projects` 得 716 列、join `compliance.workflow_executions` 得 **0 列**（`projects.id` 值域 40–306，`workflow_executions.id` 值域 1558–13258，兩者完全不重疊）。全 codebase（套件 ＋ 主專案）grep `.project.uid` / participant 的 `.project` 讀取端 **0 處**——這是一條**沒有讀者的死 relationship，且語意上指錯表**。

(c) **軟參照**：`project_id` → `compliance.projects.id`（真實對應，見上）、`user_id` → `public.users.id`、`group_id` / `control_id` → 稽核插件的控制項概念（無實體表對應）、`task_assignees.task_id` → `compliance.job_executions.id`（DEV 實查 12479 列中 12470 列 join 得到）。

(d) **raw SQL**：套件內 0 處。

表名產品詞彙：`control_group_participants` / `project_control_participants` 的 `control` 是稽核控制項詞彙（NIST/CMMC control），`task_assignees.task_template_id` 亦然。程式層另有兩處洩漏：`app/service/project_participant_service.py:35,44,126`（`project_assessment_plan_mapping_service` 建構子參數＋ `_append_user_to_ssp_metadata_parties` 方法，該方法 line 131-132 是 **FR-038 2A 留下的 `return` 空殼 stub**）、`common/participant_enum.py:7` 註解提及 `common/authz/ssp.py`。

**Q3 Port**

`domain/ports.py` 宣告 **6 張**（全部 `Protocol` + `runtime_checkable`）：

| Port | 簽名 | 用在哪 | 缺線行為 |
|---|---|---|---|
| `IUserDirectory` (line 33) | `get_user(uid)` / `get_users_by_ids(ids)` / `get_org_units_by_ids(ids)` / `get_users_by_login_names(names)` | `common/user_directory.py` 模組級 configure；六支 service 建構子 | 拒絕掛載（plugin `_assert_wiring`） |
| `IProjectDirectory` (line 84) | `get_by_uid(project_uid) -> Any` | **零使用** | 文件宣稱「拒絕掛載」 |
| `IProjectRoleGuard` (line 91) | `assert_manager(role_service, project_id, group_id, control_id)` | `common/guard.py:27` | 拒絕掛載 |
| `IAuditLogger` (line 112) | `emit(*, event_code, message, **extra)` | 未以此名注入（實際走 `ParticipantConfig.audit_event_codes` dict） | 降級不記錄 ＋ WARNING |
| `IJobNotifier` (line 119) | `notify_user_todo_job(job_execution_id, assignees)` | `app/service/task_assignee_service.py:34`（參數名叫 `workflow_execution_service`） | 降級不通知 |
| `IJobEnrichment` (line 126) | `count_attachment_evidence_by_job_ids` / `get_completed_survey_job_ids` | `task_assignee_service.py:40-41`（參數名 `job_evidence_domain_service` / `task_survey_domain_service`） | 降級回 0/False |

**宣告了但套件內零使用的 port：`IProjectDirectory`（樣板殘留）**。全 monorepo ＋ 主專案 grep `IProjectDirectory` **只有 `domain/ports.py:21`（文件表格）與 `:84`（宣告）兩處**，`plugin.py` 的 `_assert_wiring`（line 176-186）只檢查 `auth_required` / `project_role_guard` / `user_directory` **三項**，不含 `IProjectDirectory`；`ParticipantAdapters`（line 118-122）根本**沒有 `project_directory` 欄位**。文件表格說「不注入拒絕掛載」是不成立的。

→ **回答派工單的問題**：participant → task-platform 的兩處 import 是 `app/service/task_assignee_service.py:10` 與 `app/service/project_participant_service.py:6`，兩處都 import `ProjectDomainService`（具體類別，非 port）。**兩處都繞過了已定義的 `IProjectDirectory` port**——port 定義得好好的（`get_by_uid`），而 `project_participant_service.py:188` 的 `self.project_domain_service.get_by_uid(project_uid)` 正好就是那張 port 的簽名，卻直接 import 具體套件的 domain service。`task_assignee_service.py:36` 的 `project_domain_service: ProjectDomainService = None` 更只是型別註記（DI 注入、預設 None），拔 import 改鴨子型別即可——與 task-platform 對 `jedi_iam.UserDomainService` 的處置（CM-1512）同形，但這裡沒做。

**另一個宿主接線缺口（DEV 實況）**：主專案 `api/participant/__init__.py:75-90` 的 `_UserDirectoryAdapter` **只轉發 2 支方法**（`get_user` / `get_users_by_ids`），而底層 `infra/participant/user_directory_adapter.py:43-93` 有 **4 支**（另有 `get_org_units_by_ids` / `get_users_by_login_names`，CM-1512 加的）。套件側 `common/user_directory.py:80-87,107-114` 對「adapter 未實作該方法」的處置是 `getattr(...) is None → 回空 dict ＋ WARNING`。消費端 jedi-survey `infra/repository/identity_enrichment.py:62,129` 正好呼叫這兩支——**問卷的部門名與審計 nickname 走的是降級路徑**（唯一接線點只有 `api/participant/__init__.py:116` 一處，無其他 configure）。未在 log 中驗證是否真的觸發（見「未查證事項」）。

提供給別人的入口：`jedi_participant.plugin.{register, create_blueprint, ParticipantAdapters, ParticipantConfig, ParticipantServices}`、`jedi_participant.common.guard.assert_project_manager`（**canonical，survey 與 CA 都在用**）、`jedi_participant.common.user_directory`（模組級名冊，**canonical**，TP／survey／CA 都在用）、`jedi_participant.common.participant_enum.ParticipantRole`、`domain.entity.task_assignee_query_entity.TaskAssigneeQueryEntity`、`domain.service.*_domain_service`、`app.dto.*`、`api.serializers.*`。

**Q4 消費者**

(a) 主專案（**98 處**）：`di_containers/flow_engine/project_participant_containers.py`（18）、`infra/flow_control/repository`（9）、`app/flow_control/service`（8）、`app/flow_engine/service|dto`（12）、`infra/readmodel/audit`（5）、`infra/flow_engine/{repository,models,mapper}`（10）、`app/associations/service`（4）、`api/flow_engine/serializers`（3）、`api/participant/__init__.py`（1，插件接線）等。

(b) 其他套件：**jedi-compliance-audit → 8 處**（`app/dto/{control_group,control,project}_dto.py` 借 `ParticipantMenuDTO` / `ProjectParticipantDTO`、`api/serializers/{task_setup,project}.py` 借 serializer、`app/service/review_service.py:11` 借 `ParticipantRole`、`planning_readiness_checker.py:29` 借 query entity、`infra/repository/flow_control_task_setup_repo_impl.py:130` 借 `user_directory`）；**jedi-survey → 8 處**；**jedi-task-platform → 3 處**（見上）。

**方向注意**：participant 被三支套件依賴，自己又依賴 flow-engine ＋ task-platform，**與 task-platform 形成雙向循環**。

**Q5 執行期形狀**

- route **10 條 live**（`api/__init__.py:80-116`，另有 **9 條註解掉的死路徑**，FR-038 DEAD-V1），blueprint 名 `participant` / prefix `/api/1.0`：`/project-participants`、`/project-participants/menu`、`/project-participant`、`/task-assignees`、`/task-assignees/batch`、`/task-assignee`、`/project-control-participants`、`/project-control-participants/menu`、`/project-control-participant`、`/control-group-participant`。
- 背景排程／worker：**無**。
- 對外 I/O：**無**（無 SMTP/HTTP/S3/subprocess/socket/檔案；通知走 `IJobNotifier` port 交給宿主）。
- Redis／快取：**無**；`os.getenv` 0 處。
- 單獨起 process：需要 flow-engine 的 `JobExecutionService` 與 task-platform 的 `ProjectDomainService`（兩者都是**具體類別 import**，不是 port），所以**目前不行**。

**Q6 通用性**

(a) 不能直接用（要先裝 flow-engine ＋ task-platform）。(b) 寫死的產品知識：
- `common/participant_enum.py:13-18` `ParticipantRole` 四角（manager/reviewer/auditor/viewer）——**auditor 是稽核詞**，且 docstring 寫「系統送『待稽核判定』給他（Phase 3）」
- 表名 `control_group_participants` / `project_control_participants`（control ＝ 合規控制項）
- `app/service/project_participant_service.py:125-132` `_append_user_to_ssp_metadata_parties()`（OSCAL SSP parties 概念；FR-038 2A 後成 `return` 空殼，但方法與呼叫點 line 116-121 都還在，外包了一層 `savepoint()` ＋ `try/except` 保護一個什麼都不做的函式）
- `common/error_code.py` 八個碼中四個帶 `GRC_` 前綴（值凍結為 FE i18n key）
- `common/event_code.py` 申報 `TASK_ASSIGNED` / `TASK_REASSIGNED` / `TASK_UNASSIGNED` 三個事件名，數值由宿主給（`api/participant/__init__.py:99-107`）——這條設計是乾淨的

**Q7 插件完整度**

| 項 | 有/無 | 證據 |
|---|---|---|
| ① api 層隨包 | ✅ 10 條 live | `api/__init__.py:39-116` |
| ② `register(app, adapters, config, schema_extensions, mount_api)` ＋ `create_blueprint()` | ✅ | `plugin.py:238-261` / `202-235` |
| ③ migrations 隨包 | ✅ **1 支**（六張表建表，全冪等） | `jedi_participant/migrations/001-participant-tables.sql`；`plugin.iter_migrations()` (line 44) 只提供不執行 |
| ④ DI container 預設 | ❌ 0 個 | 走 provider 名冊 |
| ⑤ 獨立 harness | ✅ | `harness/dev_app.py`（但檔頭 line 11 描述已過時） |
| ⑥ 接入 README | ✅ | `README.md`（177 行） |

**Q8 糾纏對象**

**與 jedi-task-platform（最強）**：
- **雙向 import**：participant → TP 2 處（`ProjectDomainService`，繞過自家 `IProjectDirectory` port）；TP → participant 3 處（`user_directory`、`ProjectParticipantDomainService`、guard 委派）。pyproject 亦雙向宣告依賴。
- **資料上是子表**：DEV 實查六張表的 `project_id` 100% 指向 `compliance.projects.id`（`task_assignees` 12479/12479），只是**約束層留白**（0 FK）。
- **同一交易寫兩邊**：`task_assignee_service.add_task_assignee()`（`app/service/task_assignee_service.py:145-178`）一個 `@transaction` 內：查 flow-engine 的 `job_execution` → 寫 participant 的 `task_assignees` → 呼叫 `notify_user_todo_job`；而 TP 的 `task_execution_service._check_manager()` 一個 `@transaction` 內先查 TP `Project` 再查 participant `ProjectParticipant`。**兩邊互相在對方的交易裡出現。**
- **guard 是 canonical 跨包共用**：`common/guard.py` 的 `assert_project_manager` 被 survey（`app/common/task_survey_guard.py:24`）與 CA（間接）使用，而它的實作由宿主 `common/authz/project.py` 注入——三包共用一支宿主守門。

**與 jedi-flow-engine**：1 條 ORM relationship（死的、指錯表，見 Q2b）＋ `task_assignee_service.py:8-9,88,110,154,212,242` 六處呼叫 `JobExecutionService.get_job_execution()`（每次派工／查詢都要先問「這個 task_uid 是哪個 job」）。資料上 `task_assignees.task_id` 對得上 `job_executions.id`（12470/12479）但無 FK。

**看起來像但其實不是一件事**：
- **jedi-survey**：survey 借 participant 的 `TaskAssigneeQueryEntity` ＋ guard 做守門判定（8 處），是「問卷要知道誰被指派」的**單向讀取**；participant 對 survey 的認識只有 `IJobEnrichment` port 的一支方法（`get_completed_survey_job_ids`，未注入即降級）。
- **jedi-iam**：程式層 import **已歸零**（CM-1484），六張表無 users FK，身分一律經 `IUserDirectory`。是本批中疆界隔離做得最徹底的一條。

---

## jedi-flow-engine

> 決策者已裁：**flow-engine 不併入 task-platform**。以下只擺事實。

**Q1 它是什麼**

BPMN 2.0 流程引擎——解析 BPMN XML、建流程執行實例（含主／子流程樹）、推進流程節點、記錄任務執行與元素變數。

README 與程式碼**一致且異常誠實**：`README.md:9-22` 開宗明義「本套件目前**沒有 api 層**，`register()` 會拿到一個空的 blueprint，這是刻意的」；`:173-185` 的「驗收邊界（誠實聲明）」表格逐項標 ✅/❌，明寫「拔掉測試 ❌ 做不到——沒有 route 就沒有可驗，**不用假端點充數**」。實查全部屬實。唯一小落差：README:142 說 models 有「9 張表」、`:117` 說「本套件 5 張表全在 compliance schema」——**現況是 5 張**（`infra/models/__init__.py:19-31` 列 5 個，另四張 `hi_*` 歷程表已於 CM-1499 退役，`README.md:120-122` 有記載但 142 行的「9 張」沒改）。

**Q2 資料**

自己擁有的表（5 張，全在 `compliance`）：

| 表 | 存什麼 | 關鍵欄位 |
|---|---|---|
| `workflow_templates` | BPMN 範本快照（版本／父子樹／JSON）（`infra/models/workflow_template.py:12`） | `parent_template_id`（自 FK CASCADE）/ `template_id` / `tenant_id` / `org_unit_id` |
| `workflow_templates_trans` | 範本翻譯（`workflow_template_trans.py:11`） | `workflow_template_id`（FK CASCADE）/ locale |
| `workflow_executions` | 流程執行實例，支援主／子流程巢狀（`workflow_execution.py:19`） | `main_workflow_execution_id` / `parent_id`（皆自 FK）/ `template_id`（FK）/ `status` |
| `job_executions` | 工作節點執行紀錄（`job_execution.py:16`） | `main_workflow_execution_id` / `workflow_execution_id`（皆 FK）/ `template_id`（FK）/ `type` / `status` |
| `element_variables` | 流程／工作的變數值（`element_variable.py:12`） | `workflow_id`（FK CASCADE）/ `element_id` |

對別人的表的參照：

(a) **真 FK 出邊（跨疆界）**：DEV 實查 `workflow_executions` / `job_executions` / `workflow_templates` 三張各有 `tenant_id → tenants` 與 `org_unit_id → org_units` 兩條 FK（來自 `TenantScopedMixinModel`，屬宿主身分／租戶表）。其餘 FK 全在套件內部。

(b) **ORM relationship 跨包：0 條**。

(c) 軟參照：無（套件不存別人的 id）。

(d) raw SQL 直打別人的表：無。

**別人指向它的 FK（DEV 實查，這是耦合的核心事實）**：
- `compliance.job_evidences.{job_execution_id, workflow_execution_id, main_workflow_execution_id}` → job_executions / workflow_executions（**主專案的表**）
- `compliance.job_execution_device_mapping.{job_execution_id, workflow_execution_id}`、`compliance.project_job_execution_device_mapping.job_execution_id`（**主專案 associations**）
- `public.workflow_execution_control_mapping.workflow_execution_id`（**主專案**）
- ⚠️ task-platform 的四張綁定表**已無 FK 指向 job_executions**（CM-1490 拆成軟參照，DEV 已驗證）

表名產品詞彙：**乾淨**（workflow_/job_/element_，全通用流程詞）。全套件產品詞 grep 只中 1 處，且是英文技術用語誤中（`common/utils/bpmn_topology_validator.py:39` 註解 `Reverse detection`）。

**Q3 Port**

`plugin.py` 宣告 2 張 `Protocol`：`ISettingsReader.get(key, default)`（line 114-121）、`INotifier.send(*, to, subject, body)`（line 124-133）。**兩張皆零使用**，且 docstring 自己明說「本套件目前零使用」「插槽開齊是為了日後簽名不用改，不是現在有東西被 port 化了」（`README.md:87-104` 同）。這是**誠實標註的樣板殘留**，不是隱藏債。

提供給別人的入口：`jedi_flow_engine.infra.models.{JobExecution, WorkflowExecution, WorkflowTemplate, ElementVariable}`（ORM，**被跨包直接 import**）、`common.enum.job_code.{JobType, JobStatus}` / `workflow_code.WorkflowStatus`、`common.utils.bpmn_uilts.BpmnUtils`（BPMN 解析核心）、`common.utils.bpmn_topology_validator`、`domain.entity.*QueryEntity`、`domain.service.*DomainService`、`app.service.{workflow_template_service, workflow_execution_service, job_execution_service}`、`app.dto.*`、`plugin.{register, create_blueprint}`。

**Q4 消費者（誰呼叫它）**

(a) 主專案（**122 處**，本批最高）：`app/flow_engine/service`（30，含 `workflow_execution_service.py` 一支就 17 條 import）、`infra/flow_control/repository`（16）、`di_containers/flow_engine/workflow_excution_containers.py`（11）、`app/module_frame/service`（9）、`app/flow_control/service`（6）、`app/cloud_integration/service`（6）、`api/flow_engine/routes`（4）、`infra/flow_engine/models`（4）、`infra/readmodel/tasks`（3）、`app/oscal/service`（2）等。

⚠️ **`jedi_flow_engine.plugin` 在主專案 0 處被呼叫**（grep 全 codebase 無 `jedi_flow_engine.plugin` / `from jedi_flow_engine import plugin`）——主專案是把它當純 library 用，**`register()` 從未被執行**。連帶事實：`plugin.py:84-97` 的 `set_repo_providers(...)` 是 **import 期副作用**，而 `app/service/workflow_execution_service.py:25` 的 `get_repos()` 依賴它；主專案既然從不 import `plugin`，就**從不會呼叫套件自己的 `WorkflowExecutionService`**——實查確認主專案 0 處 import `jedi_flow_engine.app.service.workflow_execution_service`（用的是主專案自己那支 6,375 行的同名 service）。套件的 `WorkflowExecutionService` 在本產品是**死碼路徑**。

(b) 其他套件：
- **jedi-compliance-audit → 10 處**：`infra/repository/{flow_control_control_repo_impl.py:21-22, flow_control_control_group_repo_impl.py:17-18, flow_control_task_setup_repo_impl.py:14-16, oscal_audit_query.py:12}` import **ORM `JobExecution` / `WorkflowTemplate`** ＋ `JobType/JobStatus` enum；`app/service/planning_readiness_checker.py:24-25` 用 `BpmnUtils` ＋ query entity
- **jedi-detection → 5 處**：`app/service/detection_result_handler.py:30-31`、`detection_orchestration_service.py:34-36`（`JobStatus` / `ErrorCode` / `JobExecutionQueryEntity`）
- **jedi-participant → 3 處**：`infra/model/project_participant.py:6`（死 relationship）、`app/service/task_assignee_service.py:8-9`
- **jedi-survey → 2 處**：`infra/models/task_survey.py:2-3`（ORM relationship，見 survey 段）
- **jedi-task-platform → 0 處**（CM-1492 已用 `ITaskExistenceQuery` port 償清）

→ **回答派工單的問題「誰呼叫它」**：主專案 `app/flow_engine/` 與 `app/flow_control/` 是最大宗（宿主自己的 flow_control 那 6,375 行）；task-platform **不再**呼叫（已 port 化）；survey 只有 ORM relationship 不呼叫 service；另外 **jedi-detection 也是消費者**（5 處），這支不在本批但屬同一張依賴圖。

**`workflow_executions` 表的 ORM model 在套件與主專案各有一個嗎**：**是，但不是兩個 class 定義同一張表——是繼承 ＋ `extend_existing`**。
- 套件：`jedi_flow_engine/infra/models/workflow_execution.py:18-20` `class WorkflowExecution(BaseModel, TenantScopedMixinModel)`，`__tablename__ = "workflow_executions"`, schema `compliance`
- 主專案：`infra/flow_engine/models/ext_workflow_execution.py:18-20` `class ExtWorkflowExecution(BaseWorkflowExecution)`（**繼承套件那支**），`__tablename__ = "workflow_executions"`, `__table_args__ = {"extend_existing": True, "schema": "compliance"}`
- Ext 這支**重新宣告了套件已有的四個 relationship**（`main_workflow_execution` / `parent` / `sub_workflows` / `variables`，line 22-61，覆寫 lazy 策略），**新增三個跨包 relationship 到 participant 的表**（line 125-163：`project_participants` / `process_participants` / `task_assignees`，全 `viewonly=True` ＋ `foreign()` 手動 primaryjoin）＋ 兩個 hybrid_property 聚合（`sub_workflows_count` / `sub_workflows_completion_percentage`，line 64-123）。
- 效果：主專案是靠**繼承 ＋ extend_existing 在宿主側把 participant 的表縫回 workflow_executions**，這是「宿主用 ORM 補回被拆掉的跨包 relationship」的實例。

**Q5 執行期形狀**

- route：**0 條**（`plugin.create_blueprint()` line 207-229 建的 blueprint 沒有任何 `add_resource`，且有測試焊死 `tests/unittest/test_plugin_contract.py`）。→ **派工單問「附錄說 0 條」——確認為真。**
- 背景 worker／長駐 thread／排程：**無**。
- 對外 I/O：**無 SMTP / HTTP / S3 / socket**；有 XML 解析（`common/utils/bpmn_generator.py` 用 `defusedxml.ElementTree.parse`，`pyproject.toml:30` 有宣告）與 `xmltodict`。套件內另有三份 `.bpmn` 範例檔（`common/utils/{camunda8測試,網路設備風險評估,資安健診}.bpmn`）——**產品用的 BPMN 範本檔案跟著套件走**。
- Redis／快取：**無**；`os.getenv` / `os.environ` **0 處**（README:96 宣稱，實查屬實）。
- 單獨起 process：**技術上不行且沒意義**——0 條 route、`register()` 從未被呼叫、真正的流程推進邏輯（6,375 行）在主專案。它現況是純 library。

**Q6 通用性**

(a) **接近可以直接用**——本批中通用性最高的一支。(b) 硬編碼的 Guidant 產品知識：**幾乎沒有**。
- 產品詞 grep 全套件 1 中（誤中，見 Q2）
- 唯二的宿主前提（`README.md:108-118`）是技術性的：`ENABLE_MULTI_TENANT=true` 必須在 import model 前設好；`tenants` / `org_units` 兩張表要在 SQLAlchemy metadata 裡（`TenantScopedMixinModel` 的 FK 在 mapper 設定期就要解析）——這是 jedi-common 的租戶 mixin 帶來的，不是 flow-engine 自己的產品知識
- 錯誤碼 `common/enum/error_code.py` 9 個成員，前綴非 `GRC_`
- 三份 `.bpmn` 範例檔帶產品情境（資安健診／網路設備風險評估），但只是檔案不是邏輯

**Q7 插件完整度**

| 項 | 有/無 | 證據 |
|---|---|---|
| ① api 層隨包 | ❌ **0 條**（刻意，有測試焊死） | `plugin.py:37-43`、`README.md:9-22, 183` |
| ② `register(app, adapters, config, schema_extensions, mount_api)` | ✅ | `plugin.py:232-261` |
| ③ migrations 隨包 | ❌ **0 支**（README:163-169 明說「本套件不隨包 migration，九張表的建表腳本在主專案 `scripts/sql/`」） | — |
| ④ DI container 預設 | ❌ 0 個（有 `app/wiring/` 的 provider 注入縫隙，由 plugin.py import 期接線） | `app/wiring/__init__.py:43-60` |
| ⑤ 獨立 harness | ✅ **本批唯一有 docker-compose 的** | `harness/dev_app.py` ＋ `harness/docker-compose.yml`（port 5498） |
| ⑥ 接入 README | ✅ | `README.md`（185 行） |

**Q8 糾纏對象**

**與 jedi-task-platform**：曾經是四條 CASCADE FK（TP 的四張綁定表 → `job_executions`），CM-1490 拆成軟參照、CM-1492 拆掉最後兩條 import。**現況：程式碼 0 依賴、DB 0 FK，只剩「四張表的 `job_execution_id` 欄位在語意上指這裡」＋ 主專案要跑每日孤兒清理排程來補 DB 不再保證的完整性**。TP 的 README:121-126 仍留著「這是合體的最強理由」那段（決策者已裁不併）。

**與主專案 `app/flow_engine/` ＋ `app/flow_control/`（最強，但對象是宿主不是套件）**：19 條 route 中只有 1 條吃套件的 service（`api/flow_engine/__init__.py:41` `/flow-engine/process-definition/<uid>`），其餘 18 條吃主專案 6,375 行的 `app/flow_engine/`。主專案的 `ExtWorkflowExecution` 用繼承 ＋ `extend_existing` 覆寫並擴充套件的 ORM model。**「引擎在套件、驅動引擎的業務在宿主」是這支最大的分裂**。

**與 jedi-compliance-audit**：CA 的 4 支 repo 直接 `session.query(JobExecution)`（`flow_control_control_repo_impl.py:22` 等）與 raw SQL `JOIN compliance.job_executions`（`flow_control_task_setup_repo_impl.py` 實查）——**稽核插件直接查流程引擎的表**，沒有 port。

**看起來像但其實不是一件事**：
- **jedi-survey**：只有 `infra/models/task_survey.py:2-3` 兩條 ORM relationship（`main_project` / `process` → `WorkflowExecution`、`task` → `JobExecution`，全 viewonly），無 service 呼叫、無 FK（DEV 實查 `survey.task_surveys` 只有 pkey ＋ uid unique，**零 FK**）。
- **jedi-participant**：那條 `ProjectParticipant.project → WorkflowExecution` relationship 是**死的且指錯表**（見 participant Q2b），實際耦合只有 `JobExecutionService.get_job_execution()` 六處呼叫。

---

## jedi-survey

**Q1 它是什麼**

「設計一份問卷、發給人填、記錄他填了什麼與改過什麼」——資料夾／問卷／頁／題／顯示條件／版本快照（設計層）＋ 任務問卷／答案／答案歷史與還原／作答狀態機／多人即時共編（作答層）。

README 與程式碼**一致**，且 `README.md:136-172` 的「誠實聲明（待償項）」逐條屬實：7.1 說「只能裝在有 jedi-iam / jedi-participant / jedi-flow-engine / jedi-device 的宿主上」——實查四支 import 都在；7.2 說「沒有 harness」——實查 `jedi-survey/harness/` 目錄不存在。

**Q2 資料**

自己擁有的表（**14 張**，全在 `survey` schema）：

| 表 | 存什麼 | 關鍵欄位 |
|---|---|---|
| `surveys` | 問卷主檔（`infra/models/survey.py:18`） | `folder_id`（FK CASCADE）/ `version_no` / 發布狀態 / `tenant_id` |
| `surveys_trans` | 問卷翻譯 | `survey_id`（FK CASCADE） |
| `survey_folders` | 資料夾樹（`survey_folder.py:18`） | `parent_id`（自 FK） |
| `survey_folders_trans` | 資料夾翻譯 | `survey_folder_id`（FK CASCADE） |
| `survey_pages` | 問卷頁（樹狀）（`survey_page.py:15`） | `survey_id`（FK CASCADE）/ `parent_id`（自 FK） |
| `survey_pages_trans` | 頁翻譯 | `survey_page_id`（FK CASCADE） |
| `survey_questions` | 題目（樹狀）（`survey_question.py:18`） | `page_id`（FK CASCADE）/ `parent_id`（自 FK）/ 顯示條件 |
| `survey_questions_trans` | 題翻譯 | `survey_question_id`（FK CASCADE） |
| `survey_discussions` | 問卷討論（`survey_discussion.py:15`） | `survey_id`（FK CASCADE） |
| `question_answers` | 答案（`question_answer.py:16`） | `question_id`（**FK → survey_questions**）/ `task_survey_id` / `answer` |
| `question_answer_histories` | 作答歷史（`question_answer_history.py:15`） | 使用者 login_name（審計）/ 時間 |
| `question_answer_history_details` | 歷史明細（`question_answer_history_detail.py:18`） | `history_id`（FK）/ `question_id`（**FK → survey_questions**） |
| `task_surveys` | 任務↔問卷（作答實例）（`task_survey.py:19`） | `main_project_id` / `process_id` / `task_id` / `survey_id` / `device_id` / `department_id` / `status` / `job_evidence_id` / **`inherited_from_round_id`** |
| `task_survey_ref_items` | 任務問卷參考項目（`task_survey_ref_item.py:10`） | — |

對別人的表的參照：

(a) **真 FK：0 條跨疆界**。DEV 實查 `survey.task_surveys` 的 constraint 只有 `task_surveys_pkey` ＋ `task_surveys_uid_key`，**零 FK**。`question_answers` / `question_answer_history_details` 的 FK 全指向 `survey.survey_questions`（自家表，正是 README:15-24 說的「兩條真實外鍵橫跨原本的套件／主專案邊界，所以合併不抽兩包」）。

(b) **ORM relationship 跨包：4 條，全在 `infra/models/task_survey.py`**：
- line 106-113 `main_project` → `jedi_flow_engine.WorkflowExecution`（primaryjoin `WorkflowExecution.id == TaskSurvey.main_project_id`，viewonly）＋ `main_project_uid` association_proxy (line 114)
- line 116-123 `process` → `WorkflowExecution`（`TaskSurvey.process_id`）＋ `process_uid` (line 124)
- line 126-133 `task` → `jedi_flow_engine.JobExecution`（`TaskSurvey.task_id`）＋ `task_uid` (line 134)
- line 136-143 `device` → `jedi_device.Device`（`TaskSurvey.device_id`）＋ `device_uid` / `device_name` (line 144-145)
- import 在 `task_survey.py:1-3`

→ **回答派工單的問題「`task_surveys` 對 flow-engine 與 device 的 ORM relationship 在哪」**：全部集中在 `jedi_survey/infra/models/task_survey.py:106-145`（四條 relationship ＋ 六個 association_proxy）。**沒有任何 FK 撐它們**（DEV 實查），純靠 SQLAlchemy 的 `primaryjoin` ＋ `viewonly=True` ＋ `foreign()` 縫起來。相對地，同檔 line 147-152 的部門那條**已於 CM-1512 拔除**改走名冊 port，紅字禁止加回——**同一個檔裡，身分那條償了、業務那三條沒償**。

(c) **軟參照**：`task_surveys.job_evidence_id` → `compliance.job_evidences.id`（`task_survey.py:83-87` comment 明寫 soft ref）；**`task_surveys.inherited_from_round_id` → `compliance.project_audit_rounds.id`**（`task_survey.py:89-93`，comment 逐字：「承襲來源輪次ID（soft ref → compliance.project_audit_rounds.id，無 FK）；覆核輪自母輪 clone 的問卷帶母輪 id，本輪原生建立為 NULL（FR-053.1）」）；`department_id` → `public.org_units.id`。

→ **回答派工單的問題「`inherited_from_round_id` 欄位」**：這是**問卷疆界裡的稽核輪次概念**——欄位存在 survey schema 的表上，指向 compliance-audit 擁有的 `project_audit_rounds`。套件內**只有讀取端**（`infra/mapper/task_survey_mapper.py:46` 讀出、`domain/entities/task_survey_entity.py:36,57` 放進 entity），**沒有任何寫入端**。真正的寫入者在**主專案**：`infra/flow_control/repository/reverify_clone_query.py:128-160`（`clone_task_surveys()` 用 raw SQL `INSERT INTO survey.task_surveys (... inherited_from_round_id ...)` 帶母輪 id）——**主專案的稽核覆核流程用 raw SQL 直寫問卷套件的表**。

(d) **raw SQL 直打別人的表**：套件內無（反向：**主專案用 raw SQL 直寫 survey 的表**，見上）。

表名產品詞彙：表名乾淨（survey_* / question_* / task_survey*）。**欄位名不乾淨**：`inherited_from_round_id`（round ＝ 稽核輪次）、`job_evidence_id`（稽核證據）。

**Q3 Port**

`domain/ports.py` 宣告 3 張 ABC ＋ 1 個型別別名：

| Port | 簽名 | 缺線行為 |
|---|---|---|
| `INotifier` (line 34) | `get_channel_config(channel)` / `send_mail_notification(*a,**k)` / `send_discord_notification` / `send_telegram_notification` | 降級不發通知 |
| `IPresenceStore` (line 50) | `members(room_key)` / `join(room_key, user)` / `leave(room_key, user)` | 降級：共編在線名單為空 |
| `IAuditNicknameEnricher` (line 63) | `enrich_objs(items) -> list` | 降級：少 `*_name` 欄位 |
| `ConfigReader` (line 89，型別別名 `Callable[[str,str],Any]`) | `(group, key) -> 值或 None` | 降級回內建預設 |

**刻意不自建的 port**：專案角色守門直接復用 `jedi_participant.common.guard`（`domain/ports.py:13-18` 明說「再開一張同義的 port 等於同一件事兩套真相」）；license 走自家 `api/guard.py:32-56` 的 `configure()` / `require_license`（缺接線在掛載當下拋 RuntimeError）。

宣告了但零使用的 port：**無**，但 `IConfigReader` 有**歷史錯誤已修正並記錄**（`domain/ports.py:72-93`：原本是 ABC 宣告 `.get(group,key)`，唯一消費端卻當裸 callable 呼叫，宿主又從未接線 → 三層各說各話，2026-09-01 arc-review I-8 收斂成 Callable 別名）。這是本批中對「port 樣板殘留」處理得最誠實的一處。

提供給別人的入口：`jedi_survey.plugin.{register, create_blueprints, SurveyAdapters, SurveyConfig, SurveyServices}`、`app.task_type_declaration.survey_task_type_spec()`（申報書）。**其他套件 import survey：0 處**（monorepo 全掃）。

**Q4 消費者**

(a) 主專案（**65 處**）：`di_containers/survey/survey_containers.py`（18）、`di_containers/task_survey/question_answer_containers.py`（15）、`di_containers/task_survey/task_survey_containers.py`（6）、`infra/flow_control/repository`（4）、`app/flow_engine/service`（4）、`app/flow_control/service`（4）、`infra/readmodel/tasks`（2）、`core/app_factory.py:473`（socketio 模式的 library 註冊）、`api/{survey,task_survey,flow_control}/__init__.py`（3）。另有 52 支 re-export shim（README:161-172）讓既有 import 零改動。

(b) 其他 jedi 套件：**0 處**。survey 是本批中唯一「沒有任何套件依賴它」的一支。

**Q5 執行期形狀**

- route **32 條**（`api/__init__.py:65-93` 設計層 20 條、`:121-135` 作答層 12 條），**兩個 blueprint**（`survey` prefix `/api/1.0`、`task-survey`；`README.md:95-97` 說明分兩個是刻意的，合併會讓 endpoint 改名而 `url_for` 靜默匹配 0 條）。
- **背景 thread：有**——`app/service/task_survey_service.py:526,537,543` 三處 `threading.Thread(...).start()` 發通知（mail / telegram / discord），fire-and-forget 無 join、無錯誤回收。
- **SocketIO namespace**：`app/handler/fill_survey_socketio_handler.py`（共編），繼承 `jedi_iam.middleware.socketio_auth.AuthenticatedNamespace`（line 8）；`api/routes/question_answer_history_route.py:60` 直接讀 `flask.current_app.extensions['socketio']` 發 emit。
- **對外 I/O**：`os.getenv("SYSTEM_NAME")` / `os.getenv("SYSTEM_URL")`（`task_survey_service.py:468-469`，**本批唯一直接讀 env 的套件**）；Excel 讀寫（`api/routes/task_survey_route.py:11-12,21,92-107` openpyxl ＋ pandas ExcelWriter、`survey_route.py:24` pandas）；`send_file` 下載；通知走 port 由宿主寄。
- **Redis**：不直接用，走 `IPresenceStore` port（宿主 `infra/survey/adapters.py` 的 `RedisPresenceStoreAdapter`）。
- 單獨起 process：**不行**（README:147-150 自承）——四支跨套件 relationship ＋ socketio 需要宿主的 `AuthenticatedNamespace`。

**Q6 通用性**

(a) 不能直接用（要先裝 iam / participant / flow-engine / device 四支）。(b) 寫死的產品知識：
- `infra/models/task_survey.py:89-93` **`inherited_from_round_id`**（稽核輪次）
- `infra/models/task_survey.py:83-87` `job_evidence_id`（稽核證據）
- 四條 ORM relationship 到 flow-engine / device 的實體（line 106-145）
- `app/task_type_declaration.py:43` `SURVEY_JOB_TYPE = "survey"` 落庫值、`:46` `SURVEY_REQUIRED_MODULES = ("survey",)`（Guidant 授權模組粒度）
- `common/enum/error_code.py`：34 個碼，其中 `HostAuthzErrorCode` 是**宿主碼表的逐字複本**（`task_survey_guard.py:25` 在用）
- `SurveyConfig(audit_logger_name="app.task_survey")` 必須傳宿主的 logger 名，不傳事件從 `public.system_logs` 整批消失且無錯誤（`README.md:126-132`）
- `common/utils/common_util.py` 等處的作答狀態機五態（未填寫／編輯中／待審核／補充說明／完成）是產品定義

**Q7 插件完整度**

| 項 | 有/無 | 證據 |
|---|---|---|
| ① api 層隨包 | ✅ 32 條 / 2 blueprint | `api/__init__.py` |
| ② `register(app, adapters, config, schema_extensions, mount_api, declare_task_type)` | ✅（**六參數，比別支多 `declare_task_type`**） | `plugin.py:312`；`create_blueprints()` line 273 |
| ③ migrations 隨包 | ❌ 0 支 | — |
| ④ DI container 預設 | ❌ 0 個 | — |
| ⑤ 獨立 harness | ❌ **無**（`harness/` 目錄不存在，README:147-150 誠實聲明「跨四套件相依，架 harness 需要先斷 relationship，不假裝有」） | — |
| ⑥ 接入 README | ✅ | `README.md`（220 行） |

**Q8 糾纏對象**

**與 jedi-participant（7-8 處 import，全部是「借判定資料」）**——派工單問「survey 對 participant 的 7 處 import 是拿什麼」，實查 **7 條真 import 語句**（另 3 處是 docstring 提及）：

| # | 位置 | 拿什麼 | 用途 |
|---|---|---|---|
| 1 | `plugin.py:241` | `common.guard as participant_guard` | `register()` 把宿主的 `IProjectRoleGuard` 轉接進 participant 的模組級 guard |
| 2 | `infra/repository/identity_enrichment.py:44` | `common.user_directory` | 批次填部門名（`get_org_units_by_ids`, line 62）＋ 審計 nickname（`get_users_by_login_names`, line 129） |
| 3 | `app/common/task_survey_guard.py:24` | `common.guard.assert_project_manager` | manager 代填 override |
| 4 | `app/common/task_survey_guard.py:27` | `common.participant_enum.ParticipantRole` | 判 `role == MANAGER` |
| 5 | `app/common/task_survey_guard.py:28` | `domain.entity.task_assignee_query_entity.TaskAssigneeQueryEntity` | 用 `(task_id, user_id)` 查「這人是不是這個 task 的 assignee」 |
| 6 | `app/service/task_survey_service.py:15` | 同上 `TaskAssigneeQueryEntity` | 通知對象反查 |
| 7 | `app/service/task_survey_service.py:16` | `domain.service.task_assignee_domain_service.TaskAssigneeDomainService` | 同上（DI 注入的實體型別） |

歸納：**全部是「問 participant：這個 task 派給誰了 / 這人是不是 manager」**——填答守門與通知對象兩件事。無寫入、無 ORM relationship、無 FK。這是**單向讀取式耦合**，方向 survey → participant。

**與 jedi-flow-engine / jedi-device**：4 條 ORM relationship（Q2b），全 `viewonly`、無 FK、無 service 呼叫。**只為了填 `main_project_uid` / `process_uid` / `task_uid` / `device_uid` / `device_name` 五個顯示欄位**（association_proxy），與 CM-1512 已償還的部門那條性質完全相同——同檔內一條償了另三條沒償。

**與 jedi-compliance-audit（間接，透過主專案 raw SQL）**：`task_surveys.inherited_from_round_id` 指向 CA 的 `project_audit_rounds`；寫入者是主專案 `infra/flow_control/repository/reverify_clone_query.py:128-160` 的 raw SQL。survey 與 CA **程式碼層 0 互相 import**，但**資料層有一條稽核概念的軟參照，且由第三方（宿主）寫入**。

**看起來像但其實不是一件事**：
- **jedi-task-platform**：只有申報書一支 import（`app/task_type_declaration.py:35`），且核心路徑零依賴有 AST 守衛測試 ＋ 突變驗證（`tests/unittest/test_plugin_contract.py:119-141`）。TP 側 `job_execution_surveys.survey_id` 有真 FK 指 `survey.surveys`（CASCADE），但那是 TP 的表指過來，survey 不知道它存在。這是本批中疆界最清楚的一組。

---

## jedi-compliance-audit

**Q1 它是什麼**

「一次合規稽核從規劃到結案的整條流程」——稽核輪次 7 態狀態機、評估計畫（AP）、評估結果（AR）判定與發現、改善計畫（POA&M）、SSP 參照文件、覆核輪繼承、審閱標記、稽核人員儀表板、任務設置（走 OSCAL catalog 解析出 AO 樹）。

README 與程式碼**一致，且是本批中最誠實的一份**——`README.md:69-157` 整節「誠實聲明」逐條列 16 支留守檔、4 支 D-4 讀寫混合 query、readmodel 全目錄不搬、錯誤碼是複製不是共用、ORM 仍認識其他套件實體。實查全部屬實。唯一要補充的是 README 沒說的：`api/routes/{audit_route,poam_route}.py` 兩支檔的 8 個 Resource class **完全沒有被 `mount_routes()` 掛載**（見 Q5）。

→ **回答派工單的問題「它是稽核業務還是通用平台」**：**明確是稽核業務**。判準三條：① `pyproject.toml:4` 自稱「合規稽核插件…插在 jedi-task-platform 上」；② 表名／欄位名／entity 名／error code 全帶產品詞（`project_audit_rounds` / `poams` / `ssp_reference_documents` / `round_stage_transitions` / `FlowControlArControlEntity` / 231 個 `GRC_*` 碼）；③ 它依賴 7 支 jedi 套件（本批最多）而**無任何套件依賴它**（monorepo 全掃 0 處）——它是依賴圖的葉節點，典型的最上層業務插件。

**Q2 資料**

自己擁有的表（**9 張**，跨 `compliance` 與 `oscal` 兩個 schema）：

| 表 | schema | 存什麼 | 關鍵欄位 |
|---|---|---|---|
| `project_audit_rounds` | compliance | 稽核輪次主檔，7 態狀態機（`infra/model/project_audit_round.py:23`） | `project_id`(soft) / `round_no` / `round_type`(initial/close-out/surveillance) / `status` / `parent_round_id` / `assessment_plan_id` / `ssp_id` / `ar_result_id` / `poam_id` |
| `round_stage_transitions` | compliance | 階段歷程時間軸（`round_stage_transition.py:13`） | — |
| `round_rollback_supersessions` | compliance | 回退 supersession 標記（`round_rollback_supersession.py:13`） | — |
| `review_marks` | compliance | 審閱標記（`review_mark.py:9`，`extend_existing=True`） | — |
| **`poams`** | **compliance** | 改善計畫（`poam_model.py:11`） | `uid` / `assessment_plan_id` / `ar_finding_id` / `control_identifier` / `ao_uid` / `status` / `tenant_id` / `remediation_plan` / `due_date` / `assignee_uid` |
| `ap_docx_parse_jobs` | oscal | AP docx 解析工作紀錄（`ap_docx_parse_job.py:14`） | — |
| `ar_xlsx_parse_jobs` | oscal | AR xlsx 解析工作紀錄（`ar_xlsx_parse_job.py:14`） | — |
| `ssp_reference_documents` | oscal | SSP 引用文件（`ssp_reference_document.py:19`） | `file_id`（FK → `upload_files`） |
| `ssp_reference_document_mappings` | oscal | 引用文件對照（`ssp_reference_document_mapping.py:19`） | `reference_document_id`（FK CASCADE） |

**`poams` 表在它與 oscal-v2 各定義一次，是同一張表嗎？→ 不是。是兩張不同 schema 的同名表。**（DEV 實查）

| | `jedi-compliance-audit` 的 `PoamModel` | `jedi-oscal-v2` 的 `OscalPoam` |
|---|---|---|
| 檔案 | `jedi_compliance_audit/infra/model/poam_model.py:10-25` | `jedi_oscal_v2/infra/model/poam/oscal_poam.py:27-32` |
| `__tablename__` | `"poams"` | `"poams"` |
| `__table_args__` schema | **`compliance`** | **`oscal`** |
| DEV 實查存在 | ✅ `compliance.poams` | ✅ `oscal.poams` |
| DEV 欄位 | id(int) / uid(varchar) / assessment_plan_id / ar_finding_id / control_identifier / ao_uid / status / closed_at / tenant_id / org_unit_id / created_* / remediation_plan / due_date / assignee_uid（17 欄） | id(bigint) / **uuid**(uuid) / metadata_id / status / import_ssp_id / system_id / system_id_identifier_type / local_definitions(jsonb) / created_*（12 欄） |
| DEV 筆數 | **92** | **9** |
| 被指向的 FK | 0 條 | 4 條（`oscal.{assessment_findings, assessment_observations, assessment_risks, poam_items}.poam_id`） |
| 語意 | GRC 產品的「一條待改善項」（一個 AR finding 一條） | OSCAL 標準的 POA&M **文件 root**（一份文件） |

兩張表**schema 不同、欄位幾乎不重疊、粒度不同（項 vs 文件）、資料量差 10 倍**——是兩個不同概念剛好撞名，不是同一張表被定義兩次。`project_audit_rounds.poam_id`（`project_audit_round.py:36`）comment 明寫指向 **`oscal.poams.id`**，也就是 CA 自己同時用到兩張。

對別人的表的參照：

(a) **真 FK**：`ssp_reference_documents.file_id` → `public.upload_files.id`（DEV 實查，跨疆界指向宿主的檔案表）；`ssp_reference_document_mappings.reference_document_id` → 自家表。`project_audit_rounds` 的四條 FK（DEV 實查）→ `oscal.{assessment_plans, ar_results, system_security_plans}` ＋ 自 FK `parent_round_id`——**這四條是真 FK，指向 jedi-oscal-v2 擁有的表**（雖然 model docstring line 19-20 說「跨/同 schema soft-ref…比照慣例不建 ORM relationship」，DB 層實際有 FK 約束）。

(b) **ORM relationship 跨包**：`ssp_reference_document.py:43` `relationship("UploadFile")`（字串引用宿主的 model）；`ssp_reference_document_mapping.py:40` 自家表。

(c) **軟參照**：`project_audit_rounds.project_id` → `compliance.projects.id`（TP 的表，無 FK）、`flow_template_snapshot_uid` / `workflow_execution_uid` → flow-engine；`poams.assessment_plan_id` / `ar_finding_id` / `ao_uid` → oscal-v2。

(d) **raw SQL 直打別人的表**（實查 `grep -oE "FROM|JOIN <schema>.<table>"`）：
- `infra/repository/flow_control_review_repo_impl.py`：`FROM compliance.projects`（TP 的表）、`FROM/JOIN oscal.catalog_controls`、`oscal.catalog_control_parts`、`JOIN oscal.profile_imports`、`JOIN oscal.system_security_plans`（皆 oscal-v2 的表）
- `infra/repository/flow_control_task_setup_repo_impl.py`：`FROM/JOIN compliance.projects`（TP）、`JOIN compliance.job_executions`、`FROM compliance.workflow_templates`、`JOIN compliance.workflow_executions`（皆 flow-engine 的表）、`FROM compliance.task_assignees`（**participant 的表**，line 133-135）、`FROM oscal.assessment_plans`
- `infra/repository/wf_control_mapping_lookup_query.py:53`：`FROM public.workflow_execution_control_mapping`（**主專案的表**）
- 註：`JOIN public.users` 那條已於 CM-1512 改成「查自家 task_assignees ＋ 名冊 port」（`flow_control_task_setup_repo_impl.py:126-142`），但改成的做法是**改去 raw SQL 查 participant 的表**——身分那條償了，人員指派那條沒償。

表名產品詞彙：**全部帶產品詞**（audit_round / poam / ssp / review / ar_xlsx / ap_docx）——這是它的疆界，不是洩漏。

**Q3 Port**

`domain/ports.py` 宣告 3 張 ABC：`ILicenseGuard`(21) / `IProjectRoleGuard`(40) / `IIdentityGuard`(62)。

→ **回答派工單的問題「三張 guard 與 task-platform 那份是否逐字相同」——`diff` 實測：程式碼逐字相同，只有 4 行 docstring 文字不同**：

```
line 4:   「平台層不認識…」  vs 「本套件不認識…」
line 9:   :func:`jedi_task_platform.task.plugin._assert_wiring`  vs  :func:`jedi_compliance_audit.plugin._assert_wiring`
line 11:  「…讀寫別人專案的任務資料」  vs  「…讀寫別人專案的稽核資料」
line 32:  「平台層用到 "project"」  vs  「本套件用到 "project" / "audit"」
```

三個 class 名、五個 method 簽名（`require` / `viewer_licensed_modules` / `assert_role` / `assert_role_fallback` / `viewer_is_super_admin`）、參數名、預設值、型別註記**完全一致**。這是**同一份 port 定義的兩份複本**——兩包各自複製一份而非共用，且 CA 依賴 TP（`pyproject.toml:44`），技術上可以 import TP 那份。

宣告了但零使用的 port：無（三張都在 `plugin._assert_wiring` 與 `common/guard.py` 內使用）。

提供給別人的入口：`jedi_compliance_audit.plugin.{register, create_blueprint, attach, FlowControlAdapters, FlowControlServices}`、`app.service.{audit_round_app_service, assessment_result_app_service, poam_app_service, ap_docx_import_app_service, ar_import_app_service}`、`api.serializers.*`。**其他 jedi 套件 import 它：0 處。**

**Q4 消費者**

(a) 主專案（**129 處**，本批最高）：`di_containers/flow_control/flow_control_containers.py`（**43**）、`infra/flow_control/repository`（9）、`app/oscal/service`（7）、`app/flow_control/service`（6）、`api/project/routes`（5，`ar_import_route.py` / `audit_round_route.py` / `ap_docx_import_route.py`）、`di_containers/{oscal,module_frame}`（6）、`app/module_frame/service`（3）、`api/flow_control/{__init__,routes,serializers}`（4）、`api/flow_engine/routes/stage_rollback_route.py:19`（1）、`infra/oscal/clone`（2）等；另 test 20+ 處。

(b) 其他 jedi 套件：**0 處**。

**它自己依賴 7 支 jedi 套件**（`pyproject.toml:42-48`）：iam / flow-engine / task-platform / participant / survey / device / oscal-v2。實查 import：oscal-v2 **41 處**、flow-engine 10 處、task-platform 13 處、participant 8 處、iam 5 處、**survey 0 處**、**device 0 處**（後兩支是 pyproject 虛掛，程式碼零 import）。

→ **回答派工單的問題「它直接 import oscal-v2 的 infra `*_repo_impl` 幾處」——30 處**（`grep -c "jedi_oscal_v2.infra.repository"`），分佈在 **4 個檔**：
- `app/service/assessment_result_app_service.py`：**16 處**（line 36-64：ap_repo / ap_reviewed_controls / ap_assessment_subjects / ap_tasks / ap_assessment_activities / ap_task_subjects / ar_finding / ar_finding_risk / ar_observation / ar_results / ar_risk / ar_remediation / catalog_control_part / catalog_control / profile_import / ssp）
- `app/service/poam_app_service.py`：**7 處**（line 24-30：ar_finding / ar_finding_risk / ar_observation / ar_remediation / ar_risk / poam_item / poam_milestone）
- `app/service/audit_round_app_service.py`：**3 處**（line 26-30：ap_repo / profile_import / ssp）
- `infra/repository/flow_control_task_setup_repo_impl.py`：**4 處**（line 51-62，函式內延後 import：ssp_repo / profile_import_repo / catalog_group_repo / catalog_control_part_repo）
（另 11 處是 `jedi_oscal_v2` 的非 infra import：domain entity / domain service / app service / common enum）

**這是雙重違規**：① 跨套件；② **app service 層直接 import infra 層的 repo 實作**（主專案 CLAUDE.md「app 層禁止直接 import ORM model 做查詢…所有 DB 操作必須透過 domain service → repository interface」）。23 處在 app service 內。

**Q5 執行期形狀**

- route **5 條 live**（`api/__init__.py:66-79`，`tests/urls_frozen.txt` 5 行焊死），掛宿主的 `grc_projects` blueprint / prefix `/api/1.0/grc`：`/project/<uid>/current-ssp-uid`、`/project/<pu>/ap/<ap>/task-setup/tree`、`/project/<pu>/ap/<ap>/control/<cu>/review`、`/project/<pu>/ap/<ap>/control/<cu>/ao/<ao>/review`、`/audits/my/list`。
- **⚠️ 另有 8 個 Resource class 是死碼**：`api/routes/audit_route.py`（6 個：ArControlList / ArControlDetail / ArVerdict / ArFindingCreate / ArFindingDetail / ArRoundList）與 `api/routes/poam_route.py`（2 個：PoamList / PoamDetail）**完全沒有出現在 `mount_routes()`**，主專案 `api/flow_control/__init__.py:230-237` 有對應的 `# api.add_resource(...)` 註解行（FR-038 DEAD-V1，2026-06-18 停用）。也就是說**兩支檔的 route class 隨包搬過來了，但沒有任何地方掛它們**。
- 背景排程／worker／長駐 thread：**無**。
- 對外 I/O：**檔案系統**——`app/service/ap_docx_import_app_service.py:21,90-99`（`tempfile.NamedTemporaryFile` ＋ `BytesIO` 收 docx 上傳）、`app/service/ar_import_app_service.py:21`（同，收 xlsx）；文件解析 `app/service/ap_report_parser/registry.py:14,26`（`import docx`）、`ar_report_parser/registry.py:8,17`（`import openpyxl`）。無 SMTP / HTTP / S3 / subprocess / socket。
- ⚠️ **`pyproject.toml` 未宣告 `python-docx` 與 `openpyxl`**（`grep "docx|openpyxl|pandas" pyproject.toml` 無結果），但套件源碼直接 `import docx` / `import openpyxl` ——**漏宣告的直接依賴**（現在能跑是靠宿主裝了）。同一份 pyproject 卻列了程式碼零 import 的 `jedi-survey` 與 `jedi-device`（虛掛）。
- Redis／快取：無；`os.getenv` 0 處。
- 單獨起 process：不行（7 支跨套件依賴 ＋ 30 處直 import oscal-v2 的 repo impl ＋ raw SQL 跨 4 個 schema）。

**Q6 通用性**

(a) **完全不能用於非稽核產品，且不應該被期待可以**——它就是稽核業務本身。(b) 產品知識不是「洩漏」而是「內容」：7 態輪次狀態機（`common/round_guard.py:1-13`）、AP/AR/POA&M/SSP/AO 全套 OSCAL 概念、231 個 `GRC_*` error code（`common/error_code.py`，README:135-142 說明是主專案 `common/code/flow_control_error_code.py` 的**逐字複製**，套件實際用 50 個，兩份漂移由 `tests/test_error_code_contract.py` 焊死）、CMMC 專屬 parser（`app/service/ap_report_parser/airasia_cmmc_l1_v1.py`、`ar_report_parser/airasia_cmmc_l1_ar_v1.py`、`ar_framework_profile/cmmc_l1.py`——**客戶名 airasia 進了檔名**）。

**Q7 插件完整度**

| 項 | 有/無 | 證據 |
|---|---|---|
| ① api 層隨包 | ✅ 5 條 live（＋8 個未掛載的死 class） | `api/__init__.py:52-79` |
| ② `register(app, adapters, config, schema_extensions, mount_api)` ＋ `attach(bp, adapters)` ＋ `create_blueprint()` | ✅ | `plugin.py:215, 174, 199` |
| ③ migrations 隨包 | ❌ 0 支 | — |
| ④ DI container 預設 | ❌ 0 個（宿主 `di_containers/flow_control/flow_control_containers.py` 43 處 import 撐起整包接線） | — |
| ⑤ 獨立 harness | ✅ | `harness/dev_app.py`（檔頭 line 8-11：假 guard ＋ 假 service，實打一支端點；**不連 DB**） |
| ⑥ 接入 README | ✅ | `README.md`（156 行，含最完整的誠實聲明） |

**Q8 糾纏對象**

**與 jedi-oscal-v2（最強，遠超其他）**：41 處 import，其中 **30 處直取 infra 層 `*_repo_impl`**，23 處在 app service 內（違反自家分層）。`assessment_result_app_service.py` 一支就 import 了 oscal-v2 的 16 支 repo impl——它實際上是「用 oscal-v2 的 repo 拼出 AR 匯入流程」的組裝器。`project_audit_rounds` 對 `oscal.{assessment_plans, ar_results, system_security_plans}` 有 **3 條真 FK**（DEV 實查）。raw SQL 亦直 JOIN `oscal.catalog_controls` / `catalog_control_parts` / `profile_imports` / `system_security_plans`。**一邊沒有另一邊就沒意義**：CA 的核心（AP/AR/POA&M/AO 樹）全部是 OSCAL 文件模型的產品化包裝。

**與 jedi-task-platform**：13 處 import，其中 **6 處直接 import ORM `Project`**。最深的一條是 `infra/repository/project_extension_repo_impl.py:14-43`——它以 TP 的 `Project` 為 `BaseRepositoryImpl` 的 model，`add()`（line 90-111）與 `update_owner()`（line 113-127）**UPDATE `compliance.projects` 的 `module_frame_id` / `living_ssp_id` / `owner_id` / `deleted_at` 四欄**。也就是**稽核插件在寫平台的主檔表**，而那四欄正是 TP 的 `projects` 表上的產品欄位（Q2）。這是「同一交易寫兩邊」的直接證據：`project_start_app_service` 啟動專案時先由 TP 的 `ProjectService` 建列、再由 CA 這支補四欄。

**與 jedi-flow-engine**：10 處，其中 4 處直接 `session.query(JobExecution)` / import `WorkflowTemplate` ORM，raw SQL 亦 `JOIN compliance.job_executions` / `workflow_executions` / `workflow_templates`。`project_audit_rounds.workflow_execution_uid` 軟參照。

**與 jedi-participant**：8 處，全部是「借 DTO / serializer / enum / query entity / 名冊」——`app/dto/{control_group,control,project}_dto.py` 直接把 participant 的 `ParticipantMenuDTO` / `ProjectParticipantDTO` 當自己 DTO 的欄位型別，`api/serializers/{task_setup,project}.py` 直接 import participant 的 marshmallow schema。**API 契約層跨套件共用**。另 `flow_control_task_setup_repo_impl.py:133-135` raw SQL 直查 `compliance.task_assignees`。

**看起來像但其實不是一件事**：
- **jedi-survey**：`pyproject.toml:46` 宣告依賴，**程式碼 0 處 import**（虛掛）。CA 與 survey 唯一的交會是 `task_surveys.inherited_from_round_id` 這個軟參照欄位，而寫入者是主專案的 raw SQL，兩包互不相識。
- **jedi-device**：`pyproject.toml:47` 宣告，**程式碼 0 處 import**（虛掛）。
- **jedi-iam**：只剩 5 處，全是 app service 層的型別註記（`UserDomainService` / `UserQueryEntity`），實體由宿主 DI 注入。infra 層 JOIN 已於 CM-1512 償清。這是正當的身分疆界取用。

---

## 跨套件觀察

### 依賴矩陣（列 import 行；數字＝實際 import 語句數，非 pyproject 宣告）

| ↓import→ | task-platform | participant | flow-engine | survey | compliance-audit | oscal-v2 | iam | device |
|---|---|---|---|---|---|---|---|---|
| **task-platform** | — | **3** | 0 (CM-1492 償) | 0 | 0（守衛焊死） | 0 | 0 (CM-1512 償) | 0 |
| **participant** | **2** | — | **3** | 0 | 0 | 0 | 0 (CM-1484 償) | 0 |
| **flow-engine** | 0 | 0 | — | 0 | 0 | 0 | 0 | 0 |
| **survey** | 1（申報書） | **7** | 2 | — | 0 | 0 | 7 | 1 |
| **compliance-audit** | **13** | **8** | **10** | 0（虛掛） | — | **41**（30 直取 infra repo） | 5 | 0（虛掛） |
| *主專案（宿主）* | 104 | 98 | 122 | 65 | 129 | — | — | — |
| *jedi-detection* | 0 | 0 | **5** | 0 | 0 | — | — | — |

🔴 **唯一的循環：task-platform ⇄ participant**（TP→P 3 處、P→TP 2 處；pyproject 亦雙向宣告）。

### 跨疆界真 FK（DEV `guidant_ai_dev` `pg_constraint` 實查）

| 來源表.欄位 | → 目標表 | 誰擁有來源 | 誰擁有目標 | ondelete |
|---|---|---|---|---|
| `compliance.job_execution_comments.author_id` | `public.users` | task-platform | 宿主/iam | SET NULL |
| `compliance.job_execution_devices.device_id` | `public.devices` | task-platform | device | CASCADE |
| `compliance.job_execution_org_units.org_unit_id` | `public.org_units` | task-platform | 宿主/iam | CASCADE |
| `compliance.job_execution_surveys.survey_id` | `survey.surveys` | task-platform | **survey** | CASCADE |
| `compliance.project_audit_rounds.assessment_plan_id` | `oscal.assessment_plans` | compliance-audit | **oscal-v2** | NO ACTION |
| `compliance.project_audit_rounds.ssp_id` | `oscal.system_security_plans` | compliance-audit | **oscal-v2** | NO ACTION |
| `compliance.project_audit_rounds.ar_result_id` | `oscal.ar_results` | compliance-audit | **oscal-v2** | NO ACTION |
| `oscal.ssp_reference_documents.file_id` | `public.upload_files` | compliance-audit | 宿主/file-upload | NO ACTION |
| `compliance.{workflow_executions,job_executions,workflow_templates}.{tenant_id,org_unit_id}` | `tenants` / `org_units` | flow-engine | 宿主/iam | NO ACTION |
| `compliance.job_evidences.{job_execution_id,workflow_execution_id,main_workflow_execution_id}` | flow-engine 兩表 | **主專案** | flow-engine | — |
| `public.workflow_execution_control_mapping.workflow_execution_id` | `compliance.workflow_executions` | **主專案** | flow-engine | — |

**零 FK 的表（重要對照）**：participant 六張全 0；`survey.task_surveys` 0；task-platform 四張綁定表指向 `job_executions` 的 FK **已拆**（CM-1490）。

### 「想建 FK 卻沒建」的軟參照（資料上真對得上）

| 來源 | → 目標 | 證據 |
|---|---|---|
| participant 六張表 `.project_id` | `compliance.projects.id`（task-platform） | DEV：`project_participants` 716/752、`task_assignees` **12479/12479** 對得上；對 `workflow_executions` **0 列** |
| `task_assignees.task_id` | `compliance.job_executions.id`（flow-engine） | DEV：12470/12479 |
| task-platform 四張綁定表 `.job_execution_id` | `compliance.job_executions.id` | 原為 CASCADE FK，CM-1490 拆；代價轉為主專案每日 03:10 UTC 孤兒清理排程（`core/scheduler.py:374-431`） |
| `compliance.projects.living_ssp_id` | `oscal.system_security_plans.id` | `infra/model/project.py:85-89` ＋ `ix_projects_living_ssp_id` 索引 |
| `survey.task_surveys.inherited_from_round_id` | `compliance.project_audit_rounds.id`（CA） | `task_survey.py:89-93`；**寫入者是主專案 raw SQL** `reverify_clone_query.py:128-160` |
| `compliance.project_audit_rounds.project_id` | `compliance.projects.id`（TP） | `project_audit_round.py:27` |

### Port 供需關係

| 提供者 | Port / 入口 | 消費者 | 缺線行為 |
|---|---|---|---|
| task-platform | `ILicenseGuard`/`IProjectRoleGuard`/`IIdentityGuard` | 宿主注入 | 拒絕掛載 |
| task-platform | `ITaskExecutionQuery` / `ITaskExistenceQuery` | 宿主注入（實作留主專案） | 前者必填、後者降級 |
| task-platform | `TaskTypeSpec` / `register_job_type()` 申報制 | survey（申報書）、主專案 `app/detection_tools/task_type_declaration.py` | 選配 |
| **participant** | **`common.user_directory`（canonical 名冊）** | **task-platform / survey / compliance-audit** 三包 ＋ 宿主 | 降級回空 ＋ WARNING |
| **participant** | **`common.guard.assert_project_manager`（canonical 守門）** | **survey**（`task_survey_guard.py:24`）＋ 宿主 authz | 拒絕掛載 |
| participant | `IUserDirectory` / `IProjectRoleGuard`（要宿主給） | 宿主 `api/participant/__init__.py:112-117` | 拒絕掛載 |
| participant | `IProjectDirectory` | **零消費者**（樣板殘留） | 文件說拒絕掛載，實際 `_assert_wiring` 不檢查 |
| participant | `IAuditLogger`/`IJobNotifier`/`IJobEnrichment` | 宿主（以別的參數名注入） | 降級 |
| flow-engine | `ISettingsReader` / `INotifier` | **零消費者**（誠實標註的樣板殘留） | — |
| survey | `INotifier`/`IPresenceStore`/`IAuditNicknameEnricher`/`ConfigReader` | 宿主 | 全降級 |
| survey | license 走 `api/guard.configure()` | 宿主 | 拒絕掛載 |
| compliance-audit | `ILicenseGuard`/`IProjectRoleGuard`/`IIdentityGuard`（與 TP 逐字相同的第二份） | 宿主 | 拒絕掛載 |

### 三張 guard port 的複本情形

`ILicenseGuard` / `IProjectRoleGuard` / `IIdentityGuard` 在 **task-platform `task/domain/ports.py:21-67`** 與 **compliance-audit `domain/ports.py:21-67`** 各存在一份，`diff` 實測**程式碼逐字相同、僅 4 行 docstring 措辭不同**。CA 依賴 TP，技術上可 import TP 那份而非複製。（第三份在宿主 `common/authz/`，那是實作端。）

### 插件完整度總表

| | api 隨包 | register() | migrations | DI container | harness | README |
|---|---|---|---|---|---|---|
| task-platform | ✅ 10 條 | ✅ ＋`attach` | ❌ 0 | ❌ | ✅ | ✅ |
| participant | ✅ 10 條 live（9 條註解死路徑） | ✅ | ✅ **1 支** | ❌ | ✅（檔頭過時） | ✅ |
| flow-engine | ❌ **0 條**（刻意，測試焊死） | ✅ | ❌ 0（明說不隨包） | ❌（有 `app/wiring` 縫隙） | ✅ **＋docker-compose** | ✅ |
| survey | ✅ 32 條 / 2 bp | ✅ **6 參數** | ❌ 0 | ❌ | ❌ **無**（誠實聲明） | ✅ |
| compliance-audit | ✅ 5 條 live（**＋8 個未掛載的死 class**） | ✅ ＋`attach` | ❌ 0 | ❌ | ✅（不連 DB） | ✅ |

### 規模

| | 套件源碼行數 | error code 數 | 自有表數 | 依賴 jedi 套件數（實際 import） |
|---|---|---|---|---|
| task-platform | 3,447 | 5 ＋3 | 5 | 1（participant） |
| participant | 5,310 | 8 | 6 | 2（flow-engine, task-platform） |
| flow-engine | 6,728 | 9 | 5 | **0** |
| survey | 11,315 | 34 | 14 | 4（participant, flow-engine, iam, device） |
| compliance-audit | 13,604 | **231**（複製，用 50） | 9 | **5**（oscal-v2, task-platform, flow-engine, participant, iam；另 2 支虛掛） |

### 額外發現（非 8 題直接問，但屬事實）

1. **participant 的 `ProjectParticipant.project` relationship 指錯表且無讀者**（`infra/model/project_participant.py:23-30` 指 `WorkflowExecution.id`，DEV 實查 0 列對得上；全 codebase 0 處讀取）。
2. **宿主 `_UserDirectoryAdapter` 只轉發 4 支中的 2 支**（主專案 `api/participant/__init__.py:75-90` vs `infra/participant/user_directory_adapter.py:43-93`），survey 的 `get_org_units_by_ids` / `get_users_by_login_names` 走的是「adapter 未實作 → 回空 ＋ WARNING」的降級路徑。唯一接線點無其他 configure。
3. **`jedi_flow_engine.plugin` 在主專案 0 處被 import**，`register()` 從未執行；連帶套件自己的 `WorkflowExecutionService`（依賴 `plugin.py` import 期的 `set_repo_providers`）在本產品是死碼路徑，主專案用的是自己那支 6,375 行的同名 service。
4. **compliance-audit 的 `pyproject.toml` 漏宣告 `python-docx` / `openpyxl`**（源碼直接 import），同時虛掛 `jedi-survey` / `jedi-device`（源碼 0 import）。
5. **compliance-audit 8 個 route class 隨包搬來但無人掛載**（`api/routes/{audit_route,poam_route}.py`）。
6. **jedi-detection 也是 flow-engine 的消費者**（5 處），不在本批但在同一張依賴圖上。
7. **task-platform 的守衛只掃 `task/` 子樹**，`jedi_task_platform/`（project 那半）的 `living_ssp_id` 掃不到；且守衛黑名單不含 `detection`，而 `task/app/service/task_execution_service.py` 有 17 處 detection。

### 未查證事項

- **STG / POC DB 未查**（只查 DEV）。三環境 FK 是否一致未驗——CM-1490 拆 FK 的 migration 是否已套到 STG/POC 不明。
- **`log/app.log` 未見「名冊 adapter 未實作」的 WARNING**（grep 無結果），但該 log 只有 604KB 且不知涵蓋時間範圍與是否曾走到相關程式路徑，故**無法據此判定第 2 點是否真的在跑時觸發**——只確認了程式碼層的方法數落差。
- **未實際執行任何套件的測試或 harness**（唯讀盤點）。各 README 宣稱的測試數字（如 flow-engine「26 failed / 9 errors 是既有狀態」、survey「48 failed / 42 errors 基線」）照抄未驗。
- **未驗證 `jedi_participant/migrations/001-participant-tables.sql` 與 DEV 實際 schema 是否仍一致**（只確認六張表都存在且 0 FK）。
- **CA 的 41 處 oscal-v2 import 未逐一開檔確認用途**，只做了 `grep -c` ＋ 檔案分佈統計；「23 處在 app service 層」是按檔名歸類，未逐行確認每一處都在 class body 內。
- **未查 FE 是否仍呼叫 CA 那 8 個未掛載 route 對應的路徑**（主專案註解說 2026-06-18 起 FE 無 caller，照抄未驗）。
