refactor: move award application and progression choice resolution into the library#159
Merged
QMalcolm merged 3 commits intoJul 21, 2026
Merged
Conversation
Part of #131. Award semantics lived in the JSON server: apply_award!/3 interpreted value_type ("integer" / "next_level_xp") and built effects, and compute_next_level_xp!/3 owned the level-up XP computation. Per the library/config boundary, value_type and its values are structural vocabulary — the library's own language for interpreting award concepts — so only the JSON server could apply awards, and any second frontend would have to re-implement the interpretation. Characters.apply_award/4 now owns the pipeline: award lookup, value resolution, effect construction, and pending-choice-slot recompute (an award can change the character's level and thereby unlock slots, so the returned character is always internally consistent). The server handler shrinks to load / call / save / serialize plus error-message formatting, and apply_award! / compute_next_level_xp! are deleted. Vocabulary gains the reserved "award" type ID and "value_type" joins the structural metadata keys (documented in the Loader moduledoc); value_type is accordingly removed from dnd_5e_srd's custom_metadata_keys, where it had been parked while the server owned the interpretation. Tests live in a new test/characters/apply_award_test.exs rather than characters_test.exs, which is at its code-health size threshold. One deliberate edge-case change: a request with an explicit JSON null value is now treated as "compute the value" (nil is the library's compute sentinel) rather than being applied verbatim. The old behavior applied null as an effect value for next_level_xp awards — a latent crash at evaluation time — so the new interpretation is strictly safer, and no test or client sends explicit nulls. next_level_xp with an explicit value still applies the explicit value unvalidated, matching the old server behavior.
Part of #131. characters.ex sits at the code-health size threshold (~1000 lines), and the next change — moving progression choice resolution out of the CLI server — would push it well past. Award application is the start of a cohesive "advancement" area (awards now, progression choice resolution next), so it moves to a dedicated Characters.Advancement module before that lands. Characters.apply_award/4 remains the public entry point, delegating to the new module, so callers and the documented API are unchanged. The implementation is moved verbatim; Advancement reaches back into Characters only through its public functions (xp_to_next_level, compute_pending_choice_slots).
…rary Closes #131. The characters.resolve_choice progression handler re-implemented the library's own choice pipeline in the transport layer: already-selected filtering, slot capping (cap_resolved_for_slot), option rejection, and choice-number derivation — all logic that selection_progression_choices and pending_choices/3 already compute, drifting copies waiting to disagree. Characters.resolve_progression_choice/4,5 (implemented in Characters.Advancement) now owns resolution. Selection progressions validate against the pending_choices/3 entry for the progression, which is the single source of truth for valid options (already-selected exclusion and slot-level caps included); the decision is recorded via next_progression_decision — made public for this, as it is the canonical constructor any frontend needs — the pending slot is consumed, and the concept added to typed inventory. Value progressions record the method label and contribute the integer value to the progression's effect_target. Two consequences of validating against pending_choices rather than the server's re-derivation: - Over-resolution (resolving a selection progression with nothing pending) is now rejected with {:no_pending_choice, id}; the old server path only filtered already-selected options and would accept an extra resolution beyond required_count. No client or test relied on that. - Value progressions remain deliberately unvalidated against pending state, matching old behavior (documented on the function). The server handler shrinks to load / call / save / serialize plus error formatting, and cap_resolved_for_slot, consume_slot (moved into the library), load_progression_target!, parse_effect_target!, and apply_inventory_addition! are deleted. validate_concept_selection! stays: the scoped sub-choice handlers still use it against Serializer-computed option lists, which are outside this issue's scope.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #131.
What
Business logic that lived in the JSON server moves into the library, in three commits:
Characters.apply_award/4— owns award lookup,value_typeinterpretation ("integer"/"next_level_xp"), effect construction, and pending-choice-slot recompute (awards can level a character up, so the returned character always has consistent slots).Vocabularygains the reserved"award"type ID,value_typejoins the structural metadata keys (documented in the Loader moduledoc), and it's removed from dnd_5e_srd'scustom_metadata_keyswhere it was parked while the server owned the interpretation.apply_award!andcompute_next_level_xp!are deleted from the server.Extract
Characters.Advancement—characters.exsits at its code-health size threshold, so the advancement area gets its own module before growing.Characters.apply_award/4stays the public entry point.Characters.resolve_progression_choice/4,5— selection progressions now validate against thepending_choices/3entry, the single source of truth for valid options (already-selected exclusion and slot caps included), instead of the server's parallel re-derivation.cap_resolved_for_slot,consume_slot,load_progression_target!,parse_effect_target!, andapply_inventory_addition!are deleted from the server; both handlers are now load / call library / save / serialize plus error-message formatting.next_progression_decision/3becomes public as the canonical decision constructor.Behavior notes (details in commit messages)
{:no_pending_choice, id}; the old path would accept resolutions beyondrequired_count. No client or test relied on that.nullaward value is now treated as "compute the value" rather than applied verbatim (the old behavior storednullas an effect value — a latent evaluator crash).Tests
17 new library tests in
test/characters/apply_award_test.exsandtest/characters/resolve_progression_choice_test.exs(new files —characters_test.exsis at its size threshold). All 382 existing tests pass unchanged; credo and dialyzer clean.Summary by Bito