給下個 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,非假設)。
| 項目 | 值 |
|---|---|
| 進度 | 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.py。dev 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 明示 |
一句話:FR-038 把舊「只套 OSCAL 概念、大量自定義欄位、拿 AP 兼當稽核輪次」的實作,打掉重練成「正式 OSCAL v1.2.2 物件模型 + 三層 clone/snapshot 邊界 + 稽核輪次 first-class」的專業 OSCAL GRC 系統。市場核心價值在 Phase 3(外部稽核員寫 AR/findings/risks)——多數產品到此斷掉,這正是要補的。
目標新模型(必懂):
compliance.project_audit_rounds(7 態狀態機),取代「AP 當輪次」。engagement 模型:initial/surveillance 起新 AP+AR;close-out 覆核沿用母輪 AP+AR、append ar_results;parent_round_id 串血緣。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 讀序。
requirement-analysis.md §4.5(AP 四件事)/ §4.6(AR AO 全量矩陣 + 風險)/ §4.7(POA&M 三層)/ §4.8(結案/覆核)+ design.md §3(engagement 模型)api-contract.md §5(AP)/ §6(AR)/ §7(POA&M)/ §8(結案) ← B4.2/B5 照此重建端點app/grc/service/audit_round_app_service.py(狀態機 pattern)、app/grc/service/assessment_plan_app_service.py(包 v2 AP service pattern)新 app service 包 v2 primitive(補主專案職責:pager/filter/uid/RLS/權限/enrich)→ container wire → route → 註冊 → boot + app-service smoke(真資料) + pytest 不多紅 → 顯式 git add commit。
2ba7a7c4)app/project/service/project_start_app_service.py:start_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_id → profile_imports.source_catalog_id 導航(不另存,避免 desync)。_init_ssp_control_implementations 從 resolve_profile 為 living SSP 建 1:1 control-implementation + 每控制一筆 implemented-requirement(SoA props 預設 applicability=applicable / implementation-status=unknown)。POST /oscal-project/start 重寫(request 改 resource_library_uid,移除 profile_uid/ssp_uid/flow_template_uid)。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)= 對外操作識別碼。POST /projects/<uid>/audit-rounds[/list]、GET /audit-round/<uid>、/launch-audit、/start-auditing。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.py:get_ap_for_round / get_ap(詳情:reviewed-controls 從 control_selections JSONB 解析 + subjects + tasks)。GET /audit-round/<uid>/ap。AP import_ssp_id = 凍結快照(PM 教材:AP 對定版快照不是 living SSP)。project_oscal_v1_v2_cannot_coexist):任何新 code 只接 jedi_oscal_v2,禁 import 舊 jedi_oscal。OscalProjectService 舊 wiring、project_participant_service(full)等仍 reference 2A 已移除的 oscal provider → 建構即炸。需要 participant 寫入用 lean project_participant_domain_service(B2 已示範)。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。launch-reverify / close-round 還沒做:因母輪要走到 pending_reverify/remediation 才能觸發,而那需要 B5 的 AR finalize / POA&M。B5 一起做這兩個轉換(api-contract §4.4 / §8)。python -m pytest test/(單數 test/,60 檔);boot 驗證要補 dummy GITLAB/GITHUB env(見 §6)。api-contract §5.2~§5.4。v2 AssessmentPlanService 已備 set_reviewed_controls / set_assessment_subjects / set_tasks(build_control_selections 在 jedi_oscal_v2.domain.service.ap.ap_draft_service)。在 AssessmentPlanAppService 加:
PUT /ap/<ap_uid>/reviewed-controls:body {control_ids:[...]} → build_control_selections → set_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)。GRC_AP_REVIEWED_CONTROLS_REQUIRED(空清單,412,grep 最大序號續編)。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)。
round.ar_result_id);AR import_ap_id=本輪 AP。launch-reverify(母輪 pending_reverify → 開新 close-out round + 重新 snapshot)+ close-round(POA&M 全 closed → 母輪 pending_reverify;close-out 連動關母輪)。ssp_control_implementations(搭 Q1,視 user 是否解 workflow 設定)。每 phase:app service 包 v2 → wire → route → 註冊 → boot + 真資料 smoke + pytest 不多紅 → 顯式 git add commit。
set_*,每端點 smoke + commit。| 檔案 | 為何讀 |
|---|---|
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/*.py、poam/*.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 複驗) |
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在 shellsource會在 JSON 行報 parse error;跑 python script 改用from dotenv import load_dotenv; load_dotenv()(見下方 §7 smoke 開頭)。
本 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
feature/oscal-refactor;branch 不對停下問 user)。git add 檔名、禁 -am;各 repo 分開);push / 收尾等 user 明示。pyproject.toml path-dep 勿 commit(dev-only);v2 套件 dev 改動別搞丟、別發版。feedback_cross_schema_fk_must_qualify)。--single-transaction -v ON_ERROR_STOP=1,新表 GRANT cm_app,收尾 INSERT schema_migrations;stg/poc 待遷移(本期 2 條 migration 只套 dev)。common/code/grc_error_code.py 最大序號續編。盤點 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。
pyproject.toml 改回 pin。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)。2026-06-15-fr038-project-extensions-living-ssp.sql、2026-06-15-fr038-audit-rounds-uid.sql。讀 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 額外解釋即可開工。