# FR-080 批次 D 套件內容盤點（唯讀）

盤點對象：jedi-oscal-v2、jedi-detection、jedi-evidence-classification、jedi-ai-dashboard、jedi-ai-bot
證據路徑前綴：套件 = `~/Projects/Jedicogy/module/jedi-python-package/`，宿主 = `~/Projects/Billows/Audit-Manager/compliance-manager-be/`
DB 查證：DEV `192.168.50.188:25432 / guidant_ai_dev`（唯讀 SELECT，cmmgr）

---

## jedi-oscal-v2

**Q1 它是什麼**

把 OSCAL v1.2.2 標準的六類文件（Catalog / Profile / SSP / AP / AR / POA&M）攤成關聯式資料表並提供讀寫、複製（snapshot / clone）、匯入匯出（JSON ↔ DB）、以及 CMMC PDF/Excel 解析成 catalog 的一支**純函式庫**。

README 只有 7 行（`jedi-oscal-v2/README.md:1-7`），宣稱「OSCAL v1.2.2 relational service，DDD 結構 `common/domain/infra/app/ports`，backed by normalized OSCAL v1.2.2 relational schema in the `oscal` Postgres schema」。**程式碼與 README 一致**——目錄結構確為五層（`jedi_oscal_v2/{common,domain,infra,app,ports}`），45 張表全部 `{"schema": "oscal"}`。

**README 未提、但實際存在的落差**：README 說「service」，實際上是**無 route、無 register()、無 migration 的 library**（見 Q7）。另外 README 沒提 CMMC 解析器，而套件內確有一支 639 行的 CMMC 2.0 Level 2 PDF/Excel adapter（`jedi_oscal_v2/infra/adapter/cmmc/cmmc2_lv2_parser_adapter.py`）。

**Q2 資料**

自有 45 張表（`grep __tablename__ jedi_oscal_v2/infra/model/` 計 45 筆），**全部在 `oscal` schema**。按 OSCAL 文件類型分六組：

*base（4 張，所有 OSCAL 根文件共用）*
| 表 | 一句話 | 關鍵欄位 |
|---|---|---|
| `metadata` | OSCAL metadata 區塊 | id / title / version / oscal_version |
| `parties` | metadata.parties（組織與人員） | metadata_id / party_uuid / type / name |
| `roles` | metadata.roles（OSCAL 角色定義） | metadata_id / role_id / title / short_name（`infra/model/base/oscal_role.py:14-33`）|
| `resources` | back-matter.resources | metadata_id / resource_uuid / title |

*catalog（5 張）*：`catalogs`（控制項目錄 root）、`catalog_groups`（自巢狀分組）、`catalog_controls`（控制項，`parent_id` 非空＝enhancement）、`catalog_control_parts`（自巢狀 prose 樹，`name` = AO 鑑別子）、`catalog_control_params`（控制項參數）。

*framework（2 張）*：`frameworks`（合規框架，comment 寫「CMMC / ISO 27001 / NIST 800-53 等」，`infra/model/framework/oscal_framework.py:20`）、`framework_versions`（每版掛一份 catalog snapshot）。

*profile（2 張）*：`profiles`（控制項基準線）、`profile_imports`（一條來源匯入指令＋include/exclude 選取）。

*SSP（13 張）*：`system_security_plans`（root）、`ssp_system_characteristics`（1:1，FIPS-199 分類）、`ssp_system_implementation`（1:1 容器）、`ssp_control_implementations`（1:1 容器）、`ssp_implemented_requirements`（一控制一筆，含 SoA props）、`ssp_statements`、`ssp_by_components`、`ssp_components`、`ssp_inventory_items`、`ssp_system_users`、`ssp_information_types`、`ssp_leveraged_authorizations`、`ssp_diagrams`。

*AP（8 張）*：`assessment_plans`（root）、`ap_reviewed_controls`（1:1）、`ap_local_objectives`、`ap_assessment_subjects`、`ap_assessment_activities`、`ap_tasks`（可巢狀任務樹）、`ap_task_subjects`（FR-040 受評對象 link）、`ap_task_participants`（FR-040 參與人員 link）。

*AR（8 張）*：`assessment_results`（root）、`ar_results`（結果子條目）、`ar_assessment_subjects`、`assessment_findings`（雙親擇一：ar_result 或 poam）、`assessment_observations`（同雙親）、`assessment_risks`（同雙親）、`assessment_finding_risks`（純 link）、`assessment_remediations`（掛在 risk 下）。

*POA&M（3 張）*：`poams`（root）、`poam_items`、`poam_milestones`。

**跨疆界參照 —— 幾乎沒有**：

- (a) **真 FK**：`grep -o 'ForeignKey("[^"]*"' jedi_oscal_v2/infra/model/` 共 23 種目標，**全部指向 `oscal.*` 自己的表**，零跨疆界 FK。
- (b) **ORM relationship 跨包**：0（無 `relationship()` 跨包宣告）。
- (c) **軟參照**：只有一處 —— `poam_milestones.assignee_user_id` / `assignee_org_unit_id`，plain BigInteger nullable，`infra/model/poam/oscal_poam_milestone.py:57-61` 註解明寫「plain BigInteger」不建 FK。
- (d) **raw SQL 直打別人的表**：0。

**表名的產品詞彙**：表名本身全是 OSCAL 標準名詞（`system_security_plans` / `assessment_plans` / `poams` / `catalogs`），**沒有 Guidant 產品詞**（無 round / detection / 稽核）。「SSP / POA&M / AP / AR」是 OSCAL 規範術語不是產品詞。程式碼註解裡有 CMMC 字樣但只在 parser adapter 與 comment（見 Q6）。

**🔴 `oscal.roles` vs iam `roles` —— 不是同一張表（DB 實查）**

| | schema | 欄位 |
|---|---|---|
| oscal-v2 | `oscal.roles` | id, metadata_id, sort_order, role_id, title, short_name, description, props, links, remarks, created_at/updated_at/created_user/updated_user |
| jedi-iam | `public.roles` | id, uid, pid, name, description, enable, is_admin, created_user/created_at/updated_user/updated_at, is_delete, tenant_id |

證據：DEV `information_schema.columns` 查詢回上表；`jedi-iam/jedi_iam/infra/models/role.py:23` 是 `__tablename__ = "roles"` **無 `schema` 指定**（落 public），`jedi-oscal-v2/jedi_oscal_v2/infra/model/base/oscal_role.py:14-17` 是 `"schema": "oscal"`。**語意完全不同**：oscal.roles 是 OSCAL 文件內的角色宣告（掛 metadata），public.roles 是 RBAC 的系統角色（掛 tenant）。**同名不同物，無關聯**。

**🔴 `oscal.poams` vs compliance-audit `poams` —— 不是同一張表（DB 實查）**

| | schema | 欄位 |
|---|---|---|
| oscal-v2 | `oscal.poams` | id, uuid, metadata_id, status, import_ssp_id, system_id, system_id_identifier_type, local_definitions, +audit |
| compliance-audit | `compliance.poams` | id, uid, assessment_plan_id, ar_finding_id, control_identifier, ao_uid, status, closed_at, tenant_id, org_unit_id, +audit, remediation_plan, due_date, assignee_uid |

證據：`jedi-compliance-audit/jedi_compliance_audit/infra/model/poam_model.py:11-12` 是 `"schema": "compliance"`；`jedi-oscal-v2/.../poam/oscal_poam.py:28-32` 是 `"schema": "oscal"`。**oscal.poams 是 OSCAL POA&M 文件 root**（一份文件一筆），**compliance.poams 是產品的缺失追蹤列**（一條 finding 一筆、帶 assignee/due_date/tenant）。同名不同物。

**🔴 但 `oscal` schema 不是 oscal-v2 獨佔**：DEV 實查 `oscal` schema 有 **58 張表**，套件只認 45 張。剩下 13 張分屬他人：
- `cd_capabilities` / `cd_components` / `cd_control_implementations` / `cd_implemented_requirements` / `cd_statements` / `component_definitions`（6 張，**無任何 Python model 宣告**，grep 全 monorepo + 宿主 0 命中 —— 疑似孤兒表或純 SQL 維護）
- `ssp_docx_parse_jobs` / `ssp_excel_parse_jobs` / `ap_docx_parse_jobs` / `ar_xlsx_parse_jobs` / `framework_parse_jobs`（5 張，其中 `ssp_docx_parse_jobs`/`framework_parse_jobs` 屬**宿主** `infra/oscal/model/ssp_docx_parse_job.py:14`、`infra/oscal/model/framework_parse_job.py:15`；`ap_docx_parse_jobs`/`ar_xlsx_parse_jobs` 屬 **compliance-audit** `jedi_compliance_audit/infra/model/ap_docx_parse_job.py:14`、`ar_xlsx_parse_job.py:14`）
- `ssp_reference_documents` / `ssp_reference_document_mappings`（2 張，屬 **compliance-audit** `jedi_compliance_audit/infra/model/ssp_reference_document.py:19` + `{"schema": "oscal"}`）

即 **`oscal` schema 由三個 code owner 共寫**（oscal-v2 45 張 + 宿主 2 張 + compliance-audit 4 張 + 6 張無主）。

**RLS**：DEV 實查 `oscal` schema 只有 4 張表開 RLS（`ap_docx_parse_jobs` / `ar_xlsx_parse_jobs` / `ssp_docx_parse_jobs` / `ssp_excel_parse_jobs`），**全部不是 oscal-v2 的表**。oscal-v2 的 45 張表**零 RLS、零 tenant_id 欄位**（`grep tenant_id jedi_oscal_v2/infra/model/` 0 命中）。

**Q3 Port**

*需要宿主提供的 port*：**0 張**。`jedi_oscal_v2/ports/` 只有兩支檔，且都不是「要宿主實作」的方向：
- `ports/oscal_parser_provider.py:24-43` — `IOscalParserProvider`（ABC，4 method：`pdf_parser` / `excel_parser` / `gen_excel_file` / `convert_to_oscal_catalog_entity`）。**實作在套件自己內部**（`infra/adapter/cmmc/cmmc2_lv2_parser_adapter.py`），不是宿主要填的插槽。
- `ports/oscal_parser_factory.py:17-19` — `TYPE` dict，**硬編一組 framework code → adapter class**。

*提供給別人的能力入口*：13 支 app service（`app/service/{ap,ar,catalog,framework,io,poam,profile,snapshot,ssp}/`，合計 4178 行）＋ 全部 repository impl ＋ 全部 domain entity。實際 caller 直接 import 到 `infra/repository/*_repo_impl` 這一層（見 Q4），**不是只用 app service**。

*宣告了但零使用的 port*：**`ParserAdapterType` 有三個保留常數零實作** —— `ISO_27002` / `NIST_SP_800_53_R5` / `NIST_SP_800_171_R2`（`common/code/parser_adapter_type.py:18-20`），但 `oscal_parser_factory.TYPE` 只註冊了 `CMMC_2`（`ports/oscal_parser_factory.py:17-19`）。傳這三個 code 進 `get_oscal_parser_adapter()` 會拋 `BadRequestError(OSCAL_V2_PARSER_PROVIDER_NOT_SUPPORTED)`（同檔 :28-31）。檔頭註解自陳「Wave 1 wires only CMMC」。

**🔴 `oscal_parser_factory.TYPE` 硬編 CMMC 的確切位置**：`jedi_oscal_v2/ports/oscal_parser_factory.py:13`（import `CMMC2Level2Adapter`）+ `:17-19`（`TYPE: dict = {ParserAdapterType.CMMC_2: CMMC2Level2Adapter}`）。

**Q4 消費者**

(a) 主專案：**180 處 import**（`grep -rn jedi_oscal_v2 --include=*.py` 排除 .venv）。按目錄：
| 目錄 | 處數 | import 什麼 |
|---|---|---|
| `app/oscal/` | 68 | entity + repo_impl + enum + app service（如 `framework_version_edit_service.py:32-57` 一次 import 12 個）|
| `app/module_frame/` | 31 | entity + repo_impl |
| `di_containers/oscal/` | 24 | repo_impl（DI wiring，容器在**宿主**不在套件）|
| `app/flow_control/` | 19 | AP entity + repo_impl（`assessment_plan_app_service.py:25-55`）|
| `app/project/` | 9 | SSP/catalog/profile repo_impl（`project_start_app_service.py:39-56`）|
| `infra/flow_control/` | 7 | SspRepoImpl + ProfileResolutionService（函式內 lazy import，`job_export_query.py:77-87`）|
| `infra/oscal/` | 3 | catalog repo + profile resolution（`ssp_catalog_title_query.py:21-27`）|
| `di_containers/module_frame/`, `api/project/` | 各 1 | |

**注意方向**：主專案依賴 oscal-v2，且**深入到 infra/repository 層**（不是只用 app service），耦合面極寬。

(b) 其他 jedi-* 套件：**只有 jedi-compliance-audit，38 處**，6 個檔：
- `app/service/assessment_result_app_service.py:33-64` — 一次 import 17 個（AP repo ×5 / AR repo ×6 / catalog repo ×2 / profile / ssp / `FindingState` enum / `build_control_selections` domain service / `RemediationService` app service）
- `app/service/poam_app_service.py:24-30` — AR repo ×5 + poam repo ×2
- `app/service/audit_round_app_service.py:26-30` — ApRepoImpl / ProfileImportRepoImpl / SspRepoImpl
- `app/service/project_current_ssp_service.py:16,32` — `SspQueryEntity` + 建構子收 oscal-v2 的 `SspService`
- `infra/repository/flow_control_task_setup_repo_impl.py:51-61` — 5 個 lazy import
- `app/service/ao_derivation.py:26` — 註解提及

**方向：compliance-audit → oscal-v2**（單向，oscal-v2 對 compliance-audit 0 import）。

**Q5 執行期形狀**

- **route：0 條。無 Flask 依賴**（`grep "Blueprint|flask|Flask|MethodResource" jedi_oscal_v2/` 0 命中；`pyproject.toml:10-21` 無 Flask）。
- **背景排程 / worker / 長駐 thread：無**（無 threading / schedule import）。
- **對外 I/O**：只有 `pdfplumber`（讀 PDF stream）、`openpyxl`（產 Excel 到 BytesIO）、`pandas`、`tqdm`。**無 SMTP / 對外 HTTP / S3 / subprocess / socket**。檔案操作全走傳入的 `BinaryIO`，不自己開檔路徑。
- **快取 / Redis**：無。
- **DB**：吃 `jedi_common.session.database` 的 thread-local session（`BaseRepositoryImpl`）。

**能不能單獨起成 process**：**不能，因為它根本沒有 process 形狀**——沒有 HTTP 入口、沒有 CLI、沒有 worker loop。它是純 library，要嘛被 in-process import、要嘛得有人替它寫一層服務外殼。

**Q6 通用性**

(a) **一個工單系統能不能直接用**：技術上裝得起來（只依賴 jedi-common + SQLAlchemy + pdfplumber/pandas/openpyxl），但**沒有意義**——這支套件的整個資料模型就是 OSCAL 標準（NIST 的合規文件規範），工單系統不會有 SSP / AP / AR / POA&M 這些概念。它不是「通用套件洩漏了產品詞」，它是**一個特定標準的實作**，領域本身就窄。

(b) **寫死的產品/框架知識**：
1. `ports/oscal_parser_factory.py:17-19` — `TYPE` 只註冊 CMMC_2，換框架要改套件源碼（不是加設定）。
2. `common/code/parser_adapter_type.py:18-20` — ISO / NIST 三個 code 保留但無實作，傳進去 400。
3. `infra/adapter/cmmc/cmmc2_lv2_parser_adapter.py`（639 行）— 整支是 CMMC 2.0 Level 2 PDF 版面的硬編解析規則。
4. `infra/model/framework/oscal_framework.py:20` — table comment 寫死「CMMC / ISO 27001 / NIST 800-53 等」。
5. `domain/service/profile/profile_resolution_service.py:84-90` — profile-importing-a-profile 未實作，錯誤訊息硬寫「CMMC/ISO/NIST use catalog imports」。
6. `infra/model/ssp/oscal_ssp_implemented_requirement.py:26` comment 提及「含 SoA props 落點」（SoA = Statement of Applicability，ISO 27001 產品概念混進 OSCAL 表）。
7. `ap_task_subjects` / `ap_task_participants` 兩張表的 comment 標「FR-040」（`infra/model/ap/oscal_ap_task_subjects.py:26`）——**Guidant 的 FR 編號寫進了 DB comment**。

**Q7 插件完整度**

| 項目 | 有/無 | 證據 |
|---|:--:|---|
| ① api 層隨包 | **無** | 無 `api/` 目錄、無 Flask 依賴 |
| ② `register()` 註冊入口 | **無** | 全套件無 `plugin.py`、無 `def register(` |
| ③ migrations/ 隨包 | **無** | 無 `migrations/` 目錄；45 張表全靠宿主 `scripts/sql/` 建 |
| ④ DI container 預設 | **無** | 容器在宿主 `di_containers/oscal/oscal_containers.py`（24 處 import）|
| ⑤ 獨立 harness | **無** | 無 `harness/`、無 `dev_app.py`、無 docker-compose |
| ⑥ 接入 README | **無** | README 只 7 行，零接入說明、零範例 |

**六項全無 —— 它是 FR-069 插件化改造前的舊式 library**，與同批的 detection / evidence-classification / ai-dashboard / ai-bot 形狀完全不同。有 `tests/`（65 支測試檔）與 `[tool.pytest.ini_options]`。

**Q8 糾纏對象**

**最像同一件事的兩半：jedi-compliance-audit。**

事實（不下結論）：
1. **compliance-audit 的 poam_app_service 幾乎全部在操作 oscal-v2 的表**：`poam_app_service.py:24-30` import 了 5 支 AR repo（finding / finding_risk / observation / remediation / risk）+ 2 支 POAM repo（poam_item / poam_milestone），而它自己只有一張 `compliance.poams`。
2. **同一交易寫兩邊**：`assessment_result_app_service.py:33-64` 一次 import 17 個 oscal-v2 元件（含跨 AP/AR/catalog/profile/ssp 五個子域），與自己的 `project_audit_rounds` / `round_stage_transitions` 在同一 app service 內操作。
3. **同名表分屬兩邊**：`compliance.poams`（compliance-audit）與 `oscal.poams`（oscal-v2）名字相同、語意不同，且 compliance-audit 的 `poams.ar_finding_id` 是指向 `oscal.assessment_findings.id` 的**軟參照**（`poam_model.py:16`，plain Integer 無 FK）——這正是「想建外鍵但跨了 schema/套件所以沒建」的形狀。
4. **compliance-audit 建自己的表在 `oscal` schema 裡**：`ssp_reference_documents` / `ssp_reference_document_mappings` 用 `{"schema": "oscal"}`（`ssp_reference_document.py:19-23`），與 oscal-v2 的 45 張表**同 schema 共存**。
5. **一邊沒有另一邊就沒意義**：compliance-audit 對 oscal-v2 是 38 處單向 import，拔掉 oscal-v2，compliance-audit 的 AR/POA&M/AP 三條主線全部 import 失敗。反向 0 處。

**看起來像但其實不是一件事**：
- **jedi-iam**：兩邊都有 `roles` 表，**但 DB 實查證實是完全不同的兩張表**（不同 schema、不同欄位、不同語意，見 Q2）。oscal-v2 對 jedi-iam **0 import**。這是純粹的命名巧合。
- **jedi-detection**：同批盤點，但 oscal-v2 ↔ detection **雙向 0 import**，無共表、無共 port。宿主的 `app/oscal/service/ssp_control_implementation_service.py:31-36` 同時 import 兩者，那是**宿主的組裝**不是套件間的耦合。

---

## jedi-detection

**Q1 它是什麼**

管「有哪些檢測工具（OpenVAS / Nessus / SonarQube…）、每支吃什麼參數、租戶的工具連線憑證、檢測基準庫（TWGCB 那類 OS 硬化基準）、任務綁了哪支工具要派到哪幾台機器、派工出去、收回掃描報告、落成執行紀錄」的一支**插件**（自帶 35 條 HTTP 端點 + 11 張表 + 2 支 migration + 5.4MB 基準檔）。

README（`jedi-detection/README.md:6-19`）的疆界宣告與程式碼**大致相符**，但 README 自己在 :114-124 標了「🔴 誠實聲明」承認**四支疆界依賴未償**（remote-agent / flow-engine / file-upload / iam 在型別層直接相認）——這點程式碼實查確認屬實（見 Q2/Q4）。

**Q2 資料**

自有 **11 張表**，跨兩個 schema：

*「檢測工具定義」類（7 張，config schema）*
| 表 | 存什麼 | 關鍵欄位 |
|---|---|---|
| `config.detection_tools` | 工具目錄（平台級全域字典） | code（openvas/nessus/sonarqube, unique）/ connection_type（API/CLI）/ config_field_schema(JSONB) / requires_credentials / enabled（`infra/detection_tools/model/detection_tool.py:12-54`）|
| `config.detection_tool_param_schemas` | 每支工具的任務參數定義（可版本化） | detection_tool_id / version / param_schema(JSONB) / is_current（同上 `detection_tool_param_schema.py:12-25`）|
| `config.tenant_detection_tool_configs` | **租戶的工具連線設定與憑證** | org_unit_id / detection_tool_id / **credentials_encrypted** / field_values(JSONB) / status / last_tested_at（`tenant_detection_tool_config.py:15-39`）|
| `config.detection_profile_taxonomies` | 基準分類 enum（受控兩軸 target_type / benchmark_family） | axis / key / sort_order（`detection_profile_taxonomy.py:26-52`）|
| `config.detection_profiles` | 檢測基準主檔（一列＝一支基準） | scope(SYSTEM/TENANT) / detection_tool_id / target_type / target_product / benchmark_family（`detection_profile.py:24-58`）|
| `config.detection_profile_versions` | 基準版本從檔 | profile_id / source_type(file/url) / file_id / sha256 / extraction_status（`detection_profile_version.py:26-56`）|
| `config.detection_profile_controls` | 基準控制項（一條一列） | version_id / control_id / severity_raw / severity_norm / attributes(JSONB) / origin（`detection_profile_control.py:24-56`）|

*「綁定與派工」類（2 張，config schema）— 介於定義與執行之間*
| 表 | 存什麼 |
|---|---|
| `config.job_execution_detection_tools` | 任務↔工具綁定（`job_execution_detection_tool.py:13`）：job_execution_id / detection_tool_id / tenant_config_id / tool_params(JSONB) / completion_mode |
| `config.job_execution_detection_tool_agents` | 綁定的**分派列**（每台機器一列，`job_execution_detection_tool_agent.py:15-46`）：job_execution_detection_tool_id / agent_uid / scan_targets(JSONB) / scheduled_at |

*「執行結果」類（2 張，compliance schema）*
| 表 | 存什麼 |
|---|---|
| `compliance.detection_executions` | **每次執行一筆，重掃不覆蓋**（`detection_execution.py:15-50`）：agent_task_uid / job_execution_uid / detection_tool_id / started_at/finished_at / status(running/succeeded/failed/cancelled/scheduled) / summary(JSONB) / report_file_id / evidence_id / group_uid / assignment_uid |
| `compliance.detection_execution_groups` | 執行群組（一次執行的彙總單位，`detection_execution_group.py:19-44`）：job_execution_uid / status / total_count / closed_at |

**分類結論**：7 張定義類（工具目錄 3 ＋ 基準庫 4）、2 張綁定/派工類、2 張執行結果類。

**跨疆界參照**：
- (a) **真 FK**：套件 ORM **0 處 `ForeignKey()`**（`grep ForeignKey jedi_detection/infra/` 0 命中），migration SQL 也 **0 處 REFERENCES / FOREIGN KEY**（`grep -ci references 001-detection-tables.sql` = 0），檔頭 `migrations/001-detection-tables.sql:16-17` 明寫「跨疆界外鍵刻意不含（D17 律②）」。
  **🔴 但 DEV 實查有 7 條 FK 存在於這些表上**（`pg_constraint` 查詢）：
  ```
  detection_profile_controls → detection_profile_versions
  detection_profile_versions → detection_profiles
  detection_profiles(current_version) → detection_profile_versions
  detection_tool_param_schemas → detection_tools
  job_execution_detection_tool_agents → job_execution_detection_tools
  job_execution_detection_tools → detection_tools     ← README:128 引用的那條 fk_jedt_detection_tool
  tenant_detection_tool_configs → detection_tools
  ```
  **全部是檢測疆界「內部」FK，零跨疆界 FK** —— 這點與 README 的說法一致。**但套件隨包的 001 migration 不含這 7 條 FK**（只有 PK/UNIQUE/CHECK/INDEX），意即**用套件 migration 裝出來的庫，FK 完整性弱於 DEV 現況**。這是一個 README 沒說的落差。
- (c) **軟參照（只存 id/uid 不建 FK）**：`detection_executions.agent_task_uid` → `compliance.agent_tasks.uid`、`.job_execution_uid` → `job_executions.uid`、`.report_file_id` → `upload_files.id`、`.evidence_id` → `job_evidences.id`、`job_execution_detection_tool_agents.agent_uid` → `compliance.remote_agents.uid`、`job_execution_detection_tools.job_execution_id` → `job_executions.id`（證據：`migrations/001-detection-tables.sql:447-457` 的 COMMENT 逐條標明「soft-ref →」；ORM 側 `job_execution_detection_tool_agent.py:33` 註解同）。
- (d) **raw SQL 直打別人的表**：套件內 0；但套件宣告的 `IProfileUsageQuery` port 的實作住宿主 `infra/readmodel/detection/detection_profile_usage_query.py:78-108`，該 SQL UNION 了 `config.job_execution_detection_tools` + `compliance.agent_tasks` + `compliance.detection_executions`/`job_executions`，再 LEFT JOIN `compliance.task_assignees` + `compliance.projects` —— **刻意留在宿主就是為了不讓套件認得那三張別人的表**（port docstring `domain/ports.py:162-186` 說明）。
- **另有一處套件內直查別人 ORM model**：`infra/detection_execution/repository/detection_report_file_query.py:15` `from jedi_file_upload.infra.models.upload_file import UploadFile`，直接 `session.query(UploadFile.id, UploadFile.uid)`（同檔 :28-32）。這是跨套件直查 ORM，不是 port。

**表名的產品詞**：表名全帶 `detection_` 前綴（產品詞「detection」本身就是這支的疆界名，屬合理）。**沒有 CMMC/SSP/round/稽核**。但**程式碼註解與 comment 大量帶 Guidant 產品詞**：`migrations/001-detection-tables.sql` 的 COMMENT 帶「FR-056.4」「FR-060.1」「FR-067.2」「D10」「D12」「CM-952」等 Guidant 內部編號（:445-470 多處）；`common/event_code.py:18-20` 硬編三個 Guidant 稽核事件碼數值（6120/6121/6122），檔頭自陳「抽出前是 `from common.enum.event_code import EventCode`……逐字複製」。

**Q3 Port**

*需要宿主提供（5 張，`domain/ports.py`）*：
| port | 方法簽名 | 用在哪 | 缺了 |
|---|---|---|---|
| `IEvidenceSink` (:55) | `add(evidence)` / `build_evidence(**fields)` / `list_active_by_job_execution(job_execution_id) -> List` | 掃描報告轉佐證 | 降級：掃描照跑、不產佐證 |
| `INotifyConfig` (:92) | `read_value(tenant_id, key, default=None)` | Discord/Telegram 通知設定 | 降級：視為未設定，email 照發 |
| `ICrypto` (:107) | `encrypt(plaintext) -> str` / `decrypt(ciphertext) -> str` | 憑證與 secret 參數加解密 | **🔴 拒絕掛載**（`plugin.py:208-214` REQUIRED_WIRING）|
| `IAgentDirectory` (:129) | `get_dispatchable_agents(tenant_id, capability, **kw)` / `diagnose_dispatchability(agent_uid, **kw)` / `get_remote_agent_by_id(agent_id)` / `get_remote_agents(*a, **kw)` | 派工前找機器 | 降級：回「查不到可用機器」 |
| `IProfileUsageQuery` (:162) | `find_refs(version_uids, profile_refs) -> List[dict]` | 基準「被誰用過」查詢 | 降級：回「未使用」 |

另有三軸授權守門（`license_guard` / `capability_guard` / `identity_guard`）+ `auth_required` + `platform_admin_check` + `agent_auth_settings_provider` + `response_builder`，都在 `plugin.py:139-168` 的 `DetectionAdapters` 上（不是 ABC，是 duck-typed 欄位）。

*提供給別人*：`jedi_detection.plugin` 的 `register` / `create_blueprint` / `iter_migrations` / `profiles_dir` / `DetectionAdapters` / `DetectionServices` / `DetectionConfig`；另外宿主大量直接 import 它的 domain service 與 common util（見 Q4）。

*宣告了但零使用*：`SchemaExtensions`（`plugin.py:125-136`）—— docstring 自陳「插槽先開齊，本套件尚無產品使用」。

**🔴 Q3 補充：detection 對 remote-agent 的 6 處 import 拿什麼**

實查 `grep -rn jedi_remote_agent jedi_detection/`，扣掉 2 處純註解，**實際 import 共 6 處、只拿 2 種東西，全部是「agent 認證原語」——不是 port、不是 ORM、不是 service**：

| 位置 | 拿什麼 | 種類 |
|---|---|---|
| `infra/detection_tools/connector/agent_probe_client.py:18` | `jedi_remote_agent.common.agent_auth.jwt_util` | **工具模組**（簽短效 JWT）|
| `infra/detection_tools/connector/agent_probe_client.py:20` | `...agent_auth.tls.build_cloud_mtls_context` | **工具函式**（建 mTLS SSLContext）|
| `infra/detection_tools/connector/agent_cancel_client.py:16` | `jwt_util` | 同上 |
| `infra/detection_tools/connector/agent_cancel_client.py:18` | `build_cloud_mtls_context` | 同上 |
| `app/service/detection_result_handler.py:36` | `jwt_util` | 同上 |
| `app/service/detection_result_handler.py:38` | `build_cloud_mtls_context` | 同上 |

用途：三條「檢測 → agent 的資料面 HTTP 呼叫」（測試連線 probe / 取消掃描 / 取報告檔），每條都要 mTLS + 短效 JWT。**設定值本身走 port**（`common/agent_auth.py:38-41` 的 `configure(settings_provider)`，宿主注入 `get_agent_auth_settings` 函式），**只有兩支認證原語仍是直接型別依賴**。反向 remote-agent → detection 是 **0 處**（`domain/ports.py:19-31` 明載 AST 全掃結果，且 remote-agent 需要檢測知識的兩處已在 FR-069 P4 port 化到宿主 `app/remote_agent/adapter/`）。

**🔴 Q3 補充：detection 對 flow-engine 的 5 處 import 拿什麼**

| 位置 | 拿什麼 | 種類 |
|---|---|---|
| `app/service/detection_orchestration_service.py:34` | `jedi_flow_engine.common.enum.job_code.JobStatus` | **enum**（比對 `job.status != JobStatus.PROCESSING.value`，:197, :1661）|
| `app/service/detection_orchestration_service.py:35` | `...common.enum.error_code.ErrorCode` | **error code**（拋 `FLOW_ENGINE_JOB_NOT_FOUND`，:192, :873, :1111, :1271）|
| `app/service/detection_orchestration_service.py:36` | `...domain.entity.job_execution_query_entity.JobExecutionQueryEntity` | **query entity**（查任務，8 處呼叫 `get_job_execution(...)`）|
| `app/service/detection_result_handler.py:30` | `ErrorCode as FlowEngineErrorCode` | error code |
| `app/service/detection_result_handler.py:31` | `JobExecutionQueryEntity` | query entity |

即：**三種型別（狀態 enum / error code / query entity），零 ORM model、零 repository、零 service class**。但 `job_execution` domain service 本身是**建構子注入的**（`self._job_execution.get_job_execution(...)`），只有查詢參數的型別是硬依賴。檔頭 `detection_orchestration_service.py:10` 明寫「**不新增任何 JobStatus，不動 jedi_flow_engine**」。

**另外兩支疆界依賴**（README:119-124 一併列的）：
- `jedi_file_upload`：**1 處**，`infra/detection_execution/repository/detection_report_file_query.py:15` import `UploadFile` **ORM model** 並直接 `session.query()`（這是五者中唯一的 ORM 直查）。
- `jedi_iam`：**2 處**，`app/service/detection_profile_service.py:50` 與 `app/service/detection_orchestration_service.py:26`，都只拿 `UserQueryEntity`（**query entity**，用來 enrich 使用者暱稱）。

**Q4 消費者**

(a) 主專案：**101 處**（含 test）；扣掉 test 約 **60 處**。按目錄：
| 目錄 | 處數 | import 什麼 |
|---|---|---|
| `di_containers/detection_tools/` | 28 | 全套 service / repo wiring |
| `di_containers/detection_execution/` | 4 | 同上 |
| `app/flow_control/` | 8 | `detection_job_binding_handler`（`job_handlers/__init__.py:31`）、DTO util（`dto/job_dto.py:5-11`：`detection_source_file` / `detection_assignment_params` / `detection_secret_params` / `scan_target_spec`）、三支 domain service（`service/job_service.py:21-25`）|
| `app/detection_tools/` | 8 | **shim 檔**（`dto/__init__.py:16` 與 `service/__init__.py:16` 把整棵子樹掛回舊路徑）|
| `app/oscal/` | 3 | `ssp_control_implementation_service.py:31-36` import 三支 common util（assignment_params / source_file / secret_params）|
| `app/remote_agent/` | 2 | `adapter/detection_task_payload_provider.py:24-25` import `detection_source_file` + `detection_secret_params` |
| `infra/flow_control/` | 3 | `flow_control_job_repo_impl.py:975-979` 函式內 lazy import **三個 ORM model**（`DetectionTool` / `JobExecutionDetectionTool` / `JobExecutionDetectionToolAgent`）|
| `infra/readmodel/` | 2 | `detection/detection_profile_usage_query.py:61` import `IProfileUsageQuery`（實作 port）|
| `common/util/` | 3 | `profile_extractor/__init__.py:5-6` shim |
| `api/detection_tools/` | 3 | `__init__.py:22` `from jedi_detection.plugin import ...` |
| `infra/detection_tools/` | 1 | `adapters.py` 四張 port 實作 |

**🔴 注意**：`infra/flow_control/flow_control_job_repo_impl.py:975-979` 是**宿主直接 import 套件的 ORM model**（三張表），這正是 README:128-131 記的「CM-1487 16 支留守清單 detection 邊，改走 port 的工由後續棒接手」的欠債現場。

(b) 其他 jedi-* 套件：**0 處**（`grep -rn jedi_detection` 在其他套件 0 命中）。detection 是**葉節點被消費者**，只有宿主用它。

**Q5 執行期形狀**

- **route：35 條**，1 個 blueprint（`plugin.py:65` `DEFAULT_BLUEPRINT_NAME = "detection-tools"`，prefix `/api/1.0`）。URL 分五群（`api/__init__.py:88-187`）：`/detection-tools*`（工具與租戶設定 7 條）、`/detection-tool-profiles*`（基準庫 10 條）、`/detection-profile-taxonomies*`（分類 3 條）、`/detection-tool-profile-versions*`（版本 5 條）、`/detection-tools/jobs|execution-groups|executions*`（派工與執行 10 條）。
- **背景 thread（套件內自起）**：
  - `app/service/detection_profile_extraction_service.py:173` — `threading.Thread(target=self._run_worker, daemon=True)`，基準壓縮檔的**背景抽取 worker**（上傳後排一次，立即返回不等結果）。
  - `app/service/detection_orchestration_service.py:2215/2227/2234` — 三個通知 thread（email / telegram / discord），fire-and-forget。
- **排程 tick：套件不含，由宿主排**。宿主 `core/scheduler.py:341-371` `_detection_execution_timeout_tick`，APScheduler interval 15 分鐘，呼叫套件的 `detection_orchestration_service.converge_timed_out_executions()`（檢測執行逾時收斂）。**套件提供方法，宿主決定何時呼叫**。
- **對外 HTTP**：**有，三條**。`infra/detection_tools/connector/agent_probe_client.py:16` / `agent_cancel_client.py:14` / `app/service/detection_result_handler.py:28` 都 `import httpx`，走 mTLS + 短效 JWT 打 remote agent 的資料面端點（如 `PROBE_PATH = "/detection/probe"`，`agent_probe_client.py:26`）。另有 `common/safe_http_fetch.py`（`:62` httpx、`:59` socket、`:399` `socket.getaddrinfo` 做 SSRF 防護的 DNS 解析），用於「從 URL 抓基準檔」。
- **subprocess**：**無**（套件內 0 處；`profiles/tools/*.py` 是隨包的離線轉檔腳本，不在 runtime 路徑）。
- **SMTP**：不直接發，走注入的 `notification_service`（`detection_orchestration_service.py:2205` `self._wf_svc.notification_service`）。
- **檔案系統**：`profiles_dir()`（`plugin.py:82-92`）讀隨包 5.4MB 基準檔；基準壓縮檔解壓走暫存目錄。
- **Redis / 自有快取**：無。

**能不能單獨起成 process**：**部分可以** —— 它自帶 Flask blueprint + migration，形狀上是完整的服務切片。**但 README:114-124 自陳「只能裝在同時有 remote-agent / flow-engine / file-upload / iam 四支的宿主上」**，且 `mount_api=True` 時缺 5 項接線（auth + 三軸 guard + crypto）直接拒絕掛載（`plugin.py:217-230`）。**無 harness**（`ls harness` 不存在），README:133-136 自陳「service 相依鏈深，架 standalone 宿主成本遠高於收益」。

**Q6 通用性**

(a) **一個工單系統能不能直接用**：**不能直接裝** —— pyproject.toml:35-38 硬性依賴 `jedi-remote-agent` / `jedi-flow-engine` / `jedi-file-upload` / `jedi-iam` 四支（且 pyproject 自己在 :24-34 用大段註解標記這是「待償的疆界依賴、不是正常的插件依賴」）。裝了之後還要實作 5 張 port + 3 軸授權 guard + crypto。**概念上**「工具目錄→派工→收報告→存紀錄」對工單系統是有意義的，但實際耦合面太寬。

(b) **寫死的產品知識**：
1. `common/event_code.py:18-20` — 三個 Guidant 稽核事件碼**數值硬編**（6120/6121/6122），檔頭 :4-9 自陳是主專案 `EventCode` 的「逐字複製」且「值凍結」。
2. `migrations/001-detection-tables.sql` COMMENT 大量帶 Guidant 內部編號：`FR-056.1`（:88 附近）、`FR-056.2`、`FR-056.4`、`FR-060.1`、`FR-060.2`、`FR-067.2`、`D10`/`D12`/`D17`/`D19`、`CM-952`（:449 等多處）—— 這些會**進 DB comment 落到客戶的庫裡**。
3. `infra/detection_tools/model/detection_tool.py:18` — code 欄位 comment 硬列「openvas/nessus/sonarqube」。
4. `jedi_detection/profiles/` 5.4MB **全部是台灣 TWGCB 基準**（8 個 twgcb-01-0xx 目錄 + gcb-demo-win），檔案本身即產品市場假設。
5. `common/detection_job_binding_error_code.py:27` — 註解記「與主專案 SSP Doc Parser 的 GRC_400105 撞號修復」，即 error code 命名空間與**宿主的 SSP 模組**共享。
6. `IEvidenceSink` port 名字裡的 "Evidence"（佐證）本身是稽核概念，雖已 port 化但 port 名仍帶產品詞（`domain/ports.py:55`）。
7. `app/dto/detection_profile_dto.py:147` 等多處註解用「稽核欄位」指 created_user/updated_user（用詞習慣，非行為）。

**Q7 插件完整度**

| 項目 | 有/無 | 證據 |
|---|:--:|---|
| ① api 層隨包 | **有** | `jedi_detection/api/routes/`（3 檔 1050 行）+ `api/__init__.py::mount_routes()` 35 條 |
| ② `register()` | **有** | `plugin.py:269` `register(app, adapters=None, config=None, schema_extensions=None, mount_api=True) -> PluginHandle`；另 `create_blueprint(adapters, config, schema_extensions)` :233 |
| ③ migrations/ 隨包 | **有，2 支** | `migrations/001-detection-tables.sql`（11 表 + 序列 + 索引 + 約束 + COMMENT）、`002-detection-rls-grants.sql`（RLS + GRANT）；入口 `plugin.py:68 iter_migrations()`；pyproject.toml:49-52 顯式 include |
| ④ DI container 預設 | **無** | 套件內 0 個 container 檔；容器全在宿主 `di_containers/detection_tools/`（28 處）+ `detection_execution/`（4 處）|
| ⑤ 獨立 harness | **無** | 無 `harness/` 目錄；README:133-136 明說「無 harness」並給理由 |
| ⑥ 接入 README | **有** | README.md 145 行，含 quickstart / 四道防線 / port 表 / migration / 誠實聲明 |

四有兩無（缺 DI container 預設 + harness）。另 `plugin.py:82 profiles_dir()` 是額外的隨包資料入口。

**Q8 糾纏對象**

**最像同一件事的兩半：jedi-remote-agent（派工執行面）與 jedi-flow-engine（任務狀態面）—— 兩者性質不同。**

*對 remote-agent 的事實*：
1. **綁定表的分派列直接持有 agent 身分**：`config.job_execution_detection_tool_agents.agent_uid` soft-ref → `compliance.remote_agents.uid`（`job_execution_detection_tool_agent.py:33` 註解，跨 schema 不建 FK）。執行紀錄也持有 `agent_task_uid` soft-ref → `compliance.agent_tasks.uid`（migration COMMENT :447）。
2. **detection 側每次派工都要問 remote-agent**：`IAgentDirectory` 四個方法（找機器 / 診斷 / 依 id 取 / 批次取）。
3. **兩邊共用同一組認證原語**：detection 的三支對外 client 直接 import remote-agent 的 `jwt_util` + `build_cloud_mtls_context`（6 處，見 Q3）——這是「同一條資料面通道的兩端」。
4. **但方向是單向的**：remote-agent → detection **0 import**（`domain/ports.py:22-23` 記 AST 全掃結果）；remote-agent 需要檢測知識的兩處已 port 化到宿主 `app/remote_agent/adapter/`。**套件層從來沒有迴圈**（同檔 :28）。

*對 flow-engine 的事實*：
1. **綁定表以 job_execution 為軸**：`job_execution_detection_tools.job_execution_id` 是軟參照 job_executions.id，且 `uq_jedt_job_active` 唯一索引就建在 `job_execution_id` 上（`migrations/001:437`）——一個任務最多一筆有效綁定。
2. **DEV 實查：綁定表對 `job_executions` 0 條 FK**（`pg_constraint` 查詢確認），對 `config.detection_tools` **有硬 FK `fk_jedt_detection_tool`**。這是 `domain/ports.py:40-42` 用來裁定「綁定表留檢測側」的 D17 律①證據，實查吻合。
3. **detection 的 orchestration service 每個進入點都先查 job**：8 處 `get_job_execution(JobExecutionQueryEntity(uid=...))`（`detection_orchestration_service.py:190, 871, 1108, 1268, 1535, 1576, 1645, 1918`），且判 `job.status != JobStatus.PROCESSING.value` 才准動（:197, :1661）。**一邊沒有另一邊就沒意義**：拔掉 flow-engine，detection 的派工鏈全部進不去。
4. **但只依賴三種型別**（enum / error code / query entity），零 ORM、零 repo。

*對 jedi-file-upload 的事實*：**唯一一處 ORM 直查** —— `infra/detection_execution/repository/detection_report_file_query.py:15` import `UploadFile` model 並 `session.query(UploadFile.id, UploadFile.uid)`（:28-32）。檔頭自陳理由是「`UploadFileDomainService` 沒有『一批 id』的入口，逐筆查就是每列一次 round trip」。這是**為效能而破疆界**的形狀，不是概念上的同一件事。

**看起來像但其實不是一件事**：
- **jedi-evidence-classification**：兩支都「跑一個外部東西、收回結果、存成紀錄」，且都有 `evidence` 字樣。但實查**雙向 0 import、0 共表、0 共 port、0 共設定鍵**。detection 的 `IEvidenceSink`（`domain/ports.py:55`）是把掃描報告掛成**任務佐證**，指向宿主的 `job_evidences`；evidence-classification 處理的是**雲端硬碟上的證據檔分類**，兩者的 "evidence" 是不同的東西。
- **jedi-oscal-v2**：雙向 0 import。宿主 `app/oscal/service/ssp_control_implementation_service.py:31-36` 同時用兩者，那是宿主組裝。

---

## jedi-evidence-classification

**Q1 它是什麼**

把一批放在 Google Drive 上的證據檔丟給一個 **docker 容器裡的 AI 分類器**去判斷「哪個檔案對應到哪個評估項目（Assessment Objective）」，把結果寫回 Drive 供人審閱改派，最後歸檔到各 AO 的資料夾，並產出三份成效報表（驗證 / 裁決 / 跨批總表）。

README（`jedi-evidence-classification/README.md:1-31`）的疆界宣告與程式碼**相符**。README 特別強調「插件互不相依：runtime 依賴只有 jedi-common」——實查確認：`grep -rho "from jedi_[a-z_]*" jedi_evidence_classification/` 只回 `jedi_common` 與自己，**零 jedi-* 套件依賴**（pyproject.toml:10-26 亦只有 jedi-common + Flask 三件套 + google-api-python-client）。

**Q2 資料**

自有 **2 張表**，都在 `compliance` schema：

| 表 | 存什麼 | 關鍵欄位 |
|---|---|---|
| `compliance.evidence_classification_runs` | 一次分類 run 的結果（鏡像 Drive run folder 的三份檔案） | `run_folder_id`(自然鍵, unique) / tenant_id / project_id / project_uid / ap_uid / **framework_id** / model / confidence_threshold / status / input_file_count / classified_count / **estimated_cost_usd** / report_original(JSONB) / state(JSONB) / container_log(TEXT)（`migrations/001-evidence-classification-tables.sql:23-53`；ORM `infra/model/classification_run_model.py`）|
| `compliance.evidence_classification_ground_truth` | 正解基準（per-tenant per-framework 一列） | tenant_id / **framework_id** / mapping(JSONB) / source / note；自然鍵 `(tenant_id, framework_id)` unique（同 SQL :67-83）|

**跨疆界參照**：
- (a) **真 FK**：**0 條**。migration 檔頭 `001-...sql:15-18` 明寫「本套件的兩張表都是軟參照（D17 律②）：tenant_id / project_id / project_uid / ap_uid / org_unit_id / *_user_id 全部不建跨疆界外鍵約束」。SQL 內 0 處 REFERENCES。
- (b) ORM relationship 跨包：0。
- (c) **軟參照**：`tenant_id`（→ iam tenants）、`project_id` / `project_uid`（→ 宿主 projects）、`ap_uid`（→ `oscal.assessment_plans.uuid`）、`org_unit_id`（→ iam org_units）、`triggered_by_user_id` / `last_edited_by_user_id` / `archived_by_user_id`（→ iam users）、`run_folder_id` / Drive file id（→ Google Drive，外部系統）。
- (d) raw SQL 直打別人的表：0。

**RLS**：兩張表**刻意不掛 RLS**（README:149-152 記 DEV 實查 `pg_class.relrowsecurity` 與 `pg_policies` 皆為零，租戶隔離由應用層 service 帶 tenant_id 承擔，且有測試 `test_002_does_not_enable_rls_on_these_two_tables` 焊死）。

**表名的產品詞**：`evidence_classification_*` —— "evidence"（佐證/證據）是稽核領域詞。**表名無 CMMC/SSP/round/detection**。

**🔴 `framework_id` server_default `'cmmc-l1'` 的確切位置（三處）**：
1. **migration SQL**：`migrations/001-evidence-classification-tables.sql:29` — `framework_id VARCHAR(64) NOT NULL DEFAULT 'cmmc-l1'`（runs 表）
2. **migration SQL**：同檔 `:69` — 同樣宣告（ground_truth 表）
3. **ORM model**：`infra/model/classification_run_model.py:23` 與 `infra/model/classification_ground_truth_model.py:16` — `mapped_column(String(64), nullable=False, server_default="cmmc-l1")`

**另有兩處硬編 cmmc-l1（非 default，是硬拒絕）**：
- `app/service/catalog_builder.py:21-24` — `if framework_id != "cmmc-l1": raise ...("Framework '{}' not yet supported. v1 experimental phase only ships cmmc-l1.")`
- `infra/classifier_container_runner.py:77` — `run(..., framework_id: str = "cmmc-l1", ...)` 方法預設值

以及隨包的兩份 CMMC 資料檔：`resources/cmmc_l1_aos.json`（220 行，框架 AO 目錄）、`resources/cmmc_l1_canon.json`（435 行，正解基準）。

**🔴 Q2 補充：它用的 LLM 呼叫走什麼**

**都不是。它不直接呼叫任何 LLM** —— 沒有 httpx、沒有 openai sdk、沒有 anthropic sdk、沒有宿主 port。實查 `grep -rn "anthropic|openai|httpx|import requests" jedi_evidence_classification/` **零命中（除了兩行 env key 名字的字串）**。

真正的路徑是 **subprocess 起 docker 容器**：
- `infra/classifier_container_runner.py:141-143` — `subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_seconds)`
- cmd 組在 `:117-130`：`docker run --rm -v {job_dir}:/job [-e KEY=VAL ...] cmmc-classifier:latest service-classify --tenant-id ... --evidence-folder-id ... --catalog-file /job/catalog.json --output-dir /job --framework-id ... --model ... --min-confidence ... --workers ...`
- image 名硬編 `DEFAULT_IMAGE = "cmmc-classifier:latest"`（`:26`）
- **API key 靠 `-e` 環境變數轉交給容器**（`:103-115` 的 `env_keys` list 含 `ANTHROPIC_API_KEY`、`DB_SECRET`、`DRIVE_TOKEN_ENCRYPTION_KEY`、三個 `GOOGLE_DRIVE_OAUTH_*`），值從 `(extra_env or {}).get(k) or os.environ.get(k)` 取（`:113`）—— **這裡套件直接讀了 `os.environ`**，與 ai-bot/ai-dashboard 的「套件不讀 env」原則不一致。
- 結果靠**檔案交換**：容器寫 `/job/_state.json`，套件讀回（`:189-197`）。
- 模型白名單在套件內：`app/service/evidence_classification_service.py:83-86` `ALLOWED_MODELS = {"claude-sonnet-4-6", "claude-opus-4-8"}`，預設 `DEFAULT_MODEL = "claude-sonnet-4-6"`（`:79`）。

**唯一的直接對外 SDK 呼叫是 Google Drive**：`infra/evidence_drive_ops.py:17-18` import `google.oauth2.credentials.Credentials` + `googleapiclient.discovery.build`，`:42` `build("drive", "v3", credentials=creds, cache_discovery=False)`。權杖走建構子注入的 `token_provider` callable（pyproject.toml:21-23 註解說明「取權杖才是宿主的事」）。

**🔴 Q2 補充：它跟 ai-bot / ai-dashboard 三支之間有沒有共用**

| 面向 | 結果 |
|---|---|
| **import** | **零**。三支互相 0 import（`grep -rho "from jedi_[a-z_]*" ` 三支各自只有 jedi_common + 自己）|
| **共表** | **零**。evidence-classification 有 2 張表（compliance schema）；ai-bot **0 表**（README:7「無 DB 表、無 migration」）；ai-dashboard **0 表**（README:55、:211）|
| **共 port** | **零**。三支的 port 定義完全不相交：evidence-classification 5 張（IProjectDirectory / IProjectRoleGuard / IEvidenceSource / IControlCatalog / IDocumentConverter）；ai-bot 1 張（IChatHistoryStore）；ai-dashboard 0 張 ABC（只有 adapters 欄位 + DashboardApiRegistry）|
| **共設定鍵** | **有，一個：`ANTHROPIC_API_KEY`**。三支都用它但**用法完全不同**：ai-bot 由宿主讀後傳進 config（`api/ai/__init__.py:42` `AiBotConfig(api_key=os.getenv("ANTHROPIC_API_KEY"))`，且 :40 註解明寫「與 AI Dashboard 共用同一把 key」）；ai-dashboard 是**套件自己讀 env**（`infra/ai_client/claude_client.py:37` `os.getenv('ANTHROPIC_API_KEY')`）；evidence-classification 是**讀 env 後用 `-e` 轉交給 docker 容器**（`classifier_container_runner.py:109,113`）。三種讀法、三個位置、零共用程式碼。|
| **共 AI 供應商** | 都用 Anthropic Claude，但**模型不同**：ai-bot `claude-haiku-4-5-20251001`（`ai_bot_service.py:24`）、ai-dashboard 由 `provider`/`speed` 參數決定（三家可選）、evidence-classification `claude-sonnet-4-6` / `claude-opus-4-8`（`evidence_classification_service.py:83-86`）|

**結論（事實層）：三支 AI 套件之間唯一的交集是一個環境變數名字，且各自獨立讀取。**

**Q3 Port**

*需要宿主提供（5 張，`domain/ports.py`，全部是 `Protocol` + `@runtime_checkable` 不是 ABC）*：
| port | 方法簽名 | 缺了 |
|---|---|---|
| `IProjectDirectory` (:47) | `get_by_uid(project_uid) -> Optional[Any]` | **拒絕掛載** |
| `IProjectRoleGuard` (:58) | `is_project_manager(project_id, user_id) -> bool` / `is_any_project_manager(user_id) -> bool` | **拒絕掛載** |
| `IEvidenceSource` (:78) | `is_connected(tenant_id) -> bool` / `get_folder_id(tenant_id, scope, scope_uid) -> Optional[str]` | **拒絕掛載** |
| `IControlCatalog` (:94) | `build_classifier_catalog(project_id) -> Optional[dict]` | 降級：`EC_CATALOG_BUILD_FAILED` |
| `IDocumentConverter` (:109) | `convert_to_pdf(data: bytes) -> bytes` | 降級：回原始 bytes |

`REQUIRED_WIRING = ("auth_required", "project_directory", "project_role_guard", "evidence_source")`（`plugin.py:177`）。

另有兩個常數 `SCOPE_EVIDENCES = "EVIDENCES"` / `SCOPE_AP = "AP"`（`domain/ports.py:42-43`），檔頭自陳是**宿主 `drive_folder_mappings.scope_type` 字面值的複製**（「只複製真的會用到的兩個」）。

*提供給別人*：`jedi_evidence_classification.plugin` 的 `register`（:240）/ `create_blueprint`（:210）/ `build_service`（:259）/ `iter_migrations`（:50）/ 三個 dataclass。

*宣告了但零使用*：`SchemaExtensions`（`plugin.py:89`）—— README:70、:174-175 自陳「插槽開齊，尚無產品使用」。

**🔴 `IControlCatalog` 的宿主實作實際上打的是 oscal-v2**：宿主 `infra/evidence_classification/adapters.py:109-128` `LivingSspControlCatalogAdapter.build_classifier_catalog()` → 取 `project_extension.living_ssp_id` → `di.oscal_container.ssp_control_implementation_service().build_classifier_catalog_by_ssp_id(...)`。即 **evidence-classification 與 oscal-v2 之間隔著一張 port + 宿主 adapter，套件層零耦合**。

**Q4 消費者**

(a) 主專案：**23 處**（含 test 8 處），扣 test 約 **15 處**：
| 檔案 | import 什麼 |
|---|---|
| `api/evidence_classification/__init__.py:16` | `plugin` 的四個入口 |
| `di_containers/evidence_classification/evidence_classification_containers.py:19-35` | app service / 2 支 domain service / `ClassifierContainerRunner` / `EvidenceDriveOps` / 2 支 repo impl（**共 8 個 import**）|
| `infra/evidence_classification/adapters.py` | 五張 port 的實作（`:36 ProjectDirectoryAdapter` / `:46 ProjectRoleGuardAdapter` / `:79 DriveEvidenceSourceAdapter` / `:109 LivingSspControlCatalogAdapter` / `:131 LibreOfficeDocumentConverterAdapter`）|
| `test/` ×8 檔 | report builder / db persist / job registry / error code 唯一性 / FR-048 guard |

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

**Q5 執行期形狀**

- **route：10 條**，1 blueprint（`api/__init__.py:65-102`）：`/project/<uid>/ap/<uid>/classify-evidence`（觸發）、`/project/<uid>/classify-evidence/jobs`、`/.../jobs/<job_uid>`、`/.../summary`、`/classification-run/<id>/state`、`/archive`、`/file/<id>/preview`、`/report/validation`、`/report/adjudication`、`/classification-ground-truth`。
- **背景 thread**：`app/service/evidence_classification_service.py:244` `threading.Thread(...)` —— 觸發分類後開背景執行緒跑容器（HTTP 立即返回 job_uid，前端輪詢）。`app/service/job_registry.py:14,21` 用 `threading.Lock` 保護一個 **in-memory dict**。
- **🔴 in-memory job 狀態**：README:176-179 自陳「`JobRegistry` 是 in-memory 的（實驗期設計，原碼註記 Production will swap for DB-backed jobs）：BE 重啟後進行中的 job 狀態會消失」。
- **subprocess：有** —— `docker run`（見 Q2）。這是**五支裡唯一起子行程的**。
- **對外 HTTP：有** —— Google Drive v3 API（`infra/evidence_drive_ops.py:42`）。
- **檔案系統：有** —— job 目錄預設 `~/.cm-jobs/<job_uid>`（`classifier_container_runner.py:63`，mode 0o700），寫 `catalog.json`、讀 `_state.json` / `_report-original.json` / `_container-log.txt`。
- **SMTP / S3 / socket / Redis**：無。
- **DB**：2 張表，走 jedi-common session。

**能不能單獨起成 process**：**形狀上最完整的一支** —— 有 api + register + 2 支 migration + harness（`harness/dev_app.py` + `harness/docker-compose.yml`，Postgres port 55499）+ runtime 依賴只有 jedi-common。**但**：① 需要 host 上有 docker CLI 與 `cmmc-classifier:latest` image；② README:171-173 誠實聲明「harness 只驗到掛得起來 + 認證守門生效 + URL 正確，沒有打到 DB、沒有真的跑一次分類」。

**Q6 通用性**

(a) **一個工單系統能不能直接用**：**技術上最接近可以**（唯一 runtime jedi 依賴是 jedi-common，五張 port 都是窄介面）。**但實質不行** —— 整支的核心語意是「把檔案分類到 Assessment Objective」，AO 是 NIST/CMMC 合規概念；且 `framework_id` 只認 `cmmc-l1`。工單系統要用，等於只用它的「docker 分類器編排 + Drive 檔案操作」骨架，領域邏輯全要換。

(b) **寫死的產品知識**：
1. **`framework_id` default `'cmmc-l1'`**：`migrations/001:29` + `:69`（DDL DEFAULT）、`infra/model/classification_run_model.py:23` + `classification_ground_truth_model.py:16`（server_default）。
2. **`catalog_builder.py:21-24` 硬拒非 cmmc-l1**：`if framework_id != "cmmc-l1": raise`。
3. **docker image 名硬編**：`classifier_container_runner.py:26` `DEFAULT_IMAGE = "cmmc-classifier:latest"` —— image 名字裡就是產品框架名。
4. **容器子命令硬編**：`:121` `"service-classify"`。
5. **隨包 CMMC 資料**：`resources/cmmc_l1_aos.json`（220 行）、`resources/cmmc_l1_canon.json`（435 行）。
6. **模型白名單硬編**：`evidence_classification_service.py:83-86` 兩個 Claude 模型。
7. **Drive 資料夾命名前綴中文硬編**：`:77` `RUN_FOLDER_NAME_PREFIX = "自動分類"`。
8. **scope 字面值是宿主表的複製**：`domain/ports.py:42-43` `"EVIDENCES"` / `"AP"`，自陳來自宿主 `drive_folder_mappings.scope_type`。
9. **env key 清單硬編且含宿主專有鍵**：`classifier_container_runner.py:103-110` 列 `DRIVE_TOKEN_ENCRYPTION_KEY` / `GOOGLE_DRIVE_OAUTH_*` —— 套件直接讀 `os.environ`（`:113`），破了「套件不讀 env」原則。
10. **AO 資料夾樹格式硬編**：README:16「`[領域] 名稱 / [控制項] 名稱 / [代號] 說明`」是 CMMC domain/practice 的結構。

**Q7 插件完整度**

| 項目 | 有/無 | 證據 |
|---|:--:|---|
| ① api 層隨包 | **有** | `api/routes/` + `api/__init__.py::mount_routes()` 10 條 |
| ② `register()` | **有** | `plugin.py:240` `register(app, adapters, config=None, schema_extensions=None, mount_api=True)`；另 `create_blueprint`(:210) / `build_service`(:259) |
| ③ migrations/ 隨包 | **有，2 支** | `001-evidence-classification-tables.sql`（83 行）/ `002-evidence-classification-grants.sql`（32 行）；入口 `plugin.py:50 iter_migrations()`；pyproject.toml:36-39 顯式 include |
| ④ DI container 預設 | **無** | 套件內無 container；容器在宿主 `di_containers/evidence_classification/`（8 處 import）|
| ⑤ 獨立 harness | **有** | `harness/dev_app.py` + `harness/docker-compose.yml`（Postgres 55499）|
| ⑥ 接入 README | **有** | 197 行，含疆界表 / quickstart / 四道防線 / port 表 / migration / 打包注意 / 誠實聲明 / 契約凍結 |

**五有一無（只缺 DI container 預設）—— 五支裡插件完整度最高的。**

**Q8 糾纏對象**

**最像同一件事的兩半：沒有明確的一支。**這是五支裡耦合最乾淨的。事實：

1. **零套件間 import**：runtime 依賴只有 jedi-common（pyproject.toml:10-26），README:29-31 明載「抽出前直接 import 了 `jedi_task_platform` 與 `domain.participant`，兩者都改走 port」——這兩條**已經斷乾淨了**。
2. **零共表**：兩張表都是自己的，且零跨疆界 FK。
3. **零同交易寫兩邊**：`@transaction` 只包自己的兩張表。

**與 oscal-v2 有「概念上的一半」但已隔開**：`IControlCatalog` 的實質內容是「這個專案的 in-scope 控制集」，那是 OSCAL SSP 的知識（宿主 adapter `infra/evidence_classification/adapters.py:118-128` 實際打到 `oscal_container.ssp_control_implementation_service`）。**一邊沒有另一邊會怎樣**：缺 `IControlCatalog` 只降級成 `EC_CATALOG_BUILD_FAILED`，不拒絕掛載——即分類功能在**沒有 OSCAL 的產品上仍能安裝**，只是觸發時會失敗。這條線已 port 化。

**看起來像但其實不是一件事**：
- **jedi-ai-bot / jedi-ai-dashboard**：同為「AI 功能」，但**零 import、零共表、零共 port**，唯一交集是 `ANTHROPIC_API_KEY` 這個字串（三支各自讀取，見 Q2 補充）。三者的**操作者、輸入資料、輸出形態、持久化模型全部不同**。
- **jedi-detection**：兩支都「跑外部東西收結果」，且都有 evidence 字樣，但 detection 的 evidence 是「掃描報告當佐證」（指向 `job_evidences`），這支的 evidence 是「Drive 上的證據檔」。雙向 0 import、0 共表。

**🔴 操作者是誰 / 輸入資料從哪來**（題目指定必答）：
- **操作者：終端使用者中的「專案管理者」（project manager）**。三張守門 port 有兩張是授權相關，`_require_project_manager`（`evidence_classification_service.py:135-138`）拋 `EC_NOT_MANAGER`；ground-truth 匯入要 `is_any_project_manager`（:147-151）。非背景 job、非平台管理員。FE 入口在專案的 AP 頁（`api.js:503` `EVIDENCE_CLASSIFICATION_TRIGGER(projectUid, apUid)`）。
- **輸入資料**：① **證據檔** —— 租戶接上的 **Google Drive** 上，由 `IEvidenceSource.get_folder_id(tenant_id, "EVIDENCES"|"AP", scope_uid)` 解出資料夾 id（可被 request body 的 `evidence_folder_id_override` 覆蓋，`evidence_classification_service.py:162`）；② **控制集** —— 走 `IControlCatalog` 由宿主從 living SSP 算出（實質是 oscal-v2 的資料）；③ **正解基準** —— 隨包 `resources/cmmc_l1_canon.json` 或租戶自匯入的 `evidence_classification_ground_truth.mapping`。

---

## jedi-ai-dashboard

**Q1 它是什麼**

使用者打一句話（「看一下專案」），套件跑一套**五階段編排**生出一個前端能渲染的儀表板 JSON：① 讓 AI 從「資料源目錄」裡挑一支最合適的查詢 API → ② 呼叫它拿真實資料 → ③ 本地算統計分佈 → ④ 讓 AI 依統計設計版面區塊 → ⑤ 組成 UI JSON。整流程打 **2 次 LLM API**（階段 1 與 4），其餘本地執行。

README（`jedi-ai-dashboard/README.md:9-27`）的五階段表與程式碼**逐一對得上**（`app/service/ai_dashboard_app_service.py:65-105` 的五段註解 `── 階段 1 ──` ~ `── 階段 5 ──`）。README:22-27 說「有哪些資料可以查是宿主的知識」——實查確認登記簿全在宿主 `di_containers/dashboard_apis/`（13 個申報檔、27 條 API）。

**Q2 資料**

**自有 0 張表、0 支 migration。**（`grep __tablename__` 0 命中；無 `migrations/` 目錄；README:55、:211 兩處明說。）

**對別人的表的參照**：
- (a)(b)(c) 全部 0 —— 套件不碰任何表。
- (d) **間接**：`DataAPIService.call_api()` 呼叫的是**宿主注入的 service**，那些 service 會查 DB。README:57-64 特別警告「沒有表不等於不需要 DB session」——route 與兩支 app service 仍掛 `@transaction`（`app/service/ai_dashboard_app_service.py:44`、`data_api_service.py:18` import transaction），consumer 必須先 `init_db(...)`，否則 `TypeError: 'NoneType' object is not callable`。

**表名產品詞**：不適用（無表）。

**Q3 Port**

*需要宿主提供*：**沒有 ABC 形式的 port**，全部是 `AiDashboardAdapters` dataclass 的 duck-typed 欄位（`plugin.py:99-118`）：
| 欄位 | 型別 | 必填 | 缺了 |
|---|---|:--:|---|
| `auth_required` | `Callable[[Callable], Callable]` | ✅ | **拒絕掛載** |
| `license_guard` | 有 `require(resource_type)` 的物件 | ✅ | **拒絕掛載** |
| `services.ai_dashboard_app_service` | `Callable[[], Any]`（provider 不是實例）| 掛 route 時需要 | |
| `locale_provider` | `Callable[[], str]` | ⭕ | 用 `DashboardContext` 預設 `zh_Hant_TW` |
| `response_builder` | `Callable[..., Any]` | ⭕ | 用 `jedi_common.utils.response_util.return_response` |

另有一支**真 ABC**但方向相反：`domain/gateway/ai_client_gateway.py` 的 `IAIClientFactory`（72 行）—— **實作在套件自己內部**（`infra/ai_client/{claude,openai,google}_client.py`），不是宿主要填的。

**🔴 資料源 registry 綁宿主 data API 的形式**（題目指定）：
- **不走 adapters，走 app service 建構鏈**（README:115-138）。宿主組一份 `DashboardApiRegistry` 傳給 `DataAPIService(registry=...)`（`app/service/data_api_service.py:46-59`）。
- **每條資料源是一個 `DashboardApi` frozen dataclass**（`app/registry.py:48-82`）：`api_key`（`"<module>.<動作>"`，**對外契約，AI 的 prompt 會看到**）、`module` / `service` / `method` / `return_type` / `description`（**AI 選 API 的主要依據，措辭即行為**）/ `provider`（🔴 **每次呼叫都重建 service 的 callable，不是實例**）/ `parameters` / `required_params` / `optional_params`。
- **`provider` 是關鍵綁定點**：`data_api_service.py:74-84` `_get_service_instance()` 就是 `api.provider()`，**每次呼叫重新解析不快取**（同檔 :65-70 註解記載改造前有 `_service_cache` 且 service 是 Singleton，是「平常看不出、併發才爆」的壞）。
- **重複 api_key 當場拋**：`app/registry.py:117-124` `DuplicateDashboardApiError`（README:137-138 記理由：靜默覆蓋的症狀是「AI 選了 A 卻查到 B 的資料」）。
- **宿主側申報：13 個檔、27 條 API**（`di_containers/dashboard_apis/`）：auth ×4（get_org_units / get_roles / get_tenants / get_users）、participant ×7、flow_engine ×3、oscal ×2（get_oscal_frameworks / get_oscal_framework_versions）、project ×2、survey ×2、bulletin / device / feedback / module_frame / system_config / system_menu / user_auth_provider 各 ×1。收集在 `di_containers/ai_dashboard/dashboard_registry_wiring.py:40,67`。
- **自動注入的上下文**：`data_api_service.py:104-109` 從 `get_user_context()` 注入 `user_id` / `user_uid` / `user` / `tenant_id` / `org_unit_id`。

*提供給別人*：`jedi_ai_dashboard.plugin` 的 `register` / `create_blueprint` + `jedi_ai_dashboard.app.registry` 的 `DashboardApi` / `DashboardApiRegistry` / `PARAM_KWARGS` / `DuplicateDashboardApiError` + `app/service/*` + `domain/service/*` + `infra/ai_client.AIClientFactory`。

*宣告了但零使用*：`SchemaExtensions`（`plugin.py:85-89`）—— README:78、:212-213 自陳「插槽開齊，尚無產品使用」。

**Q4 消費者**

(a) 主專案：**28 處**：
| 檔案 | import 什麼 |
|---|---|
| `api/ai_dashboard/__init__.py:19-24` | `plugin` 的四個入口 |
| `di_containers/ai_dashboard/ai_dashboard_containers.py:35-40` | `AIDashboardAppService` / `DataAPIService` / `DashboardGenerationDomainService` / `AIClientFactory`（**直接 import 套件的 infra**）|
| `di_containers/ai_dashboard/dashboard_registry_wiring.py:40` | `DashboardApiRegistry` |
| `di_containers/dashboard_apis/*.py` ×13 | `DashboardApi`（+ 7 檔另 import `PARAM_KWARGS`）|
| `common/code/error_code.py:147` | 註解提及 `AiDashboardErrorCode` |

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

**Q5 執行期形狀**

- **route：2 條**，1 blueprint（`api/__init__.py::mount_routes()`，README:186-193 表）：
  - `GET /api/1.0/ai-dashboard/health` — **刻意免認證**（README:91-95：抽出前 FE `DynamicDashboard.vue` 在 `onMounted()` 判斷登入狀態前就打它；`tests/test_url_contract.py` 焊死這個不對稱）
  - `POST /api/1.0/ai-dashboard/auto-generate` — 需認證 + 需 license `"ai-dashboard"`（**連字號不是底線**，README:104）
- **背景排程 / worker / thread**：**無**。五階段全在 request 內同步完成。
- **🔴 對外 I/O：兩次 LLM API 呼叫**（每次 `auto-generate` 請求）：
  - `infra/ai_client/claude_client.py:47,82` — `Anthropic(api_key=self.api_key)`，key 從 `os.getenv('ANTHROPIC_API_KEY')`（`:37`），未設拋 `BadRequestError(AI_DASHBOARD_API_KEY_NOT_SET)`（`:38-39`）
  - `infra/ai_client/openai_client.py:36` — `OpenAI(api_key=...)`，key 從 `os.getenv('OPENAI_API_KEY')`（`:27`）
  - `infra/ai_client/google_client.py:36` — `genai.configure(api_key=...)`，key 從 `os.getenv('GOOGLE_API_KEY')`（`:27`）
  - **🔴 憑證從哪讀：套件自己 `os.getenv`**，不是宿主注入 —— 與 ai-bot 的「套件不讀環境變數、由 config 傳入」原則相反（對照 `jedi-ai-bot/jedi_ai_bot/app/service/ai_bot_service.py:39-50` 收 `api_key` 建構參數）。
  - 三家 SDK **不在 runtime dependencies**（pyproject.toml:21-36：lazy import + `ImportError` 接成「請安裝」的正常回應；`[project.optional-dependencies]` 提供 `claude`/`openai`/`google` extras）。
- **SMTP / S3 / subprocess / socket / Redis / 檔案系統**：全無。
- **DB**：無表，但吃 session（`@transaction`，見 Q2）。

**能不能單獨起成 process**：**技術上可以**（有 api + register + harness，runtime 依賴只有 jedi-common + Flask 三件套），**但起來是空的** —— 沒有登記簿就沒有任何資料可查。harness（`harness/dev_app.py` 199 行）用**假 AI client + 假資料源**跑通全鏈（README:36-49，不打真 LLM 也就不花錢）。

**Q6 通用性**

(a) **一個工單系統能不能直接用**：**這是五支裡最接近「真通用」的一支** —— 零表、零 jedi 套件依賴（只有 jedi-common）、疆界外的「有哪些資料可查」已完全外包給宿主的登記簿。工單系統只要申報自己的 `DashboardApi` 清單就能用。

(b) **但仍有寫死的產品知識**（都在統計/顯示層）：
1. **`analyze_data_stats()` 硬編三個欄位名**：`domain/service/dashboard_generation_domain_service.py:47-51` —
   ```python
   for field, attr in [('project_name','projects'), ('group_name','groups'), ('control_no','controls')]:
   ```
   —— `control_no`（控制項編號）是合規產品欄位；工單系統的資料沒這些欄位，統計就全是 0。
2. **狀態欄位硬編 `job_status`**：同檔 `:57` `item_dict.get('job_status') or item_dict.get('status')` —— `job_status` 是任務平台的欄位名。
3. **`DataStats` DTO 的欄位名本身就是產品詞**：`unique_projects` / `unique_groups` / `unique_controls`（`:61-64`，DTO 在 `domain/dto/data_stats.py`）。
4. **中文顯示名硬編**：`:286` `('project_name','專案'), ('group_name','控制群組')` —— 且是**繁中硬編不走 i18n**。
5. **`data_source` 預設值 `'projects'`**：`:203` `block.get('data_source', 'projects')`。
6. **health 訊息中文硬編**：`plugin.py:81` `health_message: str = "AI Dashboard API 運行正常"`（可由 config 覆寫，較輕）。
7. **license resource_type 字串**：README:104 `"ai-dashboard"`（連字號），是宿主 license 模型的字面值。

**Q7 插件完整度**

| 項目 | 有/無 | 證據 |
|---|:--:|---|
| ① api 層隨包 | **有** | `api/routes/ai_dashboard_route.py`(149行) + `api/serializers/` + `api/guard.py` + `api/__init__.py::mount_routes()` 2 條 |
| ② `register()` | **有** | `plugin.py` 的 `register` / `create_blueprint`（簽名同 D6 四道防線）|
| ③ migrations/ 隨包 | **無（且不適用）** | 無表 → 無 migration（README:211 明說）|
| ④ DI container 預設 | **無** | 容器在宿主 `di_containers/ai_dashboard/`（2 檔）+ `di_containers/dashboard_apis/`（13 檔）|
| ⑤ 獨立 harness | **有** | `harness/dev_app.py`（199 行，假 AI client + 假資料源；無需 docker-compose 因無表）|
| ⑥ 接入 README | **有** | 219 行，含五階段表 / quickstart / 四道防線 / port 表 / 登記簿寫法 / Guidant 接線實例 / URL 表 / SDK extras / 誠實聲明 |

四有兩無（③ 因無表不適用、④ 缺）。

**Q8 糾纏對象**

**最像同一件事的兩半：沒有。這是五支裡最獨立的一支。**

事實：
1. **零表** → 不可能與任何人「想建外鍵」或「同一交易寫兩邊」（Q8 的三條判準前兩條直接不成立）。
2. **零 jedi-* 套件依賴**（除 jedi-common）、**零其他套件 import 它**。
3. **「一邊沒有另一邊就沒意義」——這條成立，但對象是「全部 27 支資料源」而不是某一支**：`di_containers/dashboard_apis/` 的 13 個申報檔涵蓋 auth / participant / flow_engine / oscal / project / survey / bulletin / device / feedback / module_frame / system_config / system_menu / user_auth_provider。拔掉任何**一支**，只是登記簿少幾條、AI 少一個選項；拔掉**全部**，儀表板就無資料可生。這是**一對多的鬆散消費關係，不是兩半**。而且這個依賴**已經是 port 化的**（provider callable），套件層零 import。

**看起來像但其實不是一件事**：
- **jedi-ai-bot**：名字都是 AI、都打 Anthropic、都是插件、都無表（ai-bot 也是 0 表）。**但**：零 import、零共 port（IChatHistoryStore vs 無 ABC）、操作者相同但**輸入輸出完全不同**（ai-bot 是自由對話回文字；ai-dashboard 是查產品資料回 UI JSON）。唯一共用是 `ANTHROPIC_API_KEY` 字串，且**讀法不同**（ai-bot 由宿主讀後傳 config；ai-dashboard 套件自己 `os.getenv`）。
- **jedi-evidence-classification**：同為 AI 功能，零交集（見前一節 Q2 補充表）。

**🔴 操作者是誰 / 輸入資料從哪來**（題目指定必答）：
- **操作者：終端使用者（任何登入且有 `ai-dashboard` license 的人）**。`auth_required` + `license_guard.require("ai-dashboard")` 兩道（`plugin.py` REQUIRED，README:81-89）。**沒有角色/manager 守門** —— 但 `DataAPIService` 注入的 `user_id`/`tenant_id` 會傳給下游 service，實際可見範圍由那些 service 與 RLS 決定。FE 入口 `api.js:488` `AI_DASHBOARD_AUTO_GENERATE`。非背景 job、非管理員專屬。
- **輸入資料**：① **使用者的一句話 prompt**（request body）；② **登記簿的 27 條 API metadata**（`description` / `return_type` 進 AI 的 prompt，是階段 1 選 API 的依據）；③ **階段 2 從宿主 service 撈回的真實業務資料**（走 `api.provider()` 現場建 service 再呼叫其 method）；④ 階段 4 送給 AI 的是**統計結果 + 前 5 筆樣本**（`ai_dashboard_app_service.py:90` `sample_data = source_data[:5]`）。

---

## jedi-ai-bot

**Q1 它是什麼**

系統內建的 AI 聊天視窗：使用者送一句話給 Anthropic Claude，套件負責帶上該 user + session 的歷史對話、裁切輪數、把回覆存回歷史。**兩條端點（POST 對話 / DELETE 清歷史）、無 DB 表、無 migration**，對話歷史存在由 consumer 注入的 key-value store。

README（`jedi-ai-bot/README.md:1-18`）與程式碼**完全相符**，且是五支裡自我描述最誠實的（:17 明寫「做到的是安裝時可插拔，不是執行中熱插拔」）。README:12-15 自陳它是 FR-069 D6 插件契約的**首例**，P2–P5 照抄本簽名 —— 實查確認 detection / evidence-classification / ai-dashboard 三支的 `plugin.py` 結構（`AdaptersXxx` / `ConfigXxx` / `SchemaExtensions` / `PluginHandle` / `_RuntimeContext` / `create_blueprint` / `register(mount_api=)`）確為同一形狀。

**規模**：套件源碼 **合計 632 行**（`jedi_ai_bot/` 扣 harness/tests），是五支裡最小的（oscal-v2 15837 / detection 17013 / evidence-classification 3955 / ai-dashboard 2296）。

**Q2 資料**

**自有 0 張表、0 支 migration。**（README:7、:180-183 兩處明說；無 `migrations/` 目錄；`grep __tablename__` 0 命中。）

**對別人的表的參照**：**全部 0**。套件甚至不吃 DB session —— 全套件無 `@transaction`、無 `get_session`、無 SQLAlchemy 依賴（pyproject.toml:10-20 只有 anthropic + Flask 三件套 + marshmallow）。這是五支裡**唯一完全不碰 DB 的**。

歷史資料的落點：`IChatHistoryStore` 的實作（宿主是 Redis）。key 格式硬編在套件：`chat_history:{user_id}:{session_id}`（`app/service/ai_bot_service.py:60`）。

**Q3 Port**

*需要宿主提供*：
- **1 張真 ABC**：`domain/repository/chat_history_store.py:17-33` `IChatHistoryStore` —— `get(key) -> Optional[str]` / `set(key, value, ttl_seconds) -> None` / `delete(key) -> None`。檔頭 :8-9 說明「只有三個方法是刻意的：port 越窄，換後端的成本越低」。
- **`AiBotAdapters` 的三個必填欄位**（`plugin.py:97-118`，dataclass 無預設值即必填）：`history_store`（上述 port 的實作）、`current_user_id: Callable[[], int]`、`auth_required: Callable[[Callable], Callable]`（**刻意無預設值**，:105-107「預設放行會讓忘記傳變成無聲的未授權端點」）。選填 `response_builder`（預設 `jedi_ai_bot.common.response.return_response`）。

*提供給別人*：`jedi_ai_bot.plugin` 的 `register`（:234）/ `create_blueprint`（:186）/ `build_service`（:171）/ `AiBotAdapters` / `AiBotConfig` / `SchemaExtensions` / `PluginHandle` / `EXTENSION_KEY`；`jedi_ai_bot.domain.repository.chat_history_store.IChatHistoryStore`。

*宣告了但零使用*：`SchemaExtensions`（`plugin.py:122-146`）—— docstring :124 自陳「**本套件目前無產品使用**，插槽先開齊」，README:129-143 給了 Guidant 的假想用法（多帶 `project_uid`）但未實作。

**Q4 消費者**

(a) 主專案：**8 處**（含 test 3 處），扣 test **5 處**：
| 檔案 | import 什麼 |
|---|---|
| `api/ai/__init__.py:27` | `AiBotAdapters, AiBotConfig, create_blueprint` |
| `infra/ai/redis_chat_history_store.py:16` | `IChatHistoryStore`（實作 port）|
| `scripts/gen_postman_collection.py:366` | 套件名（Postman 分組）|
| `test/test_module_boundaries.py:245,341,371` | 疆界守衛測試 |

**這是五支裡宿主耦合面最小的**（5 處 vs oscal-v2 的 180 處）。

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

**Q5 執行期形狀**

- **route：2 條，1 個 Resource**（`api/ai_bot_route.py:40` `AiBotRoute`，blueprint name `aibot`，prefix `/api/1.0`，endpoint `/ai-chatbot`，`plugin.py:71-73`）：
  - `POST /api/1.0/ai-chatbot` — body `{message, session_id?}`，回 `{status, data: reply}`
  - `DELETE /api/1.0/ai-chatbot?session_id=xxx` — 清該 session 歷史
  兩條都套 `adapters.auth_required`（`plugin.py:219-226` 用 `type()` 動態產子類，避免污染跨 app 的共用類別）。
- **背景排程 / worker / thread**：**無**。
- **🔴 對外 I/O：一次 Anthropic Claude API 呼叫**（每次 POST）：
  - `app/service/ai_bot_service.py:95-100` — `Anthropic(api_key=self._api_key).messages.create(model=..., max_tokens=..., messages=history)`
  - **🔴 憑證從哪讀：宿主讀後傳進 config，套件不讀 env**。`AiBotConfig.api_key: Optional[str] = None`（`plugin.py:85`），docstring :79-83「純參數值，不含任何行為」，且檔頭 :29-34 專段說明「套件若自己讀 `os.getenv` 或 import 主專案 config，就又把產品綁死在套件裡」。宿主側 `api/ai/__init__.py:42` `AiBotConfig(api_key=os.getenv("ANTHROPIC_API_KEY"))`，且 :40 註解明寫「與 AI Dashboard 共用同一把 key（既有變數，不另立新名）」。
  - **模型預設**：`ai_bot_service.py:24` `DEFAULT_MODEL = 'claude-haiku-4-5-20251001'`（可由 config 覆寫），`:25` `DEFAULT_MAX_TOKENS = 4096`。
  - **失敗降級**：`:102-104` 接住所有 Exception，回固定中文 `API_ERROR_REPLY = "系統錯誤，請稍後再試。"`（`:28`），**不拋例外、不寫入殘缺歷史**（README:217）。
- **SMTP / S3 / subprocess / socket / 檔案系統**：全無。
- **Redis**：**套件不碰**（走 `IChatHistoryStore` port）；宿主實作是 Redis，harness 用自己起的 Redis（port 6399，`harness/docker-compose.yml`，README:34-36 刻意避開 6379）。
- **DB**：**完全不碰**。

**能不能單獨起成 process**：**是，五支裡最容易的** —— 無表、無 DB、無 jedi 套件依賴（連 jedi-common 都沒有，pyproject.toml:10-20 確認）、有完整 harness。README:31-78 的 quickstart 每一步都經實跑驗證（含 `poetry run pytest -q # 26 passed`）。

**Q6 通用性**

(a) **一個工單系統能不能直接用**：**是，五支裡唯一可以真正「直接用」的** —— 零 jedi 依賴、零表、零產品概念。實作三個方法的 store + 給一個 user id 函式 + 給一個認證 decorator 就能跑。

(b) **寫死的產品知識 —— 幾乎沒有，只有三處字面值**：
1. **失敗訊息硬編繁中**：`app/service/ai_bot_service.py:28` `API_ERROR_REPLY = "系統錯誤，請稍後再試。"` —— **不走 i18n、不可由 config 覆寫**（`AiBotConfig` 無此欄位）。這是唯一真正「換產品就得改套件」的點。
2. **history key 前綴硬編**：`:60` `f"chat_history:{user_id}:{session_id}"` —— 多個產品共用同一顆 Redis 會撞 key（無 namespace 參數）。
3. **Swagger tag 硬編**：`api/ai_bot_route.py:42,67` `tags=['AI Bot']`。

**其餘全部可配置**：model / max_tokens / TTL / 輪數上限 / url_prefix / endpoint_path / blueprint_name 都在 `AiBotConfig`（`plugin.py:85-93`）。

**Q7 插件完整度**

| 項目 | 有/無 | 證據 |
|---|:--:|---|
| ① api 層隨包 | **有** | `api/ai_bot_route.py`（76 行，2 條端點）|
| ② `register()` | **有** | `plugin.py:234` `register(app, adapters, config=None, schema_extensions=None, mount_api=True) -> PluginHandle`；另 `create_blueprint`(:186) / `build_service`(:171) |
| ③ migrations/ 隨包 | **無（且不適用）** | 無表；`plugin.py:48` 與 README:180-183「本套件無 DB 表，故無 migration 隨包需求（該機制首例留給 P2–P5 第一支有表的套件）」|
| ④ DI container 預設 | **無** | 套件內無 container；宿主也**沒有** ai_bot 專屬 container（`api/ai/__init__.py:32-39` 直接在 `create_module()` 內組 adapters，不走 DI）|
| ⑤ 獨立 harness | **有** | `harness/dev_app.py`（116 行）+ `harness/docker-compose.yml`（Redis 6399）|
| ⑥ 接入 README | **有** | 218 行，含 quickstart（每步實跑驗證）/ 四道防線表 / adapters 表 / config 表 / 兩種註冊寫法 / **拔掉測試步驟**（:187-194，D6 可插拔驗收）/ 目錄結構 / 行為備忘 |

四有兩無（③ 因無表不適用、④ 缺）。**是 D6 插件契約的參考實作**。

**Q8 糾纏對象**

**最像同一件事的兩半：沒有。這是五支裡耦合最少的（宿主 5 處 import、套件間 0 import、無表、無 DB）。**

三條判準逐條不成立：
1. **想建外鍵**：無表，不成立。
2. **同一交易寫兩邊**：不碰 DB session，不成立。
3. **一邊沒有另一邊就沒意義**：**不成立** —— README:187-194 的「拔掉測試」明確要求「註解掉註冊行 → 服務正常啟動 → 打端點 404 → 其餘端點不受影響 → 還原 → 功能回來」，且宿主 `api/ai/__init__.py` 只有 46 行、`create_module()` 是自足的。

**看起來像但其實不是一件事**：
- **jedi-ai-dashboard**：最容易被誤認 —— 都叫 AI、都打 Claude、都無表、都是插件、**都用同一把 `ANTHROPIC_API_KEY`**（宿主 `api/ai/__init__.py:40` 註解本身就說「與 AI Dashboard 共用同一把 key」）。**但**：① 雙向 0 import；② 0 共 port（`IChatHistoryStore` vs 無 ABC）；③ **key 的讀法相反**（ai-bot 由宿主讀後傳 config、套件宣稱不讀 env；ai-dashboard 套件自己 `os.getenv`，`infra/ai_client/claude_client.py:37`）；④ **模型不同**（haiku vs 由 provider/speed 決定）；⑤ **ai-dashboard 吃 DB session、ai-bot 完全不吃**；⑥ 輸入輸出不同（自由對話文字 vs 產品資料 UI JSON）。共用的只有一個字串常數名。
- **jedi-evidence-classification**：同為 AI 功能，同用 Claude，但 evidence-classification 是 subprocess 起 docker 容器、有 2 張表、有 5 張 port、操作者限 project manager。零交集。

**🔴 操作者是誰 / 輸入資料從哪來**（題目指定必答）：
- **操作者：終端使用者（任何登入者）**。只有 `auth_required` 一道（`plugin.py:116`，宿主傳 `jwt_required()`），**無角色守門、無 license 守門**（對照 ai-dashboard 有 license、evidence-classification 有 manager 守門）。user 身分靠 `current_user_id()`（宿主 `api/ai/__init__.py:34` `lambda: get_user_context().id`）。非背景 job、非管理員。FE 入口 `api.js:485` `AI_BOT`。
- **輸入資料**：① **使用者當下打的一句話**（request body `message`）；② **同 user+session 的歷史對話**（從注入的 store 讀 `chat_history:{user_id}:{session_id}`，`ai_bot_service.py:62-69`，最多留 `max_history_turns`×2 筆 = 預設 20 輪，TTL 3600 秒閒置過期）。**沒有任何產品業務資料進入** —— 它不查 DB、不呼叫任何宿主 service，是純粹的無狀態對話代理 + 歷史快取。

---

## 跨套件觀察

### 本批五支之間的關係矩陣

| 從 ↓ 對 → | oscal-v2 | detection | evidence-cls | ai-dashboard | ai-bot |
|---|---|---|---|---|---|
| **oscal-v2** | — | 0 | 0 | 0 | 0 |
| **detection** | 0 | — | 0 | 0 | 0 |
| **evidence-cls** | 0 | 0 | — | 0 | 0 |
| **ai-dashboard** | 0 | 0 | 0 | — | 0 |
| **ai-bot** | 0 | 0 | 0 | 0 | — |

**🔴 本批五支之間 import 全零。** 所有交會都發生在**宿主的組裝層**，不在套件層。

### FK / 表 / port / 設定鍵 供需表

| 面向 | oscal-v2 | detection | evidence-cls | ai-dashboard | ai-bot |
|---|---|---|---|---|---|
| 自有表數 | **45**（oscal schema）| **11**（config ×9 / compliance ×2）| **2**（compliance）| **0** | **0** |
| 跨疆界真 FK | 0 | 0 | 0 | — | — |
| 疆界內 FK | 23 種 ORM 宣告 | **7 條（DEV 實有，但套件 migration 不含）** | 0 | — | — |
| 軟參照 | 1 處（poam_milestone assignee）| 6 種（agent_task / job_execution / upload_file / job_evidence / remote_agent）| 8 種（tenant/project/ap/org/3×user/drive）| — | — |
| 需宿主的 port | **0** | **5**（+3 軸 guard + crypto）| **5** | **0 ABC**（5 個 adapter 欄位）| **1**（+2 必填欄位）|
| 對外 LLM | 無 | 無 | **間接**（docker `-e` 轉交 key）| **直接**（3 家 SDK，套件自讀 env）| **直接**（Anthropic，宿主傳 key）|
| 對外 HTTP | 無 | **有**（httpx → agent mTLS，3 條）| **有**（Google Drive v3）| 無（除 LLM）| 無（除 LLM）|
| subprocess | 無 | 無 | **有**（`docker run`）| 無 | 無 |
| 自起 thread | 無 | **有**（抽取 worker + 3 通知）| **有**（分類 worker）| 無 | 無 |
| 宿主排的 tick | 無 | **有**（15 分逾時收斂）| 無 | 無 | 無 |
| 吃 DB session | **是** | **是** | **是** | **是（無表卻吃）** | **否** |
| 宿主 import 處數 | **180** | ~60（扣 test）| ~15 | 28 | **5** |
| 其他套件 import 它 | **38**（compliance-audit）| 0 | 0 | 0 | 0 |
| route 條數 | **0** | **35** | **10** | **2** | **2** |
| register() | ✗ | ✓ | ✓ | ✓ | ✓ |
| migration 隨包 | ✗ | ✓(2) | ✓(2) | n/a | n/a |
| harness | ✗ | ✗ | ✓ | ✓ | ✓ |
| 接入 README | ✗(7行) | ✓(145) | ✓(197) | ✓(219) | ✓(218) |
| runtime jedi 依賴 | jedi-common | **remote-agent + flow-engine + file-upload + iam + common** | **只有 common** | **只有 common** | **零** |

### 共用設定鍵（唯一的跨套件交集）

`ANTHROPIC_API_KEY` 被三支使用，**三種讀法、零共用程式碼**：

| 套件 | 讀法 | 位置 |
|---|---|---|
| ai-bot | **宿主讀 → 傳 config**（套件宣稱不讀 env）| 宿主 `api/ai/__init__.py:42`；套件 `plugin.py:85` |
| ai-dashboard | **套件自己 `os.getenv`** | `infra/ai_client/claude_client.py:37` |
| evidence-cls | **套件讀 env → `-e` 轉交 docker 容器** | `infra/classifier_container_runner.py:109,113` |

（另 ai-dashboard 還讀 `OPENAI_API_KEY`（`openai_client.py:27`）與 `GOOGLE_API_KEY`（`google_client.py:27`）；三個 key 在宿主 `.env:78-80` 都有。）

### 兩支對宿主/他人有實質資料層糾纏的

**① oscal-v2 ↔ jedi-compliance-audit**（本批唯一的跨套件強耦合）
- compliance-audit → oscal-v2 **38 處單向 import**，深入到 repo_impl 層
- compliance-audit 在 **`oscal` schema 內建自己的 2 張表**（`ssp_reference_documents` / `ssp_reference_document_mappings`）
- `compliance.poams.ar_finding_id` → `oscal.assessment_findings.id` 是**無 FK 的軟參照**（跨 schema）
- 同名不同物：`compliance.poams` vs `oscal.poams`（DB 實查欄位完全不同）

**② detection ↔ remote-agent / flow-engine / file-upload / iam**（README 自陳的四支待償依賴）
- 型別層直接相認：認證原語 ×6 處（remote-agent）、狀態 enum + error code + query entity ×5 處（flow-engine）、**ORM model 直查 ×1 處**（file-upload `UploadFile`）、query entity ×2 處（iam）
- pyproject.toml:24-38 用大段註解標記這四條「不是正常的插件依賴」，並明載「本套件目前只能裝在同時有這四支套件的宿主上」

### `oscal` schema 的多主現象（DB 實查）

DEV `oscal` schema 共 **58 張表**，分屬四方：
- jedi-oscal-v2：**45 張**
- jedi-compliance-audit：**4 張**（`ap_docx_parse_jobs` / `ar_xlsx_parse_jobs` / `ssp_reference_documents` / `ssp_reference_document_mappings`）
- 宿主主專案：**3 張**（`ssp_docx_parse_jobs` / `ssp_excel_parse_jobs` / `framework_parse_jobs` — 後兩張在 `infra/oscal/model/`）
- **無主 6 張**：`component_definitions` / `cd_capabilities` / `cd_components` / `cd_control_implementations` / `cd_implemented_requirements` / `cd_statements` —— grep 全 monorepo + 宿主 **0 個 Python model 宣告**（OSCAL Component Definition 的表結構，疑似建了但沒接程式碼）

`oscal` schema 只有 **4 張表開 RLS**（全部是 parse_jobs 類，**沒有一張是 oscal-v2 的**）；oscal-v2 的 45 張表零 RLS、零 tenant_id 欄位。

### 三支 AI 套件的定位對照（題目指定的操作者 / 輸入資料）

| | ai-bot | ai-dashboard | evidence-classification |
|---|---|---|---|
| **操作者** | 終端使用者（任何登入者）| 終端使用者（登入 + `ai-dashboard` license）| 終端使用者中的**專案管理者**（manager）|
| **守門層數** | 1（auth）| 2（auth + license）| 3（auth + project 存在 + manager 角色）＋ Drive 連線檢查 |
| **輸入資料** | 使用者一句話 + 該 session 歷史（KV store）| 使用者一句話 + 27 條 API metadata + 宿主 service 撈回的業務資料 | Google Drive 資料夾內的證據檔 + living SSP 控制集 + 正解基準 JSON |
| **輸出** | 一段文字 | UI JSON（Ultima 格式）| Drive 上的分類資料夾樹 + `_state.json` + 3 份報表 + 2 張表的紀錄 |
| **持久化** | KV store（Redis，TTL 1h）| **無** | 2 張 DB 表 + Drive 檔案 |
| **LLM 呼叫次數/請求** | 1 | 2 | N（容器內每檔一次，`--workers 5` 併發）|
| **成本可見度** | 無記錄 | 回傳 token 統計（不落庫）| **`estimated_cost_usd` 落庫** |
| **執行模式** | 同步（request 內完成）| 同步（request 內完成）| **非同步**（背景 thread + docker，前端輪詢）|
| **狀態遺失風險** | 歷史 TTL 過期即失 | 無狀態 | **JobRegistry in-memory，BE 重啟即失**（README:176-179）|

---

## 盤點過程中發現的、與文件記載不符之處（事實陳述，不下建議）

1. **detection 隨包 migration 缺 7 條 FK**：`migrations/001-detection-tables.sql` 0 處 REFERENCES（實測 `grep -ci` = 0），但 DEV 實查這 11 張表上有 7 條**檢測疆界內部** FK（`detection_profile_controls→versions`、`versions→profiles`、`profiles→current_version`、`param_schemas→tools`、`jedt_agents→jedt`、`jedt→tools`、`tenant_configs→tools`）。README:100-103 討論了 RLS 的取捨、`domain/ports.py:40-42` 引用了 `fk_jedt_detection_tool` 當裁決證據，但**沒有任何地方說明隨包 migration 為何不含這些內部 FK**。用套件 migration 裝出來的庫，參照完整性弱於 DEV 現況。

2. **evidence-classification 套件直接讀 `os.environ`**：`infra/classifier_container_runner.py:113` `(extra_env or {}).get(k) or os.environ.get(k)`，讀 9 個 key（含 `DB_SECRET`、`ANTHROPIC_API_KEY`、`DRIVE_TOKEN_ENCRYPTION_KEY`、3 個 `GOOGLE_DRIVE_OAUTH_*`）。這與同批 ai-bot 明文宣示的「套件不讀環境變數」原則（`plugin.py:29-34`）相反，且 README 未提及。

3. **ai-dashboard 套件直接讀 `os.getenv`**：三支 client 各讀一個 API key（`claude_client.py:37` / `openai_client.py:27` / `google_client.py:27`）。README:99-107 的 port 表沒有列 api_key 這一項，`AiDashboardConfig`（`plugin.py:66-81`）也無 api_key 欄位 —— 即**憑證是唯一繞過 adapters 契約的東西**。

4. **oscal-v2 的 README 與實際規模嚴重不成比例**：7 行 README 對 15837 行源碼、45 張表、180 處宿主 import、38 處跨套件 import。且 README 稱其為 "service"，實際無 route / 無 register / 無 migration / 無 harness。

5. **detection 的 `event_code.py` 把宿主稽核事件碼數值複製進套件**：`common/event_code.py:18-20`（6120/6121/6122），檔頭自陳「值凍結，改了等於回溯竄改稽核軌跡」。這是「產品業務碼進套件」的實例，檔頭本身也承認 audit_log 上移 jedi-common 時「事件碼仍留主專案，不屬套件」。

6. **detection migration 的 DB COMMENT 帶 Guidant 內部追蹤編號**：`FR-056.x` / `FR-060.x` / `FR-067.2` / `D10` / `D12` / `D17` / `D19` / `CM-952` 等散布於 `001-detection-tables.sql:445-470`，這些會隨 migration 落進任何 consumer 的資料庫。

---

## 未查證事項

- **jedi-detection 的 `002-detection-rls-grants.sql`（9476 bytes）未逐行讀**，只確認存在與 README 對它的描述（7/11 張表掛 RLS）。未實查 DEV 的 `pg_policies` 驗證那 7 張是否真的與 README 一致。
- **`oscal` schema 那 6 張無主的 `cd_*` / `component_definitions` 表**：只確認 grep 不到 Python model 宣告，未查 `scripts/sql/` 內是否有建表腳本、未查表內是否有資料。
- **oscal-v2 的 45 張表未逐檔細讀**（依指示「每張一句話」），表用途取自 `__table_args__` 的 comment 字串，未逐一比對欄位定義與實際 DB schema。
- **detection 17013 行源碼未全讀**：`detection_orchestration_service.py` 約 2200+ 行只讀了 thread / flow-engine import / 通知段；`detection_job_binding_handler.py`（600+ 行）只 grep 未細讀。可能有未被 grep 關鍵字命中的對外 I/O 或跨疆界呼叫。
- **未驗證 STG / POC 的 schema 是否與 DEV 一致**（本次 DB 查證只做 DEV）。
- **未查證各套件在 Nexus 上的實際發版版本與本地源碼是否一致**（讀的是 monorepo 工作副本；宿主 `pyproject.toml` 當下有未 commit 修改 `M pyproject.toml`，可能處於 path dependency 狀態）。
- **`jedi_detection/profiles/tools/*.py`（twgcb2inspec / gpo_extract / linux_rules 等）未讀**，只確認它們不在 runtime import 路徑上（grep 套件內無人 import 它們）；若實際上有透過檔案路徑動態執行，本次未發現。
- **ai-dashboard 的 `harness/dev_app.py`（199 行）與各套件 tests 未讀**，Q7 的 harness 有無只憑檔案存在判定，未實跑驗證其宣稱的能力。
