給下個 session 的 prompt:「讀
docs/features/FR-038-2606-oscal-redesign/handoff/2026-06-15-B3-finish-docpool-mappings-and-control-impl-import-START-HERE-handoff.md,先過「🧭 WHY」+ §0 讀序硬 gate,能答冷接自檢 4 問再碰 code。跑 §6 pre-flight + §7 verify。接手:把『control_identifier / AO → v2 id』的解析 + catalog 標題/AO 清單,從已不存在的舊 AP 表改建到 v2 catalog 鏈,解開 document_pool 的控制項/AO mapping 半 + control_impl_import 整條。」 本檔自包含。狀態 2026-06-15(B3 15/16 route ship + verified;剩 1.5 條卡同一塊地基)。
| 項目 | 值 |
|---|---|
| 進度 | B3 SSP 維護 15/16 route-group 接 v2 + commit、零新回歸。剩 1.5 條卡在「舊 AP 表已不存在」:① document_pool 的控制項/AO mapping 半(4 條 route,池 CRUD ✅ 能用)② control_impl_import(SoA Excel 匯入匯出,整條 disabled) |
| 主專案 branch / HEAD | feature/oscal-refactor / fd668423 |
| 主專案 working tree | 乾淨,只有 M pyproject.toml(jedi-oscal-v2 dev path-dep,照規範勿 commit) |
| 套件 branch / HEAD | ~/Projects/Jedicogy/module/jedi-python-package feature/oscal-refactor / c2b7502(B3 statement CRUD;領先 origin,未 push) |
| ⚠️ 套件未 commit 的 B1 dev 改動(別搞丟/別發版) | M catalog_service.py、M framework_service.py、?? tests/catalog/test_catalog_service_import.py、?? jedi-device/CLAUDE.md、?? jedi-project/CLAUDE.md。dev path-dep、BE 重啟即生效;發版等整 feature + user 明示 |
| 跑得起來嗎 | create_app() BOOT OK;BE python -m pytest test/ = 47 failed + 50 errors(≤ baseline 56f/50e,零新回歸)+ 9 skipped |
| push | BE + 套件都未 push,等 user 明示 |
| 未收尾 | 整個 B3 弧(13+ commits)的 changelog / analysis / SUMMARY / memory / Notion 全未寫。user 還沒下收尾命令。下次 user 說「收尾」一起補 |
FR-038 一句話:把舊「只套 OSCAL 概念、大量自定義扁平欄位」的實作,打掉重練成「正式 OSCAL v1.2.2 物件模型 + 三層 clone/snapshot 邊界」。Wave 2A 已把 BE 從舊 jedi_oscal 全翻到 jedi_oscal_v2、B1~B5 業務全 disable;Wave 2B 在 v2 地基上逐一 re-enable。
B3 = SSP 維護:把「答案卷(SSP)」的 7 個編輯 tab(受評標的 / 參與人員 / 元件 / 資產清冊 / 外部服務 / 控制實作 / 程序書)接回 v2,兩種落點:① 資源庫範本 SSP(compliance.module_frames.template_ssp_id)② 專案 living SSP(B2 clone 出來,存 compliance.project_extensions.living_ssp_id)。前 15 條都做完了(見 §11 commits)。
剩這 1.5 條為什麼卡 —— 舊 AP 結構在新模型不存在:
document_pool 的「控制項/AO 程序書關聯」+ control_impl_import 的「Excel 匯出(控制項標題 + AO 清單)」,舊版都靠這條鏈:
SSP → assessment_plans → assessment_plan_controls / assessment_plan_tasks / assessment_task_controls
但 FR-038 重設計了 AP 模型(B4 已 ship):新 AP 用 ap_reviewed_controls / ap_tasks(per-round),舊的 assessment_plan_controls / assessment_plan_tasks / assessment_task_controls 三張表在新 schema 根本不存在(已實測 MISSING,見 §7)。所以:
infra/grc/repository/ssp_document_pool_query.py 的 get_ap_control_id_by_identifier / get_ap_task_id_by_uid 在 2A 被 stub 成 return None(dark)→ document_pool 的 mapping route 查不到 id、形同壞掉。infra/oscal/repository/ssp_catalog_title_query.py 直接 from jedi_oscal.infra.model.ap... import 舊 AP ORM model 查那三張表 → ① 表不存在 ② import v1 jedi_oscal 進 boot graph 會撞 v2 同名表 MetaData 直接炸(我實測過,§2 trap 1)。目標模型(這一棒要鋪的地基):把「control_identifier / AO → 一個穩定 v2 id」的解析,從舊 AP 改建到 v2 結構:
SspImplementedRequirement.id(per control,套件 SspService 已有 get/list/add)。SspStatement.id(per AO,套件 SspService 本棒已補 statement CRUD)。ssp.import_profile_id → profile_imports.source_catalog_id → catalog_controls.title(控制標題)+ catalog_control_parts(filter name=='assessment-objective')(AO 清單)。這就是 B5/Q1 已在用的 AO 推導(見 §5 範本 app/grc/service/assessment_result_app_service.py:_project_catalog_controls / _derive_ao_map + 共用 ao_derivation.derive_ao_pairs)。冷接自檢 4 問(答不出回 §0 讀序):① 這 1.5 條卡的根因是哪三張表不存在、新模型改用哪兩張?② v2 裡「控制項 → id」「AO → id」分別該對應哪個 entity 的 id?③ 控制標題 + 完整 AO 清單在 v2 從哪條鏈拿(不經 AP)?④ 為什麼不能直接 import
SspCatalogTitleQuery(現況)進 DI?
api-contract.md §9(SSP 維護端點);design.md §4.2(套件 SspService 契約)/ §4.4(套件 vs 主專案邊界)app/grc/service/assessment_result_app_service.py 的 _project_catalog_controls(L127)+ _derive_ao_map(L141)+ 共用 helper app/grc/service/ao_derivation.py 的 derive_ao_pairs(grep 確認路徑)。control_impl_import 的 catalog 標題/AO 清單必用同一套定義(只算 assessment-objective part;見 Q1 handoff 2026-06-15-Q1-prep-jobs-START-HERE-handoff.md §2「AO 定義」)app/oscal/service/ssp_control_implementation_service.py(本棒最重要參照 — 它已把控制實作接到 v2 IR + statement,含 _get_or_create_ir / _get_or_create_statement / _ir_for_control / _statement_for,document_pool mapping 要 reuse 同樣的 IR/statement 解析 + 同一個 context_id 空間)app/oscal/service/ssp_document_pool_service.py(mapping 半)/ infra/oscal/repository/ssp_catalog_title_query.py(整個重寫)/ app/oscal/service/ssp_control_impl_import_service.py(v2 改寫,我上一棒寫過一版又還原了,可從本檔 §3-B 復原)/ssp/<uid>/document-pool GET/POST/DELETE(上傳/列出/刪除程序書到池)。已 commit(e309cb9c)+ real-DB smoke 過。/control-implementation/<ci>/document-mappings GET/POST、.../document-mapping/<doc_uid> DELETE、.../objective/<si>/document-mappings GET/POST、對應 DELETE)。原因:service 內 _get_ap_control_id_or_404 / _get_ao_task_id_or_404 呼 _pool_query.get_ap_control_id_by_identifier / get_ap_task_id_by_uid,這兩個在 ssp_document_pool_query.py:216,221 是 return None(2A dark)→ 永遠 raise GRC_CONTROL_NOT_FOUND / GRC_ASSESSMENT_OBJECT_NOT_FOUND。e309cb9c 時 message 寫「6 條 route 接好」,這是不準的——mapping 半其實沒通(smoke 只驗了池 CRUD,沒 AP 資料沒驗 mapping)。下一棒修好後回頭把這點在 changelog 講清楚。/ssp/<uid>/control-implementations/export(GET 匯出 Excel)/ /import(POST 上傳驗證)/ /import/validate(POST 重驗)/ /import/confirm(POST 確認寫入)。config/di_modules.py EXCLUDE_MODULES、blueprint 未註冊、ssp_control_impl_import_service.py 是 v1 原版(import 舊 jedi_oscal)。SspControlImplementationService.update_control_implementation/update_objective);卡的是 export + validate 需要 SspCatalogTitleQuery(控制標題 + AO 清單),而那支查 MISSING 的舊表 + import v1 model。jedi_oscal 進 boot graph(memory project_oscal_v1_v2_cannot_coexist)。我上一棒把 SspControlImplImportService wire 進 DI、它 chain 到 SspCatalogTitleQuery(from jedi_oscal.infra.model.ap...)→ create_app() 直接炸 Table 'oscal.assessment_plans' is already defined for this MetaData instance。重寫 SspCatalogTitleQuery 必須拔光 v1 import,改用 raw SQL(text())或 v2 catalog repo。assessment_plan_controls / assessment_plan_tasks / assessment_task_controls = MISSING;新 AP = ap_reviewed_controls / ap_tasks = EXISTS。但本棒的標題/AO 來源不是新 AP,是 v2 catalog(AP 是 per-round 凍結快照,維護期的 living SSP 不一定有 AP;catalog 經 profile 才是 always-available 的控制母體)。ssp_control_implementation_service.py(已 ship)的程序書用 ssp_reference_documents 表、context_type control_implementation→IR.id / objective→statement.id。document_pool 的 mapping 用 ssp_reference_document_mappings 表(不同表),context_id 要用同一組 IR.id / statement.id,兩邊才對得起來。scripts/sql/2026-06-15-fr038-b3-ssp-reference-documents.sql,DEV 已套;stg/poc/prod 未套)。SspDocumentPoolQuery 的 list_mappings / add_mappings / resolve_doc_uids_to_ids / get_pool_name_to_uid_map / list_pool 都查這 2 表 + 是 raw SQL、v1-clean、能用——只有 get_ap_* 兩個是 dark。c2b7502):list_statements(ir_id) / get_statement(id) / add_statement / update_statement / delete_statement,加既有 list_implemented_requirements_for_ssp(ssp_id) / get_or_create_control_implementation(ssp_id, user) / add_implemented_requirement。dev path-dep、BE 重啟生效。statement_identifier、在 document_pool AO mapping 也傳 statement_identifier、export Excel 的隱藏欄存 AO 識別子。三處必須是同一個值才串得起來。建議:統一用 catalog AO part 的 part_id(OSCAL AO token,如 ac-1_smt.a 或 [a])當 statement_id。先讀 catalog_control_parts 實際 part_id 長相 + control_implementation route 目前 FE 傳什麼,對齊後再實作(memory feedback_plan_vs_reality_verify_first)。SspPermissionChecker(已 wire 在 oscal_container):讀=require_participant、寫=require_manager。ssp_uid → ctx.ssp.id。python -m pytest;BE boot 補 dummy GITLAB/GITHUB env(見 §6)。catalog_control_parts / profile_imports 實際欄位、derive_ao_pairs 簽章、control_implementation route FE 實傳值,不憑本檔摘要刻。改 app/oscal/service/ssp_document_pool_service.py:
ssp_service(v2 SspService)dep。DI provider(di_containers/oscal/oscal_containers.py 的 ssp_document_pool_service)加 ssp_service=ssp_service。_get_ap_control_id_or_404(ssp_id, control_identifier) 改成 _resolve_ir_id(ssp_id, control_identifier, create):reuse ssp_control_implementation_service 的 _ir_for_control(讀)/ _get_or_create_ir(寫)邏輯(可抽共用 helper 或複製那幾行)。回傳 IR.id。_get_ao_task_id_or_404(statement_identifier) 改成 _resolve_stmt_id(ssp_id, control_identifier, statement_identifier, create):先解 IR、再 _statement_for(讀)/ _get_or_create_statement(寫)→ 回 statement.id。注意現簽章只有 statement_identifier 沒 control_identifier——AO mapping route 其實有帶 control_identifier(list_ao_mappings(ssp_uid, control_identifier, statement_identifier)),把它一路傳進來。list_*_mappings(讀)用 create=False,解不到回 [];add_*_mappings / remove_*_mapping(寫)用 create=True / 解不到回 False。其餘 _pool_query.list_mappings/add_mappings/remove_mapping/resolve_doc_uids_to_ids 不動(查 mapping 表、能用)。SELECT control_id FROM oscal.ssp_implemented_requirements ir JOIN oscal.ssp_control_implementations ci ON ci.id=ir.control_implementation_id WHERE ci.ssp_id=1482 LIMIT 1;沒有就先用 control_implementation route 建一個)。infra/oscal/repository/ssp_catalog_title_query.py(拔光 v1 import):實作 ISspCatalogTitleQuery 三個 method,改用 v2 catalog 鏈:
ssp.import_profile_id → profile_imports.source_catalog_id → catalog_id(raw SQL 或 v2 repo;範本 _project_catalog_controls)。get_titles_by_ssp_id(ssp_id):{control_id: catalog_controls.title}(該 catalog 的 controls)。get_ao_list_by_ssp_id(ssp_id):{control_id: [(stmt_id, ao_title, stmt_id), ...]}——AO 從 catalog_control_parts filter name=='assessment-objective'(reuse derive_ao_pairs 或同邏輯)。第三個 tuple 元素(舊版是 task_uid)改成 stmt_id 自己(沒 AP task 了;隱藏欄存 AO part_id,confirm 時當 statement_identifier)。get_objective_titles_by_ssp_id:照 §6 AO 識別子一致性回 {control_id: {stmt_id: title}}。ssp_id: str(舊版因 assessment_plans.ssp_id 是 VARCHAR)。v2 直接用 int ssp_id,呼叫端配合改(或內部 int(ssp_id))。app/oscal/service/ssp_control_impl_import_service.py 接 v2(我寫過一版、為救 boot 還原了;可從本對話 JSONL 復原,或照下列重做):
(ssp_permission_checker, ssp_service, ssp_ctrl_impl_service, catalog_title_query, document_pool_query),拔掉 v1 ssp_domain_service / ctrl_impl_domain_service / objective_domain_service。_get_ssp_or_404 → permission checker(export/validate/revalidate=require_participant、confirm=require_manager),ssp_id = ctx.ssp.id。_controls_view(ssp_id):ssp_service.list_implemented_requirements_for_ssp + list_statements,組出 mirror 舊 ControlImplementationEntity 介面的 SimpleNamespace(.control_identifier=ir.control_id、.implementation_status/.implementation_description 從 IR props 讀、.objectives=statements 各自 props)。取代所有 _ctrl_ds.get_all_by_ssp_id。implementation-status / implementation-description。self._ssp_svc.update_control_implementation/update_objective(v2)+ doc pool mapping(用 §3-A 修好的 resolve)——把 mapping 部分也改成走新的 IR/statement id(別再用 get_ap_*)。ssp_catalog_title_query Singleton + ssp_control_impl_import_service Factory)→ config/di_modules.py 移除 api.oscal.routes.ssp.ssp_control_impl_import_route → api/oscal/__init__.py 註冊 4 條 route(URL 見 §11 git 對照)。data[:2]==b'PK')+ confirm_import 一筆控制項 + 權限負面(stranger ForbiddenError)。| 檔案 | 為何 |
|---|---|
app/grc/service/assessment_result_app_service.py(_project_catalog_controls L127 / _derive_ao_map L141) |
v2 catalog→controls→AO 的範本,照抄解析鏈 |
app/grc/service/ao_derivation.py(derive_ao_pairs) |
共用 AO 推導(只算 assessment-objective),catalog_title_query 重寫 reuse |
app/oscal/service/ssp_control_implementation_service.py |
已 ship 的 IR/statement 解析範本;document_pool mapping reuse 它的 _get_or_create_ir/_get_or_create_statement,context_id 同空間 |
app/oscal/service/ssp_document_pool_service.py |
§3-A 改:加 ssp_service + IR/statement 解析取代 dark AP 查詢 |
infra/oscal/repository/ssp_catalog_title_query.py |
§3-B-1 整支重寫(拔 v1 import、改 v2 catalog 鏈) |
app/oscal/service/ssp_control_impl_import_service.py |
§3-B-2 v2 改寫(現為 v1 原版) |
infra/grc/repository/ssp_document_pool_query.py |
讀懂:get_ap_* 是 dark(不用它);mapping 表 CRUD 能用 |
di_containers/oscal/oscal_containers.py / config/di_modules.py / api/oscal/__init__.py |
wire provider + re-enable EXCLUDE + 註冊 blueprint |
~/Projects/Jedicogy/.../jedi-oscal-v2/.../catalog/catalog_control_part_repo_impl.py / catalog_control_repo_impl.py |
v2 catalog repo(標題 + AO parts 來源) |
cd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
git branch --show-current # feature/oscal-refactor
git status --short # 僅 ' M pyproject.toml'
git log --oneline -3 # fd668423 / 0662360e / e309cb9c
( cd ~/Projects/Jedicogy/module/jedi-python-package && git log --oneline -1 ) # c2b7502
set -a; source .env 2>/dev/null; set +a
export GITLAB_API_VERSION=4 GITLAB_URL=http://localhost GITLAB_PRIVATE_TOKEN=dummy GITHUB_PRIVATE_TOKEN=dummy
# BOOT(含 blueprint 註冊用 main_app.py 的迴圈才驗得到 route;create_app 只驗 DI)
poetry run python -c "
from dotenv import load_dotenv; load_dotenv()
import eventlet; eventlet.monkey_patch(all=False, socket=True)
import sys; sys.setrecursionlimit(5000)
from core.app_factory import create_app; create_app(); print('BOOT OK')"
# baseline 存起來,之後每次改完 comm -23 比新回歸
poetry run python -m pytest test/ -q -p no:cacheprovider --continue-on-collection-errors 2>&1 | grep -E '^(FAILED|ERROR)' | sort > /tmp/b3fin_base.txt
poetry run python -m pytest test/ -q -p no:cacheprovider --continue-on-collection-errors 2>&1 | tail -1 # 47 failed,...,50 errorscd /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be
set -a; source .env 2>/dev/null; set +a
export GITLAB_API_VERSION=4 GITLAB_URL=http://localhost GITLAB_PRIVATE_TOKEN=dummy GITHUB_PRIVATE_TOKEN=dummy
# 舊 AP 三表 MISSING、新 AP 兩表在、catalog parts 在
poetry run python -c "
from dotenv import load_dotenv; load_dotenv()
import eventlet; eventlet.monkey_patch(all=False, socket=True)
import sys; sys.setrecursionlimit(5000)
from core.app_factory import create_app; create_app(enable_socketio=False)
from jedi_common.session.database.db import session_scope
from jedi_common.session.database.session_context import get_session
from sqlalchemy import text
with session_scope():
for t in ['assessment_plan_controls','assessment_plan_tasks','assessment_task_controls','ap_reviewed_controls','ap_tasks','catalog_control_parts','ssp_reference_documents','ssp_reference_document_mappings']:
r=get_session().execute(text(f\"SELECT count(*) FROM information_schema.tables WHERE table_name='{t}'\")).scalar()
print(f'{t}:', 'EXISTS' if r else 'MISSING')"
# 套件 statement CRUD 在
poetry run python -c "from jedi_oscal_v2.app.service.ssp.ssp_service import SspService; print(sorted(m for m in dir(SspService) if 'statement' in m))"
# 測試資料座標:專案 266 / living_ssp_id 1482 / manager user 17;範本 mf template_ssp_id 1481預期:舊三表 MISSING、ap_reviewed_controls/ap_tasks/catalog_control_parts/程序書池 2 表 EXISTS;statement CRUD 5 method 在。
feature/oscal-refactor;branch 不對停下問 user)。git add 檔名、禁 -am;各 repo 分開);push / 收尾 / 套件發版等 user 明示。pyproject.toml path-dep 勿 commit;套件 dev 改動 commit 進套件 branch 防搞丟、不發 Nexus。jedi_oscal 進 boot graph(§2-1);重寫 query 用 raw SQL / v2 repo。盤點 commits(BE+套件)→ changelog(B3 各 route group + 本棒 + 更正 e309cb9c document_pool mapping 半當時沒通的事實)→ analysis(IR/statement 當 v2 control/AO id 空間 / catalog 鏈取代舊 AP / props 承載 status-description 的決策)→ SUMMARY → 回頭更新橫向文件(api-contract §9 標完工、design §4.2 SspService 補 statement CRUD、frontend-overview 若動 payload)→ memory feedback(≥1 條,建議「舊 AP 表退役、id 解析改建 v2 catalog/IR/statement」+「import 半路 v1 query 炸 boot 的 baseline-stash-diff 抓法」)→ Notion。push 等 user。 stg/poc/prod 記得跟著套程序書池 migration。
2026-06-15-Q1-prep-jobs-START-HERE-handoff.md,跟 B3 平行、不互卡。feature/oscal-refactor,未 push;HEAD fd668423)fd668423 feat(FR-038): B3 — 專案 SSP 控制實作 SoA(control-implementation)接 v2
0662360e feat(FR-038): B3 — 受評標的 ssp-resources(devices/info-systems)接 v2(MF+專案)
e309cb9c feat(FR-038): B3 — 專案 SSP document-pool(程序書池)接 v2 + 補回 2 張漏建表 ← mapping 半當時未通,§1 已註
f15c55b2 feat(FR-038): B3 — 專案 SSP components/inventory/leveraged/party 接 v2
b14af00c feat(FR-038): B3 — 重建 SspProjectResolver(v2) + 專案 SSP system-characteristic 接 v2
96becd58 feat(FR-038): B3 — module_frame inventory / leveraged / party 接 v2 SspService
3e311aca feat(FR-038): B3 — module_frame components 接 v2 SspService
e232ada5 feat(FR-038): B3 vertical slice — module_frame system-characteristic 接 v2 SspService
(更前面 cf9694dd 起為前一棒 import-ssp 設計/實作。)
~/Projects/Jedicogy/module/jedi-python-package,未 push;HEAD c2b7502)c2b7502 feat(oscal-v2): SspService statement CRUD — B3 控制實作 SoA per-AO 編輯地基
d9a93f1e^)/ssp/<ssp_uid>/control-implementations/export (GET)
/ssp/<ssp_uid>/control-implementations/import (POST 上傳驗證)
/ssp/<ssp_uid>/control-implementations/import/validate (POST 重驗 JSON)
/ssp/<ssp_uid>/control-implementations/import/confirm (POST 確認寫入)
Resource 類名:SspControlImplExportRoute / SspControlImplImportRoute / SspControlImplRevalidateRoute / SspControlImplImportConfirmRoute(在 api/oscal/routes/ssp/ssp_control_impl_import_route.py)。
讀 docs/features/FR-038-2606-oscal-redesign/handoff/2026-06-15-B3-finish-docpool-mappings-and-control-impl-import-START-HERE-handoff.md。
先過「🧭 WHY」+ §0 讀序硬 gate(B5/Q1 的 AO 推導範本 + 已 ship 的 control_implementation IR/statement 解析),
能答冷接自檢 4 問(卡的三張舊表/新模型用哪兩張 / 控制與AO各對哪個 v2 id / 標題+AO從哪條 catalog 鏈 / 為何不能 import 現況 SspCatalogTitleQuery)才往下。
跑 §6 pre-flight(BOOT OK + pytest 47f/50e)+ §7 verify(舊 AP 三表 MISSING、catalog/新表在、statement CRUD 在)。
接手解開最後 1.5 條:①§3-A document_pool mapping 半 — service 加 ssp_service、control_identifier→IR.id / AO→statement.id(reuse control_implementation 的 get_or_create,同 context_id 空間)取代 dark 的 get_ap_*。
②§3-B control_impl_import — 重寫 SspCatalogTitleQuery 走 v2 catalog 鏈(ssp.import_profile_id→profile_imports.source_catalog_id→catalog_controls.title + catalog_control_parts 的 assessment-objective)、拔光 v1 import(否則炸 boot);再 v2 改寫 import service + wire + re-enable。
每段 BOOT OK + baseline-diff 零新回歸 + 顯式 git add commit。push/收尾/套件發版等 user 明示;不切 branch;pyproject path-dep 勿 commit。
看本檔 + §0 讀序 + 跑 §6/§7 → 能確認現況(15/16 done、剩 1.5 條卡同一塊地基、commits)、懂 WHY(舊 AP 三表退役 → 控制/AO id 改建 v2 IR/statement、標題/AO 改走 catalog 經 profile)、知道怎麼接(§3-A/§3-B 步驟 + 範本檔 + context_id 一致 + AO 識別子一致性)、知道陷阱(v1 import 炸 boot / dark get_ap_* / migration 多環境)、知道規範界線。不需 user 額外解釋即可開工。