diff --git a/README.md b/README.md index 082d51aa..42bbc7ad 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ Контекстный компилятор цветовых токенов для дизайн-систем. +`LCS` означает **Labpics Colors Space**, `LPC` — **Labpics Perceptual Contrast**. +Это собственные концепции Labpics; текущие CAM16/Oklab/APCA-shaped компоненты не +являются их полным определением и не доказывают перцептуальное превосходство. + Lab Colors принимает конфиг клиента, компилирует его в `NamedRoleTable` и решает всю таблицу для одного локального фона и темы. Зависимые роли, уже представленные специальными рецептами, используют фактически полученные композиты. Браузерные помощники применяют результат, перепроверяют его при изменении окружения и при необходимости запускают новый resolve. ```text @@ -225,7 +229,7 @@ anchors - Нормативный floor применяется только там, где его требует контракт клиента или компонента. - Core не определяет размер текста, essentialness, disabled/decorative status по имени роли. -- Экспериментальный LPC/APCA-shaped или appearance-результат не меняет WCAG pass/fail. +- Экспериментальный компонент формы APCA текущего LPC или результат модели внешнего вида не меняет WCAG pass/fail. - Для финальной пары sRGB8 новый `wcag22-srgb8-contrast-v1` принимает явно объявленный критерий и возвращает строгий `Pass | Fail`; профиль, Q55-артефакт и full-domain proof входят в релиз. - Старое поле `wcagRatio` остаётся compatibility-диагностикой текущего resolver/runtime и не может автоматически рекламироваться как результат нового evaluator-а. - Цвет не должен быть единственным носителем смысла; текст, иконка и форма принадлежат компоненту. diff --git a/crates/labcolors-core/README.md b/crates/labcolors-core/README.md index 8263da41..1b049af8 100644 --- a/crates/labcolors-core/README.md +++ b/crates/labcolors-core/README.md @@ -72,9 +72,9 @@ offline-операции. Все поверхности используют о вычислителя или компилятора возвращаются типизированными ошибками; частичного или запасного результата нет. -Полные проверки формул `C×E`, случая `E=0`, повторов ID, ресурсных отказов и -запрета частичного результата перечислены в разделе «Конечная компиляция -выполнимости WCAG 2.2» [карты верификации](../../docs/verification-map.md). +Формулы `C×E`, случай `E=0`, повторы ID, ресурсные отказы и запрет частичного +результата исполняются непосредственно в `src/wcag22_feasibility_tests.rs`, +`tests/wcag22_feasibility.rs` и `tests/wcag22_explicit_feasibility.rs`. ```rust # #[cfg(feature = "wcag22-feasibility")] diff --git a/crates/labcolors-core/src/golden_tests.rs b/crates/labcolors-core/src/golden_tests.rs index 31e3c308..1f7a1eec 100644 --- a/crates/labcolors-core/src/golden_tests.rs +++ b/crates/labcolors-core/src/golden_tests.rs @@ -222,9 +222,9 @@ fn cam16_matches_colour_science_dim_surround() { /// console.log(APCAcontrast(y(0),y(255)));" // 106.04066682868873 /// ``` /// -/// Grey-on-grey isolates the Helmholtz-Kohlrausch term out of the metric -/// (luminance is fed directly), so this validates the curve alone. -/// Константы: APCA SAPC-8 версии 0.0.98G-4g; метрика называется LPC, не APCA. +/// Grey-on-grey bypasses the Helmholtz-Kohlrausch path because luminance is fed +/// directly. This validates the frozen candidate curve arithmetic only; it does +/// not validate APCA conformance, complete LPC or readability. type ContrastGolden = (f64, f64, f64); const ACHROMATIC_CONTRAST: [ContrastGolden; 13] = [ (0.0, 1.0, 106.04066682868873), // #000000 on #ffffff (BoW max) diff --git a/crates/labcolors-core/src/lcs.rs b/crates/labcolors-core/src/lcs.rs index 119d9300..5a9e3121 100644 --- a/crates/labcolors-core/src/lcs.rs +++ b/crates/labcolors-core/src/lcs.rs @@ -1,3 +1,8 @@ +//! Current point representation used while **Labpics Colors Space (LCS)** is +//! being reduced to one context-bound coordinate contract. The stored +//! CAM16-UCS/Oklab views are implementation inputs, not independent editable +//! definitions of LCS and not a claim of uniform perceptual attributes. + use crate::spaces::srgb::{hex_from_srgb, srgb_from_hex, srgb_to_xyz, xyz_to_srgb}; use crate::spaces::{cam16, cat16, oklab, vc::ViewingConditions}; @@ -9,8 +14,9 @@ pub struct LcsColor { pub h_ok: f64, /// Internal reparameterisation of CAM16-UCS colourfulness `M′`: /// `s = M′ / (J′ + 1)`. The `+ 1` is a regulariser against division by zero - /// as `J′ → 0`; it is lossless — `LcsColor::mp` recovers `M′` exactly as - /// `s · (J′ + 1)`. This is NOT the CAM16 saturation correlate. + /// as `J′ → 0`; `LcsColor::mp` applies the analytical inverse + /// `s · (J′ + 1)`, subject to ordinary binary64 round-off. This is NOT the + /// CAM16 saturation correlate. pub s: f64, h_cam: f64, } @@ -23,9 +29,9 @@ impl LcsColor { /// Parse from hex using the given viewing conditions. /// - /// The resulting J', saturation, and CAM16 hue reflect perception under - /// the provided VC (e.g. [`ViewingConditions::dim_surround`] for dark - /// themes). + /// The stored CAM16-UCS/Oklab coordinates are evaluated under the provided + /// VC (e.g. [`ViewingConditions::dim_surround`] for dark themes). They are + /// implementation inputs, not universal perceptual-attribute scales. pub fn from_hex_with_vc(hex: &str, vc: &ViewingConditions) -> Result { let rgb = srgb_from_hex(hex)?; let xyz = srgb_to_xyz(rgb); @@ -54,8 +60,8 @@ impl LcsColor { Self { jp, h_ok, s, h_cam } } - /// CAM16-UCS colourfulness `M'`, recovered losslessly from the stored - /// reparameterisation (see the `s` field doc). + /// CAM16-UCS colourfulness correlate `M'`, recovered through the analytical + /// inverse of the stored reparameterisation (see the `s` field doc). pub(crate) fn mp(&self) -> f64 { self.s * (self.jp + 1.0) } @@ -92,9 +98,11 @@ impl LcsColor { /// caller that already ran [`cam16::forward`] (e.g. [`crate::solve`]'s /// `finish`) reuses that result instead of recomputing it. pub(crate) fn from_cam16(j: f64, m: f64, h_cam: f64, h_ok: f64) -> Self { - // CAM16-UCS rescaling (Li et al. 2017, DOI 10.1002/col.22131): maps raw - // CIECAM16 J/M onto perceptually uniform J'/M' (J'=50 reads as - // half-lightness). Inverse in `to_xyz` via the same helpers. + // CAM16-UCS rescaling (Li et al. 2017, DOI 10.1002/col.22131). This is + // an analytically invertible coordinate transform used for + // colour-difference work; binary64 round-off is covered by the shared + // tolerance tests. No individual J'/M' value is assigned a universal + // attribute meaning here. Inverse in `to_xyz` uses the same helpers. let jp = cam16::ucs_j(j); let mp = cam16::ucs_m(m); let s = mp / (jp + 1.0); diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index d4629558..a5805430 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -77,11 +77,11 @@ mod pair_label_tests; #[cfg(test)] mod r3_byte_identity_tests; -// External published reference vectors for the deepest colour-science layers -// (sRGB EOTF & matrices, Ottosson Oklab, CAT16/CIECAM16 adapt, Hellwig-2022 H-K, -// WCAG linearise). These reach `pub(crate)` transforms an integration test in -// `tests/` cannot see; the public-API-reachable vectors live in -// `tests/reference_vectors.rs`. See `docs/verification-map.md`. +// Reference checks for the deepest colour-science layers (sRGB EOTF & matrices, +// Ottosson Oklab, CAT16/CIECAM16 adapt, Hellwig-2022 H-K, WCAG linearise). These +// reach `pub(crate)` transforms an integration test in `tests/` cannot see; the +// public-API-reachable checks live in `tests/reference_vectors.rs`, with source +// and oracle scope beside each test. #[cfg(test)] mod reference_vectors_deep; diff --git a/crates/labcolors-core/src/lpc.rs b/crates/labcolors-core/src/lpc.rs index 4bef63df..d3dd256b 100644 --- a/crates/labcolors-core/src/lpc.rs +++ b/crates/labcolors-core/src/lpc.rs @@ -1,3 +1,10 @@ +//! Candidate components of **Labpics Perceptual Contrast (LPC)**. +//! +//! The current APCA-shaped and H-K paths are characterized implementation +//! components, not the complete LPC definition and not evidence that LPC +//! outperforms APCA. Readability admission is owned by a versioned evaluator +//! profile with declared typography, observer and context applicability. + use crate::spaces::srgb::{D65_WHITE, srgb_from_hex, srgb_to_xyz}; use crate::spaces::{cam16, cat16, vc::ViewingConditions}; @@ -174,15 +181,11 @@ fn y_hk_bisect(j_hk: f64, vc: &ViewingConditions) -> f64 { (lo + hi) * 0.5 } -// Канонические константы перцептивного контраста из опубликованной формулы -// версии 0.0.98G-4g («4g»-набор SAPC-8). Имена в комментариях воспроизводят -// исходные идентификаторы, чтобы маппинг был аудируемым. -// -// Правовая позиция: Copyright (17 U.S.C. § 102(b)) не охраняет формулы и константы, -// только конкретное кодовое выражение. Данная реализация написана независимо; -// файлы репозиториев Myndex не копировались. Метрика называется LPC — не APCA, -// не APCA-совместима и не одобрена Myndex Research или Andrew Somers. -// Товарный знак «APCA» в публичных API-символах и названии метрики не используется. +// Константы candidate-кривой транскрибированы из опубликованного набора +// SAPC-8 0.0.98G-4g. Имена в комментариях воспроизводят исходные +// идентификаторы, чтобы маппинг был аудируемым. Эта транскрипция сама по себе не +// является APCA conformance, complete LPC или evidence читаемости; комментарий +// также не делает правового вывода о допустимости дальнейшего распространения. // // Это ЕДИНСТВЕННЫЙ ИСТОЧНИК ИСТИНЫ для кривой контраста: и прямой `contrast_core`, // и обратный решатель (`crate::solve`) читают значения здесь. @@ -308,27 +311,18 @@ pub(crate) fn soft_clamp_inv(clamped: f64) -> Option { Some(y) } -/// Perceptual-contrast core curve (asymmetric power contrast on luminance). +/// Candidate asymmetric power curve over a luminance-shaped scalar. /// -/// Faithful port of the published generic perceptual-contrast math — soft -/// black clamp, polarity-dependent power exponents, the minimum-luminance -/// gate, the low-contrast clip, and the polarity offsets. Fed the *same* input -/// luminance, the curve reproduces the reference; the absolute numbers agree -/// with the published APCA only at the endpoints (Y = 0 and Y = 1, e.g. black -/// on white ≈ `106.04`). For interior greys the luminance fed here is -/// `Y_hk`, not the reference's `Ys`, so LPC departs from the published APCA -/// on those: measured against `apca-w3` on the 8-bit grey axis the departure -/// stays within ~2.3 Lc (grey-on-grey pairs ≤ ~0.6 Lc, endpoints exact). On -/// near-neutrals the H-K term itself is ≈0 (M ≲ 1), so the interior departure -/// is dominated by the CAM16 lightness reconstruction inside `Y_hk`. A -/// deliberate, declared difference of the metric (the same `Y_hk` substitution -/// that makes LPC diverge from the reference on chromatic colours), not a -/// porting error. +/// The branches and constants mirror the frozen SAPC-8 0.0.98G-4g candidate: +/// soft black clamp, polarity-dependent exponents, a minimum-luminance gate, +/// low-contrast clipping and polarity offsets. Current call sites feed more +/// than one luminance definition, including an H-K/CAM16-derived scalar. That +/// composition is characterized implementation behavior, not APCA conformance, +/// complete LPC or evidence of glyph readability. /// -/// Константы: формула APCA SAPC-8 версии 0.0.98G-4g; метрика называется LPC, -/// не APCA, не одобрена Myndex Research. The achromatic alignment is locked by -/// `golden_tests::contrast_core_matches_reference_on_grey_axis`. The curve is -/// inverted by `crate::solve` to recover a foreground luminance from a target. +/// `golden_tests::contrast_core_matches_reference_on_grey_axis` pins only the +/// scalar curve arithmetic. The curve is inverted by `crate::solve` to recover +/// a foreground scalar from a target. pub(crate) fn contrast_core(y_fg: f64, y_bg: f64) -> f64 { let fg = soft_clamp(y_fg); let bg = soft_clamp(y_bg); diff --git a/crates/labcolors-core/src/pair_label_tests.rs b/crates/labcolors-core/src/pair_label_tests.rs index 55129ec9..fcc2246b 100644 --- a/crates/labcolors-core/src/pair_label_tests.rs +++ b/crates/labcolors-core/src/pair_label_tests.rs @@ -279,8 +279,9 @@ fn pair_label_spec( /// отгруженным фоном светлых labui-тем (классы совпадают в этой дизайн- /// системе по факту фикстуры); /// * `#101012` — отгруженный фон тёмных labui-тем (фикстура); -/// * `#767676` — опубликованная WCAG-граница серого (≈4.54:1 к белому, -/// см. `docs/verification-map.md`); +/// * `#767676` — опубликованная WCAG-граница серого (≈4.54:1 к белому), +/// напрямую закреплённая +/// `tests/reference_vectors.rs::wcag_published_ratios_via_public_api`; /// * `#FFF4E0` — хроматический светлый witness: точная warning-поверхность /// из graph-тестов (`appearance_graph_tests`); /// * `#0000FF` — насыщенный хроматический угол куба (sRGB primary). diff --git a/crates/labcolors-core/src/reference_vectors_deep.rs b/crates/labcolors-core/src/reference_vectors_deep.rs index d047dad8..e0eae2ea 100644 --- a/crates/labcolors-core/src/reference_vectors_deep.rs +++ b/crates/labcolors-core/src/reference_vectors_deep.rs @@ -1,10 +1,11 @@ -//! External published reference vectors for the deepest colour-science layers. +//! Reference checks for the deepest colour-science layers. //! -//! These pin the crate's transforms to CONTROL POINTS AND VECTORS PUBLISHED IN -//! STANDARDS / PEER-REVIEWED SOURCES, not to the crate's own output. They live -//! in-crate (not `tests/`) because the transforms they touch are `pub(crate)` -//! and invisible to an integration test. Public-API-reachable vectors are in -//! `tests/reference_vectors.rs`; the full map is `docs/verification-map.md`. +//! The checks combine published control points, independent transcriptions and +//! explicit identities. They live in-crate (not `tests/`) because the transforms +//! they touch are `pub(crate)` and invisible to an integration test. +//! Public-API-reachable checks are in `tests/reference_vectors.rs`; each test +//! below carries its own source and applicability boundary beside the assertion +//! it protects. //! //! Sources cited per test: //! * IEC 61966-2-1:1999 — sRGB EOTF/OETF and primaries; also W3C CSS Color 4. @@ -297,8 +298,8 @@ fn cam16_ucs_constants() { "ucs_m not invertible at {m}" ); } - // J'=50 reads as half-lightness only if the 1.7/0.007 pair is intact: - // published sanity value ucs_j(43.30..) ≈ 55.6 is a monotone lift, not 1:1. + // The rescale is not the identity: J=50 maps above 50. This checks only the + // published coordinate transform, not a human meaning for either number. assert!(ucs_j(50.0) > 50.0, "UCS lightness lift must raise J"); } diff --git a/crates/labcolors-core/src/semantic.rs b/crates/labcolors-core/src/semantic.rs index 029d3846..67ef4a8f 100644 --- a/crates/labcolors-core/src/semantic.rs +++ b/crates/labcolors-core/src/semantic.rs @@ -130,11 +130,11 @@ //! The default undertone policy is [`RoleChroma::Curve`] (v2), derived from three //! computable mechanisms rather than a flat ratio of the gamut: //! -//! 1. **Constant perceptual colorfulness** — the chroma at each role's resolved -//! lightness is solved to a *constant* CAM16-UCS `M'` (`TINT_TARGET_MP`), not -//! a fixed fraction of the gamut maximum. Because UCS is perceptually uniform, -//! one constant holds the chroma in the lights and moderates it in the middle — -//! fixing v1's inverted envelope (over-saturated middle, starved light end). +//! 1. **Constant CAM16-UCS coordinate** — the chroma at each role's resolved +//! lightness is solved to a constant `M'` (`TINT_TARGET_MP`), not a fixed +//! fraction of the gamut maximum. This is a characterized design policy that +//! holds chroma in the lights and moderates it in the middle; it does not turn +//! `M'` into a universal perceptual-colorfulness scale. //! 2. **Cusp-attracted hue** — the hue at each lightness is pulled toward the //! local chroma cusp of the sRGB gamut, penalised for leaving the canonical //! 286° (`cusp_attracted_hue`). The drift emerges from geometry; it is *not* diff --git a/crates/labcolors-core/src/spaces/cam16.rs b/crates/labcolors-core/src/spaces/cam16.rs index 9f2004b9..ee5891cd 100644 --- a/crates/labcolors-core/src/spaces/cam16.rs +++ b/crates/labcolors-core/src/spaces/cam16.rs @@ -241,9 +241,12 @@ fn forward_compute(xyz: [f64; 3], vc: &ViewingConditions) -> (f64, f64, f64) { // // J' = 1.7·J / (1 + 0.007·J), M' = ln(1 + 0.0228·M) / 0.0228. // -// Maps raw CIECAM16 J/M onto perceptually uniform J'/M' (J'=50 reads as -// half-lightness). These four helpers are the SINGLE SOURCE OF TRUTH for the -// rescale: `lcs` stores J'/M', `lpc` decompresses back to raw J/M, and the +// These four helpers are the SINGLE SOURCE OF TRUTH for the CAM16-UCS +// coordinate rescale. The forward and inverse formulae are analytically mutual +// inverses; binary64 round-trips are validated within the `1e-12` tolerance in +// `ucs_rescale_round_trips`, not claimed bit-exact. They do not assign universal +// perceptual-attribute meaning to an individual J'/M' value: `lcs` stores the +// coordinates, `lpc` transforms them back to raw J/M, and the // constants (`1.7`, `0.007`, `0.0228`) must never be re-typed inline anywhere // else (previously duplicated across `lcs::from_xyz_with_hok`, `lcs::to_xyz`, // and `lpc::y_hk_from_lcs`). diff --git a/crates/labcolors-core/tests/reference_vectors.rs b/crates/labcolors-core/tests/reference_vectors.rs index 6ad3f3ac..02324e2d 100644 --- a/crates/labcolors-core/tests/reference_vectors.rs +++ b/crates/labcolors-core/tests/reference_vectors.rs @@ -1,9 +1,10 @@ -//! External published reference vectors reachable through the PUBLIC API. +//! Reference checks reachable through the PUBLIC API. //! //! Companion to the crate-internal `reference_vectors_deep` (which reaches -//! `pub(crate)` transforms). Every vector here is a control point or worked -//! value from a STANDARD or PEER-REVIEWED SOURCE, asserted end-to-end through -//! the shipped surface. Full map: `docs/verification-map.md`. +//! `pub(crate)` transforms). The checks combine published control points, +//! independently transcribed formulae and explicit cross-boundary identities. +//! Each assertion owns its source and oracle boundary; crate-private companion +//! checks live in `src/reference_vectors_deep.rs`. //! //! Sources: //! * W3C WCAG 2.1 §1.4.3 / §1.4.11 — relative luminance & contrast ratio. @@ -262,7 +263,8 @@ fn dim_surround_shifts_lpc() { // (byte-exact round-trip, proven for the core by `oklch::round_trip_is_byte_exact`). // This file OWNS the seed set; the committed fixture is the artifact the JS test // reads; `oklch_core_vectors_fixture_is_fresh` keeps it in lock-step with the -// live emitter. See `docs/verification-map.md`. +// live emitter, while `packages/colors/test/reference-vectors.test.mjs` +// independently decodes the committed strings back to the seed bytes. // ═════════════════════════════════════════════════════════════════════════════ const FIXTURE_REL: &str = "/../../packages/colors/test/data/oklch-core-vectors.txt"; diff --git a/crates/labcolors-wasm/src/lib.rs b/crates/labcolors-wasm/src/lib.rs index e2d69df4..e4fcbc0a 100644 --- a/crates/labcolors-wasm/src/lib.rs +++ b/crates/labcolors-wasm/src/lib.rs @@ -732,7 +732,7 @@ impl LabColors { /// foregrounds against every sample of a varying backdrop (gradient / image / /// bg-blur / glass); the dominant per-foreground CAM16 forward is background- /// independent, so this shares it across all samples instead of recomputing it - /// per `recheckContrast` call — a measured ~2.6x on the multi-sample recheck. + /// per `recheckContrast` call. /// /// Returns a flat, background-major `Float64Array`: sample `s`, foreground `i` /// is at `(s * fgHexes.length + i) * 2` (`lc`) and `+1` (`wcagRatio`). The diff --git a/docs/decisions/0003-hk-scope.md b/docs/decisions/0003-hk-scope.md index 1dd85bed..9bce324c 100644 --- a/docs/decisions/0003-hk-scope.md +++ b/docs/decisions/0003-hk-scope.md @@ -87,11 +87,12 @@ H-K остаётся там, где он про яркость (свечение ### 2. APCA/SAPC определён в домене экранной яркости `Ys`, без поправки H-K -APCA (метрика, которую движок реализует независимо под именем **LPC** — так же, -как WCAG определяет *relative luminance*) вычисляет контраст целиком в домене -**экранной яркости `Ys`**: линеаризация sRGB + взвешивание коэффициентами яркости. -Все эмпирические константы работают в этом домене; поправка Гельмгольца–Кольрауша -в алгоритме **не применяется**. +Опубликованная APCA-формула вычисляет контраст в собственном домене `Ys`: +возведение каналов в степень и взвешивание коэффициентами яркости. Текущий Lab Colors +независимо использует APCA-shaped curve как один кандидат-компонент **Labpics +Perceptual Contrast (LPC)**, но подаёт в путь читаемости другую величину яркости +из старого WCAG; это не APCA и не полное определение LPC. Поправка +Гельмгольца–Кольрауша в APCA **не применяется**. - Исходный код Myndex/apca-w3 (`src/apca-w3.js`): вход — `sRGBtoY` (*«linearize r, g, b then apply coefficients and sum, return luminance»*, коэффициенты @@ -110,11 +111,11 @@ APCA (метрика, которую движок реализует незав домене `Ys`», а не делаем заявлений о деталях калибровки, которых нет в открытом доступе.) -### 3. Эффект Гельмгольца–Кольрауша — это про яркость (brightness), не про читаемость +### 3. Эффект Гельмгольца–Кольрауша — это про воспринимаемую яркость, не про читаемость H-K — перцептивный феномен **воспринимаемой яркости**: насыщенные цвета кажутся ярче, чем предсказывает их фотометрическая luminance. Это модель яркости/светлоты -(appearance), а не модель luminance и не модель разборчивости. +(внешнего вида), а не модель фотометрической яркости и не модель разборчивости. - Hellwig L., Stolitzka D., Fairchild M. (2022), *Extending CIECAM02 and CAM16 for the Helmholtz–Kohlrausch effect*, **Color Research & Application 47(5):1096–1104, @@ -123,14 +124,14 @@ H-K — перцептивный феномен **воспринимаемой реализация, что стоит в `lpc::hk_coeff` / `j_hk_from_xyz` (`J_HK = J + f(h)·C^0.587`). Оговорка: в бытовых источниках H-K слово «luminance» иногда означает *воспринимаемую -яркость*; терминологически строго H-K описывает расхождение **brightness** (перцепт) -и **luminance** (фотометрия). Это подчёркивает вывод: домен H-K (brightness) и домен -APCA (`Ys`/legibility) — разные, смешивать нельзя. +яркость*; терминологически строго H-K описывает расхождение **воспринимаемой +яркости** и **фотометрической яркости**. Это подчёркивает вывод: домен H-K и +домен читаемости APCA (`Ys`) — разные, смешивать нельзя. ### Вывод обоснования Три домена разделяются корректно: **читаемость живёт в `Ys` (люминансный -контраст); Гельмгольц–Кольрауш живёт в домене brightness (appearance)**. Ось +контраст); Гельмгольц–Кольрауш живёт в домене воспринимаемой яркости**. Ось читаемости обязана читать `Ys`. H-K уместен на осях, которые задают воспринимаемую яркость, — и вреден на оси, которая решает, читается ли текст. diff --git a/docs/verification-map.md b/docs/verification-map.md deleted file mode 100644 index 525b0950..00000000 --- a/docs/verification-map.md +++ /dev/null @@ -1,223 +0,0 @@ -# Карта верификации нижних слоёв - -Каждая формула цветовых пространств и перцептивных метрик ядра `labcolors-core` -(и JS-дубликат в `packages/colors/effective-bg.js`) — против ВНЕШНЕГО -опубликованного эталона. Столбец «чем верифицирована» называет конкретный тест -и его оракул. `[NEW]` — добавлено веткой `test/reference-vectors` (внешние -опубликованные векторы); остальное существовало ранее. - -Оракулы бывают трёх сортов: -- **публикация** — контрольные точки/векторы прямо из стандарта или статьи - (IEC 61966-2-1, W3C WCAG 2.1 / CSS Color 4, Ottosson 2020, Li et al. 2017, - Hellwig et al. 2022, APCA SAPC-8); -- **эталонный софт** — `colour-science` (Python), `apca-w3` (npm) — сам - валидирован против стандартов CIE; -- **внутренняя тождественность** — round-trip / bit-identity / анти-дрейф - (не внешний эталон, но ловит регрессию математики). - -## sRGB — `crates/labcolors-core/src/spaces/srgb.rs` - -| формула | чем верифицирована | оракул | -|---|---|---| -| `srgb_gamma_inv` / `srgb_gamma` (EOTF/OETF IEC 61966-2-1 §6.4): стыки 0.04045 / 0.0031308, наклон 12.92, γ=2.4 | `[NEW]` `reference_vectors_deep::srgb_transfer_iec_control_points` + `..._join_is_continuous` + `..._css_color4_sample` | публикация (IEC 61966-2-1; CSS Color 4 sample decode(0.5)=0.214041) | -| `DECODE_8BIT` (точный 8-бит декод) | `srgb::tests::decode_table_matches_live_math`, `decode_reproduces_legacy_powf_path_for_every_byte` | внутренняя тождественность | -| `srgb_from_hex` / `hex_from_srgb` (квантизация 8-бит) | `srgb::tests::hex_round_trip_is_identity_for_all_grey_codes`; `oklch::tests::round_trip_is_byte_exact_on_lattice` | внутренняя тождественность | -| `SRGB_TO_XYZ_D65` / `XYZ_D65_TO_SRGB` (матрицы CSS Color 4) | `[NEW]` `reference_vectors_deep::srgb_xyz_matrices_are_mutual_inverses` + `..._white_maps_to_d65` | публикация (W3C CSS Color 4 / IEC 61966-2-1) | -| `D65_WHITE` (из хроматичности 0.3127/0.3290) | `[NEW]` `reference_vectors_deep::d65_white_derives_from_chromaticity` | публикация (IEC 61966-2-1 / CSS Color 4) | - -## Oklab / OKLCH — `spaces/oklab.rs`, `spaces/oklch.rs` - -| формула | чем верифицирована | оракул | -|---|---|---| -| `SRGB_TO_LMS` … `LMS_TO_SRGB` (матрицы Ottosson 2021-01-25) → `srgb_linear_to_oklab` | `[NEW]` `reference_vectors_deep::oklab_matches_ottosson_xyz_table` (4 строки XYZ→Lab из поста Ottosson) | публикация (Ottosson 2020, таблица XYZ→Oklab) | -| Белая точка D65 → `L=1, a=0, b=0` | `oklab::tests::white_gives_l1_a0_b0`; `[NEW]` `reference_vectors::oklab_white_is_l1_c0` (публичный `oklch_from_hex`) | публикация (Ottosson design-constraint / XYZ-table строка 1) | -| `oklab_to_srgb_linear` (обратный путь) | `oklab::tests::roundtrip_five_colors` | внутренняя тождественность | -| `oklab_hue` / полярная форма OKLCH | `oklab::tests::hue_returns_degrees_0_360`; `[NEW]` `reference_vectors::oklch_primary_hues` (красный≈29°, зелёный≈142°, синий≈264°) | публикация (Ottosson/CSS Color 4 канонические углы) | -| `oklch_css_from_hex` (эмиссия), байт-точность | `oklch::tests::round_trip_is_byte_exact_on_lattice/_greys`; PR #149/#150 гетеро-оракулы | внутренняя тождественность | - -## CAT16 / CIECAM16 — `spaces/cat16.rs`, `spaces/cam16.rs`, `spaces/vc.rs` - -| формула | чем верифицирована | оракул | -|---|---|---| -| `XYZ_TO_CONE` / `CONE_TO_XYZ` (CAT16, Li et al. 2017) | `[NEW]` `reference_vectors_deep::cat16_printed_inverse_residual` (‖M·M⁻¹−I‖≈5.4e-9); транзитивно golden CAM16 | публикация (Li et al. 2017, печатная обратная матрица) | -| `adapt` / `unadapt` (пост-адаптационное сжатие, Li et al. 2017) | `[NEW]` `reference_vectors_deep::cam16_adapt_matches_published_closed_form` + `..._adapt_unadapt_round_trip` | публикация (Li et al. 2017 / CIE 248:2022) | -| `forward` (CIECAM16 `XYZ→J,M,h`) | `golden_tests::cam16_matches_colour_science_{average,dim}_surround` (24 вектора) | эталонный софт (colour-science) | -| `ucs_j`/`ucs_m` и обратные (CAM16-UCS, Li et al. 2017) | `cam16::tests::ucs_rescale_round_trips`; `[NEW]` `reference_vectors_deep::cam16_ucs_constants` | публикация + внутренняя тождественность | -| `ViewingConditions::build` (`F_L, n, z, N_bb, A_w, D, RGB_D`) | `[NEW]` `reference_vectors::cam16_viewing_conditions_derivation` (независимая транскрипция CIE 248:2022 vs публичные поля) | публикация (Li et al. 2017 / CIE 248:2022) | - -## Helmholtz–Kohlrausch — `lpc.rs` - -| формула | чем верифицирована | оракул | -|---|---|---| -| `hk_coeff` — f(h) = −0.160cos h + 0.132cos 2h − 0.405sin h + 0.080sin 2h + 0.792 (**Hellwig et al. 2022**, DOI 10.1002/col.22793) | `[NEW]` `reference_vectors_deep::hk_coeff_matches_hellwig2022_published` (коэффициенты дословно из статьи/`colour-science`) | публикация (Hellwig 2022) | -| `J_HK = J + f(h)·C^0.587` | `lpc::tests::j_hk_matches_hellwig_reference` (12 якорей vs colour-science) | эталонный софт (colour-science 0.4.7) | -| Знак H-K (насыщ. синий поднят) | `lpc::tests::blue_on_white_below_achromatic`; `[NEW]` `reference_vectors::hk_lifts_saturated_blue_via_public_lpc` | публикация (эффект H-K) | - -> **Находка:** используется формула **Hellwig, Stolitzka & Fairchild (2022)**, -> НЕ Nayatani/VAC и НЕ Fairchild-1998. Подтверждено дословным сравнением -> коэффициентов с `colour-science` `hue_angle_dependency_Hellwig2022`. - -## WCAG 2.1 — `wcag.rs` - -| формула | чем верифицирована | оракул | -|---|---|---| -| `linearise` (порог 0.03928, /12.92, γ 2.4) | `[NEW]` `reference_vectors_deep::wcag_linearise_threshold_is_original_03928` | публикация (W3C WCAG 2.1 §1.4.3, оригинальная версия 2018) | -| `relative_luminance` (0.2126/0.7152/0.0722) | `[NEW]` `reference_vectors::wcag_luminance_coefficients_isolated` (primary-on-black изолирует каждый коэффициент) | публикация (W3C WCAG 2.1) | -| `contrast_ratio` (L↑+0.05)/(L↓+0.05) | `wcag::tests::black_on_white_is_twentyone_to_one`, `grey_boundary_matches_published_value` (#767676≈4.54); `[NEW]` `reference_vectors::wcag_published_ratios_via_public_api` | публикация (W3C WCAG 2.1) | - -> **Находка (зафиксирована в тесте):** порог `0.03928` — ОРИГИНАЛЬНАЯ версия -> WCAG 2.1 (2018). Erratum W3C от 2022-02-22 (PR #1780, вошёл в Рекомендацию -> 05.2025) поднял порог до `0.04045` (= стык IEC EOTF) — то есть текущий -> нормативный текст говорит `0.04045`, а `0.03928` устарел. lab-colors -> сознательно держит `0.03928`. Ни один 8-битный код не попадает в интервал -> (0.03928, 0.04045) — 10/255 ≈ 0.039216 ниже обоих, 11/255 ≈ 0.043137 выше — то -> есть для КАЖДОГО квантованного цвета обе версии выбирают одну ветвь и -> линеаризуют идентично; расхождение только на суб-квантовых величинах. - -## WCAG 2.2 для финальной sRGB8-пары — `wcag22.rs`, `wcag22/`, `srgb8.rs` (#284) - -Это версионированный терминальный сертификат соответствия явно выбранному -критерию, а не замена перцептивной цели решателя в форме LPC/APCA. Клиент сам -передаёт критерий; Core не выводит размер текста или семантику из имени токена. - -| формула/инвариант | чем верифицирована | оракул | -|---|---|---| -| профиль с зафиксированной датой: граница IEC/WCAG EOTF `0.04045`, веса `0.2126/0.7152/0.0722`, слагаемое `0.05`, пороги `3.0` и `4.5` | неизменяемый `wcag22-srgb8-v1.json`; независимая точная копия `NORMATIVE_PROFILE_V1` в `verify_wcag22_q55.py`; `wcag22_tests::*` | публикация (W3C WCAG 2.2 Recommendation 2024-12-12) | -| 768 наружу округлённых вкладов Q55 (3 канала × 256 кодов) имеют ширину не более 1; пороговые выражения не переполняются | `Decimal` с адаптивной точностью, направленным округлением и устойчивостью при повышении точности; целочисленная проверка и проверка пятой степени каждой строки; verifier доказывает `180·(Q55+3)+7·Q55 < i64::MAX`, фиксирует запас и отказ Q56 | независимая численная транскрипция + целочисленная проверка точности и переполнения | -| полный домен `256³ = 16 777 216` цветов не содержит неразрешённых случаев для обоих пороговых законов | `verify_wcag22_q55.py`: перечисление всех интервалов sRGB8 и монотонный поиск границ; зафиксированное доказательство хранит минимальные отступы pass/fail и граничные примеры; синтетическое пересечение обязано сделать verifier RED | полный конечный перебор + мутационный оракул | -| производственный вердикт использует только наружу округлённые Q55 и целочисленные сравнения; ядро, парсер, фасад и терминальное доказательство нельзя подменить независимо | точные SHA узких production-листьев (`kernel.rs`, terminal evidence), production-only капсулы парсера в `srgb8.rs` и нормализованного фасада (исключены только три self-digest literal) + семантические проверки; `anti_epsilon_witnesses_are_definite_fail`, тесты парсера на panic и свойства | независимый verifier + внутренние граничные примеры | -| доказательство связано с фактическим публичным путём, но не с посторонним текстом всего `lib.rs` или родительского `srgb8.rs` | схема привязки источников V1: Cargo metadata фиксирует каноническую цель библиотеки `src/lib.rs`; length-prefixed SHA-256 связывает этот факт, четыре маршрута из корня crate и точное тело парсера; независимый тест отвергает перенаправления `[lib] path`, `cfg`, `path` и parser, доказывая невлияние будущих `Srgb8`/root re-export; скомпилированная проба registry вызывает оба публичных входа, границу 4.5, различие критериев 4.5/3.0, ID доказательств и невалидный транспорт | Cargo metadata + точный capsule-оракул + внешний Rust-потребитель внутри verifier-а | -| право выпускать терминальное доказательство связано с фактической типизированной строкой WCAG registry | скомпилированная Rust-проба читает действующую строку; Python канонизирует 10 значимых для выпуска полей через length-prefix/SHA-256; 10 мутаций полей + 2 мутации hex/count транспорта обязаны отказать | независимая локальная привязка допуска (в proof всего 15 негативных контролей) | -| один вердикт и его доказательство сохраняются через Core → FFI/WASM → JS/Swift/контракт соответствия | `wcag22_transport_*`, `wasm_parity`, `wcag22.test.mjs`, Swift conformance, зафиксированный набор из шести `wcag22.json`; release verifier повторно проверяет байты доказательств | дифференциальный межграничный оракул | - -## Конечная компиляция и явный выбор WCAG 2.2 — `wcag22_feasibility.rs` (#295, #296-A/B/C1) - -Модуль канонизирует непрозрачные клиентские декларации и полностью перечисляет -либо зарегистрированную нейтральную ось, либо явно объявленный клиентом конечный -набор финальных sRGB8. Оба входа используют один приватный kernel. Он не -ранжирует кандидаты, не выводит применимость или размер текста из ID и не -заменяет перцептивную цель решателя. - -| инвариант | чем верифицирован | оракул | -|---|---|---| -| все 256 нейтралей проверяются против каждого канонического применимого соседа; граничные множества для 4.5:1 и 3:1 совпадают с независимо пересчитанным эталоном | `verify_wcag22_neutral_axis.py`; `production_vectors_are_bound_to_the_exact_independent_oracle_fixture`; тесты полной матрицы и границ | независимая `Fraction`-арифметика с адаптивными точными границами корня пятой степени; производственный Q55 и Rust-вычислитель не импортируются | -| перестановки и точные дубликаты не меняют канонические ID содержимого; изменение непрозрачного ID меняет идентичность, но не физическое разбиение | `verify_wcag22_feasibility_identity.py`; `exact_identity_preimages_match_the_independent_cross_language_fixture`; тесты свойств и фиксации текущего поведения канонизации | независимая Python-транскрипция точной байтовой грамматики и SHA-256 + внутренние метаморфные тесты | -| терминал появляется только после полного `W=256E`; упакованное хранилище равно `B=0` при `A=0`, иначе `B=32(E+1)`; частичный терминал и неявная подстановка результата отсутствуют | тесты с внедрением отказов вычислителя, хранилища, выделения памяти и полноты; property-тесты проверяют точные счётчики, а максимум отдельно исполняют `exact_compact_ceiling_is_derived_and_attainable` в Protocol и прямой Core-тест `explicit_compile_profile_maximum_completes_and_plus_one_fails_before_evaluation` | типизированные негативные контроли + проверяемая целочисленная арифметика + два независимых exact-boundary witness | -| явный домен сортируется по точным UTF-8-байтам непрозрачного ID; повторы ID запрещены, одинаковый sRGB8 под разными ID сохраняет две строки; ID и физические байты входят в domain identity | `verify_wcag22_explicit_feasibility_identity.py`; Unicode-фикстура, permutation/property/duplicate positive-control и compile-fail тесты | независимая Python-транскрипция канонизации, length-prefix/SHA-256 и непрерывной LSB0-упаковки; 6 мутаций + 2 инвариантных контроля | -| для явного домена Core выводит `W=C×E`, `M=ceil(W/8)`, `P=ceil(C/8)` и при `E>0` один буфер `B=M+P`; при `E=0` результат не вычисляется и `B=0` | инструментированный приватный kernel доказывает точные вызовы/reserve/writes; property покрывает переменные `C/E`; совместная грань при максимальном `E` (`C=256,E=2047,W=524032,B=65536`) завершается, а `C+1` при том же `E` отклоняется до evaluation/allocation; отдельный public-тест успешно проходит `C=513,E=1` и границу 64-го байта partition; полный явный набор нейтралей побитно совпадает с V1 | проверяемая целочисленная арифметика + differential к прежнему публичному пути + негативные storage/tail-bit контроли | -| только `Feasible` минтит неподделываемый источник выбора; клиентский список полностью проверяется до результата, а первый feasible-ID повторно проверяется тем же #284 evaluator ровно `E` раз; после создания source/policy публичный выбор не выделяет память; каждый применимый relation ID попадает в receipt один раз, а ordinals остаются глобальными для канонического графа | `wcag22_explicit_selection`: compile-fail sealing, exhaustive outcome match, opposite-order/property, invalid-tail/zero-call, `NoSelection`, exact-cell/all-edge fault, первый/последний fault второй applicable relation, exact-`E` и граничный byte-count тесты; отдельный counting-allocator target с положительным контролем; `verify_wcag22_explicit_selection_identity.py --self-test` | независимая Unicode-фикстура точной policy/receipt SHA-256-грамматики с ведущей canonical `NotApplicable` и двумя разными applicable relations; мутации каждого relation/edge-поля, applicable-only relation ordinals, сброса edge ordinal, пропуска/повтора/перестановки и канонизационные контроли | - -Строки транспорта ниже доказывают уже отгруженный вход зарегистрированной -нейтральной оси V1 и атомарную explicit-операцию `wcag22-explicit-selection-v1` -(#296-C2/C3): её Protocol-проекция живёт за одной non-default фичей, а -compiler-WASM/npm и UniFFI/Swift адаптеры публикуют её и повекторно реплеят -conformance-family (#296-C3). - -| transport-инвариант | чем верифицирован | оракул | -|---|---|---| -| transport V1 принимает только strict UTF-8 JSON bytes, сначала применяет выведенный предел envelope, затем сохраняет полную Core-алгебру как `Success(feasibility) \| Failure(error)` | `labcolors-protocol`: exact-limit witness, limit+1 decoder-spy, strict-schema/error-algebra tests и compile-fail запрет forged outcome | литеральная грамматика + Core resource-profile SSOT + тип-уровневый негативный контроль | -| offline npm compiler проходит `labcolors-protocol → labcolors-compiler → @labpics/colors/compiler`, не повторяет Core-математику, preflight-ит intrinsic `Uint8Array.byteLength` до избегаемой ABI-копии и переносит domain/relations один раз; runtime dependency cone не содержит protocol | compiler `wasm_parity`; role-isolation dependency tests; hostile-view и feasibility boundary tests; `wcag22-feasibility.test.mjs` строит независимый запрос ровно в MAX, исполняет его реальным публичным WASM и проверяет `E=2047`, `W=524032`, матрицу/partition; локальный exact-MAX oracle не сертифицирует witness с `E=2046` и терминал с пропущенным ребром | дифференциальный compiler-boundary replay прежних семи семейств (pack 6) + независимый packed consumer + прямой exact-boundary witness | -| UniFFI/Swift вызывает тот же protocol byte path, preflight-ит `Data`/`[UInt8]` до сырой FFI-копии и исчерпывающе декодирует terminal/error algebra | FFI mechanical-shell tests; Swift replay прежних семи семейств (pack 6), limit+1 bridge spy и structural mutation tests; независимый Swift exact-MAX generator доказывает один raw-вызов, ноль scalar-вызовов, точный объём входа и тот же `E/W` без cell/proportional DTO; локальный oracle не сертифицирует witness с `E=2046`, а публичный декодер отвергает терминал с пропущенным ребром | побайтный FFI/protocol differential + независимый Swift packed consumer + наблюдаемый direct-boundary вызов | -| атомарная операция `wcag22-explicit-selection-v1`: A-ошибка приоритетна, политика валидируется единственным SSOT после каждого успешного A-терминала, невыборные терминалы связывают политику без selection-receipt, терминал A перемещается без копирования и дополнительных аллокаций, а отказ не несёт частичного feasibility | `explicit/atomic.rs` unit + `wcag22_explicit_atomic` (дифференциал к A→B, идентичность классификации по трём терминалам, приоритет A, opposite-order, forge/mispair compile-fail) + `wcag22_explicit_atomic_alloc` counting-allocator differential | дифференциал против автономных A и B на байт-идентичных входах + позитивный allocation-контроль | -| строгий explicit-транспорт: выведенный envelope точен и достижим, неизвестные schema/domain/profile/policy kind падают typed, дефекты конструирования атрибутируются своей фазе, сериализованный исход не десериализуется обратно в authority | `labcolors-protocol` explicit-selection unit seam (limit/limit+1 decoder-spy) + integration tests (достижимость MAX ровно в байтах, strict shapes, фазовая атрибуция, byte-identical feasibility-поддерево) + compile-fail Deserialize-запреты | машинно-проверяемый корнер-анализ const-assert + реальный запрос ровно в MAX байтов + Core-классификация вместо транспорта | -| conformance pack 6 добавляет только atomic explicit-selection family; прежние семь family остаются byte-identical, а 15 outcomes воспроизводятся canonical protocol encoder, собранным compiler-WASM (npm тест + wasm32 паритет) и Swift/FFI побайтно | `pack_v6_contract`; `reference_runner::protocol_reproduces_committed_wcag22_explicit_selection_exactly`; `wcag22-explicit-selection.test.mjs`; compiler `wasm_parity`; Swift ConformanceTests | SHA-256 immutable-family guards + три независимых адаптерных реплея одной protocol-границы | -| runtime и compiler WASM воспроизводимы как разные execution-role; compiler сохраняет protocol-паритет и точную транспортную границу | append-only size V6 связывает exact Linux x64 size/SHA и рецепты обеих ролей с нулевым запасом; compiler `wasm_parity` реплеит канонический pack и проверяет limit+1, а npm-тесты независимо проверяют packed LSB0 consumer | `check-wasm-size-budget.mjs`, role-budget mutation tests, browser-WASM parity и прямые protocol/npm тесты; размер применим только к точным артефактам/рецептам | - -Здесь `E` — число канонических применимых рёбер «связь × сосед» -(`E∈{0,1,…,2047}`), `A` — число применимых связей (`A∈{0,1,…,E}`; -`A=0` тогда и только тогда, когда -`E=0`), `W` — точное число вызовов вычислителя, `B` — число байтов матрицы -решений вместе с итоговым разбиением кандидатов. Для домена V1 `W=256E`, -то есть `W∈{0,256,…,524032}`, поскольку проверяются все 256 кодов нейтральной -оси. Один бит на кандидата даёт `256/8=32` байта на каждое ребро и ещё 32 байта -на итоговое разбиение, поэтому при `A>0` получаем `B=32(E+1)`, то есть -`B∈{64,96,…,65536}`; при `A=0` получаем `B=0`. Это дискретная арифметика -представления, а не настроенные пороги. Её напрямую фиксируют -`packed_storage_is_exactly_32_times_e_plus_one` и -`one_by_e_e_by_one_and_mixed_declarations_all_execute_exact_w`. - -Максимум `2047` также выведен, а не подобран: ресурсный профиль `Compile` V1 -отводит под упакованный результат объём `65536` байт (одна -64-КиБ страница WebAssembly), резервирует 32 байта для итогового разбиения и -делит оставшиеся `65504` байта на 32-байтные слоты рёбер: -`65504/32=2047`. Это граница продуктовой политики результата, а не утверждение -о полной памяти WebAssembly. - -Для явного домена `C` — выведенное Core число канонических клиентских записей, -а не переданная клиентом метаинформация. При `E>0` непрерывная candidate-major -LSB0-матрица занимает `ceil(C×E/8)` байт без выравнивания строк, partition — -`ceil(C/8)` байт; неиспользуемые хвостовые биты обязаны быть нулевыми. Полнота -означает только перебор всех членов объявленного набора, никогда не весь gamut. -Зарегистрированная нейтральная ось остаётся частным случаем `C=256` с прежними -байтами публичного V1-контракта. - -Прямая проверка transport-границы `compile-v1` не хранит результаты, историю -или время в репозитории. -Protocol, публичный compiler-WASM и Swift независимо строят допустимый запрос -ровно в `657380` байт с `2047` применимыми рёбрами и `65536` байтами непрозрачных -ID; Core отдельно исполняет максимальную геометрию общего kernel. Публичные -адаптеры обязаны получить полный терминал `W=524032`, матрицу `65504` байта и -partition `32` байта. Protocol отвергает MAX+1, Core — лишнего кандидата, -локальные exact-MAX oracle — witness с `E=2046`, а Swift-декодер — -терминал без одного ребра. Это доказывает достижимость версионированной -продуктовой границы, но не содержит утверждений о времени, общей памяти -WebAssembly или клиентской задержке: они не замерялись. - -## LPC (перцептивный контраст) — `lpc.rs` - -| формула | чем верифицирована | оракул | -|---|---|---| -| `contrast_core` (APCA SAPC-8 `0.0.98G-4g`) | `golden_tests::contrast_core_matches_reference_on_grey_axis` (13 точек vs `apca-w3`) | эталонный софт (`apca-w3` v0.1.9) | -| Конечные точки (BoW ≈106.04, WoB ≈−107.88) | `lpc::tests::black_on_white_matches_reference`; `[NEW]` `reference_vectors::lpc_endpoints_match_apca_via_public_api` | публикация (APCA SAPC-8 endpoints) | -| `soft_clamp` / `soft_clamp_inv` | `lpc::tests::soft_clamp_boundaries_are_exact`, `..._matches_reference_bisection` | внутренняя тождественность | -| `y_hk_analytic` (обратный `grey_j`) | `lpc::tests::y_hk_analytic_matches_bisection_on_grid` | внутренняя тождественность | - -## Appearance-граф — `crates/labcolors-core/src/appearance.rs` - -Приватный компилятор/исполнитель физического компонента (#307). Модуль не несёт -собственной численной политики: единственная операция — SSOT-композитор -`alpha::composite_over_srgb8`; проверяется соответствие топологии и переносу -байтов, а не новая математика. - -| формула/инвариант | чем верифицирована | оракул | -|---|---|---| -| source-over ребро графа ≡ `composite_over_srgb8` (весь домен байтов × α) | `appearance_graph_tests::graph_source_over_equals_the_independent_compositor_for_neutral_and_chromatic_inputs` (property) | дифференциальный (SSOT-композитор, сам верифицирован против reference-векторов `alpha.rs`) | -| replayable-сертификат: независимое повторение операции из полей сертификата побайтно равно записанному выходу | `appearance_graph_tests::source_over_certificate_replays_to_the_exact_recorded_bytes` (property) | внутренняя тождественность | -| канонизация: результат не зависит от порядка деклараций/значений typed handles | `appearance_graph_tests::compile_is_independent_of_declaration_order_for_the_same_handles`, `unrelated_opaque_handles_do_not_change_the_physics` | внутренняя тождественность | -| fail-closed: дубликаты/missing refs/циклы/дефекты bindings/α вне `[0,1]` — типизированные отказы | `appearance_graph_tests::compile_rejects_*`, `evaluate_rejects_*`, `graph_rejects_missing_occurrence_backdrop_and_cycles` | внутренняя тождественность | -| occurrence наблюдается против derived-поверхности, не страницы; identity-ребро не декоративно | `appearance_graph_tests::warning_occurrence_targets_the_rendered_surface_not_the_page` (+ trace-счётчики), `occurrence_source_follows_the_declared_identity_edge_not_the_composite_source` | внутренняя тождественность (witness) | -| production-миграция `PairLabel` байт-идентична замороженному legacy-пути (5 семей × 4 режима × 6 фонов + property + публичные отказы) | `pair_label_tests::migration_*` | дифференциальный (test-only legacy oracle) | - -Мутационный скоуп: модуль включён в `.cargo/mutants.toml` (`examine_globs`). - -## Численные решения — `numerics.rs`, `numerical_plan.rs` (#292) - -Три уровня контракта разделены типами: package capability -(`NumericalCapabilityManifestV2` — единственная proof-capable projection -registry SSOT) ≠ compiled -invocation plan (`CompiledNumericalPlanV1`) ≠ атомарный результат -(`NumericalDecisionV1`: `Determinate`/`Compatibility`/`Indeterminate`). -Новой математики модуль не вводит — проверяется невозможность повышения -caller-created значений и legacy-исходов до доказательств. - -| инвариант | чем верифицирован | оракул | -|---|---|---| -| registry непустой, ключи уникальны, Glow site покрыт обоими stable outcomes и registered compatibility release | `numerics::tests::migrated_registry_is_non_vacuous_unique_and_covers_glow_site` | внутренняя тождественность | -| capability manifest — каноническая projection registry: сортировка по UTF-8 `siteId`, coverage `migrated-sites-only-v1`, без выбранного mode | `numerics::tests::capability_manifest_is_canonical_registry_projection` | внутренняя тождественность | -| единственный public `numericalCapabilityManifest()` возвращает V2 с WCAG artifact/bound/proof IDs; одна декларация проецирует internal runtime и public capability без двух SSOT | `numerics::tests::unified_registry_projects_runtime_glow_and_proof_bound_wcag`, `projection::tests::capability_manifest_json_mirrors_proof_capable_core_ssot`, `packages/colors/test/capability-manifest.test.mjs` | regression pin + дифференциальный adapter/core | -| drift-checksum канонический и tamper-чувствителен: смена schema version / удаление row меняет FNV-1a-32 preimage | `numerics::tests::capability_checksum_is_canonical_and_tamper_sensitive`; независимые пересчёты: JS (`scripts/verify-package-release.mjs`) и Swift (`ConformanceTests.testCapabilityManifestChecksumRecomputes`) | внутренняя тождественность + два независимых re-implementation оракула | -| legacy-исход — атомарный `Compatibility` с registered release, не determinate evidence | `numerics::tests::legacy_result_is_compatibility_not_determinate_evidence` | тип-уровневая (взаимоисключающие варианты) + внутренняя тождественность | -| BitExact/bounded evidence минтится только registry-owned конструктором; внешний код не может ни собрать evidence, ни переупаковать genuine evidence другого site в новый terminal result | `numerics::tests::bit_exact_evidence_is_registry_owned_and_sealed`, `bit_exact_mint_is_refused_without_declared_capability` + три compile-fail doctests в шапке `numerics.rs` (удалённый classifier, приватная evidence-печать, cross-site reuse) | тип-уровневая (компилятор) | -| предметный Glow outcome также Core-owned: generic/WCAG evidence нельзя объявить `StableExactNoop`, а compatibility нельзя собрать вручную | variant-level sealing `GlowDecisionOutcomeV1` + compile-fail doctest в `glow.rs`; WASM-тесты получают оба outcome только через полный `resolve_named_set` path | тип-уровневая + boundary characterization | -| диагностический интервал проверяет только форму (конечность, порядок) и не изготовляет determinate evidence | `numerics::tests::diagnostic_interval_validates_shape_only` | внутренняя тождественность | -| invocation identity плана канонична: локальные ordinals внутри (node, site), перестановка деклараций не меняет ids/projection | `numerical_plan::tests::mixed_modes_coexist_and_ordinals_are_local`, `declaration_permutation_preserves_ids_and_canonical_projection` | внутренняя тождественность | -| план tamper-чувствителен: переименование node/site меняет identity, смена mode меняет checksum; незарегистрированный release — typed ошибка компиляции плана | `numerical_plan::tests::rename_changes_identity_and_mode_mutation_changes_checksum`, `unregistered_release_is_a_typed_compile_error` | внутренняя тождественность | -| conformance manifest публикует exact core projection (не рукописную копию) | `reference_runner::manifest_metadata_matches_core`, `labcolors_conformance::tests::manifest_numerical_registry_is_generated_from_core_ssot` | дифференциальный (закоммиченный артефакт против свежей генерации) | - -## JS-дубликат — `packages/colors/effective-bg.js` - -| формула | чем верифицирована | оракул | -|---|---|---| -| `srgbToLinear` / `linearToSrgb` (IEC 61966-2-1) | транзитивно через `parseOklch`/`oklabLerp` | внутренняя тождественность | -| `linearRgbToOklab` / `oklabToLinearRgb` (Ottosson) + `parseOklch` (oklch→sRGB байты) | `oklch-parse.test.mjs` (16 live-фикстур); `[NEW]` `reference-vectors.test.mjs` — байт-совпадение с ядром на ≥1000 сидированных строк (фикстура `test/data/oklch-core-vectors.txt`, эмитируется ядром `oklch_css_from_hex`, анти-дрейф — `reference_vectors::oklch_core_vectors_fixture_is_fresh`) | дифференциальный (ядро `labcolors-core` = оракул) | -| `parseCssColor` краевые (none/проценты/Chrome L∈0..1/out-of-gamut/H wrap) | `oklch-parse.test.mjs`; `[NEW]` `reference-vectors.test.mjs` edge-блок (семантика CSS Color 4) | публикация (CSS Color 4) | diff --git a/docs/whitepaper.md b/docs/whitepaper.md index 98945a8c..3ec4cf3a 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -30,20 +30,23 @@ - один и тот же стимул в светлом и тёмном окружении выглядит по-разному (адаптация зрения). -Дизайн-системная задача — «дай цвет с *этим* воспринимаемым контрастом на *этом* -фоне» — на таких шкалах решается вручную и дрейфует. lab-colors решает её в -собственном перцептуальном пространстве LCS (Labpics Color Space) с обратным -решателем контраста. +Задача дизайн-системы — выразить требуемые отношения цвета и контекста, а затем +вычислить допустимый физический результат. Текущий код хранит представления +CAM16-UCS и Oklab и содержит несколько возможных путей вычисления контраста; он +ещё не реализует допущенные контракты **Labpics Colors Space (LCS)** и **Labpics +Perceptual Contrast (LPC)** и не обещает универсальный решатель воспринимаемого +контраста. ### 1.2. Выбор пространства -LCS построено поверх текущей транскрипции CAM16 из Li et al. 2017 с двумя -отступлениями. Это не заявка на независимую CIE 248:2022 conformance -([`README.md`](../README.md), «LCS — Labpics Color Space»): +Текущее переходное представление `LcsColor` сочетает CAM16-UCS и Oklab. Это не +завершённое определение LCS и не заявка на независимое соответствие CIE 248:2022 +([`README.md`](../README.md), «LCS — Labpics Colors Space»): -**Яркость и цветность — из CAM16-UCS, не из «сырого» CIECAM16.** CIECAM16 даёт -J и M, но они перцептуально неоднородны; CAM16-UCS применяет рескейлинг -(Li et al. 2017, DOI [10.1002/col.22131](https://doi.org/10.1002/col.22131)): +**`J'` и `M'` — текущие координаты CAM16-UCS, а не завершённые оси LCS/LPC.** +CIECAM16 даёт корреляты `J` и `M`; текущее представление применяет опубликованный +рескейлинг CAM16-UCS (Li et al. 2017, DOI +[10.1002/col.22131](https://doi.org/10.1002/col.22131)): ```text J' = 1.7 × J / (1 + 0.007 × J) @@ -52,23 +55,25 @@ M' = ln(1 + 0.0228 × M) / 0.0228 Формулы — [`spaces/cam16.rs`](../crates/labcolors-core/src/spaces/cam16.rs) (`ucs_j` / `ucs_m`); применяет их [`lcs.rs`](../crates/labcolors-core/src/lcs.rs). -`J' = 50` — числовая середина нормированной шкалы CAM16-UCS; это не отдельное -психофизическое утверждение о воспринимаемом midpoint. +Формулы задают числовое преобразование координат. Они не объявляют `J'` +универсальной шкалой воспринимаемой яркости, а `M'` или хранимое +`s = M' / (J' + 1)` — универсальной шкалой ощущаемой цветности. `s` является +только внутренней репараметризацией и не равно корреляту насыщенности CAM16. -**Оттенок — из Oklab, не из CAM16.** Для интерполяции между цветами используется -Oklab hue `h_ok` (Ottosson 2020, «A perceptual color space for image processing»; -реализация — [`spaces/oklab.rs`](../crates/labcolors-core/src/spaces/oklab.rs)): -он перцептуально ровнее CAM16-hue в синей и жёлтой зонах. CAM16-hue `h_cam` -сохраняется в структуре только для обратной конвертации в XYZ/hex. +**Два оттенка — переходные входы реализации, не закон LCS.** Для текущей +интерполяции используется Oklab hue `h_ok` (Ottosson 2020, «A perceptual color +space for image processing»; реализация — +[`spaces/oklab.rs`](../crates/labcolors-core/src/spaces/oklab.rs)). CAM16 hue +`h_cam` сохраняется только для обратной конвертации в XYZ/hex. Итоговый носитель ([`README.md`](../README.md), «LCS»): ```text struct LcsColor { - jp: f64, // J' — перцептуальная яркость (CAM16-UCS) - h_ok: f64, // оттенок (Oklab) — для интерполяции - s: f64, // насыщенность = M' / (J' + 1) - h_cam: f64, // оттенок (CAM16) — для обратной конвертации в hex + jp: f64, // текущая координата J' CAM16-UCS + h_ok: f64, // Oklab hue для текущей интерполяции + s: f64, // внутренняя репараметризация M' / (J' + 1), не saturation + h_cam: f64, // CAM16 hue для обратной конвертации в hex } ``` @@ -405,21 +410,17 @@ Fairchild 2022, *Color Res. Appl.* 47(5):1096, `J_HK = J + f(h)·C^0.587`). H-K ## 3. Математика: солвер, констрейнты, полы, квантование -### 3.1. Контрастная метрика LPC - -LPC = опубликованная контрастная кривая (экспоненты 0.56 / 0.57 / 0.62 / 0.65, -scale 1.14, low-clip и офсеты полярности версии 0.0.98G-4g) поверх яркости, -скорректированной по Гельмгольцу-Кольраушу через CIECAM16: -`J_hk = J + HK_coeff(h) × C^0.587`, где `C = M / F_L^0.25` (Hellwig 2022); -бинарный поиск находит Y, дающий `J_hk` в стандартных условиях -([`README.md`](../README.md), «Контраст — LPC»; -[`lpc.rs`](../crates/labcolors-core/src/lpc.rs)). - -Поведение на границах задокументировано числами: на ахроматике совпадает с -референсом — чёрный-на-белом `106.04` бит-в-бит, `#444444` на белом `87.6` -против `88.8` канона (< 1.5 Lc сдвига на серой оси); на хроматике расходится -намеренно — `#0000FF` на белом ≈ Lc `68.7` против ≈ `85.8` у референса, разница -и есть вклад H-K ([`README.md`](../README.md), «LPC vs APCA»). +### 3.1. Текущие компоненты LPC + +LPC означает **Labpics Perceptual Contrast**. Текущая реализация ещё не является +полным вычислителем LPC: она содержит кривую формы APCA, отдельный вход +читаемости на величину яркости из старого WCAG и путь H-K/CAM16 для +воспринимаемой яркости. Последний моделирует ощущаемую яркость, а не распознавание +глифов. Эти пути сохраняются как охарактеризованные кандидаты до замены +версионированным многомерным отчётом LPC; ни один из них не доказывает +превосходство над APCA или универсальную читаемость. Допущенный вычислитель LPC в +отгружаемом продукте пока отсутствует; API и документация не должны обещать +более сильный статус. ### 3.2. Обратный решатель diff --git a/packages/colors/README.md b/packages/colors/README.md index cba7d9e1..d9097473 100644 --- a/packages/colors/README.md +++ b/packages/colors/README.md @@ -302,8 +302,9 @@ Core не выводит критерий из имени токена, CSS-кл формулу. Оно приходит из Rust core вместе с identity профиля, Q55-таблицы, bound-law и воспроизводимого full-domain proof. Файлы доказательства входят в npm-тарбол в `evidence/`; proof также SHA-256-связан с фактической typed -registry-строкой, разрешающей Core минтить terminal evidence. LPC/APCA-shaped -diagnostics и legacy `wcagRatio` не могут изменить этот вердикт. +registry-строкой, разрешающей Core минтить terminal evidence. APCA-shaped +diagnostic-компонент текущего LPC и legacy `wcagRatio` не могут изменить этот +вердикт. --- diff --git a/packages/colors/test/public-claims.test.mjs b/packages/colors/test/public-claims.test.mjs index 6cebf9ee..905ec0e6 100644 --- a/packages/colors/test/public-claims.test.mjs +++ b/packages/colors/test/public-claims.test.mjs @@ -17,6 +17,8 @@ const RUNTIME_DOC_PATHS = [ .map((path) => `packages/colors/${path}`), ]; const CLAIM_EXT = /\.(?:js|md|mjs|rs|ts)$/u; +const REPOSITORY_TEXT_EXT = + /\.(?:c|cc|cpp|css|go|h|hpp|html|java|js|json|jsx|kt|md|mdx|mjs|py|rs|sh|swift|toml|ts|tsx|txt|ya?ml)$/u; const CLAIM_SKIP = /(?:^|\/)(?:node_modules|pkg|target|\.git)(?:\/|$)|mutants\.out/u; const HUMAN_CLEANLINESS_VERDICTS = [ /Закон Грязи/u, @@ -58,6 +60,11 @@ const RUNTIME_DOC_FALSE_CLAIMS = [ sample: "~2.5× на 3 сэмплах", reason: "an ungated benchmark result was presented as a durable property", }, + { + pattern: /measured\s+~2\.6x/iu, + sample: "a measured ~2.6x on the multi-sample recheck", + reason: "an ungated benchmark result was presented as a durable property", + }, { pattern: /перцептуально равномерная интерполяция между двумя hex-значениями/iu, sample: "перцептуально равномерная интерполяция между двумя hex-значениями", @@ -169,13 +176,38 @@ const RUNTIME_DOC_FALSE_CLAIMS = [ reason: "the public package linked to a private-repository migration guide", }, ]; +const MANUAL_VERIFICATION_PROSE = [ + /(?:docs\/)?verification-map\.md/u, + /Карта верификации нижних слоёв/iu, + /Каждая формула[\s\S]{0,240}ВНЕШНЕГО опубликованного эталона/iu, + /\|\s*формула(?:\/инвариант)?\s*\|\s*чем верифицирован[а]?\s*\|\s*оракул\s*\|/iu, + /Every vector here[\s\S]{0,160}(?:STANDARD|PEER-REVIEWED SOURCE)/iu, + /These pin[\s\S]{0,200}STANDARDS\s*\/\s*PEER-REVIEWED SOURCES[\s\S]{0,120}not to the crate's own output/iu, +]; +const LCS_LPC_DRIFT = [ + /Labpics Color Space/u, + /Local Color State/u, + /Local Perceptual Contrast/u, + /LPC\s*=\s*APCA/iu, + /APCA[^.\n]{0,160}под именем \*\*?LPC/iu, + /^LPC\s*=\s*опубликованная контрастная кривая/imu, + /J['′]\s*=\s*50[\s\S]{0,100}half-lightness/iu, + /perceptually uniform J['′]\/M['′]/iu, + /Because UCS is perceptually uniform/iu, + /J['′]\s*[—-]\s*перцептуальн[а-яё]*\s+яркост/iu, + /s:\s*f64[\s\S]{0,80}насыщенн/iu, + /lab-colors\s+решает[^.]{0,200}перцептуальн[а-яё]*\s+пространств[а-яё]*\s+LCS/iu, + /Perceptual-contrast core curve/iu, + /generic perceptual-contrast math/iu, + /метрика\s+называется\s+LPC/iu, +]; -function claimFiles(path, files = []) { +function claimFiles(path, files = [], extensions = CLAIM_EXT) { if (!existsSync(path) || CLAIM_SKIP.test(path)) return files; for (const entry of readdirSync(path, { withFileTypes: true })) { const child = join(path, entry.name); - if (entry.isDirectory()) claimFiles(child, files); - else if (CLAIM_EXT.test(entry.name)) files.push(child); + if (entry.isDirectory()) claimFiles(child, files, extensions); + else if (extensions.test(entry.name)) files.push(child); } return files; } @@ -216,6 +248,21 @@ function runtimeDocFalseClaims(path, source) { .map(({ reason }) => `${path}: ${reason}`); } +function manualVerificationProseResidue(path, source) { + return MANUAL_VERIFICATION_PROSE + .filter((pattern) => pattern.test(source)) + .map( + () => + `${path}: hand-written verification prose competes with executable oracles`, + ); +} + +function lcsLpcDrift(path, source) { + return LCS_LPC_DRIFT.filter((pattern) => pattern.test(source)).map( + () => `${path}: LCS/LPC brand or evidence boundary drifted`, + ); +} + test("false-claim detector bites without treating hex colours as Issue links", () => { assert.equal(knownFalseClaims("x.md", "см. #89").length, 1); assert.equal(knownFalseClaims("x.md", "цвета #89CFF0 и #8944AB").length, 0); @@ -238,8 +285,103 @@ test("runtime-doc detector bites on every rejected promotion", () => { } }); +test("verification-index quarantine bites on links and renamed copies", () => { + for (const sample of [ + "See docs/verification-map.md", + "# Карта верификации нижних слоёв", + "Каждая формула проверяется против ВНЕШНЕГО опубликованного эталона", + "| формула/инвариант | чем верифицирована | оракул |", + "Every vector here is a control point from a STANDARD or PEER-REVIEWED SOURCE", + "These pin transforms to STANDARDS / PEER-REVIEWED SOURCES, not to the crate's own output", + ]) { + assert.equal( + manualVerificationProseResidue("x.md", sample).length, + 1, + `quarantine did not detect: ${sample}`, + ); + } +}); + +test("repository claim scan includes every governed text format", () => { + for (const extension of [ + "js", + "jsx", + "ts", + "tsx", + "py", + "rs", + "go", + "java", + "kt", + "cpp", + "h", + "md", + "mdx", + ]) { + assert.match(`claim.${extension}`, REPOSITORY_TEXT_EXT, extension); + } +}); + +test("live repository has no hand-written global verification index", () => { + const files = claimFiles(ROOT, [], REPOSITORY_TEXT_EXT).filter( + (file) => file !== SELF, + ); + const failures = files.flatMap((file) => + manualVerificationProseResidue( + relative(ROOT, file), + readFileSync(file, "utf8"), + ), + ); + assert.deepEqual(failures, []); +}); + +test("LCS/LPC drift detector bites on every rejected expansion or reduction", () => { + for (const sample of [ + "LCS means Labpics Color Space", + "LCS means Local Color State", + "LPC means Local Perceptual Contrast", + "LPC = APCA + H-K", + "APCA реализована под именем **LPC**", + "LPC = опубликованная контрастная кривая", + "J'=50 reads as half-lightness", + "maps correlates onto perceptually uniform J'/M'", + "Because UCS is perceptually uniform, this is a human scale", + "J' — перцептуальная яркость (CAM16-UCS)", + "s: f64, // насыщенность = M' / (J' + 1)", + "lab-colors решает её в собственном перцептуальном пространстве LCS", + "Perceptual-contrast core curve", + "Faithful port of the generic perceptual-contrast math", + "метрика называется LPC", + ]) { + assert.ok(lcsLpcDrift("x.md", sample).length >= 1, `detector did not bite: ${sample}`); + } +}); + +test("live repository keeps canonical LCS/LPC names and evidence boundaries", () => { + const files = claimFiles(ROOT, [], REPOSITORY_TEXT_EXT).filter( + (file) => file !== SELF, + ); + const failures = files.flatMap((file) => + lcsLpcDrift(relative(ROOT, file), readFileSync(file, "utf8")), + ); + assert.deepEqual(failures, []); + + assert.match( + readFileSync(join(ROOT, "crates/labcolors-core/src/lcs.rs"), "utf8"), + /Labpics Colors Space/u, + ); + assert.match( + readFileSync(join(ROOT, "crates/labcolors-core/src/lpc.rs"), "utf8"), + /Labpics Perceptual Contrast/u, + ); +}); + test("runtime docs do not promote estimates, samples, or coordinates", () => { - const failures = RUNTIME_DOC_PATHS.flatMap((path) => + const paths = [ + ...RUNTIME_DOC_PATHS, + "crates/labcolors-wasm/src/lib.rs", + ]; + const failures = paths.flatMap((path) => runtimeDocFalseClaims(path, readFileSync(join(ROOT, path), "utf8")), ); assert.deepEqual(failures, []);