Full Go port of suitbuilder.py's ConstraintSatisfactionSolver (the LIVE solver
behind the suitbuilder UI; main.py's /optimize/suits is legacy/unused):
- suit_model.go: CoverageMask + reductions, SuitItem/ItemBucket/SuitState,
SpellBitmapIndex, ScoringWeights, SearchConstraints, CompletedSuit.to_dict,
ItemPreFilter, set name<->id maps. Every sort carries (character_name, name)
tiebreakers for deterministic results.
- suit_solver.go: the 5-phase pipeline — load_items (fed in-process by the Go
/search/items), create_buckets (+multi-slot/generic-jewelry expansion),
apply_reduction_options, sort_buckets, and the depth-first recursive_search
with both Mag-SuitBuilder pruning rules, can_add_item constraints (set limits,
jewelry spell contribution, strict spell mode), scoring, and finalize.
- suit_http.go: POST /suitbuilder/search (SSE: phase/log/suit/progress/complete),
GET /suitbuilder/characters, GET /suitbuilder/sets.
- search.go: refactor handleSearchItems -> shared runSearch (the solver reuses
it so both see identical rows); emit slot_name (get_sophisticated_slot_options
+ translate_equipment_slot); fix the trinket slot_names clause to exclude
%bracelet% (matches Python).
- slotname.go: the EquipMask-based slot translation, loaded from the enum DB.
Validation: 9/9 scenarios stream byte-identical suits vs the Python service on
production data (no-spell, multi-character, locked slots with/without spells,
spell constraints, alternate set pairs, primary-only). ~45x faster than Python.
Three subtle bugs found and fixed during validation:
- slot_name is load-bearing, not display: jewelry's computed_slot_name is empty,
so load_items falls back to slot_name to bucket rings/neck/wrists/trinket.
- Python scoring uses floor division (total_armor // 100); total_armor goes
negative (non-armor items carry armor_level -1) so Go's truncation was +1 off.
- the trinket fetch must exclude bracelets or they duplicate the Wrist buckets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>