adventure-table

P2-C Closeout Checklist

P2-C — Campaign & Party Roster closeout scope。編號對應 實作規格 的「Campaign 完成條件」、「Party Roster 完成條件」與「State ownership」。

Campaign 完成條件

Party Roster 完成條件

Roster duplicate add 的契約選 idempotent:重複加入回傳既有 entry 而非衝突。並發下兩個 transaction 都可能看不到既有列而撞上 (campaign_id, character_id) unique key,敗方 rollback 後改讀已 commit 的勝方,不讓 IntegrityError 變成 500。

State ownership

承接 P2-B 的未結清項目

Verification evidence

分支 p2-c-campaign-party-roster,最終 code SHA a6bbb18

Branch / final code SHA
  p2-c-campaign-party-roster @ a6bbb18

Alembic heads
  0009_p2a_character_head (character) (head)
  0012_p2c_campaigns (web) (head)
  0012 接在 0011_p2b_room_workspace 之後,只落在 web track;
  standalone SQLite 仍只升 character@head,不會長出 Campaign schema。

Backend pytest
  1037 collected / 157 files,exit 0
  cwd apps/server,直譯器 ..\..\.venv\Scripts\python.exe
  執行於 3036cf7;3036cf7..a6bbb18 的 backend 差異只有還原一段 P2-B 註解與
  test_p2a_postgres_migration.py 的排版回復,未重跑全套。
  focused 重跑(a6bbb18):test_p2c_*.py + test_p2a_migration_tracks.py +
  test_m03d_schema_parity.py + test_m03e_capabilities.py +
  test_m03_import_boundary.py + test_p2a_room_import_boundary.py → 29 passed

PostgreSQL gate(本機,對拋棄式 database)
  DATABASE_URL / P2_POSTGRES_URL 指向 compose db service 上另建的 p2c_gate database,
  跑完即 DROP,不動開發資料。
    test_p2a_postgres_migration.py::test_fresh_web_postgres_upgrade_heads_and_readiness
    test_p2a_postgres_migration.py::test_legacy_m03_postgres_upgrade_heads_preserves_character_payloads
    test_p2b_postgres_workspace.py::test_concurrent_cross_room_association_has_exactly_one_winner[character]
    test_p2b_postgres_workspace.py::test_concurrent_cross_room_association_has_exactly_one_winner[draft]
    test_p2b_postgres_workspace.py::test_concurrent_legacy_claim_never_splits_unscoped_workspace_data
  5 passed, 2.95s
  第一支測試已擴充 _assert_p2c_web_schema(),在真正的 PostgreSQL 上驗
  campaigns / campaign_roster_entries 存在、rooms.active_campaign_id 存在,
  以及 roster 兩個 FK 的 CASCADE / RESTRICT ondelete。

Frontend unit
  189 passed / 39 files

TypeScript / build
  npm run build(tsc --noEmit && vite build)exit 0

Full Web Playwright
  npm run test:e2e:docker exit 0 @ a6bbb18
  第一輪 105 tests:102 passed / 0 failed / 3 skipped(7.1m)
  第二輪 xge-less M03-C 子集:7 passed(6.4s)
  執行時 compose server 已在 0012_p2c_campaigns,
  /api/meta/capabilities 回 campaign:true,確認 E2E 是對 P2-C backend 跑的。
  本次全套無 KI-P1D-001 失敗。

docker compose config
  exit 0

驗收條目 → 測試對應

條目 證據
1 test_p2c_campaign_policy.py::test_campaign_lifecycle_is_owner_only(member / DM 參數化皆 403 room_owner_required
2, 6 test_p2c_persistence_contract.py::test_p2c_campaign_schema_matches_contract(欄位順序、room_id CASCADE、status CheckConstraint)
3 同上:campaigns.c.keys() 逐欄鎖定,無 adventure / rules 欄位;test_campaign_router_exposes_only_p2c_surfaces 另確認 router 未提前長出 /sessions/lobby/seats
4 test_p2c_campaign_policy.py::test_campaign_service_separates_status_from_room_selection
5 同上(archive 清 selection 不刪 Character)+ test_p2c_campaign_integration.py 末段 draft hard delete
R1 test_p2c_campaign_policy.py::test_roster_management_allows_owner_and_dm_but_not_member
R2 test_p2c_campaign_policy.py::test_cross_room_character_cannot_enter_rostertest_p2c_campaign_integration.py 的 Room B Character 加入 Room A Campaign 被拒
R3, S test_p2c_campaign_integration.py::test_real_campaign_rosters_share_one_character_state_and_preserve_history
R4, R5, R6, R7 同上(status 改 retired 後 Character State 不變、archive Character 後 roster history 仍可 query)+ test_same_room_roster_is_idempotent_and_status_is_roster_only
R8 test_p2c_persistence_contract.py::test_p2c_roster_schema_is_reference_only_and_protects_history(roster 無 owner 欄位、無 state 欄位)
Roster idempotent test_p2c_campaign_policy.py::test_same_room_roster_is_idempotent_and_status_is_roster_only、整合測試的 duplicate add
archived campaign / non-draft delete test_p2c_campaign_policy.py::test_archived_campaign_cannot_be_selected_and_non_draft_cannot_hard_delete
P2-B 條目 13 補完 test_p2c_campaign_policy.py::test_web_character_delete_history_guard_blocks_roster_referencetest_p2c_room_hard_delete.py::test_roster_fk_restricts_direct_character_delete_and_room_delete_cleans_history_first
Room Hard Delete 清除順序 同上(Room A 全清、Room B 的 Campaign / roster / Character / active_campaign_id 完好)
capability 旗標 test_p2c_capabilities.py 兩條、test_m03e_capabilities.py::test_web_capabilities_enable_room_and_campaign_for_p2c
standalone 不長 Campaign schema test_m03d_schema_parity.pycampaigns / campaign_roster_entries 已加入 FORBIDDEN_MULTIPLAYER_TABLES)、test_m03_import_boundary.pytest_p2a_room_import_boundary.py
migration track 分離 test_p2a_migration_tracks.py 三條(WEB_REVISIONS 已納入 0012_p2c_campaigns
前端 route / 權限 / 文案 RoomCampaignPage.test.ts 四條、campaigns.test.ts 三條、routes.test.tshardcodedUiCopy.test.tsRoomCampaignPage.tsx 已納入掃描清單)

關門過程中修正的問題

Static review 提出 7 項,6 項已修並重新驗證,1 項判定為非違規而保留。

  1. 使用者可見文案洩漏內部 Phase 代號。 campaignCopy.tseyebrowP2-C · Campaign & Party Roster,直接印在 Campaign 列表與詳細兩個畫面上。P2-B 才剛把 Room landing 的 eyebrow 拿掉,這裡又加回來且帶開發代號。已移除該 copy key 與兩處 JSX,並在 RoomCampaignPage.test.ts 斷言兩個 locale 的 copy 都不含 P2-C

  2. 誤刪 P2-B 的 race 註解。 persistence/rooms/repository.py 中說明「concurrent first-Room bootstrap 為什麼要當成 allocation conflict 重試」的五行註解被移除。行為未變但理由消失,且與 P2-C 無關。已還原原文。

  3. test_p2a_postgres_migration.py 被無關重排版。 多個 text("""...""") 被壓成單行超長 statement,屬 P2-A 測試的格式 churn。已回復,相對 main 只剩 +24/-1 的 P2-C schema 斷言。

  4. Lv{character.level} 未進 copy table。 Roster 加入下拉是本檔唯一未本地化的使用者可見字串,兩個 locale 都顯示 Lv。已改用 copy.levelLabelLevel / 等級),並把 RoomCampaignPage.tsx 納入 hardcodedUiCopy.test.ts 的掃描清單,避免同類遺漏再次發生。

  5. Roster remove 沒有確認。 依設計 Roster 就是 Campaign history,移除是不可逆的,但 UI 是一鍵刪除。已加上 window.confirm(copy.removeConfirm),文案雙語齊備並說明「稍後重新加入會建立新的名冊項目」。

  6. P2 Non-E2E workflow 未涵蓋本分支。 已把 p2-c-campaign-party-roster 加進 push trigger 的 branches 清單。

  7. Server 不擋 archived Character 加入 Roster(保留,非違規)。 前端靠 listRoomCharacters 預設 archived=false 過濾,但 CampaignService.add_character() 本身沒驗 archived。實作規格 的 8.4 只要求「archived Character 不可成為新 Session selection」,那是 P2-E 的 Seat / Session 契約;P2-C 沒有任何條文禁止 archived Character 留在 Roster,而 archive 後 roster history 必須可查更是明訂要求。因此維持現狀,不在 P2-C 加上規格沒有要求的限制。P2-E 實作 Seat selection 時必須在 server 端擋 archived,不能沿用前端過濾。

Boundary

已知限制

Handoff

P2-C 已完成並關門。下一步是 P2-D — Seat, Controller & Lobby

P2-D 直接繼承以下 substrate,不得再造第二套: