Browse the rules, stage 3: filters for each game system (spell tradition and rank, creature traits and size, item price and bulk…) #1242

Closed
opened 2026-10-26 23:37:19 +00:00 by claude-bot · 5 comments
Collaborator

Why

Owner, 26 Oct 2026, stage 3 of 4, after #1240 and its stage 2 sibling. Kind and level are the only filters that work for every system. Real browsing in PF2e and 5e is by the system's own properties.

What

Filters that come from the game system, shown in #1240's filter row for the open kind. Examples, to be confirmed against what the packs actually carry:

  • PF2e:
    • spells: tradition, rank, school or trait, cantrip/focus;
    • creatures: level, size, traits, rarity;
    • items: level, price, bulk, traits, rarity;
    • feats: level, type (ancestry/class/general/skill), the class or ancestry they belong to;
    • rarity everywhere.
  • 5e SRD 5.2.1:
    • spells: level, school, class lists, ritual/concentration;
    • creatures: challenge rating, type, size;
    • items: category, rarity, attunement;
    • feats: category.
  • Sort options beyond A–Z where they mean something: level, price, CR.

Design questions

  • Where the filter definitions live. D76 made sheet definitions data in packs ("definitions are data, in packs… no code"). Browse filters probably belong there too: a pack, or the system's definition, declares which fields of which kind are filterable and how (choice, range, multi-select). Then a third system works without code. Alternatively the adapters in app/reference/fields.py own them. This needs a decision record.
  • How the fields are stored and queried. Are the needed values on the reference rows in a queryable shape (JSONB with an index, or extracted columns)? Measure on the 5,157-record PF2e pack.
  • Phone layout: the filters collapse behind a "Filters (3)" button.

Process

Investigate what the packs carry first and post a short report here. Then the decision record, the drawing, sign-off, build. Tests per system, plus a pack without the declared fields (the filter is hidden, not broken).

## Why Owner, 26 Oct 2026, stage 3 of 4, after #1240 and its stage 2 sibling. Kind and level are the only filters that work for every system. Real browsing in PF2e and 5e is by the system's own properties. ## What Filters that come from the game system, shown in #1240's filter row for the open kind. Examples, to be confirmed against what the packs actually carry: - **PF2e:** - spells: tradition, rank, school or trait, cantrip/focus; - creatures: level, size, traits, rarity; - items: level, price, bulk, traits, rarity; - feats: level, type (ancestry/class/general/skill), the class or ancestry they belong to; - rarity everywhere. - **5e SRD 5.2.1:** - spells: level, school, class lists, ritual/concentration; - creatures: challenge rating, type, size; - items: category, rarity, attunement; - feats: category. - **Sort options** beyond A–Z where they mean something: level, price, CR. ## Design questions - **Where the filter definitions live.** D76 made sheet definitions data in packs ("definitions are data, in packs… no code"). Browse filters probably belong there too: a pack, or the system's definition, declares which fields of which kind are filterable and how (choice, range, multi-select). Then a third system works without code. Alternatively the adapters in `app/reference/fields.py` own them. This needs a decision record. - **How the fields are stored and queried.** Are the needed values on the reference rows in a queryable shape (JSONB with an index, or extracted columns)? Measure on the 5,157-record PF2e pack. - Phone layout: the filters collapse behind a "Filters (3)" button. ## Process Investigate what the packs carry first and post a short report here. Then the decision record, the drawing, sign-off, build. Tests per system, plus a pack without the declared fields (the filter is hidden, not broken).
Author
Collaborator

Picking this up with the first step, the investigation. I'll report what the PF2e and 5e packs carry for each kind's candidate filters, and whether it's in a queryable shape (with timings on the PF2e pack), before any decision record or drawing.

Picking this up with the first step, the investigation. I'll report what the PF2e and 5e packs carry for each kind's candidate filters, and whether it's in a queryable shape (with timings on the PF2e pack), before any decision record or drawing.
Author
Collaborator

Investigation: what the packs carry (read-only)

Dev has no starter packs loaded, so the field counts come from the v4.6.2 release assets: pf2e-orc.qbpack (5,157 records) and open5e-srd.qbpack (998). The timings ran on a throwaway Postgres 16 holding both packs with today's indexes.

What exists

"col" means a column on reference_entries.

PF2e

  • Spells (1,486):
    • tradition fields.traditions[] (4 values);
    • rank fields.rank and col level;
    • cantrip fields.cantrip and focus fields.focus;
    • traits col traits (89).
  • Creatures (568):
    • level col;
    • size stats.values.size (6 values);
    • traits col (111).
  • Items (504):
    • level fields.level;
    • price fields.price_cp (98%);
    • bulk fields.bulk (0.1 is L);
    • category fields.category;
    • traits col.
  • Feats (1,817):
    • level;
    • type fields.category (class, ancestry, skill, general);
    • class/ancestry has no field of its own: it exists only as a trait.
  • Rarity:
    • extra.traits.rarity on 94% of records;
    • the traits col holds it only when it isn't common, so "common" means "no rarity trait".

5e SRD 5.2.1

  • Spells (339):
    • level fields.level and col level;
    • school fields.school;
    • class lists fields.classes[];
    • ritual fields.ritual and concentration fields.concentration.
  • Creatures (331):
    • CR col level and stats.values.challenge_rating;
    • type stats.values.creature_type (14 values);
    • size (5 values).
  • Items (203):
    • category only;
    • rarity and attunement are absent, because the SRD pack is mundane gear.
  • Feats (17): category.

Storage and speed

Every value can be queried as it is stored, in one of three places: the level/traits columns, stats, or fields. None of traits, stats or fields has an index.

Query Today With GIN/expression indexes
PF2e creatures, level 3–8, trait undead, size Medium 1.3 ms 1.3 ms
PF2e feats, trait fighter, page 5 5.4 ms 1.6 ms
PF2e spells, occult, rank 1–3 115–121 ms 42 ms

fields is the slow one because it also carries each record's full description, which gets unpacked for every matching row. Proposal: at import, copy the filterable values into a small facets JSONB column with one GIN index. This hasn't been measured yet.

Where the definitions live: recommendation

Put them in the system or pack definition, per D76 ("definitions are data, in packs"), not in the adapters.

  • The same filter sits at a different path in each system: PF2e rank is in fields, 5e CR is in level/stats, rarity is in extra.
  • Packs differ. The 5e SRD has no rarity, so a declared filter whose path a pack lacks is hidden.
  • Some filters are derived and need two declaration forms, "trait from this list" and "absent means X":
    • "common" means no rarity trait;
    • a feat's class is a trait.
  • The importer should fill facets from the same declaration, so the stored data and the filters can't drift apart.

Not checked: the cost of the visibility and campaign-pack filters at their real shape, 5e magic items (the pack has none), and Postgres 18.

Next step: a decision record (filters declared in the system definition, plus a facets column filled at import), measured on 18 once #1299 merges. Then the drawing.

## Investigation: what the packs carry (read-only) Dev has no starter packs loaded, so the field counts come from the v4.6.2 release assets: `pf2e-orc.qbpack` (5,157 records) and `open5e-srd.qbpack` (998). The timings ran on a throwaway Postgres 16 holding both packs with today's indexes. ### What exists "col" means a column on `reference_entries`. **PF2e** - **Spells (1,486):** - tradition `fields.traditions[]` (4 values); - rank `fields.rank` and col `level`; - cantrip `fields.cantrip` and focus `fields.focus`; - traits col `traits` (89). - **Creatures (568):** - level col; - size `stats.values.size` (6 values); - traits col (111). - **Items (504):** - level `fields.level`; - price `fields.price_cp` (98%); - bulk `fields.bulk` (0.1 is L); - category `fields.category`; - traits col. - **Feats (1,817):** - level; - type `fields.category` (class, ancestry, skill, general); - class/ancestry has **no field of its own**: it exists only as a trait. - **Rarity:** - `extra.traits.rarity` on 94% of records; - the `traits` col holds it only when it isn't common, so "common" means "no rarity trait". **5e SRD 5.2.1** - **Spells (339):** - level `fields.level` and col `level`; - school `fields.school`; - class lists `fields.classes[]`; - ritual `fields.ritual` and concentration `fields.concentration`. - **Creatures (331):** - CR col `level` and `stats.values.challenge_rating`; - type `stats.values.creature_type` (14 values); - size (5 values). - **Items (203):** - category only; - rarity and attunement are **absent**, because the SRD pack is mundane gear. - **Feats (17):** category. ### Storage and speed Every value can be queried as it is stored, in one of three places: the `level`/`traits` columns, `stats`, or `fields`. None of `traits`, `stats` or `fields` has an index. | Query | Today | With GIN/expression indexes | |---|---|---| | PF2e creatures, level 3–8, trait undead, size Medium | 1.3 ms | 1.3 ms | | PF2e feats, trait fighter, page 5 | 5.4 ms | 1.6 ms | | PF2e spells, occult, rank 1–3 | **115–121 ms** | 42 ms | `fields` is the slow one because it also carries each record's full description, which gets unpacked for every matching row. **Proposal:** at import, copy the filterable values into a small `facets` JSONB column with one GIN index. This hasn't been measured yet. ### Where the definitions live: recommendation Put them in the system or pack definition, per D76 ("definitions are data, in packs"), not in the adapters. - The same filter sits at a different path in each system: PF2e rank is in `fields`, 5e CR is in `level`/`stats`, rarity is in `extra`. - Packs differ. The 5e SRD has no rarity, so a declared filter whose path a pack lacks is hidden. - Some filters are derived and need two declaration forms, "trait from this list" and "absent means X": - "common" means no rarity trait; - a feat's class is a trait. - The importer should fill `facets` from the same declaration, so the stored data and the filters can't drift apart. **Not checked:** the cost of the visibility and campaign-pack filters at their real shape, 5e magic items (the pack has none), and Postgres 18. Next step: a decision record (filters declared in the system definition, plus a `facets` column filled at import), measured on 18 once #1299 merges. Then the drawing.
Author
Collaborator

The design note and the drawing are up for sign-off in PR #1307.

  • The note: docs/design/browse-filters.md. Each system declares its filters in a browse definition (data, as D76). The import copies the values into a facets column with one GIN index.
  • Measured on PostgreSQL 18.6 with both starter packs:
    • the occult rank 1–3 spell filter: 102 ms → 5 ms, and its count 158 ms → 6 ms;
    • the item price filter: 14 → 2.5 ms;
    • filling the column: 0.8 s at import.
  • The drawing: lane 6g, https://questboard-dev.rhoving.com/feedback/prototype/lane6g.
  • Questions for the owner: Q199–Q202 (SF1–SF4) on the Questions page: which filters, counts on choices, the phone, and sorting.

The build waits for the answers.

The design note and the drawing are up for sign-off in PR #1307. - **The note:** `docs/design/browse-filters.md`. Each system declares its filters in a browse definition (data, as D76). The import copies the values into a `facets` column with one GIN index. - **Measured on PostgreSQL 18.6** with both starter packs: - the occult rank 1–3 spell filter: 102 ms → 5 ms, and its count 158 ms → 6 ms; - the item price filter: 14 → 2.5 ms; - filling the column: 0.8 s at import. - **The drawing:** lane 6g, https://questboard-dev.rhoving.com/feedback/prototype/lane6g. - **Questions for the owner:** Q199–Q202 (SF1–SF4) on the Questions page: which filters, counts on choices, the phone, and sorting. The build waits for the answers.
Author
Collaborator

The owner answered Q199–Q202 on 2 Nov, all as drawn. Recorded as D310–D313:

  • D310: the filters as drawn;
  • D311: counts on the choices, counted with the other filters applied;
  • D312: on a phone, the system's filters fold behind "Filters (n)";
  • D313: sorting by A–Z, plus level, rank, price or CR where a kind has one.

The drawing says "Decided". Building now on feat/1242-system-filters, backend and frontend in parallel against one API contract:

  • f_<key> and sort params;
  • GET …/reference/filters with the counts;
  • facets filled at import, and refilled at worker start when a definition changes.

Out of scope for this build: a pack carrying its own browse definition. It will be filed as a follow-up.

The owner answered Q199–Q202 on 2 Nov, all as drawn. Recorded as D310–D313: - **D310:** the filters as drawn; - **D311:** counts on the choices, counted with the other filters applied; - **D312:** on a phone, the system's filters fold behind "Filters (n)"; - **D313:** sorting by A–Z, plus level, rank, price or CR where a kind has one. The drawing says "Decided". Building now on `feat/1242-system-filters`, backend and frontend in parallel against one API contract: - `f_<key>` and `sort` params; - `GET …/reference/filters` with the counts; - `facets` filled at import, and refilled at worker start when a definition changes. Out of scope for this build: a pack carrying its own browse definition. It will be filed as a follow-up.
Author
Collaborator

Done: #1312 merged on 2 Nov, after the owner approved the wording (part m) and CI went green. The full backend suite on postgres:18 gave 10500 passed, 0 failed.

What shipped:

  • browse definitions for PF2e and 5e (D310), and reference_entries.facets with its GIN index. The facets fill at import, and refill at worker start when a definition changes, so existing installs need no manual step;
  • f_* and sort everywhere, /reference/filters with exclude-self counts (D311), and sorts (D313);
  • the filter row, "Clear them", Sort, and filters in the address;
  • the phone fold (D312);
  • also fixed: cantrips read "Cantrip N", and a row's summary drops its markdown.

Follow-up: packs carrying their own browse definition, #1308.

Done: #1312 merged on 2 Nov, after the owner approved the wording (part m) and CI went green. The full backend suite on postgres:18 gave 10500 passed, 0 failed. What shipped: - browse definitions for PF2e and 5e (D310), and `reference_entries.facets` with its GIN index. The facets fill at import, and refill at worker start when a definition changes, so existing installs need no manual step; - `f_*` and `sort` everywhere, `/reference/filters` with exclude-self counts (D311), and sorts (D313); - the filter row, "Clear them", Sort, and filters in the address; - the phone fold (D312); - also fixed: cantrips read "Cantrip N", and a row's summary drops its markdown. Follow-up: packs carrying their own browse definition, #1308.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
rbrooks/Quest-Board#1242
No description provided.