feat(core): конфиг-ядро PR-a — ThemeConfig + from_config + ladder/alpha_analog эмиссия (ADR-0001) - #121
Conversation
…р + фикстура labui (t1) CH-02 t1 конфиг-границы (ADR-0001). Скоуп: типы конфига без serde, валидатор пределов каждой экспонируемой ручки, DIP-рефактор резолва по &RoleSpec, эталонная фикстура labui и байт-в-байт тест против сегодняшней эмиссии. - config.rs: ThemeConfig (Brand/Neutral/Palette/Sentiments/Themes/roles/aliases), VcPreset (закрытое меню Srgb|Dim|SrgbIc|DimIc), RoleRecipe (TextAnchor/DjAnchor/ DecorativeLc/Ladder/AlphaAnalog/Zero). Ladder/AlphaAnalog — честная заглушка с верным типом (ConfigError::NotYetImplemented, реализация t2). - ConfigError: ручной enum + Display + Error (ноль runtime-deps, issue #29). - Валидатор: пределы каждой ручки с обоснованием в doc-строке; невалидный hex/имя/ссылка на несуществующее семейство/роль → ConfigError. - DIP: resolve_spec_in(&RoleSpec) — общее физ-ядро; resolve_in(Role) стал обёрткой. NamedRoleTable + resolve_named_set + ThemeConfig::compile_named_role_table. - Фикстура labui_reference(): 20 ролей 1:1 из RoleTable::default, нейтраль-тройка и tint-ручки из констант semantic.rs (единый источник истины). - Тесты: байт-в-байт 240 точек (config == default) + RED-proof; валидатор на каждую ручку + RED-proof на границах; заглушки t2. Golden semantic.rs, empirical_inventory-гейт, lint_parity — зелёные. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…PERC_MIN (t2) Поглощает акцентный GAP #59: лестница акцента/сентимента/бренда как ДАННЫЕ — один тинт-якорь источника (пер-темно) × закрытая рампа альф Figma @nn. Ядро: - crate::ladder — ThemeAnchors (пер-темная четвёрка), LadderTint (Copy-payload, разложен по 4 режимам), LadderPosition (закрытое меню 13 позиций + альфы @nn). - RoleSpec::Ladder{tint, alpha} + RoleSpec::AlphaAnalog{of, alpha} (semantic.rs). - Resolved::Rgba(RgbaResolved) — rgba(тинт, α) НАПРЯМУЮ (закон лестницы labui, композитит браузер) + солид-композит на фоне резолва для замера контраста (фаза 1 AA меряет композит). resolve_spec_in — единый сеам физики обоих фронтов. - BgInput::encoded_display — device-пространство фона для композита. - sentiment: resolve_smooth_hue_explicit (config-facing), resolve_config_sentiment_solid (тинт сентимента = якорь, разведённый с брендом), s_perc_min_from_chromas (закон 2·C_rep·sin(20°/2) из конфиг-якорей), s_perc_min_frozen. Конфиг: - Brand/PaletteFamily → пер-темные якоря (ThemeAnchors) ДОСЛОВНО из reference §2. - RoleRecipe::Ladder{source, position} + AlphaAnalog{of, alpha}; LadderSource (Brand/Family/Sentiment); валидатор ссылок источника + предел альфы. - labui_reference расширена акцент/сентимент/FX/альфа-ролями (consumedRoles). Тесты (RED-proof кусаются): - diff=пусто против consumedRoles labui (удаляемые по коллапсу перечислены). - S_PERC_MIN(labui) == 0.068_703_9 (деривационная идентичность, 1e-4) + RED-proof. - Сентимент-тинт == сырой якорь при hue-дальнем бренде (Danger/Success/Warning); Info смещён от синего бренда ПО ПОСТРОЕНИЮ (честная находка, задокументирована). - rgba-эмиссия + композит; RED-proof мутаций позиции/семейства/альфы. - Байт-в-байт t1 (240) зелёный; empirical_inventory зелёный. Приложение A к ADR-0001: меню позиций + провенанс + деривационная идентичность. Известная коллизия: r3_byte_identity.rs изменён на 1 арм match (вынужденно — новый вариант Resolved::Rgba); s2b-baseline-guard требует ре-анкора владельцем эпика s2b (байт-дрейфа НЕТ, r3-тесты зелёные). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…c-строка Изолированная верификация t2 (arch-reviewer PASS_WITH_NOTES + CoVe SUPPORTED): - High-1: r3_byte_identity.rs — wildcard `_ => "UNKNOWN"` заменён на `other => panic!(...)`, чтобы golden не проглотил молча будущий вариант Resolved. - Low-2: config.rs — комментарии FX-neutral/skeleton переписаны сухо (почему+как, без потока сознания). - Low-1: wasm/engine.rs — catch-all после Rgba помечен осознанным долгом t3. - CoVe: LABUI_CONSUMED_ROLES — doc-строка про компромисс зеркала (класс дрейфа закрывается гардами поезда labui против живой эмиссии). Долг (Medium-1, не сейчас): Alpha как value-object вместо голого f64 — кандидат на будущий рефактор типобезопасности. Прогоны: cargo fmt/clippy -D warnings чисты; 273 core lib + 12 наборов зелёные; единственный красный — s2b baseline-guard на r3 (флагнут владельцу, байт-дрейфа нет). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ий тест
Приёмка нашла дефект заземления (посылка «labui Neutral = семейство blue» —
ложна). Сверено построчно со стабом labui packages/colors-stub/contract.css:
(1) НЕЙТРАЛЬНЫЙ источник ≠ blue. Добавлен LadderSource::Neutral(NeutralPick)
из neutral.anchors (Mid #787880 / Light #FFFFFF / Dark #101012).
Переведены на него: fx-skeleton-* (тинт #787880, стаб rgb(120 120 128 / …)),
fx-glow-neutral (белый @52), fx-glow-inverted, fx-focus-ring-neutral,
fill-neutral. fill-neutral-tinted/border-neutral — алиасы на core
fill-primary/border-base (стаб var()).
(2) ПЕР-ТЕМНЫЕ альфы: LadderPosition::alpha() → alpha_pair()(light,dark) +
alpha_for_vc(); RoleSpec::Ladder несёт alpha_light/alpha_dark, резолв
выбирает по теме. SkeletonBase пер-темна (light @8 / dark @12 по стабу) —
исправлены перепутанные base/highlight альфы. Добавлены SkeletonBase/
SkeletonHighlight позиции. Акцентные пары равны (у Figma нет dark-рампов
альф — стаб держит равными, помечено).
(3) КЛАСС «имена без значений»: значенческий тест
representative_roles_match_stub_values_light_and_dark — эмиссия rgba(тинт,α)
представителя каждой группы == строка стаба ПОБАЙТНО в light И dark.
Исключены намеренно расходящиеся (info — смещение солвером; focus-ring-neutral
dark / glow-inverted / fill-neutral — задокументированные gap-и). RED-proof
value_test_bites_on_alpha_mutation (мутация одной альфы → RED, проверено).
Прогоны: fmt/clippy -D warnings чисты; 275 core lib + 20 наборов зелёные;
s2b baseline-guard зелёный (r3/empirical_inventory не тронуты). Приложение A
к ADR-0001 обновлено (пер-темная таблица альф + neutral-источник + значенческая
сверка).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 22 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (12)
WalkthroughPR добавляет декларативную конфигурацию тем и модель ladder-якорей, расширяет семантику новыми rgba-ролями, обновляет sentiment/solve, реэкспортирует новые API, добавляет downstream-обработку нового варианта ChangesLadder-роли и конфигурация тем
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant ThemeConfig
participant validate_syntactic
participant compile_named_role_table
participant resolve_named_set
ThemeConfig->>validate_syntactic: validate()
validate_syntactic-->>ThemeConfig: Ok / ConfigError
ThemeConfig->>compile_named_role_table: compile_named_role_table()
compile_named_role_table-->>ThemeConfig: NamedRoleTable
ThemeConfig->>resolve_named_set: resolve_named_set(bg, table, vc)
resolve_named_set-->>ThemeConfig: Vec<(String, Resolved)>
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
…мые ошибки
CI: wasm_parity.rs не знал Resolved::Rgba (тест компилится только под wasm32
— локальный прогон слеп) — явный panic-арм на будущие варианты, не маска.
CodeRabbit (4 major + trivial): (1) дубликаты ключей всех словарей конфига
отвергаются (DuplicateKey; включая алиас, затеняющий роль) — повтор имени
делал lookup неоднозначным; (2) ошибки ссылок различимы: UnknownSentiment/
UnknownRole вместо перегруженного UnknownFamily; (3) неконечные значения
(±∞/NaN) отвергаются и open-сверху пределами (is_finite в check_gt/check_ge);
(4) preferred_side — закрытое меню {-1,+1} (0/2 отвергаются); (5) единый
vc_slot() — обе раскладки четвёрки режимов выбирают слот одним отображением.
Тесты на каждый пункт (4 новых + обновлён alias-тест под различимую ошибку).
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/labcolors-core/src/config.rs`:
- Around line 484-485: `compile_named_role_table()` currently emits only
`self.roles`, so aliases validated by `validate()` and referenced by
`labui_reference()` (including `fill-neutral-tinted` and `border-neutral`) are
missing from `NamedRoleTable`. Update `compile_named_role_table()` in
`config.rs` to include `self.aliases` when building the named-role emission, and
make sure the alias entries resolve to the existing target roles so they are
available in the compiled table.
- Around line 266-267: `compile_named_role_table()` is ignoring
`NeutralConfig::anchors` and always using `semantic::NEUTRAL_HUE_DEG`, so custom
neutral scales never affect the undertone curve. Update the neutral-role
compilation path in `config.rs` to derive the canonical neutral hue from the
provided `NeutralConfig::anchors` instead of the fixed semantic constant, and
thread that value through the same role-table build logic used for the other
named roles. Make sure the new calculation is localized near
`compile_named_role_table()` / `NeutralConfig` and does not leave an unexplained
magic constant.
- Around line 226-232: The UnknownFamily error message in the
ConfigError::UnknownFamily match arm incorrectly says “роль”, which is
misleading for non-role references like sentiments.*. Update the formatting in
config.rs to remove that word and keep the message generic using the
referenced_by and family fields so it applies correctly across all callers.
- Around line 1029-1039: Update the rustdoc on the fixture in config.rs to match
the current t2-expanded shape: the summary still says it covers only 20 emitted
roles and that accents/sentiments/alpha/ladder are t2, but the fixture now also
includes ladder/rgba roles and consumedRoles aliases. Adjust the comments around
the fixture and the resolve_named_set/resolve_set comparison so the description
reflects the actual role set and terminology, using the existing symbols
RoleTable::default, resolve_named_set, and resolve_set to locate the block.
- Around line 672-675: The `if let`-chain in `config.rs` is incompatible with
the declared MSRV 1.85. Update the `cat.preferred_side` check in the relevant
`config` handling logic to avoid `if let ... &&` syntax, either by raising
`rust-version` if that is intended or by rewriting it as a nested `if` using the
existing `cat.preferred_side` conditional path.
In `@crates/labcolors-core/src/config/tests.rs`:
- Around line 910-919: The WCAG assertion in the `composite_wcag` test is too
weak and does not enforce the behavior described in the comment. Tighten the
check in the `r.composite_wcag()` assertion for the transparent `#007AFF` over
white case by comparing against a specific expected low contrast value or a
strict upper bound (instead of the trivial 1..21 range), using the existing
`composite_hex`/`composite_wcag` test flow to keep the intent aligned with the
`r.composite_hex()` and `r.composite_wcag()` methods.
In `@crates/labcolors-core/src/semantic.rs`:
- Around line 1455-1458: The composite values are being computed from
analog.tint, but AlphaAnalog only exposes tint_hex externally, so the measured
composite can differ from what the consumer actually paints. Update finish_rgba
and the AlphaAnalog flow to compute composite_lc/composite_wcag from the emitted
tint_hex (or the quantized tint it represents), and keep composite_over_encoded
aligned with that public output; if exact identity behavior is required, change
the API to expose the unquantized tint instead of relying on tint_hex.
- Around line 1391-1396: The public RoleSpec::Ladder path should validate the
alpha/tint inputs before calling resolve_rgba_direct/composite_over_encoded,
since debug_assert checks in release do not prevent invalid values from
producing bad rgba/metrics. Add a runtime guard in the Ladder handling around
the alpha selection and tint.for_vc(vc) flow to ensure alpha stays in the
expected 0..=1 range and the encoded RGB/tint input is valid before compositing,
and apply the same check in the other affected Ladder branch near the matching
resolve_rgba_direct call.
In `@docs/decisions/0001-config-boundary.md`:
- Around line 201-210: In the decision document, fix the typo in the sentence
about intentionally divergent roles by changing “с ссылкой” to “со ссылкой”;
update the prose in the section containing
representative_roles_match_stub_values_light_and_dark so the wording is
grammatically correct while keeping the rest of the explanation unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 9dd9a716-f2c2-421c-8d63-cd0f0a361d1a
📒 Files selected for processing (11)
crates/labcolors-core/src/config.rscrates/labcolors-core/src/config/tests.rscrates/labcolors-core/src/ladder.rscrates/labcolors-core/src/lib.rscrates/labcolors-core/src/semantic.rscrates/labcolors-core/src/sentiment.rscrates/labcolors-core/src/solve.rscrates/labcolors-core/tests/r3_byte_identity.rscrates/labcolors-wasm/src/engine.rscrates/labcolors-wasm/tests/wasm_parity.rsdocs/decisions/0001-config-boundary.md
| if let Some(side) = cat.preferred_side | ||
| && side != 1 | ||
| && side != -1 | ||
| { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Проверить заявленный Rust toolchain/MSRV без запуска кода репозитория.
fd '^(Cargo\.toml|rust-toolchain(\.toml)?|\.tool-versions)$' -H -t f . \
-x sh -c 'echo "--- $1"; grep -nE "rust-version|channel|rust" "$1" || true' sh {}Repository: Labpics-Team/lab-colors
Length of output: 323
🌐 Web query:
Rust if let chains stabilized version 1.85 1.88 reference
💡 Result:
The "let chains" feature, which allows chaining let expressions with && in if and while conditions, was stabilized in Rust 1.88.0 [1][2]. This feature is available exclusively when using the Rust 2024 Edition [3][4]. It is edition-gated because it relies on changes to how if let temporary scopes are handled, which were introduced in the 2024 edition to ensure consistent behavior [3][4]. Key details: - Stabilization Version: Rust 1.88.0 (released June 26, 2025) [1][2]. - Requirement: Must be using the 2024 Edition [3][1]. - Functionality: Allows intermixing let expressions with boolean expressions using && [5][1]. Bindings from a let expression are available to subsequent conditions in the same chain and the consequent block [6][7]. - Precedence/Syntax: Because of ambiguity, let chains cannot be directly combined with || operators; parentheses must be used to group expressions if || is required [6][7].
Citations:
- 1: https://releases.rs/docs/1.88.0/
- 2: https://github.com/rust-lang/rust/releases/tag/1.88.0
- 3: https://doc.rust-lang.org/stable/edition-guide/rust-2024/let-chains.html
- 4: https://blog.rust-lang.org/2025/06/26/Rust-1.88.0/
- 5: Stabilize let chains in the 2024 edition rust-lang/rust#132833
- 6: https://doc.rust-lang.org/stable/reference/expressions/if-expr.html
- 7: https://doc.rust-lang.org/1.88.0/reference/expressions/if-expr.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '660,685p' crates/labcolors-core/src/config.rs | cat -nRepository: Labpics-Team/lab-colors
Length of output: 1648
if let-chain не совместим с заявленным MSRV 1.85
Cargo.toml указывает rust-version = "1.85", а этот синтаксис требует Rust 1.88 / Edition 2024. Либо поднимите MSRV, либо перепишите на вложенный if.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/labcolors-core/src/config.rs` around lines 672 - 675, The `if
let`-chain in `config.rs` is incompatible with the declared MSRV 1.85. Update
the `cat.preferred_side` check in the relevant `config` handling logic to avoid
`if let ... &&` syntax, either by raising `rust-version` if that is intended or
by rewriting it as a nested `if` using the existing `cat.preferred_side`
conditional path.
| ### Значенческая сверка (класс «имя без значения») | ||
|
|
||
| `representative_roles_match_stub_values_light_and_dark` сверяет эмиссию | ||
| `rgba(тинт, α)` представителя КАЖДОЙ группы со строкой стаба ПОБАЙТНО в light И | ||
| dark (нормализованный формат). Закрывает класс дефекта «роль в diff по имени, но | ||
| эмитит не то значение». Исключены намеренно расходящиеся роли (с ссылкой): | ||
| `*-info-*` (смещение солвером); `fx-focus-ring-neutral` (dark), `fx-glow-inverted`, | ||
| `fill-neutral` — задокументированные gap-и (пер-темный нейтральный край / | ||
| inverted-якоря / PROVISIONAL-литерал не выводятся из тройки `neutral.anchors`). | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Опечатка: «с ссылкой» → «со ссылкой».
✏️ Исправление
-эмитит не то значение». Исключены намеренно расходящиеся роли (с ссылкой):
+эмитит не то значение». Исключены намеренно расходящиеся роли (со ссылкой):📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ### Значенческая сверка (класс «имя без значения») | |
| `representative_roles_match_stub_values_light_and_dark` сверяет эмиссию | |
| `rgba(тинт, α)` представителя КАЖДОЙ группы со строкой стаба ПОБАЙТНО в light И | |
| dark (нормализованный формат). Закрывает класс дефекта «роль в diff по имени, но | |
| эмитит не то значение». Исключены намеренно расходящиеся роли (с ссылкой): | |
| `*-info-*` (смещение солвером); `fx-focus-ring-neutral` (dark), `fx-glow-inverted`, | |
| `fill-neutral` — задокументированные gap-и (пер-темный нейтральный край / | |
| inverted-якоря / PROVISIONAL-литерал не выводятся из тройки `neutral.anchors`). | |
| ### Значенческая сверка (класс «имя без значения») | |
| `representative_roles_match_stub_values_light_and_dark` сверяет эмиссию | |
| `rgba(тинт, α)` представителя КАЖДОЙ группы со строкой стаба ПОБАЙТНО в light И | |
| dark (нормализованный формат). Закрывает класс дефекта «роль в diff по имени, но | |
| эмитит не то значение». Исключены намеренно расходящиеся роли (со ссылкой): | |
| `*-info-*` (смещение солвером); `fx-focus-ring-neutral` (dark), `fx-glow-inverted`, | |
| `fill-neutral` — задокументированные gap-и (пер-темный нейтральный край / | |
| inverted-якоря / PROVISIONAL-литерал не выводятся из тройки `neutral.anchors`). |
🧰 Tools
🪛 LanguageTool
[uncategorized] ~206-~206: Неправильная форма предлога. Исправление: «со ссылкой».
Context: ... Исключены намеренно расходящиеся роли (с ссылкой): *-info-* (смещение солвером); `fx-f...
(S_SO)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/decisions/0001-config-boundary.md` around lines 201 - 210, In the
decision document, fix the typo in the sentence about intentionally divergent
roles by changing “с ссылкой” to “со ссылкой”; update the prose in the section
containing representative_roles_match_stub_values_light_and_dark so the wording
is grammatically correct while keeping the rest of the explanation unchanged.
Source: Linters/SAST tools
… гарды обхода 4 major: (1) алиасы переносятся в NamedRoleTable (без переноса алиасные роли контракта — fill-neutral-tinted и др. — терялись при компиляции; эмиссия потребителем = var()-ссылка, одна истина значения); (2) sentiment_s_perc_min → Result — filter_map молча считал порог разделения по неполному набору категорий (тихая математическая ложь); (3) rgba-путь резолва отвергает спеку вне домена (RoleSpec публичен — сборка в обход валидатора даёт Unreachable, не правдоподобный мусор); (4) LadderTint::new валидирует четвёрку режимов (Err с именем битого режима). Trivial/minor: Rgba в default-golden — шумная паника вместо маски «не клип» (дрейф дефолт-таблицы обязан падать); IC-наследование альф закреплено тестом (пер-темная пара skeleton реально различается); русификация приложения A. Тесты на каждый гард.
…р, signum 3 major: (1) оттенок нейтрального подтона — из КОНФИГА: явная ручка hue_override_deg (labui несёт измеренную SSOT 286.0°) либо деривация из тёмного якоря нейтрали клиента (той же формулой, которой была получена константа: #101012→285.97°) — labui-константа для чужой нейтрали была дефектом агностичности; (2) контраст rgba-роли меряется по КВАНТОВАННОМУ композиту — тому же 8-битному hex, что уходит наружу (закон solve_dj; неквантованный замер расходился с отданным на LSB); (3) preferred_side нормализуется до ±1 на публичном входе солвера (множитель направления, не масштаб). Minor/trivial: rustdoc фикстуры под t1+t2, expect с обоснованием в ladder-тесте, валидатор hue_override [0,360).
…гичный WCAG-тест major: наружу уходит 8-битный tint_hex и браузер композитит именно его — тинт теперь квантуется ДО композита в обоих rgba-путях (quantise_encoded, hex-roundtrip без строки); composite_hex/Lc/WCAG считаются из эмитируемого значения (завершение принципа «всё внешнее посчитано из внешнего»). Равенство композита солиду в alpha-analog держится в LSB-границе (#119). minor/trivial: expect с обоснованием на hex-парсингах ladder-тестов; тавтологичная проверка WCAG∈[1,21] заменена содержательной (композит @8 над белым обязан быть почти белым и вдвое контрастнее солидного тинта — доказывает замер по правильному цвету). «Различать код catch-all» — отклонено: арм уже помечен осознанным долгом t3, различение кодов там.
…порога, гарды hue 4 major: (1) fx-focus-ring-neutral / fx-glow-inverted — на пер-темных четвёрках конфига (neutral.edge #101012/#F6F8FA, neutral.inverted #B0B0B9/#3C3C43 — стаб дословно): дублирование одного края давало НЕВИДИМОЕ кольцо фокуса на тёмной теме; без поля pick — честная ошибка MissingNeutralAnchors, не выдумка; обе роли теперь в точном value-тесте обеих тем; (2) замороженный labui-S_PERC_MIN больше НЕ подмешивается в config-путь (s_min_deg на константе завышал порог низкохромным палитрам чужим законом — мог опустошить legal arc); (3) preferred_side нормализуется до ±1 (р-3 фикс МОЛЧА не применился: node-replace без assert + CRLF/LF-смесь в репо — CAPA: все replace с assert и EOL-автодетектом); (4) ахроматичные источники оттенка: серая нейтраль без hue_override — AchromaticHueSource; серый бренд — разведение сентиментов честно отключено (сентимент = сырой якорь; порог 1e-7 технический — числовая определённость atan2, не перцептивная политика). minor/trivial: expect на hex тестов лестницы; labui_reference — задокументирован как КАНОНИЧЕСКИЙ конфиг (переезд в пакет = PR-b); граничный тест α=1.0 vs 1.0+ε; тесты MissingNeutralAnchors/achromatic.
…ению Три из четырёх major — один класс (ложноположительный Ok preflight-а: ахроматичная нейтраль, отсутствующие edge/inverted, битый hex в заданных краях). Закрыт не заплатками, а по построению: validate() = компиляция с отброшенным результатом — второго списка проверок не существует, паритет validate/compile не может разъехаться; структурная фаза выделена в приватный validate_syntactic (компиляция зовёт её — не validate: рекурсия). Тест-паритет на корпусе деривационных ошибок сверяет обе ошибки байт-в-байт. Невозможный порог хорды (s_perc_min > 2C): клип-в-180° больше не скармливается p-норм солверу — smooth_separation ≥ s_min перелетает диаметраль, легальное множество вырождается в точку меры нуль и скан-сетка legalize_hue давала бы ложный «пустая дуга». Сатурация решается аналитически: диаметральный оттенок = максимум разведения («максимально приближенный приемлемый», директива владельца), пол-блокировка → граница пола; мусор-входы инверсии хорды — Err. Случай реален: приглушённый тёмный якорь при хромных светлых соседях. Домен alpha-analog: симметричный с прямым rgba-путём гард в resolve_rgba_inverted — недоменная α публичной RoleSpec резолвится в честный Unreachable, а не в правдоподобный hex через кламп резолвера инверсии (недоменный солид по построению невозможен: LadderTint::new валидирует квад). Сопутствующее: ахроматичный якорь СЕМЕЙСТВА → сырой якорь (тот же закон, что серый бренд: нет носителя оттенка — нет разведения); ACHROMATIC_CHROMA_EPS переехал к закону в sentiment.rs — реестр строка 38, GROUNDED с вычисленными границами (мин. 8-бит хрома 1.06e-3, f64-шум ≲1e-12); старый ассерт hue_floor=359.999 → Ok был ровно флагнутым ложноположительным preflight-ом — обновлён на честную семантику (в диапазоне, но дуга пуста).
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/labcolors-core/src/sentiment.rs (1)
653-660: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winДобавьте доменный guard для
brand_hueиhue_floor.Сейчас
resolve_config_sentiment_solidпроверяет anchor иs_perc_min, ноbrand_hue = NaNилиhue_floor = Some(NaN)может пройти дальше и дать неErr, а правдоподобный hex/отключённый floor.Предлагаемый guard
pub fn resolve_config_sentiment_solid( family_anchor_hex: &str, brand_hue: f64, hardness: f64, @@ preferred_side: f64, s_perc_min: f64, ) -> Result<String, String> { + if !brand_hue.is_finite() { + return Err(format!("brand_hue вне домена: {brand_hue}")); + } + if let Some(floor) = hue_floor { + if !floor.is_finite() || !(0.0..360.0).contains(&floor) { + return Err(format!("hue_floor вне домена: {floor}")); + } + } let _ = chroma_fraction; // хрома тинта = хрома якоря (сохраняем солид якоря);Согласно path instructions,
crates/labcolors-core/**/*.rs: «Float-точность» и «Error handling».🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/labcolors-core/src/sentiment.rs` around lines 653 - 660, resolve_config_sentiment_solid currently validates the anchor and s_perc_min, but it still allows non-finite domain inputs to slip through. Add an explicit guard near the start of resolve_config_sentiment_solid for brand_hue and hue_floor so NaN/invalid values are rejected with Err before any color resolution logic runs, and make sure the same validation covers both the raw brand_hue argument and the inner value of hue_floor when present.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/labcolors-core/src/config/tests.rs`:
- Around line 1433-1497: The “complete preflight” test is tautological because
validate() already delegates to compile_named_role_table().map(drop), so it only
verifies compile_named_role_table() behavior for the three derivation cases.
Update the test name/comment in validate_is_a_complete_preflight to describe it
as a regression guard for specific compile errors, and keep the assertions
focused on the concrete error variants (AchromaticHueSource,
MissingNeutralAnchors, InvalidHex) via validate() and compile_named_role_table()
so the intent matches the implementation.
---
Outside diff comments:
In `@crates/labcolors-core/src/sentiment.rs`:
- Around line 653-660: resolve_config_sentiment_solid currently validates the
anchor and s_perc_min, but it still allows non-finite domain inputs to slip
through. Add an explicit guard near the start of resolve_config_sentiment_solid
for brand_hue and hue_floor so NaN/invalid values are rejected with Err before
any color resolution logic runs, and make sure the same validation covers both
the raw brand_hue argument and the inner value of hue_floor when present.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: ed27b05a-a430-4e57-a925-0a3198b267ec
📒 Files selected for processing (7)
crates/labcolors-core/src/config.rscrates/labcolors-core/src/config/tests.rscrates/labcolors-core/src/ladder.rscrates/labcolors-core/src/semantic.rscrates/labcolors-core/src/sentiment.rsdocs/decisions/0001-config-boundary.mddocs/empirical-inventory.md
…ия, паблик-гарды углов 2 major: (1) при поле, блокирующем диаметраль, берётся граница легальной дуги [floor, 360°) с БОЛЬШИМ разведением от бренда (не всегда floor — прежний «детерминизм к полу» противоречил декларированному максимуму разведения); верхняя открытая граница — достижимая точка 360°−1e-9 (суб-LSB отступ); зеркальный тест-корпус на оба исхода; (2) домены α разводить НЕ стали — отклонено с основанием: контракт РОЛИ = (0,1] (α=0 — невидимая роль; тот же предел, что у конфиг-валидатора), α=0 принимает только библиотечный resolve_alpha_analog (вырожденный ответ tint=фон — его домен, не ролевой); починен реальный дефект — рассинхрон доков RoleSpec::AlphaAnalog ([0,1] → (0,1] с объяснением границы слоёв). minor: паблик-гарды resolve_config_sentiment_solid — неконечный brand_hue и недоменный hue_floor (вне [0,360)) в сатурированной ветке уходили мимо солвера прямо в oklab_lc_to_hex тихим неверным hex → честный Err + мусор-тесты; ACHROMATIC_CHROMA_EPS: GROUNDED → SSOT-TRACKED (внешнего стандарта не существует, деривация закрыта и живёт в таблице реестра — GROUNDED обещает цитируемый стандарт); PartialEq/NaN у ConfigError — отклонено с основанием (IEEE-семантика f64-полей, как у самого f64; дроп PartialEq сломал бы assert_eq всех потребителей) + doc-нота на enum. trivial: предикат коллапс-ролей ВЫВОДИТСЯ из деклараций COLLAPSED_ROLES (glob-сопоставитель + значенческий RED-proof гард) — второй вручную синхронизируемый список гнил молча; значенческая сверка со стабом: rgb побайтово + α числом с допуском 1e-12 — Display-сравнение f64 хрупко к хвостам представления.
…ле t2 Разовая правка залоченного r3_byte_identity.rs (арм Resolved::Rgba) была конверсией enum в #[non_exhaustive]: с ней r3 закрыт к будущим расширениям Resolved (wildcard-паника — новый вариант не требует правки файла, но не проходит golden молча). Лок гарда = «чисто против HEAD», не вечная заморозка: осознанная правка становится новым анкором через коммит + PR-гейт. Байт-дрейфа эмиссии нет: r3 240-cell golden + споты зелёные, nested-прогон baseline_r3_byte_identity_tests_pass — ok.
…наррации Ссылки на раунды ревью, задачи эпика (t1/t2/t3, №г/№д), «решение владельца» и даты процессов — разговор с ревьюером, а не ограничение кода: читателю пакета они не дают ничего и выглядят чужеродно. Каждый такой комментарий переписан до сухого «почему» (само основание везде сохранено); провенанс данных (даты замеров Figma/стаба) оставлен — он отвечает «с какого снимка истины взято значение». Ссылка на локальный путь эпика вне репо заменена самими источниками (Figma-файл + reference/ + стаб labui). Сообщение об ошибке NotYetImplemented больше не отсылает к внутренней задаче.
…ии, ADR Сообщение RoleOutcome::Unreachable у rgba-ролей несло внутренний код задачи — а это наблюдаемая поверхность API, не комментарий; заменено описанием причины (solid-only surface). Атрибуции решений (owner-провизион, owner-approved) и коды задач в заголовках ADR убраны — основание остаётся, происхождение решения читателю кода не нужно. Ссылка ADR на grounding-файл вне репо заменена самим источником (переменные Figma + путь обхода).
…ходов Ошибка сентимент-солвера (пустая легальная дуга, недоменные углы) наружу — собственным вариантом ConfigError::SentimentResolution, а не маской InvalidHex: потребитель матчится по вариантам, и ошибка политики/геометрии под видом ошибки парсинга hex ломала бы различение. Валидация hardness/s_perc_min перенесена ДО ахроматичного fast path — недоменная ручка не может «повезти» в зависимости от цвета якоря. Публичный resolve_smooth_hue_explicit получил домен-гард (конечные углы, s_min в [0,180], пол в [0,360)) — NaN в signed_delta/smooth_separation давал бы NaN-оттенок вниз по физике, а скан legalize_hue на NaN не завершается осмысленно. Отклонено с основанием: #[non_exhaustive] на RoleSpec — атрибут уже стоит (semantic.rs); придирка к GROUNDED-маркеру ACHROMATIC_CHROMA_EPS — маркер уже SSOT-TRACKED. Доки пары альф позиции уточнены: пару несёт каждая позиция, расходятся значения только у скелетон-базы.
…ючей Канонический домен [0, 360) получил один дом (sentiment, рядом с законом) и один хелпер проверки check_hue_floor_domain для обоих публичных входов — два независимых литерала/блока одного домена в цветовом коде были классом тихого расхождения; config импортирует границы (направление зависимостей внутрь). Дока провенанса альф называет солидные позиции их реальными ключами контракта (label-primary, не primary — авторы конфигов парсят key()). Тест пары альф фиксирует наследование базовой темы IC-режимами — будущая ic-ветка в alpha_for_vc не проедет молча.
Гейт-1 честно поймал немаркированные HUE_DOMAIN_*: каждая константа перцептивных модулей обязана нести paper-trail. Обе — определение домена (угол по модулю 360°), не политика: SSOT-TRACKED + строки 39/40 реестра.
…амок README (#136) * docs(readme): синхронизация с фактическим кодом ядра Секция ролей устарела полностью: реально 20 ролей (Role::ALL), имена label-* (HIG-переименование из text-*), Border/Shadow разветвлены, добавлено семейство Fill, роль Surface удалена, дефолтный подтон — RoleChroma::Curve (тонированный), а не ChromaPolicy::Neutral (другой enum решателя). Таблица переписана построчно по RoleTable::default(). - пример API семантики: Role::TextPrimary → Role::LabelPrimary, реальные значения (#0A0A10, lc 102.7) посчитаны кодом; пример компилируется - API-пример и пример семантики закреплены doctest-замком в lib.rs (#[cfg(doctest)] include_str! README) — rust-блоки компилируются навсегда; иллюстративные блоки/формулы помечены text - «Структура проекта» дополнена реальными модулями (accent, alpha, chromafast, cleanliness, config, greyfast, ladder, lut, wcag, oklch, gamma_data) с назначениями из шапок - «Тесты»: полный набор CI-гейтов (clippy+fmt, test, audit, wasm+headless, npm typecheck/test) вместо заниженного - «Что дальше»: убран сделанный runtime-engine, surface-jnd переформулирован под реальность (роли surface нет, border-*/fill-* уже якорные) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y8JyGJvXX872YqYrGa1xM * docs(pkg): формат эмиссии — vars несут oklch, не hex После релиза 0.4.0 (#129/#130) CSS-переменные несут готовое oklch(L% C H [/ A]) (wasm/lib.rs oklch_css), а сырой #RRGGBB остаётся данными роли (roles.<ключ>.hex). Исправлены вводный абзац, пример vars (реальные oklch-строки) и описание vars/SolvedColor; добавлено поле css (готовое oklch(L% C H)) — сверено с TS-типами wasm/lib.rs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y8JyGJvXX872YqYrGa1xM * docs(adr): ADR-0001 принят и внедрён Статус «предложено» → «принято, внедрено» (config.rs — #121, wasm loadConfig — #122). Синхронизированы имена API: RoleTable::from_config → ThemeConfig::compile_named_role_table → NamedRoleTable, резолв — resolve_named_set. Клейм про отпечаток актуализирован: отпечаток загруженного конфига реально вычисляется (FNV-1a над DTO, config_dto.rs) и продевается в кэш-ключ через loadConfig; встроенная дефолт-таблица остаётся на DEFAULT_TABLE_FINGERPRINT = 0, корректность при смене конфига держит wholesale clear(). Историческая аргументация не тронута. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y8JyGJvXX872YqYrGa1xM * docs(core): актуализация комментариев по фактическому коду Только комментарии и doc-строки, поведение не тронуто. - semantic.rs: проза шапки модуля и doc-строк констант называла роли text-* как текущие — обновлено на label-* (упоминания «Was text-*» как история оставлены) - cache.rs: шапка и doc DEFAULT_TABLE_FINGERPRINT утверждали «отпечаток — константа-заглушка, пока жива только дефолт-таблица» — устарело: loadConfig влит, отпечаток конфига вычисляется в engine.rs и продевается в ключ, корректность держит wholesale clear() - dto.rs: набор ролей уже 20 — комментарий больше не подаёт «10 → 20» как будущее; issue #59 (акцентная лестница) — отдельное будущее изменение - benches/resolve_set.rs: хроматика больше не «падает в полный live-solver ~1 ms» — chromafast влит (memo), в steady state бенч меряет O(1)-путь ~0.8–1.2 µs; стейл «12 ролей» убран Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y8JyGJvXX872YqYrGa1xM * docs(pkg): TranslucentRole в union + справочник loadConfig/recheckContrast/muddiness/confidence H1: union RoleResult потерял TranslucentRole — реально 4 состояния (lib.rs:100). Добавлено описание TranslucentRole (поля из TS-секции lib.rs:82-98) и «одно из четырёх состояний». L2: справочник API дополнен методами движка, сверенными с TS-секцией lib.rs — loadConfig(json)→16-hex отпечаток (главная фича конфиг-границы), recheckContrast(bgHex,fgHexes,theme)→Float64Array (примитив adaptTheme, adapt-theme.d.ts:7), muddiness(hex)→number, confidence(hex)→number. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y8JyGJvXX872YqYrGa1xM * docs(readme): фиксы по фактическим значениям и структуре гейтов - M1: комментарий sample_hex(13) — реальный вывод, пересчитан кодом (["#FFFFFF", "#F8F8FA", "#E7E8EC", ..., "#101012"]) - M2: mermaid dark #101012 — J' ≈ 9.1 (light.at(1.0).jp = 9.05), было ≈ 4 - L1: «пять гейтов» → четыре CI-джоба (lint/test/audit/wasm); npm typecheck/ test — блокирующий шаг внутри wasm-джоба (ci.yml) - M6: «акцентная лестница как рецепт (#59)» — рецепт ladder влит и поглотил GAP #59 (config.rs:26); открытое = PR-c (ломающая чистка ядра) + поезд labui - заметки: снята временная формулировка «сегодня они держат»; карта помечена как продакшн-модули (тест-модули golden_tests.rs/config/tests.rs вне карты) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y8JyGJvXX872YqYrGa1xM * docs(adr): сняты внутренние противоречия под статусом «внедрено» M3: клеймы, устаревшие после мержа #121/#122: - S_PERC_MIN уже пересчитывается из конфиг-якорей (тест s_perc_min_recomputed_from_config_anchors_matches_frozen), не «БУДЕТ» - ladder влит (RoleSpec::Ladder, crate::ladder) и поглотил GAP #59 — больше не «единственный рецепт без прототипа» - IC_DECORATIVE_FLOOR_MIN конфигом НЕ экспонирован (сверено с TS ThemeConfig) — убран из списка экспонируемых ручек, помечен как физика ядра - «Путь миграции»: PR-a/PR-b помечены [сделано, #121/#122], поезд/PR-c [открыто]; имена API в выполненных шагах синхронизированы M4: эмиссия oklch, не rgba — движок эмитит oklch(L% C H / α); несуществующий Resolved::Rgba → Resolved::Translucent (semantic.rs:1122). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y8JyGJvXX872YqYrGa1xM * docs(wasm): свип rgba→oklch на границе эмиссии + четыре исхода Только комментарии/doc-строки, поведение не тронуто. WASM — граница CSS- эмиссии, где заявляется эмитируемая форма: - M4: dto.rs, engine.rs, lib.rs — «emission is rgba(tint, alpha)» / «наружу уходит rgba» / «rgba-роли лестницы» → oklch(L% C H / A); поле tint_hex «CSS rgba() carries» → «oklch(… / A) carries» - M5: dto.rs шапка — «#59 accent ladder (a new resolved shape)» противоречил RoleOutcome::Translucent, уже присутствующему ниже; переформулировано: набор уже 20, лестница #59 добавила Translucent, дальнейшие исходы проходят так же - dto.rs: «three honest outcomes» → four (RoleOutcome несёт 4 варианта: Color/None/Translucent/Unreachable) Ядро (semantic.rs/config.rs) не тронуто: там «rgba» — внутреннее имя пути резолва (согласовано с функциями resolve_rgba_*), а не CSS-клейм. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y8JyGJvXX872YqYrGa1xM * docs(adr): нормативные секции сверены с кодом (раунд-3, потребительский FAIL) Автор конфига больше не вводится в заблуждение — каждый клейм по коду: - Приложение A: таблица позиций 15 → 25 (LadderPosition::ALL, ladder.rs:214-240); добавлены NeutralFill×4, NeutralBorder×2, Shadow×4 с точными пер-темными α (alpha_pair, ladder.rs:277-291) - «SkeletonBase — единственная пер-темная альфа» неверно: пер-темные несут скелетон + нейтральная рампа + тени (11 позиций); переписано - brand.anchor «один hex» → пер-темная четвёрка ThemeAnchors {light,dark, light_ic,dark_ic} (config.rs:295; согласовано с :190) - VC-пресеты snake «srgb_ic|dim_ic» → kebab «srgb-ic|dim-ic» (config_dto.rs:92; snake давал бы invalid_config) - меню рецептов 5 → 6: добавлен decorative-lc (config.rs:446); kinds — kebab - neutral(pick) 3 → 5: добавлены Edge, Inverted (config.rs:508) - fx-focus-ring-neutral/fx-glow-inverted заземлены пиками Edge/Inverted (config/tests.rs:1237-1242) - тени: «переход на альфа-якоря вне поезда» устарело — уже полупрозрачные ladder-позиции (config/tests.rs:1286-1295) - двойной enum тем «унифицируется» → унифицирован (theme.rs:15) - golden semantic.rs:3352-3655 → тест resolve_set_golden_hex_is_byte_for_byte_stable, const GOLDEN ≈3785 - Значенческая сверка: «rgba(тинт, α)» — внутренний формат core-теста против rgb-стаба, НЕ CSS-эмиссия (та oklch); уточнено Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y8JyGJvXX872YqYrGa1xM * docs: дешёвые фиксы читаемости README и пакета (раунд-3) README: - UCS-формулы: реализация lcs.rs → формулы в spaces/cam16.rs (ucs_j/ucs_m), lcs.rs их применяет - сквозная нумерация: сняты орфанные «2.»/«3.» с ### Кривая/### Контраст (перед ними шёл ### ViewingConditions без номера) - глоссы для дизайнера при первом употреблении: JND (минимально заметная глазу разница), C1-непрерывность (гладкая, без изломов производной) packages/colors: - убрана колонка brotli из таблицы размеров (CI меряет только raw+gzip, ci.yml:245-252), добавлена честная строка провенанса - добавлена опция root в WatchThemeOptions (watch-theme.d.ts:23, Node) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y8JyGJvXX872YqYrGa1xM * docs(adr): точный указатель ThemeAnchors — определён в ladder.rs Финдинг закрывающего верификатора раунда-3 (recommended minor): тип определён в ladder.rs:56, config.rs лишь использует его полем Brand.anchors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y8JyGJvXX872YqYrGa1xM --------- Co-authored-by: Claude Code <daniilerosov12@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Зачем
PR-a пути миграции ADR-0001 (эпик ds-config-train, CH-02 t1+t2): движок получает конфиг-вход; вся эмиссия labui-контракта выводима из конфига. Поглощает issue #59 (акцентная эмиссия). WASM/fingerprint — PR-b.
Состав
Заземление и честность
Верификация
16 сьютов зелёные (275 lib-тестов), golden/240 байт-в-байт/empirical_inventory/s2b-гард целы; clippy -D warnings чист. Изолированные ревью финального состояния: арх-ось PASS_WITH_NOTES (внесено: panic-гард в r3-golden против будущих вариантов, сухие комментарии, долг t3), CoVe-аудит 5/5 SUPPORTED. RED-proof: мутации позиции/семейства/альфы/якоря — все RED.
Коллизия с локом s2b_baseline_guards (1 match-арм в r3) — флагнута владельцу зоны отдельной задачей, байт-дрейфа эмиссии нет.
Summary by CodeRabbit