🟢 START HERE — FR-038 Wave 2B 續:B2/B3a/B4.1 已 shipped,接手 B4.2 → B5

給下個 session 的 prompt:「讀 docs/features/FR-038-2606-oscal-redesign/handoff/2026-06-15-B4.2-B5-START-HERE-handoff.md,先過 §0 讀序硬 gate 懂需求,跑 §6 pre-flight + §7 verify 確認 B2-B4 現況,再接手 B4.2(AP 編輯端點)→ B5(AR 全量矩陣 + 風險 + POA&M + 結案/覆核)。」 本檔自包含。所有狀態為 2026-06-15 verified(現場跑 smoke + pytest,非假設)。

🔖 交接現況(2026-06-15 換 session 當下)

項目
進度 2A ✅ · B1 ✅(框架+資源庫三件組)· B2 ✅(clone 三件組+SSP 控制項初始化)· B3a ✅(輪次核心+snapshot)· B4.1 ✅(AP 草稿自動生)· 下一棒 = B4.2(AP 編輯端點)→ B5(最重)
主專案 branch / HEAD feature/oscal-refactor / 8111626e(本 session 兩 commit:2ba7a7c4 B2、8111626e B3a+B4)
主專案 working tree 乾淨,只有 M pyproject.toml(jedi-oscal-v2 dev path-dep,照規範勿 commit
⚠️ v2 套件未 commit dev 改動(B1 留下,本 session 沒動套件) repo ~/Projects/Jedicogy/module/jedi-python-package(branch feature/oscal-refactor)working tree:M catalog_service.py(add_catalog + import_*)、M framework_service.py(get_version)、?? tests/catalog/test_catalog_service_import.pydev path-dep,BE 重啟即生效;發版等整個 feature 完成 + user 明示。別搞丟。
跑得起來嗎 create_app() BOOT OK on v2;python -m pytest test/ = 56 failed + 50 errors(持平 2A baseline、零新回歸)+ 9 skipped
push 未 push,等 user 明示

🧭 開工前必懂:這整件事要幹嘛 + 本棒在大圖的位置(先懂才准碰 code)

一句話:FR-038 把舊「只套 OSCAL 概念、大量自定義欄位、拿 AP 兼當稽核輪次」的實作,打掉重練成「正式 OSCAL v1.2.2 物件模型 + 三層 clone/snapshot 邊界 + 稽核輪次 first-class」的專業 OSCAL GRC 系統。市場核心價值在 Phase 3(外部稽核員寫 AR/findings/risks)——多數產品到此斷掉,這正是要補的。

目標新模型(必懂)

  • 三層 + 四 clone/snapshot 邊界:框架母版(catalog/profile) →①resolve→ 資源庫三件組(catalog+profile+ssp) →②clone脫鉤→ 專案副本(★單一真相) →③啟動稽核 snapshot→ 凍結 SSP 快照。鐵則:副本存內容/指版本化母版,不可只存 id 即時 join 回會變動母表
  • 稽核輪次 first-class = compliance.project_audit_rounds(7 態狀態機),取代「AP 當輪次」。engagement 模型:initial/surveillance 起新 AP+AR;close-out 覆核沿用母輪 AP+AR、append ar_resultsparent_round_id 串血緣。
  • 套件 vs 主專案邊界(design §4.4):jedi_oscal_v2 只管 OSCAL 物件 CRUD/resolve/snapshot/clone/IO;輪次狀態機、權限、workflow/job 綁定、project_audit_rounds 留主專案

本棒(B4.2 → B5)在大圖位置:B2 把專案 clone 起來、B3a 讓輪次能 snapshot、B4.1 讓 launch-audit 自動生 AP 草稿。接下來 B4.2 補 AP 編輯(稽核員調整查核範圍/抽查名單/行程方法),B5 做整個 Phase 3(AR AO 全量判定 + 風險總結 + POA&M 整改 + 結案/覆核)—— 這是市場空白的核心,也是最重的一棒。

冷接自檢:你應能回答 ① 四個 clone/snapshot 邊界是哪四個 ② engagement 模型怎麼把 round↔︎AP↔︎AR↔︎ar_results wire 起來 ③ 為何 AP「對著凍結快照」而非 living SSP ④ 為何 Q1 workflow job 目前被卡(見 §2)。答不出 → 回 §0 讀序。


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

  1. 本檔「🧭」節 + 通讀本檔(§1 現況 / §2 陷阱 / §3 下一棒計畫)
  2. 🔒 gate:requirement-analysis.md §4.5(AP 四件事)/ §4.6(AR AO 全量矩陣 + 風險)/ §4.7(POA&M 三層)/ §4.8(結案/覆核)+ design.md §3(engagement 模型)
  3. 🔒 gate:api-contract.md §5(AP)/ §6(AR)/ §7(POA&M)/ §8(結案) ← B4.2/B5 照此重建端點
  4. 本 session 兩個 app service 當範本:app/grc/service/audit_round_app_service.py(狀態機 pattern)、app/grc/service/assessment_plan_app_service.py(包 v2 AP service pattern)
  5. 套件對外簽章:design §4.2 + 2A handoff §5.2 v2 wiring map(開工前 pre-flight 複驗)

§1 現況:B2 / B3a / B4.1 已做什麼(2026-06-15 verified)

canonical 模式(照抄)

新 app service 包 v2 primitive(補主專案職責:pager/filter/uid/RLS/權限/enrich)→ container wire → route → 註冊 → boot + app-service smoke(真資料) + pytest 不多紅 → 顯式 git add commit。

B2 — 專案成立 clone 三件組(commit 2ba7a7c4

  • app/project/service/project_start_app_service.pystart_project(resource_library_uid, ...) → 解析資源庫 module_frame 三件組 → oscal_snapshot_service.clone_resource_library(catalog_id, profile_id, ssp_id)邊界②)→ 建 compliance.projects + project_extensions(living_ssp_id) + participants(用 lean project_participant_domain_service,因 full service 2A-dark)。
  • project_extensions.living_ssp_id(migration 2026-06-15-fr038-project-extensions-living-ssp.sql,已套 dev)= 專案 OSCAL 資產唯一錨點;profile/catalog 由 ssp.import_profile_idprofile_imports.source_catalog_id 導航(不另存,避免 desync)。
  • B2.1.5:clone 後 _init_ssp_control_implementationsresolve_profile 為 living SSP 建 1:1 control-implementation + 每控制一筆 implemented-requirement(SoA props 預設 applicability=applicable / implementation-status=unknown)。
  • route POST /oscal-project/start 重寫(request 改 resource_library_uid,移除 profile_uid/ssp_uid/flow_template_uid)。

B3a — 稽核輪次核心(commit 8111626e

  • 完整垂直:infra/grc/model/project_audit_round.py + domain/grc/entities/project_audit_round_entity.py(+query) + infra/grc/repository/project_audit_round_repo_impl.py(get_by_uid/list_by_project/max_round_no) + mapper + domain/grc/service/project_audit_round_domain_service.py
  • app/grc/service/audit_round_app_service.py 狀態機:create_round(initial/surveillance→planning) / launch_audit(snapshot living SSP 邊界③→audit_planning) / start_auditing(→auditing) / list / get。角色守門(manager/auditor)+ 非法轉換擋下。
  • project_audit_rounds.uid(migration 2026-06-15-fr038-audit-rounds-uid.sql,已套 dev)= 對外操作識別碼。
  • route:POST /projects/<uid>/audit-rounds[/list]GET /audit-round/<uid>/launch-audit/start-auditing

B4.1 — AP 草稿自動生(commit 8111626e

  • launch_audit 對 initial/surveillance 輪呼叫 assessment_plan_app_service.create_draft_for_snapshot(frozen_ssp_id) → v2 generate_draft_from_ssp(reviewed-controls = 凍結 SSP 的 SoA 適用集)→ 回填 round.assessment_plan_id
  • app/grc/service/assessment_plan_app_service.pyget_ap_for_round / get_ap(詳情:reviewed-controls 從 control_selections JSONB 解析 + subjects + tasks)。
  • route GET /audit-round/<uid>/apAP import_ssp_id = 凍結快照(PM 教材:AP 對定版快照不是 living SSP)。

§2 ⚠️ 會讓你做錯的陷阱 / 已定決策(先看)

  1. v1/v2 不可共存(記憶 project_oscal_v1_v2_cannot_coexist):任何新 code 只接 jedi_oscal_v2,禁 import 舊 jedi_oscal
  2. 2A-dark service 不能用OscalProjectService 舊 wiring、project_participant_service(full)等仍 reference 2A 已移除的 oscal provider → 建構即炸。需要 participant 寫入用 lean project_participant_domain_service(B2 已示範)。
  3. 【Q1 卡點,已查證】workflow job 沒得建:requirement §4.3b 的「為 SSP 控制項建 workflow job(My Jobs)」目前無法做 —— 舊「control→workflow template」設定(profile_assessment_workflow)在 v2 重建時 DROP 且沒重建;新資源庫(B1)沒建 workflow 設定(module_frames.template_uid 全 NULL);workflow_execution_control_mapping_service 2A stub 成回空。要做 Q1 得先設計「新模型 workflow 設定掛哪」(B1 資源庫設定 + FE Wave 3 件) → user 已拍板延後 Q1,別在 B4.2/B5 順手做。My Jobs view(vw_user_job_queue)2A 已留 control_* NULL 佔位,等 Q1。
  4. SSP 控制項來源是「自動生」(產品取捨,已做):v2 原設計 SSP 控制項靠匯入(import-ssp,defer)/ 編輯填;B2.1.5 因 import 未實作,改成專案成立時從 profile 自動帶全控制集。B4 的 AP 草稿就靠這些 SoA props。若日後接 import-ssp 要注意別重複生。
  5. launch-reverify / close-round 還沒做:因母輪要走到 pending_reverify/remediation 才能觸發,而那需要 B5 的 AR finalize / POA&M。B5 一起做這兩個轉換(api-contract §4.4 / §8)。
  6. BaseRepositoryImpl.update() 只寫非 None 值 + 用 entity.uid 定位:狀態轉換改欄位要塞非 None;要清成 NULL 不能走 update(B3a/B4 沒這需求)。
  7. pytest 用 python -m pytest test/(單數 test/,60 檔);boot 驗證要補 dummy GITLAB/GITHUB env(見 §6)。
  8. plan 假設先 verify(v2 簽章 / 欄位)才開工。不晶晶體。

§3 下一棒計畫:B4.2 → B5

B4.2 — AP 編輯端點(中,先做)

api-contract §5.2~§5.4。v2 AssessmentPlanService 已備 set_reviewed_controls / set_assessment_subjects / set_tasksbuild_control_selectionsjedi_oscal_v2.domain.service.ap.ap_draft_service)。在 AssessmentPlanAppService 加:

  • PUT /ap/<ap_uid>/reviewed-controls:body {control_ids:[...]}build_control_selectionsset_reviewed_controls。權限 auditor。
  • PUT /ap/<ap_uid>/assessment-subjects:全量覆寫(subject_type/subject_uuid/include/title)→ set_assessment_subjects
  • PUT /ap/<ap_uid>/tasks:行程+方法 → set_tasks
  • POST /audit-round/<uid>/ap/generate-draft:手動重生草稿(呼叫 generate_draft)。
  • error code GRC_AP_REVIEWED_CONTROLS_REQUIRED(空清單,412,grep 最大序號續編)。

B5 — AR + POA&M + 結案/覆核(最重)

api-contract §6/§7/§8 + requirement §4.6~§4.8。v2 service:AssessmentResultService(add_ar/add_result/upsert_finding/list_findings 全量矩陣)、AssessmentRiskService(add_risk/link_findings 多對多)、PoamService(generate_from_findings/upsert_remediation/upsert_milestone)。

  • start-auditing 時建 ar_resultround.ar_result_id);AR import_ap_id=本輪 AP。
  • AO 全量判定矩陣(每個 in-scope AO 一筆 finding met/not_met/pending)+ 風險總結(稽核員手動組、多對多 link findings、等級在 risk 層)。
  • finalize:無 not_met→round closed;有 not_met→remediation + 自動生 POA&M。
  • 補 B3a 延後的 launch-reverify(母輪 pending_reverify → 開新 close-out round + 重新 snapshot)+ close-round(POA&M 全 closed → 母輪 pending_reverify;close-out 連動關母輪)。
  • My Jobs view control_* 接 ssp_control_implementations(搭 Q1,視 user 是否解 workflow 設定)。

每 phase:app service 包 v2 → wire → route → 註冊 → boot + 真資料 smoke + pytest 不多紅 → 顯式 git add commit。


§4 開工順位

  1. 跑 §6 pre-flight(branch / 兩 repo / v2 import / BOOT OK / pytest baseline)。
  2. 讀 §0 讀序 2~5。
  3. 跑 §7 verify 確認 B2-B4 真的可動(建資源庫→成立專案→輪次→launch-audit→AP)。
  4. B4.2:照 §3 把 AP 編輯端點接 v2 set_*,每端點 smoke + commit。
  5. B5:照 api-contract §6/§7/§8 分增量(AR 矩陣 → 風險 → finalize+POA&M → close/reverify),每增量 smoke + commit。
  6. 收尾等 user 明示(changelog/SUMMARY/analysis/Notion/push)。

§5 該讀的檔案 / 預期改動範圍(BE only;FE 是 Wave 3)

檔案 為何讀
app/grc/service/assessment_plan_app_service.py B4.2 在這加 set_* 端點
app/grc/service/audit_round_app_service.py B5 在 start-auditing 建 ar_result、補 close/reverify 狀態轉換
~/Projects/Jedicogy/.../jedi-oscal-v2/.../app/service/ar/*.pypoam/*.py B5 接的 v2 service 簽章
di_containers/grc/grc_containers.py wire 新 AR/POA&M app service
api/project/routes/audit_round_route.py + api/project/__init__.py 加 AP/AR/POA&M route + 註冊
common/code/grc_error_code.py 新 error code(下一個可用:404→404039、412→412035;412031-034 / 404038 已被 B2/B3a 用;403 沿用 NOT_MANAGER/NOT_AUDITOR;開工前再 grep 複驗

§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 -2                           # 8111626e (B3a+B4) / 2ba7a7c4 (B2)
( cd ~/Projects/Jedicogy/module/jedi-python-package && git status -sb | head -5 )  # 套件 dev 改動還在
poetry run python -c "import jedi_oscal_v2; print('v2 OK')"
# BOOT(補 dummy env 否則撞 jedi_issue)
set -a; source .env; set +a 2>/dev/null
export GITLAB_API_VERSION=4 GITLAB_URL=http://localhost GITLAB_PRIVATE_TOKEN=dummy GITHUB_PRIVATE_TOKEN=dummy
poetry run python -c "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')"
poetry run python -m pytest test/ -q -p no:cacheprovider --continue-on-collection-errors 2>&1 | tail -1  # 56 failed,...,50 errors(持平)

注意 .env 在 shell source 會在 JSON 行報 parse error;跑 python script 改用 from dotenv import load_dotenv; load_dotenv()(見下方 §7 smoke 開頭)。


§7 Verify B2-B4 確實可動(必跑,可複製貼)

本 session 的 smoke script 收尾時已刪(會寫 dev 測資)。下面是可複製貼的等價 verify(fabricate 最小三件組 → 走完 B2→B3a→B4,斷言 clone 脫鉤 + snapshot 凍結 + AP 草稿)。存成 scripts/_verify.py 跑完即刪:

import eventlet; eventlet.monkey_patch(all=False, socket=True)
import os, sys, uuid; sys.setrecursionlimit(5000)
sys.path.insert(0, os.getcwd())
from datetime import datetime, timezone
from dotenv import load_dotenv; load_dotenv()
for k,v in {"GITLAB_API_VERSION":"4","GITLAB_URL":"http://localhost","GITLAB_PRIVATE_TOKEN":"dummy","GITHUB_PRIVATE_TOKEN":"dummy"}.items(): os.environ.setdefault(k,v)
from core.app_factory import create_app; app=create_app()
from jedi_common.session.database.db import session_scope
from jedi_common.session.database.session_context import get_session
import sqlalchemy as sa
from jedi_oscal_v2.common.enum.oscal_enums import PublishStatus
from jedi_oscal_v2.domain.entity.base.oscal_metadata_entity import MetadataEntity
from jedi_oscal_v2.domain.entity.catalog.oscal_catalog_entity import CatalogEntity
from jedi_oscal_v2.domain.entity.catalog.oscal_catalog_control_entity import CatalogControlEntity
from jedi_oscal_v2.domain.entity.profile.oscal_profile_entity import ProfileEntity
from jedi_oscal_v2.domain.entity.profile.oscal_profile_import_entity import ProfileImportEntity
from jedi_oscal_v2.infra.repository.base.metadata_repo_impl import MetadataRepoImpl
from jedi_oscal_v2.infra.repository.catalog.catalog_repo_impl import CatalogRepoImpl
from jedi_oscal_v2.infra.repository.catalog.catalog_control_repo_impl import CatalogControlRepoImpl
from jedi_oscal_v2.app.service.profile.profile_service import ProfileService
from jedi_oscal_v2.app.service.ssp.ssp_service import SspService
U,UU,UID="blsadmin","eac362ed-1fc7-48ac-b7e5-88c71f412c03",17
AUU,AID="9d895dc8-e189-4ed6-a830-a9e468a00a04",18  # blsit (auditor)
CIDS=["AC.L2-3.1.1","AC.L2-3.1.2","AC.L2-3.1.3"]
md=lambda t: MetadataRepoImpl().add(MetadataEntity(uid=str(uuid.uuid4()),title=t,last_modified=datetime.now(timezone.utc),version="1.0",oscal_version="1.1.2",created_user=U,updated_user=U))
with session_scope():
    s=get_session()
    cat=CatalogRepoImpl().add(CatalogEntity(uid=str(uuid.uuid4()),metadata_id=md("c").id,status=PublishStatus.DRAFT.value,created_user=U,updated_user=U))
    for i,c in enumerate(CIDS): CatalogControlRepoImpl().add(CatalogControlEntity(uid=str(uuid.uuid4()),catalog_id=cat.id,control_id=c,title=c,sort_order=i,created_user=U,updated_user=U))
    prof=ProfileService().add_profile(ProfileEntity(uid=str(uuid.uuid4()),metadata_id=md("p").id,status=PublishStatus.DRAFT.value,created_user=U,updated_user=U),imports=[ProfileImportEntity(source_catalog_id=cat.id,include_all=True,created_user=U,updated_user=U)])
    ssp=SspService().add_empty_ssp(metadata_id=md("s").id,import_profile_id=prof.id)
    rl=str(uuid.uuid4())
    s.execute(sa.text("INSERT INTO compliance.module_frames (uid,\"group\",frequency,version,name,enable,is_delete,tenant_id,oscal_profile_uid,template_ssp_id,created_user,updated_user,created_at,updated_at) VALUES (:u,'resource-library','none','1.0','vfy',1,0,102,:p,:s,:c,:c,now(),now())"),{"u":rl,"p":prof.uid,"s":ssp.id,"c":U})
from di_containers.containers import Containers; C=Containers()
p=C.project_container.project_start_app_service().start_project(name=f"vfy {uuid.uuid4().hex[:6]}",resource_library_uid=rl,participants=[{"user_uid":AUU,"role":"auditor"}],curr_user=U,curr_user_uid=UU,curr_user_id=UID)
assert p["control_count"]==3 and p["ssp_uid"]
r=C.grc_container.audit_round_app_service().create_round(project_uid=p["project_uid"],name="r1",round_type="initial",curr_user=U,curr_user_id=UID)
r2=C.grc_container.audit_round_app_service().launch_audit(r["uid"],curr_user=U,curr_user_id=UID)
assert r2["status"]=="audit_planning" and r2["ssp_uid"] and r2["ap_uid"]
ap=C.grc_container.assessment_plan_app_service().get_ap_for_round(r["uid"])
assert sorted(c["control_id"] for c in ap["reviewed_controls"])==sorted(CIDS) and ap["import_ssp_uid"]==r2["ssp_uid"]
print("✅ VERIFY PASS — B2 clone + SSP 控制項 + B3a snapshot + B4 AP 草稿全綠")

跑:poetry run python scripts/_verify.py && rm scripts/_verify.py


§8 行為規範重要提醒(適用本棒)

  • 不切 branch(兩 repo 都在 feature/oscal-refactor;branch 不對停下問 user)。
  • 可自行階段性 commit(顯式 git add 檔名、-am;各 repo 分開);push / 收尾等 user 明示
  • pyproject.toml path-dep 勿 commit(dev-only);v2 套件 dev 改動別搞丟、別發版。
  • 改 BE 後提醒 user 重啟 BE(無 hot reload);服務 user 自己起
  • 跨 schema FK 字串帶 schema 前綴(記憶 feedback_cross_schema_fk_must_qualify)。
  • SQL migration:cmmgr + --single-transaction -v ON_ERROR_STOP=1,新表 GRANT cm_app,收尾 INSERT schema_migrations;stg/poc 待遷移(本期 2 條 migration 只套 dev)。
  • 新 error code grep common/code/grc_error_code.py 最大序號續編。
  • 遇到架構/資料模型取捨、需選方案、scope 邊界 → 停下問 user(這 arc 已有多次:living-triple 存放、SSP 控制項來源、Q1 workflow 設定)。

§9 收尾流程(B4.2/B5 做完 + user 明示才做)

盤點 commits → changelog(docs/changelog/,type=feat)→ 2 份 analysis 取捨文件待補(① living-triple 只存 ssp 錨點 ② SSP 控制項自動生 vs import 取捨,含 Q1 workflow 設定缺口)→ SUMMARY → 回頭更新本 handoff ✓ + api-contract 偏差註記 → memory feedback → Notion。push 等 user。


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

  • Q1 workflow job / My Jobs control_ 接線*(卡資源庫 workflow 設定,B1/FE 件,user 已拍板延後)。
  • FE(Wave 3)。
  • 套件發 Nexus(feature 全完成 + user 明示);pyproject.toml 改回 pin。
  • 正式環境 schema 遷移(D7;stg/poc 本期 migration 待辦)。
  • ISO/NIST(本期 CMMC-only,design §7);import-ssp 套件實作(defer)。

§11 本 session commits(branch feature/oscal-refactor,未 push)

  • 2ba7a7c4 feat(FR-038): Wave 2B B2 — 專案成立 clone 三件組(邊界②)+ 專案 SSP 控制項初始化
  • 8111626e feat(FR-038): Wave 2B B3a+B4 — 稽核輪次 first-class + 啟動稽核 snapshot + AP 草稿
  • 前置:87eaf290(2B handoff 定版)等。working tree 僅 M pyproject.toml(dev path-dep)。
  • 2 條 migration 已套 dev(stg/poc 待辦):2026-06-15-fr038-project-extensions-living-ssp.sql2026-06-15-fr038-audit-rounds-uid.sql

§12 給 fresh session 的超短 prompt

讀 docs/features/FR-038-2606-oscal-redesign/handoff/2026-06-15-B4.2-B5-START-HERE-handoff.md。
先過「🧭 開工前必懂」+ §0 讀序硬 gate(requirement §4.5~4.8 + design §3 + api-contract §5~§8),
能答冷接自檢 4 問(四邊界 / engagement 模型 / AP 為何對凍結快照 / Q1 為何卡)才往下。
跑 §6 pre-flight(BOOT OK + pytest 56f/50e 持平)+ §7 verify(確認 B2-B4 可動)。
接手 B4.2(AP 編輯端點接 v2 set_*)→ B5(AR 全量矩陣 + 風險 + POA&M + 結案/覆核,含補 B3a 延後的
launch-reverify/close-round)。Q1 workflow job 延後(卡資源庫 workflow 設定)。
每 phase:app service 包 v2 → wire → route → boot+真資料 smoke → pytest 不多紅 → 顯式 git add commit。
push / 收尾 / 套件發版等 user 明示。

冷接可行性自檢 ✅

看本檔 + §0 讀序 + 跑 §6/§7 → 能確認現況(B2/B3a/B4.1 shipped + commits + 套件 dev 改動)、懂 WHY(三層邊界 / engagement / Phase 3 核心價值)、知道 B4.2/B5 接哪個 v2 service、知道 Q1 為何延後、知道規範界線。不需 user 額外解釋即可開工。