The old avatar-palette test only did expect(["white","black"]).toContain(
entry.text), which can never fail (text is typed "white"|"black" and always
assigned) — so the load-bearing property "all 20 colors are readable" was only
really checked for the single golden name. A generator bug producing a
low-contrast or out-of-gamut slot would survive the suite.
Export the four existing color-math helpers (oklchToSrgb, isInGamut,
relativeLuminance, contrastRatio — no logic change) and assert, for EVERY
PALETTE entry:
- (a) real contrast of the chosen text on the entry hex >= 3 (the code's
threshold), scale-matched (hex 0..255 → /255 before relativeLuminance). Since
buildPalette PREFERS white and only falls back to black when white fails 3:1,
the test also asserts: if text=="black" then white's contrast is < 3 (black was
mandatory) — matching the code's actual decision, not a max-contrast pick.
- (b) the OKLCH is in sRGB gamut post-clamp: isInGamut(oklchToSrgb(L,C,h)).
Demonstrated non-vacuous: a light bg mislabeled text:"white" → chosen contrast
1.67 (< 3) fails; an out-of-gamut component fails isInGamut. Golden-name and
minPairwiseDistance tests untouched.
vitest: 15 passed. No palette/hash/consumer logic changed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the inline hand-transcribed palette with the self-contained
src/lib/avatar-palette.ts: the 20-color palette is GENERATED at module load
from an OKLCH ring config (chroma clamped to sRGB, WCAG text color per color),
so it is fully tunable and validated (min pairwise ΔE-OK ≈ 0.066).
avatarStyle() slices one cyrb53 hash of the normalized name into independent
channels: base color (20) × color-wheel scheme (analogous ±20–45° / complement
180° / triadic ±120°) × split angle (24 dirs). avatarBackgroundCss() renders a
two-stop gradient with a soft boundary. Pure, cross-platform, deterministic —
same name → same avatar everywhere, nothing persisted.
The glyph now consumes avatarStyle/avatarBackgroundCss from the module;
agent-avatar-stack no longer defines its own hash/palette.
Tests: avatar-palette.test.ts pins minPairwiseDistance ≥ 0.06, PALETTE length,
normalization, and a golden name→style slice (Backend Developer →
#a55795/#90355e/150°) so a config change that repaints every avatar can't slip
through unnoticed. client tsc clean, 30 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the ad-hoc 14-color hsl palette with a perceptually-even, validated
scheme so agent glyphs are reliably distinguishable:
- cyrb53 deterministic, cross-platform 53-bit hash over a normalized name
(NFC + trim + lowercase + collapse whitespace) — no built-in/rand hash, so
the same name renders the same avatar on every device without persistence.
- 20-color OKLCH palette (12 light / 8 dark), chroma clamped to sRGB, min
pairwise ΔEOK ≈ 0.066: any two entries are identical or clearly distinct —
"almost the same" colors are impossible by construction.
- Disjoint hash-bit channels: base color (20) × gradient partner (2) ×
gradient angle (8) = 320 combinations, so a base-color collision (inevitable
past ~20 agents) is still disambiguated by the gradient — and by the emoji
drawn on top. Text color (black on light ring, white on dark) is
WCAG-checked.
Glyph now renders an explicit solid backgroundColor (fallback + testable) plus
a linear-gradient backgroundImage. avatarStyle() replaces agentGlyphBackground().
client tsc clean, 26 tests pass (avatarStyle determinism/normalization/structure
+ DOM base-color).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>