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


§1

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

§2

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。是本批中疆界隔離做得最徹底的一條。

§3

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() 六處呼叫。

§4

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 不知道它存在。這是本批中疆界最清楚的一組。

§5

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 償清。這是正當的身分疆界取用。

§6

跨套件觀察

依賴矩陣(列 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,照抄未驗)。