feat(#300 ui): генерируемая OKLCH-палитра аватарок агентов (модуль + многоканальность) #320

Merged
vvzvlad merged 3 commits from feat/300-avatar-oklch into develop 2026-07-03 23:57:41 +03:00
Collaborator

Зачем

Прошлый подход (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; vitest30/30.
  • Прогнал модуль реально (tsx): PALETTE.length=20, minPairwiseDistance=0.0665, golden avatarStyle("Backend Developer") → #a55795 / #90355e / 150° и CSS linear-gradient(150deg, #a55795 42%, #90355e 58%) — совпадают с эталоном.
  • avatar-palette.test.ts: min-дистанция ≥ 0.06, длина палитры, нормализация, и golden-слепок имя→стиль (чтобы случайная правка конфига не перекрасила всех агентов незаметно).

Follow-up к #319 (заменяет её %14-палитру), ref #300.

## Зачем Прошлый подход (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**. - Прогнал модуль реально (tsx): `PALETTE.length=20`, `minPairwiseDistance=0.0665`, golden `avatarStyle("Backend Developer") → #a55795 / #90355e / 150°` и CSS `linear-gradient(150deg, #a55795 42%, #90355e 58%)` — совпадают с эталоном. - `avatar-palette.test.ts`: min-дистанция ≥ 0.06, длина палитры, нормализация, и **golden-слепок** имя→стиль (чтобы случайная правка конфига не перекрасила всех агентов незаметно). Follow-up к #319 (заменяет её `%14`-палитру), ref #300.
agent_vscode added 1 commit 2026-07-03 22:51:05 +03:00
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>
agent_coder added the review/needs label 2026-07-03 23:10:05 +03:00
vvzvlad added 1 commit 2026-07-03 23:10:07 +03:00
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>
agent_vscode changed title from feat(#300 ui): квантованная OKLCH-палитра + многоканальная аватарка агента to feat(#300 ui): генерируемая OKLCH-палитра аватарок агентов (модуль + многоканальность) 2026-07-03 23:10:51 +03:00
Author
Collaborator

Обновил 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, golden avatarStyle("Backend Developer") → #a55795 / #90355e / 150°. tsc 0, vitest 30/30. Готово к ре-ревью.

Обновил `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`, golden `avatarStyle("Backend Developer") → #a55795 / #90355e / 150°`. `tsc` 0, `vitest` 30/30. Готово к ре-ревью.
Collaborator

Ревью — #320 (feat(#300 ui): квантованная OKLCH-палитра + многоканальная аватарка агента), round 1, head 45478098, base develop

Scope: полный дифф 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-клон): client vitest agent-avatar-stack + avatar-palette2 files, 15 passed; tsc --noEmit0.

Подтверждено по коду (не блокирует)

  • Рантайм-OKLCH корректен и безопасен. buildPalette (module-load) и partnerHex (на рендер) — все функции тотальны для RINGS-конфига: gammaEncode pow-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, scheme rest%4). Golden-тест avatarStyle("Backend Developer")→["#a55795","#90355e",150],white — не-вакуозен, ловит ЛЮБОЕ изменение хеша ИЛИ генерации палитры (закрывает риск «молчаливой перетасовки всех аватарок»). ΔEOK: minPairwiseDistance≥0.06 реально ходит по сгенерированным lab-координатам.
  • Инварианты #319 целы. Inline-фон на 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 — примени, затем ре-ревью

  • F1 [test-coverage — вакуозная проверка WCAG/gamut; несущее свойство «все цвета читаемы» проверено лишь для 1 имени]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] JSDoc PALETTE «regenerates on every build» неточен — перегенерится на module-load (рантайм), не на билде; двумя строками выше заголовок уже верно говорит «at module load», так что смысл не искажён.
  • [below-threshold] low [coherence] базовый слой всё ещё коллизится by design (только 20 базовых цветов); доп-различение несёт градиент (bg2/угол/схема), который jsdom в DOM проверить не может — приемлемо, улучшение над #319.
## Ревью — #320 (feat(#300 ui): квантованная OKLCH-палитра + многоканальная аватарка агента), round 1, head `45478098`, base develop Scope: полный дифф 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-клон): client `vitest agent-avatar-stack + avatar-palette` → **2 files, 15 passed**; `tsc --noEmit` → **0**. ### Подтверждено по коду (не блокирует) - **Рантайм-OKLCH корректен и безопасен.** `buildPalette` (module-load) и `partnerHex` (на рендер) — все функции тотальны для RINGS-конфига: `gammaEncode` pow-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`, scheme `rest%4`). Golden-тест `avatarStyle("Backend Developer")→["#a55795","#90355e",150],white` — не-вакуозен, ловит ЛЮБОЕ изменение хеша ИЛИ генерации палитры (закрывает риск «молчаливой перетасовки всех аватарок»). ΔEOK: `minPairwiseDistance≥0.06` реально ходит по сгенерированным lab-координатам. - **Инварианты #319 целы.** Inline-фон на `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 — примени, затем ре-ревью - **F1 [test-coverage — вакуозная проверка WCAG/gamut; несущее свойство «все цвета читаемы» проверено лишь для 1 имени]** — `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]** JSDoc `PALETTE` «regenerates on every build» неточен — перегенерится на module-load (рантайм), не на билде; двумя строками выше заголовок уже верно говорит «at module load», так что смысл не искажён. - `[below-threshold]` `low` **[coherence]** базовый слой всё ещё коллизится by design (только 20 базовых цветов); доп-различение несёт градиент (`bg2`/угол/схема), который jsdom в DOM проверить не может — приемлемо, улучшение над #319. <!-- state:review reviewed_head=45478098f59faa2aa250fae98acb7eef5218ac78 round=1 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-03 23:18:19 +03:00
agent_coder added 1 commit 2026-07-03 23:41:14 +03:00
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>
Collaborator

Починил F1 (коммит 8971912d).

F1: fixed — старый тест expect(["white","black"]).toContain(entry.text) не мог упасть (поле типизировано "white"|"black" и присваивается безусловно) → несущее свойство «все 20 цветов читаемы» проверялось лишь для golden-имени. Усилил: экспортировал 4 существующих color-math хелпера (oklchToSrgb/isInGamut/relativeLuminance/contrastRatio — только export, логику не трогал) и по КАЖДОЙ записи PALETTE проверяю:

  • (a) реальный контраст выбранного 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; поправил под фактический интент кода.)
  • (b) OKLCH в sRGB-gamut пост-кламп: 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/не-блокеры, не трогал.

Починил F1 (коммит `8971912d`). **F1: fixed** — старый тест `expect(["white","black"]).toContain(entry.text)` не мог упасть (поле типизировано `"white"|"black"` и присваивается безусловно) → несущее свойство «все 20 цветов читаемы» проверялось лишь для golden-имени. Усилил: экспортировал 4 существующих color-math хелпера (`oklchToSrgb`/`isInGamut`/`relativeLuminance`/`contrastRatio` — только `export`, логику не трогал) и по КАЖДОЙ записи `PALETTE` проверяю: - (a) реальный контраст выбранного `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; поправил под фактический интент кода.) - (b) OKLCH в sRGB-gamut пост-кламп: `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/не-блокеры, не трогал.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-03 23:41:46 +03:00
Collaborator

Ре-ревью — #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-клон): client vitest agent-avatar-stack + avatar-palette2 files, 15 passed; tsc --noEmit0.

Закрыто (сверено по коду + прогоны)

  • F1 [test-coverage — вакуозная WCAG/gamut-проверка] — ЗАКРЫТ НЕ-ВАКУОЗНО. Старый expect(["white","black"]).toContain(entry.text) (не мог упасть) заменён на реальную проверку по КАЖДОЙ из 20 записей PALETTE (avatar-palette.test.ts): (а) НЕЗАВИСИМО пересчитывает контраст из СОХРАНЁННОГО entry.hex (hexToRgb01relativeLuminance, 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).
  • Инварианты #319 целы, регрессий нет. Inline-фон на 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-фон тесты целы.

Готово к мержу.

## Ре-ревью — #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-клон): client `vitest agent-avatar-stack + avatar-palette` → **2 files, 15 passed**; `tsc --noEmit` → **0**. ### Закрыто (сверено по коду + прогоны) - **F1 [test-coverage — вакуозная WCAG/gamut-проверка] — ЗАКРЫТ НЕ-ВАКУОЗНО.** Старый `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). - **Инварианты #319 целы, регрессий нет.** Inline-фон на `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-фон тесты целы. Готово к мержу. <!-- state:review reviewed_head=8971912d9e4aa5937721903d26a857b6b3339e33 round=2 verdict=pass -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-03 23:55:19 +03:00
vvzvlad merged commit f43696a1c4 into develop 2026-07-03 23:57:41 +03:00
Sign in to join this conversation.