Tags across groups, made up as they're needed¶
Context and Problem Statement¶
Groups sort the inventory and the codex into one place each: a fire sword is under Weapons, the fire-resistance pot under Consumables, the fire grease somewhere else again. While playing, the player wants "everything fire" (before a burning boss, or to burn something), which the groups can't show. How do they get it, without keeping a list of tags by hand?
Goals and non-goals¶
- Goal: tap a tag on a tab and see only what carries it there; with a group chip, both (fire and Weapons: the fire weapons).
- Goal: no predefined tags; Claude makes one the first time it's needed and reuses it after, so "fire" doesn't split into "Feuer" and "fire damage". One flat tag per idea: "fire" covers dealing and resisting it.
- Goal: tags fixed through the chat like everything else: rename, merge, take off a row.
- Non-goal: a view across tabs ("fire" on items, codex and quests on one page). A tag filters the tab it's on.
- Non-goal: tags on sessions, goals, notes, the character or stat snapshots.
- Anti-goal: a tag giving away what the gate hides. A codex entry or quest the player hasn't reached stays hidden, its tags with it, and an unseen row's tag must not show up as a chip.
Considered Options¶
- A list on each row: a
tagsJSON column on items, codex entries and quests, the set worked out from what's in use. Least schema, but a rename or merge rewrites every row, and nothing but Claude keeps near-duplicates out. - A tag table and one link table (chosen):
tagsholds each name once per game;tag_linksjoins a tag to an item, codex entry or quest bywhatand id, the paircorrectalready uses. - Groups that take several values: breaks the folding bands (one item under several headings) and mixes two ideas.
Decision Outcome¶
Chosen option: a tag table and one link table, because a rename or merge is one row and a duplicate name can't exist.
Data (migration 007_tags)¶
tags:id,game_id,name,created_at; unique on (game_id,namefolded to lower case). Per game, like the inventory groups (games.item_kinds), so a second run of the same game starts with the same tags. Names in the player's words, short, lower case unless a proper name; nolocalName, since tags aren't the game's own names.tag_links:tag_id,what(item|codex|quest),row_id; primary key on all three; a row's removal removes its links (in the same transaction as the row's own delete, sincerow_idpoints at three tables). A tag with no links left is deleted.- Every read joins through the row, so links are scoped by player and the spoiler gate (
seen()) like the row itself.
Tools (MCP and the in-app chat)¶
record: items, codex entries and quests taketags: string[]. A name that matches an existing tag of the game (case-folded) reuses it; anything else creates one. The result listsnewTags, andnextasks Claude to say them back.briefing,item,codex: return the tags in use in the playthrough with their ids and counts (tags: {id, name, count}[]), counted over what came through the gate, so Claude reuses them before filing. The tool descriptions and the MCP instructions say: reuse an existing tag; make a new one only for an idea none of them covers; one tag per idea, not per direction (fire, not fire damage and fire resistance).itemandcodextake an optionaltagto list only what carries it ("what do I have against fire?").correct:what=item|codex|questtakestags(the whole new list for that row). A new targetwhat=tagrenames one (name), merges it into another (mergeInto, its links move over) or removes it. Merging and removing only when the player asked, like merging quests.
Web app¶
- Each item, codex entry and quest card shows its tags as small chips after its numbers.
- On the items, codex and quests tabs, a tag row at the top: the tags in use on that tab with their counts, only when there are any. Tapping one shows only the cards that carry it, across the folding group bands; tapping it again clears it. One tag at a time. A band with nothing left is hidden, and a band's count shows what is left.
- The tag row replaces the group chips of #37: the folding bands already give every group its place, and the player found the chip row redundant next to them.
- The row is one paper strip with the tab's colour as its spine, like the group bands; its chips are shaped like luggage tags (a
notch and a punched hole), since they hang on things across the groups.
aria-pressed, keyboard focus on the whole button, the lucideTagin a seal like the section headings, the part's Okabe-Ito tint. - Filtering happens in the browser (
components/TagFilter.tsx), every card rendered on the server withdata-tags; a link into a hidden card clears the tag (questlog:reveal).
REST¶
- Items, codex entries and quests carry
tags; the items and codex routes take?tag=;GET …/playthroughs/{id}/tagslists the tags in use with their ids and counts, as the tools do.
Consequences¶
- Good, because "everything fire" is one tap on whatever tab the player is on, and Claude keeps the set tidy as it files.
- Good, because rename and merge are one change each and duplicates can't exist.
- Bad, because tags are only as consistent as Claude's filing; the reuse rule and the list in every lookup are what keep them so.
- Bad, because
tag_linkspoints into three tables without a foreign key; deletes must clean up links themselves (tested).