A character can't be given a name another character is "heard as" (#634) #1034

Merged
claude-bot merged 3 commits from fix/634-name-vs-alias-collision into main 2026-10-02 23:11:01 +00:00
Collaborator

Closes #634.

What was wrong

The collision check ran one way only. A new misspelling ("heard as") was refused when it was another character's name, but a new name was never checked against other characters' misspellings. So: list "Wyatt" as a misspelling of Idani, then create a character called Wyatt, and both were accepted.

The issue expected that state to rewrite every "Wyatt" in a transcript to "Idani". It does not: vocabulary_service.build_alias_map already drops a misspelling that is somebody's name (covered by test_name_snapping.py). What actually happened is quieter: Idani's "Wyatt" misspelling stopped being corrected, and nothing told the GM why.

What changed

_reject_name_alias_collision in campaign_character_service.py, called from create_character and from update_character when the name actually changes. Every path that names a character goes through those two:

Path Refusal
POST …/members/{user}/characters 400
PATCH …/characters/{id} 400
PATCH /members/{user} with character_name (the older route) 422
Start a character, finishing the sheet 422
Assigning an imported member 4xx (the route's existing mapping)

Wording: "Wyatt is already listed as a misspelling of Idani's name in this campaign, so it cannot be another character's name."

Same comparison as the other direction (whitespace collapsed, case folded). A character's own misspellings don't count; another campaign's are never read. Import itself does what it does for the existing direction: nothing at import, the check runs when the member is assigned. The bot API creates no characters, so the contract version is untouched. No migration.

The issue's third question: campaigns already in that state

Nothing is done to them, and nothing needs to be: the misspelling is inert, no transcript was rewritten, and their characters can still be edited (the check runs only when a name changes). That is written on the function.

One thing to know

A player finishing Start a character with such a name gets the refusal and cannot fix the misspelling themself (the GM owns those). The message names whose misspelling it is, so they can pick another name or ask. It needs a GM to have listed a real name as a misspelling first, so it should be rare.

Checks

  • Full backend suite: 6820 passed, 14 skipped. Ruff check and format: clean.
  • Eight new tests (seven in tests/test_name_snapping.py, one in tests/sheets/test_sheets_creation.py): create, rename, case and whitespace, own misspelling allowed, another campaign allowed, the older member route, assigning an imported member, finishing a sheet. The existing direction's tests still pass.
  • No frontend change: not checked in a browser. Not verified: that each form shows the server's message.

🤖 Generated with Claude Code

Closes #634. ## What was wrong The collision check ran one way only. A new misspelling ("heard as") was refused when it was another character's name, but a new **name** was never checked against other characters' misspellings. So: list "Wyatt" as a misspelling of Idani, then create a character called Wyatt, and both were accepted. The issue expected that state to rewrite every "Wyatt" in a transcript to "Idani". It does not: `vocabulary_service.build_alias_map` already drops a misspelling that is somebody's name (covered by `test_name_snapping.py`). What actually happened is quieter: Idani's "Wyatt" misspelling stopped being corrected, and nothing told the GM why. ## What changed `_reject_name_alias_collision` in `campaign_character_service.py`, called from `create_character` and from `update_character` when the name actually changes. Every path that names a character goes through those two: | Path | Refusal | |---|---| | `POST …/members/{user}/characters` | 400 | | `PATCH …/characters/{id}` | 400 | | `PATCH /members/{user}` with `character_name` (the older route) | 422 | | Start a character, finishing the sheet | 422 | | Assigning an imported member | 4xx (the route's existing mapping) | Wording: "Wyatt is already listed as a misspelling of Idani's name in this campaign, so it cannot be another character's name." Same comparison as the other direction (whitespace collapsed, case folded). A character's own misspellings don't count; another campaign's are never read. Import itself does what it does for the existing direction: nothing at import, the check runs when the member is assigned. The bot API creates no characters, so the contract version is untouched. No migration. ## The issue's third question: campaigns already in that state Nothing is done to them, and nothing needs to be: the misspelling is inert, no transcript was rewritten, and their characters can still be edited (the check runs only when a name changes). That is written on the function. ## One thing to know A **player** finishing Start a character with such a name gets the refusal and cannot fix the misspelling themself (the GM owns those). The message names whose misspelling it is, so they can pick another name or ask. It needs a GM to have listed a real name as a misspelling first, so it should be rare. ## Checks - Full backend suite: 6820 passed, 14 skipped. Ruff check and format: clean. - Eight new tests (seven in `tests/test_name_snapping.py`, one in `tests/sheets/test_sheets_creation.py`): create, rename, case and whitespace, own misspelling allowed, another campaign allowed, the older member route, assigning an imported member, finishing a sheet. The existing direction's tests still pass. - No frontend change: not checked in a browser. Not verified: that each form shows the server's message. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
_reject_alias_collisions checked one direction: a new heard-as alias against
other characters' names. This adds the reverse: a character's NAME (on create
and on a real rename) is refused when another character in the same campaign
lists it as a heard-as alias, using the same whitespace-collapsed, casefolded
comparison. The character's own aliases and other campaigns' aliases do not
count. A rename is checked only when the name actually changes, so a campaign
already in the state the issue describes can still edit other fields.

Every write path funnels through create_character / update_character, so the
legacy member PATCH, sheet finish and import-stub claim are covered too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Say what the collision really did: the alias went quiet, nothing was rewritten (#634)
Some checks failed
CI / Does this change touch the recording path? (pull_request) Successful in 26s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot/backend version sync (pull_request) Successful in 32s
CI / Feedback site tests (pull_request) Successful in 48s
CI / Backend lint (ruff) (pull_request) Successful in 1m21s
CI / Docker image build (pull_request) Successful in 2m18s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 2m51s
CI / Bot tests and audit (pull_request) Successful in 4m35s
CI / End-to-end journeys (phone) (pull_request) Successful in 13m52s
CI / Frontend (shard 1/2) (pull_request) Successful in 21m2s
CI / Backend migration, tests, and audit (pull_request) Successful in 48m26s
CI / Frontend (shard 2/2) (pull_request) Failing after 9m26s
CI / End-to-end journeys (keyboard) (pull_request) Successful in 16m41s
CI / End-to-end journeys (desktop) (pull_request) Successful in 17m32s
4b53faecbc
build_alias_map already drops an alias that is somebody's name, so a campaign
holding the collision never had a transcript rewritten; the misspelling just
stopped being corrected, silently. The docstring and the changelog said
otherwise.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Collaborator

CI on 4b53faec, run https://git.rhoving.com/rbrooks/Quest-Board/actions/runs/1797 : three red jobs, none from this change (backend only).

  • CI / Frontend (shard 2/2): src/pages/discussions/Thread.test.jsx "keeps them marked after the thread is read again, which says nothing is new" could not find "Arrived while reading." A race in the page's refresh-on-focus hook, shown up by a busy runner; cause and fix in #1036.
  • CI / End-to-end journeys (desktop) and (keyboard): the runner killed the browser mid-run at 04:59:21 UTC ("worker process exited unexpectedly (code=null, signal=SIGKILL)", "page.goto: Page crashed"), the same minute run 1799's journey jobs failed with "no space left on device".

Not re-pushed: once the runner is healthy, those three jobs want a re-run. The shard can flake again until #1036 is on main.

CI on 4b53faec, run https://git.rhoving.com/rbrooks/Quest-Board/actions/runs/1797 : three red jobs, none from this change (backend only). - **CI / Frontend (shard 2/2)**: `src/pages/discussions/Thread.test.jsx` "keeps them marked after the thread is read again, which says nothing is new" could not find "Arrived while reading." A race in the page's refresh-on-focus hook, shown up by a busy runner; cause and fix in #1036. - **CI / End-to-end journeys (desktop)** and **(keyboard)**: the runner killed the browser mid-run at 04:59:21 UTC ("worker process exited unexpectedly (code=null, signal=SIGKILL)", "page.goto: Page crashed"), the same minute run 1799's journey jobs failed with "no space left on device". Not re-pushed: once the runner is healthy, those three jobs want a re-run. The shard can flake again until #1036 is on main.
Merge remote-tracking branch 'origin/main' into fix/634-name-vs-alias-collision
All checks were successful
CI / Docker image build (pull_request) Successful in 38s
CI / Feedback site tests (pull_request) Successful in 52s
CI / Does this change touch the recording path? (pull_request) Successful in 40s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m21s
CI / Bot/backend version sync (pull_request) Successful in 1m9s
CI / Backend lint (ruff) (pull_request) Successful in 1m24s
CI / Bot tests and audit (pull_request) Successful in 2m26s
CI / Frontend (shard 2/2) (pull_request) Successful in 9m28s
CI / Frontend (shard 1/2) (pull_request) Successful in 11m30s
CI / End-to-end journeys (desktop) (pull_request) Successful in 10m23s
CI / End-to-end journeys (phone) (pull_request) Successful in 10m16s
CI / End-to-end journeys (keyboard) (pull_request) Successful in 21m19s
CI / Backend migration, tests, and audit (pull_request) Successful in 24m1s
3fb9617d86
claude-bot deleted branch fix/634-name-vs-alias-collision 2026-10-02 23:11:03 +00:00
Sign in to join this conversation.
No description provided.