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)


§1

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 兩者,那是宿主的組裝不是套件間的耦合。

§2

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 同時用兩者,那是宿主組裝。

§3

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。

§4

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

§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,是純粹的無狀態對話代理 + 歷史快取。

§6

跨套件觀察

本批五支之間的關係矩陣

從 ↓ 對 → 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)

§7

盤點過程中發現的、與文件記載不符之處(事實陳述,不下建議)

  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 的資料庫。


§8

未查證事項

  • 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 有無只憑檔案存在判定,未實跑驗證其宣稱的能力。