Skip to content

fix(singers): merging singers unifies their identity so self-rename doesn't re-split (v0.98.0) - #209

Merged
beveradb merged 4 commits into
mainfrom
feat/sess-20260828-0134-merge-singer-identity
Aug 28, 2026
Merged

fix(singers): merging singers unifies their identity so self-rename doesn't re-split (v0.98.0)#209
beveradb merged 4 commits into
mainfrom
feat/sess-20260828-0134-merge-singer-identity

Conversation

@beveradb

Copy link
Copy Markdown
Contributor

Problem

A singer submitted karaoke songs from two browser sessions (two stable device_ids) under two typed name variants — "Jasmine" and "Jasmine!". The KJ merged them into one displayed singer on NomadPC. The merge correctly unified the rotation entries, but not the identity: when she then renamed herself on her phone, /sing/rename only rewrote the songs that one device owned (per-request edit_token), so the other session's song stayed under the old name — she re-split into two singers, only one renamed.

A KJ merge is a deliberate assertion that these are one person, so a later rename must carry the whole group.

Fix

Aliases now carry provenance. singer_aliases gains an origin column — 'kj' (a KJ rename/merge established the identity) vs 'self' (a singer's own /sing/rename). Only 'kj' marks a canonical identity. A self-rename always stamps 'self', so a singer can never launder a past merge into whole-group power over a coincidental same-name walk-in.

Merge marks a canonical identity. POST /rotation/singer/merge already aliased the source devices onto the target (persist_rename); it now also calls SingStore.mark_identity(target), tagging the keep-side devices 'kj'.

Self-rename escalates for an established identity. /sing/rename decides per old-name: if is_canonical_identity(old) and a night marker is available to scope the request rewrite, it carries the whole rotation name-group (rename_singer), migrates every device aliased to the old name (remap_aliases), and rewrites tonight's requests (persist_rename). Otherwise it fails closed to the edit_token-owned scope exactly as before — two coincidental same-name walk-ins can never rename each other, and a missing night marker never clobbers prior nights.

Migration safety

Additive singer_aliases.origin column, defaults 'self' on boot. Pre-upgrade rows can't be told apart, so they default to 'self' (the safe choice — they behave exactly as before). A merge done after the upgrade writes fresh 'kj' aliases, so the fix applies going forward.

Tests

  • 8 new unit (test_sing_store.py): the three helpers + 'kj'-only identity + self-reset-of-authority, same-name/blank no-ops, device-less skips.
  • 4 new integration (test_sing_rename.py::TestMergedIdentitySelfRename): merge→self-rename renames the whole group; migrates all device aliases; the no-merge control stays scoped; a double-self-rename-through-a-shared-name that must not hijack the other singer.
  • Full unit + integration suite green. (Pre-existing chromium-e2e / audio-hardware failures are environmental, unrelated.)

Deploy

Backend (sing.py, sing_store.py, routes.py) → requires systemctl restart kj-controller (deploy between songs). NomadPC may be mid-show — auto-deploy is paused during live shows, so this lands when the show ends.

Reviewed locally with CodeRabbit (3 cycles, 2 majors + 1 minor addressed).

@coderabbitai ignore

beveradb and others added 4 commits August 28, 2026 01:54
…(v0.98.0)

A singer who submitted from two browser sessions (two device_ids) under two
typed variants ("Jasmine" / "Jasmine!") was merged by the KJ into one displayed
singer — but the merge unified only the rotation ENTRIES, not the IDENTITY. When
she then renamed herself on her phone, /sing/rename rewrote only the songs that
one device owned (per-request edit_token), so the other session's song stayed
under the old name: she re-split, only one renamed.

A KJ merge is a deliberate assertion that these are one person, so a later
rename must carry the whole group:

- Merge now marks a canonical identity: /rotation/singer/merge calls
  SingStore.mark_identity(target) in addition to persist_rename(source, target),
  so the KEEP-side devices are recognised as the same identity too.
- Self-rename escalates for an established identity: /sing/rename renames the
  WHOLE rotation name-group (rename_singer), migrates every aliased device
  (remap_aliases) and rewrites tonight's requests (persist_rename) when
  is_canonical_identity(old) — otherwise stays scoped to edit_token-owned
  entries so two coincidental same-name walk-ins never rename each other.

New SingStore helpers: is_canonical_identity, remap_aliases, mark_identity.
Tests: 6 new unit + 3 new integration (TestMergedIdentitySelfRename).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ht scope

Two review findings on the identity-aware self-rename:

- Only KJ-established identities unlock a whole-group rename. singer_aliases
  gains an `origin` column ('kj' vs 'self'); is_canonical_identity now requires
  a 'kj' alias. A singer self-renaming their own song records a 'self' alias
  that can NOT sweep up a coincidental same-name walk-in on a later rename.
  'kj' is sticky (a self-rename never downgrades a managed identity). Pre-upgrade
  rows default to 'self' (safe — behave exactly as before). Regression test:
  double self-rename through a shared name leaves the other singer untouched.

- Fail closed when the night marker is unavailable. Self-rename only escalates
  to persist_rename (which scopes the request rewrite by night) when a night
  marker exists; otherwise it falls back to the edit_token-owned scope rather
  than risk rewriting prior nights' history.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… fail-closed night in store

- set_alias no longer keeps 'kj' sticky: a self-rename stamps 'self', so KJ
  authority never travels onto a name the singer later chose (can't launder a
  past merge into whole-group power over a coincidental walk-in). A genuinely
  KJ-merged multi-device identity stays canonical via its sibling devices.
- mark_identity + persist_rename resolve the night marker internally and return
  0 when none is available, rather than aliasing/rewriting every historical row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lias

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@beveradb
beveradb merged commit e01c55a into main Aug 28, 2026
2 checks passed
@beveradb
beveradb deleted the feat/sess-20260828-0134-merge-singer-identity branch August 28, 2026 06:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant