From aa3139b5868c4208f2541bad50394000d33f6942 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 14 Jul 2026 23:26:29 +0200 Subject: [PATCH] docs(spec): inventory search spell filters + jewelry-type selection Co-Authored-By: Claude Fable 5 --- ...-inventory-spell-jewelry-filters-design.md | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-14-inventory-spell-jewelry-filters-design.md diff --git a/docs/superpowers/specs/2026-07-14-inventory-spell-jewelry-filters-design.md b/docs/superpowers/specs/2026-07-14-inventory-spell-jewelry-filters-design.md new file mode 100644 index 00000000..15864ff7 --- /dev/null +++ b/docs/superpowers/specs/2026-07-14-inventory-spell-jewelry-filters-design.md @@ -0,0 +1,106 @@ +# Inventory search: spell filters + jewelry-type selection + +**Date:** 2026-07-14 +**Status:** Approved + +## Problem + +The inventory search UI (`static/inventory.html`) has a Legendary Cantrips +checkbox grid and a "Spell name contains..." text box, and `inventory.js` +sends them as `legendary_cantrips` / `spell_contains` query params — but the +Go inventory service (`go-services/inventory-go/search.go`) never implemented +these filters, so they are silently ignored. The legacy Python service +(`inventory-service/main.py:3345-3452`) supported them. + +Additionally there is no way to narrow a jewelry search to a specific type +(ring / bracelet / necklace / trinket), even though the backend already +supports this via the `slot_names` param (used by the suitbuilder). + +Goal: check e.g. Invulnerability + Summoning, pick jewelry type Ring, and get +back only rings carrying BOTH cantrips. + +## Decisions + +- **AND semantics** for multiple checked cantrips (matches legacy intent): + an item must carry every checked cantrip. +- **Multi-select checkboxes** for jewelry type (Ring, Bracelet, Necklace, + Trinket), shown only when the equipment-type selector is on Jewelry. + None checked = all jewelry. +- **Also wire `spell_contains`** (the dead free-text box) and `has_spell` + (exact-name variant referenced by the agent tools) in the same pass — same + mechanism, nearly free. + +## Backend design (`go-services/inventory-go/search.go`) + +Three new query params handled in `runSearch`, emitted as ordinary `WHERE` +conditions on the `items_with_slots` CTE so they compose with all existing +filters, sorting, pagination, and the count query: + +1. **`legendary_cantrips`** — CSV of display names (e.g. + `Legendary Invulnerability,Legendary Summoning Prowess`). For each name, + case-insensitive substring match (both directions, mirroring Python's + flexible matching) against the in-memory spell enum map (`s.spells`, + loaded from `comprehensive_enum_database_v2.json`) to collect that + cantrip's spell IDs. Each cantrip emits: + + ```sql + EXISTS (SELECT 1 FROM item_spells sp + WHERE sp.item_id = db_item_id AND sp.spell_id IN ($n, ...)) + ``` + + Cantrips are ANDed. A cantrip matching zero spells contributes `1 = 0`. + + Note: this deliberately does NOT replicate Python's + `COUNT(DISTINCT spell_id) >= N` trick, which can false-positive when one + cantrip name matches two spell IDs on the same item while another matches + zero. Per-cantrip EXISTS is strictly correct AND semantics. + +2. **`spell_contains`** — free text; collect all spell IDs whose name + contains the text (case-insensitive); a single `EXISTS ... IN (ids)`. + No matching spell → `1 = 0`. + +3. **`has_spell`** — exact name match (case-insensitive) to one spell ID; a + single `EXISTS`. Unknown name → `1 = 0`. + +Spell IDs originate from our own enum map, not user input, but are still +bound via the existing `argBuilder` positional params for consistency. + +## Frontend design (`static/inventory.html` + `static/inventory.js`) + +- New "Jewelry Type" filter card with checkboxes **Ring, Bracelet, Necklace, + Trinket**, shown only when equipment type = Jewelry (same show/hide pattern + as the weapon-type selector). +- `inventory.js` maps checked boxes to `slot_names` CSV using the backend's + slot vocabulary: Ring→`Ring`, Bracelet→`Bracelet`, Necklace→`Neck`, + Trinket→`Trinket`. None checked → param omitted. +- The cantrip grid and spell text box already send the correct params — no + JS changes needed for those. +- The existing `slotNameClause` per-type OR approaches (including the + Trinket clause's `%bracelet%` exclusion) are reused untouched. + +## Testing + +TDD in `go-services/inventory-go/search_test.go` (new file): + +- Name→spell-ID matcher: substring both-directions semantics, case + insensitivity, zero-match behavior. +- Condition builder: correct EXISTS SQL shape, one clause per cantrip ANDed, + `1 = 0` on no-match, arg binding via argBuilder. + +Query execution against the real DB is validated manually after deploy +(consistent with the rest of search.go, which has no DB-backed tests). +The Dockerfile's `RUN go test ./...` gates the image build. + +## Deploy + +- Backend: standard inventory-go sync + build + recreate (cutover override). +- Frontend: `inventory.html`/`inventory.js` are plain static files on the + bind mount — `git pull` on the server picks them up; `deploy-frontend.sh` + (React build) is NOT involved. + +## Out of scope + +- Cantrip tiers other than Legendary in the checkbox grid (the free-text box + covers Epic/Major/etc. searches). +- React frontend inventory window changes. +- Suitbuilder behavior (unchanged; it passes its own slot_names).