本卡屬 FR-162(母卡見「母卡」欄),批 3,等 1b,與 2a 平行(不同模組)。新專案把其他框架專案的證據帶進來,後端部分。只改 BE repo。

這張卡做什麼(白話)

客戶做完 CMMC 再開 ISO 專案時,系統要能說「你 CMMC 專案有 87 個評估目標的證據可以直接用」,並讓他看一張預覽表、按一次全部帶入。帶入的是檔案關聯不是複製:每個專案各一筆證據紀錄指到同一個檔,標「從 CMMC 專案帶入」。這張卡做配對查詢、預覽、批次帶入、單筆帶入、以及三個提醒點要用的查詢;畫面在 3b/3c。

首腦已查的接點

專案→框架版本:compliance.projects.module_frame_id → compliance.module_frames.oscal_framework_version_uid
  → oscal.framework_versions.uid(專案 clone 的 catalog framework_version_id 是 NULL,不能從 catalog 回溯;
  DEV 五個有 living SSP 的專案皆可由此回溯,2026-10-24 實查)
任務↔AO:public.workflow_execution_control_mapping(workflow_execution_id, control_id, ao_part_id, round_id)
  ← 證據→AO 的權威鏈(memory feedback_evidence_per_control_scope_wf_control_mapping)
證據:compliance.job_evidences(file_id, job_execution_id, workflow_execution_id, source CHECK 四值,
  inherited_from_round_id, is_deleted);唯一索引 (job_execution_id, file_id) WHERE is_deleted=false AND file_id IS NOT NULL
覆核輪承襲 clone 寫法(照抄形狀):infra/flow_control/repository/reverify_clone_query.py 約 155~215 行
既有候選證據池讀模型(scope 紀律照抄):infra/readmodel/audit/ar_evidence_candidate_query.py
「同租戶」:compliance.projects 只有 tenant_id/org_unit_id,無資訊系統欄(D11)

怎麼做

① 主線 migration(phase=active,scripts/sql/2026-10-xx-fr162-job-evidences-mapped.sql,照 2026-09-17-fr107-job-evidences-ai-classified.sql 改 CHECK 的寫法):job_evidences.source CHECK 加 'MAPPED';加 mapped_from_evidence_id INTEGER、mapping_entry_id BIGINT(soft-ref,不建跨 schema FK);compliance.projects 加 last_mapped_import_at TIMESTAMPTZ。ORM infra/flow_engine/models/job_evidence.py 與 JobEvidenceDTO 同步三欄;projects model 加一欄。

② 共用讀模型 infra/readmodel/audit/mapped_evidence_candidate_query.py(@property session;純查詢):

candidates(project_id, source_project_ids: list[int], job_execution_id: int | None = None) -> list[Row]
步驟:
1. 本專案框架版本 id(上面那條鏈);本專案當前輪的 wecm 列 → {(control_id, ao_part_id, workflow_execution_id, job_execution)}
   job_execution_id 給了就只取那一個任務。
2. 對每個 AO 呼叫套件 ControlMappingResolutionService.resolve(本 fv, control_id, ao_part_id, [來源專案們的 fv])。
3. 來源專案(必須同 tenant_id;由 caller 傳入、本函式再驗一次):其 wecm 列命中目標 control_id
   (objective 層命中則再比 ao_part_id)→ job_evidences is_deleted=false AND source<>'MAPPED'
   AND file_id NOT IN (本專案該任務已有的 file_id)。
4. 每列輸出:本 AO(control_id, ao_part_id, 標題, job_execution_uid)、來源專案(uid, name, 框架名)、
   來源 AO(control_id, ao_part_id, 標題)、relationship、confidence、origin、hops、
   證據[{uid, file_uid(字串 uid, 不是 file_id), file_name, description, created_at, 來源該 AO 最新判定 state}]、
   default_checked = relationship=='equivalent-to' and (confidence or 1.0) >= 0.7(閾值常數 common/constant/mapping.py)
sources(project_id) -> 同租戶其他專案中「至少一個 AO 有命中且有有效證據」的清單:{uid, name, 框架名, 可帶入 AO 數, 證據數}
new_since(project_id) -> 來源專案在已帶入過的 AO 上,created_at > projects.last_mapped_import_at 的新證據數(橫幅③)

③ app service app/flow_control/service/mapped_evidence_app_service.py(@transaction;守門:專案 manager 或任務指派人,沿用 common.authz project-role 軸):sources(project_uid)、preview(project_uid, source_project_uids)、adopt(project_uid, items:[{job_execution_uid, evidence_uid, mapping_entry_id}])、job_candidates(job_uid)、adopt_for_job(job_uid, items)、new_since_last_import(project_uid)。adopt 每筆新開 job_evidences:同 file_id、source='MAPPED'、mapped_from_evidence_id、mapping_entry_id、description='從〈專案名〉〈control_id ao_part_id〉帶入'、hash_algorithm/content_hash 複製;撞唯一索引→跳過計入 skipped;成功後寫 projects.last_mapped_import_at=now();回 {{adopted, skipped, ao_count, unmapped_ao:[...]}}(unmapped 由 preview 算:本專案 AO 中 resolve 回空的)。審計欄位 enrich 照 common/util/audit_nickname.py。

④ route api/flow_control/routes/mapped_evidence_route.py:GET /projects/<uid>/mapped-evidence/sources、POST .../preview、POST .../adopt、GET .../new-since-last-import、GET /jobs/<uid>/mapped-evidence、POST /jobs/<uid>/mapped-evidence/adopt。註冊照 api/flow_control/__init__.py 既有寫法。

⑤ error code:GRC_MAPPED_EVIDENCE_SOURCE_NOT_SAME_TENANT(403)、GRC_MAPPED_EVIDENCE_NOTHING_TO_ADOPT(412)。既有證據清單 API(drawer 讀的)回傳要多三欄(mapped_from_evidence_id、mapping_entry_id、來源專案名),走既有 DTO 加欄,evidenceMapper 由 3c 補。

手測(DEV)

紀律