An unidentified item keeps its party name outside the wiki (#877) #994

Merged
claude-bot merged 5 commits from fix/877-title-reads into main 2026-10-01 17:54:18 +00:00
Collaborator

Closes #877. A privacy fix.

The problem

An unidentified item (IS-122) shows players its player_title and GMs its true title. The wiki's own reads made that swap; about 250 other reads of a .title under app/ had never been audited. PUT /lore/{id}/identity accepts any kind of page, not only items, so quests, characters and NPCs can be unidentified too.

Sixteen leaks, each fixed with a test

All eleven new tests in tests/wiki/test_unidentified_titles.py fail against main and pass here.

  1. History: a player who may edit a page read its true name in the version list.
  2. A name-clash oracle: creating or renaming a page to a guessed true name answered "Another page is already called …" with the item's id. An unidentified page is no longer a clash for anyone but a GM.
  3. The bell, email and Discord DM for a shown handout named the true title. Each reader now gets the name their role reads.
  4. The reveal's row on the session page stored the true title.
  5. The Discord handout post sent the true title to the channel.
  6. A session's cold open: open loops, for players.
  7. The quest log.
  8. The pre-session reminder's open threads, in Discord.
  9. The bot's /quests.
  10. The campaign timeline: a row with no words of its own fell back to the true title.
  11. "Add to a sheet ▾".
  12. A new character sheet's starting name, taken from its page.
  13. A level-up's milestone, "X reached level N".
  14. A rest's refusal, "X has no hit dice".
  15. The proposal-merge prompt: the model was given the true title, and its output goes into table-visible text with no GM review.
  16. The sort order: the wiki list was ordered by true title, so an item's position showed where its real name falls. It is now ordered by the name the reader knows.

New helpers in wiki_audience: sql_table_title() and sql_title_for(viewer), checked against the Python helpers on every page in the test campaign.

So it can't reopen quietly

tests/wiki/test_title_readers.py, in the shape of test_body_readers.py. It lists every .title read under app/ by module, function and expression. Each page-title read has a count and a reason; a new read, or a second read in an already-listed function, fails CI until someone classifies it. Its docstring states its blind spots (rows read by key, a title copied into another column, aliases, anything outside app/).

Decisions

  • Words written once and read by everyone (a reveal's row, the Discord handout post, a level-up milestone, a new sheet's name) use the party's name, so a GM reads the party's name there too. This is question W18 on the Questions page.
  • No bot contract change: payload shapes are untouched; only the value of title in two events.
  • Follow-ups are in #993 (GM-reviewed prompts still get the true name; a proposal about an unidentified item doesn't match its page).

Checked

  • Full backend suite: 6580 passed, 14 skipped. After merging main (#798): tests/wiki and the retention tests, 621 passed.
  • ruff 0.4.4 format and check clean; changelog check OK (a security fragment).

🤖 Generated with Claude Code

Closes #877. A privacy fix. ## The problem An unidentified item (IS-122) shows players its `player_title` and GMs its true title. The wiki's own reads made that swap; about 250 other reads of a `.title` under `app/` had never been audited. `PUT /lore/{id}/identity` accepts any kind of page, not only items, so quests, characters and NPCs can be unidentified too. ## Sixteen leaks, each fixed with a test All eleven new tests in `tests/wiki/test_unidentified_titles.py` fail against main and pass here. 1. **History**: a player who may edit a page read its true name in the version list. 2. **A name-clash oracle**: creating or renaming a page to a guessed true name answered "Another page is already called …" with the item's id. An unidentified page is no longer a clash for anyone but a GM. 3. **The bell, email and Discord DM** for a shown handout named the true title. Each reader now gets the name their role reads. 4. **The reveal's row** on the session page stored the true title. 5. **The Discord handout post** sent the true title to the channel. 6. **A session's cold open**: open loops, for players. 7. **The quest log**. 8. **The pre-session reminder's open threads**, in Discord. 9. **The bot's `/quests`**. 10. **The campaign timeline**: a row with no words of its own fell back to the true title. 11. **"Add to a sheet ▾"**. 12. **A new character sheet's starting name**, taken from its page. 13. **A level-up's milestone**, "X reached level N". 14. **A rest's refusal**, "X has no hit dice". 15. **The proposal-merge prompt**: the model was given the true title, and its output goes into table-visible text with no GM review. 16. **The sort order**: the wiki list was ordered by true title, so an item's position showed where its real name falls. It is now ordered by the name the reader knows. New helpers in `wiki_audience`: `sql_table_title()` and `sql_title_for(viewer)`, checked against the Python helpers on every page in the test campaign. ## So it can't reopen quietly `tests/wiki/test_title_readers.py`, in the shape of `test_body_readers.py`. It lists every `.title` read under `app/` by module, function and expression. Each page-title read has a count and a reason; a new read, or a second read in an already-listed function, fails CI until someone classifies it. Its docstring states its blind spots (rows read by key, a title copied into another column, aliases, anything outside `app/`). ## Decisions - Words written once and read by everyone (a reveal's row, the Discord handout post, a level-up milestone, a new sheet's name) use the party's name, so a GM reads the party's name there too. This is question W18 on the Questions page. - No bot contract change: payload shapes are untouched; only the value of `title` in two events. - Follow-ups are in #993 (GM-reviewed prompts still get the true name; a proposal about an unidentified item doesn't match its page). ## Checked - Full backend suite: 6580 passed, 14 skipped. After merging main (#798): `tests/wiki` and the retention tests, 621 passed. - ruff 0.4.4 format and check clean; changelog check OK (a `security` fragment). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into fix/877-title-reads
All checks were successful
CI / Backend lint (ruff) (pull_request) Successful in 34s
CI / Docker image build (pull_request) Successful in 18s
CI / Does this change touch the recording path? (pull_request) Successful in 25s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 57s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Feedback site tests (pull_request) Successful in 44s
CI / Bot/backend version sync (pull_request) Successful in 59s
CI / Bot tests and audit (pull_request) Successful in 1m54s
CI / Frontend (shard 2/2) (pull_request) Successful in 6m44s
CI / Frontend (shard 1/2) (pull_request) Successful in 8m20s
CI / End-to-end journeys (keyboard) (pull_request) Successful in 8m40s
CI / End-to-end journeys (desktop) (pull_request) Successful in 8m57s
CI / End-to-end journeys (phone) (pull_request) Successful in 17m59s
CI / Backend migration, tests, and audit (pull_request) Successful in 18m54s
deb5236b5e
Sign in to join this conversation.
No description provided.