fix(auth): assign first UNUSED palette color (AUTH-03 distinct colors)
Gate 2 A3 fail: a second member (amelia) got the same color as the first (luc), both #E8734A. Color was assigned by COUNT(*) % palette; a deleted spike user shifted the count so two live members landed on the same slot. Replace with 'first palette color not already in use by another user' (fall back to count round-robin only once the palette is exhausted) — guarantees distinct, stable colors for up to palette length members. +1 regression test (deletion frees a slot → next member fills it, no collision).
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
* Source: RESEARCH.md § "User upsert with color assignment"
|
||||
*/
|
||||
|
||||
import { and, eq, sql } from 'drizzle-orm'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { db } from '../db/client.js'
|
||||
import { users } from '../db/schema.js'
|
||||
|
||||
@@ -101,13 +101,18 @@ export async function upsertUser(
|
||||
return existing[0]
|
||||
}
|
||||
|
||||
// 2. Count existing users to determine round-robin color slot
|
||||
const countResult = await db
|
||||
.select({ count: sql<number>`COUNT(*)` })
|
||||
.from(users)
|
||||
|
||||
const count = Number(countResult[0]?.count ?? 0)
|
||||
const color = COLOR_PALETTE[count % COLOR_PALETTE.length]
|
||||
// 2. Assign the first palette color NOT already in use by another member.
|
||||
// A plain COUNT(*) % palette collides under deletions: a deleted user
|
||||
// shifts the count so the next insert reuses an in-use slot (observed in
|
||||
// Gate 2 — two members both got #E8734A). Selecting the first unused color
|
||||
// guarantees distinct, stable colors for up to COLOR_PALETTE.length members
|
||||
// (AUTH-03). Falls back to round-robin by count only once the palette is
|
||||
// exhausted (more members than colors).
|
||||
const usedRows = await db.select({ color: users.color }).from(users)
|
||||
const usedColors = new Set(usedRows.map((r) => r.color))
|
||||
const color =
|
||||
COLOR_PALETTE.find((c) => !usedColors.has(c)) ??
|
||||
COLOR_PALETTE[usedColors.size % COLOR_PALETTE.length]
|
||||
|
||||
// 3. Insert new user row
|
||||
// mysql2 has no RETURNING clause — use $returningId() then re-select
|
||||
|
||||
Reference in New Issue
Block a user