Phase:P1 — Character Builder Complete
本文件是 P1 的實作契約。P1 必須建立在 P0 已存在的 Character / Rules / Content / Persistence 上,不重新發明另一套 Character model,也不提前建立 P2 以後的 Room / Permission / AI infrastructure。
最後更新:2026-08-29
三份 P1 文件使用完全一致的 Subphase 名稱與順序:
P1-A — Builder Domain & Draft Foundation
P1-B — Character Creation Basics
P1-C — Class Progression & Multiclass
P1-D — ASI, Feat & Structural Choices
P1-E — Spellcasting Progression
P1-F — Equipment, Review & Character Creation
P1-G — Level Up & Character Versions
P1-H — Full P1 Integration & Closeout
每個 Subphase 必須先完成自己的 implementation + tests + verification,再 commit 並進下一個。
P1 開工時已存在的正式地基:
apps/server/app/domain/character/schemas.py
├─ CharacterBuild
├─ CharacterState
├─ PersistedCharacter
├─ SpellAccessEntry
├─ StartingEquipmentEntry
└─ NumericOverride
apps/server/app/domain/character/validation.py
apps/server/app/domain/rules/
apps/server/app/content/
apps/server/app/persistence/characters.py
apps/server/app/api/characters.py
apps/server/app/api/reference.py
apps/web/src/features/character-sheet/
apps/web/src/api/
Persistence 已有:
characters
character_versions
character_states
P0 character_versions.build_payload 是 immutable Build snapshot;character_states.state_payload 是 mutable live state。
P1 的核心原則:
不要把 Builder 半成品塞進
CharacterBuild。建立新的 Builder Draft model;只有 Draft 通過完整 compile + validation 後,才產生正式CharacterBuild。
另外:
CharacterBuild 大量欄位改成 optional 來迎合 Draft。建議新增:
apps/server/app/domain/character_builder/
├─ __init__.py
├─ schemas.py
├─ choices.py
├─ compiler.py
├─ validation.py
├─ service.py
└─ view.py
後續 Subphase 再按實際複雜度加入:
abilities.py
progression.py
multiclass.py
features.py
spellcasting.py
equipment.py
reconciliation.py
versions.py
不要 P1-A 一次建立所有空檔案;只有對應 Subphase 開工才加入真正需要的 module。
Builder Draft 與正式 CharacterBuild 分離。
概念:
BuilderDraft
├─ id
├─ mode
├─ character_id? # create 時 null
├─ base_version_id? # create 時 null
├─ revision
├─ name / identity input
├─ target_level
├─ partial build choices
├─ initial state seed choices
├─ status
└─ created_at / updated_at
mode 至少預留:
create
level_up
build_edit
correction
P1-A 可以只正式啟用 create,但 schema / persistence 不要做成之後完全無法加入其他 mode 的形狀。
Draft payload 是輸入與選擇紀錄,不是最終 Build snapshot。
概念可包含:
basic
race_selection
background_selection
ability_generation
level_choices[]
choice_selections{}
spell_choices
starting_equipment_choices
roleplay_profile
numeric_overrides
initial_state_seed
Draft 允許缺欄位,因此使用 Builder 專用 Pydantic model。
不要直接把 partial dict 丟給 CharacterBuild.model_validate() 當 Draft validation。
新增 table:
character_build_drafts
概念欄位:
id UUID PK
mode string / enum
character_id UUID nullable
base_version_id UUID nullable
revision int not null
draft_payload JSONB not null
created_at
updated_at
P1 不需要 draft ownership / room_id / user_id。
Draft Cancel 可以 hard delete,因為它從未成為正式 Build history;P1 不需要把取消草稿做 Timeline。
Draft write 建議帶 revision:
GET Draft -> revision 7
PATCH Draft expected_revision=7
↓
成功後 revision 8
目的只是避免同一 browser 多分頁把新 Draft 內容靜默蓋掉;不是提前做 P2 concurrency subsystem。
如果實作時判定現有單人 P1 workflow 不需要 optimistic revision,可省略 API-level compare-and-swap,但 base_version_id 的 P1-G stale guard 不可省略。
建議 DTO:
BuilderIssue
├─ code
├─ severity: blocking_error | warning | non_standard
├─ path
├─ message
└─ related_refs[]
BuilderValidationResult
├─ issues[]
├─ can_confirm
└─ non_standard_count
UI 不解析錯誤字串來判斷類型。
建議 canonical builder choice:
BuilderChoice
├─ choice_id
├─ label
├─ source_ref
├─ required
├─ choose_count
├─ option_source
├─ selected_option_ids[]
└─ disabled_reason?
Option 至少可承載:
reference
counted_reference
nested_choice
category_filter
branch
SRD upstream option shapes 不直接 leak 到 React;由 server-side adapter 轉成 Builder Choice DTO。
choice_id 不能每次 GET 用 random UUID 重建。
建議由來源與 progression path deterministic 組合,例如概念:
race:<race-key>:language-choice:0
background:<background-key>:skill-choice:0
level:4:class:<class-key>:asi-or-feat
level:6:class:<class-key>:feature:<feature-key>:choice:0
實際字串格式可調整,但同一 Draft reload 後必須穩定,否則保存的 selection 會失聯。
建立純 domain compiler:
BuilderDraft
+ ContentRegistry
+ Rules Data
↓
Resolve choices
↓
Compile BuildCandidate
↓
Validate structural rules
↓
CharacterBuild candidate? / issues
Compiler 不依賴 FastAPI request、SQLAlchemy connection 或 React。
P1-A 不需要完成所有 D&D rules;先建立 extension points與 incomplete draft / missing-choice validation。
P0 data/srd5.1/ 繼續是 SRD content source of truth。
P1 若遇到 Character Builder 必要、但 upstream SRD entry 不足以可靠表達的 2014 規則常數,可新增小型 version-controlled rule data,例如:
data/rules/dnd5e-2014/character-builder.json
只放真正缺少的 reusable rules data,不把所有規則重新抄一份,也不要把可調規則數值散落 Python / React magic numbers。
建議新增 router:
/api/character-builder
最低:
POST /api/character-builder/drafts
GET /api/character-builder/drafts/{draft_id}
PATCH /api/character-builder/drafts/{draft_id}
POST /api/character-builder/drafts/{draft_id}/validate
DELETE /api/character-builder/drafts/{draft_id}
GET Draft 建議直接回傳:
draft input
resolved summary
available / required choices
validation result
或提供等價的 server-generated Builder View DTO。
關鍵是不讓 frontend 下載 raw SRD 後自己推導 eligibility。
新增 Alembic migration 建立 character_build_drafts。
不要修改 P0 character_versions immutable payload semantics。
先建立最小 feature shell:
apps/web/src/features/character-builder/
P1-A 只需要開 Draft / Save / Reload / Cancel 的最小 developer UI 或 API-driven smoke,不需要把完整 Wizard 樣式一次做完。
建議新增:
/characters
作為 Character Workshop / Manage list。
至少顯示:
P1 不做 Account / ownership filtering。
建議:
/character-builder/:draftId
不要把所有 Draft JSON 放 query string / localStorage 當 truth。
Desktop:
┌──────── Steps / Rail ───────┬──── Summary ────┐
│ Builder form │ live summary │
│ │ warnings │
└─────────────────────────────┴─────────────────┘
Mobile:Summary 改 collapsible drawer / section,不要求雙欄。
正式資料仍來自 Server Draft View。
沿用 TanStack Query:
不要把整份正式 Draft只存在 React state 等到最後一次送。
沿用 P0-E 已驗證的 accessible combobox pattern:
要求:
Draft 保存 generation method與 method-specific input,不只保存最後六個 resolved numbers。
概念:
AbilityGeneration
├─ method: standard_array | point_buy | manual
├─ assignments / base_scores
└─ provenance
Compiler:
Ability generation base
↓
Race / Subrace permanent grants
↓
later ASI / Feat permanent grants
↓
resolved CharacterBuild.ability_scores
↓
Numeric Override
↓
effective ability used by rules / prerequisite
P0 CharacterBuild.ability_scores 的語意維持不變:它仍是永久 Build effects resolved 後、Numeric Override 前的值。
P0 CharacterBuild 目前只有 race_ref,P1 若 SRD Race progression需要保留 Subrace choice,應正式擴充 Build schema,例如:
subrace_ref: StableKey | None
不要只把 Subrace traits展平後丟掉原始選擇來源,否則 Version History / correction 無法重建。
P0 fixture migration / compatibility:舊 Build 沒有 subrace_ref 時視為 None。
Background ref 續用 P0 欄位。
Background 的 mechanical grants透過 builder resolver產生:
不要把 background description text 當規則解析來源;使用 normalized structured data / adapter。
目前 production SRD 5.1 backgrounds.json 只有 Acolyte。這只是 Built-in content scope,不是 Builder capability 上限;Background resolver / canonical choice model 不得因 production 只有一筆資料而寫成 Acolyte special case。
Numeric Override UI 放 Advanced / Non-standard 區域,不與 Standard Array / Point Buy / Manual tabs 混在一起。
Summary 顯示:
Calculated: X
Current: Y ⚠
Origin: Numeric Override
P0 只有單 Character read 時,P1-B 建議補:
GET /api/characters
只回 Workshop list需要的 summary,不一次回整份 spellbook / inventory payload。
新增 / 擴充 pure domain logic:
character_builder/progression.py
character_builder/multiclass.py
核心輸入是 ordered Character Level records,不是 unordered class-level map。
Draft 概念:
level_choices: [
{ character_level: 1, class_ref: fighter, hp_method: first_level, ... },
{ character_level: 2, class_ref: fighter, hp_method: fixed, ... },
{ character_level: 3, class_ref: fighter, subclass_ref: ..., ... },
{ character_level: 6, class_ref: wizard, hp_method: manual, ... }
]
Compiler 最終仍產生 P0-compatible:
CharacterBuild.class_progression[]
CharacterBuild.hp_progression[]
CharacterBuild.subclasses[]
不建立第二份 mutually-authoritative class totals。
任何 class level 都由 ordered progression derive:
class_level_at(character_level_index)
Feature / Subclass / ASI timing 以當下該 class 已取得的 class level判定。
Content adapter 必須能取得:
如果 upstream classes.json 已有 structured multi_classing資料,直接 adapter 使用;缺失才補 project rule data。
不要用「if class == fighter」散落硬編碼。
在加入新 class level時驗:
Server validation是 authority;UI disabled只是 usability。
使用 levels.json / features.json 等 normalized content建立:
progression node
→ automatic feature refs
→ required builder choices
Automatic feature不需要使用者逐一勾選;Compiler加入 Build。
Builder adapter從 class progression知道 subclass selection timing。
Draft保存 user-selected subclass_ref;Compiler產生 P0 SubclassSelection。
如果 class未達 timing:blocking error。
每一個 Character Level保存:
hp_method
hp_base_gain
hp_method:
first_level
fixed_average
manual_rolled
hp_base_gain 是實際結果;Compile 時輸出 P0 hp_progression[]。
Fixed average值與 legality來自 rules/content,不在 React重算。
Manual Rolled Result只是輸入既有骰子結果;P1 不建立 server dice roll event。
Draft PATCH 修改 earlier level時,Server re-resolve整份 progression。
建議策略:
不要默默將不合法 option轉成另一個 default。
P1-D 完成 choices.py / features.py,使以下 source 都能輸出 canonical BuilderChoice:
Race
Subrace
Background
Class starting choice
Class feature
Subclass feature
ASI / Feat
future custom content
Resolver關注選擇結構,不負責 UI layout。
ability_score_bonuses 是累計值SRD data/srd5.1/levels.json 的 ability_score_bonuses 代表截至該 Class Level 累計取得的 ASI opportunity 數。不得使用 ability_score_bonuses > 0 判定『本級有 ASI』;那會讓第一次 ASI 之後的每個 level 都重複產生 ASI node。
對某一 class 的 Class Level L,本級新取得的 occurrence 固定由相鄰 Class Level delta 推導:
current = levels[class, L].ability_score_bonuses
previous = 0 if L == 1 else levels[class, L - 1].ability_score_bonuses
asi_occurrences_at_level = current - previous
規則:
delta == 0:本級沒有新的 ASI / Feat opportunity。delta > 0:本級建立恰好 delta 個 ASI / Feat opportunities;不要只當 boolean,避免未來資料一次增加多個 occurrence 時失真。delta < 0:視為 rules/content data error,啟動或 validation fail-fast;不得靜默修正。features[] 內的 Ability Score Improvement feature 可作交叉檢查/diagnostic evidence,但 cumulative delta 才是 occurrence count 的 canonical adapter rule;兩者矛盾時 fail-fast,不靠 feature name 猜。Draft progression node保存 ASI / Feat branch choice;CharacterBuild最終保存 resolved Ability Scores與 feat_refs。
為了 Version History / correction 可重建,P1可增加非-authoritative provenance / builder trace,例如:
build_choices / selection_records
若增加此欄位:
最簡單做法是 Compile 時同時產生 resolved fields與 deterministic selection trace,validation保證一致。
Server檢查:
Structural prerequisite計算時使用:
resolved Build score at that progression point
↓
applicable Numeric Override
↓
effective numeric value
↓
prerequisite evaluation
但 Numeric Override不產生假的 ASI node / subclass node。
不是只在 Confirm驗:
新增:
character_builder/spellcasting.py
必要時擴充 P0 domain/rules/spellcasting.py,但要區分:
不要把兩者混成一個巨大函式。
概念:
SpellcastingProfile
├─ profile_id
├─ source_type
├─ source_key
├─ class_ref
├─ ability_ref
├─ access_model
├─ max_spell_level
├─ known / prepared / spellbook limits
└─ resource_pool_type
Profile由 Build progression + content/rules derive,不靠 frontend猜。
P0 SpellAccessEntry.entry_id 是 Build內 logical stable id。
P1 compiler產生 deterministic entry id,應至少包含 source identity與 spell key,避免同 spell different source collision。
概念:
<class-or-feature-source>::<spell-key>::<access-kind>
實際格式可調整,但 Build版本內唯一且重新 Compile同樣 choice應穩定。
Draft保存 Wizard spellbook selection provenance;Compiler輸出:
SpellAccessEntry(access_type="spellbook")
Initial prepared choice保存於 Draft initial-state seed,Create Confirm後轉成:
CharacterState.prepared_spell_entry_ids[]
不得在 CharacterBuild.spell_access_entries建立 prepared access type。
對 Cleric / Druid / Paladin等:
如果 P0 prepared_spell_entry_ids 的「必須指向 Build access entry」語意不足以承載 prepared-from-full-class-list caster,P1-E 必須正式修正模型,不能為了遷就 P0 fixture把 Cleric全 spell list複製成幾百個 Build access entries而失去語意。
建議做法之一:
PreparedSpellSelection
├─ spell_key
├─ source_profile_id
└─ source_access_entry_id? # spellbook / explicit access時可有
並提供 migration / compatibility adapter讓 P0 Wizard fixture仍可讀。
具體最終 schema在 P1-E實作前先以現有測試與所有 SRD caster需求驗證,選擇最簡單而語意正確者。
Known spell selection直接編譯為 Build known access entries。
Level Up replacement:Draft記錄 remove old known / add new known的選擇意圖;Confirm產生完整新 Build snapshot,不修改舊 version。
由 content/rules resolver自動或經 feature choice產生:
access_type = always_prepared / granted
如果需要 counts_against_prepared_limit 等 metadata,P1-E正式擴充 SpellAccessEntry或 profile metadata,不靠 UI寫死 domain spell名字。
新增 pure function:
calculate_multiclass_spell_slots(build/progression, rules_data)
輸出 capacity read model,不直接寫 current used/remaining。
一般 slot capacity與各 profile spell access分開。
使用獨立 resource pool key / DTO,例如概念:
resource_pool_id = pact_magic:<source-key>
P0 CharacterState.resources 可承載 class-specific resource;若要讓 Character Sheet清楚表示 slot level / count,P1-E可擴充 State / Sheet DTO,但不把 Pact Magic塞入一般 spell_slots[level]當同一 pool。
Upstream normalization trap: SRD levels.json 的 Warlock 也使用 spell_slots_level_1~spell_slots_level_9 欄位;例如 Warlock Lv3 以 spell_slots_level_2 = 2 表示兩個二環 Pact Magic slots。欄位名稱本身無法區分 normal spell slots 與 Pact Magic。
因此 adapter 順序固定:
class / spellcasting source identity
→ classify resource_pool_type (normal_multiclass_slots | pact_magic | other)
→ parse that source's level row slot-shaped fields
→ only normal_multiclass_slots may enter combined-slot aggregation
Built-in SRD Warlock 可由 stable class identity(例如 srd5.1:class:warlock)映射到 pact_magic,但這個 source-specific mapping 必須封裝在 content/rules adapter;不要在 combined-slot calculator、API、React 等處散落 if class == warlock。未來 custom class 也應以 profile / rules metadata 分類,而不是靠英文名稱。
P0 ResourceCounter 同時保存 used + remaining。P1-E / P1-G reconciliation需要知道 Build-derived capacity。
P1應建立單一 capacity calculation truth:
Build -> ResourceCapacity
State -> current usage
Sheet -> capacity + usage -> remaining
若維持 P0 State的 used + remaining欄位,所有 mutation / reconciliation都必須由同一 Server helper重新驗證 used + remaining == capacity;不要讓兩者逐漸漂移。
若 migration成只保存 usage更簡單,可在 P1-E做,但必須保留 P0 persistence migration與 API compatibility測試。
新增:
character_builder/equipment.py
輸入 SRD class/background structured options,輸出 canonical BuilderChoice。
必須支援目前 SRD data實際出現的:
counted_reference
choice
nested choice
equipment_category
quantity
automatic equipment
不要只針對 P0 fixture 的 Chain Mail / Shield寫特殊 case。
Draft selections → P0-compatible:
CharacterBuild.starting_equipment[]
entry_id deterministic / unique。
Initial live inventory由 Starting Equipment compile結果複製一次:
starting_equipment
↓ create confirm only
InventoryEntry[]
↓
CharacterState.inventory_state
之後兩邊完全分離。
P1-F 建立:
build_initial_character_state(build_candidate, initial_state_seed)
至少初始化:
不因 Builder Confirm自動建立 Room / Campaign state。
Server提供 BuilderReviewDTO,包含:
identity
race/background
level progression
subclasses
abilities + overrides
proficiencies / skills
features / feats
spell profiles / access / initial prepared
starting equipment
calculated summary
issues
can_confirm
Frontend不要自己將 raw Draft拼成一份可能與 compiler不同的 Review。
P1-F 已經會建立 Builder-produced Character Version 1,所以 version metadata schema 不能延到 P1-G 才出現。P1-F migration 應在現有 character_versions 上建立至少:
version_kind
parent_version_id nullable
superseded_by_version_id nullable
change_note nullable
Baseline semantics:
version_kind=legacy;Build payload 不改寫。version_kind=create。parent_version_id = null、superseded_by_version_id = null;沒有 correction lineage。level_up / build_edit / correction kind 與 lineage,不再新增另一套 metadata schema 或重新分類 P1-F create versions。若最終採獨立 lineage table 而不是 superseded_by_version_id,也必須在 P1-F 先完成等價的 create / legacy baseline,讓 P1-F 與 P1-G 對 version identity 的語意一致。
新增:
POST /api/character-builder/drafts/{draft_id}/confirm
Create mode服務流程:
lock/load draft
↓
compile against current ContentRegistry
↓
full validate
↓
construct CharacterBuild
↓
construct CharacterState
↓
transaction:
characters insert
character_versions v1 insert
character_states insert
current_version_id update
↓
return Character / Sheet location
應重用 / 擴充 CharacterRepository,不要在 FastAPI router直接複製 SQL。
P0 CharacterRepository.create_character() 已能 atomic建立 Character + v1 + State。
P1-F優先重用它;必要擴充 transaction / metadata時,將版本建立能力抽成 repository method,不要繞過現有 reference/state validation。
Compiler / validation / DB failure:
Create confirm成功後可:
P1偏向 delete已完成 Create Draft即可,正式 truth已進 Character Version;若為了避免 double-submit需要狀態,可保留最小 confirmed marker / idempotency contract。
重點是 double-click confirm不得建立兩隻角色。
P0 character_versions 目前:
id
character_id
version_no
build_payload
created_at
P1-F 已先完成 version metadata baseline 與 P0 legacy backfill。P1-G 沿用同一 schema,開始填入 parent / level-up / edit / correction lineage;不得在此再新增第二套版本分類。
不要把 Current State塞進 version table。
如果 P1-F 最終採獨立 lineage table 而不是 superseded_by_version_id,P1-G 就沿用該 table;核心契約仍是 Build payload immutable、history可追 lineage。
至少:
create
level_up
build_edit
correction
P0既有 version migration / read為 legacy;P1-F Builder Create 才使用 create。P1-G 新版本使用 level_up / build_edit / correction。
Level Up:
load current PersistedCharacter
↓
create BuilderDraft(mode=level_up)
↓
base_version_id=current_version_id
↓
seed choices from current Build / builder trace
↓
append target level = current + 1
若舊 P0 Build沒有完整 builder trace,P1需提供 adapter從 resolved Build fields產生可編輯的最低可重建 Draft;不能要求刪掉舊角色重建。
可以沿用統一 Create Draft endpoint:
POST /api/character-builder/drafts
{
"mode": "level_up",
"character_id": "..."
}
或提供 convenience endpoint:
POST /api/characters/{id}/level-up-draft
不論 URL,最後必須走同一 BuilderService。
Confirm transaction內重新查:
characters.current_version_id == draft.base_version_id
不相等:reject stale_build_version。
不能只在 Draft開啟時檢查。
Repository新增 transaction method概念:
create_build_version_and_reconcile_state(
character_id,
expected_base_version_id,
new_build,
reconciled_state,
version_kind,
...
)
transaction內:
全部成功才 commit。
新增 pure domain:
character_builder/reconciliation.py
輸入:
old_build
old_state
new_build
old_sheet / calculated capacities
new_sheet / calculated capacities
輸出:
StateReconciliationPreview
├─ proposed_state
├─ changes[]
├─ blocking_issues[]
└─ warnings[]
Confirm 使用同一結果重新計算,不接受 frontend直接 POST arbitrary reconciled state。
Server:
old_damage = max(0, old_max_hp - old_current_hp)
new_current_hp = clamp(new_max_hp - old_damage, 0, new_max_hp)
這是 Level Up / Build edit的 reconciliation,不是 healing transaction。
對每個 resource pool:
old_capacity
old_used
new_capacity
原則:
new_used = min(old_used, new_capacity)
new_remaining = new_capacity - new_used
若 capacity下降導致 old_used > new_capacity:
一般升級 capacity增加時,舊 used數不變,所以新增 capacity成為 available。
普通 Level Up:
delta_total > 0 的部分加到 available。Build correction造成 die type / total下降時,依 total invariant clamp並在 preview說明。
Server用新 Build spell eligibility重新驗 existing prepared selections:
不 silent drop。
Inventory entry ids、quantity、equipped/carried原樣帶過,再用新 Build下的 state validation檢查。
Armor proficiency改變不代表自動丟裝備;角色可以攜帶甚至穿著不熟練裝備。P1不要因 Build edit擅自移除物品。
至少:
GET /api/characters/{id}/versions
GET /api/characters/{id}/versions/{version_no-or-id}
History read不回 Current State snapshot,因 Character Version不是 live save state。
建議:
/characters/:characterId/versions
顯示 summary與current marker;可查看 build snapshot / summary。
P1不做 Git branch UI、不做 Restore Current State。
P1-G建立同一版本化管線,但 UI先保持簡單:
Correct Build:修正當前 Build,產生 correction version。Edit Build:提供長期合法 Build change,如 spellbook addition / roleplay等;仍 validate。未來 P2 permission layer包住同一 service。
P1-H將 CI 升級為 P1 Full Regression,至少包含:
backend pytest
ContentRegistry / builder rules data validation
Alembic from fresh DB
Alembic upgrade from P0 schema
frontend TypeScript build
Vitest
Playwright real backend
Docker Compose readiness
P0 regressions
P1 builder fixtures / E2E
建立 P1 fixture helper,不依靠人工點 UI先建資料。
fixture來源可分:
Draft fixture
Expected CharacterBuild
Expected CharacterState
Expected Sheet summary
測試要比較 compile結果,不只看 can_confirm = true。
至少有 real-backend Playwright:
Character Workshop
→ Create
→ complete Builder
→ Review
→ Confirm
→ Character Sheet
→ reload
至少:
existing deterministic character
→ Level Up
→ make level choices
→ reconciliation preview
→ Confirm
→ Version History shows N+1
→ Character Sheet reflects new Build
→ Current State preserved per contract
P1-H必須證明在沒有:
Room
Campaign
Session
Seat
AI token
MCP server
Combat
Adventure
的情況下仍能 Create / Manage / Level Up Character。
P1 API 延續 P0 machine-readable error方向。
Builder domain error建議 code例如:
draft_not_found
stale_draft_revision
missing_required_choice
invalid_choice
prerequisite_not_met
invalid_multiclass
invalid_subclass_timing
invalid_asi_choice
feat_prerequisite_not_met
invalid_spell_choice
invalid_prepared_selection
invalid_equipment_choice
cannot_confirm_draft
stale_build_version
實際 code可在實作時微調,但 tests必須對 machine-readable code做 assertion;不要只 assert英文錯誤句子。
所有正式 write:
HTTP intent
↓
BuilderService / CharacterService
↓
Domain resolve + validation
↓
Repository transaction
↓
Authoritative persisted state
↓
DTO response
禁止:
build_payload。character_versions.build_payload。P1 migration必須支援從目前 P0 schema直接升級。
至少保證:
legacy、P1-F Builder Create v1 標為 create;P1-G 只沿用同一 schema 寫入後續 lineage,且不修改既有 Build payload。P1 不建立:
如果實作某個 P1功能只需要一個小 adapter,不要因此提前長出後續 Phase architecture。