Skip to main content
← Back to list
01Issue
BugOpenSwamp Club
AssigneesNone

Relationships

#978 Leaderboard search excludes ghosts — a visible ghost can't be found by its alias

Opened by keeb · 7/5/2026

Description

Ghost operatives appear on the ranked leaderboards (all-time, windowed, streak) with a stable, swamp-flavored alias like 👻 murk-wisp-9dc54f2a and each has a clickable /ghost/<slug> profile. But the leaderboard search box deliberately excludes ghosts, so a ghost you can plainly see on the board returns zero results when you type its alias into search. The rankings and search disagree about who exists on the board.

Root cause

ClickHouseLeaderboardQuery.board() in lib/infrastructure/clickhouse/clickhouse-score-reads.ts:913-921 appends AND is_ghost = 0 to the search filter, and matches owner ILIKE {q} against the raw owner column (the distinct_id), never against the rendered alias:

// Ghosts are excluded from SEARCH (not from ranked boards): the match
// would run against the raw distinct_id — which never renders — so a
// hit both confuses (alias doesn't contain the query) and lets a
// caller probe raw distinct_id contents by substring enumeration.
ownerFilter = `AND owner ILIKE {q:String} AND is_ghost = 0`;

The exclusion is intentional and the reasoning is sound as written: matching the query against the raw distinct_id would (a) never match the visible alias and (b) let a caller enumerate raw distinct_id substrings. But the conclusion — drop ghosts from search entirely — leaves them unsearchable rather than searchable by their alias.

findScoreTarget() (the /locate jump-to-me path, clickhouse-score-reads.ts:693-740) carries the same exclusion, so locate can't jump to a ghost either.

The alias is ghostSlug(distinctId) (lib/ghost-name.ts) — a deterministic FNV-1a hash, one-way by design, so a query fragment can't be reversed to a distinct_id in SQL. That's why the naive "just match owner" approach was rejected. A real fix has to match against the slug, not the distinct_id.

Steps to reproduce

  1. Open /leaderboard on prod (SCORE_READS=clickhouse, serving from the ClickHouse ledger).
  2. Note a ghost row, e.g. 👻 murk-wisp-9dc54f2a (visible with a /ghost/... link).
  3. Type its alias fragment (murk-wisp) into the leaderboard search box.
  4. Expected: the ghost row appears, matching what's on the board.
  5. Actual: no results — search silently drops all ghosts.

Environment

  • Path: ClickHouse read flip (SCORE_READS=clickhouse), prod. The Mongo path (mongo-leaderboard-query.ts) has no ghost concept (username: {$exists: true} filter), so this is CH-specific.
  • Files: lib/infrastructure/clickhouse/clickhouse-score-reads.ts (board() ~L913-921, findScoreTarget() ~L693-740); alias grammar in lib/ghost-name.ts.

Fix direction (not prescriptive)

Match search against the ghost slug rather than excluding ghosts: e.g. resolve the candidate ghost owner set app-side, compute ghostSlug() for each, and match the query against the slug — never exposing or probing the raw distinct_id. This keeps the anti-enumeration property (query never touches distinct_id contents) while making a visible ghost findable by the exact text shown on its row.

02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED

Open

7/5/2026, 5:31:33 PM

No activity in this phase yet.

03Sludge Pulse

Sign in to post a ripple.