soulsync/webui
Broque Thomas 0aa18b0180 Cross-script artist aliases: include canonical name + non-strict fallback
Closes #586. Follow-up to #442 — Cyrillic / kanji canonical names
weren't bridging cross-script comparisons. Reporter case: "Dmitry
Yablonsky" tracks quarantined as audio mismatch with file identified
as "Русская филармония, Дмитрий Яблонский" (4% artist sim) even
though the Cyrillic spelling is just the Russian transliteration.

Codex diagnosed three layered bugs in the alias resolution chain.
This fixes all three.

Bug 1 — fetch_artist_aliases ignores canonical name + sort-name

`core/musicbrainz_service.py:fetch_artist_aliases` only read
`data['aliases']`. For artists where MB's canonical `name` IS the
cross-script form (and the Latin spelling lives only in aliases —
or vice versa), the missing direction never made it into the
returned list. Fix: include both `data['name']` and `data['sort-name']`
alongside the explicit alias entries (deduped, also pulls each
alias entry's sort-name when present).

Bug 2 — lookup_artist_aliases ran search in strict mode only

Strict mode queries `artist:"..."` only and skips MB's alias and
sortname indexes. Cross-script searches found nothing under strict
because the user's Latin input never matches a Cyrillic canonical
name in the artist index. Fix: lifted the search-and-score logic
to a private helper `_search_and_score_artists(name, strict=)` and
fall back to non-strict when strict returns empty OR all results
fail the trust gate. Non-strict (bare query) hits all indexes.

Bug 3 — trust gate weighted local similarity 70%

Combined score = local_sim * 0.7 + mb_score/100 * 0.3. Cross-script
pairs have local sim ~0 → combined ~0.30 → below the 0.85 threshold
→ cached as empty even when MB's own confidence was 100. Fix: added
an MB-only escape — when MB score is >= 95 AND the result is
unambiguous (top result's MB score leads the runner-up by >= 5),
accept regardless of local similarity. The existing combined-score
path stays intact for same-script matches (#442 Hiroyuki Sawano
case still passes via that path).

12 new tests pin every layer:
- fetch_artist_aliases canonical-name inclusion + dedup against
  alias entries + missing-canonical handling + exception path
- strict-then-non-strict fallback (empty-strict + low-strict-score)
- trust gate MB-only escape + low-confidence rejection + ambiguity
  rejection (two artists same MB score) + same-script regression
- end-to-end reporter scenario with the real `artist_names_match`
  helper proving the bridge works for "Русская филармония, Дмитрий
  Яблонский" vs expected "Dmitry Yablonsky"

Existing alias tests in `test_artist_alias_service.py` updated to
reflect: canonical name now appears in `fetch_artist_aliases`
output, lookup makes 2 search calls (strict + non-strict fallback)
on first cache miss instead of 1.

Full suite: 3065 passed.
2026-05-14 13:07:15 -07:00
..
src Normalize issues schema naming 2026-05-13 22:26:26 +03:00
static Cross-script artist aliases: include canonical name + non-strict fallback 2026-05-14 13:07:15 -07:00
tests Keep Issues and artist detail history stable 2026-05-13 22:26:24 +03:00
.gitignore Initial Vite app scaffolding & issues page impl 2026-05-13 22:24:46 +03:00
.oxfmtrc.json Split webui tooling into separate configs 2026-05-13 22:26:25 +03:00
.oxlintrc.json Split webui tooling into separate configs 2026-05-13 22:26:25 +03:00
index.html Remove Write Artist Image button from artist detail page 2026-05-14 12:36:03 -07:00
package-lock.json Unify issues validation and metadata 2026-05-13 22:26:26 +03:00
package.json Unify issues validation and metadata 2026-05-13 22:26:26 +03:00
playwright.config.ts Initial Vite app scaffolding & issues page impl 2026-05-13 22:24:46 +03:00
README.md Move shared shell chrome into bridge 2026-05-13 22:26:26 +03:00
tsconfig.json Split webui tooling into separate configs 2026-05-13 22:26:25 +03:00
vite.config.ts Split webui tooling into separate configs 2026-05-13 22:26:25 +03:00
vitest.config.ts Split webui tooling into separate configs 2026-05-13 22:26:25 +03:00
vitest.setup.ts Add MSW-backed issue API tests 2026-05-13 22:26:24 +03:00

WebUI Hybrid Rendering

SoulSync's web UI is in a transition phase:

  • most pages still render through the legacy vanilla JS shell
  • /issues is rendered by the new React app
  • a small shell bridge keeps both runtimes aware of the active page, profile context, and navigation state

How It Fits Together

flowchart LR
    Browser["Browser parses /webui/index.html"]
    Legacy["Legacy shell scripts\n(core.js -> ... -> init.js)"]
    Bridge["shell-bridge.js\nwindow.SoulSyncWebShellBridge"]
    React["Vite React app\nsrc/app/main.tsx"]
    Router["TanStack Router\nwindow.SoulSyncWebRouter"]

    Browser --> Legacy
    Browser --> React
    Legacy --> Bridge
    React --> Router
    Router --> Bridge
    Bridge --> Legacy

Runtime Roles

  • webui/static/init.js

    • boots the legacy shell
    • selects the active profile
    • handles the legacy page loading flow
  • webui/static/shell-bridge.js

    • owns the browser-side bridge object
    • exposes window.SoulSyncWebShellBridge
    • owns the shared page chrome and route handoff helpers
  • webui/src/app/main.tsx

    • mounts the React app
    • binds window.SoulSyncWebRouter
  • webui/src/platform/shell/route-controllers.tsx

    • listens for bridge readiness
    • keeps React pages aligned with the shell

Load Order

The current order in index.html matters:

  1. legacy shell scripts load first
  2. init.js sets up the shell runtime
  3. shell-bridge.js publishes the bridge and shared chrome helpers after the shell state exists
  4. the Vite React app is injected through {{ vite_assets('body') }} and boots as a module after parsing

That order avoids load-time references to missing globals and keeps the React side able to react to bridge readiness events. The React entry can start fetching early, but the shell bridge and legacy globals are already available by the time the React runtime starts acting on them.

Notes

  • The bridge is intentionally small and browser-only.
  • This is the start of the migration, not a full replacement of the legacy shell.
  • When adding another React page, check whether it needs:
    • a route entry in webui/src/platform/shell/route-manifest.ts
    • bridge typings in webui/src/platform/shell/globals.d.ts
    • a legacy fallback path in webui/static/init.js
    • bridge glue or handoff logic in webui/static/shell-bridge.js

Folder Layout

The React webui uses a small set of predictable folders so route slices stay easy to extend, test, and understand.

webui/src/
  app/         React bootstrap, router, query client, shared API client
  components/  Shared UI primitives
  platform/    Shell bridge and browser/platform integration
  routes/      Route-local code and TanStack Router pages
  test/        Shared test utilities and setup helpers

Route Slices

  • Keep route-specific code inside webui/src/routes/<route>/.
  • Put the routing entry in route.tsx.
  • Put route-local UI in a -ui/ folder.
  • Prefix non-routing files with - so TanStack Router ignores them.
  • Keep the route slice small and cohesive.
  • Prefer a few files with clear responsibilities over many tiny files with overlapping names.

Example:

webui/src/routes/issues/
  route.tsx
  -issues.types.ts
  -issues.api.ts
  -issues.helpers.ts
  -issues.api.test.ts
  -issues.helpers.test.ts
  -ui/
    issues-page.tsx
    issue-detail-modal.tsx
    issue-domain-host.tsx

The initial issues slice is the model to follow:

  • -issues.api.ts holds request code and query options
  • -issues.helpers.ts holds pure normalization and formatting
  • -issues.types.ts holds shared types
  • -ui/ holds the page, modal, and legacy handoff UI

Shared Code

  • Put reusable UI in webui/src/components/.
  • Put shell integration in webui/src/platform/.
  • Put bootstrap and app-wide wiring in webui/src/app/.
  • Move code up a level only when it is genuinely shared.
  • Avoid creating new conventions that overlap with existing ones.

Testing Choices

We have a lot of testing tools available, but we do not need all of them for every feature.

  • Use plain unit tests for pure functions and small transforms.
  • Use React component or route tests when the behavior lives in the UI or router.
  • Use MSW-backed tests when request shape, response handling, or error handling matters.
  • Use Playwright when the behavior is best proven end-to-end with the server and browser together.
  • Prefer the smallest test setup that still proves the thing that can regress.

Development

The repo root now owns the full local-dev instructions. Start there for the portable launcher and backend/frontend setup:

  1. README.md for the end-to-end dev flow
  2. npm run check and npm run fix for React-side linting and formatting