🟢 START HERE — FR-038 B3 收尾:解開最後 1.5 條(document_pool mapping 半 + control_impl_import)

給下個 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 條卡同一塊地基)。

🔖 交接現況(換 session 當下)

項目
進度 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.pyM 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 說「收尾」一起補

🧭 WHY:這件事要幹嘛 + 這 1.5 條卡在哪(先懂才准碰 code)

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.pyget_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 結構

  • 控制項 id → 該控制的 SspImplementedRequirement.id(per control,套件 SspService 已有 get/list/add)。
  • AO id → 該 AO 的 SspStatement.id(per AO,套件 SspService 本棒已補 statement CRUD)。
  • 控制標題 + AO 清單(匯出用)→ 從 v2 catalogssp.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?


§0 接手讀序(按序,1~3 是硬 gate)

  1. 本檔「🧭 WHY」+ 通讀本檔
  2. 🔒 gate:api-contract.md §9(SSP 維護端點);design.md §4.2(套件 SspService 契約)/ §4.4(套件 vs 主專案邊界)
  3. 🔒 gate:B5/Q1 的 AO 推導範本(這一棒的核心參照):app/grc/service/assessment_result_app_service.py_project_catalog_controls(L127)+ _derive_ao_map(L141)+ 共用 helper app/grc/service/ao_derivation.pyderive_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 定義」)
  4. 已 ship 的 B3 canonical 範本: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 空間
  5. 待改檔: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 復原

§1 現況:1.5 條的精確症狀

document_pool(程序書池)— 池 CRUD ✅ / mapping 半 ❌

  • 能用/ssp/<uid>/document-pool GET/POST/DELETE(上傳/列出/刪除程序書到池)。已 commit(e309cb9c)+ real-DB smoke 過。
  • :控制項/AO 關聯 4 條 route(/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,221return None(2A dark)→ 永遠 raise GRC_CONTROL_NOT_FOUND / GRC_ASSESSMENT_OBJECT_NOT_FOUND
  • ⚠️ 我 commit e309cb9c 時 message 寫「6 條 route 接好」,這是不準的——mapping 半其實沒通(smoke 只驗了池 CRUD,沒 AP 資料沒驗 mapping)。下一棒修好後回頭把這點在 changelog 講清楚。

control_impl_import(SoA Excel 匯入匯出)— 整條 disabled

  • 4 條 route:/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)。
  • confirm_import 其實已可接 v2(它 delegate 給已 v2 化的 SspControlImplementationService.update_control_implementation/update_objective);卡的是 export + validate 需要 SspCatalogTitleQuery(控制標題 + AO 清單),而那支查 MISSING 的舊表 + import v1 model。

§2 ⚠️ 會讓你做錯的陷阱 / 已驗事實

  1. 絕不 import v1 jedi_oscal 進 boot graph(memory project_oscal_v1_v2_cannot_coexist)。我上一棒把 SspControlImplImportService wire 進 DI、它 chain 到 SspCatalogTitleQueryfrom 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
  2. 舊 AP 三表不存在(§7 已驗):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 的控制母體)。
  3. context_id 空間要跟 control_implementation 一致ssp_control_implementation_service.py(已 ship)的程序書用 ssp_reference_documents 表、context_type control_implementationIR.id / objectivestatement.id。document_pool 的 mapping 用 ssp_reference_document_mappings 表(不同表),context_id 要用同一組 IR.id / statement.id,兩邊才對得起來。
  4. 程序書池 2 表是本棒前一段補建的(migration scripts/sql/2026-06-15-fr038-b3-ssp-reference-documents.sql,DEV 已套;stg/poc/prod 未套)。SspDocumentPoolQuerylist_mappings / add_mappings / resolve_doc_uids_to_ids / get_pool_name_to_uid_map / list_pool 都查這 2 表 + 是 raw SQL、v1-clean、能用——只有 get_ap_* 兩個是 dark。
  5. 套件 SspService 本棒已補 statement CRUD(pkg 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 重啟生效。
  6. AO 識別子一致性(關鍵設計點,動手前先釘死):FE 對「同一個 AO」在 control_implementation route 傳 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)。
  7. 權限:document_pool / control_impl_import 都走 SspPermissionChecker(已 wire 在 oscal_container):讀=require_participant、寫=require_managerssp_uid → ctx.ssp.id
  8. pytest 用 python -m pytest;BE boot 補 dummy GITLAB/GITHUB env(見 §6)。
  9. plan 假設先 verify:開工前讀 catalog_control_parts / profile_imports 實際欄位、derive_ao_pairs 簽章、control_implementation route FE 實傳值,不憑本檔摘要刻。

§3 修法計畫(方法,開工前 pre-flight 驗)

§3-A:document_pool mapping 半(較單純,建議先做)

app/oscal/service/ssp_document_pool_service.py

  1. 建構子加 ssp_service(v2 SspService)dep。DI provider(di_containers/oscal/oscal_containers.pyssp_document_pool_service)加 ssp_service=ssp_service
  2. _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
  3. _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)),把它一路傳進來。
  4. 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 表、能用)。
  5. smoke:用專案 266 / living_ssp_id 1482 / manager 17,先 add_to_pool 一筆 → add_control_mappings(控制項) + add_ao_mappings(AO) → list 驗 → remove → 清乾淨。控制項用 SSP 真有 IR 的 control_id(先 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 建一個)。

§3-B:control_impl_import(重寫 catalog 標題查詢 + 接回 service)

  1. 重寫 infra/oscal/repository/ssp_catalog_title_query.py(拔光 v1 import):實作 ISspCatalogTitleQuery 三個 method,改用 v2 catalog 鏈:
    • 先 ssp_id → ssp.import_profile_idprofile_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))。
  2. 重寫 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
    • status/description 的 props 名跟 control_implementation service 一致:implementation-status / implementation-description
    • Excel 格式(HEADER_LABELS / 樣式 / dropdown)原樣保留;只換資料來源。
    • confirm_import 已 delegate self._ssp_svc.update_control_implementation/update_objective(v2)+ doc pool mapping(用 §3-A 修好的 resolve)——把 mapping 部分也改成走新的 IR/statement id(別再用 get_ap_*)。
  3. wire + re-enable:DI provider(ssp_catalog_title_query Singleton + ssp_control_impl_import_service Factory)→ config/di_modules.py 移除 api.oscal.routes.ssp.ssp_control_impl_import_routeapi/oscal/__init__.py 註冊 4 條 route(URL 見 §11 git 對照)。
  4. smoke:export_excel(ssp_uid) 回非空 xlsx(data[:2]==b'PK')+ confirm_import 一筆控制項 + 權限負面(stranger ForbiddenError)。

§4 開工順位

  1. pre-flight:跑 §6 + §7(確認 BOOT、pytest baseline、舊 AP 三表 MISSING、新表在)。
  2. §3-A document_pool mapping(單純)→ smoke → 顯式 git add commit。
  3. §3-B-1 重寫 SspCatalogTitleQuery(v2 catalog 鏈、拔 v1 import)→ 單獨先驗它不炸 boot(import 它進一個 throwaway script)。
  4. §3-B-2 重寫 import service → wire → re-enable → smoke → commit。
  5. 每段 BOOT OK + pytest ≤ baseline 紅(用 §6 baseline-diff)+ 顯式 git add commit。

§5 該讀 / 預期改動的檔案

檔案 為何
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.pyderive_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 來源)

§6 Pre-flight(必跑,可複製貼)

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 errors

§7 Verify 地基事實(必跑,可複製貼)

cd /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 在。


§8 行為規範重要提醒

  • 不切 branch(兩 repo 都在 feature/oscal-refactor;branch 不對停下問 user)。
  • 可自行階段性 commit(顯式 git add 檔名、-am;各 repo 分開);push / 收尾 / 套件發版等 user 明示
  • pyproject.toml path-dep 勿 commit;套件 dev 改動 commit 進套件 branch 防搞丟、不發 Nexus
  • 絕不 import v1 jedi_oscal 進 boot graph(§2-1);重寫 query 用 raw SQL / v2 repo。
  • 改 BE 後提醒 user 重啟 BE(無 hot reload);服務 user 自己起。
  • plan 假設先 verify(§2-6/9);AO 識別子一致性先釘死再實作。
  • 遇到架構/資料模型取捨、scope 邊界 → 停下問 user。

§9 收尾流程(整 B3 弧做完 + user 明示才做)

盤點 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。


§10 不在本期 scope(別順手做)

  • mf_ssp_export / ssp_export(SSP 匯出 docx/pdf)—— §10 deferred,跟本棒無關。
  • P4 update-diff(資源庫範本 SSP 重複匯入差異更新)—— import-ssp create-only,留 P4。
  • Q1 準備期 My Jobs(5 增量) —— 另一條 handoff 2026-06-15-Q1-prep-jobs-START-HERE-handoff.md,跟 B3 平行、不互卡。
  • B3 弧的 changelog/SUMMARY/memory/Notion —— 等 user 下收尾命令。
  • 套件發 Nexus / pyproject 改回 pin —— 整 feature 完 + user 明示。

§11 本 session(B3)commits

主專案(branch 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 編輯地基

control_impl_import 4 route 原始 URL(re-enable 用,git pre-2A 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)。


§12 給 fresh 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(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 額外解釋即可開工。