Skip to content

refactor: centralize structural vocabulary in a Vocabulary module#153

Merged
QMalcolm merged 1 commit into
mainfrom
qmalcolm--refactor-vocabulary-constants
Jul 19, 2026
Merged

refactor: centralize structural vocabulary in a Vocabulary module#153
QMalcolm merged 1 commit into
mainfrom
qmalcolm--refactor-vocabulary-constants

Conversation

@QMalcolm

@QMalcolm QMalcolm commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Closes #125

What

ExTTRPGDev.RuleSystem.Vocabulary now owns every structural name the library defines:

  • Reserved concept type IDs: progression_type/0, roll_type/0, rolling_method_type/0
  • The structural metadata key set (moved from Loader's module attribute)
  • The progression decision shape: progression_scope/1 and progression_choice_id/1 (with doctests)

"character_progression" appeared at 12 sites in characters.ex and "choice_#{n}" at 4 — all now flow through Vocabulary. The four identical decision constructions (each also recomputing the next choice number) collapse into one next_progression_decision/3 constructor. Semantics remain documented in Loader's "Structural Vocabulary" section, which Vocabulary's moduledoc references.

Notable decisions

  • Pattern positions: consumers bind names to module attributes at compile time (@progression_type Vocabulary.progression_type()) since function calls can't appear in patterns. The literal still exists only in Vocabulary.
  • Dialyzer opaqueness: Loader binds the key set to an attribute instead of calling Vocabulary inline — dialyzer loses MapSet's opaqueness across module boundaries and flags the cross-module value in MapSet.union/2. Documented in a comment.
  • CLI app untouched: the server/serializer's own "character_progression" references stay — the app is allowed to be system-aware, and migrating it can ride along with the handler split (refactor(server): split dispatch into handler modules and standardize error formatting #132).
  • A proper Decision struct remains refactor: introduce Node, Effect, and Decision structs #133's territory.

Verification

No behavior change: full suite passes unchanged (311 + 55 tests, +2 new Vocabulary doctests), credo and dialyzer clean, grep confirms zero structural literals left in library logic outside vocabulary.ex.

Summary by Bito

  • Introduced a new `Vocabulary` module to centralize and manage reserved concept type IDs and metadata keys.
  • Refactored `Characters` and `Loader` modules to use the `Vocabulary` module instead of hardcoded strings for concept types and metadata keys.
  • Standardized the creation of progression decisions by introducing `next_progression_decision/3` in the `Characters` module.
  • Improved maintainability by ensuring structural vocabulary names are defined in a single location.

Reserved type IDs and structural key names were raw string literals
scattered across the library — "character_progression" alone appeared
at 12 sites in characters.ex, and the "choice_#{n}" progression choice
ID was constructed in 4 places. Renaming or extending the vocabulary
meant a grep-sweep with no compiler support.

ExTTRPGDev.RuleSystem.Vocabulary now owns every structural name:
reserved type IDs (character_progression / roll / rolling_method), the
structural metadata key set (moved from Loader's attribute), and the
progression decision shape (progression_scope/1 and
progression_choice_id/1). Semantics stay documented in Loader's
Structural Vocabulary section; Vocabulary's moduledoc points there.

Decisions:
- Consumers bind names to module attributes at compile time
  (@progression_type Vocabulary.progression_type()) because function
  calls are not allowed in pattern-match positions. Single source is
  preserved: the literal exists only in Vocabulary.
- The four identical progression-decision constructions (which also
  each recomputed the next choice number) collapse into one
  next_progression_decision/3 constructor in Characters. A Decision
  struct remains #133's territory.
- Loader binds the structural-keys MapSet to an attribute rather than
  calling Vocabulary per-call: dialyzer loses MapSet's opaqueness
  across the module boundary and rejects the cross-module value in
  MapSet.union/2 (call_without_opaque).
- The CLI app's own "character_progression" references are untouched:
  it is allowed to be system-aware per the project boundary doc, and
  migrating it can ride along with the server handler split (#132).

No behavior change; existing tests pass unchanged.

Closes #125
@QMalcolm
QMalcolm merged commit 495b88b into main Jul 19, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: centralize structural-vocabulary constants and document reserved types

1 participant