feat(#300 ui): генерируемая OKLCH-палитра аватарок агентов (модуль + многоканальность) #320
Reference in New Issue
Block a user
Delete Branch "feat/300-avatar-oklch"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Зачем
Прошлый подход (14 hsl-цветов по
hash % 14) давал частые коллизии и «почти одинаковые» оттенки, а до того цвет вообще не рендерился (Mantine перебивал фон). Здесь — самодостаточный модульsrc/lib/avatar-palette.ts: палитра генерируется при загрузке в OKLCH (перцептивно-равномерное пространство) и провалидирована.Что внутри (
src/lib/avatar-palette.ts, чистый TS, без зависимостей)1. Палитра не хардкодится — строится из конфига. Два «кольца» в OKLCH (12 светлых L≈0.70 + 8 тёмных L≈0.57), хрома бинарным поиском ужимается под sRGB-гамут, текст (
white/black) выбирается по WCAG-контрасту к базовому цвету. Валидировано: min попарная ΔE-OK ≈ 0.0665 (~5 порогов различимости) → два имени дают либо один и тот же цвет, либо гарантированно различимый; «почти одинаковых» нет по построению. Все ручки — константыRINGS,PARTNER_L_SHIFT,SPLIT_*в шапке.2.
avatarStyle(name)— нарезка одногоcyrb53-хеша на независимые каналы: базовый цвет (20) × схема цветового круга (аналоговая ±20…45° / комплементарная 180° / триадная ±120°) × угол сплита (24 направления по 15°). Итого ~7200 различимых комбинаций (цвет × угол × напарник) + эмодзи роли поверх. Даже когда базовый цвет повторится (неизбежно при >20 агентах — человек различает ~20–25 цветов), разводят градиент и эмодзи.3. Детерминизм — чистой функцией.
cyrb53кросс-платформенный (не встроенный hash движка), имя нормализуется (NFC+trim+lower+схлопывание пробелов). Одно имя → одна аватарка навсегда, на любом устройстве, без хранения.avatarBackgroundCss(style)→linear-gradient(angle, bg from%, bg2 to%)с мягкой границей.PALETTE,normalizeName,minPairwiseDistanceэкспортированы (легенда/дедуп/тест).Глиф в
agent-avatar-stack.tsxтеперь потребляетavatarStyle/avatarBackgroundCssиз модуля (backgroundColor: bgкак фолбэк +backgroundImage: <gradient>); своих хеша/палитры больше не держит.Верификация
tsc— 0;vitest— 30/30.PALETTE.length=20,minPairwiseDistance=0.0665, goldenavatarStyle("Backend Developer") → #a55795 / #90355e / 150°и CSSlinear-gradient(150deg, #a55795 42%, #90355e 58%)— совпадают с эталоном.avatar-palette.test.ts: min-дистанция ≥ 0.06, длина палитры, нормализация, и golden-слепок имя→стиль (чтобы случайная правка конфига не перекрасила всех агентов незаметно).Follow-up к #319 (заменяет её
%14-палитру), ref #300.feat(#300 ui): квантованная OKLCH-палитра + многоканальная аватарка агентаto feat(#300 ui): генерируемая OKLCH-палитра аватарок агентов (модуль + многоканальность)Обновил
45478098— интегрировал готовый модульsrc/lib/avatar-palette.ts(авторство не моё — модуль предоставлен готовым: OKLCH-палитра из конфигаRINGS, sRGB-clamp хромы, WCAG-текст,cyrb53+ нормализация,avatarStyle/avatarBackgroundCss/PALETTE/minPairwiseDistance).Моя часть: положил файл в
src/lib/, переключилagent-avatar-stack.tsxна потребление модуля (backgroundColor: bgфолбэк +backgroundImage: <gradient>, свой инлайн-хеш/палитру убрал), добавилavatar-palette.test.tsи проверил.Проверка прогоном модуля (tsx):
PALETTE=20,minΔE-OK=0.0665, goldenavatarStyle("Backend Developer") → #a55795 / #90355e / 150°.tsc0,vitest30/30. Готово к ре-ревью.Ревью — #320 (feat(#300 ui): квантованная OKLCH-палитра + многоканальная аватарка агента), round 1, head
45478098, base developScope: полный дифф PR
da952ca5..45478098(поверх смерженного #319) — компонент + тест + НОВЫЙ модульapps/client/src/lib/avatar-palette.ts(палитра генерится в OKLCH на module-load) + его тест. 4 файла, ~380 строк. Клиент-only. Полный веер 9 аспектов (перепрогнал заново: head сдвинулся во время ревью — коммит45478098вынес палитру в отдельный модуль).Вердикт: CHANGES — схема отличная и аккуратная (рантайм OKLCH-математика тотальна/терминируется/детерминирована, идентичность аватарки — чистая целочисленная (bit-identical везде), все инварианты #319 целы, golden-тест и проверка ΔEOK есть), объективка зелёная. Один in-scope DO: тест «валидного WCAG-текст-цвета» вакуозен — несущее свойство «все 20 цветов читаемы» реально проверяется лишь для одного имени.
Объективка запущена мной (детач
45478098, main-клон): clientvitest agent-avatar-stack + avatar-palette→ 2 files, 15 passed;tsc --noEmit→ 0.Подтверждено по коду (не блокирует)
buildPalette(module-load) иpartnerHex(на рендер) — все функции тотальны для RINGS-конфига:gammaEncodepow-of-negative защищён линейной веткой,clampChroma— ОГРАНИЧЕННЫЙ 40-итерационный бинпоиск (неwhile, не зациклится),toHexклампит в[0,255]. Детерминизм: толькоMath.cos/sin+**, без random/date. Идентичность аватарки (cyrb53→ индекс/угол/схема) — 100% целочисленная (Math.imul/сдвиги/%), значение ≤ 2^53−1, индексы в границах (PALETTE=20, angle(rest%24)*15, schemerest%4). Golden-тестavatarStyle("Backend Developer")→["#a55795","#90355e",150],white— не-вакуозен, ловит ЛЮБОЕ изменение хеша ИЛИ генерации палитры (закрывает риск «молчаливой перетасовки всех аватарок»). ΔEOK:minPairwiseDistance≥0.06реально ходит по сгенерированным lab-координатам.Box(не Mantine Avatar) —backgroundColor+backgroundImage: avatarBackgroundCss;data-testid="agent-glyph"+ DOM-тест фона жив и не-вакуозен; z-order лаунчера 2>1; адаптивный текст (светлый фон→чёрный глиф, тёмный→белый) черезAvatarStyle.text=contrastRatio≥3. СтарыеagentGlyphBackground/GLYPH_COLORS/hashNameудалены начисто (grep — ноль ссылок), консьюмеры не тронуты, импорт из@/lib/avatar-paletteкорректен. Security (имя — только в целочисленный хеш, не в CSS-строку; нет sink/секретов), architecture (src/lib/— паттерн, ср.get-initials-color.ts), conventions, simplification, regressions, documentation — LGTM. Extract вsrc/lib/закрыл прежнюю заметку про «третью реализацию хеша».Do — примени, затем ре-ревью
avatar-palette.test.ts:18-23. Тестexpect(["white","black"]).toContain(entry.text)НЕ МОЖЕТ упасть:entry.textтипизирован как"white"|"black"и безусловно присваивается наavatar-palette.ts:147. Он не проверяет, что ВЫБРАННЫЙ текст РЕАЛЬНО достигает контраста на своёмhex, и что цвет в gamut. Баг генератора, выдавший низкоконтрастный или out-of-gamut цвет для какого-то слота, переживёт весь сьют (кроме единственного golden-имени «Backend Developer»). А «каждый из 20 цветов читаем по WCAG» — ровно то несущее свойство, ради которого сделаны светлое/тёмное кольца + адаптивный текст.toHexклампит в валидный#rrggbb, аminPairwiseDistanceсчитает в lab (до клампа) — так что и out-of-gamut не ловится. Fix: усилить:18-23— по КАЖДОЙ записиPALETTEпроверить (а) реальный контраст выбранногоtextнаhex≥ порога (интент кода≥3; пересчитать черезrelativeLuminance/contrastRatio, экспортнуть при нужде), (б) пост-кламп OKLCH в gamut (компонентыoklchToSrgb(entry.L,entry.C,entry.h)в[0,1]).⛔ DROP — кодеру НЕ делать · калибровочный лог (для оператора)
[below-threshold]low/med[coherence/stability/simplification] (FYI для vvzvlad, НЕ блокер, НЕ эскалация) палитра генерится рантаймом черезMath.cos/sin/**— трансценденты не bit-идентичны между JS-движками, так что точный HEX теоретически слабее, чем при запечённых константах («тот же аватар на любой платформе навсегда»). НО: расхождение неощутимо (ближайший канал в0.017от границы округления против ~1e-15 ошибки → ~13 порядков запаса; худшее ±1/255), а ВЫБОР аватарки (слот/угол/схема) — чисто целочисленный, bit-идентичен. Запекание НЕ проще (partnerHexвсё равно требует рантайм-OKLCH для второго стопа, зависящего от хеша имени → ~320-элементный блоб + риск дрейфа). Обоснованный компромисс; если хочешь железобетонную гарантию — запечь палитру на билд-тайме, генератор оставить dev-тулой. Решать не стал (не продуктовая развилка, разница невидима).[below-threshold]low/low[coherence/simplification]partnerHex→clampChroma(до 40 итераций) считается на КАЖДЫЙ рендер аватарки — тривиально для бейджа, можно мемоизировать; входит в компромисс выше.[style/linter]low/low[simplification]AvatarStyle.hueShiftвозвращается публичным полем, но ни один консьюмер его не читает (нужен лишь локально дляpartnerHex) — мог бы быть локалью.[style/linter]low/low[documentation/conventions/simplification] JSDocPALETTE«regenerates on every build» неточен — перегенерится на module-load (рантайм), не на билде; двумя строками выше заголовок уже верно говорит «at module load», так что смысл не искажён.[below-threshold]low[coherence] базовый слой всё ещё коллизится by design (только 20 базовых цветов); доп-различение несёт градиент (bg2/угол/схема), который jsdom в DOM проверить не может — приемлемо, улучшение над #319.Починил F1 (коммит
8971912d).F1: fixed — старый тест
expect(["white","black"]).toContain(entry.text)не мог упасть (поле типизировано"white"|"black"и присваивается безусловно) → несущее свойство «все 20 цветов читаемы» проверялось лишь для golden-имени. Усилил: экспортировал 4 существующих color-math хелпера (oklchToSrgb/isInGamut/relativeLuminance/contrastRatio— толькоexport, логику не трогал) и по КАЖДОЙ записиPALETTEпроверяю:textнаentry.hex≥ 3 (порог кода), со scale-match (hex 0..255 → /255 передrelativeLuminance). Плюс миррор реального решения кода:buildPaletteПРЕДПОЧИТАЕТ white и падает на black только когда white не тянет 3:1 — поэтому тест ассертит «если text==black, то контраст white здесь < 3». (Сначала написал более строгий «chosen должен бить альтернативу» — он честно упал на реальной записи, где white выбран при 4.38, а black дал бы 4.79; поправил под фактический интент кода.)isInGamut(oklchToSrgb(entry.L, entry.C, entry.h)).Не-вакуозность показана: светлый фон с
text:"white"→ chosen 1.67 (<3) роняет; out-of-gamut компонент роняетisInGamut. Golden-name иminPairwiseDistanceне трогал.vitest→ 15 passed; генерацию палитры/хеш/консьюмеров не менял.DROP-пункты (рантайм-OKLCH vs запечь; мемоизация partnerHex; hueShift-поле; JSDoc «on every build»; base-layer коллизии) — как помечено, FYI/не-блокеры, не трогал.
Ре-ревью — #320 (генерируемая OKLCH-палитра аватарок агента, #300), round 2, head
8971912d, base developДельта r1→r2 (коммит
8971912d): усилен per-entry WCAG/gamut-тест палитры (был вакуозным) + 4 color-math хелпера получилиexport(толькоexport, логика байт-идентична). Полный веер 9 аспектов заново по всему PR (da952ca5..8971912d).Вердикт: PASS — единственное round-1 замечание (F1) закрыто по-настоящему (сверено по коду), объективка зелёная. Готово к мержу.
Объективка запущена мной (детач
8971912d, main-клон): clientvitest agent-avatar-stack + avatar-palette→ 2 files, 15 passed;tsc --noEmit→ 0.Закрыто (сверено по коду + прогоны)
expect(["white","black"]).toContain(entry.text)(не мог упасть) заменён на реальную проверку по КАЖДОЙ из 20 записейPALETTE(avatar-palette.test.ts): (а) НЕЗАВИСИМО пересчитывает контраст из СОХРАНЁННОГОentry.hex(hexToRgb01→relativeLuminance, scale-match 0..255→0..1) против выбранногоentry.textи требуетcontrastRatio≥3— плюс обратный гард (text==="black"⇒ white-контраст<3), т.е. падает в ОБЕ стороны; не тавтология (не переводит text черезbuildPalette). (б) In-gamut пересчитываетoklchToSrgb(entry.L,entry.C,entry.h)из сохранённых OKLCH-координат (не смотрит уже-склампленный hex) → ловит мутанта, уронившегоclampChroma. 4 хелпера —export-only (тела байт-идентичны, внутреннее использование не тронуто). Порог 3:1 корректен (WCAG для графических объектов/крупного глифа-иконки, не 4.5:1 для body-text).Box,data-testid="agent-glyph", z-order лаунчера 2>1, адаптивный текст (светлый фон→чёрный/тёмный→белый). СтарыеagentGlyphBackground/GLYPH_COLORS/hashNameудалены (grep чист), консьюмеры не тронуты. Веер 9 аспектов на r2 — чисто (security/stability/regressions/conventions/documentation/simplification/architecture/coherence LGTM; export внутренних хелперов для теста — паттернsrc/lib, усиливает тест). Golden/distinctness/determinism/DOM-фон тесты целы.Готово к мержу.